# Spring Boot 项目编码规范

> **优先级：Rule > Skill。** 当本规则与 `skills/` 下的任何 Skill 说法不一致时，以本规则为准。
> **定位：** 本文件定义"做什么、不做什么"（约束和规则），以及何时加载哪个 Skill。

---

## 0. Skill 加载策略（Progressive Disclosure）

本规则始终加载。Skill 按需加载，不要同时加载所有 Skill。

| 阶段 | 加载的 Skill | 触发场景 |
|---|---|---|
| 分析需求 / 规划步骤 | 无 | 只用本规则即可 |
| 实现 Domain 层（聚合根、值对象、事件） | `skills/java-ddd-hexagonal/` | 写领域模型代码时 |
| 实现 Application 层（UseCase、Service） | `skills/java-ddd-hexagonal/` | 写应用层代码时 |
| 实现 Adapter 层（Web、Persistence、Event） | `skills/java-ddd-hexagonal/` | 写适配器代码时 |
| 写测试 | `skills/springboot-tdd/` | 写任何测试代码时 |
| 写测试 + 生产代码 | 两个 Skill 都加载 | 同时写实现和测试时 |

**原则：** 需要代码模式时加载 `java-ddd-hexagonal`，需要测试模式时加载 `springboot-tdd`。

### 不触发 Skill 的场景（排除规则）

以下场景即便匹配到触发词，也**不应**加载 Skill：

| 场景 | 不加载的 Skill | 原因 |
|---|---|---|
| 仅讨论/解释 DDD 概念，不写代码 | `java-ddd-hexagonal` | 讨论不需要代码模式 |
| 只是查看已有代码，不做修改 | 全部 Skill | 阅读不触发加载 |
| 修改 Rule 本身 | 全部 Skill | Rule 维护独立于 Skill |
| 回答架构设计问题（不写具体代码） | 全部 Skill | 设计讨论不需要实现细节 |

---

## 0.1. TDD 强制执行规则

每实现一个功能必须严格按以下循环推进，**不可跳过任何一步**。每一步完成后必须运行测试验证。

### 循环步骤

1. **RED — 只写测试，不写实现**
   - 只创建测试文件和测试方法，不创建任何实现类
   - 运行 `mvn test -Dtest=XxxTest`，确认测试失败（编译失败或断言失败均可）
   - 如果测试直接通过（本应失败），说明测试写错了，修正测试后再运行
   - **确认 RED 后，向用户汇报状态，继续下一步。**

2. **GREEN — 写最小实现让测试通过**
   - 只写让当前测试通过的最少代码，不过度设计
   - 运行 `mvn test -Dtest=XxxTest`，确认全部通过
   - **确认 GREEN 后，向用户汇报状态，继续下一步。**

3. **REFACTOR — 重构**
   - 在测试全部通过的前提下，改进代码结构（消除重复、改善命名）
   - 每次重构后运行 `mvn test` 确认仍然全部通过
   - 不要引入新功能

4. **重复 1-3**，直到该功能所有验收标准覆盖完毕

### 禁止行为（强制）

- **禁止在同一次输出中写包含业务逻辑的实现代码** — 必须先 RED 后 GREEN。允许创建空壳类/接口（方法体 `throw new UnsupportedOperationException()`）以满足测试编译
- **禁止在测试未确认 RED 之前写业务逻辑实现**
- **禁止跳过 `mvn test` 验证步骤**
- **禁止在 RED 阶段创建有逻辑的实现类**（空壳类/接口除外，方法体必须留空或 throw）
- **禁止一次性写完所有测试再写所有实现** — 必须一个测试一个实现循环推进

---

## 1. 架构约束

### 分层结构
```
src/main/java/{basePackage}/
├── adapter/                        # 适配器层
│   ├── persistence/{aggregate}/    # 持久化（Entity + Mapper + Adapter）
│   ├── web/{aggregate}/            # Web（Request + Response + WebAdapter）
│   └── event/                      # 事件发布适配器
├── application/                    # 应用层
│   ├── port/in/                    # 入端口（UseCase 接口）
│   ├── port/out/                   # 出端口（Port 接口）
│   ├── command/                    # 命令
│   └── service/                    # 服务（实现 UseCase）
├── domain/                         # 领域层（纯 Java，无框架依赖）
│   ├── {aggregate}/                # 按业务聚合分包
│   └── common/                     # AggregateRoot、DomainEvent、BusinessException
├── config/                         # Spring 配置类
└── common/                         # ApiResponse、ErrorCode、GlobalExceptionHandler
```

