diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index 56db4bd9..1531ffda 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • -°ËÔ 26, 2026 18:43:14 +¾ÅÔ 02, 2026 15:11:22
  • @@ -84,7 +84,7 @@

    passTest

    -

    18:43:15 ÏÂÎç / 0.04 secs

    +

    15:11:23 ÏÂÎç / 0.028 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -08.26.2026 18:43:15 -08.26.2026 18:43:15 -0.04 secs +09.02.2026 15:11:23 +09.02.2026 15:11:23 +0.028 secs
    @@ -104,7 +104,7 @@ Pass - 18:43:15 + 15:11:23 Test passed @@ -128,13 +128,13 @@

    Started

    -

    °ËÔÂ 26, 2026 18:43:14

    +

    ¾ÅÔ 02, 2026 15:11:22

    Ended

    -

    °ËÔÂ 26, 2026 18:43:15

    +

    ¾ÅÔ 02, 2026 15:11:23

    diff --git a/hiver-core/src/main/java/cc/hiver/core/common/constant/DealingsRecordConstant.java b/hiver-core/src/main/java/cc/hiver/core/common/constant/DealingsRecordConstant.java index fd68f378..c0475b36 100644 --- a/hiver-core/src/main/java/cc/hiver/core/common/constant/DealingsRecordConstant.java +++ b/hiver-core/src/main/java/cc/hiver/core/common/constant/DealingsRecordConstant.java @@ -15,7 +15,7 @@ public interface DealingsRecordConstant { /** * */ - String[] TYPE_REMARK = {"é…é€è®¢å•佣金","å”®åŽé€€æ¬¾","é…逿ޒå奖励","商家æçް","é…é€å‘˜æçް","å…å•奖励","商家结算","用户æçް","白嫖中奖","学院排ä½èµ›æŽ’å奖励","é…é€å‘˜åŽŸå› é€€æ¬¾æ‰£æ¬¾","商家原因退款补å‘é…é€ä½£é‡‘","商家退款/å”®åŽæ‰£æ¬¾"}; + String[] TYPE_REMARK = {"é…é€è®¢å•佣金","å”®åŽé€€æ¬¾","é…逿ޒå奖励","商家æçް","é…é€å‘˜æçް","å…å•奖励","商家结算","用户æçް","白嫖中奖","学院排ä½èµ›æŽ’å奖励","é…é€å‘˜åŽŸå› é€€æ¬¾æ‰£æ¬¾","商家原因退款补å‘é…é€ä½£é‡‘","商家退款/å”®åŽæ‰£æ¬¾","二级代ç†è¿”ä½£"}; /** *交易类型:0:增加 1 æçް 2消费 3å”®åŽ diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java index 19f25ce2..b1b957a4 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java @@ -20,6 +20,8 @@ import cc.hiver.mall.utils.AliPayNewUtil; import com.alipay.api.AlipayApiException; import com.alipay.api.response.AlipayFundTransUniTransferResponse; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -123,6 +125,32 @@ public class ReturnCommissionController { return new ResultUtil>().setData(list); } + @RequestMapping(value = "/page", method = RequestMethod.GET) + @ApiOperation(value = "分页查询æçŽ°è®°å½•", httpMethod = "GET") + public Result page(@RequestParam(value = "shopId", required = false) String shopId, + @RequestParam(value = "workerId", required = false) String workerId, + @RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum, + @RequestParam(value = "pageNumber", required = false) Integer pageNumber, + @RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) { + if (StringUtils.isEmpty(shopId) && StringUtils.isEmpty(workerId)) { + return ResultUtil.error("店铺id或é…é€å‘˜idä¸èƒ½ä¸ºç©ºï¼"); + } + final QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isEmpty(shopId)) { + queryWrapper.eq("shop_id", shopId); + } + if (!StringUtils.isEmpty(workerId)) { + queryWrapper.eq("worker_id", workerId); + } + queryWrapper.orderByDesc("create_time"); + final Integer currentPage = pageNumber == null ? pageNum : pageNumber; + final IPage page = returnCommissionService.page( + new Page<>(defaultPage(currentPage), defaultPageSize(pageSize)), + queryWrapper + ); + return new ResultUtil>().setData(page); + } + @Transactional @RequestMapping(value = "/handleReturn", method = RequestMethod.POST) @ApiOperation("店铺æçް处ç†") @@ -523,6 +551,17 @@ public class ReturnCommissionController { return returnCommission.getId(); } + private int defaultPage(Integer pageNum) { + return pageNum == null || pageNum < 1 ? 1 : pageNum; + } + + private int defaultPageSize(Integer pageSize) { + if (pageSize == null || pageSize < 1) { + return 10; + } + return Math.min(pageSize, 100); + } + private String buildRequestLockKey(String withdrawId) { return "return_commission:request:" + withdrawId; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java index 2d68b2b3..7ee76b6a 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java @@ -128,6 +128,10 @@ public class ShopAreaController { }else{ ShopArea.setIsParent(true); } + Result validateResult = normalizeRootAreaAgent(ShopArea); + if (validateResult != null) { + return validateResult; + } ShopArea d = shopAreaService.save(ShopArea); // å¦‚æžœä¸æ˜¯æ·»åŠ çš„ä¸€çº§ 判断设置上级为父节点标识 if (!CommonConstant.PARENT_ID.equals(ShopArea.getParentId())) { @@ -155,6 +159,10 @@ public class ShopAreaController { if (ShopArea.getId().equals(ShopArea.getParentId())) { return ResultUtil.error("上级节点ä¸èƒ½ä¸ºè‡ªå·±"); } + Result validateResult = normalizeRootAreaAgent(ShopArea); + if (validateResult != null) { + return validateResult; + } ShopArea old = shopAreaService.get(ShopArea.getId()); String oldParentId = old.getParentId(); String oldTitle = old.getTitle(); @@ -276,4 +284,22 @@ public class ShopAreaController { }); } + + private Result normalizeRootAreaAgent(ShopArea shopArea) { + if (!CommonConstant.PARENT_ID.equals(shopArea.getParentId())) { + shopArea.setParentAgentId(null); + shopArea.setParentAgentName(null); + shopArea.setCommissionRatio(0); + return null; + } + Integer commissionRatio = shopArea.getCommissionRatio(); + if (commissionRatio == null) { + shopArea.setCommissionRatio(0); + return null; + } + if (commissionRatio < 0 || commissionRatio > 100) { + return ResultUtil.error("返佣比例需为0或1-100之间的整数"); + } + return null; + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/SeckillGroupProduct.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/SeckillGroupProduct.java index 80ee4bf6..713f7182 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/SeckillGroupProduct.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/SeckillGroupProduct.java @@ -74,6 +74,9 @@ public class SeckillGroupProduct extends HiverBaseEntity { @ApiModelProperty(value = "商家所属圈层IDå¿«ç…§") private String shoArea; + @ApiModelProperty(value = "商家类型:1外å–商家 2团购商家") + private Integer merchantType; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "ç§’æ€å¼€å§‹æ—¶é—´") diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ShopArea.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ShopArea.java index bd69cc40..93123225 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ShopArea.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ShopArea.java @@ -55,6 +55,15 @@ public class ShopArea extends HiverBaseEntity { @Column(nullable = false, columnDefinition = "int default 1") private Integer deliveryEnabled = 1; + @ApiModelProperty(value = "上级代ç†id") + private String parentAgentId; + + @ApiModelProperty(value = "上级代ç†åç§°") + private String parentAgentName; + + @ApiModelProperty(value = "返佣比例,按百分比计算") + private Integer commissionRatio = 0; + @Transient @TableField(exist = false) @ApiModelProperty(value = "父节点åç§°") diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/SeckillGroupProductDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/SeckillGroupProductDTO.java index 11af5b49..91625859 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/SeckillGroupProductDTO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/SeckillGroupProductDTO.java @@ -37,6 +37,9 @@ public class SeckillGroupProductDTO { @ApiModelProperty(value = "商家所属圈层ID") private String shoArea; + @ApiModelProperty(value = "商家类型:1外å–商家 2团购商家") + private Integer merchantType; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "ç§’æ€å¼€å§‹æ—¶é—´") diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/SeckillGroupProductVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/SeckillGroupProductVO.java index e2cb22d8..b23fe1b9 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/SeckillGroupProductVO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/SeckillGroupProductVO.java @@ -74,6 +74,9 @@ public class SeckillGroupProductVO { @ApiModelProperty(value = "商家所属圈层ID") private String shoArea; + @ApiModelProperty(value = "商家类型:1外å–商家 2团购商家") + private Integer merchantType; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "ç§’æ€å¼€å§‹æ—¶é—´") 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 3d1d4ec9..0b7ef280 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 @@ -1,29 +1,38 @@ package cc.hiver.mall.quartz; +import cc.hiver.core.common.constant.CommonConstant; +import cc.hiver.core.common.constant.DealingsRecordConstant; +import cc.hiver.core.entity.User; import cc.hiver.core.entity.Worker; +import cc.hiver.core.service.UserService; import cc.hiver.core.serviceimpl.WorkerServiceImpl; +import cc.hiver.mall.dao.ShopAreaDao; import cc.hiver.mall.dao.mapper.MallReturnOrderGoodsMapper; 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 cc.hiver.mall.service.mybatis.*; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.Date; import java.util.List; @Slf4j @Component public class MerchantSettlementTask { + private static final BigDecimal ONE_PERCENT = new BigDecimal("0.01"); + private static final String SECOND_AGENT_REBATE_LINK_PREFIX = "SECOND_AGENT_REBATE:"; + @Autowired private MallOrderService mallOrderService; @Autowired @@ -40,6 +49,12 @@ public class MerchantSettlementTask { private ShopService shopService; @Autowired private WorkerDailyBalanceRecordService workerDailyBalanceRecordService; + @Autowired + private ShopAreaDao shopAreaDao; + @Autowired + private UserService userService; + @Autowired + private DealingsRecordService dealingsRecordService; @Scheduled(cron = "0 0 2 * * ?") public void executeSettlement() { @@ -109,6 +124,26 @@ public class MerchantSettlementTask { log.info("[MerchantSettlementTask] Daily settlement calculation finished."); } + @Scheduled(cron = "0 0 3 * * ?") + public void executeSecondAgentRebate() { + LocalDate settlementDate = LocalDate.now().minusDays(1); + log.info("[MerchantSettlementTask] Starting second agent rebate, date={}", settlementDate); + try { + List areas = shopAreaDao.findByParentIdOrderBySortOrder(CommonConstant.PARENT_ID); + for (ShopArea area : areas) { + try { + processSecondAgentRebate(area, settlementDate); + } catch (Exception e) { + log.error("[MerchantSettlementTask] Failed to process second agent rebate, regionId={}", + area == null ? null : area.getId(), e); + } + } + } catch (Exception e) { + log.error("[MerchantSettlementTask] Error processing second agent rebates", e); + } + log.info("[MerchantSettlementTask] Second agent rebate finished, date={}", settlementDate); + } + private void saveWorkerDailyBalanceRecord(Worker worker, LocalDate balanceDate) { if (worker == null || worker.getWorkerStatus() == null @@ -137,6 +172,106 @@ public class MerchantSettlementTask { workerDailyBalanceRecordService.saveOrUpdate(record); } + private void processSecondAgentRebate(ShopArea area, LocalDate settlementDate) { + if (area == null + || StringUtils.isBlank(area.getId()) + || StringUtils.isBlank(area.getParentAgentId()) + || area.getCommissionRatio() == null + || area.getCommissionRatio() <= 0) { + return; + } + if (area.getCommissionRatio() > 100) { + log.warn("[MerchantSettlementTask] Skip second agent rebate, invalid commissionRatio={}, regionId={}", + area.getCommissionRatio(), area.getId()); + return; + } + + String linkId = SECOND_AGENT_REBATE_LINK_PREFIX + area.getId() + ":" + settlementDate; + String dealingsWay = safeText(area.getTitle()) + DealingsRecordConstant.TYPE_REMARK[13]; + if (existsSecondAgentRebate(linkId)) { + log.info("[MerchantSettlementTask] Second agent rebate already exists, regionId={}, date={}", + area.getId(), settlementDate); + return; + } + + BigDecimal headquartersDeduction = calculateHeadquartersDeduction(area.getId(), settlementDate); + if (headquartersDeduction.compareTo(BigDecimal.ZERO) <= 0) { + return; + } + + BigDecimal rebateAmount = money(headquartersDeduction + .multiply(rateDecimal(BigDecimal.valueOf(area.getCommissionRatio())))); + if (rebateAmount.compareTo(BigDecimal.ZERO) <= 0) { + return; + } + + User agent = userService.findById(area.getParentAgentId()); + if (agent == null) { + log.warn("[MerchantSettlementTask] Skip second agent rebate, agent not found, regionId={}, agentId={}", + area.getId(), area.getParentAgentId()); + return; + } + + agent.setDepoBal(safeAmount(agent.getDepoBal()).add(rebateAmount)); + userService.update(agent); + + DealingsRecord dealingsRecord = new DealingsRecord(); + dealingsRecord.setDealingsWay(dealingsWay); + dealingsRecord.setAmount(rebateAmount); + dealingsRecord.setRegionId(area.getId()); + dealingsRecord.setLinkUserId(agent.getId()); + dealingsRecord.setLinkId(linkId); + dealingsRecord.setDealingsTime(new Date()); + dealingsRecord.setLinkType(DealingsRecordConstant.TYPE[0]); + dealingsRecord.setDealingsType(DealingsRecordConstant.DEALINGS_TYPE[0]); + dealingsRecord.setCreateBy(agent.getId()); + dealingsRecord.setCreateByName(agent.getNickname()); + dealingsRecord.setRemark("结算日期:" + settlementDate + + ",返佣比例:" + area.getCommissionRatio() + "%"); + dealingsRecordService.save(dealingsRecord); + + log.info("[MerchantSettlementTask] Second agent rebate success, regionId={}, agentId={}, amount={}, date={}", + area.getId(), agent.getId(), rebateAmount, settlementDate); + } + + private boolean existsSecondAgentRebate(String linkId) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(DealingsRecord::getLinkId, linkId); + return dealingsRecordService.count(qw) > 0; + } + + private BigDecimal calculateHeadquartersDeduction(String regionId, LocalDate settlementDate) { + Date start = startOfDay(settlementDate); + Date end = endOfDay(settlementDate); + + List orders = mallOrderService.list(new LambdaQueryWrapper() + .eq(MallOrder::getRegionId, regionId) + .ge(MallOrder::getPayTime, start) + .le(MallOrder::getPayTime, end) + .notIn(MallOrder::getStatus, 0, 6)); + + List refunds = mallRefundRecordService.list(new LambdaQueryWrapper() + .eq(MallRefundRecord::getRegionId, regionId) + .ge(MallRefundRecord::getSuccessTime, start) + .le(MallRefundRecord::getSuccessTime, end) + .in(MallRefundRecord::getStatus, 1, 4)); + + List workerBalanceRecords = workerDailyBalanceRecordService.list( + new LambdaQueryWrapper() + .eq(WorkerDailyBalanceRecord::getRegionId, regionId) + .eq(WorkerDailyBalanceRecord::getBalanceDate, settlementDate)); + + BigDecimal grossFlow = orders.stream() + .map(MallOrder::getTotalAmount) + .map(this::safeAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal refundAmount = sumRefundAmount(refunds, 1); + BigDecimal afterSaleAmount = sumRefundAmount(refunds, 4); + BigDecimal effectiveFlow = grossFlow.subtract(refundAmount).subtract(afterSaleAmount); + BigDecimal deliveryFeeAmount = sumPositiveWorkerBalance(workerBalanceRecords); + return effectiveFlow.subtract(deliveryFeeAmount).multiply(ONE_PERCENT); + } + private void processUnsettledOrder(MallOrder order) { if (!isMerchantSettlementOrder(order)) { return; @@ -278,6 +413,42 @@ public class MerchantSettlementTask { return amount == null ? BigDecimal.ZERO : amount; } + private BigDecimal money(BigDecimal amount) { + return safeAmount(amount).setScale(2, BigDecimal.ROUND_HALF_UP); + } + + private BigDecimal sumRefundAmount(List refunds, int status) { + return refunds.stream() + .filter(item -> item.getStatus() != null && item.getStatus() == status) + .map(MallRefundRecord::getRefundAmount) + .map(this::safeAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal sumPositiveWorkerBalance(List records) { + return records.stream() + .map(WorkerDailyBalanceRecord::getDailyBalance) + .map(this::safeAmount) + .filter(amount -> amount.compareTo(BigDecimal.ZERO) > 0) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private Date startOfDay(LocalDate date) { + return Date.from(LocalDateTime.of(date, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .toInstant()); + } + + private Date endOfDay(LocalDate date) { + return Date.from(LocalDateTime.of(date, LocalTime.MAX) + .atZone(ZoneId.systemDefault()) + .toInstant()); + } + + private String safeText(String value) { + return value == null ? "" : value; + } + private BigDecimal goodsPackageAmount(MallOrder order) { if (order == null) { return BigDecimal.ZERO; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/SeckillGroupProductServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/SeckillGroupProductServiceImpl.java index 032acbf5..dd47937e 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/SeckillGroupProductServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/SeckillGroupProductServiceImpl.java @@ -363,6 +363,7 @@ public class SeckillGroupProductServiceImpl extends ServiceImpl + diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/ShopAreaMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/ShopAreaMapper.xml index d9d5d8d8..eda1c4f8 100644 --- a/hiver-modules/hiver-mall/src/main/resources/mapper/ShopAreaMapper.xml +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/ShopAreaMapper.xml @@ -15,10 +15,14 @@ + + + id, create_by, create_time, del_flag, update_by, update_time, is_parent, parent_id, - sort_order, status, title, is_canteen, delivery_enabled + sort_order, status, title, is_canteen, delivery_enabled, parent_agent_id, parent_agent_name, + commission_ratio