Compare commits

..

2 Commits

Author SHA1 Message Date
irving
67692ff79f clean up
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
2025-10-28 23:39:50 -04:00
irving
7db9318a7b feat: 完善订单生命周期幂等与日志追踪 2025-10-28 23:24:33 -04:00
24 changed files with 1129 additions and 1066 deletions

View File

@@ -2,6 +2,10 @@ package com.starry.admin.modules.custom.mapper;
import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; 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接口 * 顾客Mapper接口
@@ -11,4 +15,18 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
*/ */
public interface PlayCustomUserInfoMapper extends MPJBaseMapper<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);
} }

View File

@@ -3,6 +3,7 @@ package com.starry.admin.modules.custom.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity; import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity;
import com.starry.admin.modules.system.module.entity.SysTenantEntity; import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
/** /**
@@ -100,4 +101,13 @@ public interface IPlayCustomLevelInfoService extends IService<PlayCustomLevelInf
* 删除最大等级 * 删除最大等级
*/ */
void delMaxLevelByLevel(Integer level); void delMaxLevelByLevel(Integer level);
/**
* 根据累计消费金额匹配顾客等级。
*
* @param tenantId 租户ID
* @param totalConsumption 累计消费金额
* @return 匹配到的等级,未匹配则返回 {@code null}
*/
PlayCustomLevelInfoEntity matchLevelByConsumption(String tenantId, BigDecimal totalConsumption);
} }

View File

@@ -172,4 +172,11 @@ public interface IPlayCustomUserInfoService extends IService<PlayCustomUserInfoE
* @author admin * @author admin
**/ **/
void saveOrderInfo(PlayOrderInfoEntity entity); void saveOrderInfo(PlayOrderInfoEntity entity);
/**
* 处理订单完成后的顾客统计更新。
*
* @param entity 完成的订单实体
*/
void handleOrderCompletion(PlayOrderInfoEntity entity);
} }

View File

@@ -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.custom.service.IPlayCustomLevelInfoService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity; import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.common.utils.IdUtils; import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -153,4 +155,39 @@ public class PlayCustomLevelInfoServiceImpl extends ServiceImpl<PlayCustomLevelI
queryWrapper.eq(PlayCustomLevelInfoEntity::getLevel, level); queryWrapper.eq(PlayCustomLevelInfoEntity::getLevel, level);
this.baseMapper.delete(queryWrapper); 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;
}
}
} }

View File

