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 <[email protected]>
2026-02-12 21:47:33 +08:00

52 KiB
Raw Permalink Blame History

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 第 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 统一错误响应

{
  "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 结尾

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 第 2 节。