新增打榜活动

This commit is contained in:
tianfeng 2025-11-06 00:46:58 +08:00
parent 318defb5c7
commit b7a5d6da71
20 changed files with 1793 additions and 0 deletions

View File

@ -0,0 +1,64 @@
package com.red.circle.other.adapter.app.activity;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.app.service.activity.RankingActivityService;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 排行榜活动控制器
*/
@Slf4j
@RestController
@RequestMapping("/ranking")
@RequiredArgsConstructor
public class RankingActivityRestController extends BaseController {
private final RankingActivityService rankingActivityService;
/**
* 累计排行数据
*
* 用途当用户产生消费收入等行为时调用此接口累计排行数据
*
* @param cmd 更新命令
* @return 是否成功
*/
@PostMapping("/accumulate")
public Boolean accumulateRankingData(@RequestBody @Validated RankingDataUpdateCmd cmd) {
return rankingActivityService.accumulateRankingData(cmd);
}
/**
* 查询排行榜列表
*
* 用途查询指定活动的排行榜TOP N及当前用户排名
*
* @param cmd 查询命令
* @return 排行榜列表
*/
@PostMapping("/list")
public RankingListCO getRankingList(@RequestBody @Validated RankingListQueryCmd cmd) {
return rankingActivityService.getRankingList(cmd);
}
/**
* 查询用户排名
*
* 用途查询指定用户在某活动的排名信息
*
* @param cmd 查询命令
* @return 用户排名信息
*/
@PostMapping("/my-rank")
public UserRankInfoCO getUserRank(@RequestBody @Validated UserRankQueryCmd cmd) {
return rankingActivityService.getUserRank(cmd);
}
}

View File

@ -0,0 +1,76 @@
package com.red.circle.other.app.command.activity.query;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import com.red.circle.other.infra.gateway.RankingActivityGateway;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.domain.ranking.RankingDimension;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Timestamp;
import java.util.Optional;
/**
* 累计排行数据命令执行器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RankingDataAccumulateCmdExe {
private final RankingActivityGateway rankingActivityGateway;
@Transactional(rollbackFor = Exception.class)
public Boolean execute(RankingDataUpdateCmd cmd) {
// 获取枚举
RankingActivityType activityType = RankingActivityType.getByCode(cmd.getActivityType());
RankingCycleType cycleType = RankingCycleType.getByCode(cmd.getCycleType());
RankingDimension dimension = RankingDimension.getByCode(cmd.getDimension());
// 查询用户现有记录
Optional<RankingActivityRecord> existingRecord = rankingActivityGateway.getUserRecord(
cmd.getReqSysOrigin().getOrigin(),
cmd.getUserId(),
activityType,
cmd.getCycleKey()
);
RankingActivityRecord record;
if (existingRecord.isPresent()) {
// 更新现有记录
record = existingRecord.get();
} else {
// 创建新记录
record = new RankingActivityRecord();
record.setSysOrigin(cmd.getReqSysOrigin().getOrigin());
record.setUserId(cmd.getUserId());
record.setUserSex(cmd.getUserSex());
record.setActivityType(activityType);
record.setCycleType(cycleType);
record.setCycleKey(cmd.getCycleKey());
record.setDimension(dimension);
record.setExtData(cmd.getExtData());
// 设置过期时间周期结束后30天
record.setExpiredTime(calculateExpiredTime(cycleType, cmd.getCycleKey()));
}
// 累计数据
rankingActivityGateway.accumulateRankingData(record, cmd.getIncrementQuantity());
return true;
}
/**
* 计算过期时间
*/
private Timestamp calculateExpiredTime(RankingCycleType cycleType, String cycleKey) {
// 简化实现统一设置为30天后过期
long expiredMillis = System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000;
return new Timestamp(expiredMillis);
}
}

View File

