feat(glt): 添加送水订单模块并优化经销商结算功能

- 新增送水订单实体类 GltTicketOrder 及其相关控制器、服务、映射器
- 添加送水订单参数类 GltTicketOrderParam 和 XML 映射配置
- 实现送水订单的增删改查、分页查询等完整 CRUD 功能
- 在经销商结算任务中引入分销设置功能,支持按级别控制分佣
- 更新总经销商分润计算逻辑,使用动态费率替代固定值
- 删除不再使用的中文字体修复脚本文件
- 重构经销商推荐佣金结算逻辑,支持最多三级分佣
- 优化订单状态检查逻辑,在退款流程中排除已完成订单
This commit is contained in:
2026-02-05 18:51:53 +08:00
parent 9672be2252
commit 88afd149c3
11 changed files with 627 additions and 173 deletions

View File

@@ -1,125 +0,0 @@
#!/bin/bash
###############################################################################
# 捐款证书中文乱码修复脚本
# 用途在运行中的Docker容器内安装中文字体
# 适用于:无法重新构建镜像的紧急情况
###############################################################################
set -e # 遇到错误立即退出
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 容器名称(可根据实际情况修改)
CONTAINER_NAME="websoft-api-container"
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}中文字体修复脚本${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
# 检查容器是否存在
echo -e "${YELLOW}步骤 1/6: 检查容器状态...${NC}"
if ! docker ps | grep -q "$CONTAINER_NAME"; then
echo -e "${RED}错误:容器 $CONTAINER_NAME 未运行!${NC}"
echo "当前运行的容器:"
docker ps --format "table {{.Names}}\t{{.Status}}"
echo ""
read -p "请输入正确的容器名称: " CONTAINER_NAME
if [ -z "$CONTAINER_NAME" ]; then
echo -e "${RED}容器名称不能为空,退出。${NC}"
exit 1
fi
fi
echo -e "${GREEN}✓ 容器正在运行${NC}"
echo ""
# 检查是否已安装字体
echo -e "${YELLOW}步骤 2/6: 检查是否已安装中文字体...${NC}"
if docker exec "$CONTAINER_NAME" fc-list :lang=zh 2>/dev/null | grep -q "WenQuanYi"; then
echo -e "${GREEN}✓ 中文字体已安装${NC}"
docker exec "$CONTAINER_NAME" fc-list :lang=zh
echo ""
read -p "是否重新安装?(y/N): " REINSTALL
if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then
echo "跳过安装,退出。"
exit 0
fi
else
echo -e "${YELLOW}未检测到中文字体,开始安装...${NC}"
fi
echo ""
# 安装字体工具
echo -e "${YELLOW}步骤 3/6: 安装字体工具...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "apk add --no-cache fontconfig ttf-dejavu wget" || {
echo -e "${RED}✗ 字体工具安装失败${NC}"
exit 1
}
echo -e "${GREEN}✓ 字体工具安装成功${NC}"
echo ""
# 下载中文字体
echo -e "${YELLOW}步骤 4/6: 下载文泉驿微米黑字体...${NC}"
echo "正在从GitHub下载约10MB请稍候..."
docker exec -u root "$CONTAINER_NAME" sh -c "
wget -O /tmp/wqy-microhei.ttc https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc 2>&1 | grep -E 'Connecting|Length|saved' || true
" || {
echo -e "${YELLOW}GitHub下载失败尝试使用代理...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "
wget -O /tmp/wqy-microhei.ttc https://ghproxy.com/https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc
" || {
echo -e "${RED}✗ 字体下载失败${NC}"
echo "请检查网络连接或手动下载字体文件。"
exit 1
}
}
echo -e "${GREEN}✓ 字体下载成功${NC}"
echo ""
# 安装字体
echo -e "${YELLOW}步骤 5/6: 安装字体文件...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "
mkdir -p /usr/share/fonts/truetype/wqy && \
mv /tmp/wqy-microhei.ttc /usr/share/fonts/truetype/wqy/ && \
fc-cache -fv
" || {
echo -e "${RED}✗ 字体安装失败${NC}"
exit 1
}
echo -e "${GREEN}✓ 字体安装成功${NC}"
echo ""
# 验证安装
echo -e "${YELLOW}步骤 6/6: 验证字体安装...${NC}"
FONT_COUNT=$(docker exec "$CONTAINER_NAME" fc-list :lang=zh | wc -l)
if [ "$FONT_COUNT" -gt 0 ]; then
echo -e "${GREEN}✓ 中文字体验证成功!${NC}"
echo "已安装的中文字体:"
docker exec "$CONTAINER_NAME" fc-list :lang=zh
else
echo -e "${RED}✗ 字体验证失败${NC}"
exit 1
fi
echo ""
# 完成提示
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}修复完成!${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
echo "后续步骤:"
echo "1. 不需要重启容器,字体已生效"
echo "2. 重新生成捐款证书即可看到效果"
echo "3. 如果仍有问题,请查看容器日志:"
echo " docker logs -f $CONTAINER_NAME"
echo ""
echo -e "${YELLOW}注意:${NC}"
echo "- 此修复方法在容器重启后会失效"
echo "- 建议后续使用更新后的Dockerfile重新构建镜像"
echo "- 详细文档请参考docs/chinese-font-fix-guide.md"
echo ""

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.afterturn.easypoi.excel.entity;
package com.gxwebsoft.common.core.web;
import cn.afterturn.easypoi.excel.entity.ExcelBaseParams;
import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler;
import lombok.Data;

View File

@@ -0,0 +1,127 @@
package com.gxwebsoft.glt.controller;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltTicketOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 送水订单控制器
*
* @author 科技小王子
* @since 2026-02-05 18:50:21
*/
@Tag(name = "送水订单管理")
@RestController
@RequestMapping("/api/glt/glt-ticket-order")
public class GltTicketOrderController extends BaseController {
@Resource
private GltTicketOrderService gltTicketOrderService;
@PreAuthorize("hasAuthority('glt:gltTicketOrder:list')")
@Operation(summary = "分页查询送水订单")
@GetMapping("/page")
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) {
// 使用关联查询
return success(gltTicketOrderService.pageRel(param));
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:list')")
@Operation(summary = "查询全部送水订单")
@GetMapping()
public ApiResult<List<GltTicketOrder>> list(GltTicketOrderParam param) {
// 使用关联查询
return success(gltTicketOrderService.listRel(param));
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:list')")
@Operation(summary = "根据id查询送水订单")
@GetMapping("/{id}")
public ApiResult<GltTicketOrder> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(gltTicketOrderService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:save')")
@OperationLog
@Operation(summary = "添加送水订单")
@PostMapping()
public ApiResult<?> save(@RequestBody GltTicketOrder gltTicketOrder) {
// 记录当前登录用户id
// User loginUser = getLoginUser();
// if (loginUser != null) {
// gltTicketOrder.setUserId(loginUser.getUserId());
// }
if (gltTicketOrderService.save(gltTicketOrder)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
@OperationLog
@Operation(summary = "修改送水订单")
@PutMapping()
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
if (gltTicketOrderService.updateById(gltTicketOrder)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:remove')")
@OperationLog
@Operation(summary = "删除送水订单")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (gltTicketOrderService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:save')")
@OperationLog
@Operation(summary = "批量添加送水订单")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<GltTicketOrder> list) {
if (gltTicketOrderService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
@OperationLog
@Operation(summary = "批量修改送水订单")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<GltTicketOrder> batchParam) {
if (batchParam.update(gltTicketOrderService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:remove')")
@OperationLog
@Operation(summary = "批量删除送水订单")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (gltTicketOrderService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,84 @@
package com.gxwebsoft.glt.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 送水订单
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "GltTicketOrder对象", description = "送水订单")
public class GltTicketOrder implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "用户水票ID")
private Integer userTicketId;
@Schema(description = "门店ID")
private Integer storeId;
@Schema(description = "配送员")
private Integer riderId;
@Schema(description = "仓库ID")
private Integer warehouseId;
@Schema(description = "关联收货地址")
private Integer addressId;
@Schema(description = "收货地址")
private String address;
@Schema(description = "买家留言")
private String buyerRemarks;
@Schema(description = "用于统计")
private BigDecimal price;
@Schema(description = "购买数量")
private Integer totalNum;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.glt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 送水订单Mapper
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
public interface GltTicketOrderMapper extends BaseMapper<GltTicketOrder> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<GltTicketOrder>
*/
List<GltTicketOrder> selectPageRel(@Param("page") IPage<GltTicketOrder> page,
@Param("param") GltTicketOrderParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<GltTicketOrder> selectListRel(@Param("param") GltTicketOrderParam param);
}

View File

@@ -0,0 +1,81 @@
<?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.gxwebsoft.glt.mapper.GltTicketOrderMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM glt_ticket_order a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.userTicketId != null">
AND a.user_ticket_id = #{param.userTicketId}
</if>
<if test="param.storeId != null">
AND a.store_id = #{param.storeId}
</if>
<if test="param.riderId != null">
AND a.rider_id = #{param.riderId}
</if>
<if test="param.warehouseId != null">
AND a.warehouse_id = #{param.warehouseId}
</if>
<if test="param.addressId != null">
AND a.address_id = #{param.addressId}
</if>
<if test="param.address != null">
AND a.address LIKE CONCAT('%', #{param.address}, '%')
</if>
<if test="param.buyerRemarks != null">
AND a.buyer_remarks LIKE CONCAT('%', #{param.buyerRemarks}, '%')
</if>
<if test="param.price != null">
AND a.price = #{param.price}
</if>
<if test="param.totalNum != null">
AND a.total_num = #{param.totalNum}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.comments != null">
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.glt.entity.GltTicketOrder">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.glt.entity.GltTicketOrder">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,82 @@
package com.gxwebsoft.glt.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 送水订单查询参数
*
* @author 科技小王子
* @since 2026-02-05 18:50:19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "GltTicketOrderParam对象", description = "送水订单查询参数")
public class GltTicketOrderParam extends BaseParam {
private static final long serialVersionUID = 1L;
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "用户水票ID")
@QueryField(type = QueryType.EQ)
private Integer userTicketId;
@Schema(description = "门店ID")
@QueryField(type = QueryType.EQ)
private Integer storeId;
@Schema(description = "配送员")
@QueryField(type = QueryType.EQ)
private Integer riderId;
@Schema(description = "仓库ID")
@QueryField(type = QueryType.EQ)
private Integer warehouseId;
@Schema(description = "关联收货地址")
@QueryField(type = QueryType.EQ)
private Integer addressId;
@Schema(description = "收货地址")
private String address;
@Schema(description = "买家留言")
private String buyerRemarks;
@Schema(description = "用于统计")
@QueryField(type = QueryType.EQ)
private BigDecimal price;
@Schema(description = "购买数量")
@QueryField(type = QueryType.EQ)
private Integer totalNum;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

View File

@@ -0,0 +1,42 @@
package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import java.util.List;
/**
* 送水订单Service
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
public interface GltTicketOrderService extends IService<GltTicketOrder> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<GltTicketOrder>
*/
PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<GltTicketOrder>
*/
List<GltTicketOrder> listRel(GltTicketOrderParam param);
/**
* 根据id查询
*
* @param id
* @return GltTicketOrder
*/
GltTicketOrder getByIdRel(Integer id);
}

View File

@@ -0,0 +1,47 @@
package com.gxwebsoft.glt.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.mapper.GltTicketOrderMapper;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltTicketOrderService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 送水订单Service实现
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
@Service
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
@Override
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<GltTicketOrder> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<GltTicketOrder> listRel(GltTicketOrderParam param) {
List<GltTicketOrder> list = baseMapper.selectListRel(param);
// 排序
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public GltTicketOrder getByIdRel(Integer id) {
GltTicketOrderParam param = new GltTicketOrderParam();
param.setId(id);
return param.getOne(baseMapper.selectListRel(param));
}
}

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.entity.ShopDealerSetting;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopGoods;
import com.gxwebsoft.shop.entity.ShopOrder;
@@ -15,11 +16,13 @@ import com.gxwebsoft.common.system.mapper.UserMapper;
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerRefereeService;
import com.gxwebsoft.shop.service.ShopDealerSettingService;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.util.UpstreamUserFinder;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -72,6 +75,9 @@ public class DealerOrderSettlement10584Task {
@Resource
private ShopDealerOrderService shopDealerOrderService;
@Resource
private ShopDealerSettingService shopDealerSettingService;
@Resource
private ShopGoodsService shopGoodsService;
@@ -96,10 +102,12 @@ public class DealerOrderSettlement10584Task {
// Per-run caches to reduce DB chatter across orders.
Map<Integer, Integer> level1ParentCache = new HashMap<>();
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
Integer totalDealerUserId = findTotalDealerUserId();
if (totalDealerUserId == null) {
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
ShopDealerUser totalDealerUser = findTotalDealerUser();
if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
log.warn("未找到总经销商账号,订单仍可结算但不会发放总经销商分润 - tenantId={}", TENANT_ID);
}
log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level);
log.info("租户{}待结算订单数: {}, orderNos(sample)={}",
TENANT_ID,
@@ -113,7 +121,7 @@ public class DealerOrderSettlement10584Task {
if (!claimOrderToSettle(order.getOrderId())) {
return;
}
settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUserId);
settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
});
} catch (Exception e) {
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
@@ -150,7 +158,8 @@ public class DealerOrderSettlement10584Task {
ShopOrder order,
Map<Integer, Integer> level1ParentCache,
Map<Integer, Boolean> shopRoleCache,
Integer totalDealerUserId
ShopDealerUser totalDealerUser,
int dealerLevel
) {
if (order.getUserId() == null || order.getOrderNo() == null) {
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
@@ -188,13 +197,13 @@ public class DealerOrderSettlement10584Task {
commissionConfig.storeSimpleValue);
// 1) 直推/间推shop_dealer_referee
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig);
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel);
// 2) 门店分红上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)。
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache);
// 3) 总经销商分润:固定比率,每个订单都分。
TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUserId);
TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser);
// 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission, totalDealerCommission);
@@ -202,18 +211,38 @@ public class DealerOrderSettlement10584Task {
log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount);
}
private DealerRefereeCommission settleDealerRefereeCommission(ShopOrder order, BigDecimal baseAmount, int goodsQty, CommissionConfig commissionConfig) {
private DealerRefereeCommission settleDealerRefereeCommission(
ShopOrder order,
BigDecimal baseAmount,
int goodsQty,
CommissionConfig commissionConfig,
int dealerLevel
) {
// 兼容两种数据形态:
// 1) 同一 userId 下有 level=1/2 的多级关系(直接按 level 取);
// 1) 同一 userId 下有 level=1/2/3 的多级关系(直接按 level 取);
// 2) 仅维护 level=1用“查两次”回退获取上级
Integer directDealerId = getDealerRefereeId(order.getUserId(), 1);
Integer simpleDealerId = getDealerRefereeId(order.getUserId(), 2);
if (simpleDealerId == null && directDealerId != null) {
simpleDealerId = getDealerRefereeId(directDealerId, 1);
//
// 严格按“分销设置 level”决定发放到第几级避免 level=2 时仍触发第3级发放逻辑。
int normalizedLevel = normalizeDealerLevel(dealerLevel);
Integer directDealerId = null;
Integer simpleDealerId = null;
Integer thirdDealerId = null;
if (normalizedLevel >= 1) {
directDealerId = getDealerRefereeId(order.getUserId(), 1);
}
Integer thirdDealerId = getDealerRefereeId(order.getUserId(), 3);
if (thirdDealerId == null && simpleDealerId != null) {
thirdDealerId = getDealerRefereeId(simpleDealerId, 1);
if (normalizedLevel >= 2) {
simpleDealerId = getDealerRefereeId(order.getUserId(), 2);
if (simpleDealerId == null && directDealerId != null) {
simpleDealerId = getDealerRefereeId(directDealerId, 1);
}
}
if (normalizedLevel >= 3) {
thirdDealerId = getDealerRefereeId(order.getUserId(), 3);
if (thirdDealerId == null && simpleDealerId != null) {
thirdDealerId = getDealerRefereeId(simpleDealerId, 1);
}
}
BigDecimal directMoney =
@@ -228,27 +257,33 @@ public class DealerOrderSettlement10584Task {
order.getOrderNo(), order.getUserId(), directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
// 直推:对方=买家;推荐奖(5%):对方=直推分销商(便于在资金明细中看出“来自哪个下级分销商/团队订单”)
creditDealerCommission(
directDealerId,
directMoney,
order,
order.getUserId(),
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
);
creditDealerCommission(
simpleDealerId,
simpleMoney,
order,
directDealerId,
buildCommissionComment("推荐奖", commissionConfig.commissionType, commissionConfig.dealerSimpleValue, goodsQty)
);
creditDealerCommission(
thirdDealerId,
thirdMoney,
order,
simpleDealerId,
buildCommissionComment("分润收入", commissionConfig.commissionType, commissionConfig.dealerThirdValue, goodsQty)
);
if (normalizedLevel >= 1) {
creditDealerCommission(
directDealerId,
directMoney,
order,
order.getUserId(),
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
);
}
if (normalizedLevel >= 2) {
creditDealerCommission(
simpleDealerId,
simpleMoney,
order,
directDealerId,
buildCommissionComment("推荐奖", commissionConfig.commissionType, commissionConfig.dealerSimpleValue, goodsQty)
);
}
if (normalizedLevel >= 3) {
creditDealerCommission(
thirdDealerId,
thirdMoney,
order,
simpleDealerId,
buildCommissionComment("分润收入", commissionConfig.commissionType, commissionConfig.dealerThirdValue, goodsQty)
);
}
return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
}
@@ -340,26 +375,30 @@ public class DealerOrderSettlement10584Task {
ShopOrder order,
BigDecimal baseAmount,
int goodsQty,
Integer totalDealerUserId
ShopDealerUser totalDealerUser
) {
if (totalDealerUserId == null) {
if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
return TotalDealerCommission.empty();
}
BigDecimal money = calcMoneyByCommissionType(baseAmount, TOTAL_DEALER_DIVIDEND_RATE, goodsQty, DIVIDEND_SCALE, 20);
BigDecimal rate = safePositive(totalDealerUser.getRate());
if (rate.signum() <= 0) {
rate = TOTAL_DEALER_DIVIDEND_RATE;
}
BigDecimal money = calcMoneyByCommissionType(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20);
log.info("总经销商分润发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}",
order.getOrderNo(), totalDealerUserId, TOTAL_DEALER_DIVIDEND_RATE, money);
order.getOrderNo(), totalDealerUser.getUserId(), rate, money);
creditDealerCommission(
totalDealerUserId,
totalDealerUser.getUserId(),
money,
order,
order.getUserId(),
buildCommissionComment("总经销商分润", 20, TOTAL_DEALER_DIVIDEND_RATE, goodsQty)
buildCommissionComment("总经销商分润", 20, rate, goodsQty)
);
return new TotalDealerCommission(totalDealerUserId, money);
return new TotalDealerCommission(totalDealerUser.getUserId(), money);
}
private Integer findTotalDealerUserId() {
ShopDealerUser dealerUser = shopDealerUserService.getOne(
private ShopDealerUser findTotalDealerUser() {
return shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getType, 2)
@@ -367,7 +406,46 @@ public class DealerOrderSettlement10584Task {
.orderByAsc(ShopDealerUser::getId)
.last("limit 1")
);
return dealerUser != null ? dealerUser.getUserId() : null;
}
private DealerBasicSetting findDealerBasicSetting() {
int level = 2;
ShopDealerSetting setting = shopDealerSettingService.getOne(
new LambdaQueryWrapper<ShopDealerSetting>()
.eq(ShopDealerSetting::getTenantId, TENANT_ID)
.eq(ShopDealerSetting::getKey, "basic")
.last("limit 1")
);
if (setting != null && setting.getValues() != null && !setting.getValues().isBlank()) {
try {
JSONObject json = JSONObject.parseObject(setting.getValues());
Integer levelVal = json.getInteger("level");
if (levelVal != null && levelVal > 0) {
level = Math.min(levelVal, 3);
}
} catch (Exception e) {
log.warn("解析分销设置失败,将使用默认等级 - tenantId={}, values={}", TENANT_ID, setting.getValues(), e);
}
}
return new DealerBasicSetting(level);
}
private int normalizeDealerLevel(int dealerLevel) {
if (dealerLevel <= 0) {
return 2;
}
return Math.min(dealerLevel, 3);
}
/**
* shop_dealer_setting(key=basic) 的关键配置(仅取结算任务需要的字段)。
*/
private static class DealerBasicSetting {
private final int level;
private DealerBasicSetting(int level) {
this.level = level;
}
}
/**

View File

@@ -265,7 +265,7 @@ public class ShopOrderController extends BaseController {
return fail("订单不存在");
}
// 退款相关操作单独走退款接口,便于做财务权限隔离
if (Objects.equals(shopOrder.getOrderStatus(), 4) || Objects.equals(shopOrder.getOrderStatus(), 6)) {
if (Objects.equals(shopOrder.getOrderStatus(), 6)) {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
}
ShopOrder shopOrderNow = shopOrderService.getById(shopOrder.getOrderId());