feat: 完成撤销收益扣回與限額改動

This commit is contained in:
irving
2025-11-14 00:55:05 -05:00
parent 4cd2950051
commit cec5e965f6
16 changed files with 824 additions and 203 deletions

View File

@@ -15,6 +15,7 @@ import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.annotation.Log;
import com.starry.common.context.CustomSecurityContextHolder;
@@ -28,6 +29,7 @@ import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -59,6 +61,9 @@ public class PlayOrderInfoController {
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
private IEarningsService earningsService;
/**
* 分页查询订单列表
*/
@@ -115,11 +120,8 @@ public class PlayOrderInfoController {
context.setRefundToCustomer(vo.isRefundToCustomer());
context.setRefundAmount(vo.getRefundAmount());
context.setRefundReason(vo.getRefundReason());
OrderRevocationContext.EarningsAdjustStrategy strategy = vo.getEarningsStrategy() != null
? vo.getEarningsStrategy()
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
context.setEarningsStrategy(strategy);
context.setCounterClerkId(vo.getCounterClerkId());
context.setDeductClerkEarnings(vo.isDeductClerkEarnings());
context.setEarningsAdjustAmount(vo.getDeductAmount());
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setOperatorId(SecurityUtils.getUserId());
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
@@ -127,6 +129,29 @@ public class PlayOrderInfoController {
return R.ok("撤销成功");
}
@ApiOperation(value = "撤销限额", notes = "查询指定订单可退金额与可扣回收益")
@GetMapping("/{id}/revocationLimits")
public R getRevocationLimits(@PathVariable("id") String id) {
PlayOrderInfoEntity order = orderInfoService.selectOrderInfoById(id);
BigDecimal maxRefundAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
BigDecimal maxDeductAmount = BigDecimal.ZERO;
if (order.getAcceptBy() != null) {
maxDeductAmount = Optional.ofNullable(earningsService.getRemainingEarningsForOrder(order.getId(), order.getAcceptBy()))
.orElse(BigDecimal.ZERO);
}
if (maxDeductAmount.compareTo(BigDecimal.ZERO) < 0) {
maxDeductAmount = BigDecimal.ZERO;
}
PlayOrderRevocationLimitsVo limitsVo = new PlayOrderRevocationLimitsVo();
limitsVo.setOrderId(order.getId());
limitsVo.setMaxRefundAmount(maxRefundAmount);
limitsVo.setMaxDeductAmount(maxDeductAmount);
limitsVo.setDefaultDeductAmount(maxDeductAmount);
limitsVo.setDeductible(order.getAcceptBy() != null);
return R.ok(limitsVo);
}
/**
* 管理后台强制取消进行中订单
*/

View File

@@ -26,30 +26,29 @@ public class OrderRevocationEarningsListener {
return;
}
OrderRevocationContext context = event.getContext();
switch (context.getEarningsStrategy()) {
case NONE:
return;
case REVERSE_CLERK:
earningsService.reverseByOrder(event.getOrderSnapshot().getId(), context.getOperatorId());
return;
case COUNTER_TO_PEIPEI:
createCounterLine(event);
return;
default:
throw new CustomException("未知的收益处理策略");
if (!context.isDeductClerkEarnings()) {
return;
}
createCounterLine(event);
}
private void createCounterLine(OrderRevocationEvent event) {
OrderRevocationContext context = event.getContext();
if (context == null) {
return;
}
PlayOrderInfoEntity order = event.getOrderSnapshot();
String targetClerkId = context.getCounterClerkId();
if (targetClerkId == null) {
String targetClerkId = order.getAcceptBy();
if (targetClerkId == null || targetClerkId.trim().isEmpty()) {
throw new CustomException("需要指定收益冲销目标账号");
}
BigDecimal amount = context.getRefundAmount();
BigDecimal amount = context.getEarningsAdjustAmount();
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
amount = Optional.ofNullable(order.getEstimatedRevenue()).orElse(BigDecimal.ZERO);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
}
earningsService.createCounterLine(order.getId(), order.getTenantId(), targetClerkId, amount, context.getOperatorId());
}

View File

@@ -26,31 +26,15 @@ public class OrderRevocationContext {
private boolean refundToCustomer;
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
@Nullable
private String counterClerkId;
private boolean deductClerkEarnings;
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
@Nullable
private BigDecimal earningsAdjustAmount;
public OrderRevocationContext withTriggerSource(OrderTriggerSource triggerSource) {
this.triggerSource = triggerSource;
return this;
}
public enum EarningsAdjustStrategy {
NONE("NO_ADJUST"),
REVERSE_CLERK("REV_CLERK"),
COUNTER_TO_PEIPEI("CTR_PEIPEI");
private final String logCode;
EarningsAdjustStrategy(String logCode) {
this.logCode = logCode;
}
public String getLogCode() {
return logCode;
}
}
}

View File

@@ -0,0 +1,26 @@
package com.starry.admin.modules.order.module.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.math.BigDecimal;
import lombok.Data;
@Data
@ApiModel(value = "撤销限额信息", description = "展示撤销时可退金额、可扣回收益等信息")
public class PlayOrderRevocationLimitsVo {
@ApiModelProperty("订单ID")
private String orderId;
@ApiModelProperty("最大可退金额")
private BigDecimal maxRefundAmount = BigDecimal.ZERO;
@ApiModelProperty("最大可扣回收益")
private BigDecimal maxDeductAmount = BigDecimal.ZERO;
@ApiModelProperty("建议扣回金额")
private BigDecimal defaultDeductAmount = BigDecimal.ZERO;
@ApiModelProperty("是否存在可扣回店员")
private boolean deductible;
}

View File

@@ -1,6 +1,5 @@
package com.starry.admin.modules.order.module.vo;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.math.BigDecimal;
@@ -24,9 +23,9 @@ public class PlayOrderRevocationVo {
@ApiModelProperty(value = "撤销原因")
private String refundReason;
@ApiModelProperty(value = "收益处理策略NONE/REVERSE_CLERK/COUNTER_TO_PEIPEI")
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
@ApiModelProperty(value = "是否扣回店员收益")
private boolean deductClerkEarnings;
@ApiModelProperty(value = "收益冲销目标账号ID策略为 COUNTER_TO_PEIPEI 时必填")
private String counterClerkId;
@ApiModelProperty(value = "扣回金额,未填写则默认按本单收益全额扣回")
private BigDecimal deductAmount;
}

View File

@@ -632,18 +632,32 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
throw new CustomException("仅支持撤销普通服务订单");
}
OrderRevocationContext.EarningsAdjustStrategy strategy = context.getEarningsStrategy() != null
? context.getEarningsStrategy()
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
if (strategy == OrderRevocationContext.EarningsAdjustStrategy.REVERSE_CLERK
&& earningsService.hasLockedLines(order.getId())) {
throw new CustomException("收益已提现或处理中,无法撤销");
}
String operatorType = StrUtil.isNotBlank(context.getOperatorType()) ? context.getOperatorType() : OperatorType.ADMIN.getCode();
context.setOperatorType(operatorType);
String operatorId = StrUtil.isNotBlank(context.getOperatorId()) ? context.getOperatorId() : SecurityUtils.getUserId();
context.setOperatorId(operatorId);
if (context.isDeductClerkEarnings()) {
String targetClerkId = order.getAcceptBy();
if (StrUtil.isBlank(targetClerkId)) {
throw new CustomException("未找到可冲销的店员收益账号");
}
BigDecimal availableEarnings = Optional.ofNullable(
earningsService.getRemainingEarningsForOrder(order.getId(), targetClerkId))
.orElse(BigDecimal.ZERO);
if (availableEarnings.compareTo(BigDecimal.ZERO) <= 0) {
throw new CustomException("本单店员收益已全部扣回");
}
BigDecimal requested = context.getEarningsAdjustAmount();
if (requested == null || requested.compareTo(BigDecimal.ZERO) <= 0) {
requested = availableEarnings;
}
if (requested.compareTo(availableEarnings) > 0) {
throw new CustomException("扣回金额不能超过本单收益" + availableEarnings);
}
context.setEarningsAdjustAmount(requested);
} else {
context.setEarningsAdjustAmount(BigDecimal.ZERO);
}
BigDecimal finalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
BigDecimal refundAmount = context.getRefundAmount();
@@ -708,9 +722,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
String operationType = String.format(
"%s_%s",
LifecycleOperation.REVOKE_COMPLETED.name(),
strategy != null
? strategy.getLogCode()
: OrderRevocationContext.EarningsAdjustStrategy.NONE.getLogCode());
context.isDeductClerkEarnings() ? "DEDUCT" : "KEEP");
recordOrderLog(latest, actor, context.getOperatorId(), LifecycleOperation.REVOKE_COMPLETED,
context.getRefundReason(), operationType);

