From b7a5d6da7168b9c35d3ff664b1b0732d457e9f50 Mon Sep 17 00:00:00 2001 From: tianfeng <769204422@qq.com> Date: Thu, 6 Nov 2025 00:46:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=93=E6=A6=9C=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RankingActivityRestController.java | 64 +++++++ .../query/RankingDataAccumulateCmdExe.java | 76 ++++++++ .../activity/query/RankingListQryExe.java | 178 ++++++++++++++++++ .../activity/query/UserRankQryExe.java | 69 +++++++ .../user/RankingActivityConvertor.java | 50 +++++ .../activity/RankingActivityServiceImpl.java | 42 +++++ .../activity/RankingActivityRecordCO.java | 103 ++++++++++ .../clientobject/activity/RankingListCO.java | 85 +++++++++ .../clientobject/activity/UserRankInfoCO.java | 61 ++++++ .../other/app/dto/cmd/UserRankQueryCmd.java | 44 +++++ .../cmd/activity/RankingDataUpdateCmd.java | 80 ++++++++ .../dto/cmd/activity/RankingListQueryCmd.java | 61 ++++++ .../activity/RankingActivityService.java | 37 ++++ .../domain/ranking/RankingActivityType.java | 69 +++++++ .../domain/ranking/RankingCycleType.java | 96 ++++++++++ .../domain/ranking/RankingDimension.java | 133 +++++++++++++ .../activity/RankingActivityRecord.java | 159 ++++++++++++++++ .../RankingActivityRecordRepository.java | 130 +++++++++++++ .../infra/gateway/RankingActivityGateway.java | 111 +++++++++++ .../gateway/RankingActivityGatewayImpl.java | 145 ++++++++++++++ 20 files changed, 1793 insertions(+) create mode 100644 rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/activity/RankingActivityRestController.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingDataAccumulateCmdExe.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingListQryExe.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/UserRankQryExe.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/user/RankingActivityConvertor.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/RankingActivityServiceImpl.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingActivityRecordCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingListCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/UserRankInfoCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/UserRankQueryCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingDataUpdateCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingListQueryCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/RankingActivityService.java create mode 100644 rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingActivityType.java create mode 100644 rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingCycleType.java create mode 100644 rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingDimension.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/activity/RankingActivityRecord.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/activity/RankingActivityRecordRepository.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGateway.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGatewayImpl.java diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/activity/RankingActivityRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/activity/RankingActivityRestController.java new file mode 100644 index 00000000..11817ed4 --- /dev/null +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/activity/RankingActivityRestController.java @@ -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); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingDataAccumulateCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingDataAccumulateCmdExe.java new file mode 100644 index 00000000..77afb42f --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingDataAccumulateCmdExe.java @@ -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 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); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingListQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingListQryExe.java new file mode 100644 index 00000000..1d0cedf0 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/RankingListQryExe.java @@ -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 records = rankingActivityGateway.getRankingList( + cmd.getReqSysOrigin().getOrigin(), + activityType, + cycleType, + cmd.getCycleKey(), + cmd.getUserSex(), + cmd.getTopN() + ); + + // 转换为CO并设置排名 + List 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 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; + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/UserRankQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/UserRankQryExe.java new file mode 100644 index 00000000..a4985034 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/query/UserRankQryExe.java @@ -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 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; + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/user/RankingActivityConvertor.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/user/RankingActivityConvertor.java new file mode 100644 index 00000000..9c746d30 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/user/RankingActivityConvertor.java @@ -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; + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/RankingActivityServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/RankingActivityServiceImpl.java new file mode 100644 index 00000000..76e99b91 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/activity/RankingActivityServiceImpl.java @@ -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); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingActivityRecordCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingActivityRecordCO.java new file mode 100644 index 00000000..2a5758c8 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingActivityRecordCO.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingListCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingListCO.java new file mode 100644 index 00000000..83cdf0fe --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/RankingListCO.java @@ -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 rankingList; + + /** + * 当前用户排名信息 + */ + private UserRankInfoCO currentUserRank; + + /** + * 参与总人数 + */ + private Long totalParticipants; + + /** + * 榜单更新时间 + */ + private String updateTime; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/UserRankInfoCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/UserRankInfoCO.java new file mode 100644 index 00000000..eab638e8 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/activity/UserRankInfoCO.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/UserRankQueryCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/UserRankQueryCmd.java new file mode 100644 index 00000000..3a4fdb95 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/UserRankQueryCmd.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingDataUpdateCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingDataUpdateCmd.java new file mode 100644 index 00000000..35ad454a --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingDataUpdateCmd.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingListQueryCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingListQueryCmd.java new file mode 100644 index 00000000..820d0b97 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/activity/RankingListQueryCmd.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/RankingActivityService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/RankingActivityService.java new file mode 100644 index 00000000..8c9f8731 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/activity/RankingActivityService.java @@ -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); +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingActivityType.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingActivityType.java new file mode 100644 index 00000000..5a0c1a66 --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingActivityType.java @@ -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; + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingCycleType.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingCycleType.java new file mode 100644 index 00000000..8ce5363c --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingCycleType.java @@ -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; + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingDimension.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingDimension.java new file mode 100644 index 00000000..bb62377e --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/ranking/RankingDimension.java @@ -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; + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/activity/RankingActivityRecord.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/activity/RankingActivityRecord.java new file mode 100644 index 00000000..3037349c --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/activity/RankingActivityRecord.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/activity/RankingActivityRecordRepository.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/activity/RankingActivityRecordRepository.java new file mode 100644 index 00000000..bebeec15 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/activity/RankingActivityRecordRepository.java @@ -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 { + + /** + * 查询用户在指定活动和周期的记录(用于累计) + */ + Optional findBySysOriginAndUserIdAndActivityTypeAndCycleKeyAndDeleted( + String sysOrigin, + Long userId, + RankingActivityType activityType, + String cycleKey, + Integer deleted + ); + + /** + * 查询指定活动和周期的排行榜列表(按数量倒序,支持分页) + * + * @param pageable 分页参数,用于限制查询数量(TOP N) + */ + List findBySysOriginAndActivityTypeAndCycleTypeAndCycleKeyAndUserSexAndDeletedOrderByQuantityDescCreateTimeAsc( + String sysOrigin, + RankingActivityType activityType, + RankingCycleType cycleType, + String cycleKey, + Integer userSex, + Integer deleted, + Pageable pageable + ); + + /** + * 查询指定活动和周期的排行榜列表(不限性别,按数量倒序,支持分页) + */ + List 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 findByUserIdAndDeleted(Long userId, Integer deleted); + + /** + * 批量查询用户在指定活动的记录 + */ + List findByUserIdInAndActivityTypeAndCycleKeyAndDeleted( + List userIds, + RankingActivityType activityType, + String cycleKey, + Integer deleted + ); +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGateway.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGateway.java new file mode 100644 index 00000000..67dd9356 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGateway.java @@ -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 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 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 batchGetUserRecords( + List userIds, + RankingActivityType activityType, + String cycleKey + ); +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGatewayImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGatewayImpl.java new file mode 100644 index 00000000..ace5e06f --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/RankingActivityGatewayImpl.java @@ -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 getUserRecord( + String sysOrigin, + Long userId, + RankingActivityType activityType, + String cycleKey) { + return repository.findBySysOriginAndUserIdAndActivityTypeAndCycleKeyAndDeleted( + sysOrigin, userId, activityType, cycleKey, NOT_DELETED + ); + } + + @Override + public List 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 batchGetUserRecords( + List userIds, + RankingActivityType activityType, + String cycleKey) { + return repository.findByUserIdInAndActivityTypeAndCycleKeyAndDeleted( + userIds, activityType, cycleKey, NOT_DELETED + ); + } +} \ No newline at end of file