java
tour-mate-platform/
├── pom.xml (或 build.gradle.kts)
├── README.md
├── settings.gradle.kts
└── src/
└── main/
└── kotlin (或 java)/
└── com.tourmate/
├── TourMateApplication.kt # SpringBoot 启动类
│
├── common/ # 全项目公用(通用子域)
│ ├── exception/
│ ├── result/
│ └── util/
│
├── adapter/ # 所有防腐层实现(基础设施层)
│ ├── web/ # 触发层:Controller、DTO、VO
│ │ ├── activity/ # 出行活动相关接口
│ │ │ ├── dto/
│ │ │ └── ActivityController.kt
│ │ └── user/
│ │
│ └── persistence/ # 数据库防腐层
│ └── activity/
│ ├── jpa/
│ │ ├── TravelActivityJpaRepository.kt
│ │ └── entity/ # JPA Entity(脏数据)
│ └── TravelActivityRepositoryImpl.kt # 真正的防腐层实现
│
├── application/ # 应用层(输入端口)
│ └── activity/
│ ├── port/in/ # 输入端口(UseCase)
│ │ ├── CreateActivityUseCase.kt
│ │ ├── JoinActivityUseCase.kt
│ │ └── CancelActivityUseCase.kt
│ └── service/ # 应用服务(编排)
│ └── ActivityApplicationService.kt
│
├── domain/ # 核心领域层(最干净!)
│ ├── activity/ # 出行活动上下文(核心域)
│ │ ├── model/ # 聚合根 + 值对象 + 领域事件
│ │ │ ├── TravelActivity.kt # 终极聚合根
│ │ │ ├── ActivityEnrollment.kt
│ │ │ ├── valueobject/ # Capacity、ActivityTime、Creator 等
│ │ │ └── event/ # 领域事件
│ │ │ ├── ActivityCreated.kt
│ │ │ ├── ActivityFullyBooked.kt
│ │ │ └── ActivityCancelled.kt
│ │ ├── port/out/ # 输出端口
│ │ │ ├── TravelActivityRepository.kt
│ │ │ └── NotificationPort.kt
│ │ └── service/ # 领域服务(极少用)
│ │
│ ├── user/ # 用户身份与信用上下文(支撑域)
│ └── content/ # 内容分享上下文(通用域,初期可空)
│
└── infrastructure/ # 基础设施(事件监听、外部适配)
├── event/ # 领域事件监听
│ └── activity/
│ └── ActivityEventListener.kt # 监听满员、取消等事件 → 发通知
└── messaging/ # 通知、聊天适配器
└── NotificationAdapter.kt场景:用户点【加入活动】按钮(还剩 1 个名额,100 人同时点)
完整调用链(真实代码顺序 + 每一层在干什么)
| 步骤 | 代码位置(包名) | 具体类/方法 | 这一层到底在干嘛?(大白话) | 如果没有这一层会死在哪? |
|---|---|---|---|---|
| 1 | adapter/web/activity | ActivityController.join(@PathVariable id) | 只负责“收快递”:把 HTTP 请求转成命令,几乎不写业务逻辑 | 没有也行,但容易把 HTTP 协议污染业务 |
| 2 | application/activity/port/in | JoinActivityUseCase.join(activityId, userId) | 门口保安:只负责“接需求、开事务、编排流程”,一句话调用领域 | 没有这一层,Controller 直接调聚合根,容易写成大杂烩 |
| 3 | domain/activity/model | TravelActivity.join(userId) | ★ 灵魂所在!活的活动自己说:“我看看我现在能不能让你进来” → 检查状态、人数 → 改自己状态 | 没有这一层,所有规则散在 Service,超卖、状态错、权限漏 |
| 4 | domain/activity/port/out | TravelActivityRepository.load(id) | 纯净接口:领域只说“我要整个活动”,不管你是 MySQL 还是火星文 | 没有这一层,聚合根里直接写 SQL,领域被数据库污染 |
| 5 | adapter/persistence/activity | TravelActivityRepositoryImpl.load() | 真正的脏活累活:从数据库拼装出一个“活的”TravelActivity(翻译官) | 没有这一层,你永远摆脱不了“表驱动开发” |
| 6 | domain/activity/model | TravelActivity.join() 内部 | 聚合根发现已满员 → 自己改状态 → 自己抛异常 或 自己发布领域事件 | 这一步是防超卖、发通知的根本保证 |
| 7 | adapter/persistence/activity | TravelActivityRepositoryImpl.save(activity) | 再把“活的”活动拆成多条 SQL 存回去(翻译官反向工作) | 同上 |
| 8 | infrastructure/event/activity | ActivityEventListener.on(ActivityFullyBooked) | 监听器收到“活动满员了”事件 → 发推送、发站内信、更新推荐缓存 | 没有这一层,通知逻辑散在各处,容易忘 |
真实代码执行顺序(8 步完整版)
text
1. 用户点按钮 →
2. ActivityController.join() →
3. JoinActivityUseCase.join() →
4. TravelActivityRepository.load() →
5. TravelActivityRepositoryImpl.load() → 执行 2 条 SQL 拼成 TravelActivity 对象 →
6. TravelActivity.join(userId) → 纯内存操作,判断、改状态、发布事件 →
7. TravelActivityRepositoryImpl.save() → 把修改后的聚合根拆成 update + insert →
8. 事务提交 → 领域事件发布 → ActivityEventListener 发通知每一层“值不值”的终极回答
| 层级 | 你现在觉得“多此一举” | 3 年后你会跪着感谢它的理由 |
|---|---|---|
| Controller | 多写几行转换 | HTTP 协议改了(REST → GraphQL)只改这一层 |
| UseCase | 好像就是个传话筒 | 以后微信小程序、H5、后台管理都要调同一个业务,只改这一层就行 |
| 聚合根 | 写着好麻烦 | 所有并发、状态、权限、不变量只在这一个地方,改一次全站生效,永不出 bug |
| Repository Port | 多写个接口干嘛 | 明天换 MySQL → ClickHouse、换事件溯源、换 Redis 缓存,只改 Impl 就行 |
| Repository Impl | 最啰嗦 | 数据库加 10 个字段,你只改这一个文件,其他 300 个文件不动 |
| 领域事件 + 监听器 | 又是一堆类 | 发通知、更新推荐、写审计日志、同步搜索索引……所有副作用只写一次,永不遗漏 |
| 步骤 | 你的理解(已经非常对) | 我再加 5 分的精准修正(你以后可以直接背) |
|---|---|---|
| 1 | Controller 触发加入活动 | 完全正确!这一层叫 Driving Adapter(驱动适配器),只负责把外部世界(HTTP)翻译成领域能听懂的命令 |
| 2 | 到了 application 的 UseCase 进行服务编排 | 完全正确!这一层叫 Application Layer(应用层),正式名称是 Input Port + Application Service,只做“事务脚本编排”,不放业务规则 |
| 3 | 调用核心领域层的聚合根+值对象+领域事件 | 完全正确!这一层叫 Domain Layer(领域层),是真正的王,这里放“活的对象”和业务不变量 |
| 4 | 领域层需要持久化 → 调用输出端口(Repository Port) | 完全正确!这叫 Output Port(输出端口),领域只定义“我要保存/加载”,不管怎么实现 |
| 5 | 输出端口的具体实现(RepositoryImpl)→ 操作数据库 | 完全正确!这叫 Driven Adapter(被驱动适配器)/防腐层实现,所有脏活(SQL、JPA、MyBatis)全在这 |
| 6 | (你漏了最重要的一环)领域事件发布 → 事件监听器发通知等 | 补 5 分! 聚合根在 join() 里 raise ActivityFullyBooked 事件 → 事件总线 → 监听器(在 infrastructure 层)发推送、更新推荐缓存,这才叫真正的“松耦合” |
你现在可以背下来的标准 6 层调用链(大厂面试必问)
text
1. Driving Adapter(Controller)
↓(调用)
2. Input Port(UseCase 接口)
↓(实现)
3. Application Service(事务 + 编排)
↓(调用)
4. Domain Model(聚合根执行核心业务 + raise 领域事件)
↓(需要持久化时调用)
5. Output Port(Repository 接口)
↓(实现)
6. Driven Adapter(RepositoryImpl + 事件监听器)你现在可以秒杀 99% 伪 DDD 程序员的 3 个面试题
- “领域事件放哪一层?” → raise 在聚合根里(Domain),监听器在 infrastructure(Driven Adapter)
- “为什么 Controller 不能直接调 Repository?” → 因为那样就绕过了聚合根,业务规则就散了,依赖倒置也完蛋
- “Application Service 和 Domain Service 区别?” → Application Service 编排(事务、调用多个聚合),Domain Service 放“跨聚合的纯领域行为”(比如转账)