feat: implement batch 1 skeleton for Java service template
- Create parent POM with multi-module structure - Set up four modules: tmpl-domain, tmpl-application, tmpl-infrastructure, tmpl-api - Configure Spring Boot 3.4.1 with Java 21 - Add application configurations for dev/staging/prod - Set up logging with structured JSON output - Configure database with PostgreSQL and Flyway - Add security configuration with minimal setup - Include docker-compose for local development - Add all necessary build and dependency management plugins Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Maven
|
||||
target/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
*.env
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -0,0 +1,20 @@
|
||||
# 本地开发依赖服务
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: tmpl_dev
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
+1
-1
@@ -27,12 +27,12 @@ review_cycle: 90d
|
||||
- [架构总览](02-architecture/overview.md)
|
||||
|
||||
---
|
||||
|
||||
## 🏗 架构与设计
|
||||
|
||||
- [架构总览](02-architecture/overview.md)
|
||||
- [架构约束](02-architecture/constraints.md)
|
||||
- [依赖关系](02-architecture/dependencies.md)
|
||||
- [架构规范](architecture-spec.md)
|
||||
- [ADR 列表](02-architecture/adrs/)
|
||||
|
||||
---
|
||||
|
||||
+768
-184
@@ -1,225 +1,809 @@
|
||||
# 架构规范 v1.0
|
||||
# M2 整体架构(终稿 v5——实施基准·定版)
|
||||
|
||||
## 1. 分层架构
|
||||
## 一、定位与目标
|
||||
|
||||
### 1.1 模块划分
|
||||
M2 是一个可编译、可测试、可部署、具备自动门禁的 Java 21 通用项目模板。它不是业务系统,而是所有新服务的起点。任何团队 fork 后运行 `init.sh --service=xxx`,30 分钟内应能在 dev 环境跑通完整的"建表 → 启动 → CRUD → CI 绿灯 → 镜像推送 → ArgoCD 部署"链路。
|
||||
|
||||
| 模块 | 职责 | 允许的依赖 |
|
||||
|------|------|-----------|
|
||||
| `${name}-domain` | 领域模型、业务规则、领域事件、领域异常 | 仅 JDK 标准库 + Jakarta Validation API |
|
||||
| `${name}-application` | 用例编排、应用服务、端口定义、Command/Query | domain |
|
||||
| `${name}-infrastructure` | 技术实现:JPA、Redis、MQ、HTTP Client | domain + application + Spring 全家桶 |
|
||||
| `${name}-api` | REST Controller、DTO、安全配置、启动入口 | application + infrastructure + Spring Web |
|
||||
M2 包含一个贯穿四层的示例 bounded context(Order),用于验证模板自身的正确性。通过 `init.sh --sample=none` 可清除示例代码,仅保留基础设施骨架,清除后仍可编译、启动、通过 CI、构建镜像。
|
||||
|
||||
模板仓库中模块名使用 tmpl- 前缀(tmpl-domain / tmpl-application /
|
||||
tmpl-infrastructure / tmpl-api),初始化脚本会替换为实际服务名。
|
||||
## 二、技术栈基线(版本锁定规则)
|
||||
|
||||
### 1.2 依赖方向(不可违反)
|
||||
**版本冻结规则**:`spring-boot.version` 在父 POM 中显式锁定,必须是已发布 GA 版本。模板仓库以 POM 中的实际值为唯一版本真相源,文档不声称"当前最新"。团队升级版本时必须在本地全量跑通 CI quality-gate 后方可提交版本变更 PR。
|
||||
|
||||
api → application → domain
|
||||
↑
|
||||
infrastructure ──┘
|
||||
| 组件 | 版本(以 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 |
|
||||
|
||||
- domain 不依赖任何其他模块,不依赖任何框架
|
||||
- application 只依赖 domain
|
||||
- infrastructure 实现 application 中定义的 Port 接口
|
||||
- api 负责组装所有模块(Spring Boot 启动入口在此层)
|
||||
## 三、仓库拓扑(双仓库 GitOps)
|
||||
|
||||
### 1.3 各层允许与禁止的 Spring 注解/类型(精确清单)
|
||||
```
|
||||
┌──────────────────────────────────────┐ ┌──────────────────────────────────┐
|
||||
│ java-service-template │ │ java-service-template-deploy │
|
||||
│ (应用仓库) │ │ (部署仓库) │
|
||||
│ │ │ │
|
||||
│ 源码 + Gitea CI │ │ Kustomize 清单 │
|
||||
│ quality-gate: 测试/格式/质量 │ │ base/ + overlays/dev|staging|prod│
|
||||
│ build-and-deploy: │ │ │
|
||||
│ 构建镜像 → 推送 Harbor │─────▶│ CI 更新镜像标签 │
|
||||
│ 更新部署仓库镜像标签 │ │ ArgoCD 监听 → 自动同步 K8s │
|
||||
└──────────────────────────────────────┘ └──────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### domain 层
|
||||
两个仓库通过镜像标签(git short SHA,7 位)建立唯一关联。应用仓库的 CI build-and-deploy Job 通过幂等脚本 clone 部署仓库,用 `yq` 精确替换对应 service + env 的镜像标签后 commit push。部署仓库无 CI workflow,仅由 ArgoCD 监听,不存在循环触发。
|
||||
|
||||
允许:无(零框架依赖)
|
||||
禁止:所有 Spring 注解、所有 JPA 注解、所有第三方框架类型
|
||||
## 四、四层模块架构与依赖方向
|
||||
|
||||
#### application 层
|
||||
```
|
||||
依赖方向:外 → 内,严格单向
|
||||
|
||||
允许:
|
||||
- @Service / @Component(类级别标注,用于 Spring Bean 注册)
|
||||
- @Transactional(此层是事务声明的【唯一】位置)
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 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,零框架依赖 │ │
|
||||
└──────────────────────────────┘ │
|
||||
────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
禁止:
|
||||
- Spring MVC 类型:RestController, RequestMapping, HttpServletRequest...
|
||||
- JPA 类型:JpaRepository, EntityManager, @Entity, @Table, @Column...
|
||||
- Cache 类型: @Cacheable, @CacheEvict, RedisTemplate...
|
||||
- Messaging 类型:KafkaTemplate, RabbitTemplate, @RabbitListener...
|
||||
- Security 类型: @PreAuthorize, SecurityFilterChain...
|
||||
- HTTP Client 类型:RestTemplate, WebClient, FeignClient...
|
||||
### 4.1 各层依赖铁律(ArchUnit 强制执行)
|
||||
|
||||
#### infrastructure 层
|
||||
**domain 层**:不依赖任何上层模块。禁止出现 Spring、JPA、Hibernate、Jakarta Persistence 的 import。允许 Java 标准库 + Jakarta Validation 注解(仅用于 ValueObject/Entity 的构造与不可变约束表达,禁止注入 `jakarta.validation.Validator` 或任何运行时校验器,业务错误仍通过 `DomainException(ErrorCode)` 抛出)。
|
||||
|
||||
允许:Spring 全家桶、JPA、Redis、MQ 等所有技术框架
|
||||
禁止:包含业务规则判断(业务逻辑只能在 domain 层)
|
||||
**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`)。
|
||||
|
||||
#### api 层
|
||||
**infrastructure 层**:依赖 application + domain。实现 application 定义的 Port 接口。不声明 ` @Transactional`(事务由 application 层控制)。
|
||||
|
||||
允许:Spring MVC、Spring Security 配置、SpringDoc
|
||||
禁止:直接调用 Repository/Adapter(必须经过 application 层的 UseCase)
|
||||
**api 层**:依赖 infrastructure + application + domain。是唯一的 Spring Boot 启动入口。Controller 禁止直接依赖 infrastructure 中的具体实现类,只通过 application 层的 UseCase 接口交互。禁止字段注入(` @Autowired` on field),必须使用构造器注入。
|
||||
|
||||
### 1.4 包命名规范
|
||||
## 五、多租户隔离模型
|
||||
|
||||
com.{company}.{service}/
|
||||
├── domain/
|
||||
│ ├── entity/ # 聚合根、实体
|
||||
│ ├── valueobject/ # 值对象(推荐用 Java 21 record)
|
||||
│ ├── event/ # 领域事件
|
||||
│ ├── exception/ # DomainException + ErrorCode
|
||||
│ ├── repository/ # 仓储接口(只定义,不实现)
|
||||
│ └── service/ # 领域服务接口(可选)
|
||||
├── application/
|
||||
│ ├── usecase/ # 用例类(一个类做一件事,入口方法 execute())
|
||||
│ ├── port/
|
||||
│ │ ├── in/ # 入站:Command, Query
|
||||
│ │ └── out/ # 出站:PersistencePort, EventPublisher, ExternalServicePort
|
||||
│ ├── service/ # 应用服务(编排多个 UseCase,可选)
|
||||
│ └── assembler/ # Domain ↔ Command 转换(或 MapStruct)
|
||||
├── infrastructure/
|
||||
│ ├── persistence/
|
||||
│ │ ├── entity/ # JPA @Entity(PO,持久化对象)
|
||||
│ │ ├── repository/ # Spring Data JPA Repository + PersistenceAdapter
|
||||
│ │ ├── mapper/ # MapStruct: PO ↔ Domain Entity
|
||||
│ │ └── converter/ # JPA AttributeConverter
|
||||
│ ├── cache/ # Redis 适配器(实现 out port)
|
||||
│ ├── messaging/ # MQ 适配器(按需,实现 out port)
|
||||
│ ├── external/ # 外部 HTTP 调用适配器(按需,实现 out port)
|
||||
│ └── config/ # Spring @Configuration 类
|
||||
└── api/
|
||||
├── controller/ # REST Controller
|
||||
├── dto/ # Request/Response DTO(面向前端/外部调用方)
|
||||
├── mapper/ # MapStruct: DTO ↔ Command/Response
|
||||
├── filter/ # Servlet Filter(TraceId、SecurityContext 注入)
|
||||
├── exception/ # GlobalExceptionHandler
|
||||
└── config/ # Web / Security / OpenAPI 配置
|
||||
### 5.1 字段命名与类型(冻结)
|
||||
|
||||
### 1.5 命名规范
|
||||
模板内部统一使用 `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\> | 是 | — | — | 角色集合 |
|
||||
|
||||
| 层级 | 类型 | 规则 | 示例 |
|
||||
|------|------|------|------|
|
||||
| domain | 聚合根/实体 | 业务名词 | Order, OrderItem |
|
||||
| domain | 值对象 | 业务名词 | Money, OrderStatus |
|
||||
| domain | 异常 | {名词}+Exception | OrderNotFoundException |
|
||||
| domain | 仓储接口 | {聚合根}+Repository | OrderRepository |
|
||||
| application | 用例 | {动词}{名词}+UseCase | CreateOrderUseCase |
|
||||
| application | 命令 | {动词}{名词}+Command | CreateOrderCommand |
|
||||
| application | 查询 | {名词}+Query | OrderDetailQuery |
|
||||
| infrastructure | 持久化对象 | {名词}+PO | OrderPO |
|
||||
| infrastructure | JPA 仓储 | {名词}+JpaRepository | OrderJpaRepository |
|
||||
| infrastructure | 适配器 | {名词}{技术}+Adapter | OrderPersistenceAdapter |
|
||||
| api | 控制器 | {名词}+Controller | OrderController |
|
||||
| api | 请求 DTO | {动词}{名词}+Request | CreateOrderRequest |
|
||||
| api | 响应 DTO | {名词}+Response | OrderResponse |
|
||||
**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"的脏值。
|
||||
|
||||
- UseCase 入口方法统一命名 execute(...)
|
||||
- Repository 方法:findById / save / delete / findAllBy{条件}
|
||||
- Controller 方法:create / update / delete / get / list
|
||||
可选扩展字段(不进 M2 基线,预留位置):requestId、clientId/appId。
|
||||
|
||||
### 1.6 API 规范
|
||||
### 5.2 Header 契约(冻结)
|
||||
|
||||
#### URL 规范
|
||||
- 格式:/api/v{n}/{resource}(resource 用复数)
|
||||
- 示例:POST /api/v1/orders,GET /api/v1/orders/{id}
|
||||
- 嵌套资源最多一层:GET /api/v1/orders/{id}/items
|
||||
| 标准 Header | 旧 Header(兼容期) | 说明 |
|
||||
|---|---|---|
|
||||
| X-Tenant-Id | X-Customer-Id | 由网关注入 |
|
||||
| X-Dept-Id | X-Org-Id | 由网关注入 |
|
||||
| X-User-Id | — | 由网关注入,必须为合法数字 |
|
||||
| X-User-Roles | — | 逗号分隔 |
|
||||
|
||||
#### HTTP Method 语义
|
||||
### 5.3 兼容窗口与失败策略(冻结)
|
||||
|
||||
| 操作 | Method | 幂等 | 成功状态码 |
|
||||
|------|--------|------|-----------|
|
||||
| 创建 | POST | 否(除非带幂等键) | 201 Created |
|
||||
| 查询单个 | GET | 是 | 200 OK |
|
||||
| 查询列表 | GET | 是 | 200 OK |
|
||||
| 全量更新 | PUT | 是 | 200 OK |
|
||||
| 部分更新 | PATCH | 是 | 200 OK |
|
||||
| 删除 | DELETE | 是 | 204 No Content |
|
||||
兼容行为由配置项 `app.security.header-compat` 控制,代码默认值为 `false`。
|
||||
|
||||
#### 分页规范
|
||||
- 请求参数:?page=0&size=20&sort=createTime,desc
|
||||
- 响应:使用 Spring Data 的 Page 结构(content, totalElements, totalPages, number, size)
|
||||
**配置出现规则**:仅 `application-dev.yml` 中设置 `app.security.header-compat: true`。`application-staging.yml` 中不出现(按需临时开启时可添加)。`application-prod.yml` 中不出现。
|
||||
|
||||
### 1.7 数据库规范
|
||||
**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` 值。
|
||||
|
||||
#### 表命名
|
||||
- 小写蛇形,t_ 前缀:t_order, t_order_item
|
||||
- 初始化脚本提供 --no-table-prefix 开关(非默认)
|
||||
| 环境 | 配置文件中是否出现 | 生效值 | 行为 |
|
||||
|---|---|---|---|
|
||||
| dev | 是(true) | true | 缺新 Header 时回退旧 Header,输出 WARN + 标识 `LEGACY_HEADER_FALLBACK` |
|
||||
| staging | 默认不出现 | false(除非临时开启) | 同 prod 行为 |
|
||||
| prod | 不出现 | false(启动时强制校验,环境变量也无法覆盖) | 不回退 |
|
||||
|
||||
#### 审计列标准(每张业务表必须包含)
|
||||
**失败策略——Header 解析优先级(冻结)**:
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| id | BIGINT (主键) 或 UUID | 自增/生成 | 主键 |
|
||||
| create_user_id | BIGINT | - | 创建人 ID |
|
||||
| create_time | TIMESTAMPTZ | CURRENT_TIMESTAMP | 创建时间(UTC) |
|
||||
| update_user_id | BIGINT | - | 最后修改人 ID |
|
||||
| update_time | TIMESTAMPTZ | CURRENT_TIMESTAMP | 最后修改时间(UTC) |
|
||||
| is_deleted | BOOLEAN | FALSE | 软删除标记 |
|
||||
| version | INTEGER | 0 | 乐观锁版本号 |
|
||||
TenantContextFilter 按以下优先级顺序处理,命中即停:
|
||||
|
||||
说明:统一使用 TIMESTAMPTZ(PostgreSQL 带时区时间戳),存储 UTC 时间。
|
||||
| 优先级 | 场景 | 结果 | 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 |
|
||||
|
||||
#### 索引命名
|
||||
- 普通索引:idx_{表名}_{字段名}
|
||||
- 唯一索引:uk_{表名}_{字段名}
|
||||
- 示例:idx_t_order_user_id, uk_t_order_order_no
|
||||
失败时不创建 TenantContext,不进入后续链路,直接返回统一错误格式的响应。
|
||||
|
||||
#### Flyway 脚本命名
|
||||
- V{版本}__{描述}.sql:结构变更(DDL)
|
||||
- R__{描述}.sql:可重复执行脚本
|
||||
- 版本号递增不跳号:V1__, V2__, V3__
|
||||
### 5.4 deptId=0 的权限语义(冻结)
|
||||
|
||||
### 1.8 Non-goals(本阶段不做,但预留扩展点)
|
||||
deptId=0 表示"跨部门查询",是权限决策而非查询便利。网关在鉴权时判断用户角色,仅持有 `SCOPE_ALL_DEPT`(或等价 TENANT_ADMIN)权限的用户,网关才注入 `X-Dept-Id: 0`。普通用户的 X-Dept-Id 只能是其被授权的部门 ID。
|
||||
|
||||
- app-bootstrap 独立启动模块(当出现 REST + gRPC + CLI 多入口时再拆)
|
||||
- 多数据源 / 读写分离
|
||||
- gRPC 支持
|
||||
- 消息队列集成(预留 infrastructure/messaging 包)
|
||||
模板层面: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 类中是否出现这些字段名。
|
||||
|
||||
## 六、审计列实现(冻结)
|
||||
|
||||
采用 **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` |
|
||||
| Request DTO 禁止 tenantId/deptId/userId | 扫描 `..api.dto..*Request` 类的字段名 |
|
||||
|
||||
### 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 全绿 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 可自动检查项(ArchUnit + Maven Enforcer)
|
||||
|
||||
| 规则 | 检查方式 |
|
||||
|------|---------|
|
||||
| domain 不依赖 Spring/JPA | ArchUnit: noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAnyPackage("org.springframework..", "jakarta.persistence..", "org.hibernate..") |
|
||||
| application 不依赖 JPA/MVC/Cache/MQ | ArchUnit: 同上,精确到禁止包列表 |
|
||||
| Controller 不直接调用 Repository | ArchUnit: noClasses().that().areAnnotatedWith(RestController.class).should().dependOnClassesThat().resideInAPackage("..infrastructure..") |
|
||||
| UseCase 入口方法必须名为 execute | ArchUnit: methods().that().areDeclaredInClassesThat().haveSimpleNameEndingWith("UseCase").should().haveName("execute") |
|
||||
| 禁止 @Autowired 字段注入 | ArchUnit: noFields().should().beAnnotatedWith(Autowired.class) |
|
||||
| domain 模块 pom.xml 无 Spring 依赖 | Maven Enforcer Plugin: bannedDependencies |
|
||||
|
||||
## ❌ 典型违规示例
|
||||
|
||||
### 违规 1:domain 层引入 JPA 注解
|
||||
|
||||
// 错误!domain 层禁止 JPA 注解
|
||||
@Entity @Table(name = "t_order")
|
||||
public class Order { ... }
|
||||
|
||||
// 正确:domain 层的 Order 是纯 Java 类
|
||||
// JPA @Entity 放在 infrastructure 层的 OrderPO 上
|
||||
|
||||
### 违规 2:Controller 直接调用 Repository
|
||||
|
||||
// 错误!Controller 绕过 UseCase 直接调用 Infrastructure
|
||||
@RestController
|
||||
public class OrderController {
|
||||
private final OrderJpaRepository repo; // 直接依赖 infra
|
||||
|
||||
@GetMapping("/orders/{id}")
|
||||
public Order get(@PathVariable Long id) {
|
||||
return repo.findById(id); // 违规
|
||||
}
|
||||
}
|
||||
|
||||
// 正确:Controller 调用 UseCase
|
||||
@RestController
|
||||
public class OrderController {
|
||||
private final QueryOrderUseCase queryOrderUseCase; // 依赖 application
|
||||
|
||||
@GetMapping("/orders/{id}")
|
||||
public ApiResponse<OrderResponse> get(@PathVariable Long id) {
|
||||
return ApiResponse.success(queryOrderUseCase.execute(id));
|
||||
}
|
||||
}
|
||||
以上是 M2 整体架构终稿 v5,已合并全部评审轮次的修正。文档定版,进入逐批实现。
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||
<!-- 在此添加误报抑制规则 -->
|
||||
</suppressions>
|
||||
@@ -0,0 +1,281 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>java-service-template</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>java-service-template</name>
|
||||
<description>Java 21 通用项目模板 - M2 实施基准</description>
|
||||
|
||||
<modules>
|
||||
<module>tmpl-domain</module>
|
||||
<module>tmpl-application</module>
|
||||
<module>tmpl-infrastructure</module>
|
||||
<module>tmpl-api</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<!-- ==================== 语言 ==================== -->
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- ==================== 依赖版本 ==================== -->
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<springdoc.version>2.8.3</springdoc.version>
|
||||
<logstash-logback.version>8.0</logstash-logback.version>
|
||||
<archunit.version>1.3.0</archunit.version>
|
||||
<testcontainers.version>1.20.4</testcontainers.version>
|
||||
<git-commit-id.version>9.0.1</git-commit-id.version>
|
||||
|
||||
<!-- ==================== 插件版本 ==================== -->
|
||||
<spotless.version>2.43.0</spotless.version>
|
||||
<google-java-format.version>1.24.0</google-java-format.version>
|
||||
<spotbugs-maven.version>4.8.6.6</spotbugs-maven.version>
|
||||
<owasp-dependency-check.version>11.1.1</owasp-dependency-check.version>
|
||||
<jacoco.version>0.8.12</jacoco.version>
|
||||
|
||||
<!-- ==================== 门禁阈值与开关 ==================== -->
|
||||
<jacoco.minimum.coverage>0.70</jacoco.minimum.coverage>
|
||||
|
||||
<!--
|
||||
默认全跳过:保证批次1/本地快速验证不被覆盖率、漏洞库下载等阻断。
|
||||
CI quality-gate 会显式 -Dxxx.skip=false 打开门禁。
|
||||
-->
|
||||
<jacoco.skip>true</jacoco.skip>
|
||||
<spotbugs.skip>true</spotbugs.skip>
|
||||
<dependency-check.skip>true</dependency-check.skip>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- ===== 内部模块 ===== -->
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-domain</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-infrastructure</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ===== 第三方 ===== -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
<version>${logstash-logback.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<version>${archunit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Testcontainers BOM -->
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<!-- ===== 全局生效插件(显式声明,确保吃到 pluginManagement 配置) ===== -->
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<!-- ===== Maven Compiler(MapStruct 处理器) ===== -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>-Amapstruct.defaultComponentModel=spring</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- ===== Spotless 代码格式化 ===== -->
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>${spotless.version}</version>
|
||||
<configuration>
|
||||
<java>
|
||||
<googleJavaFormat>
|
||||
<version>${google-java-format.version}</version>
|
||||
<style>AOSP</style>
|
||||
</googleJavaFormat>
|
||||
<removeUnusedImports/>
|
||||
<importOrder>
|
||||
<order>com.ljwx,com,org,javax,jakarta,java</order>
|
||||
</importOrder>
|
||||
</java>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- ===== JaCoCo 覆盖率 ===== -->
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco.version}</version>
|
||||
<configuration>
|
||||
<skip>${jacoco.skip}</skip>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<phase>test</phase>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>LINE</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>${jacoco.minimum.coverage}</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- ===== SpotBugs ===== -->
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
<version>${spotbugs-maven.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>spotbugs-check</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<skip>${spotbugs.skip}</skip>
|
||||
<effort>Max</effort>
|
||||
<threshold>Medium</threshold>
|
||||
<failOnError>true</failOnError>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- ===== OWASP Dependency Check ===== -->
|
||||
<plugin>
|
||||
<groupId>org.owasp</groupId>
|
||||
<artifactId>dependency-check-maven</artifactId>
|
||||
<version>${owasp-dependency-check.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>dependency-check</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<skip>${dependency-check.skip}</skip>
|
||||
<failBuildOnCVSS>7</failBuildOnCVSS>
|
||||
<suppressionFiles>
|
||||
<!-- 多模块下使用根目录绝对引用,确保子模块能找到 -->
|
||||
<suppressionFile>${maven.multiModuleProjectDirectory}/owasp-suppressions.xml</suppressionFile>
|
||||
</suppressionFiles>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- ===== git-commit-id ===== -->
|
||||
<plugin>
|
||||
<groupId>io.github.git-commit-id</groupId>
|
||||
<artifactId>git-commit-id-maven-plugin</artifactId>
|
||||
<version>${git-commit-id.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
||||
<failOnNoGitDirectory>false</failOnNoGitDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>java-service-template</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tmpl-api</artifactId>
|
||||
<name>tmpl-api</name>
|
||||
<description>API 层 - 启动入口 / Controller / Filter / 配置 / 测试</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 内部模块 -->
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-infrastructure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Flyway -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL Driver -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- API 文档 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结构化日志 -->
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Micrometer Prometheus -->
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ===== 测试 ===== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Spring Boot 打包 + build-info -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-info</goal>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- git-commit-id -->
|
||||
<plugin>
|
||||
<groupId>io.github.git-commit-id</groupId>
|
||||
<artifactId>git-commit-id-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<!-- Spotless -->
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<!-- JaCoCo -->
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<!-- SpotBugs -->
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<!-- OWASP -->
|
||||
<plugin>
|
||||
<groupId>org.owasp</groupId>
|
||||
<artifactId>dependency-check-maven</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ljwx.tmpl.api;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* Spring Boot 启动入口。
|
||||
*
|
||||
* <p>批次 1:骨架启动,SecurityConfig 全放行(permitAll)。
|
||||
* 鉴权由网关负责,服务内 scope 校验在批次 3 实现。
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = "com.ljwx.tmpl")
|
||||
@EntityScan(basePackages = "com.ljwx.tmpl.infrastructure.persistence")
|
||||
@EnableJpaRepositories(basePackages = "com.ljwx.tmpl.infrastructure.persistence")
|
||||
public class TmplApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TmplApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ljwx.tmpl.api.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* 最小化安全配置。
|
||||
*
|
||||
* <p>网关已完成鉴权,服务内仅做 scope validation(批次 3 实现)。
|
||||
* 当前批次为骨架启动,permitAll 放行所有请求,避免本地/CI 被安全链路阻断。
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/tmpl_dev
|
||||
username: postgres
|
||||
password: postgres
|
||||
jpa:
|
||||
show-sql: true
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
|
||||
app:
|
||||
security:
|
||||
header-compat: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.ljwx.tmpl: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
@@ -0,0 +1,14 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/${DB_NAME}
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASS}
|
||||
jpa:
|
||||
show-sql: false
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST}
|
||||
port: ${REDIS_PORT:6379}
|
||||
|
||||
# header-compat 不出现 -> 使用 application.yml 默认值 false
|
||||
# prod + header-compat=true 时 TenantHeaderStartupValidator 将 fail fast(批次 3 实现)
|
||||
@@ -0,0 +1,11 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:tmpl_staging}
|
||||
username: ${DB_USER:postgres}
|
||||
password: ${DB_PASS:postgres}
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
|
||||
# header-compat 不出现 -> 使用 application.yml 默认值 false
|
||||
@@ -0,0 +1,66 @@
|
||||
spring:
|
||||
application:
|
||||
name: tmpl-service
|
||||
|
||||
threads:
|
||||
virtual:
|
||||
enabled: true
|
||||
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:tmpl_dev}
|
||||
username: ${DB_USER:postgres}
|
||||
password: ${DB_PASS:postgres}
|
||||
|
||||
jpa:
|
||||
open-in-view: false
|
||||
hibernate:
|
||||
# 批次 1 无业务实体,使用 none 避免 validate 报错
|
||||
# 批次 5 引入 PO 后切换为 validate
|
||||
ddl-auto: none
|
||||
properties:
|
||||
hibernate:
|
||||
default_schema: public
|
||||
jdbc:
|
||||
time_zone: UTC
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: false
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
|
||||
lifecycle:
|
||||
timeout-per-shutdown-phase: 25s
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,prometheus,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
probes:
|
||||
enabled: true
|
||||
info:
|
||||
git:
|
||||
mode: full
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
shutdown: graceful
|
||||
|
||||
app:
|
||||
security:
|
||||
header-compat: false
|
||||
observability:
|
||||
slow-query-threshold-ms: 200
|
||||
@@ -0,0 +1,14 @@
|
||||
-- M2 基础初始化脚本
|
||||
-- sample=none 时保留此文件,确保 Flyway 启动不报错
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_app_meta (
|
||||
key VARCHAR(128) PRIMARY KEY,
|
||||
value VARCHAR(1024) NOT NULL,
|
||||
create_time TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
update_time TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE t_app_meta IS '应用元数据(模板基础表)';
|
||||
|
||||
INSERT INTO t_app_meta (key, value) VALUES ('schema_version', '1')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<springProperty scope="context" name="APP_NAME"
|
||||
source="spring.application.name" defaultValue="tmpl-service"/>
|
||||
|
||||
<!-- ===== dev 环境:人类可读 ===== -->
|
||||
<springProfile name="dev">
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-}] [%X{tenantId:-}] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</springProfile>
|
||||
|
||||
<!-- ===== 非 dev 环境:结构化 JSON ===== -->
|
||||
<springProfile name="!dev">
|
||||
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||
<customFields>{"app":"${APP_NAME}"}</customFields>
|
||||
<includeMdcKeyName>traceId</includeMdcKeyName>
|
||||
<includeMdcKeyName>tenantId</includeMdcKeyName>
|
||||
<includeMdcKeyName>deptId</includeMdcKeyName>
|
||||
<includeMdcKeyName>userId</includeMdcKeyName>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="JSON_CONSOLE"/>
|
||||
</root>
|
||||
</springProfile>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>java-service-template</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tmpl-application</artifactId>
|
||||
<name>tmpl-application</name>
|
||||
<description>应用层 - UseCase / Port / Context / 事务声明</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-domain</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Context(DI)+ TX(事务) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 应用层 - UseCase / Port / Context / 事务声明。
|
||||
*/
|
||||
package com.ljwx.tmpl.application;
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>java-service-template</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tmpl-domain</artifactId>
|
||||
<name>tmpl-domain</name>
|
||||
<description>领域层 - 纯 Java,零框架依赖</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Jakarta Validation 注解(仅注解,不引入运行时校验器) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 领域层 - 纯 Java,零框架依赖。
|
||||
*/
|
||||
package com.ljwx.tmpl.domain;
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>java-service-template</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tmpl-infrastructure</artifactId>
|
||||
<name>tmpl-infrastructure</name>
|
||||
<description>基础设施层 - JPA / Redis / 外部适配器</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-application</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 基础设施层 - JPA / Redis / 外部适配器。
|
||||
*/
|
||||
package com.ljwx.tmpl.infrastructure;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 持久化子包 - EntityScan / EnableJpaRepositories 扫描路径。
|
||||
* 批次 5 在此包下创建 entity / order.internal / order.adapter 等子包。
|
||||
*/
|
||||
package com.ljwx.tmpl.infrastructure.persistence;
|
||||
Reference in New Issue
Block a user