backend scaffold
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# conti-backend
|
||||
|
||||
Continental 门店零售 App 的后端服务。**模块化单体**:14 个 Gradle 模块,一个可部署 jar,
|
||||
模块边界由 Gradle 依赖图 + ArchUnit 测试守着,将来要拆微服务时按模块切即可。
|
||||
|
||||
架构决策全部写在 [`conti-docs/backend/`](../conti-docs/backend/) 的 12 篇文档里,
|
||||
本仓库是那些决策的落地。**文档是唯一事实来源**——代码和文档不一致时,改代码,不是改文档,
|
||||
也不是把 ArchUnit 规则调松(规则调松了这套结构就白建了)。
|
||||
|
||||
## 版本基线
|
||||
|
||||
| | 版本 |
|
||||
| --- | --- |
|
||||
| Java | 21(toolchain) |
|
||||
| Kotlin | 2.4.10 |
|
||||
| Spring Boot | 4.1.0 |
|
||||
| Spring Cloud | 2025.1.2 |
|
||||
| Gradle | 9.5.1(wrapper) |
|
||||
| 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)
|
||||
```
|
||||
|
||||
具体 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 / 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
|
||||
——那是越权查询的标准入口。
|
||||
Reference in New Issue
Block a user