Files
conti-docs/backend/01-project-structure.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

211 lines
10 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.
# 01. 工程结构(后端)
## 技术栈
Kotlin + Spring Boot + GradleGroovy DSL`build.gradle`)。
## 决策
单一 Gradle 多模块工程,落地为**模块化单体(Modular Monolith)**:只有一个可执行部署单元,内部按 [architecture-diagram](../Architecture-Diagram/architecture-diagram-explanation.md) 里 App Backend 的职责边界拆成多个 Gradle 子模块,用编译期依赖规则强制边界,而不是先拆成多个独立部署的微服务。
选择模块化单体而不是一开始就上微服务,原因很直接:
- 现阶段团队规模和运维能力还撑不起"多个独立部署单元 + 服务发现 + 分布式事务/一致性"的复杂度。
- App Backend 内部这几个模块(Identity/BFF/Workbench/WebView Ticket/Integration)本来就是高内聚的一套业务,拆早了只是把进程内调用换成网络调用,徒增延迟和故障点,业务上并没有获得隔离收益。
- 但如果不做任何边界约束、堆成一个大包,后期想拆也拆不动。Gradle 多模块正好卡在中间:**部署简单(一个 jar),边界物理存在(模块间依赖编译期强制)**,未来如果某个模块(比如 `f6-integration`)流量或迭代速度明显超过其他模块,再单独拆出来部署成本也低。
## 模块结构总览
```
conti-backend/
settings.gradle
build.gradle # 根工程:统一插件版本、公共依赖约束(Spring Boot BOM
bootstrap/ # 唯一可执行模块:装配所有模块,产出单一 jar/镜像
build.gradle # 依赖所有 platform-* / domains/* 模块 + @SpringBootApplication 启动类
platform/
platform-web/ # 统一异常处理、Result包装、参数校验、GlobalExceptionHandler
platform-security/ # Spring Security + JWT 解析、门店/角色上下文注入
platform-persistence/ # JPA 基础设施:审计字段、BaseEntity、分页封装、Flyway 配置
platform-observability/ # 日志格式、Trace ID 透传、Micrometer配置
platform-integration/ # WebClient/Resilience4j 基础封装(超时/重试/熔断通用能力)
domains/
identity-store/ # Identity & Store Center:登录、token、门店上下文、菜单权限
bff-orchestration/ # BFF Orchestration:面向APP的统一接口聚合与协议标准化
workbench/ # Workbench Aggregation:首页聚合、局部降级
webview-ticket/ # WebView Ticket CenterF6 WebView 换票、会话绑定
f6-integration/ # Integration LayerF6 Adapter,对应架构图里的适配层
mini-clients/ # 对 O2O/Warranty/Retail Store/ROOS 的只读客户端封装
```
(模块名称先按架构图职责命名,实际开发中如果和团队习惯冲突可以再改,不影响这套结构本身。)
## 依赖规则(编译期强制边界,是这套结构的核心价值)
- `bootstrap` 是唯一持有 `@SpringBootApplication` 的模块,依赖所有 `domains/*``platform-*`;其余模块都是普通 library 模块(没有主启动类),不能单独跑成一个服务。
- `domains/*` 之间**不允许**互相依赖,`bff-orchestration` 是唯一例外——它是编排层,天然需要依赖其他所有 domain 才能做聚合。
- `platform-*` 不含业务逻辑,各 `domains/*` 均可依赖;`platform-*` 之间尽量不互相依赖(`platform-security` 依赖 `platform-web` 里的异常类型是可以接受的例外)。
- 跨 domain 的数据/事件传递,只能走 `bff-orchestration` 编排,或者在某个 `platform-*` 定义抽象接口、各 domain 各自实现(依赖倒置),不允许 `identity-store` 直接 import `workbench` 的类。
这些规则由 Gradle 的模块依赖机制**物理强制**:`domains/identity-store``build.gradle` 里根本不会声明对 `domains/workbench` 的依赖,编译时 import 不到,不是靠 code review 口头约束。
### 根 `build.gradle` 示例
```groovy
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.20' apply false
id 'org.jetbrains.kotlin.plugin.spring' version '2.0.20' apply false
id 'org.springframework.boot' version '3.3.4' apply false
id 'io.spring.dependency-management' version '1.1.6' apply false
}
subprojects {
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'org.jetbrains.kotlin.plugin.spring'
apply plugin: 'io.spring.dependency-management'
group = 'com.continental.retailapp'
version = '0.1.0-SNAPSHOT'
java {
sourceCompatibility = JavaVersion.VERSION_21
}
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:3.3.4"
}
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-reflect'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
kotlin {
compilerOptions {
freeCompilerArgs.add('-Xjsr305=strict')
}
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
}
```
### `settings.gradle` 示例
```groovy
rootProject.name = 'conti-backend'
include 'bootstrap'
include 'platform:platform-web'
include 'platform:platform-security'
include 'platform:platform-persistence'
include 'platform:platform-observability'
include 'platform:platform-integration'
include 'domains:identity-store'
include 'domains:bff-orchestration'
include 'domains:workbench'
include 'domains:webview-ticket'
include 'domains:f6-integration'
include 'domains:mini-clients'
```
### `domains/workbench/build.gradle` 示例(体现依赖规则)
```groovy
apply plugin: 'org.springframework.boot' // 只用来获得 bootJar 之外的 starter 依赖管理,不产出可执行 jar
bootJar {
enabled = false // 非 bootstrap 模块不产出可执行 jar
}
jar {
enabled = true
}
dependencies {
implementation project(':platform:platform-web')
implementation project(':platform:platform-persistence')
implementation project(':platform:platform-integration')
implementation project(':domains:mini-clients') // 允许:workbench 依赖只读客户端封装
// 不允许出现 implementation project(':domains:identity-store') 这种同级 domain 依赖
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
```
### `bootstrap/build.gradle` 示例
```groovy
apply plugin: 'org.springframework.boot'
dependencies {
implementation project(':platform:platform-web')
implementation project(':platform:platform-security')
implementation project(':platform:platform-persistence')
implementation project(':platform:platform-observability')
implementation project(':platform:platform-integration')
implementation project(':domains:identity-store')
implementation project(':domains:bff-orchestration')
implementation project(':domains:workbench')
implementation project(':domains:webview-ticket')
implementation project(':domains:f6-integration')
implementation project(':domains:mini-clients')
}
```
```kotlin
// bootstrap/src/main/kotlin/com/continental/retailapp/BootstrapApplication.kt
@SpringBootApplication(scanBasePackages = ["com.continental.retailapp"])
class BootstrapApplication
fun main(args: Array<String>) {
runApplication<BootstrapApplication>(*args)
}
```
## 新增一个 domain 模块的标准脚手架
```
domains/xxx/
build.gradle # 依赖需要的 platform-* 模块,bootJar 禁用
src/main/kotlin/com/continental/retailapp/xxx/
api/ # Controller、请求/响应 DTO
application/ # Service,编排用例
domain/ # 可选,见 02-layering.md
infrastructure/ # repository 实现、外部 client 实现
src/main/resources/db/migration/xxx/
V1__init.sql
src/test/kotlin/...
```
新增模块后记得在 `settings.gradle``include`,并在 `bootstrap/build.gradle` 里加上依赖——这两步是唯一需要"手动接线"的地方,其余边界规则都由各模块自己的 `build.gradle` 保证。
## 附录:为什么用 Gradle 多模块,而不是单模块 + 包分层
给还没接触过这套多模块习惯的同学看的入门说明。
### 要解决的问题
如果整个后端只是一个 Gradle 模块、内部靠 `com.xxx.identity` / `com.xxx.workbench` 这样的包名分层——短期能跑,但 Java/Kotlin 的包(package**不是编译期边界**`workbench` 包里的类可以随手 `import com.xxx.identity.SomeInternalClass`,IDE 不会报错,只能靠人自觉或者 ArchUnit 这类静态检查工具事后检测。等项目大到几十人协作时,"事后检测"经常检测不过来,边界会慢慢被绕开。
**Gradle 多模块的本质**:把"包名上的软边界"换成"模块依赖上的硬边界"。`domains/identity-store` 这个模块的 `build.gradle` 里没有声明对 `domains/workbench` 的依赖,`workbench` 里的类就物理 import 不到 `identity-store` 内部的任何类型,哪怕两个模块在同一个 git 仓库、同一次构建、最终打进同一个 jar 里。
### 跟微服务的关系
Gradle 多模块和微服务解决的是同一类问题(业务边界隔离),但选择了不同的代价:
- 微服务:边界靠网络调用强制,代价是要处理服务发现、网络失败、分布式事务/最终一致性、独立的 CI/CD 和监控。
- 模块化单体:边界靠编译依赖强制,代价是无法针对单个模块独立扩缩容或独立发布,进程内故障会互相影响(一个模块 OOM 会拖垮整个进程)。
我们现在处的阶段(App Backend 内部几个模块业务强相关、团队规模有限)更适合后者;如果将来某个模块单独的流量、团队规模、发布节奏都明显跟其他模块脱节,再把它拆成独立微服务——因为模块边界已经在代码里划清楚了,拆分主要是把 `implementation project(':domains:xxx')` 换成 HTTP/消息调用,改动范围可控,不需要推倒重来。
## 参考链接
- [Gradle Multi-Project Builds](https://docs.gradle.org/current/userguide/multi_project_builds.html)
- [Spring Boot Gradle Plugin](https://docs.spring.io/spring-boot/gradle-plugin/index.html)
- [Modular Monolith: A Primer (Kamil Grzybek)](https://www.kamilgrzybek.com/blog/posts/modular-monolith-primer)
- [ArchUnit(可选的架构规则静态检查工具)](https://www.archunit.org/)