From d9e4371735d70f236fe101bc2c7254309c3e21d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 21 Apr 2026 00:04:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(shop):=20=E5=AE=9E=E7=8E=B0=E5=88=86?= =?UTF-8?q?=E9=94=80=E8=AE=A2=E5=8D=95=E6=89=8B=E5=8A=A8=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E4=BD=A3=E9=87=91=E8=A7=A3=E5=86=BB=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增ShopDealerOrderController.manualUnfreeze接口,支持通过订单号手动触发佣金解冻 - 在ShopDealerOrderService及实现类添加manualUnfreeze方法,实现手动解冻业务逻辑 - 手动解冻包含分销订单查询、状态校验、关联商城订单与水票套餐校验 - 补充手动解冻中配送奖励发放逻辑,保证幂等,记录详细处理信息 - 丰富手动解冻的异常处理与业务日志,方便排查与追踪 - 优化DealerCommissionUnfreeze10584Task定时任务,增强日志详尽度和流程步骤清晰性 - 对送水订单和非送水订单条件进行精确分类处理,避免误判 - 调整配送奖励发放逻辑,确保任务和手动触发路径一致 - 引入多处日志打印提升监控和调试能力,包括订单过滤、佣金解冻和配送奖励发放情况 --- .workbuddy/expert-history.json | 13 +- .../DealerCommissionUnfreeze10584Task.java | 369 ++++++++----- .../controller/ShopDealerOrderController.java | 19 + .../shop/service/ShopDealerOrderService.java | 9 + .../impl/ShopDealerOrderServiceImpl.java | 497 +++++++++++++++++- 5 files changed, 760 insertions(+), 147 deletions(-) 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; + } + }