refactor(task): 优化门店分佣逻辑按type字段判定
- 替换原有的角色shop判定逻辑,改为按ShopDealerUser.type=1判定门店分红用户 - 新增User和UserMapper依赖注入用于获取用户基础信息 - 添加幂等检查机制避免同一订单重复发放佣金 - 在创建分销商账户时补充基础信息防止字段约束导致插入失败 - 优化SQL查询逻辑,直接关联shop_dealer_user表而非查询系统角色表 - 更新日志信息中的描述文字以反映新的判定方式 - 添加对已存在订单记录的回填功能,支持补发门店分佣时更新分红字段
This commit is contained in:
@@ -67,7 +67,7 @@ public class ShopDealerReferee implements Serializable {
|
||||
@Schema(description = "推荐关系层级(弃用)")
|
||||
private Integer level;
|
||||
|
||||
@Schema(description = "上级是否门店角色(shop)")
|
||||
@Schema(description = "上级是否门店分红用户(ShopDealerUser.type=1;扩展字段)")
|
||||
@TableField(exist = false)
|
||||
private Boolean isShopRole;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public interface ShopDealerRefereeMapper extends BaseMapper<ShopDealerReferee> {
|
||||
/**
|
||||
* 查询指定用户的推荐关系链路(按 level 升序),并计算每个上级是否包含门店角色(shop)。
|
||||
* <p>
|
||||
* 用于定时任务分佣:避免逐个查询 sys_user_role。
|
||||
* 用于定时任务分佣:按 ShopDealerUser.type=1 判定“门店分红”上级,避免查询 core 的 sys_user_role。
|
||||
* <p>
|
||||
* 注意:对应XML里为了兼容 MyBatis-Plus 的 SQL 解析,limit 使用字面量拼接(${limit}),
|
||||
* 这里的 limit 必须由服务端常量传入(不要透传用户输入)。
|
||||
@@ -47,7 +47,7 @@ public interface ShopDealerRefereeMapper extends BaseMapper<ShopDealerReferee> {
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 查询指定用户的一级推荐人(level=1),并计算该推荐人是否包含门店角色(shop)。
|
||||
* 查询指定用户的一级推荐人(level=1),并按 ShopDealerUser.type=1 判定该推荐人是否为“门店分红”上级。
|
||||
*/
|
||||
ShopDealerReferee selectFirstLevelRefereeWithShopRole(@Param("tenantId") Integer tenantId,
|
||||
@Param("userId") Integer userId);
|
||||
|
||||
@@ -2,27 +2,13 @@
|
||||
<!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">
|
||||
|
||||
<!--
|
||||
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 id="selectSql">
|
||||
SELECT DISTINCT a.*,
|
||||
d.nickname AS dealerName,
|
||||
d.avatar AS dealerAvatar,
|
||||
d.phone AS dealerPhone,
|
||||
CASE WHEN sr.user_id IS NULL THEN 0 ELSE 1 END AS isShopRole,
|
||||
CASE WHEN du.user_id IS NULL THEN 0 ELSE 1 END AS isShopRole,
|
||||
u.nickname,
|
||||
u.avatar,
|
||||
u.alias,
|
||||
@@ -30,9 +16,11 @@
|
||||
u.is_admin as isAdmin
|
||||
FROM shop_dealer_referee a
|
||||
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
|
||||
LEFT JOIN shop_dealer_user du
|
||||
ON du.user_id = a.dealer_id
|
||||
AND du.tenant_id = a.tenant_id
|
||||
AND du.type = 1
|
||||
AND (du.is_delete = 0 OR du.is_delete IS NULL)
|
||||
INNER JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id AND u.deleted = 0
|
||||
<where>
|
||||
<if test="param.tenantId != null">
|
||||
@@ -83,11 +71,13 @@
|
||||
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
|
||||
CASE WHEN du.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
|
||||
LEFT JOIN shop_dealer_user du
|
||||
ON du.user_id = a.dealer_id
|
||||
AND du.tenant_id = a.tenant_id
|
||||
AND du.type = 1
|
||||
AND (du.is_delete = 0 OR du.is_delete IS NULL)
|
||||
WHERE a.tenant_id = #{tenantId}
|
||||
AND a.user_id = #{userId}
|
||||
ORDER BY a.level ASC, a.id DESC
|
||||
@@ -106,11 +96,13 @@
|
||||
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
|
||||
CASE WHEN du.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
|
||||
LEFT JOIN shop_dealer_user du
|
||||
ON du.user_id = a.dealer_id
|
||||
AND du.tenant_id = a.tenant_id
|
||||
AND du.type = 1
|
||||
AND (du.is_delete = 0 OR du.is_delete IS NULL)
|
||||
WHERE a.tenant_id = #{tenantId}
|
||||
AND a.user_id = #{userId}
|
||||
AND a.level = 1
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.gxwebsoft.shop.entity.ShopDealerOrder;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerReferee;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerUser;
|
||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||
import com.gxwebsoft.shop.mapper.ShopDealerRefereeMapper;
|
||||
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
|
||||
import com.gxwebsoft.shop.service.ShopDealerOrderService;
|
||||
@@ -66,6 +68,9 @@ public class DealerOrderSettlement10584Task {
|
||||
@Resource
|
||||
private ShopDealerRefereeMapper shopDealerRefereeMapper;
|
||||
|
||||
@Resource
|
||||
private UserMapper userMapper;
|
||||
|
||||
/**
|
||||
* 每30秒执行一次。
|
||||
*/
|
||||
@@ -139,7 +144,7 @@ public class DealerOrderSettlement10584Task {
|
||||
// 1) 直推/简推(shop_dealer_referee)
|
||||
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount);
|
||||
|
||||
// 2) 角色为shop的推荐人(shop_dealer_referee 链路向上查找 role=shop 的上级)
|
||||
// 2) 门店分红上级(按 ShopDealerUser.type=1,在 shop_dealer_referee 链路向上查找)
|
||||
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount);
|
||||
|
||||
// 3) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
|
||||
@@ -195,7 +200,7 @@ public class DealerOrderSettlement10584Task {
|
||||
|
||||
private ShopRoleCommission settleShopRoleRefereeCommission(ShopOrder order, BigDecimal baseAmount) {
|
||||
List<Integer> shopRoleReferees = findFirstTwoShopRoleRefereesByDealerReferee(order.getUserId());
|
||||
log.info("门店(角色shop)上级链路结果 - orderNo={}, buyerUserId={}, shopRoleReferees={}",
|
||||
log.info("门店(type=1)上级链路结果 - orderNo={}, buyerUserId={}, shopRoleReferees={}",
|
||||
order.getOrderNo(), order.getUserId(), shopRoleReferees);
|
||||
if (shopRoleReferees.isEmpty()) {
|
||||
return ShopRoleCommission.empty();
|
||||
@@ -205,7 +210,7 @@ public class DealerOrderSettlement10584Task {
|
||||
// 门店直推:2%
|
||||
BigDecimal money = calcMoney(baseAmount, RATE_0_02);
|
||||
log.info("门店直推/简推发放(仅1人) - orderNo={}, storeDirectUserId={}, money={}", order.getOrderNo(), shopRoleReferees.get(0), money);
|
||||
creditDealerCommission(shopRoleReferees.get(0), money, order, order.getUserId(), "门店直推佣金(角色shop,仅1人,2%)");
|
||||
creditDealerCommission(shopRoleReferees.get(0), money, order, order.getUserId(), "门店直推佣金(type=1,仅1人,2%)");
|
||||
return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@@ -214,8 +219,8 @@ public class DealerOrderSettlement10584Task {
|
||||
BigDecimal storeSimpleMoney = calcMoney(baseAmount, RATE_0_01);
|
||||
log.info("门店直推/门店简推发放 - orderNo={}, storeDirectUserId={}, storeDirectMoney={}, storeSimpleUserId={}, storeSimpleMoney={}",
|
||||
order.getOrderNo(), shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
|
||||
creditDealerCommission(shopRoleReferees.get(0), storeDirectMoney, order, order.getUserId(), "门店直推佣金(角色shop,第1个,2%)");
|
||||
creditDealerCommission(shopRoleReferees.get(1), storeSimpleMoney, order, order.getUserId(), "门店简推佣金(角色shop,第2个,1%)");
|
||||
creditDealerCommission(shopRoleReferees.get(0), storeDirectMoney, order, order.getUserId(), "门店直推佣金(type=1,第1个,2%)");
|
||||
creditDealerCommission(shopRoleReferees.get(1), storeSimpleMoney, order, order.getUserId(), "门店简推佣金(type=1,第2个,1%)");
|
||||
return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
|
||||
}
|
||||
|
||||
@@ -227,10 +232,10 @@ public class DealerOrderSettlement10584Task {
|
||||
List<Integer> result = new ArrayList<>(2);
|
||||
Set<Integer> visited = new HashSet<>();
|
||||
|
||||
// 优先:直接从该买家(userId)的多级关系(level=1/2/3/...)里,按 level 升序找到最近的两级门店(角色shop)。
|
||||
// 优先:直接从该买家(userId)的多级关系(level=1/2/3/...)里,按 level 升序找到最近的两级门店(type=1)。
|
||||
// 背景:部分数据只维护“买家 -> 上级们”的多级(level)记录,但不一定维护“上级 -> 更上级”的记录;
|
||||
// 若仅做链路向上查找,会导致门店直推/简推无法命中,收益与资金明细都不会写入。
|
||||
// 注意:isShopRole 为扩展字段(非表字段),需用自定义SQL带出。
|
||||
// 注意:isShopRole 为扩展字段(非表字段),由自定义SQL按 ShopDealerUser.type=1 计算带出。
|
||||
List<ShopDealerReferee> relList = shopDealerRefereeMapper.selectRefereeChainWithShopRole(
|
||||
TENANT_ID, buyerUserId, MAX_REFEREE_CHAIN_DEPTH
|
||||
);
|
||||
@@ -247,7 +252,7 @@ public class DealerOrderSettlement10584Task {
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:若只维护了 level=1 的推荐关系,则尝试按“查两次/多次”的方式一路向上找门店角色。
|
||||
// 兜底:若只维护了 level=1 的推荐关系,则尝试按“查两次/多次”的方式一路向上找门店(type=1)。
|
||||
Integer current = buyerUserId;
|
||||
for (int i = 0; i < MAX_REFEREE_CHAIN_DEPTH && current != null; i++) {
|
||||
ShopDealerReferee parentRel = shopDealerRefereeMapper.selectFirstLevelRefereeWithShopRole(TENANT_ID, current);
|
||||
@@ -256,14 +261,14 @@ public class DealerOrderSettlement10584Task {
|
||||
break;
|
||||
}
|
||||
|
||||
log.debug("门店(角色shop)链路向上 - buyerOrChildUserId={}, parentId={}", current, parentId);
|
||||
log.debug("门店(type=1)链路向上 - buyerOrChildUserId={}, parentId={}", current, parentId);
|
||||
|
||||
if (!visited.add(parentId)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (parentRel != null && Boolean.TRUE.equals(parentRel.getIsShopRole())) {
|
||||
log.debug("门店(角色shop)命中 - parentId={}", parentId);
|
||||
log.debug("门店(type=1)命中 - parentId={}", parentId);
|
||||
result.add(parentId);
|
||||
if (result.size() >= 2) {
|
||||
break;
|
||||
@@ -281,6 +286,28 @@ public class DealerOrderSettlement10584Task {
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等:同一订单同一类型佣金,避免重复发放(用于任务重跑/补发场景)
|
||||
LambdaQueryWrapper<ShopDealerCapital> idempotentQw = new LambdaQueryWrapper<ShopDealerCapital>()
|
||||
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||
.eq(ShopDealerCapital::getOrderNo, order.getOrderNo())
|
||||
.eq(ShopDealerCapital::getUserId, dealerUserId);
|
||||
if (comments != null) {
|
||||
// 以“佣金类型前缀”做幂等键,避免 comments 细节文案调整导致重复发放。
|
||||
String commentPrefix = comments;
|
||||
int idx = comments.indexOf('(');
|
||||
if (idx > 0) {
|
||||
commentPrefix = comments.substring(0, idx) + "(";
|
||||
}
|
||||
idempotentQw.likeRight(ShopDealerCapital::getComments, commentPrefix);
|
||||
} else {
|
||||
idempotentQw.isNull(ShopDealerCapital::getComments);
|
||||
}
|
||||
boolean alreadyCredited = shopDealerCapitalService.count(idempotentQw) > 0;
|
||||
if (alreadyCredited) {
|
||||
log.info("佣金已入账,跳过 - orderNo={}, toDealerUserId={}, money={}, comments={}", order.getOrderNo(), dealerUserId, money, comments);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("佣金入账 - orderNo={}, toDealerUserId={}, money={}, comments={}", order.getOrderNo(), dealerUserId, money, comments);
|
||||
|
||||
// 先累加佣金到分销商账户(避免并发下丢失更新,用SQL自增)
|
||||
@@ -314,12 +341,25 @@ public class DealerOrderSettlement10584Task {
|
||||
newDealerUser.setMoney(BigDecimal.ZERO);
|
||||
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
|
||||
newDealerUser.setTotalMoney(BigDecimal.ZERO);
|
||||
// 尽量补齐基础信息,避免表字段 NOT NULL 导致插入失败(插入失败会让门店分佣“找到了人但入不了账”)。
|
||||
try {
|
||||
User sysUser = userMapper.selectByIdIgnoreTenant(dealerUserId);
|
||||
if (sysUser != null) {
|
||||
newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname());
|
||||
newDealerUser.setMobile(sysUser.getPhone());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 拉取基础信息失败不应阻断结算;后续 update 若失败会有 warn 日志。
|
||||
log.warn("创建分销商账户时读取sys_user失败 - tenantId={}, dealerUserId={}", TENANT_ID, dealerUserId, e);
|
||||
}
|
||||
newDealerUser.setCreateTime(java.time.LocalDateTime.now());
|
||||
newDealerUser.setUpdateTime(java.time.LocalDateTime.now());
|
||||
try {
|
||||
shopDealerUserService.save(newDealerUser);
|
||||
} catch (Exception ignore) {
|
||||
// 并发下可能已被其他线程/实例创建,忽略后继续重试入账。
|
||||
} catch (Exception e) {
|
||||
// 并发下可能已被其他线程/实例创建;也可能是字段约束导致插入失败。
|
||||
// 继续重试 update,若仍失败会输出 warn,便于定位原因。
|
||||
log.warn("创建分销商账户失败 - tenantId={}, dealerUserId={}, orderNo={}", TENANT_ID, dealerUserId, order.getOrderNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,13 +395,35 @@ public class DealerOrderSettlement10584Task {
|
||||
ShopRoleCommission shopRoleCommission
|
||||
) {
|
||||
// 幂等:同一订单只写一条(依赖 order_no + tenant_id 作为业务唯一)
|
||||
boolean exists = shopDealerOrderService.count(
|
||||
ShopDealerOrder existed = shopDealerOrderService.getOne(
|
||||
new LambdaQueryWrapper<ShopDealerOrder>()
|
||||
.eq(ShopDealerOrder::getTenantId, TENANT_ID)
|
||||
.eq(ShopDealerOrder::getOrderNo, order.getOrderNo())
|
||||
) > 0;
|
||||
if (exists) {
|
||||
log.info("ShopDealerOrder已存在,跳过写入 - orderNo={}", order.getOrderNo());
|
||||
.last("limit 1")
|
||||
);
|
||||
if (existed != null) {
|
||||
// 允许“补发”门店分佣时回填分红字段,避免订单已结算但分红字段一直为空,影响排查/对账。
|
||||
LambdaUpdateWrapper<ShopDealerOrder> uw = new LambdaUpdateWrapper<ShopDealerOrder>()
|
||||
.eq(ShopDealerOrder::getTenantId, TENANT_ID)
|
||||
.eq(ShopDealerOrder::getOrderNo, order.getOrderNo());
|
||||
boolean needUpdate = false;
|
||||
if (existed.getFirstDividendUser() == null && shopRoleCommission.storeDirectUserId != null) {
|
||||
uw.set(ShopDealerOrder::getFirstDividendUser, shopRoleCommission.storeDirectUserId);
|
||||
uw.set(ShopDealerOrder::getFirstDividend, shopRoleCommission.storeDirectMoney);
|
||||
needUpdate = true;
|
||||
}
|
||||
if (existed.getSecondDividendUser() == null && shopRoleCommission.storeSimpleUserId != null) {
|
||||
uw.set(ShopDealerOrder::getSecondDividendUser, shopRoleCommission.storeSimpleUserId);
|
||||
uw.set(ShopDealerOrder::getSecondDividend, shopRoleCommission.storeSimpleMoney);
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate) {
|
||||
shopDealerOrderService.update(uw);
|
||||
log.info("ShopDealerOrder已存在,回填门店分红字段 - orderNo={}, firstDividendUser={}, secondDividendUser={}",
|
||||
order.getOrderNo(), shopRoleCommission.storeDirectUserId, shopRoleCommission.storeSimpleUserId);
|
||||
} else {
|
||||
log.info("ShopDealerOrder已存在,跳过写入 - orderNo={}", order.getOrderNo());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user