feat(integral-external): 新增寄卖外部免认证三件套页面

This commit is contained in:
danaisuiyuan
2026-05-02 06:13:41 +08:00
parent 49900919c6
commit d8ad6cde20
35 changed files with 3369 additions and 14 deletions

View File

@@ -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<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);
}
// ========================================================================
// 今日抢单用户列表
// ========================================================================
/**
* 今日抢单用户列表(免认证)
* 过滤口径:今日购买总金额 &gt; 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);
}
}

View File

@@ -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;
}

View File

@@ -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 = "团队长 IDwa_users.id为空时按团队长分组返回所有团队")
private Integer leaderId;
@ApiModelProperty(value = "查询日期 Dyyyy-MM-dd缺省为昨天")
private String date;
@ApiModelProperty(value = "是否包含禁用成员,默认 false")
private Boolean includeDisabled = Boolean.FALSE;
@ApiModelProperty(value = "限定成员 ID 列表(前端勾选过滤)")
private List<Integer> memberIds;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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("查询日期 Dyyyy-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;
}

View File

@@ -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("查询日期 Dyyyy-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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -22,6 +22,13 @@
<artifactId>crmeb-common</artifactId>
<version>${crmeb-common}</version>
</dependency>
<!-- 单元测试(仅 test 作用域;用于 util 类公式校验等纯函数测试) -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>

View File

@@ -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 JOINHAVING 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);
}

View File

@@ -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);
}

View File

@@ -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 {
/**
* 分页查询当日已发生买单且金额 &gt; 0 的用户列表。
* 排序:今日购买总金额 DESC相同时按 id DESC。
*
* @param request 搜索条件
* @param pageParamRequest 分页参数
* @return PageInfo&lt;ExternalGrabUserResponse&gt;
*/
PageInfo<ExternalGrabUserResponse> list(ExternalGrabUserRequest request,
PageParamRequest pageParamRequest);
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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<WaOrderDao, WaOrder> im
@Resource
private WaUsersDao waUsersDao;
@Resource
private WaMerchandiseDao waMerchandiseDao;
/**
* 分页列表查询
*/
@@ -108,31 +117,58 @@ public class WaOrderAdminServiceImpl extends ServiceImpl<WaOrderDao, WaOrder> im
wrapper.orderByDesc(WaOrder::getCreatedAt);
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 -> {
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<WaOrder>) new PageInfo<>(list), responseList);
}
@@ -166,7 +202,16 @@ public class WaOrderAdminServiceImpl extends ServiceImpl<WaOrderDao, WaOrder> 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;
}

View File

@@ -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 ? "正常" : "禁用";
}
}

View File

@@ -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;
}
}

View File

@@ -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 &gt;= #{todayStart}
AND pay_time &lt;= #{todayEnd}
GROUP BY buyer_id
HAVING SUM(total_money) &gt; 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 &gt;= #{todayStart}
AND pay_time &lt;= #{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 &gt;= #{yesterdayStart}
AND pay_time &lt;= #{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>

View File

@@ -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 &gt;= #{prevStart}
AND pay_time &lt;= #{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 &gt;= #{dStart}
AND pay_time &lt;= #{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 &gt;= #{dStart}
AND pay_time &lt;= #{dEnd}
GROUP BY buyer_id
) buy ON buy.uid = u.id
<where>
<choose>
<when test="leaderId != null">
u.pid = #{leaderId}
</when>
<otherwise>
u.pid &gt; 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>

View File

@@ -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));
}
}

View File

@@ -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));
}
}