package cn.iocoder.yudao.module.trade.service.brokerage; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.*; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO; import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO; import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO; import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO; import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageRecordConvert; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageRecordMapper; import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageAddReqBO; import cn.iocoder.yudao.module.trade.service.brokerage.bo.UserBrokerageSummaryRespBO; import cn.iocoder.yudao.module.trade.service.config.TradeConfigService; import com.baomidou.mybatisplus.core.metadata.IPage; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMinValue; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH; /** * 佣金记录 Service 实现类 * * @author owen */ @Slf4j @Service @Validated public class BrokerageRecordServiceImpl implements BrokerageRecordService { @Resource private BrokerageRecordMapper brokerageRecordMapper; @Resource private TradeConfigService tradeConfigService; @Resource private BrokerageUserService brokerageUserService; @Resource private ProductSpuApi productSpuApi; @Resource private ProductSkuApi productSkuApi; @Override public BrokerageRecordDO getBrokerageRecord(Long id) { return brokerageRecordMapper.selectById(id); } @Override public PageResult getBrokerageRecordPage(BrokerageRecordPageReqVO pageReqVO) { return brokerageRecordMapper.selectPage(pageReqVO); } @Override @Transactional(rollbackFor = Exception.class) public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, List list) { TradeConfigDO memberConfig = tradeConfigService.getTradeConfig(); // 0 未启用分销功能 if (memberConfig == null || !BooleanUtil.isTrue(memberConfig.getBrokerageEnabled())) { log.error("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) bizType({}) list({})", userId, bizType, list); return; } // 1.1 获得一级推广人 BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId); if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) { return; } // 1.2 计算一级分佣 addBrokerage(firstUser, list, memberConfig.getBrokerageFrozenDays(), memberConfig.getBrokerageFirstPercent(), bizType, 1); // 2.1 获得二级推广员 if (firstUser.getBindUserId() == null) { return; } BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId()); if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) { return; } // 2.2 计算二级分佣 addBrokerage(secondUser, list, memberConfig.getBrokerageFrozenDays(), memberConfig.getBrokerageSecondPercent(), bizType, 2); } @Override @Transactional(rollbackFor = Exception.class) public void cancelBrokerage(BrokerageRecordBizTypeEnum bizType, String bizId) { List records = brokerageRecordMapper.selectListByBizTypeAndBizId(bizType.getType(), bizId); if (CollUtil.isEmpty(records)) { log.error("[cancelBrokerage][bizId({}) bizType({}) 更新为已失效失败:记录不存在]", bizId, bizType); return; } records.forEach(record -> { // 1. 更新佣金记录为已失效 BrokerageRecordDO updateObj = new BrokerageRecordDO().setStatus(BrokerageRecordStatusEnum.CANCEL.getStatus()); int updateRows = brokerageRecordMapper.updateByIdAndStatus(record.getId(), record.getStatus(), updateObj); if (updateRows == 0) { log.error("[cancelBrokerage][record({}) 更新为已失效失败]", record.getId()); return; } // 2. 更新用户的佣金 if (BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus().equals(record.getStatus())) { brokerageUserService.updateUserFrozenPrice(record.getUserId(), -record.getPrice()); } else if (BrokerageRecordStatusEnum.SETTLEMENT.getStatus().equals(record.getStatus())) { brokerageUserService.updateUserPrice(record.getUserId(), -record.getPrice()); } }); } /** * 计算佣金 * * @param basePrice 佣金基数 * @param percent 佣金比例 * @param fixedPrice 固定佣金 * @return 佣金 */ int calculatePrice(Integer basePrice, Integer percent, Integer fixedPrice) { // 1. 优先使用固定佣金 if (fixedPrice != null && fixedPrice >= 0) { return ObjectUtil.defaultIfNull(fixedPrice, 0); } // 2. 根据比例计算佣金 if (basePrice != null && basePrice > 0 && percent != null && percent > 0) { return MoneyUtils.calculateRatePriceFloor(basePrice, Double.valueOf(percent)); } return 0; } /** * 增加用户佣金 * * @param user 用户 * @param list 佣金增加参数列表 * @param brokerageFrozenDays 冻结天数 * @param brokeragePercent 佣金比例 * @param bizType 业务类型 * @param sourceUserLevel 来源用户等级 */ private void addBrokerage(BrokerageUserDO user, List list, Integer brokerageFrozenDays, Integer brokeragePercent, BrokerageRecordBizTypeEnum bizType, Integer sourceUserLevel) { // 1.1 处理冻结时间 LocalDateTime unfreezeTime = null; if (brokerageFrozenDays != null && brokerageFrozenDays > 0) { unfreezeTime = LocalDateTime.now().plusDays(brokerageFrozenDays); } // 1.2 计算分佣 int totalBrokerage = 0; List records = new ArrayList<>(); for (BrokerageAddReqBO item : list) { // 计算金额 Integer fixedPrice; if (Objects.equals(sourceUserLevel, 1)) { fixedPrice = item.getFirstFixedPrice(); } else if (Objects.equals(sourceUserLevel, 2)) { fixedPrice = item.getSecondFixedPrice(); } else { throw new IllegalArgumentException(StrUtil.format("用户等级({}) 不合法", sourceUserLevel)); } int brokeragePrice = calculatePrice(item.getBasePrice(), brokeragePercent, fixedPrice); if (brokeragePrice <= 0) { continue; } totalBrokerage += brokeragePrice; // 创建记录实体 records.add(BrokerageRecordConvert.INSTANCE.convert(user, bizType, item.getBizId(), brokerageFrozenDays, brokeragePrice, unfreezeTime, item.getTitle(), item.getSourceUserId(), sourceUserLevel)); } if (CollUtil.isEmpty(records)) { return; } // 1.3 保存佣金记录 brokerageRecordMapper.insertBatch(records); // 2. 更新用户佣金 if (brokerageFrozenDays != null && brokerageFrozenDays > 0) { // 更新用户冻结佣金 brokerageUserService.updateUserFrozenPrice(user.getId(), totalBrokerage); } else { // 更新用户可用佣金 brokerageUserService.updateUserPrice(user.getId(), totalBrokerage); } } @Override public int unfreezeRecord() { // 1. 查询待结算的佣金记录 List records = brokerageRecordMapper.selectListByStatusAndUnfreezeTimeLt( BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus(), LocalDateTime.now()); if (CollUtil.isEmpty(records)) { return 0; } // 2. 遍历执行 int count = 0; for (BrokerageRecordDO record : records) { try { boolean success = getSelf().unfreezeRecord(record); if (success) { count++; } } catch (Exception e) { log.error("[unfreezeRecord][record({}) 更新为已结算失败]", record.getId(), e); } } return count; } /** * 解冻单条佣金记录 * * @param record 佣金记录 * @return 解冻是否成功 */ @Transactional(rollbackFor = Exception.class) public boolean unfreezeRecord(BrokerageRecordDO record) { // 更新记录状态 BrokerageRecordDO updateObj = new BrokerageRecordDO() .setStatus(BrokerageRecordStatusEnum.SETTLEMENT.getStatus()) .setUnfreezeTime(LocalDateTime.now()); int updateRows = brokerageRecordMapper.updateByIdAndStatus(record.getId(), record.getStatus(), updateObj); if (updateRows == 0) { log.error("[unfreezeRecord][record({}) 更新为已结算失败]", record.getId()); return false; } // 更新用户冻结佣金 brokerageUserService.updateFrozenPriceDecrAndPriceIncr(record.getUserId(), -record.getPrice()); log.info("[unfreezeRecord][record({}) 更新为已结算成功]", record.getId()); return true; } @Override public List getUserBrokerageSummaryListByUserId(Collection userIds, Integer bizType, Integer status) { if (CollUtil.isEmpty(userIds)) { return Collections.emptyList(); } return brokerageRecordMapper.selectCountAndSumPriceByUserIdInAndBizTypeAndStatus(userIds, bizType, status); } @Override public Integer getSummaryPriceByUserId(Long userId, BrokerageRecordBizTypeEnum bizType, BrokerageRecordStatusEnum status, LocalDateTime beginTime, LocalDateTime endTime) { return brokerageRecordMapper.selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(userId, bizType.getType(), status.getStatus(), beginTime, endTime); } @Override public PageResult getBrokerageUserChildSummaryPageByPrice(AppBrokerageUserRankPageReqVO pageReqVO) { IPage pageResult = brokerageRecordMapper.selectSummaryPricePageGroupByUserId( MyBatisUtils.buildPage(pageReqVO), BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), ArrayUtil.get(pageReqVO.getTimes(), 0), ArrayUtil.get(pageReqVO.getTimes(), 1)); return new PageResult<>(pageResult.getRecords(), pageResult.getTotal()); } @Override public Integer getUserRankByPrice(Long userId, LocalDateTime[] times) { // 用户的推广金额 Integer price = brokerageRecordMapper.selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(userId, BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), ArrayUtil.get(times, 0), ArrayUtil.get(times, 1)); // 排在用户前面的人数 Integer greaterCount = brokerageRecordMapper.selectCountByPriceGt(price, BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), ArrayUtil.get(times, 0), ArrayUtil.get(times, 1)); // 获得排名 return ObjUtil.defaultIfNull(greaterCount, 0) + 1; } @Override @Transactional(rollbackFor = Exception.class) public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, String bizId, Integer brokeragePrice, String title) { // 1. 校验佣金余额 BrokerageUserDO user = brokerageUserService.getBrokerageUser(userId); int balance = Optional.of(user) .map(BrokerageUserDO::getBrokeragePrice).orElse(0); if (balance + brokeragePrice < 0) { throw exception(BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH, MoneyUtils.fenToYuanStr(balance)); } // 2. 更新佣金余额 boolean success = brokerageUserService.updateUserPrice(userId, brokeragePrice); if (!success) { // 失败时,则抛出异常。只会出现扣减佣金时,余额不足的情况 throw exception(BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH, MoneyUtils.fenToYuanStr(balance)); } // 3. 新增记录 BrokerageRecordDO record = BrokerageRecordConvert.INSTANCE.convert(user, bizType, bizId, 0, brokeragePrice, null, title, null, null); brokerageRecordMapper.insert(record); } @Override public AppBrokerageProductPriceRespVO calculateProductBrokeragePrice(Long userId, Long spuId) { // 1. 构建默认的返回值 AppBrokerageProductPriceRespVO respVO = new AppBrokerageProductPriceRespVO().setEnabled(false) .setBrokerageMinPrice(0).setBrokerageMaxPrice(0); // 2.1 校验分销功能是否开启 TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig(); if (tradeConfig == null || BooleanUtil.isFalse(tradeConfig.getBrokerageEnabled())) { return respVO; } // 2.2 校验用户是否有分销资格 respVO.setEnabled(brokerageUserService.getUserBrokerageEnabled(getLoginUserId())); if (BooleanUtil.isFalse(respVO.getEnabled())) { return respVO; } // 2.3 校验商品是否存在 ProductSpuRespDTO spu = productSpuApi.getSpu(spuId); if (spu == null) { return respVO; } // 3.1 商品单独分佣模式 Integer fixedMinPrice = 0; Integer fixedMaxPrice = 0; Integer spuMinPrice = 0; Integer spuMaxPrice = 0; List skuList = productSkuApi.getSkuListBySpuId(ListUtil.of(spuId)); if (BooleanUtil.isTrue(spu.getSubCommissionType())) { fixedMinPrice = getMinValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice); fixedMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice); // 3.2 全局分佣模式(根据商品价格比例计算) } else { spuMinPrice = getMinValue(skuList, ProductSkuRespDTO::getPrice); spuMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getPrice); } respVO.setBrokerageMinPrice(calculatePrice(spuMinPrice, tradeConfig.getBrokerageFirstPercent(), fixedMinPrice)); respVO.setBrokerageMaxPrice(calculatePrice(spuMaxPrice, tradeConfig.getBrokerageFirstPercent(), fixedMaxPrice)); return respVO; } /** * 获得自身的代理对象,解决 AOP 生效问题 * * @return 自己 */ private BrokerageRecordServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } }