fix: correct order lifecycle refunds and add coverage
Some checks failed
Build and Push Backend / docker (push) Failing after 5s

This commit is contained in:
irving
2025-10-27 00:12:07 -04:00
parent f7461abc83
commit 6b2a1c2ba7
11 changed files with 816 additions and 130 deletions

View File

@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
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.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.vo.*;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
@@ -53,9 +54,7 @@ public class PlayOrderInfoController {
private IPlayCommodityInfoService playCommodityInfoService;
@Resource
private IPlayOrderRefundInfoService playOrderRefundInfoService;
@Resource
private IPlayCustomUserInfoService customUserInfoService;
private IOrderLifecycleService orderLifecycleService;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@@ -89,38 +88,14 @@ public class PlayOrderInfoController {
// @PreAuthorize("@customSs.hasPermission('order:order:update')")
@PostMapping("/orderRefund")
public R orderRefund(@ApiParam(value = "退款信息", required = true) @Validated @RequestBody PlayOrderRefundAddVo vo) {
PlayOrderInfoEntity orderInfo = orderInfoService.selectOrderInfoById(vo.getOrderId());
if (orderInfo.getFinalAmount().compareTo(vo.getRefundAmount()) < 0) {
throw new CustomException("退款金额不能大于支付金额");
}
if ("3".equals(orderInfo.getOrderStatus())) {
throw new CustomException("【已完成】的订单无法操作退款");
}
if ("4".equals(orderInfo.getOrderStatus())) {
throw new CustomException("【已取消】的订单无法操作退款");
}
if ("1".equals(orderInfo.getRefundType())) {
throw new CustomException("每个订单只能退款一次~");
}
PlayOrderInfoEntity updateOrderInfo = new PlayOrderInfoEntity();
updateOrderInfo.setId(orderInfo.getId());
updateOrderInfo.setRefundType("1");
// 订单退款,订单状态变为已取消
updateOrderInfo.setOrderStatus("4");
updateOrderInfo.setRefundAmount(vo.getRefundAmount());
// 修改订单状态
orderInfoService.update(updateOrderInfo);
// 记录退款信息
String refundType = orderInfo.getFinalAmount().compareTo(vo.getRefundAmount()) == 0 ? "0" : "1";
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.getById(orderInfo.getPurchaserBy());
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(),
customUserInfo.getAccountBalance().add(orderInfo.getOrderMoney()), "3", "订单退款",
orderInfo.getOrderMoney(), BigDecimal.ZERO, vo.getOrderId());
playOrderRefundInfoService.add(orderInfo.getId(), orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(),
orderInfo.getPayMethod(), refundType, vo.getRefundAmount(), vo.getRefundReason(), "2",
SecurityUtils.getUserId(), "0", "0");
OrderRefundContext context = new OrderRefundContext();
context.setOrderId(vo.getOrderId());
context.setRefundAmount(vo.getRefundAmount());
context.setRefundReason(vo.getRefundReason());
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setOperatorId(SecurityUtils.getUserId());
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
orderLifecycleService.refundOrder(context);
return R.ok("退款成功");
}

View File

@@ -8,9 +8,10 @@ import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.common.redis.RedisCache;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
@@ -18,7 +19,6 @@ import java.util.List;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -33,6 +33,8 @@ public class OrderJob {
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
private IOrderLifecycleService orderLifecycleService;
@Resource
public RedisTemplate<String, Object> redisTemplate;
@@ -60,11 +62,10 @@ public class OrderJob {
// 判断与开始时间相比较,如果大于服务时长,则修改订单状态为已完成
if (ca.getOrderStartTime().plusMinutes(serviceDuration).isBefore(LocalDateTime.now())) {
PlayOrderInfoEntity entity2 = new PlayOrderInfoEntity(ca.getId(), "3");
entity2.setOrderEndTime(LocalDateTime.now());
this.orderInfoMapper.updateById(entity2);
// 发送消息
wxCustomMpService.sendOrderFinishMessage(ca);
orderLifecycleService.completeOrder(
ca.getId(),
OrderCompletionContext.scheduler("auto finish by duration")
.withForceNotify(true));
}
} catch (Exception e) {

View File

@@ -211,15 +211,142 @@ public class OrderConstant {
public static final String EXCLUDE_HISTORY_NO = "0";
public static final String EXCLUDE_HISTORY_YES = "1";
// Legacy constants for backward compatibility - consider deprecating
@Deprecated
public final static String ORDER_STATUS_0 = "0";
@Deprecated
public final static String ORDER_STATUS_1 = "1";
@Deprecated
public final static String ORDER_STATUS_2 = "2";
@Deprecated
public final static String ORDER_STATUS_3 = "3";
@Deprecated
public final static String ORDER_STATUS_4 = "4";
@Getter
public enum OrderRefundFlag {
NOT_REFUNDED("0"),
REFUNDED("1");
private final String code;
OrderRefundFlag(String code) {
this.code = code;
}
public static OrderRefundFlag fromCode(String code) {
for (OrderRefundFlag flag : values()) {
if (flag.code.equals(code)) {
return flag;
}
}
throw new IllegalArgumentException("Unknown order refund flag code: " + code);
}
}
@Getter
public enum OrderRefundRecordType {
FULL("0"),
PARTIAL("1");
private final String code;
OrderRefundRecordType(String code) {
this.code = code;
}
public static OrderRefundRecordType fromCode(String code) {
for (OrderRefundRecordType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown refund record type code: " + code);
}
}
@Getter
public enum OrderRefundState {
PROCESSING("0"),
SUCCESS("1"),
CLOSED("2"),
ABNORMAL("-1");
private final String code;
OrderRefundState(String code) {
this.code = code;
}
public static OrderRefundState fromCode(String code) {
for (OrderRefundState state : values()) {
if (state.code.equals(code)) {
return state;
}
}
throw new IllegalArgumentException("Unknown refund state code: " + code);
}
}
@Getter
public enum ReviewRequirement {
NOT_REQUIRED("0"),
REQUIRED("1");
private final String code;
ReviewRequirement(String code) {
this.code = code;
}
public static ReviewRequirement fromCode(String code) {
for (ReviewRequirement requirement : values()) {
if (requirement.code.equals(code)) {
return requirement;
}
}
throw new IllegalArgumentException("Unknown review requirement code: " + code);
}
}
@Getter
public enum BalanceOperationType {
RECHARGE("0"),
CONSUME("1"),
SERVICE("2"),
REFUND("3");
private final String code;
BalanceOperationType(String code) {
this.code = code;
}
public static BalanceOperationType fromCode(String code) {
for (BalanceOperationType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown balance operation type code: " + code);
}
}
@Getter
public enum OrderTriggerSource {
UNKNOWN("unknown"),
MANUAL("manual"),
WX_CUSTOMER("wx_customer"),
WX_CLERK("wx_clerk"),
ADMIN_CONSOLE("admin_console"),
ADMIN_API("admin_api"),
REWARD_ORDER("reward_order"),
SCHEDULER("scheduler"),
SYSTEM("system");
private final String code;
OrderTriggerSource(String code) {
this.code = code;
}
public static OrderTriggerSource fromCode(String code) {
for (OrderTriggerSource source : values()) {
if (source.code.equals(code)) {
return source;
}
}
throw new IllegalArgumentException("Unknown order trigger source code: " + code);
}
}
}

View File

@@ -0,0 +1,69 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import java.util.Objects;
import lombok.Data;
import org.springframework.lang.Nullable;
/**
* 订单完成上下文信息,用于记录触发来源与操作人。
*/
@Data
public class OrderCompletionContext {
/**
* 操作人类型0:顾客;1:店员;2:管理员),可为空用于系统任务。
*/
@Nullable
private String operatorType;
/** 操作人ID可为空用于系统任务。 */
@Nullable
private String operatorId;
/** 触发来源描述,例如 wx_customer、scheduler、admin_panel。 */
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
/** 额外备注信息,可为空。 */
@Nullable
private String comment;
/** 是否强制发送完成通知。 */
private boolean forceNotify;
public static OrderCompletionContext of(@Nullable String operatorType, @Nullable String operatorId, OrderTriggerSource triggerSource) {
Objects.requireNonNull(triggerSource, "triggerSource cannot be null");
OrderCompletionContext context = new OrderCompletionContext();
context.setOperatorType(operatorType);
context.setOperatorId(operatorId);
context.setTriggerSource(triggerSource);
return context;
}
public static OrderCompletionContext of(@Nullable String operatorType, @Nullable String operatorId, OrderTriggerSource triggerSource, @Nullable String comment) {
OrderCompletionContext context = of(operatorType, operatorId, triggerSource);
return context.withComment(comment);
}
public static OrderCompletionContext scheduler(@Nullable String comment) {
return of(null, null, OrderTriggerSource.SCHEDULER, comment);
}
public static OrderCompletionContext system(OrderTriggerSource triggerSource, @Nullable String comment) {
return of(null, null, triggerSource, comment);
}
public OrderCompletionContext withForceNotify(boolean forceNotify) {
this.forceNotify = forceNotify;
return this;
}
public OrderCompletionContext withComment(@Nullable String comment) {
this.comment = comment;
return this;
}
public OrderCompletionContext withTriggerSource(OrderTriggerSource triggerSource) {
this.triggerSource = Objects.requireNonNull(triggerSource, "triggerSource");
return this;
}
}

View File

@@ -0,0 +1,44 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import java.math.BigDecimal;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.lang.Nullable;
@Data
public class OrderRefundContext {
@NotBlank
private String orderId;
@NotNull
private BigDecimal refundAmount;
@Nullable
private String refundReason;
@Nullable
private String operatorType;
@Nullable
private String operatorId;
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
private boolean fullRefund;
@Nullable
private String comment;
public OrderRefundContext withTriggerSource(OrderTriggerSource triggerSource) {
this.triggerSource = triggerSource;
return this;
}
public OrderRefundContext withComment(@Nullable String comment) {
this.comment = comment;
return this;
}
}

View File

@@ -313,4 +313,20 @@ public class PlayOrderInfoEntity extends BaseEntity<PlayOrderInfoEntity> {
this.orderType = orderType;
this.placeType = placeType;
}
/**
* 请使用 OrderCompletionService 统一处理订单完成逻辑,直接设置状态仅限初始化或遗留代码。
*/
@Deprecated
public void setOrderStatus(String orderStatus) {
this.orderStatus = orderStatus;
}
/**
* 请使用 OrderCompletionService 统一处理订单结束时间写入。
*/
@Deprecated
public void setOrderEndTime(LocalDateTime orderEndTime) {
this.orderEndTime = orderEndTime;
}
}

View File

@@ -0,0 +1,11 @@
package com.starry.admin.modules.order.service;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
public interface IOrderLifecycleService {
void completeOrder(String orderId, OrderCompletionContext context);
void refundOrder(OrderRefundContext context);
}

View File

@@ -0,0 +1,195 @@
package com.starry.admin.modules.order.service.impl;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import com.starry.admin.modules.order.module.constant.OrderConstant.ReviewRequirement;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
@Resource
private PlayOrderInfoMapper orderInfoMapper;
@Resource
private IEarningsService earningsService;
@Resource
private WxCustomMpService wxCustomMpService;
@Resource
private IPlayOrderRefundInfoService orderRefundInfoService;
@Resource
private IPlayCustomUserInfoService customUserInfoService;
@Override
@Transactional(rollbackFor = Exception.class)
public void completeOrder(String orderId, OrderCompletionContext context) {
if (StrUtil.isBlank(orderId)) {
throw new CustomException("订单ID不能为空");
}
PlayOrderInfoEntity order = orderInfoMapper.selectById(orderId);
if (order == null) {
throw new CustomException("订单不存在");
}
OrderTriggerSource source = context != null && context.getTriggerSource() != null
? context.getTriggerSource()
: OrderTriggerSource.UNKNOWN;
boolean alreadyCompleted = OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus());
if (!alreadyCompleted && !OrderStatus.IN_PROGRESS.getCode().equals(order.getOrderStatus())) {
log.warn("Skip completing order {}, unexpected status {}, source={}", orderId, order.getOrderStatus(), source.getCode());
throw new CustomException("订单状态异常,无法完成");
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime endTime = order.getOrderEndTime() != null ? order.getOrderEndTime() : now;
boolean statusUpdated = false;
if (!alreadyCompleted) {
PlayOrderInfoEntity update = new PlayOrderInfoEntity(orderId, OrderStatus.COMPLETED.getCode());
update.setOrderEndTime(endTime);
orderInfoMapper.updateById(update);
statusUpdated = true;
} else if (order.getOrderEndTime() == null) {
PlayOrderInfoEntity update = new PlayOrderInfoEntity(orderId, OrderStatus.COMPLETED.getCode());
update.setOrderEndTime(endTime);
orderInfoMapper.updateById(update);
statusUpdated = true;
}
PlayOrderInfoEntity latest = orderInfoMapper.selectById(orderId);
if (latest == null) {
throw new CustomException("订单不存在");
}
boolean earningsCreated = ensureEarnings(latest, source);
boolean forceNotify = context != null && context.isForceNotify();
boolean shouldNotify = statusUpdated || (forceNotify && earningsCreated);
if (shouldNotify) {
wxCustomMpService.sendOrderFinishMessageAsync(latest);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void refundOrder(OrderRefundContext context) {
if (context == null || StrUtil.isBlank(context.getOrderId())) {
throw new CustomException("订单ID不能为空");
}
PlayOrderInfoEntity order = orderInfoMapper.selectById(context.getOrderId());
if (order == null) {
throw new CustomException("订单不存在");
}
BigDecimal refundAmount = context.getRefundAmount();
if (refundAmount == null) {
throw new CustomException("退款金额不能为空");
}
if (refundAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new CustomException("退款金额不能小于0");
}
BigDecimal finalAmount = order.getFinalAmount() == null ? BigDecimal.ZERO : order.getFinalAmount();
if (finalAmount.compareTo(refundAmount) < 0) {
throw new CustomException("退款金额不能大于支付金额");
}
if (OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus())) {
throw new CustomException("【已完成】的订单无法操作退款");
}
if (OrderStatus.CANCELLED.getCode().equals(order.getOrderStatus())) {
throw new CustomException("【已取消】的订单无法操作退款");
}
if (OrderRefundFlag.REFUNDED.getCode().equals(order.getRefundType())) {
throw new CustomException("每个订单只能退款一次~");
}
PlayOrderInfoEntity update = new PlayOrderInfoEntity();
update.setId(order.getId());
update.setRefundType(OrderRefundFlag.REFUNDED.getCode());
update.setOrderStatus(OrderStatus.CANCELLED.getCode());
update.setRefundAmount(refundAmount);
orderInfoMapper.updateById(update);
PlayCustomUserInfoEntity customUser = customUserInfoService.getById(order.getPurchaserBy());
if (customUser == null) {
throw new CustomException("顾客信息不存在");
}
BigDecimal currentBalance = customUser.getAccountBalance() == null ? BigDecimal.ZERO : customUser.getAccountBalance();
BigDecimal refundSourceAmount = refundAmount;
customUserInfoService.updateAccountBalanceById(
customUser.getId(),
currentBalance,
currentBalance.add(refundSourceAmount),
BalanceOperationType.REFUND.getCode(),
"订单退款",
refundSourceAmount,
BigDecimal.ZERO,
order.getId());
OrderRefundRecordType refundRecordType = finalAmount.compareTo(refundAmount) == 0
? OrderRefundRecordType.FULL
: OrderRefundRecordType.PARTIAL;
String refundByType = StrUtil.isNotBlank(context.getOperatorType())
? context.getOperatorType()
: OperatorType.ADMIN.getCode();
String refundById = StrUtil.isNotBlank(context.getOperatorId()) ? context.getOperatorId() : SecurityUtils.getUserId();
orderRefundInfoService.add(
order.getId(),
order.getPurchaserBy(),
order.getAcceptBy(),
order.getPayMethod(),
refundRecordType.getCode(),
refundAmount,
context.getRefundReason(),
refundByType,
refundById,
OrderRefundState.PROCESSING.getCode(),
ReviewRequirement.NOT_REQUIRED.getCode());
}
private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) {
Long existing = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getTenantId, order.getTenantId())
.eq(EarningsLineEntity::getOrderId, order.getId())
.count();
if (existing != null && existing > 0) {
return false;
}
earningsService.createFromOrder(order);
Long after = earningsService.lambdaQuery()
.eq(EarningsLineEntity::getTenantId, order.getTenantId())
.eq(EarningsLineEntity::getOrderId, order.getId())
.count();
if (after == null || after == 0) {
log.warn("Failed to create earnings line for order {}, source={}", order.getId(), source.getCode());
return false;
}
return true;
}
}

View File

@@ -1,8 +1,5 @@
package com.starry.admin.modules.order.service.impl;
import static com.starry.admin.modules.order.module.constant.OrderConstant.ORDER_STATUS_2;
import static com.starry.admin.modules.order.module.constant.OrderConstant.ORDER_STATUS_3;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -20,12 +17,21 @@ 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.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import com.starry.admin.modules.order.module.constant.OrderConstant.ReviewRequirement;
import com.starry.admin.modules.order.module.dto.*;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderRefundInfoEntity;
import com.starry.admin.modules.order.module.vo.*;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
@@ -93,7 +99,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
private IPlayClerkLevelInfoService playClerkLevelInfoService;
@Resource
private IEarningsService earningsService;
private IOrderLifecycleService orderLifecycleService;
@Override
public List<PlayOrderInfoEntity> getTotalOrderInfo(String tenantId) {
@@ -117,7 +123,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
entity.setPlaceType(placeType);
entity.setRewardType(rewardType);
entity.setFirstOrder(firstOrder);
entity.setRefundType("0");
entity.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
entity.setCommodityId(commodityId);
entity.setCommodityType(commodityType);
entity.setCommodityPrice(commodityPrice);
@@ -162,9 +168,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
// 修改优惠券状态
playCouponDetailsService.updateCouponUseStateByIds(couponIds, "2");
if ("2".equals(placeType) && StrUtil.isNotBlank(acceptBy)) {
PlayOrderInfoEntity latest = this.selectOrderInfoById(orderId);
earningsService.createFromOrder(latest);
wxCustomMpService.sendOrderFinishMessageAsync(latest);
orderLifecycleService.completeOrder(
orderId,
OrderCompletionContext.of(null, null, OrderTriggerSource.REWARD_ORDER)
.withForceNotify(true)
.withComment("auto-complete reward order"));
}
}
@@ -204,9 +212,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
// 打赏单立即入账
if (request.isRewardOrder() && StrUtil.isNotBlank(request.getAcceptBy())) {
PlayOrderInfoEntity latest = this.selectOrderInfoById(entity.getId());
earningsService.createFromOrder(latest);
wxCustomMpService.sendOrderFinishMessageAsync(latest);
orderLifecycleService.completeOrder(
entity.getId(),
OrderCompletionContext.of(null, null, OrderTriggerSource.REWARD_ORDER)
.withForceNotify(true)
.withComment("auto-complete reward order"));
}
}
@@ -236,7 +246,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
entity.setFirstOrder(request.getFirstOrderString());
// 固定默认值
entity.setRefundType("0");
entity.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
entity.setBackendEntry("0");
entity.setPayMethod("0");
entity.setOrderSettlementState("0");
@@ -500,7 +510,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Override
public void createRechargeOrder(String orderId, BigDecimal orderMoney, BigDecimal finalAmount, String purchaserBy) {
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(IdUtils.getUuid(), ORDER_STATUS_3, "0", "-1");
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(IdUtils.getUuid(), OrderStatus.COMPLETED.getCode(), "0", "-1");
entity.setOrderNo(orderId);
entity.setId(IdUtils.getUuid());
entity.setOrderMoney(orderMoney);
@@ -538,7 +548,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
public List<PlayOrderInfoEntity> listByEndTime(String clerkId, LocalDateTime endTime) {
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getAcceptBy, clerkId);
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderStatus, "3");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
lambdaQueryWrapper.lt(PlayOrderInfoEntity::getOrderEndTime, endTime);
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderSettlementState, "0");
return this.baseMapper.selectList(lambdaQueryWrapper);
@@ -604,7 +614,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
PlayClerkUserInfoEntity entity = playClerkUserInfoService.getById(clerkId);
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getPlaceType, "1");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderStatus, "0");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.PENDING.getCode());
// lambdaQueryWrapper.eq(PlayOrderInfoEntity::getLevelId, entity.getLevelId());
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getSex, entity.getSex());
// lambdaQueryWrapper.eq(PlayOrderInfoEntity::getExcludeHistory, "0")
@@ -617,7 +627,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
public IPage<PlayRewardInfoReturnVo> selectRewardByPage(PlayRewardOrderQueryVo vo) {
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getPlaceType, "2");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getRefundType, "0");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getRefundType, OrderRefundFlag.NOT_REFUNDED.getCode());
lambdaQueryWrapper.selectAll(PlayOrderInfoEntity.class);
lambdaQueryWrapper.orderByDesc(PlayOrderInfoEntity::getPurchaserTime);
// 查询陪聊表
@@ -636,7 +646,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Override
public Boolean checkFirstOrderFlag(String customId, String clerkId) {
// 检查是否是首单
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class).eq(PlayOrderInfoEntity::getPurchaserBy, customId).eq(PlayOrderInfoEntity::getAcceptBy, clerkId).eq(PlayOrderInfoEntity::getOrderStatus, "3");
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class).eq(PlayOrderInfoEntity::getPurchaserBy, customId).eq(PlayOrderInfoEntity::getAcceptBy, clerkId).eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
return this.baseMapper.selectCount(wrapper) > 0;
}
@@ -677,7 +687,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
PlayOrderDetailsReturnVo vo = this.baseMapper.selectJoinOne(PlayOrderDetailsReturnVo.class, lambdaQueryWrapper);
// Privacy protection: Hide customer info for pending random orders
if (vo != null && OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType()) && OrderConstant.OrderStatus.PENDING.getCode().equals(vo.getOrderStatus())) {
if (vo != null && OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType()) && OrderStatus.PENDING.getCode().equals(vo.getOrderStatus())) {
vo.setCustomNickname("匿名用户");
vo.setCustomAvatar("");
vo.setCustomId("");
@@ -738,7 +748,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
PlayClerkOrderDetailsReturnVo returnVo = this.baseMapper.selectJoinOne(PlayClerkOrderDetailsReturnVo.class,
lambdaQueryWrapper);
// 如果订单状态为退款,查询订单退款原因
if (returnVo.getOrderStatus().equals(OrderConstant.ORDER_STATUS_4)) {
if (returnVo.getOrderStatus().equals(OrderStatus.CANCELLED.getCode())) {
PlayOrderRefundInfoEntity orderRefundInfoEntity = playOrderRefundInfoService
.selectPlayOrderRefundInfoByOrderId(returnVo.getId());
returnVo.setRefundByType(orderRefundInfoEntity.getRefundByType());
@@ -781,7 +791,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
PlayCustomOrderDetailsReturnVo returnVo = this.baseMapper.selectJoinOne(PlayCustomOrderDetailsReturnVo.class,
lambdaQueryWrapper);
// 如果订单状态为退款,查询订单退款原因
if (returnVo.getOrderStatus().equals(OrderConstant.ORDER_STATUS_4)) {
if (returnVo.getOrderStatus().equals(OrderStatus.CANCELLED.getCode())) {
PlayOrderRefundInfoEntity orderRefundInfoEntity = playOrderRefundInfoService
.selectPlayOrderRefundInfoByOrderId(returnVo.getId());
returnVo.setRefundByType(orderRefundInfoEntity.getRefundByType());
@@ -872,7 +882,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
// throw new CustomException("只有未接单的店员才可接单");
}
}
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderConstant.ORDER_STATUS_1);
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderStatus.ACCEPTED.getCode());
entity.setAcceptBy(acceptBy);
entity.setAcceptTime(LocalDateTime.now());
ClerkEstimatedRevenueVo estimatedRevenueVo = this.getClerkEstimatedRevenue(acceptBy, orderInfo.getCouponIds(),
@@ -944,11 +954,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Override
public void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId) {
// 开始订单只能店员或者管理员操作
if (orderState.equals(ORDER_STATUS_2) && "0".equals(operatorByType)) {
if (OrderStatus.IN_PROGRESS.getCode().equals(orderState) && "0".equals(operatorByType)) {
throw new CustomException("禁止操作");
}
// 完成订单只能顾客或者管理员操作
if (orderState.equals(ORDER_STATUS_3) && "1".equals(operatorByType)) {
if (OrderStatus.COMPLETED.getCode().equals(orderState) && "1".equals(operatorByType)) {
throw new CustomException("禁止操作");
}
PlayOrderInfoEntity entity = this.selectOrderInfoById(orderId);
@@ -960,36 +970,28 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
if ("1".equals(operatorByType) && !entity.getAcceptBy().equals(operatorBy)) {
throw new CustomException("只能操作本人订单");
}
switch (orderState) {
case ORDER_STATUS_2: {
// 开始订单前,订单状态必须为接单状态
if (!entity.getOrderStatus().equals(OrderConstant.ORDER_STATUS_1)) {
log.error("订单状态异常,不能开始接单,orderId={},orderStace={}", orderId, orderState);
throw new CustomException("订单状态异常,不能开始订单");
}
PlayOrderInfoEntity entity2 = new PlayOrderInfoEntity(orderId, ORDER_STATUS_2);
entity2.setOrderStartTime(LocalDateTime.now());
this.baseMapper.updateById(entity2);
break;
if (OrderStatus.IN_PROGRESS.getCode().equals(orderState)) {
if (!OrderStatus.ACCEPTED.getCode().equals(entity.getOrderStatus())) {
log.error("订单状态异常,不能开始接单,orderId={},orderStace={}", orderId, orderState);
throw new CustomException("订单状态异常,不能开始订单");
}
case ORDER_STATUS_3: {
// 完成订单前,订单状态必须为开始状态
if (!entity.getOrderStatus().equals(ORDER_STATUS_2)) {
log.error("订单状态异常,不能完成订单,orderId={},orderStace={}", orderId, orderState);
throw new CustomException("订单状态异常,不能开始订单");
}
// 完成订单
PlayOrderInfoEntity entity2 = new PlayOrderInfoEntity(orderId, ORDER_STATUS_3);
entity2.setOrderEndTime(LocalDateTime.now());
this.baseMapper.updateById(entity2);
PlayOrderInfoEntity latest = this.selectOrderInfoById(orderId);
wxCustomMpService.sendOrderFinishMessageAsync(latest);
earningsService.createFromOrder(latest);
break;
}
default: {
log.error("修改订单状态异常orderId={},orderStace={}", orderId, orderState);
PlayOrderInfoEntity entity2 = new PlayOrderInfoEntity(orderId, OrderStatus.IN_PROGRESS.getCode());
entity2.setOrderStartTime(LocalDateTime.now());
this.baseMapper.updateById(entity2);
} else if (OrderStatus.COMPLETED.getCode().equals(orderState)) {
if (!OrderStatus.IN_PROGRESS.getCode().equals(entity.getOrderStatus())) {
log.error("订单状态异常,不能完成订单,orderId={},orderStace={}", orderId, orderState);
throw new CustomException("订单状态异常,不能开始订单");
}
orderLifecycleService.completeOrder(
orderId,
OrderCompletionContext.of(
operatorByType,
operatorBy,
resolveCompletionSource(operatorByType),
"manual"));
} else {
log.error("修改订单状态异常orderId={},orderStace={}", orderId, orderState);
}
}
@@ -1013,21 +1015,21 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
throw new CustomException("只能操作本人订单");
}
// 取消订单(必须订单未接单或者为开始状态)
if (!orderInfo.getOrderStatus().equals(OrderConstant.ORDER_STATUS_0)
&& !orderInfo.getOrderStatus().equals(OrderConstant.ORDER_STATUS_1)) {
if (!orderInfo.getOrderStatus().equals(OrderStatus.PENDING.getCode())
&& !orderInfo.getOrderStatus().equals(OrderStatus.ACCEPTED.getCode())) {
throw new CustomException("订单状态异常,无法取消");
}
// 修改订单状态
this.baseMapper.updateById(new PlayOrderInfoEntity(orderId, OrderConstant.ORDER_STATUS_4));
this.baseMapper.updateById(new PlayOrderInfoEntity(orderId, OrderStatus.CANCELLED.getCode()));
// 用户增加余额
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.getById(orderInfo.getPurchaserBy());
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(),
customUserInfo.getAccountBalance().add(orderInfo.getOrderMoney()), "3", "订单退款",
customUserInfo.getAccountBalance().add(orderInfo.getOrderMoney()), BalanceOperationType.REFUND.getCode(), "订单退款",
orderInfo.getOrderMoney(), BigDecimal.ZERO, orderId);
// 取消订单后,记录退款信息
playOrderRefundInfoService.add(orderId, orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(),
orderInfo.getPayMethod(), "0", orderInfo.getFinalAmount(), refundReason, operatorByType, operatorBy,
"0", "0");
playOrderRefundInfoService.add(orderInfo.getId(), orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(),
orderInfo.getPayMethod(), OrderRefundRecordType.FULL.getCode(), orderInfo.getFinalAmount(), refundReason, operatorByType, operatorBy,
OrderRefundState.PROCESSING.getCode(), ReviewRequirement.NOT_REQUIRED.getCode());
wxCustomMpService.sendOrderCancelMessageAsync(orderInfo, refundReason);
}
@@ -1042,8 +1044,8 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
throw new CustomException("禁止操作");
}
PlayOrderInfoEntity orderInfo = this.selectOrderInfoById(orderId);
if (!OrderConstant.ORDER_STATUS_1.equals(orderInfo.getOrderStatus())
&& !OrderConstant.ORDER_STATUS_2.equals(orderInfo.getOrderStatus())) {
if (!OrderStatus.ACCEPTED.getCode().equals(orderInfo.getOrderStatus())
&& !OrderStatus.IN_PROGRESS.getCode().equals(orderInfo.getOrderStatus())) {
throw new CustomException("订单状态异常,无法取消");
}
BigDecimal actualRefundAmount = refundAmount != null ? refundAmount : orderInfo.getFinalAmount();
@@ -1055,12 +1057,12 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
}
LocalDateTime now = LocalDateTime.now();
PlayOrderInfoEntity updateEntity = new PlayOrderInfoEntity(orderId, OrderConstant.ORDER_STATUS_4);
PlayOrderInfoEntity updateEntity = new PlayOrderInfoEntity(orderId, OrderStatus.CANCELLED.getCode());
updateEntity.setRefundAmount(actualRefundAmount);
updateEntity.setRefundReason(refundReason);
updateEntity.setRefundType("1");
updateEntity.setRefundType(OrderRefundFlag.REFUNDED.getCode());
updateEntity.setOrderCancelTime(now);
if (OrderConstant.ORDER_STATUS_2.equals(orderInfo.getOrderStatus())) {
if (OrderStatus.IN_PROGRESS.getCode().equals(orderInfo.getOrderStatus())) {
updateEntity.setOrderEndTime(now);
}
this.baseMapper.updateById(updateEntity);
@@ -1071,11 +1073,15 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
}
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(),
customUserInfo.getAccountBalance().add(actualRefundAmount), "3", "订单取消退款",
customUserInfo.getAccountBalance().add(actualRefundAmount), BalanceOperationType.REFUND.getCode(), "订单取消退款",
actualRefundAmount, BigDecimal.ZERO, orderId);
OrderRefundRecordType forceCancelRefundType = actualRefundAmount.compareTo(orderInfo.getFinalAmount()) == 0
? OrderRefundRecordType.FULL
: OrderRefundRecordType.PARTIAL;
playOrderRefundInfoService.add(orderId, orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(),
orderInfo.getPayMethod(), "0", actualRefundAmount, refundReason, operatorByType, operatorBy, "0", "0");
orderInfo.getPayMethod(), forceCancelRefundType.getCode(), actualRefundAmount, refundReason, operatorByType, operatorBy,
OrderRefundState.PROCESSING.getCode(), ReviewRequirement.NOT_REQUIRED.getCode());
PlayOrderInfoEntity latest = this.selectOrderInfoById(orderId);
wxCustomMpService.sendOrderCancelMessageAsync(latest, refundReason);
@@ -1121,4 +1127,25 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return orderInfoMapper.deleteById(id);
}
private OrderTriggerSource resolveCompletionSource(String operatorType) {
if (operatorType == null) {
return OrderTriggerSource.UNKNOWN;
}
try {
OperatorType type = OperatorType.fromCode(operatorType);
switch (type) {
case CUSTOMER:
return OrderTriggerSource.WX_CUSTOMER;
case CLERK:
return OrderTriggerSource.WX_CLERK;
case ADMIN:
return OrderTriggerSource.ADMIN_CONSOLE;
default:
return OrderTriggerSource.UNKNOWN;
}
} catch (IllegalArgumentException ex) {
log.warn("Unknown operator type {}, fallback to unknown", operatorType, ex);
return OrderTriggerSource.UNKNOWN;
}
}
}