refactor(settlement): 优化经销商订单结算逻辑

- 移除不必要的UserRoleService依赖注入
- 将定时任务执行频率从每20秒调整为每30秒
- 删除过期的shopRoleCache缓存机制
- 重构推荐关系查询逻辑,支持多级关系和门店角色识别
- 添加对两种数据形态的兼容处理(level=1/2多级关系 vs 仅level=1关系)
- 修改佣金分配逻辑,区分直推佣金和推荐奖金
- 优化门店角色佣金计算,调整为直推2%和简推1%
- 添加分销商账户自动创建机制,确保佣金能够正常发放
- 更新资金明细记录,准确关联到对应的用户ID
- 新增自定义SQL查询方法,一次性获取推荐关系链路和门店角色信息
- 扩展ShopDealerReferee实体,增加isShopRole扩展字段
- 优化MyBatis XML映射,使用LEFT JOIN预加载角色信息避免N+1查询问题
This commit is contained in:
2026-01-26 00:14:34 +08:00
parent d15cc03e48
commit 918190148b
5 changed files with 190 additions and 51 deletions

View File

@@ -64,9 +64,13 @@ public class ShopDealerReferee implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private Boolean isAdmin; private Boolean isAdmin;
@Schema(description = "推荐关系层级(1,2,3)") @Schema(description = "推荐关系层级(弃用)")
private Integer level; private Integer level;
@Schema(description = "上级是否门店角色(shop)")
@TableField(exist = false)
private Boolean isShopRole;
@Schema(description = "来源(如 goods_share)") @Schema(description = "来源(如 goods_share)")
// NOTE: 表 shop_dealer_referee 若未新增该字段,需要 exist=false避免 MyBatis-Plus 自动生成SQL时报 Unknown column。 // NOTE: 表 shop_dealer_referee 若未新增该字段,需要 exist=false避免 MyBatis-Plus 自动生成SQL时报 Unknown column。
@TableField(exist = false) @TableField(exist = false)

View File

@@ -34,4 +34,22 @@ public interface ShopDealerRefereeMapper extends BaseMapper<ShopDealerReferee> {
*/ */
List<ShopDealerReferee> selectListRel(@Param("param") ShopDealerRefereeParam param); List<ShopDealerReferee> selectListRel(@Param("param") ShopDealerRefereeParam param);
/**
* 查询指定用户的推荐关系链路(按 level 升序),并计算每个上级是否包含门店角色(shop)。
* <p>
* 用于定时任务分佣:避免逐个查询 sys_user_role。
* <p>
* 注意对应XML里为了兼容 MyBatis-Plus 的 SQL 解析limit 使用字面量拼接(${limit}
* 这里的 limit 必须由服务端常量传入(不要透传用户输入)。
*/
List<ShopDealerReferee> selectRefereeChainWithShopRole(@Param("tenantId") Integer tenantId,
@Param("userId") Integer userId,
@Param("limit") Integer limit);
/**
* 查询指定用户的一级推荐人level=1并计算该推荐人是否包含门店角色(shop)。
*/
ShopDealerReferee selectFirstLevelRefereeWithShopRole(@Param("tenantId") Integer tenantId,
@Param("userId") Integer userId);
} }

View File

