Compare commits

...

4 Commits

Author SHA1 Message Date
irving
90849e5267 test: stabilize random order cancellation cases
Some checks failed
Build and Push Backend / docker (push) Failing after 12s
2025-12-31 23:12:51 -05:00
irving
ec5c1782c6 fix: allow manager cancellation of random orders 2025-12-31 23:02:43 -05:00
irving
911a974e51 feat: implement order relation type tracking
- Add OrderRelationType enum (UNASSIGNED, LEGACY, FIRST, CONTINUED, NEUTRAL)
- Create play_clerk_customer_relation table to track first-completed history
- Add order_relation_type column to play_order_info
- Migrate existing orders to set relation types based on completion history
- Update order services to determine relation type on creation
- Update order VOs and controllers to expose relation type in API responses
- Update clerk performance calculations to account for relation types
- Update revenue calculations to distinguish between first and continued orders
- Add comprehensive API and unit tests for order relation functionality
2025-12-31 22:06:05 -05:00
irving
f39b560a05 fix: mask random order details for non-owner clerks 2025-12-28 19:31:56 -05:00
39 changed files with 5857 additions and 206 deletions

2861
apitest.out Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,25 @@
package com.starry.admin.common.task;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
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.module.entity.PlayClerkWagesDetailsInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkWagesInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkWagesDetailsInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkWagesInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.admin.modules.system.service.ISysTenantService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
@@ -128,14 +129,13 @@ public class ClerkWagesSettlementTask {
finalAmount = finalAmount.add(orderInfo.getFinalAmount());
orderNumber++;
estimatedRevenue = estimatedRevenue.add(orderInfo.getEstimatedRevenue());
if ("0".equals(orderInfo.getFirstOrder())) {
orderContinueNumber++;
orderContinueMoney = orderContinueMoney.add(orderInfo.getFinalAmount());
}
if ("1".equals(orderInfo.getOrdersExpiredState())) {
ordersExpiredNumber++;
}
}
ContinuationMetrics continuationMetrics = calculateContinuationMetrics(orderInfoEntities);
orderContinueNumber = continuationMetrics.count;
orderContinueMoney = continuationMetrics.money;
PlayClerkWagesInfoEntity wagesInfo = new PlayClerkWagesInfoEntity();
wagesInfo.setId(wagesId);
@@ -158,4 +158,51 @@ public class ClerkWagesSettlementTask {
playClerkWagesInfoService.saveOrUpdate(wagesInfo);
}
private ContinuationMetrics calculateContinuationMetrics(List<PlayOrderInfoEntity> orders) {
List<PlayOrderInfoEntity> completedOrders = orders.stream()
.filter(order -> OrderConstant.OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus()))
.collect(Collectors.toList());
if (completedOrders.isEmpty()) {
return new ContinuationMetrics(0, BigDecimal.ZERO);
}
int continuedCount = 0;
BigDecimal continuedMoney = BigDecimal.ZERO;
for (PlayOrderInfoEntity order : completedOrders) {
String customerId = order.getPurchaserBy();
if (StrUtil.isBlank(customerId)) {
throw new CustomException("订单缺少顾客信息,无法统计续单");
}
OrderConstant.OrderRelationType relationType = normalizeRelationType(order);
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
continuedCount++;
continuedMoney = continuedMoney.add(order.getFinalAmount());
}
}
return new ContinuationMetrics(continuedCount, continuedMoney);
}
private OrderConstant.OrderRelationType normalizeRelationType(PlayOrderInfoEntity order) {
OrderConstant.OrderRelationType relationType = order.getOrderRelationType();
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(order.getPlaceType())) {
return OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
return OrderConstant.OrderRelationType.FIRST;
}
return relationType;
}
private static final class ContinuationMetrics {
private final int count;
private final BigDecimal money;
private ContinuationMetrics(int count, BigDecimal money) {
this.count = count;
this.money = money;
}
}
}

View File

@@ -1,9 +1,12 @@
package com.starry.admin.common.task;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkRankingInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkRankingInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
@@ -20,6 +23,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -134,14 +138,13 @@ public class OrderRankingSettlementTask {
for (PlayOrderInfoEntity orderInfoEntity : orderInfoEntities) {
customIds.add(orderInfoEntity.getAcceptBy());
orderMoney = orderMoney.add(orderInfoEntity.getOrderMoney());
if ("0".equals(orderInfoEntity.getFirstOrder())) {
orderContinueNumber++;
orderContinueMoney = orderContinueMoney.add(orderInfoEntity.getOrderMoney());
}
if ("1".equals(orderInfoEntity.getOrdersExpiredState())) {
ordersExpiredNumber++;
}
}
ContinuationMetrics continuationMetrics = calculateContinuationMetrics(orderInfoEntities);
orderContinueNumber = continuationMetrics.count;
orderContinueMoney = continuationMetrics.money;
BigDecimal orderContinueProportion = orderNumber == 0 ? BigDecimal.ZERO : new BigDecimal(ordersExpiredNumber).divide(new BigDecimal(orderNumber), 4, RoundingMode.HALF_UP).add(new BigDecimal(100));
BigDecimal averageUnitPrice = customIds.isEmpty() ? BigDecimal.ZERO : orderMoney.divide(new BigDecimal(customIds.size()), 4, RoundingMode.HALF_UP);
PlayClerkRankingInfoEntity rankingInfo = new PlayClerkRankingInfoEntity();
@@ -170,4 +173,51 @@ public class OrderRankingSettlementTask {
return rankingInfo;
}
private ContinuationMetrics calculateContinuationMetrics(List<PlayOrderInfoEntity> orders) {
List<PlayOrderInfoEntity> completedOrders = orders.stream()
.filter(order -> OrderConstant.OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus()))
.collect(Collectors.toList());
if (completedOrders.isEmpty()) {
return new ContinuationMetrics(0, BigDecimal.ZERO);
}
int continuedCount = 0;
BigDecimal continuedMoney = BigDecimal.ZERO;
for (PlayOrderInfoEntity order : completedOrders) {
String customerId = order.getPurchaserBy();
if (StrUtil.isBlank(customerId)) {
throw new CustomException("订单缺少顾客信息,无法统计续单");
}
OrderConstant.OrderRelationType relationType = normalizeRelationType(order);
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
continuedCount++;
continuedMoney = continuedMoney.add(order.getOrderMoney());
}
}
return new ContinuationMetrics(continuedCount, continuedMoney);
}
private OrderConstant.OrderRelationType normalizeRelationType(PlayOrderInfoEntity order) {
OrderConstant.OrderRelationType relationType = order.getOrderRelationType();
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(order.getPlaceType())) {
return OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
return OrderConstant.OrderRelationType.FIRST;
}
return relationType;
}
private static final class ContinuationMetrics {
private final int count;
private final BigDecimal money;
private ContinuationMetrics(int count, BigDecimal money) {
this.count = count;
this.money = money;
}
}
}

View File

