AI 指南(可复制)
重要
这一页本身就是那份文件。 复制下面全部内容,按 装进你的 AI IDE 表里的文件名存进工程根目录即可。里面所有「你」都指 AI 助手。
你在为一个使用 soda-core(com.hibuka.soda:soda-core-base)的 Spring Boot 工程写代码。这是一个 DDD + CQRS 框架。按下面的约定写,不要自己发明写法。
架构
请求 → Controller(适配器) → AppService(应用) → BusFacade(CQRS总线)
↓
QueryHandler / CommandHandler
↓
Repository(仓储) → DB / SLS| 层 | 职责 | 依赖方向 |
|---|---|---|
adapter/ | 接 HTTP 请求、参数校验、返回 VO | → Application |
application/ | 编排业务流程,通过 BusFacade 分发 | → Domain + Infrastructure |
domain/ | 核心业务逻辑:聚合根 + 实体 + 事件 | 不依赖任何层 |
infrastructure/ | 持久化、外部 API、对象转换 | → Domain(接口) |
核心类
| 类 | 作用 |
|---|---|
BusFacade | 唯一入口。sendQuery(query) / sendTransactCommand(cmd) / sendAsyncCommand(cmd) |
BasePageQuery<PageVO<XxxVO>> | 所有分页 Query 的基类,带 pageNumber / pageSize(MAX_PAGE_SIZE=200) |
QueryHandler<Q, R> | 实现 R handle(Q),读 |
CommandHandler<C, R> | 实现 R handle(C),写 |
EventHandler<T extends DomainEvent> | 实现 void handle(T),领域事件 |
AbstractAggregateRoot | 聚合根基类,addPendingEvent(event) 注册事件 |
WriteRepository<T> | save/update/delete/operate 四个持久化方法 |
PageVO<T> / PageResult<T> | Repository 返 PageVO,Controller 返 PageResult |
Result<T> | 非分页接口的返回包装 |
BaseException | 统一异常(RuntimeException,throws 是文档性的) |
ParamChecker | 参数校验,失败抛 BaseException(BAD_REQUEST) |
文件命名
| 层 | 文件 | 说明 |
|---|---|---|
| Domain 外层 | XxxData.java | SLS 数据包装(ts/source/endpoint/data) |
| Domain 内层 | XxxDetail.java | SLS 业务字段 |
| Domain 聚合根 | {Name}.java | extends Entity |
| Domain 实体 | {Name}Entity.java | extends AbstractAggregateRoot |
| Query | ListXxxQuery.java / GetXxxQuery.java | 读参数 |
| Command | CreatXxxCommand.java | 写参数(注意拼写是 Creat 不是 Create,跟模板保持一致) |
| VO | XxxVO.java | API 返回 |
| DO | XxxDo.java | MySQL 持久化 |
| Repository | XxxReadRepository.java / XxxWriteRepository.java | 接口在 domain,实现在 infrastructure |
| Assembler | XxxAssembler.java | 静态方法 toVo() / toDo() / toPageVo() |
| Handler | ListXxxQueryHandler.java / CreatXxxCommandHandler.java | 实现对应泛型接口 |
| Service | XxxAppService.java + XxxAppServiceImpl.java | 接口 + @Service |
| Controller | XxxController.java | @RestController @RequestMapping("/xxx") |
两个硬约束
注意
这两条违反了编译能过、跑起来才炸,或者事件静默不发。每次写代码前先对照。
1. 泛型三处必须一致
ListXxxQuery extends BasePageQuery<PageVO<XxxVO>>
⇒ QueryHandler<ListXxxQuery, PageVO<XxxVO>>
⇒ XxxAppService.listXxx(...) 返回 PageVO<XxxVO>三处的 R 必须是同一个类型。差一个泛型参数、把 XxxVO 写成 XxxData,都会出问题。
2. CommandHandler 必须返回聚合根
CommandHandler.handle(C) 的返回值是领域事件的载体。返回 void 或返回 VO,CqrsAroundHandler 就没东西可发,领域事件静默不发。
// ✅ 对
public class CreatXxxCommandHandler implements CommandHandler<CreatXxxCommand, Xxx> {
public Xxx handle(CreatXxxCommand cmd) {
Xxx agg = Xxx.create(cmd); // 内部 addPendingEvent(...)
writeRepository.save(agg); // RepositoryEventAspect 发事件
return agg; // ← 必须返回
}
}
// ❌ 错:事件永远发不出去
public Void handle(CreatXxxCommand cmd) { writeRepository.save(...); return null; }领域事件什么时候发
只有两条路径,缺一条就不发:
- CommandHandler 返回聚合根 /
DomainEvents/DomainEvent——CqrsAroundHandler发。 - Repository 的
save/update/delete/operate作用在AbstractAggregateRoot上 ——RepositoryEventAspect在事务提交后发。
只改聚合根、返回 void、又不走 *Repository 持久化 → 事件不发。聚合根必须通过 *Repository 的方法落库。
配置项
soda:
event:
bus-type: simple # simple | redis,默认 simple
redis: # bus-type=redis 时才生效
stream:
group-name: my-group
consumer-name: my-consumer
idempotency:
enabled: true
# 注意前缀是 scoda. 不是 soda.
scoda:
ddd:
cqrs:
async:
core-pool-size: 8
max-pool-size: 16注意
异步线程池的前缀是 scoda.ddd.cqrs.async.*,不是 soda.*。抄错前缀静默不生效。
构建与测试
mvn clean verify # 构建 + 测试(发版走这个)
mvn test # 只跑测试
mvn clean install # 本地装库Java 17 起,推荐 21。不要用 cp pom-3.2.xml pom.xml 那套多 POM 工作流 —— 已废弃,根目录的 pom-2.3.xml / pom-2.7.xml / pom-3.2.xml 是历史残留。
常见陷阱
警告
这些坑 AI 最容易踩,写代码前扫一眼。
- Handler 按精确类匹配。
SimpleCommandBus/SimpleQueryBus用handlers.get(command.getClass())查表,不走继承。给父类写的 Handler 不会接住子类命令。同一个命令类型只能有一个CommandHandler,后注册的会覆盖前面的。 EventHandler反而走继承。SimpleEventBus.publish会沿父类和接口找,所以EventHandler<DomainEvent>能接住所有事件。sendMqCommand是桩,永远抛UnsupportedOperationException。别调。AbstractAggregateRoot extends AbstractDomainEvent—— 聚合根本身是个事件,身上有eventId/requestId这些字段。这是历史设计,业务上用不到,别去赋值。@AutoConfigureAfter用的是字符串类名,不是Class引用(为了兼容 Spring Boot 3 和 4)。加自动配置照这个写法。PreChecker已废弃,用ParamChecker。EventSerializer是 Jackson 2/3 兼容的关键。要加 Jackson 相关注解,用 Jackson 2 的坐标com.fasterxml.jackson.annotation.*,不要引 Jackson 3 的(Spring Boot 4 上会失效)。knife4j-openapi3-spring-boot-starter是 optional。子类化BaseCommand/BaseQuery/AbstractDomainEvent时,工程自己要带io.swagger.core.v3:swagger-annotations,否则编译不过。
你不要做的事
- 不要发明
@Command/@Query/@Handler注解 —— soda 里没有任何自定义注解,全靠实现CommandHandler/QueryHandler/EventHandler接口 +@Component。 - 不要在应用层直接注入 Repository 的实现类,只注入接口(否则应用层依赖了基础设施层)。
- 不要绕过
BusFacade直接调 Handler。 - 不要在
Command里写业务规则 —— 那是聚合根的职责。Command 只放业务参数,上下文字段由框架填充。