@ -0,0 +1,178 @@
package com.red.circle.other.app.command.activity.query;
import com.red.circle.other.app.convertor.user.RankingActivityConvertor;
import com.red.circle.other.app.dto.clientobject.activity.RankingActivityRecordCO;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.domain.ranking.RankingDimension;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import com.red.circle.other.infra.gateway.RankingActivityGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 查询排行榜命令执行器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RankingListQryExe {
private final RankingActivityGateway rankingActivityGateway;
private final RankingActivityConvertor convertor;
public RankingListCO execute(RankingListQueryCmd cmd) {
// 获取枚举
RankingActivityType activityType = RankingActivityType.getByCode(cmd.getActivityType());
RankingCycleType cycleType = RankingCycleType.getByCode(cmd.getCycleType());
// 查询排行榜列表
List<RankingActivityRecord> records = rankingActivityGateway.getRankingList(
cmd.getReqSysOrigin().getOrigin(),
activityType,
cycleType,
cmd.getCycleKey(),
cmd.getUserSex(),
cmd.getTopN()
);
// 转换为CO并设置排名
List<RankingActivityRecordCO> rankingList = IntStream.range(0, records.size())
.mapToObj(i -> {
RankingActivityRecord record = records.get(i);
RankingActivityRecordCO co = convertor.toRecordCO(record);
co.setRank(i + 1); // 排名从1开始
return co;
})
.collect(Collectors.toList());
// 查询当前用户排名如果需要
UserRankInfoCO currentUserRank = null;
if (cmd.getIncludeCurrentUser() && cmd.getReqUserId() != null) {
currentUserRank = queryCurrentUserRank(cmd, activityType, cycleType);
}
// 统计参与总人数
// Long totalParticipants = rankingActivityGateway.countTotalParticipants(
// cmd.getReqSysOrigin().getOrigin(),
// activityType,
// cycleType,
// cmd.getCycleKey(),
// cmd.getUserSex()
// );
// 构建返回结果
RankingListCO result = new RankingListCO();
result.setActivityType(activityType.getCode());
result.setActivityTypeName(activityType.getName());
result.setCycleType(cycleType.getCode());
result.setCycleTypeName(cycleType.getName());
result.setCycleKey(cmd.getCycleKey());
result.setCycleDisplay(formatCycleDisplay(cycleType, cmd.getCycleKey()));
// 设置维度信息从第一条记录获取
if (!records.isEmpty()) {
RankingDimension dimension = records.get(0).getDimension();
if (dimension != null) {
result.setDimension(dimension.getCode());
result.setDimensionName(dimension.getName());
result.setDimensionUnit(dimension.getUnit());
}
}
result.setRankingList(rankingList);
result.setCurrentUserRank(currentUserRank);
// result.setTotalParticipants(totalParticipants);
result.setUpdateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return result;
}
/**
* 查询当前用户排名
*/
private UserRankInfoCO queryCurrentUserRank(
RankingListQueryCmd cmd,
RankingActivityType activityType,
RankingCycleType cycleType) {
Optional<RankingActivityRecord> userRecord = rankingActivityGateway.getUserRecord(
cmd.getReqSysOrigin().getOrigin(),
cmd.getReqUserId(),
activityType,
cmd.getCycleKey()
);
if (userRecord.isEmpty()) {
// 用户未参与
UserRankInfoCO rankInfo = new UserRankInfoCO();
rankInfo.setUserId(cmd.getReqUserId());
rankInfo.setRank(0);
rankInfo.setQuantity(0L);
rankInfo.setIsRanked(false);
return rankInfo;
}
RankingActivityRecord record = userRecord.get();
// 计算排名
Integer rank = rankingActivityGateway.calculateUserRank(
cmd.getReqSysOrigin().getOrigin(),
activityType,
cycleType,
cmd.getCycleKey(),
null,
record.getQuantity()
);
UserRankInfoCO rankInfo = new UserRankInfoCO();
rankInfo.setUserId(record.getUserId());
rankInfo.setRank(rank);
rankInfo.setQuantity(record.getQuantity());
rankInfo.setIsRanked(rank <= cmd.getTopN());
return rankInfo;
}
/**
* 格式化周期显示文本
*/
private String formatCycleDisplay(RankingCycleType cycleType, String cycleKey) {
if (cycleKey == null) {
return "";
}
switch (cycleType) {
case DAILY:
// 20250101 -> 2025-01-01
return cycleKey.substring(0, 4) + "-" + cycleKey.substring(4, 6) + "-" + cycleKey.substring(6, 8);
case WEEKLY:
// 20250101 -> 2025年第1周
return cycleKey.substring(0, 4) + "年第" + getWeekOfYear(cycleKey) + "";
case MONTHLY:
// 202501 -> 2025年1月
return cycleKey.substring(0, 4) + "" + Integer.parseInt(cycleKey.substring(4, 6)) + "";
default:
return cycleKey;
}
}
/**
* 计算是一年中的第几周简化实现
*/
private int getWeekOfYear(String cycleKey) {
// TODO: 实现准确的周数计算
return 1;
}
}

View File

