diff --git a/play-admin/pom.xml b/play-admin/pom.xml index f538f2e..8938048 100644 --- a/play-admin/pom.xml +++ b/play-admin/pom.xml @@ -158,6 +158,21 @@ mockito-junit-jupiter test + + org.springframework + spring-test + test + + + org.hamcrest + hamcrest + test + + + com.jayway.jsonpath + json-path + test + diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceController.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceController.java index d84c825..0449cc5 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceController.java @@ -9,6 +9,10 @@ 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.PlayPersonnelGroupInfoEntity; import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo; import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo; import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo; import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService; @@ -81,6 +85,20 @@ public class PlayClerkPerformanceController { return R.ok(voPage); } + @ApiOperation(value = "店员业绩概览", notes = "汇总店员业绩榜单和汇总数据") + @PostMapping("/overview") + public R overview( + @ApiParam(value = "查询条件", required = true) @Validated @RequestBody ClerkPerformanceOverviewQueryVo vo) { + return R.ok(playClerkPerformanceService.queryOverview(vo)); + } + + @ApiOperation(value = "店员业绩详情", notes = "查看单个店员的业绩构成和趋势") + @PostMapping("/detail") + public R detail( + @ApiParam(value = "查询条件", required = true) @Validated @RequestBody ClerkPerformanceDetailQueryVo vo) { + return R.ok(playClerkPerformanceService.queryDetail(vo)); + } + @ApiOperation(value = "按日查询业绩", notes = "按日期查询店员业绩统计信息") @ApiResponses({@ApiResponse(code = 200, message = "操作成功")}) @PostMapping("/listByTime") diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/enums/ClerkPerformanceSortField.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/enums/ClerkPerformanceSortField.java new file mode 100644 index 0000000..7f9e53c --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/enums/ClerkPerformanceSortField.java @@ -0,0 +1,26 @@ +package com.starry.admin.modules.statistics.module.enums; + +/** + * 排序指标字段。 + * + * @author admin + * @since 2024/08/30 + */ +public enum ClerkPerformanceSortField { + /** + * 根据成交金额(GMV)排序。 + */ + GMV, + /** + * 根据订单数量排序。 + */ + ORDER_COUNT, + /** + * 根据续单率排序。 + */ + CONTINUE_RATE, + /** + * 根据预估收入排序。 + */ + ESTIMATED_REVENUE +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/enums/SortDirection.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/enums/SortDirection.java new file mode 100644 index 0000000..aef0a31 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/enums/SortDirection.java @@ -0,0 +1,20 @@ +package com.starry.admin.modules.statistics.module.enums; + +/** + * 排序方向。 + */ +public enum SortDirection { + /** + * 升序。 + */ + ASC, + /** + * 降序。 + */ + DESC; + + /** + * 默认排序方向。 + */ + public static final SortDirection DEFAULT = DESC; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailCompositionVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailCompositionVo.java new file mode 100644 index 0000000..102f71d --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailCompositionVo.java @@ -0,0 +1,48 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import lombok.Data; + +/** + * 店员业绩详情组成。 + */ +@Data +@ApiModel("店员业绩详情组成数据") +public class ClerkPerformanceDetailCompositionVo { + + @ApiModelProperty("订单构成") + private List orderComposition = Collections.emptyList(); + + @ApiModelProperty("金额构成") + private List amountComposition = Collections.emptyList(); + + @ApiModelProperty("顾客构成") + private List customerComposition = Collections.emptyList(); + + @Data + @ApiModel("业绩组成占比") + public static class CompositionSlice { + + @ApiModelProperty("标识") + private String key; + + @ApiModelProperty("名称") + private String label; + + @ApiModelProperty("数量") + private Integer count; + + @ApiModelProperty("数量占比") + private BigDecimal countRatio = BigDecimal.ZERO; + + @ApiModelProperty("金额") + private BigDecimal amount = BigDecimal.ZERO; + + @ApiModelProperty("金额占比") + private BigDecimal amountRatio = BigDecimal.ZERO; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailQueryVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailQueryVo.java new file mode 100644 index 0000000..514da89 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailQueryVo.java @@ -0,0 +1,29 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import javax.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 店员业绩详情查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel("店员业绩详情查询参数") +public class ClerkPerformanceDetailQueryVo extends PlayClerkPerformanceInfoQueryVo { + + @Override + @NotBlank(message = "店员ID不能为空") + @ApiModelProperty(value = "店员ID", required = true) + public String getClerkId() { + return super.getClerkId(); + } + + @ApiModelProperty(value = "是否返回趋势数据") + private Boolean includeTrend = Boolean.TRUE; + + @ApiModelProperty(value = "趋势天数,默认7") + private Integer trendDays = 7; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailResponseVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailResponseVo.java new file mode 100644 index 0000000..75302a4 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceDetailResponseVo.java @@ -0,0 +1,30 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.Collections; +import java.util.List; +import lombok.Data; + +/** + * 店员业绩详情响应。 + */ +@Data +@ApiModel("店员业绩详情响应") +public class ClerkPerformanceDetailResponseVo { + + @ApiModelProperty("基础信息") + private ClerkPerformanceProfileVo profile; + + @ApiModelProperty("业绩快照") + private ClerkPerformanceSnapshotVo snapshot; + + @ApiModelProperty("业绩组成") + private ClerkPerformanceDetailCompositionVo composition; + + @ApiModelProperty("趋势数据") + private List trend = Collections.emptyList(); + + @ApiModelProperty("趋势维度") + private String trendDimension = "DAY"; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewQueryVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewQueryVo.java new file mode 100644 index 0000000..65d5260 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewQueryVo.java @@ -0,0 +1,38 @@ +package com.starry.admin.modules.statistics.module.vo; + +import com.starry.admin.modules.statistics.module.enums.ClerkPerformanceSortField; +import com.starry.admin.modules.statistics.module.enums.SortDirection; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 店员业绩概览查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel("店员业绩概览查询参数") +public class ClerkPerformanceOverviewQueryVo extends PlayClerkPerformanceInfoQueryVo { + + @ApiModelProperty(value = "排序字段", allowableValues = "GMV,ORDER_COUNT,CONTINUE_RATE,ESTIMATED_REVENUE") + private ClerkPerformanceSortField sortField = ClerkPerformanceSortField.GMV; + + @ApiModelProperty(value = "排序方向", allowableValues = "ASC,DESC") + private SortDirection sortDirection = SortDirection.DEFAULT; + + @ApiModelProperty(value = "返回前N名数据,未设置则返回分页数据") + private Integer limit; + + @ApiModelProperty(value = "是否包含汇总数据") + private Boolean includeSummary = Boolean.TRUE; + + @ApiModelProperty(value = "是否包含排行列表数据") + private Boolean includeRankings = Boolean.TRUE; + + @Override + public void setEndOrderTime(List endOrderTime) { + super.setEndOrderTime(endOrderTime); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewResponseVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewResponseVo.java new file mode 100644 index 0000000..37214d5 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewResponseVo.java @@ -0,0 +1,24 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.Collections; +import java.util.List; +import lombok.Data; + +/** + * 店员业绩概览响应。 + */ +@Data +@ApiModel("店员业绩概览响应") +public class ClerkPerformanceOverviewResponseVo { + + @ApiModelProperty("汇总信息") + private ClerkPerformanceOverviewSummaryVo summary; + + @ApiModelProperty("业绩排行榜") + private List rankings = Collections.emptyList(); + + @ApiModelProperty("榜单总人数") + private Integer total = 0; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewSummaryVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewSummaryVo.java new file mode 100644 index 0000000..7a85564 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceOverviewSummaryVo.java @@ -0,0 +1,53 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 店员业绩概览汇总。 + */ +@Data +@ApiModel("店员业绩概览汇总") +public class ClerkPerformanceOverviewSummaryVo { + + @ApiModelProperty("GMV汇总") + private BigDecimal totalGmv = BigDecimal.ZERO; + + @ApiModelProperty("订单总数") + private Integer totalOrderCount = 0; + + @ApiModelProperty("首单总数") + private Integer totalFirstOrderCount = 0; + + @ApiModelProperty("续单总数") + private Integer totalContinuedOrderCount = 0; + + @ApiModelProperty("退款总数") + private Integer totalRefundOrderCount = 0; + + @ApiModelProperty("续单率(加权)") + private BigDecimal continuedRate = BigDecimal.ZERO; + + @ApiModelProperty("续费率(续单金额/GMV)") + private BigDecimal continuedAmountRate = BigDecimal.ZERO; + + @ApiModelProperty("退单率") + private BigDecimal refundRate = BigDecimal.ZERO; + + @ApiModelProperty("客单价") + private BigDecimal averageTicketPrice = BigDecimal.ZERO; + + @ApiModelProperty("预估工资汇总") + private BigDecimal totalEstimatedRevenue = BigDecimal.ZERO; + + @ApiModelProperty("复购用户数") + private Integer totalContinuedUserCount = 0; + + @ApiModelProperty("用户总数") + private Integer totalUserCount = 0; + + @ApiModelProperty("续客率") + private BigDecimal continuedCustomerRate = BigDecimal.ZERO; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceProfileVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceProfileVo.java new file mode 100644 index 0000000..382e80a --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceProfileVo.java @@ -0,0 +1,40 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 店员基础信息。 + */ +@Data +@ApiModel("店员业绩详情-基础信息") +public class ClerkPerformanceProfileVo { + + @ApiModelProperty("店员ID") + private String clerkId; + + @ApiModelProperty("店员昵称") + private String nickname; + + @ApiModelProperty("头像地址") + private String avatar; + + @ApiModelProperty("性别") + private String sex; + + @ApiModelProperty("所属组名称") + private String groupName; + + @ApiModelProperty("等级名称") + private String levelName; + + @ApiModelProperty("上架状态") + private String listingState; + + @ApiModelProperty("在线状态") + private String onlineState; + + @ApiModelProperty("角色") + private String role; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceSnapshotVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceSnapshotVo.java new file mode 100644 index 0000000..1ed1097 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceSnapshotVo.java @@ -0,0 +1,92 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 单个店员的业绩快照。 + */ +@Data +@ApiModel("店员业绩快照") +public class ClerkPerformanceSnapshotVo { + + @ApiModelProperty("店员ID") + private String clerkId; + + @ApiModelProperty("店员昵称") + private String clerkNickname; + + @ApiModelProperty("头像地址") + private String avatar; + + @ApiModelProperty("店员性别") + private String sex; + + @ApiModelProperty("所属组名称") + private String groupName; + + @ApiModelProperty("等级名称") + private String levelName; + + @ApiModelProperty("GMV(支付金额)") + private BigDecimal gmv = BigDecimal.ZERO; + + @ApiModelProperty("首单金额") + private BigDecimal firstOrderAmount = BigDecimal.ZERO; + + @ApiModelProperty("续单金额") + private BigDecimal continuedOrderAmount = BigDecimal.ZERO; + + @ApiModelProperty("打赏金额") + private BigDecimal rewardAmount = BigDecimal.ZERO; + + @ApiModelProperty("退款金额") + private BigDecimal refundAmount = BigDecimal.ZERO; + + @ApiModelProperty("预估工资") + private BigDecimal estimatedRevenue = BigDecimal.ZERO; + + @ApiModelProperty("订单数量") + private Integer orderCount = 0; + + @ApiModelProperty("首单数量") + private Integer firstOrderCount = 0; + + @ApiModelProperty("续单数量") + private Integer continuedOrderCount = 0; + + @ApiModelProperty("退款订单数量") + private Integer refundOrderCount = 0; + + @ApiModelProperty("超时未接单数量") + private Integer expiredOrderCount = 0; + + @ApiModelProperty("用户数量") + private Integer userCount = 0; + + @ApiModelProperty("复购用户数量") + private Integer continuedUserCount = 0; + + @ApiModelProperty("续单率") + private BigDecimal continuedRate = BigDecimal.ZERO; + + @ApiModelProperty("续费率(续单金额/GMV)") + private BigDecimal continuedAmountRate = BigDecimal.ZERO; + + @ApiModelProperty("退单率") + private BigDecimal refundRate = BigDecimal.ZERO; + + @ApiModelProperty("续客率") + private BigDecimal continuedCustomerRate = BigDecimal.ZERO; + + @ApiModelProperty("客单价") + private BigDecimal averageTicketPrice = BigDecimal.ZERO; + + @ApiModelProperty("在线时长(单位:秒)") + private Long onlineDuration = 0L; + + @ApiModelProperty("排行榜名次") + private Integer rank; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceTrendPointVo.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceTrendPointVo.java new file mode 100644 index 0000000..626ab87 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/module/vo/ClerkPerformanceTrendPointVo.java @@ -0,0 +1,33 @@ +package com.starry.admin.modules.statistics.module.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Data; + +/** + * 店员业绩趋势点。 + */ +@Data +@ApiModel("店员业绩趋势点") +public class ClerkPerformanceTrendPointVo { + + @ApiModelProperty("日期") + private LocalDate date; + + @ApiModelProperty("GMV") + private BigDecimal gmv = BigDecimal.ZERO; + + @ApiModelProperty("订单数量") + private Integer orderCount = 0; + + @ApiModelProperty("续单率") + private BigDecimal continuedRate = BigDecimal.ZERO; + + @ApiModelProperty("续费金额") + private BigDecimal continuedAmount = BigDecimal.ZERO; + + @ApiModelProperty("首单金额") + private BigDecimal firstAmount = BigDecimal.ZERO; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/service/IPlayClerkPerformanceService.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/service/IPlayClerkPerformanceService.java index 155904a..43b3854 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/statistics/service/IPlayClerkPerformanceService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/service/IPlayClerkPerformanceService.java @@ -4,6 +4,10 @@ import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo; import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo; import java.util.List; @@ -29,4 +33,8 @@ public interface IPlayClerkPerformanceService { PlayClerkPerformanceInfoReturnVo getClerkPerformanceInfo(PlayClerkUserInfoEntity userInfo, List orderInfoEntities, List clerkLevelInfoEntity, List groupInfoEntities); + + ClerkPerformanceOverviewResponseVo queryOverview(ClerkPerformanceOverviewQueryVo vo); + + ClerkPerformanceDetailResponseVo queryDetail(ClerkPerformanceDetailQueryVo vo); } diff --git a/play-admin/src/main/java/com/starry/admin/modules/statistics/service/impl/PlayClerkPerformanceServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/statistics/service/impl/PlayClerkPerformanceServiceImpl.java index 9808d12..4c89dcf 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/statistics/service/impl/PlayClerkPerformanceServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/statistics/service/impl/PlayClerkPerformanceServiceImpl.java @@ -1,29 +1,75 @@ package com.starry.admin.modules.statistics.service.impl; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.starry.admin.common.exception.ServiceException; import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; +import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService; +import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; 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.PlayPersonnelGroupInfoEntity; +import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; +import com.starry.admin.modules.statistics.module.enums.ClerkPerformanceSortField; +import com.starry.admin.modules.statistics.module.enums.SortDirection; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailCompositionVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceProfileVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceTrendPointVo; import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo; import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService; +import com.starry.admin.utils.SecurityUtils; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +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; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Resource; import org.springframework.stereotype.Service; /** - * @author admin - * @since 2024/6/15 下午3:29 - **/ + * 店员业绩数据服务。 + */ @Service public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceService { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Resource + private IPlayClerkUserInfoService clerkUserInfoService; + + @Resource + private IPlayOrderInfoService playOrderInfoService; + + @Resource + private IPlayClerkLevelInfoService playClerkLevelInfoService; + + @Resource + private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService; + @Override public PlayClerkPerformanceInfoReturnVo getClerkPerformanceInfo(PlayClerkUserInfoEntity userInfo, List orderInfoEntities, List clerkLevelInfoEntities, List groupInfoEntities) { - Set customIds = new HashSet<>(); int orderContinueNumber = 0; int orderRefundNumber = 0; @@ -36,35 +82,36 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer BigDecimal estimatedRevenue = BigDecimal.ZERO; for (PlayOrderInfoEntity orderInfoEntity : orderInfoEntities) { customIds.add(orderInfoEntity.getPurchaserBy()); - finalAmount = finalAmount.add(orderInfoEntity.getFinalAmount()); + finalAmount = finalAmount.add(defaultZero(orderInfoEntity.getFinalAmount())); if ("1".equals(orderInfoEntity.getFirstOrder())) { - orderFirstAmount = orderFirstAmount.add(orderInfoEntity.getFinalAmount()); + orderFirstAmount = orderFirstAmount.add(defaultZero(orderInfoEntity.getFinalAmount())); } else { orderContinueNumber++; - orderTotalAmount = orderTotalAmount.add(orderInfoEntity.getFinalAmount()); + orderTotalAmount = orderTotalAmount.add(defaultZero(orderInfoEntity.getFinalAmount())); } if ("2".equals(orderInfoEntity.getPlaceType())) { - orderRewardAmount = orderRewardAmount.add(orderInfoEntity.getFinalAmount()); + orderRewardAmount = orderRewardAmount.add(defaultZero(orderInfoEntity.getFinalAmount())); } if ("1".equals(orderInfoEntity.getRefundType())) { orderRefundNumber++; - orderRefundAmount = orderRefundAmount.add(orderInfoEntity.getRefundAmount()); + orderRefundAmount = orderRefundAmount.add(defaultZero(orderInfoEntity.getRefundAmount())); } if ("1".equals(orderInfoEntity.getOrdersExpiredState())) { ordersExpiredNumber++; } + estimatedRevenue = estimatedRevenue.add(defaultZero(orderInfoEntity.getEstimatedRevenue())); } PlayClerkPerformanceInfoReturnVo returnVo = new PlayClerkPerformanceInfoReturnVo(); returnVo.setClerkId(userInfo.getId()); returnVo.setClerkNickname(userInfo.getNickname()); returnVo.setClerkSex(userInfo.getSex()); for (PlayClerkLevelInfoEntity infoEntity : clerkLevelInfoEntities) { - if (infoEntity.getId().equals(userInfo.getLevelId())) { + if (Objects.equals(infoEntity.getId(), userInfo.getLevelId())) { returnVo.setLevelName(infoEntity.getName()); } } for (PlayPersonnelGroupInfoEntity infoEntity : groupInfoEntities) { - if (infoEntity.getId().equals(userInfo.getGroupId())) { + if (Objects.equals(infoEntity.getId(), userInfo.getGroupId())) { returnVo.setGroupName(infoEntity.getGroupName()); } } @@ -79,7 +126,456 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer returnVo.setOrderRefundAmount(orderRefundAmount); returnVo.setCustomNumber(customIds.size()); returnVo.setEstimatedRevenue(estimatedRevenue); - return returnVo; } + + @Override + public ClerkPerformanceOverviewResponseVo queryOverview(ClerkPerformanceOverviewQueryVo vo) { + DateRange range = resolveDateRange(vo.getEndOrderTime()); + List clerks = loadAccessibleClerks(vo); + ClerkPerformanceOverviewResponseVo responseVo = new ClerkPerformanceOverviewResponseVo(); + if (CollectionUtil.isEmpty(clerks)) { + responseVo.setSummary(new ClerkPerformanceOverviewSummaryVo()); + responseVo.setRankings(Collections.emptyList()); + responseVo.setTotal(0); + return responseVo; + } + Map levelNameMap = playClerkLevelInfoService.selectAll().stream().collect( + Collectors.toMap(PlayClerkLevelInfoEntity::getId, PlayClerkLevelInfoEntity::getName, (a, b) -> a)); + Map groupNameMap = playPersonnelGroupInfoService.selectAll().stream().collect( + Collectors.toMap(PlayPersonnelGroupInfoEntity::getId, PlayPersonnelGroupInfoEntity::getGroupName, + (a, b) -> a)); + List snapshots = new ArrayList<>(clerks.size()); + for (PlayClerkUserInfoEntity clerk : clerks) { + List orders = playOrderInfoService.clerkSelectOrderInfoList(clerk.getId(), + range.startTime, range.endTime); + snapshots.add(buildSnapshot(clerk, orders, levelNameMap, groupNameMap)); + } + int total = snapshots.size(); + ClerkPerformanceOverviewSummaryVo summary = aggregateSummary(snapshots); + List sorted = new ArrayList<>(snapshots); + applySortAndRank(sorted, vo.getSortField(), vo.getSortDirection()); + if (vo.getLimit() != null && vo.getLimit() > 0 && vo.getLimit() < sorted.size()) { + sorted = new ArrayList<>(sorted.subList(0, vo.getLimit())); + applySequentialRank(sorted); + } + responseVo.setSummary(Boolean.TRUE.equals(vo.getIncludeSummary()) ? summary : null); + responseVo.setRankings(Boolean.TRUE.equals(vo.getIncludeRankings()) ? sorted : Collections.emptyList()); + responseVo.setTotal(total); + return responseVo; + } + + @Override + public ClerkPerformanceDetailResponseVo queryDetail(ClerkPerformanceDetailQueryVo vo) { + DateRange range = resolveDateRange(vo.getEndOrderTime()); + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(vo.getClerkId()); + if (clerk == null) { + throw new ServiceException("店员不存在"); + } + List accessibleIds = + playPersonnelGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null); + if (CollectionUtil.isEmpty(accessibleIds) || !accessibleIds.contains(clerk.getId())) { + throw new ServiceException("无权查看该店员业绩"); + } + Map levelNameMap = playClerkLevelInfoService.selectAll().stream().collect( + Collectors.toMap(PlayClerkLevelInfoEntity::getId, PlayClerkLevelInfoEntity::getName, (a, b) -> a)); + Map groupNameMap = playPersonnelGroupInfoService.selectAll().stream().collect( + Collectors.toMap(PlayPersonnelGroupInfoEntity::getId, PlayPersonnelGroupInfoEntity::getGroupName, + (a, b) -> a)); + List orders = playOrderInfoService.clerkSelectOrderInfoList(clerk.getId(), + range.startTime, range.endTime); + ClerkPerformanceSnapshotVo snapshot = buildSnapshot(clerk, orders, levelNameMap, groupNameMap); + ClerkPerformanceDetailResponseVo responseVo = new ClerkPerformanceDetailResponseVo(); + responseVo.setProfile(buildProfile(clerk, levelNameMap, groupNameMap)); + responseVo.setSnapshot(snapshot); + responseVo.setComposition(buildComposition(snapshot)); + if (Boolean.TRUE.equals(vo.getIncludeTrend())) { + responseVo.setTrend(buildTrend(orders, range, + vo.getTrendDays() == null || vo.getTrendDays() <= 0 ? 7 : vo.getTrendDays())); + responseVo.setTrendDimension("DAY"); + } else { + responseVo.setTrend(Collections.emptyList()); + } + return responseVo; + } + + private ClerkPerformanceProfileVo buildProfile(PlayClerkUserInfoEntity clerk, Map levelNameMap, + Map groupNameMap) { + ClerkPerformanceProfileVo profile = new ClerkPerformanceProfileVo(); + profile.setClerkId(clerk.getId()); + profile.setNickname(clerk.getNickname()); + profile.setAvatar(clerk.getAvatar()); + profile.setSex(clerk.getSex()); + profile.setGroupName(groupNameMap.getOrDefault(clerk.getGroupId(), "")); + profile.setLevelName(levelNameMap.getOrDefault(clerk.getLevelId(), "")); + profile.setListingState(clerk.getListingState()); + profile.setOnlineState(clerk.getOnlineState()); + profile.setRole(null); + return profile; + } + + private List buildTrend(List orders, DateRange range, + int trendDays) { + if (CollectionUtil.isEmpty(orders)) { + return buildEmptyTrend(range, trendDays); + } + LocalDate end = range.endDate; + LocalDate start = end.minusDays(trendDays - 1); + if (start.isBefore(range.startDate)) { + start = range.startDate; + } + Map> grouped = orders.stream() + .filter(order -> order.getPurchaserTime() != null) + .collect(Collectors.groupingBy(order -> order.getPurchaserTime().toLocalDate())); + List points = new ArrayList<>(); + LocalDate cursor = start; + while (!cursor.isAfter(end)) { + List dayOrders = grouped.getOrDefault(cursor, Collections.emptyList()); + points.add(buildTrendPoint(cursor, dayOrders)); + cursor = cursor.plusDays(1); + } + return points; + } + + private List buildEmptyTrend(DateRange range, int trendDays) { + List points = new ArrayList<>(); + LocalDate end = range.endDate; + LocalDate start = end.minusDays(trendDays - 1); + if (start.isBefore(range.startDate)) { + start = range.startDate; + } + LocalDate cursor = start; + while (!cursor.isAfter(end)) { + points.add(buildTrendPoint(cursor, Collections.emptyList())); + cursor = cursor.plusDays(1); + } + return points; + } + + private ClerkPerformanceTrendPointVo buildTrendPoint(LocalDate date, List orders) { + ClerkPerformanceTrendPointVo point = new ClerkPerformanceTrendPointVo(); + point.setDate(date); + BigDecimal gmv = BigDecimal.ZERO; + BigDecimal continuedAmount = BigDecimal.ZERO; + BigDecimal firstAmount = BigDecimal.ZERO; + int continuedCount = 0; + for (PlayOrderInfoEntity order : orders) { + BigDecimal finalAmount = defaultZero(order.getFinalAmount()); + gmv = gmv.add(finalAmount); + if ("1".equals(order.getFirstOrder())) { + firstAmount = firstAmount.add(finalAmount); + } else { + continuedAmount = continuedAmount.add(finalAmount); + continuedCount++; + } + } + point.setGmv(gmv); + point.setOrderCount(orders.size()); + point.setContinuedAmount(continuedAmount); + point.setFirstAmount(firstAmount); + point.setContinuedRate(calcPercentage(continuedCount, orders.size())); + return point; + } + + private ClerkPerformanceDetailCompositionVo buildComposition(ClerkPerformanceSnapshotVo snapshot) { + ClerkPerformanceDetailCompositionVo composition = new ClerkPerformanceDetailCompositionVo(); + int orderTotal = snapshot.getOrderCount(); + BigDecimal gmv = snapshot.getGmv(); + List orderSlices = new ArrayList<>(); + orderSlices.add(createSlice("FIRST_ORDER", "首单", snapshot.getFirstOrderCount(), orderTotal, + snapshot.getFirstOrderAmount(), gmv)); + orderSlices.add(createSlice("CONTINUED_ORDER", "续单", snapshot.getContinuedOrderCount(), orderTotal, + snapshot.getContinuedOrderAmount(), gmv)); + orderSlices.add(createSlice("REFUND_ORDER", "退款", snapshot.getRefundOrderCount(), orderTotal, + snapshot.getRefundAmount(), gmv)); + orderSlices.add(createSlice("EXPIRED_ORDER", "超时", snapshot.getExpiredOrderCount(), orderTotal, + BigDecimal.ZERO, gmv)); + composition.setOrderComposition(orderSlices); + List amountSlices = new ArrayList<>(); + amountSlices.add(createSlice("FIRST_AMOUNT", "首单金额", 0, orderTotal, snapshot.getFirstOrderAmount(), gmv)); + amountSlices.add( + createSlice("CONTINUED_AMOUNT", "续单金额", 0, orderTotal, snapshot.getContinuedOrderAmount(), gmv)); + amountSlices.add(createSlice("REWARD_AMOUNT", "打赏金额", 0, orderTotal, snapshot.getRewardAmount(), gmv)); + amountSlices.add(createSlice("REFUND_AMOUNT", "退款金额", 0, orderTotal, snapshot.getRefundAmount(), gmv)); + composition.setAmountComposition(amountSlices); + int continuedCustomers = snapshot.getContinuedUserCount(); + int totalCustomers = snapshot.getUserCount(); + int newCustomers = Math.max(totalCustomers - continuedCustomers, 0); + List customerSlices = new ArrayList<>(); + customerSlices.add(createSlice("NEW_CUSTOMER", "新顾客", newCustomers, totalCustomers, BigDecimal.ZERO, gmv)); + customerSlices + .add(createSlice("CONTINUED_CUSTOMER", "复购顾客", continuedCustomers, totalCustomers, BigDecimal.ZERO, + gmv)); + composition.setCustomerComposition(customerSlices); + return composition; + } + + private ClerkPerformanceDetailCompositionVo.CompositionSlice createSlice(String key, String label, int count, + int totalCount, BigDecimal amount, BigDecimal totalAmount) { + ClerkPerformanceDetailCompositionVo.CompositionSlice slice = + new ClerkPerformanceDetailCompositionVo.CompositionSlice(); + slice.setKey(key); + slice.setLabel(label); + slice.setCount(count); + slice.setCountRatio(calcPercentage(count, totalCount)); + slice.setAmount(amount); + slice.setAmountRatio(calcPercentage(amount, totalAmount)); + return slice; + } + + private void applySortAndRank(List snapshots, ClerkPerformanceSortField sortField, + SortDirection direction) { + if (CollectionUtil.isEmpty(snapshots)) { + return; + } + Comparator comparator; + if (sortField == null) { + sortField = ClerkPerformanceSortField.GMV; + } + switch (sortField) { + case ORDER_COUNT: + comparator = Comparator.comparing(ClerkPerformanceSnapshotVo::getOrderCount, Comparator.nullsLast(Integer::compareTo)); + break; + case CONTINUE_RATE: + comparator = Comparator.comparing(ClerkPerformanceSnapshotVo::getContinuedRate, Comparator.nullsLast(BigDecimal::compareTo)); + break; + case ESTIMATED_REVENUE: + comparator = Comparator.comparing(ClerkPerformanceSnapshotVo::getEstimatedRevenue, Comparator.nullsLast(BigDecimal::compareTo)); + break; + case GMV: + default: + comparator = Comparator.comparing(ClerkPerformanceSnapshotVo::getGmv, Comparator.nullsLast(BigDecimal::compareTo)); + break; + } + comparator = comparator.thenComparing(ClerkPerformanceSnapshotVo::getClerkNickname, + Comparator.nullsFirst(String::compareTo)); + if (direction == null) { + direction = SortDirection.DEFAULT; + } + if (direction == SortDirection.DESC) { + comparator = comparator.reversed(); + } + snapshots.sort(comparator); + applySequentialRank(snapshots); + } + + private void applySequentialRank(List snapshots) { + for (int i = 0; i < snapshots.size(); i++) { + snapshots.get(i).setRank(i + 1); + } + } + + private ClerkPerformanceOverviewSummaryVo aggregateSummary(List snapshots) { + ClerkPerformanceOverviewSummaryVo summary = new ClerkPerformanceOverviewSummaryVo(); + if (CollectionUtil.isEmpty(snapshots)) { + return summary; + } + BigDecimal totalGmv = BigDecimal.ZERO; + BigDecimal continuedAmount = BigDecimal.ZERO; + BigDecimal refundAmount = BigDecimal.ZERO; + BigDecimal estimatedRevenue = BigDecimal.ZERO; + int orderTotal = 0; + int firstOrderTotal = 0; + int continuedOrderTotal = 0; + int refundOrderTotal = 0; + int userTotal = 0; + int continuedUserTotal = 0; + for (ClerkPerformanceSnapshotVo snapshot : snapshots) { + totalGmv = totalGmv.add(defaultZero(snapshot.getGmv())); + continuedAmount = continuedAmount.add(defaultZero(snapshot.getContinuedOrderAmount())); + refundAmount = refundAmount.add(defaultZero(snapshot.getRefundAmount())); + estimatedRevenue = estimatedRevenue.add(defaultZero(snapshot.getEstimatedRevenue())); + orderTotal += snapshot.getOrderCount(); + firstOrderTotal += snapshot.getFirstOrderCount(); + continuedOrderTotal += snapshot.getContinuedOrderCount(); + refundOrderTotal += snapshot.getRefundOrderCount(); + userTotal += snapshot.getUserCount(); + continuedUserTotal += snapshot.getContinuedUserCount(); + } + summary.setTotalGmv(totalGmv); + summary.setTotalOrderCount(orderTotal); + summary.setTotalFirstOrderCount(firstOrderTotal); + summary.setTotalContinuedOrderCount(continuedOrderTotal); + summary.setTotalRefundOrderCount(refundOrderTotal); + summary.setTotalEstimatedRevenue(estimatedRevenue); + summary.setTotalUserCount(userTotal); + summary.setTotalContinuedUserCount(continuedUserTotal); + summary.setContinuedRate(calcPercentage(continuedOrderTotal, orderTotal)); + summary.setContinuedAmountRate(calcPercentage(continuedAmount, totalGmv)); + summary.setRefundRate(calcPercentage(refundOrderTotal, orderTotal)); + summary.setAverageTicketPrice(calcAverage(totalGmv, orderTotal)); + summary.setContinuedCustomerRate(calcPercentage(continuedUserTotal, userTotal)); + return summary; + } + + private ClerkPerformanceSnapshotVo buildSnapshot(PlayClerkUserInfoEntity clerk, List orders, + Map levelNameMap, Map groupNameMap) { + ClerkPerformanceSnapshotVo snapshot = new ClerkPerformanceSnapshotVo(); + snapshot.setClerkId(clerk.getId()); + snapshot.setClerkNickname(clerk.getNickname()); + snapshot.setAvatar(clerk.getAvatar()); + snapshot.setSex(clerk.getSex()); + snapshot.setGroupName(groupNameMap.getOrDefault(clerk.getGroupId(), "")); + snapshot.setLevelName(levelNameMap.getOrDefault(clerk.getLevelId(), "")); + BigDecimal gmv = BigDecimal.ZERO; + BigDecimal firstAmount = BigDecimal.ZERO; + BigDecimal continuedAmount = BigDecimal.ZERO; + BigDecimal rewardAmount = BigDecimal.ZERO; + BigDecimal refundAmount = BigDecimal.ZERO; + BigDecimal estimatedRevenue = BigDecimal.ZERO; + int firstCount = 0; + int continuedCount = 0; + int refundCount = 0; + int expiredCount = 0; + Map userOrderMap = new HashMap<>(); + for (PlayOrderInfoEntity order : orders) { + BigDecimal finalAmount = defaultZero(order.getFinalAmount()); + gmv = gmv.add(finalAmount); + userOrderMap.merge(order.getPurchaserBy(), 1, Integer::sum); + if ("1".equals(order.getFirstOrder())) { + firstCount++; + firstAmount = firstAmount.add(finalAmount); + } else { + continuedCount++; + continuedAmount = continuedAmount.add(finalAmount); + } + if ("2".equals(order.getPlaceType())) { + rewardAmount = rewardAmount.add(finalAmount); + } + if ("1".equals(order.getRefundType())) { + refundCount++; + refundAmount = refundAmount.add(defaultZero(order.getRefundAmount())); + } + if ("1".equals(order.getOrdersExpiredState())) { + expiredCount++; + } + estimatedRevenue = estimatedRevenue.add(defaultZero(order.getEstimatedRevenue())); + } + int orderCount = orders.size(); + int userCount = userOrderMap.size(); + int continuedUserCount = (int) userOrderMap.values().stream().filter(cnt -> cnt > 1).count(); + snapshot.setGmv(gmv); + snapshot.setFirstOrderAmount(firstAmount); + snapshot.setContinuedOrderAmount(continuedAmount); + snapshot.setRewardAmount(rewardAmount); + snapshot.setRefundAmount(refundAmount); + snapshot.setEstimatedRevenue(estimatedRevenue); + snapshot.setOrderCount(orderCount); + snapshot.setFirstOrderCount(firstCount); + snapshot.setContinuedOrderCount(continuedCount); + snapshot.setRefundOrderCount(refundCount); + snapshot.setExpiredOrderCount(expiredCount); + snapshot.setUserCount(userCount); + snapshot.setContinuedUserCount(continuedUserCount); + snapshot.setContinuedRate(calcPercentage(continuedCount, orderCount)); + snapshot.setContinuedAmountRate(calcPercentage(continuedAmount, gmv)); + snapshot.setRefundRate(calcPercentage(refundCount, orderCount)); + snapshot.setContinuedCustomerRate(calcPercentage(continuedUserCount, userCount)); + snapshot.setAverageTicketPrice(calcAverage(gmv, orderCount)); + snapshot.setOnlineDuration(0L); + return snapshot; + } + + private List loadAccessibleClerks(ClerkPerformanceOverviewQueryVo vo) { + List accessibleIds = + playPersonnelGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null); + if (CollectionUtil.isEmpty(accessibleIds)) { + return Collections.emptyList(); + } + LambdaQueryWrapper wrapper = + Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).in(PlayClerkUserInfoEntity::getId, accessibleIds) + .eq(PlayClerkUserInfoEntity::getClerkState, "1"); + if (StrUtil.isNotBlank(vo.getClerkId())) { + wrapper.eq(PlayClerkUserInfoEntity::getId, vo.getClerkId()); + } + if (StrUtil.isNotBlank(vo.getGroupId())) { + wrapper.eq(PlayClerkUserInfoEntity::getGroupId, vo.getGroupId()); + } + if (StrUtil.isNotBlank(vo.getSex())) { + wrapper.eq(PlayClerkUserInfoEntity::getSex, vo.getSex()); + } + if (StrUtil.isNotBlank(vo.getListingState())) { + wrapper.eq(PlayClerkUserInfoEntity::getListingState, vo.getListingState()); + } + return clerkUserInfoService.list(wrapper); + } + + private DateRange resolveDateRange(List endOrderTime) { + String startStr; + String endStr; + if (CollectionUtil.isNotEmpty(endOrderTime) && endOrderTime.size() >= 2) { + startStr = normalizeStart(endOrderTime.get(0)); + endStr = normalizeEnd(endOrderTime.get(1)); + } else { + LocalDate end = LocalDate.now(); + LocalDate start = end.minusDays(6); + startStr = start.format(DATE_FORMATTER) + " 00:00:00"; + endStr = end.format(DATE_FORMATTER) + " 23:59:59"; + } + LocalDate startDate = LocalDate.parse(startStr.substring(0, 10), DATE_FORMATTER); + LocalDate endDate = LocalDate.parse(endStr.substring(0, 10), DATE_FORMATTER); + return new DateRange(startStr, endStr, startDate, endDate); + } + + private String normalizeStart(String raw) { + if (StrUtil.isBlank(raw)) { + return LocalDate.now().minusDays(6).format(DATE_FORMATTER) + " 00:00:00"; + } + if (raw.length() > 10) { + LocalDateTime.parse(raw, DATE_TIME_FORMATTER); + return raw; + } + return raw + " 00:00:00"; + } + + private String normalizeEnd(String raw) { + if (StrUtil.isBlank(raw)) { + return LocalDate.now().format(DATE_FORMATTER) + " 23:59:59"; + } + if (raw.length() > 10) { + LocalDateTime.parse(raw, DATE_TIME_FORMATTER); + return raw; + } + return raw + " 23:59:59"; + } + + private BigDecimal calcPercentage(int numerator, int denominator) { + if (denominator <= 0) { + return BigDecimal.ZERO; + } + return BigDecimal.valueOf(numerator).multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(denominator), 2, RoundingMode.HALF_UP); + } + + private BigDecimal calcPercentage(BigDecimal numerator, BigDecimal denominator) { + if (denominator == null || BigDecimal.ZERO.compareTo(denominator) == 0 || numerator == null) { + return BigDecimal.ZERO; + } + return numerator.multiply(BigDecimal.valueOf(100)).divide(denominator, 2, RoundingMode.HALF_UP); + } + + private BigDecimal calcAverage(BigDecimal numerator, int denominator) { + if (denominator <= 0 || numerator == null) { + return BigDecimal.ZERO; + } + return numerator.divide(BigDecimal.valueOf(denominator), 2, RoundingMode.HALF_UP); + } + + private BigDecimal defaultZero(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private static final class DateRange { + private final String startTime; + private final String endTime; + private final LocalDate startDate; + private final LocalDate endDate; + + private DateRange(String startTime, String endTime, LocalDate startDate, LocalDate endDate) { + this.startTime = startTime; + this.endTime = endTime; + this.startDate = startDate; + this.endDate = endDate; + } + } } diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java index 253ea3d..8f95fa4 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java @@ -193,7 +193,7 @@ public class WxCustomMpService { * @author admin * @since 2024/7/31 10:51 **/ - public void sendOrderMessage(PlayOrderInfoEntity orderInfo) { + private void sendOrderMessage(PlayOrderInfoEntity orderInfo) { SysTenantEntity tenant = tenantService.selectSysTenantByTenantId(orderInfo.getTenantId()); PlayClerkUserInfoEntity clerkUserInfo = clerkUserInfoService.selectById(orderInfo.getAcceptBy()); PlayCustomUserInfoEntity customUserInfo = customUserInfoService.selectById(orderInfo.getPurchaserBy()); diff --git a/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java b/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java index 2b51b8f..e885589 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java @@ -472,7 +472,7 @@ class PlayOrderInfoServiceTest { any(BigDecimal.class), anyString(), anyString(), any(BigDecimal.class), any(BigDecimal.class), eq(orderId)); doNothing().when(playOrderRefundInfoService).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(), anyString(), any(BigDecimal.class), anyString(), anyString(), anyString(), anyString(), anyString()); - doNothing().when(wxCustomMpService).sendOrderCancelMessage(any(PlayOrderInfoEntity.class), anyString()); + doNothing().when(wxCustomMpService).sendOrderCancelMessageAsync(any(PlayOrderInfoEntity.class), anyString()); assertDoesNotThrow(() -> orderService.forceCancelOngoingOrder("2", "admin-1", orderId, BigDecimal.valueOf(80), "管理员取消测试", Collections.emptyList())); @@ -483,7 +483,7 @@ class PlayOrderInfoServiceTest { eq(orderId)); verify(playOrderRefundInfoService, times(1)).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(), eq("0"), eq(BigDecimal.valueOf(80)), eq("管理员取消测试"), eq("2"), eq("admin-1"), eq("0"), eq("0")); - verify(wxCustomMpService, times(1)).sendOrderCancelMessage(any(PlayOrderInfoEntity.class), + verify(wxCustomMpService, times(1)).sendOrderCancelMessageAsync(any(PlayOrderInfoEntity.class), eq("管理员取消测试")); } diff --git a/play-admin/src/test/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceControllerTest.java b/play-admin/src/test/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceControllerTest.java new file mode 100644 index 0000000..62d1923 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/statistics/controller/PlayClerkPerformanceControllerTest.java @@ -0,0 +1,159 @@ +package com.starry.admin.modules.statistics.controller; + +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailCompositionVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceProfileVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceTrendPointVo; +import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +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; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * MockMvc tests for {@link PlayClerkPerformanceController} verifying new overview/detail endpoints. + */ +@ExtendWith(MockitoExtension.class) +class PlayClerkPerformanceControllerTest { + + @Mock + private IPlayClerkPerformanceService playClerkPerformanceService; + + @InjectMocks + private PlayClerkPerformanceController controller; + + private MockMvc mockMvc; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + objectMapper.findAndRegisterModules(); + mockMvc = MockMvcBuilders.standaloneSetup(controller) + .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) + .build(); + } + + @Test + @DisplayName("POST /statistics/performance/overview should delegate and wrap service response") + void overviewEndpointReturnsAggregatedData() throws Exception { + ClerkPerformanceSnapshotVo snapshot = new ClerkPerformanceSnapshotVo(); + snapshot.setClerkId("c1"); + snapshot.setClerkNickname("Alice"); + snapshot.setGmv(new BigDecimal("300.00")); + snapshot.setOrderCount(5); + snapshot.setContinuedRate(new BigDecimal("60.00")); + + ClerkPerformanceOverviewSummaryVo summary = new ClerkPerformanceOverviewSummaryVo(); + summary.setTotalGmv(new BigDecimal("300.00")); + summary.setTotalOrderCount(5); + summary.setContinuedRate(new BigDecimal("60.00")); + + ClerkPerformanceOverviewResponseVo responseVo = new ClerkPerformanceOverviewResponseVo(); + responseVo.setSummary(summary); + responseVo.setRankings(Collections.singletonList(snapshot)); + responseVo.setTotal(1); + + when(playClerkPerformanceService.queryOverview(any())).thenReturn(responseVo); + + String payload = "{\"endOrderTime\":[\"2024-08-01 00:00:00\",\"2024-08-07 23:59:59\"],\"limit\":3}"; + + mockMvc.perform(MockMvcRequestBuilders.post("/statistics/performance/overview") + .contentType(MediaType.APPLICATION_JSON) + .content(payload)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.rankings", hasSize(1))) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.rankings[0].clerkId").value("c1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.summary.totalGmv").value(300.00)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.total").value(1)); + + ArgumentCaptor captor = ArgumentCaptor + .forClass(ClerkPerformanceOverviewQueryVo.class); + verify(playClerkPerformanceService).queryOverview(captor.capture()); + ClerkPerformanceOverviewQueryVo captured = captor.getValue(); + assertEquals(Integer.valueOf(3), captured.getLimit()); + assertEquals(Arrays.asList("2024-08-01 00:00:00", "2024-08-07 23:59:59"), captured.getEndOrderTime()); + } + + @Test + @DisplayName("POST /statistics/performance/detail should return snapshot and trend data") + void detailEndpointReturnsSnapshot() throws Exception { + ClerkPerformanceProfileVo profile = new ClerkPerformanceProfileVo(); + profile.setClerkId("c1"); + profile.setNickname("Alice"); + profile.setGroupName("一组"); + + ClerkPerformanceSnapshotVo snapshotVo = new ClerkPerformanceSnapshotVo(); + snapshotVo.setClerkId("c1"); + snapshotVo.setGmv(new BigDecimal("260.00")); + snapshotVo.setOrderCount(3); + snapshotVo.setContinuedRate(new BigDecimal("66.67")); + + ClerkPerformanceDetailCompositionVo composition = new ClerkPerformanceDetailCompositionVo(); + ClerkPerformanceDetailCompositionVo.CompositionSlice slice = new ClerkPerformanceDetailCompositionVo.CompositionSlice(); + slice.setKey("FIRST_ORDER"); + slice.setLabel("首单"); + slice.setCount(1); + slice.setCountRatio(new BigDecimal("33.33")); + composition.setOrderComposition(Collections.singletonList(slice)); + + ClerkPerformanceTrendPointVo trendPoint = new ClerkPerformanceTrendPointVo(); + trendPoint.setDate(LocalDate.of(2024, 8, 1)); + trendPoint.setGmv(new BigDecimal("120.00")); + + ClerkPerformanceDetailResponseVo detailResponse = new ClerkPerformanceDetailResponseVo(); + detailResponse.setProfile(profile); + detailResponse.setSnapshot(snapshotVo); + detailResponse.setComposition(composition); + detailResponse.setTrend(Collections.singletonList(trendPoint)); + detailResponse.setTrendDimension("DAY"); + + when(playClerkPerformanceService.queryDetail(any())).thenReturn(detailResponse); + + String payload = "{\"clerkId\":\"c1\",\"endOrderTime\":[\"2024-08-01 00:00:00\",\"2024-08-03 23:59:59\"],\"includeTrend\":true}"; + + mockMvc.perform(MockMvcRequestBuilders.post("/statistics/performance/detail") + .contentType(MediaType.APPLICATION_JSON) + .content(payload)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.profile.clerkId").value("c1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.snapshot.gmv").value(260.00)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.trend[0].gmv").value(120.00)); + + ArgumentCaptor captor = ArgumentCaptor + .forClass(ClerkPerformanceDetailQueryVo.class); + verify(playClerkPerformanceService).queryDetail(captor.capture()); + ClerkPerformanceDetailQueryVo captured = captor.getValue(); + assertTrue(captured.getIncludeTrend()); + assertEquals("c1", captured.getClerkId()); + assertEquals(Arrays.asList("2024-08-01 00:00:00", "2024-08-03 23:59:59"), captured.getEndOrderTime()); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/modules/statistics/service/PlayClerkPerformanceServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/statistics/service/PlayClerkPerformanceServiceImplTest.java new file mode 100644 index 0000000..4d477fc --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/statistics/service/PlayClerkPerformanceServiceImplTest.java @@ -0,0 +1,250 @@ +package com.starry.admin.modules.statistics.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.starry.admin.common.domain.LoginUser; +import com.starry.admin.common.exception.ServiceException; +import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; +import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; +import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService; +import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; +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.PlayPersonnelGroupInfoEntity; +import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo; +import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo; +import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Unit tests for {@link PlayClerkPerformanceServiceImpl} covering aggregation, ranking and detail logic. + */ +@ExtendWith(MockitoExtension.class) +class PlayClerkPerformanceServiceImplTest { + + @Mock + private IPlayClerkUserInfoService clerkUserInfoService; + + @Mock + private IPlayOrderInfoService playOrderInfoService; + + @Mock + private IPlayClerkLevelInfoService playClerkLevelInfoService; + + @Mock + private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService; + + @InjectMocks + private PlayClerkPerformanceServiceImpl service; + + @Test + @DisplayName("queryOverview should aggregate metrics and sort clerks by GMV") + void queryOverviewAggregatesAndSorts() { + ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo(); + vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-07 23:59:59")); + vo.setIncludeSummary(true); + vo.setIncludeRankings(true); + + PlayClerkUserInfoEntity clerkA = buildClerk("c1", "Alice", "g1", "l1"); + PlayClerkUserInfoEntity clerkB = buildClerk("c2", "Bob", "g2", "l2"); + + when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Arrays.asList("c1", "c2")); + when(clerkUserInfoService.list((Wrapper) any())).thenReturn(Arrays.asList(clerkA, clerkB)); + when(playClerkLevelInfoService.selectAll()).thenReturn(Arrays.asList(level("l1", "白银"), level("l2", "黄金"))); + when(playPersonnelGroupInfoService.selectAll()) + .thenReturn(Arrays.asList(group("g1", "一组"), group("g2", "二组"))); + + List ordersA = Arrays.asList( + order("c1", "userA", "1", "0", "0", new BigDecimal("100.00"), new BigDecimal("60.00"), + LocalDateTime.of(2024, Month.AUGUST, 1, 10, 0)), + order("c1", "userA", "0", "2", "0", new BigDecimal("150.00"), new BigDecimal("90.00"), + LocalDateTime.of(2024, Month.AUGUST, 2, 14, 0)), + withRefund(order("c1", "userB", "0", "0", "1", new BigDecimal("50.00"), new BigDecimal("30.00"), + LocalDateTime.of(2024, Month.AUGUST, 3, 9, 0)), new BigDecimal("30.00"))); + + List ordersB = Collections.singletonList( + order("c2", "userC", "1", "0", "0", new BigDecimal("80.00"), new BigDecimal("50.00"), + LocalDateTime.of(2024, Month.AUGUST, 1, 12, 0))); + + when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(ordersA); + when(playOrderInfoService.clerkSelectOrderInfoList(eq("c2"), anyString(), anyString())).thenReturn(ordersB); + + setAuthentication(); + try { + ClerkPerformanceOverviewResponseVo response = service.queryOverview(vo); + + assertNotNull(response.getSummary(), "summary should be present when includeSummary=true"); + assertEquals(2, response.getTotal()); + assertEquals(2, response.getRankings().size()); + + ClerkPerformanceSnapshotVo top = response.getRankings().get(0); + assertEquals("c1", top.getClerkId(), "Highest GMV clerk should rank first"); + assertEquals(new BigDecimal("300.00"), top.getGmv()); + assertEquals(new BigDecimal("100.00"), top.getFirstOrderAmount()); + assertEquals(new BigDecimal("200.00"), top.getContinuedOrderAmount()); + assertEquals(new BigDecimal("150.00"), top.getRewardAmount()); + assertEquals(new BigDecimal("30.00"), top.getRefundAmount()); + assertEquals(3, top.getOrderCount()); + assertEquals(2, top.getContinuedOrderCount()); + assertEquals(new BigDecimal("66.67"), top.getContinuedRate()); + assertEquals(new BigDecimal("150.00"), top.getAverageTicketPrice()); + + ClerkPerformanceOverviewSummaryVo summary = response.getSummary(); + assertEquals(new BigDecimal("380.00"), summary.getTotalGmv()); + assertEquals(4, summary.getTotalOrderCount()); + assertEquals(2, summary.getTotalFirstOrderCount()); + assertEquals(2, summary.getTotalContinuedOrderCount()); + assertEquals(new BigDecimal("50.00"), summary.getContinuedRate()); + assertEquals(new BigDecimal("126.67"), summary.getAverageTicketPrice()); + } finally { + clearAuthentication(); + } + } + + @Test + @DisplayName("queryDetail should build snapshot, composition and trend for accessible clerk") + void queryDetailBuildsSnapshotCompositionAndTrend() { + ClerkPerformanceDetailQueryVo vo = new ClerkPerformanceDetailQueryVo(); + vo.setClerkId("c1"); + vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-03 23:59:59")); + vo.setIncludeTrend(true); + vo.setTrendDays(3); + + PlayClerkUserInfoEntity clerk = buildClerk("c1", "Alice", "g1", "l1"); + + when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Collections.singletonList("c1")); + when(clerkUserInfoService.getById("c1")).thenReturn(clerk); + when(playClerkLevelInfoService.selectAll()).thenReturn(Collections.singletonList(level("l1", "白银"))); + when(playPersonnelGroupInfoService.selectAll()).thenReturn(Collections.singletonList(group("g1", "一组"))); + + List orders = Arrays.asList( + order("c1", "userA", "1", "0", "0", new BigDecimal("120.00"), new BigDecimal("70.00"), + LocalDateTime.of(2024, Month.AUGUST, 1, 9, 0)), + order("c1", "userA", "0", "0", "0", new BigDecimal("80.00"), new BigDecimal("40.00"), + LocalDateTime.of(2024, Month.AUGUST, 2, 10, 0)), + withRefund(order("c1", "userB", "0", "2", "1", new BigDecimal("60.00"), new BigDecimal("30.00"), + LocalDateTime.of(2024, Month.AUGUST, 2, 18, 0)), new BigDecimal("20.00"))); + when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(orders); + + setAuthentication(); + try { + ClerkPerformanceDetailResponseVo response = service.queryDetail(vo); + assertNotNull(response.getProfile()); + assertEquals("Alice", response.getProfile().getNickname()); + assertEquals("一组", response.getProfile().getGroupName()); + assertNotNull(response.getSnapshot()); + assertEquals(new BigDecimal("260.00"), response.getSnapshot().getGmv()); + assertEquals(3, response.getSnapshot().getOrderCount()); + assertEquals(new BigDecimal("66.67"), response.getSnapshot().getContinuedRate()); + assertEquals(new BigDecimal("130.00"), response.getSnapshot().getAverageTicketPrice()); + + assertNotNull(response.getComposition()); + assertEquals(4, response.getComposition().getOrderComposition().size()); + assertEquals(new BigDecimal("33.33"), response.getComposition().getOrderComposition().get(0).getCountRatio()); + + assertEquals(3, response.getTrend().size()); + assertEquals("DAY", response.getTrendDimension()); + assertEquals(new BigDecimal("120.00"), response.getTrend().get(0).getGmv()); + assertEquals(new BigDecimal("140.00"), response.getTrend().get(1).getGmv()); + } finally { + clearAuthentication(); + } + } + + @Test + @DisplayName("queryDetail should reject access when clerk not under current user") + void queryDetailShouldEnforcePermissions() { + ClerkPerformanceDetailQueryVo vo = new ClerkPerformanceDetailQueryVo(); + vo.setClerkId("c99"); + vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-02 23:59:59")); + + when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Collections.singletonList("c1")); + when(clerkUserInfoService.getById("c99")).thenReturn(buildClerk("c99", "Ghost", "g1", "l1")); + + setAuthentication(); + try { + ServiceException ex = assertThrows(ServiceException.class, () -> service.queryDetail(vo)); + assertTrue(ex.getMessage().contains("无权")); + } finally { + clearAuthentication(); + } + } + + private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) { + PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity(); + entity.setId(id); + entity.setNickname(name); + entity.setGroupId(groupId); + entity.setLevelId(levelId); + entity.setSex("1"); + entity.setListingState("1"); + entity.setOnlineState("1"); + return entity; + } + + private PlayClerkLevelInfoEntity level(String id, String name) { + PlayClerkLevelInfoEntity level = new PlayClerkLevelInfoEntity(); + level.setId(id); + level.setName(name); + return level; + } + + private PlayPersonnelGroupInfoEntity group(String id, String name) { + PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity(); + entity.setId(id); + entity.setGroupName(name); + return entity; + } + + private PlayOrderInfoEntity order(String clerkId, String purchaser, String firstOrder, String placeType, + String refundType, BigDecimal finalAmount, BigDecimal estimatedRevenue, LocalDateTime purchaserTime) { + PlayOrderInfoEntity order = new PlayOrderInfoEntity(); + order.setAcceptBy(clerkId); + order.setPurchaserBy(purchaser); + order.setFirstOrder(firstOrder); + order.setPlaceType(placeType); + order.setRefundType(refundType); + order.setFinalAmount(finalAmount); + order.setEstimatedRevenue(estimatedRevenue); + order.setOrdersExpiredState("1".equals(refundType) ? "1" : "0"); + order.setPurchaserTime(purchaserTime); + return order; + } + + private void setAuthentication() { + LoginUser loginUser = new LoginUser(); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private void clearAuthentication() { + SecurityContextHolder.clearContext(); + } + + private PlayOrderInfoEntity withRefund(PlayOrderInfoEntity order, BigDecimal refundAmount) { + order.setRefundAmount(refundAmount); + return order; + } +}