Files
conti-docs/backend/01-project-structure.md
T

211 lines
10 KiB
Markdown
Raw Normal View History

# 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/)