This commit is contained in:
irving
2025-10-27 22:53:40 -04:00
parent f7461abc83
commit 1ec92cc2ab
19 changed files with 1394 additions and 15 deletions

View File

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

View File

@@ -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
}

View File

@@ -0,0 +1,20 @@
package com.starry.admin.modules.statistics.module.enums;
/**
* 排序方向。
*/
public enum SortDirection {
/**
* 升序。
*/
ASC,
/**
* 降序。
*/
DESC;
/**
* 默认排序方向。
*/
public static final SortDirection DEFAULT = DESC;
}

View File

@@ -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<CompositionSlice> orderComposition = Collections.emptyList();
@ApiModelProperty("金额构成")
private List<CompositionSlice> amountComposition = Collections.emptyList();
@ApiModelProperty("顾客构成")
private List<CompositionSlice> 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;
}
}

View File

@@ -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;
}

View File

@@ -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<ClerkPerformanceTrendPointVo> trend = Collections.emptyList();
@ApiModelProperty("趋势维度")
private String trendDimension = "DAY";
}

View File

@@ -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<String> endOrderTime) {
super.setEndOrderTime(endOrderTime);
}
}

View File

@@ -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<ClerkPerformanceSnapshotVo> rankings = Collections.emptyList();
@ApiModelProperty("榜单总人数")
private Integer total = 0;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<PlayOrderInfoEntity> orderInfoEntities, List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity,
List<PlayPersonnelGroupInfoEntity> groupInfoEntities);
ClerkPerformanceOverviewResponseVo queryOverview(ClerkPerformanceOverviewQueryVo vo);
ClerkPerformanceDetailResponseVo queryDetail(ClerkPerformanceDetailQueryVo vo);
}

View File

@@ -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<PlayOrderInfoEntity> orderInfoEntities, List<PlayClerkLevelInfoEntity> clerkLevelInfoEntities,
List<PlayPersonnelGroupInfoEntity> groupInfoEntities) {
Set<String> 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<PlayClerkUserInfoEntity> 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<String, String> levelNameMap = playClerkLevelInfoService.selectAll().stream().collect(
Collectors.toMap(PlayClerkLevelInfoEntity::getId, PlayClerkLevelInfoEntity::getName, (a, b) -> a));
Map<String, String> groupNameMap = playPersonnelGroupInfoService.selectAll().stream().collect(
Collectors.toMap(PlayPersonnelGroupInfoEntity::getId, PlayPersonnelGroupInfoEntity::getGroupName,
(a, b) -> a));
List<ClerkPerformanceSnapshotVo> snapshots = new ArrayList<>(clerks.size());
for (PlayClerkUserInfoEntity clerk : clerks) {
List<PlayOrderInfoEntity> 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<ClerkPerformanceSnapshotVo> 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<String> accessibleIds =
playPersonnelGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
if (CollectionUtil.isEmpty(accessibleIds) || !accessibleIds.contains(clerk.getId())) {
throw new ServiceException("无权查看该店员业绩");
}
Map<String, String> levelNameMap = playClerkLevelInfoService.selectAll().stream().collect(
Collectors.toMap(PlayClerkLevelInfoEntity::getId, PlayClerkLevelInfoEntity::getName, (a, b) -> a));
Map<String, String> groupNameMap = playPersonnelGroupInfoService.selectAll().stream().collect(
Collectors.toMap(PlayPersonnelGroupInfoEntity::getId, PlayPersonnelGroupInfoEntity::getGroupName,
(a, b) -> a));
List<PlayOrderInfoEntity> 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<String, String> levelNameMap,
Map<String, String> 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<ClerkPerformanceTrendPointVo> buildTrend(List<PlayOrderInfoEntity> 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<LocalDate, List<PlayOrderInfoEntity>> grouped = orders.stream()
.filter(order -> order.getPurchaserTime() != null)
.collect(Collectors.groupingBy(order -> order.getPurchaserTime().toLocalDate()));
List<ClerkPerformanceTrendPointVo> points = new ArrayList<>();
LocalDate cursor = start;
while (!cursor.isAfter(end)) {
List<PlayOrderInfoEntity> dayOrders = grouped.getOrDefault(cursor, Collections.emptyList());
points.add(buildTrendPoint(cursor, dayOrders));
cursor = cursor.plusDays(1);
}
return points;
}
private List<ClerkPerformanceTrendPointVo> buildEmptyTrend(DateRange range, int trendDays) {
List<ClerkPerformanceTrendPointVo> 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<PlayOrderInfoEntity> 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<ClerkPerformanceDetailCompositionVo.CompositionSlice> 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<ClerkPerformanceDetailCompositionVo.CompositionSlice> 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<ClerkPerformanceDetailCompositionVo.CompositionSlice> 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<ClerkPerformanceSnapshotVo> snapshots, ClerkPerformanceSortField sortField,
SortDirection direction) {
if (CollectionUtil.isEmpty(snapshots)) {
return;
}
Comparator<ClerkPerformanceSnapshotVo> 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<ClerkPerformanceSnapshotVo> snapshots) {
for (int i = 0; i < snapshots.size(); i++) {
snapshots.get(i).setRank(i + 1);
}
}
private ClerkPerformanceOverviewSummaryVo aggregateSummary(List<ClerkPerformanceSnapshotVo> 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<PlayOrderInfoEntity> orders,
Map<String, String> levelNameMap, Map<String, String> 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<String, Integer> 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<PlayClerkUserInfoEntity> loadAccessibleClerks(ClerkPerformanceOverviewQueryVo vo) {
List<String> accessibleIds =
playPersonnelGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
if (CollectionUtil.isEmpty(accessibleIds)) {
return Collections.emptyList();
}
LambdaQueryWrapper<PlayClerkUserInfoEntity> 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<String> 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;
}
}
}

View File

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