diff --git a/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java b/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java index 79f1438..8ebb385 100644 --- a/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java +++ b/src/main/java/com/gxwebsoft/glt/task/DealerCommissionUnfreeze10584Task.java @@ -12,10 +12,14 @@ 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.service.ShopDealerCapitalService; import com.gxwebsoft.shop.service.ShopDealerOrderService; import com.gxwebsoft.shop.service.ShopDealerUserService; +import com.gxwebsoft.shop.service.ShopGoodsService; +import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -25,6 +29,7 @@ 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; @@ -32,6 +37,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -59,12 +65,20 @@ public class DealerCommissionUnfreeze10584Task { private static final int MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN = 200; private static final int MAX_CAPITALS_PER_RUN = 500; + private static final int FLOW_TYPE_DELIVERY_REWARD = 60; // 配送奖励(直接入可提现金额) + @Resource private TransactionTemplate transactionTemplate; @Resource private ShopOrderService shopOrderService; + @Resource + private ShopOrderGoodsService shopOrderGoodsService; + + @Resource + private ShopGoodsService shopGoodsService; + @Resource private ShopDealerCapitalService shopDealerCapitalService; @@ -110,12 +124,36 @@ public class DealerCommissionUnfreeze10584Task { return; } + // 配送奖励(与佣金解冻独立):按订单发放,幂等保证不会重复入账 + int rewarded = 0; + for (String orderNo : eligibleOrderNos) { + try { + if (settleDeliveryRewardIfNeeded(orderNo)) { + rewarded++; + } + } catch (Exception e) { + log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e); + } + } + List capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos); if (capitals.isEmpty()) { // 若本轮没有取到佣金明细,回退再按“最早确认收货”的订单扫一轮,尽量覆盖历史遗留未解冻。 eligibleOrderNos.clear(); eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, false)); eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds)); + + // 兜底扫描出来的订单也补发配送奖励(幂等) + for (String orderNo : eligibleOrderNos) { + try { + if (settleDeliveryRewardIfNeeded(orderNo)) { + rewarded++; + } + } catch (Exception e) { + log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e); + } + } + capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos); } @@ -140,11 +178,182 @@ public class DealerCommissionUnfreeze10584Task { 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); + } } finally { running.set(false); } } + private boolean settleDeliveryRewardIfNeeded(String orderNo) { + if (orderNo == null || orderNo.isBlank()) { + return false; + } + + ShopOrder order = shopOrderService.getOne( + new LambdaQueryWrapper() + .eq(ShopOrder::getTenantId, TENANT_ID) + .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, TENANT_ID) + .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD) + .eq(ShopDealerCapital::getUserId, riderId) + .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, TENANT_ID) + .eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD) + .eq(ShopDealerCapital::getUserId, riderId) + .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; + } + int qty = og.getTotalNum() == null ? 0 : og.getTotalNum(); + if (qty <= 0) { + continue; + } + BigDecimal unit = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO); + if (unit.signum() <= 0) { + continue; + } + reward = reward.add(unit.multiply(BigDecimal.valueOf(qty))); + } + + reward = reward.setScale(2, RoundingMode.HALF_UP); + if (reward.signum() <= 0) { + 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); + + 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; + } + + 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; + })); + } + private Set loadWaterFormIds() { return gltTicketTemplateService.list( new LambdaQueryWrapper() diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java b/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java index 66eae4b..183dd57 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java @@ -37,7 +37,7 @@ public class ShopDealerCapital implements Serializable { @Schema(description = "订单编号") private String orderNo; - @Schema(description = "资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入 50佣金解冻)") + @Schema(description = "资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励)") private Integer flowType; @Schema(description = "金额") diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java b/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java index 7fe522f..7d31385 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java @@ -101,6 +101,9 @@ public class ShopGoods implements Serializable { @Schema(description = "分红(二级)") private BigDecimal secondDividend; + @Schema(description = "配送奖金") + private BigDecimal deliveryMoney; + @Schema(description = "库存计算方式(10下单减库存 20付款减库存)") @JsonAlias({"cdeductStockType"}) private Integer deductStockType;