重构订单下单逻辑,引入策略模式和命令模式

- 为OrderTriggerSource枚举添加详细注释说明
- 将IOrderLifecycleService接口的initiateOrder方法重构为placeOrder
- 新增OrderPlacementCommand、OrderPlacementResult、OrderAmountBreakdown等DTO
- 实现订单下单策略模式,支持不同类型订单的差异化处理
- 优化金额计算逻辑,完善优惠券折扣计算
- 改进余额扣减逻辑,增强异常处理
- 更新相关控制器使用新的下单接口
- 完善单元测试覆盖,确保代码质量
This commit is contained in:
irving
2025-10-30 21:12:37 -04:00
parent 67692ff79f
commit e29c5db276
16 changed files with 1609 additions and 80 deletions

View File

@@ -418,14 +418,41 @@ public class OrderConstant {
@Getter
public enum OrderTriggerSource {
/**
* 未标记来源的兜底,通常用于兼容历史数据
*/
UNKNOWN("unknown"),
/**
* 运营或客服后台人工处理触发
*/
MANUAL("manual"),
/**
* 微信顾客端(小程序/公众号)下单触发
*/
WX_CUSTOMER("wx_customer"),
/**
* 微信店员端操作触发
*/
WX_CLERK("wx_clerk"),
/**
* 管理后台控制台界面发起
*/
ADMIN_CONSOLE("admin_console"),
/**
* 管理后台开放接口调用
*/
ADMIN_API("admin_api"),
/**
* 打赏单自动生成的订单流程
*/
REWARD_ORDER("reward_order"),
/**
* 定时任务/调度器触发
*/
SCHEDULER("scheduler"),
/**
* 平台内部系统逻辑触发
*/
SYSTEM("system");
private final String code;

View File

@@ -0,0 +1,17 @@
package com.starry.admin.modules.order.module.dto;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单金额拆分结果:原价、优惠、实付。
*/
@Getter
@AllArgsConstructor(staticName = "of")
public class OrderAmountBreakdown {
private final BigDecimal grossAmount;
private final BigDecimal discountAmount;
private final BigDecimal netAmount;
}

View File

@@ -0,0 +1,48 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
/**
* 下单指令,封装不同业务场景需要的参数。
*/
@Data
@Builder
public class OrderPlacementCommand {
@NonNull
private final OrderCreationContext orderContext;
private final String balanceOperationAction;
@Builder.Default
private final boolean deductBalance = true;
private final PricingInput pricingInput;
/**
* 计价所需的输入参数。
*/
@Data
@Builder
public static class PricingInput {
@NonNull
private final BigDecimal unitPrice;
private final int quantity;
@Builder.Default
private final List<String> couponIds = Collections.emptyList();
private final String commodityId;
@NonNull
private final OrderConstant.PlaceType placeType;
}
}

View File

@@ -0,0 +1,16 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单下单结果,包含订单实体和金额拆分。
*/
@Getter
@AllArgsConstructor(staticName = "of")
public class OrderPlacementResult {
private final PlayOrderInfoEntity order;
private final OrderAmountBreakdown amountBreakdown;
}

View File

@@ -2,12 +2,14 @@ package com.starry.admin.modules.order.service;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
public interface IOrderLifecycleService {
PlayOrderInfoEntity initiateOrder(OrderCreationContext context);
OrderPlacementResult placeOrder(OrderPlacementCommand command);
void completeOrder(String orderId, OrderCompletionContext context);

View File

@@ -0,0 +1,45 @@
package com.starry.admin.modules.order.service.impl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.dto.OrderAmountBreakdown;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
abstract class AbstractOrderPlacementStrategy implements OrderPlacementStrategy {
protected final OrderLifecycleServiceImpl service;
AbstractOrderPlacementStrategy(OrderLifecycleServiceImpl service) {
this.service = service;
}
@Override
public OrderPlacementResult place(OrderPlacementCommand command) {
OrderCreationContext context = command.getOrderContext();
OrderAmountBreakdown breakdown = handlePricing(command, context);
PaymentInfo paymentInfo = context.getPaymentInfo();
if (paymentInfo == null) {
throw new CustomException("支付信息不能为空");
}
PlayOrderInfoEntity order = service.createOrderRecord(context);
if (command.isDeductBalance()) {
service.deductCustomerBalance(
context.getPurchaserBy(),
service.normalizeMoney(paymentInfo.getFinalAmount()),
command.getBalanceOperationAction(),
order.getId());
}
OrderAmountBreakdown amountBreakdown =
breakdown != null ? breakdown : service.fallbackBreakdown(paymentInfo);
return OrderPlacementResult.of(order, amountBreakdown);
}
protected abstract OrderAmountBreakdown handlePricing(OrderPlacementCommand command, OrderCreationContext context);
}

View File

@@ -6,10 +6,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
@@ -25,8 +27,11 @@ import com.starry.admin.modules.order.module.constant.OrderConstant.PlaceType;
import com.starry.admin.modules.order.module.constant.OrderConstant.ReviewRequirement;
import com.starry.admin.modules.order.module.constant.OrderConstant.YesNoFlag;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderAmountBreakdown;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
@@ -37,15 +42,24 @@ import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -56,6 +70,7 @@ import org.springframework.transaction.annotation.Transactional;
public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
private static final LifecycleToken LIFECYCLE_TOKEN = new LifecycleToken();
private static final int MONEY_SCALE = 2;
private enum LifecycleOperation {
CREATE,
@@ -81,21 +96,91 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
@Resource
private IPlayCouponDetailsService playCouponDetailsService;
@Resource
private IPlayCouponInfoService playCouponInfoService;
@Resource
private ClerkRevenueCalculator clerkRevenueCalculator;
@Resource
private PlayOrderLogInfoMapper orderLogInfoMapper;
private Map<StrategyKey, OrderPlacementStrategy> placementStrategies;
@PostConstruct
void initPlacementStrategies() {
Map<StrategyKey, OrderPlacementStrategy> strategies = new HashMap<>();
OrderPlacementStrategy specifiedStrategy = new ServiceOrderPlacementStrategy(this, OrderConstant.PlaceType.SPECIFIED);
registerStrategy(strategies, OrderConstant.PlaceType.SPECIFIED, OrderConstant.RewardType.NOT_APPLICABLE, specifiedStrategy);
OrderPlacementStrategy randomStrategy = new ServiceOrderPlacementStrategy(this, OrderConstant.PlaceType.RANDOM);
registerStrategy(strategies, OrderConstant.PlaceType.RANDOM, OrderConstant.RewardType.NOT_APPLICABLE, randomStrategy);
OrderPlacementStrategy otherStrategy = new OtherOrderPlacementStrategy(this);
registerStrategy(strategies, OrderConstant.PlaceType.OTHER, OrderConstant.RewardType.NOT_APPLICABLE, otherStrategy);
OrderPlacementStrategy rewardGiftStrategy = new RewardGiftOrderPlacementStrategy(this);
registerStrategy(strategies, OrderConstant.PlaceType.REWARD, OrderConstant.RewardType.GIFT, rewardGiftStrategy);
OrderPlacementStrategy rewardTipStrategy = new RewardTipOrderPlacementStrategy(this);
registerStrategy(strategies, OrderConstant.PlaceType.REWARD, OrderConstant.RewardType.BALANCE, rewardTipStrategy);
registerStrategy(strategies, OrderConstant.PlaceType.REWARD, OrderConstant.RewardType.NOT_APPLICABLE, rewardTipStrategy);
this.placementStrategies = strategies;
}
private void registerStrategy(
Map<StrategyKey, OrderPlacementStrategy> strategies,
OrderConstant.PlaceType placeType,
OrderConstant.RewardType rewardType,
OrderPlacementStrategy strategy) {
StrategyKey key = StrategyKey.of(placeType, rewardType);
if (strategies.containsKey(key)) {
throw new IllegalStateException(String.format("Duplicate order placement strategy for %s/%s", placeType, rewardType));
}
strategies.put(key, strategy);
}
private boolean isRewardScenario(OrderCreationContext context) {
return context.getPlaceType() == OrderConstant.PlaceType.REWARD;
}
@Override
@Transactional(rollbackFor = Exception.class)
public PlayOrderInfoEntity initiateOrder(OrderCreationContext context) {
public OrderPlacementResult placeOrder(OrderPlacementCommand command) {
if (command == null || command.getOrderContext() == null) {
throw new CustomException("下单参数不能为空");
}
OrderCreationContext context = command.getOrderContext();
OrderConstant.PlaceType placeType = context.getPlaceType();
log.info("placeOrder placeType={} rewardFlag? {}", placeType, isRewardScenario(context));
if (placeType == null) {
throw new CustomException("下单类型不能为空");
}
OrderConstant.RewardType rewardType = context.getRewardType() != null
? context.getRewardType()
: OrderConstant.RewardType.NOT_APPLICABLE;
OrderPlacementStrategy strategy = placementStrategies.get(StrategyKey.of(placeType, rewardType));
if (strategy == null && rewardType != OrderConstant.RewardType.NOT_APPLICABLE) {
strategy = placementStrategies.get(StrategyKey.of(placeType, OrderConstant.RewardType.NOT_APPLICABLE));
}
if (strategy == null) {
throw new CustomException("暂不支持该类型的下单逻辑");
}
return strategy.place(command);
}
PlayOrderInfoEntity createOrderRecord(OrderCreationContext context) {
validateOrderCreationRequest(context);
PlayOrderInfoEntity entity = buildOrderEntity(context);
applyRandomOrderRequirements(entity, context.getRandomOrderRequirements());
applyAcceptByInfo(entity, context);
if (context.isRewardOrder()) {
boolean rewardScenario = isRewardScenario(context);
if (rewardScenario) {
applyRewardOrderDefaults(entity);
}
@@ -111,7 +196,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
creationOperationType);
updateCouponUsage(context.getPaymentInfo().getCouponIds());
if (context.isRewardOrder() && StrUtil.isNotBlank(context.getAcceptBy())) {
if (rewardScenario && StrUtil.isNotBlank(context.getAcceptBy())) {
completeOrder(
entity.getId(),
OrderCompletionContext.of(OrderActor.SYSTEM, null, OrderTriggerSource.REWARD_ORDER)
@@ -121,6 +206,162 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
return entity;
}
OrderAmountBreakdown calculateOrderAmounts(OrderPlacementCommand.PricingInput pricingInput) {
int quantity = pricingInput.getQuantity();
if (quantity <= 0) {
throw new CustomException("数量不能为空");
}
BigDecimal normalizedUnitPrice = normalizeMoney(pricingInput.getUnitPrice());
BigDecimal grossAmount = normalizeMoney(normalizedUnitPrice.multiply(BigDecimal.valueOf(quantity)));
List<String> couponIds = pricingInput.getCouponIds();
if (CollectionUtil.isEmpty(couponIds)) {
return OrderAmountBreakdown.of(grossAmount, BigDecimal.ZERO, grossAmount);
}
BigDecimal discountTotal = BigDecimal.ZERO;
for (String couponId : couponIds) {
PlayCouponDetailsEntity couponDetails = playCouponDetailsService.getById(couponId);
if (couponDetails == null) {
throw new CustomException("优惠券不存在");
}
PlayCouponInfoEntity couponInfo = playCouponInfoService.selectPlayCouponInfoById(couponDetails.getCouponId());
String reason = playCouponInfoService.getCouponReasonForUnavailableUse(
couponInfo,
pricingInput.getPlaceType().getCode(),
pricingInput.getCommodityId(),
quantity,
normalizedUnitPrice);
if (StrUtil.isNotBlank(reason)) {
throw new CustomException("优惠券不可用 - " + reason);
}
CouponDiscountType discountType;
try {
discountType = CouponDiscountType.fromCode(couponInfo.getDiscountType());
} catch (CustomException ex) {
log.warn("Unsupported coupon discount type {}, couponId={}", couponInfo.getDiscountType(), couponInfo.getId());
continue;
}
switch (discountType) {
case FULL_REDUCTION:
discountTotal = discountTotal.add(normalizeMoney(couponInfo.getDiscountAmount()));
break;
case PERCENTAGE:
discountTotal = discountTotal.add(calculatePercentageDiscount(grossAmount, couponInfo.getDiscountAmount(), couponInfo.getId()));
break;
default:
break;
}
}
BigDecimal discountAmount = normalizeMoney(discountTotal.min(grossAmount));
BigDecimal netAmount = normalizeMoney(grossAmount.subtract(discountAmount));
return OrderAmountBreakdown.of(grossAmount, discountAmount, netAmount);
}
void deductCustomerBalance(
String customerId,
BigDecimal netAmount,
String operationAction,
String orderId) {
PlayCustomUserInfoEntity customer = customUserInfoService.selectById(customerId);
if (customer == null) {
throw new CustomException("顾客不存在");
}
BigDecimal before = normalizeMoney(customer.getAccountBalance());
if (netAmount.compareTo(before) > 0) {
throw new ServiceException("余额不足", 998);
}
BigDecimal after = normalizeMoney(before.subtract(netAmount));
String action = StrUtil.isNotBlank(operationAction) ? operationAction : "下单";
customUserInfoService.updateAccountBalanceById(
customerId,
before,
after,
BalanceOperationType.CONSUME.getCode(),
action,
netAmount,
BigDecimal.ZERO,
orderId);
}
OrderAmountBreakdown fallbackBreakdown(PaymentInfo paymentInfo) {
return OrderAmountBreakdown.of(
normalizeMoney(paymentInfo.getOrderMoney()),
normalizeMoney(paymentInfo.getDiscountAmount()),
normalizeMoney(paymentInfo.getFinalAmount()));
}
private static final class StrategyKey {
private final OrderConstant.PlaceType placeType;
private final OrderConstant.RewardType rewardType;
private StrategyKey(OrderConstant.PlaceType placeType, OrderConstant.RewardType rewardType) {
this.placeType = placeType;
this.rewardType = rewardType;
}
static StrategyKey of(OrderConstant.PlaceType placeType, OrderConstant.RewardType rewardType) {
return new StrategyKey(placeType, rewardType);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof StrategyKey)) {
return false;
}
StrategyKey that = (StrategyKey) o;
return placeType == that.placeType && rewardType == that.rewardType;
}
@Override
public int hashCode() {
return Objects.hash(placeType, rewardType);
}
}
BigDecimal calculatePercentageDiscount(BigDecimal grossAmount, BigDecimal discountFold, String couponId) {
if (discountFold == null) {
log.warn("Percentage coupon has no discount amount configured, couponId={}", couponId);
return BigDecimal.ZERO;
}
BigDecimal normalizedFold = discountFold.abs();
// REVIEW THIS: WHY NORMALIZE this if < 10?
if (normalizedFold.compareTo(BigDecimal.TEN) > 0) {
return normalizeMoney(grossAmount);
}
BigDecimal rate;
if (normalizedFold.compareTo(BigDecimal.ONE) <= 0) {
rate = normalizedFold;
} else if (normalizedFold.compareTo(BigDecimal.TEN) <= 0) {
rate = normalizedFold.divide(BigDecimal.TEN, MONEY_SCALE + 2, RoundingMode.HALF_UP);
} else if (normalizedFold.compareTo(BigDecimal.valueOf(100)) <= 0) {
rate = normalizedFold.divide(BigDecimal.valueOf(100), MONEY_SCALE + 2, RoundingMode.HALF_UP);
} else {
log.warn("Percentage coupon rate out of range, couponId={}, value={}", couponId, discountFold);
return BigDecimal.ZERO;
}
if (rate.compareTo(BigDecimal.ZERO) <= 0) {
log.warn("Percentage coupon rate <= 0, couponId={}", couponId);
return BigDecimal.ZERO;
}
if (rate.compareTo(BigDecimal.ONE) > 0) {
log.warn("Percentage coupon rate > 1 after normalisation, couponId={}", couponId);
rate = BigDecimal.ONE;
}
BigDecimal discount = grossAmount.multiply(BigDecimal.ONE.subtract(rate));
return normalizeMoney(discount.max(BigDecimal.ZERO));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void completeOrder(String orderId, OrderCompletionContext context) {
@@ -395,6 +636,13 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
playCouponDetailsService.updateCouponUseStateByIds(couponIds, CouponUseState.USED.getCode());
}
BigDecimal normalizeMoney(BigDecimal amount) {
if (amount == null) {
return BigDecimal.ZERO.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
}
return amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
}
private String resolvePayMethod(String payMethodCode) {
if (StrUtil.isBlank(payMethodCode)) {
return PayMethod.BALANCE.getCode();

View File

@@ -0,0 +1,9 @@
package com.starry.admin.modules.order.service.impl;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
interface OrderPlacementStrategy {
OrderPlacementResult place(OrderPlacementCommand command);
}

View File

@@ -0,0 +1,30 @@
package com.starry.admin.modules.order.service.impl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.dto.OrderAmountBreakdown;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
class OtherOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
OtherOrderPlacementStrategy(OrderLifecycleServiceImpl service) {
super(service);
}
@Override
protected OrderAmountBreakdown handlePricing(OrderPlacementCommand command, OrderCreationContext context) {
PaymentInfo info = context.getPaymentInfo();
if (info == null) {
throw new CustomException("支付信息不能为空");
}
context.setPaymentInfo(PaymentInfo.builder()
.orderMoney(service.normalizeMoney(info.getOrderMoney()))
.discountAmount(service.normalizeMoney(info.getDiscountAmount()))
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
.couponIds(info.getCouponIds())
.payMethod(info.getPayMethod())
.build());
return null;
}
}

View File

@@ -0,0 +1,41 @@
package com.starry.admin.modules.order.service.impl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.OrderAmountBreakdown;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
class RewardGiftOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
RewardGiftOrderPlacementStrategy(OrderLifecycleServiceImpl service) {
super(service);
}
@Override
protected OrderAmountBreakdown handlePricing(OrderPlacementCommand command, OrderCreationContext context) {
if (context.getRewardType() != OrderConstant.RewardType.GIFT) {
throw new CustomException("礼物订单类型不匹配");
}
PaymentInfo normalized = ensurePaymentInfoPresent(context);
return service.fallbackBreakdown(normalized);
}
private PaymentInfo ensurePaymentInfoPresent(OrderCreationContext context) {
PaymentInfo info = context.getPaymentInfo();
if (info == null) {
throw new CustomException("支付信息不能为空");
}
PaymentInfo normalized = PaymentInfo.builder()
.orderMoney(service.normalizeMoney(info.getOrderMoney()))
.discountAmount(service.normalizeMoney(info.getDiscountAmount()))
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
.couponIds(info.getCouponIds())
.payMethod(info.getPayMethod())
.build();
context.setPaymentInfo(normalized);
return normalized;
}
}

View File

@@ -0,0 +1,40 @@
package com.starry.admin.modules.order.service.impl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.OrderAmountBreakdown;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
class RewardTipOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
RewardTipOrderPlacementStrategy(OrderLifecycleServiceImpl service) {
super(service);
}
@Override
protected OrderAmountBreakdown handlePricing(OrderPlacementCommand command, OrderCreationContext context) {
if (context.getRewardType() == OrderConstant.RewardType.GIFT) {
throw new CustomException("礼物订单应使用礼物策略");
}
PaymentInfo normalized = ensurePaymentInfoPresent(context);
return service.fallbackBreakdown(normalized);
}
private PaymentInfo ensurePaymentInfoPresent(OrderCreationContext context) {
PaymentInfo info = context.getPaymentInfo();
if (info == null) {
throw new CustomException("支付信息不能为空");
}
PaymentInfo normalized = PaymentInfo.builder()
.orderMoney(service.normalizeMoney(info.getOrderMoney()))
.discountAmount(service.normalizeMoney(info.getDiscountAmount()))
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
.couponIds(info.getCouponIds())
.payMethod(info.getPayMethod())
.build();
context.setPaymentInfo(normalized);
return normalized;
}
}

View File

@@ -0,0 +1,57 @@
package com.starry.admin.modules.order.service.impl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.OrderAmountBreakdown;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
class ServiceOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
private final OrderConstant.PlaceType placeType;
ServiceOrderPlacementStrategy(OrderLifecycleServiceImpl service, OrderConstant.PlaceType placeType) {
super(service);
this.placeType = placeType;
}
@Override
protected OrderAmountBreakdown handlePricing(OrderPlacementCommand command, OrderCreationContext context) {
if (context.getPlaceType() != placeType) {
throw new CustomException("下单类型与服务计价策略不匹配");
}
OrderPlacementCommand.PricingInput pricingInput = command.getPricingInput();
if (pricingInput == null) {
PaymentInfo info = context.getPaymentInfo();
if (info == null) {
throw new CustomException("支付信息不能为空");
}
PaymentInfo normalized = PaymentInfo.builder()
.orderMoney(service.normalizeMoney(info.getOrderMoney()))
.discountAmount(service.normalizeMoney(info.getDiscountAmount()))
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
.couponIds(info.getCouponIds())
.payMethod(info.getPayMethod())
.build();
context.setPaymentInfo(normalized);
return service.fallbackBreakdown(normalized);
}
if (pricingInput.getPlaceType() != placeType) {
throw new CustomException("计价参数的下单类型不匹配");
}
OrderAmountBreakdown breakdown = service.calculateOrderAmounts(pricingInput);
PaymentInfo currentInfo = context.getPaymentInfo();
PaymentInfo updated = PaymentInfo.builder()
.orderMoney(breakdown.getGrossAmount())
.discountAmount(breakdown.getDiscountAmount())
.finalAmount(breakdown.getNetAmount())
.couponIds(pricingInput.getCouponIds())
.payMethod(currentInfo != null ? currentInfo.getPayMethod() : null)
.build();
context.setPaymentInfo(updated);
return breakdown;
}
}

View File

@@ -0,0 +1,36 @@
package com.starry.admin.modules.shop.module.enums;
import com.starry.admin.common.exception.CustomException;
import lombok.Getter;
/**
* 优惠券折扣类型枚举。
*/
@Getter
public enum CouponDiscountType {
/**
* 满减券(固定金额抵扣)。
*/
FULL_REDUCTION("0"),
/**
* 折扣券(按比例折扣)。暂未支持具体计算逻辑。
*/
PERCENTAGE("1");
private final String code;
CouponDiscountType(String code) {
this.code = code;
}
public static CouponDiscountType fromCode(String code) {
for (CouponDiscountType value : values()) {
if (value.code.equals(code)) {
return value;
}
}
throw new CustomException("不支持的优惠券折扣类型: " + code);
}
}

View File

@@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.aspect.CustomUserLogin;
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.common.task.OverdueOrderHandlerTask;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
@@ -23,6 +22,8 @@ import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
@@ -32,8 +33,6 @@ import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
import com.starry.admin.modules.shop.service.*;
import com.starry.admin.modules.weichat.entity.*;
@@ -63,12 +62,13 @@ import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -230,10 +230,6 @@ public class WxCustomController {
public R rewardToOrder(@ApiParam(value = "打赏信息", required = true) @Validated @RequestBody PlayOrderInfoRewardAdd vo) {
MoneyUtils.verificationTypeIsNormal(vo.getMoney());
String userId = ThreadLocalRequestDetail.getCustomUserInfo().getId();
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.selectById(userId);
if (new BigDecimal(vo.getMoney()).compareTo(customUserInfo.getAccountBalance()) > 0) {
throw new ServiceException("余额不足", 998);
}
String orderId = IdUtils.getUuid();
// 记录订单信息
OrderCreationContext orderRequest = OrderCreationContext.builder()
@@ -265,9 +261,10 @@ public class WxCustomController {
.weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark())
.build();
orderLifecycleService.initiateOrder(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(new BigDecimal(vo.getMoney())), "1", "打赏", new BigDecimal(vo.getMoney()), BigDecimal.ZERO, orderId);
orderLifecycleService.placeOrder(OrderPlacementCommand.builder()
.orderContext(orderRequest)
.balanceOperationAction("打赏")
.build());
return R.ok("成功");
}
@@ -301,34 +298,6 @@ public class WxCustomController {
PlayCommodityInfoVo commodityInfo = playCommodityInfoService.queryCommodityInfo(vo.getCommodityId(), clerkUserInfo.getLevelId());
BigDecimal couponMoney = BigDecimal.ZERO;
for (String couponId : vo.getCouponIds()) {
PlayCouponDetailsEntity couponDetailsEntity = couponDetailsService.getById(couponId);
if (Objects.isNull(couponDetailsEntity)) {
throw new CustomException("优惠券不存在");
}
PlayCouponInfoEntity couponInfo = playCouponInfoService.selectPlayCouponInfoById(couponDetailsEntity.getCouponId());
String couponReasonForUnavailableUse = playCouponInfoService.getCouponReasonForUnavailableUse(couponInfo, "0", vo.getCommodityId(), vo.getCommodityQuantity(), commodityInfo.getCommodityPrice());
if (StrUtil.isNotBlank(couponReasonForUnavailableUse)) {
throw new CustomException("优惠券不可用");
}
if (couponInfo.getDiscountType().equals("0")) {
couponMoney = couponMoney.add(couponInfo.getDiscountAmount());
}
}
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.selectById(customId);
BigDecimal money = commodityInfo.getCommodityPrice().multiply(new BigDecimal(vo.getCommodityQuantity()));
money = money.subtract(couponMoney);
if (money.compareTo(BigDecimal.ZERO) < 1) {
money = BigDecimal.ZERO;
}
if (money.compareTo(customUserInfo.getAccountBalance()) > 0) {
throw new ServiceException("余额不足", 998);
}
String orderId = IdUtils.getUuid();
String orderNo = playOrderInfoService.getOrderNo();
// 记录订单信息
@@ -351,21 +320,39 @@ public class WxCustomController {
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(money)
.finalAmount(money)
.orderMoney(BigDecimal.ZERO)
.finalAmount(BigDecimal.ZERO)
.discountAmount(BigDecimal.ZERO)
.couponIds(vo.getCouponIds())
.couponIds(Collections.emptyList())
.build())
.purchaserBy(customId)
.acceptBy(clerkUserInfo.getId())
.weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark())
.build();
orderLifecycleService.initiateOrder(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-指定单", money, BigDecimal.ZERO, orderId);
List<String> couponIds = CollectionUtils.isEmpty(vo.getCouponIds()) ? Collections.emptyList() : vo.getCouponIds();
OrderPlacementCommand command = OrderPlacementCommand.builder()
.orderContext(orderRequest)
.balanceOperationAction("下单-指定单")
.pricingInput(OrderPlacementCommand.PricingInput.builder()
.unitPrice(commodityInfo.getCommodityPrice())
.quantity(vo.getCommodityQuantity())
.couponIds(couponIds)
.commodityId(commodityInfo.getCommodityId())
.placeType(OrderConstant.PlaceType.SPECIFIED)
.build())
.build();
OrderPlacementResult result = orderLifecycleService.placeOrder(command);
PlayOrderInfoEntity order = result.getOrder();
BigDecimal netAmount = result.getAmountBreakdown().getNetAmount();
// 发送通知给店员
wxCustomMpService.sendCreateOrderMessage(clerkUserInfo.getTenantId(), clerkUserInfo.getOpenid(), orderNo, money.toString(), commodityInfo.getCommodityName(),orderId );
wxCustomMpService.sendCreateOrderMessage(
clerkUserInfo.getTenantId(),
clerkUserInfo.getOpenid(),
orderNo,
netAmount.toString(),
commodityInfo.getCommodityName(),
order.getId());
return R.ok("成功");
}
@@ -380,12 +367,7 @@ public class WxCustomController {
public R randomToOrdder(@Validated @RequestBody PlayOrderInfoRandomAdd vo) {
// 验证当前顾客余额是否充足
String customId = ThreadLocalRequestDetail.getCustomUserInfo().getId();
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.selectById(customId);
PlayCommodityInfoVo commodityInfo = playCommodityInfoService.queryCommodityInfo(vo.getCommodityId(), vo.getLevelId());
BigDecimal money = commodityInfo.getCommodityPrice().multiply(new BigDecimal(vo.getCommodityQuantity()));
if (money.compareTo(customUserInfo.getAccountBalance()) > 0) {
throw new ServiceException("余额不足", 998);
}
String orderId = IdUtils.getUuid();
String orderNo = playOrderInfoService.getOrderNo();
// 记录订单信息
@@ -408,10 +390,10 @@ public class WxCustomController {
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(money)
.finalAmount(money)
.orderMoney(BigDecimal.ZERO)
.finalAmount(BigDecimal.ZERO)
.discountAmount(BigDecimal.ZERO)
.couponIds(vo.getCouponIds())
.couponIds(Collections.emptyList())
.build())
.purchaserBy(customId)
.weiChatCode(vo.getWeiChatCode())
@@ -422,13 +404,28 @@ public class WxCustomController {
.excludeHistory(vo.getExcludeHistory())
.build())
.build();
orderLifecycleService.initiateOrder(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-随机单", money, BigDecimal.ZERO, orderId);
List<String> couponIds = CollectionUtils.isEmpty(vo.getCouponIds()) ? Collections.emptyList() : vo.getCouponIds();
OrderPlacementCommand command = OrderPlacementCommand.builder()
.orderContext(orderRequest)
.balanceOperationAction("下单-随机单")
.pricingInput(OrderPlacementCommand.PricingInput.builder()
.unitPrice(commodityInfo.getCommodityPrice())
.quantity(vo.getCommodityQuantity())
.couponIds(couponIds)
.commodityId(commodityInfo.getCommodityId())
.placeType(OrderConstant.PlaceType.RANDOM)
.build())
.build();
OrderPlacementResult result = orderLifecycleService.placeOrder(command);
PlayOrderInfoEntity order = result.getOrder();
BigDecimal netAmount = result.getAmountBreakdown().getNetAmount();
// 给全部店员发送通知
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getClerkState, "1")
.eq(PlayClerkUserInfoEntity::getOnlineState, "1").eq(PlayClerkUserInfoEntity::getSex, vo.getSex()));
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderNo, money.toString(), commodityInfo.getCommodityName(),orderId);
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class)
.isNotNull(PlayClerkUserInfoEntity::getOpenid)
.eq(PlayClerkUserInfoEntity::getClerkState, "1")
.eq(PlayClerkUserInfoEntity::getOnlineState, "1")
.eq(PlayClerkUserInfoEntity::getSex, vo.getSex()));
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderNo, netAmount.toString(), commodityInfo.getCommodityName(),order.getId());
// 记录订单,指定指定未接单后,进行退款处理
overdueOrderHandlerTask.enqueue(orderId + "_" + SecurityUtils.getTenantId());
// 下单成功后,先根据用户条件进行随机分配

View File

@@ -2,7 +2,6 @@ package com.starry.admin.modules.weichat.service;
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
@@ -11,7 +10,10 @@ import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
@@ -79,27 +81,21 @@ public class WxGiftOrderService {
throw new CustomException("礼物金额必须大于0");
}
BigDecimal currentBalance = customUserInfo.getAccountBalance() == null
? BigDecimal.ZERO : customUserInfo.getAccountBalance();
if (totalAmount.compareTo(currentBalance) > 0) {
throw new ServiceException("余额不足", 998);
}
String orderId = IdUtils.getUuid();
OrderCreationContext orderRequest = buildOrderCreationContext(orderId, request, sessionUser.getId(),
giftInfo, totalAmount);
orderLifecycleService.initiateOrder(orderRequest);
BigDecimal newBalance = currentBalance.subtract(totalAmount);
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), currentBalance, newBalance, "1",
"赠送礼物", totalAmount, BigDecimal.ZERO, orderId);
OrderPlacementResult result = orderLifecycleService.placeOrder(OrderPlacementCommand.builder()
.orderContext(orderRequest)
.balanceOperationAction("赠送礼物")
.build());
PlayOrderInfoEntity order = result.getOrder();
String tenantId = customUserInfo.getTenantId();
long delta = giftQuantity;
playCustomGiftInfoService.incrementGiftCount(customUserInfo.getId(), request.getGiftId(), tenantId, delta);
playClerkGiftInfoService.incrementGiftCount(request.getClerkId(), request.getGiftId(), tenantId, delta);
return orderId;
return order.getId();
}
private OrderCreationContext buildOrderCreationContext(String orderId, PlayOrderInfoGiftAdd request,