@ -0,0 +1,69 @@
package com.red.circle.other.app.command.activity.query;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import com.red.circle.other.infra.gateway.RankingActivityGateway;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* 查询用户排名命令执行器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class UserRankQryExe {
private final RankingActivityGateway rankingActivityGateway;
public UserRankInfoCO execute(UserRankQueryCmd cmd) {
// 获取枚举
RankingActivityType activityType = RankingActivityType.getByCode(cmd.getActivityType());
RankingCycleType cycleType = RankingCycleType.getByCode(cmd.getCycleType());
Long queryUserId = cmd.getUserId() != null ? cmd.getUserId() : cmd.getReqUserId();
// 查询用户记录
Optional<RankingActivityRecord> userRecord = rankingActivityGateway.getUserRecord(
cmd.getReqSysOrigin().getOrigin(),
queryUserId,
activityType,
cmd.getCycleKey()
);
UserRankInfoCO rankInfo = new UserRankInfoCO();
rankInfo.setUserId(queryUserId);
if (userRecord.isEmpty()) {
// 用户未参与
rankInfo.setRank(0);
rankInfo.setQuantity(0L);
rankInfo.setIsRanked(false);
return rankInfo;
}
RankingActivityRecord record = userRecord.get();
// 计算排名
Integer rank = rankingActivityGateway.calculateUserRank(
cmd.getReqSysOrigin().getOrigin(),
activityType,
cycleType,
cmd.getCycleKey(),
null,
record.getQuantity()
);
rankInfo.setRank(rank);
rankInfo.setQuantity(record.getQuantity());
rankInfo.setIsRanked(rank > 0);
return rankInfo;
}
}

View File

@ -0,0 +1,50 @@
package com.red.circle.other.app.convertor.user;
import com.red.circle.other.app.dto.clientobject.activity.RankingActivityRecordCO;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import org.springframework.stereotype.Component;
/**
* 排行榜活动转换器
*/
@Component
public class RankingActivityConvertor {
/**
* 实体转换为 CO
*/
public RankingActivityRecordCO toRecordCO(RankingActivityRecord record) {
if (record == null) {
return null;
}
RankingActivityRecordCO co = new RankingActivityRecordCO();
co.setUserId(record.getUserId());
co.setUserSex(record.getUserSex());
co.setQuantity(record.getQuantity());
co.setExtData(record.getExtData());
// 设置活动类型信息
if (record.getActivityType() != null) {
co.setActivityType(record.getActivityType().getCode());
co.setActivityTypeName(record.getActivityType().getName());
}
// 设置周期类型信息
if (record.getCycleType() != null) {
co.setCycleType(record.getCycleType().getCode());
co.setCycleTypeName(record.getCycleType().getName());
}
co.setCycleKey(record.getCycleKey());
// 设置统计维度信息
if (record.getDimension() != null) {
co.setDimension(record.getDimension().getCode());
co.setDimensionName(record.getDimension().getName());
co.setDimensionUnit(record.getDimension().getUnit());
}
return co;
}
}

View File

@ -0,0 +1,42 @@
package com.red.circle.other.app.service.activity;
import com.red.circle.other.app.command.activity.query.RankingDataAccumulateCmdExe;
import com.red.circle.other.app.command.activity.query.RankingListQryExe;
import com.red.circle.other.app.command.activity.query.UserRankQryExe;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 排行榜活动服务实现
*
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RankingActivityServiceImpl implements RankingActivityService {
private final RankingDataAccumulateCmdExe rankingDataAccumulateCmdExe;
private final RankingListQryExe rankingListQryExe;
private final UserRankQryExe userRankQryExe;
@Override
public Boolean accumulateRankingData(RankingDataUpdateCmd cmd) {
return rankingDataAccumulateCmdExe.execute(cmd);
}
@Override
public RankingListCO getRankingList(RankingListQueryCmd cmd) {
return rankingListQryExe.execute(cmd);
}
@Override
public UserRankInfoCO getUserRank(UserRankQueryCmd cmd) {
return userRankQryExe.execute(cmd);
}
}

View File

@ -0,0 +1,103 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
/**
* 排行榜活动记录 CO
*/
@Data
public class RankingActivityRecordCO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 用户昵称
*/
private String nickname;
/**
* 用户头像
*/
private String avatar;
/**
* 用户性别: 0-, 1-
*/
private Integer userSex;
/**
* 用户等级
*/
private Integer userLevel;
/**
* 排名
*/
private Integer rank;
/**
* 统计数值
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long quantity;
/**
* 活动类型编码
*/
private Integer activityType;
/**
* 活动类型名称
*/
private String activityTypeName;
/**
* 周期类型编码
*/
private Integer cycleType;
/**
* 周期类型名称
*/
private String cycleTypeName;
/**
* 周期标识
*/
private String cycleKey;
/**
* 统计维度编码
*/
private Integer dimension;
/**
* 统计维度名称
*/
private String dimensionName;
/**
* 统计维度单位
*/
private String dimensionUnit;
/**
* 扩展数据
*/
private String extData;
/**
* 是否是当前用户
*/
private Boolean isCurrentUser;
}

