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:
@@ -16,4 +16,4 @@ public class WebSocketConfig {
|
|||||||
|
|
||||||
return exporter;
|
return exporter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,8 +293,47 @@ public class DealerOrderSettlement10584Task {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
log.warn("发放佣金失败:未找到分销商账户 - tenantId={}, dealerUserId={}, orderNo={}", TENANT_ID, dealerUserId, order.getOrderNo());
|
// 门店角色用户可能未开通分销账户:此时门店直推/简推会“找到了人但入不了账”,收益与明细都不会写入。
|
||||||
return;
|
// 这里补偿创建账户后再尝试入账一次。
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShopDealerCapital cap = new ShopDealerCapital();
|
ShopDealerCapital cap = new ShopDealerCapital();
|
||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user