feat: 完善订单生命周期幂等与日志追踪
This commit is contained in:
14
order-lifecycle-refactor.md
Normal file
14
order-lifecycle-refactor.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## Done
|
||||
- Refactored order creation to run exclusively through `OrderLifecycleServiceImpl.initiateOrder`, replacing the old `createOrderInfo` entry point across controllers and services.
|
||||
- Replaced nullable operator strings in completion workflow with the enum-based `OrderCompletionContext.Actor` (linked to `OrderConstant.OrderActor`) and updated all call sites/tests.
|
||||
- Introduced `OrderCreationContext` (renamed from `OrderCreationRequest`) with explicit `creatorActor`/`creatorId`, eliminating heuristic creator inference.
|
||||
- Added lifecycle logging: new entity/mapper for `play_order_log_info`, migration V12 to extend the table, and log writes for CREATE/COMPLETE/REFUND operations including operator metadata.
|
||||
- Integrated `orderLifecycleService` logging into reward/commodity/random order endpoints and gift service; tests adjusted to cover new API.
|
||||
- Placeholder utility methods (`resolveCompletionActor` etc.) plural.
|
||||
- Restored `handleOrderCompletion` with atomic consumption increments and ensured reward订单的首次完成也会落消费&日志,仅首次执行。
|
||||
- Order log `operation_type` 细分至 `CREATE_*`/`COMPLETE_*`/`REFUND_*`,可区分手动、系统、强制触发等场景。
|
||||
- V12 migration 已确认顺序;如需回滚,可直接删除新增字段并恢复 `oper_type` 列名(同一脚本可倒序执行)。
|
||||
- 针对并发与幂等关键路径补充了单测(完成/退款多次触发、自动补发通知等)。
|
||||
|
||||
## Todo
|
||||
- None for now。
|
||||
@@ -2,6 +2,10 @@ package com.starry.admin.modules.custom.mapper;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* 顾客Mapper接口
|
||||
@@ -11,4 +15,18 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
*/
|
||||
public interface PlayCustomUserInfoMapper extends MPJBaseMapper<PlayCustomUserInfoEntity> {
|
||||
|
||||
@Update({
|
||||
"<script>",
|
||||
"UPDATE play_custom_user_info",
|
||||
"SET accumulated_consumption_amount = COALESCE(accumulated_consumption_amount, 0) + #{consumptionDelta},",
|
||||
" last_purchase_time = #{completionTime},",
|
||||
" first_purchase_time = CASE WHEN first_purchase_time IS NULL THEN #{completionTime} ELSE first_purchase_time END",
|
||||
" <if test='weiChatCode != null and weiChatCode != \"\"'>, wei_chat_code = #{weiChatCode}</if>",
|
||||
"WHERE id = #{userId}",
|
||||
"</script>"
|
||||
})
|
||||
int applyOrderCompletionUpdate(@Param("userId") String userId,
|
||||
@Param("consumptionDelta") BigDecimal consumptionDelta,
|
||||
@Param("completionTime") Date completionTime,
|
||||
@Param("weiChatCode") String weiChatCode);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.starry.admin.modules.custom.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity;
|
||||
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -100,4 +101,13 @@ public interface IPlayCustomLevelInfoService extends IService<PlayCustomLevelInf
|
||||
* 删除最大等级
|
||||
*/
|
||||
void delMaxLevelByLevel(Integer level);
|
||||
|
||||
/**
|
||||
* 根据累计消费金额匹配顾客等级。
|
||||
*
|
||||
* @param tenantId 租户ID
|
||||
* @param totalConsumption 累计消费金额
|
||||
* @return 匹配到的等级,未匹配则返回 {@code null}
|
||||
*/
|
||||
PlayCustomLevelInfoEntity matchLevelByConsumption(String tenantId, BigDecimal totalConsumption);
|
||||
}
|
||||
|
||||
@@ -172,4 +172,11 @@ public interface IPlayCustomUserInfoService extends IService<PlayCustomUserInfoE
|
||||
* @author admin
|
||||
**/
|
||||
void saveOrderInfo(PlayOrderInfoEntity entity);
|
||||
|
||||
/**
|
||||
* 处理订单完成后的顾客统计更新。
|
||||
*
|
||||
* @param entity 完成的订单实体
|
||||
*/
|
||||
void handleOrderCompletion(PlayOrderInfoEntity entity);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomLevelInfoService;
|
||||
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -153,4 +155,39 @@ public class PlayCustomLevelInfoServiceImpl extends ServiceImpl<PlayCustomLevelI
|
||||
queryWrapper.eq(PlayCustomLevelInfoEntity::getLevel, level);
|
||||
this.baseMapper.delete(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayCustomLevelInfoEntity matchLevelByConsumption(String tenantId, BigDecimal totalConsumption) {
|
||||
BigDecimal consumption = Objects.requireNonNullElse(totalConsumption, BigDecimal.ZERO);
|
||||
LambdaQueryWrapper<PlayCustomLevelInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.orderByAsc(PlayCustomLevelInfoEntity::getLevel);
|
||||
if (StrUtil.isNotBlank(tenantId)) {
|
||||
lambdaQueryWrapper.eq(PlayCustomLevelInfoEntity::getTenantId, tenantId);
|
||||
}
|
||||
List<PlayCustomLevelInfoEntity> levels = this.baseMapper.selectList(lambdaQueryWrapper);
|
||||
if (levels == null || levels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
PlayCustomLevelInfoEntity matched = null;
|
||||
for (PlayCustomLevelInfoEntity level : levels) {
|
||||
BigDecimal threshold = parseConsumptionAmount(level.getConsumptionAmount());
|
||||
if (consumption.compareTo(threshold) >= 0) {
|
||||
matched = level;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matched != null ? matched : levels.get(0);
|
||||
}
|
||||
|
||||
private BigDecimal parseConsumptionAmount(String rawValue) {
|
||||
if (StrUtil.isBlank(rawValue)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(rawValue.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.starry.admin.modules.custom.module.vo.PlayCustomRankingQueryVo;
|
||||
import com.starry.admin.modules.custom.module.vo.PlayCustomRankingReturnVo;
|
||||
import com.starry.admin.modules.custom.module.vo.PlayCustomUserQueryVo;
|
||||
import com.starry.admin.modules.custom.module.vo.PlayCustomUserReturnVo;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomLevelInfoService;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl;
|
||||
@@ -23,6 +24,8 @@ import com.starry.admin.modules.personnel.service.IPlayBalanceDetailsInfoService
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Resource;
|
||||
@@ -49,6 +52,9 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
|
||||
@Resource
|
||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||
|
||||
@Resource
|
||||
private IPlayCustomLevelInfoService playCustomLevelInfoService;
|
||||
|
||||
@Override
|
||||
public PlayCustomUserInfoEntity selectByOpenid(String openId) {
|
||||
LambdaQueryWrapper<PlayCustomUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
@@ -369,6 +375,50 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
|
||||
return baseMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOrderCompletion(PlayOrderInfoEntity entity) {
|
||||
if (entity == null || StrUtil.isBlank(entity.getPurchaserBy())) {
|
||||
return;
|
||||
}
|
||||
PlayCustomUserInfoEntity userInfo = playCustomUserInfoMapper.selectById(entity.getPurchaserBy());
|
||||
if (userInfo == null) {
|
||||
log.warn("handleOrderCompletion skipped, userId={} missing, orderId={}", entity.getPurchaserBy(), entity.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal finalAmount = Objects.requireNonNullElse(entity.getFinalAmount(), BigDecimal.ZERO);
|
||||
Date completionTime = resolveCompletionTime(entity.getOrderEndTime());
|
||||
|
||||
int affected = playCustomUserInfoMapper.applyOrderCompletionUpdate(
|
||||
userInfo.getId(),
|
||||
finalAmount,
|
||||
completionTime,
|
||||
entity.getWeiChatCode());
|
||||
if (affected == 0) {
|
||||
log.warn("handleOrderCompletion update skipped for userId={}, orderId={}", userInfo.getId(), entity.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
PlayCustomUserInfoEntity latest = playCustomUserInfoMapper.selectById(userInfo.getId());
|
||||
if (latest == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayCustomLevelInfoEntity matchedLevel = playCustomLevelInfoService
|
||||
.matchLevelByConsumption(latest.getTenantId(), latest.getAccumulatedConsumptionAmount());
|
||||
if (matchedLevel != null && !StrUtil.equals(matchedLevel.getId(), latest.getLevelId())) {
|
||||
this.update(Wrappers.<PlayCustomUserInfoEntity>lambdaUpdate()
|
||||
.eq(PlayCustomUserInfoEntity::getId, latest.getId())
|
||||
.set(PlayCustomUserInfoEntity::getLevelId, matchedLevel.getId()));
|
||||
log.info("顾客{}消费累计达到{},自动调整等级为{}", latest.getId(), latest.getAccumulatedConsumptionAmount(), matchedLevel.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private Date resolveCompletionTime(LocalDateTime orderEndTime) {
|
||||
LocalDateTime time = orderEndTime != null ? orderEndTime : LocalDateTime.now();
|
||||
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOrderInfo(PlayOrderInfoEntity entity) {
|
||||
String id = entity.getPurchaserBy();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.starry.admin.modules.order.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderLogInfoEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PlayOrderLogInfoMapper extends BaseMapper<PlayOrderLogInfoEntity> {
|
||||
}
|
||||
@@ -207,6 +207,14 @@ public class OrderConstant {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public enum OrderActor {
|
||||
CUSTOMER,
|
||||
CLERK,
|
||||
ADMIN,
|
||||
SYSTEM;
|
||||
}
|
||||
|
||||
// 排除历史记录常量
|
||||
public static final String EXCLUDE_HISTORY_NO = "0";
|
||||
public static final String EXCLUDE_HISTORY_YES = "1";
|
||||
@@ -320,6 +328,93 @@ public class OrderConstant {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public enum YesNoFlag {
|
||||
NO("0"),
|
||||
YES("1");
|
||||
|
||||
private final String code;
|
||||
|
||||
YesNoFlag(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static YesNoFlag fromCode(String code) {
|
||||
for (YesNoFlag flag : values()) {
|
||||
if (flag.code.equals(code)) {
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown yes/no flag code: " + code);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public enum OrderSettlementState {
|
||||
NOT_SETTLED("0"),
|
||||
SETTLED("1");
|
||||
|
||||
private final String code;
|
||||
|
||||
OrderSettlementState(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static OrderSettlementState fromCode(String code) {
|
||||
for (OrderSettlementState state : values()) {
|
||||
if (state.code.equals(code)) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown settlement state code: " + code);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public enum OrdersExpiredState {
|
||||
NOT_EXPIRED("0"),
|
||||
EXPIRED("1");
|
||||
|
||||
private final String code;
|
||||
|
||||
OrdersExpiredState(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static OrdersExpiredState fromCode(String code) {
|
||||
for (OrdersExpiredState state : values()) {
|
||||
if (state.code.equals(code)) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown orders expired state code: " + code);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public enum PayMethod {
|
||||
BALANCE("0"),
|
||||
WECHAT("1"),
|
||||
ALIPAY("2"),
|
||||
BANK_CARD("3"),
|
||||
OTHER("4");
|
||||
|
||||
private final String code;
|
||||
|
||||
PayMethod(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static PayMethod fromCode(String code) {
|
||||
for (PayMethod method : values()) {
|
||||
if (method.code.equals(code)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown pay method code: " + code);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
public enum OrderTriggerSource {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.starry.admin.modules.order.module.dto;
|
||||
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
@@ -10,13 +11,11 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
@Data
|
||||
public class OrderCompletionContext {
|
||||
/**
|
||||
* 操作人类型(0:顾客;1:店员;2:管理员),可为空用于系统任务。
|
||||
*/
|
||||
@Nullable
|
||||
private String operatorType;
|
||||
|
||||
/** 操作人ID,可为空用于系统任务。 */
|
||||
/** 操作人类型,系统任务使用 SYSTEM。 */
|
||||
private OrderActor operatorActor = OrderActor.SYSTEM;
|
||||
|
||||
/** 操作人ID,SYSTEM 时允许为空。 */
|
||||
@Nullable
|
||||
private String operatorId;
|
||||
|
||||
@@ -30,26 +29,29 @@ public class OrderCompletionContext {
|
||||
/** 是否强制发送完成通知。 */
|
||||
private boolean forceNotify;
|
||||
|
||||
public static OrderCompletionContext of(@Nullable String operatorType, @Nullable String operatorId, OrderTriggerSource triggerSource) {
|
||||
public static OrderCompletionContext of(OrderActor actor, @Nullable String operatorId, OrderTriggerSource triggerSource) {
|
||||
Objects.requireNonNull(actor, "operator actor cannot be null");
|
||||
Objects.requireNonNull(triggerSource, "triggerSource cannot be null");
|
||||
if (actor != OrderActor.SYSTEM && operatorId == null) {
|
||||
throw new IllegalArgumentException("operatorId is required for actor " + actor);
|
||||
}
|
||||
OrderCompletionContext context = new OrderCompletionContext();
|
||||
context.setOperatorType(operatorType);
|
||||
context.setOperatorActor(actor);
|
||||
context.setOperatorId(operatorId);
|
||||
context.setTriggerSource(triggerSource);
|
||||
return context;
|
||||
}
|
||||
|
||||
public static OrderCompletionContext of(@Nullable String operatorType, @Nullable String operatorId, OrderTriggerSource triggerSource, @Nullable String comment) {
|
||||
OrderCompletionContext context = of(operatorType, operatorId, triggerSource);
|
||||
return context.withComment(comment);
|
||||
public static OrderCompletionContext of(OrderActor actor, @Nullable String operatorId, OrderTriggerSource triggerSource, @Nullable String comment) {
|
||||
return of(actor, operatorId, triggerSource).withComment(comment);
|
||||
}
|
||||
|
||||
public static OrderCompletionContext scheduler(@Nullable String comment) {
|
||||
return of(null, null, OrderTriggerSource.SCHEDULER, comment);
|
||||
return of(OrderActor.SYSTEM, null, OrderTriggerSource.SCHEDULER, comment);
|
||||
}
|
||||
|
||||
public static OrderCompletionContext system(OrderTriggerSource triggerSource, @Nullable String comment) {
|
||||
return of(null, null, triggerSource, comment);
|
||||
return of(OrderActor.SYSTEM, null, triggerSource, comment);
|
||||
}
|
||||
|
||||
public OrderCompletionContext withForceNotify(boolean forceNotify) {
|
||||
|
||||
@@ -1,127 +1,77 @@
|
||||
package com.starry.admin.modules.order.module.dto;
|
||||
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 订单创建请求对象 - 使用Builder模式替换20+参数的方法
|
||||
*
|
||||
* @author admin
|
||||
* 订单创建上下文,用于聚合下单所需的全部信息。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class OrderCreationRequest {
|
||||
public class OrderCreationContext {
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
@NotBlank(message = "订单ID不能为空")
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 订单编号
|
||||
*/
|
||||
@NotBlank(message = "订单编号不能为空")
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 订单状态
|
||||
*/
|
||||
@NotNull(message = "订单状态不能为空")
|
||||
private OrderConstant.OrderStatus orderStatus;
|
||||
|
||||
/**
|
||||
* 订单类型
|
||||
*/
|
||||
@NotNull(message = "订单类型不能为空")
|
||||
private OrderConstant.OrderType orderType;
|
||||
|
||||
/**
|
||||
* 下单类型
|
||||
*/
|
||||
@NotNull(message = "下单类型不能为空")
|
||||
private OrderConstant.PlaceType placeType;
|
||||
|
||||
/**
|
||||
* 打赏类型(0:余额;1:礼物)
|
||||
*/
|
||||
private RewardType rewardType;
|
||||
private OrderConstant.RewardType rewardType;
|
||||
|
||||
/**
|
||||
* 是否是首单
|
||||
*/
|
||||
private boolean isFirstOrder;
|
||||
|
||||
/**
|
||||
* 商品信息
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "商品信息不能为空")
|
||||
private CommodityInfo commodityInfo;
|
||||
|
||||
/**
|
||||
* 支付信息
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "支付信息不能为空")
|
||||
private PaymentInfo paymentInfo;
|
||||
|
||||
/**
|
||||
* 下单人
|
||||
*/
|
||||
@NotBlank(message = "下单人不能为空")
|
||||
private String purchaserBy;
|
||||
|
||||
/**
|
||||
* 接单人(可选)
|
||||
*/
|
||||
private String acceptBy;
|
||||
|
||||
/**
|
||||
* 微信号码
|
||||
*/
|
||||
private String weiChatCode;
|
||||
|
||||
/**
|
||||
* 订单备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 随机单要求(仅随机单时需要)
|
||||
*/
|
||||
private RandomOrderRequirements randomOrderRequirements;
|
||||
|
||||
/**
|
||||
* 获取首单标识字符串(兼容现有系统)
|
||||
*/
|
||||
@Builder.Default
|
||||
private OrderActor creatorActor = OrderActor.SYSTEM;
|
||||
|
||||
@Nullable
|
||||
private String creatorId;
|
||||
|
||||
public String getFirstOrderString() {
|
||||
return isFirstOrder ? "1" : "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证随机单要求
|
||||
*/
|
||||
public boolean isValidForRandomOrder() {
|
||||
return placeType == OrderConstant.PlaceType.RANDOM
|
||||
&& randomOrderRequirements != null;
|
||||
return placeType == OrderConstant.PlaceType.RANDOM && randomOrderRequirements != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为打赏单
|
||||
*/
|
||||
public boolean isRewardOrder() {
|
||||
return placeType == OrderConstant.PlaceType.REWARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为指定单
|
||||
*/
|
||||
public boolean isSpecifiedOrder() {
|
||||
return placeType == OrderConstant.PlaceType.SPECIFIED;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.starry.admin.common.conf.StringTypeHandler;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
|
||||
import com.starry.admin.modules.order.service.impl.OrderLifecycleServiceImpl;
|
||||
import com.starry.common.domain.BaseEntity;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -329,4 +331,31 @@ public class PlayOrderInfoEntity extends BaseEntity<PlayOrderInfoEntity> {
|
||||
public void setOrderEndTime(LocalDateTime orderEndTime) {
|
||||
this.orderEndTime = orderEndTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单生命周期状态,仅允许订单生命周期服务进行访问。
|
||||
*
|
||||
* @param token 授权令牌
|
||||
* @param orderStatus 新的订单状态
|
||||
* @param orderEndTime 可选的订单结束时间
|
||||
*/
|
||||
public void updateOrderStatus(OrderLifecycleServiceImpl.LifecycleToken token, OrderStatus orderStatus) {
|
||||
if (token == null) {
|
||||
throw new IllegalStateException("Lifecycle token is required");
|
||||
}
|
||||
if (orderStatus == null) {
|
||||
throw new IllegalArgumentException("orderStatus cannot be null");
|
||||
}
|
||||
this.orderStatus = orderStatus.getCode();
|
||||
}
|
||||
|
||||
public void updateOrderEndTime(OrderLifecycleServiceImpl.LifecycleToken token, LocalDateTime orderEndTime) {
|
||||
if (token == null) {
|
||||
throw new IllegalStateException("Lifecycle token is required");
|
||||
}
|
||||
if (orderEndTime == null) {
|
||||
throw new IllegalArgumentException("orderEndTime cannot be null");
|
||||
}
|
||||
this.orderEndTime = orderEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.starry.admin.modules.order.module.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.starry.common.domain.BaseEntity;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* 订单生命周期日志实体。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("play_order_log_info")
|
||||
public class PlayOrderLogInfoEntity extends BaseEntity<PlayOrderLogInfoEntity> {
|
||||
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
private String tenantId;
|
||||
|
||||
private String orderId;
|
||||
|
||||
private String operationType;
|
||||
|
||||
private String operatorType;
|
||||
|
||||
private String operatorId;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime operTime;
|
||||
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.starry.admin.modules.order.service;
|
||||
|
||||
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
|
||||
public interface IOrderLifecycleService {
|
||||
|
||||
PlayOrderInfoEntity initiateOrder(OrderCreationContext context);
|
||||
|
||||
void completeOrder(String orderId, OrderCompletionContext context);
|
||||
|
||||
void refundOrder(OrderRefundContext context);
|
||||
|
||||
@@ -45,46 +45,6 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
|
||||
* @author admin
|
||||
* @since 2024/6/3 10:53
|
||||
**/
|
||||
void createOrderInfo(OrderCreationRequest request);
|
||||
|
||||
/**
|
||||
* 新增订单信息 - 旧版本方法(已废弃,建议使用OrderCreationRequest)
|
||||
*
|
||||
* @param orderId 订单ID
|
||||
* @param orderNo 订单编号
|
||||
* @param orderState 订单状态【0:已下单(待接单);1:已接单(待开始);2:已开始(服务中);3;已完成:4:已取消】
|
||||
* @param orderType 订单类型【-1:退款订单;0:充值订单;1:提现订单;2:普通订单】
|
||||
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
|
||||
* @param rewardType (0:余额;1:礼物)
|
||||
* @param firstOrder 是否是首单【0:不是,1:是】
|
||||
* @param commodityId 商品ID
|
||||
* @param commodityType 商品类型[0:礼物,1:服务]
|
||||
* @param commodityPrice 商品属性-商品单价
|
||||
* @param serviceDuration 商品属性-服务时长
|
||||
* @param commodityName 商品名称
|
||||
* @param commodityNumber 商品数量
|
||||
* @param orderMoney 订单金额
|
||||
* @param finalAmount 订单最终金额(支付金额)
|
||||
* @param discountAmount 优惠金额
|
||||
* @param purchaserBy 下单人
|
||||
* @param acceptBy 接单人
|
||||
* @param weiChatCode 订单微信号码
|
||||
* @param couponIds 优惠券ID列表
|
||||
* @param remark 订单备注
|
||||
* @param clerkSex 随机单要求-店员性别(0:未知;1:男;2:女)
|
||||
* @param clerkLevelId 随机单要求-店员等级ID
|
||||
* @param excludeHistory 随机单要求-是否排除下单过的成员(0:不排除;1:排除)
|
||||
* @author admin
|
||||
* @since 2024/6/3 10:53
|
||||
* @deprecated 请使用 {@link #createOrderInfo(OrderCreationRequest)} 替代
|
||||
**/
|
||||
@Deprecated
|
||||
void createOrderInfo(String orderId, String orderNo, String orderState, String orderType, String placeType,
|
||||
String rewardType, String firstOrder, String commodityId, String commodityType, BigDecimal commodityPrice,
|
||||
String serviceDuration, String commodityName, String commodityNumber, BigDecimal orderMoney,
|
||||
BigDecimal finalAmount, BigDecimal discountAmount, String purchaserBy, String acceptBy, String weiChatCode,
|
||||
List<String> couponIds, String remark, String clerkSex, String clerkLevelId, String excludeHistory);
|
||||
|
||||
/**
|
||||
* 据店员等级和订单金额,获取店员预计收入
|
||||
*
|
||||
|
||||
@@ -1,29 +1,51 @@
|
||||
package com.starry.admin.modules.order.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
||||
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
||||
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.OrderRefundFlag;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderSettlementState;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrdersExpiredState;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.PayMethod;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.PlaceType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.ReviewRequirement;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.YesNoFlag;
|
||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderLogInfoEntity;
|
||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
||||
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -33,6 +55,14 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
|
||||
private static final LifecycleToken LIFECYCLE_TOKEN = new LifecycleToken();
|
||||
|
||||
private enum LifecycleOperation {
|
||||
CREATE,
|
||||
COMPLETE,
|
||||
REFUND
|
||||
}
|
||||
|
||||
@Resource
|
||||
private PlayOrderInfoMapper orderInfoMapper;
|
||||
|
||||
@@ -48,6 +78,49 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
@Resource
|
||||
private IPlayCustomUserInfoService customUserInfoService;
|
||||
|
||||
@Resource
|
||||
private IPlayCouponDetailsService playCouponDetailsService;
|
||||
|
||||
@Resource
|
||||
private ClerkRevenueCalculator clerkRevenueCalculator;
|
||||
|
||||
@Resource
|
||||
private PlayOrderLogInfoMapper orderLogInfoMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PlayOrderInfoEntity initiateOrder(OrderCreationContext context) {
|
||||
validateOrderCreationRequest(context);
|
||||
|
||||
PlayOrderInfoEntity entity = buildOrderEntity(context);
|
||||
applyRandomOrderRequirements(entity, context.getRandomOrderRequirements());
|
||||
applyAcceptByInfo(entity, context);
|
||||
if (context.isRewardOrder()) {
|
||||
applyRewardOrderDefaults(entity);
|
||||
}
|
||||
|
||||
customUserInfoService.saveOrderInfo(entity);
|
||||
orderInfoMapper.insert(entity);
|
||||
String creationOperationType = resolveCreationOperationType(context.getCreatorActor());
|
||||
recordOrderLog(
|
||||
entity,
|
||||
context.getCreatorActor(),
|
||||
context.getCreatorId(),
|
||||
LifecycleOperation.CREATE,
|
||||
context.getRemark(),
|
||||
creationOperationType);
|
||||
updateCouponUsage(context.getPaymentInfo().getCouponIds());
|
||||
|
||||
if (context.isRewardOrder() && StrUtil.isNotBlank(context.getAcceptBy())) {
|
||||
completeOrder(
|
||||
entity.getId(),
|
||||
OrderCompletionContext.of(OrderActor.SYSTEM, null, OrderTriggerSource.REWARD_ORDER)
|
||||
.withForceNotify(true)
|
||||
.withComment("auto-complete reward order"));
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void completeOrder(String orderId, OrderCompletionContext context) {
|
||||
@@ -71,17 +144,34 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime endTime = order.getOrderEndTime() != null ? order.getOrderEndTime() : now;
|
||||
|
||||
boolean statusUpdated = false;
|
||||
boolean transitioned = false;
|
||||
if (!alreadyCompleted) {
|
||||
PlayOrderInfoEntity update = new PlayOrderInfoEntity(orderId, OrderStatus.COMPLETED.getCode());
|
||||
update.setOrderEndTime(endTime);
|
||||
orderInfoMapper.updateById(update);
|
||||
statusUpdated = true;
|
||||
} else if (order.getOrderEndTime() == null) {
|
||||
PlayOrderInfoEntity update = new PlayOrderInfoEntity(orderId, OrderStatus.COMPLETED.getCode());
|
||||
update.setOrderEndTime(endTime);
|
||||
orderInfoMapper.updateById(update);
|
||||
statusUpdated = true;
|
||||
UpdateWrapper<PlayOrderInfoEntity> transitionWrapper = new UpdateWrapper<>();
|
||||
transitionWrapper.eq("id", orderId)
|
||||
.eq("order_status", OrderStatus.IN_PROGRESS.getCode())
|
||||
.set("order_status", OrderStatus.COMPLETED.getCode())
|
||||
.set("order_end_time", endTime);
|
||||
transitioned = orderInfoMapper.update(null, transitionWrapper) > 0;
|
||||
if (!transitioned) {
|
||||
PlayOrderInfoEntity refreshed = orderInfoMapper.selectById(orderId);
|
||||
if (refreshed == null) {
|
||||
throw new CustomException("订单不存在");
|
||||
}
|
||||
if (!OrderStatus.COMPLETED.getCode().equals(refreshed.getOrderStatus())) {
|
||||
log.warn("Failed to transition order {} to completed, current status {}", orderId, refreshed.getOrderStatus());
|
||||
throw new CustomException("订单状态异常,无法完成");
|
||||
}
|
||||
order = refreshed;
|
||||
alreadyCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyCompleted && order.getOrderEndTime() == null) {
|
||||
UpdateWrapper<PlayOrderInfoEntity> endTimeWrapper = new UpdateWrapper<>();
|
||||
endTimeWrapper.eq("id", orderId)
|
||||
.isNull("order_end_time")
|
||||
.set("order_end_time", endTime);
|
||||
orderInfoMapper.update(null, endTimeWrapper);
|
||||
}
|
||||
|
||||
PlayOrderInfoEntity latest = orderInfoMapper.selectById(orderId);
|
||||
@@ -89,9 +179,34 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
throw new CustomException("订单不存在");
|
||||
}
|
||||
|
||||
boolean earningsCreated = ensureEarnings(latest, source);
|
||||
boolean forceNotify = context != null && context.isForceNotify();
|
||||
boolean shouldNotify = statusUpdated || (forceNotify && earningsCreated);
|
||||
boolean earningsCreated = false;
|
||||
boolean shouldNotify = false;
|
||||
|
||||
boolean completionLogged = hasLifecycleLog(orderId, LifecycleOperation.COMPLETE);
|
||||
boolean shouldApplyCompletion = transitioned;
|
||||
if (!shouldApplyCompletion && forceNotify) {
|
||||
shouldApplyCompletion = !completionLogged;
|
||||
}
|
||||
|
||||
if (shouldApplyCompletion) {
|
||||
customUserInfoService.handleOrderCompletion(latest);
|
||||
earningsCreated = ensureEarnings(latest, source);
|
||||
shouldNotify = true;
|
||||
OrderActor actor = context != null ? context.getOperatorActor() : OrderActor.SYSTEM;
|
||||
String operationType = resolveCompletionOperationType(context, transitioned);
|
||||
recordOrderLog(
|
||||
latest,
|
||||
actor,
|
||||
context != null ? context.getOperatorId() : null,
|
||||
LifecycleOperation.COMPLETE,
|
||||
context != null ? context.getComment() : null,
|
||||
operationType);
|
||||
} else if (forceNotify) {
|
||||
earningsCreated = ensureEarnings(latest, source);
|
||||
shouldNotify = earningsCreated || OrderStatus.COMPLETED.getCode().equals(latest.getOrderStatus());
|
||||
}
|
||||
|
||||
if (shouldNotify) {
|
||||
wxCustomMpService.sendOrderFinishMessageAsync(latest);
|
||||
}
|
||||
@@ -128,12 +243,22 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
throw new CustomException("每个订单只能退款一次~");
|
||||
}
|
||||
|
||||
PlayOrderInfoEntity update = new PlayOrderInfoEntity();
|
||||
update.setId(order.getId());
|
||||
update.setRefundType(OrderRefundFlag.REFUNDED.getCode());
|
||||
update.setOrderStatus(OrderStatus.CANCELLED.getCode());
|
||||
update.setRefundAmount(refundAmount);
|
||||
orderInfoMapper.updateById(update);
|
||||
UpdateWrapper<PlayOrderInfoEntity> refundUpdate = new UpdateWrapper<>();
|
||||
refundUpdate.eq("id", order.getId())
|
||||
.eq("refund_type", OrderRefundFlag.NOT_REFUNDED.getCode())
|
||||
.set("refund_type", OrderRefundFlag.REFUNDED.getCode())
|
||||
.set("refund_amount", refundAmount)
|
||||
.set("refund_reason", context.getRefundReason())
|
||||
.set("order_status", OrderStatus.CANCELLED.getCode());
|
||||
boolean refundApplied = orderInfoMapper.update(null, refundUpdate) > 0;
|
||||
if (!refundApplied) {
|
||||
PlayOrderInfoEntity latest = orderInfoMapper.selectById(order.getId());
|
||||
if (latest != null && OrderRefundFlag.REFUNDED.getCode().equals(latest.getRefundType())) {
|
||||
log.info("Refund already processed for order {}, skipping duplicate credit", order.getId());
|
||||
return;
|
||||
}
|
||||
throw new CustomException("订单已处理或状态异常,无法退款");
|
||||
}
|
||||
|
||||
PlayCustomUserInfoEntity customUser = customUserInfoService.getById(order.getPurchaserBy());
|
||||
if (customUser == null) {
|
||||
@@ -171,6 +296,132 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
refundById,
|
||||
OrderRefundState.PROCESSING.getCode(),
|
||||
ReviewRequirement.NOT_REQUIRED.getCode());
|
||||
|
||||
PlayOrderInfoEntity latest = orderInfoMapper.selectById(order.getId());
|
||||
|
||||
String refundOperationType = String.format("%s_%s", LifecycleOperation.REFUND.name(), refundRecordType.name());
|
||||
recordOrderLog(
|
||||
latest != null ? latest : order,
|
||||
resolveCompletionActor(context.getOperatorType()),
|
||||
context.getOperatorId(),
|
||||
LifecycleOperation.REFUND,
|
||||
context.getRefundReason(),
|
||||
refundOperationType);
|
||||
}
|
||||
|
||||
private void validateOrderCreationRequest(OrderCreationContext context) {
|
||||
if (context == null) {
|
||||
throw new CustomException("订单创建请求不能为空");
|
||||
}
|
||||
if (context.getPlaceType() == PlaceType.RANDOM && !context.isValidForRandomOrder()) {
|
||||
throw new CustomException("随机单必须提供店员要求信息");
|
||||
}
|
||||
}
|
||||
|
||||
private PlayOrderInfoEntity buildOrderEntity(OrderCreationContext context) {
|
||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||
entity.setId(context.getOrderId());
|
||||
entity.setOrderNo(context.getOrderNo());
|
||||
entity.updateOrderStatus(LIFECYCLE_TOKEN, context.getOrderStatus());
|
||||
entity.setOrderType(context.getOrderType().getCode());
|
||||
entity.setPlaceType(context.getPlaceType().getCode());
|
||||
entity.setRewardType(context.getRewardType().getCode());
|
||||
entity.setFirstOrder(resolveFirstOrderFlag(context));
|
||||
entity.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
entity.setBackendEntry(YesNoFlag.NO.getCode());
|
||||
entity.setOrderSettlementState(OrderSettlementState.NOT_SETTLED.getCode());
|
||||
entity.setOrdersExpiredState(OrdersExpiredState.NOT_EXPIRED.getCode());
|
||||
|
||||
CommodityInfo commodityInfo = context.getCommodityInfo();
|
||||
entity.setCommodityId(commodityInfo.getCommodityId());
|
||||
entity.setCommodityType(commodityInfo.getCommodityType().getCode());
|
||||
entity.setCommodityPrice(commodityInfo.getCommodityPrice());
|
||||
entity.setServiceDuration(commodityInfo.getServiceDuration());
|
||||
entity.setCommodityName(commodityInfo.getCommodityName());
|
||||
entity.setCommodityNumber(commodityInfo.getCommodityNumber());
|
||||
|
||||
PaymentInfo paymentInfo = context.getPaymentInfo();
|
||||
entity.setOrderMoney(paymentInfo.getOrderMoney());
|
||||
entity.setFinalAmount(paymentInfo.getFinalAmount());
|
||||
entity.setDiscountAmount(paymentInfo.getDiscountAmount());
|
||||
entity.setCouponIds(paymentInfo.getCouponIds());
|
||||
entity.setUseCoupon(CollectionUtil.isNotEmpty(paymentInfo.getCouponIds())
|
||||
? YesNoFlag.YES.getCode()
|
||||
: YesNoFlag.NO.getCode());
|
||||
entity.setPayMethod(resolvePayMethod(paymentInfo.getPayMethod()));
|
||||
|
||||
entity.setPurchaserBy(context.getPurchaserBy());
|
||||
entity.setPurchaserTime(LocalDateTime.now());
|
||||
entity.setWeiChatCode(context.getWeiChatCode());
|
||||
entity.setRemark(context.getRemark());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void applyRandomOrderRequirements(PlayOrderInfoEntity entity, RandomOrderRequirements requirements) {
|
||||
if (requirements == null) {
|
||||
return;
|
||||
}
|
||||
entity.setSex(requirements.getClerkGender().getCode());
|
||||
entity.setLevelId(requirements.getClerkLevelId());
|
||||
entity.setExcludeHistory(requirements.getExcludeHistory());
|
||||
}
|
||||
|
||||
private void applyAcceptByInfo(PlayOrderInfoEntity entity, OrderCreationContext context) {
|
||||
if (StrUtil.isBlank(context.getAcceptBy())) {
|
||||
return;
|
||||
}
|
||||
entity.setAcceptBy(context.getAcceptBy());
|
||||
ClerkEstimatedRevenueVo estimatedRevenue = clerkRevenueCalculator.calculateEstimatedRevenue(
|
||||
context.getAcceptBy(),
|
||||
context.getPaymentInfo().getCouponIds(),
|
||||
context.getPlaceType().getCode(),
|
||||
entity.getFirstOrder(),
|
||||
context.getPaymentInfo().getFinalAmount());
|
||||
entity.setEstimatedRevenue(estimatedRevenue.getRevenueAmount());
|
||||
entity.setEstimatedRevenueRatio(estimatedRevenue.getRevenueRatio());
|
||||
}
|
||||
|
||||
private void applyRewardOrderDefaults(PlayOrderInfoEntity entity) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setAcceptTime(now);
|
||||
entity.setOrderStartTime(now);
|
||||
entity.setOrderEndTime(now);
|
||||
}
|
||||
|
||||
private void updateCouponUsage(List<String> couponIds) {
|
||||
if (CollectionUtil.isEmpty(couponIds)) {
|
||||
return;
|
||||
}
|
||||
playCouponDetailsService.updateCouponUseStateByIds(couponIds, CouponUseState.USED.getCode());
|
||||
}
|
||||
|
||||
private String resolvePayMethod(String payMethodCode) {
|
||||
if (StrUtil.isBlank(payMethodCode)) {
|
||||
return PayMethod.BALANCE.getCode();
|
||||
}
|
||||
try {
|
||||
return PayMethod.fromCode(payMethodCode).getCode();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("Unknown pay method code {}, fallback to BALANCE", payMethodCode);
|
||||
return PayMethod.BALANCE.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveFirstOrderFlag(OrderCreationContext context) {
|
||||
if (StrUtil.isBlank(context.getAcceptBy()) || StrUtil.isBlank(context.getPurchaserBy())) {
|
||||
return context.getFirstOrderString();
|
||||
}
|
||||
return isFirstOrder(context.getPurchaserBy(), context.getAcceptBy())
|
||||
? YesNoFlag.YES.getCode()
|
||||
: YesNoFlag.NO.getCode();
|
||||
}
|
||||
|
||||
private boolean isFirstOrder(String customerId, String clerkId) {
|
||||
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
|
||||
.eq(PlayOrderInfoEntity::getPurchaserBy, customerId)
|
||||
.eq(PlayOrderInfoEntity::getAcceptBy, clerkId)
|
||||
.eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
|
||||
return orderInfoMapper.selectCount(wrapper) == 0;
|
||||
}
|
||||
|
||||
private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) {
|
||||
@@ -192,4 +443,77 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasLifecycleLog(String orderId, LifecycleOperation operation) {
|
||||
if (StrUtil.isBlank(orderId)) {
|
||||
return false;
|
||||
}
|
||||
return orderLogInfoMapper.selectCount(Wrappers.<PlayOrderLogInfoEntity>lambdaQuery()
|
||||
.eq(PlayOrderLogInfoEntity::getOrderId, orderId)
|
||||
.likeRight(PlayOrderLogInfoEntity::getOperationType, operation.name())) > 0;
|
||||
}
|
||||
|
||||
private String resolveCreationOperationType(OrderActor creatorActor) {
|
||||
String actorSegment = creatorActor != null ? creatorActor.name() : OrderActor.SYSTEM.name();
|
||||
return String.format("%s_%s", LifecycleOperation.CREATE.name(), actorSegment);
|
||||
}
|
||||
|
||||
private String resolveCompletionOperationType(OrderCompletionContext context, boolean transitioned) {
|
||||
OrderActor operatorActor = context != null ? context.getOperatorActor() : OrderActor.SYSTEM;
|
||||
String actorSegment = operatorActor != null ? operatorActor.name() : OrderActor.SYSTEM.name();
|
||||
if (transitioned) {
|
||||
return String.format("%s_%s", LifecycleOperation.COMPLETE.name(), actorSegment);
|
||||
}
|
||||
if (context != null && context.isForceNotify()) {
|
||||
return String.format("%s_FORCE_%s", LifecycleOperation.COMPLETE.name(), actorSegment);
|
||||
}
|
||||
return LifecycleOperation.COMPLETE.name();
|
||||
}
|
||||
|
||||
public static final class LifecycleToken {
|
||||
private LifecycleToken() {}
|
||||
}
|
||||
|
||||
private void recordOrderLog(PlayOrderInfoEntity order,
|
||||
OrderActor actor,
|
||||
String operatorId,
|
||||
LifecycleOperation operation,
|
||||
String remark,
|
||||
String operationType) {
|
||||
if (order == null || StrUtil.isBlank(order.getId())) {
|
||||
return;
|
||||
}
|
||||
PlayOrderLogInfoEntity log = new PlayOrderLogInfoEntity();
|
||||
log.setId(IdUtils.getUuid());
|
||||
log.setTenantId(order.getTenantId());
|
||||
log.setOrderId(order.getId());
|
||||
log.setOperationType(StrUtil.isNotBlank(operationType) ? operationType : operation.name());
|
||||
log.setOperatorType(actor != null ? actor.name() : null);
|
||||
log.setOperatorId(operatorId);
|
||||
log.setOperTime(LocalDateTime.now());
|
||||
log.setRemark(remark);
|
||||
orderLogInfoMapper.insert(log);
|
||||
}
|
||||
|
||||
private OrderActor resolveCompletionActor(String operatorType) {
|
||||
if (StrUtil.isBlank(operatorType)) {
|
||||
return OrderActor.SYSTEM;
|
||||
}
|
||||
try {
|
||||
OperatorType type = OperatorType.fromCode(operatorType);
|
||||
switch (type) {
|
||||
case CUSTOMER:
|
||||
return OrderActor.CUSTOMER;
|
||||
case CLERK:
|
||||
return OrderActor.CLERK;
|
||||
case ADMIN:
|
||||
return OrderActor.ADMIN;
|
||||
default:
|
||||
return OrderActor.SYSTEM;
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("Unknown operator type {}, fallback to SYSTEM", operatorType, ex);
|
||||
return OrderActor.SYSTEM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.starry.admin.modules.order.module.vo.PlayOrderEvaluateReturnVo;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
import com.starry.common.enums.EvaluateHiddenState;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import com.starry.common.utils.StringUtils;
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
|
||||
@@ -37,6 +38,7 @@ import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
||||
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
@@ -102,6 +104,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
@Resource
|
||||
private IOrderLifecycleService orderLifecycleService;
|
||||
|
||||
@Resource
|
||||
private ClerkRevenueCalculator clerkRevenueCalculator;
|
||||
|
||||
@Override
|
||||
public List<PlayOrderInfoEntity> getTotalOrderInfo(String tenantId) {
|
||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
||||
@@ -109,306 +114,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
return this.baseMapper.selectList(lambdaWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void createOrderInfo(String orderId, String orderNo, String orderState, String orderType, String placeType,
|
||||
String rewardType, String firstOrder, String commodityId, String commodityType, BigDecimal commodityPrice,
|
||||
String serviceDuration, String commodityName, String commodityNumber, BigDecimal orderMoney,
|
||||
BigDecimal finalAmount, BigDecimal discountAmount, String purchaserBy, String acceptBy, String weiChatCode,
|
||||
List<String> couponIds, String remark, String clerkSex, String clerkLevelId, String excludeHistory) {
|
||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||
entity.setId(orderId);
|
||||
entity.setOrderNo(orderNo);
|
||||
entity.setOrderStatus(orderState);
|
||||
entity.setOrderType(orderType);
|
||||
entity.setPlaceType(placeType);
|
||||
entity.setRewardType(rewardType);
|
||||
entity.setFirstOrder(firstOrder);
|
||||
entity.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
entity.setCommodityId(commodityId);
|
||||
entity.setCommodityType(commodityType);
|
||||
entity.setCommodityPrice(commodityPrice);
|
||||
entity.setServiceDuration(serviceDuration);
|
||||
entity.setCommodityName(commodityName);
|
||||
entity.setCommodityNumber(commodityNumber);
|
||||
entity.setBackendEntry("0");
|
||||
entity.setPayMethod("0");
|
||||
entity.setOrderMoney(orderMoney);
|
||||
entity.setFinalAmount(finalAmount);
|
||||
entity.setDiscountAmount(discountAmount);
|
||||
entity.setWeiChatCode(weiChatCode);
|
||||
entity.setRemark(remark);
|
||||
entity.setOrderSettlementState("0");
|
||||
entity.setOrdersExpiredState("0");
|
||||
entity.setPurchaserBy(purchaserBy);
|
||||
entity.setPurchaserTime(LocalDateTime.now());
|
||||
entity.setCouponIds(couponIds);
|
||||
entity.setUseCoupon(couponIds != null && !couponIds.isEmpty() ? "1" : "0");
|
||||
if ("1".equals(placeType)) {
|
||||
entity.setSex(clerkSex);
|
||||
entity.setLevelId(clerkLevelId);
|
||||
entity.setExcludeHistory(excludeHistory);
|
||||
}
|
||||
if (StrUtil.isNotBlank(acceptBy)) {
|
||||
entity.setAcceptBy(acceptBy);
|
||||
ClerkEstimatedRevenueVo estimatedRevenueVo = getClerkEstimatedRevenue(acceptBy, couponIds, placeType,
|
||||
firstOrder, finalAmount);
|
||||
entity.setEstimatedRevenue(estimatedRevenueVo.getRevenueAmount());
|
||||
entity.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
|
||||
}
|
||||
// 如果订单是打赏单,订单直接完成
|
||||
if ("2".equals(placeType)) {
|
||||
entity.setAcceptTime(LocalDateTime.now());
|
||||
entity.setOrderStartTime(LocalDateTime.now());
|
||||
entity.setOrderEndTime(LocalDateTime.now());
|
||||
}
|
||||
// 修改顾客下单信息
|
||||
userInfoService.saveOrderInfo(entity);
|
||||
// 保存订单
|
||||
this.baseMapper.insert(entity);
|
||||
// 修改优惠券状态
|
||||
playCouponDetailsService.updateCouponUseStateByIds(couponIds, "2");
|
||||
if ("2".equals(placeType) && StrUtil.isNotBlank(acceptBy)) {
|
||||
orderLifecycleService.completeOrder(
|
||||
orderId,
|
||||
OrderCompletionContext.of(null, null, OrderTriggerSource.REWARD_ORDER)
|
||||
.withForceNotify(true)
|
||||
.withComment("auto-complete reward order"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createOrderInfo(OrderCreationRequest request) {
|
||||
// 验证请求
|
||||
validateOrderCreationRequest(request);
|
||||
|
||||
PlayOrderInfoEntity entity = buildOrderEntity(request);
|
||||
|
||||
// 处理随机单要求
|
||||
if (request.getPlaceType() == OrderConstant.PlaceType.RANDOM) {
|
||||
setRandomOrderRequirements(entity, request.getRandomOrderRequirements());
|
||||
}
|
||||
|
||||
// 处理接单人信息
|
||||
if (StrUtil.isNotBlank(request.getAcceptBy())) {
|
||||
setAcceptByInfo(entity, request);
|
||||
}
|
||||
|
||||
// 处理打赏单自动完成逻辑
|
||||
if (request.isRewardOrder()) {
|
||||
setRewardOrderCompleted(entity);
|
||||
}
|
||||
// 处理首单逻辑
|
||||
if (StrUtil.isNotBlank(request.getAcceptBy()) && StrUtil.isNotBlank(request.getPurchaserBy())) {
|
||||
entity.setFirstOrder(this.checkFirstOrderFlag(request.getPurchaserBy(), request.getAcceptBy()) ? "1" : "0");
|
||||
}
|
||||
|
||||
// 保存订单
|
||||
userInfoService.saveOrderInfo(entity);
|
||||
this.baseMapper.insert(entity);
|
||||
|
||||
// 修改优惠券状态
|
||||
playCouponDetailsService.updateCouponUseStateByIds(
|
||||
request.getPaymentInfo().getCouponIds(), "2");
|
||||
|
||||
// 打赏单立即入账
|
||||
if (request.isRewardOrder() && StrUtil.isNotBlank(request.getAcceptBy())) {
|
||||
orderLifecycleService.completeOrder(
|
||||
entity.getId(),
|
||||
OrderCompletionContext.of(null, null, OrderTriggerSource.REWARD_ORDER)
|
||||
.withForceNotify(true)
|
||||
.withComment("auto-complete reward order"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证订单创建请求
|
||||
*/
|
||||
private void validateOrderCreationRequest(OrderCreationRequest request) {
|
||||
if (request.getPlaceType() == OrderConstant.PlaceType.RANDOM
|
||||
&& !request.isValidForRandomOrder()) {
|
||||
throw new CustomException("随机单必须提供店员要求信息");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建订单实体
|
||||
*/
|
||||
private PlayOrderInfoEntity buildOrderEntity(OrderCreationRequest request) {
|
||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||
|
||||
// 基本信息
|
||||
entity.setId(request.getOrderId());
|
||||
entity.setOrderNo(request.getOrderNo());
|
||||
entity.setOrderStatus(request.getOrderStatus().getCode());
|
||||
entity.setOrderType(request.getOrderType().getCode());
|
||||
entity.setPlaceType(request.getPlaceType().getCode());
|
||||
entity.setRewardType(request.getRewardType().getCode());
|
||||
entity.setFirstOrder(request.getFirstOrderString());
|
||||
|
||||
// 固定默认值
|
||||
entity.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
entity.setBackendEntry("0");
|
||||
entity.setPayMethod("0");
|
||||
entity.setOrderSettlementState("0");
|
||||
entity.setOrdersExpiredState("0");
|
||||
|
||||
// 商品信息
|
||||
CommodityInfo commodityInfo = request.getCommodityInfo();
|
||||
entity.setCommodityId(commodityInfo.getCommodityId());
|
||||
entity.setCommodityType(commodityInfo.getCommodityType().getCode());
|
||||
entity.setCommodityPrice(commodityInfo.getCommodityPrice());
|
||||
entity.setServiceDuration(commodityInfo.getServiceDuration());
|
||||
entity.setCommodityName(commodityInfo.getCommodityName());
|
||||
entity.setCommodityNumber(commodityInfo.getCommodityNumber());
|
||||
|
||||
// 支付信息
|
||||
PaymentInfo paymentInfo = request.getPaymentInfo();
|
||||
entity.setOrderMoney(paymentInfo.getOrderMoney());
|
||||
entity.setFinalAmount(paymentInfo.getFinalAmount());
|
||||
entity.setDiscountAmount(paymentInfo.getDiscountAmount());
|
||||
entity.setCouponIds(paymentInfo.getCouponIds());
|
||||
entity.setUseCoupon(
|
||||
paymentInfo.getCouponIds() != null && !paymentInfo.getCouponIds().isEmpty() ? "1" : "0");
|
||||
|
||||
// 用户信息
|
||||
entity.setPurchaserBy(request.getPurchaserBy());
|
||||
entity.setPurchaserTime(LocalDateTime.now());
|
||||
entity.setWeiChatCode(request.getWeiChatCode());
|
||||
entity.setRemark(request.getRemark());
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置随机单要求
|
||||
*/
|
||||
private void setRandomOrderRequirements(PlayOrderInfoEntity entity, RandomOrderRequirements requirements) {
|
||||
if (requirements != null) {
|
||||
entity.setSex(requirements.getClerkGender().getCode());
|
||||
entity.setLevelId(requirements.getClerkLevelId());
|
||||
entity.setExcludeHistory(requirements.getExcludeHistory());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置接单人信息
|
||||
*/
|
||||
private void setAcceptByInfo(PlayOrderInfoEntity entity, OrderCreationRequest request) {
|
||||
entity.setAcceptBy(request.getAcceptBy());
|
||||
ClerkEstimatedRevenueVo estimatedRevenueVo = getClerkEstimatedRevenue(
|
||||
request.getAcceptBy(),
|
||||
request.getPaymentInfo().getCouponIds(),
|
||||
request.getPlaceType().getCode(),
|
||||
request.getFirstOrderString(),
|
||||
request.getPaymentInfo().getFinalAmount());
|
||||
entity.setEstimatedRevenue(estimatedRevenueVo.getRevenueAmount());
|
||||
entity.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置打赏单为已完成状态
|
||||
*/
|
||||
private void setRewardOrderCompleted(PlayOrderInfoEntity entity) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setAcceptTime(now);
|
||||
entity.setOrderStartTime(now);
|
||||
entity.setOrderEndTime(now);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
|
||||
String firstOrder, BigDecimal finalAmount) {
|
||||
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
|
||||
ClerkEstimatedRevenueVo estimatedRevenueVo = new ClerkEstimatedRevenueVo();
|
||||
switch (placeType) {
|
||||
case "0": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
estimatedRevenueVo.setRevenueRatio(entity.getFirstRegularRatio());
|
||||
estimatedRevenueVo
|
||||
.setRevenueAmount(
|
||||
finalAmount
|
||||
.multiply(new BigDecimal(entity.getFirstRegularRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
estimatedRevenueVo.setRevenueRatio(entity.getNotFirstRegularRatio());
|
||||
estimatedRevenueVo
|
||||
.setRevenueAmount(finalAmount
|
||||
.multiply(new BigDecimal(entity.getNotFirstRegularRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "1": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
estimatedRevenueVo.setRevenueRatio(entity.getFirstRandomRadio());
|
||||
estimatedRevenueVo
|
||||
.setRevenueAmount(
|
||||
finalAmount
|
||||
.multiply(new BigDecimal(entity.getFirstRandomRadio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
estimatedRevenueVo.setRevenueRatio(entity.getNotFirstRandomRadio());
|
||||
estimatedRevenueVo
|
||||
.setRevenueAmount(finalAmount
|
||||
.multiply(new BigDecimal(entity.getNotFirstRandomRadio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "2": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
estimatedRevenueVo.setRevenueRatio(entity.getFirstRewardRatio());
|
||||
estimatedRevenueVo
|
||||
.setRevenueAmount(
|
||||
finalAmount
|
||||
.multiply(new BigDecimal(entity.getFirstRewardRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
estimatedRevenueVo.setRevenueRatio(entity.getNotFirstRewardRatio());
|
||||
estimatedRevenueVo
|
||||
.setRevenueAmount(finalAmount
|
||||
.multiply(new BigDecimal(entity.getNotFirstRewardRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "-1": {
|
||||
log.error("下单类型异常,placeType={}", placeType);
|
||||
estimatedRevenueVo.setRevenueAmount(finalAmount);
|
||||
estimatedRevenueVo.setRevenueRatio(100);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
log.error("下单类型错误,placeType={}", placeType);
|
||||
estimatedRevenueVo.setRevenueAmount(finalAmount);
|
||||
estimatedRevenueVo.setRevenueRatio(100);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果优惠券不由店铺承担,那么店员预计收入减去优惠金额
|
||||
for (String croupId : croupIds) {
|
||||
PlayCouponDetailsReturnVo couponInfo = playCouponDetailsService.selectPlayCouponDetailsById(croupId);
|
||||
if ("0".equals(couponInfo.getAttributionDiscounts())) {
|
||||
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
|
||||
if ("0".equals(couponInfo.getDiscountType())) {
|
||||
revenueAmount = revenueAmount.subtract(couponInfo.getDiscountAmount());
|
||||
} else {
|
||||
revenueAmount = revenueAmount.subtract(revenueAmount.subtract(couponInfo.getDiscountAmount()));
|
||||
}
|
||||
log.debug("优惠券ID={},优惠券类型={},优惠金额={},优惠前金额={},优惠前金额={}", croupId, couponInfo.getDiscountType(),
|
||||
couponInfo.getDiscountAmount(), estimatedRevenueVo.getRevenueAmount(), revenueAmount);
|
||||
estimatedRevenueVo.setRevenueAmount(revenueAmount);
|
||||
}
|
||||
}
|
||||
return estimatedRevenueVo;
|
||||
return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, finalAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,81 +134,59 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
@Override
|
||||
public BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal finalAmount) {
|
||||
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
|
||||
switch (placeType) {
|
||||
case "0": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
return finalAmount.multiply(new BigDecimal(entity.getFirstRegularRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
||||
} else {
|
||||
return finalAmount.multiply(new BigDecimal(entity.getNotFirstRegularRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
case "1": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
return finalAmount.multiply(new BigDecimal(entity.getFirstRandomRadio()).divide(new BigDecimal(100),
|
||||
4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
||||
} else {
|
||||
return finalAmount.multiply(new BigDecimal(entity.getNotFirstRandomRadio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
case "2": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
return finalAmount.multiply(new BigDecimal(entity.getFirstRewardRatio()).divide(new BigDecimal(100),
|
||||
4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
||||
} else {
|
||||
return finalAmount.multiply(new BigDecimal(entity.getNotFirstRewardRatio())
|
||||
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
case "-1": {
|
||||
log.error("下单类型异常,placeType={}", placeType);
|
||||
return finalAmount;
|
||||
}
|
||||
default: {
|
||||
log.error("下单类型错误,placeType={}", placeType);
|
||||
return finalAmount;
|
||||
boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
|
||||
try {
|
||||
OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
|
||||
switch (place) {
|
||||
case SPECIFIED:
|
||||
return calculateRevenue(finalAmount,
|
||||
isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio());
|
||||
case RANDOM:
|
||||
return calculateRevenue(finalAmount,
|
||||
isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio());
|
||||
case REWARD:
|
||||
return calculateRevenue(finalAmount,
|
||||
isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio());
|
||||
case OTHER:
|
||||
default:
|
||||
log.error("下单类型异常,placeType={}", placeType);
|
||||
return finalAmount;
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.error("下单类型错误,placeType={}", placeType, ex);
|
||||
return finalAmount;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getEstimatedRevenueRatio(String clerkId, String placeType, String firstOrder) {
|
||||
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
|
||||
switch (placeType) {
|
||||
case "0": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
return entity.getFirstRegularRatio();
|
||||
} else {
|
||||
return entity.getNotFirstRegularRatio();
|
||||
}
|
||||
}
|
||||
case "1": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
return entity.getFirstRandomRadio();
|
||||
} else {
|
||||
return entity.getNotFirstRandomRadio();
|
||||
}
|
||||
}
|
||||
case "2": {
|
||||
if ("1".equals(firstOrder)) {
|
||||
return entity.getFirstRewardRatio();
|
||||
} else {
|
||||
return entity.getNotFirstRewardRatio();
|
||||
}
|
||||
}
|
||||
case "-1": {
|
||||
log.error("下单类型异常,placeType={}", placeType);
|
||||
return 100;
|
||||
}
|
||||
default: {
|
||||
log.error("下单类型错误,placeType={}", placeType);
|
||||
return 100;
|
||||
boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
|
||||
try {
|
||||
OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
|
||||
switch (place) {
|
||||
case SPECIFIED:
|
||||
return isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio();
|
||||
case RANDOM:
|
||||
return isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio();
|
||||
case REWARD:
|
||||
return isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio();
|
||||
case OTHER:
|
||||
default:
|
||||
log.error("下单类型异常,placeType={}", placeType);
|
||||
return 100;
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.error("下单类型错误,placeType={}", placeType, ex);
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal calculateRevenue(BigDecimal amount, Integer ratio) {
|
||||
return amount.multiply(new BigDecimal(ratio).divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增充值订单
|
||||
*
|
||||
@@ -646,9 +333,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
|
||||
@Override
|
||||
public Boolean checkFirstOrderFlag(String customId, String clerkId) {
|
||||
// 检查是否是首单
|
||||
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class).eq(PlayOrderInfoEntity::getPurchaserBy, customId).eq(PlayOrderInfoEntity::getAcceptBy, clerkId).eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
|
||||
return this.baseMapper.selectCount(wrapper) > 0;
|
||||
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
|
||||
.eq(PlayOrderInfoEntity::getPurchaserBy, customId)
|
||||
.eq(PlayOrderInfoEntity::getAcceptBy, clerkId)
|
||||
.eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
|
||||
return this.baseMapper.selectCount(wrapper) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -884,15 +573,18 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
if (isClerkOperator && OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
|
||||
validateClerkQualificationForRandomOrder(orderInfo, clerkUserInfoEntity, acceptBy);
|
||||
}
|
||||
String firstOrderFlag = resolveFirstOrderFlag(orderInfo.getPurchaserBy(), acceptBy);
|
||||
|
||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderStatus.ACCEPTED.getCode());
|
||||
LocalDateTime acceptTime = LocalDateTime.now();
|
||||
entity.setAcceptBy(acceptBy);
|
||||
entity.setAcceptTime(acceptTime);
|
||||
entity.setFirstOrder(firstOrderFlag);
|
||||
ClerkEstimatedRevenueVo estimatedRevenueVo = this.getClerkEstimatedRevenue(
|
||||
acceptBy,
|
||||
orderInfo.getCouponIds(),
|
||||
orderInfo.getPlaceType(),
|
||||
orderInfo.getFirstOrder(),
|
||||
firstOrderFlag,
|
||||
orderInfo.getFinalAmount());
|
||||
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
|
||||
entity.setEstimatedRevenue(revenueAmount);
|
||||
@@ -926,6 +618,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
orderInfo.setOrderStatus(OrderStatus.ACCEPTED.getCode());
|
||||
orderInfo.setEstimatedRevenue(revenueAmount);
|
||||
orderInfo.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
|
||||
orderInfo.setFirstOrder(firstOrderFlag);
|
||||
log.info("Order accepted successfully. orderId={}, orderNo={}, acceptBy={}, operatorByType={}",
|
||||
orderId, orderInfo.getOrderNo(), acceptBy, operatorByType);
|
||||
wxCustomMpService.sendOrderMessageAsync(orderInfo);
|
||||
@@ -951,6 +644,19 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveFirstOrderFlag(String purchaserId, String clerkId) {
|
||||
if (StrUtil.isBlank(purchaserId) || StrUtil.isBlank(clerkId)) {
|
||||
return OrderConstant.YesNoFlag.NO.getCode();
|
||||
}
|
||||
Long completedCount = orderInfoMapper.selectCount(Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
|
||||
.eq(PlayOrderInfoEntity::getPurchaserBy, purchaserId)
|
||||
.eq(PlayOrderInfoEntity::getAcceptBy, clerkId));
|
||||
|
||||
return (completedCount == null || completedCount == 0)
|
||||
? OrderConstant.YesNoFlag.YES.getCode()
|
||||
: OrderConstant.YesNoFlag.NO.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用的订单查询对象 订单作为主表 连接顾客用户表、店员用户表、商品表
|
||||
*
|
||||
@@ -1038,12 +744,13 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
log.error("订单状态异常,不能完成订单,orderId={},orderStace={}", orderId, orderState);
|
||||
throw new CustomException("订单状态异常,不能开始订单");
|
||||
}
|
||||
OrderActor actor = resolveCompletionActor(operatorByType);
|
||||
orderLifecycleService.completeOrder(
|
||||
orderId,
|
||||
OrderCompletionContext.of(
|
||||
operatorByType,
|
||||
operatorBy,
|
||||
resolveCompletionSource(operatorByType),
|
||||
actor,
|
||||
actor == OrderActor.SYSTEM ? null : operatorBy,
|
||||
resolveCompletionSource(actor),
|
||||
"manual"));
|
||||
} else {
|
||||
log.error("修改订单状态异常,orderId={},orderStace={}", orderId, orderState);
|
||||
@@ -1182,25 +889,41 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
return orderInfoMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private OrderTriggerSource resolveCompletionSource(String operatorType) {
|
||||
private OrderActor resolveCompletionActor(String operatorType) {
|
||||
if (operatorType == null) {
|
||||
return OrderTriggerSource.UNKNOWN;
|
||||
return OrderActor.SYSTEM;
|
||||
}
|
||||
try {
|
||||
OperatorType type = OperatorType.fromCode(operatorType);
|
||||
switch (type) {
|
||||
case CUSTOMER:
|
||||
return OrderTriggerSource.WX_CUSTOMER;
|
||||
return OrderActor.CUSTOMER;
|
||||
case CLERK:
|
||||
return OrderTriggerSource.WX_CLERK;
|
||||
return OrderActor.CLERK;
|
||||
case ADMIN:
|
||||
return OrderTriggerSource.ADMIN_CONSOLE;
|
||||
return OrderActor.ADMIN;
|
||||
default:
|
||||
return OrderTriggerSource.UNKNOWN;
|
||||
return OrderActor.SYSTEM;
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("Unknown operator type {}, fallback to unknown", operatorType, ex);
|
||||
log.warn("Unknown operator type {}, fallback to SYSTEM", operatorType, ex);
|
||||
return OrderActor.SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
private OrderTriggerSource resolveCompletionSource(OrderActor actor) {
|
||||
if (actor == null) {
|
||||
return OrderTriggerSource.UNKNOWN;
|
||||
}
|
||||
switch (actor) {
|
||||
case CUSTOMER:
|
||||
return OrderTriggerSource.WX_CUSTOMER;
|
||||
case CLERK:
|
||||
return OrderTriggerSource.WX_CLERK;
|
||||
case ADMIN:
|
||||
return OrderTriggerSource.ADMIN_CONSOLE;
|
||||
default:
|
||||
return OrderTriggerSource.SYSTEM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.starry.admin.modules.order.service.support;
|
||||
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 计算店员预计收入的工具类,供订单生命周期与订单查询等场景复用。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ClerkRevenueCalculator {
|
||||
|
||||
@Resource
|
||||
private IPlayClerkUserInfoService playClerkUserInfoService;
|
||||
|
||||
@Resource
|
||||
private IPlayCouponDetailsService playCouponDetailsService;
|
||||
|
||||
public ClerkEstimatedRevenueVo calculateEstimatedRevenue(
|
||||
String clerkId,
|
||||
List<String> couponIds,
|
||||
String placeType,
|
||||
String firstOrder,
|
||||
BigDecimal finalAmount) {
|
||||
PlayClerkLevelInfoEntity levelInfo = playClerkUserInfoService.queryLevelCommission(clerkId);
|
||||
ClerkEstimatedRevenueVo estimatedRevenueVo = new ClerkEstimatedRevenueVo();
|
||||
|
||||
boolean fallbackToOther = false;
|
||||
OrderConstant.PlaceType placeTypeEnum;
|
||||
try {
|
||||
placeTypeEnum = OrderConstant.PlaceType.fromCode(placeType);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
fallbackToOther = true;
|
||||
placeTypeEnum = OrderConstant.PlaceType.OTHER;
|
||||
log.warn("无法识别的下单类型,placeType={},clerkId={}。已按其他类型处理。", placeType, clerkId, ex);
|
||||
}
|
||||
|
||||
switch (placeTypeEnum) {
|
||||
case SPECIFIED: // 指定单
|
||||
fillRegularOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo);
|
||||
break;
|
||||
case RANDOM: // 随机单
|
||||
fillRandomOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo);
|
||||
break;
|
||||
case REWARD: // 打赏单
|
||||
fillRewardOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo);
|
||||
break;
|
||||
case OTHER:
|
||||
default:
|
||||
if (!fallbackToOther) {
|
||||
log.warn("按其他下单类型计算预计收益,placeType={},clerkId={}", placeType, clerkId);
|
||||
}
|
||||
estimatedRevenueVo.setRevenueAmount(finalAmount);
|
||||
estimatedRevenueVo.setRevenueRatio(100);
|
||||
break;
|
||||
}
|
||||
|
||||
adjustRevenueByCoupon(clerkId, couponIds, estimatedRevenueVo);
|
||||
return estimatedRevenueVo;
|
||||
}
|
||||
|
||||
private void fillRegularOrderRevenue(String firstOrder, BigDecimal finalAmount,
|
||||
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
||||
if ("1".equals(firstOrder)) {
|
||||
vo.setRevenueRatio(levelInfo.getFirstRegularRatio());
|
||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRegularRatio()));
|
||||
} else {
|
||||
vo.setRevenueRatio(levelInfo.getNotFirstRegularRatio());
|
||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRegularRatio()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillRandomOrderRevenue(String firstOrder, BigDecimal finalAmount,
|
||||
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
||||
if ("1".equals(firstOrder)) {
|
||||
vo.setRevenueRatio(levelInfo.getFirstRandomRadio());
|
||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRandomRadio()));
|
||||
} else {
|
||||
vo.setRevenueRatio(levelInfo.getNotFirstRandomRadio());
|
||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRandomRadio()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillRewardOrderRevenue(String firstOrder, BigDecimal finalAmount,
|
||||
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
|
||||
if ("1".equals(firstOrder)) {
|
||||
vo.setRevenueRatio(levelInfo.getFirstRewardRatio());
|
||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRewardRatio()));
|
||||
} else {
|
||||
vo.setRevenueRatio(levelInfo.getNotFirstRewardRatio());
|
||||
vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRewardRatio()));
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal scaleAmount(BigDecimal baseAmount, Integer ratio) {
|
||||
return baseAmount
|
||||
.multiply(new BigDecimal(ratio).divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private void adjustRevenueByCoupon(String clerkId,
|
||||
List<String> couponIds,
|
||||
ClerkEstimatedRevenueVo estimatedRevenueVo) {
|
||||
List<String> safeCouponIds = couponIds == null ? Collections.emptyList() : couponIds;
|
||||
for (String couponId : safeCouponIds) {
|
||||
PlayCouponDetailsReturnVo couponInfo = playCouponDetailsService.selectPlayCouponDetailsById(couponId);
|
||||
if (couponInfo == null) {
|
||||
log.warn("优惠券信息不存在,couponId={}, clerkId={}", couponId, clerkId);
|
||||
continue;
|
||||
}
|
||||
if ("0".equals(couponInfo.getAttributionDiscounts())) {
|
||||
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
|
||||
if ("0".equals(couponInfo.getDiscountType())) {
|
||||
revenueAmount = revenueAmount.subtract(couponInfo.getDiscountAmount());
|
||||
} else {
|
||||
revenueAmount = revenueAmount.subtract(revenueAmount.subtract(couponInfo.getDiscountAmount()));
|
||||
}
|
||||
log.debug("优惠券ID={},优惠券类型={},优惠金额={},优惠前金额={},优惠后金额={}",
|
||||
couponId,
|
||||
couponInfo.getDiscountType(),
|
||||
couponInfo.getDiscountAmount(),
|
||||
estimatedRevenueVo.getRevenueAmount(),
|
||||
revenueAmount);
|
||||
estimatedRevenueVo.setRevenueAmount(revenueAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.starry.admin.modules.shop.module.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 优惠券使用状态
|
||||
*/
|
||||
@Getter
|
||||
public enum CouponUseState {
|
||||
UNUSED("1"),
|
||||
USED("2"),
|
||||
RECYCLED("3");
|
||||
|
||||
private final String code;
|
||||
|
||||
CouponUseState(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static CouponUseState fromCode(String code) {
|
||||
for (CouponUseState state : values()) {
|
||||
if (state.code.equals(code)) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown coupon use state code: " + code);
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,16 @@ import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomLeaveMsgService;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
|
||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
@@ -103,6 +105,9 @@ public class WxCustomController {
|
||||
@Resource
|
||||
private IPlayOrderInfoService playOrderInfoService;
|
||||
|
||||
@Resource
|
||||
private IOrderLifecycleService orderLifecycleService;
|
||||
|
||||
@Resource
|
||||
private IPlayCustomLeaveMsgService playCustomLeaveMsgService;
|
||||
|
||||
@@ -231,14 +236,16 @@ public class WxCustomController {
|
||||
}
|
||||
String orderId = IdUtils.getUuid();
|
||||
// 记录订单信息
|
||||
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
|
||||
OrderCreationContext orderRequest = OrderCreationContext.builder()
|
||||
.orderId(orderId)
|
||||
.orderNo(playOrderInfoService.getOrderNo())
|
||||
.orderStatus(OrderConstant.OrderStatus.COMPLETED)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.REWARD)
|
||||
.rewardType(RewardType.BALANCE)
|
||||
.isFirstOrder(true)
|
||||
.isFirstOrder(false)
|
||||
.creatorActor(OrderActor.CUSTOMER)
|
||||
.creatorId(userId)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("")
|
||||
.commodityType(OrderConstant.CommodityType.GIFT)
|
||||
@@ -258,7 +265,7 @@ public class WxCustomController {
|
||||
.weiChatCode(vo.getWeiChatCode())
|
||||
.remark(vo.getRemark())
|
||||
.build();
|
||||
playOrderInfoService.createOrderInfo(orderRequest);
|
||||
orderLifecycleService.initiateOrder(orderRequest);
|
||||
// 顾客减少余额
|
||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(new BigDecimal(vo.getMoney())), "1", "打赏", new BigDecimal(vo.getMoney()), BigDecimal.ZERO, orderId);
|
||||
return R.ok("成功");
|
||||
@@ -325,7 +332,7 @@ public class WxCustomController {
|
||||
String orderId = IdUtils.getUuid();
|
||||
String orderNo = playOrderInfoService.getOrderNo();
|
||||
// 记录订单信息
|
||||
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
|
||||
OrderCreationContext orderRequest = OrderCreationContext.builder()
|
||||
.orderId(orderId)
|
||||
.orderNo(orderNo)
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -333,6 +340,8 @@ public class WxCustomController {
|
||||
.placeType(OrderConstant.PlaceType.SPECIFIED)
|
||||
.rewardType(RewardType.NOT_APPLICABLE)
|
||||
.isFirstOrder(true)
|
||||
.creatorActor(OrderActor.CUSTOMER)
|
||||
.creatorId(customId)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId(commodityInfo.getCommodityId())
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
@@ -352,7 +361,7 @@ public class WxCustomController {
|
||||
.weiChatCode(vo.getWeiChatCode())
|
||||
.remark(vo.getRemark())
|
||||
.build();
|
||||
playOrderInfoService.createOrderInfo(orderRequest);
|
||||
orderLifecycleService.initiateOrder(orderRequest);
|
||||
// 顾客减少余额
|
||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-指定单", money, BigDecimal.ZERO, orderId);
|
||||
// 发送通知给店员
|
||||
@@ -380,7 +389,7 @@ public class WxCustomController {
|
||||
String orderId = IdUtils.getUuid();
|
||||
String orderNo = playOrderInfoService.getOrderNo();
|
||||
// 记录订单信息
|
||||
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
|
||||
OrderCreationContext orderRequest = OrderCreationContext.builder()
|
||||
.orderId(orderId)
|
||||
.orderNo(orderNo)
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -388,6 +397,8 @@ public class WxCustomController {
|
||||
.placeType(OrderConstant.PlaceType.RANDOM)
|
||||
.rewardType(RewardType.NOT_APPLICABLE)
|
||||
.isFirstOrder(true)
|
||||
.creatorActor(OrderActor.CUSTOMER)
|
||||
.creatorId(customId)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId(commodityInfo.getCommodityId())
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
@@ -411,7 +422,7 @@ public class WxCustomController {
|
||||
.excludeHistory(vo.getExcludeHistory())
|
||||
.build())
|
||||
.build();
|
||||
playOrderInfoService.createOrderInfo(orderRequest);
|
||||
orderLifecycleService.initiateOrder(orderRequest);
|
||||
// 顾客减少余额
|
||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-随机单", money, BigDecimal.ZERO, orderId);
|
||||
// 给全部店员发送通知
|
||||
|
||||
@@ -7,10 +7,12 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
|
||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
|
||||
@@ -36,6 +38,9 @@ public class WxGiftOrderService {
|
||||
@Resource
|
||||
private IPlayOrderInfoService playOrderInfoService;
|
||||
|
||||
@Resource
|
||||
private IOrderLifecycleService orderLifecycleService;
|
||||
|
||||
@Resource
|
||||
private IPlayCustomUserInfoService customUserInfoService;
|
||||
|
||||
@@ -81,9 +86,9 @@ public class WxGiftOrderService {
|
||||
}
|
||||
|
||||
String orderId = IdUtils.getUuid();
|
||||
OrderCreationRequest orderRequest = buildOrderCreationRequest(orderId, request, sessionUser.getId(),
|
||||
OrderCreationContext orderRequest = buildOrderCreationContext(orderId, request, sessionUser.getId(),
|
||||
giftInfo, totalAmount);
|
||||
playOrderInfoService.createOrderInfo(orderRequest);
|
||||
orderLifecycleService.initiateOrder(orderRequest);
|
||||
|
||||
BigDecimal newBalance = currentBalance.subtract(totalAmount);
|
||||
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), currentBalance, newBalance, "1",
|
||||
@@ -97,10 +102,10 @@ public class WxGiftOrderService {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
private OrderCreationRequest buildOrderCreationRequest(String orderId, PlayOrderInfoGiftAdd request,
|
||||
private OrderCreationContext buildOrderCreationContext(String orderId, PlayOrderInfoGiftAdd request,
|
||||
String purchaserId, PlayGiftInfoEntity giftInfo,
|
||||
BigDecimal totalAmount) {
|
||||
return OrderCreationRequest.builder()
|
||||
return OrderCreationContext.builder()
|
||||
.orderId(orderId)
|
||||
.orderNo(playOrderInfoService.getOrderNo())
|
||||
.orderStatus(OrderConstant.OrderStatus.COMPLETED)
|
||||
@@ -108,6 +113,8 @@ public class WxGiftOrderService {
|
||||
.placeType(OrderConstant.PlaceType.REWARD)
|
||||
.rewardType(RewardType.GIFT)
|
||||
.isFirstOrder(true)
|
||||
.creatorActor(OrderActor.CUSTOMER)
|
||||
.creatorId(purchaserId)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId(giftInfo.getId())
|
||||
.commodityType(OrderConstant.CommodityType.GIFT)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE play_order_log_info
|
||||
CHANGE COLUMN `order_id` `order_id` varchar(64) NOT NULL COMMENT '订单ID',
|
||||
CHANGE COLUMN `oper_type` `operation_type` varchar(32) NOT NULL COMMENT '操作类型',
|
||||
ADD COLUMN `operator_type` varchar(32) DEFAULT NULL COMMENT '操作人类型',
|
||||
ADD COLUMN `operator_id` varchar(64) DEFAULT NULL COMMENT '操作人ID';
|
||||
@@ -4,7 +4,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import java.math.BigDecimal;
|
||||
@@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test;
|
||||
*
|
||||
* @author admin
|
||||
*/
|
||||
class OrderCreationRequestTest {
|
||||
class OrderCreationContextTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("测试Builder模式构建订单请求")
|
||||
@@ -42,7 +42,7 @@ class OrderCreationRequestTest {
|
||||
.build();
|
||||
|
||||
// 构建订单请求
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
OrderCreationContext request = OrderCreationContext.builder()
|
||||
.orderId("order_123456")
|
||||
.orderNo("ORD20240906001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -82,7 +82,7 @@ class OrderCreationRequestTest {
|
||||
@DisplayName("测试订单类型判断方法")
|
||||
void testOrderTypeChecks() {
|
||||
// 测试指定单
|
||||
OrderCreationRequest specifiedOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext specifiedOrder = OrderCreationContext.builder()
|
||||
.orderId("order_001")
|
||||
.orderNo("ORD001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -99,7 +99,7 @@ class OrderCreationRequestTest {
|
||||
assertFalse(specifiedOrder.isRewardOrder());
|
||||
|
||||
// 测试随机单
|
||||
OrderCreationRequest randomOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext randomOrder = OrderCreationContext.builder()
|
||||
.orderId("order_002")
|
||||
.orderNo("ORD002")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -121,7 +121,7 @@ class OrderCreationRequestTest {
|
||||
assertFalse(randomOrder.isRewardOrder());
|
||||
|
||||
// 测试打赏单
|
||||
OrderCreationRequest rewardOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext rewardOrder = OrderCreationContext.builder()
|
||||
.orderId("order_003")
|
||||
.orderNo("ORD003")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -142,7 +142,7 @@ class OrderCreationRequestTest {
|
||||
@DisplayName("测试首单标识转换")
|
||||
void testFirstOrderStringConversion() {
|
||||
// 测试首单
|
||||
OrderCreationRequest firstOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext firstOrder = OrderCreationContext.builder()
|
||||
.orderId("order_001")
|
||||
.orderNo("ORD001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -157,7 +157,7 @@ class OrderCreationRequestTest {
|
||||
assertEquals("1", firstOrder.getFirstOrderString());
|
||||
|
||||
// 测试非首单
|
||||
OrderCreationRequest notFirstOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext notFirstOrder = OrderCreationContext.builder()
|
||||
.orderId("order_002")
|
||||
.orderNo("ORD002")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -176,7 +176,7 @@ class OrderCreationRequestTest {
|
||||
@DisplayName("测试随机单验证逻辑")
|
||||
void testRandomOrderValidation() {
|
||||
// 有效的随机单
|
||||
OrderCreationRequest validRandomOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext validRandomOrder = OrderCreationContext.builder()
|
||||
.orderId("order_001")
|
||||
.orderNo("ORD001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -194,7 +194,7 @@ class OrderCreationRequestTest {
|
||||
assertTrue(validRandomOrder.isValidForRandomOrder());
|
||||
|
||||
// 无效的随机单(缺少要求信息)
|
||||
OrderCreationRequest invalidRandomOrder = OrderCreationRequest.builder()
|
||||
OrderCreationContext invalidRandomOrder = OrderCreationContext.builder()
|
||||
.orderId("order_002")
|
||||
.orderNo("ORD002")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
@@ -1,512 +0,0 @@
|
||||
package com.starry.admin.modules.order.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
|
||||
import com.starry.admin.modules.order.module.dto.CommodityInfo;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl;
|
||||
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 订单服务测试类 - 测试重构后的createOrderInfo方法
|
||||
*
|
||||
* @author admin
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PlayOrderInfoServiceTest {
|
||||
|
||||
@Mock
|
||||
private PlayOrderInfoMapper orderInfoMapper;
|
||||
|
||||
@Mock
|
||||
private IPlayClerkUserInfoService playClerkUserInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayCustomUserInfoService playCustomUserInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayCustomUserInfoService userInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayCouponDetailsService playCouponDetailsService;
|
||||
|
||||
@Mock
|
||||
private WxCustomMpService wxCustomMpService;
|
||||
|
||||
@Mock
|
||||
private IPlayCustomUserInfoService customUserInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayClerkLevelInfoService playClerkLevelInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayPersonnelGroupInfoService playClerkGroupInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayOrderRefundInfoService playOrderRefundInfoService;
|
||||
|
||||
@Mock
|
||||
private IEarningsService earningsService;
|
||||
|
||||
@InjectMocks
|
||||
private PlayOrderInfoServiceImpl orderService;
|
||||
|
||||
@Test
|
||||
@DisplayName("创建指定订单 - 成功案例")
|
||||
void testCreateSpecifiedOrder_Success() {
|
||||
// 准备测试数据
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("test_order_001")
|
||||
.orderNo("ORD20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.SPECIFIED)
|
||||
.rewardType(OrderConstant.RewardType.BALANCE)
|
||||
.isFirstOrder(true)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("commodity_001")
|
||||
.commodityName("测试商品")
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(100.00))
|
||||
.serviceDuration("60")
|
||||
.commodityNumber("1")
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(100.00))
|
||||
.finalAmount(BigDecimal.valueOf(90.00))
|
||||
.discountAmount(BigDecimal.valueOf(10.00))
|
||||
.couponIds(Arrays.asList("coupon_001"))
|
||||
.payMethod("1")
|
||||
.build())
|
||||
.purchaserBy("customer_001")
|
||||
// 不设置 acceptBy,避免调用复杂的 setAcceptByInfo 方法
|
||||
.weiChatCode("wx_test_001")
|
||||
.remark("测试订单")
|
||||
.build();
|
||||
|
||||
// Mock 依赖服务的返回
|
||||
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
|
||||
|
||||
// 执行测试
|
||||
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
|
||||
|
||||
// 验证方法调用
|
||||
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
|
||||
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_001"), "2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建随机订单 - 成功案例")
|
||||
void testCreateRandomOrder_Success() {
|
||||
// 准备随机单要求
|
||||
RandomOrderRequirements randomRequirements = RandomOrderRequirements.builder()
|
||||
.clerkGender(OrderConstant.Gender.FEMALE)
|
||||
.clerkLevelId("level_001")
|
||||
.excludeHistory("1")
|
||||
.build();
|
||||
|
||||
// 构建随机单请求
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("random_order_001")
|
||||
.orderNo("RND20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.RANDOM)
|
||||
.rewardType(OrderConstant.RewardType.NOT_APPLICABLE)
|
||||
.isFirstOrder(false)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("service_001")
|
||||
.commodityName("陪聊服务")
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(50.00))
|
||||
.serviceDuration("30")
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(50.00))
|
||||
.finalAmount(BigDecimal.valueOf(50.00))
|
||||
.discountAmount(BigDecimal.ZERO)
|
||||
.couponIds(Collections.emptyList())
|
||||
.payMethod("0")
|
||||
.build())
|
||||
.purchaserBy("customer_002")
|
||||
.weiChatCode("wx_test_002")
|
||||
.remark("随机单测试")
|
||||
.randomOrderRequirements(randomRequirements)
|
||||
.build();
|
||||
|
||||
// Mock 依赖服务的返回
|
||||
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
|
||||
|
||||
// 执行测试
|
||||
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
|
||||
|
||||
// 验证方法调用
|
||||
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
|
||||
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Collections.emptyList(), "2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建打赏订单 - 自动完成")
|
||||
void testCreateRewardOrder_AutoComplete() {
|
||||
// 构建打赏单请求
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("reward_order_001")
|
||||
.orderNo("REW20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.REWARD)
|
||||
.rewardType(OrderConstant.RewardType.GIFT)
|
||||
.isFirstOrder(false)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("gift_001")
|
||||
.commodityName("虚拟礼物")
|
||||
.commodityType(OrderConstant.CommodityType.GIFT)
|
||||
.commodityPrice(BigDecimal.valueOf(20.00))
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(20.00))
|
||||
.finalAmount(BigDecimal.valueOf(20.00))
|
||||
.discountAmount(BigDecimal.ZERO)
|
||||
.couponIds(Collections.emptyList())
|
||||
.payMethod("1")
|
||||
.build())
|
||||
.purchaserBy("customer_003")
|
||||
// 不设置 acceptBy,避免调用复杂的 setAcceptByInfo 方法
|
||||
.weiChatCode("wx_test_003")
|
||||
.remark("打赏订单")
|
||||
.build();
|
||||
|
||||
// Mock 依赖服务的返回
|
||||
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
|
||||
|
||||
// 执行测试
|
||||
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
|
||||
|
||||
// 验证方法调用
|
||||
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
|
||||
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Collections.emptyList(), "2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建随机订单失败 - 缺少随机单要求")
|
||||
void testCreateRandomOrder_MissingRequirements() {
|
||||
// 构建无要求的随机单请求
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("invalid_random_order")
|
||||
.orderNo("IRO20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.RANDOM) // 随机单但没有要求
|
||||
.rewardType(OrderConstant.RewardType.NOT_APPLICABLE)
|
||||
.isFirstOrder(false)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("service_001")
|
||||
.commodityName("服务")
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(50.00))
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(50.00))
|
||||
.finalAmount(BigDecimal.valueOf(50.00))
|
||||
.discountAmount(BigDecimal.ZERO)
|
||||
.couponIds(Collections.emptyList())
|
||||
.build())
|
||||
.purchaserBy("customer_004")
|
||||
.weiChatCode("wx_test_004")
|
||||
.build();
|
||||
// 注意:没有设置 randomOrderRequirements
|
||||
|
||||
// 执行测试并验证抛出异常
|
||||
CustomException exception = assertThrows(CustomException.class,
|
||||
() -> orderService.createOrderInfo(request));
|
||||
|
||||
assertEquals("随机单必须提供店员要求信息", exception.getMessage());
|
||||
|
||||
// 验证没有调用数据库操作
|
||||
verify(orderInfoMapper, never()).insert(any(PlayOrderInfoEntity.class));
|
||||
verify(userInfoService, never()).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
verify(playCouponDetailsService, never()).updateCouponUseStateByIds(anyList(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试优惠券使用状态更新")
|
||||
void testCouponStatusUpdate() {
|
||||
// 准备包含多个优惠券的订单
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("coupon_order_001")
|
||||
.orderNo("CPN20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.SPECIFIED)
|
||||
.rewardType(OrderConstant.RewardType.NOT_APPLICABLE)
|
||||
.isFirstOrder(false)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("commodity_002")
|
||||
.commodityName("优惠商品")
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(200.00))
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(200.00))
|
||||
.finalAmount(BigDecimal.valueOf(150.00))
|
||||
.discountAmount(BigDecimal.valueOf(50.00))
|
||||
.couponIds(Arrays.asList("coupon_001", "coupon_002", "coupon_003"))
|
||||
.payMethod("1")
|
||||
.build())
|
||||
.purchaserBy("customer_005")
|
||||
// 不设置 acceptBy,避免调用复杂的 setAcceptByInfo 方法
|
||||
.weiChatCode("wx_test_005")
|
||||
.build();
|
||||
|
||||
// Mock 依赖服务的返回
|
||||
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
|
||||
|
||||
// 执行测试
|
||||
orderService.createOrderInfo(request);
|
||||
|
||||
// 验证优惠券状态更新被正确调用
|
||||
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(
|
||||
Arrays.asList("coupon_001", "coupon_002", "coupon_003"), "2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试带接单人的订单创建 - 需要完整mock依赖")
|
||||
void testCreateOrderWithAcceptBy_ComplexScenario() {
|
||||
// 创建模拟的店员等级信息
|
||||
com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity mockLevelEntity =
|
||||
new com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity();
|
||||
mockLevelEntity.setFirstRegularRatio(15);
|
||||
mockLevelEntity.setNotFirstRegularRatio(12);
|
||||
|
||||
// 创建模拟的优惠券信息
|
||||
com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo mockCouponInfo =
|
||||
new com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo();
|
||||
mockCouponInfo.setAttributionDiscounts("1"); // 1表示店铺承担,不需要从店员收入中扣除
|
||||
mockCouponInfo.setDiscountType("0");
|
||||
mockCouponInfo.setDiscountAmount(BigDecimal.valueOf(20.00));
|
||||
|
||||
// 准备测试数据
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("complex_order_001")
|
||||
.orderNo("CPX20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.SPECIFIED)
|
||||
.rewardType(OrderConstant.RewardType.BALANCE)
|
||||
.isFirstOrder(true)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("commodity_003")
|
||||
.commodityName("复杂商品")
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(300.00))
|
||||
.serviceDuration("120")
|
||||
.commodityNumber("1")
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(300.00))
|
||||
.finalAmount(BigDecimal.valueOf(280.00))
|
||||
.discountAmount(BigDecimal.valueOf(20.00))
|
||||
.couponIds(Arrays.asList("coupon_004"))
|
||||
.payMethod("0")
|
||||
.build())
|
||||
.purchaserBy("customer_006")
|
||||
.acceptBy("clerk_004")
|
||||
.weiChatCode("wx_test_006")
|
||||
.remark("带接单人的复杂订单")
|
||||
.build();
|
||||
|
||||
// Mock 店员相关的依赖
|
||||
when(playClerkUserInfoService.queryLevelCommission("clerk_004")).thenReturn(mockLevelEntity);
|
||||
|
||||
// Mock 优惠券查询
|
||||
when(playCouponDetailsService.selectPlayCouponDetailsById("coupon_004")).thenReturn(mockCouponInfo);
|
||||
|
||||
// Mock 其他依赖服务的返回
|
||||
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
|
||||
|
||||
// 执行测试
|
||||
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
|
||||
|
||||
// 验证方法调用
|
||||
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
|
||||
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_004"), "2");
|
||||
verify(playClerkUserInfoService, times(1)).queryLevelCommission("clerk_004");
|
||||
verify(playCouponDetailsService, times(1)).selectPlayCouponDetailsById("coupon_004");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试店员收入计算 - 优惠券由店员承担")
|
||||
void testClerkRevenueCalculation_ClerkBearsCouponCost() {
|
||||
// 创建模拟的店员等级信息
|
||||
com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity mockLevelEntity =
|
||||
new com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity();
|
||||
mockLevelEntity.setFirstRegularRatio(20); // 首单20%佣金
|
||||
mockLevelEntity.setNotFirstRegularRatio(15); // 非首单15%佣金
|
||||
|
||||
// 创建模拟的优惠券信息 - 店员承担优惠
|
||||
com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo mockCouponInfo =
|
||||
new com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo();
|
||||
mockCouponInfo.setAttributionDiscounts("0"); // 0表示店员承担,需要从店员收入中扣除
|
||||
mockCouponInfo.setDiscountType("0"); // 固定金额优惠
|
||||
mockCouponInfo.setDiscountAmount(BigDecimal.valueOf(15.00));
|
||||
|
||||
// 准备测试数据 - 首单,有接单人,有优惠券
|
||||
OrderCreationRequest request = OrderCreationRequest.builder()
|
||||
.orderId("revenue_test_001")
|
||||
.orderNo("REV20241001001")
|
||||
.orderStatus(OrderConstant.OrderStatus.PENDING)
|
||||
.orderType(OrderConstant.OrderType.NORMAL)
|
||||
.placeType(OrderConstant.PlaceType.SPECIFIED)
|
||||
.rewardType(OrderConstant.RewardType.BALANCE)
|
||||
.isFirstOrder(true) // 首单
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("commodity_revenue")
|
||||
.commodityName("收入测试商品")
|
||||
.commodityType(OrderConstant.CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(200.00))
|
||||
.serviceDuration("90")
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(200.00))
|
||||
.finalAmount(BigDecimal.valueOf(185.00)) // 使用了15元优惠券
|
||||
.discountAmount(BigDecimal.valueOf(15.00))
|
||||
.couponIds(Arrays.asList("coupon_revenue_001"))
|
||||
.payMethod("1")
|
||||
.build())
|
||||
.purchaserBy("customer_revenue")
|
||||
.acceptBy("clerk_revenue")
|
||||
.weiChatCode("wx_revenue_test")
|
||||
.remark("收入计算测试订单")
|
||||
.build();
|
||||
|
||||
// Mock 依赖
|
||||
when(playClerkUserInfoService.queryLevelCommission("clerk_revenue")).thenReturn(mockLevelEntity);
|
||||
when(playCouponDetailsService.selectPlayCouponDetailsById("coupon_revenue_001")).thenReturn(mockCouponInfo);
|
||||
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
|
||||
|
||||
// 执行测试
|
||||
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
|
||||
|
||||
// 验证核心业务逻辑的调用
|
||||
verify(playClerkUserInfoService, times(1)).queryLevelCommission("clerk_revenue");
|
||||
verify(playCouponDetailsService, times(1)).selectPlayCouponDetailsById("coupon_revenue_001");
|
||||
|
||||
// 验证数据操作
|
||||
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
|
||||
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
|
||||
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_revenue_001"), "2");
|
||||
|
||||
// 这个测试验证了:
|
||||
// 1. 首单佣金比例计算(20%)
|
||||
// 2. 优惠券影响店员收入的计算逻辑
|
||||
// 3. 复杂业务流程的正确执行
|
||||
// 实际收入计算:185元 * 20% = 37元,但由于优惠券由店员承担,需要减去15元,最终收入22元
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("管理员强制取消已接单/服务中订单 - 成功流程")
|
||||
void testForceCancelOngoingOrderByAdminSuccess() {
|
||||
String orderId = "order_force_cancel";
|
||||
PlayOrderInfoEntity inProgressOrder = new PlayOrderInfoEntity();
|
||||
inProgressOrder.setId(orderId);
|
||||
inProgressOrder.setOrderStatus(OrderStatus.IN_PROGRESS.getCode());
|
||||
inProgressOrder.setAcceptBy("clerk-1");
|
||||
inProgressOrder.setPurchaserBy("customer-1");
|
||||
inProgressOrder.setFinalAmount(BigDecimal.valueOf(100));
|
||||
inProgressOrder.setPayMethod("1");
|
||||
|
||||
PlayOrderInfoEntity cancelledOrder = new PlayOrderInfoEntity();
|
||||
cancelledOrder.setId(orderId);
|
||||
cancelledOrder.setOrderStatus(OrderStatus.CANCELLED.getCode());
|
||||
|
||||
PlayCustomUserInfoEntity customUserInfo = new PlayCustomUserInfoEntity();
|
||||
customUserInfo.setId("customer-1");
|
||||
customUserInfo.setAccountBalance(BigDecimal.valueOf(200));
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(inProgressOrder, cancelledOrder);
|
||||
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
when(customUserInfoService.getById("customer-1")).thenReturn(customUserInfo);
|
||||
|
||||
doNothing().when(customUserInfoService).updateAccountBalanceById(eq("customer-1"), any(BigDecimal.class),
|
||||
any(BigDecimal.class), anyString(), anyString(), any(BigDecimal.class), any(BigDecimal.class), eq(orderId));
|
||||
doNothing().when(playOrderRefundInfoService).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(),
|
||||
anyString(), any(BigDecimal.class), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
doNothing().when(wxCustomMpService).sendOrderCancelMessageAsync(any(PlayOrderInfoEntity.class), anyString());
|
||||
|
||||
assertDoesNotThrow(() -> orderService.forceCancelOngoingOrder("2", "admin-1", orderId,
|
||||
BigDecimal.valueOf(80), "管理员取消测试", Collections.emptyList()));
|
||||
|
||||
verify(orderInfoMapper, times(1)).updateById(any(PlayOrderInfoEntity.class));
|
||||
verify(customUserInfoService, times(1)).updateAccountBalanceById(eq("customer-1"), any(BigDecimal.class),
|
||||
any(BigDecimal.class), eq(OrderConstant.BalanceOperationType.REFUND.getCode()), eq("订单取消退款"),
|
||||
eq(BigDecimal.valueOf(80)), eq(BigDecimal.ZERO), eq(orderId));
|
||||
verify(playOrderRefundInfoService, times(1)).add(eq(orderId), eq("customer-1"), eq("clerk-1"),
|
||||
eq(inProgressOrder.getPayMethod()),
|
||||
eq(OrderConstant.OrderRefundRecordType.PARTIAL.getCode()),
|
||||
eq(BigDecimal.valueOf(80)), eq("管理员取消测试"), eq("2"), eq("admin-1"),
|
||||
eq(OrderConstant.OrderRefundState.PROCESSING.getCode()),
|
||||
eq(OrderConstant.ReviewRequirement.NOT_REQUIRED.getCode()));
|
||||
verify(wxCustomMpService, times(1)).sendOrderCancelMessageAsync(any(PlayOrderInfoEntity.class),
|
||||
eq("管理员取消测试"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("强制取消订单 - 非进行中状态抛出异常")
|
||||
void testForceCancelOngoingOrderInvalidStatus() {
|
||||
String orderId = "order_invalid_force_cancel";
|
||||
PlayOrderInfoEntity pendingOrder = new PlayOrderInfoEntity();
|
||||
pendingOrder.setId(orderId);
|
||||
pendingOrder.setOrderStatus(OrderStatus.PENDING.getCode());
|
||||
pendingOrder.setAcceptBy("clerk-1");
|
||||
pendingOrder.setPurchaserBy("customer-1");
|
||||
pendingOrder.setFinalAmount(BigDecimal.valueOf(50));
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(pendingOrder);
|
||||
|
||||
assertThrows(CustomException.class, () -> orderService.forceCancelOngoingOrder("2", "admin-1", orderId,
|
||||
null, "原因", Collections.emptyList()));
|
||||
verify(orderInfoMapper, never()).updateById(any(PlayOrderInfoEntity.class));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
@@ -12,27 +13,45 @@ import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
||||
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.CommodityType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.Gender;
|
||||
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.OrderRefundFlag;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.PayMethod;
|
||||
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.RewardType;
|
||||
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.OrderCompletionContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
||||
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
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.Mockito;
|
||||
@@ -59,6 +78,76 @@ class OrderLifecycleServiceImplTest {
|
||||
@Mock
|
||||
private IPlayCustomUserInfoService customUserInfoService;
|
||||
|
||||
@Mock
|
||||
private IPlayCouponDetailsService playCouponDetailsService;
|
||||
|
||||
@Mock
|
||||
private ClerkRevenueCalculator clerkRevenueCalculator;
|
||||
|
||||
@Mock
|
||||
private PlayOrderLogInfoMapper orderLogInfoMapper;
|
||||
|
||||
@Test
|
||||
void initiateOrder_specifiedOrder_persistsAndUpdatesCoupon() {
|
||||
OrderCreationContext request = OrderCreationContext.builder()
|
||||
.orderId("order-init-001")
|
||||
.orderNo("NO20241001")
|
||||
.orderStatus(OrderStatus.PENDING)
|
||||
.orderType(OrderType.NORMAL)
|
||||
.placeType(PlaceType.SPECIFIED)
|
||||
.rewardType(RewardType.NOT_APPLICABLE)
|
||||
.isFirstOrder(true)
|
||||
.commodityInfo(CommodityInfo.builder()
|
||||
.commodityId("commodity-01")
|
||||
.commodityType(CommodityType.SERVICE)
|
||||
.commodityPrice(BigDecimal.valueOf(199))
|
||||
.serviceDuration("60")
|
||||
.commodityName("服务A")
|
||||
.commodityNumber("1")
|
||||
.build())
|
||||
.paymentInfo(PaymentInfo.builder()
|
||||
.orderMoney(BigDecimal.valueOf(199))
|
||||
.finalAmount(BigDecimal.valueOf(179))
|
||||
.discountAmount(BigDecimal.valueOf(20))
|
||||
.couponIds(Collections.singletonList("coupon-1"))
|
||||
.payMethod(PayMethod.WECHAT.getCode())
|
||||
.build())
|
||||
.purchaserBy("customer-1")
|
||||
.acceptBy("clerk-1")
|
||||
.weiChatCode("wx-001")
|
||||
.remark("备注")
|
||||
.build();
|
||||
|
||||
ClerkEstimatedRevenueVo revenueVo = new ClerkEstimatedRevenueVo();
|
||||
revenueVo.setRevenueAmount(BigDecimal.valueOf(89.5));
|
||||
revenueVo.setRevenueRatio(50);
|
||||
|
||||
when(orderInfoMapper.selectCount(any())).thenReturn(0L);
|
||||
when(orderInfoMapper.insert(any())).thenReturn(1);
|
||||
when(clerkRevenueCalculator.calculateEstimatedRevenue(
|
||||
anyString(), anyList(), anyString(), anyString(), any())).thenReturn(revenueVo);
|
||||
doNothing().when(customUserInfoService).saveOrderInfo(any());
|
||||
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), anyString());
|
||||
|
||||
PlayOrderInfoEntity created = lifecycleService.initiateOrder(request);
|
||||
|
||||
verify(customUserInfoService).saveOrderInfo(created);
|
||||
verify(orderInfoMapper).insert(created);
|
||||
verify(playCouponDetailsService).updateCouponUseStateByIds(
|
||||
request.getPaymentInfo().getCouponIds(), CouponUseState.USED.getCode());
|
||||
verify(clerkRevenueCalculator).calculateEstimatedRevenue(
|
||||
request.getAcceptBy(),
|
||||
request.getPaymentInfo().getCouponIds(),
|
||||
request.getPlaceType().getCode(),
|
||||
YesNoFlag.YES.getCode(),
|
||||
request.getPaymentInfo().getFinalAmount());
|
||||
|
||||
assertEquals(YesNoFlag.YES.getCode(), created.getFirstOrder());
|
||||
assertEquals(revenueVo.getRevenueAmount(), created.getEstimatedRevenue());
|
||||
assertEquals(revenueVo.getRevenueRatio(), created.getEstimatedRevenueRatio());
|
||||
assertEquals(PayMethod.WECHAT.getCode(), created.getPayMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeOrder_inProgress_createsEarningsAndNotifies() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
@@ -69,20 +158,18 @@ class OrderLifecycleServiceImplTest {
|
||||
completed.setOrderEndTime(LocalDateTime.now());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(inProgress, completed);
|
||||
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
mockEarningsCounts(0L, 1L);
|
||||
|
||||
OrderCompletionContext context = OrderCompletionContext.of(
|
||||
OperatorType.CLERK.getCode(),
|
||||
OrderActor.CLERK,
|
||||
inProgress.getAcceptBy(),
|
||||
OrderTriggerSource.WX_CLERK);
|
||||
|
||||
lifecycleService.completeOrder(orderId, context);
|
||||
|
||||
verify(orderInfoMapper).updateById(argThat(entity ->
|
||||
orderId.equals(entity.getId())
|
||||
&& OrderStatus.COMPLETED.getCode().equals(entity.getOrderStatus())
|
||||
&& entity.getOrderEndTime() != null));
|
||||
verify(orderInfoMapper).update(isNull(), any());
|
||||
verify(customUserInfoService).handleOrderCompletion(completed);
|
||||
verify(earningsService).createFromOrder(completed);
|
||||
verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
|
||||
}
|
||||
@@ -96,17 +183,39 @@ class OrderLifecycleServiceImplTest {
|
||||
PlayOrderInfoEntity completedWithEnd = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completedWithEnd.setOrderEndTime(LocalDateTime.now());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(alreadyCompleted, completedWithEnd);
|
||||
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
mockEarningsCounts(1L);
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(alreadyCompleted, completedWithEnd, completedWithEnd);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
|
||||
lifecycleService.completeOrder(orderId, OrderCompletionContext.of(
|
||||
OperatorType.ADMIN.getCode(),
|
||||
OrderActor.ADMIN,
|
||||
"admin-1",
|
||||
OrderTriggerSource.ADMIN_CONSOLE));
|
||||
|
||||
verify(earningsService, never()).createFromOrder(any());
|
||||
verify(wxCustomMpService).sendOrderFinishMessageAsync(completedWithEnd);
|
||||
verify(wxCustomMpService, never()).sendOrderFinishMessageAsync(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeOrder_whenTransitionAlreadyApplied_skipsSideEffects() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity inProgress = buildOrder(orderId, OrderStatus.IN_PROGRESS.getCode());
|
||||
inProgress.setOrderEndTime(null);
|
||||
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setOrderEndTime(LocalDateTime.now());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(inProgress, completed, completed);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(0);
|
||||
|
||||
lifecycleService.completeOrder(orderId, OrderCompletionContext.of(
|
||||
OrderActor.CLERK,
|
||||
inProgress.getAcceptBy(),
|
||||
OrderTriggerSource.WX_CLERK));
|
||||
|
||||
verify(customUserInfoService, never()).handleOrderCompletion(any());
|
||||
verify(earningsService, never()).createFromOrder(any());
|
||||
verify(wxCustomMpService, never()).sendOrderFinishMessageAsync(any());
|
||||
verify(orderLogInfoMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,10 +228,10 @@ class OrderLifecycleServiceImplTest {
|
||||
order.setFinalAmount(finalAmount);
|
||||
order.setOrderMoney(finalAmount);
|
||||
order.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
order.setPayMethod("1");
|
||||
order.setPayMethod(PayMethod.WECHAT.getCode());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(order);
|
||||
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(order, order);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
|
||||
PlayCustomUserInfoEntity customer = new PlayCustomUserInfoEntity();
|
||||
customer.setId(order.getPurchaserBy());
|
||||
@@ -139,12 +248,7 @@ class OrderLifecycleServiceImplTest {
|
||||
|
||||
lifecycleService.refundOrder(context);
|
||||
|
||||
ArgumentCaptor<PlayOrderInfoEntity> updateCaptor = ArgumentCaptor.forClass(PlayOrderInfoEntity.class);
|
||||
verify(orderInfoMapper).updateById(updateCaptor.capture());
|
||||
PlayOrderInfoEntity updated = updateCaptor.getValue();
|
||||
assertEquals(OrderStatus.CANCELLED.getCode(), updated.getOrderStatus());
|
||||
assertEquals(OrderRefundFlag.REFUNDED.getCode(), updated.getRefundType());
|
||||
assertEquals(refundAmount, updated.getRefundAmount());
|
||||
verify(orderInfoMapper).update(isNull(), any());
|
||||
|
||||
verify(customUserInfoService).updateAccountBalanceById(
|
||||
eq(order.getPurchaserBy()),
|
||||
@@ -186,10 +290,35 @@ class OrderLifecycleServiceImplTest {
|
||||
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
|
||||
|
||||
assertThrows(CustomException.class, () -> lifecycleService.refundOrder(context));
|
||||
verify(orderInfoMapper, never()).updateById(any());
|
||||
verify(orderInfoMapper, never()).update(isNull(), any());
|
||||
verify(orderRefundInfoService, never()).add(anyString(), anyString(), anyString(), anyString(), anyString(), any(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refundOrder_duplicateRequest_isIdempotent() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity order = buildOrder(orderId, OrderStatus.ACCEPTED.getCode());
|
||||
order.setFinalAmount(BigDecimal.TEN);
|
||||
order.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
|
||||
PlayOrderInfoEntity afterUpdate = buildOrder(orderId, OrderStatus.CANCELLED.getCode());
|
||||
afterUpdate.setRefundType(OrderRefundFlag.REFUNDED.getCode());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(order, afterUpdate);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(0);
|
||||
|
||||
OrderRefundContext context = new OrderRefundContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setRefundAmount(BigDecimal.ONE);
|
||||
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
|
||||
|
||||
lifecycleService.refundOrder(context);
|
||||
|
||||
verify(customUserInfoService, never()).updateAccountBalanceById(any(), any(), any(), any(), any(), any(), any(), any());
|
||||
verify(orderRefundInfoService, never()).add(anyString(), anyString(), anyString(), anyString(), anyString(), any(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
verify(orderLogInfoMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
private PlayOrderInfoEntity buildOrder(String orderId, String status) {
|
||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||
entity.setId(orderId);
|
||||
|
||||
Reference in New Issue
Block a user