新增打榜活动
This commit is contained in:
parent
318defb5c7
commit
b7a5d6da71
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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(周一日期)
|
||||
* 示例: 20250101(2025年第1周的周一)
|
||||
*/
|
||||
WEEKLY(2, "周榜", "yyyyMMdd"),
|
||||
|
||||
/**
|
||||
* 月榜
|
||||
*
|
||||
* cycleKey 格式: yyyyMM
|
||||
* 示例: 202501
|
||||
*/
|
||||
MONTHLY(3, "月榜", "yyyyMM"),
|
||||
|
||||
/**
|
||||
* 赛季榜(自定义周期)
|
||||
*
|
||||
* cycleKey 格式: yyyyMMdd-yyyyMMdd
|
||||
* 示例: 20250101-20250331(Q1 赛季)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user