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
This commit is contained in:
irving
2025-12-31 22:06:05 -05:00
parent f39b560a05
commit 911a974e51
36 changed files with 5420 additions and 179 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;
@@ -63,6 +65,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 +82,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
IPlayOrderInfoService {
@Resource
private PlayOrderInfoMapper orderInfoMapper;
@Resource
private PlayClerkCustomerRelationMapper clerkCustomerRelationMapper;
@Resource
private IPlayClerkUserInfoService playClerkUserInfoService;
@Resource
@@ -119,8 +125,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 +142,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 +170,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 +199,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 +359,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 +484,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 +544,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 +562,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 +590,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 +670,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 +693,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 +738,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 +771,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 +803,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 +849,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 +875,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();
// }
/**
* 获取通用的订单查询对象 订单作为主表 连接顾客用户表、店员用户表、商品表

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

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

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 =