Files
conti-backend/README.md
T
Guangfei.ZhaoandClaude Code aca7487be2 feat: Kotlin 语法约束——只当语法糖用,不当新范式用
新增 docs/13-kotlin-style.md,并把它落成可执行的构建门禁,而不是一份只能靠
自觉遵守的建议。两条准入标准:只当语法糖(不引入新的思维模型)、每条规则必须
说得出它挡住的具体故障。

- KotlinSyntaxScanTest(源码扫描):禁 !! / lateinit / suspend / kotlinx.coroutines
  / RxJava / Reactor / data class Entity。走源码文本而非 ArchUnit,是因为这些在
  字节码上看不出来——!! 编译成 Intrinsics.checkNotNull(...) 调用,和其他 null
  检查混在一起。
- ArchitectureRulesTest 加「禁止工具类」:只禁 Utils/Util/Helper/Common 四个
  后缀,不禁 XxxManager——Spring 自身大量使用这个命名,一刀切会误伤。
- PersistedEntity + requiredId:把「从 repository 读出来的实体必有 id」变成
  一个有名字的类型,替掉 AuthAppService / LocalSeedDataConfig 里 7 处 .id!!。
- 5 个 Entity 实现 PersistedEntity。

两条故意不自动强制的写进了文档:companion object 靠 review 判断(现存 14 处
用法均合法,硬规则只会误伤);Entity 可被 Hibernate 代理目前没守住——未 apply
kotlin("plugin.jpa"),Entity 是 final 类,fetch = LAZY 会静默降级为 eager,现
因为零懒加载关联而未暴露。

docs/01-project-structure.md 顺带补了 Gradle 版本行的双向约束说明;README 修正
了 12 篇→13 篇文档、10 条→11 条规则,并补上工具类禁名。

验证:./gradlew build -PexcludeIntegration 通过。ArchitectureRulesTest 11 项、
KotlinSyntaxScanTest、NativeQueryScanTest 全绿;两条新规则均以「植入一个故意
违规、确认它变红」的方式验证过不是空转。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:01:22 +08:00

149 lines
7.9 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/) 的 13 篇文档里,
本仓库是那些决策的落地。**文档是唯一事实来源**——代码和文档不一致时,改代码,不是改文档,
也不是把 ArchUnit 规则调松(规则调松了这套结构就白建了)。
## 版本基线
| | 版本 |
| --- | --- |
| Java | 21(toolchain) |
| Kotlin | 2.4.10 |
| Spring Boot | 4.1.0 |
| Spring Cloud | 2025.1.2 |
| Gradle | 9.5.0(wrapper)——**双向约束**:Boot 4.1 插件要 8.14+ 或 9.x,KGP 2.4.10 的完全支持上限是 9.5.0,升任一边都要重取交集 |
| 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 F6(Bulkhead + 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)
```
具体 11 条规则见
[`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);
- 不许 `XxxUtils` / `XxxUtil` / `XxxHelper` / `XxxCommon` 这类工具类(无状态逻辑用顶层函数,让文件名表达归属;有状态的用 `@Component`)。注意**不禁 `XxxManager`**——Spring 自身大量用这个后缀。
## 本地怎么跑
只有 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 / WireMock(CI 跑这个)
```
需要真实 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
——那是越权查询的标准入口。