Files
Bruno GaoandQwen-Coder 2fd1eb59ab docs: Complete production-grade documentation set
- Create SECURITY.md with vulnerability disclosure policy
- Create docs/README.md as documentation index page
- Create docs/observability.md with metrics/logs/traces/alerting/SLO specs
- Create docs/runbook.md with troubleshooting scenarios (RB-01 to RB-05)
- Create docs/security.md with security design and compliance
- Create docs/release.md with versioning and rollback strategies
- Create docs/database.md with database design and migration strategy
- Create docs/adr/0005-cache-error-handler.md for cache error handling ADR
- Update README.md with documentation index section
- Update docs/architecture.md to update B-7 rule and add trust boundary section
- Create docs/api-guide.md with API specifications
- Fix method names in OrderDao.java to match OrderMapper interface
- Add Spring Boot Actuator dependency to tmpl-infrastructure
- Fix Redis ping method in RedisHealthContributor.java
- Fix syntax errors and method names in OrderAppService.java

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-12 21:47:33 +08:00

829 lines
52 KiB
Markdown
Raw Permalink 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.
# M2 整体架构(终稿 v5——实施基准·定版)
## 一、定位与目标
M2 是一个可编译、可测试、可部署、具备自动门禁的 Java 21 通用项目模板。它不是业务系统,而是所有新服务的起点。任何团队 fork 后运行 `init.sh --service=xxx`,30 分钟内应能在 dev 环境跑通完整的"建表 → 启动 → CRUD → CI 绿灯 → 镜像推送 → ArgoCD 部署"链路。
M2 包含一个贯穿四层的示例 bounded context(Order),用于验证模板自身的正确性。通过 `init.sh --sample=none` 可清除示例代码,仅保留基础设施骨架,清除后仍可编译、启动、通过 CI、构建镜像。
## 二、技术栈基线(版本锁定规则)
**版本冻结规则**:`spring-boot.version` 在父 POM 中显式锁定,必须是已发布 GA 版本。模板仓库以 POM 中的实际值为唯一版本真相源,文档不声称"当前最新"。团队升级版本时必须在本地全量跑通 CI quality-gate 后方可提交版本变更 PR。
| 组件 | 版本(以 POM 锁定值为准) | 说明 |
|---|---|---|
| JDK | 21(Eclipse Temurin) | LTS,Virtual Threads 默认开启 |
| Spring Boot | 父 POM 锁定 GA 版本 | 升级需全量验证 |
| Maven | 3.9+ | 多模块构建 |
| PostgreSQL | 15+ | 唯一基线数据库 |
| Flyway | Spring Boot 管理版本 | 数据库迁移 |
| Hibernate | 6.x(Spring Boot 管理) | JPA 实现 |
| MapStruct | 1.6.x | 对象映射,编译期生成 |
| SpringDoc OpenAPI | 2.8+ | API 文档 |
| Redis | 7+ | 默认就绪,Spring Cache 抽象 |
| Logback + logstash-logback-encoder | 父 POM 锁定 | 结构化 JSON 日志 |
| Micrometer + Actuator | Spring Boot 管理 | 指标 + 健康检查 |
| ArchUnit | 1.3+ | 架构守护测试 |
| Spotless + Google Java Format | Maven 插件 | 代码格式化 |
| SpotBugs | Maven 插件 | 静态分析 |
| OWASP Dependency Check | Maven 插件 | 依赖漏洞扫描 |
| JaCoCo | Maven 插件 | 覆盖率门槛 ≥ 70% |
| Testcontainers | 1.20+ | 集成测试(PostgreSQL/Redis) |
| git-commit-id-maven-plugin | 父 POM 锁定 | 构建信息写入 actuator/info |
## 三、仓库拓扑(双仓库 GitOps)
```
┌──────────────────────────────────────┐ ┌──────────────────────────────────┐
│ java-service-template │ │ java-service-template-deploy │
│ (应用仓库) │ │ (部署仓库) │
│ │ │ │
│ 源码 + Gitea CI │ │ Kustomize 清单 │
│ quality-gate: 测试/格式/质量 │ │ base/ + overlays/dev|staging|prod│
│ build-and-deploy: │─────▶│ │
│ 构建镜像 → 推送 Harbor │ │ CI 更新镜像标签 │
│ 更新部署仓库镜像标签 │ │ ArgoCD 监听 → 自动同步 K8s │
└──────────────────────────────────────┘ └──────────────────────────────────┘
```
两个仓库通过镜像标签(git short SHA,7 位)建立唯一关联。应用仓库的 CI build-and-deploy Job 通过幂等脚本 clone 部署仓库,用 `yq` 精确替换对应 service + env 的镜像标签后 commit push。部署仓库无 CI workflow,仅由 ArgoCD 监听,不存在循环触发。
## 四、四层模块架构与依赖方向
```
依赖方向:外 → 内,严格单向
┌──────────────────────────────────────────────────────────┐
│ tmpl-api │
│ Spring Boot 启动类 / Controller / Filter / DTO │
│ ExceptionHandler / Config / Flyway 迁移脚本 / 配置文件 │
│ ArchUnit 测试 / 集成测试 │
├──────────────────────────┬───────────────────────────────┤
│ depends on │ depends on │
▼ ▼ │
┌──────────────────┐ ┌───────────────────────────────┐ │
│ tmpl- │ │ tmpl-application │ │
│ infrastructure │ │ │ │
│ │ │ UseCase / Port(in+out) │ │
│ JPA PO / Dao │ │ Command / Query / DTO │ │
│ Adapter │─▶│ TenantContext / TenantScope │ │
│ 审计 / 事件发布 │ │ DomainEventPublisher(接口) │ │
│ 外部服务适配 │ │ @Transactional 唯一合法位置 │ │
└──────────────────┘ └───────────────┬───────────────┘ │
│ depends on │
┌──────────────▼──────────────┐ │
│ tmpl-domain │ │
│ │ │
│ Entity / ValueObject / Enum │ │
│ Repository(接口) │ │
│ DomainException / ErrorCode │ │
│ DomainEvent │ │
│ │ │
│ 纯 Java,零框架依赖 │ │
└──────────────────────────────┘ │
────────────────────────────────────────────────────────────┘
```
### 4.1 各层依赖铁律(ArchUnit 强制执行)
**domain 层**:不依赖任何上层模块。禁止出现 Spring、JPA、Hibernate、Jakarta Persistence 的 import。允许 Java 标准库 + Jakarta Validation 注解(仅用于 ValueObject/Entity 的构造与不可变约束表达,禁止注入 `jakarta.validation.Validator` 或任何运行时校验器,业务错误仍通过 `DomainException(ErrorCode)` 抛出)。
**application 层**:依赖 domain + Spring Context/TX(用于 DI 与事务声明)。允许 ` @Transactional`、`@Service`、`@Component`。禁止依赖 Web(` @RestController`、`@RequestMapping`)、Persistence(`@Entity`、`JpaRepository`)、Cache(`@Cacheable`)、MQ(`KafkaTemplate`)、Security(`@PreAuthorize`)、HTTP Client(`RestTemplate`、`WebClient`、` @FeignClient`)。
**infrastructure 层**:依赖 application + domain。实现 application 定义的 Port 接口。不声明 ` @Transactional`(事务由 application 层控制)。
**api 层**:依赖 infrastructure + application + domain。是唯一的 Spring Boot 启动入口。Controller 禁止直接依赖 infrastructure 中的具体实现类,只通过 application 层的 UseCase 接口交互。禁止字段注入(` @Autowired` on field),必须使用构造器注入。
## 五、多租户隔离模型
### 5.1 字段命名与类型(冻结)
模板内部统一使用 `tenantId` 和 `deptId`。`customerId` / `orgId` 仅出现在 TenantContextFilter 的兼容解析逻辑中,核心域、应用层、端口、数据库查询一律不出现旧名。
| 字段 | Java 类型 | 必填 | 物理列名 | PG 列类型 | 说明 |
|---|---|---|---|---|---|
| tenantId | String | 是 | tenant_id | varchar(64) | 租户标识(原 customerId) |
| deptId | String | 是 | dept_id | varchar(64) | 部门标识(原 orgId),"0" 表示跨部门 |
| userId | Long | 是 | — | — | 当前操作人 ID(与审计列 bigint 对齐) |
| roles | Set\<String\> | 是 | — | — | 角色集合 |
**userId 类型决策说明**:userId 统一为 Long,与公司历史表规范(`create_user_id bigint`)保持一致。TenantContextFilter 解析 `X-User-Id` 时若非合法数字,直接返回 400,errorCode 为 `AUTH_INVALID_USER_ID`。系统无上下文场景(批处理/异步)使用约定系统账号 ID(`0L`)。
**deptId 存储规则(冻结)**:`dept_id` 列永远存储真实部门 ID,不允许写入 `'0'`。`"0"` 仅存在于 TenantScope 请求上下文中,用于控制"是否追加 dept_id 过滤条件"的查询逻辑。这样避免数据层出现"跨部门虚拟 dept"的脏值。
可选扩展字段(不进 M2 基线,预留位置):requestId、clientId/appId。
### 5.2 Header 契约(冻结)
| 标准 Header | 旧 Header(兼容期) | 说明 |
|---|---|---|
| X-Tenant-Id | X-Customer-Id | 由网关注入 |
| X-Dept-Id | X-Org-Id | 由网关注入 |
| X-User-Id | — | 由网关注入,必须为合法数字 |
| X-User-Roles | — | 逗号分隔 |
### 5.3 兼容窗口与失败策略(冻结)
兼容行为由配置项 `app.security.header-compat` 控制,代码默认值为 `false`。
**配置出现规则**:仅 `application-dev.yml` 中设置 `app.security.header-compat: true`。`application-staging.yml` 中不出现(按需临时开启)。`application-prod.yml` 中不出现。
**prod 硬禁保障(冻结)**:启动时校验——若 active profiles 包含 `prod` 且 `app.security.header-compat=true`(无论来自配置文件、环境变量还是启动参数),应用 **fail fast 拒绝启动**,抛出明确异常信息:`"header-compat=true is forbidden in prod profile"`。实现方式推荐 `ApplicationRunner` / `SmartLifecycle` / `InitializingBean`,确保能读取到 `spring.profiles.active` 与最终绑定的 `app.security.header-compat` 值。
| 环境 | 配置文件中是否出现 | 生效值 | 行为 |
|---|---|---|---|
| dev | 是(true) | true | 缺新 Header 时回退旧 Header,输出 WARN + 标识 `LEGACY_HEADER_FALLBACK` |
| staging | 默认不出现 | false(除非临时开启) | 同 prod 行为 |
| prod | 不出现 | false(启动时强制校验,环境变量也无法覆盖) | 不回退 |
**失败策略——Header 解析优先级(冻结)**:
TenantContextFilter 按以下优先级顺序处理,命中即停:
| 优先级 | 场景 | 结果 | HTTP | errorCode / 标识 |
|---|---|---|---|---|
| 1 | 新旧 Header 同时存在且值**不一致** | 拒绝 | 400 | AUTH_HEADER_CONFLICT |
| 2 | 新旧 Header 同时存在且值**一致** | 允许通过,使用新 Header 值 | — | WARN 日志标识 `LEGACY_HEADER_REDUNDANT` + Micrometer counter `security.header.legacy_redundant` |
| 3 | 仅有新 Header | 正常通过 | — | — |
| 4 | 仅有旧 Header 且 header-compat=true | 回退使用旧 Header 值 | — | WARN 日志标识 `LEGACY_HEADER_FALLBACK` |
| 5 | 仅有旧 Header 且 header-compat=false | 拒绝 | 400 | AUTH_LEGACY_HEADER_NOT_ALLOWED |
| 6 | 新旧均无 X-Tenant-Id | 拒绝 | 400 | AUTH_MISSING_TENANT |
| 7 | 新旧均无 X-Dept-Id | 拒绝 | 400 | AUTH_MISSING_DEPT |
| 8 | X-User-Id 非合法数字 | 拒绝 | 400 | AUTH_INVALID_USER_ID |
失败时不创建 TenantContext,不进入后续链路,直接返回统一错误格式的响应。
### 5.4 deptId=0 的权限语义(冻结)
deptId=0 表示"跨部门查询",是权限决策而非查询便利。网关在鉴权时判断用户角色,仅持有 `SCOPE_ALL_DEPT`(或等价 TENANT_ADMIN)权限的用户,网关才注入 `X-Dept-Id: 0`。普通用户的 X-Dept-Id 只能是其被授权的部门 ID。
模板层面:TenantContextFilter 不做权限判断(权限是网关职责),但提供 scope 一致性校验。dev profile 下允许 deptId=0 通过但输出 WARN(标识 `DEPT_SCOPE_ALL_DEV_ONLY`)。staging/prod 下:若 deptId=0 且 roles 不含 `SCOPE_ALL_DEPT`,返回 403,errorCode `AUTH_FORBIDDEN_SCOPE`。
**写入 contracts/security-context.md 的不可变条款**:服务内只做"范围一致性校验(scope validation)",不做权限决策(authorization decision)。权限决策由网关负责。
### 5.5 数据隔离执行点
**数据写入**:所有 Repository Port 的写方法(save / delete)显式接收 tenantId 参数,由 UseCase 从 TenantScope 传入。Adapter 持久化时将 tenantId 写入 PO。dept_id 始终写入真实部门 ID,不写入 "0"。
**数据读取**:所有查询方法必须携带 tenantId 作为过滤条件。deptId 作为范围过滤,TenantScope 中 deptId 为 "0" 时 Adapter 不添加 dept_id 条件(不代表查询 dept_id='0' 的数据)。
**Repository 安全——Dao 封装 + package-private 策略(冻结,方案 A)**:
采用 internal 包隔离实现。OrderJpaRepository 为 package-private,与 OrderDao 置于同一个 `internal` 包中。OrderDao 为 public,仅暴露带 tenant 的方法。OrderPersistenceAdapter 在 `adapter` 包中,注入 public 的 OrderDao,编译期即无法访问 package-private 的 OrderJpaRepository。
包结构冻结如下:
```
com.ljwx.tmpl.infrastructure.persistence.order.internal/
OrderJpaRepository.java ← package-private (无 public 修饰)
OrderDao.java ← public,仅暴露带 tenant 的方法
com.ljwx.tmpl.infrastructure.persistence.order.adapter/
OrderPersistenceAdapter.java ← public,注入 OrderDao
```
ArchUnit 补充规则:`..adapter..` 包不得 import `..internal..*JpaRepository`(编译期已禁止,ArchUnit 作为二次防护)。`..adapter..` 包不得位于 `..internal..` 包内(防止绕过)。
** @Query 租户检查规则(冻结——JPQL/Native SQL 分支)**:
对 `..internal..` 包中所有 ` @Query` 注解:若 `nativeQuery = true`(原生 SQL),注解值必须包含 `tenant_id`(物理列名)。若 `nativeQuery = false` 或未指定(JPQL,默认),注解值必须包含 `tenantId`(实体字段名)或 `:tenantId`(命名参数)。
**实现最佳实践**:所有 Repository 查询统一使用 `:tenantId` 命名参数(JPQL 和 native SQL 均可使用),ArchUnit 检查优先匹配 `:tenantId`,其次匹配字段名 `tenantId`(JPQL)或列名 `tenant_id`(native SQL),避免字段别名或字符串拼接导致误通过。
**上下文传播与清理(冻结)**:同步请求通过 ThreadLocal(TenantContext)传播。TenantContextFilter 必须在 finally 块中精确清理本请求写入的 MDC key(不使用 `MDC.clear()`,避免清除其他 Filter 或框架写入的字段):
```
finally {
TenantContext.clear();
MDC.remove("traceId");
MDC.remove("tenantId");
MDC.remove("deptId");
MDC.remove("userId");
}
```
这是防止"串租户"的安全底线,不可协商。任何修改 Filter 链的 PR 必须有对应的集成测试验证异常路径下上下文已清理。
**DTO 信任边界**:Controller 层的 Request DTO 禁止包含 tenantId、deptId、userId 字段。这些字段的唯一可信来源是 TenantContextFilter 从网关 Header 解析。ArchUnit B 类规则扫描 Request DTO 类中是否出现这些字段名。
### 租户 Header 信任边界
`X-Tenant-Id` Header 在 tmpl-api 内部被视为已验证的值。**但服务本身不负责校验
Header 的真实性**。在生产部署中,必须由 API Gateway 或认证层从已验证的
JWT Token 中提取并注入,不得信任客户端直接传入的值。tmpl-api 假设上游已完成校验。
信任链路:Client → API Gateway(校验 JWT → 提取 tenantId → 注入/覆盖 Header)→
tmpl-api(读取 Header → 存入 TenantContext → 附加到所有查询条件)。
详细安全设计参见 [security.md](security.md) 第 2 节。
## 六、审计列实现(冻结)
采用 **Spring Data JPA Auditing**,弃用自定义 EntityListener。
### 6.1 启用方式
配置类加 ` @EnableJpaAuditing(auditorAwareRef = "auditorAware")`。提供 `AuditorAwareImpl implements AuditorAware<Long>`,从 TenantContext 获取当前 userId(Long 类型)。批处理/异步/无上下文场景返回 `Optional.of(0L)`(约定系统账号 ID)。
### 6.2 PO 审计字段(对齐公司表规范)
| Java 字段 | Java 类型 | 注解 | 列映射 | 数据库列 | PG 类型 | 说明 |
|---|---|---|---|---|---|---|
| createUserId | Long | @CreatedBy | @Column(name="create_user_id") | create_user_id | bigint | 创建人 ID |
| updateUserId | Long | @LastModifiedBy | @Column(name="update_user_id") | update_user_id | bigint | 最后修改人 ID |
| createTime | Instant | @CreatedDate | @Column(name="create_time") | create_time | timestamptz | 创建时间 |
| updateTime | Instant | @LastModifiedDate | @Column(name="update_time") | update_time | timestamptz | 最后修改时间 |
| deleted | Boolean | — | @Column(name="is_deleted") | is_deleted | boolean default false | 软删除标记 |
| version | Integer | @Version | @Column(name="version") | version | integer | 乐观锁 |
`deleted`(映射 `is_deleted`)为软删除标记,业务显式控制,不走 Auditing。`version` 为乐观锁字段,使用 JPA ` @Version` 自动管理,不走 Auditing。
审计字段 + deleted + version 提取到 `BaseAuditPO` 抽象基类(` @MappedSuperclass` + ` @EntityListeners(AuditingEntityListener.class)`),所有业务 PO 继承即可。
### 6.3 关于 create_user / update_user(用户名)
M2 基线仅存储 userId(create_user_id / update_user_id)。用户名通过关联查询获取,避免冗余不一致。如公司规范强制要求存储用户名,可在 BaseAuditPO 中扩展 createUser / updateUser 字段(varchar),通过 ` @PrePersist` / ` @PreUpdate` 从 TenantContext 获取。此为可选扩展,不影响基线。
## 七、异常体系与错误映射
### 7.1 ErrorCode 设计(冻结——唯一映射源)
ErrorCode 枚举是 HTTP Status + 默认 message 的**唯一真相源**。GlobalExceptionHandler 只负责将 ErrorCode 翻译为 ApiResponse,不维护独立的映射表。新增 bounded context 时只需扩展 ErrorCode 枚举,Handler 无需改动。
**ErrorCode 枚举代码结构冻结**:
```
public enum ErrorCode {
COMMON_INVALID_PARAM(400, "参数校验失败"),
ORDER_NOT_FOUND(404, "订单不存在"),
...;
private final int httpStatus;
private final String defaultMessage;
// 对外序列化:errorCode 字段取值为 name()(即枚举常量名)
// 不额外定义 code 字符串字段,直接使用 Java Enum 自带的 name()
}
```
### 7.2 统一错误响应
```json
{
"code": 404,
"errorCode": "ORDER_NOT_FOUND",
"message": "订单不存在",
"details": "Order with id=xxx not found(仅非 prod 展示)",
"traceId": "abc123",
"timestamp": "2026-02-12T10:00:00Z"
}
```
`code` 固定为 HTTP 状态码整数(由 `ErrorCode.httpStatus` 决定),`errorCode` 为 `ErrorCode.name()`,前端按 `errorCode` 做分支判断。`details` 在 prod 环境下隐藏(返回 null)。`message` 取 DomainException.detail(若有),否则取 ErrorCode.defaultMessage。
### 7.3 M2 基线 ErrorCode 清单
| errorCode (name()) | httpStatus | defaultMessage |
|---|---|---|
| COMMON_INVALID_PARAM | 400 | 参数校验失败 |
| COMMON_NOT_FOUND | 404 | 资源不存在 |
| COMMON_INTERNAL_ERROR | 500 | 系统内部错误 |
| COMMON_OPTIMISTIC_LOCK | 409 | 数据已被修改,请刷新重试 |
| AUTH_MISSING_TENANT | 400 | 缺少租户标识 |
| AUTH_MISSING_DEPT | 400 | 缺少部门标识 |
| AUTH_INVALID_USER_ID | 400 | 用户标识格式不合法 |
| AUTH_HEADER_CONFLICT | 400 | 请求头信息冲突 |
| AUTH_LEGACY_HEADER_NOT_ALLOWED | 400 | 不再支持的旧版请求头 |
| AUTH_ACCESS_DENIED | 403 | 无权限 |
| AUTH_FORBIDDEN_SCOPE | 403 | 部门范围越权 |
| ORDER_NOT_FOUND | 404 | 订单不存在(示例) |
| ORDER_STATUS_INVALID | 422 | 订单状态不合法(示例) |
| ORDER_AMOUNT_INVALID | 422 | 订单金额不合法(示例) |
**注意**:`LEGACY_HEADER_REDUNDANT` 和 `LEGACY_HEADER_FALLBACK` **不进入 ErrorCode 枚举**。它们是日志/指标维度的标识,定义为独立的 `SecurityLogMarker` 常量类,Micrometer counter 使用相同标识名(如 `security.header.legacy_redundant`)。
### 7.4 ErrorCode 新增规则(冻结)
新增 ErrorCode 必须遵循前缀命名(`{BOUNDED_CONTEXT}_*`)。新增后必须在 errors.md 中同步更新。ErrorCode 只允许新增和标记 @Deprecated,不允许删除或修改已有枚举项的 name() / httpStatus。涉及前端/网关行为变更的 ErrorCode 变更必须通知相关方。
## 八、请求生命周期(一次 POST /api/v1/orders 的完整链路)
```
客户端
│
▼
网关(鉴权、限流、路由)
│ 注入 Header: X-Tenant-Id / X-Dept-Id / X-User-Id / X-User-Roles
▼
┌─ tmpl-api ────────────────────────────────────────────────────────┐
│ │
│ TraceIdFilter [Ordered.HIGHEST_PRECEDENCE] │
│ → 读取或生成 traceId → MDC.put("traceId") │
│ │
│ TenantContextFilter [Ordered.HIGHEST_PRECEDENCE + 1] │
│ → 按优先级 1-8 解析 Header(见 5.3 失败策略表) │
│ → X-User-Id 解析为 Long,失败 → 400 AUTH_INVALID_USER_ID │
│ → 填充 TenantContext(ThreadLocal) │
│ → MDC.put("tenantId", "deptId", "userId") │
│ → finally { TenantContext.clear(); │
│ MDC.remove("traceId"/"tenantId"/"deptId"/"userId"); }│
│ │
│ OrderController │
│ → 接收 CreateOrderRequest(不含租户字段, @Valid 校验) │
│ → TenantScope scope = TenantContext.currentScope() │
│ → 构建 CreateOrderCommand │
│ → 调用 createOrderUseCase.execute(command, scope) │
│ → 包装为 ApiResponse.created(OrderResponse) │
│ │
└─────────────────────────────┬──────────────────────────────────────┘
│
┌─ tmpl-application ──────────▼──────────────────────────────────────┐
│ │
│ CreateOrderUseCase │
│ @Transactional │
│ → 校验 Command │
│ → Order.create(OrderId.generate(), scope.tenantId(), ...) │
│ → orderRepository.save(order, scope.tenantId()) │
│ → eventPublisher.publish(order.domainEvents()) │
│ → 返回 OrderDetail │
│ │
└─────────────────────────────┬──────────────────────────────────────┘
│
┌─ tmpl-domain ───────────────▼──────────────────────────────────────┐
│ │
│ Order.create(id, tenantId, items, totalAmount) │
│ → totalAmount == null → 抛 ORDER_AMOUNT_INVALID │
│ → totalAmount < 0 → 抛 ORDER_AMOUNT_INVALID │
│ → totalAmount == 0 → 允许 │
│ → 设置状态 CREATED │
│ → 注册 OrderCreatedEvent │
│ → 返回 Order 聚合 │
│ │
│ Order 聚合仅含业务字段,不含审计列(审计由 PO 层承担) │
│ │
└─────────────────────────────┬──────────────────────────────────────┘
│
┌─ tmpl-infrastructure ───────▼──────────────────────────────────────┐
│ │
│ OrderPersistenceAdapter (adapter 包) │
│ → 注入 OrderDao(internal 包,public 类) │
│ → 编译期无法访问 OrderJpaRepository(internal 包,package-private)│
│ → OrderPersistenceMapper: Domain Order → OrderPO │
│ → OrderPO 继承 BaseAuditPO(审计字段自动填充) │
│ → dept_id 写入真实部门 ID,不写入 "0" │
│ → 不声明 @Transactional(事务由 UseCase 控制) │
│ │
│ LoggingOrderEventPublisher implements DomainEventPublisher │
│ → 结构化 JSON 日志输出事件(M3 可替换为 MQ) │
│ │
└─────────────────────────────┬──────────────────────────────────────┘
│
▼
PostgreSQL
(Flyway 管理 schema)
```
## 九、CI 流水线(Gitea Actions)
### 9.1 Stage 结构(冻结——单 Job 多 Step + 独立 Job)
Stage 1-3 合并为同一个 Job(quality-gate)的顺序 Step,共享 workspace。Stage 4-5 为独立 Job(build-and-deploy),仅在 main/release 分支触发,通过 `needs` 依赖 quality-gate Job。
```
push / PR (所有分支)
│
▼
┌─ Job: quality-gate (所有分支) ──────────────────────────┐
│ │
│ Step 1: Compile + Unit Test + ArchUnit │
│ mvn -T1C clean test │
│ JaCoCo agent 挂载,生成覆盖率 exec 文件 │
│ │
│ Step 2: Format Check │
│ mvn spotless:check │
│ 不触发编译,纯文本扫描,零容忍 │
│ │
│ Step 3: Quality Gate │
│ mvn verify -DskipTests │
│ 复用 Step 1 编译产出(同一 workspace,无需 artifact) │
│ SpotBugs(基于 class) │
│ OWASP Dependency Check(基于依赖树) │
│ JaCoCo 覆盖率门槛校验 ≥ 70%(基于 Step 1 的 exec) │
│ │
└──────────────────────┬───────────────────────────────────┘
│ pass
┌──────────────────────▼───────────────────────────────────┐
│ Job: build-and-deploy (仅 main / release 分支) │
│ needs: [quality-gate] │
│ │
│ Step 4: Build Image │
│ docker build → 多阶段构建 │
│ 内部 mvn -DskipTests package(CI 已保证测试通过) │
│ docker tag ${GIT_SHA_SHORT} │
│ docker push harbor.ljwx.com/library/${service}:${sha} │
│ │
│ Step 5: Update Deploy Repo │
│ 幂等脚本: │
│ clone ${service}-deploy │
│ yq 精确替换 overlays/${env}/kustomization.yaml │
│ 仅修改当前 service 的标签,不影响其他 service │
│ git commit + push → 触发 ArgoCD 同步 │
│ 分支 → 环境映射:main → dev,release/* → staging/prod │
│ │
└───────────────────────────────────────────────────────────┘
```
### 9.2 设计要点
quality-gate Job 对所有分支和 PR 生效。build-and-deploy Job 仅在 main/release 分支合并后触发。Step 1-3 共享同一 workspace,JaCoCo exec、编译后 class、surefire 报告均可直接被后续 Step 读取。build-and-deploy Job 通过 `needs: [quality-gate]` 声明依赖,quality-gate 失败则不执行。
## 十、部署架构(ArgoCD + Kustomize)
### 10.1 部署仓库结构
```
${service-name}-deploy/
│
├── base/
│ ├── deployment.yaml ← 通用 Deployment 模板
│ ├── service.yaml ← ClusterIP Service
│ ├── configmap.yaml ← 通用配置(TZ=UTC 等)
│ ├── hpa.yaml ← HPA 模板(min/max 由 overlay 覆盖)
│ └── kustomization.yaml
│
└── overlays/
├── dev/
│ ├── kustomization.yaml ← replicas:1, 镜像标签, SPRING_PROFILES_ACTIVE=dev
│ ├── configmap-patch.yaml
│ └── resource-patch.yaml
├── staging/
│ ├── kustomization.yaml ← replicas:2, staging profile
│ └── ...
└── prod/
├── kustomization.yaml ← replicas 按需, prod profile, 资源上限
└── ...
```
### 10.2 ArgoCD Application 命名
每个环境一个 Application,命名为 `${service-name}-${env}`(如 `order-service-dev`)。监听对应 overlays 目录。
### 10.3 Deployment 默认配置
优雅停机:preStop hook(sleep 5)+ terminationGracePeriodSeconds: 30 + `spring.lifecycle.timeout-per-shutdown-phase=25s`。健康探针:livenessProbe → `/actuator/health/liveness`,readinessProbe → `/actuator/health/readiness`,启动延迟 initialDelaySeconds: 30。环境变量注入:SPRING_PROFILES_ACTIVE、LOG_FORMAT=json(prod)、TZ=UTC、JAVA_OPTS(JVM 参数)。资源 requests/limits 由 overlay 覆盖,base 提供合理默认值。
## 十一、Docker 镜像策略
采用多阶段构建。第一阶段使用 Maven + JDK 21 编译打包(`mvn -DskipTests package`)。第二阶段使用 Eclipse Temurin 21 JRE 作为运行时镜像。最终镜像不包含源码、构建工具和完整 JDK,体积控制在 200MB 以内。
**skipTests 安全性保证**:Docker build 仅在 CI 的 build-and-deploy Job 中触发,该 Job 通过 `needs: [quality-gate]` 依赖 quality-gate Job 全部通过。
镜像标签固定为 git short SHA(7 位),不使用 latest,不使用分支名。通过 `spring-boot-maven-plugin` 的 `build-info` goal 和 `git-commit-id-maven-plugin` 生成构建信息,`/actuator/info` 端点返回 commit SHA、构建时间、Spring Boot 版本。
## 十二、日志策略
### 12.1 依赖与格式
依赖 `net.logstash.logback:logstash-logback-encoder`(版本在父 POM 锁定)。dev profile 使用 ConsoleAppender + PatternLayout(人类可读),非 dev profile 使用 ConsoleAppender + LogstashEncoder(JSON 输出到 stdout,由 Loki/Promtail 采集)。
### 12.2 固定 MDC 字段
| MDC Key | 注入位置 | 清理方式 | 说明 |
|---|---|---|---|
| traceId | TraceIdFilter | MDC.remove("traceId") | 请求链路 ID |
| tenantId | TenantContextFilter | MDC.remove("tenantId") | 租户 |
| deptId | TenantContextFilter | MDC.remove("deptId") | 部门 |
| userId | TenantContextFilter | MDC.remove("userId") | 操作人(Long → String) |
### 12.3 日志/指标标识常量(非 ErrorCode)
以下标识不进入 ErrorCode 枚举,定义在独立的 `SecurityLogMarker` 常量类中,用于 WARN 日志和 Micrometer counter:
| 常量名 | 用途 | Micrometer counter 名 |
|---|---|---|
| LEGACY_HEADER_FALLBACK | 兼容模式回退旧 Header | security.header.legacy_fallback |
| LEGACY_HEADER_REDUNDANT | 新旧 Header 同时存在且一致 | security.header.legacy_redundant |
| DEPT_SCOPE_ALL_DEV_ONLY | dev 环境 deptId=0 放行 | security.scope.dept_all_dev |
### 12.4 可选业务字段
在代码中按需 `MDC.put`:costMs、sqlId、rows、method、requestUri、status、spanId(为 OTel 预留)。
### 12.5 慢查询
阈值可配置(`app.observability.slow-query-threshold-ms`),默认 200ms。超阈值输出 WARN 日志,包含 sqlId、costMs、rows。
## 十三、应用仓库完整目录结构
```
java-service-template/
│
├── pom.xml ← 父 POM:版本锁定、插件统一配置
│ groupId: com.ljwx.tmpl
│
├── tmpl-domain/
│ ├── pom.xml ← 零框架依赖(仅 jakarta.validation-api)
│ └── src/main/java/com/ljwx/tmpl/domain/
│ ├── exception/
│ │ ├── ErrorCode.java ← 枚举:httpStatus + defaultMessage
│ │ └── DomainException.java ← errorCode + detail
│ ├── model/
│ │ ├── Order.java ← 聚合根(示例),纯业务字段
│ │ ├── OrderId.java ← 值对象
│ │ ├── Money.java ← 值对象
│ │ └── OrderStatus.java ← 枚举
│ ├── event/
│ │ ├── DomainEvent.java ← 标记接口
│ │ └── OrderCreatedEvent.java ← 示例事件
│ └── repository/
│ └── OrderRepository.java ← 接口,方法显式带 tenantId
│
├── tmpl-application/
│ ├── pom.xml ← 依赖 tmpl-domain + spring-context + spring-tx
│ └── src/main/java/com/ljwx/tmpl/application/
│ ├── port/
│ │ ├── in/
│ │ │ ├── CreateOrderUseCase.java
│ │ │ └── GetOrderUseCase.java
│ │ └── out/
│ │ └── DomainEventPublisher.java ← 接口
│ ├── command/
│ │ └── CreateOrderCommand.java
│ ├── query/
│ │ └── OrderDetailQuery.java
│ ├── dto/
│ │ ├── OrderDetail.java
│ │ └── PageResult.java
│ ├── context/
│ │ ├── TenantContext.java ← ThreadLocal,userId 为 Long
│ │ └── TenantScope.java ← record(tenantId, deptId, userId, roles)
│ └── service/
│ ├── CreateOrderService.java ← implements CreateOrderUseCase
│ └── GetOrderService.java ← implements GetOrderUseCase
│
├── tmpl-infrastructure/
│ ├── pom.xml ← 依赖 tmpl-application + JPA + Redis 等
│ └── src/main/java/com/ljwx/tmpl/infrastructure/
│ ├── persistence/
│ │ ├── entity/
│ │ │ ├── BaseAuditPO.java ← @MappedSuperclass + JPA Auditing
│ │ │ │ Long createUserId ( @CreatedBy, @Column("create_user_id"))
│ │ │ │ Long updateUserId ( @LastModifiedBy, @Column("update_user_id"))
│ │ │ │ Instant createTime ( @CreatedDate, @Column("create_time"))
│ │ │ │ Instant updateTime ( @LastModifiedDate, @Column("update_time"))
│ │ │ │ Boolean deleted ( @Column("is_deleted"))
│ │ │ │ Integer version ( @Version)
│ │ │ └── OrderPO.java ← 继承 BaseAuditPO
│ │ ├── converter/
│ │ │ └── OrderStatusConverter.java
│ │ ├── order/
│ │ │ ├── internal/ ← JpaRepository + Dao 同包
│ │ │ │ ├── OrderJpaRepository.java ← package-private
│ │ │ │ └── OrderDao.java ← public,仅暴露带 tenant 方法
│ │ │ └── adapter/
│ │ │ └── OrderPersistenceAdapter.java ← 注入 OrderDao
│ │ └── mapper/
│ │ └── OrderPersistenceMapper.java ← MapStruct: Domain ↔ PO
│ ├── audit/
│ │ └── AuditorAwareImpl.java ← AuditorAware<Long>,无上下文→0L
│ └── event/
│ └── LoggingOrderEventPublisher.java
│
├── tmpl-api/
│ ├── pom.xml ← 依赖全部模块 + Boot Starter + Flyway + Testcontainers
│ └── src/
│ ├── main/java/com/ljwx/tmpl/api/
│ │ ├── TmplApplication.java ← @SpringBootApplication
│ │ ├── controller/
│ │ │ └── OrderController.java
│ │ ├── dto/
│ │ │ ├── CreateOrderRequest.java ← 不含 tenantId/deptId/userId
│ │ │ ├── OrderResponse.java
│ │ │ ├── ApiResponse.java ← 统一响应包装
│ │ │ └── PageResponse.java ← 分页响应
│ │ ├── mapper/
│ │ │ └── OrderDtoMapper.java ← MapStruct
│ │ ├── filter/
│ │ │ ├── TraceIdFilter.java ← HIGHEST_PRECEDENCE
│ │ │ └── TenantContextFilter.java ← HIGHEST_PRECEDENCE + 1
│ │ │ finally { remove specific MDC keys }
│ │ ├── exception/
│ │ │ └── GlobalExceptionHandler.java ← 从 ErrorCode 取 httpStatus/message
│ │ ├── marker/
│ │ │ └── SecurityLogMarker.java ← 日志/指标标识常量(非 ErrorCode)
│ │ └── config/
│ │ ├── SecurityConfig.java
│ │ ├── TenantHeaderProperties.java ← @ConfigurationProperties,默认 compat=false
│ │ ├── TenantHeaderStartupValidator.java ← prod + compat=true → fail fast
│ │ ├── OpenApiConfig.java
│ │ ├── WebConfig.java
│ │ └── JpaAuditingConfig.java ← @EnableJpaAuditing
│ ├── main/resources/
│ │ ├── application.yml ← 公共配置 + virtual threads
│ │ ├── application-dev.yml ← header-compat: true
│ │ ├── application-staging.yml ← header-compat 不出现
│ │ ├── application-prod.yml ← header-compat 不出现
│ │ ├── logback-spring.xml ← dev=pattern, 非dev=LogstashEncoder
│ │ └── db/migration/
│ │ ├── V1__init.sql ← 基础表(sample=none 时保留)
│ │ └── V2__create_order_table.sql ← 示例表(sample=none 时删除)
│ └── test/java/com/ljwx/tmpl/
│ ├── architecture/
│ │ └── ArchitectureTest.java ← A/B/C 类规则
│ └── integration/
│ ├── OrderApiIntegrationTest.java
│ └── TenantFilterIntegrationTest.java
│
├── docs/
│ ├── 00-index.md ← 文档入口索引
│ ├── architecture-spec.md
│ ├── repo-layout.md
│ ├── contracts/
│ │ ├── security-context.md
│ │ ├── transactions.md
│ │ ├── errors.md
│ │ └── observability.md
│ ├── adrs/
│ │ └── ADR-0001-tenant-field-rename.md
│ └── prompts/
│ ├── P0_system_rules.md
│ ├── P1_task_templates/
│ │ ├── route_a_api_first.md
│ │ └── route_b_domain_first.md
│ └── P2_fixloop.md
│
├── .gitea/workflows/
│ └── ci.yaml ← 2 Job: quality-gate + build-and-deploy
├── Dockerfile ← 多阶段构建
├── .editorconfig
├── .gitignore
├── init.sh
└── README.md
```
## 十四、ArchUnit 规则分级
### A 类(必须通过,CI 阻断)
| 规则 | 说明 |
|---|---|
| domain 禁止 import Spring/JPA/Hibernate | `..domain..` 不得依赖 `org.springframework..`、`jakarta.persistence..`、`org.hibernate..` |
| domain 禁止注入 Validator | `..domain..` 不得 import `jakarta.validation.Validator` 及其实现 |
| application 禁止 import Web/Persistence/Security/Client | 白名单:Spring Context + TX |
| @Transactional 仅限 application 层 | 扫描所有 ` @Transactional`,必须在 `..application..` 包内 |
| 禁止字段注入 | 不允许 ` @Autowired` 用于字段 |
| Controller 禁止依赖 infrastructure | `..api.controller..` 不得 import `..infrastructure..` |
| Adapter 禁止 import JpaRepository | `..adapter..` 不得 import `..internal..*JpaRepository` |
| Adapter 不得位于 internal 包 | `..adapter..` 包路径不得包含 `..internal..` |
| @Query(nativeQuery=true) 必须含 tenant_id | 原生 SQL 必须包含物理列 `tenant_id` |
| @Query(JPQL) 必须含 tenantId | JPQL 必须包含 `:tenantId` 或实体字段 `tenantId` |
### B 类(强烈推荐,覆盖 ≥ 80%)
| 规则 | 说明 |
|---|---|
| UseCase 入口方法命名 | public 方法必须名为 `execute` |
| 测试类命名 | *Test / *IT 结尾 | ArchUnit `TestNamingConventionTest`(扫描 test classpath)或 CI 门禁正则 |
### C 类(可选,逐步启用)
| 规则 | 说明 |
|---|---|
| 异步代码禁止 TenantContext.current() | 标注 @Async 的方法内禁止调用 |
| Controller 参数必须 @Valid | 所有 @RequestBody 参数 |
## 十五、init.sh 行为规范
### 15.1 参数
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
| --service | 是 | — | 服务名(如 order-service) |
| --package | 否 | com.ljwx | 包根 |
| --sample | 否 | order | order 或 none |
| --no-table-prefix | 否 | 不开启 | 去掉 t_ 前缀 |
### 15.2 替换范围
目录名:`tmpl-*` → `${service}-*`。pom.xml:artifactId、groupId、module name。Java 包名:`com.ljwx.tmpl` → `${package}.${service-dotted}`。Dockerfile:镜像路径。Kustomize 引用:镜像名。README:项目名。.gitea/workflows/ci.yaml:service 名变量。
### 15.3 --sample=none 行为
删除 Order 相关所有 Java 文件。删除 `V2__create_order_table.sql`。保留 `V1__init.sql`。保留 ArchUnit 基础规则。保留 Filter、ExceptionHandler、Config、ApiResponse、BaseAuditPO、AuditorAwareImpl、TenantContext 等基础设施。Actuator /actuator/health 作为启动后可验证端点。`mvn clean test` 必须全绿。
### 15.4 输出验收清单
init.sh 执行完毕后打印:
```
✅ 初始化完成:${service-name}
groupId: ${groupId}
package: ${package}.${service-dotted}
镜像仓库: harbor.ljwx.com/library/${service-name}
镜像标签: 由 CI 生成(格式: ${GIT_SHA_SHORT},不使用 latest)
验证步骤:
1. mvn clean test ← 全部绿灯
2. docker build -t ${service}:local . ← 本地构建成功
3. docker run ... → curl http://localhost:8080/actuator/health ← {"status":"UP"}
提示:镜像推送由 CI build-and-deploy Job 自动完成,标签为 git commit short SHA。
```
## 十六、M2 Frozen Contract(不可变契约)
### 16.1 变更冻结项的强制伴随变更
任何 PR 修改以下冻结项时,必须同时更新对应位置:
| 变更范围 | 必须同步更新 |
|---|---|
| Header 规则 / 租户字段名 | docs/contracts/security-context.md + TenantFilterIntegrationTest + README |
| 层级依赖规则 | docs/architecture-spec.md + ArchitectureTest + README |
| 审计字段 | Flyway migration + BaseAuditPO + AuditorAwareImpl + 保存实体的集成测试断言 |
| ErrorCode 变更 | ErrorCode 枚举 + docs/contracts/errors.md |
| CI Stage / 门禁阈值 | ci.yaml + docs/architecture-spec.md + README |
| 镜像标签规则 | Dockerfile + ci.yaml + deploy repo kustomization + docs/contracts/observability.md |
### 16.2 ADR 要求
凡是改动租户模型、事务边界、错误码体系、CI 门禁、镜像策略的 PR,必须在 `docs/adrs/` 中补充 ADR 记录,格式为 `ADR-{序号}-{简述}.md`,包含:背景、决策、后果、替代方案。
### 16.3 ErrorCode 不可变性
ErrorCode 只允许新增和标记 @Deprecated,不允许删除或修改已有枚举项的 name() / httpStatus。
## 十七、关键设计决策索引
### 核心冻结项(31 项)
| # | 决策项 | 结论 | 契约来源 |
|---|---|---|---|
| 1 | 默认包根 | com.ljwx | architecture-spec.md |
| 2 | 默认 groupId | com.ljwx.${service} | architecture-spec.md |
| 3 | Spring Boot 版本 | 父 POM 锁定 GA 版本(以 POM 为准) | architecture-spec.md |
| 4 | 租户字段命名 | tenantId / deptId(弃用 customerId / orgId) | security-context.md |
| 5 | 租户物理列 | tenant_id varchar(64) / dept_id varchar(64) | security-context.md |
| 6 | userId 类型 | Long(与审计列 bigint 对齐) | security-context.md |
| 7 | Header 标准 | X-Tenant-Id / X-Dept-Id / X-User-Id / X-User-Roles | security-context.md |
| 8 | Header 兼容窗口 | dev 回退(WARN),prod 配置项不出现 + fail fast 校验 | security-context.md |
| 9 | 缺 Header 失败策略 | 非兼容模式直接 400 | security-context.md |
| 10 | prod 仅有旧 Header | 400 AUTH_LEGACY_HEADER_NOT_ALLOWED | security-context.md |
| 11 | 新旧 Header 同时存在且一致 | 允许通过,WARN + counter(迁移观测) | security-context.md |
| 12 | Header 冲突 | 新旧不一致 → 400 AUTH_HEADER_CONFLICT | security-context.md |
| 13 | deptId=0 语义 | 跨部门,需 SCOPE_ALL_DEPT 权限 | security-context.md |
| 14 | deptId 存储规则 | dept_id 列永远存真实部门 ID,不写入 "0" | security-context.md |
| 15 | 服务内权限边界 | 只做 scope validation,不做 authorization decision | security-context.md |
| 16 | 租户字段来源 | 仅从网关 Header 解析,DTO 禁止携带 | security-context.md |
| 17 | TenantContext 清理 | finally 块 remove 具体 MDC key,不用 MDC.clear() | security-context.md |
| 18 | 事务声明位置 | 仅 application 层 UseCase | transactions.md |
| 19 | 审计实现 | Spring Data JPA Auditing,AuditorAware\<Long\> | architecture-spec.md |
| 20 | 审计字段 | create_user_id(bigint) / update_user_id(bigint) / create_time(timestamptz) / update_time(timestamptz) | architecture-spec.md |
| 21 | 系统审计 ID | 无上下文时 userId = 0L | architecture-spec.md |
| 22 | 软删除 | Java 字段 deleted, @Column(name="is_deleted"),业务显式控制 | architecture-spec.md |
| 23 | 乐观锁 | @Version,JPA 自动管理 | transactions.md |
| 24 | 主键类型 | 领域 String(VO),PO VARCHAR(64) | architecture-spec.md |
| 25 | Repository 安全 | Dao 封装 + internal 包 package-private JpaRepository | security-context.md |
| 26 | @Query 租户检查 | JPQL 含 tenantId/:tenantId,native SQL 含 tenant_id | security-context.md |
| 27 | ErrorCode 设计 | 枚举自带 httpStatus + defaultMessage,name() 即 errorCode | errors.md |
| 28 | ErrorCode 不可变性 | 只新增/Deprecated,不删除不改 name/httpStatus | errors.md |
| 29 | 日志格式 | logstash-logback-encoder JSON;dev 用 pattern | observability.md |
| 30 | MDC 字段 | traceId / tenantId / deptId / userId | observability.md |
| 31 | 镜像标签 | git short SHA(7位),写入 actuator/info,不使用 latest | observability.md |
### 扩展冻结项(7 项)
| # | 决策项 | 结论 | 契约来源 |
|---|---|---|---|
| 32 | CI 结构 | 单 Job 多 Step(quality-gate)+ 独立 Job(build-and-deploy) | architecture-spec.md |
| 33 | Docker build 安全性 | skipTests,仅在 quality-gate 通过后触发 | architecture-spec.md |
| 34 | domain Jakarta Validation | 仅允许注解,禁止注入 Validator | architecture-spec.md |
| 35 | application 层依赖口径 | domain + Spring Context/TX,禁止 Web/Persistence/Security/Client | architecture-spec.md |
| 36 | Virtual Threads | 默认开启 spring.threads.virtual.enabled=true | architecture-spec.md |
| 37 | 表前缀 | t_(可通过 --no-table-prefix 关闭) | architecture-spec.md |
| 38 | Sample 模块 | --sample=order 默认,none 保留 V1__init.sql + 基础设施 | repo-layout.md |
## 十八、实施顺序与验收点
| 批次 | 交付内容 | 验收点 |
|---|---|---|
| 1 | 父 POM + 四模块骨架 + TmplApplication + Actuator | `mvn clean test` 通过;启动后 `/actuator/health` 返回 UP |
| 2 | ErrorCode + DomainException + TenantContext + TenantScope + SecurityLogMarker | 编译通过;domain/application 单测跑通 |
| 3 | TraceIdFilter + TenantContextFilter + TenantHeaderProperties + StartupValidator + TenantFilterIntegrationTest | 集成测试覆盖:缺失/冲突/冗余旧 Header/异常清理/prod fail-fast |
| 4 | ArchitectureTest(A 类) + Gitea CI quality-gate Job | 本地 `mvn test` 触发 ArchUnit;Gitea quality-gate 全绿 |
| 5 | Flyway V1/V2 + BaseAuditPO + AuditorAware + Order 示例 CRUD + 集成测试 | 保存 Order 断言 create_user_id / tenant_id 写入正确 |
| 6 | Dockerfile + build-and-deploy Job + deploy repo Kustomize + init.sh | main 分支推送后镜像推送成功;deploy repo 标签更新幂等;init.sh --sample=none 后 CI 全绿 |
---
## 租户 Header 信任边界
`X-Tenant-Id` Header 在 tmpl-api 内部被视为已验证的值。**但服务本身不负责校验
Header 的真实性**。在生产部署中,必须由 API Gateway 或认证层从已验证的
JWT Token 中提取并注入,不得信任客户端直接传入的值。tmpl-api 假设上游已完成校验。
信任链路:Client → API Gateway(校验 JWT → 提取 tenantId → 注入/覆盖 Header)→
tmpl-api(读取 Header → 存入 TenantContext → 附加到所有查询条件)。
详细安全设计参见 [security.md](security.md) 第 2 节。