### 依赖方向（强制）
- **domain → 不依赖任何外部层**（无 Spring、无 I/O、无第三方库）
- **application → 只依赖 domain**，定义 Port 接口但不实现
- **adapter → 依赖 application 和 domain**，实现 Port 接口
- **adapter 之间不能互相依赖**（persistence 和 web 互不感知）
- Web 适配器依赖入端口（UseCase 接口），不直接依赖 Service 实现类

### 各层职责边界

| 层 | 职责 | 禁止 |
|---|---|---|
| Domain | 业务规则、值对象、聚合根、领域事件 | 依赖 Spring、访问数据库、处理 HTTP |
| Application | 用例编排、事务管理、Port 定义 | 实现技术细节、直接操作 MyBatis/JDBC |
| Adapter-Persistence | Domain ↔ Entity 转换、数据存取 | 包含业务逻辑 |
| Adapter-Web | Request ↔ Command、Result ↔ Response 转换 | 包含业务逻辑 |
| Adapter-Event | 实现事件发布、监听领域事件 | 包含业务逻辑 |

---

## 2. 命名约定

| 类型 | 命名模式 | 示例 |
|---|---|---|
| 入端口 | `{Action}{Aggregate}UseCase` | `CreateOrderUseCase` |
| 应用服务 | `{Action}{Aggregate}Service` | `CreateOrderService` |
| 命令 | `{Action}{Aggregate}Command` | `CreateOrderCommand` |
| 出端口（读） | `Load{Aggregate}Port` | `LoadOrderPort` |
| 出端口（写） | `Save{Aggregate}Port` | `SaveOrderPort` |
| Web 适配器 | `{Aggregate}WebAdapter` | `OrderWebAdapter` |
| Request | `{Action}{Aggregate}Request` | `CreateOrderRequest` |
| Response | `{Action}{Aggregate}Response` | `CreateOrderResponse` |
| 持久化适配器 | `{Aggregate}PersistenceAdapter` | `OrderPersistenceAdapter` |
| 数据模型 | `{Aggregate}Entity` | `OrderEntity` |
| Mapper | `{Aggregate}Mapper` | `OrderMapper` |
| 聚合根 | `{Aggregate}` extends AggregateRoot | `Order` |
| 聚合根 ID | `{Aggregate}Id`（record） | `OrderId` |
| 值对象 | `{Name}`（record） | `Money`, `Email` |
| 领域事件 | `{Aggregate}{PastTense}Event`（record） | `OrderCreatedEvent` |

### 方法命名
- Web 适配器：`create{Module}(Request)` / `get{Module}(Long id)`
- 入端口：`execute(Command)` 或语义化 `createOrder(Command)`
- 出端口：简单动词 `save(Order)` / `findById(OrderId)`
- 持久化适配器转换：`toDataModel()` / `toDomain()`

### 访问器命名（强制）
- **record**（值对象、ID、事件、命令）：无 `get` 前缀 → `orderId.value()`
- **class**（聚合根）：有 `get` 前缀 → `order.getStatus()`

---

## 3. 领域建模约束

### 聚合根
- 必须继承 `AggregateRoot<ID>` 基类
- 必须提供两个静态工厂方法：
  - `create(...)` — 创建新实例，触发业务规则校验，收集领域事件
  - `from(...)` — 从持久化恢复，不触发事件
- 私有构造函数，只通过工厂方法创建
- 状态变更通过语义化方法（如 `pay()`、`cancel()`），禁止外部 `setStatus()`
- 详细模式参见 `skills/java-ddd-hexagonal/`

### 值对象
- 使用 record 类型，天然不可变
- 业务规则通过 compact constructor 封装
- 详细模式参见 `skills/java-ddd-hexagonal/`

### ID 类型
- 每个聚合根有独立 ID 类型（record），不使用原始类型
- 提供 `generate()`（新建）和 `from()`（恢复）工厂方法

