feat: 重构营销模块积分日志页面

- 优化搜索区域:支持用户昵称、用户ID、时间范围筛选
- 新增表格字段:ID、用户ID、昵称、来源/用途、积分变化、变化后积分、关联类型、状态、备注、日期
- 积分变化带颜色标识:增加(绿色+)、扣减(红色-)
- 状态标签彩色区分:订单创建/冻结期/完成/失效
- 关联类型中文映射:订单/签到/系统
- 使用已有的 integralListApi 接口
- 参考用户详情页积分明细样式
This commit is contained in:
scott
2026-03-20 15:53:48 +08:00
parent 09946536aa
commit fe9e1916fa
3 changed files with 230 additions and 147 deletions

View File

@@ -3,85 +3,102 @@
<el-card class="box-card"> <el-card class="box-card">
<div slot="header" class="clearfix"> <div slot="header" class="clearfix">
<div class="container"> <div class="container">
<el-form size="small" label-width="120px"> <el-form inline size="small" :model="searchForm" ref="searchForm" label-width="80px">
<el-form-item label="时间选择:" class="width100"> <el-row>
<el-radio-group <el-col :xs="24" :sm="24" :md="18" :lg="18" :xl="18">
v-model="tableFrom.dateLimit" <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
type="button" <el-form-item label="用户搜索:">
class="mr20" <el-input v-model="searchForm.keywords" placeholder="请输入用户昵称" clearable />
size="small" </el-form-item>
@change="selectChange(tableFrom.dateLimit)" </el-col>
> <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-radio-button v-for="(item, i) in fromList.fromTxt" :key="i" :label="item.val" <el-form-item label="用户ID">
>{{ item.text }} <el-input-number v-model="searchForm.uid" placeholder="请输入用户ID" :min="1" :controls="false" style="width: 100%" />
</el-radio-button> </el-form-item>
</el-radio-group> </el-col>
<el-date-picker <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
v-model="timeVal" <el-form-item label="时间选择:">
value-format="yyyy-MM-dd" <el-date-picker
format="yyyy-MM-dd" v-model="timeVal"
size="small" type="daterange"
type="daterange" align="right"
placement="bottom-end" unlink-panels
placeholder="自定义时间" value-format="yyyy-MM-dd"
style="width: 250px" format="yyyy-MM-dd"
@change="onchangeTime" range-separator=""
/> start-placeholder="开始日期"
</el-form-item> end-placeholder="结束日期"
<el-form-item label="用户微信昵称:"> :picker-options="pickerOptions"
<el-input v-model="tableFrom.keywords" placeholder="请输入用户昵称" class="selWidth" size="small"> @change="onchangeTime"
<el-button slot="append" icon="el-icon-search" size="small" @click="getList(1)" /> />
</el-input> </el-form-item>
</el-form-item> </el-col>
</el-col>
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6" class="text-right">
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
</div> </div>
<!--<cards-data :cardLists="cardLists"></cards-data>-->
</div> </div>
<el-table <el-table
v-loading="listLoading" v-loading="listLoading"
:data="tableData.data" :data="tableData.data"
size="small" style="width: 100%"
class="table" size="mini"
highlight-current-row highlight-current-row
:header-cell-style="{ fontWeight: 'bold' }"
> >
<el-table-column prop="id" label="ID" width="60" /> <el-table-column prop="id" label="ID" min-width="80" />
<el-table-column prop="title" label="标题" min-width="130" /> <el-table-column prop="uid" label="用户ID" min-width="80" />
<el-table-column <el-table-column prop="nickName" label="用户昵称" min-width="120" show-overflow-tooltip>
sortable <template slot-scope="scope">
prop="balance" <span>{{ scope.row.nickName || '-' }}</span>
label="积分余量" </template>
min-width="120" </el-table-column>
:sort-method=" <el-table-column prop="title" label="来源/用途" min-width="150" show-overflow-tooltip />
(a, b) => { <el-table-column prop="integral" label="积分变化" min-width="100">
return a.balance - b.balance; <template slot-scope="scope">
} <el-tag :type="scope.row.type === 1 ? 'success' : 'danger'" size="small">
" {{ scope.row.type === 1 ? '+' : '-' }}{{ scope.row.integral }}
/> </el-tag>
<el-table-column </template>
sortable </el-table-column>
label="明细数字" <el-table-column prop="balance" label="变化后积分" min-width="100" />
min-width="120" <el-table-column prop="linkType" label="关联类型" min-width="100">
prop="integral" <template slot-scope="scope">
:sort-method=" <el-tag size="small" effect="plain">
(a, b) => { {{ linkTypeFilter(scope.row.linkType) }}
return a.integral - b.integral; </el-tag>
} </template>
" </el-table-column>
/> <el-table-column prop="status" label="状态" min-width="100">
<el-table-column label="备注" min-width="120" prop="mark" /> <template slot-scope="scope">
<el-table-column label="用户昵称" min-width="120" prop="nickName" /> <el-tag :type="statusTypeFilter(scope.row.status)" size="small">
<el-table-column prop="updateTime" label=" 添加时间" min-width="150" /> {{ statusFilter(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="mark" label="备注" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.mark || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="日期" min-width="150" />
</el-table> </el-table>
<div class="block"> <div class="block">
<el-pagination <el-pagination
:page-sizes="[20, 40, 60, 80]" :page-sizes="[15, 30, 45, 60]"
:page-size="tableFrom.limit" :page-size="searchForm.limit"
:current-page="tableFrom.page" :current-page="searchForm.page"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" :total="tableData.total"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="pageChange" @current-change="handleCurrentChange"
/> />
</div> </div>
</el-card> </el-card>
@@ -89,78 +106,129 @@
</template> </template>
<script> <script>
import { integralListApi } from '@/api/marketing'; import { integralListApi } from '@/api/integral';
import cardsData from '@/components/cards/index';
export default { export default {
components: { cardsData }, name: 'IntegralLog',
data() { data() {
return { return {
loading: false, listLoading: true,
options: [],
fromList: this.$constants.fromList,
listLoading: false,
tableData: { tableData: {
data: [], data: [],
total: 0, total: 0,
}, },
tableFrom: { searchForm: {
page: 1,
limit: 20,
dateLimit: '',
keywords: '', keywords: '',
uid: null,
dateLimit: '',
page: 1,
limit: 15,
}, },
userIdList: [],
userList: [],
timeVal: [], timeVal: [],
values: [], pickerOptions: this.$timeOptions,
}; };
}, },
mounted() { mounted() {
this.getList(); this.getList();
// this.getUserList()
}, },
methods: { methods: {
seachList() { // 获取列表
this.tableFrom.page = 1;
this.getList();
},
// 选择时间
selectChange(tab) {
this.tableFrom.dateLimit = tab;
this.tableFrom.page = 1;
this.timeVal = [];
this.getList();
},
// 具体日期
onchangeTime(e) {
this.timeVal = e;
this.tableFrom.dateLimit = e ? this.timeVal.join(',') : '';
this.tableFrom.page = 1;
this.getList();
},
// 列表
getList() { getList() {
this.listLoading = true; this.listLoading = true;
integralListApi({ limit: this.tableFrom.limit, page: this.tableFrom.page }, this.tableFrom) const params = {
...this.searchForm,
};
// 移除空值
if (!params.keywords) delete params.keywords;
if (!params.uid) delete params.uid;
if (!params.dateLimit) delete params.dateLimit;
integralListApi(params)
.then((res) => { .then((res) => {
this.tableData.data = res.list; this.tableData.data = res.list || [];
this.tableData.total = res.total; this.tableData.total = res.total || 0;
this.listLoading = false; this.listLoading = false;
}) })
.catch((res) => { .catch(() => {
this.listLoading = false; this.listLoading = false;
}); });
}, },
pageChange(page) { // 搜索
this.tableFrom.page = page; handleSearch() {
this.searchForm.page = 1;
this.getList(); this.getList();
}, },
handleSizeChange(val) { // 重置
this.tableFrom.limit = val; handleReset() {
this.searchForm = {
keywords: '',
uid: null,
dateLimit: '',
page: 1,
limit: 15,
};
this.timeVal = [];
this.getList(); this.getList();
}, },
// 时间选择
onchangeTime(e) {
this.timeVal = e;
this.searchForm.dateLimit = e ? e.join(',') : '';
},
// 分页大小变化
handleSizeChange(val) {
this.searchForm.limit = val;
this.getList();
},
// 页码变化
handleCurrentChange(val) {
this.searchForm.page = val;
this.getList();
},
// 关联类型过滤器
linkTypeFilter(type) {
const typeMap = {
order: '订单',
sign: '签到',
system: '系统',
};
return typeMap[type] || type || '-';
},
// 状态过滤器
statusFilter(status) {
const statusMap = {
1: '订单创建',
2: '冻结期',
3: '完成',
4: '失效',
};
return statusMap[status] || '未知';
},
// 状态样式过滤器
statusTypeFilter(status) {
const typeMap = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger',
};
return typeMap[status] || 'info';
},
}, },
}; };
</script> </script>
<style lang="sass" scoped></style> <style scoped lang="scss">
.text-right {
text-align: right;
}
.block {
margin-top: 20px;
}
.el-input,
.el-date-picker {
width: 100%;
}
</style>

View File

@@ -14,42 +14,42 @@ package com.zbkj.common.constants;
*/ */
public class IntegralRecordConstants { public class IntegralRecordConstants {
/** 佣金记录类型—增加 */ /** 积分记录类型—增加 */
public static final Integer INTEGRAL_RECORD_TYPE_ADD = 1; public static final Integer INTEGRAL_RECORD_TYPE_ADD = 1;
/** 佣金记录类型—扣减 */ /** 积分记录类型—扣减 */
public static final Integer INTEGRAL_RECORD_TYPE_SUB = 2; public static final Integer INTEGRAL_RECORD_TYPE_SUB = 2;
/** 佣金记录状态—创建 */ /** 积分记录状态—创建 */
public static final Integer INTEGRAL_RECORD_STATUS_CREATE = 1; public static final Integer INTEGRAL_RECORD_STATUS_CREATE = 1;
/** 佣金记录状态—冻结期 */ /** 积分记录状态—冻结期 */
public static final Integer INTEGRAL_RECORD_STATUS_FROZEN = 2; public static final Integer INTEGRAL_RECORD_STATUS_FROZEN = 2;
/** 佣金记录状态—完成 */ /** 积分记录状态—完成 */
public static final Integer INTEGRAL_RECORD_STATUS_COMPLETE = 3; public static final Integer INTEGRAL_RECORD_STATUS_COMPLETE = 3;
/** 佣金记录状态—失效(订单退款) */ /** 积分记录状态—失效(订单退款) */
public static final Integer INTEGRAL_RECORD_STATUS_INVALIDATION = 4; public static final Integer INTEGRAL_RECORD_STATUS_INVALIDATION = 4;
/** 佣金记录关联类型—订单 */ /** 积分记录关联类型—订单 */
public static final String INTEGRAL_RECORD_LINK_TYPE_ORDER = "order"; public static final String INTEGRAL_RECORD_LINK_TYPE_ORDER = "order";
/** 佣金记录关联类型—签到 */ /** 积分记录关联类型—签到 */
public static final String INTEGRAL_RECORD_LINK_TYPE_SIGN = "sign"; public static final String INTEGRAL_RECORD_LINK_TYPE_SIGN = "sign";
/** 佣金记录关联类型—系统后台 */ /** 积分记录关联类型—系统后台 */
public static final String INTEGRAL_RECORD_LINK_TYPE_SYSTEM = "system"; public static final String INTEGRAL_RECORD_LINK_TYPE_SYSTEM = "system";
/** 佣金记录标题—用户订单付款成功 */ /** 积分记录标题—用户订单付款成功 */
public static final String BROKERAGE_RECORD_TITLE_ORDER = "用户订单付款成功"; public static final String BROKERAGE_RECORD_TITLE_ORDER = "用户订单付款成功";
/** 佣金记录标题—签到经验奖励 */ /** 积分记录标题—签到积分奖励 */
public static final String BROKERAGE_RECORD_TITLE_SIGN = "签到积分奖励"; public static final String BROKERAGE_RECORD_TITLE_SIGN = "签到积分奖励";
/** 佣金记录标题—后台积分操作 */ /** 积分记录标题—后台积分操作 */
public static final String BROKERAGE_RECORD_TITLE_SYSTEM = "后台积分操作"; public static final String BROKERAGE_RECORD_TITLE_SYSTEM = "后台积分操作";
/** 佣金记录标题—订单退款 */ /** 积分记录标题—订单退款 */
public static final String BROKERAGE_RECORD_TITLE_REFUND = "订单退款"; public static final String BROKERAGE_RECORD_TITLE_REFUND = "订单退款";
} }

View File

@@ -73,22 +73,18 @@ public class UserIntegralRecordServiceImpl extends ServiceImpl<UserIntegralRecor
LambdaQueryWrapper<UserIntegralRecord> lqw = Wrappers.lambdaQuery(); LambdaQueryWrapper<UserIntegralRecord> lqw = Wrappers.lambdaQuery();
lqw.eq(UserIntegralRecord::getUid, uid); lqw.eq(UserIntegralRecord::getUid, uid);
lqw.eq(UserIntegralRecord::getLinkId, orderNo); lqw.eq(UserIntegralRecord::getLinkId, orderNo);
lqw.in(UserIntegralRecord::getStatus, IntegralRecordConstants.INTEGRAL_RECORD_STATUS_CREATE, IntegralRecordConstants.INTEGRAL_RECORD_STATUS_FROZEN, IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE); lqw.in(UserIntegralRecord::getStatus, IntegralRecordConstants.INTEGRAL_RECORD_STATUS_CREATE,
IntegralRecordConstants.INTEGRAL_RECORD_STATUS_FROZEN,
IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE);
List<UserIntegralRecord> recordList = dao.selectList(lqw); List<UserIntegralRecord> recordList = dao.selectList(lqw);
if (CollUtil.isEmpty(recordList)) { if (CollUtil.isEmpty(recordList)) {
return recordList; return recordList;
} }
for (int i = 0; i < recordList.size();) { // 过滤掉已完成的增加类型记录
UserIntegralRecord record = recordList.get(i); return recordList.stream()
if (record.getType().equals(IntegralRecordConstants.INTEGRAL_RECORD_TYPE_ADD)) { .filter(record -> !(record.getType().equals(IntegralRecordConstants.INTEGRAL_RECORD_TYPE_ADD)
if (record.getStatus().equals(IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE)) { && record.getStatus().equals(IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE)))
recordList.remove(i); .collect(Collectors.toList());
continue;
}
}
i++;
}
return recordList;
} }
/** /**
@@ -101,26 +97,45 @@ public class UserIntegralRecordServiceImpl extends ServiceImpl<UserIntegralRecor
if (CollUtil.isEmpty(thawList)) { if (CollUtil.isEmpty(thawList)) {
return; return;
} }
for (UserIntegralRecord record : thawList) {
// 查询对应的用户 // 按用户分组,批量处理
User user = userService.getById(record.getUid()); Map<Integer, List<UserIntegralRecord>> userRecordMap = thawList.stream()
.collect(Collectors.groupingBy(UserIntegralRecord::getUid));
for (Map.Entry<Integer, List<UserIntegralRecord>> entry : userRecordMap.entrySet()) {
Integer uid = entry.getKey();
List<UserIntegralRecord> userRecords = entry.getValue();
User user = userService.getById(uid);
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
continue ; logger.warn("积分解冻—用户不存在uid = {}", uid);
continue;
} }
record.setStatus(IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE);
// 计算积分余额 // 批量事务处理同一用户的积分解冻
Integer balance = (user.getIntegral() != null ? user.getIntegral() : BigDecimal.ZERO).add(record.getIntegral()).intValue();
record.setBalance(balance);
record.setUpdateTime(cn.hutool.core.date.DateUtil.date());
// 解冻
Boolean execute = transactionTemplate.execute(e -> { Boolean execute = transactionTemplate.execute(e -> {
updateById(record); Integer currentIntegral = user.getIntegral();
userService.operationIntegral(record.getUid(), record.getIntegral(), user.getIntegral(), "add"); for (UserIntegralRecord record : userRecords) {
record.setStatus(IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE);
// 计算积分余额
Integer balance = (currentIntegral != null ? currentIntegral : BigDecimal.ZERO)
.add(record.getIntegral()).intValue();
record.setBalance(balance);
record.setUpdateTime(cn.hutool.core.date.DateUtil.date());
updateById(record);
// 更新用户积分
userService.operationIntegral(uid, record.getIntegral(), currentIntegral, "add");
currentIntegral = balance; // 更新当前积分供下一条记录计算
}
return Boolean.TRUE; return Boolean.TRUE;
}); });
if (!execute) { if (!execute) {
logger.error(StrUtil.format("积分解冻处理—解冻出错,记录id = {}", record.getId())); logger.error(StrUtil.format("积分解冻处理—批量解冻出错,用户uid = {}记录 = {}",
uid, userRecords.size()));
} else {
logger.info("积分解冻成功—用户uid = {},解冻记录数 = {}", uid, userRecords.size());
} }
} }
} }