火箭功能完善

This commit is contained in:
tianfeng 2025-11-11 23:34:03 +08:00
parent 4746377fc6
commit e279ad51db
27 changed files with 2420 additions and 21 deletions

622
ROCKET_REQUIREMENT_DOC.md Normal file
View File

@ -0,0 +1,622 @@
# 🚀 语聊房火箭系统 - 完整需求文档
---
## 📋 文档信息
| 项目 | 内容 |
|-----|------|
| **文档名称** | 语聊房火箭系统需求文档 |
| **版本** | v1.0 |
| **创建日期** | 2025-01-15 |
| **所属项目** | Likei Services |
| **所属模块** | rc-service-other |
| **项目路径** | D:\workspace\likei-services\rc-service\rc-service-other |
---
## 1. 业务需求
### 1.1 功能概述
语聊房火箭是一个房间级互动玩法,用户通过送礼积累火箭能量,达到阈值后自动发射并分发奖励。
### 1.2 核心规则
#### 能量积累规则
- **基础规则**1金币礼物 = 1火箭能量点
- **幸运加成**幸运礼物额外增加4%能量
- **实时累积**:每次送礼立即增加能量
#### 火箭等级系统
| 等级 | 能量阈值 | 说明 |
|-----|---------|------|
| Lv1 | 150,000 | 一级火箭 |
| Lv2 | 500,000 | 二级火箭 |
| Lv3 | 1,000,000 | 三级火箭 |
#### 发射机制
- 能量达到阈值自动触发发射
- 发射后进入"领取中"状态
- 领取有效期1分钟
#### 奖励领取规则
- 房间内所有用户可领取
- 贡献者和普通用户奖励不同(配置待定)
- 每人限领一次
- 过期后自动重置
#### 每日重置
- 每天00:00重置火箭状态
- 历史记录保存到MongoDB
---
## 2. 技术架构
### 2.1 技术栈
| 组件 | 技术 |
|-----|------|
| **语言** | Java 17+ |
| **框架** | Spring Boot |
| **数据库** | MySQL + MongoDB |
| **缓存** | Redis |
| **架构模式** | DDD + CQRS |
### 2.2 数据库设计
#### MongoDB 集合
**room_rocket_status** - 火箭实时状态
```javascript
{
"_id": ObjectId,
"room_id": NumberLong,
"date": "2025-01-15",
"level": 1, // 1/2/3
"current_energy": NumberLong(85000),
"max_energy": NumberLong(150000),
"status": 1, // 1:CHARGING 2:LAUNCHED 3:CLAIMING
"contributors": [
{
"user_id": NumberLong,
"user_name": "张三",
"user_avatar": "https://...",
"energy": NumberLong(50000),
"contribution_rate": 58.82,
"gift_count": 125,
"first_contribute_time": ISODate,
"last_contribute_time": ISODate
}
],
"launch_time": ISODate,
"claim_expire_time": ISODate,
"claim_count": 0,
"created_at": ISODate,
"updated_at": ISODate
}
// 索引
db.room_rocket_status.createIndex({"room_id": 1, "date": 1}, {unique: true})
db.room_rocket_status.createIndex({"status": 1})
```
#### MySQL 表
**rocket_config** - 火箭配置表
```sql
CREATE TABLE `rocket_config` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`level` TINYINT UNSIGNED NOT NULL COMMENT '等级 1/2/3',
`max_energy` BIGINT UNSIGNED NOT NULL COMMENT '最大能量值',
`reward_config` JSON DEFAULT NULL COMMENT '奖励配置(预留)',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_level` (`level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `rocket_config` (`level`, `max_energy`) VALUES
(1, 150000),
(2, 500000),
(3, 1000000);
```
**rocket_reward_claim_log** - 奖励领取记录表
```sql
CREATE TABLE `rocket_reward_claim_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`rocket_history_id` VARCHAR(32) NOT NULL COMMENT 'MongoDB _id',
`room_id` BIGINT UNSIGNED NOT NULL,
`user_id` BIGINT UNSIGNED NOT NULL,
`is_contributor` TINYINT UNSIGNED NOT NULL COMMENT '是否贡献者',
`reward_type` VARCHAR(32) NOT NULL COMMENT 'GOLD/DIAMOND/PROP',
`reward_amount` BIGINT UNSIGNED NOT NULL,
`claimed_at` DATETIME NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rocket_user` (`rocket_history_id`, `user_id`),
KEY `idx_room_id` (`room_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 2.3 Redis 缓存设计
| Key | 类型 | TTL | 用途 |
|-----|------|-----|------|
| ROCKET:STATUS:{roomId} | String(JSON) | 24h | 状态缓存 |
| ROCKET:LOCK:{roomId} | String | 5s | 分布式锁 |
| ROCKET:CLAIMED:{historyId} | Set | 2min | 已领取用户 |
| ROCKET:CONFIG:{level} | String(JSON) | 1h | 配置缓存 |
---
## 3. 核心流程
### 3.1 能量积累流程
```
用户送礼 → GiftSendEvent
RocketEnergyAddCmdExe
1. 获取分布式锁 (Redis)
2. 查询火箭状态 (MongoDB)
3. 计算能量值 (普通/幸运)
4. 更新能量和贡献者
5. 检查等级提升
6. 保存状态
7. 判断是否达到发射条件
├─ 是 → 触发发射
└─ 否 → 推送能量更新
```
### 3.2 火箭发射流程
```
能量达到阈值
RocketLaunchCmdExe
1. 获取分布式锁
2. 校验状态和能量
3. 发射火箭 (status → LAUNCHED)
4. 设置领取过期时间 (+1分钟)
5. 进入领取状态 (status → CLAIMING)
6. 保存状态
7. 推送发射动画 (TODO: WebSocket)
8. 保存历史记录 (TODO)
```
### 3.3 奖励领取流程
```
用户点击领取
RocketRewardClaimCmdExe
1. 获取分布式锁
2. 校验领取资格
├─ 状态是否为CLAIMING
├─ 是否过期
└─ 是否重复领取
3. 判断是否贡献者
4. 计算奖励金额 (TODO: 配置)
5. 调用钱包服务发放 (TODO)
6. 记录领取日志 (MySQL)
7. 更新领取人数
8. 返回奖励详情
```
### 3.4 每日重置流程
```
定时任务 00:00
RocketDailyResetTask
1. 查询所有CHARGING状态火箭
2. 保存历史记录 (TODO)
3. 重置火箭状态
├─ level = 1
├─ current_energy = 0
├─ contributors = []
└─ date = 今天
4. 清除Redis缓存
```
---
## 4. API 接口设计
### 4.1 查询火箭状态
**接口**: `GET /api/rocket/status`
**请求参数**:
```json
{
"roomId": 123456,
"currentUserId": 789 // 可选
}
```
**响应数据**:
```json
{
"roomId": 123456,
"date": "2025-01-15",
"level": 2,
"levelName": "二级火箭",
"currentEnergy": 350000,
"maxEnergy": 500000,
"energyPercent": 70.00,
"status": "CHARGING",
"statusDesc": "充能中",
"topContributors": [
{
"userId": 789,
"userName": "张三",
"userAvatar": "https://...",
"energy": 200000,
"contributionRate": 57.14,
"rank": 1,
"giftCount": 500
}
],
"totalContributors": 25,
"claimCount": 0,
"canClaim": false,
"hasClaimed": false,
"myContribution": 200000,
"myContributionRate": 57.14
}
```
### 4.2 领取奖励
**接口**: `POST /api/rocket/claim`
**请求参数**:
```json
{
"roomId": 123456,
"userId": 789,
"rocketHistoryId": "507f1f77bcf86cd799439011"
}
```
**响应数据**:
```json
{
"rewardType": "GOLD",
"rewardAmount": 1000,
"isContributor": true,
"contributionEnergy": 200000,
"contributionRate": 57.14,
"message": "恭喜您获得贡献者奖励!"
}
```
### 4.3 手动触发发射 (测试用)
**接口**: `POST /api/rocket/manual-launch`
**请求参数**:
```
roomId=123456
```
---
## 5. 已实现文件清单
### 5.1 Domain 层
```
✅ RocketLevel.java - 等级枚举
✅ RocketStatusEnum.java - 状态枚举
✅ RocketContributor.java - 贡献者值对象
✅ RocketStatus.java - 火箭状态领域对象
✅ RocketStatusGateway.java - Gateway接口
```
### 5.2 Infrastructure 层
```
✅ RocketKeys.java - Redis Key枚举
✅ RocketStatusDocument.java - MongoDB文档
✅ RocketStatusRepository.java - MongoDB Repository
✅ RocketStatusGatewayImpl.java - Gateway实现
```
### 5.3 Client 层
```
✅ RocketEnergyAddCmd.java - 能量增加命令
✅ RocketRewardClaimCmd.java - 奖励领取命令
✅ RocketStatusQueryCmd.java - 状态查询命令
✅ RocketStatusCO.java - 状态客户端对象
✅ RocketContributorCO.java - 贡献者客户端对象
✅ RocketRewardCO.java - 奖励客户端对象
✅ RocketService.java - 服务接口
```
### 5.4 Application 层
```
✅ RocketEnergyAddCmdExe.java - 能量增加执行器
✅ RocketLaunchCmdExe.java - 火箭发射执行器
✅ RocketStatusQryExe.java - 状态查询执行器
✅ RocketRewardClaimCmdExe.java - 奖励领取执行器
✅ RocketServiceImpl.java - 服务实现
✅ RocketDailyResetTask.java - 每日重置任务
✅ RocketClaimExpireTask.java - 过期检查任务
```
### 5.5 Adapter 层
```
✅ RocketRestController.java - REST控制器
```
---
## 6. 待实现功能清单
### 6.1 高优先级 (P0)
#### 1. 礼物事件监听
**文件**: `GiftSendRocketListener.java`
**位置**: `other-application/src/.../app/listener/`
**功能**: 监听送礼事件,自动触发能量增加
```java
@EventListener
public void onGiftSend(GiftSendEvent event) {
RocketEnergyAddCmd cmd = new RocketEnergyAddCmd();
cmd.setRoomId(event.getRoomId());
cmd.setUserId(event.getUserId());
cmd.setGiftGoldValue(event.getGiftGoldValue());
cmd.setIsLucky(event.getIsLuckyGift());
cmd.setBizNo("GIFT_" + event.getGiftOrderId());
rocketService.addEnergy(cmd);
}
```
#### 2. MySQL 表创建
**执行SQL**:
- `rocket_config` 配置表
- `rocket_reward_claim_log` 领取记录表
#### 3. MongoDB 索引创建
```javascript
db.room_rocket_status.createIndex(
{"room_id": 1, "date": 1},
{unique: true, name: "uk_room_date"}
)
db.room_rocket_status.createIndex(
{"status": 1},
{name: "idx_status"}
)
```
#### 4. 奖励发放集成
**需要**: 调用 `wallet-inner-api`
**实现**: 在 `RocketRewardClaimCmdExe` 中补充
```java
// 注入 Feign Client
@Autowired
private WalletGoldClient walletGoldClient;
// 发放奖励
String bizNo = "ROCKET_" + rocketHistoryId + "_" + userId;
WalletReceiptReqDTO req = new WalletReceiptReqDTO();
req.setUserId(userId);
req.setAmount(rewardAmount);
req.setBizType(WalletBizType.ROCKET_REWARD);
req.setBizNo(bizNo);
walletGoldClient.income(req);
```
#### 5. 领取防重实现
**实现**: MySQL唯一索引 + Redis检查
```java
// 查询MySQL
boolean hasClaimed = rocketRewardLogGateway.existsByRocketAndUser(
rocketHistoryId, userId
);
if (hasClaimed) {
throw new RuntimeException("您已经领取过奖励了");
}
```
### 6.2 中优先级 (P1)
#### 6. 历史记录保存
**文件**: `RocketHistoryDocument.java` + `RocketHistoryRepository.java`
**功能**: 发射后保存完整历史
#### 7. WebSocket 推送
**功能**:
- 能量更新推送
- 发射动画推送
- 领取通知推送
#### 8. 奖励配置管理
**功能**: 后台配置各等级奖励规则
#### 9. 排行榜查询
**文件**: `RocketRankingQryExe.java`
**功能**: 查询贡献排行榜
### 6.3 低优先级 (P2)
#### 10. 管理后台接口
**功能**:
- 配置管理
- 数据统计
- 手动重置
#### 11. 数据统计报表
**功能**: 发射次数、领取人数等统计
#### 12. 监控告警
**功能**: 关键指标监控、异常告警
---
## 7. 测试用例
### 7.1 单元测试
```java
@Test
public void testEnergyAdd_Normal() {
// 正常送礼增加能量
}
@Test
public void testEnergyAdd_Lucky() {
// 幸运礼物增加能量
}
@Test
public void testRocketLaunch_AutoTrigger() {
// 能量满自动发射
}
@Test
public void testRewardClaim_Contributor() {
// 贡献者领取奖励
}
@Test
public void testRewardClaim_Duplicate() {
// 重复领取防重
}
```
### 7.2 集成测试场景
1. **完整流程**: 送礼 → 发射 → 领取
2. **并发送礼**: 100人同时送礼
3. **分布式锁**: 高并发下的锁测试
4. **过期重置**: 领取过期自动重置
---
## 8. 部署说明
### 8.1 环境要求
- MongoDB 4.4+
- MySQL 8.0+
- Redis 6.0+
### 8.2 配置示例
```yaml
# application.yml
spring:
data:
mongodb:
uri: mongodb://localhost:27017/likei
datasource:
url: jdbc:mysql://localhost:3306/likei
username: root
password: password
rocket:
enabled: true
lucky-bonus-rate: 0.04
claim-expire-seconds: 60
```
### 8.3 启动顺序
1. 启动 MongoDB
2. 启动 MySQL
3. 启动 Redis
4. 执行数据库初始化脚本
5. 启动应用
---
## 9. 注意事项
### 9.1 幂等性
- 能量增加使用 `bizNo` 防重
- 奖励领取使用 MySQL 唯一索引防重
### 9.2 并发控制
- 使用 Redis 分布式锁
- 锁粒度:房间级别
### 9.3 数据一致性
- MongoDB 保存实时状态
- MySQL 保存领取记录
- 通过业务逻辑保证一致性
### 9.4 性能优化
- 热点房间使用 Redis 缓存
- 批量查询贡献者
- 异步推送消息
---
d
## 10. 联系方式
**技术支持**: system
**文档维护**: Leo
**最后更新**: 2025-01-15
---
## 附录:快速上手指南
### A. 快速测试流程
1. **初始化数据库**
```sql
-- 执行 MySQL 建表语句
-- 插入火箭配置数据
```
2. **创建 MongoDB 索引**
```javascript
use likei
db.room_rocket_status.createIndex({"room_id": 1, "date": 1}, {unique: true})
```
3. **启动应用**
```bash
cd other-start
mvn spring-boot:run
```
4. **测试接口**
```bash
# 查询状态
curl "http://localhost:8080/api/rocket/status?roomId=123456"
# 手动发射 (测试用)
curl -X POST "http://localhost:8080/api/rocket/manual-launch?roomId=123456"
# 领取奖励
curl -X POST "http://localhost:8080/api/rocket/claim" \
-H "Content-Type: application/json" \
-d '{"roomId":123456,"userId":789,"rocketHistoryId":"xxx"}'
```
### B. 常见问题
**Q: 如何手动触发能量增加?**
A: 调用 `RocketService.addEnergy(cmd)` 或监听送礼事件
**Q: 如何查看当前火箭状态?**
A: 访问 MongoDB `room_rocket_status` 集合或调用查询接口
**Q: 如何重置火箭?**
A: 等待每日00:00自动重置或手动调用重置方法
---
**祝开发顺利!🚀**

View File

@ -0,0 +1,48 @@
-- ============================================
-- 火箭系统数据库初始化脚本
-- ============================================
-- 1. 创建火箭配置表
CREATE TABLE IF NOT EXISTS `rocket_config` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`level` TINYINT UNSIGNED NOT NULL COMMENT '火箭等级 1/2/3',
`max_energy` BIGINT UNSIGNED NOT NULL COMMENT '最大能量值',
`reward_config` JSON DEFAULT NULL COMMENT '奖励配置(JSON,预留)',
`contributor_reward_rate` DECIMAL(5,2) DEFAULT NULL COMMENT '贡献者奖励比例%',
`normal_reward_config` JSON DEFAULT NULL COMMENT '普通用户奖励配置(JSON,预留)',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态 1:启用 0:禁用',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_level` (`level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='火箭配置表';
-- 2. 插入初始配置数据
INSERT INTO `rocket_config` (`level`, `max_energy`, `status`) VALUES
(1, 150000, 1),
(2, 500000, 1),
(3, 1000000, 1)
ON DUPLICATE KEY UPDATE `max_energy` = VALUES(`max_energy`);
-- 3. 创建奖励领取记录表
CREATE TABLE IF NOT EXISTS `rocket_reward_claim_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`rocket_history_id` VARCHAR(32) NOT NULL COMMENT '火箭历史记录ID(MongoDB _id)',
`room_id` BIGINT UNSIGNED NOT NULL COMMENT '房间ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`rocket_level` TINYINT UNSIGNED NOT NULL COMMENT '火箭等级',
`is_contributor` TINYINT UNSIGNED NOT NULL COMMENT '是否贡献者 1:是 0:否',
`contribution_energy` BIGINT UNSIGNED DEFAULT 0 COMMENT '贡献能量值',
`contribution_rate` DECIMAL(8,4) DEFAULT 0.0000 COMMENT '贡献率%',
`reward_type` VARCHAR(32) NOT NULL COMMENT '奖励类型 GOLD/DIAMOND/PROP',
`reward_item_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '奖励物品ID',
`reward_amount` BIGINT UNSIGNED NOT NULL COMMENT '奖励数量',
`wallet_biz_no` VARCHAR(64) DEFAULT NULL COMMENT '钱包业务单号',
`claimed_at` DATETIME NOT NULL COMMENT '领取时间',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rocket_user` (`rocket_history_id`, `user_id`),
KEY `idx_room_id` (`room_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_claimed_at` (`claimed_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='火箭奖励领取记录表';

View File

@ -0,0 +1,82 @@
// ============================================
// 火箭系统MongoDB索引初始化脚本
// 执行方式: mongosh < rocket_mongo_init.js
// ============================================
// 切换到likei数据库 (根据实际数据库名修改)
use likei
print("开始创建火箭系统MongoDB索引...");
// 1. room_rocket_status 集合索引
print("1. 创建 room_rocket_status 索引...");
// 唯一索引房间ID+日期
db.room_rocket_status.createIndex(
{ "room_id": 1, "date": 1 },
{ unique: true, name: "uk_room_date", background: true }
);
print(" ✓ 创建唯一索引 uk_room_date");
// 普通索引:状态
db.room_rocket_status.createIndex(
{ "status": 1 },
{ name: "idx_status", background: true }
);
print(" ✓ 创建索引 idx_status");
// 普通索引:状态+过期时间 (用于定时任务清理)
db.room_rocket_status.createIndex(
{ "status": 1, "claim_expire_time": 1 },
{ name: "idx_status_expire", background: true }
);
print(" ✓ 创建索引 idx_status_expire");
// TTL索引30天自动过期
db.room_rocket_status.createIndex(
{ "updated_at": 1 },
{ expireAfterSeconds: 2592000, name: "ttl_30days", background: true }
);
print(" ✓ 创建TTL索引 ttl_30days (30天过期)");
// 2. rocket_launch_history 集合索引
print("2. 创建 rocket_launch_history 索引...");
// 普通索引房间ID+发射时间 (倒序)
db.rocket_launch_history.createIndex(
{ "room_id": 1, "launch_time": -1 },
{ name: "idx_room_launch_time", background: true }
);
print(" ✓ 创建索引 idx_room_launch_time");
// 普通索引:日期
db.rocket_launch_history.createIndex(
{ "date": 1 },
{ name: "idx_date", background: true }
);
print(" ✓ 创建索引 idx_date");
// 普通索引:等级
db.rocket_launch_history.createIndex(
{ "level": 1 },
{ name: "idx_level", background: true }
);
print(" ✓ 创建索引 idx_level");
// 普通索引:发射时间 (用于时间范围查询)
db.rocket_launch_history.createIndex(
{ "launch_time": -1 },
{ name: "idx_launch_time", background: true }
);
print(" ✓ 创建索引 idx_launch_time");
print("\n索引创建完成\n");
// 验证索引
print("验证 room_rocket_status 索引:");
printjson(db.room_rocket_status.getIndexes());
print("\n验证 rocket_launch_history 索引:");
printjson(db.rocket_launch_history.getIndexes());
print("\n火箭系统MongoDB初始化完成!");

View File

@ -1,7 +1,12 @@
package com.red.circle.other.app.command.rocket;
import com.red.circle.other.app.convertor.RocketConvertor;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import com.red.circle.other.app.manager.RocketImPushManager;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.other.domain.gateway.RocketHistoryGateway;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketHistory;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.domain.rocket.RocketStatusEnum;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
@ -23,7 +28,10 @@ import java.time.LocalDate;
public class RocketLaunchCmdExe {
private final RocketStatusGateway rocketStatusGateway;
private final RocketHistoryGateway rocketHistoryGateway;
private final DistributedLockUtil distributedLockUtil;
private final RocketImPushManager rocketImPushManager;
private final RocketConvertor rocketConvertor;
/**
* 执行火箭发射
@ -74,14 +82,28 @@ public class RocketLaunchCmdExe {
// 6. 保存状态
rocketStatusGateway.save(rocketStatus);
// 7. 保存历史记录
try {
RocketHistory history = RocketHistory.fromRocketStatus(rocketStatus);
String historyId = rocketHistoryGateway.save(history);
log.info("保存火箭历史记录成功: historyId={}, roomId={}", historyId, roomId);
} catch (Exception e) {
log.error("保存火箭历史记录失败: roomId={}", roomId, e);
}
// 8. 推送发射动画消息
try {
RocketStatusCO statusCO = rocketConvertor.toStatusCO(rocketStatus);
rocketImPushManager.pushRocketLaunch(roomId, statusCO);
} catch (Exception e) {
log.error("推送火箭发射消息失败: roomId={}", roomId, e);
}
log.info("火箭发射成功, roomId={}, level={}, finalEnergy={}, contributors={}, launchTime={}",
roomId,
rocketStatus.getLevel().getLevel(),
rocketStatus.getCurrentEnergy(),
rocketStatus.getContributors() != null ? rocketStatus.getContributors().size() : 0,
rocketStatus.getLaunchTime());
// TODO: 7. 推送发射动画 (WebSocket)
// TODO: 8. 保存历史记录
}
}

View File

@ -2,9 +2,13 @@ package com.red.circle.other.app.command.rocket;
import com.red.circle.other.app.dto.clientobject.RocketRewardCO;
import com.red.circle.other.app.dto.cmd.RocketRewardClaimCmd;
import com.red.circle.other.app.manager.RocketImPushManager;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.other.domain.gateway.RocketHistoryGateway;
import com.red.circle.other.domain.gateway.RocketRewardLogGateway;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketContributor;
import com.red.circle.other.domain.rocket.RocketRewardLog;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
import lombok.RequiredArgsConstructor;
@ -12,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 火箭奖励领取命令执行器
@ -25,8 +30,13 @@ import java.time.LocalDate;
public class RocketRewardClaimCmdExe {
private final RocketStatusGateway rocketStatusGateway;
private final RocketRewardLogGateway rocketRewardLogGateway;
private final RocketHistoryGateway rocketHistoryGateway;
private final DistributedLockUtil distributedLockUtil;
// TODO: 后续需要注入 WalletGoldClient 调用钱包服务
private final RocketImPushManager rocketImPushManager;
// TODO: 注入钱包服务Feign Client
// private final WalletGoldClient walletGoldClient;
/**
* 执行奖励领取
@ -60,46 +70,67 @@ public class RocketRewardClaimCmdExe {
throw new RuntimeException("当前状态不可领取奖励");
}
// 3. 检查是否已领取 (TODO: 需要查询MySQL防重)
// boolean hasClaimed = checkHasClaimed(cmd.getRoomId(), cmd.getUserId(), rocketStatus.getId());
// if (hasClaimed) {
// throw new RuntimeException("您已经领取过奖励了");
// }
// 3. 检查是否已领取 (MySQL防重)
boolean hasClaimed = rocketRewardLogGateway.existsByRocketAndUser(
cmd.getRocketHistoryId(), cmd.getUserId());
if (hasClaimed) {
throw new RuntimeException("您已经领取过奖励了");
}
// 4. 判断是否为贡献者
RocketContributor contributor = findContributor(rocketStatus, cmd.getUserId());
boolean isContributor = contributor != null;
// 5. 计算奖励 (TODO: 奖励配置后续补充)
// 5. 计算奖励 (TODO: 后续根据配置计算)
Long rewardAmount = calculateReward(isContributor, contributor, rocketStatus);
RocketRewardCO rewardCO = new RocketRewardCO();
rewardCO.setIsContributor(isContributor);
rewardCO.setRewardType("GOLD");
rewardCO.setRewardAmount(rewardAmount);
if (isContributor) {
// 贡献者奖励
rewardCO.setContributionEnergy(contributor.getEnergy());
rewardCO.setContributionRate(contributor.getContributionRate());
rewardCO.setRewardType("GOLD");
rewardCO.setRewardAmount(1000L); // TODO: 根据配置计算
rewardCO.setMessage("恭喜您获得贡献者奖励!");
} else {
// 普通用户奖励
rewardCO.setRewardType("GOLD");
rewardCO.setRewardAmount(100L); // TODO: 根据配置计算
rewardCO.setMessage("恭喜您获得参与奖励!");
}
// 6. 发放奖励 (TODO: 调用钱包服务)
// String bizNo = "ROCKET_" + rocketStatus.getId() + "_" + cmd.getUserId();
// walletGoldClient.income(...);
// 6. 发放奖励 (调用钱包服务)
String walletBizNo = RocketRewardLog.buildBizNo(cmd.getRocketHistoryId(), cmd.getUserId());
try {
sendRewardToWallet(cmd.getUserId(), rewardAmount, walletBizNo);
} catch (Exception e) {
log.error("调用钱包服务发放奖励失败: userId={}, amount={}", cmd.getUserId(), rewardAmount, e);
throw new RuntimeException("奖励发放失败,请稍后重试");
}
// 7. 记录领取日志 (TODO: 保存到MySQL)
// 7. 记录领取日志 (MySQL)
saveRewardLog(cmd, rocketStatus, isContributor, contributor, rewardAmount, walletBizNo);
// 8. 更新领取人数
rocketStatus.incrementClaimCount();
rocketStatusGateway.save(rocketStatus);
// 9. 更新历史记录的领取统计
try {
rocketHistoryGateway.updateClaimStats(cmd.getRocketHistoryId(), 1, rewardAmount);
} catch (Exception e) {
log.error("更新历史记录领取统计失败: historyId={}", cmd.getRocketHistoryId(), e);
}
// 10. 推送领取成功消息
try {
// TODO: 获取用户名称
String userName = "用户" + cmd.getUserId();
rocketImPushManager.pushRewardClaimed(cmd.getRoomId(), cmd.getUserId(), userName, rewardAmount);
} catch (Exception e) {
log.error("推送奖励领取消息失败: roomId={}, userId={}", cmd.getRoomId(), cmd.getUserId(), e);
}
log.info("火箭奖励领取成功, roomId={}, userId={}, isContributor={}, rewardAmount={}",
cmd.getRoomId(), cmd.getUserId(), isContributor, rewardCO.getRewardAmount());
cmd.getRoomId(), cmd.getUserId(), isContributor, rewardAmount);
return rewardCO;
}
@ -116,4 +147,73 @@ public class RocketRewardClaimCmdExe {
.findFirst()
.orElse(null);
}
/**
* 计算奖励金额
* TODO: 后续根据配置表计算
*/
private Long calculateReward(boolean isContributor, RocketContributor contributor, RocketStatus rocketStatus) {
if (isContributor) {
// 贡献者奖励 = 基础奖励 + 按贡献率分配的额外奖励
// 目前固定返回1000后续根据配置计算
return 1000L;
} else {
// 普通用户固定奖励
return 100L;
}
}
/**
* 调用钱包服务发放奖励
* TODO: 集成wallet-inner-api
*/
private void sendRewardToWallet(Long userId, Long amount, String bizNo) {
// 示例代码需要引入实际的Feign Client
/*
WalletReceiptReqDTO req = new WalletReceiptReqDTO();
req.setUserId(userId);
req.setAmount(PennyAmount.of(amount)); // 金币
req.setBizType(WalletBizType.ROCKET_REWARD);
req.setBizNo(bizNo);
req.setRemark("火箭奖励");
ResultResponse<Void> response = walletGoldClient.income(req);
if (!response.isSuccess()) {
throw new RuntimeException("钱包服务调用失败: " + response.getMessage());
}
*/
log.info("TODO: 调用钱包服务发放奖励, userId={}, amount={}, bizNo={}", userId, amount, bizNo);
}
/**
* 保存领取记录
*/
private void saveRewardLog(RocketRewardClaimCmd cmd, RocketStatus rocketStatus,
boolean isContributor, RocketContributor contributor,
Long rewardAmount, String walletBizNo) {
RocketRewardLog log = new RocketRewardLog();
log.setRocketHistoryId(cmd.getRocketHistoryId());
log.setRoomId(cmd.getRoomId());
log.setUserId(cmd.getUserId());
log.setRocketLevel(rocketStatus.getLevel().getLevel());
log.setIsContributor(isContributor ? 1 : 0);
if (isContributor && contributor != null) {
log.setContributionEnergy(contributor.getEnergy());
log.setContributionRate(contributor.getContributionRate());
} else {
log.setContributionEnergy(0L);
log.setContributionRate(null);
}
log.setRewardType("GOLD");
log.setRewardItemId(null);
log.setRewardAmount(rewardAmount);
log.setWalletBizNo(walletBizNo);
log.setClaimedAt(LocalDateTime.now());
log.setCreatedAt(LocalDateTime.now());
rocketRewardLogGateway.save(log);
}
}

View File

@ -0,0 +1,72 @@
package com.red.circle.other.app.listener;
import com.red.circle.other.app.dto.cmd.RocketEnergyAddCmd;
import com.red.circle.other.app.service.RocketService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* 礼物送出事件监听器 - 触发火箭能量增加
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GiftSendRocketListener {
private final RocketService rocketService;
/**
* 监听礼物送出事件
* 注意需要项目中实际的GiftSendEvent类这里使用占位符
*/
@Async("rocketExecutor")
@EventListener
public void onGiftSend(Object event) {
// TODO: 替换为实际的GiftSendEvent类型
// 示例代码需要根据实际事件结构调整
/*
try {
// 1. 从事件中提取数据
Long roomId = event.getRoomId();
Long userId = event.getUserId();
Long giftGoldValue = event.getGiftGoldValue();
Boolean isLucky = event.getIsLuckyGift();
String giftOrderId = event.getGiftOrderId();
// 2. 构建命令
RocketEnergyAddCmd cmd = new RocketEnergyAddCmd();
cmd.setRoomId(roomId);
cmd.setUserId(userId);
cmd.setGiftGoldValue(giftGoldValue);
cmd.setIsLucky(isLucky != null && isLucky);
cmd.setBizNo("GIFT_" + giftOrderId);
// 3. 执行能量增加
rocketService.addEnergy(cmd);
log.info("礼物事件触发火箭能量增加成功: roomId={}, userId={}, energy={}",
roomId, userId, giftGoldValue);
} catch (Exception e) {
log.error("礼物事件触发火箭能量增加失败", e);
}
*/
log.warn("GiftSendRocketListener: 需要接入实际的礼物送出事件");
}
/**
* 说明
* 1. 需要在项目中找到实际的礼物送出事件类可能在live-service或other-service中
* 2. 替换方法参数中的 Object 为实际的事件类型
* 3. 根据实际事件结构提取 roomIduserIdgiftGoldValue 等字段
* 4. 确保事件中包含 isLuckyGift 字段或通过其他方式判断是否幸运礼物
*/
}

View File

@ -0,0 +1,178 @@
package com.red.circle.other.app.manager;
import com.alibaba.fastjson.JSON;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 火箭IM消息推送管理器
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketImPushManager {
// TODO: 注入实际的IM推送服务
// private final ImPushService imPushService;
/**
* 推送能量更新消息
*/
public void pushEnergyUpdate(Long roomId, RocketStatusCO status) {
try {
// 构建消息
ImMessage message = buildEnergyUpdateMessage(status);
// TODO: 调用实际的IM推送服务
// imPushService.pushToRoom(roomId, message);
log.info("推送火箭能量更新消息成功: roomId={}, level={}, energy={}/{}",
roomId, status.getLevel(), status.getCurrentEnergy(), status.getMaxEnergy());
} catch (Exception e) {
log.error("推送火箭能量更新消息失败: roomId={}", roomId, e);
}
}
/**
* 推送火箭发射消息
*/
public void pushRocketLaunch(Long roomId, RocketStatusCO status) {
try {
// 构建消息
ImMessage message = buildLaunchMessage(status);
// TODO: 调用实际的IM推送服务
// imPushService.pushToRoom(roomId, message);
log.info("推送火箭发射消息成功: roomId={}, level={}", roomId, status.getLevel());
} catch (Exception e) {
log.error("推送火箭发射消息失败: roomId={}", roomId, e);
}
}
/**
* 推送奖励领取成功消息
*/
public void pushRewardClaimed(Long roomId, Long userId, String userName, Long rewardAmount) {
try {
// 构建消息
ImMessage message = buildRewardClaimedMessage(userId, userName, rewardAmount);
// TODO: 调用实际的IM推送服务
// imPushService.pushToRoom(roomId, message);
log.info("推送奖励领取消息成功: roomId={}, userId={}, amount={}", roomId, userId, rewardAmount);
} catch (Exception e) {
log.error("推送奖励领取消息失败: roomId={}, userId={}", roomId, userId, e);
}
}
/**
* 推送火箭重置消息
*/
public void pushRocketReset(Long roomId) {
try {
// 构建消息
ImMessage message = buildResetMessage();
// TODO: 调用实际的IM推送服务
// imPushService.pushToRoom(roomId, message);
log.info("推送火箭重置消息成功: roomId={}", roomId);
} catch (Exception e) {
log.error("推送火箭重置消息失败: roomId={}", roomId, e);
}
}
/**
* 构建能量更新消息
*/
private ImMessage buildEnergyUpdateMessage(RocketStatusCO status) {
ImMessage message = new ImMessage();
message.setType("ROCKET_ENERGY_UPDATE");
message.setData(JSON.toJSONString(status));
return message;
}
/**
* 构建发射消息
*/
private ImMessage buildLaunchMessage(RocketStatusCO status) {
ImMessage message = new ImMessage();
message.setType("ROCKET_LAUNCH");
message.setData(JSON.toJSONString(status));
return message;
}
/**
* 构建奖励领取消息
*/
private ImMessage buildRewardClaimedMessage(Long userId, String userName, Long rewardAmount) {
RewardClaimedData data = new RewardClaimedData();
data.setUserId(userId);
data.setUserName(userName);
data.setRewardAmount(rewardAmount);
ImMessage message = new ImMessage();
message.setType("ROCKET_REWARD_CLAIMED");
message.setData(JSON.toJSONString(data));
return message;
}
/**
* 构建重置消息
*/
private ImMessage buildResetMessage() {
ImMessage message = new ImMessage();
message.setType("ROCKET_RESET");
message.setData("{}");
return message;
}
/**
* IM消息对象示例
* TODO: 替换为实际的IM消息类
*/
private static class ImMessage {
private String type;
private String data;
public void setType(String type) {
this.type = type;
}
public void setData(String data) {
this.data = data;
}
}
/**
* 奖励领取数据
*/
private static class RewardClaimedData {
private Long userId;
private String userName;
private Long rewardAmount;
public void setUserId(Long userId) {
this.userId = userId;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setRewardAmount(Long rewardAmount) {
this.rewardAmount = rewardAmount;
}
}
}

View File

@ -0,0 +1,39 @@
package com.red.circle.other.domain.gateway;
import com.red.circle.other.domain.rocket.RocketConfig;
import java.util.List;
/**
* 火箭配置网关
*
* @author system
* @date 2025-01-15
*/
public interface RocketConfigGateway {
/**
* 根据等级查询配置
*/
RocketConfig findByLevel(Integer level);
/**
* 查询所有配置
*/
List<RocketConfig> findAll();
/**
* 查询启用的配置
*/
List<RocketConfig> findAllEnabled();
/**
* 保存配置
*/
void save(RocketConfig config);
/**
* 更新配置
*/
void update(RocketConfig config);
}

View File

@ -0,0 +1,45 @@
package com.red.circle.other.domain.gateway;
import com.red.circle.other.domain.rocket.RocketHistory;
import java.time.LocalDateTime;
import java.util.List;
/**
* 火箭历史网关
*
* @author system
* @date 2025-01-15
*/
public interface RocketHistoryGateway {
/**
* 保存历史记录
*/
String save(RocketHistory history);
/**
* 根据ID查询
*/
RocketHistory findById(String id);
/**
* 根据房间ID查询历史
*/
List<RocketHistory> findByRoomId(Long roomId);
/**
* 根据房间ID和日期查询
*/
List<RocketHistory> findByRoomIdAndDate(Long roomId, String date);
/**
* 根据房间ID和时间范围查询
*/
List<RocketHistory> findByRoomIdAndTimeRange(Long roomId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 更新领取统计
*/
void updateClaimStats(String id, int claimedCount, long rewardSent);
}

View File

@ -0,0 +1,39 @@
package com.red.circle.other.domain.gateway;
import com.red.circle.other.domain.rocket.RocketRewardLog;
import java.util.List;
/**
* 火箭奖励领取记录网关
*
* @author system
* @date 2025-01-15
*/
public interface RocketRewardLogGateway {
/**
* 保存领取记录
*/
void save(RocketRewardLog log);
/**
* 检查是否已领取
*/
boolean existsByRocketAndUser(String rocketHistoryId, Long userId);
/**
* 根据火箭历史ID查询所有领取记录
*/
List<RocketRewardLog> findByRocketHistoryId(String rocketHistoryId);
/**
* 根据房间ID和用户ID查询
*/
List<RocketRewardLog> findByRoomIdAndUserId(Long roomId, Long userId);
/**
* 统计某次发射的领取人数
*/
int countByRocketHistoryId(String rocketHistoryId);
}

View File

@ -0,0 +1,83 @@
package com.red.circle.other.domain.rocket;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 火箭配置领域对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketConfig {
/**
* 主键ID
*/
private Long id;
/**
* 火箭等级
*/
private Integer level;
/**
* 最大能量值
*/
private Long maxEnergy;
/**
* 奖励配置(JSON预留)
*/
private String rewardConfig;
/**
* 贡献者奖励比例%
*/
private BigDecimal contributorRewardRate;
/**
* 普通用户奖励配置(JSON预留)
*/
private String normalRewardConfig;
/**
* 状态 1:启用 0:禁用
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
/**
* 是否启用
*/
public boolean isEnabled() {
return Integer.valueOf(1).equals(this.status);
}
/**
* 获取等级名称
*/
public String getLevelName() {
if (level == null) {
return "";
}
return switch (level) {
case 1 -> "一级火箭";
case 2 -> "二级火箭";
case 3 -> "三级火箭";
default -> "未知等级";
};
}
}

View File

@ -0,0 +1,122 @@
package com.red.circle.other.domain.rocket;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 火箭历史领域对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketHistory {
private String id;
private Long roomId;
private String date;
private Integer level;
private Long finalEnergy;
private Long overEnergy;
private List<HistoryContributor> contributors;
private Integer totalContributors;
private RewardPool rewardPool;
private ClaimStats claimStats;
private LocalDateTime launchTime;
private LocalDateTime claimStartTime;
private LocalDateTime claimEndTime;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@Data
public static class HistoryContributor {
private Long userId;
private String userName;
private String userAvatar;
private Long energy;
private BigDecimal contributionRate;
private Integer rank;
}
@Data
public static class RewardPool {
private Long totalGold;
private Long contributorGold;
private Long normalGold;
public static RewardPool empty() {
RewardPool pool = new RewardPool();
pool.setTotalGold(0L);
pool.setContributorGold(0L);
pool.setNormalGold(0L);
return pool;
}
}
@Data
public static class ClaimStats {
private Integer totalClaimed;
private Integer contributorClaimed;
private Integer normalClaimed;
private Long totalRewardSent;
public static ClaimStats init() {
ClaimStats stats = new ClaimStats();
stats.setTotalClaimed(0);
stats.setContributorClaimed(0);
stats.setNormalClaimed(0);
stats.setTotalRewardSent(0L);
return stats;
}
}
/**
* 从RocketStatus创建历史记录
*/
public static RocketHistory fromRocketStatus(RocketStatus status) {
RocketHistory history = new RocketHistory();
history.setRoomId(status.getRoomId());
history.setDate(status.getDate());
history.setLevel(status.getLevel().getLevel());
history.setFinalEnergy(status.getCurrentEnergy());
// 计算超出能量
Long overEnergy = status.getCurrentEnergy() - status.getLevel().getMaxEnergy();
history.setOverEnergy(Math.max(overEnergy, 0L));
// 转换贡献者
List<HistoryContributor> historyContributors = new ArrayList<>();
for (int i = 0; i < Math.min(status.getContributors().size(), 100); i++) {
RocketContributor contributor = status.getContributors().get(i);
HistoryContributor hc = new HistoryContributor();
hc.setUserId(contributor.getUserId());
hc.setUserName(contributor.getUserName());
hc.setUserAvatar(contributor.getUserAvatar());
hc.setEnergy(contributor.getEnergy());
hc.setContributionRate(contributor.getContributionRate());
hc.setRank(i + 1);
historyContributors.add(hc);
}
history.setContributors(historyContributors);
history.setTotalContributors(status.getContributors().size());
// 设置奖励池 (预留)
history.setRewardPool(RewardPool.empty());
// 初始化领取统计
history.setClaimStats(ClaimStats.init());
// 设置时间
history.setLaunchTime(status.getLaunchTime());
history.setClaimStartTime(status.getLaunchTime());
history.setClaimEndTime(status.getClaimExpireTime());
history.setCreatedAt(LocalDateTime.now());
history.setUpdatedAt(LocalDateTime.now());
return history;
}
}

View File

@ -0,0 +1,100 @@
package com.red.circle.other.domain.rocket;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 火箭奖励领取记录领域对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketRewardLog {
/**
* 主键ID
*/
private Long id;
/**
* 火箭历史记录ID (MongoDB _id)
*/
private String rocketHistoryId;
/**
* 房间ID
*/
private Long roomId;
/**
* 用户ID
*/
private Long userId;
/**
* 火箭等级
*/
private Integer rocketLevel;
/**
* 是否贡献者 1: 0:
*/
private Integer isContributor;
/**
* 贡献能量值
*/
private Long contributionEnergy;
/**
* 贡献率%
*/
private BigDecimal contributionRate;
/**
* 奖励类型 GOLD/DIAMOND/PROP
*/
private String rewardType;
/**
* 奖励物品ID
*/
private Long rewardItemId;
/**
* 奖励数量
*/
private Long rewardAmount;
/**
* 钱包业务单号
*/
private String walletBizNo;
/**
* 领取时间
*/
private LocalDateTime claimedAt;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 判断是否为贡献者
*/
public boolean isContributorUser() {
return Integer.valueOf(1).equals(this.isContributor);
}
/**
* 构建业务单号
*/
public static String buildBizNo(String rocketHistoryId, Long userId) {
return "ROCKET_" + rocketHistoryId + "_" + userId;
}
}

View File

@ -0,0 +1,31 @@
package com.red.circle.other.infra.convertor;
import com.red.circle.other.domain.rocket.RocketConfig;
import com.red.circle.other.infra.database.rds.entity.RocketConfigEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* 火箭配置转换器
*
* @author system
* @date 2025-01-15
*/
@Mapper(componentModel = "spring")
public interface RocketConfigConvertor {
RocketConfigConvertor INSTANCE = Mappers.getMapper(RocketConfigConvertor.class);
/**
* Entity转Domain
*/
RocketConfig toRocketConfig(RocketConfigEntity entity);
/**
* Domain转Entity
*/
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
RocketConfigEntity toEntity(RocketConfig config);
}

View File

@ -0,0 +1,28 @@
package com.red.circle.other.infra.convertor;
import com.red.circle.other.domain.rocket.RocketHistory;
import com.red.circle.other.infra.database.mongo.document.RocketHistoryDocument;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 火箭历史转换器
*
* @author system
* @date 2025-01-15
*/
@Mapper(componentModel = "spring")
public interface RocketHistoryConvertor {
RocketHistoryConvertor INSTANCE = Mappers.getMapper(RocketHistoryConvertor.class);
/**
* Document转Domain
*/
RocketHistory toDomain(RocketHistoryDocument document);
/**
* Domain转Document
*/
RocketHistoryDocument toDocument(RocketHistory history);
}

View File

@ -0,0 +1,30 @@
package com.red.circle.other.infra.convertor;
import com.red.circle.other.domain.rocket.RocketRewardLog;
import com.red.circle.other.infra.database.rds.entity.RocketRewardClaimLogEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* 火箭奖励领取记录转换器
*
* @author system
* @date 2025-01-15
*/
@Mapper(componentModel = "spring")
public interface RocketRewardLogConvertor {
RocketRewardLogConvertor INSTANCE = Mappers.getMapper(RocketRewardLogConvertor.class);
/**
* Entity转Domain
*/
RocketRewardLog toDomain(RocketRewardClaimLogEntity entity);
/**
* Domain转Entity
*/
@Mapping(target = "createdAt", ignore = true)
RocketRewardClaimLogEntity toEntity(RocketRewardLog log);
}

View File

@ -0,0 +1,113 @@
package com.red.circle.other.infra.database.mongo.document;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDateTime;
import java.util.List;
/**
* 火箭发射历史文档
*
* @author system
* @date 2025-01-15
*/
@Data
@Document(collection = "rocket_launch_history")
public class RocketHistoryDocument {
@Id
private String id;
@Field("room_id")
private Long roomId;
@Field("date")
private String date;
@Field("level")
private Integer level;
@Field("final_energy")
private Long finalEnergy;
@Field("over_energy")
private Long overEnergy;
@Field("contributors")
private List<HistoryContributor> contributors;
@Field("total_contributors")
private Integer totalContributors;
@Field("reward_pool")
private RewardPool rewardPool;
@Field("claim_stats")
private ClaimStats claimStats;
@Field("launch_time")
private LocalDateTime launchTime;
@Field("claim_start_time")
private LocalDateTime claimStartTime;
@Field("claim_end_time")
private LocalDateTime claimEndTime;
@Field("created_at")
private LocalDateTime createdAt;
@Field("updated_at")
private LocalDateTime updatedAt;
@Data
public static class HistoryContributor {
@Field("user_id")
private Long userId;
@Field("user_name")
private String userName;
@Field("user_avatar")
private String userAvatar;
@Field("energy")
private Long energy;
@Field("contribution_rate")
private Double contributionRate;
@Field("rank")
private Integer rank;
}
@Data
public static class RewardPool {
@Field("total_gold")
private Long totalGold;
@Field("contributor_gold")
private Long contributorGold;
@Field("normal_gold")
private Long normalGold;
}
@Data
public static class ClaimStats {
@Field("total_claimed")
private Integer totalClaimed;
@Field("contributor_claimed")
private Integer contributorClaimed;
@Field("normal_claimed")
private Integer normalClaimed;
@Field("total_reward_sent")
private Long totalRewardSent;
}
}

View File

@ -0,0 +1,42 @@
package com.red.circle.other.infra.database.mongo.repository;
import com.red.circle.other.infra.database.mongo.document.RocketHistoryDocument;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
* 火箭历史Repository
*
* @author system
* @date 2025-01-15
*/
@Repository
public interface RocketHistoryRepository extends MongoRepository<RocketHistoryDocument, String> {
/**
* 根据房间ID查询历史
*/
List<RocketHistoryDocument> findByRoomIdOrderByLaunchTimeDesc(Long roomId);
/**
* 根据房间ID和日期查询
*/
List<RocketHistoryDocument> findByRoomIdAndDateOrderByLaunchTimeDesc(Long roomId, String date);
/**
* 根据房间ID和时间范围查询
*/
List<RocketHistoryDocument> findByRoomIdAndLaunchTimeBetweenOrderByLaunchTimeDesc(
Long roomId,
LocalDateTime startTime,
LocalDateTime endTime
);
/**
* 根据等级查询
*/
List<RocketHistoryDocument> findByLevelOrderByLaunchTimeDesc(Integer level);
}

View File

@ -0,0 +1,47 @@
package com.red.circle.other.infra.database.rds.dao;
import com.red.circle.other.infra.database.rds.entity.RocketConfigEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 火箭配置Mapper
*
* @author system
* @date 2025-01-15
*/
@Mapper
public interface RocketConfigMapper {
/**
* 根据等级查询
*/
RocketConfigEntity selectByLevel(@Param("level") Integer level);
/**
* 查询所有
*/
List<RocketConfigEntity> selectAll();
/**
* 查询启用的
*/
List<RocketConfigEntity> selectAllEnabled();
/**
* 插入
*/
int insert(RocketConfigEntity entity);
/**
* 更新
*/
int update(RocketConfigEntity entity);
/**
* 根据ID查询
*/
RocketConfigEntity selectById(@Param("id") Long id);
}

View File

@ -0,0 +1,44 @@
package com.red.circle.other.infra.database.rds.dao;
import com.red.circle.other.infra.database.rds.entity.RocketRewardClaimLogEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 火箭奖励领取记录Mapper
*
* @author system
* @date 2025-01-15
*/
@Mapper
public interface RocketRewardClaimLogMapper {
/**
* 插入
*/
int insert(RocketRewardClaimLogEntity entity);
/**
* 检查是否已领取
*/
int countByRocketAndUser(@Param("rocketHistoryId") String rocketHistoryId,
@Param("userId") Long userId);
/**
* 根据火箭历史ID查询
*/
List<RocketRewardClaimLogEntity> selectByRocketHistoryId(@Param("rocketHistoryId") String rocketHistoryId);
/**
* 根据房间ID和用户ID查询
*/
List<RocketRewardClaimLogEntity> selectByRoomIdAndUserId(@Param("roomId") Long roomId,
@Param("userId") Long userId);
/**
* 统计某次发射的领取人数
*/
int countByRocketHistoryId(@Param("rocketHistoryId") String rocketHistoryId);
}

View File

@ -0,0 +1,63 @@
package com.red.circle.other.infra.database.rds.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 火箭配置实体
*
* @author system
* @date 2025-01-15
*/
@Data
@TableName("rocket_config")
public class RocketConfigEntity {
/**
* 主键ID
*/
private Long id;
/**
* 火箭等级 1/2/3
*/
private Integer level;
/**
* 最大能量值
*/
private Long maxEnergy;
/**
* 奖励配置(JSON)
*/
private String rewardConfig;
/**
* 贡献者奖励比例%
*/
private BigDecimal contributorRewardRate;
/**
* 普通用户奖励配置(JSON)
*/
private String normalRewardConfig;
/**
* 状态 1:启用 0:禁用
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,88 @@
package com.red.circle.other.infra.database.rds.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 火箭奖励领取记录实体
*
* @author system
* @date 2025-01-15
*/
@Data
@TableName("rocket_reward_claim_log")
public class RocketRewardClaimLogEntity {
/**
* 主键ID
*/
private Long id;
/**
* 火箭历史记录ID
*/
private String rocketHistoryId;
/**
* 房间ID
*/
private Long roomId;
/**
* 用户ID
*/
private Long userId;
/**
* 火箭等级
*/
private Integer rocketLevel;
/**
* 是否贡献者
*/
private Integer isContributor;
/**
* 贡献能量值
*/
private Long contributionEnergy;
/**
* 贡献率%
*/
private BigDecimal contributionRate;
/**
* 奖励类型
*/
private String rewardType;
/**
* 奖励物品ID
*/
private Long rewardItemId;
/**
* 奖励数量
*/
private Long rewardAmount;
/**
* 钱包业务单号
*/
private String walletBizNo;
/**
* 领取时间
*/
private LocalDateTime claimedAt;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,63 @@
package com.red.circle.other.infra.gateway;
import com.red.circle.other.domain.gateway.RocketConfigGateway;
import com.red.circle.other.domain.rocket.RocketConfig;
import com.red.circle.other.infra.convertor.RocketConfigConvertor;
import com.red.circle.other.infra.database.rds.dao.RocketConfigMapper;
import com.red.circle.other.infra.database.rds.entity.RocketConfigEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 火箭配置网关实现
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketConfigGatewayImpl implements RocketConfigGateway {
private final RocketConfigMapper rocketConfigMapper;
private final RocketConfigConvertor rocketConfigConvertor;
@Override
public RocketConfig findByLevel(Integer level) {
RocketConfigEntity entity = rocketConfigMapper.selectByLevel(level);
return rocketConfigConvertor.toRocketConfig(entity);
}
@Override
public List<RocketConfig> findAll() {
List<RocketConfigEntity> entities = rocketConfigMapper.selectAll();
return entities.stream()
.map(rocketConfigConvertor::toRocketConfig)
.collect(Collectors.toList());
}
@Override
public List<RocketConfig> findAllEnabled() {
List<RocketConfigEntity> entities = rocketConfigMapper.selectAllEnabled();
return entities.stream()
.map(rocketConfigConvertor::toRocketConfig)
.collect(Collectors.toList());
}
@Override
public void save(RocketConfig config) {
RocketConfigEntity entity = rocketConfigConvertor.toEntity(config);
rocketConfigMapper.insert(entity);
config.setId(entity.getId());
}
@Override
public void update(RocketConfig config) {
RocketConfigEntity entity = rocketConfigConvertor.toEntity(config);
rocketConfigMapper.update(entity);
}
}

View File

@ -0,0 +1,88 @@
package com.red.circle.other.infra.gateway;
import com.red.circle.other.domain.gateway.RocketHistoryGateway;
import com.red.circle.other.domain.rocket.RocketHistory;
import com.red.circle.other.infra.convertor.RocketHistoryConvertor;
import com.red.circle.other.infra.database.mongo.document.RocketHistoryDocument;
import com.red.circle.other.infra.database.mongo.repository.RocketHistoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 火箭历史网关实现
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketHistoryGatewayImpl implements RocketHistoryGateway {
private final RocketHistoryRepository rocketHistoryRepository;
private final RocketHistoryConvertor rocketHistoryConvertor;
private final MongoTemplate mongoTemplate;
@Override
public String save(RocketHistory history) {
RocketHistoryDocument document = rocketHistoryConvertor.toDocument(history);
document.setCreatedAt(LocalDateTime.now());
document.setUpdatedAt(LocalDateTime.now());
RocketHistoryDocument saved = rocketHistoryRepository.save(document);
return saved.getId();
}
@Override
public RocketHistory findById(String id) {
return rocketHistoryRepository.findById(id)
.map(rocketHistoryConvertor::toDomain)
.orElse(null);
}
@Override
public List<RocketHistory> findByRoomId(Long roomId) {
List<RocketHistoryDocument> documents = rocketHistoryRepository.findByRoomIdOrderByLaunchTimeDesc(roomId);
return documents.stream()
.map(rocketHistoryConvertor::toDomain)
.collect(Collectors.toList());
}
@Override
public List<RocketHistory> findByRoomIdAndDate(Long roomId, String date) {
List<RocketHistoryDocument> documents =
rocketHistoryRepository.findByRoomIdAndDateOrderByLaunchTimeDesc(roomId, date);
return documents.stream()
.map(rocketHistoryConvertor::toDomain)
.collect(Collectors.toList());
}
@Override
public List<RocketHistory> findByRoomIdAndTimeRange(Long roomId, LocalDateTime startTime, LocalDateTime endTime) {
List<RocketHistoryDocument> documents =
rocketHistoryRepository.findByRoomIdAndLaunchTimeBetweenOrderByLaunchTimeDesc(
roomId, startTime, endTime);
return documents.stream()
.map(rocketHistoryConvertor::toDomain)
.collect(Collectors.toList());
}
@Override
public void updateClaimStats(String id, int claimedCount, long rewardSent) {
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update()
.inc("claim_stats.total_claimed", 1)
.inc("claim_stats.total_reward_sent", rewardSent)
.set("updated_at", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, RocketHistoryDocument.class);
}
}

View File

@ -0,0 +1,64 @@
package com.red.circle.other.infra.gateway;
import com.red.circle.other.domain.gateway.RocketRewardLogGateway;
import com.red.circle.other.domain.rocket.RocketRewardLog;
import com.red.circle.other.infra.convertor.RocketRewardLogConvertor;
import com.red.circle.other.infra.database.rds.dao.RocketRewardClaimLogMapper;
import com.red.circle.other.infra.database.rds.entity.RocketRewardClaimLogEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 火箭奖励领取记录网关实现
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketRewardLogGatewayImpl implements RocketRewardLogGateway {
private final RocketRewardClaimLogMapper rocketRewardClaimLogMapper;
private final RocketRewardLogConvertor rocketRewardLogConvertor;
@Override
public void save(RocketRewardLog log) {
RocketRewardClaimLogEntity entity = rocketRewardLogConvertor.toEntity(log);
rocketRewardClaimLogMapper.insert(entity);
log.setId(entity.getId());
}
@Override
public boolean existsByRocketAndUser(String rocketHistoryId, Long userId) {
int count = rocketRewardClaimLogMapper.countByRocketAndUser(rocketHistoryId, userId);
return count > 0;
}
@Override
public List<RocketRewardLog> findByRocketHistoryId(String rocketHistoryId) {
List<RocketRewardClaimLogEntity> entities =
rocketRewardClaimLogMapper.selectByRocketHistoryId(rocketHistoryId);
return entities.stream()
.map(rocketRewardLogConvertor::toDomain)
.collect(Collectors.toList());
}
@Override
public List<RocketRewardLog> findByRoomIdAndUserId(Long roomId, Long userId) {
List<RocketRewardClaimLogEntity> entities =
rocketRewardClaimLogMapper.selectByRoomIdAndUserId(roomId, userId);
return entities.stream()
.map(rocketRewardLogConvertor::toDomain)
.collect(Collectors.toList());
}
@Override
public int countByRocketHistoryId(String rocketHistoryId) {
return rocketRewardClaimLogMapper.countByRocketHistoryId(rocketHistoryId);
}
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.other.infra.database.rds.dao.RocketConfigMapper">
<resultMap id="BaseResultMap" type="com.red.circle.other.infra.database.rds.entity.RocketConfigEntity">
<id column="id" property="id"/>
<result column="level" property="level"/>
<result column="max_energy" property="maxEnergy"/>
<result column="reward_config" property="rewardConfig"/>
<result column="contributor_reward_rate" property="contributorRewardRate"/>
<result column="normal_reward_config" property="normalRewardConfig"/>
<result column="status" property="status"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<sql id="Base_Column_List">
id, level, max_energy, reward_config, contributor_reward_rate,
normal_reward_config, status, created_at, updated_at
</sql>
<select id="selectByLevel" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM rocket_config
WHERE level = #{level}
</select>
<select id="selectAll" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM rocket_config
ORDER BY level ASC
</select>
<select id="selectAllEnabled" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM rocket_config
WHERE status = 1
ORDER BY level ASC
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM rocket_config
WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.red.circle.other.infra.database.rds.entity.RocketConfigEntity"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO rocket_config (
level, max_energy, reward_config, contributor_reward_rate,
normal_reward_config, status, created_at, updated_at
) VALUES (
#{level}, #{maxEnergy}, #{rewardConfig}, #{contributorRewardRate},
#{normalRewardConfig}, #{status}, #{createdAt}, #{updatedAt}
)
</insert>
<update id="update" parameterType="com.red.circle.other.infra.database.rds.entity.RocketConfigEntity">
UPDATE rocket_config
<set>
<if test="maxEnergy != null">max_energy = #{maxEnergy},</if>
<if test="rewardConfig != null">reward_config = #{rewardConfig},</if>
<if test="contributorRewardRate != null">contributor_reward_rate = #{contributorRewardRate},</if>
<if test="normalRewardConfig != null">normal_reward_config = #{normalRewardConfig},</if>
<if test="status != null">status = #{status},</if>
updated_at = NOW()
</set>
WHERE id = #{id}
</update>
</mapper>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.other.infra.database.rds.dao.RocketRewardClaimLogMapper">
<resultMap id="BaseResultMap" type="com.red.circle.other.infra.database.rds.entity.RocketRewardClaimLogEntity">
<id column="id" property="id"/>
<result column="rocket_history_id" property="rocketHistoryId"/>
<result column="room_id" property="roomId"/>
<result column="user_id" property="userId"/>
<result column="rocket_level" property="rocketLevel"/>
<result column="is_contributor" property="isContributor"/>
<result column="contribution_energy" property="contributionEnergy"/>
<result column="contribution_rate" property="contributionRate"/>
<result column="reward_type" property="rewardType"/>
<result column="reward_item_id" property="rewardItemId"/>
<result column="reward_amount" property="rewardAmount"/>
<result column="wallet_biz_no" property="walletBizNo"/>
<result column="claimed_at" property="claimedAt"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<sql id="Base_Column_List">
id, rocket_history_id, room_id, user_id, rocket_level, is_contributor,
contribution_energy, contribution_rate, reward_type, reward_item_id,
reward_amount, wallet_biz_no, claimed_at, created_at
</sql>
<insert id="insert" parameterType="com.red.circle.other.infra.database.rds.entity.RocketRewardClaimLogEntity"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO rocket_reward_claim_log (
rocket_history_id, room_id, user_id, rocket_level, is_contributor,
contribution_energy, contribution_rate, reward_type, reward_item_id,
reward_amount, wallet_biz_no, claimed_at, created_at
) VALUES (
#{rocketHistoryId}, #{roomId}, #{userId}, #{rocketLevel}, #{isContributor},
#{contributionEnergy}, #{contributionRate}, #{rewardType}, #{rewardItemId},
#{rewardAmount}, #{walletBizNo}, #{claimedAt}, #{createdAt}
)
</insert>
<select id="countByRocketAndUser" resultType="int">
SELECT COUNT(1)
FROM rocket_reward_claim_log
WHERE rocket_history_id = #{rocketHistoryId}
AND user_id = #{userId}
</select>
<select id="selectByRocketHistoryId" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM rocket_reward_claim_log
WHERE rocket_history_id = #{rocketHistoryId}
ORDER BY claimed_at DESC
</select>
<select id="selectByRoomIdAndUserId" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM rocket_reward_claim_log
WHERE room_id = #{roomId}
AND user_id = #{userId}
ORDER BY claimed_at DESC
</select>
<select id="countByRocketHistoryId" resultType="int">
SELECT COUNT(1)
FROM rocket_reward_claim_log
WHERE rocket_history_id = #{rocketHistoryId}
</select>
</mapper>