chatapp3-java/rc-service/rc-service-other/docs/OTHER模块DDD架构说明.md
2026-04-09 21:33:09 +08:00

689 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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. 实现 CmdExeother-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. 实现 ServiceImplother-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. 创建 Controllerother-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. ✅ 先定义 DTOCmd、CO
2. ✅ 定义 Service 接口
3. ✅ 实现 CmdExe核心业务逻辑
4. ✅ 实现 ServiceImpl调用 CmdExe
5. ✅ 创建 Controller暴露 API
6. ✅ 根据需要创建 Gateway 或直接用 DatabaseService
7. ✅ 如需跨服务调用,添加 Inner API
### 核心理念
- **领域驱动**:按业务领域划分模块
- **分层清晰**:各层职责明确,禁止跨层调用
- **简单优先**:不过度设计,够用即可
- **质量第一**:代码清晰、测试完善、文档齐全
---
**文档版本**v1.0
**最后更新**2025-01-15
**维护者**:开发团队