Files
conti-backend/README.md
T
2026-08-17 15:31:27 +08:00

148 lines
7.6 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.
# conti-backend
Continental 门店零售 App 的后端服务。**模块化单体**:14 个 Gradle 模块,一个可部署 jar
模块边界由 Gradle 依赖图 + ArchUnit 测试守着,将来要拆微服务时按模块切即可。
架构决策全部写在 [`conti-docs/backend/`](../conti-docs/backend/) 的 12 篇文档里,
本仓库是那些决策的落地。**文档是唯一事实来源**——代码和文档不一致时,改代码,不是改文档,
也不是把 ArchUnit 规则调松(规则调松了这套结构就白建了)。
## 版本基线
| | 版本 |
| --- | --- |
| Java | 21toolchain |
| Kotlin | 2.4.10 |
| Spring Boot | 4.1.0 |
| Spring Cloud | 2025.1.2 |
| Gradle | 9.5.1wrapper |
| MySQL | 8.4 |
版本对齐一律走 Gradle 原生 `platform()` BOM**不引** `io.spring.dependency-management` 插件。
## 模块结构
```
bootstrap 入口 + 全部 application*.yml;唯一产出 jar 的模块
architecture-test ArchUnit 规则 + 原生 SQL 跨库扫描
platform/ 横切基础设施。互相之间只能依赖 platform-web
platform-web ApiResult / ErrorCode / 全局异常处理 / 幂等 / AuditContext
platform-security JWT 签发与校验 / StoreContextHolder / SecurityConfig
platform-persistence BaseEntity / JPA 审计 / 多实例 Flyway / 调度 / 缓存
platform-observability traceId 桥接 / @Audited 审计日志 / logback-spring.xml
platform-integration RestClientFactory / 连接池与超时 / trace 透传
domains/ 业务域。**domain 之间不许直接依赖**
identity-store 认证与门店(本骨架里唯一实现完整的纵切)
identity-store-contract 给别的 domain 用的只读契约,不含 Spring Web / JPA
workbench 工作台聚合(并行聚合 + 按 tile 降级的示例)
webview-ticket WebView 票据(domain 层 + 领域事件的示例)
bff-orchestration 预留空模块,暂无代码
integration/ 外部系统适配器
f6-adapter F6Bulkhead + CircuitBreaker + Retry + fallback
mini-clients 小程序侧 O2O 等
```
### 依赖规则(由 `architecture-test` 强制)
```
api → application → domain
↘ infrastructure ↗
domains/A ─X─> domains/B 直接依赖:禁止
domains/A ───> domains/B-contract 只读查询:允许
domains/A ~~~> ApplicationEvent 写操作/状态联动:@TransactionalEventListener(AFTER_COMMIT)
```
具体 10 条规则见
[`architecture-test/.../ArchitectureRulesTest.kt`](architecture-test/src/test/kotlin/com/continental/retailapp/architecture/ArchitectureRulesTest.kt)
每条规则的注释里都写了它对应文档的哪一节。几条最容易踩的:
- Entity 只能待在 `infrastructure` 包里,`api` 层碰不到 Entity
- `domain` 层不许出现 Spring / JPA 的任何引用,它的 bean 由 `infrastructure/config/` 显式 `@Bean` 装配;
- Controller 的端点方法必须返回 `ApiResult`
- 不许字段注入(`@Autowired` 到字段上);
- 不许用 `java.util.Date` / `Calendar` / `SimpleDateFormat`,时间一律 `Instant`UTC)。
## 本地怎么跑
只有 MySQL 跑在 Docker 里,应用本身用 Gradle 跑,断点和热重载都还在:
```bash
docker compose up -d mysql
SPRING_PROFILES_ACTIVE=local ./gradlew :bootstrap:bootRun
```
- 建库脚本在 `docker/mysql/init/`,表结构由 Flyway 建(本地和 dev/uat/prod 是同一套迁移脚本)。
- `local` profile 关掉了 Spring Cloud Kubernetes,不需要任何 K8s 环境。
- 种子数据由 `LocalSeedDataConfig` 在启动时写入(账号 `demo` / `demo1234`,两家门店),
**只在 `local` profile 下存在**——种子数据绝不写进 Flyway 脚本,那些脚本在 dev/uat/prod 也会跑。
- Swagger UI<http://localhost:8080/swagger-ui.html>`local`/`dev`/`uat` 开,`prod` 关)。
数据库要重来一遍:
```bash
docker compose down -v && docker compose up -d mysql
```
## 构建与测试
```bash
./gradlew build -PexcludeIntegration # 编译 + 单测 + ArchUnit,不需要 Docker,约 1 分钟
./gradlew build # 完整,含 Testcontainers / WireMockCI 跑这个)
```
需要真实 MySQL 或 WireMock 的测试都打了 `@Tag("integration")``-PexcludeIntegration` 会跳过它们。
本地日常改代码用前者,提 MR 前跑一次后者。
## 部署
| 环境 | 跑在哪 | 走 CI/CD | 怎么部署 |
| --- | --- | --- | --- |
| local | 自己电脑 | 否 | 见上面 |
| dev | 内网 Ubuntu 上的 k3s | 否 | `./scripts/deploy-dev.sh <commit-sha>`(先连 VPN |
| uat / prod | Azure Private AKS | 是 | 打 protected tag → GitLab 手动点 Promote |
- 镜像里**不编译代码**`Dockerfile` 直接消费 CI `validate` 阶段产出的 jar
保证部署的字节就是被测试验证过的字节。
- **一个镜像走所有环境**,环境差异只在 ConfigMap / Secret / `SPRING_PROFILES_ACTIVE`
- 真实密钥不在这个仓库里的任何一个文件中。`k8s/secret-uat.yaml` 里只有 `__injected_by_pipeline__`
占位符,真值由流水线从 Azure Key Vault 现取现渲染。
k8s 清单在 `k8s/`,流水线在 `.gitlab-ci.yml`
`lint`ktlint/detekt)和覆盖率两个 job 目前**以注释形式留着**——插件还没接,
放开就是必红;文件里写了启用需要补什么。
## 新增一个 domain 要接线的地方
漏掉任何一处,症状都不明显(编译过、启动过,但 Flyway 不跑、ArchUnit 不检查),所以逐条对:
1. **`settings.gradle`** 加 `include 'domains:xxx'`,并写 `domains/xxx/build.gradle`
——只依赖 `platform-*` 和别的 domain 的 `-contract`**不许**依赖别的 domain 本身。
2. **`bootstrap/build.gradle`** 加 `implementation project(':domains:xxx')`
否则这个模块根本不会被打进 jar。
3. **`DomainFlywayConfig.DOMAIN_SCHEMAS`** 加库名,并建
`domains/xxx/src/main/resources/db/migration/<库名>/V1__init.sql`
——Flyway 的 location 不能是空目录;同时在 `docker/mysql/init/01-create-databases.sql` 里加建库语句。
4. **`architecture-test`**`ArchitectureRulesTest.domains` 加包名(如 `xxx`),
`NativeQueryScanTest.ownerByModule``"xxx" to "<库名>"`
——不加的话跨域依赖和跨库 SQL 这两条规则对新模块是完全失效的。
前三步是"能跑起来",第四步是"边界还守得住"。
## 一些容易踩的坑
- **Boot 4 的包名搬过家**`@DataJpaTest``org.springframework.boot.data.jpa.test.autoconfigure`
`@AutoConfigureTestDatabase``org.springframework.boot.jdbc.test.autoconfigure`
都要单独引 `spring-boot-starter-data-jpa-test``spring-boot-starter-aop` 改叫 `spring-boot-starter-aspectj`
- **Gradle 9 不再自带 JUnit Platform launcher**,根 `build.gradle` 里那行
`testRuntimeOnly 'org.junit.platform:junit-platform-launcher'` 删掉的话所有 test 任务会直接失败。
- **已经执行过的 Flyway 脚本不可修改**,改了 checksum 对不上,下次启动直接 `Validate failed`
要改就新写一个版本号更大的脚本,这条对 Dev 环境同样适用。
- **每个迁移脚本必须前向兼容**`maxUnavailable: 0` 意味着滚动更新期间新旧两版 Pod 连同一个库,
删列改列一律走 expand-contract 两次发布。
- **门店维度的查询一律用 `StoreContextHolder.currentStoreId()`**,绝不用请求参数里的 storeId
——那是越权查询的标准入口。