@@ -2,12 +2,27 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.shop.mapper.ShopDealerRefereeMapper"> <mapper namespace="com.gxwebsoft.shop.mapper.ShopDealerRefereeMapper">
<!--
MyBatis-Plus 3.4.x uses JSqlParser 3.2 to parse and rewrite SQL (e.g. for tenant/pagination).
JSqlParser 3.2 has limited MySQL expression support and may fail on constructs like:
- `EXISTS(subquery)` in SELECT items
- `MAX((cond) OR (cond))` in derived tables
This derived table keeps the SQL parse-friendly: one row per user_id that has role=shop.
-->
<sql id="shopRoleUserIds">
SELECT DISTINCT ur.user_id
FROM gxwebsoft_core.sys_user_role ur
LEFT JOIN gxwebsoft_core.sys_role r ON ur.role_id = r.role_id AND r.deleted = 0
WHERE (LOWER(TRIM(r.role_code)) = 'shop' OR LOWER(TRIM(r.role_name)) = 'shop')
</sql>
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT DISTINCT a.*, SELECT DISTINCT a.*,
d.nickname AS dealerName, d.nickname AS dealerName,
d.avatar AS dealerAvatar, d.avatar AS dealerAvatar,
d.phone AS dealerPhone, d.phone AS dealerPhone,
CASE WHEN sr.user_id IS NULL THEN 0 ELSE 1 END AS isShopRole,
u.nickname, u.nickname,
u.avatar, u.avatar,
u.alias, u.alias,
@@ -15,8 +30,14 @@
u.is_admin as isAdmin u.is_admin as isAdmin
FROM shop_dealer_referee a FROM shop_dealer_referee a
INNER JOIN gxwebsoft_core.sys_user d ON a.dealer_id = d.user_id AND d.deleted = 0 INNER JOIN gxwebsoft_core.sys_user d ON a.dealer_id = d.user_id AND d.deleted = 0
LEFT JOIN (
<include refid="shopRoleUserIds"/>
) sr ON sr.user_id = a.dealer_id
INNER JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id AND u.deleted = 0 INNER JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id AND u.deleted = 0
<where> <where>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.id != null"> <if test="param.id != null">
AND a.id = #{param.id} AND a.id = #{param.id}
</if> </if>
@@ -55,4 +76,46 @@
<include refid="selectSql"></include> <include refid="selectSql"></include>
</select> </select>
<!-- 定时任务:查某个用户的推荐关系链路(按 level 升序),并带出是否门店角色 -->
<select id="selectRefereeChainWithShopRole" resultType="com.gxwebsoft.shop.entity.ShopDealerReferee">
SELECT a.id,
a.dealer_id AS dealerId,
a.user_id AS userId,
a.level,
a.tenant_id AS tenantId,
CASE WHEN sr.user_id IS NULL THEN 0 ELSE 1 END AS isShopRole
FROM shop_dealer_referee a
LEFT JOIN (
<include refid="shopRoleUserIds"/>
) sr ON sr.user_id = a.dealer_id
WHERE a.tenant_id = #{tenantId}
AND a.user_id = #{userId}
ORDER BY a.level ASC, a.id DESC
<!--
MyBatis-Plus TenantLineInnerInterceptor uses JSqlParser to parse SQL.
Some versions can't parse "LIMIT ?" (prepared placeholder), so use a literal here.
`limit` is an Integer passed from server-side constants (no user input).
-->
LIMIT ${limit}
</select>
<!-- 定时任务:查某个用户的一级推荐关系(level=1),并带出是否门店角色 -->
<select id="selectFirstLevelRefereeWithShopRole" resultType="com.gxwebsoft.shop.entity.ShopDealerReferee">
SELECT a.id,
a.dealer_id AS dealerId,
a.user_id AS userId,
a.level,
a.tenant_id AS tenantId,
CASE WHEN sr.user_id IS NULL THEN 0 ELSE 1 END AS isShopRole
FROM shop_dealer_referee a
LEFT JOIN (
<include refid="shopRoleUserIds"/>
) sr ON sr.user_id = a.dealer_id
WHERE a.tenant_id = #{tenantId}
AND a.user_id = #{userId}
AND a.level = 1
ORDER BY a.id DESC
LIMIT 1
</select>
</mapper> </mapper>

View File

