Compare commits
6 Commits
7b6943d391
...
49867a30dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49867a30dd | ||
|
|
51c4a5438d | ||
|
|
e616dd6a13 | ||
|
|
ed0edf584a | ||
|
|
b9250566fb | ||
|
|
984e33bd94 |
18
backup-dev-db.sh
Executable file
18
backup-dev-db.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_HOST="primary"
|
||||||
|
DB_PORT="3306"
|
||||||
|
DB_NAME="play-with"
|
||||||
|
DB_USER="root"
|
||||||
|
DB_PASSWORD="123456"
|
||||||
|
|
||||||
|
stamp="$(date +%F)"
|
||||||
|
backup_dir="yunpei/backup/dev/${stamp}"
|
||||||
|
mkdir -p "${backup_dir}"
|
||||||
|
|
||||||
|
echo "[backup] dumping ${DB_NAME} from ${DB_HOST}:${DB_PORT} -> ${backup_dir}/dev.sql.gz"
|
||||||
|
mysqldump -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
|
||||||
|
| gzip > "${backup_dir}/dev.sql.gz"
|
||||||
|
|
||||||
|
echo "[backup] done"
|
||||||
@@ -7,6 +7,7 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
|||||||
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.OrderTriggerSource;
|
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.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
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.*;
|
import com.starry.admin.modules.order.module.vo.*;
|
||||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||||
@@ -106,6 +107,26 @@ public class PlayOrderInfoController {
|
|||||||
return R.ok("退款成功");
|
return R.ok("退款成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "撤销已完成订单", notes = "管理员操作撤销,支持可选退款与收益处理")
|
||||||
|
@PostMapping("/revokeCompleted")
|
||||||
|
public R revokeCompleted(@Validated @RequestBody PlayOrderRevocationVo vo) {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(vo.getOrderId());
|
||||||
|
context.setRefundToCustomer(vo.isRefundToCustomer());
|
||||||
|
context.setRefundAmount(vo.getRefundAmount());
|
||||||
|
context.setRefundReason(vo.getRefundReason());
|
||||||
|
OrderRevocationContext.EarningsAdjustStrategy strategy = vo.getEarningsStrategy() != null
|
||||||
|
? vo.getEarningsStrategy()
|
||||||
|
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
|
||||||
|
context.setEarningsStrategy(strategy);
|
||||||
|
context.setCounterClerkId(vo.getCounterClerkId());
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setOperatorId(SecurityUtils.getUserId());
|
||||||
|
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
|
||||||
|
orderLifecycleService.revokeCompletedOrder(context);
|
||||||
|
return R.ok("撤销成功");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理后台强制取消进行中订单
|
* 管理后台强制取消进行中订单
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.starry.admin.modules.order.listener;
|
||||||
|
|
||||||
|
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.module.constant.OrderConstant.BalanceOperationType;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class OrderRevocationBalanceListener {
|
||||||
|
|
||||||
|
private final IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
|
||||||
|
public OrderRevocationBalanceListener(IPlayCustomUserInfoService customUserInfoService) {
|
||||||
|
this.customUserInfoService = customUserInfoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
|
||||||
|
public void handle(OrderRevocationEvent event) {
|
||||||
|
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.getContext().isRefundToCustomer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal refundAmount = Optional.ofNullable(event.getContext().getRefundAmount()).orElse(BigDecimal.ZERO);
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||||
|
PlayCustomUserInfoEntity customer = customUserInfoService.getById(order.getPurchaserBy());
|
||||||
|
if (customer == null) {
|
||||||
|
throw new CustomException("顾客信息不存在");
|
||||||
|
}
|
||||||
|
BigDecimal currentBalance = Optional.ofNullable(customer.getAccountBalance()).orElse(BigDecimal.ZERO);
|
||||||
|
customUserInfoService.updateAccountBalanceById(
|
||||||
|
customer.getId(),
|
||||||
|
currentBalance,
|
||||||
|
currentBalance.add(refundAmount),
|
||||||
|
BalanceOperationType.REFUND.getCode(),
|
||||||
|
"已完成订单撤销退款",
|
||||||
|
refundAmount,
|
||||||
|
BigDecimal.ZERO,
|
||||||
|
order.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.starry.admin.modules.order.listener;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class OrderRevocationEarningsListener {
|
||||||
|
|
||||||
|
private final IEarningsService earningsService;
|
||||||
|
|
||||||
|
public OrderRevocationEarningsListener(IEarningsService earningsService) {
|
||||||
|
this.earningsService = earningsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
|
||||||
|
public void handle(OrderRevocationEvent event) {
|
||||||
|
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OrderRevocationContext context = event.getContext();
|
||||||
|
switch (context.getEarningsStrategy()) {
|
||||||
|
case NONE:
|
||||||
|
return;
|
||||||
|
case REVERSE_CLERK:
|
||||||
|
earningsService.reverseByOrder(event.getOrderSnapshot().getId(), context.getOperatorId());
|
||||||
|
return;
|
||||||
|
case COUNTER_TO_PEIPEI:
|
||||||
|
createCounterLine(event);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new CustomException("未知的收益处理策略");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createCounterLine(OrderRevocationEvent event) {
|
||||||
|
OrderRevocationContext context = event.getContext();
|
||||||
|
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||||
|
String targetClerkId = context.getCounterClerkId();
|
||||||
|
if (targetClerkId == null) {
|
||||||
|
throw new CustomException("需要指定收益冲销目标账号");
|
||||||
|
}
|
||||||
|
BigDecimal amount = context.getRefundAmount();
|
||||||
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
amount = Optional.ofNullable(order.getEstimatedRevenue()).orElse(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
earningsService.createCounterLine(order.getId(), order.getTenantId(), targetClerkId, amount, context.getOperatorId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ public class OrderConstant {
|
|||||||
ACCEPTED("1", "已接单(待开始)"),
|
ACCEPTED("1", "已接单(待开始)"),
|
||||||
IN_PROGRESS("2", "已开始(服务中)"),
|
IN_PROGRESS("2", "已开始(服务中)"),
|
||||||
COMPLETED("3", "已完成"),
|
COMPLETED("3", "已完成"),
|
||||||
CANCELLED("4", "已取消");
|
CANCELLED("4", "已取消"),
|
||||||
|
REVOKED("5", "已撤销");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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 lombok.Data;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class OrderRevocationContext {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String operatorId;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String operatorType;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private BigDecimal refundAmount;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String refundReason;
|
||||||
|
|
||||||
|
private boolean refundToCustomer;
|
||||||
|
|
||||||
|
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String counterClerkId;
|
||||||
|
|
||||||
|
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
|
||||||
|
|
||||||
|
public OrderRevocationContext withTriggerSource(OrderTriggerSource triggerSource) {
|
||||||
|
this.triggerSource = triggerSource;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum EarningsAdjustStrategy {
|
||||||
|
NONE("NO_ADJUST"),
|
||||||
|
REVERSE_CLERK("REV_CLERK"),
|
||||||
|
COUNTER_TO_PEIPEI("CTR_PEIPEI");
|
||||||
|
|
||||||
|
private final String logCode;
|
||||||
|
|
||||||
|
EarningsAdjustStrategy(String logCode) {
|
||||||
|
this.logCode = logCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogCode() {
|
||||||
|
return logCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.starry.admin.modules.order.module.event;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class OrderRevocationEvent {
|
||||||
|
|
||||||
|
private final OrderRevocationContext context;
|
||||||
|
private final PlayOrderInfoEntity orderSnapshot;
|
||||||
|
|
||||||
|
public OrderRevocationEvent(OrderRevocationContext context, PlayOrderInfoEntity orderSnapshot) {
|
||||||
|
this.context = context;
|
||||||
|
this.orderSnapshot = orderSnapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.starry.admin.modules.order.module.vo;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "订单撤销参数", description = "撤销已完成订单的请求参数")
|
||||||
|
public class PlayOrderRevocationVo {
|
||||||
|
|
||||||
|
@NotBlank(message = "订单ID不能为空")
|
||||||
|
@ApiModelProperty(value = "订单ID", required = true)
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否退还顾客余额")
|
||||||
|
private boolean refundToCustomer;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "退款金额,未填写则默认订单实付金额")
|
||||||
|
private BigDecimal refundAmount;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "撤销原因")
|
||||||
|
private String refundReason;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "收益处理策略:NONE/REVERSE_CLERK/COUNTER_TO_PEIPEI")
|
||||||
|
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "收益冲销目标账号ID,策略为 COUNTER_TO_PEIPEI 时必填")
|
||||||
|
private String counterClerkId;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
|||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
|
||||||
public interface IOrderLifecycleService {
|
public interface IOrderLifecycleService {
|
||||||
@@ -14,4 +15,6 @@ public interface IOrderLifecycleService {
|
|||||||
void completeOrder(String orderId, OrderCompletionContext context);
|
void completeOrder(String orderId, OrderCompletionContext context);
|
||||||
|
|
||||||
void refundOrder(OrderRefundContext context);
|
void refundOrder(OrderRefundContext context);
|
||||||
|
|
||||||
|
void revokeCompletedOrder(OrderRevocationContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
|
|||||||
*/
|
*/
|
||||||
List<PlayOrderInfoEntity> customSelectOrderInfoByList(String customId);
|
List<PlayOrderInfoEntity> customSelectOrderInfoByList(String customId);
|
||||||
|
|
||||||
|
void revokeCompletedOrder(OrderRevocationContext context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改订单状态为接单 只有管理员或者店员本人才能操作
|
* 修改订单状态为接单 只有管理员或者店员本人才能操作
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
|||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
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.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.entity.PlayOrderLogInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
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;
|
||||||
@@ -61,9 +63,11 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -77,7 +81,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
private enum LifecycleOperation {
|
private enum LifecycleOperation {
|
||||||
CREATE,
|
CREATE,
|
||||||
COMPLETE,
|
COMPLETE,
|
||||||
REFUND
|
REFUND,
|
||||||
|
REVOKE_COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -110,6 +115,9 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
private Map<StrategyKey, OrderPlacementStrategy> placementStrategies;
|
private Map<StrategyKey, OrderPlacementStrategy> placementStrategies;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@@ -520,7 +528,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
throw new CustomException("每个订单只能退款一次~");
|
throw new CustomException("每个订单只能退款一次~");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBalancePaidOrder(order) && !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
if (isBalancePaidOrder(order)
|
||||||
|
&& !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||||
throw new CustomException("订单未发生余额扣款,无法退款");
|
throw new CustomException("订单未发生余额扣款,无法退款");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,6 +612,107 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void revokeCompletedOrder(OrderRevocationContext context) {
|
||||||
|
if (context == null || StrUtil.isBlank(context.getOrderId())) {
|
||||||
|
throw new CustomException("订单ID不能为空");
|
||||||
|
}
|
||||||
|
PlayOrderInfoEntity order = orderInfoMapper.selectById(context.getOrderId());
|
||||||
|
if (order == null) {
|
||||||
|
throw new CustomException("订单不存在");
|
||||||
|
}
|
||||||
|
if (OrderStatus.REVOKED.getCode().equals(order.getOrderStatus())) {
|
||||||
|
throw new CustomException("订单已撤销");
|
||||||
|
}
|
||||||
|
if (!OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus())) {
|
||||||
|
throw new CustomException("当前状态无法撤销");
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderRevocationContext.EarningsAdjustStrategy strategy = context.getEarningsStrategy() != null
|
||||||
|
? context.getEarningsStrategy()
|
||||||
|
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
|
||||||
|
if (strategy != OrderRevocationContext.EarningsAdjustStrategy.NONE && earningsService.hasLockedLines(order.getId())) {
|
||||||
|
throw new CustomException("收益已提现或处理中,无法撤销");
|
||||||
|
}
|
||||||
|
|
||||||
|
String operatorType = StrUtil.isNotBlank(context.getOperatorType()) ? context.getOperatorType() : OperatorType.ADMIN.getCode();
|
||||||
|
context.setOperatorType(operatorType);
|
||||||
|
String operatorId = StrUtil.isNotBlank(context.getOperatorId()) ? context.getOperatorId() : SecurityUtils.getUserId();
|
||||||
|
context.setOperatorId(operatorId);
|
||||||
|
|
||||||
|
BigDecimal finalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
|
||||||
|
BigDecimal refundAmount = context.getRefundAmount();
|
||||||
|
if (refundAmount == null) {
|
||||||
|
refundAmount = context.isRefundToCustomer() ? finalAmount : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
throw new CustomException("退款金额不能小于0");
|
||||||
|
}
|
||||||
|
if (refundAmount.compareTo(finalAmount) > 0) {
|
||||||
|
throw new CustomException("退款金额不能大于支付金额");
|
||||||
|
}
|
||||||
|
context.setRefundAmount(refundAmount);
|
||||||
|
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) > 0 && context.isRefundToCustomer()) {
|
||||||
|
if (isBalancePaidOrder(order)
|
||||||
|
&& !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||||
|
throw new CustomException("订单未发生余额扣款,无法退款");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWrapper<PlayOrderInfoEntity> updateWrapper = new UpdateWrapper<>();
|
||||||
|
updateWrapper.eq("id", order.getId())
|
||||||
|
.eq("order_status", OrderStatus.COMPLETED.getCode())
|
||||||
|
.set("order_status", OrderStatus.REVOKED.getCode())
|
||||||
|
.set("order_cancel_time", LocalDateTime.now())
|
||||||
|
.set("refund_amount", refundAmount)
|
||||||
|
.set("refund_reason", context.getRefundReason());
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
updateWrapper.set("refund_type", OrderRefundFlag.REFUNDED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updated = orderInfoMapper.update(null, updateWrapper) > 0;
|
||||||
|
PlayOrderInfoEntity latest = orderInfoMapper.selectById(order.getId());
|
||||||
|
if (!updated && (latest == null || !OrderStatus.REVOKED.getCode().equals(latest.getOrderStatus()))) {
|
||||||
|
throw new CustomException("订单状态已变化,无法撤销");
|
||||||
|
}
|
||||||
|
if (latest == null) {
|
||||||
|
latest = order;
|
||||||
|
latest.setOrderStatus(OrderStatus.REVOKED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) > 0 && context.isRefundToCustomer()) {
|
||||||
|
OrderRefundRecordType recordType = finalAmount.compareTo(refundAmount) == 0
|
||||||
|
? OrderRefundRecordType.FULL
|
||||||
|
: OrderRefundRecordType.PARTIAL;
|
||||||
|
orderRefundInfoService.add(
|
||||||
|
latest.getId(),
|
||||||
|
latest.getPurchaserBy(),
|
||||||
|
latest.getAcceptBy(),
|
||||||
|
latest.getPayMethod(),
|
||||||
|
recordType.getCode(),
|
||||||
|
refundAmount,
|
||||||
|
context.getRefundReason(),
|
||||||
|
context.getOperatorType(),
|
||||||
|
context.getOperatorId(),
|
||||||
|
OrderRefundState.PROCESSING.getCode(),
|
||||||
|
ReviewRequirement.NOT_REQUIRED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderActor actor = resolveCompletionActor(context.getOperatorType());
|
||||||
|
String operationType = String.format(
|
||||||
|
"%s_%s",
|
||||||
|
LifecycleOperation.REVOKE_COMPLETED.name(),
|
||||||
|
strategy != null
|
||||||
|
? strategy.getLogCode()
|
||||||
|
: OrderRevocationContext.EarningsAdjustStrategy.NONE.getLogCode());
|
||||||
|
recordOrderLog(latest, actor, context.getOperatorId(), LifecycleOperation.REVOKE_COMPLETED,
|
||||||
|
context.getRefundReason(), operationType);
|
||||||
|
|
||||||
|
applicationEventPublisher.publishEvent(new OrderRevocationEvent(context, latest));
|
||||||
|
}
|
||||||
|
|
||||||
private void validateOrderCreationRequest(OrderCreationContext context) {
|
private void validateOrderCreationRequest(OrderCreationContext context) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw new CustomException("订单创建请求不能为空");
|
throw new CustomException("订单创建请求不能为空");
|
||||||
|
|||||||
@@ -547,6 +547,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
public IPage<PlayCustomOrderListReturnVo> customSelectOrderInfoByPage(PlayCustomOrderInfoQueryVo vo) {
|
public IPage<PlayCustomOrderListReturnVo> customSelectOrderInfoByPage(PlayCustomOrderInfoQueryVo vo) {
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
||||||
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class));
|
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class));
|
||||||
|
if (StringUtils.isBlank(vo.getOrderType())) {
|
||||||
|
lambdaQueryWrapper.notIn(PlayOrderInfoEntity::getOrderType,
|
||||||
|
OrderConstant.OrderType.RECHARGE.getCode(),
|
||||||
|
OrderConstant.OrderType.WITHDRAWAL.getCode());
|
||||||
|
}
|
||||||
IPage<PlayCustomOrderListReturnVo> page = this.baseMapper.selectJoinPage(
|
IPage<PlayCustomOrderListReturnVo> page = this.baseMapper.selectJoinPage(
|
||||||
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayCustomOrderListReturnVo.class, lambdaQueryWrapper);
|
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayCustomOrderListReturnVo.class, lambdaQueryWrapper);
|
||||||
// 获取当前顾客所有订单评价信息,将订单评价信息转化为 map<订单ID,订单ID>的结构
|
// 获取当前顾客所有订单评价信息,将订单评价信息转化为 map<订单ID,订单ID>的结构
|
||||||
@@ -943,6 +948,14 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
notificationSender.sendOrderCancelMessageAsync(latest, refundReason);
|
notificationSender.sendOrderCancelMessageAsync(latest, refundReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void revokeCompletedOrder(OrderRevocationContext context) {
|
||||||
|
if (context == null || StrUtil.isBlank(context.getOrderId())) {
|
||||||
|
throw new CustomException("订单信息缺失");
|
||||||
|
}
|
||||||
|
orderLifecycleService.revokeCompletedOrder(context);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlayOrderInfoEntity queryByOrderNo(String orderNo) {
|
public PlayOrderInfoEntity queryByOrderNo(String orderNo) {
|
||||||
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public class PlayCustomOrderDetailsReturnVo {
|
|||||||
private String orderNo;
|
private String orderNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4:5】
|
||||||
|
* 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消 5:已撤销
|
||||||
*/
|
*/
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ public class PlayCustomOrderInfoQueryVo extends BasePageEntity {
|
|||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单 1:已接单 2:已开始 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4:5】
|
||||||
|
* 0:已下单 1:已接单 2:已开始 3:已完成 4:已取消 5:已撤销
|
||||||
*/
|
*/
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单类型【0:充值订单;1:提现订单;2:普通订单】
|
* 订单类型(为空时默认排除充值/提现)
|
||||||
*/
|
*/
|
||||||
private String orderType = "2";
|
private String orderType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下单类型(0:指定单,1:随机单。2:打赏单)
|
* 下单类型(0:指定单,1:随机单。2:打赏单)
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public class PlayCustomOrderListReturnVo {
|
|||||||
private String orderNo;
|
private String orderNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4:5】
|
||||||
|
* 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消 5:已撤销
|
||||||
*/
|
*/
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,10 @@ public interface IEarningsService extends IService<EarningsLineEntity> {
|
|||||||
LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now);
|
LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now);
|
||||||
|
|
||||||
List<EarningsLineEntity> findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now);
|
List<EarningsLineEntity> findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now);
|
||||||
|
|
||||||
|
void reverseByOrder(String orderId, String operatorId);
|
||||||
|
|
||||||
|
void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId);
|
||||||
|
|
||||||
|
boolean hasLockedLines(String orderId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.starry.admin.modules.withdraw.service.impl;
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
@@ -12,6 +14,7 @@ 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.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -90,4 +93,49 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
|||||||
if (acc.compareTo(amount) < 0) return new ArrayList<>();
|
if (acc.compareTo(amount) < 0) return new ArrayList<>();
|
||||||
return picked;
|
return picked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reverseByOrder(String orderId, String operatorId) {
|
||||||
|
if (StrUtil.isBlank(orderId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.in(EarningsLineEntity::getStatus, Arrays.asList("available", "frozen"))
|
||||||
|
.set(EarningsLineEntity::getStatus, "reversed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId) {
|
||||||
|
if (StrUtil.hasBlank(orderId, tenantId, targetClerkId)) {
|
||||||
|
throw new IllegalArgumentException("创建冲销收益时参数缺失");
|
||||||
|
}
|
||||||
|
BigDecimal normalized = amount == null ? BigDecimal.ZERO : amount.abs();
|
||||||
|
if (normalized.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EarningsLineEntity line = new EarningsLineEntity();
|
||||||
|
line.setId(IdUtils.getUuid());
|
||||||
|
line.setOrderId(orderId);
|
||||||
|
line.setTenantId(tenantId);
|
||||||
|
line.setClerkId(targetClerkId);
|
||||||
|
line.setAmount(normalized.negate());
|
||||||
|
line.setEarningType(EarningsType.ORDER);
|
||||||
|
line.setStatus("available");
|
||||||
|
line.setUnlockTime(LocalDateTime.now());
|
||||||
|
this.save(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasLockedLines(String orderId) {
|
||||||
|
if (StrUtil.isBlank(orderId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long count = this.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.in(EarningsLineEntity::getStatus, Arrays.asList("withdrawing", "withdrawn"))
|
||||||
|
.count();
|
||||||
|
return count != null && count > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ class BlindBoxPoolControllerApiTest extends AbstractApiTest {
|
|||||||
|
|
||||||
private PlayGiftInfoEntity seedGift(String name) {
|
private PlayGiftInfoEntity seedGift(String name) {
|
||||||
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
gift.setId("gift-admin-" + IdUtils.getUuid().substring(0, 12));
|
gift.setId("gift-admin-" + IdUtils.getUuid());
|
||||||
gift.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
gift.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
gift.setHistory(GiftHistory.CURRENT.getCode());
|
gift.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
gift.setName(name);
|
gift.setName(name);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.assertj.core.api.SoftAssertions;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,529 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderEvaluateInfoService playOrderEvaluateInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderComplaintInfoService playOrderComplaintInfoService;
|
||||||
|
|
||||||
|
private final List<String> orderIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> evalIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> complaintIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUpOrders() {
|
||||||
|
if (!orderIdsToCleanup.isEmpty()) {
|
||||||
|
playOrderInfoService.removeByIds(orderIdsToCleanup);
|
||||||
|
orderIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!evalIdsToCleanup.isEmpty()) {
|
||||||
|
playOrderEvaluateInfoService.removeByIds(evalIdsToCleanup);
|
||||||
|
evalIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!complaintIdsToCleanup.isEmpty()) {
|
||||||
|
playOrderComplaintInfoService.removeByIds(complaintIdsToCleanup);
|
||||||
|
complaintIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageFiltersRevokedOrdersAndDetailShowsReason() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String remark = "revoked-flow-" + IdUtils.getUuid();
|
||||||
|
placeRandomOrder(remark, customerToken);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity createdOrder = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(createdOrder).as("Order with remark %s should exist", remark).isNotNull();
|
||||||
|
|
||||||
|
String orderId = createdOrder.getId();
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.lambdaUpdate()
|
||||||
|
.set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.GIFT.getCode())
|
||||||
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||||
|
.update();
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo1(
|
||||||
|
OrderConstant.OperatorType.CLERK.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
orderId);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo23(
|
||||||
|
OrderConstant.OperatorType.CLERK.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
OrderConstant.OrderStatus.IN_PROGRESS.getCode(),
|
||||||
|
orderId);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo23(
|
||||||
|
OrderConstant.OperatorType.ADMIN.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||||||
|
OrderConstant.OrderStatus.COMPLETED.getCode(),
|
||||||
|
orderId);
|
||||||
|
|
||||||
|
String revokeReason = "auto-revoke-" + IdUtils.getUuid();
|
||||||
|
String revokePayload = "{" +
|
||||||
|
"\"orderId\":\"" + orderId + "\"," +
|
||||||
|
"\"refundToCustomer\":false," +
|
||||||
|
"\"refundReason\":\"" + revokeReason + "\"," +
|
||||||
|
"\"earningsStrategy\":\"NONE\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(revokePayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ObjectNode filterPayload = basePayload(1, 10);
|
||||||
|
filterPayload.put("orderStatus", OrderConstant.OrderStatus.REVOKED.getCode());
|
||||||
|
JsonNode listRoot = executeOrderQuery(customerToken, filterPayload);
|
||||||
|
JsonNode dataNode = listRoot.path("data");
|
||||||
|
JsonNode records = dataNode.isArray() ? dataNode : dataNode.path("records");
|
||||||
|
assertThat(records.isArray()).as("List response should contain records array").isTrue();
|
||||||
|
assertThat(records.size()).as("Should return at least one revoked order").isGreaterThan(0);
|
||||||
|
boolean found = false;
|
||||||
|
for (JsonNode node : records) {
|
||||||
|
assertThat(node.path("orderStatus").asText()).isEqualTo("5");
|
||||||
|
if (orderId.equals(node.path("id").asText())) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(found).as("Revoked order should be present in filter result").isTrue();
|
||||||
|
|
||||||
|
MvcResult detailResult = mockMvc.perform(get("/wx/custom/order/queryById")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.param("id", orderId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode detailRoot = objectMapper.readTree(detailResult.getResponse().getContentAsString());
|
||||||
|
JsonNode detail = detailRoot.path("data");
|
||||||
|
assertThat(detail.path("orderStatus").asText()).isEqualTo("5");
|
||||||
|
assertThat(detail.path("refundReason").asText()).isEqualTo(revokeReason);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageSkipsRechargeOrdersByDefaultButAllowsExplicitFilter() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String rechargeRemark = "recharge-like-" + IdUtils.getUuid();
|
||||||
|
LocalDateTime now = LocalDateTime.now().minusMinutes(20);
|
||||||
|
PlayOrderInfoEntity rechargeOrder = persistOrder(now, order -> {
|
||||||
|
order.setRemark(rechargeRemark);
|
||||||
|
order.setOrderType(OrderConstant.OrderType.RECHARGE.getCode());
|
||||||
|
});
|
||||||
|
String giftRemark = "gift-like-" + IdUtils.getUuid();
|
||||||
|
PlayOrderInfoEntity giftOrder = persistOrder(now.plusMinutes(5), order -> {
|
||||||
|
order.setRemark(giftRemark);
|
||||||
|
order.setOrderType(OrderConstant.OrderType.GIFT.getCode());
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode defaultPayload = basePayload(1, 20);
|
||||||
|
JsonNode defaultRecords = queryOrders(customerToken, defaultPayload);
|
||||||
|
assertThat(defaultRecords.size()).isGreaterThan(0);
|
||||||
|
assertThat(defaultRecords).noneMatch(node -> rechargeOrder.getId().equals(node.path("id").asText()));
|
||||||
|
|
||||||
|
ObjectNode explicitPayload = basePayload(1, 20);
|
||||||
|
explicitPayload.put("orderType", OrderConstant.OrderType.GIFT.getCode());
|
||||||
|
JsonNode filteredRecords = queryOrders(customerToken, explicitPayload);
|
||||||
|
assertThat(filteredRecords)
|
||||||
|
.anyMatch(node -> giftOrder.getId().equals(node.path("id").asText()));
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageReturnsOnlyOrdersBelongingToCurrentCustomer() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
LocalDateTime base = LocalDateTime.now().minusMinutes(30);
|
||||||
|
PlayOrderInfoEntity own = persistOrder(base, order -> order.setOrderNo("OWN-" + IdUtils.getUuid().substring(0, 6)));
|
||||||
|
PlayOrderInfoEntity foreign = persistOrder(base.plusMinutes(5), order -> {
|
||||||
|
order.setPurchaserBy("other-customer");
|
||||||
|
order.setOrderNo("FOREIGN-" + IdUtils.getUuid().substring(0, 6));
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 20);
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
List<String> ids = new ArrayList<>();
|
||||||
|
records.forEach(node -> ids.add(node.path("id").asText()));
|
||||||
|
assertThat(ids).contains(own.getId());
|
||||||
|
assertThat(ids).doesNotContain(foreign.getId());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageSupportsPagingMeta() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
LocalDateTime base = LocalDateTime.now().plusHours(2);
|
||||||
|
String pageMarker = "PAGE-" + IdUtils.getUuid().substring(0, 4);
|
||||||
|
String pageGroup = "group-" + pageMarker;
|
||||||
|
PlayOrderInfoEntity first = persistOrder(base, order -> {
|
||||||
|
order.setOrderNo(pageMarker + "A");
|
||||||
|
order.setGroupId(pageGroup);
|
||||||
|
});
|
||||||
|
PlayOrderInfoEntity second = persistOrder(base.plusMinutes(2), order -> {
|
||||||
|
order.setOrderNo(pageMarker + "B");
|
||||||
|
order.setGroupId(pageGroup);
|
||||||
|
});
|
||||||
|
PlayOrderInfoEntity third = persistOrder(base.plusMinutes(4), order -> {
|
||||||
|
order.setOrderNo(pageMarker + "C");
|
||||||
|
order.setGroupId(pageGroup);
|
||||||
|
});
|
||||||
|
ArrayNode purchaserWindow = range(base.minusMinutes(1), base.plusMinutes(5));
|
||||||
|
|
||||||
|
ObjectNode pageOne = basePayload(1, 2);
|
||||||
|
pageOne.set("purchaserTime", purchaserWindow);
|
||||||
|
pageOne.put("orderNo", pageMarker);
|
||||||
|
pageOne.put("groupId", pageGroup);
|
||||||
|
JsonNode rootPageOne = executeOrderQuery(token, pageOne);
|
||||||
|
JsonNode recordsOne = recordsFromRoot(rootPageOne);
|
||||||
|
assertThat(recordsOne.size()).isEqualTo(2);
|
||||||
|
assertThat(rootPageOne.path("pageInfo").path("pageSize").asInt()).isEqualTo(2);
|
||||||
|
assertThat(rootPageOne.path("pageInfo").path("currentPage").asInt()).isEqualTo(1);
|
||||||
|
|
||||||
|
ObjectNode pageTwo = basePayload(2, 2);
|
||||||
|
pageTwo.set("purchaserTime", purchaserWindow);
|
||||||
|
pageTwo.put("orderNo", pageMarker);
|
||||||
|
pageTwo.put("groupId", pageGroup);
|
||||||
|
JsonNode rootPageTwo = executeOrderQuery(token, pageTwo);
|
||||||
|
JsonNode recordsTwo = recordsFromRoot(rootPageTwo);
|
||||||
|
assertThat(recordsTwo.size()).isGreaterThan(0);
|
||||||
|
assertThat(rootPageTwo.path("pageInfo").path("pageSize").asInt()).isEqualTo(2);
|
||||||
|
assertThat(rootPageTwo.path("pageInfo").path("currentPage").asInt()).isEqualTo(2);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageFiltersByOrderStatus() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
PlayOrderInfoEntity pending = persistOrder(LocalDateTime.now().minusMinutes(50),
|
||||||
|
order -> order.setOrderStatus(OrderConstant.OrderStatus.PENDING.getCode()));
|
||||||
|
PlayOrderInfoEntity completed = persistOrder(LocalDateTime.now().minusMinutes(40),
|
||||||
|
order -> order.setOrderStatus(OrderConstant.OrderStatus.COMPLETED.getCode()));
|
||||||
|
PlayOrderInfoEntity revoked = persistOrder(LocalDateTime.now().minusMinutes(30),
|
||||||
|
order -> order.setOrderStatus(OrderConstant.OrderStatus.REVOKED.getCode()));
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 10);
|
||||||
|
payload.put("orderStatus", OrderConstant.OrderStatus.REVOKED.getCode());
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
assertThat(records.size()).isGreaterThan(0);
|
||||||
|
records.forEach(node -> assertThat(node.path("orderStatus").asText())
|
||||||
|
.isEqualTo(OrderConstant.OrderStatus.REVOKED.getCode()));
|
||||||
|
assertThat(findById(records, revoked.getId())).isNotNull();
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageFiltersByPlaceTypeAndCompositeCriteria() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
LocalDateTime base = LocalDateTime.now().minusMinutes(90);
|
||||||
|
PlayOrderInfoEntity target = persistOrder(base, order -> {
|
||||||
|
order.setOrderStatus(OrderConstant.OrderStatus.IN_PROGRESS.getCode());
|
||||||
|
order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
order.setOrderNo("FOCUS-" + IdUtils.getUuid().substring(0, 4));
|
||||||
|
order.setUseCoupon("1");
|
||||||
|
order.setBackendEntry("1");
|
||||||
|
order.setFirstOrder("1");
|
||||||
|
order.setGroupId("group-focus");
|
||||||
|
order.setSex("1");
|
||||||
|
order.setPurchaserTime(base.plusMinutes(5));
|
||||||
|
order.setAcceptTime(base.plusMinutes(10));
|
||||||
|
order.setOrderEndTime(base.plusMinutes(50));
|
||||||
|
});
|
||||||
|
persistOrder(base.plusMinutes(5), order -> {
|
||||||
|
order.setOrderStatus(OrderConstant.OrderStatus.IN_PROGRESS.getCode());
|
||||||
|
order.setPlaceType(OrderConstant.PlaceType.RANDOM.getCode());
|
||||||
|
order.setUseCoupon("0");
|
||||||
|
order.setBackendEntry("0");
|
||||||
|
order.setFirstOrder("0");
|
||||||
|
order.setGroupId("group-noise");
|
||||||
|
order.setSex("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 10);
|
||||||
|
payload.put("placeType", OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
payload.put("orderNo", target.getOrderNo().substring(0, 6));
|
||||||
|
payload.put("useCoupon", "1");
|
||||||
|
payload.put("backendEntry", "1");
|
||||||
|
payload.put("firstOrder", "1");
|
||||||
|
payload.put("groupId", "group-focus");
|
||||||
|
payload.put("sex", "1");
|
||||||
|
payload.set("purchaserTime", range(target.getPurchaserTime().minusMinutes(1), target.getPurchaserTime().plusMinutes(1)));
|
||||||
|
payload.set("acceptTime", range(target.getAcceptTime().minusMinutes(1), target.getAcceptTime().plusMinutes(1)));
|
||||||
|
payload.set("endOrderTime", range(target.getOrderEndTime().minusMinutes(1), target.getOrderEndTime().plusMinutes(1)));
|
||||||
|
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
assertThat(records.size()).isGreaterThan(0);
|
||||||
|
JsonNode targetNode = findById(records, target.getId());
|
||||||
|
assertThat(targetNode).isNotNull();
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageMarksEvaluateAndComplaintFlags() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
PlayOrderInfoEntity evaluated = persistOrder(LocalDateTime.now().minusMinutes(10),
|
||||||
|
order -> order.setOrderNo("EVAL-" + IdUtils.getUuid().substring(0, 4)));
|
||||||
|
PlayOrderInfoEntity complained = persistOrder(LocalDateTime.now().minusMinutes(8),
|
||||||
|
order -> order.setOrderNo("COMP-" + IdUtils.getUuid().substring(0, 4)));
|
||||||
|
markEvaluated(evaluated.getId());
|
||||||
|
markComplained(complained.getId());
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 20);
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
String evalFlag = findById(records, evaluated.getId()).path("evaluate").asText();
|
||||||
|
String complaintFlag = findById(records, complained.getId()).path("complaint").asText();
|
||||||
|
assertThat(evalFlag).isEqualTo("1");
|
||||||
|
assertThat(complaintFlag).isEqualTo("1");
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeRandomOrder(String remark, String customerToken) throws Exception {
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("下单成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayOrderInfoEntity persistOrder(LocalDateTime baseTime, java.util.function.Consumer<PlayOrderInfoEntity> customizer) {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-" + IdUtils.getUuid());
|
||||||
|
order.setOrderNo("WXQ-" + IdUtils.getUuid().substring(0, 8));
|
||||||
|
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
order.setOrderStatus(OrderConstant.OrderStatus.PENDING.getCode());
|
||||||
|
order.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
|
||||||
|
order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
order.setRewardType("0");
|
||||||
|
order.setFirstOrder("0");
|
||||||
|
order.setRefundType("0");
|
||||||
|
order.setRefundAmount(BigDecimal.ZERO);
|
||||||
|
order.setOrderMoney(new BigDecimal("99.00"));
|
||||||
|
order.setFinalAmount(new BigDecimal("99.00"));
|
||||||
|
order.setDiscountAmount(BigDecimal.ZERO);
|
||||||
|
order.setEstimatedRevenue(new BigDecimal("40.00"));
|
||||||
|
order.setEstimatedRevenueRatio(40);
|
||||||
|
order.setUseCoupon("0");
|
||||||
|
order.setBackendEntry("0");
|
||||||
|
order.setCouponIds(java.util.Collections.emptyList());
|
||||||
|
order.setPaymentSource("balance");
|
||||||
|
order.setPayMethod("0");
|
||||||
|
order.setPayState("1");
|
||||||
|
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
order.setPurchaserTime(baseTime);
|
||||||
|
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
order.setAcceptTime(baseTime.plusMinutes(5));
|
||||||
|
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
|
||||||
|
order.setOrderStartTime(baseTime.plusMinutes(10));
|
||||||
|
order.setOrderEndTime(baseTime.plusMinutes(40));
|
||||||
|
order.setOrdersExpiredState("0");
|
||||||
|
order.setOrderSettlementState("0");
|
||||||
|
order.setSex("2");
|
||||||
|
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
order.setCommodityType("1");
|
||||||
|
order.setCommodityPrice(new BigDecimal("99.00"));
|
||||||
|
order.setCommodityName("Weixin Order");
|
||||||
|
order.setServiceDuration("60min");
|
||||||
|
order.setCommodityNumber("1");
|
||||||
|
order.setRemark("auto");
|
||||||
|
order.setBackendRemark("auto");
|
||||||
|
Date createdDate = toDate(baseTime);
|
||||||
|
order.setCreatedTime(createdDate);
|
||||||
|
order.setUpdatedTime(createdDate);
|
||||||
|
order.setCreatedBy("wx-test");
|
||||||
|
order.setUpdatedBy("wx-test");
|
||||||
|
customizer.accept(order);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.save(order);
|
||||||
|
orderIdsToCleanup.add(order.getId());
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode basePayload(int pageNum, int pageSize) {
|
||||||
|
ObjectNode node = objectMapper.createObjectNode();
|
||||||
|
node.put("pageNum", pageNum);
|
||||||
|
node.put("pageSize", pageSize);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayNode range(LocalDateTime start, LocalDateTime end) {
|
||||||
|
ArrayNode node = objectMapper.createArrayNode();
|
||||||
|
node.add(DATE_TIME_FORMATTER.format(start));
|
||||||
|
node.add(DATE_TIME_FORMATTER.format(end));
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date toDate(LocalDateTime time) {
|
||||||
|
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ensureCustomerToken() {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
return customerToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markEvaluated(String orderId) {
|
||||||
|
PlayOrderEvaluateInfoEntity entity = new PlayOrderEvaluateInfoEntity();
|
||||||
|
entity.setId("eval-" + IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setOrderId(orderId);
|
||||||
|
entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
entity.setAnonymous("0");
|
||||||
|
entity.setEvaluateType("0");
|
||||||
|
entity.setEvaluateLevel(5);
|
||||||
|
entity.setEvaluateCon("Great job");
|
||||||
|
entity.setEvaluateTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
|
||||||
|
entity.setHidden("0");
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderEvaluateInfoService.save(entity);
|
||||||
|
evalIdsToCleanup.add(entity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markComplained(String orderId) {
|
||||||
|
PlayOrderComplaintInfoEntity entity = new PlayOrderComplaintInfoEntity();
|
||||||
|
entity.setId("complaint-" + IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setOrderId(orderId);
|
||||||
|
entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
entity.setComplaintCon("Need assistance");
|
||||||
|
entity.setComplaintTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
|
||||||
|
entity.setHidden("0");
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderComplaintInfoService.save(entity);
|
||||||
|
complaintIdsToCleanup.add(entity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode findById(JsonNode records, String id) {
|
||||||
|
for (JsonNode node : records) {
|
||||||
|
if (id.equals(node.path("id").asText())) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Record with id " + id + " not found in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode queryOrders(String customerToken, ObjectNode payload) throws Exception {
|
||||||
|
JsonNode root = executeOrderQuery(customerToken, payload);
|
||||||
|
JsonNode dataNode = root.path("data");
|
||||||
|
return dataNode.isArray() ? dataNode : dataNode.path("records");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode recordsFromRoot(JsonNode root) {
|
||||||
|
JsonNode dataNode = root.path("data");
|
||||||
|
return dataNode.isArray() ? dataNode : dataNode.path("records");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode executeOrderQuery(String customerToken, ObjectNode payload) throws Exception {
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/custom/order/queryByPage")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
return objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.starry.admin.modules.order.listener;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class OrderRevocationEarningsListenerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
|
||||||
|
private OrderRevocationEarningsListener listener;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
listener = new OrderRevocationEarningsListener(earningsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handle_counterStrategyFallsBackToEstimatedRevenueWhenRefundAmountZero() {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId("order-counter-1");
|
||||||
|
context.setOperatorId("admin-op");
|
||||||
|
context.setEarningsStrategy(EarningsAdjustStrategy.COUNTER_TO_PEIPEI);
|
||||||
|
context.setRefundAmount(BigDecimal.ZERO);
|
||||||
|
context.setCounterClerkId("ops-clerk");
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-counter-1");
|
||||||
|
order.setTenantId("tenant-77");
|
||||||
|
order.setEstimatedRevenue(BigDecimal.valueOf(68));
|
||||||
|
|
||||||
|
listener.handle(new OrderRevocationEvent(context, order));
|
||||||
|
|
||||||
|
verify(earningsService)
|
||||||
|
.createCounterLine(order.getId(), order.getTenantId(), "ops-clerk", BigDecimal.valueOf(68), "admin-op");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handle_reverseStrategyRevertsAvailableLines() {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId("order-reverse-2");
|
||||||
|
context.setOperatorId("admin-reviewer");
|
||||||
|
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-reverse-2");
|
||||||
|
|
||||||
|
listener.handle(new OrderRevocationEvent(context, order));
|
||||||
|
|
||||||
|
verify(earningsService).reverseByOrder("order-reverse-2", "admin-reviewer");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handle_noneStrategyIsNoOp() {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId("order-none-3");
|
||||||
|
context.setOperatorId("admin-noop");
|
||||||
|
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-none-3");
|
||||||
|
|
||||||
|
listener.handle(new OrderRevocationEvent(context, order));
|
||||||
|
|
||||||
|
verifyNoInteractions(earningsService);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,9 +40,12 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
|||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
|
||||||
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.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
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.order.service.support.ClerkRevenueCalculator;
|
||||||
@@ -65,10 +68,12 @@ import java.util.UUID;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class OrderLifecycleServiceImplTest {
|
class OrderLifecycleServiceImplTest {
|
||||||
@@ -106,11 +111,99 @@ class OrderLifecycleServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initStrategies() {
|
void initStrategies() {
|
||||||
lifecycleService.initPlacementStrategies();
|
lifecycleService.initPlacementStrategies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_updatesStatusAndPublishesEvent() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
completed.setFinalAmount(BigDecimal.valueOf(188));
|
||||||
|
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
revoked.setFinalAmount(BigDecimal.valueOf(188));
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||||
|
when(playBalanceDetailsInfoService.existsCustomerConsumeRecord(completed.getPurchaserBy(), orderId))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-8");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundAmount(BigDecimal.valueOf(88));
|
||||||
|
context.setRefundReason("客户投诉");
|
||||||
|
context.setRefundToCustomer(true);
|
||||||
|
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||||
|
|
||||||
|
lifecycleService.revokeCompletedOrder(context);
|
||||||
|
|
||||||
|
verify(orderInfoMapper).update(isNull(), any());
|
||||||
|
verify(orderLogInfoMapper).insert(any());
|
||||||
|
ArgumentCaptor<OrderRevocationEvent> captor = ArgumentCaptor.forClass(OrderRevocationEvent.class);
|
||||||
|
verify(applicationEventPublisher).publishEvent(captor.capture());
|
||||||
|
OrderRevocationEvent event = captor.getValue();
|
||||||
|
assertEquals(orderId, event.getContext().getOrderId());
|
||||||
|
assertEquals(EarningsAdjustStrategy.REVERSE_CLERK, event.getContext().getEarningsStrategy());
|
||||||
|
assertEquals(BigDecimal.valueOf(88), event.getContext().getRefundAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_defersBalanceCreditToListener() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
completed.setFinalAmount(BigDecimal.valueOf(208));
|
||||||
|
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
revoked.setFinalAmount(BigDecimal.valueOf(208));
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||||
|
when(playBalanceDetailsInfoService.existsCustomerConsumeRecord(completed.getPurchaserBy(), orderId))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-9");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundAmount(BigDecimal.valueOf(108));
|
||||||
|
context.setRefundReason("质量问题");
|
||||||
|
context.setRefundToCustomer(true);
|
||||||
|
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||||
|
|
||||||
|
lifecycleService.revokeCompletedOrder(context);
|
||||||
|
|
||||||
|
verify(customUserInfoService, never()).getById(anyString());
|
||||||
|
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_blocksWhenEarningsLocked() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed);
|
||||||
|
when(earningsService.hasLockedLines(orderId)).thenReturn(true);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-locked");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundToCustomer(false);
|
||||||
|
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||||
|
|
||||||
|
assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||||
|
verify(orderInfoMapper, never()).update(isNull(), any());
|
||||||
|
verify(applicationEventPublisher, never()).publishEvent(any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void placeOrder_throwsWhenCommandNull() {
|
void placeOrder_throwsWhenCommandNull() {
|
||||||
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
|
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
|
||||||
@@ -1223,6 +1316,13 @@ class OrderLifecycleServiceImplTest {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PlayCustomUserInfoEntity buildCustomer(String id, BigDecimal balance) {
|
||||||
|
PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setAccountBalance(balance);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
private void stubDefaultPersistence() {
|
private void stubDefaultPersistence() {
|
||||||
lenient().when(orderInfoMapper.selectCount(any())).thenReturn(0L);
|
lenient().when(orderInfoMapper.selectCount(any())).thenReturn(0L);
|
||||||
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);
|
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);
|
||||||
|
|||||||
@@ -25,13 +25,17 @@ import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
|
|||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
||||||
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
|
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
|
||||||
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||||
|
import com.starry.admin.utils.DateRangeUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.Month;
|
import java.time.Month;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
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;
|
||||||
@@ -318,6 +322,106 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
Arrays.asList(o1.getId(), o2.getId(), o3.getId(), o4.getId(), o5.getId())));
|
Arrays.asList(o1.getId(), o2.getId(), o3.getId(), o4.getId(), o5.getId())));
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
|
@DisplayName("queryOverview filters completed orders according to complex date/time ranges")
|
||||||
|
void queryOverviewHonorsDateRangesForMultipleClerks() {
|
||||||
|
PlayClerkUserInfoEntity alpha = buildClerk("c-alpha", "Alpha", "g-alpha", "l-alpha");
|
||||||
|
PlayClerkUserInfoEntity beta = buildClerk("c-beta", "Beta", "g-beta", "l-beta");
|
||||||
|
PlayClerkUserInfoEntity gamma = buildClerk("c-gamma", "Gamma", "g-gamma", "l-gamma");
|
||||||
|
List<PlayClerkUserInfoEntity> clerks = Arrays.asList(alpha, beta, gamma);
|
||||||
|
|
||||||
|
when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any()))
|
||||||
|
.thenReturn(Arrays.asList(alpha.getId(), beta.getId(), gamma.getId()));
|
||||||
|
when(clerkUserInfoService.list((Wrapper<PlayClerkUserInfoEntity>) any())).thenReturn(clerks);
|
||||||
|
when(playClerkLevelInfoService.selectAll()).thenReturn(Arrays.asList(
|
||||||
|
level(alpha.getLevelId(), "铂金"),
|
||||||
|
level(beta.getLevelId(), "黄金"),
|
||||||
|
level(gamma.getLevelId(), "白银")));
|
||||||
|
when(playPersonnelGroupInfoService.selectAll()).thenReturn(Arrays.asList(
|
||||||
|
group(alpha.getGroupId(), "一组"),
|
||||||
|
group(beta.getGroupId(), "二组"),
|
||||||
|
group(gamma.getGroupId(), "三组")));
|
||||||
|
|
||||||
|
Map<String, List<PlayOrderInfoEntity>> ordersByClerk = new HashMap<>();
|
||||||
|
ordersByClerk.put(alpha.getId(), Arrays.asList(
|
||||||
|
order(alpha.getId(), "userA1", "1", "0", "0", new BigDecimal("100.00"), new BigDecimal("70.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 1, 10, 0)),
|
||||||
|
order(alpha.getId(), "userA2", "0", "0", "0", new BigDecimal("210.00"), new BigDecimal("120.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 3, 15, 30)),
|
||||||
|
order(alpha.getId(), "userA3", "0", "0", "0", new BigDecimal("150.00"), new BigDecimal("80.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 7, 9, 15)),
|
||||||
|
order(alpha.getId(), "userA4", "0", "0", "0", new BigDecimal("130.00"), new BigDecimal("75.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 10, 18, 45))));
|
||||||
|
ordersByClerk.put(beta.getId(), Arrays.asList(
|
||||||
|
order(beta.getId(), "userB1", "1", "0", "0", new BigDecimal("95.00"), new BigDecimal("50.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 2, 11, 30)),
|
||||||
|
order(beta.getId(), "userB2", "0", "0", "0", new BigDecimal("120.00"), new BigDecimal("65.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 4, 13, 0)),
|
||||||
|
order(beta.getId(), "userB3", "0", "0", "0", new BigDecimal("85.00"), new BigDecimal("40.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 8, 16, 0)),
|
||||||
|
order(beta.getId(), "userB4", "0", "0", "0", new BigDecimal("150.00"), new BigDecimal("85.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 10, 19, 0))));
|
||||||
|
ordersByClerk.put(gamma.getId(), Arrays.asList(
|
||||||
|
order(gamma.getId(), "userC1", "1", "0", "0", new BigDecimal("70.00"), new BigDecimal("35.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 1, 8, 0)),
|
||||||
|
order(gamma.getId(), "userC2", "0", "0", "0", new BigDecimal("135.00"), new BigDecimal("70.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 5, 17, 20)),
|
||||||
|
order(gamma.getId(), "userC3", "0", "0", "0", new BigDecimal("75.00"), new BigDecimal("45.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 6, 14, 50)),
|
||||||
|
order(gamma.getId(), "userC4", "0", "0", "0", new BigDecimal("160.00"), new BigDecimal("90.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 9, 21, 10))));
|
||||||
|
|
||||||
|
when(playOrderInfoService.clerkSelectOrderInfoList(anyString(), anyString(), anyString()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String clerkId = invocation.getArgument(0);
|
||||||
|
String startStr = invocation.getArgument(1);
|
||||||
|
String endStr = invocation.getArgument(2);
|
||||||
|
LocalDateTime start = LocalDateTime.parse(startStr, DateRangeUtils.DATE_TIME_FORMATTER);
|
||||||
|
LocalDateTime end = LocalDateTime.parse(endStr, DateRangeUtils.DATE_TIME_FORMATTER);
|
||||||
|
return ordersByClerk.getOrDefault(clerkId, Collections.emptyList()).stream()
|
||||||
|
.filter(order -> !order.getPurchaserTime().isBefore(start)
|
||||||
|
&& !order.getPurchaserTime().isAfter(end))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
});
|
||||||
|
|
||||||
|
setAuthentication();
|
||||||
|
try {
|
||||||
|
ClerkPerformanceOverviewResponseVo fullRange =
|
||||||
|
service.queryOverview(buildOverviewVo("2024-07-01", "2024-07-10"));
|
||||||
|
ClerkPerformanceOverviewSummaryVo fullSummary = fullRange.getSummary();
|
||||||
|
assertEquals(new BigDecimal("1480.00"), fullSummary.getTotalGmv());
|
||||||
|
assertEquals(12, fullSummary.getTotalOrderCount());
|
||||||
|
assertEquals(new BigDecimal("590.00"), snapshotFor(fullRange.getRankings(), alpha.getId()).getGmv());
|
||||||
|
assertEquals(new BigDecimal("450.00"), snapshotFor(fullRange.getRankings(), beta.getId()).getGmv());
|
||||||
|
assertEquals(new BigDecimal("440.00"), snapshotFor(fullRange.getRankings(), gamma.getId()).getGmv());
|
||||||
|
|
||||||
|
ClerkPerformanceOverviewResponseVo midRange =
|
||||||
|
service.queryOverview(buildOverviewVo("2024-07-04", "2024-07-08"));
|
||||||
|
ClerkPerformanceOverviewSummaryVo midSummary = midRange.getSummary();
|
||||||
|
assertEquals(5, midSummary.getTotalOrderCount());
|
||||||
|
assertEquals(new BigDecimal("565.00"), midSummary.getTotalGmv());
|
||||||
|
List<ClerkPerformanceSnapshotVo> midRankings = midRange.getRankings();
|
||||||
|
assertEquals("c-gamma", midRankings.get(0).getClerkId());
|
||||||
|
assertEquals(new BigDecimal("210.00"), midRankings.get(0).getGmv());
|
||||||
|
assertEquals("c-beta", midRankings.get(1).getClerkId());
|
||||||
|
assertEquals(new BigDecimal("205.00"), midRankings.get(1).getGmv());
|
||||||
|
assertEquals("c-alpha", midRankings.get(2).getClerkId());
|
||||||
|
assertEquals(new BigDecimal("150.00"), midRankings.get(2).getGmv());
|
||||||
|
|
||||||
|
ClerkPerformanceOverviewResponseVo shortRange = service.queryOverview(
|
||||||
|
buildOverviewVo("2024-07-10 18:30:00", "2024-07-10 18:59:59"));
|
||||||
|
ClerkPerformanceOverviewSummaryVo shortSummary = shortRange.getSummary();
|
||||||
|
assertEquals(1, shortSummary.getTotalOrderCount());
|
||||||
|
assertEquals(new BigDecimal("130.00"), shortSummary.getTotalGmv());
|
||||||
|
ClerkPerformanceSnapshotVo alphaSnapshot = snapshotFor(shortRange.getRankings(), alpha.getId());
|
||||||
|
assertEquals(new BigDecimal("130.00"), alphaSnapshot.getGmv());
|
||||||
|
assertEquals(1, alphaSnapshot.getOrderCount());
|
||||||
|
assertEquals(BigDecimal.ZERO, snapshotFor(shortRange.getRankings(), beta.getId()).getGmv());
|
||||||
|
assertEquals(BigDecimal.ZERO, snapshotFor(shortRange.getRankings(), gamma.getId()).getGmv());
|
||||||
|
} finally {
|
||||||
|
clearAuthentication();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
@DisplayName("getClerkPerformanceInfo should ignore non-completed orders when aggregating GMV")
|
@DisplayName("getClerkPerformanceInfo should ignore non-completed orders when aggregating GMV")
|
||||||
void getClerkPerformanceInfoSkipsNonCompletedOrders() {
|
void getClerkPerformanceInfoSkipsNonCompletedOrders() {
|
||||||
PlayClerkUserInfoEntity clerk = buildClerk("c9", "Nine", "gX", "lX");
|
PlayClerkUserInfoEntity clerk = buildClerk("c9", "Nine", "gX", "lX");
|
||||||
@@ -348,6 +452,22 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
assertTrue(idCaptor.getValue().contains(completed.getId()));
|
assertTrue(idCaptor.getValue().contains(completed.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ClerkPerformanceOverviewQueryVo buildOverviewVo(String start, String end) {
|
||||||
|
ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo();
|
||||||
|
vo.setEndOrderTime(Arrays.asList(start, end));
|
||||||
|
vo.setIncludeSummary(true);
|
||||||
|
vo.setIncludeRankings(true);
|
||||||
|
vo.setLimit(10);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClerkPerformanceSnapshotVo snapshotFor(List<ClerkPerformanceSnapshotVo> snapshots, String clerkId) {
|
||||||
|
return snapshots.stream()
|
||||||
|
.filter(snapshot -> clerkId.equals(snapshot.getClerkId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("Snapshot not found for clerk " + clerkId));
|
||||||
|
}
|
||||||
|
|
||||||
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
|
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
|
||||||
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
|||||||
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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;
|
||||||
@@ -68,4 +70,54 @@ class EarningsServiceImplTest {
|
|||||||
|
|
||||||
verify(baseMapper, never()).insert(any());
|
verify(baseMapper, never()).insert(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createCounterLine_persistsNegativeAvailableLine() {
|
||||||
|
when(baseMapper.insert(any())).thenReturn(1);
|
||||||
|
|
||||||
|
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(88), "admin");
|
||||||
|
|
||||||
|
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||||
|
verify(baseMapper).insert(captor.capture());
|
||||||
|
EarningsLineEntity saved = captor.getValue();
|
||||||
|
assertEquals(new BigDecimal("-88"), saved.getAmount());
|
||||||
|
assertEquals("clerk-c", saved.getClerkId());
|
||||||
|
assertEquals("available", saved.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWithdrawable_returnsEmptyWhenCounterLinesCreateDebt() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<EarningsLineEntity> lines = Arrays.asList(
|
||||||
|
line("neg", new BigDecimal("-60")),
|
||||||
|
line("pos", new BigDecimal("40")));
|
||||||
|
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
|
||||||
|
|
||||||
|
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(30), now);
|
||||||
|
|
||||||
|
assertEquals(0, picked.size(), "净额不足时不应允许提现");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWithdrawable_allowsWithdrawalAfterPositiveLinesCoverDebt() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<EarningsLineEntity> lines = Arrays.asList(
|
||||||
|
line("neg", new BigDecimal("-60")),
|
||||||
|
line("first", new BigDecimal("40")),
|
||||||
|
line("second", new BigDecimal("150")));
|
||||||
|
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
|
||||||
|
|
||||||
|
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(70), now);
|
||||||
|
|
||||||
|
assertEquals(3, picked.size());
|
||||||
|
assertEquals("second", picked.get(2).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsLineEntity line(String id, BigDecimal amount) {
|
||||||
|
EarningsLineEntity entity = new EarningsLineEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setAmount(amount);
|
||||||
|
entity.setStatus("available");
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user