View File

@ -0,0 +1,85 @@
package com.red.circle.other.app.dto.clientobject.activity;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 排行榜列表 CO
*/
@Data
public class RankingListCO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 活动类型编码
*/
private Integer activityType;
/**
* 活动类型名称
*/
private String activityTypeName;
/**
* 周期类型编码
*/
private Integer cycleType;
/**
* 周期类型名称
*/
private String cycleTypeName;
/**
* 周期标识
*/
private String cycleKey;
/**
* 周期显示文本
*
* 示例
* - 日榜: 2025-01-01
* - 周榜: 2025年第1周
* - 月榜: 2025年1月
*/
private String cycleDisplay;
/**
* 统计维度编码
*/
private Integer dimension;
/**
* 统计维度名称
*/
private String dimensionName;
/**
* 统计维度单位
*/
private String dimensionUnit;
/**
* 排行榜列表TOP N
*/
private List<RankingActivityRecordCO> rankingList;
/**
* 当前用户排名信息
*/
private UserRankInfoCO currentUserRank;
/**
* 参与总人数
*/
private Long totalParticipants;
/**
* 榜单更新时间
*/
private String updateTime;
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
/**
* 用户排名信息 CO
*
*/
@Data
public class UserRankInfoCO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 当前排名
*
* 0 表示未上榜
*/
private Integer rank;
/**
* 统计数值
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long quantity;
/**
* 是否已上榜
*/
private Boolean isRanked;
/**
* 距离上榜还需要的数值
*
* 仅在未上榜时有值
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long quantityToRank;
/**
* 排名变化
*
* 正数上升负数下降0不变
*/
private Integer rankChange;
/**
* 上期排名
*/
private Integer lastRank;
}

View File

@ -0,0 +1,44 @@
package com.red.circle.other.app.dto.cmd;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 查询用户排名命令
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserRankQueryCmd extends AppExtCommand {
/**
* 用户ID不传则查询当前登录用户
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 活动类型
*/
@NotNull(message = "活动类型不能为空")
private Integer activityType;
/**
* 周期类型
*/
@NotNull(message = "周期类型不能为空")
private Integer cycleType;
/**
* 周期标识
*/
private String cycleKey;
/**
* 用户性别筛选null 表示不限
*/
private Integer userSex;
}

View File

