Compare commits
4 Commits
67692ff79f
...
c9439e1021
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9439e1021 | ||
|
|
db6132d7e3 | ||
|
|
48bdc9af33 | ||
|
|
e29c5db276 |
@@ -418,14 +418,41 @@ public class OrderConstant {
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public enum OrderTriggerSource {
|
public enum OrderTriggerSource {
|
||||||
|
/**
|
||||||
|
* 未标记来源的兜底,通常用于兼容历史数据
|
||||||
|
*/
|
||||||
UNKNOWN("unknown"),
|
UNKNOWN("unknown"),
|
||||||
|
/**
|
||||||
|
* 运营或客服后台人工处理触发
|
||||||
|
*/
|
||||||
MANUAL("manual"),
|
MANUAL("manual"),
|
||||||
|
/**
|
||||||
|
* 微信顾客端(小程序/公众号)下单触发
|
||||||
|
*/
|
||||||
WX_CUSTOMER("wx_customer"),
|
WX_CUSTOMER("wx_customer"),
|
||||||
|
/**
|
||||||
|
* 微信店员端操作触发
|
||||||
|
*/
|
||||||
WX_CLERK("wx_clerk"),
|
WX_CLERK("wx_clerk"),
|
||||||
|
/**
|
||||||
|
* 管理后台控制台界面发起
|
||||||
|
*/
|
||||||
ADMIN_CONSOLE("admin_console"),
|
ADMIN_CONSOLE("admin_console"),
|
||||||
|
/**
|
||||||
|
* 管理后台开放接口调用
|
||||||
|
*/
|
||||||
ADMIN_API("admin_api"),
|
ADMIN_API("admin_api"),
|
||||||
|
/**
|
||||||
|
* 打赏单自动生成的订单流程
|
||||||
|
*/
|
||||||
REWARD_ORDER("reward_order"),
|
REWARD_ORDER("reward_order"),
|
||||||
|
/**
|
||||||
|
* 定时任务/调度器触发
|
||||||
|
*/
|
||||||
SCHEDULER("scheduler"),
|
SCHEDULER("scheduler"),
|
||||||
|
/**
|
||||||
|
* 平台内部系统逻辑触发
|
||||||
|
*/
|
||||||
SYSTEM("system");
|
SYSTEM("system");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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.OrderCompletionContext;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
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.OrderRefundContext;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
|
||||||
public interface IOrderLifecycleService {
|
public interface IOrderLifecycleService {
|
||||||
|
|
||||||
PlayOrderInfoEntity initiateOrder(OrderCreationContext context);
|
OrderPlacementResult placeOrder(OrderPlacementCommand command);
|
||||||
|
|
||||||
void completeOrder(String orderId, OrderCompletionContext context);
|
void completeOrder(String orderId, OrderCompletionContext context);
|
||||||
|
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
|
|||||||
* @param croupIds 优惠券ID列表
|
* @param croupIds 优惠券ID列表
|
||||||
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
||||||
* @param firstOrder 是否是首单【0:不是,1:是】
|
* @param firstOrder 是否是首单【0:不是,1:是】
|
||||||
* @param finalAmount 订单支付金额
|
* @param orderAmount 订单金额
|
||||||
* @return com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo
|
* @return com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo
|
||||||
* @author admin
|
* @author admin
|
||||||
* @since 2024/7/18 16:39
|
* @since 2024/7/18 16:39
|
||||||
**/
|
**/
|
||||||
ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
|
ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
|
||||||
String firstOrder, BigDecimal finalAmount);
|
String firstOrder, BigDecimal orderAmount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据店员等级和订单金额,获取店员预计收入
|
* 根据店员等级和订单金额,获取店员预计收入
|
||||||
@@ -66,12 +66,12 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
|
|||||||
* @param clerkId 店员ID
|
* @param clerkId 店员ID
|
||||||
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
||||||
* @param firstOrder 是否是首单【0:不是,1:是】
|
* @param firstOrder 是否是首单【0:不是,1:是】
|
||||||
* @param finalAmount 订单支付金额
|
* @param orderAmount 订单金额
|
||||||
* @return math.BigDecimal 店员预计收入
|
* @return math.BigDecimal 店员预计收入
|
||||||
* @author admin
|
* @author admin
|
||||||
* @since 2024/6/3 11:12
|
* @since 2024/6/3 11:12
|
||||||
**/
|
**/
|
||||||
BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal finalAmount);
|
BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal orderAmount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据店员等级,获取店员提成比例
|
* 根据店员等级,获取店员提成比例
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
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.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
||||||
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
|
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.BalanceOperationType;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
|
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.ReviewRequirement;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.YesNoFlag;
|
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.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.OrderCompletionContext;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
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.OrderRefundContext;
|
||||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
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.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.IPlayOrderRefundInfoService;
|
||||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
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.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.IPlayCouponDetailsService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -56,6 +70,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||||
|
|
||||||
private static final LifecycleToken LIFECYCLE_TOKEN = new LifecycleToken();
|
private static final LifecycleToken LIFECYCLE_TOKEN = new LifecycleToken();
|
||||||
|
private static final int MONEY_SCALE = 2;
|
||||||
|
|
||||||
private enum LifecycleOperation {
|
private enum LifecycleOperation {
|
||||||
CREATE,
|
CREATE,
|
||||||
@@ -81,21 +96,91 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayCouponDetailsService playCouponDetailsService;
|
private IPlayCouponDetailsService playCouponDetailsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayCouponInfoService playCouponInfoService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ClerkRevenueCalculator clerkRevenueCalculator;
|
private ClerkRevenueCalculator clerkRevenueCalculator;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private PlayOrderLogInfoMapper orderLogInfoMapper;
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@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);
|
validateOrderCreationRequest(context);
|
||||||
|
|
||||||
PlayOrderInfoEntity entity = buildOrderEntity(context);
|
PlayOrderInfoEntity entity = buildOrderEntity(context);
|
||||||
applyRandomOrderRequirements(entity, context.getRandomOrderRequirements());
|
applyRandomOrderRequirements(entity, context.getRandomOrderRequirements());
|
||||||
applyAcceptByInfo(entity, context);
|
applyAcceptByInfo(entity, context);
|
||||||
if (context.isRewardOrder()) {
|
boolean rewardScenario = isRewardScenario(context);
|
||||||
|
if (rewardScenario) {
|
||||||
applyRewardOrderDefaults(entity);
|
applyRewardOrderDefaults(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +196,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
creationOperationType);
|
creationOperationType);
|
||||||
updateCouponUsage(context.getPaymentInfo().getCouponIds());
|
updateCouponUsage(context.getPaymentInfo().getCouponIds());
|
||||||
|
|
||||||
if (context.isRewardOrder() && StrUtil.isNotBlank(context.getAcceptBy())) {
|
if (rewardScenario && StrUtil.isNotBlank(context.getAcceptBy())) {
|
||||||
completeOrder(
|
completeOrder(
|
||||||
entity.getId(),
|
entity.getId(),
|
||||||
OrderCompletionContext.of(OrderActor.SYSTEM, null, OrderTriggerSource.REWARD_ORDER)
|
OrderCompletionContext.of(OrderActor.SYSTEM, null, OrderTriggerSource.REWARD_ORDER)
|
||||||
@@ -121,6 +206,169 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
return entity;
|
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());
|
||||||
|
playCouponInfoService.getCouponDetailRestrictionReason(
|
||||||
|
couponInfo,
|
||||||
|
couponDetails.getUseState(),
|
||||||
|
couponDetails.getObtainingTime())
|
||||||
|
.ifPresent(reason -> {
|
||||||
|
throw new CustomException("优惠券不可用 - " + reason);
|
||||||
|
});
|
||||||
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void completeOrder(String orderId, OrderCompletionContext context) {
|
public void completeOrder(String orderId, OrderCompletionContext context) {
|
||||||
@@ -376,7 +624,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
context.getPaymentInfo().getCouponIds(),
|
context.getPaymentInfo().getCouponIds(),
|
||||||
context.getPlaceType().getCode(),
|
context.getPlaceType().getCode(),
|
||||||
entity.getFirstOrder(),
|
entity.getFirstOrder(),
|
||||||
context.getPaymentInfo().getFinalAmount());
|
context.getPaymentInfo().getOrderMoney());
|
||||||
entity.setEstimatedRevenue(estimatedRevenue.getRevenueAmount());
|
entity.setEstimatedRevenue(estimatedRevenue.getRevenueAmount());
|
||||||
entity.setEstimatedRevenueRatio(estimatedRevenue.getRevenueRatio());
|
entity.setEstimatedRevenueRatio(estimatedRevenue.getRevenueRatio());
|
||||||
}
|
}
|
||||||
@@ -395,6 +643,13 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
playCouponDetailsService.updateCouponUseStateByIds(couponIds, CouponUseState.USED.getCode());
|
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) {
|
private String resolvePayMethod(String payMethodCode) {
|
||||||
if (StrUtil.isBlank(payMethodCode)) {
|
if (StrUtil.isBlank(payMethodCode)) {
|
||||||
return PayMethod.BALANCE.getCode();
|
return PayMethod.BALANCE.getCode();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,8 +116,8 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
|
public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
|
||||||
String firstOrder, BigDecimal finalAmount) {
|
String firstOrder, BigDecimal orderAmount) {
|
||||||
return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, finalAmount);
|
return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, orderAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,35 +126,36 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
* @param clerkId 店员ID
|
* @param clerkId 店员ID
|
||||||
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
||||||
* @param firstOrder 是否是首单【0:不是,1:是】
|
* @param firstOrder 是否是首单【0:不是,1:是】
|
||||||
* @param finalAmount 订单支付金额
|
* @param orderAmount 订单金额
|
||||||
* @return math.BigDecimal 店员预计收入
|
* @return math.BigDecimal 店员预计收入
|
||||||
* @author admin
|
* @author admin
|
||||||
* @since 2024/6/3 11:12
|
* @since 2024/6/3 11:12
|
||||||
**/
|
**/
|
||||||
@Override
|
@Override
|
||||||
public BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal finalAmount) {
|
public BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal orderAmount) {
|
||||||
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
|
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
|
||||||
boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
|
boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
|
||||||
|
BigDecimal safeOrderAmount = orderAmount == null ? BigDecimal.ZERO : orderAmount;
|
||||||
try {
|
try {
|
||||||
OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
|
OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
|
||||||
switch (place) {
|
switch (place) {
|
||||||
case SPECIFIED:
|
case SPECIFIED:
|
||||||
return calculateRevenue(finalAmount,
|
return calculateRevenue(safeOrderAmount,
|
||||||
isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio());
|
isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio());
|
||||||
case RANDOM:
|
case RANDOM:
|
||||||
return calculateRevenue(finalAmount,
|
return calculateRevenue(safeOrderAmount,
|
||||||
isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio());
|
isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio());
|
||||||
case REWARD:
|
case REWARD:
|
||||||
return calculateRevenue(finalAmount,
|
return calculateRevenue(safeOrderAmount,
|
||||||
isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio());
|
isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio());
|
||||||
case OTHER:
|
case OTHER:
|
||||||
default:
|
default:
|
||||||
log.error("下单类型异常,placeType={}", placeType);
|
log.error("下单类型异常,placeType={}", placeType);
|
||||||
return finalAmount;
|
return safeOrderAmount;
|
||||||
}
|
}
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
log.error("下单类型错误,placeType={}", placeType, ex);
|
log.error("下单类型错误,placeType={}", placeType, ex);
|
||||||
return finalAmount;
|
return safeOrderAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +446,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
returnVo.setRefundById(orderRefundInfoEntity.getRefundById());
|
returnVo.setRefundById(orderRefundInfoEntity.getRefundById());
|
||||||
returnVo.setRefundReason(orderRefundInfoEntity.getRefundReason());
|
returnVo.setRefundReason(orderRefundInfoEntity.getRefundReason());
|
||||||
}
|
}
|
||||||
returnVo.setEstimatedRevenue(returnVo.getFinalAmount());
|
if (returnVo.getEstimatedRevenue() == null) {
|
||||||
|
returnVo.setEstimatedRevenue(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
return returnVo;
|
return returnVo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +588,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
orderInfo.getCouponIds(),
|
orderInfo.getCouponIds(),
|
||||||
orderInfo.getPlaceType(),
|
orderInfo.getPlaceType(),
|
||||||
firstOrderFlag,
|
firstOrderFlag,
|
||||||
orderInfo.getFinalAmount());
|
orderInfo.getOrderMoney());
|
||||||
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
|
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
|
||||||
entity.setEstimatedRevenue(revenueAmount);
|
entity.setEstimatedRevenue(revenueAmount);
|
||||||
entity.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
|
entity.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,8 +32,9 @@ public class ClerkRevenueCalculator {
|
|||||||
List<String> couponIds,
|
List<String> couponIds,
|
||||||
String placeType,
|
String placeType,
|
||||||
String firstOrder,
|
String firstOrder,
|
||||||
BigDecimal finalAmount) {
|
BigDecimal orderAmount) {
|
||||||
PlayClerkLevelInfoEntity levelInfo = playClerkUserInfoService.queryLevelCommission(clerkId);
|
PlayClerkLevelInfoEntity levelInfo = playClerkUserInfoService.queryLevelCommission(clerkId);
|
||||||
|
BigDecimal baseAmount = orderAmount == null ? BigDecimal.ZERO : orderAmount;
|
||||||
ClerkEstimatedRevenueVo estimatedRevenueVo = new ClerkEstimatedRevenueVo();
|
ClerkEstimatedRevenueVo estimatedRevenueVo = new ClerkEstimatedRevenueVo();
|
||||||
|
|
||||||
boolean fallbackToOther = false;
|
boolean fallbackToOther = false;
|
||||||
@@ -48,20 +49,20 @@ public class ClerkRevenueCalculator {
|
|||||||
|
|
||||||
switch (placeTypeEnum) {
|
switch (placeTypeEnum) {
|
||||||
case SPECIFIED: // 指定单
|
case SPECIFIED: // 指定单
|
||||||
fillRegularOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo);
|
fillRegularOrderRevenue(firstOrder, baseAmount, levelInfo, estimatedRevenueVo);
|
||||||
break;
|
break;
|
||||||
case RANDOM: // 随机单
|
case RANDOM: // 随机单
|
||||||
fillRandomOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo);
|
fillRandomOrderRevenue(firstOrder, baseAmount, levelInfo, estimatedRevenueVo);
|
||||||
break;
|
break;
|
||||||
case REWARD: // 打赏单
|
case REWARD: // 打赏单
|
||||||
fillRewardOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo);
|
fillRewardOrderRevenue(firstOrder, baseAmount, levelInfo, estimatedRevenueVo);
|
||||||
break;
|
break;
|
||||||
case OTHER:
|
case OTHER:
|
||||||
default:
|
default:
|
||||||
if (!fallbackToOther) {
|
if (!fallbackToOther) {
|
||||||
log.warn("按其他下单类型计算预计收益,placeType={},clerkId={}", placeType, clerkId);
|
log.warn("按其他下单类型计算预计收益,placeType={},clerkId={}", placeType, clerkId);
|
||||||
}
|
}
|
||||||
estimatedRevenueVo.setRevenueAmount(finalAmount);
|
estimatedRevenueVo.setRevenueAmount(baseAmount);
|
||||||
estimatedRevenueVo.setRevenueRatio(100);
|
estimatedRevenueVo.setRevenueRatio(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -70,36 +71,36 @@ public class ClerkRevenueCalculator {
|
|||||||
return estimatedRevenueVo;
|
return estimatedRevenueVo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillRegularOrderRevenue(String firstOrder, BigDecimal finalAmount,
|
private void fillRegularOrderRevenue(String firstOrder, BigDecimal orderAmount,
|
||||||
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
||||||
if ("1".equals(firstOrder)) {
|
if ("1".equals(firstOrder)) {
|
||||||
vo.setRevenueRatio(levelInfo.getFirstRegularRatio());
|
vo.setRevenueRatio(levelInfo.getFirstRegularRatio());
|
||||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRegularRatio()));
|
vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getFirstRegularRatio()));
|
||||||
} else {
|
} else {
|
||||||
vo.setRevenueRatio(levelInfo.getNotFirstRegularRatio());
|
vo.setRevenueRatio(levelInfo.getNotFirstRegularRatio());
|
||||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRegularRatio()));
|
vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getNotFirstRegularRatio()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillRandomOrderRevenue(String firstOrder, BigDecimal finalAmount,
|
private void fillRandomOrderRevenue(String firstOrder, BigDecimal orderAmount,
|
||||||
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
||||||
if ("1".equals(firstOrder)) {
|
if ("1".equals(firstOrder)) {
|
||||||
vo.setRevenueRatio(levelInfo.getFirstRandomRadio());
|
vo.setRevenueRatio(levelInfo.getFirstRandomRadio());
|
||||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRandomRadio()));
|
vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getFirstRandomRadio()));
|
||||||
} else {
|
} else {
|
||||||
vo.setRevenueRatio(levelInfo.getNotFirstRandomRadio());
|
vo.setRevenueRatio(levelInfo.getNotFirstRandomRadio());
|
||||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRandomRadio()));
|
vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getNotFirstRandomRadio()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillRewardOrderRevenue(String firstOrder, BigDecimal finalAmount,
|
private void fillRewardOrderRevenue(String firstOrder, BigDecimal orderAmount,
|
||||||
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
||||||
if ("1".equals(firstOrder)) {
|
if ("1".equals(firstOrder)) {
|
||||||
vo.setRevenueRatio(levelInfo.getFirstRewardRatio());
|
vo.setRevenueRatio(levelInfo.getFirstRewardRatio());
|
||||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRewardRatio()));
|
vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getFirstRewardRatio()));
|
||||||
} else {
|
} else {
|
||||||
vo.setRevenueRatio(levelInfo.getNotFirstRewardRatio());
|
vo.setRevenueRatio(levelInfo.getNotFirstRewardRatio());
|
||||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRewardRatio()));
|
vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getNotFirstRewardRatio()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import com.starry.admin.common.exception.CustomException;
|
|||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
import com.starry.admin.modules.shop.module.vo.*;
|
import com.starry.admin.modules.shop.module.vo.*;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
|
||||||
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
import com.starry.common.annotation.Log;
|
import com.starry.common.annotation.Log;
|
||||||
import com.starry.common.enums.BusinessType;
|
import com.starry.common.enums.BusinessType;
|
||||||
@@ -38,9 +38,6 @@ public class PlayCouponInfoController {
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayCouponInfoService playCouponInfoService;
|
private IPlayCouponInfoService playCouponInfoService;
|
||||||
|
|
||||||
@Resource
|
|
||||||
private IPlayCouponDetailsService playCouponDetailsService;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IPlayCustomUserInfoService playCustomUserInfoService;
|
private IPlayCustomUserInfoService playCustomUserInfoService;
|
||||||
|
|
||||||
@@ -85,9 +82,9 @@ public class PlayCouponInfoController {
|
|||||||
@PostMapping("/sendCoupon")
|
@PostMapping("/sendCoupon")
|
||||||
public R sendCoupon(@ApiParam(value = "优惠券发放信息", required = true) @Validated @RequestBody PlayCouponInfoSendVo vo) {
|
public R sendCoupon(@ApiParam(value = "优惠券发放信息", required = true) @Validated @RequestBody PlayCouponInfoSendVo vo) {
|
||||||
PlayCustomUserInfoEntity customUserInfo = playCustomUserInfoService.selectById(vo.getCustomId());
|
PlayCustomUserInfoEntity customUserInfo = playCustomUserInfoService.selectById(vo.getCustomId());
|
||||||
for (Integer i = 0; i < vo.getSendNumber(); i++) {
|
for (int i = 0; i < vo.getSendNumber(); i++) {
|
||||||
playCouponDetailsService.create(customUserInfo.getId(), customUserInfo.getNickname(),
|
playCouponInfoService.claimCouponForCustom(
|
||||||
customUserInfo.getLevelId(), vo.getId(), "2", "1");
|
vo.getId(), customUserInfo, CouponObtainChannel.BACKEND_GRANT);
|
||||||
}
|
}
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
@@ -131,6 +128,31 @@ public class PlayCouponInfoController {
|
|||||||
if ("2".equals(vo.getValidityPeriodType()) && vo.getEffectiveDay() <= 0) {
|
if ("2".equals(vo.getValidityPeriodType()) && vo.getEffectiveDay() <= 0) {
|
||||||
throw new CustomException("用券时间输入错误,有效天数不能为0");
|
throw new CustomException("用券时间输入错误,有效天数不能为0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean unlimitedInventory = "1".equals(vo.getCouponQuantityType());
|
||||||
|
if (unlimitedInventory) {
|
||||||
|
entity.setCouponQuantity(null);
|
||||||
|
entity.setRemainingQuantity(null);
|
||||||
|
} else {
|
||||||
|
Integer totalQuantity = vo.getCouponQuantity();
|
||||||
|
if (totalQuantity == null || totalQuantity <= 0) {
|
||||||
|
throw new CustomException("优惠券总数必须大于0");
|
||||||
|
}
|
||||||
|
entity.setCouponQuantity(totalQuantity);
|
||||||
|
entity.setRemainingQuantity(totalQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean unlimitedPerUser = "1".equals(vo.getClerkObtainedMaxQuantityType());
|
||||||
|
if (unlimitedPerUser) {
|
||||||
|
entity.setClerkObtainedMaxQuantity(null);
|
||||||
|
} else {
|
||||||
|
Integer perUserLimit = vo.getClerkObtainedMaxQuantity();
|
||||||
|
if (perUserLimit == null || perUserLimit <= 0) {
|
||||||
|
throw new CustomException("每人限领数量必须大于0");
|
||||||
|
}
|
||||||
|
entity.setClerkObtainedMaxQuantity(perUserLimit);
|
||||||
|
}
|
||||||
|
|
||||||
String discountContent = "";
|
String discountContent = "";
|
||||||
if (BigDecimal.ZERO.compareTo(vo.getUseMinAmount()) == 0) {
|
if (BigDecimal.ZERO.compareTo(vo.getUseMinAmount()) == 0) {
|
||||||
discountContent = "无门槛";
|
discountContent = "无门槛";
|
||||||
|
|||||||
@@ -2,13 +2,33 @@ package com.starry.admin.modules.shop.mapper;
|
|||||||
|
|
||||||
import com.github.yulichang.base.MPJBaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优惠券信息Mapper接口
|
* 优惠券信息Mapper接口
|
||||||
*
|
|
||||||
* @author admin
|
|
||||||
* @since 2024-07-04
|
|
||||||
*/
|
*/
|
||||||
|
@Mapper
|
||||||
public interface PlayCouponInfoMapper extends MPJBaseMapper<PlayCouponInfoEntity> {
|
public interface PlayCouponInfoMapper extends MPJBaseMapper<PlayCouponInfoEntity> {
|
||||||
|
|
||||||
|
@Update({
|
||||||
|
"UPDATE play_coupon_info",
|
||||||
|
"SET issued_quantity = IFNULL(issued_quantity, 0) + 1,",
|
||||||
|
" remaining_quantity = CASE",
|
||||||
|
" WHEN remaining_quantity IS NULL THEN NULL",
|
||||||
|
" WHEN remaining_quantity < 0 THEN remaining_quantity",
|
||||||
|
" ELSE remaining_quantity - 1",
|
||||||
|
" END",
|
||||||
|
"WHERE id = #{id}",
|
||||||
|
" AND (remaining_quantity IS NULL OR remaining_quantity <> 0)"
|
||||||
|
})
|
||||||
|
int increaseIssuedAndConsumeRemaining(String id);
|
||||||
|
|
||||||
|
@Select({
|
||||||
|
"SELECT * FROM play_coupon_info",
|
||||||
|
"WHERE id = #{id}",
|
||||||
|
"FOR UPDATE"
|
||||||
|
})
|
||||||
|
PlayCouponInfoEntity selectByIdForUpdate(String id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.enums;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠券领取渠道
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum CouponObtainChannel {
|
||||||
|
SELF_SERVICE("1"),
|
||||||
|
BACKEND_GRANT("2"),
|
||||||
|
REFUND_RETURN("3"),
|
||||||
|
SHARE("4"),
|
||||||
|
LOTTERY("5");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
CouponObtainChannel(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<CouponObtainChannel> fromCode(String code) {
|
||||||
|
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.enums;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠券上下架状态
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum CouponOnlineState {
|
||||||
|
OFFLINE("0"),
|
||||||
|
ONLINE("1");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
CouponOnlineState(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<CouponOnlineState> fromCode(String code) {
|
||||||
|
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.enums;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠券有效期类型
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum CouponValidityPeriodType {
|
||||||
|
PERMANENT("0"),
|
||||||
|
FIXED_RANGE("1"),
|
||||||
|
RELATIVE_DAYS("2");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
CouponValidityPeriodType(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<CouponValidityPeriodType> fromCode(String code) {
|
||||||
|
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates how coupon inventory should behave (finite vs. unlimited).
|
||||||
|
*/
|
||||||
|
public enum CouponInventoryPolicy {
|
||||||
|
UNLIMITED,
|
||||||
|
FINITE;
|
||||||
|
|
||||||
|
public boolean isUnlimited() {
|
||||||
|
return this == UNLIMITED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean consumesStock() {
|
||||||
|
return this == FINITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasStock(Integer remainingQuantity) {
|
||||||
|
if (isUnlimited()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return remainingQuantity == null || remainingQuantity > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CouponInventoryPolicy resolve(Integer totalQuantity, Integer remainingQuantity) {
|
||||||
|
if (isUnlimitedSentinel(totalQuantity) || isUnlimitedSentinel(remainingQuantity)) {
|
||||||
|
return UNLIMITED;
|
||||||
|
}
|
||||||
|
if (totalQuantity == null && remainingQuantity == null) {
|
||||||
|
return UNLIMITED;
|
||||||
|
}
|
||||||
|
return FINITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isUnlimitedSentinel(Integer value) {
|
||||||
|
return value != null && value < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,11 @@ public interface IPlayCouponDetailsService extends IService<PlayCouponDetailsEnt
|
|||||||
*/
|
*/
|
||||||
boolean create(PlayCouponDetailsEntity playCouponDetails);
|
boolean create(PlayCouponDetailsEntity playCouponDetails);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定用户的有效优惠券数量(排除已回收)。
|
||||||
|
*/
|
||||||
|
long countActiveByCustomAndCoupon(String couponId, String customId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改优惠券详情
|
* 修改优惠券详情
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优惠券信息Service接口
|
* 优惠券信息Service接口
|
||||||
@@ -35,6 +38,27 @@ public interface IPlayCouponInfoService extends IService<PlayCouponInfoEntity> {
|
|||||||
String getCouponReasonForUnavailableUse(PlayCouponInfoEntity couponInfo, String placeType, String commodityId,
|
String getCouponReasonForUnavailableUse(PlayCouponInfoEntity couponInfo, String placeType, String commodityId,
|
||||||
Integer commodityQuantity, BigDecimal price);
|
Integer commodityQuantity, BigDecimal price);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取优惠券在状态层面的不可用原因(如已使用、已过期等)。
|
||||||
|
*
|
||||||
|
* @param couponInfo
|
||||||
|
* 优惠券信息
|
||||||
|
* @param useState
|
||||||
|
* 优惠券详情使用状态
|
||||||
|
* @param obtainingTime
|
||||||
|
* 优惠券领取时间
|
||||||
|
* @return 不可用原因
|
||||||
|
*/
|
||||||
|
Optional<String> getCouponDetailRestrictionReason(
|
||||||
|
PlayCouponInfoEntity couponInfo,
|
||||||
|
String useState,
|
||||||
|
LocalDateTime obtainingTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客领取优惠券,校验库存与限领并生成明细。
|
||||||
|
*/
|
||||||
|
void claimCouponForCustom(String couponId, PlayCustomUserInfoEntity customUserInfo, CouponObtainChannel obtainChannel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取优惠券不可领取的原因
|
* 获取优惠券不可领取的原因
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||||
import com.starry.admin.modules.shop.mapper.PlayCouponDetailsMapper;
|
import com.starry.admin.modules.shop.mapper.PlayCouponDetailsMapper;
|
||||||
|
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.PlayCouponDetailsEntity;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsQueryVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsQueryVo;
|
||||||
@@ -123,6 +124,15 @@ public class PlayCouponDetailsServiceImpl extends ServiceImpl<PlayCouponDetailsM
|
|||||||
return save(playCouponDetails);
|
return save(playCouponDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long countActiveByCustomAndCoupon(String couponId, String customId) {
|
||||||
|
return lambdaQuery()
|
||||||
|
.eq(PlayCouponDetailsEntity::getCouponId, couponId)
|
||||||
|
.eq(PlayCouponDetailsEntity::getCustomId, customId)
|
||||||
|
.eq(PlayCouponDetailsEntity::getUseState, CouponUseState.UNUSED.getCode())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PlayCouponDetailsReturnVo> selectByCustomId(String customId) {
|
public List<PlayCouponDetailsReturnVo> selectByCustomId(String customId) {
|
||||||
MPJLambdaWrapper<PlayCouponDetailsEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
MPJLambdaWrapper<PlayCouponDetailsEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
||||||
|
|||||||
@@ -9,17 +9,26 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
|||||||
import com.starry.admin.common.exception.CustomException;
|
import com.starry.admin.common.exception.CustomException;
|
||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.shop.mapper.PlayCouponInfoMapper;
|
import com.starry.admin.modules.shop.mapper.PlayCouponInfoMapper;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||||
|
import com.starry.admin.modules.shop.module.support.CouponInventoryPolicy;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优惠券信息Service业务层处理
|
* 优惠券信息Service业务层处理
|
||||||
@@ -34,6 +43,9 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl<PlayCouponInfoMapper,
|
|||||||
@Resource
|
@Resource
|
||||||
private PlayCouponInfoMapper playCouponInfoMapper;
|
private PlayCouponInfoMapper playCouponInfoMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayCouponDetailsService playCouponDetailsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取优惠券不可用的原因
|
* 获取优惠券不可用的原因
|
||||||
*
|
*
|
||||||
@@ -71,6 +83,108 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl<PlayCouponInfoMapper,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getCouponDetailRestrictionReason(
|
||||||
|
PlayCouponInfoEntity couponInfo,
|
||||||
|
String useState,
|
||||||
|
LocalDateTime obtainingTime) {
|
||||||
|
if (couponInfo == null) {
|
||||||
|
return Optional.of("优惠券不存在");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(useState)) {
|
||||||
|
return Optional.of("优惠券状态异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
CouponUseState state;
|
||||||
|
try {
|
||||||
|
state = CouponUseState.fromCode(useState);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return Optional.of("优惠券状态异常");
|
||||||
|
}
|
||||||
|
if (state != CouponUseState.UNUSED) {
|
||||||
|
return Optional.of("优惠券已使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<CouponOnlineState> onlineStateOpt = CouponOnlineState.fromCode(couponInfo.getCouponOnLineState());
|
||||||
|
if (onlineStateOpt.isEmpty()) {
|
||||||
|
return Optional.of("优惠券状态异常");
|
||||||
|
}
|
||||||
|
if (onlineStateOpt.get() != CouponOnlineState.ONLINE) {
|
||||||
|
return Optional.of("优惠券已下架");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
CouponValidityPeriodType validityType = CouponValidityPeriodType
|
||||||
|
.fromCode(couponInfo.getValidityPeriodType())
|
||||||
|
.orElse(CouponValidityPeriodType.PERMANENT);
|
||||||
|
if (validityType == CouponValidityPeriodType.FIXED_RANGE) {
|
||||||
|
if (couponInfo.getProductiveTime() != null && now.isBefore(couponInfo.getProductiveTime())) {
|
||||||
|
return Optional.of("优惠券未生效");
|
||||||
|
}
|
||||||
|
if (couponInfo.getExpirationTime() != null && now.isAfter(couponInfo.getExpirationTime())) {
|
||||||
|
return Optional.of("优惠券已过期");
|
||||||
|
}
|
||||||
|
} else if (validityType == CouponValidityPeriodType.RELATIVE_DAYS) {
|
||||||
|
Integer effectiveDay = couponInfo.getEffectiveDay();
|
||||||
|
if (obtainingTime == null || effectiveDay == null || effectiveDay <= 0) {
|
||||||
|
return Optional.of("优惠券有效期未配置");
|
||||||
|
}
|
||||||
|
LocalDateTime expiration = obtainingTime.plusDays(effectiveDay);
|
||||||
|
if (now.isAfter(expiration)) {
|
||||||
|
return Optional.of("优惠券已过期");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void claimCouponForCustom(
|
||||||
|
String couponId,
|
||||||
|
PlayCustomUserInfoEntity customUserInfo,
|
||||||
|
CouponObtainChannel obtainChannel) {
|
||||||
|
if (customUserInfo == null) {
|
||||||
|
throw new CustomException("顾客信息不存在");
|
||||||
|
}
|
||||||
|
CouponObtainChannel channel = obtainChannel != null ? obtainChannel : CouponObtainChannel.SELF_SERVICE;
|
||||||
|
|
||||||
|
PlayCouponInfoEntity couponInfo = playCouponInfoMapper.selectByIdForUpdate(couponId);
|
||||||
|
if (couponInfo == null) {
|
||||||
|
throw new CustomException("优惠券不存在");
|
||||||
|
}
|
||||||
|
Optional<CouponOnlineState> onlineState = CouponOnlineState.fromCode(couponInfo.getCouponOnLineState());
|
||||||
|
if (onlineState.isPresent() && onlineState.get() != CouponOnlineState.ONLINE) {
|
||||||
|
throw new CustomException("优惠券已下架");
|
||||||
|
}
|
||||||
|
|
||||||
|
CouponInventoryPolicy inventoryPolicy = CouponInventoryPolicy
|
||||||
|
.resolve(couponInfo.getCouponQuantity(), couponInfo.getRemainingQuantity());
|
||||||
|
if (!inventoryPolicy.hasStock(couponInfo.getRemainingQuantity())) {
|
||||||
|
throw new CustomException("优惠券已领完");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer maxQuantityPerUser = couponInfo.getClerkObtainedMaxQuantity();
|
||||||
|
if (maxQuantityPerUser != null && maxQuantityPerUser > 0) {
|
||||||
|
long claimed = playCouponDetailsService.countActiveByCustomAndCoupon(couponId, customUserInfo.getId());
|
||||||
|
if (claimed >= maxQuantityPerUser) {
|
||||||
|
throw new CustomException("优惠券已达到领取上限");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int affected = playCouponInfoMapper.increaseIssuedAndConsumeRemaining(couponId);
|
||||||
|
if (inventoryPolicy.consumesStock() && affected == 0) {
|
||||||
|
throw new CustomException("优惠券已领完");
|
||||||
|
}
|
||||||
|
|
||||||
|
playCouponDetailsService.create(
|
||||||
|
customUserInfo.getId(),
|
||||||
|
customUserInfo.getNickname(),
|
||||||
|
customUserInfo.getLevelId(),
|
||||||
|
couponId,
|
||||||
|
channel.getCode(),
|
||||||
|
CouponUseState.UNUSED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取优惠券不可领取的原因
|
* 获取优惠券不可领取的原因
|
||||||
*
|
*
|
||||||
@@ -85,6 +199,22 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl<PlayCouponInfoMapper,
|
|||||||
@Override
|
@Override
|
||||||
public String getReasonForNotObtainingCoupons(PlayCouponInfoEntity entity,
|
public String getReasonForNotObtainingCoupons(PlayCouponInfoEntity entity,
|
||||||
PlayCustomUserInfoEntity customUserInfo) {
|
PlayCustomUserInfoEntity customUserInfo) {
|
||||||
|
CouponInventoryPolicy inventoryPolicy = CouponInventoryPolicy
|
||||||
|
.resolve(entity.getCouponQuantity(), entity.getRemainingQuantity());
|
||||||
|
if (!inventoryPolicy.hasStock(entity.getRemainingQuantity())) {
|
||||||
|
return "优惠券已领完";
|
||||||
|
}
|
||||||
|
if (customUserInfo != null && playCouponDetailsService != null) {
|
||||||
|
Integer perUserLimit = entity.getClerkObtainedMaxQuantity();
|
||||||
|
if (perUserLimit != null && perUserLimit > 0) {
|
||||||
|
long claimed = playCouponDetailsService.countActiveByCustomAndCoupon(
|
||||||
|
entity.getId(),
|
||||||
|
customUserInfo.getId());
|
||||||
|
if (claimed >= perUserLimit) {
|
||||||
|
return "优惠券已达到领取上限";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CouponClaimConditionType type = CouponClaimConditionType.of(entity.getClaimConditionType());
|
CouponClaimConditionType type = CouponClaimConditionType.of(entity.getClaimConditionType());
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ALL:
|
case ALL:
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
|||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||||
|
import com.starry.admin.modules.shop.module.support.CouponInventoryPolicy;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
||||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||||
@@ -26,10 +30,12 @@ import io.swagger.annotations.ApiOperation;
|
|||||||
import io.swagger.annotations.ApiParam;
|
import io.swagger.annotations.ApiParam;
|
||||||
import io.swagger.annotations.ApiResponse;
|
import io.swagger.annotations.ApiResponse;
|
||||||
import io.swagger.annotations.ApiResponses;
|
import io.swagger.annotations.ApiResponses;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
@@ -71,10 +77,13 @@ public class WxCouponController {
|
|||||||
PlayCouponInfoEntity entity = couponInfoService.selectPlayCouponInfoById(id);
|
PlayCouponInfoEntity entity = couponInfoService.selectPlayCouponInfoById(id);
|
||||||
String msg = couponInfoService.getReasonForNotObtainingCoupons(entity, customUserInfo);
|
String msg = couponInfoService.getReasonForNotObtainingCoupons(entity, customUserInfo);
|
||||||
boolean success = StrUtil.isBlank(msg);
|
boolean success = StrUtil.isBlank(msg);
|
||||||
// 优惠券领取验证通过,发放优惠券
|
|
||||||
if (success) {
|
if (success) {
|
||||||
couponDetailsService.create(customUserInfo.getId(), customUserInfo.getNickname(),
|
try {
|
||||||
customUserInfo.getLevelId(), entity.getId(), "1", "1");
|
couponInfoService.claimCouponForCustom(entity.getId(), customUserInfo, CouponObtainChannel.SELF_SERVICE);
|
||||||
|
} catch (CustomException ex) {
|
||||||
|
success = false;
|
||||||
|
msg = ex.getMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("success", success);
|
result.put("success", success);
|
||||||
@@ -99,11 +108,39 @@ public class WxCouponController {
|
|||||||
List<PlayCouponDetailsReturnVo> obtainedCoupons = couponDetailsService
|
List<PlayCouponDetailsReturnVo> obtainedCoupons = couponDetailsService
|
||||||
.selectByCustomId(currentCustomId);
|
.selectByCustomId(currentCustomId);
|
||||||
List<PlayCouponInfoEntity> couponInfoEntities = couponInfoService.queryAll();
|
List<PlayCouponInfoEntity> couponInfoEntities = couponInfoService.queryAll();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
List<WxCouponReceiveReturnVo> returnVos = new ArrayList<>(couponInfoEntities.size());
|
List<WxCouponReceiveReturnVo> returnVos = new ArrayList<>(couponInfoEntities.size());
|
||||||
for (PlayCouponInfoEntity couponInfoEntity : couponInfoEntities) {
|
for (PlayCouponInfoEntity couponInfoEntity : couponInfoEntities) {
|
||||||
if ("0".equals(couponInfoEntity.getCouponOnLineState())) {
|
boolean online = CouponOnlineState.fromCode(couponInfoEntity.getCouponOnLineState())
|
||||||
|
.map(state -> state == CouponOnlineState.ONLINE)
|
||||||
|
.orElse(false);
|
||||||
|
if (!online) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
CouponInventoryPolicy inventoryPolicy = CouponInventoryPolicy
|
||||||
|
.resolve(couponInfoEntity.getCouponQuantity(), couponInfoEntity.getRemainingQuantity());
|
||||||
|
if (!inventoryPolicy.hasStock(couponInfoEntity.getRemainingQuantity())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CouponValidityPeriodType validityType = CouponValidityPeriodType
|
||||||
|
.fromCode(couponInfoEntity.getValidityPeriodType())
|
||||||
|
.orElse(CouponValidityPeriodType.PERMANENT);
|
||||||
|
if (validityType == CouponValidityPeriodType.FIXED_RANGE) {
|
||||||
|
if (couponInfoEntity.getProductiveTime() != null
|
||||||
|
&& now.isBefore(couponInfoEntity.getProductiveTime())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (couponInfoEntity.getExpirationTime() != null
|
||||||
|
&& now.isAfter(couponInfoEntity.getExpirationTime())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (validityType == CouponValidityPeriodType.RELATIVE_DAYS) {
|
||||||
|
Integer effectiveDay = couponInfoEntity.getEffectiveDay();
|
||||||
|
if (effectiveDay == null || effectiveDay <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// RELATIVE_DAYS 按领取时间计算,这里仅进行配置有效性校验,实际过期由详情校验处理
|
||||||
|
}
|
||||||
// 领取白名单:非白名单用户不可见
|
// 领取白名单:非白名单用户不可见
|
||||||
if (CouponClaimConditionType.WHITELIST.code().equals(couponInfoEntity.getClaimConditionType())) {
|
if (CouponClaimConditionType.WHITELIST.code().equals(couponInfoEntity.getClaimConditionType())) {
|
||||||
List<String> wl = couponInfoEntity.getCustomWhitelist();
|
List<String> wl = couponInfoEntity.getCustomWhitelist();
|
||||||
@@ -151,6 +188,14 @@ public class WxCouponController {
|
|||||||
try {
|
try {
|
||||||
PlayCouponInfoEntity couponInfo = couponInfoService.selectPlayCouponInfoById(couponDetails.getCouponId());
|
PlayCouponInfoEntity couponInfo = couponInfoService.selectPlayCouponInfoById(couponDetails.getCouponId());
|
||||||
WxCouponOrderReturnVo wxCouponReturnVo = ConvertUtil.entityToVo(couponDetails, WxCouponOrderReturnVo.class);
|
WxCouponOrderReturnVo wxCouponReturnVo = ConvertUtil.entityToVo(couponDetails, WxCouponOrderReturnVo.class);
|
||||||
|
Optional<String> statusReason = couponInfoService.getCouponDetailRestrictionReason(
|
||||||
|
couponInfo,
|
||||||
|
couponDetails.getUseState(),
|
||||||
|
couponDetails.getObtainingTime());
|
||||||
|
if (statusReason.isPresent()) {
|
||||||
|
// 已使用、过期、下架等状态的优惠券不展示
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String couponReasonForUnavailableUse = couponInfoService.getCouponReasonForUnavailableUse(couponInfo,
|
String couponReasonForUnavailableUse = couponInfoService.getCouponReasonForUnavailableUse(couponInfo,
|
||||||
vo.getPlaceType(), vo.getCommodityId(), vo.getCommodityQuantity(),
|
vo.getPlaceType(), vo.getCommodityId(), vo.getCommodityQuantity(),
|
||||||
commodityInfo.getCommodityPrice());
|
commodityInfo.getCommodityPrice());
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|||||||
import com.starry.admin.common.aspect.CustomUserLogin;
|
import com.starry.admin.common.aspect.CustomUserLogin;
|
||||||
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
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.common.task.OverdueOrderHandlerTask;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
|
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.constant.OrderConstant.RewardType;
|
||||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
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.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.PaymentInfo;
|
||||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
|
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.IPlayOrderComplaintInfoService;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
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.module.vo.PlayCommodityInfoVo;
|
||||||
import com.starry.admin.modules.shop.service.*;
|
import com.starry.admin.modules.shop.service.*;
|
||||||
import com.starry.admin.modules.weichat.entity.*;
|
import com.starry.admin.modules.weichat.entity.*;
|
||||||
@@ -63,12 +62,13 @@ import io.swagger.annotations.ApiResponse;
|
|||||||
import io.swagger.annotations.ApiResponses;
|
import io.swagger.annotations.ApiResponses;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -230,10 +230,6 @@ public class WxCustomController {
|
|||||||
public R rewardToOrder(@ApiParam(value = "打赏信息", required = true) @Validated @RequestBody PlayOrderInfoRewardAdd vo) {
|
public R rewardToOrder(@ApiParam(value = "打赏信息", required = true) @Validated @RequestBody PlayOrderInfoRewardAdd vo) {
|
||||||
MoneyUtils.verificationTypeIsNormal(vo.getMoney());
|
MoneyUtils.verificationTypeIsNormal(vo.getMoney());
|
||||||
String userId = ThreadLocalRequestDetail.getCustomUserInfo().getId();
|
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();
|
String orderId = IdUtils.getUuid();
|
||||||
// 记录订单信息
|
// 记录订单信息
|
||||||
OrderCreationContext orderRequest = OrderCreationContext.builder()
|
OrderCreationContext orderRequest = OrderCreationContext.builder()
|
||||||
@@ -265,9 +261,10 @@ public class WxCustomController {
|
|||||||
.weiChatCode(vo.getWeiChatCode())
|
.weiChatCode(vo.getWeiChatCode())
|
||||||
.remark(vo.getRemark())
|
.remark(vo.getRemark())
|
||||||
.build();
|
.build();
|
||||||
orderLifecycleService.initiateOrder(orderRequest);
|
orderLifecycleService.placeOrder(OrderPlacementCommand.builder()
|
||||||
// 顾客减少余额
|
.orderContext(orderRequest)
|
||||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(new BigDecimal(vo.getMoney())), "1", "打赏", new BigDecimal(vo.getMoney()), BigDecimal.ZERO, orderId);
|
.balanceOperationAction("打赏")
|
||||||
|
.build());
|
||||||
return R.ok("成功");
|
return R.ok("成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,34 +298,6 @@ public class WxCustomController {
|
|||||||
|
|
||||||
PlayCommodityInfoVo commodityInfo = playCommodityInfoService.queryCommodityInfo(vo.getCommodityId(), clerkUserInfo.getLevelId());
|
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 orderId = IdUtils.getUuid();
|
||||||
String orderNo = playOrderInfoService.getOrderNo();
|
String orderNo = playOrderInfoService.getOrderNo();
|
||||||
// 记录订单信息
|
// 记录订单信息
|
||||||
@@ -351,21 +320,39 @@ public class WxCustomController {
|
|||||||
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
|
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
|
||||||
.build())
|
.build())
|
||||||
.paymentInfo(PaymentInfo.builder()
|
.paymentInfo(PaymentInfo.builder()
|
||||||
.orderMoney(money)
|
.orderMoney(BigDecimal.ZERO)
|
||||||
.finalAmount(money)
|
.finalAmount(BigDecimal.ZERO)
|
||||||
.discountAmount(BigDecimal.ZERO)
|
.discountAmount(BigDecimal.ZERO)
|
||||||
.couponIds(vo.getCouponIds())
|
.couponIds(Collections.emptyList())
|
||||||
.build())
|
.build())
|
||||||
.purchaserBy(customId)
|
.purchaserBy(customId)
|
||||||
.acceptBy(clerkUserInfo.getId())
|
.acceptBy(clerkUserInfo.getId())
|
||||||
.weiChatCode(vo.getWeiChatCode())
|
.weiChatCode(vo.getWeiChatCode())
|
||||||
.remark(vo.getRemark())
|
.remark(vo.getRemark())
|
||||||
.build();
|
.build();
|
||||||
orderLifecycleService.initiateOrder(orderRequest);
|
List<String> couponIds = CollectionUtils.isEmpty(vo.getCouponIds()) ? Collections.emptyList() : vo.getCouponIds();
|
||||||
// 顾客减少余额
|
OrderPlacementCommand command = OrderPlacementCommand.builder()
|
||||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-指定单", money, BigDecimal.ZERO, orderId);
|
.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("成功");
|
return R.ok("成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,12 +367,7 @@ public class WxCustomController {
|
|||||||
public R randomToOrdder(@Validated @RequestBody PlayOrderInfoRandomAdd vo) {
|
public R randomToOrdder(@Validated @RequestBody PlayOrderInfoRandomAdd vo) {
|
||||||
// 验证当前顾客余额是否充足
|
// 验证当前顾客余额是否充足
|
||||||
String customId = ThreadLocalRequestDetail.getCustomUserInfo().getId();
|
String customId = ThreadLocalRequestDetail.getCustomUserInfo().getId();
|
||||||
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.selectById(customId);
|
|
||||||
PlayCommodityInfoVo commodityInfo = playCommodityInfoService.queryCommodityInfo(vo.getCommodityId(), vo.getLevelId());
|
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 orderId = IdUtils.getUuid();
|
||||||
String orderNo = playOrderInfoService.getOrderNo();
|
String orderNo = playOrderInfoService.getOrderNo();
|
||||||
// 记录订单信息
|
// 记录订单信息
|
||||||
@@ -408,10 +390,10 @@ public class WxCustomController {
|
|||||||
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
|
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
|
||||||
.build())
|
.build())
|
||||||
.paymentInfo(PaymentInfo.builder()
|
.paymentInfo(PaymentInfo.builder()
|
||||||
.orderMoney(money)
|
.orderMoney(BigDecimal.ZERO)
|
||||||
.finalAmount(money)
|
.finalAmount(BigDecimal.ZERO)
|
||||||
.discountAmount(BigDecimal.ZERO)
|
.discountAmount(BigDecimal.ZERO)
|
||||||
.couponIds(vo.getCouponIds())
|
.couponIds(Collections.emptyList())
|
||||||
.build())
|
.build())
|
||||||
.purchaserBy(customId)
|
.purchaserBy(customId)
|
||||||
.weiChatCode(vo.getWeiChatCode())
|
.weiChatCode(vo.getWeiChatCode())
|
||||||
@@ -422,13 +404,28 @@ public class WxCustomController {
|
|||||||
.excludeHistory(vo.getExcludeHistory())
|
.excludeHistory(vo.getExcludeHistory())
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
orderLifecycleService.initiateOrder(orderRequest);
|
List<String> couponIds = CollectionUtils.isEmpty(vo.getCouponIds()) ? Collections.emptyList() : vo.getCouponIds();
|
||||||
// 顾客减少余额
|
OrderPlacementCommand command = OrderPlacementCommand.builder()
|
||||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-随机单", money, BigDecimal.ZERO, orderId);
|
.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")
|
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class)
|
||||||
.eq(PlayClerkUserInfoEntity::getOnlineState, "1").eq(PlayClerkUserInfoEntity::getSex, vo.getSex()));
|
.isNotNull(PlayClerkUserInfoEntity::getOpenid)
|
||||||
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderNo, money.toString(), commodityInfo.getCommodityName(),orderId);
|
.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());
|
overdueOrderHandlerTask.enqueue(orderId + "_" + SecurityUtils.getTenantId());
|
||||||
// 下单成功后,先根据用户条件进行随机分配
|
// 下单成功后,先根据用户条件进行随机分配
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.starry.admin.modules.weichat.service;
|
|||||||
|
|
||||||
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
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.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
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.constant.OrderConstant.RewardType;
|
||||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
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.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.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.IOrderLifecycleService;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
@@ -79,27 +81,21 @@ public class WxGiftOrderService {
|
|||||||
throw new CustomException("礼物金额必须大于0");
|
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();
|
String orderId = IdUtils.getUuid();
|
||||||
OrderCreationContext orderRequest = buildOrderCreationContext(orderId, request, sessionUser.getId(),
|
OrderCreationContext orderRequest = buildOrderCreationContext(orderId, request, sessionUser.getId(),
|
||||||
giftInfo, totalAmount);
|
giftInfo, totalAmount);
|
||||||
orderLifecycleService.initiateOrder(orderRequest);
|
OrderPlacementResult result = orderLifecycleService.placeOrder(OrderPlacementCommand.builder()
|
||||||
|
.orderContext(orderRequest)
|
||||||
BigDecimal newBalance = currentBalance.subtract(totalAmount);
|
.balanceOperationAction("赠送礼物")
|
||||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), currentBalance, newBalance, "1",
|
.build());
|
||||||
"赠送礼物", totalAmount, BigDecimal.ZERO, orderId);
|
PlayOrderInfoEntity order = result.getOrder();
|
||||||
|
|
||||||
String tenantId = customUserInfo.getTenantId();
|
String tenantId = customUserInfo.getTenantId();
|
||||||
long delta = giftQuantity;
|
long delta = giftQuantity;
|
||||||
playCustomGiftInfoService.incrementGiftCount(customUserInfo.getId(), request.getGiftId(), tenantId, delta);
|
playCustomGiftInfoService.incrementGiftCount(customUserInfo.getId(), request.getGiftId(), tenantId, delta);
|
||||||
playClerkGiftInfoService.incrementGiftCount(request.getClerkId(), request.getGiftId(), tenantId, delta);
|
playClerkGiftInfoService.incrementGiftCount(request.getClerkId(), request.getGiftId(), tenantId, delta);
|
||||||
|
|
||||||
return orderId;
|
return order.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private OrderCreationContext buildOrderCreationContext(String orderId, PlayOrderInfoGiftAdd request,
|
private OrderCreationContext buildOrderCreationContext(String orderId, PlayOrderInfoGiftAdd request,
|
||||||
|
|||||||
@@ -162,9 +162,9 @@ public class EarningsBackfillServiceImpl implements IEarningsBackfillService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
BigDecimal amount = order.getEstimatedRevenue() != null ? order.getEstimatedRevenue()
|
BigDecimal amount = order.getEstimatedRevenue() != null ? order.getEstimatedRevenue()
|
||||||
: (order.getFinalAmount() != null ? order.getFinalAmount() : BigDecimal.ZERO);
|
: (order.getOrderMoney() != null ? order.getOrderMoney() : BigDecimal.ZERO);
|
||||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
if (amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
addWarning(warnings, "订单 " + order.getId() + " 收益金额为0,跳过");
|
addWarning(warnings, "订单 " + order.getId() + " 收益金额为负数,跳过");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (order.getOrderEndTime() == null) {
|
if (order.getOrderEndTime() == null) {
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
|||||||
@Override
|
@Override
|
||||||
public void createFromOrder(PlayOrderInfoEntity orderInfo) {
|
public void createFromOrder(PlayOrderInfoEntity orderInfo) {
|
||||||
if (orderInfo == null || orderInfo.getAcceptBy() == null) return;
|
if (orderInfo == null || orderInfo.getAcceptBy() == null) return;
|
||||||
// amount from estimatedRevenue; fallback to finalAmount if null
|
// amount from estimatedRevenue; fallback to orderMoney if null
|
||||||
BigDecimal amount = orderInfo.getEstimatedRevenue() != null ? orderInfo.getEstimatedRevenue()
|
BigDecimal amount = orderInfo.getEstimatedRevenue() != null ? orderInfo.getEstimatedRevenue()
|
||||||
: (orderInfo.getFinalAmount() != null ? orderInfo.getFinalAmount() : BigDecimal.ZERO);
|
: (orderInfo.getOrderMoney() != null ? orderInfo.getOrderMoney() : BigDecimal.ZERO);
|
||||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) return;
|
if (amount.compareTo(BigDecimal.ZERO) < 0) return;
|
||||||
|
|
||||||
int freezeHours = freezePolicyService
|
int freezeHours = freezePolicyService
|
||||||
.resolveFreezeHours(orderInfo.getTenantId(), orderInfo.getAcceptBy());
|
.resolveFreezeHours(orderInfo.getTenantId(), orderInfo.getAcceptBy());
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,33 @@
|
|||||||
package com.starry.admin.modules.shop.service;
|
package com.starry.admin.modules.shop.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||||
|
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
||||||
|
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
import com.starry.admin.modules.shop.service.impl.PlayCouponInfoServiceImpl;
|
import com.starry.admin.modules.shop.service.impl.PlayCouponInfoServiceImpl;
|
||||||
import com.starry.admin.modules.weichat.controller.WxCouponController;
|
import com.starry.admin.modules.weichat.controller.WxCouponController;
|
||||||
|
import com.starry.admin.modules.weichat.entity.WxCouponOrderQueryVo;
|
||||||
|
import com.starry.admin.modules.weichat.entity.WxCouponOrderReturnVo;
|
||||||
import com.starry.admin.modules.weichat.entity.WxCouponReceiveReturnVo;
|
import com.starry.admin.modules.weichat.entity.WxCouponReceiveReturnVo;
|
||||||
import com.starry.common.result.R;
|
import com.starry.common.result.R;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -97,4 +108,131 @@ public class CouponWhitelistTest {
|
|||||||
assertEquals(1, list.size(), "非白名单券应被过滤不可见");
|
assertEquals(1, list.size(), "非白名单券应被过滤不可见");
|
||||||
assertEquals("c1", list.get(0).getId());
|
assertEquals("c1", list.get(0).getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("列表过滤-状态:下架或过期优惠券不可见")
|
||||||
|
void testQueryAllFiltersInvalidStatus() {
|
||||||
|
PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity();
|
||||||
|
current.setId("uid-2");
|
||||||
|
ThreadLocalRequestDetail.setRequestDetail(current);
|
||||||
|
|
||||||
|
PlayCouponInfoEntity offline = new PlayCouponInfoEntity();
|
||||||
|
offline.setId("offline");
|
||||||
|
offline.setCouponOnLineState(CouponOnlineState.OFFLINE.getCode());
|
||||||
|
|
||||||
|
PlayCouponInfoEntity expired = new PlayCouponInfoEntity();
|
||||||
|
expired.setId("expired");
|
||||||
|
expired.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
expired.setValidityPeriodType(CouponValidityPeriodType.FIXED_RANGE.getCode());
|
||||||
|
expired.setProductiveTime(LocalDateTime.now().minusDays(5));
|
||||||
|
expired.setExpirationTime(LocalDateTime.now().minusDays(1));
|
||||||
|
expired.setRemainingQuantity(5);
|
||||||
|
|
||||||
|
PlayCouponInfoEntity active = new PlayCouponInfoEntity();
|
||||||
|
active.setId("active");
|
||||||
|
active.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
active.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
active.setRemainingQuantity(10);
|
||||||
|
|
||||||
|
when(couponDetailsService.selectByCustomId("uid-2")).thenReturn(Collections.emptyList());
|
||||||
|
when(couponInfoService.queryAll()).thenReturn(Arrays.asList(offline, expired, active));
|
||||||
|
|
||||||
|
R resp = wxCouponController.queryAll();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<WxCouponReceiveReturnVo> list = (List<WxCouponReceiveReturnVo>) resp.getData();
|
||||||
|
|
||||||
|
assertNotNull(list);
|
||||||
|
assertEquals(1, list.size(), "仅应展示在线有效的优惠券");
|
||||||
|
assertEquals("active", list.get(0).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("列表过滤-库存:无限库存的优惠券可见")
|
||||||
|
void testQueryAllShowsUnlimitedInventory() {
|
||||||
|
PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity();
|
||||||
|
current.setId("uid-4");
|
||||||
|
ThreadLocalRequestDetail.setRequestDetail(current);
|
||||||
|
|
||||||
|
PlayCouponInfoEntity unlimited = new PlayCouponInfoEntity();
|
||||||
|
unlimited.setId("coupon-unlimited");
|
||||||
|
unlimited.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
unlimited.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
unlimited.setRemainingQuantity(-1);
|
||||||
|
unlimited.setCouponQuantity(-1);
|
||||||
|
|
||||||
|
when(couponDetailsService.selectByCustomId("uid-4")).thenReturn(Collections.emptyList());
|
||||||
|
when(couponInfoService.queryAll()).thenReturn(Collections.singletonList(unlimited));
|
||||||
|
|
||||||
|
R resp = wxCouponController.queryAll();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<WxCouponReceiveReturnVo> list = (List<WxCouponReceiveReturnVo>) resp.getData();
|
||||||
|
|
||||||
|
assertNotNull(list);
|
||||||
|
assertEquals(1, list.size(), "无限库存优惠券不应被过滤");
|
||||||
|
assertEquals("coupon-unlimited", list.get(0).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("下单查询:存在限制原因的优惠券被过滤")
|
||||||
|
void testQueryByOrderFiltersRestrictedCoupons() {
|
||||||
|
PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity();
|
||||||
|
current.setId("uid-3");
|
||||||
|
ThreadLocalRequestDetail.setRequestDetail(current);
|
||||||
|
|
||||||
|
PlayCouponDetailsReturnVo restricted = new PlayCouponDetailsReturnVo();
|
||||||
|
restricted.setId("detail-1");
|
||||||
|
restricted.setCouponId("coupon-restrict");
|
||||||
|
restricted.setUseState(CouponUseState.USED.getCode());
|
||||||
|
restricted.setObtainingTime(LocalDateTime.now().minusDays(3));
|
||||||
|
|
||||||
|
PlayCouponDetailsReturnVo usable = new PlayCouponDetailsReturnVo();
|
||||||
|
usable.setId("detail-2");
|
||||||
|
usable.setCouponId("coupon-usable");
|
||||||
|
usable.setUseState(CouponUseState.UNUSED.getCode());
|
||||||
|
usable.setObtainingTime(LocalDateTime.now().minusDays(1));
|
||||||
|
|
||||||
|
when(couponDetailsService.selectByCustomId("uid-3"))
|
||||||
|
.thenReturn(Arrays.asList(restricted, usable));
|
||||||
|
|
||||||
|
PlayCouponInfoEntity restrictedInfo = new PlayCouponInfoEntity();
|
||||||
|
restrictedInfo.setId("coupon-restrict");
|
||||||
|
restrictedInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
restrictedInfo.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
|
||||||
|
PlayCouponInfoEntity usableInfo = new PlayCouponInfoEntity();
|
||||||
|
usableInfo.setId("coupon-usable");
|
||||||
|
usableInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
usableInfo.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
|
||||||
|
when(couponInfoService.selectPlayCouponInfoById("coupon-restrict")).thenReturn(restrictedInfo);
|
||||||
|
when(couponInfoService.selectPlayCouponInfoById("coupon-usable")).thenReturn(usableInfo);
|
||||||
|
when(couponInfoService.getCouponDetailRestrictionReason(eq(restrictedInfo), eq(restricted.getUseState()), any(LocalDateTime.class)))
|
||||||
|
.thenReturn(Optional.of("优惠券已使用"));
|
||||||
|
when(couponInfoService.getCouponDetailRestrictionReason(eq(usableInfo), eq(usable.getUseState()), any(LocalDateTime.class)))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
when(couponInfoService.getCouponReasonForUnavailableUse(eq(usableInfo), anyString(), anyString(), anyInt(), any()))
|
||||||
|
.thenReturn("");
|
||||||
|
|
||||||
|
PlayCommodityInfoVo commodityInfo = new PlayCommodityInfoVo();
|
||||||
|
commodityInfo.setCommodityId("cmd-1");
|
||||||
|
commodityInfo.setCommodityPrice(BigDecimal.valueOf(50));
|
||||||
|
when(commodityInfoService.queryCommodityInfo("cmd-1", "level-1")).thenReturn(commodityInfo);
|
||||||
|
|
||||||
|
WxCouponOrderQueryVo vo = new WxCouponOrderQueryVo();
|
||||||
|
vo.setCommodityId("cmd-1");
|
||||||
|
vo.setLevelId("level-1");
|
||||||
|
vo.setClerkId("");
|
||||||
|
vo.setPlaceType("0");
|
||||||
|
vo.setCommodityQuantity(1);
|
||||||
|
|
||||||
|
R resp = wxCouponController.queryByOrder(vo);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<WxCouponOrderReturnVo> list = (List<WxCouponOrderReturnVo>) resp.getData();
|
||||||
|
|
||||||
|
assertNotNull(list);
|
||||||
|
assertEquals(1, list.size(), "受限优惠券应被过滤");
|
||||||
|
assertEquals("detail-2", list.get(0).getId());
|
||||||
|
assertEquals("1", list.get(0).getAvailable());
|
||||||
|
verify(couponInfoService).getCouponDetailRestrictionReason(eq(restrictedInfo), eq(restricted.getUseState()), any(LocalDateTime.class));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.starry.admin.modules.shop.service.impl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.mapper.PlayCouponInfoMapper;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PlayCouponInfoServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PlayCouponInfoMapper playCouponInfoMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IPlayCouponDetailsService playCouponDetailsService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private PlayCouponInfoServiceImpl service;
|
||||||
|
|
||||||
|
private PlayCouponInfoEntity couponInfo;
|
||||||
|
private PlayCustomUserInfoEntity customer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
couponInfo = new PlayCouponInfoEntity();
|
||||||
|
couponInfo.setId("coupon-1");
|
||||||
|
couponInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
couponInfo.setCouponQuantity(5);
|
||||||
|
couponInfo.setRemainingQuantity(5);
|
||||||
|
|
||||||
|
customer = new PlayCustomUserInfoEntity();
|
||||||
|
customer.setId("custom-1");
|
||||||
|
customer.setNickname("nick");
|
||||||
|
customer.setLevelId("level-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void claimCouponForCustom_consumesInventoryAndCreatesDetail() {
|
||||||
|
couponInfo.setClerkObtainedMaxQuantity(2);
|
||||||
|
when(playCouponInfoMapper.selectByIdForUpdate("coupon-1")).thenReturn(couponInfo);
|
||||||
|
when(playCouponDetailsService.countActiveByCustomAndCoupon("coupon-1", "custom-1"))
|
||||||
|
.thenReturn(0L);
|
||||||
|
when(playCouponInfoMapper.increaseIssuedAndConsumeRemaining("coupon-1")).thenReturn(1);
|
||||||
|
|
||||||
|
service.claimCouponForCustom("coupon-1", customer, CouponObtainChannel.SELF_SERVICE);
|
||||||
|
|
||||||
|
verify(playCouponInfoMapper).increaseIssuedAndConsumeRemaining("coupon-1");
|
||||||
|
verify(playCouponDetailsService)
|
||||||
|
.create(eq("custom-1"), eq(customer.getNickname()), eq(customer.getLevelId()),
|
||||||
|
eq("coupon-1"), eq(CouponObtainChannel.SELF_SERVICE.getCode()), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void claimCouponForCustom_throwsWhenLimitReached() {
|
||||||
|
couponInfo.setClerkObtainedMaxQuantity(1);
|
||||||
|
when(playCouponInfoMapper.selectByIdForUpdate("coupon-1")).thenReturn(couponInfo);
|
||||||
|
when(playCouponDetailsService.countActiveByCustomAndCoupon("coupon-1", "custom-1"))
|
||||||
|
.thenReturn(1L);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () ->
|
||||||
|
service.claimCouponForCustom("coupon-1", customer, CouponObtainChannel.SELF_SERVICE));
|
||||||
|
|
||||||
|
assertEquals("优惠券已达到领取上限", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void claimCouponForCustom_throwsWhenDepleted() {
|
||||||
|
couponInfo.setRemainingQuantity(0);
|
||||||
|
when(playCouponInfoMapper.selectByIdForUpdate("coupon-1")).thenReturn(couponInfo);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () ->
|
||||||
|
service.claimCouponForCustom("coupon-1", customer, CouponObtainChannel.SELF_SERVICE));
|
||||||
|
|
||||||
|
assertEquals("优惠券已领完", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void claimCouponForCustom_allowsUnlimitedInventory() {
|
||||||
|
couponInfo.setRemainingQuantity(-1);
|
||||||
|
couponInfo.setCouponQuantity(-1);
|
||||||
|
when(playCouponInfoMapper.selectByIdForUpdate("coupon-1")).thenReturn(couponInfo);
|
||||||
|
when(playCouponInfoMapper.increaseIssuedAndConsumeRemaining("coupon-1")).thenReturn(1);
|
||||||
|
|
||||||
|
service.claimCouponForCustom("coupon-1", customer, CouponObtainChannel.BACKEND_GRANT);
|
||||||
|
|
||||||
|
verify(playCouponInfoMapper).increaseIssuedAndConsumeRemaining("coupon-1");
|
||||||
|
verify(playCouponDetailsService)
|
||||||
|
.create(eq("custom-1"), eq(customer.getNickname()), eq(customer.getLevelId()),
|
||||||
|
eq("coupon-1"), eq(CouponObtainChannel.BACKEND_GRANT.getCode()), anyString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class EarningsBackfillServiceImplTest {
|
||||||
|
|
||||||
|
private final EarningsBackfillServiceImpl service = new EarningsBackfillServiceImpl();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private BigDecimal invokeResolveAmount(PlayOrderInfoEntity order, List<String> warnings) throws Exception {
|
||||||
|
Method method = EarningsBackfillServiceImpl.class.getDeclaredMethod("resolveAmount", PlayOrderInfoEntity.class,
|
||||||
|
List.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (BigDecimal) method.invoke(service, order, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveAmount_acceptsZeroAmountWithoutWarnings() throws Exception {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-zero");
|
||||||
|
order.setAcceptBy("clerk-001");
|
||||||
|
order.setOrderEndTime(LocalDateTime.now());
|
||||||
|
order.setOrderMoney(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
|
BigDecimal amount = invokeResolveAmount(order, warnings);
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.ZERO, amount);
|
||||||
|
assertTrue(warnings.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveAmount_rejectsNegativeAmount() throws Exception {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-negative");
|
||||||
|
order.setAcceptBy("clerk-001");
|
||||||
|
order.setOrderEndTime(LocalDateTime.now());
|
||||||
|
order.setEstimatedRevenue(BigDecimal.valueOf(-5));
|
||||||
|
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
|
BigDecimal amount = invokeResolveAmount(order, warnings);
|
||||||
|
|
||||||
|
assertNull(amount);
|
||||||
|
assertEquals(1, warnings.size());
|
||||||
|
assertTrue(warnings.get(0).contains("order-negative"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class EarningsServiceImplTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private EarningsServiceImpl earningsService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EarningsLineMapper baseMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IFreezePolicyService freezePolicyService;
|
||||||
|
|
||||||
|
private PlayOrderInfoEntity baselineOrder() {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-001");
|
||||||
|
order.setTenantId("tenant-001");
|
||||||
|
order.setAcceptBy("clerk-001");
|
||||||
|
order.setOrderEndTime(LocalDateTime.now());
|
||||||
|
order.setOrderSettlementState("0");
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromOrder_usesOrderAmountWhenEstimatedMissing_evenIfZero() {
|
||||||
|
PlayOrderInfoEntity order = baselineOrder();
|
||||||
|
order.setEstimatedRevenue(null);
|
||||||
|
order.setOrderMoney(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
when(freezePolicyService.resolveFreezeHours("tenant-001", "clerk-001")).thenReturn(0);
|
||||||
|
when(baseMapper.insert(any())).thenReturn(1);
|
||||||
|
|
||||||
|
earningsService.createFromOrder(order);
|
||||||
|
|
||||||
|
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||||
|
verify(baseMapper).insert(captor.capture());
|
||||||
|
assertEquals(BigDecimal.ZERO, captor.getValue().getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromOrder_skipsWhenAmountNegative() {
|
||||||
|
PlayOrderInfoEntity order = baselineOrder();
|
||||||
|
order.setEstimatedRevenue(BigDecimal.valueOf(-10));
|
||||||
|
order.setOrderMoney(BigDecimal.valueOf(20));
|
||||||
|
|
||||||
|
earningsService.createFromOrder(order);
|
||||||
|
|
||||||
|
verify(baseMapper, never()).insert(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user