diff --git a/backend-adminend/src/api/integralExternal.js b/backend-adminend/src/api/integralExternal.js index b0561ae..effc8f3 100644 --- a/backend-adminend/src/api/integralExternal.js +++ b/backend-adminend/src/api/integralExternal.js @@ -39,3 +39,59 @@ export function getExternalIntegralLog(data) { 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, + }); +} diff --git a/backend-adminend/src/router/modules/integralExternal.js b/backend-adminend/src/router/modules/integralExternal.js index 7ba5f1d..6371445 100644 --- a/backend-adminend/src/router/modules/integralExternal.js +++ b/backend-adminend/src/router/modules/integralExternal.js @@ -24,6 +24,24 @@ const integralExternalRouter = { name: 'IntegralExternalUserDetail', 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: '今日抢单用户' }, + }, ], }; diff --git a/backend-adminend/src/utils/requestNoAuth.js b/backend-adminend/src/utils/requestNoAuth.js index 5e03ffb..de53dca 100644 --- a/backend-adminend/src/utils/requestNoAuth.js +++ b/backend-adminend/src/utils/requestNoAuth.js @@ -28,6 +28,11 @@ service.interceptors.request.use( // 响应拦截器 — 不拦截 401 跳转 service.interceptors.response.use( (response) => { + // Blob / arraybuffer 等二进制响应直接透传,不做业务码拆包 + const responseType = response.config && response.config.responseType; + if (responseType === 'blob' || responseType === 'arraybuffer') { + return response.data; + } const res = response.data; if (res.code !== 0 && res.code !== 200) { Message({ diff --git a/backend-adminend/src/views/dashboard/components/gridMenu.vue b/backend-adminend/src/views/dashboard/components/gridMenu.vue index 1a80c95..10916a6 100644 --- a/backend-adminend/src/views/dashboard/components/gridMenu.vue +++ b/backend-adminend/src/views/dashboard/components/gridMenu.vue @@ -121,6 +121,27 @@ export default { url: '/integral-external/user/integral-detail', 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: [ { title: '待发货订单', num: 0, path: '/order/index', perms: ['admin:order:list'] }, diff --git a/backend-adminend/src/views/integral-external/grab-user/index.vue b/backend-adminend/src/views/integral-external/grab-user/index.vue new file mode 100644 index 0000000..b6814fd --- /dev/null +++ b/backend-adminend/src/views/integral-external/grab-user/index.vue @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + + + + + + + {{ scope.row.username || scope.row.mobile || '-' }} + + + + + {{ scope.row.nickname || '-' }} + + + + + {{ scope.row.prevSellCnt || 0 }}/{{ scope.row.todayBuyCnt || 0 }} + + + + + 查看 + - + + + + + + {{ scope.row.pid || '-' }} + + + + + {{ scope.row.maxOrder ? scope.row.maxOrder : '未设置' }} + + + + + + + + {{ scope.row.levelName || '普通用户' }} + + + + + + + + + + + {{ scope.row.statusStr || (scope.row.status === 1 ? '正常' : '禁用') }} + + + + + + + 修改上级 + 合同重签 + 编辑 + + + + + + + + + + + + {{ writeTipText }} + + 知道了 + + + + + + + + diff --git a/backend-adminend/src/views/integral-external/team-report/index.vue b/backend-adminend/src/views/integral-external/team-report/index.vue new file mode 100644 index 0000000..689504b --- /dev/null +++ b/backend-adminend/src/views/integral-external/team-report/index.vue @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + 查询 + 重置 + 导出 Excel + + + + Tips: 团队长 ID 留空时返回所有团队的日报,导出 Excel 仅支持单团队。 + + + + + + + + 全部团队总计 · {{ report.teamCount }} 个团队 · 共 {{ report.totalMemberCount }} 人 · {{ report.date }} + + + 服务费率 {{ report.serviceRate }} · E 积分率 {{ report.eScoreRate }} + + + + + + {{ report.previousDate }} 买单合计 + {{ formatNum(report.grandSummary.prevBuy) }} + + + + + {{ report.date }} 卖单合计 + {{ formatNum(report.grandSummary.todaySell) }} + + + + + {{ report.date }} 买单合计 + {{ formatNum(report.grandSummary.todayBuy) }} + + + + + 实际收付合计 + {{ formatNum(report.grandSummary.actual) }} + + + + + + + + + + + 团队长 {{ team.leaderNickname || team.teamCode || '-' }} + · {{ team.memberCount }} 人 · {{ team.date }} + + 导出该团队 + + + + + + + {{ team.previousDate }} 买单合计 + {{ formatNum(team.summary.prevBuy) }} + + + + + {{ team.date }} 卖单合计 + {{ formatNum(team.summary.todaySell) }} + + + + + {{ team.date }} 买单合计 + {{ formatNum(team.summary.todayBuy) }} + + + + + 实际收付合计 + {{ formatNum(team.summary.actual) }} + + + + + + + + {{ scope.row.nickname || '-' }} + 已禁用 + + + + {{ formatNum(scope.row.prevBuy) }} + + + {{ formatNum(scope.row.todaySell) }} + + + {{ formatNum(scope.row.todayBuy) }} + + + {{ formatNum(scope.row.serviceFee) }} + + + {{ formatNum(scope.row.eScore) }} + + + + {{ formatNum(scope.row.actual) }} + + + + + {{ team.leaderNickname || team.teamCode || '-' }} + + + + + + + + + 该团队当日无成员数据 + + + + + 未查询到任何团队数据。 + + + + + + + diff --git a/backend-adminend/src/views/integral-external/wa-order/index.vue b/backend-adminend/src/views/integral-external/wa-order/index.vue new file mode 100644 index 0000000..e5be50c --- /dev/null +++ b/backend-adminend/src/views/integral-external/wa-order/index.vue @@ -0,0 +1,747 @@ + + + + + + + + 全部 + 待付款 + 已支付 + 交易完成 + 已取消 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + 全选 + 反选 + 恢复默认 + + + {{ col.label }} + + + 列设置 + + + + + + + + + + + - + + + + + + + 详情 + + + + + + + + + + + + + + + + + 订单号 + {{ detail ? detail.orderSn : '-' }} + 复制 + + + {{ statusLabel(detail) }} + 转拍 + 已取消 + + + + + + + + + 订单时间链路 + + + + + + + + + + + 订单基础 + + {{ detail.id }} + + ¥{{ detail.totalMoney }} + + {{ detail.merchandiseId || '-' }} + {{ detail.merchandiseTitle || '-' }} + {{ detail.isShow === 1 ? '显示' : '隐藏' }} + {{ detail.oldId || '-' }} + + + - + + + + + + + 买家信息 + + {{ detail.buyerId || '-' }} + {{ detail.buyerName || '-' }} + {{ detail.consignee || '-' }} + {{ detail.phone || '-' }} + {{ joinArea(detail) }} + {{ detail.address || '-' }} + + + + + + 卖家信息 + + + {{ detail.sellerId === 0 ? '0(平台)' : detail.sellerId }} + + + {{ detail.sellerId === 0 ? '平台' : (detail.sellerName || '-') }} + + + + + + + 支付与日志 + + {{ detail.buyTime || '-' }} + {{ detail.payTime || '-' }} + {{ detail.confirmTime || '-' }} + {{ detail.createdAt || '-' }} + {{ detail.buyIp || '-' }} + + {{ detail.cancelIp || '-' }} + + + + - + + + + + + + + + + + + + + diff --git a/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java b/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java index 8f3b57f..6b945ef 100644 --- a/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java +++ b/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java @@ -3,20 +3,35 @@ package com.zbkj.admin.controller; import cn.hutool.core.collection.CollUtil; import com.zbkj.common.page.CommonPage; import com.zbkj.common.request.*; +import com.zbkj.common.response.ExternalGrabUserResponse; 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.UserResponse; +import com.zbkj.common.response.WaOrderResponse; import com.zbkj.common.result.CommonResult; +import com.zbkj.service.service.ExternalGrabUserService; import com.zbkj.service.service.StoreOrderService; +import com.zbkj.service.service.TeamReportExternalService; import com.zbkj.service.service.UserIntegralRecordService; import com.zbkj.service.service.UserService; +import com.zbkj.service.service.WaOrderAdminService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; 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.web.bind.annotation.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + /** * 积分模块外部免认证接口 Controller * 供管理后台外部页面(/integral-external/*)调用,跳过登录验证。 @@ -40,6 +55,15 @@ public class ExternalIntegralController { @Autowired private UserService userService; + @Autowired + private WaOrderAdminService waOrderAdminService; + + @Autowired + private TeamReportExternalService teamReportExternalService; + + @Autowired + private ExternalGrabUserService externalGrabUserService; + /** * 积分明细分页列表(免认证) * 复用 UserIntegralRecordService.findAdminList,与 /admin/user/integral/list 逻辑完全一致。 @@ -93,4 +117,90 @@ public class ExternalIntegralController { } return CommonResult.success(restPage); } + + // ======================================================================== + // 寄卖订单管理(wa_order) + // 复用 WaOrderAdminService(仅读路径),不使用 @PreAuthorize。 + // ======================================================================== + + /** + * 寄卖订单分页列表(免认证) + */ + @ApiOperation(value = "寄卖订单分页列表(免认证)") + @GetMapping(value = "/wa-order/list") + public CommonResult> waOrderList( + @ModelAttribute @Validated WaOrderSearchRequest request, + @Validated PageParamRequest pageParamRequest) { + CommonPage restPage = + CommonPage.restPage(waOrderAdminService.getAdminList(request, pageParamRequest)); + return CommonResult.success(restPage); + } + + /** + * 寄卖订单详情(免认证) + */ + @ApiOperation(value = "寄卖订单详情(免认证)") + @GetMapping(value = "/wa-order/info") + public CommonResult waOrderInfo(@RequestParam Integer id) { + return CommonResult.success(waOrderAdminService.getDetailById(id)); + } + + // ======================================================================== + // 团队每日对账日报 + // ======================================================================== + + /** + * 团队每日对账日报(免认证)。 + * - leaderId 非空:返回该团队对账(teams 仅 1 项) + * - leaderId 为空:按团队长分组返回所有团队对账 + 跨团队总计 + */ + @ApiOperation(value = "团队每日对账(免认证)") + @GetMapping(value = "/team-report/daily") + public CommonResult teamDailyReport( + @ModelAttribute @Validated TeamDailyReportRequest request) { + return CommonResult.success(teamReportExternalService.getMultiDailyReport(request)); + } + + /** + * 团队每日对账日报 - Excel 导出(免认证;仅支持单团队,必须传 leaderId) + */ + @ApiOperation(value = "团队每日对账 Excel 导出(免认证)") + @GetMapping(value = "/team-report/daily/export") + public ResponseEntity 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> grabUserList( + @ModelAttribute @Validated ExternalGrabUserRequest request, + @Validated PageParamRequest pageParamRequest) { + CommonPage restPage = + CommonPage.restPage(externalGrabUserService.list(request, pageParamRequest)); + return CommonResult.success(restPage); + } } diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/request/ExternalGrabUserRequest.java b/backend/crmeb-common/src/main/java/com/zbkj/common/request/ExternalGrabUserRequest.java new file mode 100644 index 0000000..60e5dec --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/request/ExternalGrabUserRequest.java @@ -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; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/request/TeamDailyReportRequest.java b/backend/crmeb-common/src/main/java/com/zbkj/common/request/TeamDailyReportRequest.java new file mode 100644 index 0000000..26f9d65 --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/request/TeamDailyReportRequest.java @@ -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 memberIds; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/ExternalGrabUserResponse.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/ExternalGrabUserResponse.java new file mode 100644 index 0000000..8e2079e --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/ExternalGrabUserResponse.java @@ -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; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyMemberRow.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyMemberRow.java new file mode 100644 index 0000000..7e7b13e --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyMemberRow.java @@ -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; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyMultiReportResponse.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyMultiReportResponse.java new file mode 100644 index 0000000..20ef2d2 --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyMultiReportResponse.java @@ -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 teams; + + @ApiModelProperty("跨团队总计") + private TeamDailySummary grandSummary; + + @ApiModelProperty("警告信息(如非法 memberIds)") + private List warnings; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyReportResponse.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyReportResponse.java new file mode 100644 index 0000000..472f5af --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailyReportResponse.java @@ -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 rows; + + @ApiModelProperty("小计") + private TeamDailySummary summary; + + @ApiModelProperty("警告信息(如非法 memberIds)") + private List warnings; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailySummary.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailySummary.java new file mode 100644 index 0000000..f31662f --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/TeamDailySummary.java @@ -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; +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/WaOrderResponse.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/WaOrderResponse.java index d3d6fca..92106f3 100644 --- a/backend/crmeb-common/src/main/java/com/zbkj/common/response/WaOrderResponse.java +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/WaOrderResponse.java @@ -93,6 +93,12 @@ public class WaOrderResponse implements Serializable { @ApiModelProperty(value = "寄售商品ID") private Integer merchandiseId; + @ApiModelProperty(value = "寄售商品标题") + private String merchandiseTitle; + + @ApiModelProperty(value = "寄售商品图片") + private String merchandiseImage; + @ApiModelProperty(value = "确认收货时间") private Date confirmTime; diff --git a/backend/crmeb-service/pom.xml b/backend/crmeb-service/pom.xml index 5daec8e..9a6c2dd 100644 --- a/backend/crmeb-service/pom.xml +++ b/backend/crmeb-service/pom.xml @@ -22,6 +22,13 @@ crmeb-common ${crmeb-common} + + + junit + junit + 4.12 + test + com.jayway.jsonpath json-path diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/dao/consignment/ExternalGrabUserDao.java b/backend/crmeb-service/src/main/java/com/zbkj/service/dao/consignment/ExternalGrabUserDao.java new file mode 100644 index 0000000..fab1752 --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/dao/consignment/ExternalGrabUserDao.java @@ -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 { + + /** + * 列表查询。 + * + * @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 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); +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/dao/consignment/TeamReportDao.java b/backend/crmeb-service/src/main/java/com/zbkj/service/dao/consignment/TeamReportDao.java new file mode 100644 index 0000000..787702a --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/dao/consignment/TeamReportDao.java @@ -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 { + + /** + * 查询团队成员每日对账原始数据。 + * + * @param leaderId 团队长 ID;为 null 时返回所有有 pid 的成员,由 Service 按 leader_id 分组 + */ + List 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 memberIds); + + /** + * 读取 wa_setting 中某 key 的字符串值(不存在时返回 null)。 + * KV 表无独立 Model,使用注解直接查询。 + */ + @Select("SELECT `value` FROM wa_setting WHERE `name` = #{name} LIMIT 1") + String selectSettingValue(@Param("name") String name); +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/ExternalGrabUserService.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/ExternalGrabUserService.java new file mode 100644 index 0000000..36a22ba --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/ExternalGrabUserService.java @@ -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 list(ExternalGrabUserRequest request, + PageParamRequest pageParamRequest); +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/TeamReportExternalService.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/TeamReportExternalService.java new file mode 100644 index 0000000..efd0707 --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/TeamReportExternalService.java @@ -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; +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/ExternalGrabUserServiceImpl.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/ExternalGrabUserServiceImpl.java new file mode 100644 index 0000000..c00c02f --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/ExternalGrabUserServiceImpl.java @@ -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 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 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()); + } +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/TeamReportExternalServiceImpl.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/TeamReportExternalServiceImpl.java new file mode 100644 index 0000000..7aa4492 --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/TeamReportExternalServiceImpl.java @@ -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 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> groups = new LinkedHashMap<>(); + Set 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 leaderMap = new HashMap<>(); + if (!leaderIds.isEmpty()) { + for (WaUsers u : waUsersDao.selectBatchIds(leaderIds)) { + leaderMap.put(u.getId(), u); + } + } + + // 组装每个团队子报表 + List teams = new ArrayList<>(groups.size()); + TeamDailySummary grand = new TeamDailySummary(); + int totalMembers = 0; + for (Map.Entry> e : groups.entrySet()) { + Integer lid = e.getKey(); + List 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 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); + } +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaOrderAdminServiceImpl.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaOrderAdminServiceImpl.java index 303d822..97c3fd7 100644 --- a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaOrderAdminServiceImpl.java +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaOrderAdminServiceImpl.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; 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.WaUsers; 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.WaOrderUpdateRequest; 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.WaUsersDao; import com.zbkj.service.service.WaOrderAdminService; @@ -20,7 +22,11 @@ import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -44,6 +50,9 @@ public class WaOrderAdminServiceImpl extends ServiceImpl im @Resource private WaUsersDao waUsersDao; + @Resource + private WaMerchandiseDao waMerchandiseDao; + /** * 分页列表查询 */ @@ -108,31 +117,58 @@ public class WaOrderAdminServiceImpl extends ServiceImpl im wrapper.orderByDesc(WaOrder::getCreatedAt); List list = waOrderDao.selectList(wrapper); + + // 批量收集所需关联 ID,避免循环 N+1 查询 + Set userIds = new HashSet<>(); + Set 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 userMap = new HashMap<>(); + if (!userIds.isEmpty()) { + for (WaUsers u : waUsersDao.selectBatchIds(userIds)) { + userMap.put(u.getId(), u); + } + } + Map merchandiseMap = new HashMap<>(); + if (!merchandiseIds.isEmpty()) { + for (WaMerchandise m : waMerchandiseDao.selectBatchIds(merchandiseIds)) { + merchandiseMap.put(m.getId(), m); + } + } + List responseList = list.stream().map(item -> { WaOrderResponse response = new WaOrderResponse(); BeanUtils.copyProperties(item, response); - - // 获取卖家名称 + + // 卖家名称 if (item.getSellerId() != null && item.getSellerId() > 0) { - WaUsers seller = waUsersDao.selectById(item.getSellerId()); - if (seller != null) { - response.setSellerName(seller.getNickname()); - } + WaUsers seller = userMap.get(item.getSellerId()); + if (seller != null) response.setSellerName(seller.getNickname()); } else { response.setSellerName("平台"); } - - // 获取买家名称 + + // 买家名称 if (item.getBuyerId() != null && item.getBuyerId() > 0) { - WaUsers buyer = waUsersDao.selectById(item.getBuyerId()); - if (buyer != null) { - response.setBuyerName(buyer.getNickname()); + WaUsers buyer = userMap.get(item.getBuyerId()); + if (buyer != null) 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()); } } - + return response; }).collect(Collectors.toList()); - + return CommonPage.copyPageInfo((PageInfo) new PageInfo<>(list), responseList); } @@ -166,7 +202,16 @@ public class WaOrderAdminServiceImpl extends ServiceImpl im response.setBuyerName(buyer.getNickname()); } } - + + // 商品名称 / 图片 + 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; } diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/util/GrabUserFormatter.java b/backend/crmeb-service/src/main/java/com/zbkj/service/util/GrabUserFormatter.java new file mode 100644 index 0000000..4e2c98f --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/util/GrabUserFormatter.java @@ -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 ? "正常" : "禁用"; + } +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/util/TeamReportFormula.java b/backend/crmeb-service/src/main/java/com/zbkj/service/util/TeamReportFormula.java new file mode 100644 index 0000000..79374f7 --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/util/TeamReportFormula.java @@ -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 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; + } +} diff --git a/backend/crmeb-service/src/main/resources/mapper/consignment/ExternalGrabUserMapper.xml b/backend/crmeb-service/src/main/resources/mapper/consignment/ExternalGrabUserMapper.xml new file mode 100644 index 0000000..3f6f979 --- /dev/null +++ b/backend/crmeb-service/src/main/resources/mapper/consignment/ExternalGrabUserMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + AND u.id = #{uid} + + + AND (u.mobile LIKE CONCAT('%', #{mobile}, '%') + OR u.username LIKE CONCAT('%', #{mobile}, '%')) + + + AND u.pid = #{pid} + + + ORDER BY today_buy_amount DESC, u.id DESC + + + diff --git a/backend/crmeb-service/src/main/resources/mapper/consignment/TeamReportMapper.xml b/backend/crmeb-service/src/main/resources/mapper/consignment/TeamReportMapper.xml new file mode 100644 index 0000000..eb7e470 --- /dev/null +++ b/backend/crmeb-service/src/main/resources/mapper/consignment/TeamReportMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + 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 + + + + u.pid = #{leaderId} + + + u.pid > 0 + + + + AND u.status = 1 + + + AND u.id IN + + #{mid} + + + + ORDER BY u.pid ASC, u.id ASC + + + diff --git a/backend/crmeb-service/src/test/java/com/zbkj/service/util/GrabUserFormatterTest.java b/backend/crmeb-service/src/test/java/com/zbkj/service/util/GrabUserFormatterTest.java new file mode 100644 index 0000000..b0970b4 --- /dev/null +++ b/backend/crmeb-service/src/test/java/com/zbkj/service/util/GrabUserFormatterTest.java @@ -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)); + } +} diff --git a/backend/crmeb-service/src/test/java/com/zbkj/service/util/TeamReportFormulaTest.java b/backend/crmeb-service/src/test/java/com/zbkj/service/util/TeamReportFormulaTest.java new file mode 100644 index 0000000..f226302 --- /dev/null +++ b/backend/crmeb-service/src/test/java/com/zbkj/service/util/TeamReportFormulaTest.java @@ -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 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)); + } +} diff --git a/docs/今日抢单用户列表_开发说明文档.docx b/docs/今日抢单用户列表_开发说明文档.docx new file mode 100644 index 0000000..dc41d97 Binary files /dev/null and b/docs/今日抢单用户列表_开发说明文档.docx differ diff --git a/docs/团队每日对账日报_开发说明文档.docx b/docs/团队每日对账日报_开发说明文档.docx new file mode 100644 index 0000000..41446ae Binary files /dev/null and b/docs/团队每日对账日报_开发说明文档.docx differ diff --git a/docs/寄卖外部免认证三件功能_开发计划.docx b/docs/寄卖外部免认证三件功能_开发计划.docx new file mode 100644 index 0000000..e152b90 Binary files /dev/null and b/docs/寄卖外部免认证三件功能_开发计划.docx differ diff --git a/docs/寄卖订单管理_开发说明文档.docx b/docs/寄卖订单管理_开发说明文档.docx new file mode 100644 index 0000000..cbe9b1f Binary files /dev/null and b/docs/寄卖订单管理_开发说明文档.docx differ diff --git a/restart-backend.command b/restart-backend.command new file mode 100755 index 0000000..03b124d --- /dev/null +++ b/restart-backend.command @@ -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"