diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json
index 9071aad..c0f2131 100644
--- a/.workbuddy/expert-history.json
+++ b/.workbuddy/expert-history.json
@@ -44,7 +44,18 @@
"usedAt": 1776443595917,
"industryId": "02-Engineering"
}
+ ],
+ "e339ec20b1ef45479756bdfdf93c3654": [
+ {
+ "expertId": "SeniorDeveloper",
+ "name": "吴八哥",
+ "profession": "高级开发工程师",
+ "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
+ "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
+ "usedAt": 1776696820692,
+ "industryId": "02-Engineering"
+ }
]
},
- "lastUpdated": 1776444657438
+ "lastUpdated": 1776699418893
}
\ No newline at end of file
diff --git a/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java b/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java
index 1791b86..41ae77b 100644
--- a/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java
+++ b/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java
@@ -33,6 +33,7 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -48,7 +49,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*
1) 送水套餐(formId in 水票模板 goodsId):订单号关联的水票产生了第一次送水订单,且该第一次送水订单状态=已完成(40) -> 解冻。
* 2) 非送水套餐(formId not in 水票模板 goodsId):订单已确认收货(orderStatus=1) -> 解冻。
*
- * 实现策略:以 ShopDealerCapital(flowType=10) 的“佣金明细”为解冻粒度,
+ *
实现策略:以 ShopDealerCapital(flowType=10) 的"佣金明细"为解冻粒度,
* 每条佣金明细对应生成一条 ShopDealerCapital(flowType=50) 作为幂等标记,并执行
* ShopDealerUser.freezeMoney -> ShopDealerUser.money 的转移。
*/
@@ -76,7 +77,7 @@ public class DealerCommissionUnfreeze10584Task {
if (rawRate == null || rawRate.signum() <= 0) {
return null;
}
- // 如果录入 >= 1,按“百分比”处理(1 => 1%)
+ // 如果录入 >= 1,按"百分比"处理(1 => 1%)
if (rawRate.compareTo(BigDecimal.ONE) >= 0) {
return rawRate.movePointLeft(2);
}
@@ -115,7 +116,7 @@ public class DealerCommissionUnfreeze10584Task {
private final AtomicBoolean running = new AtomicBoolean(false);
- @Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/20 * * * * ?}")
+ @Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/50 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() {
if (!running.compareAndSet(false, true)) {
@@ -124,46 +125,77 @@ public class DealerCommissionUnfreeze10584Task {
}
try {
+ // ========== 步骤1: 加载水票模板 ==========
Set waterFormIds = loadWaterFormIds();
+ log.info("【步骤1】加载水票模板 - tenantId={}, waterFormIds={}", TENANT_ID, waterFormIds);
if (waterFormIds.isEmpty()) {
// 送水/非送水的判断依赖模板 goodsId;拿不到会导致误判,宁可跳过本轮。
log.warn("分销佣金解冻任务跳过:未找到水票模板 goodsId - tenantId={}", TENANT_ID);
return;
}
- // 先按“最近确认收货”的订单扫描,避免总是卡在很早的历史订单上。
+ // ========== 步骤2: 扫描非送水订单(优先最新) ==========
Set eligibleOrderNos = new HashSet<>();
- eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, true));
- eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
+ List nonWaterOrders = findEligibleNonWaterOrderNos(waterFormIds, true);
+ log.info("【步骤2】扫描非送水订单(最新优先)- tenantId={}, count={}, orderNos={}", TENANT_ID, nonWaterOrders.size(), nonWaterOrders.size() <= 20 ? nonWaterOrders : nonWaterOrders.subList(0, 20));
+ eligibleOrderNos.addAll(nonWaterOrders);
+
+ // ========== 步骤3: 扫描送水订单(第一条送水完成) ==========
+ Set waterOrders = findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds);
+ log.info("【步骤3】扫描送水订单(第一条送水完成)- tenantId={}, count={}, orderNos={}", TENANT_ID, waterOrders.size(), waterOrders);
+ eligibleOrderNos.addAll(waterOrders);
if (eligibleOrderNos.isEmpty()) {
+ log.info("【步骤4-9】无可处理订单,本轮结束 - tenantId={}", TENANT_ID);
return;
}
+ // 订单太多时不打印完整列表
+ String orderNosSummary = eligibleOrderNos.size() <= 30 ? eligibleOrderNos.toString() : eligibleOrderNos.size() + " orders (too many to show)";
+ log.info("【步骤4】汇总待处理订单 - tenantId={}, totalCount={}, orderNos={}", TENANT_ID, eligibleOrderNos.size(), orderNosSummary);
- // 配送奖励(与佣金解冻独立):按订单发放,幂等保证不会重复入账
+ // ========== 步骤5: 发放配送奖励 ==========
+ log.info("【步骤5】开始发放配送奖励 - tenantId={}, orderCount={}", TENANT_ID, eligibleOrderNos.size());
int rewarded = 0;
+ List rewardedOrders = new ArrayList<>();
for (String orderNo : eligibleOrderNos) {
try {
if (settleDeliveryRewardIfNeeded(orderNo)) {
rewarded++;
+ rewardedOrders.add(orderNo);
}
} catch (Exception e) {
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
}
}
+ log.info("【步骤5】配送奖励发放完成 - tenantId={}, rewardedCount={}, rewardedOrders={}", TENANT_ID, rewarded, rewardedOrders);
+ // ========== 步骤6: 查询佣金明细 ==========
+ log.info("【步骤6】查询佣金明细(flowType=10)- tenantId={}, orderCount={}", TENANT_ID, eligibleOrderNos.size());
List capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
+ String capitalsSummary = capitals.size() <= 50
+ ? capitals.stream().map(c -> "capitalId=" + c.getId() + ", orderNo=" + c.getOrderNo() + ", amount=" + c.getMoney()).toList().toString()
+ : capitals.size() + " capitals (too many to show)";
+ log.info("【步骤6】查询到佣金明细 - tenantId={}, count={}, capitals={}", TENANT_ID, capitals.size(), capitalsSummary);
+
if (capitals.isEmpty()) {
- // 若本轮没有取到佣金明细,回退再按“最早确认收货”的订单扫一轮,尽量覆盖历史遗留未解冻。
+ // ========== 步骤6.1: 兜底扫描历史订单 ==========
+ log.info("【步骤6.1】本轮未取到佣金明细,执行兜底扫描(最早确认收货) - tenantId={}", TENANT_ID);
eligibleOrderNos.clear();
- eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, false));
- eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
+ List fallbackNonWater = findEligibleNonWaterOrderNos(waterFormIds, false);
+ log.info("【步骤6.1】兜底-非送水订单 - tenantId={}, count={}", TENANT_ID, fallbackNonWater.size());
+ eligibleOrderNos.addAll(fallbackNonWater);
+
+ Set fallbackWater = findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds);
+ log.info("【步骤6.1】兜底-送水订单 - tenantId={}, count={}", TENANT_ID, fallbackWater.size());
+ eligibleOrderNos.addAll(fallbackWater);
// 兜底扫描出来的订单也补发配送奖励(幂等)
+ log.info("【步骤6.1】兜底扫描后补发配送奖励 - tenantId={}, orderCount={}", TENANT_ID, eligibleOrderNos.size());
for (String orderNo : eligibleOrderNos) {
try {
if (settleDeliveryRewardIfNeeded(orderNo)) {
rewarded++;
+ rewardedOrders.add(orderNo + "(兜底)");
}
} catch (Exception e) {
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
@@ -171,32 +203,41 @@ public class DealerCommissionUnfreeze10584Task {
}
capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
+ log.info("【步骤6.1】兜底扫描到佣金明细 - tenantId={}, count={}", TENANT_ID, capitals.size());
}
if (capitals.isEmpty()) {
+ log.info("【步骤7-9】仍未查到佣金明细,本轮结束 - tenantId={}", TENANT_ID);
return;
}
+ // ========== 步骤7: 执行佣金解冻 ==========
+ log.info("【步骤7】开始执行佣金解冻 - tenantId={}, totalCount={}", TENANT_ID, capitals.size());
int unfrozen = 0;
+ List unfrozenDetails = new ArrayList<>();
for (ShopDealerCapital cap : capitals) {
try {
boolean ok = unfreezeOneCapitalIfNeeded(cap);
if (ok) {
unfrozen++;
+ unfrozenDetails.add("capitalId=" + cap.getId() + ", orderNo=" + cap.getOrderNo() + ", amount=" + cap.getMoney());
}
} catch (Exception e) {
log.error("解冻佣金失败,将在下次任务重试 - tenantId={}, capitalId={}, orderNo={}, userId={}",
TENANT_ID, cap != null ? cap.getId() : null, cap != null ? cap.getOrderNo() : null, cap != null ? cap.getUserId() : null, e);
}
}
+ log.info("【步骤7】佣金解冻完成 - tenantId={}, unfrozenDetails={}", TENANT_ID, unfrozenDetails);
- if (unfrozen > 0) {
- log.info("分销佣金解冻完成 - tenantId={}, eligibleOrderNos={}, scannedCapitals={}, unfrozen={}",
- TENANT_ID, eligibleOrderNos.size(), capitals.size(), unfrozen);
- }
- if (rewarded > 0) {
- log.info("配送奖励发放完成 - tenantId={}, eligibleOrderNos={}, rewarded={}", TENANT_ID, eligibleOrderNos.size(), rewarded);
- }
+ // ========== 步骤8: 更新分销订单状态 ==========
+ log.info("【步骤8】检查并更新分销订单状态(isUnfreeze=1)- tenantId={}, unfrozenCount={}", TENANT_ID, unfrozen);
+
+ // ========== 步骤9: 汇总报告 ==========
+ log.info("========================================");
+ log.info("【步骤9】本轮任务执行完毕 - tenantId={}", TENANT_ID);
+ log.info(" - 配送奖励: rewarded={}, orders={}", rewarded, rewardedOrders);
+ log.info(" - 佣金解冻: unfrozen={}, details={}", unfrozen, unfrozenDetails);
+ log.info("========================================");
} finally {
running.set(false);
}
@@ -204,8 +245,10 @@ public class DealerCommissionUnfreeze10584Task {
private boolean settleDeliveryRewardIfNeeded(String orderNo) {
if (orderNo == null || orderNo.isBlank()) {
+ log.debug("【步骤5.X】配送奖励跳过:orderNo为空 - tenantId={}", TENANT_ID);
return false;
}
+ log.debug("【步骤5.X】开始处理配送奖励 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper()
@@ -215,13 +258,16 @@ public class DealerCommissionUnfreeze10584Task {
.last("limit 1")
);
if (order == null) {
+ log.debug("【步骤5.X】配送奖励跳过:订单不存在 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
return false;
}
Integer riderId = order.getRiderId();
if (riderId == null || riderId <= 0) {
+ log.debug("【步骤5.X】配送奖励跳过:无配送员 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
return false;
}
+ log.debug("【步骤5.X】找到配送员 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
// 快速幂等检查:已发放则跳过(事务内仍会二次校验避免并发重复)
boolean already = shopDealerCapitalService.count(
@@ -231,147 +277,162 @@ public class DealerCommissionUnfreeze10584Task {
.eq(ShopDealerCapital::getOrderNo, orderNo)
) > 0;
if (already) {
+ log.debug("【步骤5.X】配送奖励跳过:已发放(幂等)- tenantId={}, orderNo={}", TENANT_ID, orderNo);
return false;
}
+ log.debug("【步骤5.X】进入事务 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
- return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
- LocalDateTime now = LocalDateTime.now();
+ try {
+ return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
+ LocalDateTime now = LocalDateTime.now();
+ log.debug("【步骤5.X】事务内开始 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
- // 锁定配送员资金明细 marker,确保并发幂等
- ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
- new LambdaQueryWrapper()
- .eq(ShopDealerCapital::getTenantId, TENANT_ID)
- .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
- .eq(ShopDealerCapital::getOrderNo, orderNo)
- .last("limit 1 for update")
- );
- if (existedMarker != null) {
- return false;
- }
-
- Integer orderId = order.getOrderId();
- if (orderId == null) {
- return false;
- }
-
- List orderGoodsList = shopOrderGoodsService.list(
- new LambdaQueryWrapper()
- .eq(ShopOrderGoods::getTenantId, TENANT_ID)
- .eq(ShopOrderGoods::getOrderId, orderId)
- );
- if (orderGoodsList == null || orderGoodsList.isEmpty()) {
- return false;
- }
-
- List goodsIds = orderGoodsList.stream()
- .map(ShopOrderGoods::getGoodsId)
- .filter(Objects::nonNull)
- .distinct()
- .toList();
- if (goodsIds.isEmpty()) {
- return false;
- }
-
- Map goodsDeliveryMoneyMap = shopGoodsService.list(
- new LambdaQueryWrapper()
- .eq(ShopGoods::getTenantId, TENANT_ID)
- .in(ShopGoods::getGoodsId, goodsIds)
- ).stream().collect(java.util.stream.Collectors.toMap(
- ShopGoods::getGoodsId,
- g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
- (a, b) -> a
- ));
-
- BigDecimal reward = BigDecimal.ZERO;
- for (ShopOrderGoods og : orderGoodsList) {
- Integer goodsId = og.getGoodsId();
- if (goodsId == null) {
- continue;
+ // 锁定配送员资金明细 marker,确保并发幂等
+ ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, TENANT_ID)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ .last("limit 1 for update")
+ );
+ if (existedMarker != null) {
+ log.debug("【步骤5.X】配送奖励跳过:事务内检测已存在 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
+ return false;
}
- int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
- if (qty <= 0) {
- continue;
- }
- BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
- BigDecimal rate = normalizeDeliveryRate(rawRate);
- if (rate == null || rate.signum() <= 0) {
- continue;
- }
- BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
- if (unitPrice.signum() <= 0) {
- continue;
- }
- BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
- reward = reward.add(lineAmount.multiply(rate));
- }
- reward = reward.setScale(2, RoundingMode.HALF_UP);
- if (reward.signum() <= 0) {
- return false;
- }
+ Integer orderId = order.getOrderId();
+ if (orderId == null) {
+ log.debug("【步骤5.X】配送奖励跳过:orderId为空 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
+ return false;
+ }
- // 锁定/创建配送员分销账户
- ShopDealerUser dealerUser = shopDealerUserService.getOne(
- new LambdaQueryWrapper()
- .eq(ShopDealerUser::getTenantId, TENANT_ID)
- .eq(ShopDealerUser::getUserId, riderId)
- .last("limit 1 for update")
- );
- if (dealerUser == null) {
- ShopDealerUser newDealerUser = new ShopDealerUser();
- newDealerUser.setTenantId(TENANT_ID);
- newDealerUser.setUserId(riderId);
- 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(now);
- newDealerUser.setUpdateTime(now);
- shopDealerUserService.save(newDealerUser);
+ List orderGoodsList = shopOrderGoodsService.list(
+ new LambdaQueryWrapper()
+ .eq(ShopOrderGoods::getTenantId, TENANT_ID)
+ .eq(ShopOrderGoods::getOrderId, orderId)
+ );
+ if (orderGoodsList == null || orderGoodsList.isEmpty()) {
+ log.debug("【步骤5.X】配送奖励跳过:订单商品为空 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
+ return false;
+ }
- dealerUser = shopDealerUserService.getOne(
+ List goodsIds = orderGoodsList.stream()
+ .map(ShopOrderGoods::getGoodsId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (goodsIds.isEmpty()) {
+ log.debug("【步骤5.X】配送奖励跳过:商品ID列表为空 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
+ return false;
+ }
+
+ Map goodsDeliveryMoneyMap = shopGoodsService.list(
+ new LambdaQueryWrapper()
+ .eq(ShopGoods::getTenantId, TENANT_ID)
+ .in(ShopGoods::getGoodsId, goodsIds)
+ ).stream().collect(java.util.stream.Collectors.toMap(
+ ShopGoods::getGoodsId,
+ g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
+ (a, b) -> a
+ ));
+
+ BigDecimal reward = BigDecimal.ZERO;
+ for (ShopOrderGoods og : orderGoodsList) {
+ Integer goodsId = og.getGoodsId();
+ if (goodsId == null) {
+ continue;
+ }
+ int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
+ if (qty <= 0) {
+ continue;
+ }
+ BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
+ BigDecimal rate = normalizeDeliveryRate(rawRate);
+ if (rate == null || rate.signum() <= 0) {
+ continue;
+ }
+ BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
+ if (unitPrice.signum() <= 0) {
+ continue;
+ }
+ BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
+ reward = reward.add(lineAmount.multiply(rate));
+ }
+
+ reward = reward.setScale(2, RoundingMode.HALF_UP);
+ if (reward.signum() <= 0) {
+ log.debug("【步骤5.X】配送奖励跳过:计算金额为0 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
+ return false;
+ }
+ log.debug("【步骤5.X】计算配送奖励 - tenantId={}, orderNo={}, reward={}", TENANT_ID, orderNo, reward);
+
+ // 锁定/创建配送员分销账户
+ ShopDealerUser dealerUser = shopDealerUserService.getOne(
new LambdaQueryWrapper()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, riderId)
.last("limit 1 for update")
);
if (dealerUser == null) {
- log.warn("配送奖励入账失败:未找到/创建分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
+ log.info("【步骤5.X】创建配送员分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
+ ShopDealerUser newDealerUser = new ShopDealerUser();
+ newDealerUser.setTenantId(TENANT_ID);
+ newDealerUser.setUserId(riderId);
+ 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(now);
+ newDealerUser.setUpdateTime(now);
+ shopDealerUserService.save(newDealerUser);
+
+ dealerUser = shopDealerUserService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerUser::getTenantId, TENANT_ID)
+ .eq(ShopDealerUser::getUserId, riderId)
+ .last("limit 1 for update")
+ );
+ if (dealerUser == null) {
+ log.warn("配送奖励入账失败:未找到/创建分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
+ return false;
+ }
+ }
+
+ BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
+ BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
+ dealerUser.setMoney(moneyVal.add(reward));
+ dealerUser.setTotalMoney(totalMoneyVal.add(reward));
+ dealerUser.setUpdateTime(now);
+ if (!shopDealerUserService.updateById(dealerUser)) {
+ log.warn("配送奖励入账失败:更新分销账户失败 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
return false;
}
- }
- BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
- BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
- dealerUser.setMoney(moneyVal.add(reward));
- dealerUser.setTotalMoney(totalMoneyVal.add(reward));
- dealerUser.setUpdateTime(now);
- if (!shopDealerUserService.updateById(dealerUser)) {
- log.warn("配送奖励入账失败:更新分销账户失败 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
- return false;
- }
+ ShopDealerCapital cap = new ShopDealerCapital();
+ cap.setUserId(riderId);
+ cap.setOrderNo(orderNo);
+ cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
+ cap.setMoney(reward);
+ cap.setComments("配送奖励");
+ cap.setToUserId(order.getUserId());
+ cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
+ cap.setTenantId(TENANT_ID);
+ cap.setCreateTime(now);
+ cap.setUpdateTime(now);
+ shopDealerCapitalService.save(cap);
- ShopDealerCapital cap = new ShopDealerCapital();
- cap.setUserId(riderId);
- cap.setOrderNo(orderNo);
- cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
- cap.setMoney(reward);
- cap.setComments("配送奖励");
- cap.setToUserId(order.getUserId());
- cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
- cap.setTenantId(TENANT_ID);
- cap.setCreateTime(now);
- cap.setUpdateTime(now);
- shopDealerCapitalService.save(cap);
-
- log.info("配送奖励发放成功 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
- return true;
- }));
+ log.info("【步骤5.X】配送奖励发放成功 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
+ return true;
+ }));
+ } catch (Exception e) {
+ log.error("【步骤5.X】配送奖励发放异常 - tenantId={}, orderNo={}, error={}", TENANT_ID, orderNo, e.getMessage(), e);
+ return false;
+ }
}
private Set loadWaterFormIds() {
@@ -408,10 +469,11 @@ public class DealerCommissionUnfreeze10584Task {
}
private Set findEligibleWaterOrderNosByFirstFinishedTicketOrder(Set waterFormIds) {
- // 缓存减少 DB 往返:userTicketId -> 是否“第一次送水单已完成”
+ // 缓存减少 DB 往返:userTicketId -> 是否"第一次送水单已完成"
Map firstFinishedCache = new HashMap<>();
Map userTicketOrderNoCache = new HashMap<>();
+ log.info("【步骤3.1】查询已完成的送水订单 - tenantId={}, limit={}", TENANT_ID, MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN);
List finishedTicketOrders = gltTicketOrderService.list(
new LambdaQueryWrapper()
.eq(GltTicketOrder::getTenantId, TENANT_ID)
@@ -421,21 +483,27 @@ public class DealerCommissionUnfreeze10584Task {
.orderByDesc(GltTicketOrder::getId)
.last("limit " + MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN)
);
+ log.info("【步骤3.1】查到已完成的送水订单数量 - tenantId={}, count={}", TENANT_ID, finishedTicketOrders.size());
Set orderNos = new HashSet<>();
+ int checked = 0, skippedNull = 0, skippedNotFirst = 0, skippedNoOrderNo = 0, skippedNotWater = 0;
for (GltTicketOrder ticketOrder : finishedTicketOrders) {
Integer userTicketId = ticketOrder.getUserTicketId();
+ checked++;
if (userTicketId == null) {
+ skippedNull++;
continue;
}
boolean firstFinished = firstFinishedCache.computeIfAbsent(userTicketId, id -> isFirstTicketOrderFinished(id));
if (!firstFinished) {
+ skippedNotFirst++;
continue;
}
String orderNo = userTicketOrderNoCache.computeIfAbsent(userTicketId, id -> findOrderNoByUserTicketId(id));
if (orderNo == null || orderNo.isBlank()) {
+ skippedNoOrderNo++;
continue;
}
@@ -449,6 +517,7 @@ public class DealerCommissionUnfreeze10584Task {
.last("limit 1")
);
if (shopOrder == null || shopOrder.getFormId() == null || !waterFormIds.contains(shopOrder.getFormId())) {
+ skippedNotWater++;
continue;
}
@@ -457,6 +526,8 @@ public class DealerCommissionUnfreeze10584Task {
break;
}
}
+ log.info("【步骤3.2】送水订单筛选完成 - tenantId={}, checked={}, skippedNull={}, skippedNotFirst={}, skippedNoOrderNo={}, skippedNotWater={}, matched={}, orderNos={}",
+ TENANT_ID, checked, skippedNull, skippedNotFirst, skippedNoOrderNo, skippedNotWater, orderNos.size(), orderNos);
return orderNos;
}
@@ -493,7 +564,9 @@ public class DealerCommissionUnfreeze10584Task {
if (eligibleOrderNos == null || eligibleOrderNos.isEmpty()) {
return List.of();
}
- return shopDealerCapitalService.list(
+ // 先按 eligibleOrderNos 集合的顺序(最新订单优先)处理佣金
+ // 注意:MyBatis-Plus 的 in 查询会保持 in() 列表的顺序
+ List capitals = shopDealerCapitalService.list(
new LambdaQueryWrapper()
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
.eq(ShopDealerCapital::getFlowType, 10)
@@ -502,6 +575,14 @@ public class DealerCommissionUnfreeze10584Task {
.orderByAsc(ShopDealerCapital::getId)
.last("limit " + MAX_CAPITALS_PER_RUN)
);
+ // 按 eligibleOrderNos 的顺序重新排序(最新订单优先)
+ List orderList = List.copyOf(eligibleOrderNos);
+ capitals.sort((a, b) -> {
+ int idxA = orderList.indexOf(a.getOrderNo());
+ int idxB = orderList.indexOf(b.getOrderNo());
+ return Integer.compare(idxA, idxB);
+ });
+ return capitals;
}
private boolean unfreezeOneCapitalIfNeeded(ShopDealerCapital cap) {
@@ -546,7 +627,7 @@ public class DealerCommissionUnfreeze10584Task {
return false;
}
- // RR 隔离级别下,先锁 user 行,再用锁定读检查 marker,避免“读到旧快照”导致重复解冻。
+ // RR 隔离级别下,先锁 user 行,再用锁定读检查 marker,避免"读到旧快照"导致重复解冻。
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
new LambdaQueryWrapper()
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
@@ -590,7 +671,7 @@ public class DealerCommissionUnfreeze10584Task {
marker.setUpdateTime(now);
shopDealerCapitalService.save(marker);
- // 佣金全部解冻完成后,将分销订单状态置为“已解冻”(0)。
+ // 佣金全部解冻完成后,将分销订单状态置为"已解冻"(0)。
// 以当前任务生成的 flowType=50 marker 数量作为完成度判断,避免提前将订单置为已解冻。
setDealerOrderUnfrozenIfCompleted(orderNo, now);
diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerOrderController.java
index 2ecc05e..8b7aca1 100644
--- a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerOrderController.java
+++ b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerOrderController.java
@@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
+import java.util.Map;
/**
* 分销商订单记录表控制器
@@ -168,4 +169,22 @@ public class ShopDealerOrderController extends BaseController {
}
return fail("导入失败", null);
}
+
+ @PreAuthorize("hasAuthority('shop:shopDealerOrder:update')")
+ @OperationLog
+ @Operation(summary = "手动触发单条订单佣金解冻")
+ @PostMapping("/unfreeze")
+ public ApiResult manualUnfreeze(@RequestBody Map body) {
+ String orderNo = (String) body.get("orderNo");
+ if (orderNo == null || orderNo.isBlank()) {
+ return fail("订单编号不能为空", null);
+ }
+ Integer tenantId = getTenantId();
+ try {
+ String detail = shopDealerOrderService.manualUnfreeze(orderNo, tenantId);
+ return success("解冻执行完成", detail);
+ } catch (Exception e) {
+ return fail(e.getMessage(),null);
+ }
+ }
}
diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopDealerOrderService.java b/src/main/java/com/gxwebsoft/shop/service/ShopDealerOrderService.java
index 8390e29..4e4f057 100644
--- a/src/main/java/com/gxwebsoft/shop/service/ShopDealerOrderService.java
+++ b/src/main/java/com/gxwebsoft/shop/service/ShopDealerOrderService.java
@@ -39,4 +39,13 @@ public interface ShopDealerOrderService extends IService {
*/
ShopDealerOrder getByIdRel(Integer id);
+ /**
+ * 手动触发单条订单的佣金解冻(保留与定时任务相同的前置条件检查)
+ *
+ * @param orderNo 分销订单号
+ * @param tenantId 租户ID
+ * @return 解冻结果描述(包含成功/失败详情)
+ */
+ String manualUnfreeze(String orderNo, Integer tenantId);
+
}
diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerOrderServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerOrderServiceImpl.java
index 0bab68f..e34a778 100644
--- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerOrderServiceImpl.java
+++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerOrderServiceImpl.java
@@ -1,15 +1,43 @@
package com.gxwebsoft.shop.service.impl;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.gxwebsoft.shop.mapper.ShopDealerOrderMapper;
-import com.gxwebsoft.shop.service.ShopDealerOrderService;
+import com.gxwebsoft.common.core.exception.BusinessException;
+import com.gxwebsoft.glt.entity.GltTicketOrder;
+import com.gxwebsoft.glt.entity.GltTicketTemplate;
+import com.gxwebsoft.glt.entity.GltUserTicket;
+import com.gxwebsoft.glt.service.GltTicketOrderService;
+import com.gxwebsoft.glt.service.GltTicketTemplateService;
+import com.gxwebsoft.glt.service.GltUserTicketService;
+import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
+import com.gxwebsoft.shop.entity.ShopDealerUser;
+import com.gxwebsoft.shop.entity.ShopGoods;
+import com.gxwebsoft.shop.entity.ShopOrder;
+import com.gxwebsoft.shop.entity.ShopOrderGoods;
+import com.gxwebsoft.shop.mapper.ShopDealerOrderMapper;
+import com.gxwebsoft.shop.service.ShopDealerCapitalService;
+import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.param.ShopDealerOrderParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* 分销商订单记录表Service实现
@@ -18,8 +46,41 @@ import java.util.List;
* @since 2025-08-12 11:55:18
*/
@Service
+@Slf4j
public class ShopDealerOrderServiceImpl extends ServiceImpl implements ShopDealerOrderService {
+ private static final int ORDER_STATUS_CONFIRMED_RECEIVE = 1;
+ private static final int FLOW_TYPE_COMMISSION = 10;
+ private static final int FLOW_TYPE_UNFREEZE = 50;
+ private static final int FLOW_TYPE_DELIVERY_REWARD = 60;
+
+ @Resource
+ private TransactionTemplate transactionTemplate;
+
+ @Resource
+ private com.gxwebsoft.shop.service.ShopOrderService shopOrderService;
+
+ @Resource
+ private com.gxwebsoft.shop.service.ShopOrderGoodsService shopOrderGoodsService;
+
+ @Resource
+ private com.gxwebsoft.shop.service.ShopGoodsService shopGoodsService;
+
+ @Resource
+ private ShopDealerCapitalService shopDealerCapitalService;
+
+ @Resource
+ private com.gxwebsoft.shop.service.ShopDealerUserService shopDealerUserService;
+
+ @Resource
+ private GltUserTicketService gltUserTicketService;
+
+ @Resource
+ private GltTicketOrderService gltTicketOrderService;
+
+ @Resource
+ private GltTicketTemplateService gltTicketTemplateService;
+
@Override
public PageResult pageRel(ShopDealerOrderParam param) {
PageParam page = new PageParam<>(param);
@@ -44,4 +105,436 @@ public class ShopDealerOrderServiceImpl extends ServiceImpl()
+ .eq(ShopDealerOrder::getTenantId, tenantId)
+ .eq(ShopDealerOrder::getOrderNo, orderNo)
+ .last("limit 1")
+ );
+ if (dealerOrder == null) {
+ throw new BusinessException("分销订单不存在: " + orderNo);
+ }
+ result.append(" 分销订单ID=").append(dealerOrder.getId())
+ .append(", isSettled=").append(dealerOrder.getIsSettled())
+ .append(", isUnfreeze=").append(dealerOrder.getIsUnfreeze()).append("\n");
+
+ if (dealerOrder.getIsSettled() == null || dealerOrder.getIsSettled() != 1) {
+ throw new BusinessException("订单未结算,无法解冻");
+ }
+ if (dealerOrder.getIsUnfreeze() != null && dealerOrder.getIsUnfreeze() == 1) {
+ throw new BusinessException("订单已解冻,无需重复操作");
+ }
+
+ // 2) 查商城订单,判断解冻前置条件
+ ShopOrder shopOrder = shopOrderService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopOrder::getTenantId, tenantId)
+ .eq(ShopOrder::getDeleted, 0)
+ .eq(ShopOrder::getOrderNo, orderNo)
+ .last("limit 1")
+ );
+ if (shopOrder == null) {
+ throw new BusinessException("商城订单不存在: " + orderNo);
+ }
+ result.append(" 商城订单: payStatus=").append(shopOrder.getPayStatus())
+ .append(", orderStatus=").append(shopOrder.getOrderStatus())
+ .append(", formId=").append(shopOrder.getFormId()).append("\n");
+
+ // 3) 加载水票模板 goodsId 集合(用于判断是否送水套餐)
+ Set waterFormIds = gltTicketTemplateService.list(
+ new LambdaQueryWrapper()
+ .eq(GltTicketTemplate::getTenantId, tenantId)
+ .eq(GltTicketTemplate::getDeleted, 0)
+ .isNotNull(GltTicketTemplate::getGoodsId)
+ ).stream()
+ .map(GltTicketTemplate::getGoodsId)
+ .collect(Collectors.toSet());
+ result.append(" 水票模板goodsId集合: ").append(waterFormIds).append("\n");
+
+ boolean isWaterOrder = shopOrder.getFormId() != null && waterFormIds.contains(shopOrder.getFormId());
+ result.append(" 是否送水套餐: ").append(isWaterOrder).append("\n");
+
+ // 4) 检查解冻前置条件
+ if (isWaterOrder) {
+ // 送水套餐:至少有一条送水订单 deliveryStatus=40(已完成)
+ GltUserTicket userTicket = gltUserTicketService.getOne(
+ new LambdaQueryWrapper()
+ .eq(GltUserTicket::getTenantId, tenantId)
+ .eq(GltUserTicket::getDeleted, 0)
+ .eq(GltUserTicket::getOrderNo, orderNo)
+ .last("limit 1")
+ );
+ if (userTicket == null) {
+ // 兜底:通过 orderId 反查
+ userTicket = gltUserTicketService.getOne(
+ new LambdaQueryWrapper()
+ .eq(GltUserTicket::getTenantId, tenantId)
+ .eq(GltUserTicket::getDeleted, 0)
+ .eq(GltUserTicket::getOrderId, shopOrder.getOrderId())
+ .last("limit 1")
+ );
+ }
+ if (userTicket == null) {
+ throw new BusinessException("未找到关联的水票记录,无法确认送水状态");
+ }
+
+ GltTicketOrder firstTicketOrder = gltTicketOrderService.getOne(
+ new LambdaQueryWrapper()
+ .eq(GltTicketOrder::getTenantId, tenantId)
+ .eq(GltTicketOrder::getDeleted, 0)
+ .eq(GltTicketOrder::getUserTicketId, userTicket.getId())
+ .orderByAsc(GltTicketOrder::getId)
+ .last("limit 1")
+ );
+
+ boolean firstFinished = firstTicketOrder != null
+ && firstTicketOrder.getDeliveryStatus() != null
+ && firstTicketOrder.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED;
+
+ result.append(" 送水订单检查: userTicketId=").append(userTicket.getId())
+ .append(", firstTicketOrderId=").append(firstTicketOrder != null ? firstTicketOrder.getId() : "null")
+ .append(", deliveryStatus=").append(firstTicketOrder != null ? firstTicketOrder.getDeliveryStatus() : "null")
+ .append(", firstFinished=").append(firstFinished).append("\n");
+
+ if (!firstFinished) {
+ throw new BusinessException("送水套餐订单尚未完成第一次配送确认收货,不满足解冻条件");
+ }
+ } else {
+ // 非送水套餐:商城订单 orderStatus=1(已确认收货)
+ boolean orderCompleted = shopOrder.getOrderStatus() != null
+ && shopOrder.getOrderStatus() == ORDER_STATUS_CONFIRMED_RECEIVE
+ && shopOrder.getPayStatus() != null
+ && shopOrder.getPayStatus();
+
+ result.append(" 非送水订单检查: orderStatus=").append(shopOrder.getOrderStatus())
+ .append(", payStatus=").append(shopOrder.getPayStatus())
+ .append(", completed=").append(orderCompleted).append("\n");
+
+ if (!orderCompleted) {
+ throw new BusinessException("订单尚未确认收货,不满足解冻条件 (orderStatus=" + shopOrder.getOrderStatus() + ")");
+ }
+ }
+
+ // 5) 发放配送奖励(幂等)
+ result.append(" 开始发放配送奖励...\n");
+ try {
+ boolean rewarded = settleDeliveryRewardIfNeeded(orderNo, tenantId);
+ result.append(" 配送奖励结果: ").append(rewarded ? "已发放" : "跳过(无配送员/已发放)").append("\n");
+ } catch (Exception e) {
+ result.append(" 配送奖励异常: ").append(e.getMessage()).append("\n");
+ log.warn("手动解冻-配送奖励发放失败 - orderNo={}, error={}", orderNo, e.getMessage(), e);
+ }
+
+ // 6) 查询该订单的佣金明细(flowType=10)
+ List capitals = shopDealerCapitalService.list(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_COMMISSION)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ );
+ result.append(" 佣金明细(flowType=10)数量: ").append(capitals.size()).append("\n");
+
+ if (capitals.isEmpty()) {
+ throw new BusinessException("未找到佣金明细(flowType=10),无法解冻");
+ }
+
+ // 7) 逐条执行解冻
+ int unfrozen = 0;
+ for (ShopDealerCapital cap : capitals) {
+ try {
+ boolean ok = unfreezeOneCapitalIfNeeded(cap, tenantId);
+ if (ok) {
+ unfrozen++;
+ result.append(" 解冻成功: capitalId=").append(cap.getId())
+ .append(", userId=").append(cap.getUserId())
+ .append(", amount=").append(cap.getMoney()).append("\n");
+ } else {
+ result.append(" 解冻跳过(已解冻/幂等): capitalId=").append(cap.getId()).append("\n");
+ }
+ } catch (Exception e) {
+ result.append(" 解冻失败: capitalId=").append(cap.getId())
+ .append(", error=").append(e.getMessage()).append("\n");
+ log.error("手动解冻-单条佣金解冻失败 - orderNo={}, capitalId={}, error={}", orderNo, cap.getId(), e.getMessage(), e);
+ }
+ }
+
+ // 8) 检查是否全部解冻完成
+ if (unfrozen > 0) {
+ long totalCommissions = shopDealerCapitalService.count(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_COMMISSION)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ );
+ long unfrozenMarkers = shopDealerCapitalService.count(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_UNFREEZE)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ .like(ShopDealerCapital::getComments, "佣金解冻(capitalId=")
+ );
+ result.append(" 解冻进度: ").append(unfrozenMarkers).append("/").append(totalCommissions).append("\n");
+
+ if (unfrozenMarkers >= totalCommissions) {
+ LocalDateTime now = LocalDateTime.now();
+ boolean updated = this.update(
+ new LambdaUpdateWrapper()
+ .eq(ShopDealerOrder::getTenantId, tenantId)
+ .eq(ShopDealerOrder::getOrderNo, orderNo)
+ .set(ShopDealerOrder::getIsUnfreeze, 1)
+ .set(ShopDealerOrder::getUnfreezeTime, now)
+ .set(ShopDealerOrder::getUpdateTime, now)
+ );
+ result.append(" 订单解冻状态更新: ").append(updated ? "成功(isUnfreeze=1)" : "失败").append("\n");
+ }
+ }
+
+ result.append("【手动解冻完成】共解冻 ").append(unfrozen).append("/").append(capitals.size()).append(" 条佣金");
+ return result.toString();
+ }
+
+ /**
+ * 发放配送奖励(从定时任务复制,支持 tenantId 参数)
+ */
+ private boolean settleDeliveryRewardIfNeeded(String orderNo, Integer tenantId) {
+ if (orderNo == null || orderNo.isBlank() || tenantId == null) {
+ return false;
+ }
+
+ ShopOrder order = shopOrderService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopOrder::getTenantId, tenantId)
+ .eq(ShopOrder::getDeleted, 0)
+ .eq(ShopOrder::getOrderNo, orderNo)
+ .last("limit 1")
+ );
+ if (order == null) return false;
+
+ Integer riderId = order.getRiderId();
+ if (riderId == null || riderId <= 0) return false;
+
+ // 幂等检查
+ boolean already = shopDealerCapitalService.count(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ ) > 0;
+ if (already) return false;
+
+ return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
+ LocalDateTime now = LocalDateTime.now();
+
+ // 锁定 marker 幂等
+ ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ .last("limit 1 for update")
+ );
+ if (existedMarker != null) return false;
+
+ Integer orderId = order.getOrderId();
+ if (orderId == null) return false;
+
+ List orderGoodsList = shopOrderGoodsService.list(
+ new LambdaQueryWrapper()
+ .eq(ShopOrderGoods::getTenantId, tenantId)
+ .eq(ShopOrderGoods::getOrderId, orderId)
+ );
+ if (orderGoodsList == null || orderGoodsList.isEmpty()) return false;
+
+ List goodsIds = orderGoodsList.stream()
+ .map(ShopOrderGoods::getGoodsId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (goodsIds.isEmpty()) return false;
+
+ Map goodsDeliveryMoneyMap = shopGoodsService.list(
+ new LambdaQueryWrapper()
+ .eq(ShopGoods::getTenantId, tenantId)
+ .in(ShopGoods::getGoodsId, goodsIds)
+ ).stream().collect(Collectors.toMap(
+ ShopGoods::getGoodsId,
+ g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
+ (a, b) -> a
+ ));
+
+ BigDecimal reward = BigDecimal.ZERO;
+ for (ShopOrderGoods og : orderGoodsList) {
+ Integer goodsId = og.getGoodsId();
+ if (goodsId == null) continue;
+ int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
+ if (qty <= 0) continue;
+ BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
+ BigDecimal rate = normalizeDeliveryRate(rawRate);
+ if (rate == null || rate.signum() <= 0) continue;
+ BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
+ if (unitPrice.signum() <= 0) continue;
+ BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
+ reward = reward.add(lineAmount.multiply(rate));
+ }
+
+ reward = reward.setScale(2, RoundingMode.HALF_UP);
+ if (reward.signum() <= 0) return false;
+
+ // 锁定/创建配送员分销账户
+ ShopDealerUser dealerUser = shopDealerUserService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerUser::getTenantId, tenantId)
+ .eq(ShopDealerUser::getUserId, riderId)
+ .last("limit 1 for update")
+ );
+ if (dealerUser == null) {
+ ShopDealerUser newDealerUser = new ShopDealerUser();
+ newDealerUser.setTenantId(tenantId);
+ newDealerUser.setUserId(riderId);
+ 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(now);
+ newDealerUser.setUpdateTime(now);
+ shopDealerUserService.save(newDealerUser);
+ dealerUser = shopDealerUserService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerUser::getTenantId, tenantId)
+ .eq(ShopDealerUser::getUserId, riderId)
+ .last("limit 1 for update")
+ );
+ if (dealerUser == null) return false;
+ }
+
+ BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
+ BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
+ dealerUser.setMoney(moneyVal.add(reward));
+ dealerUser.setTotalMoney(totalMoneyVal.add(reward));
+ dealerUser.setUpdateTime(now);
+ if (!shopDealerUserService.updateById(dealerUser)) return false;
+
+ ShopDealerCapital cap = new ShopDealerCapital();
+ cap.setUserId(riderId);
+ cap.setOrderNo(orderNo);
+ cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
+ cap.setMoney(reward);
+ cap.setComments("配送奖励");
+ cap.setToUserId(order.getUserId());
+ cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
+ cap.setTenantId(tenantId);
+ cap.setCreateTime(now);
+ cap.setUpdateTime(now);
+ shopDealerCapitalService.save(cap);
+
+ log.info("手动解冻-配送奖励发放成功 - orderNo={}, riderId={}, reward={}", orderNo, riderId, reward);
+ return true;
+ }));
+ }
+
+ /**
+ * 解冻单条佣金(从定时任务复制,支持 tenantId 参数)
+ */
+ private boolean unfreezeOneCapitalIfNeeded(ShopDealerCapital cap, Integer tenantId) {
+ if (cap == null) return false;
+ Integer capitalId = cap.getId();
+ Integer dealerUserId = cap.getUserId();
+ String orderNo = cap.getOrderNo();
+ BigDecimal amount = cap.getMoney();
+ if (capitalId == null || dealerUserId == null || orderNo == null || orderNo.isBlank() || amount == null || amount.signum() <= 0) {
+ return false;
+ }
+
+ String markerComment = "佣金解冻(capitalId=" + capitalId + ")";
+
+ // 快速幂等检查
+ boolean already = shopDealerCapitalService.count(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_UNFREEZE)
+ .eq(ShopDealerCapital::getUserId, dealerUserId)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ .eq(ShopDealerCapital::getComments, markerComment)
+ ) > 0;
+ if (already) return false;
+
+ return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
+ LocalDateTime now = LocalDateTime.now();
+
+ ShopDealerUser dealerUser = shopDealerUserService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerUser::getTenantId, tenantId)
+ .eq(ShopDealerUser::getUserId, dealerUserId)
+ .last("limit 1 for update")
+ );
+ if (dealerUser == null) {
+ log.warn("手动解冻失败:未找到分销账户 - orderNo={}, dealerUserId={}, amount={}", orderNo, dealerUserId, amount);
+ return false;
+ }
+
+ // 二次幂等检查
+ ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
+ new LambdaQueryWrapper()
+ .eq(ShopDealerCapital::getTenantId, tenantId)
+ .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_UNFREEZE)
+ .eq(ShopDealerCapital::getUserId, dealerUserId)
+ .eq(ShopDealerCapital::getOrderNo, orderNo)
+ .eq(ShopDealerCapital::getComments, markerComment)
+ .last("limit 1 for update")
+ );
+ if (existedMarker != null) return false;
+
+ BigDecimal freezeMoney = dealerUser.getFreezeMoney() != null ? dealerUser.getFreezeMoney() : BigDecimal.ZERO;
+ if (freezeMoney.compareTo(amount) < 0) {
+ log.warn("手动解冻失败:冻结金额不足 - orderNo={}, dealerUserId={}, freezeMoney={}, amount={}",
+ orderNo, dealerUserId, freezeMoney, amount);
+ return false;
+ }
+
+ BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
+ dealerUser.setFreezeMoney(freezeMoney.subtract(amount));
+ dealerUser.setMoney(moneyVal.add(amount));
+ dealerUser.setUpdateTime(now);
+ if (!shopDealerUserService.updateById(dealerUser)) {
+ log.warn("手动解冻失败:更新分销账户失败 - orderNo={}, dealerUserId={}, amount={}", orderNo, dealerUserId, amount);
+ return false;
+ }
+
+ ShopDealerCapital marker = new ShopDealerCapital();
+ marker.setUserId(dealerUserId);
+ marker.setOrderNo(orderNo);
+ marker.setFlowType(FLOW_TYPE_UNFREEZE);
+ marker.setMoney(amount);
+ marker.setComments(markerComment);
+ marker.setToUserId(cap.getToUserId());
+ marker.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
+ marker.setTenantId(tenantId);
+ marker.setCreateTime(now);
+ marker.setUpdateTime(now);
+ shopDealerCapitalService.save(marker);
+
+ log.info("手动解冻成功 - orderNo={}, dealerUserId={}, amount={}, capitalId={}", orderNo, dealerUserId, amount, capitalId);
+ return true;
+ }));
+ }
+
+ private static BigDecimal normalizeDeliveryRate(BigDecimal rawRate) {
+ if (rawRate == null || rawRate.signum() <= 0) return null;
+ if (rawRate.compareTo(BigDecimal.ONE) >= 0) return rawRate.movePointLeft(2);
+ return rawRate;
+ }
+
}