View File

@@ -8,7 +8,8 @@ import com.fasterxml.jackson.annotation.JsonValue;
*/
public enum EarningsType {
ORDER("ORDER"),
COMMISSION("COMMISSION");
COMMISSION("COMMISSION"),
ADJUSTMENT("ADJUSTMENT");
@EnumValue
@JsonValue

View File

@@ -18,9 +18,7 @@ public interface IEarningsService extends IService<EarningsLineEntity> {
List<EarningsLineEntity> findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now);
void reverseByOrder(String orderId, String operatorId);
void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId);
boolean hasLockedLines(String orderId);
BigDecimal getRemainingEarningsForOrder(String orderId, String clerkId);
}

View File

@@ -2,7 +2,6 @@ package com.starry.admin.modules.withdraw.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
@@ -15,7 +14,6 @@ import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -98,17 +96,6 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
return picked;
}
@Override
public void reverseByOrder(String orderId, String operatorId) {
if (StrUtil.isBlank(orderId)) {
return;
}
this.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
.eq(EarningsLineEntity::getOrderId, orderId)
.in(EarningsLineEntity::getStatus, Arrays.asList("available", "frozen"))
.set(EarningsLineEntity::getStatus, "reversed"));
}
@Override
public void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId) {
if (StrUtil.hasBlank(orderId, tenantId, targetClerkId)) {
@@ -119,27 +106,63 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
return;
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime resolvedUnlock = now;
String resolvedStatus = "available";
List<EarningsLineEntity> references = this.baseMapper.selectList(new LambdaQueryWrapper<EarningsLineEntity>()
.eq(EarningsLineEntity::getOrderId, orderId)
.eq(EarningsLineEntity::getClerkId, targetClerkId)
.eq(EarningsLineEntity::getDeleted, false)
.orderByAsc(EarningsLineEntity::getUnlockTime));
EarningsLineEntity reference = references.stream()
.filter(line -> line.getAmount() != null && line.getAmount().compareTo(BigDecimal.ZERO) > 0)
.findFirst()
.orElse(null);
if (reference == null) {
throw new IllegalStateException("未找到可冲销的收益记录");
}
LocalDateTime refUnlock = reference.getUnlockTime();
String refStatus = reference.getStatus();
boolean shouldPreserveFreeze = "frozen".equalsIgnoreCase(refStatus)
&& refUnlock != null
&& refUnlock.isAfter(now);
if (shouldPreserveFreeze) {
resolvedUnlock = refUnlock;
resolvedStatus = "frozen";
} else {
resolvedUnlock = now;
resolvedStatus = "available";
}
EarningsLineEntity line = new EarningsLineEntity();
line.setId(IdUtils.getUuid());
line.setOrderId(orderId);
line.setTenantId(tenantId);
line.setClerkId(targetClerkId);
line.setAmount(normalized.negate());
line.setEarningType(EarningsType.ORDER);
line.setStatus("available");
line.setUnlockTime(LocalDateTime.now());
line.setEarningType(EarningsType.ADJUSTMENT);
line.setStatus(resolvedStatus);
line.setUnlockTime(resolvedUnlock);
this.save(line);
}
@Override
public boolean hasLockedLines(String orderId) {
if (StrUtil.isBlank(orderId)) {
return false;
public BigDecimal getRemainingEarningsForOrder(String orderId, String clerkId) {
if (StrUtil.hasBlank(orderId, clerkId)) {
return BigDecimal.ZERO;
}
Long count = this.lambdaQuery()
List<EarningsLineEntity> lines = this.lambdaQuery()
.eq(EarningsLineEntity::getOrderId, orderId)
.in(EarningsLineEntity::getStatus, Arrays.asList("withdrawing", "withdrawn"))
.count();
return count != null && count > 0;
.eq(EarningsLineEntity::getClerkId, clerkId)
.eq(EarningsLineEntity::getDeleted, false)
.list();
BigDecimal total = BigDecimal.ZERO;
for (EarningsLineEntity line : lines) {
BigDecimal amount = line.getAmount() == null ? BigDecimal.ZERO : line.getAmount();
total = total.add(amount);
}
return total;
}
}