@@ -3,13 +3,12 @@ package com.gxwebsoft.shop.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.annotation.IgnoreTenant; import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.common.system.entity.Role;
import com.gxwebsoft.common.system.service.UserRoleService;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder; import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerReferee; import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.mapper.ShopDealerRefereeMapper;
import com.gxwebsoft.shop.service.ShopDealerCapitalService; import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerOrderService; import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerRefereeService; import com.gxwebsoft.shop.service.ShopDealerRefereeService;
@@ -65,12 +64,12 @@ public class DealerOrderSettlement10584Task {
private ShopDealerOrderService shopDealerOrderService; private ShopDealerOrderService shopDealerOrderService;
@Resource @Resource
private UserRoleService userRoleService; private ShopDealerRefereeMapper shopDealerRefereeMapper;
/** /**
* 每20秒执行一次。 * 每30秒执行一次。
*/ */
@Scheduled(cron = "0/20 * * * * ?") @Scheduled(cron = "0/30 * * * * ?")
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常") @IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584Orders() { public void settleTenant10584Orders() {
try { try {
@@ -84,9 +83,6 @@ public class DealerOrderSettlement10584Task {
orders.size(), orders.size(),
orders.stream().limit(10).map(ShopOrder::getOrderNo).toList()); orders.stream().limit(10).map(ShopOrder::getOrderNo).toList());
// 缓存:减少同一批次内重复查角色
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
for (ShopOrder order : orders) { for (ShopOrder order : orders) {
try { try {
transactionTemplate.executeWithoutResult(status -> { transactionTemplate.executeWithoutResult(status -> {
@@ -94,7 +90,7 @@ public class DealerOrderSettlement10584Task {
if (!claimOrderToSettle(order.getOrderId())) { if (!claimOrderToSettle(order.getOrderId())) {
return; return;
} }
settleOneOrder(order, shopRoleCache); settleOneOrder(order);
}); });
} catch (Exception e) { } catch (Exception e) {
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e); log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
@@ -127,7 +123,7 @@ public class DealerOrderSettlement10584Task {
); );
} }
private void settleOneOrder(ShopOrder order, Map<Integer, Boolean> shopRoleCache) { private void settleOneOrder(ShopOrder order) {
if (order.getUserId() == null || order.getOrderNo() == null) { if (order.getUserId() == null || order.getOrderNo() == null) {
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId()); throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
} }
@@ -144,7 +140,7 @@ public class DealerOrderSettlement10584Task {
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount); DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount);
// 2) 角色为shop的推荐人shop_dealer_referee 链路向上查找 role=shop 的上级) // 2) 角色为shop的推荐人shop_dealer_referee 链路向上查找 role=shop 的上级)
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, shopRoleCache); ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount);
// 3) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准) // 3) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission); createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission);
@@ -153,9 +149,14 @@ public class DealerOrderSettlement10584Task {
} }
private DealerRefereeCommission settleDealerRefereeCommission(ShopOrder order, BigDecimal baseAmount) { private DealerRefereeCommission settleDealerRefereeCommission(ShopOrder order, BigDecimal baseAmount) {
// level=2 可能不可靠:按“查两次”方式获取简推(直推的上级)。 // 兼容两种数据形态:
Integer directDealerId = getDealerRefereeId(order.getUserId()); // 1) 同一 userId 下有 level=1/2 的多级关系(直接按 level 取);
Integer simpleDealerId = directDealerId != null ? getDealerRefereeId(directDealerId) : null; // 2) 仅维护 level=1用“查两次”回退获取上级
Integer directDealerId = getDealerRefereeId(order.getUserId(), 1);
Integer simpleDealerId = getDealerRefereeId(order.getUserId(), 2);
if (simpleDealerId == null && directDealerId != null) {
simpleDealerId = getDealerRefereeId(directDealerId, 1);
}
BigDecimal directMoney = directDealerId != null ? calcMoney(baseAmount, RATE_0_10) : BigDecimal.ZERO; BigDecimal directMoney = directDealerId != null ? calcMoney(baseAmount, RATE_0_10) : BigDecimal.ZERO;
// 允许同一条线内同一个人同时拿到“直推 + 简推”(即使 directDealerId == simpleDealerId 也照常发放两笔) // 允许同一条线内同一个人同时拿到“直推 + 简推”(即使 directDealerId == simpleDealerId 也照常发放两笔)
@@ -164,13 +165,18 @@ public class DealerOrderSettlement10584Task {
log.info("分销直推/简推查询结果 - orderNo={}, buyerUserId={}, directDealerId={}, directMoney={}, simpleDealerId={}, simpleMoney={}", log.info("分销直推/简推查询结果 - orderNo={}, buyerUserId={}, directDealerId={}, directMoney={}, simpleDealerId={}, simpleMoney={}",
order.getOrderNo(), order.getUserId(), directDealerId, directMoney, simpleDealerId, simpleMoney); order.getOrderNo(), order.getUserId(), directDealerId, directMoney, simpleDealerId, simpleMoney);
creditDealerCommission(directDealerId, directMoney, order, "直推佣金(10%)"); // 直推:对方=买家;推荐奖(5%):对方=直推分销商(便于在资金明细中看出“来自哪个下级分销商/团队订单”)
creditDealerCommission(simpleDealerId, simpleMoney, order, "推佣金(5%)"); creditDealerCommission(directDealerId, directMoney, order, order.getUserId(), "推佣金(10%)");
creditDealerCommission(simpleDealerId, simpleMoney, order, directDealerId, "推荐奖(5%)");
return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney); return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney);
} }
private Integer getDealerRefereeId(Integer userId) { private Integer getDealerRefereeId(Integer userId) {
return getDealerRefereeId(userId, 1);
}
private Integer getDealerRefereeId(Integer userId, int level) {
if (userId == null) { if (userId == null) {
return null; return null;
} }
@@ -178,17 +184,17 @@ public class DealerOrderSettlement10584Task {
new LambdaQueryWrapper<ShopDealerReferee>() new LambdaQueryWrapper<ShopDealerReferee>()
.eq(ShopDealerReferee::getTenantId, TENANT_ID) .eq(ShopDealerReferee::getTenantId, TENANT_ID)
.eq(ShopDealerReferee::getUserId, userId) .eq(ShopDealerReferee::getUserId, userId)
.eq(ShopDealerReferee::getLevel, 1) .eq(ShopDealerReferee::getLevel, level)
.orderByDesc(ShopDealerReferee::getId) .orderByDesc(ShopDealerReferee::getId)
.last("limit 1") .last("limit 1")
); );
log.debug("shop_dealer_referee(level=1) 查询 - tenantId={}, userId={}, dealerId={}", log.debug("shop_dealer_referee(level={}) 查询 - tenantId={}, userId={}, dealerId={}",
TENANT_ID, userId, rel != null ? rel.getDealerId() : null); level, TENANT_ID, userId, rel != null ? rel.getDealerId() : null);
return rel != null ? rel.getDealerId() : null; return rel != null ? rel.getDealerId() : null;
} }
private ShopRoleCommission settleShopRoleRefereeCommission(ShopOrder order, BigDecimal baseAmount, Map<Integer, Boolean> shopRoleCache) { private ShopRoleCommission settleShopRoleRefereeCommission(ShopOrder order, BigDecimal baseAmount) {
List<Integer> shopRoleReferees = findFirstTwoShopRoleRefereesByDealerReferee(order.getUserId(), shopRoleCache); List<Integer> shopRoleReferees = findFirstTwoShopRoleRefereesByDealerReferee(order.getUserId());
log.info("门店(角色shop)上级链路结果 - orderNo={}, buyerUserId={}, shopRoleReferees={}", log.info("门店(角色shop)上级链路结果 - orderNo={}, buyerUserId={}, shopRoleReferees={}",
order.getOrderNo(), order.getUserId(), shopRoleReferees); order.getOrderNo(), order.getUserId(), shopRoleReferees);
if (shopRoleReferees.isEmpty()) { if (shopRoleReferees.isEmpty()) {
@@ -196,23 +202,24 @@ public class DealerOrderSettlement10584Task {
} }
if (shopRoleReferees.size() == 1) { if (shopRoleReferees.size() == 1) {
BigDecimal money = calcMoney(baseAmount, RATE_0_10); // 门店直推2%
BigDecimal money = calcMoney(baseAmount, RATE_0_02);
log.info("门店直推/简推发放(仅1人) - orderNo={}, storeDirectUserId={}, money={}", order.getOrderNo(), shopRoleReferees.get(0), money); log.info("门店直推/简推发放(仅1人) - orderNo={}, storeDirectUserId={}, money={}", order.getOrderNo(), shopRoleReferees.get(0), money);
creditDealerCommission(shopRoleReferees.get(0), money, order, "门店直推佣金(角色shop仅1人10%)"); creditDealerCommission(shopRoleReferees.get(0), money, order, order.getUserId(), "门店直推佣金(角色shop仅1人2%)");
return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO); return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO);
} }
// 两个或以上第一个0.02第二个0.08 // 两个或以上第一个0.02第二个0.01
BigDecimal storeDirectMoney = calcMoney(baseAmount, RATE_0_02); BigDecimal storeDirectMoney = calcMoney(baseAmount, RATE_0_02);
BigDecimal storeSimpleMoney = calcMoney(baseAmount, RATE_0_01); BigDecimal storeSimpleMoney = calcMoney(baseAmount, RATE_0_01);
log.info("门店直推/门店简推发放 - orderNo={}, storeDirectUserId={}, storeDirectMoney={}, storeSimpleUserId={}, storeSimpleMoney={}", log.info("门店直推/门店简推发放 - orderNo={}, storeDirectUserId={}, storeDirectMoney={}, storeSimpleUserId={}, storeSimpleMoney={}",
order.getOrderNo(), shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney); order.getOrderNo(), shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
creditDealerCommission(shopRoleReferees.get(0), storeDirectMoney, order, "门店直推佣金(角色shop第1个2%)"); creditDealerCommission(shopRoleReferees.get(0), storeDirectMoney, order, order.getUserId(), "门店直推佣金(角色shop第1个2%)");
creditDealerCommission(shopRoleReferees.get(1), storeSimpleMoney, order, "门店简推佣金(角色shop第2个1%)"); creditDealerCommission(shopRoleReferees.get(1), storeSimpleMoney, order, order.getUserId(), "门店简推佣金(角色shop第2个1%)");
return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney); return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
} }
private List<Integer> findFirstTwoShopRoleRefereesByDealerReferee(Integer buyerUserId, Map<Integer, Boolean> shopRoleCache) { private List<Integer> findFirstTwoShopRoleRefereesByDealerReferee(Integer buyerUserId) {
if (buyerUserId == null) { if (buyerUserId == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -220,9 +227,31 @@ public class DealerOrderSettlement10584Task {
List<Integer> result = new ArrayList<>(2); List<Integer> result = new ArrayList<>(2);
Set<Integer> visited = new HashSet<>(); Set<Integer> visited = new HashSet<>();
// 优先:直接从该买家(userId)的多级关系(level=1/2/3/...)里,按 level 升序找到最近的两级门店(角色shop)。
// 背景:部分数据只维护“买家 -> 上级们”的多级(level)记录,但不一定维护“上级 -> 更上级”的记录;
// 若仅做链路向上查找,会导致门店直推/简推无法命中,收益与资金明细都不会写入。
// 注意isShopRole 为扩展字段(非表字段)需用自定义SQL带出。
List<ShopDealerReferee> relList = shopDealerRefereeMapper.selectRefereeChainWithShopRole(
TENANT_ID, buyerUserId, MAX_REFEREE_CHAIN_DEPTH
);
for (ShopDealerReferee rel : relList) {
Integer dealerId = rel != null ? rel.getDealerId() : null;
if (dealerId == null || !visited.add(dealerId)) {
continue;
}
if (Boolean.TRUE.equals(rel.getIsShopRole())) {
result.add(dealerId);
if (result.size() >= 2) {
return result;
}
}
}
// 兜底:若只维护了 level=1 的推荐关系,则尝试按“查两次/多次”的方式一路向上找门店角色。
Integer current = buyerUserId; Integer current = buyerUserId;
for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null; i++) { for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null; i++) {
Integer parentId = getDealerRefereeId(current); ShopDealerReferee parentRel = shopDealerRefereeMapper.selectFirstLevelRefereeWithShopRole(TENANT_ID, current);
Integer parentId = parentRel != null ? parentRel.getDealerId() : null;
if (parentId == null) { if (parentId == null) {
break; break;
} }
@@ -233,7 +262,7 @@ public class DealerOrderSettlement10584Task {
break; break;
} }
if (hasShopRole(parentId, shopRoleCache)) { if (parentRel != null && Boolean.TRUE.equals(parentRel.getIsShopRole())) {
log.debug("门店(角色shop)命中 - parentId={}", parentId); log.debug("门店(角色shop)命中 - parentId={}", parentId);
result.add(parentId); result.add(parentId);
if (result.size() >= 2) { if (result.size() >= 2) {
@@ -247,23 +276,7 @@ public class DealerOrderSettlement10584Task {
return result; return result;
} }
private boolean hasShopRole(Integer userId, Map<Integer, Boolean> shopRoleCache) { private void creditDealerCommission(Integer dealerUserId, BigDecimal money, ShopOrder order, Integer toUserId, String comments) {
Boolean cached = shopRoleCache.get(userId);
if (cached != null) {
return cached;
}
List<Role> roles = userRoleService.listByUserId(userId);
boolean isShop = roles != null && roles.stream().anyMatch(r -> "shop".equalsIgnoreCase(r.getRoleCode()));
log.debug("角色判定 - userId={}, roles={}, isShop={}",
userId,
roles == null ? null : roles.stream().map(Role::getRoleCode).toList(),
isShop);
shopRoleCache.put(userId, isShop);
return isShop;
}
private void creditDealerCommission(Integer dealerUserId, BigDecimal money, ShopOrder order, String comments) {
if (dealerUserId == null || money == null || money.signum() <= 0) { if (dealerUserId == null || money == null || money.signum() <= 0) {
return; return;
} }
@@ -280,9 +293,48 @@ public class DealerOrderSettlement10584Task {
); );
if (!updated) { if (!updated) {
log.warn("发放佣金失败:未找到分销账户 - tenantId={}, dealerUserId={}, orderNo={}", TENANT_ID, dealerUserId, order.getOrderNo()); // 门店角色用户可能未开通分销账户:此时门店直推/简推会“找到了人但入不了账”,收益与明细都不会写入。
// 这里补偿创建账户后再尝试入账一次。
ShopDealerUser existed = shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, dealerUserId)
.last("limit 1")
);
if (existed == null) {
ShopDealerUser newDealerUser = new ShopDealerUser();
newDealerUser.setTenantId(TENANT_ID);
newDealerUser.setUserId(dealerUserId);
newDealerUser.setType(0);
newDealerUser.setIsDelete(0);
newDealerUser.setSortNumber(0);
newDealerUser.setFirstNum(0);
newDealerUser.setSecondNum(0);
newDealerUser.setThirdNum(0);
newDealerUser.setMoney(BigDecimal.ZERO);
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
newDealerUser.setTotalMoney(BigDecimal.ZERO);
newDealerUser.setCreateTime(java.time.LocalDateTime.now());
newDealerUser.setUpdateTime(java.time.LocalDateTime.now());
try {
shopDealerUserService.save(newDealerUser);
} catch (Exception ignore) {
// 并发下可能已被其他线程/实例创建,忽略后继续重试入账。
}
}
updated = shopDealerUserService.update(
new LambdaUpdateWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, dealerUserId)
.setSql("money = IFNULL(money,0) + " + money.toPlainString())
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
);
if (!updated) {
log.warn("发放佣金失败:未找到/创建分销商账户 - tenantId={}, dealerUserId={}, orderNo={}", TENANT_ID, dealerUserId, order.getOrderNo());
return; return;
} }
}
ShopDealerCapital cap = new ShopDealerCapital(); ShopDealerCapital cap = new ShopDealerCapital();
cap.setUserId(dealerUserId); cap.setUserId(dealerUserId);
@@ -290,7 +342,7 @@ public class DealerOrderSettlement10584Task {
cap.setFlowType(10); cap.setFlowType(10);
cap.setMoney(money); cap.setMoney(money);
cap.setComments(comments); cap.setComments(comments);
cap.setToUserId(order.getUserId()); cap.setToUserId(toUserId);
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"))); cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
cap.setTenantId(TENANT_ID); cap.setTenantId(TENANT_ID);
shopDealerCapitalService.save(cap); shopDealerCapitalService.save(cap);
@@ -332,6 +384,8 @@ public class DealerOrderSettlement10584Task {
dealerOrder.setIsSettled(1); dealerOrder.setIsSettled(1);
dealerOrder.setSettleTime(java.time.LocalDateTime.now()); dealerOrder.setSettleTime(java.time.LocalDateTime.now());
// ShopDealerCapital 关联查询会拿 shop_dealer_order.month覆盖 a.month这里需要同步填充。
dealerOrder.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
dealerOrder.setTenantId(TENANT_ID); dealerOrder.setTenantId(TENANT_ID);
dealerOrder.setComments(buildCommissionTraceComment(dealerRefereeCommission, shopRoleCommission)); dealerOrder.setComments(buildCommissionTraceComment(dealerRefereeCommission, shopRoleCommission));