feat(integral-external): 新增寄卖外部免认证三件套页面
This commit is contained in:
@@ -39,3 +39,59 @@ export function getExternalIntegralLog(data) {
|
|||||||
data: body,
|
data: body,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寄卖订单 列表(免认证)
|
||||||
|
*/
|
||||||
|
export function getExternalWaOrderList(params) {
|
||||||
|
return requestNoAuth({
|
||||||
|
url: 'external/integral/wa-order/list',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寄卖订单 详情(免认证)
|
||||||
|
*/
|
||||||
|
export function getExternalWaOrderInfo(id) {
|
||||||
|
return requestNoAuth({
|
||||||
|
url: 'external/integral/wa-order/info',
|
||||||
|
method: 'get',
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账报表(免认证)
|
||||||
|
*/
|
||||||
|
export function getExternalTeamDailyReport(params) {
|
||||||
|
return requestNoAuth({
|
||||||
|
url: 'external/integral/team-report/daily',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账报表 - Excel 导出(免认证)
|
||||||
|
*/
|
||||||
|
export function exportExternalTeamDailyReport(params) {
|
||||||
|
return requestNoAuth({
|
||||||
|
url: 'external/integral/team-report/daily/export',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表(免认证)
|
||||||
|
*/
|
||||||
|
export function getExternalGrabUserList(params) {
|
||||||
|
return requestNoAuth({
|
||||||
|
url: 'external/integral/grab-user/list',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ const integralExternalRouter = {
|
|||||||
name: 'IntegralExternalUserDetail',
|
name: 'IntegralExternalUserDetail',
|
||||||
meta: { title: '用户积分明细' },
|
meta: { title: '用户积分明细' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'wa-order',
|
||||||
|
component: () => import('@/views/integral-external/wa-order/index'),
|
||||||
|
name: 'IntegralExternalWaOrder',
|
||||||
|
meta: { title: '寄卖订单' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'team-report',
|
||||||
|
component: () => import('@/views/integral-external/team-report/index'),
|
||||||
|
name: 'IntegralExternalTeamReport',
|
||||||
|
meta: { title: '团队日报' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'grab-user',
|
||||||
|
component: () => import('@/views/integral-external/grab-user/index'),
|
||||||
|
name: 'IntegralExternalGrabUser',
|
||||||
|
meta: { title: '今日抢单用户' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ service.interceptors.request.use(
|
|||||||
// 响应拦截器 — 不拦截 401 跳转
|
// 响应拦截器 — 不拦截 401 跳转
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
|
// Blob / arraybuffer 等二进制响应直接透传,不做业务码拆包
|
||||||
|
const responseType = response.config && response.config.responseType;
|
||||||
|
if (responseType === 'blob' || responseType === 'arraybuffer') {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
const res = response.data;
|
const res = response.data;
|
||||||
if (res.code !== 0 && res.code !== 200) {
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
Message({
|
Message({
|
||||||
|
|||||||
@@ -121,6 +121,27 @@ export default {
|
|||||||
url: '/integral-external/user/integral-detail',
|
url: '/integral-external/user/integral-detail',
|
||||||
alwaysShow: true,
|
alwaysShow: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
bgColor: '#F56C6C',
|
||||||
|
icon: 'icondingdanguanli',
|
||||||
|
title: '寄卖订单',
|
||||||
|
url: '/integral-external/wa-order',
|
||||||
|
alwaysShow: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '#67C23A',
|
||||||
|
icon: 'iconfenxiaoguanli',
|
||||||
|
title: '团队日报',
|
||||||
|
url: '/integral-external/team-report',
|
||||||
|
alwaysShow: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '#E6A23C',
|
||||||
|
icon: 'iconhuiyuanguanli',
|
||||||
|
title: '今日抢单用户',
|
||||||
|
url: '/integral-external/grab-user',
|
||||||
|
alwaysShow: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
statisticData: [
|
statisticData: [
|
||||||
{ title: '待发货订单', num: 0, path: '/order/index', perms: ['admin:order:list'] },
|
{ title: '待发货订单', num: 0, path: '/order/index', perms: ['admin:order:list'] },
|
||||||
|
|||||||
297
backend-adminend/src/views/integral-external/grab-user/index.vue
Normal file
297
backend-adminend/src/views/integral-external/grab-user/index.vue
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
<template>
|
||||||
|
<div class="divBox relative">
|
||||||
|
<!-- 顶部搜索区 -->
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div class="container">
|
||||||
|
<el-form
|
||||||
|
ref="searchForm"
|
||||||
|
:model="tableFrom"
|
||||||
|
inline
|
||||||
|
size="small"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户ID:">
|
||||||
|
<el-input
|
||||||
|
v-model="tableFrom.uid"
|
||||||
|
placeholder="请输入用户ID"
|
||||||
|
class="selWidth"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="seachList"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系方式:">
|
||||||
|
<el-input
|
||||||
|
v-model="tableFrom.mobile"
|
||||||
|
placeholder="请输入手机号 / 账号"
|
||||||
|
class="selWidth"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="seachList"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="上级ID:">
|
||||||
|
<el-input
|
||||||
|
v-model="tableFrom.pid"
|
||||||
|
placeholder="请输入上级ID"
|
||||||
|
class="selWidth"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="seachList"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="seachList">查询</el-button>
|
||||||
|
<el-button icon="el-icon-refresh-left" @click="resetHandler">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 主表 -->
|
||||||
|
<el-card class="box-card mt10">
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="mini" icon="el-icon-refresh" circle @click="getList" />
|
||||||
|
<el-button size="mini" icon="el-icon-upload2" circle @click="exportCsv" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="tableData.data"
|
||||||
|
size="mini"
|
||||||
|
class="table"
|
||||||
|
highlight-current-row
|
||||||
|
:header-cell-style="{ fontWeight: 'bold' }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="用户ID" min-width="80" />
|
||||||
|
<el-table-column prop="username" label="账号" min-width="130">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.username || scope.row.mobile || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="nickname" label="昵称" min-width="100" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.nickname || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="昨日卖/今日买" min-width="120" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.prevSellCnt || 0 }}/{{ scope.row.todayBuyCnt || 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="合同" min-width="80" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-link
|
||||||
|
v-if="scope.row.contract"
|
||||||
|
type="primary"
|
||||||
|
:underline="false"
|
||||||
|
@click="openContract(scope.row.contract)"
|
||||||
|
>查看</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="mobile" label="联系方式" min-width="130" />
|
||||||
|
<el-table-column label="上级ID" min-width="90" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.pid || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最高可抢单数" min-width="110" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.maxOrder ? scope.row.maxOrder : '未设置' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="todayBuyAmount" label="今日购买总金额" min-width="130" align="right" />
|
||||||
|
<el-table-column prop="todaySellAmount" label="今日卖出总金额" min-width="130" align="right" />
|
||||||
|
<el-table-column label="用户等级" min-width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span :style="{ color: levelColor(scope.row.level) }">
|
||||||
|
{{ scope.row.levelName || '普通用户' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="money" label="余额" min-width="100" align="right" />
|
||||||
|
<el-table-column prop="coupon" label="优惠券" min-width="100" align="right" />
|
||||||
|
<el-table-column prop="selfBonus" label="个人奖金" min-width="100" align="right" />
|
||||||
|
<el-table-column prop="shareBonus" label="推广奖金" min-width="100" align="right" />
|
||||||
|
<el-table-column label="状态" min-width="80" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="mini">
|
||||||
|
{{ scope.row.statusStr || (scope.row.status === 1 ? '正常' : '禁用') }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="updatedAt" label="更新时间" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" min-width="160" fixed="right" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="small" @click="onEditPid(scope.row)">修改上级</el-button>
|
||||||
|
<el-button type="text" size="small" @click="onContractRenew(scope.row)">合同重签</el-button>
|
||||||
|
<el-button type="text" size="small" @click="onEdit(scope.row)">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="block mt20">
|
||||||
|
<el-pagination
|
||||||
|
:page-sizes="[15, 30, 45, 60]"
|
||||||
|
:page-size="tableFrom.limit"
|
||||||
|
:current-page="tableFrom.page"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="tableData.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 写操作提示对话框(外部页面默认不直接修改资金类字段) -->
|
||||||
|
<el-dialog title="提示" :visible.sync="writeTipVisible" width="380px" append-to-body>
|
||||||
|
<span>{{ writeTipText }}</span>
|
||||||
|
<span slot="footer">
|
||||||
|
<el-button type="primary" size="small" @click="writeTipVisible = false">知道了</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getExternalGrabUserList } from '@/api/integralExternal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegralExternalGrabUser',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listLoading: false,
|
||||||
|
tableData: { data: [], total: 0 },
|
||||||
|
tableFrom: {
|
||||||
|
uid: '',
|
||||||
|
mobile: '',
|
||||||
|
pid: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
writeTipVisible: false,
|
||||||
|
writeTipText: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
const params = { ...this.tableFrom };
|
||||||
|
// 空字段不发送
|
||||||
|
Object.keys(params).forEach((k) => {
|
||||||
|
if (params[k] === '' || params[k] === null) delete params[k];
|
||||||
|
});
|
||||||
|
getExternalGrabUserList(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.tableData.data = res.list || [];
|
||||||
|
this.tableData.total = res.total || 0;
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
seachList() {
|
||||||
|
this.tableFrom.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetHandler() {
|
||||||
|
this.tableFrom = { uid: '', mobile: '', pid: '', page: 1, limit: 15 };
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.tableFrom.limit = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.tableFrom.page = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
openContract(url) {
|
||||||
|
if (!url) return;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
},
|
||||||
|
levelColor(level) {
|
||||||
|
switch (level) {
|
||||||
|
case 2:
|
||||||
|
return '#E6A23C';
|
||||||
|
case 3:
|
||||||
|
return '#67C23A';
|
||||||
|
default:
|
||||||
|
return '#409EFF';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEditPid() {
|
||||||
|
this.writeTipText = '请在管理后台「用户管理」中修改上级;本页面不支持写操作。';
|
||||||
|
this.writeTipVisible = true;
|
||||||
|
},
|
||||||
|
onContractRenew() {
|
||||||
|
this.writeTipText = '请在管理后台「用户管理」中进行合同重签;本页面不支持写操作。';
|
||||||
|
this.writeTipVisible = true;
|
||||||
|
},
|
||||||
|
onEdit() {
|
||||||
|
this.writeTipText = '请在管理后台「用户管理」中编辑;本页面不支持写操作。';
|
||||||
|
this.writeTipVisible = true;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 简易 CSV 导出(仅当前页)
|
||||||
|
* 生产环境如需全量导出建议改为后端 /grab-user/list/export
|
||||||
|
*/
|
||||||
|
exportCsv() {
|
||||||
|
const rows = this.tableData.data || [];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.$message.info('当前页没有数据可导出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headers = [
|
||||||
|
'用户ID', '账号', '昵称', '昨日卖/今日买', '联系方式', '上级ID',
|
||||||
|
'最高可抢单数', '今日购买总金额', '今日卖出总金额', '用户等级',
|
||||||
|
'余额', '优惠券', '个人奖金', '推广奖金', '状态', '更新时间',
|
||||||
|
];
|
||||||
|
const lines = [headers.join(',')];
|
||||||
|
rows.forEach((r) => {
|
||||||
|
const fields = [
|
||||||
|
r.id,
|
||||||
|
r.username || r.mobile || '',
|
||||||
|
r.nickname || '',
|
||||||
|
`${r.prevSellCnt || 0}/${r.todayBuyCnt || 0}`,
|
||||||
|
r.mobile || '',
|
||||||
|
r.pid || '',
|
||||||
|
r.maxOrder || '',
|
||||||
|
r.todayBuyAmount || '0',
|
||||||
|
r.todaySellAmount || '0',
|
||||||
|
r.levelName || '普通用户',
|
||||||
|
r.money || '0',
|
||||||
|
r.coupon || '0',
|
||||||
|
r.selfBonus || '0',
|
||||||
|
r.shareBonus || '0',
|
||||||
|
r.statusStr || (r.status === 1 ? '正常' : '禁用'),
|
||||||
|
r.updatedAt || '',
|
||||||
|
].map((v) => `"${String(v).replace(/"/g, '""')}"`);
|
||||||
|
lines.push(fields.join(','));
|
||||||
|
});
|
||||||
|
const blob = new Blob(['' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `今日抢单用户_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mt10 { margin-top: 10px; }
|
||||||
|
.mt20 { margin-top: 20px; }
|
||||||
|
.selWidth { width: 220px; }
|
||||||
|
.header-actions {
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.block { text-align: right; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
<template>
|
||||||
|
<div class="divBox relative">
|
||||||
|
<!-- 条件区 -->
|
||||||
|
<el-card class="box-card">
|
||||||
|
<el-form
|
||||||
|
ref="searchForm"
|
||||||
|
:model="tableFrom"
|
||||||
|
inline
|
||||||
|
size="small"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<el-form-item label="团队长 ID:">
|
||||||
|
<el-input
|
||||||
|
v-model.number="tableFrom.leaderId"
|
||||||
|
placeholder="留空=全部"
|
||||||
|
class="leaderIdWidth"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="seachList"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日期 D:">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="tableFrom.date"
|
||||||
|
type="date"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
placeholder="缺省=昨天"
|
||||||
|
class="dateWidth"
|
||||||
|
:picker-options="pickerOptions"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="含禁用成员:">
|
||||||
|
<el-switch v-model="tableFrom.includeDisabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="seachList">查询</el-button>
|
||||||
|
<el-button icon="el-icon-refresh-left" @click="resetHandler">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-download"
|
||||||
|
:disabled="!tableFrom.leaderId"
|
||||||
|
@click="exportExcel"
|
||||||
|
>导出 Excel</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div v-if="!tableFrom.leaderId" class="hint">
|
||||||
|
Tips: 团队长 ID 留空时返回所有团队的日报,导出 Excel 仅支持单团队。
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 跨团队总计(仅在多团队模式下显示) -->
|
||||||
|
<el-card v-if="report && report.teams && report.teams.length > 1" class="box-card mt10">
|
||||||
|
<div class="report-header">
|
||||||
|
<span class="title">
|
||||||
|
全部团队总计 · {{ report.teamCount }} 个团队 · 共 {{ report.totalMemberCount }} 人 · {{ report.date }}
|
||||||
|
</span>
|
||||||
|
<span class="rate-info">
|
||||||
|
服务费率 {{ report.serviceRate }} · E 积分率 {{ report.eScoreRate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="16" class="summary-row">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">{{ report.previousDate }} 买单合计</div>
|
||||||
|
<div class="summary-value">{{ formatNum(report.grandSummary.prevBuy) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">{{ report.date }} 卖单合计</div>
|
||||||
|
<div class="summary-value">{{ formatNum(report.grandSummary.todaySell) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">{{ report.date }} 买单合计</div>
|
||||||
|
<div class="summary-value">{{ formatNum(report.grandSummary.todayBuy) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">实际收付合计</div>
|
||||||
|
<div
|
||||||
|
class="summary-value"
|
||||||
|
:class="actualClass(report.grandSummary.actual)"
|
||||||
|
>{{ formatNum(report.grandSummary.actual) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 各团队子报表 -->
|
||||||
|
<template v-if="report && report.teams && report.teams.length">
|
||||||
|
<el-card
|
||||||
|
v-for="team in report.teams"
|
||||||
|
:key="team.leaderId"
|
||||||
|
class="box-card mt10 team-card"
|
||||||
|
>
|
||||||
|
<div class="report-header">
|
||||||
|
<span class="title">
|
||||||
|
团队长 {{ team.leaderNickname || team.teamCode || '-' }}
|
||||||
|
· {{ team.memberCount }} 人 · {{ team.date }}
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
v-if="!tableFrom.leaderId"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-download"
|
||||||
|
@click="exportTeamExcel(team)"
|
||||||
|
>导出该团队</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 团队级 4 卡片摘要(仅在单团队模式 / 想看每个团队也可以看;保留显示) -->
|
||||||
|
<el-row v-if="report.teams.length === 1" :gutter="16" class="summary-row">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">{{ team.previousDate }} 买单合计</div>
|
||||||
|
<div class="summary-value">{{ formatNum(team.summary.prevBuy) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">{{ team.date }} 卖单合计</div>
|
||||||
|
<div class="summary-value">{{ formatNum(team.summary.todaySell) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">{{ team.date }} 买单合计</div>
|
||||||
|
<div class="summary-value">{{ formatNum(team.summary.todayBuy) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-label">实际收付合计</div>
|
||||||
|
<div
|
||||||
|
class="summary-value"
|
||||||
|
:class="actualClass(team.summary.actual)"
|
||||||
|
>{{ formatNum(team.summary.actual) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="team.rows"
|
||||||
|
size="mini"
|
||||||
|
class="table"
|
||||||
|
border
|
||||||
|
highlight-current-row
|
||||||
|
:header-cell-style="headerCellStyle"
|
||||||
|
:show-summary="true"
|
||||||
|
:summary-method="(meta) => getTeamSummaries(meta, team)"
|
||||||
|
>
|
||||||
|
<el-table-column label="昵称" prop="nickname" min-width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.nickname || '-' }}</span>
|
||||||
|
<el-tag v-if="scope.row.status === 0" size="mini" type="info" style="margin-left: 4px">已禁用</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="prevBuyLabel(team)" prop="prevBuy" min-width="110" align="right">
|
||||||
|
<template slot-scope="scope">{{ formatNum(scope.row.prevBuy) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="todaySellLabel(team)" prop="todaySell" min-width="110" align="right">
|
||||||
|
<template slot-scope="scope">{{ formatNum(scope.row.todaySell) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="todayBuyLabel(team)" prop="todayBuy" min-width="110" align="right">
|
||||||
|
<template slot-scope="scope">{{ formatNum(scope.row.todayBuy) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="serviceFeeLabel(team)" prop="serviceFee" min-width="120" align="right">
|
||||||
|
<template slot-scope="scope">{{ formatNum(scope.row.serviceFee) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="E积分" prop="eScore" min-width="90" align="right">
|
||||||
|
<template slot-scope="scope">{{ formatNum(scope.row.eScore) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="实际收付" prop="actual" min-width="110" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span :class="actualClass(scope.row.actual)">{{ formatNum(scope.row.actual) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="团队" min-width="120" align="center">
|
||||||
|
<template>
|
||||||
|
<span>{{ team.leaderNickname || team.teamCode || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" min-width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-input
|
||||||
|
v-model="scope.row.remark"
|
||||||
|
size="mini"
|
||||||
|
placeholder="可填写(仅当前会话有效)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div v-if="!team.rows || team.rows.length === 0" class="empty-tip">该团队当日无成员数据</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-card v-else-if="report" class="box-card mt10">
|
||||||
|
<div class="empty-tip">未查询到任何团队数据。</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import {
|
||||||
|
getExternalTeamDailyReport,
|
||||||
|
exportExternalTeamDailyReport,
|
||||||
|
} from '@/api/integralExternal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegralExternalTeamReport',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listLoading: false,
|
||||||
|
report: null,
|
||||||
|
tableFrom: {
|
||||||
|
leaderId: '',
|
||||||
|
date: this.yesterdayStr(),
|
||||||
|
includeDisabled: false,
|
||||||
|
},
|
||||||
|
pickerOptions: {
|
||||||
|
disabledDate(t) {
|
||||||
|
return t.getTime() > Date.now();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 进入页面默认拉取所有团队(不传 leaderId)
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
yesterdayStr() {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() - 1);
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${m}-${day}`;
|
||||||
|
},
|
||||||
|
headerCellStyle() {
|
||||||
|
return {
|
||||||
|
background: '#C6EFCE',
|
||||||
|
color: '#000',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
seachList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
const params = {
|
||||||
|
date: this.tableFrom.date,
|
||||||
|
includeDisabled: this.tableFrom.includeDisabled,
|
||||||
|
};
|
||||||
|
if (this.tableFrom.leaderId) {
|
||||||
|
params.leaderId = this.tableFrom.leaderId;
|
||||||
|
}
|
||||||
|
getExternalTeamDailyReport(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.report = res || null;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.report = null;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetHandler() {
|
||||||
|
this.tableFrom = {
|
||||||
|
leaderId: '',
|
||||||
|
date: this.yesterdayStr(),
|
||||||
|
includeDisabled: false,
|
||||||
|
};
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
formatNum(v) {
|
||||||
|
if (v === null || v === undefined || v === '') return '0';
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isNaN(n)) return v;
|
||||||
|
const sign = n < 0 ? '-' : '';
|
||||||
|
const abs = Math.abs(n);
|
||||||
|
const fixed = abs.toFixed(2);
|
||||||
|
const [intPart, decPart] = fixed.split('.');
|
||||||
|
const withSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
return `${sign}${withSep}.${decPart}`;
|
||||||
|
},
|
||||||
|
actualClass(v) {
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isNaN(n)) return '';
|
||||||
|
if (n < 0) return 'text-danger';
|
||||||
|
if (n === 0) return 'text-muted';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
prevBuyLabel(team) {
|
||||||
|
return team && team.previousDate ? `${team.previousDate.slice(5)} 买单` : 'D-1 买单';
|
||||||
|
},
|
||||||
|
todaySellLabel(team) {
|
||||||
|
return team && team.date ? `${team.date.slice(5)} 卖单` : 'D 卖单';
|
||||||
|
},
|
||||||
|
todayBuyLabel(team) {
|
||||||
|
return team && team.date ? `${team.date.slice(5)} 买单` : 'D 买单';
|
||||||
|
},
|
||||||
|
serviceFeeLabel(team) {
|
||||||
|
return team && team.serviceRate
|
||||||
|
? `服务费*${team.serviceRate}`
|
||||||
|
: `服务费*${this.report ? this.report.serviceRate : '0.02'}`;
|
||||||
|
},
|
||||||
|
/** 团队子表的小计行(直接取 team.summary,不在前端逐行累加) */
|
||||||
|
getTeamSummaries({ columns }, team) {
|
||||||
|
const result = new Array(columns.length).fill('');
|
||||||
|
if (!team || !team.summary) return result;
|
||||||
|
const s = team.summary;
|
||||||
|
const map = {
|
||||||
|
nickname: '小计',
|
||||||
|
prevBuy: s.prevBuy,
|
||||||
|
todaySell: s.todaySell,
|
||||||
|
todayBuy: s.todayBuy,
|
||||||
|
serviceFee:s.serviceFee,
|
||||||
|
eScore: s.eScore,
|
||||||
|
actual: s.actual,
|
||||||
|
};
|
||||||
|
columns.forEach((col, idx) => {
|
||||||
|
const v = map[col.property];
|
||||||
|
if (v !== undefined && v !== null) {
|
||||||
|
result[idx] = col.property === 'nickname' ? v : this.formatNum(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
/** 工具栏里的"导出 Excel"按钮:使用当前 tableFrom 的 leaderId */
|
||||||
|
exportExcel() {
|
||||||
|
if (!this.tableFrom.leaderId) {
|
||||||
|
this.$message.warning('Excel 导出需要指定团队长 ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.doExport(this.tableFrom.leaderId, '');
|
||||||
|
},
|
||||||
|
/** 多团队卡片右上角的"导出该团队"按钮 */
|
||||||
|
exportTeamExcel(team) {
|
||||||
|
if (!team || !team.leaderId) return;
|
||||||
|
this.doExport(team.leaderId, team.leaderNickname || team.teamCode || '');
|
||||||
|
},
|
||||||
|
doExport(leaderId, nickname) {
|
||||||
|
this.listLoading = true;
|
||||||
|
exportExternalTeamDailyReport({
|
||||||
|
leaderId,
|
||||||
|
date: this.tableFrom.date,
|
||||||
|
includeDisabled: this.tableFrom.includeDisabled,
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
const file = blob instanceof Blob ? blob : new Blob([blob], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const date = this.tableFrom.date || this.yesterdayStr();
|
||||||
|
const name = nickname || `leader${leaderId}`;
|
||||||
|
saveAs(file, `团队${name}_${date}.xlsx`);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mt10 { margin-top: 10px; }
|
||||||
|
.selWidth { width: 240px; }
|
||||||
|
.leaderIdWidth { width: 100px; }
|
||||||
|
.dateWidth { width: 160px; }
|
||||||
|
.hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.team-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.report-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.rate-info {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.summary-row {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.summary-card {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
.summary-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.summary-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.text-danger { color: #C00000; font-weight: bold; }
|
||||||
|
.text-muted { color: #909399; }
|
||||||
|
.empty-tip {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
747
backend-adminend/src/views/integral-external/wa-order/index.vue
Normal file
747
backend-adminend/src/views/integral-external/wa-order/index.vue
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
<template>
|
||||||
|
<div class="divBox relative">
|
||||||
|
<!-- 顶部搜索区 -->
|
||||||
|
<el-card class="box-card">
|
||||||
|
<el-form size="small" inline label-width="100px">
|
||||||
|
<el-form-item label="订单状态:">
|
||||||
|
<el-radio-group v-model="uiStatus" type="button" size="mini" @change="onStatusChange">
|
||||||
|
<el-radio-button label="all">全部</el-radio-button>
|
||||||
|
<el-radio-button label="unPaid">待付款</el-radio-button>
|
||||||
|
<el-radio-button label="paid">已支付</el-radio-button>
|
||||||
|
<el-radio-button label="complete">交易完成</el-radio-button>
|
||||||
|
<el-radio-button label="cancel">已取消</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="时间字段:">
|
||||||
|
<el-select
|
||||||
|
v-model="timeField"
|
||||||
|
placeholder="选择时间维度"
|
||||||
|
style="width: 140px"
|
||||||
|
size="mini"
|
||||||
|
@change="seachList"
|
||||||
|
>
|
||||||
|
<el-option label="抢购时间" value="buyTime" />
|
||||||
|
<el-option label="支付时间" value="payTime" />
|
||||||
|
<el-option label="完成时间" value="confirmTime" />
|
||||||
|
<el-option label="创建时间" value="createdAt" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日期范围:">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="timeVal"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
format="yyyy-MM-dd"
|
||||||
|
size="mini"
|
||||||
|
type="daterange"
|
||||||
|
placeholder="自定义时间"
|
||||||
|
style="width: 220px"
|
||||||
|
@change="onTimeChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单号:">
|
||||||
|
<el-input
|
||||||
|
v-model="tableFrom.orderSn"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
clearable
|
||||||
|
class="selWidth"
|
||||||
|
size="mini"
|
||||||
|
@keyup.enter.native="seachList"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="买家ID:">
|
||||||
|
<el-input
|
||||||
|
v-model.number="tableFrom.buyerId"
|
||||||
|
placeholder="买家用户ID"
|
||||||
|
clearable
|
||||||
|
class="selWidth"
|
||||||
|
size="mini"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="卖家ID:">
|
||||||
|
<el-input
|
||||||
|
v-model.number="tableFrom.sellerId"
|
||||||
|
placeholder="卖家ID(0=平台)"
|
||||||
|
clearable
|
||||||
|
class="selWidth"
|
||||||
|
size="mini"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否转拍:">
|
||||||
|
<el-select
|
||||||
|
v-model="tableFrom.isResell"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 100px"
|
||||||
|
size="mini"
|
||||||
|
@change="seachList"
|
||||||
|
>
|
||||||
|
<el-option :value="1" label="是" />
|
||||||
|
<el-option :value="0" label="否" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="seachList">查询</el-button>
|
||||||
|
<el-button icon="el-icon-refresh-left" size="mini" @click="resetHandler">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 操作按钮区 -->
|
||||||
|
<el-card class="box-card mt10">
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-popover
|
||||||
|
placement="bottom-end"
|
||||||
|
width="300"
|
||||||
|
trigger="click"
|
||||||
|
v-model="columnPopoverVisible"
|
||||||
|
>
|
||||||
|
<div class="column-setting">
|
||||||
|
<div class="column-setting-actions">
|
||||||
|
<el-button size="mini" type="text" @click="selectAllColumns">全选</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="invertColumns">反选</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="resetColumns">恢复默认</el-button>
|
||||||
|
</div>
|
||||||
|
<el-checkbox-group v-model="visibleColumns" class="column-checkbox-group">
|
||||||
|
<el-checkbox
|
||||||
|
v-for="col in columnsConfig"
|
||||||
|
:key="col.prop"
|
||||||
|
:label="col.prop"
|
||||||
|
:disabled="col.fixed"
|
||||||
|
>{{ col.label }}</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
<el-button slot="reference" size="mini" icon="el-icon-setting">列设置</el-button>
|
||||||
|
</el-popover>
|
||||||
|
<el-button size="mini" icon="el-icon-refresh" @click="getList" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="tableData.data"
|
||||||
|
size="mini"
|
||||||
|
class="table"
|
||||||
|
highlight-current-row
|
||||||
|
:header-cell-style="{ fontWeight: 'bold' }"
|
||||||
|
>
|
||||||
|
<template v-for="col in columnsConfig">
|
||||||
|
<el-table-column
|
||||||
|
v-if="col.prop !== 'actions' && visibleColumns.includes(col.prop)"
|
||||||
|
:key="col.prop"
|
||||||
|
:label="col.label"
|
||||||
|
:min-width="col.width"
|
||||||
|
:show-overflow-tooltip="col.tooltip"
|
||||||
|
:align="col.align || 'left'"
|
||||||
|
>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<!-- 商品图片单元格使用 el-image,其他文本走 formatCell -->
|
||||||
|
<el-image
|
||||||
|
v-if="col.prop === 'merchandiseImage' && scope.row.merchandiseImage"
|
||||||
|
:src="scope.row.merchandiseImage"
|
||||||
|
:preview-src-list="[scope.row.merchandiseImage]"
|
||||||
|
style="width:48px;height:48px;border-radius:4px"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span v-else-if="col.prop === 'merchandiseImage'">-</span>
|
||||||
|
<span v-else v-html="formatCell(col.prop, scope.row)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
<el-table-column label="操作" min-width="120" fixed="right" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="small" @click="openDetail(scope.row.id)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="block mt20">
|
||||||
|
<el-pagination
|
||||||
|
:page-sizes="[15, 30, 45, 60]"
|
||||||
|
:page-size="tableFrom.limit"
|
||||||
|
:current-page="tableFrom.page"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="tableData.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 详情抽屉 -->
|
||||||
|
<el-drawer
|
||||||
|
:visible.sync="drawerVisible"
|
||||||
|
direction="rtl"
|
||||||
|
size="680px"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:with-header="false"
|
||||||
|
append-to-body
|
||||||
|
custom-class="wa-order-drawer"
|
||||||
|
@close="onDrawerClose"
|
||||||
|
>
|
||||||
|
<div v-loading="detailLoading" class="drawer-body">
|
||||||
|
<!-- 顶部摘要:订单号 + 状态徽标 + 关闭 -->
|
||||||
|
<div class="drawer-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="order-sn-row">
|
||||||
|
<span class="order-sn-label">订单号</span>
|
||||||
|
<span class="order-sn">{{ detail ? detail.orderSn : '-' }}</span>
|
||||||
|
<el-button
|
||||||
|
v-if="detail"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-document-copy"
|
||||||
|
@click="copyOrderSn"
|
||||||
|
>复制</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="tag-row">
|
||||||
|
<el-tag v-if="detail" :type="statusTagType(detail)" size="small">{{ statusLabel(detail) }}</el-tag>
|
||||||
|
<el-tag v-if="detail && detail.isResell === 1" type="warning" size="small" effect="plain">转拍</el-tag>
|
||||||
|
<el-tag v-if="detail && detail.isCancel === 1" type="danger" size="small" effect="plain">已取消</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
class="close-btn"
|
||||||
|
icon="el-icon-close"
|
||||||
|
type="text"
|
||||||
|
@click="drawerVisible = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="detail" class="drawer-content">
|
||||||
|
<!-- 时间链路 -->
|
||||||
|
<el-card shadow="never" class="block-card">
|
||||||
|
<div slot="header" class="block-title">订单时间链路</div>
|
||||||
|
<el-steps :active="stepActive(detail)" finish-status="success" align-center>
|
||||||
|
<el-step title="抢购下单" :description="detail.buyTime || '-'" />
|
||||||
|
<el-step
|
||||||
|
v-if="detail.isResell === 1"
|
||||||
|
title="转拍生成"
|
||||||
|
:description="detail.createdAt || '-'"
|
||||||
|
/>
|
||||||
|
<el-step title="支付" :description="detail.payTime || '-'" />
|
||||||
|
<el-step title="完成" :description="detail.confirmTime || '-'" />
|
||||||
|
</el-steps>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单基础 -->
|
||||||
|
<el-card shadow="never" class="block-card">
|
||||||
|
<div slot="header" class="block-title">订单基础</div>
|
||||||
|
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
|
||||||
|
<el-descriptions-item label="订单ID">{{ detail.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单金额">
|
||||||
|
<span class="price">¥{{ detail.totalMoney }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品ID">{{ detail.merchandiseId || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称">{{ detail.merchandiseTitle || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="是否显示">{{ detail.isShow === 1 ? '显示' : '隐藏' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原订单ID">{{ detail.oldId || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品图片" :span="2">
|
||||||
|
<el-image
|
||||||
|
v-if="detail.merchandiseImage"
|
||||||
|
:src="detail.merchandiseImage"
|
||||||
|
:preview-src-list="[detail.merchandiseImage]"
|
||||||
|
style="width:80px;height:80px;border-radius:4px"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 买家 -->
|
||||||
|
<el-card shadow="never" class="block-card">
|
||||||
|
<div slot="header" class="block-title">买家信息</div>
|
||||||
|
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
|
||||||
|
<el-descriptions-item label="买家ID">{{ detail.buyerId || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="买家昵称">{{ detail.buyerName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货人">{{ detail.consignee || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货电话">{{ detail.phone || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货地区" :span="2">{{ joinArea(detail) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="详细地址" :span="2">{{ detail.address || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 卖家 -->
|
||||||
|
<el-card shadow="never" class="block-card">
|
||||||
|
<div slot="header" class="block-title">卖家信息</div>
|
||||||
|
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
|
||||||
|
<el-descriptions-item label="卖家ID">
|
||||||
|
{{ detail.sellerId === 0 ? '0(平台)' : detail.sellerId }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家昵称">
|
||||||
|
{{ detail.sellerId === 0 ? '平台' : (detail.sellerName || '-') }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付与日志 -->
|
||||||
|
<el-card shadow="never" class="block-card">
|
||||||
|
<div slot="header" class="block-title">支付与日志</div>
|
||||||
|
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
|
||||||
|
<el-descriptions-item label="抢购时间">{{ detail.buyTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付时间">{{ detail.payTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="完成时间">{{ detail.confirmTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ detail.createdAt || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="下单IP">{{ detail.buyIp || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="detail.isCancel === 1" label="取消IP">
|
||||||
|
{{ detail.cancelIp || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付凭证" :span="2">
|
||||||
|
<el-image
|
||||||
|
v-if="detail.payImg"
|
||||||
|
:src="detail.payImg"
|
||||||
|
:preview-src-list="[detail.payImg]"
|
||||||
|
style="width:80px;height:80px;border-radius:4px"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<el-button @click="drawerVisible = false">关 闭</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getExternalWaOrderList,
|
||||||
|
getExternalWaOrderInfo,
|
||||||
|
} from '@/api/integralExternal';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'waOrderColumns:external';
|
||||||
|
|
||||||
|
const COLUMN_DEFS = [
|
||||||
|
{ prop: 'id', label: '订单ID', width: 80, defaultVisible: true, fixed: true, tooltip: false, align: 'left' },
|
||||||
|
{ prop: 'orderSn', label: '订单号', width: 210, defaultVisible: true, fixed: true, tooltip: true },
|
||||||
|
{ prop: 'merchandiseId', label: '商品ID', width: 90, defaultVisible: true, align: 'left' },
|
||||||
|
{ prop: 'merchandiseTitle', label: '商品名称', width: 200, defaultVisible: true, tooltip: true },
|
||||||
|
{ prop: 'merchandiseImage', label: '商品图片', width: 90, defaultVisible: false, align: 'center' },
|
||||||
|
{ prop: 'totalMoney', label: '订单金额', width: 110, defaultVisible: true, align: 'right' },
|
||||||
|
{ prop: 'statusStr', label: '订单状态', width: 110, defaultVisible: true, align: 'center' },
|
||||||
|
{ prop: 'isResell', label: '是否转拍', width: 90, defaultVisible: false, align: 'center' },
|
||||||
|
{ prop: 'buyerName', label: '买家昵称', width: 120, defaultVisible: true, tooltip: true },
|
||||||
|
{ prop: 'buyerId', label: '买家ID', width: 90, defaultVisible: false },
|
||||||
|
{ prop: 'sellerName', label: '卖家昵称', width: 120, defaultVisible: true, tooltip: true },
|
||||||
|
{ prop: 'sellerId', label: '卖家ID', width: 90, defaultVisible: false },
|
||||||
|
{ prop: 'consignee', label: '收货人', width: 100, defaultVisible: false },
|
||||||
|
{ prop: 'phone', label: '收货电话', width: 130, defaultVisible: false },
|
||||||
|
{ prop: 'areaText', label: '收货地区', width: 180, defaultVisible: false, tooltip: true },
|
||||||
|
{ prop: 'address', label: '详细地址', width: 220, defaultVisible: false, tooltip: true },
|
||||||
|
{ prop: 'buyTime', label: '抢购时间', width: 160, defaultVisible: true },
|
||||||
|
{ prop: 'createdAt', label: '转拍/创建时间', width: 160, defaultVisible: false },
|
||||||
|
{ prop: 'payTime', label: '支付时间', width: 160, defaultVisible: true },
|
||||||
|
{ prop: 'confirmTime', label: '完成时间', width: 160, defaultVisible: true },
|
||||||
|
{ prop: 'updatedAt', label: '更新时间', width: 160, defaultVisible: false },
|
||||||
|
{ prop: 'buyIp', label: '下单IP', width: 130, defaultVisible: false },
|
||||||
|
{ prop: 'cancelIp', label: '取消IP', width: 130, defaultVisible: false },
|
||||||
|
{ prop: 'isShow', label: '显示', width: 80, defaultVisible: false, align: 'center' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegralExternalWaOrder',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listLoading: false,
|
||||||
|
tableData: { data: [], total: 0 },
|
||||||
|
tableFrom: {
|
||||||
|
orderSn: '',
|
||||||
|
buyerId: '',
|
||||||
|
sellerId: '',
|
||||||
|
status: null,
|
||||||
|
isCancel: null,
|
||||||
|
isResell: null,
|
||||||
|
buyTimeStart: '',
|
||||||
|
buyTimeEnd: '',
|
||||||
|
confirmTimeStart: '',
|
||||||
|
confirmTimeEnd: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
uiStatus: 'all',
|
||||||
|
timeField: 'buyTime',
|
||||||
|
timeVal: [],
|
||||||
|
columnsConfig: COLUMN_DEFS.concat([{ prop: 'actions', label: '操作', fixed: true }]),
|
||||||
|
visibleColumns: this.loadColumns(),
|
||||||
|
columnPopoverVisible: false,
|
||||||
|
drawerVisible: false,
|
||||||
|
detail: null,
|
||||||
|
detailLoading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 支持 ?detailId=xxx 直接打开抽屉
|
||||||
|
if (this.$route && this.$route.query && this.$route.query.detailId) {
|
||||||
|
this.openDetail(this.$route.query.detailId);
|
||||||
|
}
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadColumns() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const arr = JSON.parse(raw);
|
||||||
|
if (Array.isArray(arr) && arr.length) {
|
||||||
|
const validProps = COLUMN_DEFS.map((c) => c.prop);
|
||||||
|
// 1) 与最新字段定义做交集;2) 锁定列必须存在;3) 新增的 defaultVisible 列自动并入
|
||||||
|
const next = arr.filter((p) => validProps.includes(p));
|
||||||
|
COLUMN_DEFS.forEach((c) => {
|
||||||
|
if ((c.fixed || c.defaultVisible) && !next.includes(c.prop)) {
|
||||||
|
next.push(c.prop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
return COLUMN_DEFS.filter((c) => c.defaultVisible || c.fixed).map((c) => c.prop);
|
||||||
|
},
|
||||||
|
persistColumns() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.visibleColumns));
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
},
|
||||||
|
selectAllColumns() {
|
||||||
|
this.visibleColumns = COLUMN_DEFS.map((c) => c.prop);
|
||||||
|
this.persistColumns();
|
||||||
|
},
|
||||||
|
invertColumns() {
|
||||||
|
const all = COLUMN_DEFS.map((c) => c.prop);
|
||||||
|
const next = all.filter((p) => !this.visibleColumns.includes(p));
|
||||||
|
// 锁定列必须保留
|
||||||
|
COLUMN_DEFS.filter((c) => c.fixed).forEach((c) => {
|
||||||
|
if (!next.includes(c.prop)) next.push(c.prop);
|
||||||
|
});
|
||||||
|
this.visibleColumns = next;
|
||||||
|
this.persistColumns();
|
||||||
|
},
|
||||||
|
resetColumns() {
|
||||||
|
this.visibleColumns = COLUMN_DEFS.filter((c) => c.defaultVisible || c.fixed).map((c) => c.prop);
|
||||||
|
this.persistColumns();
|
||||||
|
},
|
||||||
|
onStatusChange() {
|
||||||
|
this.applyUiStatus();
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
applyUiStatus() {
|
||||||
|
// UI 标签 → status / isCancel 组合
|
||||||
|
switch (this.uiStatus) {
|
||||||
|
case 'unPaid':
|
||||||
|
this.tableFrom.status = 0;
|
||||||
|
this.tableFrom.isCancel = 0;
|
||||||
|
break;
|
||||||
|
case 'paid':
|
||||||
|
this.tableFrom.status = 1;
|
||||||
|
this.tableFrom.isCancel = 0;
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
this.tableFrom.status = 2;
|
||||||
|
this.tableFrom.isCancel = 0;
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
this.tableFrom.status = null;
|
||||||
|
this.tableFrom.isCancel = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.tableFrom.status = null;
|
||||||
|
this.tableFrom.isCancel = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTimeChange(val) {
|
||||||
|
// 清掉所有时间字段
|
||||||
|
this.tableFrom.buyTimeStart = '';
|
||||||
|
this.tableFrom.buyTimeEnd = '';
|
||||||
|
this.tableFrom.confirmTimeStart = '';
|
||||||
|
this.tableFrom.confirmTimeEnd = '';
|
||||||
|
if (val && val.length === 2) {
|
||||||
|
const startKey = `${this.timeField}Start`;
|
||||||
|
const endKey = `${this.timeField}End`;
|
||||||
|
// WaOrderSearchRequest 仅支持 buyTime / confirmTime;其余维度回退到 buyTime
|
||||||
|
if (startKey in this.tableFrom) {
|
||||||
|
this.tableFrom[startKey] = `${val[0]} 00:00:00`;
|
||||||
|
this.tableFrom[endKey] = `${val[1]} 23:59:59`;
|
||||||
|
} else {
|
||||||
|
this.tableFrom.buyTimeStart = `${val[0]} 00:00:00`;
|
||||||
|
this.tableFrom.buyTimeEnd = `${val[1]} 23:59:59`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
seachList() {
|
||||||
|
this.tableFrom.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetHandler() {
|
||||||
|
this.tableFrom = {
|
||||||
|
orderSn: '', buyerId: '', sellerId: '',
|
||||||
|
status: null, isCancel: null, isResell: null,
|
||||||
|
buyTimeStart: '', buyTimeEnd: '', confirmTimeStart: '', confirmTimeEnd: '',
|
||||||
|
page: 1, limit: 15,
|
||||||
|
};
|
||||||
|
this.uiStatus = 'all';
|
||||||
|
this.timeField = 'buyTime';
|
||||||
|
this.timeVal = [];
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
const params = { ...this.tableFrom };
|
||||||
|
// null/空字段不发
|
||||||
|
Object.keys(params).forEach((k) => {
|
||||||
|
if (params[k] === '' || params[k] === null) delete params[k];
|
||||||
|
});
|
||||||
|
getExternalWaOrderList(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.tableData.data = res.list || [];
|
||||||
|
this.tableData.total = res.total || 0;
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.tableFrom.limit = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.tableFrom.page = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
formatCell(prop, row) {
|
||||||
|
if (!row) return '-';
|
||||||
|
switch (prop) {
|
||||||
|
case 'totalMoney':
|
||||||
|
return row.totalMoney != null ? `¥${row.totalMoney}` : '-';
|
||||||
|
case 'statusStr': {
|
||||||
|
const status = this.statusLabel(row);
|
||||||
|
const type = this.statusTagType(row);
|
||||||
|
return `<span class="el-tag el-tag--${type} el-tag--mini">${status}</span>`;
|
||||||
|
}
|
||||||
|
case 'isResell':
|
||||||
|
return row.isResell === 1
|
||||||
|
? '<span style="color:#E6A23C">是</span>'
|
||||||
|
: '否';
|
||||||
|
case 'sellerId':
|
||||||
|
return row.sellerId === 0 ? '0(平台)' : row.sellerId;
|
||||||
|
case 'sellerName':
|
||||||
|
return row.sellerId === 0 ? '平台' : (row.sellerName || '-');
|
||||||
|
case 'areaText':
|
||||||
|
return this.joinArea(row);
|
||||||
|
case 'isShow':
|
||||||
|
return row.isShow === 1 ? '是' : '否';
|
||||||
|
case 'merchandiseTitle':
|
||||||
|
return row.merchandiseTitle || row.productName || '-';
|
||||||
|
default: {
|
||||||
|
const v = row[prop];
|
||||||
|
return v == null || v === '' ? '-' : v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusLabel(row) {
|
||||||
|
if (!row) return '-';
|
||||||
|
if (row.isCancel === 1) return '已取消';
|
||||||
|
if (row.isResell === 1 && row.status === 0) return '转拍中';
|
||||||
|
switch (row.status) {
|
||||||
|
case 0: return '待付款';
|
||||||
|
case 1: return '已支付';
|
||||||
|
case 2: return '交易完成';
|
||||||
|
default: return '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusTagType(row) {
|
||||||
|
if (!row) return 'info';
|
||||||
|
if (row.isCancel === 1) return 'info';
|
||||||
|
switch (row.status) {
|
||||||
|
case 0: return 'warning';
|
||||||
|
case 1: return 'primary';
|
||||||
|
case 2: return 'success';
|
||||||
|
default: return 'info';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stepActive(row) {
|
||||||
|
if (!row) return 0;
|
||||||
|
if (row.confirmTime) return row.isResell === 1 ? 4 : 3;
|
||||||
|
if (row.payTime) return row.isResell === 1 ? 3 : 2;
|
||||||
|
if (row.isResell === 1 && row.createdAt) return 2;
|
||||||
|
if (row.buyTime) return 1;
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
joinArea(row) {
|
||||||
|
if (!row) return '-';
|
||||||
|
const parts = [row.province, row.city, row.area].filter(Boolean);
|
||||||
|
return parts.length ? parts.join(' / ') : '-';
|
||||||
|
},
|
||||||
|
openDetail(id) {
|
||||||
|
if (!id) return;
|
||||||
|
this.drawerVisible = true;
|
||||||
|
this.detailLoading = true;
|
||||||
|
this.detail = null;
|
||||||
|
getExternalWaOrderInfo(id)
|
||||||
|
.then((res) => {
|
||||||
|
this.detail = res || null;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$message.error('订单详情加载失败');
|
||||||
|
this.drawerVisible = false;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.detailLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDrawerClose() {
|
||||||
|
this.detail = null;
|
||||||
|
// 清掉 URL 上的 detailId
|
||||||
|
if (this.$route && this.$route.query && this.$route.query.detailId) {
|
||||||
|
const q = { ...this.$route.query };
|
||||||
|
delete q.detailId;
|
||||||
|
this.$router.replace({ path: this.$route.path, query: q });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyOrderSn() {
|
||||||
|
if (!this.detail || !this.detail.orderSn) return;
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = this.detail.orderSn;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
try { document.execCommand('copy'); this.$message.success('已复制订单号'); }
|
||||||
|
catch (e) { this.$message.error('复制失败'); }
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visibleColumns: {
|
||||||
|
deep: true,
|
||||||
|
handler() { this.persistColumns(); },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mt10 { margin-top: 10px; }
|
||||||
|
.mt20 { margin-top: 20px; }
|
||||||
|
.selWidth { width: 180px; }
|
||||||
|
.header-actions {
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.column-setting {
|
||||||
|
.column-setting-actions {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed #ebeef5;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.column-checkbox-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 6px 0;
|
||||||
|
}
|
||||||
|
.block { text-align: right; }
|
||||||
|
|
||||||
|
/* ===== 抽屉详情样式 ===== */
|
||||||
|
.drawer-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.drawer-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
.header-left { flex: 1; min-width: 0; }
|
||||||
|
.order-sn-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
.order-sn-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.order-sn {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tag-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drawer-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.drawer-footer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
text-align: right;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.block-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
::v-deep .el-card__header {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.block-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 8px;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 3px;
|
||||||
|
width: 3px; height: 14px;
|
||||||
|
background: #409EFF;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.info-desc {
|
||||||
|
::v-deep .el-descriptions__label {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: normal;
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
::v-deep .el-descriptions__content {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
::v-deep .wa-order-drawer .el-drawer__body {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,20 +3,35 @@ package com.zbkj.admin.controller;
|
|||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import com.zbkj.common.page.CommonPage;
|
import com.zbkj.common.page.CommonPage;
|
||||||
import com.zbkj.common.request.*;
|
import com.zbkj.common.request.*;
|
||||||
|
import com.zbkj.common.response.ExternalGrabUserResponse;
|
||||||
import com.zbkj.common.response.StoreOrderDetailResponse;
|
import com.zbkj.common.response.StoreOrderDetailResponse;
|
||||||
|
import com.zbkj.common.response.TeamDailyMultiReportResponse;
|
||||||
|
import com.zbkj.common.response.TeamDailyReportResponse;
|
||||||
import com.zbkj.common.response.UserIntegralRecordResponse;
|
import com.zbkj.common.response.UserIntegralRecordResponse;
|
||||||
import com.zbkj.common.response.UserResponse;
|
import com.zbkj.common.response.UserResponse;
|
||||||
|
import com.zbkj.common.response.WaOrderResponse;
|
||||||
import com.zbkj.common.result.CommonResult;
|
import com.zbkj.common.result.CommonResult;
|
||||||
|
import com.zbkj.service.service.ExternalGrabUserService;
|
||||||
import com.zbkj.service.service.StoreOrderService;
|
import com.zbkj.service.service.StoreOrderService;
|
||||||
|
import com.zbkj.service.service.TeamReportExternalService;
|
||||||
import com.zbkj.service.service.UserIntegralRecordService;
|
import com.zbkj.service.service.UserIntegralRecordService;
|
||||||
import com.zbkj.service.service.UserService;
|
import com.zbkj.service.service.UserService;
|
||||||
|
import com.zbkj.service.service.WaOrderAdminService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 积分模块外部免认证接口 Controller
|
* 积分模块外部免认证接口 Controller
|
||||||
* 供管理后台外部页面(/integral-external/*)调用,跳过登录验证。
|
* 供管理后台外部页面(/integral-external/*)调用,跳过登录验证。
|
||||||
@@ -40,6 +55,15 @@ public class ExternalIntegralController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WaOrderAdminService waOrderAdminService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TeamReportExternalService teamReportExternalService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ExternalGrabUserService externalGrabUserService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 积分明细分页列表(免认证)
|
* 积分明细分页列表(免认证)
|
||||||
* 复用 UserIntegralRecordService.findAdminList,与 /admin/user/integral/list 逻辑完全一致。
|
* 复用 UserIntegralRecordService.findAdminList,与 /admin/user/integral/list 逻辑完全一致。
|
||||||
@@ -93,4 +117,90 @@ public class ExternalIntegralController {
|
|||||||
}
|
}
|
||||||
return CommonResult.success(restPage);
|
return CommonResult.success(restPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 寄卖订单管理(wa_order)
|
||||||
|
// 复用 WaOrderAdminService(仅读路径),不使用 @PreAuthorize。
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寄卖订单分页列表(免认证)
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "寄卖订单分页列表(免认证)")
|
||||||
|
@GetMapping(value = "/wa-order/list")
|
||||||
|
public CommonResult<CommonPage<WaOrderResponse>> waOrderList(
|
||||||
|
@ModelAttribute @Validated WaOrderSearchRequest request,
|
||||||
|
@Validated PageParamRequest pageParamRequest) {
|
||||||
|
CommonPage<WaOrderResponse> restPage =
|
||||||
|
CommonPage.restPage(waOrderAdminService.getAdminList(request, pageParamRequest));
|
||||||
|
return CommonResult.success(restPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寄卖订单详情(免认证)
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "寄卖订单详情(免认证)")
|
||||||
|
@GetMapping(value = "/wa-order/info")
|
||||||
|
public CommonResult<WaOrderResponse> waOrderInfo(@RequestParam Integer id) {
|
||||||
|
return CommonResult.success(waOrderAdminService.getDetailById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 团队每日对账日报
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报(免认证)。
|
||||||
|
* - leaderId 非空:返回该团队对账(teams 仅 1 项)
|
||||||
|
* - leaderId 为空:按团队长分组返回所有团队对账 + 跨团队总计
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "团队每日对账(免认证)")
|
||||||
|
@GetMapping(value = "/team-report/daily")
|
||||||
|
public CommonResult<TeamDailyMultiReportResponse> teamDailyReport(
|
||||||
|
@ModelAttribute @Validated TeamDailyReportRequest request) {
|
||||||
|
return CommonResult.success(teamReportExternalService.getMultiDailyReport(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 - Excel 导出(免认证;仅支持单团队,必须传 leaderId)
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "团队每日对账 Excel 导出(免认证)")
|
||||||
|
@GetMapping(value = "/team-report/daily/export")
|
||||||
|
public ResponseEntity<byte[]> teamDailyReportExport(
|
||||||
|
@ModelAttribute @Validated TeamDailyReportRequest request) throws IOException {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
TeamDailyReportResponse data = teamReportExternalService.exportDailyReport(request, out);
|
||||||
|
|
||||||
|
String fileName = String.format("团队%s_%s.xlsx",
|
||||||
|
data.getTeamCode() == null ? "" : data.getTeamCode(),
|
||||||
|
data.getDate());
|
||||||
|
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.parseMediaType(
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||||
|
headers.add(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"" + encoded + "\"; filename*=UTF-8''" + encoded);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(out.toByteArray(), headers, org.springframework.http.HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 今日抢单用户列表
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表(免认证)
|
||||||
|
* 过滤口径:今日购买总金额 > 0;已在 SQL 层落实。
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "今日抢单用户分页列表(免认证)")
|
||||||
|
@GetMapping(value = "/grab-user/list")
|
||||||
|
public CommonResult<CommonPage<ExternalGrabUserResponse>> grabUserList(
|
||||||
|
@ModelAttribute @Validated ExternalGrabUserRequest request,
|
||||||
|
@Validated PageParamRequest pageParamRequest) {
|
||||||
|
CommonPage<ExternalGrabUserResponse> restPage =
|
||||||
|
CommonPage.restPage(externalGrabUserService.list(request, pageParamRequest));
|
||||||
|
return CommonResult.success(restPage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zbkj.common.request;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表 查询请求(外部免认证)
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "ExternalGrabUserRequest 对象", description = "今日抢单用户列表查询请求")
|
||||||
|
public class ExternalGrabUserRequest implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户ID(精确)")
|
||||||
|
private Integer uid;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "联系方式(模糊匹配 mobile / username)")
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "上级ID(精确)")
|
||||||
|
private Integer pid;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.zbkj.common.request;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 查询请求(外部免认证)
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "TeamDailyReportRequest 对象", description = "团队每日对账查询请求")
|
||||||
|
public class TeamDailyReportRequest implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "团队长 ID(wa_users.id);为空时按团队长分组返回所有团队")
|
||||||
|
private Integer leaderId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "查询日期 D(yyyy-MM-dd),缺省为昨天")
|
||||||
|
private String date;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否包含禁用成员,默认 false")
|
||||||
|
private Boolean includeDisabled = Boolean.FALSE;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "限定成员 ID 列表(前端勾选过滤)")
|
||||||
|
private List<Integer> memberIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.zbkj.common.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表 响应对象(外部免认证)
|
||||||
|
* 字段保留 3 位小数(与上传参考图一致),由 Service 层格式化为字符串。
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "ExternalGrabUserResponse 对象", description = "今日抢单用户列表响应")
|
||||||
|
public class ExternalGrabUserResponse implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户ID")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "账号 / 用户名")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "昵称")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "手机号 / 联系方式")
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "合同 URL(为空表示未上传)")
|
||||||
|
private String contract;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "上级ID")
|
||||||
|
private Integer pid;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "最高可抢单数")
|
||||||
|
private Integer maxOrder;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户等级(数值)")
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户等级文案")
|
||||||
|
private String levelName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "余额(保留 3 位小数)")
|
||||||
|
private String money;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "优惠券(保留 3 位小数)")
|
||||||
|
private String coupon;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "个人奖金(保留 3 位小数)")
|
||||||
|
private String selfBonus;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "推广奖金(保留 3 位小数)")
|
||||||
|
private String shareBonus;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "状态:0=禁用,1=正常")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "状态文案")
|
||||||
|
private String statusStr;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
|
||||||
|
private Date updatedAt;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "今日购买总金额(保留 3 位小数)")
|
||||||
|
private String todayBuyAmount;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "今日卖出总金额(保留 3 位小数)")
|
||||||
|
private String todaySellAmount;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "今日买单数")
|
||||||
|
private Integer todayBuyCnt;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "昨日卖单数")
|
||||||
|
private Integer prevSellCnt;
|
||||||
|
|
||||||
|
/** 内部使用:SQL 聚合后由 Service 二次处理为格式化字符串;不输出到 JSON */
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private BigDecimal todayBuyAmountRaw;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private BigDecimal todaySellAmountRaw;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private BigDecimal moneyRaw;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private BigDecimal couponRaw;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private BigDecimal selfBonusRaw;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private BigDecimal shareBonusRaw;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.zbkj.common.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 - 单成员行
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "TeamDailyMemberRow 对象", description = "团队每日对账 - 单成员行")
|
||||||
|
public class TeamDailyMemberRow implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "成员用户 ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "成员昵称")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "团队代号")
|
||||||
|
private String teamCode;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "成员状态:1=启用,0=禁用")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/** 内部使用:所属团队长 ID(即 wa_users.pid);不输出到 JSON */
|
||||||
|
@JsonIgnore
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
private Integer leaderId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "D-1 买单合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal prevBuy;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "D 卖单合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal todaySell;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "D 买单合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal todayBuy;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "服务费 = D买单 × service_rate")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal serviceFee;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "E 积分 = D买单 × e_score_rate")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal eScore;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "实际收付 = D卖单 − D买单 − 服务费 − E积分")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal actual;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "备注(前端会话级,非持久化)")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.zbkj.common.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 — 多团队聚合响应(外部免认证)。
|
||||||
|
*
|
||||||
|
* 当前端不传 leaderId 时返回此结构,按团队长分组列出全部团队的报表,
|
||||||
|
* 并附带跨团队的总计。当 leaderId 传入时 teams 仅含 1 项。
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "TeamDailyMultiReportResponse 对象", description = "多团队每日对账响应")
|
||||||
|
public class TeamDailyMultiReportResponse implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("查询日期 D(yyyy-MM-dd)")
|
||||||
|
private String date;
|
||||||
|
|
||||||
|
@ApiModelProperty("D-1 日期(yyyy-MM-dd)")
|
||||||
|
private String previousDate;
|
||||||
|
|
||||||
|
@ApiModelProperty("服务费率")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal serviceRate;
|
||||||
|
|
||||||
|
@ApiModelProperty("E 积分率")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal eScoreRate;
|
||||||
|
|
||||||
|
@ApiModelProperty("团队数")
|
||||||
|
private Integer teamCount;
|
||||||
|
|
||||||
|
@ApiModelProperty("成员合计(跨团队)")
|
||||||
|
private Integer totalMemberCount;
|
||||||
|
|
||||||
|
@ApiModelProperty("各团队报表(按 leaderId 分组)")
|
||||||
|
private List<TeamDailyReportResponse> teams;
|
||||||
|
|
||||||
|
@ApiModelProperty("跨团队总计")
|
||||||
|
private TeamDailySummary grandSummary;
|
||||||
|
|
||||||
|
@ApiModelProperty("警告信息(如非法 memberIds)")
|
||||||
|
private List<String> warnings;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.zbkj.common.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 响应(外部免认证)
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "TeamDailyReportResponse 对象", description = "团队每日对账日报响应")
|
||||||
|
public class TeamDailyReportResponse implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("团队长 ID")
|
||||||
|
private Integer leaderId;
|
||||||
|
|
||||||
|
@ApiModelProperty("团队长昵称")
|
||||||
|
private String leaderNickname;
|
||||||
|
|
||||||
|
@ApiModelProperty("团队代号")
|
||||||
|
private String teamCode;
|
||||||
|
|
||||||
|
@ApiModelProperty("成员人数")
|
||||||
|
private Integer memberCount;
|
||||||
|
|
||||||
|
@ApiModelProperty("查询日期 D(yyyy-MM-dd)")
|
||||||
|
private String date;
|
||||||
|
|
||||||
|
@ApiModelProperty("D-1 日期(yyyy-MM-dd)")
|
||||||
|
private String previousDate;
|
||||||
|
|
||||||
|
@ApiModelProperty("服务费率")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal serviceRate;
|
||||||
|
|
||||||
|
@ApiModelProperty("E 积分率")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal eScoreRate;
|
||||||
|
|
||||||
|
@ApiModelProperty("成员行")
|
||||||
|
private List<TeamDailyMemberRow> rows;
|
||||||
|
|
||||||
|
@ApiModelProperty("小计")
|
||||||
|
private TeamDailySummary summary;
|
||||||
|
|
||||||
|
@ApiModelProperty("警告信息(如非法 memberIds)")
|
||||||
|
private List<String> warnings;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.zbkj.common.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 - 小计
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@ApiModel(value = "TeamDailySummary 对象", description = "团队每日对账 - 小计")
|
||||||
|
public class TeamDailySummary implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("D-1 买单合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal prevBuy = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("D 卖单合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal todaySell = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("D 买单合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal todayBuy = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("服务费合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal serviceFee = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("E 积分合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal eScore = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("实际收付合计")
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private BigDecimal actual = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
@@ -93,6 +93,12 @@ public class WaOrderResponse implements Serializable {
|
|||||||
@ApiModelProperty(value = "寄售商品ID")
|
@ApiModelProperty(value = "寄售商品ID")
|
||||||
private Integer merchandiseId;
|
private Integer merchandiseId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "寄售商品标题")
|
||||||
|
private String merchandiseTitle;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "寄售商品图片")
|
||||||
|
private String merchandiseImage;
|
||||||
|
|
||||||
@ApiModelProperty(value = "确认收货时间")
|
@ApiModelProperty(value = "确认收货时间")
|
||||||
private Date confirmTime;
|
private Date confirmTime;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,13 @@
|
|||||||
<artifactId>crmeb-common</artifactId>
|
<artifactId>crmeb-common</artifactId>
|
||||||
<version>${crmeb-common}</version>
|
<version>${crmeb-common}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 单元测试(仅 test 作用域;用于 util 类公式校验等纯函数测试) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.jayway.jsonpath</groupId>
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
<artifactId>json-path</artifactId>
|
<artifactId>json-path</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.zbkj.service.dao.consignment;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.zbkj.common.model.consignment.WaUsers;
|
||||||
|
import com.zbkj.common.response.ExternalGrabUserResponse;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户 DAO(外部免认证)
|
||||||
|
* 通过自定义 SQL 一次性聚合:
|
||||||
|
* - 今日买单合计 / 笔数(INNER JOIN,HAVING SUM>0)
|
||||||
|
* - 今日卖单合计
|
||||||
|
* - 昨日卖单笔数
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ExternalGrabUserDao extends BaseMapper<WaUsers> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表查询。
|
||||||
|
*
|
||||||
|
* @param todayStart 今天 00:00:00
|
||||||
|
* @param todayEnd 今天 23:59:59
|
||||||
|
* @param yesterdayStart 昨天 00:00:00
|
||||||
|
* @param yesterdayEnd 昨天 23:59:59
|
||||||
|
* @param uid 用户 ID(精确,可空)
|
||||||
|
* @param mobile 模糊匹配 mobile / username(可空)
|
||||||
|
* @param pid 上级 ID(精确,可空)
|
||||||
|
*/
|
||||||
|
List<ExternalGrabUserResponse> selectGrabUserList(
|
||||||
|
@Param("todayStart") Date todayStart,
|
||||||
|
@Param("todayEnd") Date todayEnd,
|
||||||
|
@Param("yesterdayStart") Date yesterdayStart,
|
||||||
|
@Param("yesterdayEnd") Date yesterdayEnd,
|
||||||
|
@Param("uid") Integer uid,
|
||||||
|
@Param("mobile") String mobile,
|
||||||
|
@Param("pid") Integer pid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.zbkj.service.dao.consignment;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.zbkj.common.model.consignment.WaUsers;
|
||||||
|
import com.zbkj.common.response.TeamDailyMemberRow;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 DAO(外部免认证)
|
||||||
|
* 通过自定义 SQL 一次性聚合每个直推下级的:D-1 买单 / D 卖单 / D 买单。
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TeamReportDao extends BaseMapper<WaUsers> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询团队成员每日对账原始数据。
|
||||||
|
*
|
||||||
|
* @param leaderId 团队长 ID;为 null 时返回所有有 pid 的成员,由 Service 按 leader_id 分组
|
||||||
|
*/
|
||||||
|
List<TeamDailyMemberRow> selectTeamMemberAggregates(
|
||||||
|
@Param("leaderId") Integer leaderId,
|
||||||
|
@Param("dStart") Date dStart,
|
||||||
|
@Param("dEnd") Date dEnd,
|
||||||
|
@Param("prevStart") Date prevStart,
|
||||||
|
@Param("prevEnd") Date prevEnd,
|
||||||
|
@Param("includeDisabled") Boolean includeDisabled,
|
||||||
|
@Param("memberIds") List<Integer> memberIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 wa_setting 中某 key 的字符串值(不存在时返回 null)。
|
||||||
|
* KV 表无独立 Model,使用注解直接查询。
|
||||||
|
*/
|
||||||
|
@Select("SELECT `value` FROM wa_setting WHERE `name` = #{name} LIMIT 1")
|
||||||
|
String selectSettingValue(@Param("name") String name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zbkj.service.service;
|
||||||
|
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.zbkj.common.request.ExternalGrabUserRequest;
|
||||||
|
import com.zbkj.common.request.PageParamRequest;
|
||||||
|
import com.zbkj.common.response.ExternalGrabUserResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表 Service(外部免认证)
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
public interface ExternalGrabUserService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询当日已发生买单且金额 > 0 的用户列表。
|
||||||
|
* 排序:今日购买总金额 DESC,相同时按 id DESC。
|
||||||
|
*
|
||||||
|
* @param request 搜索条件
|
||||||
|
* @param pageParamRequest 分页参数
|
||||||
|
* @return PageInfo<ExternalGrabUserResponse>
|
||||||
|
*/
|
||||||
|
PageInfo<ExternalGrabUserResponse> list(ExternalGrabUserRequest request,
|
||||||
|
PageParamRequest pageParamRequest);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.zbkj.service.service;
|
||||||
|
|
||||||
|
import com.zbkj.common.request.TeamDailyReportRequest;
|
||||||
|
import com.zbkj.common.response.TeamDailyMultiReportResponse;
|
||||||
|
import com.zbkj.common.response.TeamDailyReportResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 Service(外部免认证)
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
public interface TeamReportExternalService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询某团队某日对账数据(要求 leaderId 非空)。
|
||||||
|
*/
|
||||||
|
TeamDailyReportResponse getDailyReport(TeamDailyReportRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询多团队对账数据:
|
||||||
|
* - leaderId 为空:按团队长分组返回所有团队
|
||||||
|
* - leaderId 非空:teams 仅含该团队
|
||||||
|
*
|
||||||
|
* @return 多团队报表(含 grandSummary 与每个团队的子报表)
|
||||||
|
*/
|
||||||
|
TeamDailyMultiReportResponse getMultiDailyReport(TeamDailyReportRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excel 导出(仅支持单团队,要求 leaderId 非空)。
|
||||||
|
*/
|
||||||
|
TeamDailyReportResponse exportDailyReport(TeamDailyReportRequest request, OutputStream out) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.zbkj.service.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.github.pagehelper.PageHelper;
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.zbkj.common.request.ExternalGrabUserRequest;
|
||||||
|
import com.zbkj.common.request.PageParamRequest;
|
||||||
|
import com.zbkj.common.response.ExternalGrabUserResponse;
|
||||||
|
import com.zbkj.service.dao.consignment.ExternalGrabUserDao;
|
||||||
|
import com.zbkj.service.service.ExternalGrabUserService;
|
||||||
|
import com.zbkj.service.util.GrabUserFormatter;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表 Service 实现(外部免认证)
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ExternalGrabUserServiceImpl implements ExternalGrabUserService {
|
||||||
|
|
||||||
|
private static final ZoneId ZONE_CN = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ExternalGrabUserDao externalGrabUserDao;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageInfo<ExternalGrabUserResponse> list(ExternalGrabUserRequest request,
|
||||||
|
PageParamRequest pageParamRequest) {
|
||||||
|
if (request == null) {
|
||||||
|
request = new ExternalGrabUserRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间窗(业务时区 CST)
|
||||||
|
LocalDate today = LocalDate.now(ZONE_CN);
|
||||||
|
Date todayStart = toDate(today.atStartOfDay());
|
||||||
|
Date todayEnd = toDate(today.atTime(LocalTime.MAX));
|
||||||
|
LocalDate yesterday = today.minusDays(1);
|
||||||
|
Date yesterdayStart = toDate(yesterday.atStartOfDay());
|
||||||
|
Date yesterdayEnd = toDate(yesterday.atTime(LocalTime.MAX));
|
||||||
|
|
||||||
|
// 分页(PageHelper 在执行下一条 SQL 时生效)
|
||||||
|
int page = pageParamRequest != null && pageParamRequest.getPage() > 0 ? pageParamRequest.getPage() : 1;
|
||||||
|
int limit = pageParamRequest != null && pageParamRequest.getLimit() > 0 ? Math.min(pageParamRequest.getLimit(), 100) : 15;
|
||||||
|
PageHelper.startPage(page, limit);
|
||||||
|
|
||||||
|
List<ExternalGrabUserResponse> rows = externalGrabUserDao.selectGrabUserList(
|
||||||
|
todayStart, todayEnd, yesterdayStart, yesterdayEnd,
|
||||||
|
request.getUid(),
|
||||||
|
StrUtil.trimToNull(request.getMobile()),
|
||||||
|
request.getPid());
|
||||||
|
|
||||||
|
// 后处理:金额格式化、状态/等级文案
|
||||||
|
for (ExternalGrabUserResponse row : rows) {
|
||||||
|
row.setMoney(GrabUserFormatter.formatAmount(row.getMoneyRaw()));
|
||||||
|
row.setCoupon(GrabUserFormatter.formatAmount(row.getCouponRaw()));
|
||||||
|
row.setSelfBonus(GrabUserFormatter.formatAmount(row.getSelfBonusRaw()));
|
||||||
|
row.setShareBonus(GrabUserFormatter.formatAmount(row.getShareBonusRaw()));
|
||||||
|
row.setTodayBuyAmount(GrabUserFormatter.formatAmount(row.getTodayBuyAmountRaw()));
|
||||||
|
row.setTodaySellAmount(GrabUserFormatter.formatAmount(row.getTodaySellAmountRaw()));
|
||||||
|
row.setStatusStr(GrabUserFormatter.mapStatus(row.getStatus()));
|
||||||
|
row.setLevelName(GrabUserFormatter.mapLevelName(row.getLevel()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PageInfo<>(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Date toDate(LocalDateTime dt) {
|
||||||
|
return Date.from(dt.atZone(ZONE_CN).toInstant());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
package com.zbkj.service.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.zbkj.common.exception.CrmebException;
|
||||||
|
import com.zbkj.common.model.consignment.WaUsers;
|
||||||
|
import com.zbkj.common.request.TeamDailyReportRequest;
|
||||||
|
import com.zbkj.common.response.TeamDailyMemberRow;
|
||||||
|
import com.zbkj.common.response.TeamDailyMultiReportResponse;
|
||||||
|
import com.zbkj.common.response.TeamDailyReportResponse;
|
||||||
|
import com.zbkj.common.response.TeamDailySummary;
|
||||||
|
import com.zbkj.service.dao.consignment.TeamReportDao;
|
||||||
|
import com.zbkj.service.dao.consignment.WaUsersDao;
|
||||||
|
import com.zbkj.service.service.TeamReportExternalService;
|
||||||
|
import com.zbkj.service.util.TeamReportFormula;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 Service 实现(外部免认证)
|
||||||
|
*
|
||||||
|
* 关键设计:
|
||||||
|
* 1) 费率从 wa_setting 读取(service_rate / e_score_rate),缺省 0.02 / 0.005;
|
||||||
|
* 2) BigDecimal 全程 HALF_UP 保留 2 位;
|
||||||
|
* 3) 小计的服务费 / E积分 / 实际收付 由"成员级别已舍入数值"再求和,避免逐行二次舍入;
|
||||||
|
* 4) 时区:业务时区 Asia/Shanghai。
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||||
|
* +----------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TeamReportExternalServiceImpl implements TeamReportExternalService {
|
||||||
|
|
||||||
|
private static final ZoneId ZONE_CN = ZoneId.of("Asia/Shanghai");
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
private static final BigDecimal DEFAULT_SERVICE_RATE = new BigDecimal("0.02");
|
||||||
|
private static final BigDecimal DEFAULT_E_SCORE_RATE = new BigDecimal("0.005");
|
||||||
|
private static final int SCALE = 2;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TeamReportDao teamReportDao;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WaUsersDao waUsersDao;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TeamDailyReportResponse getDailyReport(TeamDailyReportRequest request) {
|
||||||
|
if (request == null || request.getLeaderId() == null) {
|
||||||
|
throw new CrmebException("团队长 ID 不能为空");
|
||||||
|
}
|
||||||
|
return assembleSingleTeam(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TeamDailyMultiReportResponse getMultiDailyReport(TeamDailyReportRequest request) {
|
||||||
|
if (request == null) request = new TeamDailyReportRequest();
|
||||||
|
|
||||||
|
Ctx ctx = prepare(request);
|
||||||
|
|
||||||
|
// 拉取原始聚合(leaderId 为空时一次拿所有团队成员)
|
||||||
|
List<TeamDailyMemberRow> rows = teamReportDao.selectTeamMemberAggregates(
|
||||||
|
request.getLeaderId(), ctx.dStart, ctx.dEnd, ctx.pStart, ctx.pEnd,
|
||||||
|
request.getIncludeDisabled(),
|
||||||
|
(request.getMemberIds() != null && !request.getMemberIds().isEmpty())
|
||||||
|
? request.getMemberIds() : null);
|
||||||
|
if (rows == null) rows = new ArrayList<>();
|
||||||
|
|
||||||
|
// 公式计算
|
||||||
|
for (TeamDailyMemberRow r : rows) {
|
||||||
|
TeamReportFormula.applyFormula(r, ctx.serviceRate, ctx.eScoreRate);
|
||||||
|
r.setRemark("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 leaderId 分组(保持稳定顺序)
|
||||||
|
Map<Integer, List<TeamDailyMemberRow>> groups = new LinkedHashMap<>();
|
||||||
|
Set<Integer> leaderIds = new HashSet<>();
|
||||||
|
for (TeamDailyMemberRow r : rows) {
|
||||||
|
Integer lid = r.getLeaderId();
|
||||||
|
if (lid == null || lid <= 0) continue;
|
||||||
|
groups.computeIfAbsent(lid, k -> new ArrayList<>()).add(r);
|
||||||
|
leaderIds.add(lid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量拉取团队长信息
|
||||||
|
Map<Integer, WaUsers> leaderMap = new HashMap<>();
|
||||||
|
if (!leaderIds.isEmpty()) {
|
||||||
|
for (WaUsers u : waUsersDao.selectBatchIds(leaderIds)) {
|
||||||
|
leaderMap.put(u.getId(), u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装每个团队子报表
|
||||||
|
List<TeamDailyReportResponse> teams = new ArrayList<>(groups.size());
|
||||||
|
TeamDailySummary grand = new TeamDailySummary();
|
||||||
|
int totalMembers = 0;
|
||||||
|
for (Map.Entry<Integer, List<TeamDailyMemberRow>> e : groups.entrySet()) {
|
||||||
|
Integer lid = e.getKey();
|
||||||
|
List<TeamDailyMemberRow> teamRows = e.getValue();
|
||||||
|
WaUsers leader = leaderMap.get(lid);
|
||||||
|
String leaderNickname = leader != null ? leader.getNickname() : null;
|
||||||
|
String teamCode = leader != null && StrUtil.isNotBlank(leader.getInvite())
|
||||||
|
? leader.getInvite()
|
||||||
|
: String.valueOf(lid);
|
||||||
|
|
||||||
|
TeamDailySummary teamSummary = TeamReportFormula.aggregateSummary(teamRows);
|
||||||
|
|
||||||
|
teams.add(new TeamDailyReportResponse()
|
||||||
|
.setLeaderId(lid)
|
||||||
|
.setLeaderNickname(leaderNickname)
|
||||||
|
.setTeamCode(teamCode)
|
||||||
|
.setMemberCount(teamRows.size())
|
||||||
|
.setDate(ctx.d.format(DATE_FMT))
|
||||||
|
.setPreviousDate(ctx.prev.format(DATE_FMT))
|
||||||
|
.setServiceRate(ctx.serviceRate)
|
||||||
|
.setEScoreRate(ctx.eScoreRate)
|
||||||
|
.setRows(teamRows)
|
||||||
|
.setSummary(teamSummary));
|
||||||
|
|
||||||
|
// 累加 grand summary
|
||||||
|
grand.setPrevBuy(grand.getPrevBuy().add(teamSummary.getPrevBuy()));
|
||||||
|
grand.setTodaySell(grand.getTodaySell().add(teamSummary.getTodaySell()));
|
||||||
|
grand.setTodayBuy(grand.getTodayBuy().add(teamSummary.getTodayBuy()));
|
||||||
|
grand.setServiceFee(grand.getServiceFee().add(teamSummary.getServiceFee()));
|
||||||
|
grand.setEScore(grand.getEScore().add(teamSummary.getEScore()));
|
||||||
|
grand.setActual(grand.getActual().add(teamSummary.getActual()));
|
||||||
|
totalMembers += teamRows.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TeamDailyMultiReportResponse()
|
||||||
|
.setDate(ctx.d.format(DATE_FMT))
|
||||||
|
.setPreviousDate(ctx.prev.format(DATE_FMT))
|
||||||
|
.setServiceRate(ctx.serviceRate)
|
||||||
|
.setEScoreRate(ctx.eScoreRate)
|
||||||
|
.setTeamCount(teams.size())
|
||||||
|
.setTotalMemberCount(totalMembers)
|
||||||
|
.setTeams(teams)
|
||||||
|
.setGrandSummary(grand);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TeamDailyReportResponse exportDailyReport(TeamDailyReportRequest request, OutputStream out) throws IOException {
|
||||||
|
if (request == null || request.getLeaderId() == null) {
|
||||||
|
throw new CrmebException("Excel 导出仅支持单团队,必须指定团队长 ID");
|
||||||
|
}
|
||||||
|
TeamDailyReportResponse data = assembleSingleTeam(request);
|
||||||
|
writeExcel(data, out);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 主流程:单团队装配
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private TeamDailyReportResponse assembleSingleTeam(TeamDailyReportRequest request) {
|
||||||
|
// 校验团队长存在
|
||||||
|
WaUsers leader = waUsersDao.selectById(request.getLeaderId());
|
||||||
|
if (leader == null) {
|
||||||
|
throw new CrmebException("团队长不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ctx ctx = prepare(request);
|
||||||
|
|
||||||
|
// 拉取原始聚合
|
||||||
|
List<TeamDailyMemberRow> rows = teamReportDao.selectTeamMemberAggregates(
|
||||||
|
request.getLeaderId(), ctx.dStart, ctx.dEnd, ctx.pStart, ctx.pEnd,
|
||||||
|
request.getIncludeDisabled(),
|
||||||
|
(request.getMemberIds() != null && !request.getMemberIds().isEmpty())
|
||||||
|
? request.getMemberIds() : null);
|
||||||
|
if (rows == null) rows = new ArrayList<>();
|
||||||
|
|
||||||
|
for (TeamDailyMemberRow r : rows) {
|
||||||
|
TeamReportFormula.applyFormula(r, ctx.serviceRate, ctx.eScoreRate);
|
||||||
|
r.setRemark("");
|
||||||
|
}
|
||||||
|
TeamDailySummary summary = TeamReportFormula.aggregateSummary(rows);
|
||||||
|
|
||||||
|
String teamCode = StrUtil.isNotBlank(leader.getInvite())
|
||||||
|
? leader.getInvite()
|
||||||
|
: String.valueOf(leader.getId());
|
||||||
|
|
||||||
|
return new TeamDailyReportResponse()
|
||||||
|
.setLeaderId(leader.getId())
|
||||||
|
.setLeaderNickname(leader.getNickname())
|
||||||
|
.setTeamCode(teamCode)
|
||||||
|
.setMemberCount(rows.size())
|
||||||
|
.setDate(ctx.d.format(DATE_FMT))
|
||||||
|
.setPreviousDate(ctx.prev.format(DATE_FMT))
|
||||||
|
.setServiceRate(ctx.serviceRate)
|
||||||
|
.setEScoreRate(ctx.eScoreRate)
|
||||||
|
.setRows(rows)
|
||||||
|
.setSummary(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单/多团队共用的上下文(日期、时间窗、费率) */
|
||||||
|
private Ctx prepare(TeamDailyReportRequest request) {
|
||||||
|
LocalDate today = LocalDate.now(ZONE_CN);
|
||||||
|
LocalDate d;
|
||||||
|
if (StrUtil.isNotBlank(request.getDate())) {
|
||||||
|
d = LocalDate.parse(request.getDate(), DATE_FMT);
|
||||||
|
} else {
|
||||||
|
d = today.minusDays(1);
|
||||||
|
}
|
||||||
|
if (d.isAfter(today)) {
|
||||||
|
throw new CrmebException("不能查询未来日期");
|
||||||
|
}
|
||||||
|
LocalDate prev = d.minusDays(1);
|
||||||
|
Ctx ctx = new Ctx();
|
||||||
|
ctx.d = d;
|
||||||
|
ctx.prev = prev;
|
||||||
|
ctx.dStart = toDate(d, true);
|
||||||
|
ctx.dEnd = toDate(d, false);
|
||||||
|
ctx.pStart = toDate(prev, true);
|
||||||
|
ctx.pEnd = toDate(prev, false);
|
||||||
|
ctx.serviceRate = readRate("service_rate", DEFAULT_SERVICE_RATE);
|
||||||
|
ctx.eScoreRate = readRate("e_score_rate", DEFAULT_E_SCORE_RATE);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Service 内部上下文 */
|
||||||
|
private static class Ctx {
|
||||||
|
LocalDate d;
|
||||||
|
LocalDate prev;
|
||||||
|
Date dStart;
|
||||||
|
Date dEnd;
|
||||||
|
Date pStart;
|
||||||
|
Date pEnd;
|
||||||
|
BigDecimal serviceRate;
|
||||||
|
BigDecimal eScoreRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 工具方法
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private static Date toDate(LocalDate date, boolean start) {
|
||||||
|
return Date.from((start ? date.atStartOfDay() : date.atTime(LocalTime.MAX))
|
||||||
|
.atZone(ZONE_CN).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal readRate(String name, BigDecimal fallback) {
|
||||||
|
String v = teamReportDao.selectSettingValue(name);
|
||||||
|
if (StrUtil.isBlank(v)) return fallback;
|
||||||
|
try {
|
||||||
|
return new BigDecimal(v.trim());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Excel 导出(Apache POI)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private void writeExcel(TeamDailyReportResponse data, OutputStream out) throws IOException {
|
||||||
|
try (Workbook wb = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = wb.createSheet("团队日报");
|
||||||
|
|
||||||
|
// 表头横幅:团队长 昵称 · N 人 · 日期
|
||||||
|
Row banner = sheet.createRow(0);
|
||||||
|
Cell bannerCell = banner.createCell(0);
|
||||||
|
String leaderShown = data.getLeaderNickname() != null && !data.getLeaderNickname().isEmpty()
|
||||||
|
? data.getLeaderNickname()
|
||||||
|
: (data.getTeamCode() == null ? "" : data.getTeamCode());
|
||||||
|
bannerCell.setCellValue(String.format("团队长 %s · %d 人 · %s",
|
||||||
|
leaderShown, data.getMemberCount(), data.getDate()));
|
||||||
|
CellStyle bannerStyle = wb.createCellStyle();
|
||||||
|
Font bannerFont = wb.createFont();
|
||||||
|
bannerFont.setBold(true);
|
||||||
|
bannerFont.setFontHeightInPoints((short) 14);
|
||||||
|
bannerStyle.setFont(bannerFont);
|
||||||
|
bannerStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
bannerCell.setCellStyle(bannerStyle);
|
||||||
|
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 8));
|
||||||
|
|
||||||
|
// 表头
|
||||||
|
String[] headers = {
|
||||||
|
"昵称",
|
||||||
|
data.getPreviousDate() + " 买单",
|
||||||
|
data.getDate() + " 卖单",
|
||||||
|
data.getDate() + " 买单",
|
||||||
|
"服务费*" + data.getServiceRate().toPlainString(),
|
||||||
|
"E积分",
|
||||||
|
"实际收付",
|
||||||
|
"团队",
|
||||||
|
"备注"
|
||||||
|
};
|
||||||
|
CellStyle headStyle = wb.createCellStyle();
|
||||||
|
Font headFont = wb.createFont();
|
||||||
|
headFont.setBold(true);
|
||||||
|
headStyle.setFont(headFont);
|
||||||
|
headStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
headStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
|
||||||
|
headStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
applyBorders(headStyle);
|
||||||
|
Row headRow = sheet.createRow(1);
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
Cell cell = headRow.createCell(i);
|
||||||
|
cell.setCellValue(headers[i]);
|
||||||
|
cell.setCellStyle(headStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小计行(紧跟表头)
|
||||||
|
CellStyle summaryStyle = wb.createCellStyle();
|
||||||
|
Font summaryFont = wb.createFont();
|
||||||
|
summaryFont.setBold(true);
|
||||||
|
summaryFont.setColor(IndexedColors.RED.getIndex());
|
||||||
|
summaryStyle.setFont(summaryFont);
|
||||||
|
applyBorders(summaryStyle);
|
||||||
|
|
||||||
|
CellStyle moneyStyle = wb.createCellStyle();
|
||||||
|
moneyStyle.setDataFormat(wb.createDataFormat().getFormat("#,##0.00;-#,##0.00"));
|
||||||
|
applyBorders(moneyStyle);
|
||||||
|
|
||||||
|
CellStyle moneyBoldRedStyle = wb.createCellStyle();
|
||||||
|
moneyBoldRedStyle.cloneStyleFrom(moneyStyle);
|
||||||
|
moneyBoldRedStyle.setFont(summaryFont);
|
||||||
|
|
||||||
|
Row summaryRow = sheet.createRow(2);
|
||||||
|
TeamDailySummary s = data.getSummary();
|
||||||
|
summaryRow.createCell(0).setCellValue("小计");
|
||||||
|
summaryRow.getCell(0).setCellStyle(summaryStyle);
|
||||||
|
writeMoney(summaryRow, 1, s.getPrevBuy(), moneyBoldRedStyle);
|
||||||
|
writeMoney(summaryRow, 2, s.getTodaySell(), moneyBoldRedStyle);
|
||||||
|
writeMoney(summaryRow, 3, s.getTodayBuy(), moneyBoldRedStyle);
|
||||||
|
writeMoney(summaryRow, 4, s.getServiceFee(), moneyBoldRedStyle);
|
||||||
|
writeMoney(summaryRow, 5, s.getEScore(), moneyBoldRedStyle);
|
||||||
|
writeMoney(summaryRow, 6, s.getActual(), moneyBoldRedStyle);
|
||||||
|
summaryRow.createCell(7).setCellStyle(summaryStyle);
|
||||||
|
summaryRow.createCell(8).setCellStyle(summaryStyle);
|
||||||
|
|
||||||
|
// 数据行(团队列填团队长昵称)
|
||||||
|
int rowIdx = 3;
|
||||||
|
for (TeamDailyMemberRow r : data.getRows()) {
|
||||||
|
Row row = sheet.createRow(rowIdx++);
|
||||||
|
row.createCell(0).setCellValue(r.getNickname() == null ? "" : r.getNickname());
|
||||||
|
writeMoney(row, 1, r.getPrevBuy(), moneyStyle);
|
||||||
|
writeMoney(row, 2, r.getTodaySell(), moneyStyle);
|
||||||
|
writeMoney(row, 3, r.getTodayBuy(), moneyStyle);
|
||||||
|
writeMoney(row, 4, r.getServiceFee(), moneyStyle);
|
||||||
|
writeMoney(row, 5, r.getEScore(), moneyStyle);
|
||||||
|
writeMoney(row, 6, r.getActual(), moneyStyle);
|
||||||
|
row.createCell(7).setCellValue(leaderShown);
|
||||||
|
// 备注列保留空白(运营手填)
|
||||||
|
row.createCell(8).setCellValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列宽
|
||||||
|
int[] widths = { 14, 14, 14, 14, 16, 12, 14, 8, 20 };
|
||||||
|
for (int i = 0; i < widths.length; i++) {
|
||||||
|
sheet.setColumnWidth(i, widths[i] * 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
wb.write(out);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeMoney(Row row, int col, BigDecimal value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(col);
|
||||||
|
cell.setCellValue(value == null ? 0d : value.doubleValue());
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBorders(CellStyle style) {
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|||||||
import com.github.pagehelper.PageHelper;
|
import com.github.pagehelper.PageHelper;
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
import com.zbkj.common.exception.CrmebException;
|
import com.zbkj.common.exception.CrmebException;
|
||||||
|
import com.zbkj.common.model.consignment.WaMerchandise;
|
||||||
import com.zbkj.common.model.consignment.WaOrder;
|
import com.zbkj.common.model.consignment.WaOrder;
|
||||||
import com.zbkj.common.model.consignment.WaUsers;
|
import com.zbkj.common.model.consignment.WaUsers;
|
||||||
import com.zbkj.common.page.CommonPage;
|
import com.zbkj.common.page.CommonPage;
|
||||||
@@ -13,6 +14,7 @@ import com.zbkj.common.request.PageParamRequest;
|
|||||||
import com.zbkj.common.request.WaOrderSearchRequest;
|
import com.zbkj.common.request.WaOrderSearchRequest;
|
||||||
import com.zbkj.common.request.WaOrderUpdateRequest;
|
import com.zbkj.common.request.WaOrderUpdateRequest;
|
||||||
import com.zbkj.common.response.WaOrderResponse;
|
import com.zbkj.common.response.WaOrderResponse;
|
||||||
|
import com.zbkj.service.dao.consignment.WaMerchandiseDao;
|
||||||
import com.zbkj.service.dao.consignment.WaOrderDao;
|
import com.zbkj.service.dao.consignment.WaOrderDao;
|
||||||
import com.zbkj.service.dao.consignment.WaUsersDao;
|
import com.zbkj.service.dao.consignment.WaUsersDao;
|
||||||
import com.zbkj.service.service.WaOrderAdminService;
|
import com.zbkj.service.service.WaOrderAdminService;
|
||||||
@@ -20,7 +22,11 @@ import org.springframework.beans.BeanUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +50,9 @@ public class WaOrderAdminServiceImpl extends ServiceImpl<WaOrderDao, WaOrder> im
|
|||||||
@Resource
|
@Resource
|
||||||
private WaUsersDao waUsersDao;
|
private WaUsersDao waUsersDao;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WaMerchandiseDao waMerchandiseDao;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页列表查询
|
* 分页列表查询
|
||||||
*/
|
*/
|
||||||
@@ -108,25 +117,52 @@ public class WaOrderAdminServiceImpl extends ServiceImpl<WaOrderDao, WaOrder> im
|
|||||||
wrapper.orderByDesc(WaOrder::getCreatedAt);
|
wrapper.orderByDesc(WaOrder::getCreatedAt);
|
||||||
|
|
||||||
List<WaOrder> list = waOrderDao.selectList(wrapper);
|
List<WaOrder> list = waOrderDao.selectList(wrapper);
|
||||||
|
|
||||||
|
// 批量收集所需关联 ID,避免循环 N+1 查询
|
||||||
|
Set<Integer> userIds = new HashSet<>();
|
||||||
|
Set<Integer> merchandiseIds = new HashSet<>();
|
||||||
|
for (WaOrder o : list) {
|
||||||
|
if (o.getSellerId() != null && o.getSellerId() > 0) userIds.add(o.getSellerId());
|
||||||
|
if (o.getBuyerId() != null && o.getBuyerId() > 0) userIds.add(o.getBuyerId());
|
||||||
|
if (o.getMerchandiseId() != null && o.getMerchandiseId() > 0) merchandiseIds.add(o.getMerchandiseId());
|
||||||
|
}
|
||||||
|
Map<Integer, WaUsers> userMap = new HashMap<>();
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
for (WaUsers u : waUsersDao.selectBatchIds(userIds)) {
|
||||||
|
userMap.put(u.getId(), u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Integer, WaMerchandise> merchandiseMap = new HashMap<>();
|
||||||
|
if (!merchandiseIds.isEmpty()) {
|
||||||
|
for (WaMerchandise m : waMerchandiseDao.selectBatchIds(merchandiseIds)) {
|
||||||
|
merchandiseMap.put(m.getId(), m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<WaOrderResponse> responseList = list.stream().map(item -> {
|
List<WaOrderResponse> responseList = list.stream().map(item -> {
|
||||||
WaOrderResponse response = new WaOrderResponse();
|
WaOrderResponse response = new WaOrderResponse();
|
||||||
BeanUtils.copyProperties(item, response);
|
BeanUtils.copyProperties(item, response);
|
||||||
|
|
||||||
// 获取卖家名称
|
// 卖家名称
|
||||||
if (item.getSellerId() != null && item.getSellerId() > 0) {
|
if (item.getSellerId() != null && item.getSellerId() > 0) {
|
||||||
WaUsers seller = waUsersDao.selectById(item.getSellerId());
|
WaUsers seller = userMap.get(item.getSellerId());
|
||||||
if (seller != null) {
|
if (seller != null) response.setSellerName(seller.getNickname());
|
||||||
response.setSellerName(seller.getNickname());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
response.setSellerName("平台");
|
response.setSellerName("平台");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取买家名称
|
// 买家名称
|
||||||
if (item.getBuyerId() != null && item.getBuyerId() > 0) {
|
if (item.getBuyerId() != null && item.getBuyerId() > 0) {
|
||||||
WaUsers buyer = waUsersDao.selectById(item.getBuyerId());
|
WaUsers buyer = userMap.get(item.getBuyerId());
|
||||||
if (buyer != null) {
|
if (buyer != null) response.setBuyerName(buyer.getNickname());
|
||||||
response.setBuyerName(buyer.getNickname());
|
}
|
||||||
|
|
||||||
|
// 商品名称 / 图片
|
||||||
|
if (item.getMerchandiseId() != null && item.getMerchandiseId() > 0) {
|
||||||
|
WaMerchandise mh = merchandiseMap.get(item.getMerchandiseId());
|
||||||
|
if (mh != null) {
|
||||||
|
response.setMerchandiseTitle(mh.getTitle());
|
||||||
|
response.setMerchandiseImage(mh.getImage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +203,15 @@ public class WaOrderAdminServiceImpl extends ServiceImpl<WaOrderDao, WaOrder> im
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 商品名称 / 图片
|
||||||
|
if (order.getMerchandiseId() != null && order.getMerchandiseId() > 0) {
|
||||||
|
WaMerchandise mh = waMerchandiseDao.selectById(order.getMerchandiseId());
|
||||||
|
if (mh != null) {
|
||||||
|
response.setMerchandiseTitle(mh.getTitle());
|
||||||
|
response.setMerchandiseImage(mh.getImage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.zbkj.service.util;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表 — 字段格式化工具。
|
||||||
|
* 抽出为单独类便于单元测试。
|
||||||
|
*/
|
||||||
|
public final class GrabUserFormatter {
|
||||||
|
|
||||||
|
public static final int AMOUNT_SCALE = 3;
|
||||||
|
|
||||||
|
private GrabUserFormatter() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金额格式化:保留 3 位小数(与上传参考图一致)。
|
||||||
|
*/
|
||||||
|
public static String formatAmount(BigDecimal raw) {
|
||||||
|
BigDecimal v = raw == null ? BigDecimal.ZERO : raw;
|
||||||
|
return v.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP).toPlainString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户等级映射;未知等级回退为「普通用户」。
|
||||||
|
*/
|
||||||
|
public static String mapLevelName(Integer level) {
|
||||||
|
if (level == null) return "普通用户";
|
||||||
|
switch (level) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
return "普通用户";
|
||||||
|
case 2:
|
||||||
|
return "VIP";
|
||||||
|
case 3:
|
||||||
|
return "合伙人";
|
||||||
|
default:
|
||||||
|
return "等级" + level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态文案:1=正常 / 其它=禁用。
|
||||||
|
*/
|
||||||
|
public static String mapStatus(Integer status) {
|
||||||
|
return status != null && status == 1 ? "正常" : "禁用";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.zbkj.service.util;
|
||||||
|
|
||||||
|
import com.zbkj.common.response.TeamDailyMemberRow;
|
||||||
|
import com.zbkj.common.response.TeamDailySummary;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 — 公式工具类。
|
||||||
|
* 设计要点:
|
||||||
|
* - 所有金额按 HALF_UP 舍入到 2 位;
|
||||||
|
* - 小计的服务费 / E积分 / 实际收付 由"成员级别已舍入数值"再求和,
|
||||||
|
* 避免逐行二次舍入。
|
||||||
|
*
|
||||||
|
* 抽出为单独类便于单元测试(无需 DB / Spring 上下文)。
|
||||||
|
*/
|
||||||
|
public final class TeamReportFormula {
|
||||||
|
|
||||||
|
public static final int SCALE = 2;
|
||||||
|
|
||||||
|
private TeamReportFormula() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单成员金额舍入与公式计算。
|
||||||
|
* 修改入参对象的 prevBuy / todaySell / todayBuy / serviceFee / eScore / actual。
|
||||||
|
*
|
||||||
|
* @param row 成员行(prevBuy / todaySell / todayBuy 已由 SQL 填充)
|
||||||
|
* @param serviceRate 服务费率
|
||||||
|
* @param eScoreRate E 积分率
|
||||||
|
*/
|
||||||
|
public static void applyFormula(TeamDailyMemberRow row,
|
||||||
|
BigDecimal serviceRate,
|
||||||
|
BigDecimal eScoreRate) {
|
||||||
|
if (row == null) return;
|
||||||
|
row.setPrevBuy(scale(row.getPrevBuy()));
|
||||||
|
row.setTodaySell(scale(row.getTodaySell()));
|
||||||
|
row.setTodayBuy(scale(row.getTodayBuy()));
|
||||||
|
|
||||||
|
BigDecimal serviceFee = scale(row.getTodayBuy().multiply(serviceRate));
|
||||||
|
BigDecimal eScore = scale(row.getTodayBuy().multiply(eScoreRate));
|
||||||
|
BigDecimal actual = scale(row.getTodaySell()
|
||||||
|
.subtract(row.getTodayBuy())
|
||||||
|
.subtract(serviceFee)
|
||||||
|
.subtract(eScore));
|
||||||
|
|
||||||
|
row.setServiceFee(serviceFee);
|
||||||
|
row.setEScore(eScore);
|
||||||
|
row.setActual(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 累加每行已舍入数值得到小计;不再做二次舍入。
|
||||||
|
*/
|
||||||
|
public static TeamDailySummary aggregateSummary(List<TeamDailyMemberRow> rows) {
|
||||||
|
TeamDailySummary s = new TeamDailySummary();
|
||||||
|
if (rows == null) return s;
|
||||||
|
for (TeamDailyMemberRow r : rows) {
|
||||||
|
s.setPrevBuy(s.getPrevBuy().add(nz(r.getPrevBuy())));
|
||||||
|
s.setTodaySell(s.getTodaySell().add(nz(r.getTodaySell())));
|
||||||
|
s.setTodayBuy(s.getTodayBuy().add(nz(r.getTodayBuy())));
|
||||||
|
s.setServiceFee(s.getServiceFee().add(nz(r.getServiceFee())));
|
||||||
|
s.setEScore(s.getEScore().add(nz(r.getEScore())));
|
||||||
|
s.setActual(s.getActual().add(nz(r.getActual())));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigDecimal scale(BigDecimal v) {
|
||||||
|
return v == null ? BigDecimal.ZERO.setScale(SCALE)
|
||||||
|
: v.setScale(SCALE, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal nz(BigDecimal v) {
|
||||||
|
return v == null ? BigDecimal.ZERO : v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zbkj.service.dao.consignment.ExternalGrabUserDao">
|
||||||
|
|
||||||
|
<resultMap id="GrabUserResultMap" type="com.zbkj.common.response.ExternalGrabUserResponse">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="username" column="username"/>
|
||||||
|
<result property="nickname" column="nickname"/>
|
||||||
|
<result property="mobile" column="mobile"/>
|
||||||
|
<result property="contract" column="contract"/>
|
||||||
|
<result property="pid" column="pid"/>
|
||||||
|
<result property="maxOrder" column="max_order"/>
|
||||||
|
<result property="level" column="level"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="updatedAt" column="updated_at"/>
|
||||||
|
<result property="moneyRaw" column="money"/>
|
||||||
|
<result property="couponRaw" column="coupon"/>
|
||||||
|
<result property="selfBonusRaw" column="self_bonus"/>
|
||||||
|
<result property="shareBonusRaw" column="share_bonus"/>
|
||||||
|
<result property="todayBuyAmountRaw" column="today_buy_amount"/>
|
||||||
|
<result property="todaySellAmountRaw" column="today_sell_amount"/>
|
||||||
|
<result property="todayBuyCnt" column="today_buy_cnt"/>
|
||||||
|
<result property="prevSellCnt" column="prev_sell_cnt"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
SQL 设计要点:
|
||||||
|
1) INNER JOIN buy + HAVING SUM(total_money)>0 把"今日购买总金额>0"过滤直接落到 SQL 层;
|
||||||
|
2) LEFT JOIN sell / prev 不影响过滤结果集;
|
||||||
|
3) is_cancel=0 全程过滤已取消订单。
|
||||||
|
-->
|
||||||
|
<select id="selectGrabUserList" resultMap="GrabUserResultMap">
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.username,
|
||||||
|
u.nickname,
|
||||||
|
u.mobile,
|
||||||
|
u.contract,
|
||||||
|
u.pid,
|
||||||
|
u.max_order,
|
||||||
|
u.level,
|
||||||
|
u.money,
|
||||||
|
u.coupon,
|
||||||
|
u.self_bonus,
|
||||||
|
u.share_bonus,
|
||||||
|
u.status,
|
||||||
|
u.updated_at,
|
||||||
|
COALESCE(buy.amt, 0) AS today_buy_amount,
|
||||||
|
COALESCE(sell.amt, 0) AS today_sell_amount,
|
||||||
|
COALESCE(buy.cnt, 0) AS today_buy_cnt,
|
||||||
|
COALESCE(prev.cnt, 0) AS prev_sell_cnt
|
||||||
|
FROM wa_users u
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT buyer_id AS uid, SUM(total_money) AS amt, COUNT(*) AS cnt
|
||||||
|
FROM wa_order
|
||||||
|
WHERE is_cancel = 0
|
||||||
|
AND pay_time >= #{todayStart}
|
||||||
|
AND pay_time <= #{todayEnd}
|
||||||
|
GROUP BY buyer_id
|
||||||
|
HAVING SUM(total_money) > 0
|
||||||
|
) buy ON buy.uid = u.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT seller_id AS uid, SUM(total_money) AS amt
|
||||||
|
FROM wa_order
|
||||||
|
WHERE is_cancel = 0
|
||||||
|
AND pay_time >= #{todayStart}
|
||||||
|
AND pay_time <= #{todayEnd}
|
||||||
|
GROUP BY seller_id
|
||||||
|
) sell ON sell.uid = u.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT seller_id AS uid, COUNT(*) AS cnt
|
||||||
|
FROM wa_order
|
||||||
|
WHERE is_cancel = 0
|
||||||
|
AND pay_time >= #{yesterdayStart}
|
||||||
|
AND pay_time <= #{yesterdayEnd}
|
||||||
|
GROUP BY seller_id
|
||||||
|
) prev ON prev.uid = u.id
|
||||||
|
<where>
|
||||||
|
<if test="uid != null">
|
||||||
|
AND u.id = #{uid}
|
||||||
|
</if>
|
||||||
|
<if test="mobile != null and mobile != ''">
|
||||||
|
AND (u.mobile LIKE CONCAT('%', #{mobile}, '%')
|
||||||
|
OR u.username LIKE CONCAT('%', #{mobile}, '%'))
|
||||||
|
</if>
|
||||||
|
<if test="pid != null">
|
||||||
|
AND u.pid = #{pid}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY today_buy_amount DESC, u.id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zbkj.service.dao.consignment.TeamReportDao">
|
||||||
|
|
||||||
|
<resultMap id="MemberAggregateResultMap" type="com.zbkj.common.response.TeamDailyMemberRow">
|
||||||
|
<id property="userId" column="user_id"/>
|
||||||
|
<result property="nickname" column="nickname"/>
|
||||||
|
<result property="teamCode" column="team_code"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="leaderId" column="leader_id"/>
|
||||||
|
<result property="prevBuy" column="prev_buy"/>
|
||||||
|
<result property="todaySell" column="today_sell"/>
|
||||||
|
<result property="todayBuy" column="today_buy"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
SQL 设计:
|
||||||
|
- 一次三 LEFT JOIN 聚合:prev / sell / buy
|
||||||
|
- WHERE u.pid = leaderId 限定团队成员(直推下级)
|
||||||
|
- includeDisabled=false 时仅取 status=1
|
||||||
|
- memberIds 非空时再二次过滤
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
team-report 聚合查询:
|
||||||
|
- leaderId 非空:仅查该团队长的下级(u.pid = leaderId)
|
||||||
|
- leaderId 为空:查所有有上级的成员(u.pid > 0),后续在 Service 按 leader_id 分组
|
||||||
|
-->
|
||||||
|
<select id="selectTeamMemberAggregates" resultMap="MemberAggregateResultMap">
|
||||||
|
SELECT
|
||||||
|
u.id AS user_id,
|
||||||
|
u.nickname AS nickname,
|
||||||
|
u.invite AS team_code,
|
||||||
|
u.status AS status,
|
||||||
|
u.pid AS leader_id,
|
||||||
|
COALESCE(prev.amt, 0) AS prev_buy,
|
||||||
|
COALESCE(sell.amt, 0) AS today_sell,
|
||||||
|
COALESCE(buy.amt, 0) AS today_buy
|
||||||
|
FROM wa_users u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT buyer_id AS uid, SUM(total_money) AS amt
|
||||||
|
FROM wa_order
|
||||||
|
WHERE is_cancel = 0
|
||||||
|
AND pay_time >= #{prevStart}
|
||||||
|
AND pay_time <= #{prevEnd}
|
||||||
|
GROUP BY buyer_id
|
||||||
|
) prev ON prev.uid = u.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT seller_id AS uid, SUM(total_money) AS amt
|
||||||
|
FROM wa_order
|
||||||
|
WHERE is_cancel = 0
|
||||||
|
AND pay_time >= #{dStart}
|
||||||
|
AND pay_time <= #{dEnd}
|
||||||
|
GROUP BY seller_id
|
||||||
|
) sell ON sell.uid = u.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT buyer_id AS uid, SUM(total_money) AS amt
|
||||||
|
FROM wa_order
|
||||||
|
WHERE is_cancel = 0
|
||||||
|
AND pay_time >= #{dStart}
|
||||||
|
AND pay_time <= #{dEnd}
|
||||||
|
GROUP BY buyer_id
|
||||||
|
) buy ON buy.uid = u.id
|
||||||
|
<where>
|
||||||
|
<choose>
|
||||||
|
<when test="leaderId != null">
|
||||||
|
u.pid = #{leaderId}
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
u.pid > 0
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
<if test="includeDisabled == null or !includeDisabled">
|
||||||
|
AND u.status = 1
|
||||||
|
</if>
|
||||||
|
<if test="memberIds != null and memberIds.size() > 0">
|
||||||
|
AND u.id IN
|
||||||
|
<foreach collection="memberIds" item="mid" open="(" separator="," close=")">
|
||||||
|
#{mid}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY u.pid ASC, u.id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.zbkj.service.util;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日抢单用户列表 - 格式化工具单元测试。
|
||||||
|
*/
|
||||||
|
public class GrabUserFormatterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void formatAmount_null_should_be_zero() {
|
||||||
|
assertEquals("0.000", GrabUserFormatter.formatAmount(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void formatAmount_should_keep_three_decimals() {
|
||||||
|
assertEquals("0.000", GrabUserFormatter.formatAmount(new BigDecimal("0")));
|
||||||
|
assertEquals("226.383", GrabUserFormatter.formatAmount(new BigDecimal("226.383")));
|
||||||
|
assertEquals("100.000", GrabUserFormatter.formatAmount(new BigDecimal("100")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void formatAmount_should_round_half_up() {
|
||||||
|
// 0.0005 → 0.001
|
||||||
|
assertEquals("0.001", GrabUserFormatter.formatAmount(new BigDecimal("0.0005")));
|
||||||
|
// 0.0014 → 0.001
|
||||||
|
assertEquals("0.001", GrabUserFormatter.formatAmount(new BigDecimal("0.0014")));
|
||||||
|
// 0.0015 → 0.002
|
||||||
|
assertEquals("0.002", GrabUserFormatter.formatAmount(new BigDecimal("0.0015")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void formatAmount_should_handle_negative() {
|
||||||
|
assertEquals("-1.234", GrabUserFormatter.formatAmount(new BigDecimal("-1.2340")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapLevelName_known_levels() {
|
||||||
|
assertEquals("普通用户", GrabUserFormatter.mapLevelName(null));
|
||||||
|
assertEquals("普通用户", GrabUserFormatter.mapLevelName(0));
|
||||||
|
assertEquals("普通用户", GrabUserFormatter.mapLevelName(1));
|
||||||
|
assertEquals("VIP", GrabUserFormatter.mapLevelName(2));
|
||||||
|
assertEquals("合伙人", GrabUserFormatter.mapLevelName(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapLevelName_unknown_levels_fallback() {
|
||||||
|
assertEquals("等级5", GrabUserFormatter.mapLevelName(5));
|
||||||
|
assertEquals("等级99", GrabUserFormatter.mapLevelName(99));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapStatus_should_match_spec() {
|
||||||
|
assertEquals("正常", GrabUserFormatter.mapStatus(1));
|
||||||
|
assertEquals("禁用", GrabUserFormatter.mapStatus(0));
|
||||||
|
assertEquals("禁用", GrabUserFormatter.mapStatus(null));
|
||||||
|
assertEquals("禁用", GrabUserFormatter.mapStatus(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.zbkj.service.util;
|
||||||
|
|
||||||
|
import com.zbkj.common.response.TeamDailyMemberRow;
|
||||||
|
import com.zbkj.common.response.TeamDailySummary;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队每日对账日报 公式工具单元测试。
|
||||||
|
* 覆盖:
|
||||||
|
* - 上传参考图样例 4 名成员(王珍华 / 柯美燕 / 王兵启 / 胡晓彩)
|
||||||
|
* - 仅有卖单 / 仅有买单 / 全空 等边界
|
||||||
|
* - 小计精度:与逐行二次舍入对比
|
||||||
|
*/
|
||||||
|
public class TeamReportFormulaTest {
|
||||||
|
|
||||||
|
private static final BigDecimal SERVICE_RATE = new BigDecimal("0.02");
|
||||||
|
private static final BigDecimal E_SCORE_RATE = new BigDecimal("0.005");
|
||||||
|
|
||||||
|
/** 王珍华:22151 - 21418 - 428.36 - 107.09 = 197.55 */
|
||||||
|
@Test
|
||||||
|
public void member_with_buy_and_sell_should_match_doc() {
|
||||||
|
TeamDailyMemberRow r = new TeamDailyMemberRow()
|
||||||
|
.setNickname("王珍华")
|
||||||
|
.setPrevBuy(new BigDecimal("21506"))
|
||||||
|
.setTodaySell(new BigDecimal("22151"))
|
||||||
|
.setTodayBuy(new BigDecimal("21418"));
|
||||||
|
TeamReportFormula.applyFormula(r, SERVICE_RATE, E_SCORE_RATE);
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("428.36"), r.getServiceFee());
|
||||||
|
assertEquals(new BigDecimal("107.09"), r.getEScore());
|
||||||
|
assertEquals(new BigDecimal("197.55"), r.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 胡晓彩:28259 - 27708 - 554.16 - 138.54 = -141.70 */
|
||||||
|
@Test
|
||||||
|
public void member_with_negative_actual() {
|
||||||
|
TeamDailyMemberRow r = new TeamDailyMemberRow()
|
||||||
|
.setNickname("胡晓彩")
|
||||||
|
.setPrevBuy(new BigDecimal("27436"))
|
||||||
|
.setTodaySell(new BigDecimal("28259"))
|
||||||
|
.setTodayBuy(new BigDecimal("27708"));
|
||||||
|
TeamReportFormula.applyFormula(r, SERVICE_RATE, E_SCORE_RATE);
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("554.16"), r.getServiceFee());
|
||||||
|
assertEquals(new BigDecimal("138.54"), r.getEScore());
|
||||||
|
assertEquals(new BigDecimal("-141.70"), r.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 柯美燕 / 王兵启:仅有卖单。服务费/E积分=0;实际=卖单。 */
|
||||||
|
@Test
|
||||||
|
public void member_only_sell_should_zero_fees() {
|
||||||
|
TeamDailyMemberRow r = new TeamDailyMemberRow()
|
||||||
|
.setNickname("柯美燕")
|
||||||
|
.setPrevBuy(new BigDecimal("22519"))
|
||||||
|
.setTodaySell(new BigDecimal("23195"))
|
||||||
|
.setTodayBuy(BigDecimal.ZERO);
|
||||||
|
TeamReportFormula.applyFormula(r, SERVICE_RATE, E_SCORE_RATE);
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getServiceFee());
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getEScore());
|
||||||
|
assertEquals(new BigDecimal("23195.00"), r.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅有买单:实际收付为负值(-买 - 服务费 - E积分)。 */
|
||||||
|
@Test
|
||||||
|
public void member_only_buy_should_negative_actual() {
|
||||||
|
TeamDailyMemberRow r = new TeamDailyMemberRow()
|
||||||
|
.setTodaySell(BigDecimal.ZERO)
|
||||||
|
.setTodayBuy(new BigDecimal("1000"));
|
||||||
|
TeamReportFormula.applyFormula(r, SERVICE_RATE, E_SCORE_RATE);
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("20.00"), r.getServiceFee());
|
||||||
|
assertEquals(new BigDecimal("5.00"), r.getEScore());
|
||||||
|
assertEquals(new BigDecimal("-1025.00"), r.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Null 字段:当作 0 处理,不抛异常。 */
|
||||||
|
@Test
|
||||||
|
public void member_with_null_amounts_should_be_zero() {
|
||||||
|
TeamDailyMemberRow r = new TeamDailyMemberRow();
|
||||||
|
TeamReportFormula.applyFormula(r, SERVICE_RATE, E_SCORE_RATE);
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getPrevBuy());
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getTodaySell());
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getTodayBuy());
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getServiceFee());
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getEScore());
|
||||||
|
assertEquals(new BigDecimal("0.00"), r.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小计:4 名成员全量样例对账。 */
|
||||||
|
@Test
|
||||||
|
public void summary_should_match_doc_team_F_sample() {
|
||||||
|
List<TeamDailyMemberRow> rows = Arrays.asList(
|
||||||
|
row("王珍华", "21506", "22151", "21418"),
|
||||||
|
row("柯美燕", "22519", "23195", "0"),
|
||||||
|
row("王兵启", "34266", "35294", "0"),
|
||||||
|
row("胡晓彩", "27436", "28259", "27708")
|
||||||
|
);
|
||||||
|
rows.forEach(r -> TeamReportFormula.applyFormula(r, SERVICE_RATE, E_SCORE_RATE));
|
||||||
|
TeamDailySummary s = TeamReportFormula.aggregateSummary(rows);
|
||||||
|
|
||||||
|
// 文档样例核对:D-1 买单合计 = 105727
|
||||||
|
assertEquals(new BigDecimal("105727.00"), s.getPrevBuy());
|
||||||
|
// D 卖单合计 = 108899
|
||||||
|
assertEquals(new BigDecimal("108899.00"), s.getTodaySell());
|
||||||
|
// D 买单合计 = 49126
|
||||||
|
assertEquals(new BigDecimal("49126.00"), s.getTodayBuy());
|
||||||
|
// 服务费合计:428.36 + 0 + 0 + 554.16 = 982.52
|
||||||
|
assertEquals(new BigDecimal("982.52"), s.getServiceFee());
|
||||||
|
// E积分合计:107.09 + 0 + 0 + 138.54 = 245.63
|
||||||
|
assertEquals(new BigDecimal("245.63"), s.getEScore());
|
||||||
|
// 实际收付合计:197.55 + 23195 + 35294 + (-141.70) = 58544.85
|
||||||
|
assertEquals(new BigDecimal("58544.85"), s.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空成员列表:小计全 0,不抛异常。 */
|
||||||
|
@Test
|
||||||
|
public void summary_empty_rows_should_zero() {
|
||||||
|
TeamDailySummary s = TeamReportFormula.aggregateSummary(Collections.emptyList());
|
||||||
|
assertNotNull(s);
|
||||||
|
assertEquals(BigDecimal.ZERO, s.getPrevBuy());
|
||||||
|
assertEquals(BigDecimal.ZERO, s.getTodayBuy());
|
||||||
|
assertEquals(BigDecimal.ZERO, s.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 不同费率:service_rate=0.03 / e_score_rate=0.01。 */
|
||||||
|
@Test
|
||||||
|
public void custom_rates_should_be_applied() {
|
||||||
|
TeamDailyMemberRow r = new TeamDailyMemberRow()
|
||||||
|
.setTodaySell(new BigDecimal("0"))
|
||||||
|
.setTodayBuy(new BigDecimal("1000"));
|
||||||
|
TeamReportFormula.applyFormula(r, new BigDecimal("0.03"), new BigDecimal("0.01"));
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("30.00"), r.getServiceFee());
|
||||||
|
assertEquals(new BigDecimal("10.00"), r.getEScore());
|
||||||
|
assertEquals(new BigDecimal("-1040.00"), r.getActual());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TeamDailyMemberRow row(String name, String prev, String sell, String buy) {
|
||||||
|
return new TeamDailyMemberRow()
|
||||||
|
.setNickname(name)
|
||||||
|
.setPrevBuy(new BigDecimal(prev))
|
||||||
|
.setTodaySell(new BigDecimal(sell))
|
||||||
|
.setTodayBuy(new BigDecimal(buy));
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/今日抢单用户列表_开发说明文档.docx
Normal file
BIN
docs/今日抢单用户列表_开发说明文档.docx
Normal file
Binary file not shown.
BIN
docs/团队每日对账日报_开发说明文档.docx
Normal file
BIN
docs/团队每日对账日报_开发说明文档.docx
Normal file
Binary file not shown.
BIN
docs/寄卖外部免认证三件功能_开发计划.docx
Normal file
BIN
docs/寄卖外部免认证三件功能_开发计划.docx
Normal file
Binary file not shown.
BIN
docs/寄卖订单管理_开发说明文档.docx
Normal file
BIN
docs/寄卖订单管理_开发说明文档.docx
Normal file
Binary file not shown.
59
restart-backend.command
Executable file
59
restart-backend.command
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 双击此文件即可在新终端启动 backend:
|
||||||
|
# 1) 杀掉占用 20600 的旧进程
|
||||||
|
# 2) 用 Maven 把 crmeb-common / crmeb-service 安装到本地 m2(这样 crmeb-admin 单模块运行时能找到依赖)
|
||||||
|
# 3) 进入 crmeb-admin 目录,用完整 GAV 调用 spring-boot:run
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
PROFILE="${BACKEND_PROFILE:-byjyw149}"
|
||||||
|
|
||||||
|
echo "🛑 Stopping any process listening on :20600 ..."
|
||||||
|
PIDS=$(lsof -t -iTCP:20600 -sTCP:LISTEN 2>/dev/null || true)
|
||||||
|
if [ -n "$PIDS" ]; then
|
||||||
|
echo " killing PIDs: $PIDS"
|
||||||
|
kill -9 $PIDS 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
echo " no existing process on :20600"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 自动定位 Java(沿用 start-backend.sh 的逻辑)
|
||||||
|
find_java() {
|
||||||
|
if /usr/libexec/java_home &>/dev/null; then
|
||||||
|
echo "$(/usr/libexec/java_home)/bin/java"; return
|
||||||
|
fi
|
||||||
|
for p in /opt/homebrew/opt/openjdk*/bin/java /opt/homebrew/opt/openjdk/bin/java \
|
||||||
|
/usr/local/opt/openjdk*/bin/java /usr/local/opt/openjdk/bin/java; do
|
||||||
|
[ -x "$p" ] && echo "$p" && return
|
||||||
|
done
|
||||||
|
[ -n "$SDKMAN_DIR" ] && [ -x "$SDKMAN_DIR/candidates/java/current/bin/java" ] && \
|
||||||
|
echo "$SDKMAN_DIR/candidates/java/current/bin/java" && return
|
||||||
|
command -v java 2>/dev/null
|
||||||
|
}
|
||||||
|
JAVA_BIN=$(find_java)
|
||||||
|
if [ -z "$JAVA_BIN" ]; then
|
||||||
|
echo "❌ 未找到 Java,请先安装 JDK 11(brew install openjdk@11)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export JAVA_HOME="$(dirname "$(dirname "$JAVA_BIN")")"
|
||||||
|
echo "☕ Java: $JAVA_BIN"
|
||||||
|
"$JAVA_BIN" -version
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 第一步:把依赖模块编译并安装到本地 m2(首次执行会下载依赖;只在源代码变更后需要重跑)
|
||||||
|
echo "🔧 Step 1: install crmeb-common + crmeb-service to local m2 ..."
|
||||||
|
echo ""
|
||||||
|
./mvnw install -pl crmeb-common,crmeb-service -am -Dmaven.test.skip=true -q
|
||||||
|
|
||||||
|
# 第二步:进入 crmeb-admin 单模块跑 spring-boot:run(避免根 pom 触发 main class 错误)
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Step 2: launch crmeb-admin (profile=$PROFILE) ..."
|
||||||
|
echo ""
|
||||||
|
cd crmeb-admin
|
||||||
|
exec ../mvnw \
|
||||||
|
org.springframework.boot:spring-boot-maven-plugin:2.3.0.RELEASE:run \
|
||||||
|
-Dmaven.test.skip=true \
|
||||||
|
-Dspring-boot.run.profiles="$PROFILE"
|
||||||
Reference in New Issue
Block a user