### 领域事件
- record 类型，实现 `DomainEvent` 标记接口
- 聚合根负责收集事件（`registerEvent()`），不负责发布
- 应用层通过 `EventPublisherPort` 发布，适配器桥接 Spring

---

## 4. 数据转换约束

- 每一层有自己的数据结构（Request / Command / Domain / Entity）
- 转换逻辑集中在对应适配器中，以 `to{Target}` 命名
- 转换函数是 static 方法，无状态
- 不在多处散落转换逻辑

---

## 5. API 设计规范

### RESTful
- URL：`/api/v{version}/{resources}`
- 创建：`POST` → `201 Created`
- 查询：`GET /{id}` → `200 OK`
- 更新：`PUT /{id}` → `200 OK`
- 删除：`DELETE /{id}` → `204 No Content`

### 统一响应格式
```json
{ "code": 0, "message": "操作成功", "data": { ... } }
```
- `code`：`0` 成功，`40000-49999` 客户端错误，`50000-59999` 服务端错误
- `message`：面向用户的友好提示
- `data`：错误时为 `null`

### 异常处理（强制）
`GlobalExceptionHandler` 必须至少处理以下异常：

| 异常 | 场景 | HTTP 状态码 |
|---|---|---|
| `BusinessException` | 业务规则违反 | 400 |
| `MethodArgumentNotValidException` | `@Valid` 校验失败 | 400 |
| `HttpMessageNotReadableException` | 请求体反序列化失败 | 400 |
| `BindException` | 查询参数绑定失败 | 400 |
| `Exception` | 兜底：未预期异常 | 500 |

---

## 6. 校验规范

- **Web 适配器**：校验请求格式（`@Valid` + Jakarta Validation），面向用户提示
- **应用服务**：校验业务前置条件
- **领域层**：校验核心业务不变量，违反时抛 `BusinessException`
- 同一字段在各层的校验规则必须一致

---

## 7. Spring Boot & MyBatis-Plus 约束

- 构造器注入 + Lombok `@RequiredArgsConstructor`
- 应用服务写方法用 `@Transactional`，只读方法用 `@Transactional(readOnly = true)`
- Mapper 标注 `@Mapper`，继承 `BaseMapper<Entity>`
- Entity 使用 `@TableName`、`@TableId`
- 持久化适配器通过构造器注入 Mapper，不暴露给应用层

---

## 8. 配置管理

```
application.yml          # 通用配置
application-dev.yml      # 开发环境（H2 内存数据库）
application-test.yml     # 测试环境
application-prod.yml     # 生产环境（MySQL）
```

- 敏感信息使用环境变量，不硬编码
- 业务配置使用 `@ConfigurationProperties(prefix = "app.{module}")`

---

## 9. 测试约束

- 测试类与被测类同包结构
- 测试方法名描述行为：`test{Method}_{Scenario}_Should{Expected}`
- 应用服务测试：Mock Port 接口，验证编排逻辑
- 持久化适配器测试：使用 `@MybatisPlusTest`
- Web 适配器测试：使用 `@WebMvcTest` + `@MockBean`/`@MockitoBean`
- 详细测试模式（含 `@MockBean`/`@MockitoBean` 版本差异）参见 `skills/springboot-tdd/`

---

## 10. 安全规范

- 认证信息从 Spring Security Context 获取，不从请求参数直接读取
- 敏感字段在响应中脱敏

---

## 11. 常见架构违规（强制检查清单）

以下为最常见的架构违规，代码审查时必须逐条检查：

| # | 违规 | 正确做法 |
|---|---|---|
| 1 | Domain 层 import 了 `@Service`、`@Component`、`@Repository` 等 Spring 注解 | Domain 必须是纯 Java，零框架依赖 |
| 2 | Adapter 之间互相注入（如 Web Adapter 注入 Persistence Adapter） | Adapter 通过 Port 接口间接协作，互不感知 |
| 3 | Web Adapter 直接注入 Service 实现类而非 UseCase 接口 | 只能注入 UseCase 接口（入端口） |
| 4 | Application 层直接操作 MyBatis Mapper 或 JDBC | 通过出端口（Port）接口隔离 |
| 5 | Domain 层使用了 `@TableName`、`@Column` 等 ORM 注解 | 持久化注解只出现在 Adapter 的 Entity 中 |
| 6 | 聚合根有 public setter（如 `setStatus()`） | 使用语义化方法（`pay()`、`cancel()`）变更状态 |
| 7 | 转换逻辑散落在多个类中（Adapter 有、Service 也有） | 转换集中在对应 Adapter 的 static 方法中 |
| 8 | Domain 测试加载了 Spring 上下文（用了 `@SpringBootTest`） | Domain 测试必须是纯 JUnit |