@@ -62,6 +62,7 @@ public class BlindBoxDispatchService {
.orderType(OrderConstant.OrderType.GIFT)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(OrderConstant.RewardType.GIFT)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.paymentSource(OrderConstant.PaymentSource.BLIND_BOX)
.sourceRewardId(reward.getId())
.paymentInfo(paymentInfo)

View File

@@ -40,6 +40,7 @@ import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaStatus;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
@@ -510,7 +511,17 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
int orderContinueNumber = 0;
int orderNumber = 0;
for (PlayOrderInfoEntity orderInfo : playOrderInfoService.queryBySettlementOrder(record.getId(), "")) {
if ("0".equals(orderInfo.getFirstOrder())) {
OrderConstant.OrderRelationType relationType = orderInfo.getOrderRelationType();
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
relationType = OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
relationType = OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
orderContinueNumber++;
}
orderNumber++;

View File

@@ -18,6 +18,7 @@ import com.starry.admin.modules.custom.module.vo.PlayCustomUserQueryVo;
import com.starry.admin.modules.custom.module.vo.PlayCustomUserReturnVo;
import com.starry.admin.modules.custom.service.IPlayCustomLevelInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl;
import com.starry.admin.modules.personnel.service.IPlayBalanceDetailsInfoService;
@@ -169,7 +170,17 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
if (orderInfo.getId() == null) {
continue;
}
if ("0".equals(orderInfo.getFirstOrder())) {
OrderConstant.OrderRelationType relationType = orderInfo.getOrderRelationType();
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
relationType = OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
relationType = OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
orderContinueNumber++;
}
orderNumber++;

View File

@@ -0,0 +1,31 @@
package com.starry.admin.modules.order.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.order.module.entity.PlayClerkCustomerRelationEntity;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/**
* 客户-店员关系Mapper接口
*/
public interface PlayClerkCustomerRelationMapper extends MPJBaseMapper<PlayClerkCustomerRelationEntity> {
@Select("SELECT * FROM play_clerk_customer_relation "
+ "WHERE tenant_id = #{tenantId} AND purchaser_by = #{purchaserBy} AND accept_by = #{acceptBy} "
+ "AND deleted = 0 FOR UPDATE")
PlayClerkCustomerRelationEntity selectForUpdate(@Param("tenantId") String tenantId,
@Param("purchaserBy") String purchaserBy,
@Param("acceptBy") String acceptBy);
@Update("UPDATE play_clerk_customer_relation "
+ "SET deleted = 0, has_completed = '0', first_completed_order_id = NULL, first_completed_time = NULL "
+ "WHERE tenant_id = #{tenantId} AND purchaser_by = #{purchaserBy} AND accept_by = #{acceptBy}")
int restoreRelation(@Param("tenantId") String tenantId,
@Param("purchaserBy") String purchaserBy,
@Param("acceptBy") String acceptBy);
@Delete("DELETE FROM play_clerk_customer_relation WHERE tenant_id = #{tenantId}")
int hardDeleteByTenant(@Param("tenantId") String tenantId);
}

View File

@@ -123,6 +123,37 @@ public class OrderConstant {
}
}
/**
* 订单关系类型
*/
@Getter
public enum OrderRelationType {
UNASSIGNED("UNASSIGNED", "未分配"),
LEGACY("LEGACY", "历史存量"),
FIRST("FIRST", "首单"),
CONTINUED("CONTINUED", "续单"),
NEUTRAL("NEUTRAL", "中性");
@com.baomidou.mybatisplus.annotation.EnumValue
@com.fasterxml.jackson.annotation.JsonValue
private final String code;
private final String description;
OrderRelationType(String code, String description) {
this.code = code;
this.description = description;
}
public static OrderRelationType fromCode(String code) {
for (OrderRelationType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown order relation type code: " + code);
}
}
/**
* 性别枚举
*/

View File

@@ -33,7 +33,8 @@ public class OrderCreationContext {
private OrderConstant.RewardType rewardType;
private boolean isFirstOrder;
@NotNull(message = "订单关系类型不能为空")
private OrderConstant.OrderRelationType orderRelationType;
private OrderConstant.PaymentSource paymentSource;
@@ -64,10 +65,6 @@ public class OrderCreationContext {
@Nullable
private String creatorId;
public String getFirstOrderString() {
return isFirstOrder ? "1" : "0";
}
public boolean isValidForRandomOrder() {
return placeType == OrderConstant.PlaceType.RANDOM && randomOrderRequirements != null;
}

View File

@@ -0,0 +1,58 @@
package com.starry.admin.modules.order.module.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.starry.common.domain.BaseEntity;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 客户-店员关系(完成历史快照)
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("play_clerk_customer_relation")
public class PlayClerkCustomerRelationEntity extends BaseEntity<PlayClerkCustomerRelationEntity> {
/**
* 主键
*/
private String id;
/**
* 顾客ID
*/
@TableField("purchaser_by")
private String purchaserBy;
/**
* 租户ID
*/
@TableField("tenant_id")
private String tenantId;
/**
* 店员ID
*/
@TableField("accept_by")
private String acceptBy;
/**
* 是否有已完成历史
*/
@TableField("has_completed")
private String hasCompleted;
/**
* 首次完成订单ID
*/
@TableField("first_completed_order_id")
private String firstCompletedOrderId;
/**
* 首次完成时间
*/
@TableField("first_completed_time")
private LocalDateTime firstCompletedTime;
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.starry.admin.common.conf.StringTypeHandler;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.service.impl.OrderLifecycleServiceImpl;
import com.starry.common.domain.BaseEntity;
@@ -64,8 +65,15 @@ public class PlayOrderInfoEntity extends BaseEntity<PlayOrderInfoEntity> {
/**
* 是否是首单【0不是1是】
*/
@TableField(exist = false)
private String firstOrder;
/**
* 订单关系类型UNASSIGNED/FIRST/CONTINUED/NEUTRAL
*/
@TableField("order_relation_type")
private OrderConstant.OrderRelationType orderRelationType;
/**
* 退款类型【0未退款1已退款】
*/
@@ -368,4 +376,14 @@ public class PlayOrderInfoEntity extends BaseEntity<PlayOrderInfoEntity> {
}
this.orderEndTime = orderEndTime;
}
public String getFirstOrder() {
if (orderRelationType == null) {
throw new IllegalStateException("订单关系类型不能为空");
}
if (orderRelationType == OrderConstant.OrderRelationType.CONTINUED) {
return OrderConstant.YesNoFlag.NO.getCode();
}
return OrderConstant.YesNoFlag.YES.getCode();
}
}

View File

@@ -126,6 +126,11 @@ public class PlayOrderDetailsReturnVo {
*/
private String placeType;
/**
* 订单关系类型FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 要求店员性别0:未知;1:男;2:女)- 仅随机单有效
*/

View File

@@ -140,6 +140,11 @@ public class PlayOrderInfoReturnVo {
*/
private String firstOrder;
/**
* 订单关系类型UNASSIGNED/FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 订单最终金额(支付金额)
*/

View File

@@ -2,6 +2,7 @@ package com.starry.admin.modules.order.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.*;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.vo.*;
@@ -51,39 +52,40 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
* @param clerkId 店员ID
* @param croupIds 优惠券ID列表
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
* @param firstOrder 是否是首单【0不是1是】
* @param relationType 订单关系类型
* @param orderAmount 订单金额
* @return com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo
* @author admin
* @since 2024/7/18 16:39
**/
ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
String firstOrder, BigDecimal orderAmount);
OrderConstant.OrderRelationType relationType, BigDecimal orderAmount);
/**
* 根据店员等级和订单金额,获取店员预计收入
*
* @param clerkId 店员ID
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
* @param firstOrder 是否是首单【0不是1是】
* @param relationType 订单关系类型
* @param orderAmount 订单金额
* @return math.BigDecimal 店员预计收入
* @author admin
* @since 2024/6/3 11:12
**/
BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal orderAmount);
BigDecimal getEstimatedRevenue(String clerkId, String placeType, OrderConstant.OrderRelationType relationType,
BigDecimal orderAmount);
/**
* 根据店员等级,获取店员提成比例
*
* @param clerkId 店员ID
* @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单)
* @param firstOrder 是否是首单【0不是1是】
* @param relationType 订单关系类型
* @return math.BigDecimal 店员预计收入
* @author admin
* @since 2024/6/3 11:12
**/
Integer getEstimatedRevenueRatio(String clerkId, String placeType, String firstOrder);
Integer getEstimatedRevenueRatio(String clerkId, String placeType, OrderConstant.OrderRelationType relationType);
/**
* 根据订单结算状态查询订单

View File

@@ -9,6 +9,7 @@ import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayClerkCustomerRelationMapper;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
@@ -37,6 +38,7 @@ 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.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayClerkCustomerRelationEntity;
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.event.OrderRevocationEvent;
@@ -68,6 +70,7 @@ import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -112,6 +115,9 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
@Resource
private PlayOrderLogInfoMapper orderLogInfoMapper;
@Resource
private PlayClerkCustomerRelationMapper clerkCustomerRelationMapper;
@Resource
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@@ -463,6 +469,9 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
if (latest == null) {
throw new CustomException("订单不存在");
}
if (OrderStatus.COMPLETED.getCode().equals(latest.getOrderStatus())) {
updateRelationOnCompletion(latest);
}
boolean forceNotify = context != null && context.isForceNotify();
boolean earningsCreated = false;
@@ -765,7 +774,16 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
entity.setOrderType(context.getOrderType().getCode());
entity.setPlaceType(context.getPlaceType().getCode());
entity.setRewardType(context.getRewardType().getCode());
entity.setFirstOrder(resolveFirstOrderFlag(context));
OrderConstant.OrderRelationType relationType = context.getOrderRelationType();
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (context.getPlaceType() == OrderConstant.PlaceType.RANDOM
&& relationType != OrderConstant.OrderRelationType.FIRST) {
throw new CustomException("随机单必须为首单关系");
}
entity.setOrderRelationType(relationType);
// entity.setFirstOrder(resolveFirstOrderFlag(context));
entity.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
entity.setBackendEntry(YesNoFlag.NO.getCode());
entity.setOrderSettlementState(OrderSettlementState.NOT_SETTLED.getCode());
@@ -812,11 +830,17 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
return;
}
entity.setAcceptBy(context.getAcceptBy());
OrderConstant.OrderRelationType relationType = resolveOrderRelationType(
context.getPurchaserBy(),
context.getAcceptBy(),
context.getOrderId(),
context.getPlaceType());
entity.setOrderRelationType(relationType);
ClerkEstimatedRevenueVo estimatedRevenue = clerkRevenueCalculator.calculateEstimatedRevenue(
context.getAcceptBy(),
context.getPaymentInfo().getCouponIds(),
context.getPlaceType().getCode(),
entity.getFirstOrder(),
relationType,
context.getPaymentInfo().getOrderMoney());
entity.setEstimatedRevenue(estimatedRevenue.getRevenueAmount());
entity.setEstimatedRevenueRatio(estimatedRevenue.getRevenueRatio());
@@ -855,21 +879,80 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
}
}
private String resolveFirstOrderFlag(OrderCreationContext context) {
if (StrUtil.isBlank(context.getAcceptBy()) || StrUtil.isBlank(context.getPurchaserBy())) {
return context.getFirstOrderString();
private OrderConstant.OrderRelationType resolveOrderRelationType(String customerId, String clerkId, String orderId,
OrderConstant.PlaceType placeType) {
if (StrUtil.isBlank(orderId)) {
throw new CustomException("订单关系计算缺少订单ID");
}
return isFirstOrder(context.getPurchaserBy(), context.getAcceptBy())
? YesNoFlag.YES.getCode()
: YesNoFlag.NO.getCode();
if (placeType == OrderConstant.PlaceType.RANDOM) {
return OrderConstant.OrderRelationType.FIRST;
}
if (StrUtil.isBlank(customerId) || StrUtil.isBlank(clerkId)) {
return OrderConstant.OrderRelationType.UNASSIGNED;
}
PlayClerkCustomerRelationEntity relation = ensureRelationForUpdate(customerId, clerkId);
if (relation == null) {
throw new CustomException("订单关系初始化失败");
}
if (StrUtil.isBlank(relation.getHasCompleted())) {
throw new CustomException("订单关系缺少完成标记");
}
if (OrderConstant.YesNoFlag.YES.getCode().equals(relation.getHasCompleted())) {
return OrderConstant.OrderRelationType.CONTINUED;
}
return OrderConstant.OrderRelationType.FIRST;
}
private boolean isFirstOrder(String customerId, String clerkId) {
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
.eq(PlayOrderInfoEntity::getPurchaserBy, customerId)
.eq(PlayOrderInfoEntity::getAcceptBy, clerkId)
.eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
return orderInfoMapper.selectCount(wrapper) == 0;
private PlayClerkCustomerRelationEntity ensureRelationForUpdate(String customerId, String clerkId) {
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException("租户信息不能为空");
}
PlayClerkCustomerRelationEntity relation = clerkCustomerRelationMapper.selectForUpdate(tenantId, customerId, clerkId);
if (relation != null) {
return relation;
}
PlayClerkCustomerRelationEntity toCreate = new PlayClerkCustomerRelationEntity();
toCreate.setId(IdUtils.getUuid());
toCreate.setPurchaserBy(customerId);
toCreate.setTenantId(tenantId);
toCreate.setAcceptBy(clerkId);
toCreate.setHasCompleted(OrderConstant.YesNoFlag.NO.getCode());
toCreate.setDeleted(Boolean.FALSE);
try {
clerkCustomerRelationMapper.insert(toCreate);
} catch (DuplicateKeyException ex) {
log.debug("Relation already exists for customer {} and clerk {}", customerId, clerkId);
clerkCustomerRelationMapper.restoreRelation(tenantId, customerId, clerkId);
}
return clerkCustomerRelationMapper.selectForUpdate(tenantId, customerId, clerkId);
}
private void updateRelationOnCompletion(PlayOrderInfoEntity order) {
if (order == null) {
throw new CustomException("订单信息不能为空");
}
if (StrUtil.isBlank(order.getPurchaserBy()) || StrUtil.isBlank(order.getAcceptBy())) {
return;
}
PlayClerkCustomerRelationEntity relation = ensureRelationForUpdate(order.getPurchaserBy(), order.getAcceptBy());
if (relation == null) {
throw new CustomException("订单关系初始化失败");
}
if (OrderConstant.YesNoFlag.YES.getCode().equals(relation.getHasCompleted())
&& relation.getFirstCompletedTime() != null
&& StrUtil.isNotBlank(relation.getFirstCompletedOrderId())) {
return;
}
relation.setHasCompleted(OrderConstant.YesNoFlag.YES.getCode());
if (relation.getFirstCompletedTime() == null) {
LocalDateTime completionTime = order.getOrderEndTime() != null ? order.getOrderEndTime() : LocalDateTime.now();
relation.setFirstCompletedTime(completionTime);
}
if (StrUtil.isBlank(relation.getFirstCompletedOrderId())) {
relation.setFirstCompletedOrderId(order.getId());
}
clerkCustomerRelationMapper.updateById(relation);
}
private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) {

View File

@@ -75,9 +75,9 @@ public class PlayOrderContinueInfoServiceImpl
entity.setReviewedTime(LocalDateTime.now());
this.baseMapper.updateById(entity);
// 添加订单信息
if ("1".equals(vo.getReviewState())) {
}
// if ("1".equals(vo.getReviewState())) {
//
// }
}
@Override

View File

@@ -18,6 +18,7 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayClerkCustomerRelationMapper;
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;
@@ -30,6 +31,7 @@ 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.PlayClerkCustomerRelationEntity;
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;
@@ -41,7 +43,9 @@ import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelAdminInfoService;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
@@ -63,6 +67,7 @@ import java.util.Random;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -79,6 +84,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
IPlayOrderInfoService {
@Resource
private PlayOrderInfoMapper orderInfoMapper;
@Resource
private PlayClerkCustomerRelationMapper clerkCustomerRelationMapper;
@Resource
private IPlayClerkUserInfoService playClerkUserInfoService;
@Resource
@@ -92,6 +100,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Resource
private IPlayPersonnelGroupInfoService playClerkGroupInfoService;
@Resource
private IPlayPersonnelAdminInfoService playPersonnelAdminInfoService;
@Resource
private IPlayCouponDetailsService playCouponDetailsService;
@@ -119,8 +130,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Override
public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
String firstOrder, BigDecimal orderAmount) {
return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, orderAmount);
OrderConstant.OrderRelationType relationType,
BigDecimal orderAmount) {
return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, relationType, orderAmount);
}
/**
@@ -135,22 +147,22 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
* @since 2024/6/3 11:12
**/
@Override
public BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal orderAmount) {
public BigDecimal getEstimatedRevenue(String clerkId, String placeType,
OrderConstant.OrderRelationType relationType, BigDecimal orderAmount) {
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
boolean continued = ensureRelationTypeForRevenue(relationType);
BigDecimal safeOrderAmount = orderAmount == null ? BigDecimal.ZERO : orderAmount;
try {
OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
switch (place) {
case SPECIFIED:
return calculateRevenue(safeOrderAmount,
isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio());
continued ? entity.getNotFirstRegularRatio() : entity.getFirstRegularRatio());
case RANDOM:
return calculateRevenue(safeOrderAmount,
isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio());
return calculateRevenue(safeOrderAmount, entity.getFirstRandomRadio());
case REWARD:
return calculateRevenue(safeOrderAmount,
isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio());
continued ? entity.getNotFirstRewardRatio() : entity.getFirstRewardRatio());
case OTHER:
default:
log.error("下单类型异常placeType={}", placeType);
@@ -163,18 +175,19 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
}
@Override
public Integer getEstimatedRevenueRatio(String clerkId, String placeType, String firstOrder) {
public Integer getEstimatedRevenueRatio(String clerkId, String placeType,
OrderConstant.OrderRelationType relationType) {
PlayClerkLevelInfoEntity entity = playClerkUserInfoService.queryLevelCommission(clerkId);
boolean isFirst = OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder);
boolean continued = ensureRelationTypeForRevenue(relationType);
try {
OrderConstant.PlaceType place = OrderConstant.PlaceType.fromCode(placeType);
switch (place) {
case SPECIFIED:
return isFirst ? entity.getFirstRegularRatio() : entity.getNotFirstRegularRatio();
return continued ? entity.getNotFirstRegularRatio() : entity.getFirstRegularRatio();
case RANDOM:
return isFirst ? entity.getFirstRandomRadio() : entity.getNotFirstRandomRadio();
return entity.getFirstRandomRadio();
case REWARD:
return isFirst ? entity.getFirstRewardRatio() : entity.getNotFirstRewardRatio();
return continued ? entity.getNotFirstRewardRatio() : entity.getFirstRewardRatio();
case OTHER:
default:
log.error("下单类型异常placeType={}", placeType);
@@ -191,6 +204,19 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
.setScale(2, RoundingMode.HALF_UP);
}
private boolean ensureRelationTypeForRevenue(OrderConstant.OrderRelationType relationType) {
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (relationType == OrderConstant.OrderRelationType.UNASSIGNED) {
throw new CustomException("未分配订单不可计算预计收益");
}
if (relationType == OrderConstant.OrderRelationType.LEGACY) {
return false;
}
return relationType == OrderConstant.OrderRelationType.CONTINUED;
}
/**
* 新增充值订单
*
@@ -338,11 +364,94 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Override
public Boolean checkFirstOrderFlag(String customId, String clerkId) {
LambdaQueryWrapper<PlayOrderInfoEntity> wrapper = Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
.eq(PlayOrderInfoEntity::getPurchaserBy, customId)
.eq(PlayOrderInfoEntity::getAcceptBy, clerkId)
.eq(PlayOrderInfoEntity::getOrderStatus, OrderStatus.COMPLETED.getCode());
return this.baseMapper.selectCount(wrapper) == 0;
if (StringUtils.isBlank(customId) || StringUtils.isBlank(clerkId)) {
throw new CustomException("用户或店员信息不能为空");
}
PlayClerkCustomerRelationEntity relation = clerkCustomerRelationMapper.selectOne(
Wrappers.lambdaQuery(PlayClerkCustomerRelationEntity.class)
.eq(PlayClerkCustomerRelationEntity::getPurchaserBy, customId)
.eq(PlayClerkCustomerRelationEntity::getAcceptBy, clerkId)
.eq(PlayClerkCustomerRelationEntity::getDeleted, Boolean.FALSE));
if (relation == null) {
return Boolean.TRUE;
}
if (StringUtils.isBlank(relation.getHasCompleted())) {
throw new CustomException("订单关系缺少完成标记");
}
return !OrderConstant.YesNoFlag.YES.getCode().equals(relation.getHasCompleted());
}
private OrderConstant.OrderRelationType resolveOrderRelationType(String customId, String clerkId, String orderId,
String placeType) {
if (StringUtils.isBlank(orderId)) {
throw new CustomException("订单关系计算缺少订单ID");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(placeType)) {
return OrderConstant.OrderRelationType.FIRST;
}
if (StringUtils.isBlank(customId) || StringUtils.isBlank(clerkId)) {
return OrderConstant.OrderRelationType.UNASSIGNED;
}
PlayClerkCustomerRelationEntity relation = ensureRelationForUpdate(customId, clerkId);
if (relation == null) {
throw new CustomException("订单关系初始化失败");
}
if (StringUtils.isBlank(relation.getHasCompleted())) {
throw new CustomException("订单关系缺少完成标记");
}
if (OrderConstant.YesNoFlag.YES.getCode().equals(relation.getHasCompleted())) {
return OrderConstant.OrderRelationType.CONTINUED;
}
return OrderConstant.OrderRelationType.FIRST;
}
private PlayClerkCustomerRelationEntity ensureRelationForUpdate(String customerId, String clerkId) {
String tenantId = SecurityUtils.getTenantId();
if (StringUtils.isBlank(tenantId)) {
throw new CustomException("租户信息不能为空");
}
PlayClerkCustomerRelationEntity relation = clerkCustomerRelationMapper.selectForUpdate(tenantId, customerId, clerkId);
if (relation != null) {
return relation;
}
PlayClerkCustomerRelationEntity toCreate = new PlayClerkCustomerRelationEntity();
toCreate.setId(IdUtils.getUuid());
toCreate.setPurchaserBy(customerId);
toCreate.setTenantId(tenantId);
toCreate.setAcceptBy(clerkId);
toCreate.setHasCompleted(OrderConstant.YesNoFlag.NO.getCode());
toCreate.setDeleted(Boolean.FALSE);
try {
clerkCustomerRelationMapper.insert(toCreate);
} catch (DuplicateKeyException ex) {
log.debug("Relation already exists for customer {} and clerk {}", customerId, clerkId);
clerkCustomerRelationMapper.restoreRelation(tenantId, customerId, clerkId);
}
return clerkCustomerRelationMapper.selectForUpdate(tenantId, customerId, clerkId);
}
private String mapFirstOrder(OrderConstant.OrderRelationType relationType) {
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
return OrderConstant.YesNoFlag.NO.getCode();
}
return OrderConstant.YesNoFlag.YES.getCode();
}
private OrderConstant.OrderRelationType normalizeRelationType(OrderConstant.OrderRelationType relationType,
String placeType) {
if (relationType == null) {
throw new CustomException("订单关系类型不能为空");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(placeType)) {
return OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
return OrderConstant.OrderRelationType.FIRST;
}
return relationType;
}
/**
@@ -380,6 +489,12 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getId, orderId);
lambdaQueryWrapper.orderByDesc(PlayOrderInfoEntity::getPurchaserTime);
PlayOrderDetailsReturnVo vo = this.baseMapper.selectJoinOne(PlayOrderDetailsReturnVo.class, lambdaQueryWrapper);
if (vo != null) {
OrderConstant.OrderRelationType relationType =
normalizeRelationType(vo.getOrderRelationType(), vo.getPlaceType());
vo.setOrderRelationType(relationType);
vo.setFirstOrder(mapFirstOrder(relationType));
}
// Privacy protection: Hide customer info for pending random orders
if (vo != null && OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType()) && OrderStatus.PENDING.getCode().equals(vo.getOrderStatus())) {
@@ -434,7 +549,14 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getBackendEntry, vo.getBackendEntry());
}
if (StringUtils.isNotBlank(vo.getFirstOrder())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getFirstOrder, vo.getFirstOrder());
String firstOrder = vo.getFirstOrder();
if (OrderConstant.YesNoFlag.YES.getCode().equals(firstOrder)) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderRelationType, OrderConstant.OrderRelationType.FIRST);
} else if (OrderConstant.YesNoFlag.NO.getCode().equals(firstOrder)) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderRelationType, OrderConstant.OrderRelationType.CONTINUED);
} else {
throw new CustomException("首单筛选条件不合法");
}
}
applyRangeFilter(lambdaQueryWrapper, vo.getPurchaserTime(), PlayOrderInfoEntity::getPurchaserTime);
applyRangeFilter(lambdaQueryWrapper, vo.getAcceptTime(), PlayOrderInfoEntity::getAcceptTime);
@@ -445,8 +567,17 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
vo.getClerkNickName());
lambdaQueryWrapper.and(
i -> i.isNull(PlayOrderInfoEntity::getAcceptBy).or().in(PlayOrderInfoEntity::getAcceptBy, clerkIdList));
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayOrderInfoReturnVo.class, lambdaQueryWrapper);
IPage<PlayOrderInfoReturnVo> page = this.baseMapper.selectJoinPage(
new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayOrderInfoReturnVo.class,
lambdaQueryWrapper);
for (PlayOrderInfoReturnVo record : page.getRecords()) {
OrderConstant.OrderRelationType relationType =
normalizeRelationType(record.getOrderRelationType(), record.getPlaceType());
record.setOrderRelationType(relationType);
record.setFirstOrder(mapFirstOrder(relationType));
}
return page;
}
@Override
@@ -464,6 +595,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
if (returnVo == null) {
throw new CustomException("订单不存在或已失效");
}
OrderConstant.OrderRelationType relationType =
normalizeRelationType(returnVo.getOrderRelationType(), returnVo.getPlaceType());
returnVo.setOrderRelationType(relationType);
returnVo.setFirstOrder(mapFirstOrder(relationType));
// 如果订单状态为退款,查询订单退款原因
if (OrderStatus.CANCELLED.getCode().equals(returnVo.getOrderStatus())) {
PlayOrderRefundInfoEntity orderRefundInfoEntity = playOrderRefundInfoService
@@ -540,6 +675,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
lambdaQueryWrapper);
for (PlayClerkOrderListReturnVo record : page.getRecords()) {
OrderConstant.OrderRelationType relationType =
normalizeRelationType(record.getOrderRelationType(), record.getPlaceType());
record.setOrderRelationType(relationType);
record.setFirstOrder(mapFirstOrder(relationType));
if (OrderConstant.PlaceType.RANDOM.getCode().equals(record.getPlaceType())
&& OrderStatus.PENDING.getCode().equals(record.getOrderStatus())) {
record.setCustomNickname("匿名用户");
@@ -559,6 +698,12 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(entity, null);
PlayCustomOrderDetailsReturnVo returnVo = this.baseMapper.selectJoinOne(PlayCustomOrderDetailsReturnVo.class,
lambdaQueryWrapper);
if (returnVo != null) {
OrderConstant.OrderRelationType relationType =
normalizeRelationType(returnVo.getOrderRelationType(), returnVo.getPlaceType());
returnVo.setOrderRelationType(relationType);
returnVo.setFirstOrder(mapFirstOrder(relationType));
}
// 如果订单状态为退款,查询订单退款原因
if (returnVo.getOrderStatus().equals(OrderStatus.CANCELLED.getCode())) {
PlayOrderRefundInfoEntity orderRefundInfoEntity = playOrderRefundInfoService
@@ -598,6 +743,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
PlayOrderEvaluateInfoEntity::getOrderId));
for (PlayCustomOrderListReturnVo record : page.getRecords()) {
record.setEvaluate(evaluateInfos.containsKey(record.getId()) ? "1" : "0");
OrderConstant.OrderRelationType relationType =
normalizeRelationType(record.getOrderRelationType(), record.getPlaceType());
record.setOrderRelationType(relationType);
record.setFirstOrder(mapFirstOrder(relationType));
}
// 获取当前顾客所有订单投诉信息,将订单评价信息转化为 map<订单ID订单ID>的结构
PlayOrderComplaintInfoEntity orderComplaintInfoEntity = new PlayOrderComplaintInfoEntity();
@@ -627,6 +776,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
* @param orderId 订单Id
**/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStateTo1(String operatorByType, String operatorBy, String acceptBy, String orderId) {
boolean isClerkOperator = OrderConstant.OperatorType.CLERK.getCode().equals(operatorByType);
boolean isAdminOperator = OrderConstant.OperatorType.ADMIN.getCode().equals(operatorByType);
@@ -658,18 +808,19 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
if (isClerkOperator && OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
validateClerkQualificationForRandomOrder(orderInfo, clerkUserInfoEntity, acceptBy);
}
String firstOrderFlag = resolveFirstOrderFlag(orderInfo.getPurchaserBy(), acceptBy);
OrderConstant.OrderRelationType relationType =
resolveOrderRelationType(orderInfo.getPurchaserBy(), acceptBy, orderId, orderInfo.getPlaceType());
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderStatus.ACCEPTED.getCode());
LocalDateTime acceptTime = LocalDateTime.now();
entity.setAcceptBy(acceptBy);
entity.setAcceptTime(acceptTime);
entity.setFirstOrder(firstOrderFlag);
entity.setOrderRelationType(relationType);
ClerkEstimatedRevenueVo estimatedRevenueVo = this.getClerkEstimatedRevenue(
acceptBy,
orderInfo.getCouponIds(),
orderInfo.getPlaceType(),
firstOrderFlag,
relationType,
orderInfo.getOrderMoney());
BigDecimal revenueAmount = estimatedRevenueVo.getRevenueAmount();
entity.setEstimatedRevenue(revenueAmount);
@@ -703,7 +854,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
orderInfo.setOrderStatus(OrderStatus.ACCEPTED.getCode());
orderInfo.setEstimatedRevenue(revenueAmount);
orderInfo.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
orderInfo.setFirstOrder(firstOrderFlag);
orderInfo.setOrderRelationType(relationType);
log.info("Order accepted successfully. orderId={}, orderNo={}, acceptBy={}, operatorByType={}",
orderId, orderInfo.getOrderNo(), acceptBy, operatorByType);
notificationSender.sendOrderMessageAsync(orderInfo);
@@ -729,18 +880,18 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
}
}
private String resolveFirstOrderFlag(String purchaserId, String clerkId) {
if (StrUtil.isBlank(purchaserId) || StrUtil.isBlank(clerkId)) {
return OrderConstant.YesNoFlag.NO.getCode();
}
Long completedCount = orderInfoMapper.selectCount(Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
.eq(PlayOrderInfoEntity::getPurchaserBy, purchaserId)
.eq(PlayOrderInfoEntity::getAcceptBy, clerkId));
return (completedCount == null || completedCount == 0)
? OrderConstant.YesNoFlag.YES.getCode()
: OrderConstant.YesNoFlag.NO.getCode();
}
// private String resolveFirstOrderFlag(String purchaserId, String clerkId) {
// if (StrUtil.isBlank(purchaserId) || StrUtil.isBlank(clerkId)) {
// return OrderConstant.YesNoFlag.NO.getCode();
// }
// Long completedCount = orderInfoMapper.selectCount(Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
// .eq(PlayOrderInfoEntity::getPurchaserBy, purchaserId)
// .eq(PlayOrderInfoEntity::getAcceptBy, clerkId));
//
// return (completedCount == null || completedCount == 0)
// ? OrderConstant.YesNoFlag.YES.getCode()
// : OrderConstant.YesNoFlag.NO.getCode();
// }
/**
* 获取通用的订单查询对象 订单作为主表 连接顾客用户表、店员用户表、商品表
@@ -911,7 +1062,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
throw new CustomException("只能操作本人订单");
}
if ("1".equals(operatorByType) && !operatorBy.equals(orderInfo.getAcceptBy())) {
throw new CustomException("只能操作本人订单");
if (!isClerkManagementOperator(operatorBy)) {
throw new CustomException("只能操作本人订单");
}
}
// 取消订单(必须订单未接单或者为开始状态)
if (!orderInfo.getOrderStatus().equals(OrderStatus.PENDING.getCode())
@@ -933,6 +1086,22 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
notificationSender.sendOrderCancelMessageAsync(orderInfo, refundReason);
}
private boolean isClerkManagementOperator(String clerkId) {
if (StringUtils.isBlank(clerkId)) {
return false;
}
PlayClerkUserInfoEntity clerkInfo = playClerkUserInfoService.selectById(clerkId);
if (clerkInfo == null || StringUtils.isBlank(clerkInfo.getSysUserId())) {
return false;
}
PlayPersonnelAdminInfoEntity adminInfo = playPersonnelAdminInfoService.selectByUserId(clerkInfo.getSysUserId());
if (adminInfo != null) {
return true;
}
PlayPersonnelGroupInfoEntity groupInfo = playClerkGroupInfoService.selectByUserId(clerkInfo.getSysUserId());
return groupInfo != null;
}
/**
* 已接单/服务中的订单强制取消,仅允许店员本人或管理员操作
*/
@@ -941,7 +1110,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
public void forceCancelOngoingOrder(String operatorByType, String operatorBy, String orderId, BigDecimal refundAmount,
String refundReason, List<String> images) {
if (!"2".equals(operatorByType)) {
throw new CustomException("禁止操作");
if (!("1".equals(operatorByType) && isClerkManagementOperator(operatorBy))) {
throw new CustomException("禁止操作");
}
}
PlayOrderInfoEntity orderInfo = this.selectOrderInfoById(orderId);
if (!OrderStatus.ACCEPTED.getCode().equals(orderInfo.getOrderStatus())

View File

@@ -31,7 +31,7 @@ public class ClerkRevenueCalculator {
String clerkId,
List<String> couponIds,
String placeType,
String firstOrder,
OrderConstant.OrderRelationType relationType,
BigDecimal orderAmount) {
PlayClerkLevelInfoEntity levelInfo = playClerkUserInfoService.queryLevelCommission(clerkId);
BigDecimal baseAmount = orderAmount == null ? BigDecimal.ZERO : orderAmount;
@@ -56,13 +56,13 @@ public class ClerkRevenueCalculator {
switch (placeTypeEnum) {
case SPECIFIED: // 指定单
fillRegularOrderRevenue(clerkId, firstOrder, baseAmount, levelInfo, estimatedRevenueVo);
fillRegularOrderRevenue(clerkId, relationType, baseAmount, levelInfo, estimatedRevenueVo);
break;
case RANDOM: // 随机单
fillRandomOrderRevenue(clerkId, firstOrder, baseAmount, levelInfo, estimatedRevenueVo);
fillRandomOrderRevenue(clerkId, relationType, baseAmount, levelInfo, estimatedRevenueVo);
break;
case REWARD: // 打赏单
fillRewardOrderRevenue(clerkId, firstOrder, baseAmount, levelInfo, estimatedRevenueVo);
fillRewardOrderRevenue(clerkId, relationType, baseAmount, levelInfo, estimatedRevenueVo);
break;
case OTHER:
default:
@@ -78,9 +78,11 @@ public class ClerkRevenueCalculator {
return estimatedRevenueVo;
}
private void fillRegularOrderRevenue(String clerkId, String firstOrder, BigDecimal orderAmount,
private void fillRegularOrderRevenue(String clerkId, OrderConstant.OrderRelationType relationType,
BigDecimal orderAmount,
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
if ("1".equals(firstOrder)) {
boolean continued = validateRelationType(relationType);
if (!continued) {
int ratio = safeRatio(levelInfo.getFirstRegularRatio(), "firstRegularRatio", clerkId);
vo.setRevenueRatio(ratio);
vo.setRevenueAmount(scaleAmount(orderAmount, ratio));
@@ -91,22 +93,19 @@ public class ClerkRevenueCalculator {
}
}
private void fillRandomOrderRevenue(String clerkId, String firstOrder, BigDecimal orderAmount,
private void fillRandomOrderRevenue(String clerkId, OrderConstant.OrderRelationType relationType,
BigDecimal orderAmount,
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
if ("1".equals(firstOrder)) {
int ratio = safeRatio(levelInfo.getFirstRandomRadio(), "firstRandomRatio", clerkId);
vo.setRevenueRatio(ratio);
vo.setRevenueAmount(scaleAmount(orderAmount, ratio));
} else {
int ratio = safeRatio(levelInfo.getNotFirstRandomRadio(), "notFirstRandomRatio", clerkId);
vo.setRevenueRatio(ratio);
vo.setRevenueAmount(scaleAmount(orderAmount, ratio));
}
int ratio = safeRatio(levelInfo.getFirstRandomRadio(), "firstRandomRatio", clerkId);
vo.setRevenueRatio(ratio);
vo.setRevenueAmount(scaleAmount(orderAmount, ratio));
}
private void fillRewardOrderRevenue(String clerkId, String firstOrder, BigDecimal orderAmount,
private void fillRewardOrderRevenue(String clerkId, OrderConstant.OrderRelationType relationType,
BigDecimal orderAmount,
PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) {
if ("1".equals(firstOrder)) {
boolean continued = validateRelationType(relationType);
if (!continued) {
int ratio = safeRatio(levelInfo.getFirstRewardRatio(), "firstRewardRatio", clerkId);
vo.setRevenueRatio(ratio);
vo.setRevenueAmount(scaleAmount(orderAmount, ratio));
@@ -125,6 +124,19 @@ public class ClerkRevenueCalculator {
return ratio;
}
private boolean validateRelationType(OrderConstant.OrderRelationType relationType) {
if (relationType == null) {
throw new IllegalArgumentException("订单关系类型不能为空");
}
if (relationType == OrderConstant.OrderRelationType.UNASSIGNED) {
throw new IllegalArgumentException("未分配订单不可计算预计收益");
}
if (relationType == OrderConstant.OrderRelationType.LEGACY) {
return false;
}
return relationType == OrderConstant.OrderRelationType.CONTINUED;
}
private BigDecimal scaleAmount(BigDecimal baseAmount, int ratio) {
return baseAmount
.multiply(BigDecimal.valueOf(ratio).divide(new BigDecimal(100), 4, RoundingMode.HALF_UP))

View File

@@ -226,12 +226,12 @@ public class PlayClerkPerformanceController {
customIds.add(orderInfoEntity.getPurchaserBy());
finalAmount = finalAmount.add(orderInfoEntity.getFinalAmount());
orderMoney = orderMoney.add(orderInfoEntity.getOrderMoney());
if ("1".equals(orderInfoEntity.getFirstOrder())) {
orderFirstAmount = orderFirstAmount.add(orderInfoEntity.getFinalAmount());
} else {
orderContinueNumber++;
orderTotalAmount = orderTotalAmount.add(orderInfoEntity.getFinalAmount());
}
// if ("1".equals(orderInfoEntity.getFirstOrder())) {
// orderFirstAmount = orderFirstAmount.add(orderInfoEntity.getFinalAmount());
// } else {
// orderContinueNumber++;
// orderTotalAmount = orderTotalAmount.add(orderInfoEntity.getFinalAmount());
// }
if ("2".equals(orderInfoEntity.getPlaceType())) {
orderRewardAmount = orderRewardAmount.add(orderInfoEntity.getFinalAmount());
}
@@ -303,12 +303,12 @@ public class PlayClerkPerformanceController {
customIds.add(orderInfoEntity.getPurchaserBy());
finalAmount = finalAmount.add(orderInfoEntity.getFinalAmount());
orderMoney = orderMoney.add(orderInfoEntity.getOrderMoney());
if ("1".equals(orderInfoEntity.getFirstOrder())) {
orderFirstAmount = orderFirstAmount.add(orderInfoEntity.getFinalAmount());
} else {
orderContinueNumber++;
orderTotalAmount = orderTotalAmount.add(orderInfoEntity.getFinalAmount());
}
// if ("1".equals(orderInfoEntity.getFirstOrder())) {
// orderFirstAmount = orderFirstAmount.add(orderInfoEntity.getFinalAmount());
// } else {
// orderContinueNumber++;
// orderTotalAmount = orderTotalAmount.add(orderInfoEntity.getFinalAmount());
// }
if ("2".equals(orderInfoEntity.getPlaceType())) {
orderRewardAmount = orderRewardAmount.add(orderInfoEntity.getFinalAmount());
}

View File

@@ -33,11 +33,9 @@ import com.starry.admin.utils.SecurityUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -91,12 +89,12 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
completedOrderCount++;
customIds.add(orderInfoEntity.getPurchaserBy());
finalAmount = finalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
if (OrderConstant.YesNoFlag.YES.getCode().equals(orderInfoEntity.getFirstOrder())) {
orderFirstAmount = orderFirstAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
} else {
orderContinueNumber++;
orderTotalAmount = orderTotalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
}
// if (OrderConstant.YesNoFlag.YES.getCode().equals(orderInfoEntity.getFirstOrder())) {
// orderFirstAmount = orderFirstAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
// } else {
// orderContinueNumber++;
// orderTotalAmount = orderTotalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
// }
if (OrderConstant.PlaceType.REWARD.getCode().equals(orderInfoEntity.getPlaceType())) {
orderRewardAmount = orderRewardAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
}
@@ -202,7 +200,7 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
responseVo.setSnapshot(snapshot);
responseVo.setComposition(buildComposition(snapshot));
if (Boolean.TRUE.equals(vo.getIncludeTrend())) {
responseVo.setTrend(buildTrend(orders, range,
responseVo.setTrend(buildTrend(clerk.getId(), orders, range,
vo.getTrendDays() == null || vo.getTrendDays() <= 0 ? 7 : vo.getTrendDays()));
responseVo.setTrendDimension("DAY");
} else {
@@ -226,8 +224,8 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
return profile;
}
private List<ClerkPerformanceTrendPointVo> buildTrend(List<PlayOrderInfoEntity> orders, DateRange range,
int trendDays) {
private List<ClerkPerformanceTrendPointVo> buildTrend(String clerkId, List<PlayOrderInfoEntity> orders,
DateRange range, int trendDays) {
List<PlayOrderInfoEntity> completedOrders = orders.stream()
.filter(this::isCompletedOrder)
.collect(Collectors.toList());
@@ -277,11 +275,12 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
for (PlayOrderInfoEntity order : orders) {
BigDecimal finalAmount = defaultZero(order.getFinalAmount());
gmv = gmv.add(finalAmount);
if (OrderConstant.YesNoFlag.YES.getCode().equals(order.getFirstOrder())) {
firstAmount = firstAmount.add(finalAmount);
} else {
OrderConstant.OrderRelationType relationType = normalizeRelationType(order);
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
continuedAmount = continuedAmount.add(finalAmount);
continuedCount++;
} else {
firstAmount = firstAmount.add(finalAmount);
}
}
point.setGmv(gmv);
@@ -443,6 +442,23 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
return defaultZero(sum);
}
private OrderConstant.OrderRelationType normalizeRelationType(PlayOrderInfoEntity order) {
if (order == null) {
throw new ServiceException("订单不能为空,无法统计首单/续单");
}
OrderConstant.OrderRelationType relationType = order.getOrderRelationType();
if (relationType == null) {
throw new ServiceException("订单关系类型不能为空,无法统计首单/续单");
}
if (OrderConstant.PlaceType.RANDOM.getCode().equals(order.getPlaceType())) {
return OrderConstant.OrderRelationType.FIRST;
}
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
return OrderConstant.OrderRelationType.FIRST;
}
return relationType;
}
private ClerkPerformanceSnapshotVo buildSnapshot(PlayClerkUserInfoEntity clerk, List<PlayOrderInfoEntity> orders,
Map<String, String> levelNameMap, Map<String, String> groupNameMap, String startTime, String endTime) {
ClerkPerformanceSnapshotVo snapshot = new ClerkPerformanceSnapshotVo();
@@ -461,23 +477,20 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
int continuedCount = 0;
int refundCount = 0;
int expiredCount = 0;
Map<String, Integer> userOrderMap = new HashMap<>();
int orderCount = 0;
for (PlayOrderInfoEntity order : orders) {
if (!isCompletedOrder(order)) {
continue;
}
orderCount++;
List<PlayOrderInfoEntity> completedOrders = orders.stream()
.filter(this::isCompletedOrder)
.collect(Collectors.toList());
int orderCount = completedOrders.size();
Set<String> userIds = new HashSet<>();
Set<String> continuedUserIds = new HashSet<>();
for (PlayOrderInfoEntity order : completedOrders) {
BigDecimal finalAmount = defaultZero(order.getFinalAmount());
gmv = gmv.add(finalAmount);
userOrderMap.merge(order.getPurchaserBy(), 1, Integer::sum);
if (OrderConstant.YesNoFlag.YES.getCode().equals(order.getFirstOrder())) {
firstCount++;
firstAmount = firstAmount.add(finalAmount);
} else {
continuedCount++;
continuedAmount = continuedAmount.add(finalAmount);
String customerId = order.getPurchaserBy();
if (StrUtil.isBlank(customerId)) {
throw new ServiceException("订单缺少顾客信息,无法统计首单/续单");
}
userIds.add(customerId);
if (OrderConstant.PlaceType.REWARD.getCode().equals(order.getPlaceType())) {
rewardAmount = rewardAmount.add(finalAmount);
}
@@ -488,9 +501,18 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
if (OrderConstant.OrdersExpiredState.EXPIRED.getCode().equals(order.getOrdersExpiredState())) {
expiredCount++;
}
OrderConstant.OrderRelationType relationType = normalizeRelationType(order);
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
continuedCount++;
continuedAmount = continuedAmount.add(finalAmount);
continuedUserIds.add(customerId);
} else {
firstCount++;
firstAmount = firstAmount.add(finalAmount);
}
}
int userCount = userOrderMap.size();
int continuedUserCount = (int) userOrderMap.values().stream().filter(cnt -> cnt > 1).count();
int userCount = userIds.size();
int continuedUserCount = continuedUserIds.size();
BigDecimal estimatedRevenue = calculateEarningsAmount(clerk.getId(), orders, startTime, endTime);
snapshot.setGmv(gmv);
snapshot.setFirstOrderAmount(firstAmount);

View File

@@ -235,7 +235,7 @@ public class WxCustomController {
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.BALANCE)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(userId)
.commodityInfo(CommodityInfo.builder()
@@ -304,7 +304,7 @@ public class WxCustomController {
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(customId)
.commodityInfo(CommodityInfo.builder()
@@ -376,7 +376,7 @@ public class WxCustomController {
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true)
.orderRelationType(OrderConstant.OrderRelationType.FIRST)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(customId)
.commodityInfo(CommodityInfo.builder()

View File

@@ -122,25 +122,27 @@ public class WxOrderInfoController {
if (vo == null) {
throw new CustomException("订单不存在");
}
// Privacy protection: Hide customer info for pending random orders that current clerk hasn't accepted
String currentClerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
if (OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType()) && OrderConstant.OrderStatus.PENDING.getCode().equals(vo.getOrderStatus())) {
// Random order pending - customer info already hidden by service layer
vo.setWeiChatCode("");
} else if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(currentClerkId)) {
// Order accepted by another clerk - hide WeChat and customer info
vo.setWeiChatCode("");
vo.setCustomNickname("匿名用户");
vo.setCustomAvatar("");
vo.setCustomId("");
}
if(vo.getOrderStatus().equals("4")){
vo.setWeiChatCode("");
if (OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType())) {
boolean acceptedByCurrentClerk = StringUtils.isNotEmpty(vo.getAcceptBy())
&& vo.getAcceptBy().equals(currentClerkId);
if (!acceptedByCurrentClerk) {
maskRandomOrderForNonOwner(vo, currentClerkId);
}
}
return R.ok(vo);
}
private void maskRandomOrderForNonOwner(PlayOrderDetailsReturnVo vo, String currentClerkId) {
vo.setWeiChatCode("");
vo.setCustomNickname("匿名用户");
vo.setCustomAvatar("");
vo.setCustomId("");
if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(currentClerkId)) {
vo.setOrderStatus("");
}
}
/**
* 店员查询打赏动态
*

View File

@@ -34,6 +34,16 @@ public class PlayClerkOrderDetailsReturnVo {
*/
private String placeType;
/**
* 订单关系类型FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 是否是首单【0不是1是】
*/
private String firstOrder;
/**
* 商品数量
*/

View File

@@ -34,6 +34,11 @@ public class PlayClerkOrderListReturnVo {
*/
private String placeType;
/**
* 订单关系类型FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 是否是首单【0不是1是】
*/

View File

@@ -35,6 +35,16 @@ public class PlayCustomOrderDetailsReturnVo {
*/
private String placeType;
/**
* 订单关系类型FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 是否是首单【0不是1是】
*/
private String firstOrder;
/**
* 商品数量
*/

View File

@@ -35,6 +35,16 @@ public class PlayCustomOrderListReturnVo {
*/
private String placeType;
/**
* 订单关系类型FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 是否是首单【0不是1是】
*/
private String firstOrder;
/**
* 商品数量
*/

View File

@@ -32,6 +32,11 @@ public class PlayRandomOrderInfoReturnVo {
*/
private String placeType;
/**
* 订单关系类型FIRST/CONTINUED/NEUTRAL
*/
private com.starry.admin.modules.order.module.constant.OrderConstant.OrderRelationType orderRelationType;
/**
* 是否是首单【0不是1是】
*/

View File

@@ -179,7 +179,7 @@ public class WxBlindBoxOrderService {
.orderType(OrderConstant.OrderType.BLIND_BOX_PURCHASE)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.GIFT)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(customer.getId())
.commodityInfo(CommodityInfo.builder()

View File

@@ -108,7 +108,7 @@ public class WxGiftOrderService {
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.GIFT)
.isFirstOrder(true)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.creatorActor(OrderActor.CUSTOMER)
.creatorId(purchaserId)
.commodityInfo(CommodityInfo.builder()

View File

@@ -0,0 +1,67 @@
CREATE TABLE play_clerk_customer_relation (
id VARCHAR(64) NOT NULL COMMENT '主键',
purchaser_by VARCHAR(64) NOT NULL COMMENT '顾客ID',
tenant_id VARCHAR(32) DEFAULT NULL COMMENT '租户ID',
accept_by VARCHAR(64) NOT NULL COMMENT '店员ID',
has_completed VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '是否有已完成历史 0否 1是',
first_completed_order_id VARCHAR(64) DEFAULT NULL COMMENT '首次完成订单ID',
first_completed_time DATETIME DEFAULT NULL COMMENT '首次完成时间',
created_by VARCHAR(64) DEFAULT NULL COMMENT '创建人',
created_time DATETIME DEFAULT NULL COMMENT '创建时间',
updated_by VARCHAR(64) DEFAULT NULL COMMENT '更新人',
updated_time DATETIME DEFAULT NULL COMMENT '更新时间',
deleted TINYINT(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 1已删除 0未删除',
PRIMARY KEY (id),
UNIQUE KEY uk_clerk_customer_relation (tenant_id, purchaser_by, accept_by)
) COMMENT '客户-店员关系(完成历史)';
INSERT INTO play_clerk_customer_relation (
id,
purchaser_by,
tenant_id,
accept_by,
has_completed,
first_completed_time,
created_time,
updated_time,
deleted
)
SELECT
REPLACE(UUID(), '-', ''),
purchaser_by,
tenant_id,
accept_by,
'1',
MIN(COALESCE(order_end_time, purchaser_time)),
NOW(),
NOW(),
0
FROM play_order_info
WHERE order_status = '3'
AND purchaser_by IS NOT NULL
AND purchaser_by <> ''
AND tenant_id IS NOT NULL
AND tenant_id <> ''
AND accept_by IS NOT NULL
AND accept_by <> ''
GROUP BY tenant_id, purchaser_by, accept_by;
ALTER TABLE play_order_info
ADD COLUMN order_relation_type VARCHAR(32) NOT NULL DEFAULT 'LEGACY';
UPDATE play_order_info o
LEFT JOIN (
SELECT purchaser_by, accept_by, MIN(purchaser_time) AS first_completed_time
FROM play_order_info
WHERE order_status = '3'
GROUP BY purchaser_by, accept_by
) h ON o.purchaser_by = h.purchaser_by AND o.accept_by = h.accept_by
SET o.order_relation_type = CASE
WHEN o.place_type = '1' THEN 'FIRST'
WHEN o.accept_by IS NULL OR o.accept_by = '' THEN 'LEGACY'
WHEN h.first_completed_time IS NOT NULL AND o.purchaser_time > h.first_completed_time THEN 'CONTINUED'
ELSE 'FIRST'
END
WHERE o.order_relation_type IS NULL
OR o.order_relation_type = ''
OR o.order_relation_type = 'LEGACY';

View File

@@ -0,0 +1,197 @@
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.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.PlayOrderInfoEntity;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
class LegacyOrderIntegrationTest extends WxCustomOrderApiTestSupport {
private String orderIdToCleanup;
@AfterEach
void tearDown() {
if (orderIdToCleanup != null) {
ensureTenantContext();
playOrderInfoService.removeById(orderIdToCleanup);
orderIdToCleanup = null;
}
}
@Test
void getLegacyOrderDetails_Admin_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
mockMvc.perform(get("/order/order/" + legacyOrder.getId())
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(USER_HEADER, DEFAULT_USER)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.orderRelationType").value("LEGACY"));
}
@Test
void listByPage_Admin_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
ObjectNode payload = objectMapper.createObjectNode();
payload.put("pageNum", 1);
payload.put("pageSize", 10);
payload.put("orderNo", legacyOrder.getOrderNo());
String response = mockMvc.perform(post("/order/order/listByPage")
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(USER_HEADER, ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn().getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(response);
JsonNode data = root.path("data");
JsonNode records = data.isArray() ? data : data.path("records");
String type = records.get(0).path("orderRelationType").asText();
assertThat(type).isEqualTo("LEGACY");
}
private String ensureCustomerToken() {
resetCustomerBalance();
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
return customerToken;
}
@Test
void queryById_WxCustom_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
String customerToken = ensureCustomerToken();
mockMvc.perform(get("/wx/custom/order/queryById")
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.param("id", legacyOrder.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.orderRelationType").value("LEGACY"));
}
@Test
void queryByPage_WxCustom_ShouldReturnLegacyType() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
String customerToken = ensureCustomerToken();
ObjectNode payload = objectMapper.createObjectNode();
payload.put("pageNum", 1);
payload.put("pageSize", 20);
String response = mockMvc.perform(post("/wx/custom/order/queryByPage")
.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().getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(response);
JsonNode records = root.path("data").isArray() ? root.path("data") : root.path("data").path("records");
boolean found = false;
for (JsonNode node : records) {
if (legacyOrder.getId().equals(node.path("id").asText())) {
assertThat(node.path("orderRelationType").asText()).isEqualTo("LEGACY");
found = true;
break;
}
}
assertThat(found).withFailMessage("Legacy order not found in customer list").isTrue();
}
@Test
void calculateEstimatedRevenue_ForLegacyOrder_ShouldNotFail() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity legacyOrder = createLegacyOrder();
BigDecimal revenue = playOrderInfoService.getEstimatedRevenue(
legacyOrder.getAcceptBy(),
legacyOrder.getPlaceType(),
legacyOrder.getOrderRelationType(),
legacyOrder.getOrderMoney()
);
assertThat(revenue).isNotNull();
assertThat(revenue).isGreaterThanOrEqualTo(BigDecimal.ZERO);
}
@Test
void calculateEstimatedRevenue_ForUnassignedOrder_ShouldThrowException() {
ensureTenantContext();
try {
playOrderInfoService.getEstimatedRevenue(
ApiTestDataSeeder.DEFAULT_CLERK_ID,
"0", // Specified
OrderConstant.OrderRelationType.UNASSIGNED,
new BigDecimal("100.00")
);
assertThat(true).withFailMessage("Should have thrown exception for UNASSIGNED relation type").isFalse();
} catch (Exception e) {
assertThat(e).isInstanceOf(RuntimeException.class);
assertThat(e.getMessage()).contains("未分配订单不可计算预计收益");
}
}
private PlayOrderInfoEntity createLegacyOrder() {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
String id = "legacy-" + IdUtils.getUuid();
order.setId(id);
order.setOrderNo("LEGACY" + IdUtils.getUuid().substring(0, 6));
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
order.setOrderStatus("3"); // Completed
order.setOrderType("2"); // Normal
order.setPlaceType("0"); // Specified
order.setOrderRelationType(OrderConstant.OrderRelationType.LEGACY);
order.setOrderMoney(new BigDecimal("100.00"));
order.setFinalAmount(new BigDecimal("100.00"));
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
order.setPurchaserTime(LocalDateTime.now().minusDays(100));
order.setCreatedTime(new Date());
order.setUpdatedTime(new Date());
order.setDeleted(false);
order.setPayMethod("2");
order.setUseCoupon("0");
order.setBackendEntry("0");
order.setSex("2");
order.setCommodityType("1");
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
order.setCommodityName("Legacy Service");
order.setCommodityPrice(new BigDecimal("100.00"));
order.setCommodityNumber("1");
order.setRefundType("0");
order.setRefundAmount(BigDecimal.ZERO);
playOrderInfoService.save(order);
orderIdToCleanup = id;
return order;
}
}

View File

@@ -42,6 +42,32 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final BigDecimal DEFAULT_AMOUNT = new BigDecimal("188.00");
private static final int ID_SUFFIX_LENGTH = 6;
private static final String FIRST_FLAG = OrderConstant.YesNoFlag.YES.getCode();
private static final String CONTINUED_FLAG = OrderConstant.YesNoFlag.NO.getCode();
private static final String RELATION_FIRST = OrderConstant.OrderRelationType.FIRST.getCode();
private static final String RELATION_CONTINUED = OrderConstant.OrderRelationType.CONTINUED.getCode();
private static final String RELATION_UNASSIGNED = OrderConstant.OrderRelationType.UNASSIGNED.getCode();
private static final String RELATION_NEUTRAL = OrderConstant.OrderRelationType.NEUTRAL.getCode();
private static final String RELATION_LEGACY = OrderConstant.OrderRelationType.LEGACY.getCode();
private static final String FIELD_ORDER_RELATION_TYPE = "orderRelationType";
private static final String FIELD_FIRST_ORDER = "firstOrder";
private static final String PREFIX_FIRST_RELATION = "FR";
private static final String PREFIX_CONTINUED_RELATION = "CR";
private static final String PREFIX_UNASSIGNED_RELATION = "UR";
private static final String PREFIX_NEUTRAL_RELATION = "NR";
private static final String PREFIX_RANDOM_RELATION = "RR";
private static final String PREFIX_FIRST_FILTER_YES = "FY";
private static final String PREFIX_FIRST_FILTER_NO = "FN";
private static final String PREFIX_BACKWARD_COMPAT = "BC";
private static final String TOKEN_LEGACY_CLIENT = "legacy-client";
private static final String TOKEN_LEGACY_CONTINUED = "legacy-continued";
private static final long HOURS_OFFSET_ONE = 1L;
private static final long HOURS_OFFSET_TWO = 2L;
private static final long HOURS_OFFSET_THREE = 3L;
private static final long HOURS_OFFSET_FOUR = 4L;
private static final long HOURS_OFFSET_FIVE = 5L;
private static final long MINUTES_OFFSET_TEN = 10L;
@Autowired
private IPlayOrderInfoService orderInfoService;
@@ -87,20 +113,20 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
order.setPayMethod("2");
order.setUseCoupon("1");
order.setBackendEntry("1");
order.setFirstOrder("0");
order.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
order.setSex("2");
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
});
persistOrder(marker, "noise", reference.minusDays(3), order -> {
PlayOrderInfoEntity noiseOrder = persistOrder(marker, "noise", reference.minusDays(3), order -> {
order.setOrderStatus("0");
order.setPlaceType("0");
order.setPayMethod("0");
order.setUseCoupon("0");
order.setBackendEntry("0");
order.setFirstOrder("1");
order.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
order.setGroupId(marker + "-grp");
order.setSex("1");
order.setAcceptBy(null);
@@ -149,9 +175,13 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
// firstOrder filter
ObjectNode firstOrderPayload = queryWithMarker(marker);
firstOrderPayload.put("firstOrder", matching.getFirstOrder());
firstOrderPayload.put("firstOrder", OrderConstant.YesNoFlag.NO.getCode());
assertFilterMatches(firstOrderPayload, matching.getId());
ObjectNode firstOrderYesPayload = queryWithMarker(marker);
firstOrderYesPayload.put("firstOrder", OrderConstant.YesNoFlag.YES.getCode());
assertFilterMatches(firstOrderYesPayload, noiseOrder.getId());
// groupId filter
ObjectNode groupPayload = queryWithMarker(marker);
groupPayload.put("groupId", matching.getGroupId());
@@ -192,7 +222,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
combinedPayload.put("payMethod", matching.getPayMethod());
combinedPayload.put("useCoupon", matching.getUseCoupon());
combinedPayload.put("backendEntry", matching.getBackendEntry());
combinedPayload.put("firstOrder", matching.getFirstOrder());
combinedPayload.put("firstOrder", OrderConstant.YesNoFlag.NO.getCode());
combinedPayload.put("groupId", matching.getGroupId());
combinedPayload.put("sex", matching.getSex());
combinedPayload.set("purchaserTime", range(
@@ -207,6 +237,191 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
assertFilterMatches(combinedPayload, matching.getId());
}
/**
* 验证首单关系在列表中映射为 firstOrder=1。
*/
@Test
void listByPage_mapsFirstOrderFlagForFirstRelationType() throws Exception {
ensureTenantContext();
String marker = (PREFIX_FIRST_RELATION + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_FOUR).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, "first", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_FIRST, FIRST_FLAG);
}
/**
* 验证续单关系在列表中映射为 firstOrder=0。
*/
@Test
void listByPage_mapsFirstOrderFlagForContinuedRelationType() throws Exception {
ensureTenantContext();
String marker = (PREFIX_CONTINUED_RELATION + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_THREE).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, "continued", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_CONTINUED, CONTINUED_FLAG);
}
/**
* 验证未分配订单在列表中映射为 firstOrder=1。
*/
@Test
void listByPage_mapsFirstOrderFlagForUnassignedRelationType() throws Exception {
ensureTenantContext();
String marker = (PREFIX_UNASSIGNED_RELATION + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_TWO).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, "unassigned", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.UNASSIGNED);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
entity.setAcceptBy(null);
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_UNASSIGNED, FIRST_FLAG);
}
/**
* 验证历史存量关系在列表中映射为首单。
*/
@Test
void listByPage_mapsFirstOrderFlagForLegacyRelationType() throws Exception {
ensureTenantContext();
String marker = (RELATION_LEGACY + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_TWO).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, "legacy", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.LEGACY);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_LEGACY, FIRST_FLAG);
}
/**
* 验证中性关系在列表展示中被视为首单。
*/
@Test
void listByPage_normalizesNeutralRelationTypeToFirst() throws Exception {
ensureTenantContext();
String marker = (PREFIX_NEUTRAL_RELATION + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(2).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, "neutral", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.NEUTRAL);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_FIRST, FIRST_FLAG);
}
/**
* 验证随机单强制展示为首单关系。
*/
@Test
void listByPage_normalizesRandomRelationTypeToFirst() throws Exception {
ensureTenantContext();
String marker = (PREFIX_RANDOM_RELATION + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_ONE).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, "random", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
entity.setPlaceType(OrderConstant.PlaceType.RANDOM.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_FIRST, FIRST_FLAG);
}
/**
* 验证首单筛选仅返回首单关系订单。
*/
@Test
void listByPage_filtersFirstOrderYesMatchesFirstRelationType() throws Exception {
ensureTenantContext();
String marker = (PREFIX_FIRST_FILTER_YES + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_FIVE).withNano(0);
PlayOrderInfoEntity firstOrder = persistOrder(marker, "first", reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
});
persistOrder(marker, "continued", reference.plusMinutes(MINUTES_OFFSET_TEN), entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
});
ObjectNode payload = queryWithMarker(marker);
payload.put("firstOrder", FIRST_FLAG);
assertFilterMatches(payload, firstOrder.getId());
}
/**
* 验证续单筛选仅返回续单关系订单。
*/
@Test
void listByPage_filtersFirstOrderNoMatchesContinuedRelationType() throws Exception {
ensureTenantContext();
String marker = (PREFIX_FIRST_FILTER_NO + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_FIVE).withNano(0);
persistOrder(marker, "first", reference, entity -> entity.setOrderRelationType(OrderConstant.OrderRelationType.FIRST));
PlayOrderInfoEntity continuedOrder = persistOrder(marker, "continued", reference.plusMinutes(MINUTES_OFFSET_TEN), entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
});
ObjectNode payload = queryWithMarker(marker);
payload.put("firstOrder", CONTINUED_FLAG);
assertFilterMatches(payload, continuedOrder.getId());
}
/**
* 验证未传首单筛选时,列表仍返回首单字段(向后兼容)。
*/
@Test
void listByPage_includesFirstOrderFieldWhenFilterNotProvided() throws Exception {
ensureTenantContext();
String marker = (PREFIX_BACKWARD_COMPAT + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_TWO).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, TOKEN_LEGACY_CLIENT, reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_FIRST, FIRST_FLAG);
}
/**
* 验证续单关系在旧字段中仍保持兼容输出。
*/
@Test
void listByPage_includesFirstOrderFieldForContinuedRelation() throws Exception {
ensureTenantContext();
String marker = (PREFIX_BACKWARD_COMPAT + IdUtils.getUuid().replace("-", "").substring(0, ID_SUFFIX_LENGTH)).toUpperCase();
LocalDateTime reference = LocalDateTime.now().minusHours(HOURS_OFFSET_THREE).withNano(0);
PlayOrderInfoEntity order = persistOrder(marker, TOKEN_LEGACY_CONTINUED, reference, entity -> {
entity.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
entity.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
});
JsonNode record = fetchSingleRecord(queryWithMarker(marker), order.getId());
assertRelationFields(record, RELATION_CONTINUED, CONTINUED_FLAG);
}
@Test
void listByPage_keywordFiltersByOrderNoOrClerkName() throws Exception {
ensureTenantContext();
@@ -736,7 +951,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
order.setOrderType("2");
order.setPlaceType("1");
order.setRewardType("0");
order.setFirstOrder("0");
order.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
order.setRefundType("0");
order.setRefundAmount(BigDecimal.ZERO);
order.setRefundReason(null);
@@ -846,6 +1061,25 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
.isEqualTo(expectedOrderId);
}
private JsonNode fetchSingleRecord(ObjectNode payload, String expectedOrderId) throws Exception {
RecordsResponse response = executeList(payload);
JsonNode records = response.records;
assertThat(records.isArray())
.withFailMessage("Records payload is not an array for body=%s | response=%s", payload, response.rawResponse)
.isTrue();
for (JsonNode record : records) {
if (expectedOrderId.equals(record.path("id").asText())) {
return record;
}
}
throw new AssertionError("未找到订单 " + expectedOrderId);
}
private void assertRelationFields(JsonNode record, String relationCode, String firstOrderFlag) {
assertThat(record.path(FIELD_ORDER_RELATION_TYPE).asText()).isEqualTo(relationCode);
assertThat(record.path(FIELD_FIRST_ORDER).asText()).isEqualTo(firstOrderFlag);
}
private RecordsResponse executeList(ObjectNode payload) throws Exception {
MvcResult result = mockMvc.perform(post("/order/order/listByPage")
.contentType(MediaType.APPLICATION_JSON)

View File

@@ -362,7 +362,7 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
order.setOrderNo("FOCUS-" + IdUtils.getUuid().substring(0, 4));
order.setUseCoupon("1");
order.setBackendEntry("1");
order.setFirstOrder("1");
order.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
order.setGroupId("group-focus");
order.setSex("1");
order.setPurchaserTime(base.plusMinutes(5));
@@ -374,7 +374,7 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
order.setPlaceType(OrderConstant.PlaceType.RANDOM.getCode());
order.setUseCoupon("0");
order.setBackendEntry("0");
order.setFirstOrder("0");
order.setOrderRelationType(OrderConstant.OrderRelationType.CONTINUED);
order.setGroupId("group-noise");
order.setSex("2");
});
@@ -384,7 +384,7 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
payload.put("orderNo", target.getOrderNo().substring(0, 6));
payload.put("useCoupon", "1");
payload.put("backendEntry", "1");
payload.put("firstOrder", "1");
payload.put("firstOrder", OrderConstant.YesNoFlag.YES.getCode());
payload.put("groupId", "group-focus");
payload.put("sex", "1");
payload.set("purchaserTime", range(target.getPurchaserTime().minusMinutes(1), target.getPurchaserTime().plusMinutes(1)));
@@ -455,7 +455,7 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
order.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
order.setRewardType("0");
order.setFirstOrder("0");
order.setOrderRelationType(OrderConstant.OrderRelationType.FIRST);
order.setRefundType("0");
order.setRefundAmount(BigDecimal.ZERO);
order.setOrderMoney(new BigDecimal("99.00"));

View File

@@ -15,8 +15,11 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.task.OverdueOrderHandlerTask;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelAdminInfoService;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
@@ -39,6 +42,20 @@ import org.springframework.test.web.servlet.MvcResult;
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
private static final String DEFAULT_WECHAT_CODE = "apitest-customer-wx";
private static final String CANCEL_REASON = "API random cancel by role";
private static final int CODE_OK = 200;
private static final int CODE_FAIL = 500;
private static final int SINGLE_QUANTITY = 1;
private static final String CLERK_STATE_ACTIVE = "1";
private static final String CLERK_ONLINE_ENABLED = "1";
private static final String CLERK_LISTING_ENABLED = "1";
private static final String CLERK_DISPLAY_ENABLED = "1";
private static final String CLERK_RANDOM_ORDER_ENABLED = "1";
private static final String CLERK_FIXING_LEVEL = "1";
private static final String CLERK_ONBOARDING_STATE = "1";
private static final BigDecimal HIGH_BALANCE = new BigDecimal("1000.00");
@MockBean
private NotificationSender notificationSender;
@@ -51,6 +68,9 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
@org.springframework.beans.factory.annotation.Autowired
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
@org.springframework.beans.factory.annotation.Autowired
private IPlayPersonnelAdminInfoService playPersonnelAdminInfoService;
@Test
void randomOrderFailsWhenBalanceInsufficient() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
@@ -243,6 +263,230 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
}
}
@Test
void randomOrderCancellationPermissions_pendingOrder() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String nonManagerClerkId = "clerk-apitest-nonmgr-" + IdUtils.getUuid().substring(0, 6);
String adminClerkId = "clerk-apitest-admin-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserId = "apitest-admin-user-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserCode = "apitest-admin-code-" + IdUtils.getUuid().substring(0, 6);
String customerToken = null;
String leaderToken = null;
String nonManagerToken = null;
String adminToken = null;
try {
resetCustomerBalance();
setCustomerBalance(HIGH_BALANCE);
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
leaderToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, leaderToken);
nonManagerToken = createClerk(nonManagerClerkId, null, null);
adminToken = createClerk(adminClerkId, adminSysUserId, null);
createAdminInfo(adminSysUserId, adminSysUserCode);
String pendingOrderId = createRandomOrder("API random pending " + IdUtils.getUuid(), customerToken);
assertCancelAsClerk(pendingOrderId, nonManagerToken, CODE_FAIL);
String leaderOrderId = createRandomOrder("API random pending leader " + IdUtils.getUuid(), customerToken);
assertCancelAsClerk(leaderOrderId, leaderToken, CODE_OK);
assertOrderStatus(leaderOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
String adminOrderId = createRandomOrder("API random pending admin " + IdUtils.getUuid(), customerToken);
assertCancelAsClerk(adminOrderId, adminToken, CODE_OK);
assertOrderStatus(adminOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
} finally {
if (nonManagerClerkId != null) {
removeClerk(nonManagerClerkId);
}
if (adminClerkId != null) {
removeClerk(adminClerkId);
}
removeAdminInfo(adminSysUserId);
CustomSecurityContextHolder.remove();
}
}
@Test
void randomOrderCancellationPermissions_acceptedOrder() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String nonManagerClerkId = "clerk-apitest-nonmgr-" + IdUtils.getUuid().substring(0, 6);
String otherClerkId = "clerk-apitest-other-" + IdUtils.getUuid().substring(0, 6);
String adminClerkId = "clerk-apitest-admin-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserId = "apitest-admin-user-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserCode = "apitest-admin-code-" + IdUtils.getUuid().substring(0, 6);
String customerToken = null;
String leaderToken = null;
String nonManagerToken = null;
String otherToken = null;
String adminToken = null;
try {
resetCustomerBalance();
setCustomerBalance(HIGH_BALANCE);
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
leaderToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, leaderToken);
nonManagerToken = createClerk(nonManagerClerkId, null, null);
otherToken = createClerk(otherClerkId, null, null);
adminToken = createClerk(adminClerkId, adminSysUserId, null);
createAdminInfo(adminSysUserId, adminSysUserCode);
String acceptedOrderId = createRandomOrder("API random accepted " + IdUtils.getUuid(), customerToken);
acceptOrder(acceptedOrderId, nonManagerToken);
assertCancelAsClerk(acceptedOrderId, otherToken, CODE_FAIL);
assertCancelAsClerk(acceptedOrderId, nonManagerToken, CODE_OK);
assertOrderStatus(acceptedOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
String leaderOrderId = createRandomOrder("API random accepted leader " + IdUtils.getUuid(), customerToken);
acceptOrder(leaderOrderId, nonManagerToken);
assertCancelAsClerk(leaderOrderId, leaderToken, CODE_OK);
assertOrderStatus(leaderOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
String adminOrderId = createRandomOrder("API random accepted admin " + IdUtils.getUuid(), customerToken);
acceptOrder(adminOrderId, nonManagerToken);
assertCancelAsClerk(adminOrderId, adminToken, CODE_OK);
assertOrderStatus(adminOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
} finally {
removeClerk(nonManagerClerkId);
removeClerk(otherClerkId);
removeClerk(adminClerkId);
removeAdminInfo(adminSysUserId);
CustomSecurityContextHolder.remove();
}
}
private String createRandomOrder(String remark, String customerToken) throws Exception {
String payload = "{" +
"\"sex\":\"" + OrderConstant.Gender.FEMALE.getCode() + "\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":" + SINGLE_QUANTITY + "," +
"\"weiChatCode\":\"" + DEFAULT_WECHAT_CODE + "\"," +
"\"excludeHistory\":\"" + OrderConstant.EXCLUDE_HISTORY_NO + "\"," +
"\"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(CODE_OK));
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
Assertions.assertThat(order).isNotNull();
return order.getId();
}
private void acceptOrder(String orderId, String clerkTokenValue) throws Exception {
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkTokenValue))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(CODE_OK));
}
private void assertCancelAsClerk(String orderId, String clerkTokenValue, int expectedCode) throws Exception {
String payload = "{" +
"\"orderId\":\"" + orderId + "\"," +
"\"refundReason\":\"" + CANCEL_REASON + "\"," +
"\"images\":[]" +
"}";
mockMvc.perform(post("/wx/clerk/order/cancellation")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkTokenValue)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(expectedCode));
}
private void assertOrderStatus(String orderId, String expectedStatus) {
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.selectOrderInfoById(orderId);
Assertions.assertThat(order).isNotNull();
Assertions.assertThat(order.getOrderStatus()).isEqualTo(expectedStatus);
}
private String createClerk(String clerkId, String sysUserId, String groupId) {
ensureTenantContext();
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
entity.setId(clerkId);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setSysUserId(sysUserId);
entity.setOpenid("openid-" + clerkId);
entity.setNickname("测试店员-" + clerkId);
entity.setGroupId(groupId);
entity.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
entity.setFixingLevel(CLERK_FIXING_LEVEL);
entity.setSex(OrderConstant.Gender.FEMALE.getCode());
entity.setPhone("1390000" + clerkId.substring(0, 4));
entity.setWeiChatCode("apitest-clerk-" + clerkId);
entity.setAvatar("https://example.com/avatar.png");
entity.setAccountBalance(BigDecimal.ZERO);
entity.setOnboardingState(CLERK_ONBOARDING_STATE);
entity.setListingState(CLERK_LISTING_ENABLED);
entity.setDisplayState(CLERK_DISPLAY_ENABLED);
entity.setOnlineState(CLERK_ONLINE_ENABLED);
entity.setRandomOrderState(CLERK_RANDOM_ORDER_ENABLED);
entity.setClerkState(CLERK_STATE_ACTIVE);
entity.setEntryTime(LocalDateTime.now());
clerkUserInfoService.save(entity);
String token = wxTokenService.createWxUserToken(clerkId);
clerkUserInfoService.updateTokenById(clerkId, token);
return token;
}
private void removeClerk(String clerkId) {
if (clerkId == null) {
return;
}
ensureTenantContext();
clerkUserInfoService.removeById(clerkId);
}
private void createAdminInfo(String sysUserId, String sysUserCode) {
if (sysUserId == null) {
return;
}
ensureTenantContext();
PlayPersonnelAdminInfoEntity entity = new PlayPersonnelAdminInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setSysUserId(sysUserId);
entity.setSysUserCode(sysUserCode);
entity.setAdminName("API测试管理员");
entity.setLeaderName("API测试负责人");
entity.setAddTime(LocalDateTime.now());
playPersonnelAdminInfoService.save(entity);
}
private void removeAdminInfo(String sysUserId) {
if (sysUserId == null) {
return;
}
ensureTenantContext();
playPersonnelAdminInfoService.lambdaUpdate()
.eq(PlayPersonnelAdminInfoEntity::getSysUserId, sysUserId)
.remove();
}
@Test
// 测试用例:订单已接单后由管理员强制取消,也应释放所使用的优惠券
void randomOrderForceCancelReleasesCoupon() throws Exception {

View File

@@ -15,7 +15,9 @@ 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.common.task.OverdueOrderHandlerTask;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.module.entity.PlayOrderContinueInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderContinueInfoService;
@@ -24,9 +26,13 @@ import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.common.constant.Constants;
import com.starry.common.context.CustomSecurityContextHolder;
import java.time.LocalDateTime;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -34,11 +40,21 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
private enum RandomOrderOwnership {
UNACCEPTED,
OWNED_BY_CURRENT,
OWNED_BY_OTHER
}
private static final String MESSAGE_OPERATION_SUCCESS = "操作成功";
private static final String REVIEW_PENDING_STATE = "0";
private static final String EXCLUDE_HISTORY_DISABLED = "0";
private static final String OTHER_CLERK_ID = "clerk-apitest-other";
private static final String CUSTOMER_AVATAR_URL = "https://example.com/avatar.png";
private static final String ANON_NICKNAME = "匿名用户";
private static final String CUSTOMER_WECHAT_CODE = "apitest-customer-wx";
private static final String PRIVACY_MARKER_PREFIX = "privacy-";
private static final int SINGLE_QUANTITY = 1;
@MockBean
@@ -90,7 +106,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
@Test
void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception {
String marker = "privacy-" + LocalDateTime.now().toString();
String marker = PRIVACY_MARKER_PREFIX + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
@@ -153,7 +169,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
@Test
void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception {
String marker = "privacy-accepted-" + LocalDateTime.now();
String marker = PRIVACY_MARKER_PREFIX + "accepted-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
ensureTenantContext();
@@ -177,13 +193,88 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@ParameterizedTest
@MethodSource("randomOrderMaskingCases")
void selectRandomOrderByIdAppliesPrivacyRules(OrderStatus status,
RandomOrderOwnership ownership,
boolean expectCustomerMasked,
boolean expectStatusMasked) throws Exception {
String marker = PRIVACY_MARKER_PREFIX + "matrix-" + status.name() + "-" + ownership.name() + "-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
ensureTenantContext();
customUserInfoService.lambdaUpdate()
.set(PlayCustomUserInfoEntity::getAvatar, CUSTOMER_AVATAR_URL)
.eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.update();
updateRandomOrderStatusAndOwner(orderId, status, ownership);
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
if (expectCustomerMasked) {
assertMaskedCustomerInfo(data);
} else {
assertVisibleCustomerInfo(data);
}
if (expectStatusMasked) {
assertThat(data.path("orderStatus").asText()).isEmpty();
} else {
assertThat(data.path("orderStatus").asText()).isEqualTo(status.getCode());
}
}
@Test
void selectRandomOrderByIdForCancelledRandomOrderMasksCustomerAvatarWhenNotAccepted() throws Exception {
String marker = PRIVACY_MARKER_PREFIX + "cancelled-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
ensureTenantContext();
customUserInfoService.lambdaUpdate()
.set(PlayCustomUserInfoEntity::getAvatar, CUSTOMER_AVATAR_URL)
.eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.update();
playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.CANCELLED.getCode())
.update();
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@Test
void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-leak-" + LocalDateTime.now().toString();
String marker = PRIVACY_MARKER_PREFIX + "leak-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
// Access via the generic Clerk Order Detail endpoint (which the notification likely links to)
@@ -203,7 +294,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@@ -236,7 +327,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
// For the owning clerk, customer info should not be forced to anonymous
String nickname = data.path("customNickname").asText();
assertThat(nickname).isNotEmpty();
assertThat(nickname).isNotIn("匿名用户", "匿名用户");
assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
assertThat(data.path("weiChatCode").asText())
.withFailMessage("Owner clerk should see customer wechat for accepted random order")
.isNotEmpty();
@@ -271,7 +362,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@@ -304,12 +395,12 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.ACCEPTED.getCode());
// We still expect customer info to not be forcibly anonymized by the random-order privacy rules
String nickname = data.path("customNickname").asText();
assertThat(nickname).isNotIn("匿名用户", "匿名用户");
assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
}
@Test
void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-list-" + LocalDateTime.now().toString();
String marker = PRIVACY_MARKER_PREFIX + "list-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
// Ensure the created pending random order appears in the clerk's own order list
@@ -348,7 +439,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
if (orderId.equals(node.path("id").asText())) {
found = true;
String nickname = node.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(node.path("customAvatar").asText()).isEmpty();
assertThat(node.path("customId").asText()).isEmpty();
}
@@ -366,7 +457,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
payload.put("commodityQuantity", SINGLE_QUANTITY);
payload.put("weiChatCode", "apitest-customer-wx");
payload.put("weiChatCode", CUSTOMER_WECHAT_CODE);
payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED);
payload.set("couponIds", mapper.createArrayNode());
payload.put("remark", remark);
@@ -391,6 +482,54 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
return order.getId();
}
private void updateRandomOrderStatusAndOwner(String orderId, OrderStatus status, RandomOrderOwnership ownership) {
ensureTenantContext();
var updater = playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getOrderStatus, status.getCode());
if (ownership == RandomOrderOwnership.OWNED_BY_CURRENT) {
updater.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID);
} else if (ownership == RandomOrderOwnership.OWNED_BY_OTHER) {
updater.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID);
}
updater.update();
}
private void assertMaskedCustomerInfo(JsonNode data) {
assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
private void assertVisibleCustomerInfo(JsonNode data) {
assertThat(data.path("weiChatCode").asText()).isNotEmpty();
assertThat(data.path("customId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
String nickname = data.path("customNickname").asText();
assertThat(nickname).isNotEmpty();
assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
assertThat(data.path("customAvatar").asText()).isEqualTo(CUSTOMER_AVATAR_URL);
}
private static Stream<Arguments> randomOrderMaskingCases() {
return Stream.of(
Arguments.of(OrderStatus.PENDING, RandomOrderOwnership.UNACCEPTED, true, false),
Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.UNACCEPTED, true, false),
Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.UNACCEPTED, true, false),
Arguments.of(OrderStatus.ACCEPTED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.IN_PROGRESS, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.COMPLETED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.ACCEPTED, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.IN_PROGRESS, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.COMPLETED, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.OWNED_BY_OTHER, true, true)
);
}
private void acceptOrder(String orderId) throws Exception {
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId)

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,7 @@ class OrderCreationContextTest {
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(OrderConstant.RewardType.BALANCE)
.isFirstOrder(true)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.commodityInfo(commodityInfo)
.paymentInfo(paymentInfo)
.purchaserBy("customer_001")
@@ -64,8 +64,7 @@ class OrderCreationContextTest {
assertEquals(OrderConstant.OrderStatus.PENDING, request.getOrderStatus());
assertEquals(OrderConstant.OrderType.NORMAL, request.getOrderType());
assertEquals(OrderConstant.PlaceType.SPECIFIED, request.getPlaceType());
assertTrue(request.isFirstOrder());
assertEquals("1", request.getFirstOrderString());
assertEquals(OrderConstant.OrderRelationType.UNASSIGNED, request.getOrderRelationType());
// 验证商品信息
assertNotNull(request.getCommodityInfo());
@@ -88,7 +87,7 @@ class OrderCreationContextTest {
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
@@ -105,7 +104,7 @@ class OrderCreationContextTest {
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.FIRST)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
@@ -127,7 +126,7 @@ class OrderCreationContextTest {
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
@@ -139,37 +138,35 @@ class OrderCreationContextTest {
}
@Test
@DisplayName("测试首单标识转换")
void testFirstOrderStringConversion() {
// 测试首单
@DisplayName("测试订单关系类型设置")
void testRelationTypeAssignment() {
OrderCreationContext firstOrder = OrderCreationContext.builder()
.orderId("order_001")
.orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.isFirstOrder(true)
.orderRelationType(OrderConstant.OrderRelationType.FIRST)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertEquals("1", firstOrder.getFirstOrderString());
assertEquals(OrderConstant.OrderRelationType.FIRST, firstOrder.getOrderRelationType());
// 测试非首单
OrderCreationContext notFirstOrder = OrderCreationContext.builder()
OrderCreationContext continuedOrder = OrderCreationContext.builder()
.orderId("order_002")
.orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.CONTINUED)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertEquals("0", notFirstOrder.getFirstOrderString());
assertEquals(OrderConstant.OrderRelationType.CONTINUED, continuedOrder.getOrderRelationType());
}
@Test
@@ -182,7 +179,7 @@ class OrderCreationContextTest {
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.FIRST)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
@@ -200,7 +197,7 @@ class OrderCreationContextTest {
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.FIRST)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")

View File

@@ -17,6 +17,7 @@ import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayClerkCustomerRelationMapper;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
@@ -46,6 +47,7 @@ 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.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayClerkCustomerRelationEntity;
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;
@@ -60,6 +62,7 @@ import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.NotificationSender;
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 java.util.Arrays;
@@ -110,6 +113,9 @@ class OrderLifecycleServiceImplTest {
@Mock
private PlayOrderLogInfoMapper orderLogInfoMapper;
@Mock
private PlayClerkCustomerRelationMapper clerkCustomerRelationMapper;
@Mock
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@@ -118,6 +124,12 @@ class OrderLifecycleServiceImplTest {
@BeforeEach
void initStrategies() {
SecurityUtils.setTenantId("tenant-test");
PlayClerkCustomerRelationEntity relation = new PlayClerkCustomerRelationEntity();
relation.setHasCompleted(YesNoFlag.NO.getCode());
relation.setDeleted(Boolean.FALSE);
lenient().when(clerkCustomerRelationMapper.selectForUpdate(anyString(), anyString(), anyString()))
.thenReturn(relation);
lifecycleService.initPlacementStrategies();
}
@@ -302,7 +314,7 @@ class OrderLifecycleServiceImplTest {
.orderType(OrderType.NORMAL)
.placeType(PlaceType.OTHER)
.rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(false)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity")
.commodityType(CommodityType.SERVICE)
@@ -332,7 +344,7 @@ class OrderLifecycleServiceImplTest {
.orderType(OrderType.NORMAL)
.placeType(PlaceType.SPECIFIED)
.rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true)
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity-01")
.commodityType(CommodityType.SERVICE)
@@ -370,10 +382,10 @@ class OrderLifecycleServiceImplTest {
revenueVo.setRevenueAmount(BigDecimal.valueOf(89.5));
revenueVo.setRevenueRatio(50);
when(orderInfoMapper.selectCount(any())).thenReturn(0L);
when(orderInfoMapper.insert(any())).thenReturn(1);
when(clerkRevenueCalculator.calculateEstimatedRevenue(
anyString(), anyList(), anyString(), anyString(), any())).thenReturn(revenueVo);
anyString(), anyList(), anyString(), any(OrderConstant.OrderRelationType.class), any()))
.thenReturn(revenueVo);
doNothing().when(customUserInfoService).saveOrderInfo(any());
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), anyString());
@@ -403,10 +415,10 @@ class OrderLifecycleServiceImplTest {
request.getAcceptBy(),
request.getPaymentInfo().getCouponIds(),
request.getPlaceType().getCode(),
YesNoFlag.YES.getCode(),
OrderConstant.OrderRelationType.FIRST,
request.getPaymentInfo().getOrderMoney());
assertEquals(YesNoFlag.YES.getCode(), created.getFirstOrder());
assertEquals(OrderConstant.OrderRelationType.FIRST, created.getOrderRelationType());
assertEquals(revenueVo.getRevenueAmount(), created.getEstimatedRevenue());
assertEquals(revenueVo.getRevenueRatio(), created.getEstimatedRevenueRatio());
assertEquals(PayMethod.WECHAT.getCode(), created.getPayMethod());
@@ -1411,7 +1423,8 @@ class OrderLifecycleServiceImplTest {
revenueVo.setRevenueAmount(BigDecimal.ZERO);
revenueVo.setRevenueRatio(0);
lenient().when(clerkRevenueCalculator.calculateEstimatedRevenue(
anyString(), anyList(), anyString(), anyString(), any())).thenReturn(revenueVo);
anyString(), anyList(), anyString(), any(OrderConstant.OrderRelationType.class), any()))
.thenReturn(revenueVo);
}
private PaymentInfo payment(BigDecimal gross, BigDecimal net, BigDecimal discount, List<String> couponIds) {
@@ -1432,7 +1445,9 @@ class OrderLifecycleServiceImplTest {
.orderType(OrderType.NORMAL)
.placeType(placeType)
.rewardType(rewardType)
.isFirstOrder(true)
.orderRelationType(placeType == PlaceType.RANDOM
? OrderConstant.OrderRelationType.FIRST
: OrderConstant.OrderRelationType.UNASSIGNED)
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity-" + placeType.getCode())
.commodityType(CommodityType.SERVICE)

View File

@@ -36,6 +36,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -70,6 +71,11 @@ class PlayClerkPerformanceServiceImplTest {
@InjectMocks
private PlayClerkPerformanceServiceImpl service;
@BeforeEach
void setupDefaultMocks() {
lenient().when(playOrderInfoService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
}
@Test
@DisplayName("queryOverview should aggregate metrics and sort clerks by GMV")
void queryOverviewAggregatesAndSorts() {
@@ -499,7 +505,7 @@ class PlayClerkPerformanceServiceImplTest {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setAcceptBy(clerkId);
order.setPurchaserBy(purchaser);
order.setFirstOrder(firstOrder);
order.setOrderRelationType(resolveRelationType(firstOrder, placeType));
order.setPlaceType(placeType);
order.setRefundType(refundType);
order.setFinalAmount(finalAmount);
@@ -511,6 +517,16 @@ class PlayClerkPerformanceServiceImplTest {
return order;
}
private OrderConstant.OrderRelationType resolveRelationType(String firstOrder, String placeType) {
if (OrderConstant.PlaceType.RANDOM.getCode().equals(placeType)) {
return OrderConstant.OrderRelationType.FIRST;
}
if (OrderConstant.YesNoFlag.NO.getCode().equals(firstOrder)) {
return OrderConstant.OrderRelationType.CONTINUED;
}
return OrderConstant.OrderRelationType.FIRST;
}
private void setAuthentication() {
LoginUser loginUser = new LoginUser();
UsernamePasswordAuthenticationToken authentication =