diff --git a/src/main/java/com/gxwebsoft/common/core/enums/ShopDealerTypeEnum.java b/src/main/java/com/gxwebsoft/common/core/enums/ShopDealerTypeEnum.java index 51cb3be..55fce65 100644 --- a/src/main/java/com/gxwebsoft/common/core/enums/ShopDealerTypeEnum.java +++ b/src/main/java/com/gxwebsoft/common/core/enums/ShopDealerTypeEnum.java @@ -13,7 +13,8 @@ public enum ShopDealerTypeEnum { FREEZE_ACCOUNT(1, "操作冻结账户余额"), WITHDRAW_ACCOUNT(2, "操作提现账户余额【直接结算】"), - DEFROST(3, "解冻"); + DEFROST(3, "解冻"), + ORDER_REFUND(4, "退单"); private final Integer code; private final String desc; diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerUserController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerUserController.java index 9cecbd2..fd4a688 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerUserController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerUserController.java @@ -11,6 +11,7 @@ import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.shop.dto.ShopDealerRefundDto; import com.gxwebsoft.shop.dto.ShopDealerUserReduceDto; import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.param.ShopDealerUserImportParam; @@ -185,4 +186,11 @@ public class ShopDealerUserController extends BaseController { return success(shopDealerUserService.reduceBalance(reduceDto)); } + @Operation(summary = "分销退单") + @PostMapping("/refundOrder") + @Deprecated + public ApiResult refundOrder(@RequestBody ShopDealerRefundDto refundDto) { + return success(shopDealerUserService.refundOrder(refundDto)); + } + } diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java index 57fb70d..0127250 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java @@ -256,7 +256,8 @@ public class ShopDealerWithdrawController extends BaseController { } // 使用提现记录ID构造单号,保持幂等;微信要求 5-32 且仅字母/数字 - String outBillNo = String.format("WD%03d", db.getId()); + String outBillNo = db.getOrderNo(); + String remark = "分销商提现"; String userName = db.getRealName(); diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java index efba38e..95cea99 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java @@ -17,6 +17,7 @@ import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.system.entity.Payment; import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.redis.OrderNoUtils; import com.gxwebsoft.glt.service.GltTicketIssueService; import com.gxwebsoft.glt.service.GltTicketRevokeService; import com.gxwebsoft.payment.dto.PaymentResponse; @@ -111,6 +112,8 @@ public class ShopOrderController extends BaseController { private ShopDealerCommissionRollbackService shopDealerCommissionRollbackService; @Resource private GltTicketIssueService gltTicketIssueService; + @Resource + private OrderNoUtils orderNoUtils; @Operation(summary = "分页查询订单") @GetMapping("/page") @@ -493,12 +496,12 @@ public class ShopOrderController extends BaseController { if (!Boolean.TRUE.equals(current.getPayStatus())) { return fail("订单未支付,无法退款"); } - if (StrUtil.isNotBlank(current.getRefundOrder())) { - logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", current.getOrderNo(), current.getRefundOrder()); + if (StrUtil.isNotBlank(current.getRefundOrder()) || current.getOrderStatus() == 6) { + logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", current.getOrderNo(), current.getRefundOrder() != null ? current.getRefundOrder() : ""); return fail("订单已退款,请勿重复操作"); } - String refundNo = "RF" + IdUtil.getSnowflakeNextId(); + String refundNo = orderNoUtils.generate("RF"); BigDecimal refundAmount = req.getRefundMoney(); if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) { @@ -572,7 +575,7 @@ public class ShopOrderController extends BaseController { rollbackOrder.setOrderNo(current.getOrderNo()); rollbackOrder.setPayPrice(current.getPayPrice()); rollbackOrder.setTotalPrice(current.getTotalPrice()); - boolean rollbackOk = shopDealerCommissionRollbackService.rollbackOnOrderRefund(rollbackOrder, refundAmount); + boolean rollbackOk = shopDealerCommissionRollbackService.rollbackOnOrderRefundV2(rollbackOrder, refundAmount); if (!rollbackOk) { logger.error("退款成功但回退分红/分润/佣金失败 - tenantId={}, orderId={}, orderNo={}", tenantId, current.getOrderId(), current.getOrderNo()); diff --git a/src/main/java/com/gxwebsoft/shop/dto/ShopDealerRefundDto.java b/src/main/java/com/gxwebsoft/shop/dto/ShopDealerRefundDto.java new file mode 100644 index 0000000..c1576f3 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/dto/ShopDealerRefundDto.java @@ -0,0 +1,28 @@ +package com.gxwebsoft.shop.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * 分销订单退款 + * @author xm + * @since 2026-05-13 + */ +@Data +@Schema(name = "ShopDealerUserReduceDto", description = "分销订单退款") +public class ShopDealerRefundDto { + + @Schema(description = "订单号") + @NotNull(message = "变订单号不可为空!") + private String orderNo; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "退款金额") + private BigDecimal refundAmount; + +} diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java b/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java index 864b842..cbddfe8 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopDealerCapital.java @@ -35,7 +35,7 @@ public class ShopDealerCapital implements Serializable { @Schema(description = "分销商用户ID") private Integer userId; - @Schema(description = "变动类型 1-操作冻结账户余额 2-操作提现账户余额【直接结算】 3-解冻") + @Schema(description = "变动类型 1-操作冻结账户余额 2-操作提现账户余额【直接结算】 3-解冻 4-退款") private Integer type; @Schema(description = "分销商昵称") @@ -49,7 +49,7 @@ public class ShopDealerCapital implements Serializable { @TableField(exist = false) private Integer orderStatus; - @Schema(description = "资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励 70佣金退回【退单】)") + @Schema(description = "资金流动类型 (10分销收入 11团队管理津贴收入 12分红收入 13现场推广收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励 70佣金退回【退单】)") private Integer flowType; @Schema(description = "变更金额") diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopDealerCommissionRollbackService.java b/src/main/java/com/gxwebsoft/shop/service/ShopDealerCommissionRollbackService.java index e0fca92..5dbd54f 100644 --- a/src/main/java/com/gxwebsoft/shop/service/ShopDealerCommissionRollbackService.java +++ b/src/main/java/com/gxwebsoft/shop/service/ShopDealerCommissionRollbackService.java @@ -17,5 +17,7 @@ public interface ShopDealerCommissionRollbackService { * @return true=执行成功或无可回退数据;false=执行失败 */ boolean rollbackOnOrderRefund(ShopOrder order, BigDecimal refundAmount); + + boolean rollbackOnOrderRefundV2(ShopOrder order, BigDecimal refundAmount); } diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopDealerUserService.java b/src/main/java/com/gxwebsoft/shop/service/ShopDealerUserService.java index ef2a6d5..dc4131c 100644 --- a/src/main/java/com/gxwebsoft/shop/service/ShopDealerUserService.java +++ b/src/main/java/com/gxwebsoft/shop/service/ShopDealerUserService.java @@ -2,6 +2,7 @@ package com.gxwebsoft.shop.service; import com.baomidou.mybatisplus.extension.service.IService; import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.shop.dto.ShopDealerRefundDto; import com.gxwebsoft.shop.dto.ShopDealerUserReduceDto; import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.param.ShopDealerUserParam; @@ -50,4 +51,11 @@ public interface ShopDealerUserService extends IService { * @return */ Boolean reduceBalance(ShopDealerUserReduceDto reduceDto); + + /** + * 分销订单退款 + * @param refundDto + * @return + */ + Boolean refundOrder(ShopDealerRefundDto refundDto); } diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerCommissionRollbackServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerCommissionRollbackServiceImpl.java index 232854d..7625bd7 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerCommissionRollbackServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerCommissionRollbackServiceImpl.java @@ -1,8 +1,10 @@ package com.gxwebsoft.shop.service.impl; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.gxwebsoft.shop.dto.ShopDealerRefundDto; import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerOrder; import com.gxwebsoft.shop.entity.ShopDealerUser; @@ -137,6 +139,59 @@ public class ShopDealerCommissionRollbackServiceImpl implements ShopDealerCommis return true; } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean rollbackOnOrderRefundV2(ShopOrder order, BigDecimal refundAmount) { + if (order == null || order.getTenantId() == null || StrUtil.isEmpty(order.getOrderNo())) { + return true; + } + Integer tenantId = order.getTenantId(); + String orderNo = order.getOrderNo(); + + //判断退款金额不能大于实付金额 + if(refundAmount.compareTo(order.getPayPrice()) > 0){ + throw new RuntimeException("退款金额不能大于订单实付金额!"); + } + + //执行分销订单退款业务 + ShopDealerRefundDto refundDto = new ShopDealerRefundDto(); + refundDto.setOrderNo(orderNo); + refundDto.setTenantId(tenantId); + refundDto.setRefundAmount(refundAmount); + Boolean refundResult = shopDealerUserService.refundOrder(refundDto); + + if(refundResult){ + //退款金额与实付金额一致,标记分销订单失效 + if (order.getPayPrice().compareTo(refundAmount) == 0) { + markDealerOrderInvalid(tenantId, orderNo); + }else { + BigDecimal rate = refundAmount.divide(order.getPayPrice(), 3, RoundingMode.HALF_UP); + BigDecimal remainRate = BigDecimal.ONE.subtract(rate); + LambdaQueryWrapper orderLambdaQueryWrapper = new LambdaQueryWrapper().eq(ShopDealerOrder::getOrderNo, orderNo); + ShopDealerOrder shopDealerOrder = shopDealerOrderService.getOne(orderLambdaQueryWrapper); + if(shopDealerOrder != null){ + if(shopDealerOrder.getFirstMoney().compareTo(BigDecimal.ZERO) > 0){ + shopDealerOrder.setFirstMoney(shopDealerOrder.getFirstMoney().multiply(remainRate)); + } + if(shopDealerOrder.getSecondMoney().compareTo(BigDecimal.ZERO) > 0){ + shopDealerOrder.setSecondMoney(shopDealerOrder.getSecondMoney().multiply(remainRate)); + } + if(shopDealerOrder.getFirstDividend().compareTo(BigDecimal.ZERO) > 0){ + shopDealerOrder.setFirstDividend(shopDealerOrder.getFirstDividend().multiply(remainRate)); + } + if(shopDealerOrder.getSecondDividend().compareTo(BigDecimal.ZERO) > 0){ + shopDealerOrder.setSecondDividend(shopDealerOrder.getSecondDividend().multiply(remainRate)); + } + shopDealerOrder.setUpdateTime(LocalDateTime.now()); + shopDealerOrderService.updateById(shopDealerOrder); + } + } + return Boolean.TRUE; + }else { + return Boolean.FALSE; + } + } + private boolean hasUnfreezeMarker(Integer tenantId, ShopDealerCapital cap) { if (tenantId == null || cap == null || cap.getId() == null || cap.getUserId() == null || cap.getOrderNo() == null) { return false; diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerUserServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerUserServiceImpl.java index 12fc4e3..80c00e2 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerUserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopDealerUserServiceImpl.java @@ -10,12 +10,16 @@ import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.mapper.UserMapper; import com.gxwebsoft.common.system.redis.OrderNoUtils; +import com.gxwebsoft.shop.dto.ShopDealerRefundDto; import com.gxwebsoft.shop.dto.ShopDealerUserReduceDto; import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerUser; +import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.mapper.ShopDealerCapitalMapper; import com.gxwebsoft.shop.mapper.ShopDealerUserMapper; +import com.gxwebsoft.shop.mapper.ShopOrderMapper; import com.gxwebsoft.shop.param.ShopDealerUserParam; +import com.gxwebsoft.shop.service.ShopDealerCapitalService; import com.gxwebsoft.shop.service.ShopDealerUserService; import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; @@ -23,7 +27,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -44,6 +51,10 @@ public class ShopDealerUserServiceImpl extends ServiceImpl dealerCapitalList = shopDealerCapitalMapper.selectList( + new LambdaQueryWrapper() + .eq(ShopDealerCapital::getTenantId, entity.getTenantId()) + .eq(ShopDealerCapital::getOrderNo, entity.getOrderNo()) + .in(ShopDealerCapital::getFlowType, Arrays.asList(ShopDealerCapitalUpdateEnum.DISTRIBUTION_INCOME.getType(), + ShopDealerCapitalUpdateEnum.MANAGEMENT_INCOME.getType(), ShopDealerCapitalUpdateEnum.DIVIDEND_INCOME.getType(), + ShopDealerCapitalUpdateEnum.PROMOTION_INCOME.getType(), ShopDealerCapitalUpdateEnum.FREEZE_MONEY_THAW.getType(), + ShopDealerCapitalUpdateEnum.DELIVERY_INCOME.getType())) + .isNotNull(ShopDealerCapital::getUserId) + ); + + if(CollectionUtils.isNotEmpty(dealerCapitalList)){ + //2.查询订单信息 + ShopOrder order = shopOrderMapper.selectOne(new LambdaQueryWrapper().select(ShopOrder::getOrderId, ShopOrder::getPayPrice, ShopOrder::getUserId) + .eq(ShopOrder::getOrderNo, entity.getOrderNo())); + + //3.订单不存在则回退 + if(order == null){ + throw new RuntimeException("退款查询订单信息失败!"); + } + + //4.订单实付金额为0则不执行退款业务 + if(order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0){ + return Boolean.TRUE; + } + + //5.退款金额不能大于实付金额 + if(entity.getRefundAmount().compareTo(order.getPayPrice()) > 0){ + throw new RuntimeException("退款金额不能大于实付金额!"); + } + + //6.查询分销用户信息 + List shopDealerUserList = new ArrayList<>(); + List userIdList = dealerCapitalList.stream().map(ShopDealerCapital::getUserId).distinct().collect(Collectors.toList()); + if(CollectionUtils.isNotEmpty(userIdList)){ + shopDealerUserList = lambdaQuery().in(ShopDealerUser::getUserId, userIdList).list(); + } + + //7.创建批量更新用户钱包、批量信息分销明细集合对象 + List shopDealerUserUpdateList = new ArrayList<>(); + List newCapitalList = new ArrayList<>(); + + //8.以用户分组,对结算订单进行业务操作 + Map> userDealerMap = dealerCapitalList.stream().collect(Collectors.groupingBy(ShopDealerCapital::getUserId)); + + //9.计算退款比率【退款金额/实付金额】 + BigDecimal rate = entity.getRefundAmount().divide(order.getPayPrice(), 3, RoundingMode.HALF_UP);; + + //10.遍历用户分销对象数据执行业务 + List finalShopDealerUserList = shopDealerUserList; + BigDecimal finalRate = rate; + userDealerMap.forEach((k, value) ->{ + BigDecimal money = value.stream().map(ShopDealerCapital::getMoney).reduce(BigDecimal.ZERO, BigDecimal::add).multiply(finalRate); + BigDecimal freezeMoney = value.stream().map(ShopDealerCapital::getFreezeMoney).reduce(BigDecimal.ZERO, BigDecimal::add).multiply(finalRate); + + ShopDealerUser dealerUser = finalShopDealerUserList.stream().filter(shopDealerUser -> k.equals(shopDealerUser.getUserId())).findFirst().orElse(null); + if(dealerUser != null){ + //11.修改分销用户账户信息 + dealerUser.setMoney(dealerUser.getMoney().subtract(money)); + dealerUser.setFreezeMoney(dealerUser.getFreezeMoney().subtract(freezeMoney)); + dealerUser.setTotalMoney(dealerUser.getTotalMoney().subtract(money.add(freezeMoney))); + dealerUser.setUpdateTime(LocalDateTime.now()); + shopDealerUserUpdateList.add(dealerUser); + + //12.新增退单分销记录 + ShopDealerCapital capital = new ShopDealerCapital(); + String no = orderNoUtils.generate("R"); + capital.setNo(no); + capital.setUserId(k); + capital.setType(ShopDealerTypeEnum.ORDER_REFUND.getCode()); + capital.setOrderNo(entity.getOrderNo()); + capital.setFlowType(ShopDealerCapitalUpdateEnum.ORDER_REFUND.getType()); + capital.setMoney(money.negate()); + capital.setMoneyAfter(dealerUser.getMoney()); + capital.setFreezeMoney(freezeMoney.negate()); + capital.setFreezeMoneyAfter(dealerUser.getFreezeMoney()); + capital.setComments(ShopDealerCapitalUpdateEnum.ORDER_REFUND.getDescription()); + capital.setToUserId(order.getUserId()); + capital.setTenantId(entity.getTenantId()); + User loginUser = LoginUserUtil.getLoginUser(); + if(loginUser != null){ + capital.setCreator(loginUser.getUserId()); + } + capital.setCreateTime(LocalDateTime.now()); + newCapitalList.add(capital); + } + }); + + //13.批量修改分销用户钱包余额 + if(CollectionUtils.isNotEmpty(shopDealerUserUpdateList)){ + updateBatchById(shopDealerUserUpdateList); + } + + //14.批量生成分销记录数据 + if(CollectionUtils.isNotEmpty(newCapitalList)){ + shopDealerCapitalService.saveBatch(newCapitalList); + } + return Boolean.TRUE; + }else { + return Boolean.TRUE; + } + } + public ShopDealerUser getDealerUser(Integer userId){ ShopDealerUser shopDealerUser = baseMapper.selectOne(new LambdaQueryWrapper().eq(ShopDealerUser::getUserId, userId)); if(shopDealerUser == null){ diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c22fd88..9398561 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -18,6 +18,11 @@ spring: host: localhost port: 6379 +# database: 0 +# host: 8.134.55.105 +# port: 16379 +# password: redis_t74P8C + # 日志配置 logging: level: