Files
conti-docs/backend/02-layering.md
T
Guangfei.Zhao 1e0cbb86a2 feat: Add comprehensive documentation for integration layer, API design, config governance, observability, build/deploy, and testing strategies
- Introduced integration layer design with Resilience4j for external vendor calls.
- Established API design standards with unified response structures and global exception handling.
- Defined configuration and service governance using Kubernetes native solutions.
- Implemented observability practices including trace ID propagation and structured logging.
- Outlined build and multi-environment deployment strategies using Gradle and GitLab CI/CD.
- Specified testing strategies across different layers, utilizing JUnit, MockK, Testcontainers, and WireMock.
2026-08-12 18:23:11 +08:00

148 lines
7.3 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.
# 02. 分层规范(后端)
## 决策
每个 `domains/*` 模块内部采用简化分层,`domain` 层**可选**,判断标准与 Flutter 端 [02-layering.md](../02-layering.md) 保持一致的思路:
```
domains/xxx/
src/main/kotlin/com/continental/retailapp/xxx/
api/ # Controller、请求/响应 DTO
application/ # Service,编排用例、跨 repository 协调
domain/ # 可选:领域模型、repository/client 接口、状态机/复杂业务规则
infrastructure/ # JPA repository 实现、外部 client 实现
```
## 各层职责
- **api**`Controller`(只做参数校验 + 调用 `application`)、请求/响应 DTO。不写业务逻辑,不直接依赖 `infrastructure` 的具体实现类。
- **application**`Service`,编排用例、事务边界(`@Transactional` 一般加在这一层)。依赖 `domain` 定义的接口(有 domain 层时),或直接依赖 `infrastructure` 暴露的接口(跳过 domain 层时)。
- **domain**(可选):领域模型(可以是纯 Kotlin data class,不一定是 JPA entity)、repository/client 接口、封装多步骤业务规则或状态机的领域服务。不依赖 Spring Web/JPA 相关类型,可以脱离容器单独做单元测试。
- **infrastructure**`domain`(或 `application`,跳过 domain 层时)里接口的具体实现——JPA repository 实现、`WebClient`/Feign 外部调用实现。
## 何时可以跳过 domain 层
- **可以跳过**:简单 CRUD、没有跨 repository 协调、没有状态机——`application` 直接依赖 `infrastructure` 里定义的 repository/client 接口即可(接口和实现放在同一层)。
- **必须要有**:多步骤业务规则(如 WebView 换票的状态校验)、需要协调多个数据源(如 `workbench` 聚合多个 Mini 域)、包含状态机或需要独立于容器做单元测试的核心业务逻辑——接口定义在 `domain``infrastructure` 反向实现。
## 依赖方向
```
api → application → domain(或直接 → infrastructure 的接口,若跳过 domain
domain → 不依赖 api / infrastructure
infrastructure → 依赖 domain 的接口(若有),依赖 platform-persistence / platform-integration
```
`domain` 层的类不 import `org.springframework.web.*` / `jakarta.persistence.*`,保证这一层的单元测试不需要起 Spring 容器、不需要真实数据库。
## 示例一:有 domain 层(`webview-ticket`,换票——多步骤状态校验)
```kotlin
// domain/model/WebviewTicket.kt
data class WebviewTicket(
val ticketId: String,
val storeId: Long,
val userId: Long,
val status: TicketStatus,
val expiresAt: Instant,
)
enum class TicketStatus { ISSUED, CONSUMED, EXPIRED, INVALIDATED }
// domain/repository/WebviewTicketRepository.kt
interface WebviewTicketRepository {
fun findActiveTicket(userId: Long, storeId: Long): WebviewTicket?
fun save(ticket: WebviewTicket)
}
// domain/service/IssueWebviewTicketService.kt
class IssueWebviewTicketService(
private val ticketRepository: WebviewTicketRepository,
private val clock: Clock,
) {
fun issue(userId: Long, storeId: Long): WebviewTicket {
val existing = ticketRepository.findActiveTicket(userId, storeId)
if (existing != null && existing.status == TicketStatus.ISSUED && existing.expiresAt.isAfter(clock.instant())) {
return existing // 已有有效票据,直接复用,不重复签发
}
val ticket = WebviewTicket(
ticketId = UUID.randomUUID().toString(),
storeId = storeId,
userId = userId,
status = TicketStatus.ISSUED,
expiresAt = clock.instant().plus(Duration.ofMinutes(5)),
)
ticketRepository.save(ticket)
return ticket
}
}
// application/WebviewTicketAppService.kt
@Service
class WebviewTicketAppService(
private val issueWebviewTicketService: IssueWebviewTicketService,
) {
@Transactional
fun issueTicket(userId: Long, storeId: Long): WebviewTicketResponse {
val ticket = issueWebviewTicketService.issue(userId, storeId)
return WebviewTicketResponse(ticket.ticketId, ticket.expiresAt)
}
}
// infrastructure/persistence/WebviewTicketRepositoryImpl.kt
@Repository
class WebviewTicketRepositoryImpl(
private val jpaRepository: WebviewTicketJpaRepository,
) : WebviewTicketRepository {
override fun findActiveTicket(userId: Long, storeId: Long): WebviewTicket? =
jpaRepository.findByUserIdAndStoreIdAndStatus(userId, storeId, TicketStatus.ISSUED)?.toDomain()
override fun save(ticket: WebviewTicket) {
jpaRepository.save(ticket.toEntity())
}
}
```
`IssueWebviewTicketService` 的复用/过期判断规则可以直接用一个假的 `WebviewTicketRepository` 实现做单元测试,不需要起 Spring 容器或真实数据库,也不需要 mock HTTP。
## 示例二:跳过 domain 层(`identity-store`,门店列表——简单查询)
```kotlin
// infrastructure/persistence/StoreRepository.kt
interface StoreRepository {
fun findStoresByUserId(userId: Long): List<StoreEntity>
}
@Repository
interface StoreJpaRepository : JpaRepository<StoreEntity, Long>, StoreRepository {
@Query("select s from StoreEntity s join UserStoreEntity us on us.storeId = s.id where us.userId = :userId")
override fun findStoresByUserId(userId: Long): List<StoreEntity>
}
// application/StoreAppService.kt
@Service
class StoreAppService(
private val storeRepository: StoreRepository,
) {
fun listStores(userId: Long): List<StoreResponse> =
storeRepository.findStoresByUserId(userId).map { StoreResponse(it.id, it.name) }
}
```
没有多步骤规则、没有跨 repository 协调,接口和实现直接放在 `infrastructure``application` 直接依赖 `StoreRepository` 这个接口,省掉一层 `domain` 目录。
## 附录:为什么要分层,以及依赖倒置在这里怎么体现
如果 `Controller` 里直接写 `EntityManager` 查询、直接 `new WebClient.create(...)` 调用 F6——短期能跑,但会导致:
1. **业务规则没法脱离容器单独测试**:想验证"换票是否要判断过期时间",得连 Spring 容器、连数据库一起跑测试。
2. **换底层实现要动到业务代码**:比如把 JPA 换成 jOOQ,或者把 F6 调用从 `RestTemplate` 换成 `WebClient`,如果业务代码直接依赖具体实现类,改动会散落得到处都是。
分层的关键不是"分了几层",而是**依赖方向单向流动**,`domain` 只定义接口("我需要一个能查到 `WebviewTicket` 的东西"),不关心 `infrastructure` 具体怎么实现——这是[依赖倒置原则](https://en.wikipedia.org/wiki/Dependency_inversion_principle)。我们只取这套思想里最实用的一层隔离,不套用完整的 DDD 战术模式(聚合根、值对象、领域事件那一整套),避免简单模块也被迫按重量级模板写代码。
## 参考链接
- [依赖倒置原则(Dependency Inversion Principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle)
- [The Clean ArchitectureUncle Bob 原文)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Spring 官方分层架构指南](https://docs.spring.io/spring-framework/reference/core/beans/introduction.html)