新增7天签到功能
This commit is contained in:
parent
e97867d156
commit
62c9864ea5
@ -281,6 +281,11 @@ public enum SendPropsOrigin {
|
|||||||
*/
|
*/
|
||||||
SEASON_CP_GIFT(""),
|
SEASON_CP_GIFT(""),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签到奖励
|
||||||
|
*/
|
||||||
|
SIGN_IN_REWARD("Sign in Rewards"),
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
private final String desc;
|
private final String desc;
|
||||||
|
|||||||
@ -172,6 +172,43 @@ public enum OtherErrorCode implements IResponseErrorCode {
|
|||||||
* 用户财富等级不足10级
|
* 用户财富等级不足10级
|
||||||
*/
|
*/
|
||||||
USER_WEALTH_NEED_THAN_10(40303, "User wealth level needs to be greater than 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;
|
private final int code;
|
||||||
|
|||||||
@ -747,6 +747,16 @@ public enum GoldOrigin {
|
|||||||
* cp榜单(周榜和季度榜)
|
* cp榜单(周榜和季度榜)
|
||||||
*/
|
*/
|
||||||
CP_RANKING_REWARD("CP ranking reward"),
|
CP_RANKING_REWARD("CP ranking reward"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签到付费
|
||||||
|
*/
|
||||||
|
SIGN_IN_PAID("Sign-in paid"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签到补签
|
||||||
|
*/
|
||||||
|
SIGN_IN_SUPPLEMENT("Sign-in supplement"),
|
||||||
;
|
;
|
||||||
|
|
||||||
private final String desc;
|
private final String desc;
|
||||||
|
|||||||
688
rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md
Normal file
688
rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md
Normal file
@ -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<T>`
|
||||||
|
- **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<LotteryRecordCO> 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<LotteryRecordCO> 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<LotteryTicket> {}
|
||||||
|
|
||||||
|
// 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<T>`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public PageResult<LotteryRecordCO> listMyRecords(LotteryRecordQryCmd cmd) {
|
||||||
|
// 查询数据
|
||||||
|
List<LotteryRecordCO> 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
|
||||||
|
**维护者**:开发团队
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<UserSignInRecord>()
|
||||||
|
.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<UserSignInRecord>()
|
||||||
|
.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<SignInDailyConfig>()
|
||||||
|
.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<PropsActivityRewardConfig> 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())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<SignInDailyConfig> configs = dailyConfigDAO.selectList(
|
||||||
|
new LambdaQueryWrapper<SignInDailyConfig>()
|
||||||
|
.orderByAsc(SignInDailyConfig::getDayIndex)
|
||||||
|
);
|
||||||
|
|
||||||
|
List<UserSignInRecord> records = recordDAO.selectList(
|
||||||
|
new LambdaQueryWrapper<UserSignInRecord>()
|
||||||
|
.eq(UserSignInRecord::getUserId, userId)
|
||||||
|
.eq(UserSignInRecord::getWeekStartDate, weekStartDate)
|
||||||
|
);
|
||||||
|
|
||||||
|
Set<Long> groupIdSet = configs.stream().map(SignInDailyConfig::getResourceGroupId).collect(Collectors.toSet());
|
||||||
|
Map<Long, ActivityPropsGroup> propsGroupMap = activitySourceGroupGateway.mapActivityPropsGroup(SysOriginPlatformEnum.LIKEI.name(), groupIdSet);
|
||||||
|
|
||||||
|
Map<Integer, UserSignInRecord> recordMap = records.stream()
|
||||||
|
.collect(Collectors.toMap(UserSignInRecord::getDayIndex, r -> r));
|
||||||
|
|
||||||
|
List<SignInDailyConfigCO> dailyConfigCOs = new ArrayList<>();
|
||||||
|
List<Integer> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<UserSignInRecord>()
|
||||||
|
.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<UserSignInRecord>()
|
||||||
|
.eq(UserSignInRecord::getUserId, userId)
|
||||||
|
.eq(UserSignInRecord::getWeekStartDate, weekStartDate)
|
||||||
|
.eq(UserSignInRecord::getDayIndex, i)
|
||||||
|
);
|
||||||
|
ResponseAssert.isTrue(OtherErrorCode.SIGN_IN_NEED_PREVIOUS, count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SignInDailyConfig> configList = dailyConfigDAO.selectList(
|
||||||
|
new LambdaQueryWrapper<SignInDailyConfig>()
|
||||||
|
.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<PropsActivityRewardConfig> 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())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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<Integer> signedDays;
|
||||||
|
|
||||||
|
private List<SignInDailyConfigCO> dailyConfigs;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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<SignInDailyConfig> {
|
||||||
|
}
|
||||||
@ -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<UserSignInRecord> {
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -116,23 +116,13 @@ public class AnchorTargetTest {
|
|||||||
@Test
|
@Test
|
||||||
public void batchAnchorTargetDeduct() {
|
public void batchAnchorTargetDeduct() {
|
||||||
Map<Long, Long> map = Map.ofEntries(
|
Map<Long, Long> map = Map.ofEntries(
|
||||||
Map.entry(8832761L, 150000L),
|
Map.entry(101010L, 150000L),
|
||||||
Map.entry(8834979L, 300000L),
|
Map.entry(8834453L, 525000L),
|
||||||
Map.entry(8834654L, 300000L),
|
Map.entry(8837161L, 150000L),
|
||||||
Map.entry(8839847L, 300000L),
|
Map.entry(8836399L, 525000L),
|
||||||
Map.entry(8827498L, 150000L),
|
|
||||||
Map.entry(8839866L, 150000L),
|
Map.entry(8839866L, 150000L),
|
||||||
Map.entry(8839902L, 300000L),
|
Map.entry(8839923L, 300000L),
|
||||||
Map.entry(8839851L, 300000L),
|
Map.entry(8839943L, 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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 将account转换为userId并构建batchDeductMap
|
// 将account转换为userId并构建batchDeductMap
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user