@ -0,0 +1,80 @@
package com.red.circle.other.app.dto.cmd.activity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 排行榜数据更新命令
*
* 用于实时更新用户在排行榜中的数据
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RankingDataUpdateCmd extends AppExtCommand {
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空")
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 用户性别: 0-, 1-
*/
@NotNull(message = "用户性别不能为空")
private Integer userSex;
/**
* 活动类型
* RankingActivityType
*/
@NotNull(message = "活动类型不能为空")
private Integer activityType;
/**
* 周期类型
*
* RankingCycleType
*/
@NotNull(message = "周期类型不能为空")
private Integer cycleType;
/**
* 周期标识
*/
@NotNull(message = "周期标识不能为空")
private String cycleKey;
/**
* 统计维度
*
* RankingDimension
*/
@NotNull(message = "统计维度不能为空")
private Integer dimension;
/**
* 增量数值支持负数
*
* 正数增加负数减少
*/
@NotNull(message = "增量数值不能为空")
@JsonSerialize(using = ToStringSerializer.class)
private Long incrementQuantity;
/**
* 业务单号幂等性保证
*/
@NotNull(message = "业务单号不能为空")
private String bizNo;
/**
* 扩展数据JSON 格式
*/
private String extData;
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.app.dto.cmd.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 排行榜列表查询命令
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RankingListQueryCmd extends AppExtCommand {
/**
* 活动类型
*
* RankingActivityType
*/
@NotNull(message = "活动类型不能为空")
private Integer activityType;
/**
* 周期类型
*
* RankingCycleType
*/
@NotNull(message = "周期类型不能为空")
private Integer cycleType;
/**
* 周期标识
*
* 格式
* - 日榜: yyyyMMdd
* - 周榜: yyyyMMdd (周一)
* - 月榜: yyyyMM
*/
private String cycleKey;
/**
* 用户性别筛选
*
* 0-, 1-, null-全部
*/
private Integer userSex;
/**
* 查询数量TOP N
*/
@Min(value = 1, message = "查询数量最小为1")
@Max(value = 100, message = "查询数量最大为100")
private Integer topN = 50;
/**
* 是否包含当前用户排名
*/
private Boolean includeCurrentUser = true;
}

View File

@ -0,0 +1,37 @@
package com.red.circle.other.app.service.activity;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
/**
* 排行榜活动服务
*/
public interface RankingActivityService {
/**
* 累计排行数据
*
* @param cmd 更新命令
* @return 是否成功
*/
Boolean accumulateRankingData(RankingDataUpdateCmd cmd);
/**
* 查询排行榜列表
*
* @param cmd 查询命令
* @return 排行榜列表
*/
RankingListCO getRankingList(RankingListQueryCmd cmd);
/**
* 查询用户排名
*
* @param cmd 查询命令
* @return 用户排名信息
*/
UserRankInfoCO getUserRank(UserRankQueryCmd cmd);
}

View File

@ -0,0 +1,69 @@
package com.red.circle.other.domain.ranking;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 排行榜活动类型枚举
*
* 扩展方式新增活动类型只需在此添加枚举值即可
*/
@Getter
@AllArgsConstructor
public enum RankingActivityType {
/**
* 革命日
*/
REVOLUTION_DAY(1, "革命日", "统计用户送礼消费排行"),
/**
* 英雄日
*/
HEROES_DAY(2, "英雄日", "统计主播收礼收入排行"),
/**
* 充值榜
*
*/
RECHARGE(3, "充值榜", "统计用户充值金额排行"),
;
/**
* 类型编码
*/
private final Integer code;
/**
* 类型名称
*/
private final String name;
/**
* 类型描述
*/
private final String description;
/**
* 根据编码获取枚举
*/
public static RankingActivityType getByCode(Integer code) {
if (code == null) {
return null;
}
for (RankingActivityType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
/**
* 判断是否是有效的活动类型
*/
public static boolean isValid(Integer code) {
return getByCode(code) != null;
}
}

View File

@ -0,0 +1,96 @@
package com.red.circle.other.domain.ranking;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 排行榜周期类型枚举
*/
@Getter
@AllArgsConstructor
public enum RankingCycleType {
/**
* 日榜
*
* cycleKey 格式: yyyyMMdd
* 示例: 20250101
*/
DAILY(1, "日榜", "yyyyMMdd"),
/**
* 周榜
*
* cycleKey 格式: yyyyMMdd周一日期
* 示例: 202501012025年第1周的周一
*/
WEEKLY(2, "周榜", "yyyyMMdd"),
/**
* 月榜
*
* cycleKey 格式: yyyyMM
* 示例: 202501
*/
MONTHLY(3, "月榜", "yyyyMM"),
/**
* 赛季榜自定义周期
*
* cycleKey 格式: yyyyMMdd-yyyyMMdd
* 示例: 20250101-20250331Q1 赛季
*/
SEASON(4, "赛季榜", "yyyyMMdd-yyyyMMdd"),
/**
* 年榜
*
* cycleKey 格式: yyyy
* 示例: 2025
*/
YEARLY(5, "年榜", "yyyy"),
/**
* 总榜不限周期
*
* cycleKey 格式: all
*/
ALL_TIME(6, "总榜", "all");
/**
* 类型编码
*/
private final Integer code;
/**
* 类型名称
*/
private final String name;
/**
* cycleKey 格式
*/
private final String keyFormat;
/**
* 根据编码获取枚举
*/
public static RankingCycleType getByCode(Integer code) {
if (code == null) {
return null;
}
for (RankingCycleType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
/**
* 判断是否是有效的周期类型
*/
public static boolean isValid(Integer code) {
return getByCode(code) != null;
}
}

View File

@ -0,0 +1,133 @@
package com.red.circle.other.domain.ranking;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 排行榜统计维度枚举
*
* 定义排行榜按什么指标进行统计和排序
*/
@Getter
@AllArgsConstructor
public enum RankingDimension {
/**
* 消费金额金币
*
* 适用活动国王/皇后榜送礼榜
*/
CONSUME_AMOUNT(1, "消费金额", "金币"),
/**
* 收入金额钻石
*
* 适用活动收礼榜主播收益榜
*/
INCOME_AMOUNT(2, "收入金额", "钻石"),
/**
* 直播时长
*
* 适用活动直播时长榜
* 单位
*/
LIVE_DURATION(3, "直播时长", ""),
/**
* 充值金额
*
* 适用活动充值榜
* 单位钻石
*/
RECHARGE_AMOUNT(4, "充值金额", "钻石"),
/**
* 魅力值
*
* 适用活动魅力榜
* 综合计算收礼 + 粉丝 + 互动等
*/
CHARM_VALUE(5, "魅力值", ""),
/**
* 贵族积分
*
* 适用活动贵族榜
* 根据贵族等级计算积分
*/
NOBILITY_SCORE(6, "贵族积分", ""),
/**
* 粉丝数量
*
* 适用活动粉丝榜
*/
FANS_COUNT(7, "粉丝数量", ""),
/**
* 活跃度积分
*
* 适用活动活跃度榜
* 综合计算登录 + 互动 + 时长等
*/
ACTIVITY_SCORE(8, "活跃度积分", ""),
/**
* 家族贡献值
*
* 适用活动家族榜
*/
FAMILY_CONTRIBUTION(9, "家族贡献值", ""),
/**
* 礼物数量
*
* 适用活动特定礼物榜
*/
GIFT_COUNT(10, "礼物数量", ""),
/**
* 经验值
*
* 适用活动等级榜
*/
EXPERIENCE(11, "经验值", "");
/**
* 维度编码
*/
private final Integer code;
/**
* 维度名称
*/
private final String name;
/**
* 单位
*/
private final String unit;
/**
* 根据编码获取枚举
*/
public static RankingDimension getByCode(Integer code) {
if (code == null) {
return null;
}
for (RankingDimension dimension : values()) {
if (dimension.getCode().equals(code)) {
return dimension;
}
}
return null;
}
/**
* 判断是否是有效的维度
*/
public static boolean isValid(Integer code) {
return getByCode(code) != null;
}
}

View File

@ -0,0 +1,159 @@
package com.red.circle.other.infra.database.mongo.entity.activity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.domain.ranking.RankingDimension;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.sql.Timestamp;
/**
* 通用排行榜活动记录
*
* 设计说明
* 1. 支持多种活动类型送礼榜/收礼榜等
* 2. 支持多种周期日榜/周榜/月榜
* 3. 支持多种统计维度消费金额/收入金额/时长等
* 4. 使用复合索引优化查询性能
*
* 索引设计
* - sysOrigin + activityType + cycleType + cycleKey + userSex快速查询指定活动的排行榜
* - userId + activityType + cycleKey快速查询用户在某活动的排名
* - expiredTime用于定时清理过期数据
*/
@Data
@Accessors(chain = true)
@Document("ranking_activity_record")
@FieldDefaults(level = AccessLevel.PRIVATE)
@CompoundIndexes({
@CompoundIndex(name = "idx_ranking_query",
def = "{'sysOrigin': 1, 'activityType': 1, 'cycleType': 1, 'cycleKey': 1, 'userSex': 1, 'quantity': -1}"),
@CompoundIndex(name = "idx_user_ranking",
def = "{'userId': 1, 'activityType': 1, 'cycleKey': 1}"),
@CompoundIndex(name = "idx_expired",
def = "{'expiredTime': 1}")
})
public class RankingActivityRecord implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 记录ID
*/
@Id
@JsonSerialize(using = ToStringSerializer.class)
String id;
/**
* 归属平台
*/
@Indexed
String sysOrigin;
/**
* 用户ID
*/
@JsonSerialize(using = ToStringSerializer.class)
Long userId;
/**
* 用户性别: 0-, 1-, 2-全部
*/
Integer userSex;
/**
* 活动类型
*
* @see RankingActivityType
*/
@Indexed
RankingActivityType activityType;
/**
* 周期类型
*
* @see RankingCycleType
*/
RankingCycleType cycleType;
/**
* 周期标识
*
* 格式示例
* - 日榜: 20250101
* - 周榜: 20250101 (周一日期)
* - 月榜: 202501
*/
@Indexed
String cycleKey;
/**
* 统计维度
*
* @see RankingDimension
*/
RankingDimension dimension;
/**
* 统计数值具体含义由 dimension 决定
*
* 示例
* - CONSUME_AMOUNT: 消费金币数量
* - INCOME_AMOUNT: 收入钻石数量
* - LIVE_DURATION: 直播时长
*/
@JsonSerialize(using = ToStringSerializer.class)
Long quantity;
/**
* 排名瞬态字段不存储到数据库
*
* 查询时动态计算
*/
@Transient
Integer rank;
/**
* 扩展数据JSON 格式
*
* 用于存储活动特有的额外信息例如
* - 国王/皇后活动: {"region": "asia", "level": 5}
* - 礼物榜: {"giftIds": [1,2,3]}
*/
String extData;
/**
* 创建时间
*/
Timestamp createTime;
/**
* 更新时间
*/
Timestamp updateTime;
/**
* 过期时间
*
* 用于数据清理建议设置为周期结束后 30
*/
@Indexed
Timestamp expiredTime;
/**
* 是否已删除: 0-, 1-
*/
Integer deleted;
}

View File

@ -0,0 +1,130 @@
package com.red.circle.other.infra.database.mongo.service.activity;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
/**
* 排行榜活动记录 Repository
*/
@Repository
public interface RankingActivityRecordRepository extends MongoRepository<RankingActivityRecord, String> {
/**
* 查询用户在指定活动和周期的记录用于累计
*/
Optional<RankingActivityRecord> findBySysOriginAndUserIdAndActivityTypeAndCycleKeyAndDeleted(
String sysOrigin,
Long userId,
RankingActivityType activityType,
String cycleKey,
Integer deleted
);
/**
* 查询指定活动和周期的排行榜列表按数量倒序支持分页
*
* @param pageable 分页参数用于限制查询数量TOP N
*/
List<RankingActivityRecord> findBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndUserSexAndDeletedOrderByQuantityDescCreateTimeAsc(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Integer deleted,
Pageable pageable
);
/**
* 查询指定活动和周期的排行榜列表不限性别按数量倒序支持分页
*/
List<RankingActivityRecord> findBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndDeletedOrderByQuantityDescCreateTimeAsc(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer deleted,
Pageable pageable
);
/**
* 统计指定活动和周期的参与人数按性别
*/
Long countBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndUserSexAndDeleted(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Integer deleted
);
/**
* 统计指定活动和周期的参与人数不限性别
*/
Long countBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndDeleted(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer deleted
);
/**
* 统计指定活动和周期中数量大于指定值的记录数用于计算排名
*
* 用途计算用户排名 = count(quantity > userQuantity) + 1
*/
@Query("{'sysOrigin': ?0, 'activityType': ?1, 'cycleType': ?2, 'cycleKey': ?3, 'userSex': ?4, 'deleted': ?5, 'quantity': {'$gt': ?6}}")
Long countByQuantityGreaterThan(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Integer deleted,
Long quantity
);
/**
* 统计指定活动和周期中数量大于指定值的记录数不限性别
*/
@Query("{'sysOrigin': ?0, 'activityType': ?1, 'cycleType': ?2, 'cycleKey': ?3, 'deleted': ?4, 'quantity': {'$gt': ?5}}")
Long countByQuantityGreaterThanWithoutSex(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer deleted,
Long quantity
);
/**
* 删除过期数据
*/
void deleteByExpiredTimeBefore(Timestamp expiredTime);
/**
* 查询用户参与的所有活动记录
*/
List<RankingActivityRecord> findByUserIdAndDeleted(Long userId, Integer deleted);
/**
* 批量查询用户在指定活动的记录
*/
List<RankingActivityRecord> findByUserIdInAndActivityTypeAndCycleKeyAndDeleted(
List<Long> userIds,
RankingActivityType activityType,
String cycleKey,
Integer deleted
);
}

View File

@ -0,0 +1,111 @@
package com.red.circle.other.infra.gateway;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import java.util.List;
import java.util.Optional;
/**
* 排行榜活动领域网关
*/
public interface RankingActivityGateway {
/**
* 累计用户排行数据
*
* @param record 排行记录
* @param incrementQuantity 增量数值
* @return 更新后的记录
*/
RankingActivityRecord accumulateRankingData(RankingActivityRecord record, Long incrementQuantity);
/**
* 查询用户在指定活动的记录
*
* @param sysOrigin 平台
* @param userId 用户ID
* @param activityType 活动类型
* @param cycleKey 周期标识
* @return 用户记录
*/
Optional<RankingActivityRecord> getUserRecord(
String sysOrigin,
Long userId,
RankingActivityType activityType,
String cycleKey
);
/**
* 查询排行榜列表TOP N
*
* @param sysOrigin 平台
* @param activityType 活动类型
* @param cycleType 周期类型
* @param cycleKey 周期标识
* @param userSex 用户性别null 表示不限
* @param topN 查询数量
* @return 排行榜列表
*/
List<RankingActivityRecord> getRankingList(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Integer topN
);
/**
* 计算用户排名
*
* @param sysOrigin 平台
* @param activityType 活动类型
* @param cycleType 周期类型
* @param cycleKey 周期标识
* @param userSex 用户性别null 表示不限
* @param quantity 用户数值
* @return 排名1表示第一名
*/
Integer calculateUserRank(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Long quantity
);
/**
* 统计参与总人数
*
* @param sysOrigin 平台
* @param activityType 活动类型
* @param cycleType 周期类型
* @param cycleKey 周期标识
* @param userSex 用户性别null 表示不限
* @return 参与人数
*/
Long countTotalParticipants(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex
);
/**
* 保存排行记录
*/
RankingActivityRecord save(RankingActivityRecord record);
/**
* 批量查询用户记录
*/
List<RankingActivityRecord> batchGetUserRecords(
List<Long> userIds,
RankingActivityType activityType,
String cycleKey
);
}

View File

@ -0,0 +1,145 @@
package com.red.circle.other.infra.gateway;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import com.red.circle.other.infra.database.mongo.service.activity.RankingActivityRecordRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
/**
* 排行榜活动领域网关实现
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RankingActivityGatewayImpl implements RankingActivityGateway {
private final RankingActivityRecordRepository repository;
private static final Integer NOT_DELETED = 0;
@Override
public RankingActivityRecord accumulateRankingData(RankingActivityRecord record, Long incrementQuantity) {
Timestamp now = new Timestamp(System.currentTimeMillis());
if (record.getId() == null) {
// 新记录
record.setQuantity(incrementQuantity);
record.setCreateTime(now);
record.setUpdateTime(now);
record.setDeleted(NOT_DELETED);
} else {
// 累计更新
Long currentQuantity = record.getQuantity() == null ? 0L : record.getQuantity();
record.setQuantity(currentQuantity + incrementQuantity);
record.setUpdateTime(now);
}
return repository.save(record);
}
@Override
public Optional<RankingActivityRecord> getUserRecord(
String sysOrigin,
Long userId,
RankingActivityType activityType,
String cycleKey) {
return repository.findBySysOriginAndUserIdAndActivityTypeAndCycleKeyAndDeleted(
sysOrigin, userId, activityType, cycleKey, NOT_DELETED
);
}
@Override
public List<RankingActivityRecord> getRankingList(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Integer topN) {
PageRequest pageRequest = PageRequest.of(0, topN);
if (userSex == null) {
// 不限性别
return repository.findBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndDeletedOrderByQuantityDescCreateTimeAsc(
sysOrigin, activityType, cycleType, cycleKey, NOT_DELETED, pageRequest
);
} else {
// 按性别筛选
return repository.findBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndUserSexAndDeletedOrderByQuantityDescCreateTimeAsc(
sysOrigin, activityType, cycleType, cycleKey, userSex, NOT_DELETED, pageRequest
);
}
}
@Override
public Integer calculateUserRank(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex,
Long quantity) {
Long countGreater;
if (userSex == null) {
// 不限性别
countGreater = repository.countByQuantityGreaterThanWithoutSex(
sysOrigin, activityType, cycleType, cycleKey, NOT_DELETED, quantity
);
} else {
// 按性别筛选
countGreater = repository.countByQuantityGreaterThan(
sysOrigin, activityType, cycleType, cycleKey, userSex, NOT_DELETED, quantity
);
}
// 排名 = 比我多的人数 + 1
return Math.toIntExact((countGreater == null ? 0 : countGreater) + 1);
}
@Override
public Long countTotalParticipants(
String sysOrigin,
RankingActivityType activityType,
RankingCycleType cycleType,
String cycleKey,
Integer userSex) {
if (userSex == null) {
// 不限性别
return repository.countBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndDeleted(
sysOrigin, activityType, cycleType, cycleKey, NOT_DELETED
);
} else {
// 按性别筛选
return repository.countBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndUserSexAndDeleted(
sysOrigin, activityType, cycleType, cycleKey, userSex, NOT_DELETED
);
}
}
@Override
public RankingActivityRecord save(RankingActivityRecord record) {
return repository.save(record);
}
@Override
public List<RankingActivityRecord> batchGetUserRecords(
List<Long> userIds,
RankingActivityType activityType,
String cycleKey) {
return repository.findByUserIdInAndActivityTypeAndCycleKeyAndDeleted(
userIds, activityType, cycleKey, NOT_DELETED
);
}
}