@@ -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.PlayCustomRankingReturnVo;
import com.starry.admin.modules.custom.module.vo.PlayCustomUserQueryVo; import com.starry.admin.modules.custom.module.vo.PlayCustomUserQueryVo;
import com.starry.admin.modules.custom.module.vo.PlayCustomUserReturnVo; 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.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl; 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 com.starry.common.utils.IdUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import javax.annotation.Resource;
@@ -49,6 +52,9 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
@Resource @Resource
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService; private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@Resource
private IPlayCustomLevelInfoService playCustomLevelInfoService;
@Override @Override
public PlayCustomUserInfoEntity selectByOpenid(String openId) { public PlayCustomUserInfoEntity selectByOpenid(String openId) {
LambdaQueryWrapper<PlayCustomUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PlayCustomUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
@@ -369,6 +375,50 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
return baseMapper.selectList(wrapper); 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 @Override
public void saveOrderInfo(PlayOrderInfoEntity entity) { public void saveOrderInfo(PlayOrderInfoEntity entity) {
String id = entity.getPurchaserBy(); String id = entity.getPurchaserBy();

View File

@@ -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> {
}

View File

@@ -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_NO = "0";
public static final String EXCLUDE_HISTORY_YES = "1"; 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 @Getter
public enum OrderTriggerSource { public enum OrderTriggerSource {

View File

@@ -1,5 +1,6 @@
package com.starry.admin.modules.order.module.dto; 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 com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import java.util.Objects; import java.util.Objects;
import lombok.Data; import lombok.Data;
@@ -10,13 +11,11 @@ import org.springframework.lang.Nullable;
*/ */
@Data @Data
public class OrderCompletionContext { public class OrderCompletionContext {
/**
* 操作人类型0:顾客;1:店员;2:管理员),可为空用于系统任务。
*/
@Nullable
private String operatorType;
/** 操作人ID可为空用于系统任务。 */ /** 操作人类型,系统任务使用 SYSTEM。 */
private OrderActor operatorActor = OrderActor.SYSTEM;
/** 操作人IDSYSTEM 时允许为空。 */
@Nullable @Nullable
private String operatorId; private String operatorId;
@@ -30,26 +29,29 @@ public class OrderCompletionContext {
/** 是否强制发送完成通知。 */ /** 是否强制发送完成通知。 */
private boolean forceNotify; 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"); 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(); OrderCompletionContext context = new OrderCompletionContext();
context.setOperatorType(operatorType); context.setOperatorActor(actor);
context.setOperatorId(operatorId); context.setOperatorId(operatorId);
context.setTriggerSource(triggerSource); context.setTriggerSource(triggerSource);
return context; return context;
} }
public static OrderCompletionContext of(@Nullable String operatorType, @Nullable String operatorId, OrderTriggerSource triggerSource, @Nullable String comment) { public static OrderCompletionContext of(OrderActor actor, @Nullable String operatorId, OrderTriggerSource triggerSource, @Nullable String comment) {
OrderCompletionContext context = of(operatorType, operatorId, triggerSource); return of(actor, operatorId, triggerSource).withComment(comment);
return context.withComment(comment);
} }
public static OrderCompletionContext scheduler(@Nullable String 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) { 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) { public OrderCompletionContext withForceNotify(boolean forceNotify) {

View File

@@ -1,127 +1,77 @@
package com.starry.admin.modules.order.module.dto; 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;
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.Valid;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import org.springframework.lang.Nullable;
/** /**
* 订单创建请求对象 - 使用Builder模式替换20+参数的方法 * 订单创建上下文用于聚合下单所需的全部信息
*
* @author admin
*/ */
@Data @Data
@Builder @Builder
public class OrderCreationRequest { public class OrderCreationContext {
/**
* 订单ID
*/
@NotBlank(message = "订单ID不能为空") @NotBlank(message = "订单ID不能为空")
private String orderId; private String orderId;
/**
* 订单编号
*/
@NotBlank(message = "订单编号不能为空") @NotBlank(message = "订单编号不能为空")
private String orderNo; private String orderNo;
/**
* 订单状态
*/
@NotNull(message = "订单状态不能为空") @NotNull(message = "订单状态不能为空")
private OrderConstant.OrderStatus orderStatus; private OrderConstant.OrderStatus orderStatus;
/**
* 订单类型
*/
@NotNull(message = "订单类型不能为空") @NotNull(message = "订单类型不能为空")
private OrderConstant.OrderType orderType; private OrderConstant.OrderType orderType;
/**
* 下单类型
*/
@NotNull(message = "下单类型不能为空") @NotNull(message = "下单类型不能为空")
private OrderConstant.PlaceType placeType; private OrderConstant.PlaceType placeType;
/** private OrderConstant.RewardType rewardType;
* 打赏类型0:余额;1:礼物
*/
private RewardType rewardType;
/**
* 是否是首单
*/
private boolean isFirstOrder; private boolean isFirstOrder;
/**
* 商品信息
*/
@Valid @Valid
@NotNull(message = "商品信息不能为空") @NotNull(message = "商品信息不能为空")
private CommodityInfo commodityInfo; private CommodityInfo commodityInfo;
/**
* 支付信息
*/
@Valid @Valid
@NotNull(message = "支付信息不能为空") @NotNull(message = "支付信息不能为空")
private PaymentInfo paymentInfo; private PaymentInfo paymentInfo;
/**
* 下单人
*/
@NotBlank(message = "下单人不能为空") @NotBlank(message = "下单人不能为空")
private String purchaserBy; private String purchaserBy;
/**
* 接单人可选
*/
private String acceptBy; private String acceptBy;
/**
* 微信号码
*/
private String weiChatCode; private String weiChatCode;
/**
* 订单备注
*/
private String remark; private String remark;
/**
* 随机单要求仅随机单时需要
*/
private RandomOrderRequirements randomOrderRequirements; private RandomOrderRequirements randomOrderRequirements;
/** @Builder.Default
* 获取首单标识字符串兼容现有系统 private OrderActor creatorActor = OrderActor.SYSTEM;
*/
@Nullable
private String creatorId;
public String getFirstOrderString() { public String getFirstOrderString() {
return isFirstOrder ? "1" : "0"; return isFirstOrder ? "1" : "0";
} }
/**
* 验证随机单要求
*/
public boolean isValidForRandomOrder() { public boolean isValidForRandomOrder() {
return placeType == OrderConstant.PlaceType.RANDOM return placeType == OrderConstant.PlaceType.RANDOM && randomOrderRequirements != null;
&& randomOrderRequirements != null;
} }
/**
* 是否为打赏单
*/
public boolean isRewardOrder() { public boolean isRewardOrder() {
return placeType == OrderConstant.PlaceType.REWARD; return placeType == OrderConstant.PlaceType.REWARD;
} }
/**
* 是否为指定单
*/
public boolean isSpecifiedOrder() { public boolean isSpecifiedOrder() {
return placeType == OrderConstant.PlaceType.SPECIFIED; return placeType == OrderConstant.PlaceType.SPECIFIED;
} }

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.starry.admin.common.conf.StringTypeHandler; 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 com.starry.common.domain.BaseEntity;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -329,4 +331,31 @@ public class PlayOrderInfoEntity extends BaseEntity<PlayOrderInfoEntity> {
public void setOrderEndTime(LocalDateTime orderEndTime) { public void setOrderEndTime(LocalDateTime orderEndTime) {
this.orderEndTime = 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;
}
} }

View File

@@ -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;
}

View File

@@ -1,10 +1,14 @@
package com.starry.admin.modules.order.service; package com.starry.admin.modules.order.service;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext; import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderRefundContext; import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
public interface IOrderLifecycleService { public interface IOrderLifecycleService {
PlayOrderInfoEntity initiateOrder(OrderCreationContext context);
void completeOrder(String orderId, OrderCompletionContext context); void completeOrder(String orderId, OrderCompletionContext context);
void refundOrder(OrderRefundContext context); void refundOrder(OrderRefundContext context);

View File

@@ -45,46 +45,6 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
* @author admin * @author admin
* @since 2024/6/3 10:53 * @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);
/** /**
* 据店员等级和订单金额,获取店员预计收入 * 据店员等级和订单金额,获取店员预计收入
* *

View File

@@ -1,29 +1,51 @@
package com.starry.admin.modules.order.service.impl; package com.starry.admin.modules.order.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil; 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.common.exception.CustomException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper; import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType; import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType; import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag; 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.OrderRefundRecordType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState; 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.OrderStatus;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource; 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.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.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.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.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.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService; import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.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.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService; import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils; import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import javax.annotation.Resource; import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -33,6 +55,14 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class OrderLifecycleServiceImpl implements IOrderLifecycleService { public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
private static final LifecycleToken LIFECYCLE_TOKEN = new LifecycleToken();
private enum LifecycleOperation {
CREATE,
COMPLETE,
REFUND
}
@Resource @Resource
private PlayOrderInfoMapper orderInfoMapper; private PlayOrderInfoMapper orderInfoMapper;
@@ -48,6 +78,49 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
@Resource @Resource
private IPlayCustomUserInfoService customUserInfoService; 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void completeOrder(String orderId, OrderCompletionContext context) { public void completeOrder(String orderId, OrderCompletionContext context) {
@@ -71,17 +144,34 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
LocalDateTime endTime = order.getOrderEndTime() != null ? order.getOrderEndTime() : now; LocalDateTime endTime = order.getOrderEndTime() != null ? order.getOrderEndTime() : now;
boolean statusUpdated = false; boolean transitioned = false;
if (!alreadyCompleted) { if (!alreadyCompleted) {
PlayOrderInfoEntity update = new PlayOrderInfoEntity(orderId, OrderStatus.COMPLETED.getCode()); UpdateWrapper<PlayOrderInfoEntity> transitionWrapper = new UpdateWrapper<>();
update.setOrderEndTime(endTime); transitionWrapper.eq("id", orderId)
orderInfoMapper.updateById(update); .eq("order_status", OrderStatus.IN_PROGRESS.getCode())
statusUpdated = true; .set("order_status", OrderStatus.COMPLETED.getCode())
} else if (order.getOrderEndTime() == null) { .set("order_end_time", endTime);
PlayOrderInfoEntity update = new PlayOrderInfoEntity(orderId, OrderStatus.COMPLETED.getCode()); transitioned = orderInfoMapper.update(null, transitionWrapper) > 0;
update.setOrderEndTime(endTime); if (!transitioned) {
orderInfoMapper.updateById(update); PlayOrderInfoEntity refreshed = orderInfoMapper.selectById(orderId);
statusUpdated = true; 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); PlayOrderInfoEntity latest = orderInfoMapper.selectById(orderId);
@@ -89,9 +179,34 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
throw new CustomException("订单不存在"); throw new CustomException("订单不存在");
} }
boolean earningsCreated = ensureEarnings(latest, source);
boolean forceNotify = context != null && context.isForceNotify(); 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) { if (shouldNotify) {
wxCustomMpService.sendOrderFinishMessageAsync(latest); wxCustomMpService.sendOrderFinishMessageAsync(latest);
} }
@@ -128,12 +243,22 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
throw new CustomException("每个订单只能退款一次~"); throw new CustomException("每个订单只能退款一次~");
} }
PlayOrderInfoEntity update = new PlayOrderInfoEntity(); UpdateWrapper<PlayOrderInfoEntity> refundUpdate = new UpdateWrapper<>();
update.setId(order.getId()); refundUpdate.eq("id", order.getId())
update.setRefundType(OrderRefundFlag.REFUNDED.getCode()); .eq("refund_type", OrderRefundFlag.NOT_REFUNDED.getCode())
update.setOrderStatus(OrderStatus.CANCELLED.getCode()); .set("refund_type", OrderRefundFlag.REFUNDED.getCode())
update.setRefundAmount(refundAmount); .set("refund_amount", refundAmount)
orderInfoMapper.updateById(update); .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()); PlayCustomUserInfoEntity customUser = customUserInfoService.getById(order.getPurchaserBy());
if (customUser == null) { if (customUser == null) {
@@ -171,6 +296,132 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
refundById, refundById,
OrderRefundState.PROCESSING.getCode(), OrderRefundState.PROCESSING.getCode(),
ReviewRequirement.NOT_REQUIRED.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) { private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) {
@@ -192,4 +443,77 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
} }
return true; 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;
}
}
} }

View File

@@ -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.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.utils.SecurityUtils; import com.starry.admin.utils.SecurityUtils;
import com.starry.common.enums.EvaluateHiddenState;
import com.starry.common.utils.IdUtils; import com.starry.common.utils.IdUtils;
import com.starry.common.utils.StringUtils; import com.starry.common.utils.StringUtils;
import java.util.Arrays; import java.util.Arrays;

View File

@@ -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;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType; import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType; import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag; 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.OrderRefundRecordType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState; 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.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService; import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService; import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo; import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService; import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
@@ -102,6 +104,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Resource @Resource
private IOrderLifecycleService orderLifecycleService; private IOrderLifecycleService orderLifecycleService;
@Resource
private ClerkRevenueCalculator clerkRevenueCalculator;
@Override @Override
public List<PlayOrderInfoEntity> getTotalOrderInfo(String tenantId) { public List<PlayOrderInfoEntity> getTotalOrderInfo(String tenantId) {
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>(); MPJLambdaWrapper<PlayOrderInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>();
@@ -109,306 +114,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return this.baseMapper.selectList(lambdaWrapper); 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 @Override
public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType, public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
String firstOrder, BigDecimal finalAmount) { String firstOrder, BigDecimal finalAmount) {
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId); return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, finalAmount);
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;
} }
/** /**
@@ -425,79 +134,57 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Override @Override
public BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal finalAmount) { public BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal finalAmount) {
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId); PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
switch (placeType) { boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
case "0": { try {
if ("1".equals(firstOrder)) { OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
return finalAmount.multiply(new BigDecimal(entity.getFirstRegularRatio()) switch (place) {
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP); case SPECIFIED:
} else { return calculateRevenue(finalAmount,
return finalAmount.multiply(new BigDecimal(entity.getNotFirstRegularRatio()) isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio());
.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP); case RANDOM:
} return calculateRevenue(finalAmount,
} isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio());
case "1": { case REWARD:
if ("1".equals(firstOrder)) { return calculateRevenue(finalAmount,
return finalAmount.multiply(new BigDecimal(entity.getFirstRandomRadio()).divide(new BigDecimal(100), isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio());
4, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP); case OTHER:
} else { default:
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); log.error("下单类型异常placeType={}", placeType);
return finalAmount; return finalAmount;
} }
default: { } catch (IllegalArgumentException ex) {
log.error("下单类型错误placeType={}", placeType); log.error("下单类型错误placeType={}", placeType, ex);
return finalAmount; return finalAmount;
} }
} }
}
@Override @Override
public Integer getEstimatedRevenueRatio(String clerkId, String placeType, String firstOrder) { public Integer getEstimatedRevenueRatio(String clerkId, String placeType, String firstOrder) {
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId); PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
switch (placeType) { boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
case "0": { try {
if ("1".equals(firstOrder)) { OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
return entity.getFirstRegularRatio(); switch (place) {
} else { case SPECIFIED:
return entity.getNotFirstRegularRatio(); return isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio();
} case RANDOM:
} return isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio();
case "1": { case REWARD:
if ("1".equals(firstOrder)) { return isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio();
return entity.getFirstRandomRadio(); case OTHER:
} else { default:
return entity.getNotFirstRandomRadio();
}
}
case "2": {
if ("1".equals(firstOrder)) {
return entity.getFirstRewardRatio();
} else {
return entity.getNotFirstRewardRatio();
}
}
case "-1": {
log.error("下单类型异常placeType={}", placeType); log.error("下单类型异常placeType={}", placeType);
return 100; return 100;
} }
default: { } catch (IllegalArgumentException ex) {
log.error("下单类型错误placeType={}", placeType); log.error("下单类型错误placeType={}", placeType, ex);
return 100; 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 @Override
public Boolean checkFirstOrderFlag(String customId, String clerkId) { public Boolean checkFirstOrderFlag(String customId, String clerkId) {
// 检查是否是首单 LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class).eq(PlayOrderInfoEntity::getPurchaserBy, customId).eq(PlayOrderInfoEntity::getAcceptBy, clerkId).eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode()); .eq(PlayOrderInfoEntity::getPurchaserBy, customId)
return this.baseMapper.selectCount(wrapper) > 0; .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())) { if (isClerkOperator && OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
validateClerkQualificationForRandomOrder(orderInfo, clerkUserInfoEntity, acceptBy); validateClerkQualificationForRandomOrder(orderInfo, clerkUserInfoEntity, acceptBy);
} }
String firstOrderFlag = resolveFirstOrderFlag(orderInfo.getPurchaserBy(), acceptBy);
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderStatus.ACCEPTED.getCode()); PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderStatus.ACCEPTED.getCode());
LocalDateTime acceptTime = LocalDateTime.now(); LocalDateTime acceptTime = LocalDateTime.now();
entity.setAcceptBy(acceptBy); entity.setAcceptBy(acceptBy);
entity.setAcceptTime(acceptTime); entity.setAcceptTime(acceptTime);
entity.setFirstOrder(firstOrderFlag);
ClerkEstimatedRevenueVo estimatedRevenueVo = this.getClerkEstimatedRevenue( ClerkEstimatedRevenueVo estimatedRevenueVo = this.getClerkEstimatedRevenue(
acceptBy, acceptBy,
orderInfo.getCouponIds(), orderInfo.getCouponIds(),
orderInfo.getPlaceType(), orderInfo.getPlaceType(),
orderInfo.getFirstOrder(), firstOrderFlag,
orderInfo.getFinalAmount()); orderInfo.getFinalAmount());
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount(); BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
entity.setEstimatedRevenue(revenueAmount); entity.setEstimatedRevenue(revenueAmount);
@@ -926,6 +618,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
orderInfo.setOrderStatus(OrderStatus.ACCEPTED.getCode()); orderInfo.setOrderStatus(OrderStatus.ACCEPTED.getCode());
orderInfo.setEstimatedRevenue(revenueAmount); orderInfo.setEstimatedRevenue(revenueAmount);
orderInfo.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio()); orderInfo.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
orderInfo.setFirstOrder(firstOrderFlag);
log.info("Order accepted successfully. orderId={}, orderNo={}, acceptBy={}, operatorByType={}", log.info("Order accepted successfully. orderId={}, orderNo={}, acceptBy={}, operatorByType={}",
orderId, orderInfo.getOrderNo(), acceptBy, operatorByType); orderId, orderInfo.getOrderNo(), acceptBy, operatorByType);
wxCustomMpService.sendOrderMessageAsync(orderInfo); 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); log.error("订单状态异常,不能完成订单,orderId={},orderStace={}", orderId, orderState);
throw new CustomException("订单状态异常,不能开始订单"); throw new CustomException("订单状态异常,不能开始订单");
} }
OrderActor actor = resolveCompletionActor(operatorByType);
orderLifecycleService.completeOrder( orderLifecycleService.completeOrder(
orderId, orderId,
OrderCompletionContext.of( OrderCompletionContext.of(
operatorByType, actor,
operatorBy, actor == OrderActor.SYSTEM ? null : operatorBy,
resolveCompletionSource(operatorByType), resolveCompletionSource(actor),
"manual")); "manual"));
} else { } else {
log.error("修改订单状态异常orderId={},orderStace={}", orderId, orderState); log.error("修改订单状态异常orderId={},orderStace={}", orderId, orderState);
@@ -1182,13 +889,33 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return orderInfoMapper.deleteById(id); return orderInfoMapper.deleteById(id);
} }
private OrderTriggerSource resolveCompletionSource(String operatorType) { private OrderActor resolveCompletionActor(String operatorType) {
if (operatorType == null) { if (operatorType == null) {
return OrderTriggerSource.UNKNOWN; return OrderActor.SYSTEM;
} }
try { try {
OperatorType type = OperatorType.fromCode(operatorType); OperatorType type = OperatorType.fromCode(operatorType);
switch (type) { 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;
}
}
private OrderTriggerSource resolveCompletionSource(OrderActor actor) {
if (actor == null) {
return OrderTriggerSource.UNKNOWN;
}
switch (actor) {
case CUSTOMER: case CUSTOMER:
return OrderTriggerSource.WX_CUSTOMER; return OrderTriggerSource.WX_CUSTOMER;
case CLERK: case CLERK:
@@ -1196,11 +923,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
case ADMIN: case ADMIN:
return OrderTriggerSource.ADMIN_CONSOLE; return OrderTriggerSource.ADMIN_CONSOLE;
default: default:
return OrderTriggerSource.UNKNOWN; return OrderTriggerSource.SYSTEM;
}
} catch (IllegalArgumentException ex) {
log.warn("Unknown operator type {}, fallback to unknown", operatorType, ex);
return OrderTriggerSource.UNKNOWN;
} }
} }
} }

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.IPlayCustomLeaveMsgService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; 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;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType; import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
import com.starry.admin.modules.order.module.dto.CommodityInfo; import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.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.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements; import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; 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.IPlayOrderComplaintInfoService;
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService; import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService; import com.starry.admin.modules.order.service.IPlayOrderInfoService;
@@ -103,6 +105,9 @@ public class WxCustomController {
@Resource @Resource
private IPlayOrderInfoService playOrderInfoService; private IPlayOrderInfoService playOrderInfoService;
@Resource
private IOrderLifecycleService orderLifecycleService;
@Resource @Resource
private IPlayCustomLeaveMsgService playCustomLeaveMsgService; private IPlayCustomLeaveMsgService playCustomLeaveMsgService;
@@ -231,14 +236,16 @@ public class WxCustomController {
} }
String orderId = IdUtils.getUuid(); String orderId = IdUtils.getUuid();
// 记录订单信息 // 记录订单信息
OrderCreationRequest orderRequest = OrderCreationRequest.builder() OrderCreationContext orderRequest = OrderCreationContext.builder()
.orderId(orderId) .orderId(orderId)
.orderNo(playOrderInfoService.getOrderNo()) .orderNo(playOrderInfoService.getOrderNo())
.orderStatus(OrderConstant.OrderStatus.COMPLETED) .orderStatus(OrderConstant.OrderStatus.COMPLETED)
.orderType(OrderConstant.OrderType.NORMAL) .orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD) .placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.BALANCE) .rewardType(RewardType.BALANCE)
.isFirstOrder(true) .isFirstOrder(false)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(userId)
.commodityInfo(CommodityInfo.builder() .commodityInfo(CommodityInfo.builder()
.commodityId("") .commodityId("")
.commodityType(OrderConstant.CommodityType.GIFT) .commodityType(OrderConstant.CommodityType.GIFT)
@@ -258,7 +265,7 @@ public class WxCustomController {
.weiChatCode(vo.getWeiChatCode()) .weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark()) .remark(vo.getRemark())
.build(); .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); customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(new BigDecimal(vo.getMoney())), "1", "打赏", new BigDecimal(vo.getMoney()), BigDecimal.ZERO, orderId);
return R.ok("成功"); return R.ok("成功");
@@ -325,7 +332,7 @@ public class WxCustomController {
String orderId = IdUtils.getUuid(); String orderId = IdUtils.getUuid();
String orderNo = playOrderInfoService.getOrderNo(); String orderNo = playOrderInfoService.getOrderNo();
// 记录订单信息 // 记录订单信息
OrderCreationRequest orderRequest = OrderCreationRequest.builder() OrderCreationContext orderRequest = OrderCreationContext.builder()
.orderId(orderId) .orderId(orderId)
.orderNo(orderNo) .orderNo(orderNo)
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -333,6 +340,8 @@ public class WxCustomController {
.placeType(OrderConstant.PlaceType.SPECIFIED) .placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(RewardType.NOT_APPLICABLE) .rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true) .isFirstOrder(true)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(customId)
.commodityInfo(CommodityInfo.builder() .commodityInfo(CommodityInfo.builder()
.commodityId(commodityInfo.getCommodityId()) .commodityId(commodityInfo.getCommodityId())
.commodityType(OrderConstant.CommodityType.SERVICE) .commodityType(OrderConstant.CommodityType.SERVICE)
@@ -352,7 +361,7 @@ public class WxCustomController {
.weiChatCode(vo.getWeiChatCode()) .weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark()) .remark(vo.getRemark())
.build(); .build();
playOrderInfoService.createOrderInfo(orderRequest); orderLifecycleService.initiateOrder(orderRequest);
// 顾客减少余额 // 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-指定单", money, BigDecimal.ZERO, orderId); 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 orderId = IdUtils.getUuid();
String orderNo = playOrderInfoService.getOrderNo(); String orderNo = playOrderInfoService.getOrderNo();
// 记录订单信息 // 记录订单信息
OrderCreationRequest orderRequest = OrderCreationRequest.builder() OrderCreationContext orderRequest = OrderCreationContext.builder()
.orderId(orderId) .orderId(orderId)
.orderNo(orderNo) .orderNo(orderNo)
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -388,6 +397,8 @@ public class WxCustomController {
.placeType(OrderConstant.PlaceType.RANDOM) .placeType(OrderConstant.PlaceType.RANDOM)
.rewardType(RewardType.NOT_APPLICABLE) .rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true) .isFirstOrder(true)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(customId)
.commodityInfo(CommodityInfo.builder() .commodityInfo(CommodityInfo.builder()
.commodityId(commodityInfo.getCommodityId()) .commodityId(commodityInfo.getCommodityId())
.commodityType(OrderConstant.CommodityType.SERVICE) .commodityType(OrderConstant.CommodityType.SERVICE)
@@ -411,7 +422,7 @@ public class WxCustomController {
.excludeHistory(vo.getExcludeHistory()) .excludeHistory(vo.getExcludeHistory())
.build()) .build())
.build(); .build();
playOrderInfoService.createOrderInfo(orderRequest); orderLifecycleService.initiateOrder(orderRequest);
// 顾客减少余额 // 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-随机单", money, BigDecimal.ZERO, orderId); customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-随机单", money, BigDecimal.ZERO, orderId);
// 给全部店员发送通知 // 给全部店员发送通知

View File

@@ -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.IPlayCustomGiftInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; 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;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor;
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType; import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
import com.starry.admin.modules.order.module.dto.CommodityInfo; import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.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.PaymentInfo;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService; import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity; import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService; import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
@@ -36,6 +38,9 @@ public class WxGiftOrderService {
@Resource @Resource
private IPlayOrderInfoService playOrderInfoService; private IPlayOrderInfoService playOrderInfoService;
@Resource
private IOrderLifecycleService orderLifecycleService;
@Resource @Resource
private IPlayCustomUserInfoService customUserInfoService; private IPlayCustomUserInfoService customUserInfoService;
@@ -81,9 +86,9 @@ public class WxGiftOrderService {
} }
String orderId = IdUtils.getUuid(); String orderId = IdUtils.getUuid();
OrderCreationRequest orderRequest = buildOrderCreationRequest(orderId, request, sessionUser.getId(), OrderCreationContext orderRequest = buildOrderCreationContext(orderId, request, sessionUser.getId(),
giftInfo, totalAmount); giftInfo, totalAmount);
playOrderInfoService.createOrderInfo(orderRequest); orderLifecycleService.initiateOrder(orderRequest);
BigDecimal newBalance = currentBalance.subtract(totalAmount); BigDecimal newBalance = currentBalance.subtract(totalAmount);
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), currentBalance, newBalance, "1", customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), currentBalance, newBalance, "1",
@@ -97,10 +102,10 @@ public class WxGiftOrderService {
return orderId; return orderId;
} }
private OrderCreationRequest buildOrderCreationRequest(String orderId, PlayOrderInfoGiftAdd request, private OrderCreationContext buildOrderCreationContext(String orderId, PlayOrderInfoGiftAdd request,
String purchaserId, PlayGiftInfoEntity giftInfo, String purchaserId, PlayGiftInfoEntity giftInfo,
BigDecimal totalAmount) { BigDecimal totalAmount) {
return OrderCreationRequest.builder() return OrderCreationContext.builder()
.orderId(orderId) .orderId(orderId)
.orderNo(playOrderInfoService.getOrderNo()) .orderNo(playOrderInfoService.getOrderNo())
.orderStatus(OrderConstant.OrderStatus.COMPLETED) .orderStatus(OrderConstant.OrderStatus.COMPLETED)
@@ -108,6 +113,8 @@ public class WxGiftOrderService {
.placeType(OrderConstant.PlaceType.REWARD) .placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.GIFT) .rewardType(RewardType.GIFT)
.isFirstOrder(true) .isFirstOrder(true)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(purchaserId)
.commodityInfo(CommodityInfo.builder() .commodityInfo(CommodityInfo.builder()
.commodityId(giftInfo.getId()) .commodityId(giftInfo.getId())
.commodityType(OrderConstant.CommodityType.GIFT) .commodityType(OrderConstant.CommodityType.GIFT)

View File

@@ -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';

View File

@@ -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.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.CommodityInfo; import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.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.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements; import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test;
* *
* @author admin * @author admin
*/ */
class OrderCreationRequestTest { class OrderCreationContextTest {
@Test @Test
@DisplayName("测试Builder模式构建订单请求") @DisplayName("测试Builder模式构建订单请求")
@@ -42,7 +42,7 @@ class OrderCreationRequestTest {
.build(); .build();
// 构建订单请求 // 构建订单请求
OrderCreationRequest request = OrderCreationRequest.builder() OrderCreationContext request = OrderCreationContext.builder()
.orderId("order_123456") .orderId("order_123456")
.orderNo("ORD20240906001") .orderNo("ORD20240906001")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -82,7 +82,7 @@ class OrderCreationRequestTest {
@DisplayName("测试订单类型判断方法") @DisplayName("测试订单类型判断方法")
void testOrderTypeChecks() { void testOrderTypeChecks() {
// 测试指定单 // 测试指定单
OrderCreationRequest specifiedOrder = OrderCreationRequest.builder() OrderCreationContext specifiedOrder = OrderCreationContext.builder()
.orderId("order_001") .orderId("order_001")
.orderNo("ORD001") .orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -99,7 +99,7 @@ class OrderCreationRequestTest {
assertFalse(specifiedOrder.isRewardOrder()); assertFalse(specifiedOrder.isRewardOrder());
// 测试随机单 // 测试随机单
OrderCreationRequest randomOrder = OrderCreationRequest.builder() OrderCreationContext randomOrder = OrderCreationContext.builder()
.orderId("order_002") .orderId("order_002")
.orderNo("ORD002") .orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -121,7 +121,7 @@ class OrderCreationRequestTest {
assertFalse(randomOrder.isRewardOrder()); assertFalse(randomOrder.isRewardOrder());
// 测试打赏单 // 测试打赏单
OrderCreationRequest rewardOrder = OrderCreationRequest.builder() OrderCreationContext rewardOrder = OrderCreationContext.builder()
.orderId("order_003") .orderId("order_003")
.orderNo("ORD003") .orderNo("ORD003")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -142,7 +142,7 @@ class OrderCreationRequestTest {
@DisplayName("测试首单标识转换") @DisplayName("测试首单标识转换")
void testFirstOrderStringConversion() { void testFirstOrderStringConversion() {
// 测试首单 // 测试首单
OrderCreationRequest firstOrder = OrderCreationRequest.builder() OrderCreationContext firstOrder = OrderCreationContext.builder()
.orderId("order_001") .orderId("order_001")
.orderNo("ORD001") .orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -157,7 +157,7 @@ class OrderCreationRequestTest {
assertEquals("1", firstOrder.getFirstOrderString()); assertEquals("1", firstOrder.getFirstOrderString());
// 测试非首单 // 测试非首单
OrderCreationRequest notFirstOrder = OrderCreationRequest.builder() OrderCreationContext notFirstOrder = OrderCreationContext.builder()
.orderId("order_002") .orderId("order_002")
.orderNo("ORD002") .orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -176,7 +176,7 @@ class OrderCreationRequestTest {
@DisplayName("测试随机单验证逻辑") @DisplayName("测试随机单验证逻辑")
void testRandomOrderValidation() { void testRandomOrderValidation() {
// 有效的随机单 // 有效的随机单
OrderCreationRequest validRandomOrder = OrderCreationRequest.builder() OrderCreationContext validRandomOrder = OrderCreationContext.builder()
.orderId("order_001") .orderId("order_001")
.orderNo("ORD001") .orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)
@@ -194,7 +194,7 @@ class OrderCreationRequestTest {
assertTrue(validRandomOrder.isValidForRandomOrder()); assertTrue(validRandomOrder.isValidForRandomOrder());
// 无效的随机单缺少要求信息 // 无效的随机单缺少要求信息
OrderCreationRequest invalidRandomOrder = OrderCreationRequest.builder() OrderCreationContext invalidRandomOrder = OrderCreationContext.builder()
.orderId("order_002") .orderId("order_002")
.orderNo("ORD002") .orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING) .orderStatus(OrderConstant.OrderStatus.PENDING)

View File

@@ -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));
}
}

View File

@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; 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.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper; import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType; 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.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.OrderRefundFlag;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType; 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.OrderRefundState;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus; 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.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.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.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.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.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.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.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService; import com.starry.admin.modules.withdraw.service.IEarningsService;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID; import java.util.UUID;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
@@ -59,6 +78,76 @@ class OrderLifecycleServiceImplTest {
@Mock @Mock
private IPlayCustomUserInfoService customUserInfoService; 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 @Test
void completeOrder_inProgress_createsEarningsAndNotifies() { void completeOrder_inProgress_createsEarningsAndNotifies() {
String orderId = UUID.randomUUID().toString(); String orderId = UUID.randomUUID().toString();
@@ -69,20 +158,18 @@ class OrderLifecycleServiceImplTest {
completed.setOrderEndTime(LocalDateTime.now()); completed.setOrderEndTime(LocalDateTime.now());
when(orderInfoMapper.selectById(orderId)).thenReturn(inProgress, completed); 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); mockEarningsCounts(0L, 1L);
OrderCompletionContext context = OrderCompletionContext.of( OrderCompletionContext context = OrderCompletionContext.of(
OperatorType.CLERK.getCode(), OrderActor.CLERK,
inProgress.getAcceptBy(), inProgress.getAcceptBy(),
OrderTriggerSource.WX_CLERK); OrderTriggerSource.WX_CLERK);
lifecycleService.completeOrder(orderId, context); lifecycleService.completeOrder(orderId, context);
verify(orderInfoMapper).updateById(argThat(entity -> verify(orderInfoMapper).update(isNull(), any());
orderId.equals(entity.getId()) verify(customUserInfoService).handleOrderCompletion(completed);
&& OrderStatus.COMPLETED.getCode().equals(entity.getOrderStatus())
&& entity.getOrderEndTime() != null));
verify(earningsService).createFromOrder(completed); verify(earningsService).createFromOrder(completed);
verify(wxCustomMpService).sendOrderFinishMessageAsync(completed); verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
} }
@@ -96,17 +183,39 @@ class OrderLifecycleServiceImplTest {
PlayOrderInfoEntity completedWithEnd = buildOrder(orderId, OrderStatus.COMPLETED.getCode()); PlayOrderInfoEntity completedWithEnd = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
completedWithEnd.setOrderEndTime(LocalDateTime.now()); completedWithEnd.setOrderEndTime(LocalDateTime.now());
when(orderInfoMapper.selectById(orderId)).thenReturn(alreadyCompleted, completedWithEnd); when(orderInfoMapper.selectById(orderId)).thenReturn(alreadyCompleted, completedWithEnd, completedWithEnd);
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1); when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
mockEarningsCounts(1L);
lifecycleService.completeOrder(orderId, OrderCompletionContext.of( lifecycleService.completeOrder(orderId, OrderCompletionContext.of(
OperatorType.ADMIN.getCode(), OrderActor.ADMIN,
"admin-1", "admin-1",
OrderTriggerSource.ADMIN_CONSOLE)); OrderTriggerSource.ADMIN_CONSOLE));
verify(earningsService, never()).createFromOrder(any()); 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 @Test
@@ -119,10 +228,10 @@ class OrderLifecycleServiceImplTest {
order.setFinalAmount(finalAmount); order.setFinalAmount(finalAmount);
order.setOrderMoney(finalAmount); order.setOrderMoney(finalAmount);
order.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode()); order.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
order.setPayMethod("1"); order.setPayMethod(PayMethod.WECHAT.getCode());
when(orderInfoMapper.selectById(orderId)).thenReturn(order); when(orderInfoMapper.selectById(orderId)).thenReturn(order, order);
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1); when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
PlayCustomUserInfoEntity customer = new PlayCustomUserInfoEntity(); PlayCustomUserInfoEntity customer = new PlayCustomUserInfoEntity();
customer.setId(order.getPurchaserBy()); customer.setId(order.getPurchaserBy());
@@ -139,12 +248,7 @@ class OrderLifecycleServiceImplTest {
lifecycleService.refundOrder(context); lifecycleService.refundOrder(context);
ArgumentCaptor<PlayOrderInfoEntity> updateCaptor = ArgumentCaptor.forClass(PlayOrderInfoEntity.class); verify(orderInfoMapper).update(isNull(), any());
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(customUserInfoService).updateAccountBalanceById( verify(customUserInfoService).updateAccountBalanceById(
eq(order.getPurchaserBy()), eq(order.getPurchaserBy()),
@@ -186,10 +290,35 @@ class OrderLifecycleServiceImplTest {
context.withTriggerSource(OrderTriggerSource.ADMIN_API); context.withTriggerSource(OrderTriggerSource.ADMIN_API);
assertThrows(CustomException.class, () -> lifecycleService.refundOrder(context)); 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()); 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) { private PlayOrderInfoEntity buildOrder(String orderId, String status) {
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(); PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
entity.setId(orderId); entity.setId(orderId);