---

## 12. 元规则

1. **修改本 Rule 后，必须同步检查所有 Skill 中的相关引用和示例代码**，确保一致性。
2. Skill 中的代码示例不得与本 Rule 的约束冲突。Skill 展示"怎么做"，Rule 定义"做什么/不做什么"。
3. 新增约束优先写入 Rule，新增代码模式优先写入 Skill，避免双向冗余。

---

## 13. 日志规范

- 使用 Lombok `@Slf4j` 注解，统一使用 `log` 作为日志对象名
- **info**：关键业务流程节点（订单创建成功、支付完成），一次请求最多 3 条
- **warn**：可恢复异常（参数校验失败、降级处理），不打印完整堆栈
- **error**：不可恢复异常（数据库连接失败、消息投递失败），必须打印完整堆栈
- 日志中包含业务标识（如 orderId），方便追踪
- **禁止**：循环中打印日志、打印完整请求/响应体（含敏感信息）、使用 `System.out`

```java
@Slf4j
@Service
public class CreateOrderService implements CreateOrderUseCase {
    public Order createOrder(CreateOrderCommand command) {
        log.info("创建订单开始, userId={}, merchantId={}", command.userId(), command.merchantId());
        // ...
        log.info("订单创建成功, orderId={}, finalAmount={}", order.getId().value(), pricing.finalAmount());
    }
}
```

---

## 14. 分页排序 API 模式

### 请求参数

| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `page` | int | 1 | 页码（从 1 开始） |
| `size` | int | 20 | 每页数量（上限 100） |
| `sort` | String | `createdAt` | 排序字段名（驼峰） |
| `order` | String | `desc` | `asc` 或 `desc` |

### 响应格式

```json
{
  "code": 0,
  "message": "操作成功",
  "data": {
    "content": [...],
    "page": 1,
    "size": 20,
    "totalElements": 150,
    "totalPages": 8
  }
}
```

### URL 映射

- 查询列表时使用 `@RequestParam` 接收分页参数
- Web Adapter 负责将 `sort` 转换为数据库列名，防止 SQL 注入
- 禁止直接将 `sort` 参数拼接到 SQL 中

---

## 15. 幂等性处理

- 创建类接口（POST）必须支持幂等请求
- 客户端传入 `X-Idempotency-Key` 请求头，服务端缓存处理结果
- 幂等键格式：`{userId}:{action}:{businessKey}`，如 `user001:create:order:merchantM001`
- 幂等缓存有效期：24 小时
- 重复请求返回 `200 OK` + 原结果，而非 `201 Created`

---

## 16. Flyway 迁移文件命名规范

- 格式：`V{version}__{description}.sql`（`V` 大写，双下划线分隔）
- 版本号：`V1`、`V2`、`V3`... 递增，每个 JIRA/需求一个迁移文件
- 描述：小写 snake_case，如 `V1__create_order_tables.sql`
- 每个迁移文件只做一件事（建表、加列、加索引分开）
- 禁止：在已部署的迁移文件中修改 SQL（应新建迁移文件）

---

## 17. pom.xml 依赖管理规则

- 版本号集中在 `<properties>` 中声明，禁止硬编码在 `<dependency>` 内
- 新增依赖必须有明确理由，优先使用 Spring Boot 已管理的版本
- 第三方库选择：优先官方/社区活跃（GitHub stars > 1k、最近 3 个月有提交）
- 禁止引入与已有库功能重叠的依赖（如同时用 MyBatis-Plus 和 JPA）
- scope 标注准确：`compile`（默认）、`runtime`（数据库驱动）、`test`（测试库）、`provided`（容器提供）
