# 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\ | 是 | — | — | 角色集合 | **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`,从 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,无上下文→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\ | 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 节。