From 124117767683bd92448e8df3c89827016f925d05 Mon Sep 17 00:00:00 2001 From: wangfukang <15630117759@163.com> Date: Sun, 16 Aug 2026 18:12:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=8B=BC=E5=9B=A2=E6=95=B0?= =?UTF-8?q?=E6=8D=AE1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hiver-admin/test-output/test-report.html | 16 +- .../AdminDataStatisticsController.java | 158 ++++++++++++++---- .../controller/AdminSettlementController.java | 14 +- .../dao/mapper/MallRefundRecordMapper.java | 18 ++ .../WorkerDailyBalanceRecordMapper.java | 9 + .../mall/entity/WorkerDailyBalanceRecord.java | 46 +++++ .../mall/quartz/MerchantSettlementTask.java | 83 +++++++-- .../WorkerDailyBalanceRecordService.java | 7 + .../mybatis/MallDeliveryOrderServiceImpl.java | 91 ++++++++-- .../mybatis/MallRefundRecordServiceImpl.java | 140 ++++++++++++++-- .../WorkerDailyBalanceRecordServiceImpl.java | 12 ++ .../db/worker_daily_balance_record.sql | 15 ++ 12 files changed, 534 insertions(+), 75 deletions(-) create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/WorkerDailyBalanceRecordMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/WorkerDailyBalanceRecord.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/WorkerDailyBalanceRecordService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/WorkerDailyBalanceRecordServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/resources/db/worker_daily_balance_record.sql diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index 6087dbba..a7aa68f5 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • -°ËÔÂ 13, 2026 11:52:38 +°ËÔÂ 16, 2026 18:06:44
  • @@ -84,7 +84,7 @@

    passTest

    -

    11:52:38 ÉÏÎç / 0.033 secs

    +

    18:06:45 ÏÂÎç / 0.022 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -08.13.2026 11:52:38 -08.13.2026 11:52:39 -0.033 secs +08.16.2026 18:06:45 +08.16.2026 18:06:45 +0.022 secs
    @@ -104,7 +104,7 @@ Pass - 11:52:39 + 18:06:45 Test passed @@ -128,13 +128,13 @@

    Started

    -

    °ËÔÂ 13, 2026 11:52:38

    +

    °ËÔÂ 16, 2026 18:06:44

    Ended

    -

    °ËÔÂ 13, 2026 11:52:39

    +

    °ËÔÂ 16, 2026 18:06:45

    diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java index 90a9998e..b511dcd4 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java @@ -6,11 +6,14 @@ import cc.hiver.mall.billing.entity.BillingUsageRecord; import cc.hiver.mall.billing.mapper.BillingUsageRecordMapper; import cc.hiver.mall.dao.mapper.*; import cc.hiver.mall.entity.*; +import cc.hiver.mall.service.mybatis.WorkerDailyBalanceRecordService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiOperation; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; @@ -29,7 +32,9 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +@Slf4j @RestController @Api(tags = "åŽå°ç®¡ç†ç³»ç»Ÿæ•°æ®ç»Ÿè®¡æŽ¥å£") @RequestMapping("/hiver/mall/admin/dataStatistics") @@ -59,6 +64,8 @@ public class AdminDataStatisticsController { private BillingUsageRecordMapper billingUsageRecordMapper; @Autowired private MallSettlementRecordMapper mallSettlementRecordMapper; + @Autowired + private WorkerDailyBalanceRecordService workerDailyBalanceRecordService; @Data public static class DataStatisticsQuery { @@ -87,7 +94,10 @@ public class AdminDataStatisticsController { private Boolean actualIncomeAvailable; private BigDecimal settledAmount; private BigDecimal headquartersDeductionAmount; + private BigDecimal wechatReceiptAmount; + private BigDecimal wechatFeeAmount; private BigDecimal deliveryFeeAmount; + private Long workerDebtCount; private Long smsCount; private Long contentAuditCount; private Long realNameAuthCount; @@ -101,6 +111,14 @@ public class AdminDataStatisticsController { private Long orderUserCount; } + @Data + public static class WorkerDebtQuery extends DataStatisticsQuery { + @ApiModelProperty(value = "页ç ") + private int pageNumber = 1; + @ApiModelProperty(value = "æ¯é¡µæ¡æ•°") + private int pageSize = 10; + } + @PostMapping("/summary") @ApiOperation(value = "æŒ‰æ ¡åŒºå’Œæ—¥æœŸèŒƒå›´èŽ·å–æ•°æ®ç»Ÿè®¡æ±‡æ€»") public Result summary(@RequestBody DataStatisticsQuery query) { @@ -143,12 +161,16 @@ public class AdminDataStatisticsController { .ge(BillingUsageRecord::getBillingDate, range.startDate) .le(BillingUsageRecord::getBillingDate, range.endDate)); + List workerBalanceRecords = workerDailyBalanceRecordService.list(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), WorkerDailyBalanceRecord::getRegionId, regionId) + .ge(WorkerDailyBalanceRecord::getBalanceDate, range.startDate) + .le(WorkerDailyBalanceRecord::getBalanceDate, range.endDate)); + DataStatisticsSummaryVo vo = new DataStatisticsSummaryVo(); vo.setStartDate(range.startDate.toString()); vo.setEndDate(range.endDate.toString()); Set refundedOrderIds = new HashSet<>(); - Set deliveryRefundOrderIds = new HashSet<>(); for (MallRefundRecord refund : refunds) { if (StringUtils.isBlank(refund.getOrderId())) { continue; @@ -156,24 +178,12 @@ public class AdminDataStatisticsController { if (refund.getStatus() != null && refund.getStatus() == 1) { refundedOrderIds.add(refund.getOrderId()); } - if (refund.getRefundTypeStatus() != null && (refund.getRefundTypeStatus() == 2 || refund.getRefundTypeStatus() == 3 || refund.getRefundTypeStatus() == 4)) { - deliveryRefundOrderIds.add(refund.getOrderId()); - } } - BigDecimal goodsAndPackage = BigDecimal.ZERO; - BigDecimal grossDeliveryFee = BigDecimal.ZERO; - BigDecimal settlementDeliveryFee = BigDecimal.ZERO; BigDecimal koiDiscount = BigDecimal.ZERO; Set shopIds = new HashSet<>(); Set userIds = new HashSet<>(); for (MallOrder order : orders) { - goodsAndPackage = goodsAndPackage.add(amount(order.getGoodsAmount())).add(amount(order.getPackageFee())); - BigDecimal orderDeliveryFee = amount(order.getDeliveryFee()); - grossDeliveryFee = grossDeliveryFee.add(orderDeliveryFee); - if (!deliveryRefundOrderIds.contains(order.getId())) { - settlementDeliveryFee = settlementDeliveryFee.add(orderDeliveryFee); - } if (!isRefundedOrder(order, refundedOrderIds)) { koiDiscount = koiDiscount.add(amount(order.getFreeAmount())); } @@ -184,13 +194,50 @@ public class AdminDataStatisticsController { userIds.add(order.getUserId()); } } - - BigDecimal deliveryFeeAmount = subtractWechatFee(settlementDeliveryFee); - BigDecimal grossFlow = subtractWechatFee(goodsAndPackage.add(grossDeliveryFee)); + /* + * 原é…é€è´¹æ€»é¢é€»è¾‘:从订å•é…é€è´¹å推,并按退款/å”®åŽã€å¹³å°è¿½åŠ é…é€è´¹ã€æœªå–货全退等规则修正。 + * 现改为查询 worker_daily_balance_record,累加所选日期内 daily_balance > 0 的余é¢å¿«ç…§ã€‚ + * + * BigDecimal settlementDeliveryFee = BigDecimal.ZERO; + * Set settlementDeliveryOrderIds = new HashSet<>(); + * for (MallOrder order : orders) { + * BigDecimal orderDeliveryFee = amount(order.getDeliveryFee()); + * if (!deliveryRefundOrderIds.contains(order.getId())) { + * settlementDeliveryFee = settlementDeliveryFee.add(orderDeliveryFee); + * if (StringUtils.isNotBlank(order.getId())) { + * settlementDeliveryOrderIds.add(order.getId()); + * } + * } + * } + * if (!settlementDeliveryOrderIds.isEmpty()) { + * List settlementDeliveries = mallDeliveryOrderMapper.selectList(new LambdaQueryWrapper() + * .eq(StringUtils.isNotBlank(regionId), MallDeliveryOrder::getRegionId, regionId) + * .in(MallDeliveryOrder::getOrderId, settlementDeliveryOrderIds)); + * BigDecimal marketplaceDeliveryFee = settlementDeliveries.stream() + * .map(MallDeliveryOrder::getDeliveryFeeMarketplace) + * .map(this::amount) + * .reduce(BigDecimal.ZERO, BigDecimal::add); + * BigDecimal unpickedMerchantFullRefundDeliveryFee = settlementDeliveries.stream() + * .filter(item -> merchantFullRefundOrderIds.contains(item.getOrderId())) + * .filter(item -> item.getGetTime() == null) + * .map(this::deliverySettlementAmount) + * .reduce(BigDecimal.ZERO, BigDecimal::add); + * settlementDeliveryFee = settlementDeliveryFee.add(marketplaceDeliveryFee).subtract(unpickedMerchantFullRefundDeliveryFee); + * } + * BigDecimal deliveryFeeAmount = settlementDeliveryFee; + */ + BigDecimal deliveryFeeAmount = sumPositiveWorkerBalance(workerBalanceRecords); + long workerDebtCount = workerBalanceRecords.stream() + .filter(item -> amount(item.getDailyBalance()).compareTo(BigDecimal.ZERO) < 0) + .count(); + BigDecimal grossFlow = orders.stream() + .map(MallOrder::getTotalAmount) + .map(this::amount) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal refundAmount = sumRefundAmount(refunds, 1); BigDecimal afterSaleAmount = sumRefundAmount(refunds, 4); - BigDecimal effectiveFlow = subtractWechatFee(goodsAndPackage); - BigDecimal headquartersDeduction = effectiveFlow.multiply(ONE_PERCENT); + BigDecimal effectiveFlow = grossFlow.subtract(refundAmount).subtract(afterSaleAmount); + BigDecimal headquartersDeduction = effectiveFlow.subtract(deliveryFeeAmount).multiply(ONE_PERCENT); BigDecimal collegeRankReward = sumDealingsAmount(rewardRecords, REWARD_COLLEGE_RANK); BigDecimal deliveryRankReward = sumDealingsAmount(rewardRecords, REWARD_DELIVERY_RANK); @@ -199,6 +246,8 @@ public class AdminDataStatisticsController { .map(this::amount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal drawPoolAmount = sumDealingsAmount(rewardRecords, REWARD_DRAW_POOL); + BigDecimal wechatReceiptAmount = effectiveFlow; + BigDecimal wechatFee = wechatReceiptAmount.multiply(WECHAT_FEE_RATE); long smsCount = billingRecords.stream().filter(item -> "sms".equals(item.getRecordType())).count(); long contentAuditCount = billingRecords.stream().filter(item -> "content_audit".equals(item.getRecordType())).count(); @@ -209,13 +258,10 @@ public class AdminDataStatisticsController { BigDecimal rewardTotalAmount = collegeRankReward.add(drawPoolAmount).add(deliveryRankReward); boolean actualIncomeAvailable = isActualIncomeAvailable(range); BigDecimal settledAmount = actualIncomeAvailable ? sumSettledAmount(regionId, range) : BigDecimal.ZERO; - BigDecimal actualIncome = grossFlow + BigDecimal actualIncome = wechatReceiptAmount .subtract(deliveryFeeAmount) .subtract(headquartersDeduction) - .subtract(koiDiscount) - .subtract(refundAmount) - .subtract(afterSaleAmount) - .subtract(couponDiscount) + .subtract(wechatFee) .subtract(smsCost) .subtract(contentAuditCost) .subtract(realNameCost) @@ -225,14 +271,17 @@ public class AdminDataStatisticsController { vo.setOrderCount((long) orders.size()); vo.setGrossFlowAmount(money(grossFlow)); vo.setDeliveryFeeAmount(money(deliveryFeeAmount)); + vo.setWorkerDebtCount(workerDebtCount); vo.setRefundAmount(money(refundAmount)); vo.setAfterSaleAmount(money(afterSaleAmount)); vo.setEffectiveFlowAmount(money(effectiveFlow)); vo.setKoiDiscountAmount(money(koiDiscount)); vo.setCouponDiscountAmount(money(couponDiscount)); vo.setOnTimeRate(onTimeRate(deliveries)); - vo.setCustomerAverageAmount(orders.isEmpty() ? BigDecimal.ZERO : money(goodsAndPackage.divide(BigDecimal.valueOf(orders.size()), 4, RoundingMode.HALF_UP))); + vo.setCustomerAverageAmount(orders.isEmpty() ? BigDecimal.ZERO : money(grossFlow.divide(BigDecimal.valueOf(orders.size()), 4, RoundingMode.HALF_UP))); vo.setHeadquartersDeductionAmount(money(headquartersDeduction)); + vo.setWechatReceiptAmount(money(wechatReceiptAmount)); + vo.setWechatFeeAmount(money(wechatFee)); vo.setActualIncomeAmount(actualIncomeAvailable ? money(actualIncome) : BigDecimal.ZERO); vo.setActualIncomeAvailable(actualIncomeAvailable); vo.setSettledAmount(money(settledAmount)); @@ -250,6 +299,31 @@ public class AdminDataStatisticsController { return new ResultUtil().setData(vo); } + @PostMapping("/workerDebtPage") + @ApiOperation(value = "分页查询欠款é…é€å‘˜æ¯æ—¥ä½™é¢è®°å½•") + public Result> workerDebtPage(@RequestBody WorkerDebtQuery query) { + DateRange range = resolveDateRange(query); + String regionId = query == null ? null : query.getRegionId(); + Page page = workerDailyBalanceRecordService.page( + new Page<>(pageNumber(query), pageSize(query)), + new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), WorkerDailyBalanceRecord::getRegionId, regionId) + .ge(WorkerDailyBalanceRecord::getBalanceDate, range.startDate) + .le(WorkerDailyBalanceRecord::getBalanceDate, range.endDate) + .lt(WorkerDailyBalanceRecord::getDailyBalance, BigDecimal.ZERO) + .orderByAsc(WorkerDailyBalanceRecord::getBalanceDate) + .orderByAsc(WorkerDailyBalanceRecord::getWorkerName)); + return new ResultUtil>().setData(page); + } + + private BigDecimal sumPositiveWorkerBalance(List records) { + return records.stream() + .map(WorkerDailyBalanceRecord::getDailyBalance) + .map(this::amount) + .filter(amount -> amount.compareTo(BigDecimal.ZERO) > 0) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + private BigDecimal sumRefundAmount(List refunds, int status) { return refunds.stream() .filter(item -> item.getStatus() != null && item.getStatus() == status) @@ -258,6 +332,13 @@ public class AdminDataStatisticsController { .reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal deliverySettlementAmount(MallDeliveryOrder delivery) { + if (delivery == null) { + return BigDecimal.ZERO; + } + return amount(delivery.getDeliveryFee()).add(amount(delivery.getDeliveryFeeMarketplace())); + } + private BigDecimal sumDealingsAmount(List records, String dealingsWay) { return records.stream() .filter(item -> dealingsWay.equals(item.getDealingsWay())) @@ -266,18 +347,39 @@ public class AdminDataStatisticsController { .reduce(BigDecimal.ZERO, BigDecimal::add); } + private void printSettlementDeliveryOrderIds(Set orderIds) { + if (orderIds == null || orderIds.isEmpty()) { + log.info("æ•°æ®ç»Ÿè®¡ç»“ç®—é…é€è®¢å•ID为空"); + return; + } + String orderIdSqlIn = orderIds.stream() + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("','", "'", "'")); + log.info("æ•°æ®ç»Ÿè®¡ç»“ç®—é…é€è®¢å•ID, count={}, sql=select * from mall_delivery_order where order_id in ({});", orderIds.size(), orderIdSqlIn); + } + private BigDecimal sumSettledAmount(String regionId, DateRange range) { + Date settlementStart = Date.from(range.start.toInstant().plus(1, ChronoUnit.DAYS)); + Date settlementEnd = Date.from(range.end.toInstant().plus(1, ChronoUnit.DAYS)); List settlementRecords = mallSettlementRecordMapper.selectList(new LambdaQueryWrapper() .eq(StringUtils.isNotBlank(regionId), MallSettlementRecord::getRegionId, regionId) .eq(MallSettlementRecord::getStatus, 1) - .ge(MallSettlementRecord::getCreateTime, range.start) - .le(MallSettlementRecord::getCreateTime, range.end)); + .ge(MallSettlementRecord::getCreateTime, settlementStart) + .le(MallSettlementRecord::getCreateTime, settlementEnd)); return settlementRecords.stream() .map(MallSettlementRecord::getSettlementAmount) .map(this::amount) .reduce(BigDecimal.ZERO, BigDecimal::add); } + private int pageNumber(WorkerDebtQuery query) { + return query == null ? 1 : Math.max(query.getPageNumber(), 1); + } + + private int pageSize(WorkerDebtQuery query) { + return query == null ? 10 : Math.max(query.getPageSize(), 10); + } + private boolean isRefundedOrder(MallOrder order, Set refundedOrderIds) { return (order.getStatus() != null && order.getStatus() == 8) || refundedOrderIds.contains(order.getId()); @@ -301,10 +403,6 @@ public class AdminDataStatisticsController { .divide(BigDecimal.valueOf(deliveries.size()), 2, RoundingMode.HALF_UP); } - private BigDecimal subtractWechatFee(BigDecimal value) { - return amount(value).multiply(BigDecimal.ONE.subtract(WECHAT_FEE_RATE)); - } - private BigDecimal amount(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java index d3f3bd18..eb02a8f3 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java @@ -155,7 +155,7 @@ public class AdminSettlementController { QueryWrapper qw = new QueryWrapper<>(); qw.select("shop_id", "COALESCE(SUM(base_amount), 0) AS total_base_amount", - "COALESCE(SUM(commission_amount), 0) AS total_commission_amount", + "COALESCE(SUM(CASE WHEN type = 2 THEN -ABS(commission_amount) ELSE commission_amount END), 0) AS total_commission_amount", "COALESCE(SUM(settlement_amount), 0) AS total_settlement_amount", "COUNT(1) AS record_count", "SUM(CASE WHEN type = 2 THEN 0 ELSE 1 END) AS positive_count", @@ -246,7 +246,7 @@ public class AdminSettlementController { "COUNT(DISTINCT shop_id) AS shop_count", "COUNT(1) AS record_count", "COALESCE(SUM(base_amount), 0) AS total_base_amount", - "COALESCE(SUM(commission_amount), 0) AS total_commission_amount", + "COALESCE(SUM(CASE WHEN type = 2 THEN -ABS(commission_amount) ELSE commission_amount END), 0) AS total_commission_amount", "COALESCE(SUM(settlement_amount), 0) AS total_settlement_amount", "COALESCE(SUM(CASE WHEN type = 2 THEN 0 ELSE settlement_amount END), 0) AS positive_settlement_amount", "COALESCE(SUM(CASE WHEN type = 2 THEN settlement_amount ELSE 0 END), 0) AS negative_settlement_amount"); @@ -342,6 +342,7 @@ public class AdminSettlementController { record.setOrderNumber(order.getNumberCode()); record.setPayTime(order.getPayTime()); } + applyDisplayCommissionAmount(record); } return new ResultUtil>().setData(page); } @@ -375,6 +376,15 @@ public class AdminSettlementController { return value == null ? 0 : Integer.parseInt(String.valueOf(value)); } + private void applyDisplayCommissionAmount(MallSettlementRecord record) { + if (record == null + || !Integer.valueOf(2).equals(record.getType()) + || record.getCommissionAmount() == null) { + return; + } + record.setCommissionAmount(record.getCommissionAmount().abs().negate()); + } + @Data public static class ConfirmShopReq { @ApiModelProperty(value = "选中的商家ID列表") diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallRefundRecordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallRefundRecordMapper.java index f2c01a38..b7fad913 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallRefundRecordMapper.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallRefundRecordMapper.java @@ -5,12 +5,30 @@ import cc.hiver.mall.pojo.query.MallRefundRecordPageQuery; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; +import java.math.BigDecimal; +import java.util.Map; + /** * 退款记录 Mapper æŽ¥å£ */ @Repository public interface MallRefundRecordMapper extends BaseMapper { IPage selectPageVO(IPage page, @Param("q") MallRefundRecordPageQuery q); + + @Update("UPDATE t_worker " + + "SET depo_bal_rel = CASE " + + "WHEN COALESCE(depo_bal, 0) >= #{amount} THEN COALESCE(depo_bal_rel, 0) " + + "ELSE COALESCE(depo_bal_rel, 0) - (#{amount} - COALESCE(depo_bal, 0)) END, " + + "depo_bal = CASE " + + "WHEN COALESCE(depo_bal, 0) >= #{amount} THEN COALESCE(depo_bal, 0) - #{amount} " + + "ELSE 0 END " + + "WHERE worker_id = #{workerId}") + int deductWorkerBalanceAtomic(@Param("workerId") String workerId, @Param("amount") BigDecimal amount); + + @Select("SELECT depo_bal AS depoBal, depo_bal_rel AS depoBalRel FROM t_worker WHERE worker_id = #{workerId}") + Map selectWorkerBalanceForDebug(@Param("workerId") String workerId); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/WorkerDailyBalanceRecordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/WorkerDailyBalanceRecordMapper.java new file mode 100644 index 00000000..f3f65f49 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/WorkerDailyBalanceRecordMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.dao.mapper; + +import cc.hiver.mall.entity.WorkerDailyBalanceRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface WorkerDailyBalanceRecordMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/WorkerDailyBalanceRecord.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/WorkerDailyBalanceRecord.java new file mode 100644 index 00000000..e68da868 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/WorkerDailyBalanceRecord.java @@ -0,0 +1,46 @@ +package cc.hiver.mall.entity; + +import cc.hiver.core.common.utils.SnowFlakeUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Date; + +@Data +@TableName("worker_daily_balance_record") +@ApiModel(value = "é…é€å‘˜æ¯æ—¥ä½™é¢è®°å½•") +public class WorkerDailyBalanceRecord { + + @TableId + @ApiModelProperty(value = "主键") + private String id = SnowFlakeUtil.nextId().toString(); + + @ApiModelProperty(value = "校区/学校ID") + private String regionId; + + @ApiModelProperty(value = "余颿—¥æœŸ") + private LocalDate balanceDate; + + @ApiModelProperty(value = "é…é€å‘˜ID") + private String workerId; + + @ApiModelProperty(value = "é…é€å‘˜å§“å") + private String workerName; + + @ApiModelProperty(value = "手机å·") + private String mobile; + + @ApiModelProperty(value = "昨日佣金余é¢ï¼Œä¿ç•™æ­£è´Ÿæ•°") + private BigDecimal dailyBalance; + + @ApiModelProperty(value = "å˜æ›´åŽå¯æçް余é¢") + private BigDecimal availableBalanceAfter; + + @ApiModelProperty(value = "创建时间") + private Date createTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/MerchantSettlementTask.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/MerchantSettlementTask.java index 8df553d5..3d1d4ec9 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/MerchantSettlementTask.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/quartz/MerchantSettlementTask.java @@ -3,15 +3,13 @@ package cc.hiver.mall.quartz; import cc.hiver.core.entity.Worker; import cc.hiver.core.serviceimpl.WorkerServiceImpl; import cc.hiver.mall.dao.mapper.MallReturnOrderGoodsMapper; -import cc.hiver.mall.entity.MallOrder; -import cc.hiver.mall.entity.MallRefundRecord; -import cc.hiver.mall.entity.MallSettlementRecord; -import cc.hiver.mall.entity.ShopTakeaway; +import cc.hiver.mall.entity.*; import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.ShopTakeawayService; import cc.hiver.mall.service.mybatis.MallOrderService; import cc.hiver.mall.service.mybatis.MallRefundRecordService; import cc.hiver.mall.service.mybatis.MallSettlementRecordService; +import cc.hiver.mall.service.mybatis.WorkerDailyBalanceRecordService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +17,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Date; import java.util.List; @@ -39,6 +38,8 @@ public class MerchantSettlementTask { private WorkerServiceImpl workerServiceImpl; @Autowired private ShopService shopService; + @Autowired + private WorkerDailyBalanceRecordService workerDailyBalanceRecordService; @Scheduled(cron = "0 0 2 * * ?") public void executeSettlement() { @@ -83,11 +84,17 @@ public class MerchantSettlementTask { // é…é€å‘˜ä½™é¢ try { List workers = workerServiceImpl.getAll(); + LocalDate balanceDate = LocalDate.now().minusDays(1); for (Worker worker : workers) { try { - if(worker.getWorkerStatus() != null && worker.getWorkerStatus() != 3 && worker.getDepoBal().signum() > 0){ - worker.setDepoBalRel(worker.getDepoBal().add(worker.getDepoBalRel())); + try { + saveWorkerDailyBalanceRecord(worker, balanceDate); + } catch (Exception e) { + log.error("[MerchantSettlementTask] Failed to save worker daily balance record {}", worker.getWorkerId(), e); + } + if(worker.getWorkerStatus() != null && worker.getWorkerStatus() != 3 && worker.getDepoBal() != null && worker.getDepoBal().signum() != 0){ + worker.setDepoBalRel(worker.getDepoBal().add(safeAmount(worker.getDepoBalRel()))); worker.setDepoBal(BigDecimal.ZERO); workerServiceImpl.update(worker); } @@ -102,6 +109,34 @@ public class MerchantSettlementTask { log.info("[MerchantSettlementTask] Daily settlement calculation finished."); } + private void saveWorkerDailyBalanceRecord(Worker worker, LocalDate balanceDate) { + if (worker == null + || worker.getWorkerStatus() == null + || worker.getWorkerStatus() == 3 + || worker.getDepoBal() == null + || worker.getDepoBal().signum() == 0) { + return; + } + BigDecimal availableBalanceAfter = safeAmount(worker.getDepoBalRel()).add(worker.getDepoBal()); + + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(WorkerDailyBalanceRecord::getBalanceDate, balanceDate); + qw.eq(WorkerDailyBalanceRecord::getWorkerId, worker.getWorkerId()); + WorkerDailyBalanceRecord record = workerDailyBalanceRecordService.getOne(qw, false); + if (record == null) { + record = new WorkerDailyBalanceRecord(); + record.setCreateTime(new Date()); + } + record.setRegionId(worker.getRegion()); + record.setBalanceDate(balanceDate); + record.setWorkerId(worker.getWorkerId()); + record.setWorkerName(worker.getWorkerName()); + record.setMobile(worker.getMobile()); + record.setDailyBalance(worker.getDepoBal()); + record.setAvailableBalanceAfter(availableBalanceAfter); + workerDailyBalanceRecordService.saveOrUpdate(record); + } + private void processUnsettledOrder(MallOrder order) { if (!isMerchantSettlementOrder(order)) { return; @@ -113,7 +148,8 @@ public class MerchantSettlementTask { long count = mallSettlementRecordService.count(existsQw); if (count > 0) return; // Skip if already pending confirmation - saveMerchantSettlement(order, null, 1, goodsPackageAmount(order), false); + saveMerchantSettlement(order, null, 1, goodsPackageAmount(order), false, + hasApprovedWorkerFullRefundOrAfterSale(order)); } private void processApprovedRefund(MallRefundRecord refund) { @@ -153,7 +189,8 @@ public class MerchantSettlementTask { return; } if (Integer.valueOf(2).equals(refund.getRefundTypeStatus()) && isWorkerRecord(refund)) { - saveMerchantSettlement(order, refund.getId(), 1, goodsPackageAmount(order), false); + saveMerchantSettlement(order, refund.getId(), 1, goodsPackageAmount(order), false, + isWorkerFullRefundOrAfterSale(refund)); } else if (Integer.valueOf(1).equals(refund.getRefundTypeStatus()) && isMerchantDeliveryOrder(order) && !isWorkerRecord(refund)) { BigDecimal deliveryResponsibilityAmount = merchantDeliveryResponsibilityAmount(order, refund); saveMerchantSettlement(order, refund.getId(), 2, deliveryResponsibilityAmount, true); @@ -161,6 +198,11 @@ public class MerchantSettlementTask { } private void saveMerchantSettlement(MallOrder order, String refundId, Integer type, BigDecimal baseAmount, boolean deliveryFeeIncluded) { + saveMerchantSettlement(order, refundId, type, baseAmount, deliveryFeeIncluded, false); + } + + private void saveMerchantSettlement(MallOrder order, String refundId, Integer type, BigDecimal baseAmount, + boolean deliveryFeeIncluded, boolean commissionWaived) { if (baseAmount == null || baseAmount.compareTo(BigDecimal.ZERO) <= 0 || existsSettlement(refundId, type, order.getId())) { return; } @@ -169,8 +211,9 @@ public class MerchantSettlementTask { if (commissionBase.compareTo(BigDecimal.ZERO) < 0) { commissionBase = BigDecimal.ZERO; } - BigDecimal actualCommission = commissionBase.multiply(rateDecimal(commissionRate)).setScale(2, BigDecimal.ROUND_HALF_UP); - BigDecimal settlementAmount = baseAmount.subtract(actualCommission); + BigDecimal calculatedCommission = commissionBase.multiply(rateDecimal(commissionRate)).setScale(2, BigDecimal.ROUND_HALF_UP); + BigDecimal actualCommission = commissionWaived ? BigDecimal.ZERO : calculatedCommission; + BigDecimal settlementAmount = baseAmount.subtract(calculatedCommission); if (type != null && type == 2) { settlementAmount = settlementAmount.negate(); } @@ -286,4 +329,24 @@ public class MerchantSettlementTask { private boolean isWorkerRecord(MallRefundRecord refund) { return refund != null && refund.getLinkId() != null && refund.getLinkId().toUpperCase().startsWith("W"); } + + private boolean isWorkerFullRefundOrAfterSale(MallRefundRecord refund) { + return refund != null + && Integer.valueOf(3).equals(refund.getRefundType()) + && Integer.valueOf(2).equals(refund.getRefundTypeStatus()) + && isWorkerRecord(refund); + } + + private boolean hasApprovedWorkerFullRefundOrAfterSale(MallOrder order) { + if (order == null || order.getId() == null) { + return false; + } + LambdaQueryWrapper refundQw = new LambdaQueryWrapper<>(); + refundQw.eq(MallRefundRecord::getOrderId, order.getId()); + refundQw.in(MallRefundRecord::getStatus, 1, 4); + refundQw.eq(MallRefundRecord::getRefundType, 3); + refundQw.eq(MallRefundRecord::getRefundTypeStatus, 2); + refundQw.apply("UPPER(link_id) LIKE {0}", "W%"); + return mallRefundRecordService.count(refundQw) > 0; + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/WorkerDailyBalanceRecordService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/WorkerDailyBalanceRecordService.java new file mode 100644 index 00000000..f17f2f65 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/WorkerDailyBalanceRecordService.java @@ -0,0 +1,7 @@ +package cc.hiver.mall.service.mybatis; + +import cc.hiver.mall.entity.WorkerDailyBalanceRecord; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface WorkerDailyBalanceRecordService extends IService { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java index 684dc6be..827294df 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java @@ -69,6 +69,8 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); - uw.eq(MallDeliveryOrder::getId, deliveryId) - .set(MallDeliveryOrder::getStatus, STATUS_WAIT_PICKUP) - .set(MallDeliveryOrder::getWorkerId, workerId) - .set(MallDeliveryOrder::getWorkerName, workerName) - .set(MallDeliveryOrder::getWorkerPhone, workerPhone) - .set(MallDeliveryOrder::getAcceptTime, acceptTime) - .set(MallDeliveryOrder::getTransferPickupCode, transferPickupCode); + LambdaUpdateWrapper acceptUw = new LambdaUpdateWrapper<>(); + acceptUw.eq(MallDeliveryOrder::getId, deliveryId) + .eq(MallDeliveryOrder::getStatus, STATUS_WAIT_ACCEPT) + .set(MallDeliveryOrder::getStatus, STATUS_WAIT_PICKUP) + .set(MallDeliveryOrder::getWorkerId, workerId) + .set(MallDeliveryOrder::getWorkerName, workerName) + .set(MallDeliveryOrder::getWorkerPhone, workerPhone) + .set(MallDeliveryOrder::getAcceptTime, acceptTime) + .set(MallDeliveryOrder::getTransferPickupCode, transferPickupCode); + if (StringUtils.isBlank(originalWorkerId)) { + acceptUw.and(w -> w.isNull(MallDeliveryOrder::getWorkerId).or().eq(MallDeliveryOrder::getWorkerId, "")); + } else { + acceptUw.eq(MallDeliveryOrder::getWorkerId, workerId); + } if (!appointmentDelivery) { - uw.set(MallDeliveryOrder::getMustFinishTime, mustFinishTime); + acceptUw.set(MallDeliveryOrder::getMustFinishTime, mustFinishTime); } if (transferDelivery) { - uw.set(MallDeliveryOrder::getShopAddress, delivery.getShopAddress()); + acceptUw.set(MallDeliveryOrder::getShopAddress, delivery.getShopAddress()); + } + if (!this.update(acceptUw)) { + return 1; } String numberCode = ""; @@ -464,6 +476,8 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); + uw.eq(MallDeliveryOrder::getId, deliveryId); uw.set(MallDeliveryOrder::getNumberCode, numberCode); this.update(uw); @@ -801,6 +815,10 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl completedOrders, int freeOrderCount) { if (delivery == null || worker == null) { return; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java index cd361f69..6bef38a5 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java @@ -23,16 +23,20 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.Date; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -43,6 +47,8 @@ import java.util.stream.Collectors; public class MallRefundRecordServiceImpl extends ServiceImpl implements MallRefundRecordService { + private static final Logger log = LoggerFactory.getLogger(MallRefundRecordServiceImpl.class); + // 订å•状æ€å¸¸é‡æ•°æ®åº“存储为 "01",用数字10代替 private static final int STATUS_WAIT_SHOP = 1; private static final int STATUS_WAIT_PICKUP = 3; @@ -60,6 +66,12 @@ public class MallRefundRecordServiceImpl extends ServiceImpl RELEASE_WORKER_BALANCE_LOCK_SCRIPT = new DefaultRedisScript<>( + "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", + Long.class + ); @Autowired private MallRefundRecordMapper mallRefundRecordMapper; @@ -275,26 +287,45 @@ public class MallRefundRecordServiceImpl extends ServiceImpl= 0) { - worker.setDepoBal(currentDepoBal.subtract(amount)); - worker.setDepoBalRel(currentDepoBalRel); - } else { - worker.setDepoBal(BigDecimal.ZERO); - worker.setDepoBalRel(currentDepoBalRel.subtract(amount.subtract(currentDepoBal))); + Map beforeBalance = mallRefundRecordMapper.selectWorkerBalanceForDebug(refundRecord.getLinkId()); + log.info("[worker-refund-debug] deduct before atomic refundId={}, workerId={}, amount={}, dbBalance={}", + refundRecord.getId(), refundRecord.getLinkId(), amount, beforeBalance); + int updated = mallRefundRecordMapper.deductWorkerBalanceAtomic(refundRecord.getLinkId(), amount); + Map afterBalance = mallRefundRecordMapper.selectWorkerBalanceForDebug(refundRecord.getLinkId()); + log.info("[worker-refund-debug] deduct after atomic refundId={}, workerId={}, amount={}, updated={}, dbBalance={}", + refundRecord.getId(), refundRecord.getLinkId(), amount, updated, afterBalance); + if (updated <= 0) { + log.info("[worker-refund-debug] skip deduct dealings, worker balance row not updated refundId={}, workerId={}", + refundRecord.getId(), refundRecord.getLinkId()); + return; + } + Worker worker = workerServiceImpl.findByWorkerId(refundRecord.getLinkId()); + if (worker == null) { + log.info("[worker-refund-debug] skip deduct dealings, worker not found after atomic update refundId={}, workerId={}", + refundRecord.getId(), refundRecord.getLinkId()); + return; } - workerServiceImpl.update(worker); saveWorkerDealingsRecord( worker, refundRecord.getId(), @@ -304,6 +335,8 @@ public class MallRefundRecordServiceImpl extends ServiceImpl 0) { return; } + String lockKey = buildWorkerBalanceLockKey(delivery.getWorkerId()); + String lockToken = lockWorkerBalance(lockKey); + if (lockToken != null) { + releaseWorkerBalanceLockAfterCompletion(lockKey, lockToken); + } Worker worker = workerServiceImpl.findByWorkerId(delivery.getWorkerId()); if (worker == null) { return; @@ -334,6 +372,68 @@ public class MallRefundRecordServiceImpl extends ServiceImpl + implements WorkerDailyBalanceRecordService { +} diff --git a/hiver-modules/hiver-mall/src/main/resources/db/worker_daily_balance_record.sql b/hiver-modules/hiver-mall/src/main/resources/db/worker_daily_balance_record.sql new file mode 100644 index 00000000..c906bb7b --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/resources/db/worker_daily_balance_record.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS `worker_daily_balance_record` ( + `id` varchar(64) NOT NULL COMMENT '主键ID', + `region_id` varchar(64) DEFAULT NULL COMMENT '校区/学校ID', + `balance_date` date NOT NULL COMMENT 'ä½™é¢å½’属日期,凌晨任务按昨日ä¿å­˜', + `worker_id` varchar(64) NOT NULL COMMENT 'é…é€å‘˜ID', + `worker_name` varchar(128) DEFAULT NULL COMMENT 'é…é€å‘˜å§“å', + `mobile` varchar(32) DEFAULT NULL COMMENT '手机å·', + `daily_balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '昨日佣金余é¢ï¼Œä¿ç•™æ­£è´Ÿæ•°', + `available_balance_after` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT 'å˜æ›´åŽå¯æçް余é¢', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_worker_balance_date` (`balance_date`, `worker_id`), + KEY `idx_worker_balance_region_date` (`region_id`, `balance_date`), + KEY `idx_worker_balance_amount` (`daily_balance`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é…é€å‘˜æ¯æ—¥ä½™é¢è®°å½•表';