From 62c9864ea505f846486a2985f37cc9a06dd2009e Mon Sep 17 00:00:00 2001 From: tianfeng <769204422@qq.com> Date: Thu, 15 Jan 2026 10:56:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E7=E5=A4=A9=E7=AD=BE=E5=88=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/enums/SendPropsOrigin.java | 5 + .../other/inner/asserts/OtherErrorCode.java | 37 + .../wallet/inner/model/enums/GoldOrigin.java | 10 + .../docs/OTHER模块DDD架构说明.md | 688 ++++++++++++++++++ .../adapter/app/signin/SignInController.java | 56 ++ .../app/command/signin/SignCheckInExe.java | 151 ++++ .../command/signin/SignInStatusQueryExe.java | 88 +++ .../command/signin/SupplementCheckInExe.java | 156 ++++ .../app/service/signin/SignInServiceImpl.java | 41 ++ .../clientobject/signin/CheckInResultCO.java | 15 + .../signin/SignInDailyConfigCO.java | 37 + .../clientobject/signin/SignInStatusCO.java | 20 + .../other/app/dto/cmd/signin/CheckInCmd.java | 12 + .../app/dto/cmd/signin/SupplementCmd.java | 18 + .../app/service/signin/SignInService.java | 15 + .../rds/dao/checkin/SignInDailyConfigDAO.java | 7 + .../rds/dao/checkin/UserSignInRecordDAO.java | 7 + .../rds/entity/SignInDailyConfig.java | 31 + .../database/rds/entity/UserSignInRecord.java | 44 ++ .../src/test/java/AnchorTargetTest.java | 22 +- 20 files changed, 1444 insertions(+), 16 deletions(-) create mode 100644 rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md create mode 100644 rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/signin/SignInController.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignCheckInExe.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignInStatusQueryExe.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SupplementCheckInExe.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/signin/SignInServiceImpl.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/CheckInResultCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInDailyConfigCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInStatusCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/CheckInCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/SupplementCmd.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/signin/SignInService.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/SignInDailyConfigDAO.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/UserSignInRecordDAO.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/SignInDailyConfig.java create mode 100644 rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserSignInRecord.java diff --git a/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java b/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java index bb47cc5c..a3e7870b 100644 --- a/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java +++ b/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java @@ -281,6 +281,11 @@ public enum SendPropsOrigin { */ SEASON_CP_GIFT(""), + /** + * 签到奖励 + */ + SIGN_IN_REWARD("Sign in Rewards"), + ; private final String desc; diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java index bbd3f6db..78268457 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java @@ -172,6 +172,43 @@ public enum OtherErrorCode implements IResponseErrorCode { * 用户财富等级不足10级 */ USER_WEALTH_NEED_THAN_10(40303, "User wealth level needs to be greater than 10"), + + // ==================== 签到相关错误码 3280-3299 ==================== + + /** + * 签到配置不存在 + */ + SIGN_IN_CONFIG_NOT_FOUND(3280, "Sign-in configuration not found"), + + /** + * 今天已经签到过了 + */ + SIGN_IN_ALREADY_CHECKED(3281, "Already signed in today"), + + /** + * 不能提前签到 + */ + SIGN_IN_CANNOT_ADVANCE(3282, "Cannot sign in advance"), + + /** + * 请先完成之前的签到或补签 + */ + SIGN_IN_NEED_PREVIOUS(3283, "Please complete previous sign-ins or make-up first"), + + /** + * 只能补签过去的日期 + */ + SIGN_IN_SUPPLEMENT_ONLY_PAST(3284, "Can only make up for past dates"), + + /** + * 该日期已经签到 + */ + SIGN_IN_DATE_ALREADY_CHECKED(3285, "This date has already been signed in"), + + /** + * 签到操作频繁 + */ + SIGN_IN_OPERATION_TOO_FREQUENT(3286, "Sign-in operation too frequent, please try again later"), ; private final int code; diff --git a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java index f1ba2d1a..f3208aec 100644 --- a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java +++ b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java @@ -747,6 +747,16 @@ public enum GoldOrigin { * cp榜单(周榜和季度榜) */ CP_RANKING_REWARD("CP ranking reward"), + + /** + * 签到付费 + */ + SIGN_IN_PAID("Sign-in paid"), + + /** + * 签到补签 + */ + SIGN_IN_SUPPLEMENT("Sign-in supplement"), ; private final String desc; diff --git a/rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md b/rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md new file mode 100644 index 00000000..b950f0c6 --- /dev/null +++ b/rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md @@ -0,0 +1,688 @@ +# Other 模块 DDD 架构说明文档 + +## 一、模块概览 + +Other 服务是一个综合性的业务模块,包含用户、房间、游戏、活动、家族、礼物、任务等多个子领域。采用标准的 DDD(领域驱动设计)+ CQRS 架构模式。 + +### 模块列表 + +``` +rc-service-other/ +├── other-domain # 领域层(核心业务实体) +├── other-infrastructure # 基础设施层(数据访问、外部服务) +├── other-client # 客户端接口层(DTO、Service接口) +├── other-application # 应用层(业务逻辑实现) +├── other-adapter # 适配层(REST API、Controller) +├── other-inner-endpoint # 内部服务端点(跨服务调用实现) +└── other-start # 启动模块(Spring Boot入口) +``` + +--- + +## 二、各模块详细说明 + +### 1. other-domain(领域层) +**作用**:定义核心领域模型和业务规则 +**依赖**:无外部依赖(最纯净的业务模型) +**路径**:`other-domain/src/main/java/com/red/circle/other/domain/` + +**职责**: +- 定义领域实体(Entity) +- 定义值对象(Value Object) +- 定义领域服务接口 +- 不包含任何基础设施相关代码 + +**典型文件**: +- 领域实体定义 +- 业务规则封装 + +--- + +### 2. other-infrastructure(基础设施层) +**作用**:提供技术基础设施支持 +**依赖**:`other-domain`、框架组件(MyBatis、Redis、MongoDB、RocketMQ) +**路径**:`other-infrastructure/src/main/java/com/red/circle/other/infra/` + +**目录结构**: +``` +infra/ +├── database/ # 数据访问 +│ ├── rds/ # MySQL 数据库 +│ │ ├── dao/ # MyBatis DAO(继承BaseDAO) +│ │ ├── entity/ # 数据库实体 +│ │ ├── service/ # 数据库服务封装 +│ │ └── query/ # 查询条件构建 +│ ├── mongo/ # MongoDB 数据库 +│ └── cache/ # Redis 缓存 +├── gateway/ # 网关实现(可选) +├── convertor/ # 对象转换器 +├── config/ # 配置类 +└── utils/ # 工具类 +``` + +**关键说明**: +- **DAO 命名规范**:`{实体名}DAO`,继承 `BaseDAO` +- **Service 命名规范**:`{功能}DatabaseService` +- **不是所有功能都需要 Gateway**,简单功能直接在 CmdExe 中调用 Infrastructure 的接口 + +--- + +### 3. other-client(客户端接口层) +**作用**:定义对外暴露的接口和数据模型 +**依赖**:无 +**路径**:`other-client/src/main/java/com/red/circle/other/app/` + +**目录结构**: +``` +app/ +├── dto/ # 数据传输对象 +│ ├── cmd/ # 命令对象(写操作) +│ │ └── {功能模块}/ +│ └── clientobject/ # 客户端对象(读操作返回值) +│ └── {功能模块}/ +├── service/ # 服务接口定义 +│ └── {功能模块}/ +└── enums/ # 枚举类 +``` + +**命名规范**: +- **Cmd(命令)**:用于写操作,如 `LotteryDrawCmd`、`UserProfileModifyCmd` +- **CO(客户端对象)**:用于读操作返回,如 `LotteryActivityCO`、`UserProfileCO` +- **QryCmd(查询命令)**:用于查询操作,如 `LotteryRecordQryCmd` + +**注解规范**: +- 使用 `@Data` + 文件格式注释 +- Cmd 需要时继承 `com.red.circle.common.business.dto.cmd.AppExtCommand` +- 分页查询继承 `com.red.circle.common.business.dto.PageQueryCmd` +- 校验注解使用 `jakarta.validation.constraints.*`(不用 javax) + +--- + +### 4. other-application(应用层) +**作用**:实现业务逻辑 +**依赖**:`other-client`、`other-infrastructure` +**路径**:`other-application/src/main/java/com/red/circle/other/app/` + +**目录结构**: +``` +app/ +├── command/ # 命令执行器 +│ └── {功能模块}/ +│ ├── {功能}CmdExe.java # 写操作执行器 +│ └── query/ # 查询执行器 +│ └── {功能}QryExe.java +├── service/ # 服务实现 +│ └── {功能模块}/ +│ └── {功能}ServiceImpl.java +├── convertor/ # 对象转换器(MapStruct) +├── listener/ # 事件监听器 +├── scheduler/ # 定时任务 +├── manager/ # 业务管理器(复杂业务编排) +├── common/ # 通用业务逻辑 +└── config/ # 应用配置 +``` + +**核心原则**: +- **ServiceImpl 调用 CmdExe**:ServiceImpl 是门面,具体业务由 CmdExe 实现 +- **CmdExe 命名规范**:`{功能名}CmdExe` 或 `{功能名}QryExe` +- **是否需要 Gateway**: + - ✅ 简单功能:CmdExe 直接调用 Infrastructure 的 DatabaseService + - ✅ 复杂功能:CmdExe 通过 Gateway 封装复杂的数据访问逻辑 + - 原则:**简单直接,复杂封装,不要过度设计** + +**代码质量要求**: +- 关键业务必须添加 `@Transactional` 事务 +- 金额操作必须保证幂等性(使用 bizNo) +- 重要操作记录日志(`@Slf4j`) +- 完善的参数校验和异常处理 + +--- + +### 5. other-adapter(适配层) +**作用**:对外提供 REST API +**依赖**:`other-application` +**路径**:`other-adapter/src/main/java/com/red/circle/other/adapter/app/` + +**目录结构**: +``` +adapter/app/ +└── {功能模块}/ + └── {功能名}RestController.java +``` + +**命名规范**: +- Controller 命名:`{功能名}RestController` +- 继承 `BaseController` +- 使用 `@RestController` + `@RequestMapping` + +**注解规范**: +- API 文档使用 `@eo.` 系列注解(不使用 Swagger) +- 路径规范:`/功能模块/资源`,如 `/activity/lottery` + +**职责**: +- 接收 HTTP 请求 +- 参数校验(`@Validated`) +- 调用 Service 层 +- 返回统一格式的响应 + +--- + +### 6. other-inner-endpoint(内部服务端点) +**作用**:提供跨服务调用的实现(供其他微服务通过 Feign 调用) +**依赖**:`other-application`、`other-inner-api` +**路径**:`other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/` + +**与 other-client 的区别**: +| 维度 | other-client | other-inner-endpoint | +|------|-------------|---------------------| +| 调用方 | 前端 App、Web | 其他微服务(如 wallet、live) | +| 传输协议 | HTTP REST | HTTP Feign | +| 接口定义位置 | `other-client/service/` | `other-inner-api/endpoint/` | +| 接口实现位置 | `other-application/service/` | `other-inner-endpoint/service/` | +| DTO 定义位置 | `other-client/dto/` | `other-inner-model/model/` | + +**典型场景**: +- 钱包服务调用 other 服务获取用户信息 +- 直播服务调用 other 服务更新房间状态 + +--- + +### 7. other-start(启动模块) +**作用**:Spring Boot 应用入口 +**依赖**:`other-adapter`、`other-inner-endpoint` +**路径**:`other-start/src/main/java/com/red/circle/` + +**职责**: +- 定义 Spring Boot 主类 +- 引入所有模块依赖 +- 配置文件(application.yml) + +--- + +## 三、跨模块通信说明 + +### 内部模块调用关系图 +``` +┌─────────────────────────────────────────────────┐ +│ other-start │ +│ (Spring Boot 入口) │ +└────────────┬────────────────────┬────────────────┘ + │ │ + ┌────────▼────────┐ ┌───────▼────────────┐ + │ other-adapter │ │ other-inner-endpoint│ + │ (REST API) │ │ (Feign 服务端) │ + └────────┬────────┘ └───────┬────────────┘ + │ │ + └────────┬───────────┘ + │ + ┌─────────▼──────────┐ + │ other-application │ + │ (业务逻辑层) │ + │ ├── CmdExe │ + │ ├── ServiceImpl │ + │ └── Manager │ + └─────────┬──────────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ +┌───────▼────────┐ ┌─▼──────────┐ ┌▼──────────┐ +│ other-client │ │ other-infra│ │other-domain│ +│ (接口+DTO) │ │ (数据访问) │ │ (实体) │ +└────────────────┘ └────────────┘ └───────────┘ +``` + +### 跨服务调用说明 + +**场景示例**:钱包服务需要调用 other 服务获取用户信息 + +1. **定义 API 接口**(在 `rc-inner-api/other-inner/other-inner-api/`): +```java +// endpoint 包下定义 Feign 客户端接口 +@FeignClient(name = "other-service") +public interface UserProfileClientService { + UserProfileDTO getByUserId(Long userId); +} +``` + +2. **定义数据模型**(在 `rc-inner-api/other-inner/other-inner-model/`): +```java +// model 包下定义传输对象 +public class UserProfileDTO { + private Long userId; + private String nickname; + // ... +} +``` + +3. **实现服务端点**(在 `other-inner-endpoint/`): +```java +@RestController +public class UserProfileClientServiceImpl implements UserProfileClientService { + @Autowired + private UserProfileService userProfileService; + + public UserProfileDTO getByUserId(Long userId) { + // 调用 application 层服务 + return userProfileService.getByUserId(userId); + } +} +``` + +4. **钱包服务调用**(在 wallet 服务中): +```java +@Service +public class WalletServiceImpl { + @Autowired + private UserProfileClientService userProfileClient; // Feign 客户端 + + public void someMethod() { + UserProfileDTO user = userProfileClient.getByUserId(123L); + } +} +``` + +--- + +## 四、新增功能开发流程 + +### 标准开发顺序(必须遵循) + +以"抽奖活动"为例,演示完整的开发流程: + +#### 1. 定义数据模型(other-client) +``` +other-client/src/main/java/com/red/circle/other/app/dto/ +├── cmd/activity/ +│ ├── LotteryDrawCmd.java # 抽奖命令 +│ └── LotteryRecordQryCmd.java # 查询抽奖记录 +└── clientobject/activity/ + ├── LotteryActivityCO.java # 活动信息 + └── LotteryDrawResultCO.java # 抽奖结果 +``` + +#### 2. 定义 Service 接口(other-client) +```java +// other-client/src/main/java/com/red/circle/other/app/service/activity/ +public interface LotteryActivityRestService { + LotteryDrawResultCO draw(LotteryDrawCmd cmd); + PageResult listMyRecords(LotteryRecordQryCmd cmd); +} +``` + +#### 3. 实现 CmdExe(other-application) +``` +other-application/src/main/java/com/red/circle/other/app/command/activity/ +├── LotteryDrawExe.java # 抽奖执行器 +└── query/ + └── LotteryRecordQryExe.java # 查询执行器 +``` + +**示例代码**: +```java +@Component +@RequiredArgsConstructor +public class LotteryDrawExe { + private final LotteryActivityDatabaseService lotteryDbService; + private final LotteryPrizeGrantService prizeService; + + @Transactional + public LotteryDrawResultCO execute(LotteryDrawCmd cmd) { + // 1. 参数校验 + validateTicket(cmd.getUserId()); + + // 2. 扣除抽奖券(直接调用 Infrastructure 的服务) + lotteryDbService.consumeTicket(cmd.getUserId(), cmd.getActivityId()); + + // 3. 随机抽奖 + LotteryPrize prize = prizeService.randomDraw(cmd.getActivityId()); + + // 4. 记录中奖 + lotteryDbService.saveDrawRecord(cmd.getUserId(), prize); + + // 5. 返回结果 + return convertToResult(prize); + } +} +``` + +#### 4. 实现 ServiceImpl(other-application) +```java +@Service +@RequiredArgsConstructor +public class LotteryActivityRestServiceImpl implements LotteryActivityRestService { + private final LotteryDrawExe lotteryDrawExe; + private final LotteryRecordQryExe recordQryExe; + + @Override + public LotteryDrawResultCO draw(LotteryDrawCmd cmd) { + return lotteryDrawExe.execute(cmd); + } + + @Override + public PageResult listMyRecords(LotteryRecordQryCmd cmd) { + return recordQryExe.query(cmd); + } +} +``` + +#### 5. 创建 Controller(other-adapter) +```java +@RestController +@RequestMapping("/activity/lottery") +@RequiredArgsConstructor +public class LotteryActivityRestController extends BaseController { + private final LotteryActivityRestService lotteryService; + + /** + * 执行抽奖 + * @eo.name 执行抽奖 + * @eo.url /draw + * @eo.method post + */ + @PostMapping("/draw") + public LotteryDrawResultCO draw(@RequestBody @Validated LotteryDrawCmd cmd) { + return lotteryService.draw(cmd); + } +} +``` + +#### 6. 数据访问层(other-infrastructure) + +**简单功能**(直接使用 DAO 和 DatabaseService): +```java +// DAO +public interface LotteryTicketDAO extends BaseDAO {} + +// DatabaseService +@Service +public class LotteryActivityDatabaseService { + @Autowired + private LotteryTicketDAO ticketDAO; + + public void consumeTicket(Long userId, Long activityId) { + // 直接操作数据库 + } +} +``` + +**复杂功能**(使用 Gateway 封装): +```java +// Gateway 接口 +public interface LotteryActivityGateway { + void consumeTicketWithLock(Long userId, Long activityId); +} + +// Gateway 实现 +@Component +public class LotteryActivityGatewayImpl implements LotteryActivityGateway { + @Autowired + private LotteryTicketDAO ticketDAO; + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void consumeTicketWithLock(Long userId, Long activityId) { + // 分布式锁 + 数据库操作 + 缓存更新 + String lockKey = "lottery:lock:" + userId; + // ... 复杂逻辑 + } +} +``` + +#### 7. 跨服务调用(可选) + +如果需要被其他服务调用,创建 Inner API: + +``` +rc-inner-api/other-inner/ +├── other-inner-api/endpoint/activity/ +│ └── LotteryActivityClientService.java # Feign 接口 +├── other-inner-model/model/activity/ +│ └── LotteryDrawDTO.java # 数据模型 +└── other-inner-endpoint/service/activity/ + └── LotteryActivityClientServiceImpl.java # 实现 +``` + +--- + +## 五、文件路径快速参考 + +### 按功能类型查找路径 + +| 文件类型 | 路径模板 | 示例 | +|---------|---------|------| +| **Controller** | `other-adapter/src/main/java/com/red/circle/other/adapter/app/{模块}/` | `activity/LotteryActivityRestController.java` | +| **Service 接口** | `other-client/src/main/java/com/red/circle/other/app/service/{模块}/` | `activity/LotteryActivityRestService.java` | +| **Cmd** | `other-client/src/main/java/com/red/circle/other/app/dto/cmd/{模块}/` | `activity/LotteryDrawCmd.java` | +| **CO** | `other-client/src/main/java/com/red/circle/other/app/dto/clientobject/{模块}/` | `activity/LotteryDrawResultCO.java` | +| **CmdExe** | `other-application/src/main/java/com/red/circle/other/app/command/{模块}/` | `activity/LotteryDrawExe.java` | +| **QryExe** | `other-application/src/main/java/com/red/circle/other/app/command/{模块}/query/` | `activity/query/LotteryRecordQryExe.java` | +| **ServiceImpl** | `other-application/src/main/java/com/red/circle/other/app/service/{模块}/` | `activity/LotteryActivityRestServiceImpl.java` | +| **Gateway** | `other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/{模块}/` | `activity/LotteryActivityGatewayImpl.java` | +| **DAO** | `other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/` | `LotteryTicketDAO.java` | +| **Entity** | `other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/` | `LotteryTicket.java` | +| **DatabaseService** | `other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/` | `LotteryActivityDatabaseService.java` | +| **Inner API** | `rc-inner-api/other-inner/other-inner-api/endpoint/{模块}/` | `activity/LotteryActivityClientService.java` | +| **Inner Model** | `rc-inner-api/other-inner/other-inner-model/model/{模块}/` | `activity/LotteryDrawDTO.java` | +| **Inner Endpoint** | `other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/{模块}/` | `activity/LotteryActivityClientServiceImpl.java` | + +### 按模块查找路径 + +#### 常见功能模块 +- `activity` - 活动相关 +- `user` - 用户相关 +- `room` - 房间相关 +- `game` - 游戏相关 +- `family` - 家族相关 +- `gift` - 礼物相关 +- `task` - 任务相关 +- `material` - 素材/道具相关 + +--- + +## 六、核心开发规范 + +### 1. 命名规范 + +| 类型 | 命名规则 | 示例 | +|------|---------|------| +| Controller | `{功能}RestController` | `LotteryActivityRestController` | +| Service 接口 | `{功能}Service` | `LotteryActivityRestService` | +| Service 实现 | `{功能}ServiceImpl` | `LotteryActivityRestServiceImpl` | +| CmdExe | `{功能}CmdExe` / `{功能}Exe` | `LotteryDrawExe` | +| QryExe | `{功能}QryExe` | `LotteryRecordQryExe` | +| Cmd | `{功能}Cmd` | `LotteryDrawCmd` | +| QryCmd | `{功能}QryCmd` | `LotteryRecordQryCmd` | +| CO | `{功能}CO` | `LotteryDrawResultCO` | +| DAO | `{实体名}DAO` | `LotteryTicketDAO` | +| Entity | `{实体名}` | `LotteryTicket` | +| Gateway | `{功能}Gateway` + Impl | `LotteryActivityGateway` | + +### 2. 注解规范 + +#### DTO 层 +```java +@Data // Lombok +public class LotteryDrawCmd extends AppExtCommand { + @NotNull + private Long activityId; + + @Min(1) + @Max(10) + private Integer drawCount; +} +``` + +#### 分页查询 +```java +@Data +public class LotteryRecordQryCmd extends PageQueryCmd { + private Long activityId; +} +``` + +#### Controller 层 +```java +@RestController +@RequestMapping("/activity/lottery") +@RequiredArgsConstructor // Lombok 构造器注入 +public class LotteryActivityRestController extends BaseController { + + /** + * 执行抽奖 + * @eo.name 执行抽奖 + * @eo.url /draw + * @eo.method post + * @eo.request-type json + */ + @PostMapping("/draw") + public LotteryDrawResultCO draw(@RequestBody @Validated LotteryDrawCmd cmd) { + return lotteryService.draw(cmd); + } +} +``` + +#### Service 层 +```java +@Service +@RequiredArgsConstructor +@Slf4j // 日志 +public class LotteryActivityRestServiceImpl implements LotteryActivityRestService { + + private final LotteryDrawExe lotteryDrawExe; + + @Override + @Transactional // 需要事务的方法 + public LotteryDrawResultCO draw(LotteryDrawCmd cmd) { + log.info("用户抽奖: userId={}, activityId={}", + cmd.getReqUserId(), cmd.getActivityId()); + return lotteryDrawExe.execute(cmd); + } +} +``` + +### 3. 分页返回 + +统一使用 `com.red.circle.framework.dto.PageResult`: + +```java +public PageResult listMyRecords(LotteryRecordQryCmd cmd) { + // 查询数据 + List records = queryRecords(cmd); + // 查询总数 + long total = countRecords(cmd); + + return PageResult.of(records, total, cmd.getPageNum(), cmd.getPageSize()); +} +``` + +### 4. 异常处理 + +使用业务异常,不直接抛出 RuntimeException: + +```java +if (ticket == null) { + throw new BusinessException("抽奖券不足"); +} +``` + +### 5. 事务控制 + +- **写操作**:必须添加 `@Transactional` +- **读操作**:不需要事务 +- **跨服务调用**:考虑分布式事务或最终一致性 + +--- + +## 七、关键设计原则 + +### 1. Gateway 使用原则 + +**何时使用 Gateway**: +- ✅ 需要分布式锁的场景 +- ✅ 需要缓存 + 数据库双写的场景 +- ✅ 需要多个数据源聚合的场景 +- ✅ 需要复杂的事务协调 +- ✅ 业务逻辑复杂,需要封装的场景 + +**何时不用 Gateway**: +- ❌ 简单的 CRUD 操作 +- ❌ 单表查询 +- ❌ 不需要缓存的简单操作 +- ❌ 逻辑简单清晰的场景 + +**原则**:简单直接,复杂封装,避免过度设计 + +### 2. 层次调用原则 + +``` +Controller → Service → CmdExe → Gateway/DatabaseService → DAO +``` + +- Controller 只调用 Service +- Service 只调用 CmdExe +- CmdExe 可直接调用 Infrastructure(简单场景)或通过 Gateway(复杂场景) +- 禁止跨层调用 + +### 3. 代码质量原则 + +参考主项目文档中的质量检查清单: + +- [ ] 命名清晰(避免 a、b、temp) +- [ ] 完整的注释 +- [ ] 参数校验(@Valid、@NotNull) +- [ ] 异常处理 +- [ ] 并发安全(金额操作) +- [ ] 幂等性(bizNo) +- [ ] 事务注解(@Transactional) +- [ ] 关键日志(log.info/error) +- [ ] 性能考虑(N+1、缓存) +- [ ] 安全防护(SQL 注入、XSS) + +--- + +## 八、常见问题 FAQ + +### Q1: 什么时候需要 Gateway? +A: 当业务逻辑涉及多个数据源、分布式锁、缓存同步等复杂场景时使用 Gateway。简单的 CRUD 直接使用 DatabaseService。 + +### Q2: CmdExe 和 ServiceImpl 的区别? +A: ServiceImpl 是门面,负责接口定义和调用编排。CmdExe 是具体的业务逻辑实现。一个 ServiceImpl 可能调用多个 CmdExe。 + +### Q3: 跨服务调用如何实现? +A: 通过 Inner API + Feign。在 `other-inner-api` 定义接口,在 `other-inner-endpoint` 实现,其他服务通过 Feign 客户端调用。 + +### Q4: 如何处理分布式事务? +A: 优先考虑最终一致性(如消息队列)。如果必须强一致,考虑 TCC 或 Saga 模式,但要评估复杂度。 + +### Q5: MongoDB 和 MySQL 如何选择? +A: +- MySQL:强一致性、事务、关系查询 +- MongoDB:高性能、灵活 schema、大数据量统计 + +--- + +## 九、总结 + +### 开发新功能的黄金路径 + +1. ✅ 先定义 DTO(Cmd、CO) +2. ✅ 定义 Service 接口 +3. ✅ 实现 CmdExe(核心业务逻辑) +4. ✅ 实现 ServiceImpl(调用 CmdExe) +5. ✅ 创建 Controller(暴露 API) +6. ✅ 根据需要创建 Gateway 或直接用 DatabaseService +7. ✅ 如需跨服务调用,添加 Inner API + +### 核心理念 + +- **领域驱动**:按业务领域划分模块 +- **分层清晰**:各层职责明确,禁止跨层调用 +- **简单优先**:不过度设计,够用即可 +- **质量第一**:代码清晰、测试完善、文档齐全 + +--- + +**文档版本**:v1.0 +**最后更新**:2025-01-15 +**维护者**:开发团队 diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/signin/SignInController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/signin/SignInController.java new file mode 100644 index 00000000..13a8f141 --- /dev/null +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/signin/SignInController.java @@ -0,0 +1,56 @@ +package com.red.circle.other.adapter.app.signin; + +import com.red.circle.common.business.dto.cmd.AppExtCommand; +import com.red.circle.framework.web.controller.BaseController; +import com.red.circle.other.app.dto.clientobject.signin.CheckInResultCO; +import com.red.circle.other.app.dto.clientobject.signin.SignInStatusCO; +import com.red.circle.other.app.dto.cmd.signin.CheckInCmd; +import com.red.circle.other.app.dto.cmd.signin.SupplementCmd; +import com.red.circle.other.app.service.signin.SignInService; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/signin") +@RequiredArgsConstructor +public class SignInController extends BaseController { + + private final SignInService signInService; + + /** + * 查询签到状态 + * @eo.name 查询签到状态 + * @eo.url /status + * @eo.method get + */ + @GetMapping("/status") + public SignInStatusCO getStatus(AppExtCommand cmd) { + Long userId = cmd.getReqUserId(); + return signInService.getStatus(userId); + } + + /** + * 执行签到 + * @eo.name 执行签到 + * @eo.url /check-in + * @eo.method post + * @eo.request-type json + */ + @PostMapping("/check-in") + public CheckInResultCO checkIn(@RequestBody @Validated CheckInCmd cmd) { + return signInService.checkIn(cmd); + } + + /** + * 补签 + * @eo.name 补签 + * @eo.url /supplement + * @eo.method post + * @eo.request-type json + */ + @PostMapping("/supplement") + public CheckInResultCO supplement(@RequestBody @Validated SupplementCmd cmd) { + return signInService.supplement(cmd); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignCheckInExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignCheckInExe.java new file mode 100644 index 00000000..b2a00c47 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignCheckInExe.java @@ -0,0 +1,151 @@ +package com.red.circle.other.app.command.signin; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.common.business.enums.SendPropsOrigin; +import com.red.circle.component.redis.service.RedisService; +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.framework.core.response.CommonErrorCode; +import com.red.circle.other.app.dto.clientobject.signin.CheckInResultCO; +import com.red.circle.other.app.dto.cmd.signin.CheckInCmd; +import com.red.circle.other.infra.common.props.PropsSendCommon; +import com.red.circle.other.infra.database.rds.dao.checkin.SignInDailyConfigDAO; +import com.red.circle.other.infra.database.rds.dao.checkin.UserSignInRecordDAO; +import com.red.circle.other.infra.database.rds.entity.SignInDailyConfig; +import com.red.circle.other.infra.database.rds.entity.UserSignInRecord; +import com.red.circle.other.infra.database.rds.entity.activity.PropsActivityRewardConfig; +import com.red.circle.other.infra.database.rds.service.activity.PropsActivityRewardConfigService; +import com.red.circle.other.inner.asserts.OtherErrorCode; +import com.red.circle.other.inner.model.cmd.material.PrizeDescribe; +import com.red.circle.other.inner.model.cmd.material.PrizeDescribeRewardCmd; +import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; +import com.red.circle.tool.core.tuple.PennyAmount; +import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SignCheckInExe { + + private final SignInDailyConfigDAO dailyConfigDAO; + private final UserSignInRecordDAO recordDAO; + private final WalletGoldClient walletGoldClient; + private final PropsSendCommon propsSendCommon; + private final PropsActivityRewardConfigService propsActivityRewardConfigService; + + @Transactional(rollbackFor = Exception.class) + public CheckInResultCO execute(CheckInCmd cmd) { + Long userId = cmd.getReqUserId(); + LocalDate saudiToday = ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); + LocalDate weekStartDate = saudiToday.with(DayOfWeek.MONDAY); + + int targetDayIndex = saudiToday.getDayOfWeek().getValue(); + + ResponseAssert.isTrue(OtherErrorCode.SIGN_IN_CANNOT_ADVANCE, + targetDayIndex >= 1); + + long alreadySigned = recordDAO.selectCount( + new LambdaQueryWrapper() + .eq(UserSignInRecord::getUserId, userId) + .eq(UserSignInRecord::getWeekStartDate, weekStartDate) + .eq(UserSignInRecord::getDayIndex, targetDayIndex) + ); + ResponseAssert.isFalse(OtherErrorCode.SIGN_IN_ALREADY_CHECKED, alreadySigned > 0); + + for (int i = 1; i < targetDayIndex; i++) { + long count = recordDAO.selectCount( + new LambdaQueryWrapper() + .eq(UserSignInRecord::getUserId, userId) + .eq(UserSignInRecord::getWeekStartDate, weekStartDate) + .eq(UserSignInRecord::getDayIndex, i) + ); + ResponseAssert.isTrue(OtherErrorCode.SIGN_IN_NEED_PREVIOUS, count > 0); + } + + SignInDailyConfig config = dailyConfigDAO.selectList( + new LambdaQueryWrapper() + .eq(SignInDailyConfig::getDayIndex, targetDayIndex) + ).get(0); + ResponseAssert.notNull(OtherErrorCode.SIGN_IN_CONFIG_NOT_FOUND, config); + + Integer paidCost = 0; + if (config.getIsPaid() == 1) { + paidCost = config.getPaidCost(); + decreaseUserGold(userId, cmd.getReqSysOrigin().getOrigin(), paidCost, + weekStartDate + "_" + targetDayIndex); + } + + UserSignInRecord record = new UserSignInRecord(); + record.setUserId(userId); + record.setWeekStartDate(weekStartDate); + record.setSignDate(saudiToday); + record.setDayIndex(targetDayIndex); + record.setIsPaid(config.getIsPaid()); + record.setPaidCost(paidCost); + record.setIsSupplement(0); + record.setSupplementCost(0); + record.setIsRewarded(1); + record.setResourceGroupId(config.getResourceGroupId()); + recordDAO.insert(record); + + sendReward(userId, config); + + CheckInResultCO resultCO = new CheckInResultCO(); + resultCO.setSuccess(true); + resultCO.setMessage("Sign-in successful"); + resultCO.setPaidCost(paidCost); + resultCO.setSupplementCost(0); + + return resultCO; + } + + private void decreaseUserGold(Long userId, String sysOrigin, Integer amount, String eventId) { + GoldReceiptCmd goldCmd = GoldReceiptCmd.builder() + .appExpenditure() + .userId(userId) + .amount(PennyAmount.ofDollar(amount.longValue())) + .eventId(eventId) + .sysOrigin(sysOrigin) + .origin(GoldOrigin.SIGN_IN_PAID) + .build(); + + try { + walletGoldClient.changeBalance(goldCmd); + } catch (Exception e) { + log.error("Decrease gold failed userId={} amount={} eventId={}", userId, amount, eventId, e); + throw new RuntimeException("Failed to decrease gold"); + } + } + + private void sendReward(Long userId, SignInDailyConfig signInDailyConfig) { + Long resourceGroupId = signInDailyConfig.getResourceGroupId(); + List configs = propsActivityRewardConfigService.listByGroupId(resourceGroupId); + ResponseAssert.notEmpty(CommonErrorCode.NOT_FOUND_MAPPING_INFO, configs); + + PropsActivityRewardConfig config = configs.get(0); + + propsSendCommon.send(new PrizeDescribeRewardCmd() + .setTrackId(config.getGroupId()) + .setAcceptUserId(userId) + .setOrigin(SendPropsOrigin.SIGN_IN_REWARD) + .setSysOrigin(SysOriginPlatformEnum.LIKEI) + .setPrizes(Collections.singletonList(new PrizeDescribe() + .setTrackId(config.getGroupId()) + .setType(config.getType()) + .setDetailType(config.getDetailType()) + .setContent(config.getContent()) + .setQuantity(config.getQuantity()))) + ); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignInStatusQueryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignInStatusQueryExe.java new file mode 100644 index 00000000..ac586a06 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SignInStatusQueryExe.java @@ -0,0 +1,88 @@ +package com.red.circle.other.app.command.signin; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.other.app.dto.clientobject.signin.SignInDailyConfigCO; +import com.red.circle.other.app.dto.clientobject.signin.SignInStatusCO; +import com.red.circle.other.domain.gateway.props.ActivitySourceGroupGateway; +import com.red.circle.other.infra.database.rds.dao.checkin.SignInDailyConfigDAO; +import com.red.circle.other.infra.database.rds.dao.checkin.UserSignInRecordDAO; +import com.red.circle.other.infra.database.rds.entity.SignInDailyConfig; +import com.red.circle.other.infra.database.rds.entity.UserSignInRecord; +import com.red.circle.other.inner.model.dto.activity.props.ActivityPropsGroup; +import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class SignInStatusQueryExe { + + private final SignInDailyConfigDAO dailyConfigDAO; + private final UserSignInRecordDAO recordDAO; + private final ActivitySourceGroupGateway activitySourceGroupGateway; + + public SignInStatusCO execute(Long userId) { + LocalDate saudiToday = ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); + LocalDate weekStartDate = saudiToday.with(DayOfWeek.MONDAY); + int currentDayIndex = saudiToday.getDayOfWeek().getValue(); + + List configs = dailyConfigDAO.selectList( + new LambdaQueryWrapper() + .orderByAsc(SignInDailyConfig::getDayIndex) + ); + + List records = recordDAO.selectList( + new LambdaQueryWrapper() + .eq(UserSignInRecord::getUserId, userId) + .eq(UserSignInRecord::getWeekStartDate, weekStartDate) + ); + + Set groupIdSet = configs.stream().map(SignInDailyConfig::getResourceGroupId).collect(Collectors.toSet()); + Map propsGroupMap = activitySourceGroupGateway.mapActivityPropsGroup(SysOriginPlatformEnum.LIKEI.name(), groupIdSet); + + Map recordMap = records.stream() + .collect(Collectors.toMap(UserSignInRecord::getDayIndex, r -> r)); + + List dailyConfigCOs = new ArrayList<>(); + List signedDays = new ArrayList<>(); + + for (SignInDailyConfig config : configs) { + SignInDailyConfigCO co = new SignInDailyConfigCO(); + co.setDayIndex(config.getDayIndex()); + co.setIsPaid(config.getIsPaid() == 1); + co.setPaidCost(config.getPaidCost()); + co.setAddActivityPropsGroup(propsGroupMap.get(config.getResourceGroupId())); + + UserSignInRecord record = recordMap.get(config.getDayIndex()); + if (record != null) { + co.setIsSigned(true); + co.setIsRewarded(record.getIsRewarded() == 1); + co.setIsSupplement(record.getIsSupplement() == 1); + signedDays.add(config.getDayIndex()); + } else { + co.setIsSigned(false); + co.setIsRewarded(false); + co.setIsSupplement(false); + } + + dailyConfigCOs.add(co); + } + + SignInStatusCO statusCO = new SignInStatusCO(); + statusCO.setWeekStartDate(weekStartDate.toString()); + statusCO.setCurrentDayIndex(currentDayIndex); + statusCO.setSignedDays(signedDays); + statusCO.setDailyConfigs(dailyConfigCOs); + + return statusCO; + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SupplementCheckInExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SupplementCheckInExe.java new file mode 100644 index 00000000..52116510 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/signin/SupplementCheckInExe.java @@ -0,0 +1,156 @@ +package com.red.circle.other.app.command.signin; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.common.business.enums.SendPropsOrigin; +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.framework.core.response.CommonErrorCode; +import com.red.circle.other.app.dto.clientobject.signin.CheckInResultCO; +import com.red.circle.other.app.dto.cmd.signin.SupplementCmd; +import com.red.circle.other.infra.common.props.PropsSendCommon; +import com.red.circle.other.infra.database.rds.dao.checkin.SignInDailyConfigDAO; +import com.red.circle.other.infra.database.rds.dao.checkin.UserSignInRecordDAO; +import com.red.circle.other.infra.database.rds.entity.SignInDailyConfig; +import com.red.circle.other.infra.database.rds.entity.UserSignInRecord; +import com.red.circle.other.infra.database.rds.entity.activity.PropsActivityRewardConfig; +import com.red.circle.other.infra.database.rds.service.activity.PropsActivityRewardConfigService; +import com.red.circle.other.inner.asserts.OtherErrorCode; +import com.red.circle.other.inner.model.cmd.material.PrizeDescribe; +import com.red.circle.other.inner.model.cmd.material.PrizeDescribeRewardCmd; +import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; +import com.red.circle.tool.core.tuple.PennyAmount; +import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SupplementCheckInExe { + + private final SignInDailyConfigDAO dailyConfigDAO; + private final UserSignInRecordDAO recordDAO; + private final WalletGoldClient walletGoldClient; + private final PropsSendCommon propsSendCommon; + private final PropsActivityRewardConfigService propsActivityRewardConfigService; + + private static final int BASE_SUPPLEMENT_COST = 500; + + @Transactional(rollbackFor = Exception.class) + public CheckInResultCO execute(SupplementCmd cmd) { + Long userId = cmd.getReqUserId(); + Integer targetDayIndex = cmd.getDayIndex(); + + LocalDate saudiToday = ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); + LocalDate weekStartDate = saudiToday.with(DayOfWeek.MONDAY); + int currentDayIndex = saudiToday.getDayOfWeek().getValue(); + + ResponseAssert.isTrue(OtherErrorCode.SIGN_IN_SUPPLEMENT_ONLY_PAST, + targetDayIndex < currentDayIndex); + + long alreadySigned = recordDAO.selectCount( + new LambdaQueryWrapper() + .eq(UserSignInRecord::getUserId, userId) + .eq(UserSignInRecord::getWeekStartDate, weekStartDate) + .eq(UserSignInRecord::getDayIndex, targetDayIndex) + ); + ResponseAssert.isFalse(OtherErrorCode.SIGN_IN_DATE_ALREADY_CHECKED, alreadySigned > 0); + + for (int i = 1; i < targetDayIndex; i++) { + long count = recordDAO.selectCount( + new LambdaQueryWrapper() + .eq(UserSignInRecord::getUserId, userId) + .eq(UserSignInRecord::getWeekStartDate, weekStartDate) + .eq(UserSignInRecord::getDayIndex, i) + ); + ResponseAssert.isTrue(OtherErrorCode.SIGN_IN_NEED_PREVIOUS, count > 0); + } + + List configList = dailyConfigDAO.selectList( + new LambdaQueryWrapper() + .eq(SignInDailyConfig::getDayIndex, targetDayIndex) + ); + ResponseAssert.isTrue(OtherErrorCode.SIGN_IN_CONFIG_NOT_FOUND, !configList.isEmpty()); + + SignInDailyConfig config = configList.get(0); + + int supplementCost = BASE_SUPPLEMENT_COST; + if (config.getIsPaid() == 1) { + supplementCost = config.getPaidCost() + BASE_SUPPLEMENT_COST; + } + + decreaseUserGold(userId, cmd.getReqSysOrigin().getOrigin(), supplementCost, weekStartDate + "_supplement_" + targetDayIndex); + + UserSignInRecord record = new UserSignInRecord(); + record.setUserId(userId); + record.setWeekStartDate(weekStartDate); + record.setSignDate(saudiToday); + record.setDayIndex(targetDayIndex); + record.setIsPaid(config.getIsPaid()); + record.setPaidCost(config.getIsPaid() == 1 ? config.getPaidCost() : 0); + record.setIsSupplement(1); + record.setSupplementCost(supplementCost); + record.setIsRewarded(1); + record.setResourceGroupId(config.getResourceGroupId()); + recordDAO.insert(record); + + sendReward(userId, config); + + CheckInResultCO resultCO = new CheckInResultCO(); + resultCO.setSuccess(true); + resultCO.setMessage("Make-up successful"); + resultCO.setPaidCost(config.getIsPaid() == 1 ? config.getPaidCost() : 0); + resultCO.setSupplementCost(supplementCost); + + return resultCO; + } + + private void decreaseUserGold(Long userId, String sysOrigin, Integer amount, String eventId) { + GoldReceiptCmd goldCmd = GoldReceiptCmd.builder() + .appExpenditure() + .userId(userId) + .amount(PennyAmount.ofDollar(amount.longValue())) + .eventId(eventId) + .sysOrigin(sysOrigin) + .origin(GoldOrigin.SIGN_IN_SUPPLEMENT) + .build(); + + try { + walletGoldClient.changeBalance(goldCmd); + } catch (Exception e) { + log.error("Decrease gold failed for supplement userId={} amount={} eventId={}", + userId, amount, eventId, e); + throw new RuntimeException("Failed to decrease gold for supplement"); + } + } + + private void sendReward(Long userId, SignInDailyConfig signInDailyConfig) { + Long resourceGroupId = signInDailyConfig.getResourceGroupId(); + List configs = propsActivityRewardConfigService.listByGroupId(resourceGroupId); + ResponseAssert.notEmpty(CommonErrorCode.NOT_FOUND_MAPPING_INFO, configs); + + PropsActivityRewardConfig config = configs.get(0); + + propsSendCommon.send(new PrizeDescribeRewardCmd() + .setTrackId(config.getGroupId()) + .setAcceptUserId(userId) + .setOrigin(SendPropsOrigin.SIGN_IN_REWARD) + .setSysOrigin(SysOriginPlatformEnum.LIKEI) + .setPrizes(Collections.singletonList(new PrizeDescribe() + .setTrackId(config.getGroupId()) + .setType(config.getType()) + .setDetailType(config.getDetailType()) + .setContent(config.getContent()) + .setQuantity(config.getQuantity()))) + ); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/signin/SignInServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/signin/SignInServiceImpl.java new file mode 100644 index 00000000..422a434c --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/signin/SignInServiceImpl.java @@ -0,0 +1,41 @@ +package com.red.circle.other.app.service.signin; + +import com.red.circle.other.app.command.signin.SignCheckInExe; +import com.red.circle.other.app.command.signin.SignInStatusQueryExe; +import com.red.circle.other.app.command.signin.SupplementCheckInExe; +import com.red.circle.other.app.dto.clientobject.signin.CheckInResultCO; +import com.red.circle.other.app.dto.clientobject.signin.SignInStatusCO; +import com.red.circle.other.app.dto.cmd.signin.CheckInCmd; +import com.red.circle.other.app.dto.cmd.signin.SupplementCmd; +import com.red.circle.other.app.util.DistributedLockUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SignInServiceImpl implements SignInService { + + private final SignInStatusQueryExe statusQueryExe; + private final SignCheckInExe signCheckInExe; + private final SupplementCheckInExe supplementCheckInExe; + private final DistributedLockUtil distributedLockUtil; + private static final String SIGN_IN_LOCK_KEY = "sign_in:lock:"; + private static final String SUPPLEMENT_LOCK_KEY = "sign_in:supplement:lock:"; + + @Override + public SignInStatusCO getStatus(Long userId) { + return statusQueryExe.execute(userId); + } + + @Override + public CheckInResultCO checkIn(CheckInCmd cmd) { + return distributedLockUtil.executeWithLock(SIGN_IN_LOCK_KEY + cmd.getReqUserId(), + () -> signCheckInExe.execute(cmd)); + } + + @Override + public CheckInResultCO supplement(SupplementCmd cmd) { + return distributedLockUtil.executeWithLock(SUPPLEMENT_LOCK_KEY + cmd.getReqUserId(), + () -> supplementCheckInExe.execute(cmd)); + } +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/CheckInResultCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/CheckInResultCO.java new file mode 100644 index 00000000..b755a195 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/CheckInResultCO.java @@ -0,0 +1,15 @@ +package com.red.circle.other.app.dto.clientobject.signin; + +import lombok.Data; + +@Data +public class CheckInResultCO { + + private Boolean success; + + private String message; + + private Integer paidCost; + + private Integer supplementCost; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInDailyConfigCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInDailyConfigCO.java new file mode 100644 index 00000000..5f70f305 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInDailyConfigCO.java @@ -0,0 +1,37 @@ +package com.red.circle.other.app.dto.clientobject.signin; + +import com.red.circle.other.inner.model.dto.activity.props.ActivityPropsGroup; +import lombok.Data; + +@Data +public class SignInDailyConfigCO { + + private Integer dayIndex; + + /** + * 是否付费日 + */ + private Boolean isPaid; + + /** + * 付费金额 + */ + private Integer paidCost; + + /** + * 是否已签到 + */ + private Boolean isSigned; + + /** + * 是否已领取 + */ + private Boolean isRewarded; + + /** + * 是否补签:0-正常签到;1-补签 + */ + private Boolean isSupplement; + + private ActivityPropsGroup addActivityPropsGroup; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInStatusCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInStatusCO.java new file mode 100644 index 00000000..8fdc39a6 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/signin/SignInStatusCO.java @@ -0,0 +1,20 @@ +package com.red.circle.other.app.dto.clientobject.signin; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +@Data +public class SignInStatusCO { + + private String weekStartDate; + + private Integer currentDayIndex; + + private List signedDays; + + private List dailyConfigs; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/CheckInCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/CheckInCmd.java new file mode 100644 index 00000000..b97bd2c3 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/CheckInCmd.java @@ -0,0 +1,12 @@ +package com.red.circle.other.app.dto.cmd.signin; + +import com.red.circle.common.business.dto.cmd.AppExtCommand; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CheckInCmd extends AppExtCommand { + + private Integer dayIndex; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/SupplementCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/SupplementCmd.java new file mode 100644 index 00000000..412516fe --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/signin/SupplementCmd.java @@ -0,0 +1,18 @@ +package com.red.circle.other.app.dto.cmd.signin; + +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 SupplementCmd extends AppExtCommand { + + @NotNull + @Min(1) + @Max(7) + private Integer dayIndex; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/signin/SignInService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/signin/SignInService.java new file mode 100644 index 00000000..40ae6031 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/signin/SignInService.java @@ -0,0 +1,15 @@ +package com.red.circle.other.app.service.signin; + +import com.red.circle.other.app.dto.clientobject.signin.CheckInResultCO; +import com.red.circle.other.app.dto.clientobject.signin.SignInStatusCO; +import com.red.circle.other.app.dto.cmd.signin.CheckInCmd; +import com.red.circle.other.app.dto.cmd.signin.SupplementCmd; + +public interface SignInService { + + SignInStatusCO getStatus(Long userId); + + CheckInResultCO checkIn(CheckInCmd cmd); + + CheckInResultCO supplement(SupplementCmd cmd); +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/SignInDailyConfigDAO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/SignInDailyConfigDAO.java new file mode 100644 index 00000000..e3bbcb77 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/SignInDailyConfigDAO.java @@ -0,0 +1,7 @@ +package com.red.circle.other.infra.database.rds.dao.checkin; + +import com.red.circle.framework.mybatis.dao.BaseDAO; +import com.red.circle.other.infra.database.rds.entity.SignInDailyConfig; + +public interface SignInDailyConfigDAO extends BaseDAO { +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/UserSignInRecordDAO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/UserSignInRecordDAO.java new file mode 100644 index 00000000..acd5ae88 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/checkin/UserSignInRecordDAO.java @@ -0,0 +1,7 @@ +package com.red.circle.other.infra.database.rds.dao.checkin; + +import com.red.circle.framework.mybatis.dao.BaseDAO; +import com.red.circle.other.infra.database.rds.entity.UserSignInRecord; + +public interface UserSignInRecordDAO extends BaseDAO { +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/SignInDailyConfig.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/SignInDailyConfig.java new file mode 100644 index 00000000..ed897fe5 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/SignInDailyConfig.java @@ -0,0 +1,31 @@ +package com.red.circle.other.infra.database.rds.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 签到每日配置 + */ +@Data +@TableName("sign_in_daily_config") +public class SignInDailyConfig { + + @TableId(type = IdType.AUTO) + private Long id; + + private Integer dayIndex; + + private Integer isPaid; + + private Integer paidCost; + + private Long resourceGroupId; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserSignInRecord.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserSignInRecord.java new file mode 100644 index 00000000..f03db692 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserSignInRecord.java @@ -0,0 +1,44 @@ +package com.red.circle.other.infra.database.rds.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 用户签到记录 + */ +@Data +@TableName("user_sign_in_record") +public class UserSignInRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long userId; + + private LocalDate weekStartDate; + + private LocalDate signDate; + + private Integer dayIndex; + + private Integer isPaid; + + private Integer paidCost; + + private Integer isSupplement; + + private Integer supplementCost; + + private Integer isRewarded; + + private Long resourceGroupId; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} diff --git a/rc-service/rc-service-other/other-start/src/test/java/AnchorTargetTest.java b/rc-service/rc-service-other/other-start/src/test/java/AnchorTargetTest.java index 3247051d..3a4a417a 100644 --- a/rc-service/rc-service-other/other-start/src/test/java/AnchorTargetTest.java +++ b/rc-service/rc-service-other/other-start/src/test/java/AnchorTargetTest.java @@ -116,23 +116,13 @@ public class AnchorTargetTest { @Test public void batchAnchorTargetDeduct() { Map map = Map.ofEntries( - Map.entry(8832761L, 150000L), - Map.entry(8834979L, 300000L), - Map.entry(8834654L, 300000L), - Map.entry(8839847L, 300000L), - Map.entry(8827498L, 150000L), + Map.entry(101010L, 150000L), + Map.entry(8834453L, 525000L), + Map.entry(8837161L, 150000L), + Map.entry(8836399L, 525000L), Map.entry(8839866L, 150000L), - Map.entry(8839902L, 300000L), - Map.entry(8839851L, 300000L), - Map.entry(8839894L, 300000L), - Map.entry(8840819L, 300000L), - Map.entry(8839949L, 300000L), - Map.entry(8839954L, 300000L), - Map.entry(8839957L, 300000L), - Map.entry(90090L, 1500000L), - Map.entry(8831059L, 150000L), - Map.entry(8829177L, 150000L), - Map.entry(8840323L, 300000L) + Map.entry(8839923L, 300000L), + Map.entry(8839943L, 300000L) ); // 将account转换为userId并构建batchDeductMap