From afb867070d2e610756a6d58e8ca83cd7fe982585 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Thu, 4 Jun 2026 19:51:35 +0800 Subject: [PATCH 01/10] fix gitignore --- .claude/settings.json | 7 +++++++ .gitignore | 7 +++++++ .../springboot/framework/event/EventTraceContext.java | 1 - 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..19959dfbb --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(echo \"exit: $?\")" + ] + } +} diff --git a/.gitignore b/.gitignore index 5c67716d3..adf1f08ad 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,12 @@ build/ *.db *.trace.db +### macOS ### +.DS_Store + ### jars ### /jars/ + + +### qwen ### +.qwen/ \ No newline at end of file diff --git a/springboot-starter/src/main/java/com/codingapi/springboot/framework/event/EventTraceContext.java b/springboot-starter/src/main/java/com/codingapi/springboot/framework/event/EventTraceContext.java index 6088234ce..4ed131712 100644 --- a/springboot-starter/src/main/java/com/codingapi/springboot/framework/event/EventTraceContext.java +++ b/springboot-starter/src/main/java/com/codingapi/springboot/framework/event/EventTraceContext.java @@ -1,5 +1,4 @@ package com.codingapi.springboot.framework.event; - import com.codingapi.springboot.framework.exception.EventLoopException; import com.codingapi.springboot.framework.utils.RandomGenerator; import lombok.Getter; From c9e98d992dd74edf336029252669541ef20cc1c6 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Sat, 6 Jun 2026 16:32:05 +0800 Subject: [PATCH 02/10] add CLAUDE.md for Claude Code guidance Add project documentation covering build commands, module architecture, core patterns (event system, unified response, dynamic query, SQL interception, workflow engine), and DDD example structure. Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..8be15d3f0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,156 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +springboot-framework 是一个基于 Spring Boot 3.3.5(Java 17)的多模块企业级开发框架,由 CodingApi 团队维护。核心定位是为领域驱动设计(DDD)与事件风暴提供开箱即用的基础设施,当前版本为 `3.4.54`。 + +**回复语言**:请使用中文进行回答。 + +## 常用命令 + +```bash +# 全量构建 +./mvnw clean install + +# 仅运行测试(CI 使用) +./mvnw clean test -P travis + +# 运行单个模块的测试 +./mvnw test -pl springboot-starter +./mvnw test -pl springboot-starter-flow +./mvnw test -pl springboot-starter-security + +# 运行单个测试类 +./mvnw test -pl springboot-starter -Dtest=SomeTestClass + +# 运行 example 示例应用(端口 8090,H2 文件数据库) +./mvnw spring-boot:run -pl example/example-server +``` + +### 前端(admin-ui / mobile-ui) + +```bash +cd admin-ui # 或 mobile-ui +npm install +npm run dev # 开发模式(代理后端) +npm run mock # Mock 模式(前端 mock 数据) +npm run build # 生产构建 +npm test # Jest 测试 +``` + +- admin-ui 基于 React 18 + Ant Design 5 + Ant Design Pro Components + Rsbuild +- mobile-ui 基于 React 18 + Ant Design Mobile 5 + Rsbuild +- 两者都使用 Module Federation 做微前端集成 + +## 模块架构 + +### Starter 模块(框架核心) + +| 模块 | 职责 | +|------|------| +| `springboot-starter` | 核心基础:统一 Response DTO、分页 PageRequest/Filter、事件系统、国际化异常、事务管理 | +| `springboot-starter-security` | Spring Security + JWT 无状态认证 / Redis 有状态认证,加解密支持 | +| `springboot-starter-data-fast` | JPA 增强:FastRepository 支持动态过滤查询、HQL 构建、SearchRequest | +| `springboot-starter-data-authorization` | 数据权限:通过 JDBC Connection/Statement 代理拦截 SQL,透明注入权限条件 | +| `springboot-starter-flow` | 工作流引擎:流程定义、节点流转、审批、委托、会签、数据快照、事件通知 | +| `springboot-starter-script` | Groovy 脚本引擎:运行时编译、LRU 缓存、热更新,提供 REST API | + +### 自动配置注册 + +各 starter 模块同时维护 `META-INF/spring.factories`(兼容旧版)和 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`(Spring Boot 3.x 标准)。新增 starter 时需同时注册这两种文件。 + +### 模块间依赖关系 + +``` +springboot-starter (核心,无外部依赖) + ├── springboot-starter-security (依赖 starter) + ├── springboot-starter-data-fast (依赖 starter) + ├── springboot-starter-data-authorization (依赖 starter + JSqlParser) + ├── springboot-starter-flow (依赖 starter) + └── springboot-starter-script (依赖 starter + Groovy) +``` + +## 核心架构模式 + +### 事件系统(Event System) + +框架自建的发布-订阅事件机制,是 DDD 领域事件的核心基础设施: + +- **`IEvent`** — 事件标记接口,分为 `ISyncEvent`(同步)和 `IAsyncEvent`(异步) +- **`IHandler`** — 事件处理器,支持 `order()` 排序和 `error()` 异常回调 +- **`EventPusher.push(event, sync)`** — 事件推送入口 +- **`DomainEvent`** — 领域实体事件基类,派生 `DomainCreateEvent`、`DomainChangeEvent`、`DomainDeleteEvent` +- **`DomainChangeInterceptor`** — 通过代理拦截实体字段变更,自动推送 `DomainChangeEvent` + +Handler 通过 Spring Bean 自动注册,由 `HandlerBeanDefinitionRegistrar` 扫描所有 `IHandler` 实现并注册到 `ApplicationHandlerUtils`。 + +### 统一响应封装 + +```java +Response // 基础响应(success / errCode / errMessage) +SingleResponse // 单对象响应 +MultiResponse // 列表响应 +MapResponse // Map 响应 +``` + +### 数据查询(PageRequest + FastRepository) + +`PageRequest` 扩展了 Spring Data 的 `PageRequest`,增加了 `RequestFilter` 动态过滤条件: + +```java +PageRequest request = PageRequest.of(0, 20); +request.addFilter("name", "张三"); +request.addFilter("age", Relation.GT, 18); +Page page = userRepository.findAll(request); +``` + +`FastRepository.findAll(PageRequest)` 自动根据 Filter 构建 Example 或 HQL 查询。 + +### 数据权限(SQL 拦截) + +`ConnectionProxy` → `PreparedStatementProxy` / `StatementProxy` 代理链,在 SQL 执行前通过 `SQLRunningContext.intercept(sql)` 注入权限条件,实现透明的行级数据过滤。使用 JSqlParser 解析和改写 SQL。 + +### 工作流引擎 + +核心流程:`FlowWork`(流程定义)→ `FlowNode`(节点)→ `FlowRecord`(审批记录)→ `FlowBackup`(版本快照)。发起流程通过 `FlowStartService.startFlow()` 驱动,审批通过 `FlowNodeService` 逐节点流转,每个状态变更推送 `FlowApprovalEvent`。 + +## DDD 示例项目结构(example 模块) + +``` +example/ + example-server — Spring Boot 启动入口 + example-interface — 接口层:Controller(API)、Handler(事件处理)、Runner + example-app — 应用层 + example-app-query — 查询服务(CQRS Query 侧) + example-app-cmd-domain — 命令服务(CQRS Command 侧,领域编排) + example-app-cmd-meta — 元数据命令服务 + example-domain — 领域层 + example-domain-leave — 请假领域(Entity、Repository、Service) + example-domain-user — 用户领域(Entity、Repository、Service、Event、Gateway) + example-infra — 基础设施层 + example-infra-jpa — JPA 持久化实现 + example-infra-flow — 工作流集成 + example-infra-security — 安全配置 +``` + +遵循 DDD 分层依赖规则:`interface → app → domain ← infra`。domain 层定义 Repository 接口,infra 层提供实现。 + +## 框架配置项 + +在 `application.properties` 中可用的配置前缀: + +```properties +# 安全模块 +codingapi.security.jwt.enable=true # 启用 JWT 认证 +codingapi.security.redis.enable=true # 启用 Redis 有状态认证 +codingapi.security.ignore-urls=/open/**,/#/** # 免认证 URL 列表 + +# 框架核心 +codingapi.framework.handler-thread-pool-size=20 # 事件异步线程池大小 +``` + +## 关键依赖版本 + +见根 `pom.xml` 的 `` 区块。主要版本:Groovy 4.0.24、JSqlParser 5.0、Fastjson 2.0.53、JJWT 0.12.6、H2 2.3.232、Kryo 5.6.2。 From cb06e5a3e52efcb82a3dae41deee035f8e1823b3 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Sat, 6 Jun 2026 16:33:24 +0800 Subject: [PATCH 03/10] update .gitignore to include .codegraph directory --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index adf1f08ad..7aa04dbf4 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ build/ ### qwen ### -.qwen/ \ No newline at end of file +.qwen/ + +### codegraph ### +.codegraph \ No newline at end of file From 1be6d860a43be8d5e2c1b167e3a1aa8cc2afe93b Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Sat, 6 Jun 2026 17:30:33 +0800 Subject: [PATCH 04/10] ignore serene --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7aa04dbf4..9ece17937 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ build/ .qwen/ ### codegraph ### -.codegraph \ No newline at end of file +.codegraph + +### serena ### +.serena \ No newline at end of file From b3bdc37f3c90dbe059d90dff8131663a86dd4b9e Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 8 Jun 2026 17:04:00 +0800 Subject: [PATCH 05/10] =?UTF-8?q?docs:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20PKR?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E6=B3=A8=E5=86=8C=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 30 篇能力文档(13 个项目自有 + 17 个三方框架)和 8 篇规范文档(4 个后端 + 4 个前端), 在 CLAUDE.md 中追加 PKR 知识查阅约束。 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 27 ++ docs/capabilities/apache-groovy.md | 86 +++++ docs/capabilities/bouncycastle.md | 128 ++++++++ docs/capabilities/byte-buddy.md | 136 ++++++++ docs/capabilities/command-executor.md | 279 +++++++++++++++++ docs/capabilities/commons-crypto.md | 110 +++++++ docs/capabilities/commons-io.md | 100 ++++++ docs/capabilities/commons-text.md | 87 ++++++ docs/capabilities/data-authorization.md | 206 ++++++++++++ docs/capabilities/domain-event.md | 202 ++++++++++++ docs/capabilities/event-system.md | 211 +++++++++++++ docs/capabilities/exception-handling.md | 254 +++++++++++++++ docs/capabilities/fast-repository.md | 213 +++++++++++++ docs/capabilities/fastjson.md | 121 +++++++ docs/capabilities/groovy-script-engine.md | 237 ++++++++++++++ docs/capabilities/http-client-factory.md | 165 ++++++++++ docs/capabilities/httpclient5.md | 119 +++++++ docs/capabilities/index.md | 42 +++ docs/capabilities/jackson-databind.md | 124 ++++++++ docs/capabilities/jjwt.md | 146 +++++++++ docs/capabilities/jsqlparser.md | 78 +++++ docs/capabilities/kryo.md | 87 ++++++ docs/capabilities/page-request-filter.md | 219 +++++++++++++ docs/capabilities/rest-template-context.md | 113 +++++++ docs/capabilities/security-auth.md | 263 ++++++++++++++++ docs/capabilities/spring-data-jpa.md | 77 +++++ docs/capabilities/spring-data-redis.md | 58 ++++ docs/capabilities/spring-ioc-container.md | 85 +++++ docs/capabilities/spring-security.md | 66 ++++ docs/capabilities/spring-web-mvc.md | 109 +++++++ docs/capabilities/unified-response.md | 208 ++++++++++++ docs/capabilities/workflow-engine.md | 243 +++++++++++++++ docs/conventions/ddd-layered-architecture.md | 182 +++++++++++ docs/conventions/event-handler-pattern.md | 142 +++++++++ docs/conventions/frontend-css-variables.md | 194 ++++++++++++ docs/conventions/frontend-http-client.md | 212 +++++++++++++ .../conventions/frontend-module-federation.md | 295 ++++++++++++++++++ docs/conventions/frontend-state-management.md | 286 +++++++++++++++++ docs/conventions/global-exception-handling.md | 178 +++++++++++ docs/conventions/index.md | 20 ++ .../unified-response-convention.md | 161 ++++++++++ 41 files changed, 6269 insertions(+) create mode 100644 docs/capabilities/apache-groovy.md create mode 100644 docs/capabilities/bouncycastle.md create mode 100644 docs/capabilities/byte-buddy.md create mode 100644 docs/capabilities/command-executor.md create mode 100644 docs/capabilities/commons-crypto.md create mode 100644 docs/capabilities/commons-io.md create mode 100644 docs/capabilities/commons-text.md create mode 100644 docs/capabilities/data-authorization.md create mode 100644 docs/capabilities/domain-event.md create mode 100644 docs/capabilities/event-system.md create mode 100644 docs/capabilities/exception-handling.md create mode 100644 docs/capabilities/fast-repository.md create mode 100644 docs/capabilities/fastjson.md create mode 100644 docs/capabilities/groovy-script-engine.md create mode 100644 docs/capabilities/http-client-factory.md create mode 100644 docs/capabilities/httpclient5.md create mode 100644 docs/capabilities/index.md create mode 100644 docs/capabilities/jackson-databind.md create mode 100644 docs/capabilities/jjwt.md create mode 100644 docs/capabilities/jsqlparser.md create mode 100644 docs/capabilities/kryo.md create mode 100644 docs/capabilities/page-request-filter.md create mode 100644 docs/capabilities/rest-template-context.md create mode 100644 docs/capabilities/security-auth.md create mode 100644 docs/capabilities/spring-data-jpa.md create mode 100644 docs/capabilities/spring-data-redis.md create mode 100644 docs/capabilities/spring-ioc-container.md create mode 100644 docs/capabilities/spring-security.md create mode 100644 docs/capabilities/spring-web-mvc.md create mode 100644 docs/capabilities/unified-response.md create mode 100644 docs/capabilities/workflow-engine.md create mode 100644 docs/conventions/ddd-layered-architecture.md create mode 100644 docs/conventions/event-handler-pattern.md create mode 100644 docs/conventions/frontend-css-variables.md create mode 100644 docs/conventions/frontend-http-client.md create mode 100644 docs/conventions/frontend-module-federation.md create mode 100644 docs/conventions/frontend-state-management.md create mode 100644 docs/conventions/global-exception-handling.md create mode 100644 docs/conventions/index.md create mode 100644 docs/conventions/unified-response-convention.md diff --git a/CLAUDE.md b/CLAUDE.md index 8be15d3f0..ba71046ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,3 +154,30 @@ codingapi.framework.handler-thread-pool-size=20 # 事件异步线程池大小 ## 关键依赖版本 见根 `pom.xml` 的 `` 区块。主要版本:Groovy 4.0.24、JSqlParser 5.0、Fastjson 2.0.53、JJWT 0.12.6、H2 2.3.232、Kryo 5.6.2。 + + + +## PKR 知识查阅(编码前必须) + +进入计划模式或实现功能前,必须查阅: +1. [docs/capabilities/index.md](./docs/capabilities/index.md) — 已有可复用能力 +2. [docs/conventions/index.md](./docs/conventions/index.md) — 开发规范 + +已有能力必须复用,禁止重新实现。编码必须遵循已注册的规范。 + +### 计划模式约束 + +计划方案中必须包含: +1. **复用了哪些已有能力** — 列出从 PKR 中找到并复用的 Capability +2. **遵循了哪些规范** — 列出遵守的 Convention +3. **是否有新增能力** — 如果本次开发产生了可复用的新能力,完成后通过 `/pkr-scan add` 注册 + +### 知识管理命令 + +| 命令 | 用途 | +|------|------| +| `/pkr-scan init` | 首次扫描项目,发现候选能力和规范 | +| `/pkr-scan sync` | 全量同步,对比代码变更 | +| `/pkr-scan update ` | 单项更新指定能力或规范 | +| `/pkr-scan add` | 手动注册新的能力或规范 | + diff --git a/docs/capabilities/apache-groovy.md b/docs/capabilities/apache-groovy.md new file mode 100644 index 000000000..785bfa7cd --- /dev/null +++ b/docs/capabilities/apache-groovy.md @@ -0,0 +1,86 @@ +--- +name: apache-groovy +description: 动态语言运行时,支持脚本编译与执行 +status: 已实现 +scope: 后端 +source: 框架:Apache Groovy +--- + +## 解决什么问题 + +Apache Groovy 为项目提供运行时脚本引擎能力,解决了以下问题: + +- **动态逻辑**:允许在不重启应用的情况下修改业务逻辑,适用于规则引擎、动态报表等场景 +- **热更新**:脚本存储在数据库中,支持运行时编译和缓存,修改即时生效 +- **REST API 驱动**:通过 REST 接口管理脚本的编译和执行,便于运维操作 +- **与工作流集成**:flow 模块使用 GroovyShell 执行流程条件表达式 + +## 如何使用 + +项目使用 Groovy 4.0.24,在 `springboot-starter-script` 和 `springboot-starter-flow` 两个模块中使用: + +1. **GroovyScriptRuntime**:核心运行时,基于 `GroovyShell` + `GroovyClassLoader`,内置 LRU 缓存避免重复编译 +2. **编译模式**:`compile(script, cache)` 支持缓存和非缓存两种模式,缓存 key 为脚本 SHA256 哈希 +3. **函数调用**:`invoke(method, script, returnType, binds, args)` 支持调用脚本中定义的函数并绑定变量 +4. **启动加载**:`GroovyScriptEngineRunner` 在应用启动时从数据库加载临时脚本到缓存 +5. **REST API**:`GroovyScriptController` 提供脚本编译和执行的 HTTP 接口 + +## 使用实例 + +### GroovyScriptRuntime 编译与缓存 + +```java +public class GroovyScriptRuntime { + + private final GroovyShell shell; + private final Map cache; // LRU 缓存 + + public void compile(String script, boolean cache) { + if (cache) { + String key = SHA256.sha256(script); + Script runtime = this.cache.get(key); + if (runtime == null) { + runtime = this.shell.parse(script); + this.cache.put(key, runtime); + } + } else { + this.shell.parse(script); + } + } + + public T invoke(String method, String script, Class returnType, + Map binds, Object... args) { + // 执行脚本中的指定函数 + } +} +``` + +### 启动时自动加载脚本 + +```java +public class GroovyScriptEngineRunner implements InitializingBean, DisposableBean { + + @Override + public void afterPropertiesSet() throws Exception { + // 从数据库分页加载所有临时脚本到缓存 + PageRequest request = PageRequest.of(0, 100); + Page page = TempGroovyScriptRepositoryContext.getInstance().find(request); + while (page.hasNext()) { + TempGroovyScriptContext.getInstance().loadAll(page.getContent()); + request = PageRequest.of(request.getCurrent() + 1, 100); + page = TempGroovyScriptRepositoryContext.getInstance().find(request); + } + } +} +``` + +### Flow 模块中的 GroovyShell 使用 + +```java +// springboot-starter-flow 中使用 GroovyShell 执行流程条件脚本 +public class GroovyShellContext { + public T run(String script, Class returnType, Object... args) { + // 执行流程节点的条件判断脚本 + } +} +``` diff --git a/docs/capabilities/bouncycastle.md b/docs/capabilities/bouncycastle.md new file mode 100644 index 000000000..35295d753 --- /dev/null +++ b/docs/capabilities/bouncycastle.md @@ -0,0 +1,128 @@ +--- +name: bouncycastle +description: 密码学算法库,提供 AES/RSA 等加密算法支持 +status: 已实现 +scope: 后端 +source: 框架:BouncyCastle +--- + +## 解决什么问题 + +框架需要提供数据加解密能力,用于: +- 安全模块中 Token IV(初始化向量)的 AES 加密/解密 +- RSA 非对称加密用于数据传输安全 +- 作为 JCA/JCE Provider 注册,增强 JDK 默认的密码学算法支持 + +BouncyCastle 作为 Java 密码学扩展提供者(Provider),通过 `Security.addProvider()` 注册后,JDK 标准 Cipher API 即可使用更丰富的算法实现。 + +## 如何使用 + +### 依赖引入 + +```xml + + + org.bouncycastle + bcprov-jdk18on + 1.79 + +``` + +### 核心类 + +- `AES` — AES 对称加密封装类(支持 CBC/PKCS5Padding) +- `RSA` — RSA 非对称加密封装类(2048 位密钥对) +- `AESUtils` — AES 工具类(单例模式) +- `RSAUtils` — RSA 工具类(单例模式,内置默认密钥对) + +## 使用实例 + +### 1. AES 对称加密 + +```java +// AES.java - 使用 BouncyCastle Provider +public class AES { + public static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + + public AES() throws Exception { + this(CIPHER_ALGORITHM, 256); + } + + public AES(String algorithm, int generateKeySize) throws Exception { + // 注册 BouncyCastle Provider + Security.addProvider(new BouncyCastleProvider()); + this.algorithm = algorithm; + this.key = generateKey(generateKeySize); + this.iv = generateIV(randomIv()); + } + + public byte[] encrypt(byte[] data) throws Exception { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + return cipher.doFinal(data); + } + + public byte[] decrypt(byte[] encryptedData) throws Exception { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(encryptedData); + } +} +``` + +### 2. RSA 非对称加密 + +```java +// RSA.java - 使用 BouncyCastle Provider +public class RSA { + public RSA() throws NoSuchAlgorithmException { + Security.addProvider(new BouncyCastleProvider()); + KeyPair keyPair = generateKey(); + this.privateKey = keyPair.getPrivate(); + this.publicKey = keyPair.getPublic(); + } + + public KeyPair generateKey() throws NoSuchAlgorithmException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } + + public byte[] encrypt(byte[] text) throws Exception { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(text); + } + + public byte[] decrypt(byte[] text) throws Exception { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(text); + } +} +``` + +### 3. Security 模块中的 AES 工具 + +```java +// AESTools.java - Security 模块中使用 AES 加密 Token IV +public class AESTools { + private AES aes; + + public void init(AES aes) { + this.aes = aes; + } + + // Token 创建时加密 IV + // Token.java: this.iv = AESTools.getInstance().encode(iv); + // Token 解析时解密 IV + // Token.java: return AESTools.getInstance().decode(iv); +} +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter | `AES`、`RSA`、`AESUtils`、`RSAUtils` 加密工具类 | +| springboot-starter-security | `AESTools` Token IV 加解密 | diff --git a/docs/capabilities/byte-buddy.md b/docs/capabilities/byte-buddy.md new file mode 100644 index 000000000..64bcd3801 --- /dev/null +++ b/docs/capabilities/byte-buddy.md @@ -0,0 +1,136 @@ +--- +name: byte-buddy +description: 字节码操作库,用于运行时动态生成 JPA 实体类 +status: 已实现 +scope: 后端 +source: 框架:Byte Buddy +--- + +## 解决什么问题 + +在 springboot-starter-data-fast 模块中,需要支持**动态表结构**场景: +- 根据元数据(`TableEntityMetadata`)在运行时动态生成 JPA 实体类 +- 生成的实体类需要携带 `@Entity`、`@Table`、`@Column`、`@Id` 等 JPA 注解 +- 生成的类需要有标准的 getter/setter 方法 +- 生成的类需要注册到自定义 ClassLoader 中以被 Hibernate/JPA 识别 + +Byte Buddy 提供了类型安全的字节码操作 API,可以在运行时创建带有注解和方法的 Java 类,无需编译期代码生成。 + +## 如何使用 + +### 依赖引入 + +```xml + + + net.bytebuddy + byte-buddy + +``` + +### 核心类 + +- `TableEntityClassBuilder` — 基于 Byte Buddy 的动态实体类构建器 +- `TableEntityMetadata` — 实体元数据定义(表名、列信息、注解等) +- `DynamicTableClassLoader` — 动态类加载器,注册运行时生成的类 + +## 使用实例 + +### 1. 动态构建 JPA 实体类 + +```java +// TableEntityClassBuilder.java +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.FieldAccessor; + +class TableEntityClassBuilder { + + private final TableEntityMetadata metadata; + private DynamicType.Builder builder; + + public TableEntityClassBuilder(TableEntityMetadata metadata) { + this.metadata = metadata; + // 创建子类并添加 @Entity、@Table 注解 + this.builder = new ByteBuddy() + .subclass(Object.class) + .name(metadata.getClassName()) + .implement(Serializable.class) + .annotateType(buildEntityAnnotations()); + } + + public Class build() { + this.buildColumns(); + Class clazz = builder.make() + .load(TableEntityClassBuilder.class.getClassLoader(), + ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + // 注册到动态类加载器 + DynamicTableClassLoader.getInstance().registerClass(clazz); + return clazz; + } +} +``` + +### 2. 动态添加字段和注解 + +```java +// TableEntityClassBuilder.java - 添加带 JPA 注解的字段 +private void addColumnField(TableEntityMetadata.ColumnMeta columnMeta) { + String fieldName = columnMeta.getFieldName(); + Class fieldType = columnMeta.getType(); + + // 构建字段注解 (@Column, @Id, @GeneratedValue 等) + List fieldAnnotations = buildFieldAnnotations(columnMeta); + + // 定义私有字段并添加注解 + builder = builder.defineField(fieldName, fieldType, Visibility.PRIVATE) + .annotateField(fieldAnnotations.toArray(new AnnotationDescription[0])); + + // 自动生成 getter/setter 方法 + String capitalizedFieldName = capitalize(fieldName); + builder = builder + .defineMethod("get" + capitalizedFieldName, fieldType, Visibility.PUBLIC) + .intercept(FieldAccessor.ofField(fieldName)) + .defineMethod("set" + capitalizedFieldName, void.class, Visibility.PUBLIC) + .withParameter(fieldType) + .intercept(FieldAccessor.ofField(fieldName)); +} +``` + +### 3. 构建实体类注解 + +```java +// TableEntityClassBuilder.java - 动态构建 @Entity + @Table 注解 +private AnnotationDescription[] buildEntityAnnotations() { + List annotations = new ArrayList<>(); + + // @Entity + annotations.add(AnnotationDescription.Builder.ofType(Entity.class).build()); + + // @Table(name = "xxx") + if (metadata.getTable() != null) { + AnnotationDescription.Builder tableBuilder = + AnnotationDescription.Builder.ofType(Table.class); + tableBuilder = tableBuilder.define("name", metadata.getTable().getName()); + annotations.add(tableBuilder.build()); + } + + // @Comment + if (StringUtils.hasText(metadata.getTable().getComment())) { + annotations.add(AnnotationDescription.Builder.ofType(Comment.class) + .define("value", metadata.getTable().getComment()).build()); + } + + return annotations.toArray(new AnnotationDescription[0]); +} +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter-data-fast | `TableEntityClassBuilder` 运行时动态生成 JPA 实体类 | diff --git a/docs/capabilities/command-executor.md b/docs/capabilities/command-executor.md new file mode 100644 index 000000000..8ab8c386d --- /dev/null +++ b/docs/capabilities/command-executor.md @@ -0,0 +1,279 @@ +--- +name: command-executor +description: CQRS 命令执行器接口,定义标准化的命令-响应契约,支持带参和无参两种执行模式 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在 CQRS(命令查询职责分离)架构中,写操作(Command)和读操作(Query)需要明确分离。实际开发中常见以下问题: + +1. **Service 方法签名不统一**:有的方法返回 void,有的返回 DTO,有的返回 Response,调用方无法预期 +2. **命令对象缺乏规范**:不同开发者定义的命令参数格式各异,缺少统一的输入输出契约 +3. **响应与命令脱节**:命令执行后的结果没有标准化的封装方式,成功/失败判断逻辑分散 +4. **DDD 应用层编排困难**:Application Service 需要协调多个领域服务,但缺乏清晰的命令执行抽象 + +`IExecutor` 接口定义了两种命令执行器契约: +- `IExecutor.Command` — 接收命令对象 C,返回响应 R +- `IExecutor.Void` — 无参执行,返回响应 R + +其中 R 必须继承自 `Response`,确保所有命令执行结果都遵循统一响应格式。 + +## 如何使用 + +### 1. 定义命令对象 + +命令对象是纯数据载体,描述"要做什么": + +```java +@Getter +@Setter +public class CreateUserCommand { + private String name; + private String email; + private Integer age; +} +``` + +### 2. 实现 Command 执行器 + +```java +@Component +public class CreateUserExecutor implements IExecutor.Command, CreateUserCommand> { + + @Autowired + private UserRepository userRepository; + + @Override + public SingleResponse execute(CreateUserCommand command) { + User user = new User(); + user.setName(command.getName()); + user.setEmail(command.getEmail()); + user.setAge(command.getAge()); + + userRepository.save(user); + + UserDTO dto = convertToDTO(user); + return SingleResponse.of(dto); + } +} +``` + +### 3. 实现 Void 执行器 + +适用于不需要传入命令对象的场景: + +```java +@Component +public class SystemCleanupExecutor implements IExecutor.Void { + + @Autowired + private CacheService cacheService; + + @Override + public Response execute() { + cacheService.clearAll(); + return Response.buildSuccess(); + } +} +``` + +### 4. 在 Controller 或 Application Service 中调用 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private CreateUserExecutor createUserExecutor; + + @PostMapping + public SingleResponse createUser(@RequestBody CreateUserCommand cmd) { + return createUserExecutor.execute(cmd); + } +} +``` + +## 使用实例 + +### DDD 应用层中的命令编排 + +```java +// ===== 命令定义 ===== + +@Getter @Setter +public class TransferMoneyCommand { + private Long fromAccountId; + private Long toAccountId; + private BigDecimal amount; + private String remark; +} + +// ===== 执行器实现 ===== + +@Component +public class TransferMoneyExecutor + implements IExecutor.Command, TransferMoneyCommand> { + + @Autowired + private AccountRepository accountRepository; + + @Autowired + private TransactionRepository transactionRepository; + + @Override + @Transactional + public SingleResponse execute(TransferMoneyCommand cmd) { + // 1. 加载聚合根 + Account fromAccount = accountRepository.findById(cmd.getFromAccountId()) + .orElseThrow(() -> LocaleMessageException.of("ACCOUNT_NOT_FOUND", cmd.getFromAccountId())); + Account toAccount = accountRepository.findById(cmd.getToAccountId()) + .orElseThrow(() -> LocaleMessageException.of("ACCOUNT_NOT_FOUND", cmd.getToAccountId())); + + // 2. 执行业务逻辑 + fromAccount.debit(cmd.getAmount()); + toAccount.credit(cmd.getAmount()); + + // 3. 保存状态 + accountRepository.save(fromAccount); + accountRepository.save(toAccount); + + // 4. 创建交易记录 + Transaction tx = new Transaction(fromAccount, toAccount, cmd.getAmount(), cmd.getRemark()); + transactionRepository.save(tx); + + // 5. 推送领域事件 + EventPusher.push(new MoneyTransferredEvent(tx.getId())); + + return SingleResponse.of(convertToDTO(tx)); + } +} + +// ===== Controller ===== + +@RestController +@RequestMapping("/api/transfers") +public class TransferController { + + @Autowired + private TransferMoneyExecutor transferExecutor; + + @PostMapping + public SingleResponse transfer(@RequestBody TransferMoneyCommand cmd) { + return transferExecutor.execute(cmd); + } +} +``` + +### 批量命令执行器 + +```java +@Getter @Setter +public class BatchImportUsersCommand { + private List users; +} + +@Component +public class BatchImportUsersExecutor + implements IExecutor.Command, BatchImportUsersCommand> { + + @Autowired + private UserRepository userRepository; + + @Override + @Transactional + public MultiResponse execute(BatchImportUsersCommand cmd) { + List results = new ArrayList<>(); + + for (UserImportItem item : cmd.getUsers()) { + User user = new User(); + user.setName(item.getName()); + user.setEmail(item.getEmail()); + userRepository.save(user); + results.add(convertToDTO(user)); + } + + return MultiResponse.of(results, results.size()); + } +} +``` + +### 在 Application Service 中组合多个执行器 + +```java +@Service +public class OrderApplicationService { + + @Autowired + private CreateOrderExecutor createOrderExecutor; + + @Autowired + private DeductInventoryExecutor deductInventoryExecutor; + + @Autowired + private SendNotificationExecutor sendNotificationExecutor; + + @Transactional + public SingleResponse placeOrder(CreateOrderCommand cmd) { + // 1. 创建订单 + SingleResponse orderResponse = createOrderExecutor.execute(cmd); + if (!orderResponse.isSuccess()) { + return orderResponse; + } + + // 2. 扣减库存 + DeductInventoryCommand inventoryCmd = new DeductInventoryCommand(); + inventoryCmd.setOrderId(orderResponse.getData().getId()); + inventoryCmd.setItems(cmd.getItems()); + Response inventoryResp = deductInventoryExecutor.execute(inventoryCmd); + if (!inventoryResp.isSuccess()) { + throw LocaleMessageException.of("INVENTORY_DEDUCT_FAILED"); + } + + // 3. 发送通知(异步,不影响主流程) + try { + sendNotificationExecutor.execute( + new SendOrderNotificationCommand(orderResponse.getData().getId())); + } catch (Exception e) { + log.warn("Failed to send order notification", e); + } + + return orderResponse; + } +} +``` + +### IExecutor 接口结构 + +```java +public interface IExecutor { + + /** + * 带命令参数的执行器 + * @param 响应类型,必须继承 Response + * @param 命令类型 + */ + interface Command { + R execute(C command); + } + + /** + * 无参数的执行器 + * @param 响应类型,必须继承 Response + */ + interface Void { + R execute(); + } +} +``` + +### 设计原则 + +1. **单一职责**:每个执行器只负责一个业务命令 +2. **响应标准化**:返回值必须是 `Response` 或其子类 +3. **命令不可变**:命令对象作为输入不应在执行过程中被修改 +4. **事务边界清晰**:`@Transactional` 加在执行器方法上,而非 Controller +5. **异常即响应**:业务异常通过 `LocaleMessageException` 抛出,由全局处理器转为标准错误响应 diff --git a/docs/capabilities/commons-crypto.md b/docs/capabilities/commons-crypto.md new file mode 100644 index 000000000..c67c7c6ba --- /dev/null +++ b/docs/capabilities/commons-crypto.md @@ -0,0 +1,110 @@ +--- +name: commons-crypto +description: Apache 加密工具封装,提供 AES 等加密操作 +status: 已实现 +scope: 后端 +source: 框架:Apache Commons Crypto +--- + +## 解决什么问题 + +框架在 springboot-starter 模块中提供了统一的加密工具抽象,用于: +- AES 对称加密的便捷封装 +- 安全模块中 Token IV 向量的加解密 +- 为上层业务提供简洁的加密/解密 API + +项目中通过 `AES`、`AESUtils`、`AESTools` 等类对加密操作进行了封装,结合 BouncyCastle Provider 提供完整的加密能力。 + +## 如何使用 + +### 依赖引入 + +```xml + + + org.apache.commons + commons-crypto + 1.2.0 + +``` + +### 核心类 + +- `AES` — AES 加密封装(位于 `com.codingapi.springboot.framework.crypto`) +- `AESUtils` — AES 单例工具类 +- `AESTools` — Security 模块的 AES 工具(自动注入配置密钥) + +## 使用实例 + +### 1. AESUtils 单例使用 + +```java +// AESUtils.java - 框架级 AES 工具 +public class AESUtils { + private static final AESUtils instance = new AESUtils(); + private final AES aes; + + private AESUtils() { + // 初始化默认 AES 实例 + this.aes = new AES(); + } + + public static AESUtils getInstance() { + return instance; + } + + // 提供 encode/decode 方法供业务调用 +} +``` + +### 2. Security 模块 AESTools + +```java +// AESTools.java - 安全模块中的 AES 工具 +@Component +public class AESTools { + private static AESTools instance; + private AES aes; + + @PostConstruct + public void init(AES aes) { + this.aes = aes; + instance = this; + } + + public static AESTools getInstance() { + return instance; + } + + // 在 Token 中使用: + // 加密: AESTools.getInstance().encode(iv) + // 解密: AESTools.getInstance().decode(iv) +} +``` + +### 3. Security 加密配置 + +```java +// SecurityCryptoConfiguration.java +@Configuration +public class SecurityCryptoConfiguration { + + @Bean + public AES aes(CodingApiSecurityProperties properties) { + // 根据配置创建 AES 实例 + return new AES(properties.getAesKey(), properties.getAesIv()); + } + + @Bean + public AESTools aesTools() { + return new AESTools(); + } +} +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter | `AES`、`AESUtils` 基础加密工具 | +| springboot-starter-security | `AESTools`、`SecurityCryptoConfiguration` 安全加密配置 | diff --git a/docs/capabilities/commons-io.md b/docs/capabilities/commons-io.md new file mode 100644 index 000000000..2c42bdb88 --- /dev/null +++ b/docs/capabilities/commons-io.md @@ -0,0 +1,100 @@ +--- +name: commons-io +description: IO 工具类库,简化文件/流操作 +status: 已实现 +scope: 后端 +source: 框架:Apache Commons IO +--- + +## 解决什么问题 + +在 Spring Security Filter 链中,需要从 HTTP 请求/响应中读取和写入数据流: +- 从 `HttpServletRequest` 中读取请求体内容 +- 向 `HttpServletResponse` 中写入 JSON 响应 +- 测试中读取文件资源 + +Apache Commons IO 的 `IOUtils` 提供了简洁的流读写 API,避免了手动处理流的样板代码。 + +## 如何使用 + +### 依赖引入 + +```xml + + + commons-io + commons-io + 2.17.0 + +``` + +### 核心用法 + +主要使用 `org.apache.commons.io.IOUtils` 进行流的读写操作。 + +## 使用实例 + +### 1. Security Filter 中读取请求体 + +```java +// MyLoginFilter.java - 读取登录请求体 +import org.apache.commons.io.IOUtils; + +@Override +public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) { + // 从请求输入流中读取 JSON 数据 + String body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); + JSONObject json = JSONObject.parseObject(body); + String username = json.getString("username"); + String password = json.getString("password"); + // ... 执行认证逻辑 +} +``` + +### 2. Security Filter 中写入响应 + +```java +// MyAccessDeniedHandler.java / MyUnAuthenticationEntryPoint.java +import org.apache.commons.io.IOUtils; + +@Override +public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + JSONObject json = new JSONObject(); + json.put("errCode", "403"); + json.put("errMessage", "访问被拒绝"); + // 使用 IOUtils 将 JSON 字符串写入响应输出流 + IOUtils.write(json.toJSONString(), response.getOutputStream(), StandardCharsets.UTF_8); +} +``` + +### 3. MyAuthenticationFilter 中的 Token 读取 + +```java +// MyAuthenticationFilter.java - 从请求头或请求体中读取 Token +import org.apache.commons.io.IOUtils; + +// 读取请求内容进行 Token 验证 +String content = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); +``` + +### 4. 测试中的文件读取 + +```java +// SearchRequestTest.java - 测试中读取测试资源文件 +import org.apache.commons.io.FileUtils; + +String content = FileUtils.readFileToString( + new File("src/test/resources/test-data.json"), + StandardCharsets.UTF_8 +); +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter-security | Filter 链中请求体读取、响应体写入(IOUtils) | +| springboot-starter | 测试中文件操作(FileUtils) | diff --git a/docs/capabilities/commons-text.md b/docs/capabilities/commons-text.md new file mode 100644 index 000000000..1960739de --- /dev/null +++ b/docs/capabilities/commons-text.md @@ -0,0 +1,87 @@ +--- +name: commons-text +description: 文本处理工具库,提供字符串命名转换等功能 +status: 已实现 +scope: 后端 +source: 框架:Apache Commons Text +--- + +## 解决什么问题 + +在数据查询模块(springboot-starter-data-fast)中,JDBC 查询返回的结果集列名通常是数据库风格的下划线命名(如 `user_name`),而 Java Bean 属性使用驼峰命名(如 `userName`)。需要自动将列名转换为驼峰格式以匹配 Bean 属性。 + +Apache Commons Text 的 `CaseUtils.toCamelCase()` 提供了开箱即用的命名风格转换功能。 + +## 如何使用 + +### 依赖引入 + +```xml + + + org.apache.commons + commons-text + 1.12.0 + +``` + +### 核心用法 + +主要使用 `org.apache.commons.text.CaseUtils` 进行命名风格转换。 + +## 使用实例 + +### JdbcQuery 中的驼峰命名映射 + +```java +// JdbcQuery.java - JDBC 查询结果集的列名转驼峰 +import org.apache.commons.text.CaseUtils; + +public class JdbcQuery { + + /** + * 自定义 RowMapper,将下划线列名自动转为驼峰命名 + */ + private static class CamelCaseRowMapper implements RowMapper> { + + @Override + public Map mapRow(ResultSet rs, int rowNum) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + Map map = new HashMap<>(columnCount); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnLabel(i); + // 将 user_name → userName, create_time → createTime + map.put(CaseUtils.toCamelCase(columnName, false), rs.getObject(i)); + } + return map; + } + } + + /** + * 查询返回 Map 列表(自动驼峰转换) + */ + public List> queryForMapList(String sql, Object... params) { + return jdbcTemplate.query(sql, new CamelCaseRowMapper(), params); + } + + /** + * 分页查询返回 Map 页面(自动驼峰转换) + */ + public Page> queryForMapPage(String sql, String countSql, + PageRequest pageRequest, + Object... params) { + List> list = jdbcTemplate.query( + sql, new CamelCaseRowMapper(), params + ); + long count = this.countQuery(countSql, params); + return new PageImpl<>(list, pageRequest, count); + } +} +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter-data-fast | `JdbcQuery.CamelCaseRowMapper` 列名下划线转驼峰 | diff --git a/docs/capabilities/data-authorization.md b/docs/capabilities/data-authorization.md new file mode 100644 index 000000000..22dc8ca09 --- /dev/null +++ b/docs/capabilities/data-authorization.md @@ -0,0 +1,206 @@ +--- +name: data-authorization +description: 基于 JDBC 代理链的透明数据权限过滤,支持行级和列级权限控制 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在企业级多租户或多部门系统中,不同用户只能查看和操作其权限范围内的数据。传统做法需要在每个业务查询中手动拼接权限条件,代码侵入性强且容易遗漏。 + +`springboot-starter-data-authorization` 通过 JDBC 驱动层代理拦截所有 SQL 执行,在 SQL 到达数据库之前自动注入行级过滤条件(WHERE / JOIN),并在结果集返回时提供列级权限处理能力。业务代码无需任何修改即可获得透明的数据权限控制。 + +核心设计思路: +- **JDBC 驱动代理**:`AuthorizationJdbcDriver` 作为 JDBC Driver 注册,将所有 `Connection` 包装为 `ConnectionProxy` +- **SQL 拦截改写**:通过 `SQLRunningContext.intercept(sql)` 在 `prepareStatement` / `prepareCall` / `createStatement` 时解析并改写 SQL +- **行级权限**:`RowHandler` + `DataAuthorizationFilter` 为每张表动态注入 WHERE 条件或 JOIN 关联 +- **列级权限**:`ColumnHandler` 在 `ResultSetProxy` 读取字段值时进行脱敏或过滤 +- **跳过机制**:`SQLRunningContext.skipDataAuthorization()` 允许特定操作绕过权限拦截 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-data-authorization + +``` + +### 2. 配置 JDBC 驱动 + +将 JDBC URL 中的 driver 替换为 `com.codingapi.springboot.authorization.jdbc.AuthorizationJdbcDriver`,该驱动会自动查找并代理真实的数据库驱动: + +```properties +spring.datasource.driver-class-name=com.codingapi.springboot.authorization.jdbc.AuthorizationJdbcDriver +``` + +### 3. 注册数据权限过滤器 + +实现 `DataAuthorizationFilter` 接口并注册到 `DataAuthorizationContext`: + +```java +// 添加行级权限过滤器 +DataAuthorizationContext.getInstance().addDataAuthorizationFilter(filter); +``` + +`DataAuthorizationFilter` 需要实现以下方法: +- `supportRowAuthorization(tableName, tableAlias)` — 判断是否对指定表启用行级权限 +- `rowAuthorization(tableName, tableAlias)` — 返回 `Condition` 对象,包含要注入的 WHERE 或 JOIN 条件 +- `supportColumnAuthorization(tableName, columnName, value)` — 判断是否对指定列启用列级权限 +- `columnAuthorization(tableName, columnName, value)` — 返回处理后的列值 + +### 4. 构建行级权限条件 + +使用 `Condition` 类的静态工厂方法构建权限条件: + +```java +// WHERE 条件 +Condition.customCondition("department_id = 10"); + +// 格式化条件 +Condition.formatCondition("department_id = %d", deptId); + +// 默认放行条件 +Condition.defaultCondition(); // 等价于 "1=1" + +// 无权限(返回 null) +Condition.emptyCondition(); +``` + +对于 JOIN 类型的权限条件,框架内置了 `JoinConditionSQLHandler`,支持 INNER / LEFT / RIGHT JOIN 的自动注入。 + +### 5. 自定义 RowHandler + +如需自定义行权限处理逻辑,可实现 `RowHandler` 接口并通过 `RowHandlerContext` 设置: + +```java +RowHandlerContext.getInstance().setRowHandler((subSql, tableName, tableAlias) -> { + return Condition.customCondition("tenant_id = '" + getCurrentTenantId() + "'"); +}); +``` + +### 6. 自定义列级权限处理器 + +实现 `ColumnHandler` 接口以对特定列的值进行脱敏或转换: + +```java +// ColumnHandler 接口提供了所有 JDBC 类型的 get 方法 +String getString(SQLExecuteState interceptState, int columnIndex, + String tableName, String columnName, String value); +int getInt(SQLExecuteState interceptState, int columnIndex, + String tableName, String columnName, int value); +// ... 其他类型同理 +``` + +### 7. 跳过数据权限拦截 + +在某些管理操作或系统内部查询中需要绕过权限过滤: + +```java +// 方式一:使用 Supplier +List users = SQLRunningContext.getInstance() + .skipDataAuthorization(() -> userRepository.findAll()); + +// 方式二:使用 Runnable +SQLRunningContext.getInstance() + .skipDataAuthorization(() -> userRepository.deleteAll()); +``` + +### 8. 自定义跳过拦截时的 SQL 处理 + +实现 `SkipAuthorizationFilter` 接口,在跳过权限拦截时对 SQL 进行额外处理: + +```java +DataAuthorizationContext.getInstance() + .setSkipAuthorizationFilter(sql -> { + // 对跳过拦截的 SQL 做特殊处理 + return sql; + }); +``` + +## 使用实例 + +### 完整的多租户数据权限示例 + +```java +@Configuration +public class DataPermissionConfig { + + @PostConstruct + public void initDataPermission() { + // 注册数据权限过滤器 + DataAuthorizationContext.getInstance().addDataAuthorizationFilter( + new DataAuthorizationFilter() { + + @Override + public boolean supportRowAuthorization(String tableName, String tableAlias) { + // 对订单表和员工表启用行级权限 + return "t_order".equals(tableName) || "t_employee".equals(tableName); + } + + @Override + public Condition rowAuthorization(String tableName, String tableAlias) { + Long deptId = SecurityUtils.getCurrentDeptId(); + if (deptId == null) { + return Condition.emptyCondition(); + } + return Condition.formatCondition("%s.dept_id = %d", tableAlias, deptId); + } + + @Override + public boolean supportColumnAuthorization( + String tableName, String columnName, T value) { + // 对员工表的手机号列启用脱敏 + return "t_employee".equals(tableName) && "phone".equals(columnName); + } + + @Override + public T columnAuthorization( + String tableName, String columnName, T value) { + if (value instanceof String phone) { + return (T) phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); + } + return value; + } + } + ); + } +} +``` + +### 代理链工作原理 + +``` +应用代码调用 prepareStatement(sql) + ↓ +ConnectionProxy.prepareStatement(sql) + ↓ +SQLRunningContext.intercept(sql) + ├── skipInterceptor=true → 直接返回原始 SQL + └── SQLInterceptor.beforeHandler(sql) → true + ↓ + SQLInterceptor.postHandler(sql) → DataPermissionSQL(newSql, aliasContext) + ↓ + connection.prepareStatement(newSql) → PreparedStatementProxy + ↓ + executeQuery() → ResultSetProxy(resultSet, interceptState) + ↓ + ResultSetProxy.next()/getXxx() → ColumnHandler 列级权限处理 +``` + +### 异常处理 + +当权限校验失败时,框架抛出 `NotAuthorizationException`(继承自 `SQLException`),可在上层统一捕获处理: + +```java +try { + List orders = orderRepository.findAll(); +} catch (NotAuthorizationException e) { + // 处理无权限异常 + throw new BusinessException("无权访问该数据"); +} +``` diff --git a/docs/capabilities/domain-event.md b/docs/capabilities/domain-event.md new file mode 100644 index 000000000..01964314e --- /dev/null +++ b/docs/capabilities/domain-event.md @@ -0,0 +1,202 @@ +--- +name: domain-event +description: 领域实体事件自动追踪机制,支持创建、变更、删除事件的自动推送与字段级变更检测 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在 DDD 实践中,领域实体的生命周期事件(创建、修改、删除)是业务流转的关键信号。手动在每个业务方法中调用 `EventPusher.push()` 存在以下痛点: + +1. **侵入性强**:每个 setter 或状态变更方法都需要显式推送事件,代码冗余 +2. **遗漏风险**:开发者可能忘记在某些字段变更后推送事件 +3. **变更详情缺失**:普通事件只能知道"实体变了",无法知道"哪个字段从什么值变成了什么值" +4. **实体创建事件易遗漏**:新创建的实体如果没有及时推送事件,下游监听器将无法感知 + +框架通过 `IDomain` 接口提供标准的持久化和删除事件入口,并通过 `DomainProxyFactory` + CGLIB 代理实现字段级变更拦截,自动推送 `DomainChangeEvent`,携带字段名、旧值、新值信息。 + +## 如何使用 + +### 1. 实体类实现 IDomain 接口 + +```java +public class User implements IDomain { + private Long id; + private String name; + private Integer age; + + // getter/setter... +} +``` + +### 2. 使用 DomainProxyFactory 创建代理实例 + +通过工厂创建的实例会自动: +- 推送 `DomainCreateEvent`(创建时) +- 拦截所有带参数的 setter 方法,自动对比字段变化并推送 `DomainChangeEvent` + +```java +// 创建代理实例(自动推送 DomainCreateEvent) +User user = DomainProxyFactory.create(User.class); +user.setName("张三"); // 自动推送 DomainChangeEvent(fieldName="name", oldValue=null, newValue="张三") +``` + +### 3. 手动触发持久化或删除事件 + +```java +// 保存后推送持久化事件 +user.persist(); // 推送 DomainPersistEvent + +// 删除前推送删除事件 +user.delete(); // 推送 DomainDeleteEvent +``` + +### 4. 订阅领域事件 + +```java +@Component +public class UserChangeLogger implements IHandler { + @Override + public void handler(DomainChangeEvent event) { + log.info("Entity {} field '{}' changed from {} to {}", + event.getEntityClass().getSimpleName(), + event.getFieldName(), + event.getOldValue(), + event.getNewValue() + ); + } +} + +@Component +public class UserCreatedNotifier implements IHandler { + @Override + public void handler(DomainCreateEvent event) { + log.info("New entity created: {}", event.getEntityClass().getSimpleName()); + } +} +``` + +## 使用实例 + +### 完整示例:用户信息管理 + +```java +// 1. 定义用户实体 +public class User implements IDomain { + private Long id; + private String name; + private String email; + private Integer status; + + // 必须有无参构造或有参构造供代理使用 + public User() {} +} + +// 2. 监听创建事件 +@Component +public class UserCreatedHandler implements IHandler { + @Override + public int order() { return 0; } + + @Override + public void handler(DomainCreateEvent event) { + if (event.getEntity() instanceof User user) { + log.info("User created: {}", user.getId()); + // 初始化默认配置等 + } + } +} + +// 3. 监听字段变更事件 +@Component +public class UserChangeAuditHandler implements IHandler { + @Override + public void handler(DomainChangeEvent event) { + if (event.getEntity() instanceof User) { + auditLogService.record( + event.getEntityClass().getName(), + event.getFieldName(), + String.valueOf(event.getOldValue()), + String.valueOf(event.getNewValue()) + ); + } + } +} + +// 4. 监听删除事件 +@Component +public class UserDeletedHandler implements IHandler { + @Override + public void handler(DomainDeleteEvent event) { + if (event.getEntity() instanceof User user) { + cleanupService.cleanUserData(user.getId()); + } + } +} + +// 5. Service 中使用 +@Service +public class UserService { + + public User createUser(String name, String email) { + // 通过工厂创建,自动推送 DomainCreateEvent + User user = DomainProxyFactory.create(User.class); + user.setName(name); // 自动推送 DomainChangeEvent + user.setEmail(email); // 自动推送 DomainChangeEvent + + userRepository.save(user); + user.persist(); // 推送 DomainPersistEvent + return user; + } + + public void updateUserName(Long userId, String newName) { + User user = userRepository.findById(userId).orElseThrow(); + // 注意:直接从 Repository 获取的对象不会自动追踪变更 + // 如需追踪,需重新通过代理包装或使用其他 AOP 方案 + user.setName(newName); + userRepository.save(user); + } + + public void deleteUser(Long userId) { + User user = userRepository.findById(userId).orElseThrow(); + user.delete(); // 推送 DomainDeleteEvent + userRepository.delete(user); + } +} +``` + +### 嵌套对象字段变更检测 + +`DomainChangeInterceptor` 支持嵌套对象的字段级变更检测: + +```java +public class Address { + private String city; + private String street; + // getter/setter... +} + +public class User implements IDomain { + private String name; + private Address address; + // getter/setter... +} + +// 当 address.city 发生变化时,fieldName 为 "address.city" +// DomainChangeEvent.getFieldName() = "address.city" +// DomainChangeEvent.getOldValue() = "北京" +// DomainChangeEvent.getNewValue() = "上海" +``` + +### 事件类型继承关系 + +``` +IEvent + └── DomainEvent (com.codingapi.springboot.framework.domain.event.DomainEvent) + ├── DomainCreateEvent — 实体创建 + ├── DomainChangeEvent — 字段变更(含 fieldName, oldValue, newValue) + ├── DomainDeleteEvent — 实体删除 + └── DomainPersistEvent — 实体持久化 +``` diff --git a/docs/capabilities/event-system.md b/docs/capabilities/event-system.md new file mode 100644 index 000000000..ad694dcc7 --- /dev/null +++ b/docs/capabilities/event-system.md @@ -0,0 +1,211 @@ +--- +name: event-system +description: 基于发布-订阅模式的事件系统,支持同步/异步事件处理、事件排序和异常隔离 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在领域驱动设计(DDD)中,领域事件是实现业务解耦的核心机制。传统 Spring Event 存在以下问题: + +1. **事务耦合**:Spring Event 默认与主业务事务绑定,事件失败会影响主流程 +2. **缺乏类型安全**:事件处理器需要手动判断事件类型 +3. **缺少执行顺序控制**:多个处理器无法保证执行顺序 +4. **异常传播**:一个处理器异常会中断其他处理器的执行 +5. **循环调用检测缺失**:事件之间可能形成循环依赖导致栈溢出 + +框架自建的事件系统通过 `IEvent` + `IHandler` 的泛型绑定、`order()` 排序机制、`error()` 异常回调以及 `EventLoopException` 循环检测,解决了上述所有问题。同时支持同步(`ISyncEvent`)和异步(`IAsyncEvent`)两种执行模式,异步事件可配置线程池大小。 + +## 如何使用 + +### 1. 定义事件 + +实现 `IEvent` 接口或其子接口: + +```java +// 同步事件 - 在当前线程立即执行 +public class UserCreatedEvent implements ISyncEvent { + private final Long userId; + + public UserCreatedEvent(Long userId) { + this.userId = userId; + } +} + +// 异步事件 - 在线程池中异步执行 +public class NotificationEvent implements IAsyncEvent { + private final String message; + + public NotificationEvent(String message) { + this.message = message; + } +} +``` + +### 2. 实现事件处理器 + +实现 `IHandler` 接口,泛型参数指定要订阅的事件类型: + +```java +@Component +public class UserCreatedHandler implements IHandler { + + // 可选:指定执行顺序,数值越小优先级越高 + @Override + public int order() { + return 0; + } + + @Override + public void handler(UserCreatedEvent event) { + // 处理用户创建逻辑 + System.out.println("User created: " + event.getUserId()); + } + + // 可选:自定义异常处理,避免中断其他处理器 + @Override + public void error(Exception exception) throws Exception { + log.error("Failed to handle user created event", exception); + // 不抛出异常,允许后续处理器继续执行 + } +} +``` + +### 3. 推送事件 + +使用静态方法 `EventPusher.push()` 推送事件: + +```java +// 推送同步事件 +EventPusher.push(new UserCreatedEvent(1L)); + +// 推送异步事件 +EventPusher.push(new NotificationEvent("Welcome!")); + +// 禁用循环检测(仅在明确知道无循环风险时使用) +EventPusher.push(event, true); +``` + +### 4. 配置异步线程池 + +在 `application.properties` 中配置: + +```properties +codingapi.framework.handler-thread-pool-size=20 +``` + +### 5. 启用事务后提交触发 + +如需在事务提交后再触发事件处理器,启用事务事件监听器: + +```properties +codingapi.framework.event.transaction.enable=true +``` + +启用后将使用 `@TransactionalEventListener(phase = AFTER_COMMIT)` 替代默认的 `@EventListener`。 + +## 使用实例 + +### 完整示例:订单创建流程 + +```java +// 1. 定义订单创建事件 +public class OrderCreatedEvent implements ISyncEvent { + private final String orderId; + private final BigDecimal amount; + + public OrderCreatedEvent(String orderId, BigDecimal amount) { + this.orderId = orderId; + this.amount = amount; + } +} + +// 2. 库存扣减处理器(优先执行) +@Component +public class InventoryDeductHandler implements IHandler { + @Override + public int order() { + return 0; // 最高优先级 + } + + @Override + public void handler(OrderCreatedEvent event) { + inventoryService.deduct(event.getOrderId(), event.getAmount()); + } +} + +// 3. 积分累加处理器(次优先) +@Component +public class PointsAccumulateHandler implements IHandler { + @Override + public int order() { + return 10; + } + + @Override + public void handler(OrderCreatedEvent event) { + pointsService.addPoints(event.getOrderId()); + } + + @Override + public void error(Exception exception) throws Exception { + // 积分失败不影响主流程,仅记录日志 + log.warn("Points accumulation failed for order: {}", + event.getOrderId(), exception); + } +} + +// 4. 发送通知处理器(最后执行) +@Component +public class NotificationSendHandler implements IHandler { + @Override + public int order() { + return 20; + } + + @Override + public void handler(OrderCreatedEvent event) { + notificationService.sendOrderConfirmation(event.getOrderId()); + } +} + +// 5. 在 Service 中推送事件 +@Service +public class OrderService { + public void createOrder(CreateOrderCommand cmd) { + Order order = new Order(cmd); + orderRepository.save(order); + + // 推送事件,三个 Handler 按 order 顺序依次执行 + EventPusher.push(new OrderCreatedEvent(order.getId(), order.getAmount())); + } +} +``` + +### 循环调用防护 + +当检测到事件循环时,框架会自动抛出 `EventLoopException`: + +```java +// 错误示例:A 触发 B,B 又触发 A +public class EventA implements ISyncEvent {} +public class EventB implements ISyncEvent {} + +@Component +public class HandlerA implements IHandler { + @Override + public void handler(EventA event) { + EventPusher.push(new EventB()); // 触发 B + } +} + +@Component +public class HandlerB implements IHandler { + @Override + public void handler(EventB event) { + EventPusher.push(new EventA()); // 再次触发 A → 抛出 EventLoopException + } +} +``` diff --git a/docs/capabilities/exception-handling.md b/docs/capabilities/exception-handling.md new file mode 100644 index 000000000..533da47e9 --- /dev/null +++ b/docs/capabilities/exception-handling.md @@ -0,0 +1,254 @@ +--- +name: exception-handling +description: 国际化异常体系与事件异常处理,支持多语言错误消息、占位符参数和事件循环检测 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +企业级应用的异常处理面临以下挑战: + +1. **错误消息硬编码**:异常信息写死在代码中,无法根据用户语言环境切换 +2. **错误码管理混乱**:不同模块使用不同的错误码格式,缺乏统一管理 +3. **占位符支持缺失**:类似"用户 {userId} 不存在"的消息需要手动拼接字符串 +4. **事件异常丢失**:多个事件处理器并发执行时,部分异常可能被吞掉 +5. **循环调用难排查**:事件之间形成循环依赖时,缺少清晰的堆栈信息 + +框架提供三层异常基础设施: +- `LocaleMessageException` — 基于 Spring MessageSource 的国际化业务异常 +- `EventException` — 聚合多个事件处理器异常的容器异常 +- `EventLoopException` — 事件循环调用的专用检测异常 + +配合 `ExceptionConfiguration` 自动装配和 `MessageContext` 全局上下文,实现开箱即用的国际化异常体系。 + +## 如何使用 + +### 1. 配置消息资源文件 + +在 `src/main/resources/` 下创建 messages 属性文件: + +```properties +# messages.properties (默认/中文) +USER_NOT_FOUND=用户不存在 +USER_DUPLICATE=用户名 {0} 已存在 +ORDER_AMOUNT_INVALID=订单金额 {0} 超出限制,最大允许 {1} +ACCOUNT_INSUFFICIENT=账户余额不足,当前余额 {0},需要 {1} + +# messages_en.properties (英文) +USER_NOT_FOUND=User not found +USER_DUPLICATE=Username {0} already exists +ORDER_AMOUNT_INVALID=Order amount {0} exceeds limit, maximum allowed {1} +ACCOUNT_INSUFFICIENT=Insufficient balance, current {0}, required {1} +``` + +### 2. 抛出 LocaleMessageException + +```java +// 简单错误码 +throw new LocaleMessageException("USER_NOT_FOUND"); + +// 带占位符参数 +throw LocaleMessageException.of("USER_DUPLICATE", username); +throw LocaleMessageException.of("ORDER_AMOUNT_INVALID", amount, maxAmount); + +// 带原始异常 +throw new LocaleMessageException("SYSTEM_ERROR", cause); + +// 直接指定消息(不走国际化) +throw new LocaleMessageException("CUSTOM_ERROR", "自定义错误消息"); +``` + +### 3. 获取错误码和消息 + +```java +try { + userService.create(cmd); +} catch (LocaleMessageException e) { + String errCode = e.getErrCode(); // "USER_DUPLICATE" + String errMsg = e.getErrMessage(); // "用户名 admin 已存在" +} +``` + +### 4. 处理 EventException + +当多个事件处理器中有多个抛出异常时,框架会将它们收集到 `EventException` 中: + +```java +try { + EventPusher.push(new OrderCreatedEvent(orderId)); +} catch (EventException e) { + List errors = e.getError(); + log.error("Event processing failed with {} errors", errors.size()); + for (Exception error : errors) { + log.error(" - {}", error.getMessage()); + } +} +``` + +### 5. 处理 EventLoopException + +当事件形成循环调用时自动抛出: + +```java +try { + EventPusher.push(new EventA()); +} catch (EventLoopException e) { + List> stack = e.getStack(); + log.error("Event loop detected! Stack: {}", stack); + // 输出: event loop error current event class:class EventA, + // history event stack:[class EventA, class EventB] +} +``` + +## 使用实例 + +### 完整的 Service 层异常处理 + +```java +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + public User createUser(CreateUserCommand cmd) { + // 1. 唯一性校验 - 带占位符的国际化异常 + if (userRepository.existsByName(cmd.getName())) { + throw LocaleMessageException.of("USER_DUPLICATE", cmd.getName()); + } + + // 2. 业务规则校验 + if (cmd.getAge() != null && cmd.getAge() < 0) { + throw LocaleMessageException.of("AGE_INVALID", cmd.getAge()); + } + + // 3. 创建用户 + User user = new User(); + user.setName(cmd.getName()); + user.setAge(cmd.getAge()); + + try { + userRepository.save(user); + } catch (DataAccessException e) { + // 包装底层异常为业务异常 + throw new LocaleMessageException("USER_CREATE_FAILED", e); + } + + return user; + } + + public User findById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new LocaleMessageException("USER_NOT_FOUND")); + } + + public void transferPoints(Long fromUserId, Long toUserId, int points) { + User fromUser = findById(fromUserId); + User toUser = findById(toUserId); + + if (fromUser.getPoints() < points) { + throw LocaleMessageException.of("POINTS_INSUFFICIENT", + fromUser.getPoints(), points); + } + + fromUser.setPoints(fromUser.getPoints() - points); + toUser.setPoints(toUser.getPoints() + points); + + userRepository.save(fromUser); + userRepository.save(toUser); + } +} +``` + +### Controller 层的全局异常处理配合 + +框架的 `LocaleMessageException` 通常由全局异常处理器捕获并转为标准响应: + +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理国际化业务异常 + */ + @ExceptionHandler(LocaleMessageException.class) + public Response handleLocaleMessageException(LocaleMessageException e) { + return Response.buildFailure(e.getErrCode(), e.getErrMessage()); + } + + /** + * 处理事件聚合异常 + */ + @ExceptionHandler(EventException.class) + public Response handleEventException(EventException e) { + log.error("Event processing errors", e); + return Response.buildFailure("EVENT_PROCESSING_ERROR", + "事件处理异常,共 " + e.getError().size() + " 个错误"); + } + + /** + * 处理事件循环异常 + */ + @ExceptionHandler(EventLoopException.class) + public Response handleEventLoopException(EventLoopException e) { + log.error("Event loop detected", e); + return Response.buildFailure("EVENT_LOOP_ERROR", + "检测到事件循环调用"); + } +} +``` + +### messages.properties 完整示例 + +```properties +# ===== 用户模块 ===== +USER_NOT_FOUND=用户不存在 +USER_DUPLICATE=用户名 {0} 已存在 +USER_DISABLED=用户 {0} 已被禁用 +USER_CREATE_FAILED=用户创建失败 +AGE_INVALID=年龄 {0} 无效,必须大于等于 0 + +# ===== 订单模块 ===== +ORDER_NOT_FOUND=订单 {0} 不存在 +ORDER_STATUS_INVALID=订单状态不允许此操作,当前状态: {0} +ORDER_AMOUNT_INVALID=订单金额 {0} 超出限制,最大允许 {1} +ORDER_ALREADY_PAID=订单 {0} 已支付,请勿重复操作 + +# ===== 账户模块 ===== +ACCOUNT_NOT_FOUND=账户 {0} 不存在 +ACCOUNT_INSUFFICIENT=账户余额不足,当前余额 {0},需要 {1} +ACCOUNT_FROZEN=账户 {0} 已冻结 + +# ===== 系统模块 ===== +SYSTEM_ERROR=系统内部错误 +PARAM_MISSING=缺少必要参数: {0} +PARAM_INVALID=参数 {0} 格式不正确 +UNAUTHORIZED=未授权访问 +FORBIDDEN=无权执行此操作 +``` + +### 异常类层次结构 + +``` +RuntimeException + ├── LocaleMessageException — 国际化业务异常 + │ └── getErrCode() — 错误码 + │ └── getErrMessage() — 国际化后的错误消息 + │ + ├── EventException — 事件处理器聚合异常 + │ └── getError(): List — 所有处理器抛出的异常列表 + │ + └── EventLoopException — 事件循环调用异常 + └── getStack(): List> — 事件调用栈(按触发顺序) +``` + +### 关键设计要点 + +1. **errCode 是稳定的标识符**:前端应根据 errCode 而非 errMessage 做逻辑判断,因为 errMessage 会随语言变化 +2. **占位符从 {0} 开始**:遵循 Java MessageFormat 规范 +3. **LocaleMessage 自动初始化**:通过 `ExceptionConfiguration` 的 `@Bean(initMethod = "init")` 自动注册到 `MessageContext` +4. **EventException 保留原始异常链**:通过 `addSuppressed()` 将所有子异常附加到主异常上,确保日志可追溯 +5. **EventLoopException 包含完整调用栈**:stack 列表按事件触发顺序排列,便于定位循环点 +6. **线程安全**:`MessageContext` 使用单例模式 + synchronized 保证初始化安全;`LocaleContextHolder` 保证多语言请求隔离 diff --git a/docs/capabilities/fast-repository.md b/docs/capabilities/fast-repository.md new file mode 100644 index 000000000..442b7f783 --- /dev/null +++ b/docs/capabilities/fast-repository.md @@ -0,0 +1,213 @@ +--- +name: fast-repository +description: JPA Repository 增强,支持基于 PageRequest 的动态过滤查询、HQL 构建和 SearchRequest 自动解析 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +Spring Data JPA 的 `JpaRepository` 提供了基础的 CRUD 操作,但在实际业务中存在以下不足: + +1. **动态条件查询繁琐**:需要手动编写 `Specification` 或 `@Query`,每个查询场景都要新建方法 +2. **前端过滤条件无法直传**:前端传来的 JSON 过滤参数需要逐字段解析再拼装查询 +3. **Example 查询局限**:Spring Data Example 只支持等值和部分模糊匹配,不支持 BETWEEN、IN、大于小于等关系 +4. **分页与过滤割裂**:分页参数和过滤条件分散在不同对象中,缺乏统一封装 +5. **原生 SQL 构建困难**:复杂动态查询需要手写字符串拼接,容易出错且不安全 + +`FastRepository` 扩展了 `JpaRepository` + `JpaSpecificationExecutor`,内置三种查询模式: +- `findAll(PageRequest)` — 使用 Spring Data Example 进行简单等值/模糊查询 +- `pageRequest(PageRequest)` — 使用 HQL 动态构建,支持全部 `Relation` 关系 +- `searchRequest(SearchRequest)` — 直接从 HTTP 请求参数解析并执行查询 + +## 如何使用 + +### 1. 定义 Repository 接口 + +只需继承 `FastRepository`,无需额外代码: + +```java +public interface UserRepository extends FastRepository { + // 自动获得 findAll(PageRequest)、pageRequest(PageRequest)、searchRequest(SearchRequest) 等方法 +} +``` + +### 2. 使用 findAll(PageRequest) — Example 模式 + +适用于简单的等值和 LIKE 查询,底层使用 Spring Data Example: + +```java +PageRequest request = PageRequest.of(0, 20); +request.addFilter("name", "张三"); // EQUAL +request.addFilter("email", Relation.LIKE, "@example.com"); + +Page page = userRepository.findAll(request); +``` + +> **注意**:Example 模式仅支持有限的 Relation 类型。如需完整关系支持,请使用 `pageRequest()` 方法。 + +### 3. 使用 pageRequest(PageRequest) — HQL 模式 + +支持所有 `Relation` 枚举定义的比较关系,底层动态生成 HQL: + +```java +PageRequest request = PageRequest.of(0, 20) + .addFilter("age", Relation.GREATER_THAN, 18) + .addFilter("createTime", Relation.BETWEEN, startDate, endDate) + .addFilter("status", Relation.IN, 1, 2, 3) + .addSort(Sort.by("createTime").descending()); + +Page page = userRepository.pageRequest(request); +``` + +### 4. 使用 searchRequest(SearchRequest) — HTTP 自动解析模式 + +直接将 HTTP 请求参数转为查询条件,适合前后端联调: + +```java +@GetMapping("/users") +public MultiResponse list(SearchRequest searchRequest) { + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page); +} +``` + +前端可通过 URL 参数传递过滤条件: +``` +GET /api/users?current=0&pageSize=20&name=张三&age=18 +``` + +或通过 Base64 编码的 filter/sort 参数传递复杂条件。 + +### 5. 结合 DynamicNativeRepository + +`FastRepository` 同时继承了 `DynamicNativeRepository`,支持原生 SQL 动态查询(具体用法参见相关文档)。 + +## 使用实例 + +### 完整的用户管理 CRUD + 动态查询 + +```java +// Entity +@Entity +@Table(name = "t_user") +public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String email; + private Integer age; + private Integer status; + private LocalDateTime createTime; +} + +// Repository +public interface UserRepository extends FastRepository { + // 可继续添加自定义查询方法 + List findByStatus(Integer status); +} + +// Service +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + /** + * 简单等值查询 + */ + public Page findByName(String name, int page, int size) { + PageRequest request = PageRequest.of(page, size); + request.addFilter("name", name); + return userRepository.findAll(request); + } + + /** + * 复杂条件查询 + */ + public Page advancedSearch(UserSearchCriteria criteria) { + PageRequest request = PageRequest.of(criteria.getPage(), criteria.getSize()) + .addSort(Sort.by("createTime").descending()); + + if (StringUtils.hasText(criteria.getName())) { + request.addFilter("name", Relation.LIKE, criteria.getName()); + } + if (criteria.getMinAge() != null) { + request.addFilter("age", Relation.GREATER_THAN_EQUAL, criteria.getMinAge()); + } + if (criteria.getMaxAge() != null) { + request.addFilter("age", Relation.LESS_THAN_EQUAL, criteria.getMaxAge()); + } + if (criteria.getStatuses() != null && !criteria.getStatuses().isEmpty()) { + request.addFilter("status", Relation.IN, + criteria.getStatuses().toArray()); + } + if (criteria.getStartDate() != null && criteria.getEndDate() != null) { + request.addFilter("createTime", Relation.BETWEEN, + criteria.getStartDate(), criteria.getEndDate()); + } + + return userRepository.pageRequest(request); + } + + /** + * HTTP 参数自动解析查询 + */ + public Page searchFromHttp(SearchRequest searchRequest) { + return userRepository.searchRequest(searchRequest); + } +} + +// Controller +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/simple") + public MultiResponse simpleSearch( + @RequestParam(required = false) String name, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + Page result = userService.findByName(name, page, size); + return MultiResponse.of(result); + } + + @GetMapping("/advanced") + public MultiResponse advancedSearch(UserSearchCriteria criteria) { + Page result = userService.advancedSearch(criteria); + return MultiResponse.of(result); + } + + @GetMapping + public MultiResponse autoSearch(SearchRequest searchRequest) { + Page result = userService.searchFromHttp(searchRequest); + return MultiResponse.of(result); + } +} +``` + +### 三种查询模式对比 + +| 特性 | `findAll(PageRequest)` | `pageRequest(PageRequest)` | `searchRequest(SearchRequest)` | +|------|------------------------|----------------------------|--------------------------------| +| 底层实现 | Spring Data Example | 动态 HQL | 转 PageRequest → HQL | +| 支持的 Relation | EQUAL, LIKE (有限) | 全部 14 种 | 全部 14 种 | +| 组合条件 (AND/OR) | ❌ | ✅ | ✅ | +| HTTP 参数自动解析 | ❌ | ❌ | ✅ | +| 嵌套属性查询 | 有限支持 | ✅ | ✅ | +| 性能 | 较好 | 良好 | 良好 | +| 适用场景 | 简单等值筛选 | 复杂业务查询 | 前后端联调/通用列表页 | + +### 注意事项 + +1. **实体类要求**:过滤字段名必须与实体类的属性名一致(不是数据库列名) +2. **类型匹配**:过滤值的类型应与实体字段类型兼容,框架会尝试自动转换 +3. **空值处理**:当过滤值为 null 或空字符串时,该条件会被跳过 +4. **SQL 注入防护**:HQL 模式使用参数化查询,不存在 SQL 注入风险 +5. **分页起始页**:`PageRequest.of(page, size)` 中 page 从 0 开始 diff --git a/docs/capabilities/fastjson.md b/docs/capabilities/fastjson.md new file mode 100644 index 000000000..d55242be6 --- /dev/null +++ b/docs/capabilities/fastjson.md @@ -0,0 +1,121 @@ +--- +name: fastjson +description: 高性能 JSON 序列化/反序列化库 +status: 已实现 +scope: 后端 +source: 框架:Fastjson +--- + +## 解决什么问题 + +在框架中需要频繁进行 Java 对象与 JSON 字符串之间的转换,包括: +- Response 统一响应体的序列化 +- Token 对象的 JSON 序列化/反序列化 +- DTO 数据的 Map 转换 +- Security Filter 中的请求/响应体 JSON 处理 +- Flow 工作流模块的数据快照绑定 + +Fastjson 提供了比 Jackson 更简洁的 API(`JSONObject.toJSONString()` / `JSONObject.parseObject()`),适合框架内部轻量级序列化场景。 + +## 如何使用 + +### 依赖引入 + +```xml + + + com.alibaba.fastjson2 + fastjson2 + 2.0.53 + +``` + +### 核心接口:JsonSerializable + +框架定义了 `JsonSerializable` 接口,所有需要 JSON 序列化的 DTO/实体类实现此接口即可获得 `toJson()` 方法: + +```java +public interface JsonSerializable extends Serializable { + default String toJson() { + return JSONObject.toJSONString(this); + } +} +``` + +### 核心接口:MapSerializable + +提供对象到 Map 的转换能力: + +```java +public interface MapSerializable { + default Map toMap() { + return JSON.parseObject(JSON.toJSONString(this)); + } +} +``` + +## 使用实例 + +### 1. Token 序列化(JWT 模式) + +```java +// JwtTokenGateway.java - 创建 JWT Token +public Token create(String username, String iv, List authorities, String extra) { + Token token = new Token(username, iv, extra, authorities, validTime, restTime); + // 将 Token 对象序列化为 JSON 作为 JWT subject + String jwt = Jwts.builder().subject(token.toJson()).signWith(key).compact(); + token.setToken(jwt); + return token; +} + +// 解析 JWT Token +public Token parser(String sign) { + Jws jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(sign); + String subject = jws.getPayload().getSubject(); + // 从 JSON 反序列化为 Token 对象 + return JSONObject.parseObject(subject, Token.class); +} +``` + +### 2. Security Filter 中的 JSON 响应 + +```java +// MyAuthenticationFilter.java / MyLoginFilter.java 等 +// 使用 JSONObject 构建错误响应 +JSONObject json = new JSONObject(); +json.put("errCode", "401"); +json.put("errMessage", "认证失败"); +response.getWriter().write(json.toJSONString()); +``` + +### 3. Redis Token 存储 + +```java +// RedisTokenGateway.java - 使用 fastjson2 进行 Token 持久化 +import com.alibaba.fastjson2.JSONObject; + +// Token 序列化存入 Redis +String json = token.toJson(); +redisTemplate.opsForValue().set(key, json); + +// 从 Redis 读取并反序列化 +String json = redisTemplate.opsForValue().get(key); +Token token = JSONObject.parseObject(json, Token.class); +``` + +### 4. Flow 数据快照绑定 + +```java +// BindDataSnapshot.java / FlowMapBindData.java +// 使用 JSONObject 处理流程数据快照 +JSONObject data = new JSONObject(); +data.put("key", value); +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter | `JsonSerializable`、`MapSerializable` 接口定义 | +| springboot-starter-security | Token 序列化、Filter JSON 响应、JWT/Redis 网关 | +| springboot-starter-flow | 数据快照绑定、Schema 解析 | diff --git a/docs/capabilities/groovy-script-engine.md b/docs/capabilities/groovy-script-engine.md new file mode 100644 index 000000000..915286669 --- /dev/null +++ b/docs/capabilities/groovy-script-engine.md @@ -0,0 +1,237 @@ +--- +name: groovy-script-engine +description: Groovy 脚本运行时编译、缓存与热更新引擎,提供 REST API 管理脚本生命周期 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在企业应用中,部分业务规则(如流程节点的标题生成、操作者匹配、数据校验等)需要在不重启服务的情况下动态调整。硬编码这些规则会导致每次变更都需要重新部署,而使用配置化方案又难以表达复杂逻辑。 + +`springboot-starter-script` 提供了一个基于 Groovy 的脚本引擎,支持: + +- **运行时编译**:通过 `GroovyScript.compile(cache)` 在运行时编译 Groovy 脚本 +- **LRU 缓存**:编译后的脚本对象通过 `GroovyScriptRuntimeContext` 缓存,避免重复编译 +- **热更新**:修改脚本后重新调用 `compile(true)` 即可生效,无需重启 +- **元数据扫描**:通过注解(`@ScriptType`、`@ScriptField`、`@ScriptFunction`、`@ScriptParameter`)自动提取脚本的类型、字段、方法等元数据信息 +- **策略扩展**:通过 `GroovyTypeFixStrategy`、`GroovyMetadataGenerateStrategy`、`ScriptTypeMapping` 三种策略接口扩展脚本处理逻辑 +- **REST API**:内置 `GroovyScriptController` 提供脚本编译、查询、保存等 HTTP 接口 +- **临时脚本**:支持临时存储和定时清理机制,方便开发调试 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-script + +``` + +### 2. 配置属性 + +在 `application.properties` 中配置脚本引擎参数: + +```properties +# 脚本引擎配置前缀 +codingapi.script.*=... +``` + +配置通过 `GroovyScriptProperties` 绑定,由 `AutoConfiguration` 自动注册。 + +### 3. 创建和执行脚本 + +使用 `GroovyScript.Builder` 构建脚本对象: + +```java +GroovyScript script = GroovyScript.builder("my_script_key") + .script("return 'Hello, ' + name") + .description("问候脚本") + .build(); + +// 编译脚本(启用缓存) +script.compile(true); + +// 执行脚本 +Map binds = Map.of("name", "World"); +String result = script.invoke(TransactionMode.NONE, binds); +``` + +核心 API: +- `GroovyScript.builder(key)` — 创建构建器 +- `builder.script(String)` — 设置脚本内容 +- `builder.description(String)` — 设置描述 +- `script.compile(boolean cache)` — 编译脚本,`cache=true` 时缓存编译结果 +- `script.invoke(TransactionMode, Map binds, Object... requests)` — 执行脚本 +- `script.save()` — 持久化脚本到仓储 +- `script.temp()` — 临时存储脚本(会被定时清理) +- `script.toMetadata()` — 提取脚本元数据 + +### 4. 脚本元数据注解 + +通过注解为脚本添加结构化元数据: + +```groovy +@ScriptType(name = "LeaveApproval", description = "请假审批脚本") +class LeaveApproval { + + @ScriptField(description = "请假天数") + int days + + @ScriptFunction(description = "计算审批级别") + int calculateLevel( + @ScriptParameter(description = "员工职级") int rank + ) { + return rank > 5 ? 2 : 1 + } +} +``` + +框架通过 `GroovyMetadataScannerUtils.scanner(groovyScript)` 自动扫描这些注解,生成 `GroovyMetadata` 对象,包含 `GroovyType`、`GroovyField`、`GroovyFunction` 等信息。 + +### 5. 扩展策略 + +#### 类型修复策略 (GroovyTypeFixStrategy) + +当需要对特定类型的脚本进行额外处理时: + +```java +GroovyTypeFixStrategyContext.getInstance().addFixStrategy( + new GroovyTypeFixStrategy() { + @Override + public boolean support(Class clazz) { + return MyInterface.class.isAssignableFrom(clazz); + } + + @Override + public void fix(GroovyScript groovyScript, GroovyType groovyType) { + // 对 groovyType 进行修复或增强 + } + } +); +``` + +#### 元数据生成策略 (GroovyMetadataGenerateStrategy) + +自定义元数据生成逻辑(优先于注解扫描): + +```java +GroovyMetadataGenerateStrategyContext.getInstance().addGenerateStrategy( + new GroovyMetadataGenerateStrategy() { + @Override + public boolean support(GroovyScript groovyScript) { + return "special_key".equals(groovyScript.getKey()); + } + + @Override + public GroovyMetadata generate(GroovyScript groovyScript) { + // 手动构建元数据 + return customMetadata; + } + } +); +``` + +#### 类型映射策略 (ScriptTypeMapping) + +将脚本中的类型映射为目标类型: + +```java +ScriptTypeMappingContext.getInstance().addMapping( + new ScriptTypeMapping() { + @Override + public boolean support(Class target) { + return target == OriginalType.class; + } + + @Override + public Class mapping(Class target) { + return MappedType.class; + } + } +); +``` + +### 6. REST API + +框架内置了 `GroovyScriptController`,提供以下端点: + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/groovy-script/compile` | 编译脚本 | +| GET | `/api/groovy-script/getScript?key=xxx` | 获取脚本内容 | +| GET | `/api/groovy-script/getMetadata?key=xxx` | 获取脚本元数据 | +| POST | `/api/groovy-script/save` | 保存脚本 | + +编译请求体 (`ScriptCompileRequest`): +```json +{ + "script": "return 'hello'", + "cache": true +} +``` + +保存请求体 (`ScriptSaveRequest`): +```json +{ + "key": "my_script_key", + "script": "return 'updated content'" +} +``` + +## 使用实例 + +### 脚本生命周期管理 + +```java +// 1. 创建脚本 +GroovyScript script = GroovyScript.builder("order_validator") + .script(""" + boolean validate(Map order) { + return order.amount > 0 && order.items != null + } + """) + .description("订单校验脚本") + .build(); + +// 2. 临时存储(用于开发调试,会被定时清理) +script.temp(); + +// 3. 编译并缓存 +script.compile(true); + +// 4. 执行 +Map binds = Map.of("order", orderData); +Boolean valid = script.invoke(TransactionMode.NONE, binds); + +// 5. 确认无误后持久化 +script.save(); +``` + +### 引擎启动与关闭 + +`GroovyScriptEngineRunner` 实现了 `InitializingBean` 和 `DisposableBean`: + +- **启动时**(`afterPropertiesSet`):从 `TempGroovyScriptRepository` 加载所有临时脚本到内存缓存 +- **关闭时**(`destroy`):将内存中的临时脚本持久化到 `TempGroovyScriptRepository` + +这确保了服务重启后临时脚本不会丢失。 + +### 策略上下文清理 + +所有策略上下文都提供了 `clear()` 方法,可在测试或重新初始化时清空已注册的策略: + +```java +GroovyTypeFixStrategyContext.getInstance().clear(); +GroovyMetadataGenerateStrategyContext.getInstance().clear(); +ScriptTypeMappingContext.getInstance().clear(); +``` + +### 自动配置 + +`AutoConfiguration` 类通过 `@ComponentScan(basePackages = "com.codingapi.springboot.script")` 自动扫描脚本模块的所有组件,并注册: +- `GroovyScriptEngineRunner` Bean — 负责引擎生命周期管理 +- `GroovyScriptProperties` Bean — 绑定 `codingapi.script.*` 配置项 diff --git a/docs/capabilities/http-client-factory.md b/docs/capabilities/http-client-factory.md new file mode 100644 index 000000000..e2b05e6db --- /dev/null +++ b/docs/capabilities/http-client-factory.md @@ -0,0 +1,165 @@ +--- +name: http-client-factory +description: 信任所有证书的 Apache HttpClient 5 工厂,用于内部 HTTPS 调用跳过证书验证 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在微服务架构或企业内部系统集成中,服务间通信常使用自签名证书或内部 CA 签发的证书。标准的 Apache HttpClient 会严格校验 SSL 证书链,导致请求失败并抛出 `SSLHandshakeException`。 + +`TrustAnyHttpClientFactory` 提供了一个静态工厂方法,创建跳过 SSL 证书验证的 Apache HttpClient 5 实例。它同时被 `RestTemplateContext` 作为底层 HttpClient 使用,也可以独立用于任何需要信任所有证书的场景。 + +核心设计: +- **内部 TrustManager**:`TrustAnyTrustManager` 实现了 `X509TrustManager`,`checkClientTrusted` 和 `checkServerTrusted` 均为空实现 +- **NoopHostnameVerifier**:不校验服务器主机名与证书的匹配 +- **双协议支持**:通过 `Registry` 同时注册 HTTP(`PlainConnectionSocketFactory`)和 HTTPS(`SSLConnectionSocketFactory`) +- **循环重定向**:默认开启 `circularRedirectsAllowed` + +## 如何使用 + +### 1. 创建信任所有证书的 HttpClient + +```java +import com.codingapi.springboot.framework.utils.TrustAnyHttpClientFactory; +import org.apache.hc.client5.http.classic.HttpClient; + +HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); +``` + +### 2. 配合 RestTemplate 使用 + +```java +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +HttpComponentsClientHttpRequestFactory factory = + new HttpComponentsClientHttpRequestFactory( + TrustAnyHttpClientFactory.createTrustAnyHttpClient() + ); +RestTemplate restTemplate = new RestTemplate(factory); +``` + +### 3. 直接使用 HttpClient 执行请求 + +```java +HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); + +// 使用 Apache HC5 API 执行请求 +httpClient.execute(new HttpGet("https://internal-service.example.com/api/data"), response -> { + int statusCode = response.getCode(); + String body = EntityUtils.toString(response.getEntity()); + // 处理响应... + return body; +}); +``` + +## 使用实例 + +### 自定义 HttpClient 配置 + +如需在信任所有证书的基础上添加额外配置(如超时、连接池等),可参考 `TrustAnyHttpClientFactory` 的实现模式自行扩展: + +```java +@SneakyThrows +public static HttpClient createCustomTrustAnyHttpClient() { + // 1. 创建信任所有证书的 SSL 上下文 + SSLContext sslContext = SSLContext.getInstance("TLS"); + X509TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + @Override + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + }; + sslContext.init(null, new TrustManager[]{trustManager}, null); + + // 2. 创建 SSL 连接工厂 + SSLConnectionSocketFactory sslFactory = + new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + + // 3. 注册协议 + Registry registry = RegistryBuilder.create() + .register("https", sslFactory) + .register("http", new PlainConnectionSocketFactory()) + .build(); + + // 4. 配置请求参数 + RequestConfig requestConfig = RequestConfig.custom() + .setCircularRedirectsAllowed(true) + .setResponseTimeout(Timeout.ofSeconds(30)) + .setConnectTimeout(Timeout.ofSeconds(10)) + .build(); + + // 5. 构建 HttpClient + BasicHttpClientConnectionManager connManager = + new BasicHttpClientConnectionManager(registry); + + return HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(connManager) + .build(); +} +``` + +### 内部类 TrustAnyTrustManager + +`TrustAnyTrustManager` 是 `TrustAnyHttpClientFactory` 的内部私有类,实现了 `X509TrustManager` 接口: + +```java +private static class TrustAnyTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + // 空实现:信任所有客户端证书 + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + // 空实现:信任所有服务端证书 + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} +``` + +### 与其他模块的关系 + +`TrustAnyHttpClientFactory` 被 `RestTemplateContext` 直接引用作为底层 HttpClient 的创建方式: + +```java +// RestTemplateContext 构造函数 +private RestTemplateContext() { + HttpComponentsClientHttpRequestFactory requestFactory = + new HttpComponentsClientHttpRequestFactory( + TrustAnyHttpClientFactory.createTrustAnyHttpClient() + ); + this.restTemplate = restTemplate(requestFactory); +} +``` + +如果只需要底层的 HttpClient 而不需要 RestTemplate 封装,可以直接使用 `TrustAnyHttpClientFactory`。 + +### ⚠️ 安全提示 + +此工厂创建的 HttpClient **完全跳过了 SSL/TLS 证书验证**,包括: +- 不验证证书链是否由受信任的 CA 签发 +- 不验证证书是否过期 +- 不验证服务器主机名是否与证书中的 CN/SAN 匹配 + +**仅应在以下场景使用:** +- 内部微服务间的 HTTPS 通信(自签名证书) +- 开发/测试环境调试 +- 对接已知可信但证书不规范的第三方系统 + +**不应在以下场景使用:** +- 面向公网的用户数据传输 +- 涉及敏感数据的跨组织通信 +- 任何需要保证传输安全性的生产环境外部调用 diff --git a/docs/capabilities/httpclient5.md b/docs/capabilities/httpclient5.md new file mode 100644 index 000000000..fa5f4d0bf --- /dev/null +++ b/docs/capabilities/httpclient5.md @@ -0,0 +1,119 @@ +--- +name: httpclient5 +description: HTTP 客户端库,支持 HTTPS 信任所有证书的请求 +status: 已实现 +scope: 后端 +source: 框架:Apache HttpClient 5 +--- + +## 解决什么问题 + +框架需要发起外部 HTTP/HTTPS 请求的场景: +- 对接第三方服务时需要跳过 SSL 证书验证(开发/测试环境) +- 通过 `RestTemplate` 发送 HTTP 请求时底层使用高性能连接管理 +- 支持代理配置的企业内网环境 + +Apache HttpClient 5 提供了成熟的 HTTP 客户端实现,项目中主要用于创建信任所有证书的 `HttpClient` 实例,作为 Spring `RestTemplate` 的底层传输层。 + +## 如何使用 + +### 依赖引入 + +```xml + + + org.apache.httpcomponents.client5 + httpclient5 + +``` + +### 核心类 + +- `TrustAnyHttpClientFactory` — 创建信任所有证书的 HttpClient 工厂 +- `RestTemplateContext` — 基于 HttpClient 5 的 RestTemplate 单例上下文 +- `HttpClient` — 框架封装的 HTTP 客户端(GET/POST) +- `HttpRequest` — 底层请求执行器(支持代理、重定向) + +## 使用实例 + +### 1. 创建信任所有证书的 HttpClient + +```java +// TrustAnyHttpClientFactory.java +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; + +public class TrustAnyHttpClientFactory { + + public static HttpClient createTrustAnyHttpClient() { + // 创建信任所有证书的 SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{new TrustAnyTrustManager()}, null); + + // 使用 NoopHostnameVerifier 跳过主机名验证 + SSLConnectionSocketFactory sslSocketFactory = + new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + + // 注册 HTTP 和 HTTPS 协议 + Registry registry = RegistryBuilder + .create() + .register("https", sslSocketFactory) + .register("http", new PlainConnectionSocketFactory()) + .build(); + + BasicHttpClientConnectionManager connectionManager = + new BasicHttpClientConnectionManager(registry); + + return HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom() + .setCircularRedirectsAllowed(true).build()) + .setConnectionManager(connectionManager) + .build(); + } +} +``` + +### 2. RestTemplate 集成 + +```java +// RestTemplateContext.java - 使用 HttpClient 5 作为底层传输 +public class RestTemplateContext { + private static RestTemplateContext instance; + private final RestTemplate restTemplate; + + private RestTemplateContext() { + // 使用 Apache HttpClient 5 创建请求工厂 + HttpComponentsClientHttpRequestFactory requestFactory = + new HttpComponentsClientHttpRequestFactory( + TrustAnyHttpClientFactory.createTrustAnyHttpClient() + ); + this.restTemplate = new RestTemplate(requestFactory); + } + + public static RestTemplateContext getInstance() { /* 双重检查锁单例 */ } +} +``` + +### 3. 框架封装的 HttpClient + +```java +// HttpClient.java - 简洁的 HTTP 调用 API +HttpClient client = new HttpClient(); + +// POST JSON 请求 +String result = client.post(url, headers, jsonObject); + +// POST 表单请求 +String result = client.post(url, headers, formData); + +// GET 请求 +String result = client.get(url, headers, uriVariables); +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter | `TrustAnyHttpClientFactory`、`RestTemplateContext`、`HttpClient`、`HttpRequest` | diff --git a/docs/capabilities/index.md b/docs/capabilities/index.md new file mode 100644 index 000000000..3af101158 --- /dev/null +++ b/docs/capabilities/index.md @@ -0,0 +1,42 @@ +# 能力(Capabilities) + +> 🔄 此文件由 `rebuild_pkr_index.py` 自动生成,请勿手动编辑。 + +## ✅ 已实现 + +| 名称 | 描述 | 范围 | 来源 | +|------|------|------|------| +| [apache-groovy](./apache-groovy.md) | 动态语言运行时,支持脚本编译与执行 | 后端 | 框架:Apache Groovy | +| [bouncycastle](./bouncycastle.md) | 密码学算法库,提供 AES/RSA 等加密算法支持 | 后端 | 框架:BouncyCastle | +| [byte-buddy](./byte-buddy.md) | 字节码操作库,用于运行时动态生成 JPA 实体类 | 后端 | 框架:Byte Buddy | +| [command-executor](./command-executor.md) | CQRS 命令执行器接口,定义标准化的命令-响应契约,支持带参和无参两种执行模式 | 后端 | 项目自有 | +| [commons-crypto](./commons-crypto.md) | Apache 加密工具封装,提供 AES 等加密操作 | 后端 | 框架:Apache Commons Crypto | +| [commons-io](./commons-io.md) | IO 工具类库,简化文件/流操作 | 后端 | 框架:Apache Commons IO | +| [commons-text](./commons-text.md) | 文本处理工具库,提供字符串命名转换等功能 | 后端 | 框架:Apache Commons Text | +| [data-authorization](./data-authorization.md) | 基于 JDBC 代理链的透明数据权限过滤,支持行级和列级权限控制 | 后端 | 项目自有 | +| [domain-event](./domain-event.md) | 领域实体事件自动追踪机制,支持创建、变更、删除事件的自动推送与字段级变更检测 | 后端 | 项目自有 | +| [event-system](./event-system.md) | 基于发布-订阅模式的事件系统,支持同步/异步事件处理、事件排序和异常隔离 | 后端 | 项目自有 | +| [exception-handling](./exception-handling.md) | 国际化异常体系与事件异常处理,支持多语言错误消息、占位符参数和事件循环检测 | 后端 | 项目自有 | +| [fast-repository](./fast-repository.md) | JPA Repository 增强,支持基于 PageRequest 的动态过滤查询、HQL 构建和 SearchRequest 自动解析 | 后端 | 项目自有 | +| [fastjson](./fastjson.md) | 高性能 JSON 序列化/反序列化库 | 后端 | 框架:Fastjson | +| [groovy-script-engine](./groovy-script-engine.md) | Groovy 脚本运行时编译、缓存与热更新引擎,提供 REST API 管理脚本生命周期 | 后端 | 项目自有 | +| [http-client-factory](./http-client-factory.md) | 信任所有证书的 Apache HttpClient 5 工厂,用于内部 HTTPS 调用跳过证书验证 | 后端 | 项目自有 | +| [httpclient5](./httpclient5.md) | HTTP 客户端库,支持 HTTPS 信任所有证书的请求 | 后端 | 框架:Apache HttpClient 5 | +| [jackson-databind](./jackson-databind.md) | JSON 数据绑定库,Spring Boot 默认 JSON 处理器 | 后端 | 框架:Jackson Databind | +| [jjwt](./jjwt.md) | JWT 令牌生成与解析库 | 后端 | 框架:JJWT | +| [jsqlparser](./jsqlparser.md) | SQL 解析与改写库,用于数据权限 SQL 注入 | 后端 | 框架:JSqlParser | +| [kryo](./kryo.md) | 高性能 Java 序列化框架,用于数据快照 | 后端 | 框架:Kryo | +| [page-request-filter](./page-request-filter.md) | 动态查询过滤请求封装,支持多种比较关系、组合条件、HTTP 参数自动解析 | 后端 | 项目自有 | +| [rest-template-context](./rest-template-context.md) | 预配置的 RestTemplate 单例上下文,内置信任所有证书的 HttpClient,简化内部服务间 HTTP 调用 | 后端 | 项目自有 | +| [security-auth](./security-auth.md) | 基于 Spring Security 的 JWT/Redis 双模式认证框架,支持 Token 自动续期与登录扩展 | 后端 | 项目自有 | +| [spring-data-jpa](./spring-data-jpa.md) | ORM 映射、Repository 抽象、自动分页 | 后端 | 框架:Spring Data JPA | +| [spring-data-redis](./spring-data-redis.md) | Redis 客户端抽象,支持缓存和有状态认证 Token 存储 | 后端 | 框架:Spring Data Redis | +| [spring-ioc-container](./spring-ioc-container.md) | Spring IoC 容器提供依赖注入、Bean 生命周期管理、AOP 支持 | 后端 | 框架:Spring Framework | +| [spring-security](./spring-security.md) | 认证授权框架,提供 Filter Chain、CSRF 防护 | 后端 | 框架:Spring Security | +| [spring-web-mvc](./spring-web-mvc.md) | Web MVC 框架,提供 RESTful API 路由、请求参数绑定、响应序列化 | 后端 | 框架:Spring Web MVC | +| [unified-response](./unified-response.md) | 统一 API 响应封装,提供 Response、SingleResponse、MultiResponse、MapResponse 四种标准响应格式 | 后端 | 项目自有 | +| [workflow-engine](./workflow-engine.md) | 内置工作流引擎,支持流程定义、节点流转、审批、退回、会签、抄送、委托与数据快照 | 后端 | 项目自有 | + +--- + +**统计**: 共 30 篇 — 已实现 30 / 计划中 0 / 已废弃 0 diff --git a/docs/capabilities/jackson-databind.md b/docs/capabilities/jackson-databind.md new file mode 100644 index 000000000..5992e9b9a --- /dev/null +++ b/docs/capabilities/jackson-databind.md @@ -0,0 +1,124 @@ +--- +name: jackson-databind +description: JSON 数据绑定库,Spring Boot 默认 JSON 处理器 +status: 已实现 +scope: 后端 +source: 框架:Jackson Databind +--- + +## 解决什么问题 + +作为 Spring Boot 默认的 JSON 处理库,Jackson Databind 在框架中承担: +- Controller 层请求体(`@RequestBody`)的自动反序列化 +- Controller 层响应体的自动序列化 +- 自定义枚举序列化(将 `IEnum` 接口实现类序列化为 code 值而非名称) +- 与 Spring MVC 的消息转换器(`MappingJackson2HttpMessageConverter`)集成 + +虽然框架内部使用 Fastjson 做轻量级序列化,但 Jackson 仍然是 Spring MVC 层面的默认 JSON 处理器,负责所有 REST API 的请求/响应转换。 + +## 如何使用 + +### 依赖引入 + +```xml + + + org.springframework.boot + spring-boot-starter-web + +``` + +### 核心用法 + +#### 自定义枚举序列化器 + +框架定义了 `EnumSerializer`,将实现 `IEnum` 接口的枚举序列化为数字 code 值: + +```java +// EnumSerializer.java +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class EnumSerializer extends JsonSerializer { + + @Override + public void serialize(IEnum iEnum, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + // 将枚举序列化为 code 数字,而非枚举名称字符串 + jsonGenerator.writeNumber(iEnum.getCode()); + } +} +``` + +## 使用实例 + +### 1. IEnum 接口定义 + +```java +// IEnum.java - 枚举编码接口 +public interface IEnum { + int getCode(); +} + +// 使用示例 +public enum ApprovalType implements IEnum { + APPROVE(1), + REJECT(2), + WITHDRAW(3); + + private final int code; + ApprovalType(int code) { this.code = code; } + + @Override + public int getCode() { return code; } +} +``` + +### 2. 配置 Jackson 使用自定义序列化器 + +```java +// 在 Jackson 配置中注册 EnumSerializer +@Configuration +public class JacksonConfiguration { + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addSerializer(IEnum.class, new EnumSerializer()); + mapper.registerModule(module); + return mapper; + } +} +``` + +### 3. Controller 中的自动序列化 + +```java +// 当 Controller 返回包含 IEnum 字段的对象时, +// Jackson 自动使用 EnumSerializer 将枚举序列化为数字 +@GetMapping("/flow/nodes") +public MultiResponse getNodes() { + // FlowNodeDTO.approvalType 字段如果实现了 IEnum, + // 会被序列化为 {"approvalType": 1} 而非 {"approvalType": "APPROVE"} + return Response.buildSuccess(nodeService.getNodes()); +} +``` + +### 4. 与 Fastjson 的分工 + +| 场景 | 使用的库 | 说明 | +|------|----------|------| +| Spring MVC 请求/响应 | Jackson | `@RequestBody` / 返回值自动序列化 | +| 框架内部 DTO 序列化 | Fastjson | `JsonSerializable.toJson()` | +| Token JWT payload | Fastjson | `JSONObject.toJSONString(token)` | +| 枚举 code 序列化 | Jackson | `EnumSerializer` 自定义序列化器 | +| Security Filter JSON 响应 | Fastjson | 手动构建 JSONObject | + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter | `EnumSerializer` 自定义枚举序列化 | +| 所有 Web 模块 | Spring MVC 默认 JSON 消息转换 | diff --git a/docs/capabilities/jjwt.md b/docs/capabilities/jjwt.md new file mode 100644 index 000000000..8cad670f0 --- /dev/null +++ b/docs/capabilities/jjwt.md @@ -0,0 +1,146 @@ +--- +name: jjwt +description: JWT 令牌生成与解析库 +status: 已实现 +scope: 后端 +source: 框架:JJWT +--- + +## 解决什么问题 + +在安全认证模块中,需要实现无状态的 JWT(JSON Web Token)认证机制: +- 用户登录后生成签名的 JWT Token +- 后续请求中验证 Token 签名和有效期 +- 将用户信息(用户名、权限、扩展数据)嵌入 Token payload + +JJWT 提供了类型安全的 JWT 构建器和解析器 API,支持 HMAC-SHA 签名算法。 + +## 如何使用 + +### 依赖引入 + +```xml + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + +``` + +### 配置项 + +```properties +# application.properties +codingapi.security.jwt.enable=true +codingapi.security.jwt.secret-key=your-secret-key-at-least-256-bits +codingapi.security.jwt.valid-time=3600000 +codingapi.security.jwt.rest-time=1800000 +``` + +### 核心类 + +- `JwtTokenGateway` — JWT Token 的创建与解析核心逻辑 +- `JWTTokenGatewayImpl` — 实现 `TokenGateway` 接口的适配器 +- `JWTSecurityConfiguration` — Spring Boot 自动配置类 + +## 使用实例 + +### 1. 创建 JWT Token + +```java +// JwtTokenGateway.java +public class JwtTokenGateway { + private final SecretKey key; + private final int validTime; + private final int restTime; + + public JwtTokenGateway(SecurityJWTProperties properties) { + // 使用 HMAC-SHA 密钥 + this.key = Keys.hmacShaKeyFor( + properties.getSecretKey().getBytes(StandardCharsets.UTF_8) + ); + this.validTime = properties.getValidTime(); + this.restTime = properties.getRestTime(); + } + + public Token create(String username, String iv, List authorities, String extra) { + Token token = new Token(username, iv, extra, authorities, validTime, restTime); + // 将 Token 对象序列化为 JSON 作为 JWT subject + String jwt = Jwts.builder() + .subject(token.toJson()) + .signWith(key) + .compact(); + token.setToken(jwt); + return token; + } +} +``` + +### 2. 解析 JWT Token + +```java +// JwtTokenGateway.java +public Token parser(String sign) { + try { + // 验证签名并解析 Claims + Jws jws = Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(sign); + if (jws != null) { + String subject = jws.getPayload().getSubject(); + return JSONObject.parseObject(subject, Token.class); + } + throw new LocaleMessageException("token.error", "token失效,请重新登录."); + } catch (Exception exp) { + throw new LocaleMessageException("token.error", exp.getMessage()); + } +} +``` + +### 3. 自动配置注册 + +```java +// JWTSecurityConfiguration.java +@Configuration +@ConditionalOnProperty(prefix = "codingapi.security.jwt", name = "enable", havingValue = "true") +public class JWTSecurityConfiguration { + + @Bean + @ConfigurationProperties(prefix = "codingapi.security.jwt") + public SecurityJWTProperties securityJWTProperties() { + return new SecurityJWTProperties(); + } + + @Bean + @ConditionalOnMissingBean + public JwtTokenGateway jwtTokenGateway(SecurityJWTProperties properties) { + return new JwtTokenGateway(properties); + } + + @Bean + @ConditionalOnMissingBean + public TokenGateway jwtTokenGatewayImpl(JwtTokenGateway jwtTokenGateway) { + return new JWTTokenGatewayImpl(jwtTokenGateway); + } +} +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter-security | JWT Token 生成/解析、自动配置、TokenGateway 实现 | diff --git a/docs/capabilities/jsqlparser.md b/docs/capabilities/jsqlparser.md new file mode 100644 index 000000000..73040ad3c --- /dev/null +++ b/docs/capabilities/jsqlparser.md @@ -0,0 +1,78 @@ +--- +name: jsqlparser +description: SQL 解析与改写库,用于数据权限 SQL 注入 +status: 已实现 +scope: 后端 +source: 框架:JSqlParser +--- + +## 解决什么问题 + +JSqlParser 为项目的数据权限模块提供 SQL 解析和改写能力,解决了以下问题: + +- **SQL 解析**:将原始 SQL 字符串解析为结构化的 AST(抽象语法树),理解表名、字段、WHERE 条件等 +- **SQL 改写**:在 SELECT 语句中自动注入数据权限过滤条件,实现透明的行级数据隔离 +- **零侵入**:业务代码无需修改,权限条件通过 JDBC 代理层自动注入 + +## 如何使用 + +项目使用 JSqlParser 5.0,仅在 `springboot-starter-data-authorization` 模块中使用: + +1. **SQL 解析**:通过 `CCJSqlParserUtil.parse(sql)` 将 SQL 解析为 `Statement` 对象 +2. **条件注入**:`DataPermissionSQLEnhancer` 遍历 SELECT 语句,根据 `RowHandler` 定义的权限规则向 WHERE 子句追加条件 +3. **代理链拦截**:`ConnectionProxy` → `PreparedStatementProxy` / `StatementProxy` 在 SQL 执行前调用 `SQLRunningContext.intercept(sql)` 完成改写 +4. **跳过拦截**:通过 `SQLRunningContext.skipDataAuthorization()` 可临时跳过权限过滤 + +## 使用实例 + +### DataPermissionSQLEnhancer 核心逻辑 + +```java +public class DataPermissionSQLEnhancer { + + public DataPermissionSQLEnhancer(String sql, RowHandler rowHandler) throws SQLException { + this.sql = sql.replaceAll("\\?", " ? "); + this.rowHandler = rowHandler; + // 使用 JSqlParser 解析 SQL + this.statement = CCJSqlParserUtil.parse(this.sql); + this.tableColumnAliasHolder = new TableColumnAliasHolder(statement); + } + + public String getNewSQL() throws SQLException { + if (statement instanceof Select) { + tableColumnAliasHolder.holderAlias(); + Select select = (Select) statement; + this.deepMatch(select); // 递归注入权限条件 + return statement.toString(); // 返回改写后的 SQL + } + return sql; + } +} +``` + +### ConnectionProxy 中的拦截点 + +```java +public class ConnectionProxy implements Connection { + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + // 在创建 PreparedStatement 前拦截并改写 SQL + this.interceptState = SQLRunningContext.getInstance().intercept(sql); + return new PreparedStatementProxy( + connection.prepareStatement(interceptState.getSql()), + interceptState + ); + } +} +``` + +### 临时跳过数据权限 + +```java +// 某些管理操作需要查询全量数据,跳过权限过滤 +Object result = SQLRunningContext.getInstance() + .skipDataAuthorization(() -> { + return userRepository.findAll(); + }); +``` diff --git a/docs/capabilities/kryo.md b/docs/capabilities/kryo.md new file mode 100644 index 000000000..7d5133d17 --- /dev/null +++ b/docs/capabilities/kryo.md @@ -0,0 +1,87 @@ +--- +name: kryo +description: 高性能 Java 序列化框架,用于数据快照 +status: 已实现 +scope: 后端 +source: 框架:Kryo +--- + +## 解决什么问题 + +Kryo 为项目的工作流模块提供高性能序列化能力,解决了以下问题: + +- **流程快照**:将完整的流程定义(FlowWork)序列化为字节数组存储,支持版本回溯 +- **存储优化**:相比 Java 原生序列化,Kryo 生成的字节流更小、速度更快,降低数据库存储压力 +- **版本去重**:自动判断是否存在相同版本的流程快照,避免重复存储 + +## 如何使用 + +项目使用 Kryo 5.6.2,仅在 `springboot-starter-flow` 模块的 `FlowWorkSerializable` 类中使用: + +1. **序列化**:`FlowWorkSerializable.toSerializable()` 将流程对象转为 `byte[]` +2. **反序列化**:`FlowWorkSerializable.fromSerializable(byte[])` 从字节数组恢复流程对象 +3. **类型注册**:需要预先注册所有参与序列化的类(包括枚举、嵌套对象等) +4. **与 FlowBackup 集成**:`FlowBackup` 持有序列化后的字节数组,通过 `resume()` 方法恢复完整流程 + +## 使用实例 + +### 流程序列化 + +```java +public class FlowWorkSerializable implements Serializable { + + public byte[] toSerializable() { + Kryo kryo = new Kryo(); + // 注册所有需要序列化的类型 + kryo.register(ArrayList.class); + kryo.register(FlowNodeSerializable.class); + kryo.register(FlowRelationSerializable.class); + kryo.register(FlowWorkSerializable.class); + kryo.register(ApprovalType.class); + kryo.register(NodeType.class); + kryo.register(FlowButton.class); + kryo.register(FlowButtonType.class); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Output output = new Output(outputStream); + kryo.writeObject(output, this); + output.close(); + return outputStream.toByteArray(); + } +} +``` + +### 流程反序列化 + +```java +public static FlowWorkSerializable fromSerializable(byte[] bytes) { + Kryo kryo = new Kryo(); + kryo.register(ArrayList.class); + kryo.register(FlowNodeSerializable.class); + kryo.register(FlowRelationSerializable.class); + kryo.register(FlowWorkSerializable.class); + kryo.register(ApprovalType.class); + kryo.register(NodeType.class); + kryo.register(FlowButton.class); + kryo.register(FlowButtonType.class); + return kryo.readObject(new Input(bytes), FlowWorkSerializable.class); +} +``` + +### FlowBackup 中的使用 + +```java +// 创建快照:将流程定义序列化后存储 +public FlowBackup(FlowWork flowWork) { + this.bytes = flowWork.toSerializable().toSerializable(); // Kryo 序列化 + this.workVersion = flowWork.getUpdateTime(); + this.workId = flowWork.getId(); + this.createTime = System.currentTimeMillis(); +} + +// 恢复快照:从字节数组反序列化并重建流程对象 +public FlowWork resume(FlowOperatorRepository flowOperatorRepository) { + return FlowWorkSerializable.fromSerializable(this.bytes) + .toFlowWork(flowOperatorRepository); +} +``` diff --git a/docs/capabilities/page-request-filter.md b/docs/capabilities/page-request-filter.md new file mode 100644 index 000000000..2aa4e788e --- /dev/null +++ b/docs/capabilities/page-request-filter.md @@ -0,0 +1,219 @@ +--- +name: page-request-filter +description: 动态查询过滤请求封装,支持多种比较关系、组合条件、HTTP 参数自动解析 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在企业级应用的列表查询场景中,前端通常需要传递多个筛选条件。传统做法存在以下问题: + +1. **Controller 参数爆炸**:每个查询字段都需要一个 `@RequestParam`,方法签名冗长 +2. **条件拼接复杂**:需要手动构建 JPA Specification 或 HQL,代码重复度高 +3. **前后端协议不统一**:不同项目的分页参数命名(page/pageNo/current)和过滤格式各异 +4. **动态条件难维护**:新增查询字段需要同时修改 Controller、Service、Repository 三层 +5. **类型转换繁琐**:字符串参数需要手动转为对应的实体字段类型 + +框架通过 `PageRequest` + `RequestFilter` + `Relation` 三件套提供统一的动态查询抽象。`SearchRequest` 进一步封装了 HTTP 请求参数的自动解析,支持 Base64 编码的 filter/sort/params JSON 参数,实现前端零配置对接。 + +## 如何使用 + +### 1. PageRequest — 编程式构建查询条件 + +```java +// 基础分页 +PageRequest request = PageRequest.of(0, 20); + +// 添加等值过滤 +request.addFilter("name", "张三"); + +// 添加指定关系的过滤 +request.addFilter("age", Relation.GREATER_THAN, 18); +request.addFilter("createTime", Relation.BETWEEN, startDate, endDate); +request.addFilter("status", Relation.IN, 1, 2, 3); + +// 添加排序 +request.addSort(Sort.by("createTime").descending()); + +// 链式调用 +PageRequest req = PageRequest.of(0, 20) + .addFilter("name", Relation.LIKE, "张") + .addFilter("age", Relation.GREATER_THAN_EQUAL, 18); +``` + +### 2. Relation — 支持的比较关系 + +| 枚举值 | SQL 等价 | 说明 | +|--------|----------|------| +| `EQUAL` | `= value` | 等值匹配(默认) | +| `NOT_EQUAL` | `!= value` | 不等 | +| `LIKE` | `LIKE %value%` | 模糊匹配 | +| `LEFT_LIKE` | `LIKE %value` | 左模糊 | +| `RIGHT_LIKE` | `LIKE value%` | 右模糊 | +| `BETWEEN` | `BETWEEN v1 AND v2` | 区间 | +| `IN` | `IN (v1, v2, ...)` | 包含 | +| `NOT_IN` | `NOT IN (v1, v2, ...)` | 不包含 | +| `IS_NULL` | `IS NULL` | 为空 | +| `IS_NOT_NULL` | `IS NOT NULL` | 非空 | +| `GREATER_THAN` | `> value` | 大于 | +| `LESS_THAN` | `< value` | 小于 | +| `GREATER_THAN_EQUAL` | `>= value` | 大于等于 | +| `LESS_THAN_EQUAL` | `<= value` | 小于等于 | + +### 3. Filter — 组合条件 + +使用 `Filter.and()` 和 `Filter.or()` 构建复合条件: + +```java +// AND 组合:name LIKE '%张%' AND age > 18 +request.andFilter( + Filter.as("name", Relation.LIKE, "张"), + Filter.as("age", Relation.GREATER_THAN, 18) +); + +// OR 组合:status = 1 OR status = 2 +request.orFilters( + Filter.as("status", Relation.EQUAL, 1), + Filter.as("status", Relation.EQUAL, 2) +); +``` + +### 4. SearchRequest — HTTP 参数自动解析 + +在 Controller 中直接使用 `SearchRequest`,自动从 HTTP 请求中提取分页和过滤参数: + +```java +@GetMapping("/users") +public MultiResponse list(SearchRequest searchRequest) { + // 传入实体类用于类型推断 + PageRequest pageRequest = searchRequest.toPageRequest(User.class); + Page page = userRepository.pageRequest(pageRequest); + return MultiResponse.of(page); +} +``` + +**前端传参方式:** + +- 直接参数:`?current=0&pageSize=20&name=张三&age=18` +- Base64 编码 filter:`?filter=eyJuYW1lIjpbIuW8oCJdfQ==`(解码后为 `{"name":["张"]}` ) +- Base64 编码 sort:`?sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9` +- Base64 编码 params(指定操作符):`?params=W3sia2V5IjoiYWdlIiwidHlwZSI6IkdSRUFURVJfVEhBTiJ9XQ==` + +### 5. RequestFilter — 读取已设置的过滤条件 + +```java +// 检查是否有过滤条件 +if (request.hasFilter()) { ... } + +// 获取特定过滤值 +String name = request.getStringFilter("name"); +int age = request.getIntFilter("age", 0); // 带默认值 + +// 移除某个过滤条件 +request.removeFilter("name"); +``` + +## 使用实例 + +### 完整的用户列表查询 + +```java +// Entity +@Entity +public class User { + @Id @GeneratedValue + private Long id; + private String name; + private Integer age; + private String email; + private LocalDateTime createTime; + private Integer status; +} + +// Repository +public interface UserRepository extends FastRepository { +} + +// Controller +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + /** + * 方式一:编程式构建 + */ + @GetMapping("/search") + public MultiResponse search( + @RequestParam(required = false) String name, + @RequestParam(required = false) Integer minAge, + @RequestParam(defaultValue = "0") int current, + @RequestParam(defaultValue = "20") int pageSize) { + + PageRequest request = PageRequest.of(current, pageSize) + .addSort(Sort.by("createTime").descending()); + + if (StringUtils.hasText(name)) { + request.addFilter("name", Relation.LIKE, name); + } + if (minAge != null) { + request.addFilter("age", Relation.GREATER_THAN_EQUAL, minAge); + } + + Page page = userRepository.findAll(request); + return MultiResponse.of(page); + } + + /** + * 方式二:SearchRequest 自动解析 + */ + @GetMapping + public MultiResponse list(SearchRequest searchRequest) { + PageRequest pageRequest = searchRequest.toPageRequest(User.class); + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page); + } +} +``` + +### 复杂组合查询示例 + +```java +// 查询:(name LIKE '%张%' AND age >= 18) OR status = 1 +PageRequest request = PageRequest.of(0, 20); + +request.orFilters( + Filter.and( + Filter.as("name", Relation.LIKE, "张"), + Filter.as("age", Relation.GREATER_THAN_EQUAL, 18) + ), + Filter.as("status", Relation.EQUAL, 1) +); + +Page page = userRepository.pageRequest(request); +``` + +### Filter 工具方法速查 + +```java +// 快捷创建 +Filter.as("key", value) // EQUAL +Filter.as("key", Relation.LIKE, val) // 指定关系 +Filter.and(f1, f2, ...) // AND 组合 +Filter.or(f1, f2, ...) // OR 组合 + +// 判断类型 +filter.isEqual() +filter.isLike() +filter.isBetween() +filter.isIn() +filter.isAndFilters() +filter.isOrFilters() + +// 获取值(自动类型转换) +Object val = filter.getFilterValue(Integer.class); +``` diff --git a/docs/capabilities/rest-template-context.md b/docs/capabilities/rest-template-context.md new file mode 100644 index 000000000..8c04c4c46 --- /dev/null +++ b/docs/capabilities/rest-template-context.md @@ -0,0 +1,113 @@ +--- +name: rest-template-context +description: 预配置的 RestTemplate 单例上下文,内置信任所有证书的 HttpClient,简化内部服务间 HTTP 调用 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在企业内部系统集成场景中,服务间通过 HTTPS 通信时经常遇到自签名证书或内部 CA 签发的证书不被信任的问题。每次创建 `RestTemplate` 都需要手动配置 SSL 信任策略,代码重复且容易出错。 + +`RestTemplateContext` 提供了一个全局单例的 `RestTemplate` 实例,内置了信任所有证书的 `HttpClient`(基于 Apache HttpComponents 5),适用于内部服务间调用、第三方接口对接等不需要严格证书校验的场景。 + +核心特点: +- **单例模式**:通过双重检查锁确保线程安全的懒初始化 +- **信任所有证书**:底层使用 `TrustAnyHttpClientFactory` 创建的 HttpClient,跳过 SSL 证书验证 +- **支持 HTTP/HTTPS**:同时注册了 HTTP 和 HTTPS 的连接工厂 +- **允许循环重定向**:默认开启 `circularRedirectsAllowed` + +## 如何使用 + +### 1. 获取 RestTemplate 实例 + +```java +RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); +``` + +### 2. 发起 HTTP 请求 + +```java +RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); + +// GET 请求 +String result = restTemplate.getForObject("https://internal-api.example.com/data", String.class); + +// POST 请求 +Map body = Map.of("key", "value"); +ResponseEntity response = restTemplate.postForEntity( + "https://internal-api.example.com/submit", + body, + String.class +); + +// 带 Header 的请求 +HttpHeaders headers = new HttpHeaders(); +headers.setContentType(MediaType.APPLICATION_JSON); +headers.setBearerAuth(token); +HttpEntity> entity = new HttpEntity<>(body, headers); +ResponseEntity resp = restTemplate.exchange( + "https://api.example.com/resource", + HttpMethod.GET, + entity, + String.class +); +``` + +### 3. 自定义 ClientHttpRequestFactory + +如需使用不同的请求工厂创建新的 RestTemplate: + +```java +ClientHttpRequestFactory customFactory = new HttpComponentsClientHttpRequestFactory(customHttpClient); +RestTemplate customTemplate = RestTemplateContext.getInstance().restTemplate(customFactory); +``` + +## 使用实例 + +### 内部服务调用示例 + +```java +@Service +public class ExternalServiceClient { + + private final RestTemplate restTemplate; + + public ExternalServiceClient() { + this.restTemplate = RestTemplateContext.getInstance().getRestTemplate(); + } + + public UserInfo getUserFromRemote(Long userId) { + String url = "https://user-service.internal/api/users/" + userId; + return restTemplate.getForObject(url, UserInfo.class); + } + + public void notifyRemote(String message) { + String url = "https://notification-service.internal/api/notify"; + Map payload = Map.of("message", message); + restTemplate.postForEntity(url, payload, Void.class); + } +} +``` + +### 架构说明 + +``` +RestTemplateContext (Singleton) + └── RestTemplate + └── HttpComponentsClientHttpRequestFactory + └── HttpClient (Apache HC5) + ├── SSLConnectionSocketFactory (TLS, TrustAnyTrustManager, NoopHostnameVerifier) + └── PlainConnectionSocketFactory (HTTP) +``` + +`TrustAnyHttpClientFactory.createTrustAnyHttpClient()` 创建的 HttpClient 具有以下特性: +- SSL 上下文使用 `TrustAnyTrustManager`,对所有服务端证书返回信任 +- 主机名验证使用 `NoopHostnameVerifier`,不校验主机名 +- 连接管理器使用 `BasicHttpClientConnectionManager` +- 请求配置允许循环重定向 + +### ⚠️ 安全提示 + +此 RestTemplate **仅适用于受信任的内部网络环境**。由于跳过了 SSL 证书验证,在公网环境下使用存在中间人攻击风险。对于面向公网的外部 API 调用,应使用标准配置的 RestTemplate 并正确配置证书信任链。 diff --git a/docs/capabilities/security-auth.md b/docs/capabilities/security-auth.md new file mode 100644 index 000000000..20179477d --- /dev/null +++ b/docs/capabilities/security-auth.md @@ -0,0 +1,263 @@ +--- +name: security-auth +description: 基于 Spring Security 的 JWT/Redis 双模式认证框架,支持 Token 自动续期与登录扩展 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +Spring Security 默认的表单登录和 Session 机制不适合前后端分离的 RESTful API 场景。企业应用通常需要: +- 无状态的 JWT Token 认证或有状态的 Redis Token 认证 +- JSON 格式的统一登录/登出响应(而非重定向) +- Token 自动续期机制 +- 可扩展的登录前置/后置处理逻辑 +- 统一的未认证、无权限错误响应 + +`springboot-starter-security` 在 Spring Security 基础上封装了完整的认证框架,提供开箱即用的 JWT / Redis 双模式认证,所有安全事件均以 JSON 格式返回统一的 `Response` 结构。 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-security + +``` + +### 2. 配置安全属性 + +在 `application.properties` 中配置: + +```properties +# 安全模块配置前缀 +codingapi.security.login-processing-url=/login # 登录接口路径 +codingapi.security.logout-url=/logout # 登出接口路径 +codingapi.security.ignore-urls=/open/**,/#/** # 免认证 URL 列表 +codingapi.security.authenticated-urls=/api/** # 需认证的 URL 列表 +codingapi.security.disable-cors=false # 是否禁用 CORS(false=启用默认 CORS) + +# JWT 模式 +codingapi.security.jwt.enable=true # 启用 JWT 认证 + +# Redis 模式 +codingapi.security.redis.enable=true # 启用 Redis 有状态认证 +``` + +配置通过 `CodingApiSecurityProperties` 绑定。 + +### 3. Token 网关 (TokenGateway) + +框架定义了 `TokenGateway` 接口作为 Token 操作的抽象层: + +```java +public interface TokenGateway { + // 创建 Token + Token create(String username, String iv, List authorities, String extra); + + // 解析 Token + Token parser(String sign); +} +``` + +框架提供了两种实现: +- **`JWTTokenGatewayImpl`**:基于 JJWT 的无状态 JWT Token +- **`RedisTokenGatewayImpl`**:基于 Redis 的有状态 Token + +根据配置自动选择实现。 + +### 4. Token 对象 + +`Token` 类封装了认证信息: + +```java +public class Token implements JsonSerializable { + private String username; // 用户名 + private String extra; // 额外信息(JSON 字符串) + private String iv; // 加密向量(AES 编码) + private String token; // Token 值 + private List authorities; // 权限列表 + private long expireTime; // 过期时间戳 + private long remindTime; // 提醒续期时间戳 + + // 验证 Token 是否过期 + void verify() throws TokenExpiredException; + + // 是否需要续期(未过期且已过提醒时间) + boolean canRestToken(); + + // 解析额外信息为指定类型 + T parseExtra(Class clazz); + + // 转换为 Spring Security Authentication + UsernamePasswordAuthenticationToken getAuthenticationToken(); +} +``` + +### 5. 获取当前用户信息 + +通过 `TokenContext` 获取当前登录用户的 Token: + +```java +// 获取当前 Token +Token token = TokenContext.current(); +String username = token.getUsername(); +List authorities = token.getAuthorities(); + +// 获取额外信息 +String extra = TokenContext.getExtra(); +UserInfo userInfo = token.parseExtra(UserInfo.class); +``` + +### 6. 自定义登录处理 + +实现 `SecurityLoginHandler` 接口以扩展登录逻辑: + +```java +@Bean +public SecurityLoginHandler securityLoginHandler() { + return new SecurityLoginHandler() { + @Override + public void preHandle(HttpServletRequest request, + HttpServletResponse response, + LoginRequest loginRequest) throws Exception { + // 登录前置处理:验证码校验、IP 黑名单检查等 + if (!captchaService.verify(loginRequest.getCaptcha())) { + throw new Exception("验证码错误"); + } + } + + @Override + public LoginResponse postHandle(HttpServletRequest request, + HttpServletResponse response, + LoginRequest loginRequest, + UserDetails user, + Token token) { + // 登录后置处理:记录登录日志、附加额外信息等 + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setToken(token.getToken()); + loginResponse.setUsername(token.getUsername()); + loginResponse.setAuthorities(token.getAuthorities()); + loginResponse.setData(Map.of("lastLoginTime", System.currentTimeMillis())); + return loginResponse; + } + }; +} +``` + +`LoginResponse` 结构: +```java +public class LoginResponse { + private String username; + private String token; + private List authorities; + private Object data; // 自定义附加数据 +} +``` + +### 7. 自定义 Token 过滤 + +实现 `AuthenticationTokenFilter` 在每次请求认证通过后执行额外逻辑: + +```java +@Bean +public AuthenticationTokenFilter authenticationTokenFilter() { + return (request, response) -> { + // 例如:更新最后登录时间、审计日志等 + }; +} +``` + +### 8. 自定义 HttpSecurity 配置 + +实现 `HttpSecurityCustomer` 接口以进一步定制 Spring Security 配置: + +```java +@Bean +public HttpSecurityCustomer httpSecurityCustomer(CodingApiSecurityProperties properties) { + return new DefaultHttpSecurityCustomer(properties); + // 或自定义实现 +} +``` + +## 使用实例 + +### 认证过滤器链工作原理 + +``` +HTTP Request + ↓ +MyUnAuthenticationEntryPoint ← 未认证时返回 {"errCode":"not.login","errMessage":"please to login."} + ↓ +MyAccessDeniedHandler ← 无权限时返回 {"errCode":"not.access","errMessage":"please check user authentication."} + ↓ +MyLoginFilter (POST /login) ← 登录认证 + ├── attemptAuthentication() → 读取 JSON body → SecurityLoginHandler.preHandle() → authenticate() + ├── successfulAuthentication() → TokenGateway.create() → SecurityLoginHandler.postHandle() → 返回 LoginResponse + └── unsuccessfulAuthentication() → 返回 {"errCode":"login.error","errMessage":"..."} + ↓ +MyAuthenticationFilter ← Token 认证 + ├── 匹配 authenticatedUrls + ├── 从 Header "Authorization" 读取 Token + ├── TokenGateway.parser(sign) → Token.verify() + ├── Token.canRestToken() → 自动续期,新 Token 写入响应头 + └── SecurityContextHolder.setAuthentication() + ↓ +MyLogoutSuccessHandler (POST /logout) ← 登出成功返回 {"success":true} +``` + +### 统一错误响应格式 + +所有安全相关的错误都以统一的 `Response` JSON 格式返回: + +| 场景 | errCode | errMessage | +|------|---------|------------| +| 未登录 | `not.login` | `please to login.` | +| 无权限 | `not.access` | `please check user authentication.` | +| 登录失败 | `login.error` | 具体异常信息 | +| Token 为空 | `token.error` | `token must not null.` | +| Token 过期 | `token.expire` | `token expire.` | + +### Token 自动续期 + +当 Token 未过期但已超过 `remindTime` 时,`MyAuthenticationFilter` 会自动创建新 Token 并通过响应头 `Authorization` 返回: + +```java +// MyAuthenticationFilter.doFilterInternal() 中的关键逻辑 +if (token.canRestToken()) { + Token newSign = tokenGateway.create( + token.getUsername(), + token.decodeIv(), + token.getAuthorities(), + token.getExtra() + ); + response.setHeader("Authorization", newSign.getToken()); +} +``` + +客户端应在每次响应后检查 `Authorization` 响应头,如有新值则替换本地存储的 Token。 + +### 密码编码器 + +框架默认使用 Spring Security 的 `DelegatingPasswordEncoder`,支持多种编码格式的自动识别: + +```java +@Bean +@ConditionalOnMissingBean +public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); +} +``` + +如需自定义密码编码器,只需声明自己的 `PasswordEncoder` Bean 即可覆盖默认实现。 + +### 默认用户(开发环境) + +当没有自定义 `UserDetailsService` 时,框架自动创建内存用户: +- `admin / admin` — ROLE_ADMIN +- `user / admin` — ROLE_USER + +生产环境应提供自己的 `UserDetailsService` Bean 来覆盖此默认行为。 diff --git a/docs/capabilities/spring-data-jpa.md b/docs/capabilities/spring-data-jpa.md new file mode 100644 index 000000000..3036331d2 --- /dev/null +++ b/docs/capabilities/spring-data-jpa.md @@ -0,0 +1,77 @@ +--- +name: spring-data-jpa +description: ORM 映射、Repository 抽象、自动分页 +status: 已实现 +scope: 后端 +source: 框架:Spring Data JPA +--- + +## 解决什么问题 + +Spring Data JPA 是项目中数据持久化的基础层,解决了以下问题: + +- **ORM 映射**:将数据库表映射为 Java Entity,减少手写 SQL +- **Repository 抽象**:通过接口定义数据访问方法,由框架自动生成实现 +- **自动分页**:内置 `Pageable` / `Page` 支持,简化分页查询逻辑 +- **作为 FastRepository 的底层**:项目的 `springboot-starter-data-fast` 模块在此基础上扩展了动态过滤查询能力 + +## 如何使用 + +项目使用 `spring-boot-starter-data-jpa`(随 Spring Boot 3.3.5),在 DDD 架构中处于 infra 层: + +1. **Entity 定义**:在 `example-infra-jpa` 模块中定义 JPA Entity(如 `UserEntity`) +2. **JPA Repository**:继承 `JpaRepository` 或 `FastRepository` 获得 CRUD + 动态查询能力 +3. **Domain Repository 实现**:在 infra 层实现 domain 层定义的 Repository 接口,内部委托给 JPA Repository +4. **Convertor 转换**:使用 Convertor 类在 Entity 与 Domain Entity 之间转换,隔离持久化细节 + +## 使用实例 + +### JPA Repository 定义 + +```java +// example-infra-jpa 模块 +public interface UserEntityRepository extends FastRepository { + + UserEntity getUserEntityByUsername(String username); + + UserEntity getUserEntityById(long id); +} +``` + +### Domain Repository 接口(domain 层) + +```java +// example-domain-user 模块 —— 仅定义接口,不依赖 JPA +public interface UserRepository { + User getUserByUsername(String username); + User getUserById(long id); + void save(User user); + void delete(long id); +} +``` + +### Infra 层实现(桥接 Domain 与 JPA) + +```java +@Repository +@AllArgsConstructor +public class UserRepositoryImpl implements UserRepository { + + private final UserEntityRepository userEntityRepository; + + @Override + public User getUserByUsername(String username) { + return UserConvertor.convert( + userEntityRepository.getUserEntityByUsername(username), + userEntityRepository + ); + } + + @Override + public void save(User user) { + UserEntity entity = UserConvertor.convert(user); + entity = userEntityRepository.save(entity); + user.setId(entity.getId()); + } +} +``` diff --git a/docs/capabilities/spring-data-redis.md b/docs/capabilities/spring-data-redis.md new file mode 100644 index 000000000..7338ff9a4 --- /dev/null +++ b/docs/capabilities/spring-data-redis.md @@ -0,0 +1,58 @@ +--- +name: spring-data-redis +description: Redis 客户端抽象,支持缓存和有状态认证 Token 存储 +status: 已实现 +scope: 后端 +source: 框架:Spring Data Redis +--- + +## 解决什么问题 + +Spring Data Redis 为项目提供 Redis 访问能力,解决了以下问题: + +- **有状态认证**:在 Redis 模式下存储用户 Token 和会话信息,支持分布式部署 +- **缓存**:为热点数据提供高速缓存层,减少数据库压力 +- **分布式协调**:可作为分布式锁、消息队列等基础设施的底层支撑 + +## 如何使用 + +项目使用 `spring-boot-starter-data-redis`(随 Spring Boot 3.3.5),主要在安全模块中使用: + +1. **启用 Redis 认证**:配置 `codingapi.security.redis.enable=true` +2. **Token 存储**:登录成功后将 Token 存入 Redis,后续请求从 Redis 验证 Token 有效性 +3. **与 JWT 互斥**:JWT 模式(无状态)和 Redis 模式(有状态)二选一 +4. **连接配置**:通过标准 Spring Boot Redis 配置项指定连接信息 + +## 使用实例 + +### 启用 Redis 有状态认证 + +```properties +# application.properties +codingapi.security.redis.enable=true +codingapi.security.jwt.enable=false + +# Redis 连接配置 +spring.data.redis.host=localhost +spring.data.redis.port=6379 +spring.data.redis.password=your-password +``` + +### 免认证 URL 配置 + +```properties +# 这些路径不需要 Token 验证 +codingapi.security.ignore-urls=/open/**,/#/**,/api/query/** +``` + +### 架构说明 + +``` +用户登录 → MyLoginFilter 验证凭据 + → 生成 Token 存入 Redis + → 返回 Token 给客户端 + +后续请求 → MyAuthenticationFilter 拦截 + → 从 Redis 读取 Token 验证 + → 验证通过则放行,否则返回 401 +``` diff --git a/docs/capabilities/spring-ioc-container.md b/docs/capabilities/spring-ioc-container.md new file mode 100644 index 000000000..58bffabe2 --- /dev/null +++ b/docs/capabilities/spring-ioc-container.md @@ -0,0 +1,85 @@ +--- +name: spring-ioc-container +description: Spring IoC 容器提供依赖注入、Bean 生命周期管理、AOP 支持 +status: 已实现 +scope: 后端 +source: 框架:Spring Framework +--- + +## 解决什么问题 + +Spring IoC(控制反转)容器是项目的核心基础设施,解决了以下问题: + +- **对象创建与组装**:自动管理 Bean 的创建、依赖注入和生命周期,避免手动 new 对象和硬编码依赖 +- **模块解耦**:通过接口注入而非具体类依赖,使各 starter 模块可以独立开发和测试 +- **横切关注点**:通过 AOP 支持事务管理、日志记录等横切逻辑的统一处理 +- **自动配置**:Spring Boot 的条件化自动配置机制使框架的各 starter 模块可按需激活 + +## 如何使用 + +项目基于 Spring Boot 3.3.5(Java 17),使用方式遵循标准 Spring Boot 约定: + +1. **依赖注入**:使用构造器注入(推荐 `@AllArgsConstructor` + `final` 字段)或 `@Autowired` +2. **Bean 声明**:使用 `@Component`、`@Service`、`@Repository`、`@Configuration` 等注解 +3. **自动配置注册**:各 starter 模块通过 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 和 `META-INF/spring.factories` 双重注册 +4. **ImportBeanDefinitionRegistrar**:框架使用此扩展点动态注册 Bean,如 `HandlerBeanDefinitionRegistrar` 自动扫描事件处理器 + +## 使用实例 + +### 构造器注入(项目推荐方式) + +```java +@Slf4j +@Service +@AllArgsConstructor +public class LeaveHandler implements IHandler { + + private final LeaveService leaveService; // 自动注入 + + @Override + public void handler(FlowApprovalEvent event) { + if (event.isFinish() && event.match(LeaveForm.class)) { + LeaveForm form = (LeaveForm) event.getBindData(); + leaveService.create(form.toLeave()); + } + } +} +``` + +### Repository 实现注入 + +```java +@Repository +@AllArgsConstructor +public class UserRepositoryImpl implements UserRepository { + + private final UserEntityRepository userEntityRepository; // JPA Repository 自动注入 + + @Override + public User getUserByUsername(String username) { + return UserConvertor.convert( + userEntityRepository.getUserEntityByUsername(username), + userEntityRepository + ); + } +} +``` + +### 自定义 Bean 注册(ImportBeanDefinitionRegistrar) + +```java +@Configuration +public class HandlerBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { + + @SneakyThrows + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + RegisterBeanScanner scanner = new RegisterBeanScanner(importingClassMetadata, Handler.class); + List beanDefinitions = scanner.findBeanDefinitions(); + for (BeanDefinition candidate : beanDefinitions) { + registry.registerBeanDefinition(candidate.getBeanClassName(), candidate); + } + } +} +``` diff --git a/docs/capabilities/spring-security.md b/docs/capabilities/spring-security.md new file mode 100644 index 000000000..2297378a8 --- /dev/null +++ b/docs/capabilities/spring-security.md @@ -0,0 +1,66 @@ +--- +name: spring-security +description: 认证授权框架,提供 Filter Chain、CSRF 防护 +status: 已实现 +scope: 后端 +source: 框架:Spring Security +--- + +## 解决什么问题 + +Spring Security 为项目提供安全基础设施,解决了以下问题: + +- **认证**:验证用户身份,支持 JWT 无状态认证和 Redis 有状态认证两种模式 +- **授权**:基于 URL 模式和角色的访问控制 +- **Filter Chain**:通过过滤器链拦截请求,统一处理安全逻辑 +- **密码加密**:提供 PasswordEncoder 接口进行安全的密码哈希 + +## 如何使用 + +项目使用 `spring-boot-starter-security`(随 Spring Boot 3.3.5),被 `springboot-starter-security` 模块封装: + +1. **JWT 模式**:配置 `codingapi.security.jwt.enable=true`,使用 `JwtTokenGateway` 签发和验证 Token +2. **Redis 模式**:配置 `codingapi.security.redis.enable=true`,Token 存储在 Redis 中 +3. **免认证 URL**:通过 `codingapi.security.ignore-urls` 配置白名单路径 +4. **自定义 Filter**:框架提供 `MyAuthenticationFilter`、`MyLoginFilter`、`MyLogoutSuccessHandler` 等组件 +5. **异常处理**:`MyAccessDeniedHandler` 和 `MyUnAuthenticationEntryPoint` 统一返回 `Response` 格式的错误响应 + +## 使用实例 + +### 安全配置项 + +```properties +# application.properties +codingapi.security.jwt.enable=true +codingapi.security.redis.enable=false +codingapi.security.ignore-urls=/open/**,/#/**,/api/query/** +``` + +### Controller 中使用认证信息 + +```java +@RestController +@RequestMapping("/api/cmd/user") +@AllArgsConstructor +public class UserDomainCmdController { + + private final UserRouter userRouter; + + @PostMapping("/save") + public Response save(@RequestBody UserCmd.UpdateRequest request) { + // 请求已通过 Security Filter Chain 认证 + userRouter.createOrUpdate(request); + return Response.buildSuccess(); + } +} +``` + +### Domain 层定义密码加密网关 + +```java +// domain 层定义接口,不依赖 Spring Security +public interface PasswordEncoder { + String encode(String rawPassword); + boolean matches(String rawPassword, String encodedPassword); +} +``` diff --git a/docs/capabilities/spring-web-mvc.md b/docs/capabilities/spring-web-mvc.md new file mode 100644 index 000000000..60015788a --- /dev/null +++ b/docs/capabilities/spring-web-mvc.md @@ -0,0 +1,109 @@ +--- +name: spring-web-mvc +description: Web MVC 框架,提供 RESTful API 路由、请求参数绑定、响应序列化 +status: 已实现 +scope: 后端 +source: 框架:Spring Web MVC +--- + +## 解决什么问题 + +框架需要提供 RESTful API 能力: +- 安全模块暴露版本查询接口 +- Groovy 脚本模块暴露脚本编译/保存/查询接口 +- 统一响应体(Response/SingleResponse/MultiResponse)的自动 JSON 序列化 +- 请求参数绑定(@RequestBody、@RequestParam) + +Spring Web MVC 是 Spring Boot Web 的核心组件,通过 `spring-boot-starter-web` 依赖引入。 + +## 如何使用 + +### 依赖引入 + +```xml + + + org.springframework.boot + spring-boot-starter-web + +``` + +### 核心注解 + +- `@RestController` — 声明 REST 控制器 +- `@GetMapping` / `@PostMapping` — 路由映射 +- `@RequestBody` — 请求体 JSON 反序列化 +- `@RequestParam` — 查询参数绑定 + +## 使用实例 + +### 1. Security 版本查询接口 + +```java +// VersionController.java +@RestController +public class VersionController { + + @GetMapping("/open/version") + public SingleResponse version() { + return Response.buildSuccess("3.4.54"); + } +} +``` + +### 2. Groovy 脚本管理接口 + +```java +// GroovyScriptController.java +@RestController +@RequestMapping("/api/groovy-script") +public class GroovyScriptController { + + @PostMapping("/compile") + public Response compile(@RequestBody ScriptCompileRequest request) { + // 编译 Groovy 脚本 + return Response.buildSuccess(); + } + + @GetMapping("/getScript") + public SingleResponse getScript(@RequestParam(name = "key") String key) { + // 获取脚本内容 + return Response.buildSuccess(script); + } + + @GetMapping("/getMetadata") + public SingleResponse getMetadata( + @RequestParam(name = "key") String key) { + // 获取脚本元数据 + return Response.buildSuccess(metadata); + } + + @PostMapping("/save") + public Response save(@RequestBody ScriptSaveRequest request) { + // 保存脚本 + return Response.buildSuccess(); + } +} +``` + +### 3. 统一响应体与 Spring MVC 集成 + +框架定义的 `Response`、`SingleResponse`、`MultiResponse` 等响应类通过 Jackson(Spring Boot 默认)自动序列化为 JSON: + +```java +// Controller 返回统一响应格式 +@GetMapping("/users") +public MultiResponse listUsers(PageRequest request) { + Page page = userRepository.findAll(request); + return Response.buildSuccess(page.getContent()); +} +// 自动序列化为: {"success": true, "data": [...]} +``` + +### 涉及模块 + +| 模块 | 使用场景 | +|------|----------| +| springboot-starter-security | `VersionController` 版本查询 API | +| springboot-starter-script | `GroovyScriptController` 脚本管理 API | +| springboot-starter-data-fast | 动态表管理的 REST 接口(example 中使用) | diff --git a/docs/capabilities/unified-response.md b/docs/capabilities/unified-response.md new file mode 100644 index 000000000..8b376026e --- /dev/null +++ b/docs/capabilities/unified-response.md @@ -0,0 +1,208 @@ +--- +name: unified-response +description: 统一 API 响应封装,提供 Response、SingleResponse、MultiResponse、MapResponse 四种标准响应格式 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +在企业级应用中,API 响应格式不统一会导致以下问题: + +1. **前端解析困难**:不同接口返回不同的 JSON 结构,前端需要针对每个接口做适配 +2. **错误处理不一致**:有些接口用 HTTP 状态码表示错误,有些用业务字段,缺乏统一规范 +3. **分页数据格式混乱**:列表接口的分页信息(总数、页码、列表)命名和结构各不相同 +4. **国际化支持缺失**:错误消息硬编码在代码中,无法根据用户语言动态切换 +5. **序列化兼容性**:不同 DTO 的 JSON 序列化行为不可预测 + +框架提供四种标准响应类型,所有响应都继承自 `Response` 基类,包含统一的 `success`、`errCode`、`errMessage` 字段。结合 `LocaleMessageException` 可实现错误的国际化输出。 + +## 如何使用 + +### 1. Response — 基础响应 + +仅表示操作成功或失败,不携带数据: + +```java +// 成功响应 +return Response.buildSuccess(); + +// 失败响应 +return Response.buildFailure("USER_NOT_FOUND", "用户不存在"); +``` + +### 2. SingleResponse\ — 单对象响应 + +返回单个业务对象: + +```java +// 返回数据 +User user = userService.findById(id); +return SingleResponse.of(user); + +// 返回空数据(表示查询无结果但不是错误) +return SingleResponse.empty(); +``` + +### 3. MultiResponse\ — 列表响应 + +返回集合数据,支持分页信息: + +```java +// 从 Spring Data Page 构建(自动提取 total 和 content) +Page page = userRepository.findAll(pageable); +return MultiResponse.of(page); + +// 手动指定数据和总数 +List users = userService.search(keyword); +long total = userService.countByKeyword(keyword); +return MultiResponse.of(users, total); + +// 仅传集合(total 自动设为集合大小) +return MultiResponse.of(users); + +// 空列表 +return MultiResponse.empty(); +``` + +### 4. MapResponse — Map 响应 + +返回键值对结构的灵活数据: + +```java +return MapResponse.create() + .add("userName", "张三") + .add("roleCount", 3) + .add("permissions", Arrays.asList("READ", "WRITE")); +``` + +## 使用实例 + +### Controller 中的完整使用示例 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserService userService; + + /** + * 查询单个用户 + */ + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + UserDTO user = userService.findById(id); + if (user == null) { + // 抛出国际化异常,由全局异常处理器转为 Response + throw LocaleMessageException.of("USER_NOT_FOUND", id); + } + return SingleResponse.of(user); + } + + /** + * 分页查询用户列表 + */ + @GetMapping + public MultiResponse listUsers(PageRequest request) { + Page page = userService.findAll(request); + return MultiResponse.of(page); + } + + /** + * 创建用户 + */ + @PostMapping + public SingleResponse createUser(@RequestBody CreateUserCommand cmd) { + UserDTO user = userService.create(cmd); + return SingleResponse.of(user); + } + + /** + * 删除用户 + */ + @DeleteMapping("/{id}") + public Response deleteUser(@PathVariable Long id) { + userService.delete(id); + return Response.buildSuccess(); + } + + /** + * 获取用户统计信息 + */ + @GetMapping("/stats") + public MapResponse getStats() { + return MapResponse.create() + .add("totalUsers", userService.count()) + .add("activeUsers", userService.countActive()) + .add("todayNewUsers", userService.countTodayNew()); + } +} +``` + +### 响应 JSON 结构示例 + +**SingleResponse 成功:** +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "id": 1, + "name": "张三", + "email": "zhangsan@example.com" + } +} +``` + +**MultiResponse 分页:** +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "total": 100, + "list": [ + {"id": 1, "name": "张三"}, + {"id": 2, "name": "李四"} + ] + } +} +``` + +**Response 失败:** +```json +{ + "success": false, + "errCode": "USER_NOT_FOUND", + "errMessage": "用户不存在" +} +``` + +### 与全局异常处理的配合 + +当 Controller 抛出 `LocaleMessageException` 时,全局异常处理器会自动将其转换为标准的 `Response` 格式: + +```java +// Service 层抛出异常 +if (user == null) { + throw LocaleMessageException.of("USER_NOT_FOUND", userId); +} + +// 框架自动捕获并返回: +// {"success": false, "errCode": "USER_NOT_FOUND", "errMessage": "用户不存在"} +``` + +### 响应类型选择指南 + +| 场景 | 推荐类型 | 说明 | +|------|----------|------| +| 增删改操作 | `Response` | 只需告知成功/失败 | +| 查询单个对象 | `SingleResponse` | 可能为 null,用 `empty()` 表示 | +| 查询列表/分页 | `MultiResponse` | 携带 total 和 list | +| 聚合统计/灵活结构 | `MapResponse` | 键值对形式 | +| 操作失败 | `Response.buildFailure()` 或抛异常 | 推荐抛 `LocaleMessageException` | diff --git a/docs/capabilities/workflow-engine.md b/docs/capabilities/workflow-engine.md new file mode 100644 index 000000000..84c9043f7 --- /dev/null +++ b/docs/capabilities/workflow-engine.md @@ -0,0 +1,243 @@ +--- +name: workflow-engine +description: 内置工作流引擎,支持流程定义、节点流转、审批、退回、会签、抄送、委托与数据快照 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +企业应用中审批流程是核心业务场景,但引入外部工作流引擎(如 Activiti、Camunda)往往带来过重的依赖和复杂的 BPMN 学习成本。许多中小规模审批场景只需要一个轻量、可编程、与 DDD 架构自然融合的流程引擎。 + +`springboot-starter-flow` 提供了一个自研的轻量级工作流引擎,具备以下特点: + +- **Schema 驱动**:流程定义通过 JSON Schema 描述,支持可视化编辑器生成,运行时由 `SchemaReader` 解析为 `FlowNode` + `FlowRelation` 对象图 +- **完整的审批语义**:支持通过、退回、转办、委托、会签、抄送、催办、撤销、作废、停止等操作 +- **数据快照**:流程发起时对绑定数据进行快照(`BindDataSnapshot`),确保审批过程中数据一致性 +- **版本管理**:通过 `FlowBackup` 对流程定义进行版本快照,运行中的流程始终使用发起时的版本 +- **事件驱动**:每个状态变更推送 `FlowApprovalEvent`(同步事件),业务层通过 `IHandler` 响应 +- **脚本化扩展**:节点的标题生成(`TitleGenerator`)、操作者匹配(`OperatorMatcher`)、异常触发(`ErrTrigger`)均支持脚本表达式 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-flow + +``` + +### 2. 实现 Repository 接口 + +框架定义了以下仓储接口,需由基础设施层提供持久化实现: + +| 接口 | 职责 | +|------|------| +| `FlowWorkRepository` | 流程定义的增删改查 | +| `FlowRecordRepository` | 审批记录的保存与查询 | +| `FlowBackupRepository` | 流程版本快照的存储与恢复 | +| `FlowProcessRepository` | 流程实例的管理 | +| `FlowOperatorRepository` | 流程参与者的查询(`findByIds`) | +| `FlowBindDataRepository` | 绑定数据快照的存储 | + +### 3. 实现 IFlowOperator 接口 + +所有流程参与者需实现 `IFlowOperator` 接口: + +```java +public interface IFlowOperator { + long getUserId(); // 用户ID + String getName(); // 用户名称 + boolean isFlowManager(); // 是否流程管理员(可强制干预流程) + IFlowOperator entrustOperator(); // 委托操作者(不为空时由委托人执行) +} +``` + +### 4. 设计流程 + +通过 `FlowWork` 构建流程定义: + +```java +FlowWork flowWork = new FlowWork(createUser); +flowWork.setTitle("请假审批"); +flowWork.setCode("leave_approval"); +flowWork.setDescription("员工请假审批流程"); + +// 通过 Schema 解析节点和关系 +flowWork.schema(schemaJson); + +// 验证并启用 +flowWork.verify(); +flowWork.enable(); +``` + +核心领域对象: +- **`FlowWork`**:流程定义,包含节点列表(`nodes`)和关系列表(`relations`),支持 `copy()` 复制、`schema()` 解析、`verify()` 校验 +- **`FlowNode`**:流程节点,具有 `code`、`type`(NodeType)、`approvalType`(ApprovalType)、`titleGenerator`、`operatorMatcher`、`errTrigger`、`buttons`、`timeout` 等属性 +- **`FlowRelation`**:节点间关系,定义源节点到目标节点的流转规则,支持条件触发(`OutTrigger`)和回退标记(`isBack`) +- **`FlowRecord`**:审批记录,记录每个节点的审批状态、操作者、意见等 + +### 5. 发起流程 + +使用 `FlowStartService` 发起流程: + +```java +FlowStartService startService = new FlowStartService( + workCode, // 流程编码 + operator, // 发起人 (IFlowOperator) + bindData, // 绑定数据 (IBindData) + advice, // 发起意见 + repositoryHolder // 仓储持有者 +); + +FlowResult result = startService.startFlow(); +``` + +`startFlow()` 的执行流程: +1. 加载并验证流程定义(`loadFlowWork`) +2. 创建或获取流程版本快照(`loadFlowBackup`) +3. 保存流程实例(`saveFlowProcess`) +4. 保存绑定数据快照(`saveBindDataSnapshot`) +5. 构建节点服务并创建待办记录(`buildFlowNodeService` → `createRecord`) +6. 推送 `FlowApprovalEvent` 事件(STATE_CREATE / STATE_TODO / STATE_SAVE / STATE_FINISH) + +### 6. 提交审批 + +使用 `FlowSubmitService` 处理审批操作: + +```java +FlowSubmitService submitService = new FlowSubmitService( + recordId, // 当前审批记录ID + currentOperator, // 当前操作者 + bindData, // 绑定数据 + opinion, // 审批意见 (Opinion) + repositoryHolder // 仓储持有者 +); +``` + +`Opinion` 封装了审批动作: +- `Opinion.pass(advice)` — 通过 +- `Opinion.reject(advice)` — 拒绝 +- `Opinion.transfer(operatorIds)` — 转办 +- 支持设置自定义操作者 `opinion.getOperatorIds()` + +### 7. 查看流程步骤预览 + +使用 `FlowStepService` 获取流程步骤预览信息: + +```java +FlowStepService stepService = new FlowStepService( + recordId, // 记录ID(0 表示新流程预览) + workCode, // 流程编码 + currentOperator, // 当前操作者 + bindData, // 绑定数据 + repositoryHolder // 仓储持有者 +); + +FlowStepResult stepResult = stepService.getFlowStep(); +``` + +### 8. 监听流程事件 + +实现 `IHandler` 响应流程状态变更: + +```java +@Component +public class FlowEventHandler implements IHandler { + + @Override + public void handler(FlowApprovalEvent event) { + if (event.isCreate()) { + // 流程创建 + } else if (event.isTodo()) { + // 新待办产生,发送通知 + } else if (event.isPass()) { + // 审批通过 + } else if (event.isReject()) { + // 审批拒绝 + } else if (event.isFinish()) { + // 流程结束 + } else if (event.isTransfer()) { + // 转办 + } else if (event.isCirculate()) { + // 抄送 + } + // ... 其他状态 + } +} +``` + +`FlowApprovalEvent` 支持的状态常量: + +| 常量 | 值 | 含义 | +|------|-----|------| +| `STATE_CREATE` | 1 | 创建流程 | +| `STATE_PASS` | 2 | 审批通过 | +| `STATE_REJECT` | 3 | 审批拒绝 | +| `STATE_TRANSFER` | 4 | 转办 | +| `STATE_RECALL` | 5 | 撤销 | +| `STATE_FINISH` | 6 | 流程完成 | +| `STATE_TODO` | 7 | 创建待办 | +| `STATE_URGE` | 8 | 催办 | +| `STATE_CIRCULATE` | 9 | 抄送 | +| `STATE_SAVE` | 10 | 保存 | +| `STATE_DELETE` | 11 | 删除 | +| `STATE_BACK` | 12 | 退回 | +| `STATE_VOIDED` | 13 | 作废 | +| `STATE_STOP` | 14 | 停止 | + +## 使用实例 + +### 流程流转方向控制 + +`FlowDirectionService` 负责解析审批方向: + +```java +FlowDirectionService directionService = + new FlowDirectionService(flowNode, flowWork, opinion); +directionService.bindHistoryRecords(historyRecords); +directionService.loadFlowSourceDirection(); + +// 判断是否为通过方向 +if (directionService.isPassRecord()) { + flowNodeService.loadNextPassNode(currentNode); +} else { + flowNodeService.loadDefaultBackNode(currentNode, currentRecord); +} +``` + +### 会签处理 + +对于会签节点(`flowNode.isSign() == true`),`FlowDirectionService.reloadFlowSourceDirection()` 会根据历史记录中所有人的提交状态重新计算审批方向——只有所有人通过后才会继续流转,否则转为拒绝方向。 + +### 流程版本快照 + +```java +// 自动备份(在 FlowStartService 中调用) +FlowBackup backup = flowBackupRepository.backup(flowWork); + +// 从快照恢复流程定义 +FlowWork restoredWork = backup.resume(flowOperatorRepository); +``` + +`FlowBackup` 将 `FlowWork` 序列化为字节数组存储,恢复时通过 `FlowWorkSerializable.fromSerializable(bytes).toFlowWork()` 反序列化,确保运行中的流程不受后续流程定义修改的影响。 + +### FlowSession 上下文 + +`FlowSession` 封装了流程运行时的完整上下文,供 `TitleGenerator`、`OperatorMatcher`、`ErrTrigger`、`FlowRelation.trigger()` 等脚本化组件使用: + +```java +FlowSession session = new FlowSession( + flowRecord, // 当前审批记录 + flowWork, // 流程定义 + flowNode, // 当前节点 + createOperator, // 发起人 + currentOperator, // 当前操作者 + bindData, // 绑定数据 + opinion, // 审批意见 + historyRecords // 历史记录 +); +``` diff --git a/docs/conventions/ddd-layered-architecture.md b/docs/conventions/ddd-layered-architecture.md new file mode 100644 index 000000000..c654008b6 --- /dev/null +++ b/docs/conventions/ddd-layered-architecture.md @@ -0,0 +1,182 @@ +--- +name: ddd-layered-architecture +description: DDD 分层架构与依赖规范 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +项目采用领域驱动设计(DDD)分层架构,解决了以下问题: + +- **关注点分离**:将接口、应用、领域、基础设施四层职责清晰划分 +- **领域纯净**:domain 层不依赖任何外部框架和基础设施实现,只包含纯业务逻辑 +- **可替换性**:infra 层的实现可以替换(如 JPA → MyBatis),不影响 domain 层 +- **CQRS 分离**:查询侧(Query)和命令侧(Command)独立演进,避免读写耦合 + +## 如何使用 + +### 分层结构与依赖方向 + +``` +interface 层(Controller + Handler + Runner) + ↓ 依赖 +app 层(Query Service / Cmd Router) + ↓ 依赖 +domain 层(Entity / Repository接口 / Domain Service / Event / Gateway接口) + ↑ 实现(反向依赖) +infra 层(JPA Entity / Repository实现 / Convertor / Flow集成 / Security配置) +``` + +**核心规则**:依赖只能从上到下,domain 层绝不依赖 infra 层。infra 层通过实现 domain 层定义的接口来注入。 + +### 模块命名规范 + +| 层级 | 模块命名 | 示例 | +|------|---------|------| +| 接口层 | `example-interface` | Controller、Handler、Runner | +| 应用查询 | `example-app-query` | QueryService | +| 应用命令 | `example-app-cmd-domain` | Router / CmdService | +| 应用元数据 | `example-app-cmd-meta` | 元数据相关命令 | +| 领域 | `example-domain-{聚合名}` | Entity、Repository接口、Service | +| 基础设施 | `example-infra-{技术}` | JPA实现、Flow集成、Security配置 | + +### 包结构规范 + +``` +com.codingapi.example. +├── api.domain/ # interface 层 - 命令 Controller +├── api.query/ # interface 层 - 查询 Controller +├── handler/ # interface 层 - 事件 Handler +├── runner/ # interface 层 - 启动 Runner +├── app.query.service/ # app 层 - 查询服务 +├── app.cmd.domain/ # app 层 - 命令路由 +├── domain.{聚合}.entity/ # domain 层 - 实体 +├── domain.{聚合}.repository/ # domain 层 - 仓库接口 +├── domain.{聚合}.service/ # domain 层 - 领域服务 +├── domain.{聚合}.event/ # domain 层 - 领域事件 +├── domain.{聚合}.gateway/ # domain 层 - 外部网关接口 +├── infra.db.entity/ # infra 层 - JPA Entity +├── infra.db.jpa/ # infra 层 - JPA Repository +├── infra.db.repository/ # infra 层 - Domain Repository 实现 +└── infra.db.convert/ # infra 层 - Entity 转换器 +``` + +## 使用实例 + +### ✅ 正确示例:Domain 层定义 Repository 接口 + +```java +// example-domain-user —— 纯 Java 接口,无任何框架依赖 +package com.codingapi.example.domain.user.repository; + +public interface UserRepository { + User getUserByUsername(String username); + User getUserById(long id); + void save(User user); + void delete(long id); +} +``` + +### ✅ 正确示例:Infra 层实现 Repository + +```java +// example-infra-jpa —— 实现 domain 层接口,内部委托 JPA +@Repository +@AllArgsConstructor +public class UserRepositoryImpl implements UserRepository { + + private final UserEntityRepository userEntityRepository; + + @Override + public User getUserByUsername(String username) { + return UserConvertor.convert( + userEntityRepository.getUserEntityByUsername(username), + userEntityRepository + ); + } + + @Override + public void save(User user) { + UserEntity entity = UserConvertor.convert(user); + entity = userEntityRepository.save(entity); + user.setId(entity.getId()); + } +} +``` + +### ✅ 正确示例:Interface 层按 CQRS 分离 Controller + +```java +// 查询 Controller —— 调用 app-query 服务 +@RestController +@RequestMapping("/api/query/user") +@AllArgsConstructor +public class UserQueryController { + private final UserQueryService userQueryService; + + @GetMapping("/list") + public MultiResponse list(SearchRequest searchRequest) { + return MultiResponse.of(userQueryService.list(searchRequest)); + } +} + +// 命令 Controller —— 调用 app-cmd 路由 +@RestController +@RequestMapping("/api/cmd/user") +@AllArgsConstructor +public class UserDomainCmdController { + private final UserRouter userRouter; + + @PostMapping("/save") + public Response save(@RequestBody UserCmd.UpdateRequest request) { + userRouter.createOrUpdate(request); + return Response.buildSuccess(); + } +} +``` + +### ❌ 错误示例:Domain 层直接依赖 JPA + +```java +// domain 层不应引入 javax.persistence / jakarta.persistence +@Entity // ← 禁止!domain 层不能使用 JPA 注解 +@Table(name = "t_user") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; +} +``` + +### ❌ 错误示例:Domain 层依赖 Infra 层的具体类 + +```java +package com.codingapi.example.domain.user.service; + +import com.codingapi.example.infra.db.jpa.UserEntityRepository; // ← 禁止! + +public class UserService { + private final UserEntityRepository repository; // 应使用 UserRepository 接口 +} +``` + +### ❌ 错误示例:查询和命令混在同一个 Controller + +```java +@RestController +@RequestMapping("/api/user") +public class UserController { + // 查询方法 + @GetMapping("/list") + public MultiResponse list() { ... } + + // 命令方法 —— 不应与查询混在一起 + @PostMapping("/save") + public Response save(@RequestBody UserCmd cmd) { ... } + + @DeleteMapping("/remove") + public Response remove(@RequestBody IdRequest req) { ... } +} +``` diff --git a/docs/conventions/event-handler-pattern.md b/docs/conventions/event-handler-pattern.md new file mode 100644 index 000000000..b00f4aba1 --- /dev/null +++ b/docs/conventions/event-handler-pattern.md @@ -0,0 +1,142 @@ +--- +name: event-handler-pattern +description: 事件处理器自动注册规范 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +框架自建的事件系统需要一套统一的 Handler 编写和注册规范,以确保: + +- **自动发现**:新增 Handler 无需手动注册,Spring 容器启动时自动扫描并挂载 +- **类型安全**:通过泛型 `IHandler` 确保 Handler 只接收匹配的事件类型 +- **执行顺序可控**:同一事件的多个 Handler 可通过 `order()` 控制执行顺序 +- **异常隔离**:单个 Handler 的异常不会阻断其他 Handler 的执行(除非主动抛出) +- **循环检测**:自动检测事件链中的循环调用,防止无限递归 + +## 如何使用 + +### 核心接口 + +```java +// 事件标记接口 +public interface IEvent extends Serializable {} +public interface ISyncEvent extends IEvent {} // 同步事件 +public interface IAsyncEvent extends IEvent {} // 异步事件 + +// 事件处理器 +public interface IHandler { + default int order() { return 0; } // 执行顺序,数值越小越先执行 + void handler(T event); // 事件处理逻辑 + default void error(Exception exception) throws Exception { + throw exception; // 默认重新抛出异常 + } +} +``` + +### 注册流程 + +1. 编写 Handler 类,实现 `IHandler` 接口 +2. 标记为 Spring Bean(`@Service`、`@Component` 等) +3. `HandlerBeanDefinitionRegistrar` 在启动时自动扫描所有带 `@Handler` 注解的类 +4. `SpringDefaultEventHandler` / `SpringTransactionEventHandler` 构造函数中收集所有 `IHandler` Bean,注册到 `ApplicationHandlerUtils` +5. 事件推送时由 `ApplicationHandlerUtils.handler()` 按类型匹配 + order 排序后依次调用 + +### 推送事件 + +```java +// 同步推送(默认) +EventPusher.push(event); + +// 指定是否允许循环事件 +EventPusher.push(event, hasLoopEvent); +``` + +## 使用实例 + +### ✅ 正确示例:标准 Handler 编写 + +```java +@Slf4j +@Service +@AllArgsConstructor +public class LeaveHandler implements IHandler { + + private final LeaveService leaveService; + + @Override + public void handler(FlowApprovalEvent event) { + log.info("LeaveHandler: {}", event); + if (event.isFinish() && event.match(LeaveForm.class)) { + LeaveForm form = (LeaveForm) event.getBindData(); + leaveService.create(form.toLeave()); + } + } + + @Override + public int order() { + return 10; // 明确指定执行顺序 + } + + @Override + public void error(Exception exception) throws Exception { + log.error("LeaveHandler error", exception); + // 自定义异常处理,不重新抛出则不会阻断后续 Handler + } +} +``` + +### ✅ 正确示例:定义同步/异步事件 + +```java +// 同步事件 —— 在当前线程中执行 +public class FlowApprovalEvent implements ISyncEvent { + // ... +} + +// 异步事件 —— 在线程池中执行 +public class UserNotificationEvent implements IAsyncEvent { + // ... +} + +// 默认同步(未实现 ISyncEvent 或 IAsyncEvent 时视为同步) +public class UserEvent implements IEvent { + // ... +} +``` + +### ❌ 错误示例:忘记标记为 Spring Bean + +```java +// 没有 @Service/@Component,不会被自动发现和注册 +public class LeaveHandler implements IHandler { + @Override + public void handler(FlowApprovalEvent event) { + // 永远不会被调用 + } +} +``` + +### ❌ 错误示例:在 Handler 中触发相同类型的事件导致循环 + +```java +@Service +public class RecursiveHandler implements IHandler { + @Override + public void handler(UserEvent event) { + // 再次推送同类型事件 → 触发 EventLoopException + EventPusher.push(new UserEvent()); + } +} +``` + +### ❌ 错误示例:在 error() 中静默吞掉异常且不记录 + +```java +@Override +public void error(Exception exception) throws Exception { + // 既不记录日志也不重新抛出,问题被完全隐藏 +} +``` diff --git a/docs/conventions/frontend-css-variables.md b/docs/conventions/frontend-css-variables.md new file mode 100644 index 000000000..353d3c9a3 --- /dev/null +++ b/docs/conventions/frontend-css-variables.md @@ -0,0 +1,194 @@ +--- +name: frontend-css-variables +description: 前端 CSS 变量定义与使用规范 +status: 已实现 +scope: 前端 +source: 项目自有 +--- + +## 解决什么问题 + +在前端项目中,主题颜色、字体大小、背景色等设计令牌(Design Tokens)需要在多处复用,且需要与 Ant Design / Ant Design Mobile 组件库的主题配置保持同步。如果直接在样式文件中硬编码颜色值或尺寸,会导致: + +1. **主题不一致**:修改主色时需要逐个文件查找替换,容易遗漏 +2. **JS/CSS 不同步**:Ant Design 主题 token 和 CSS 样式使用不同的颜色来源 +3. **多端适配困难**:admin-ui 和 mobile-ui 有不同的设计令牌,但缺乏统一的管理方式 + +本规范通过 CSS Custom Properties(CSS 变量)+ SCSS 变量桥接 + `CSSUtils.getRootVariable()` 运行时读取三层机制,实现设计令牌的单一来源管理。 + +## 如何使用 + +### 1. 在 `index.scss` 的 `:root` 中定义 CSS 变量 + +所有 CSS 变量的**实际值**必须在 `src/styles/index.scss` 的 `:root` 选择器中定义。这是唯一的设计令牌来源。 + +**admin-ui/src/styles/index.scss** 定义的变量: +```scss +:root { + --primary-color: #094edc; + --body-background-color: #fdfdfd; + + --content-font-size-large: 24px; + --content-font-size-middle: 16px; + --content-font-size-small: 12px; + + --content-font-size: var(--content-font-size-middle); +} +``` + +**mobile-ui/src/styles/index.scss** 定义的变量: +```scss +:root { + --primary-color: #0f58ea; + --body-background-color: #e6e7ea; + + --content-font-size-large: 24px; + --content-font-size-middle: 16px; + --content-font-size-small: 12px; + + --content-font-size: var(--content-font-size-middle); +} + +// 移动端额外:将主色注入 antd-mobile 主题 +:root:root { + --adm-color-primary: var(--primary-color); +} +``` + +### 2. 在 `variable.scss` 中创建 SCSS 变量桥接 + +为了让 SCSS 文件也能使用这些变量,在 `src/styles/variable.scss` 中用 SCSS 变量包装 CSS 变量,并提供 fallback 值: + +```scss +// 主题颜色 +$theme-primary-color: var(--primary-color, #4a79d8); +// 背景颜色 +$body-background-color: var(--body-background-color, #fdfdfd); +// 标题字体大小 +$title-font-size: var(--content-font-size-middle, 16px); +// 内容字体大小 +$content-font-size: var(--content-font-size, 12px); +``` + +> mobile-ui 的 `variable.scss` 还定义了移动端专属变量(如 `$page-header-height`、`$page-footer-height`),这些不需要 CSS 变量形式,可直接使用固定值。 + +### 3. 在 SCSS 文件中通过 `@use` 引入变量 + +```scss +@use "@/styles/variable" as *; + +.page-header { + background-color: white; + height: $page-header-height !important; + box-shadow: inset 0 0 0 1px $body-background-color; +} +``` + +### 4. 在 TypeScript/React 中通过 `CSSUtils.getRootVariable()` 读取 + +当需要在 JS/TS 代码中获取 CSS 变量值时(如配置 Ant Design 主题),使用 `@codingapi/ui-framework` 提供的 `CSSUtils`: + +```typescript +import {CSSUtils, ThemeConfig, ThemeProvider} from "@codingapi/ui-framework"; + +const theme = { + token: { + colorPrimary: CSSUtils.getRootVariable('--primary-color'), + contentFontSize: CSSUtils.getRootVariable('--content-font-size'), + } +} as ThemeConfig; +``` + +然后将 `theme` 同时传给 `` 和 Ant Design 的 ``,确保框架组件和 UI 库使用同一套主题。 + +## 使用实例 + +### ✅ 正确示例 + +**在 SCSS 中使用变量:** +```scss +@use "@/styles/variable" as *; + +.my-component { + color: $theme-primary-color; + font-size: $content-font-size; + background-color: $body-background-color; +} +``` + +**在 TSX 中注入 Ant Design 主题:** +```tsx +import {CSSUtils, ThemeConfig, ThemeProvider} from "@codingapi/ui-framework"; +import {ConfigProvider} from "antd"; + +const theme = { + token: { + colorPrimary: CSSUtils.getRootVariable('--primary-color'), + contentFontSize: CSSUtils.getRootVariable('--content-font-size'), + } +} as ThemeConfig; + + + + + + +``` + +**在 `:root` 中添加新的 CSS 变量:** +```scss +// index.scss +:root { + --primary-color: #094edc; + --body-background-color: #fdfdfd; + // ✅ 新增变量放在 :root 中 + --sidebar-width: 240px; +} +``` + +```scss +// variable.scss +// ✅ 对应添加 SCSS 桥接变量 +$sidebar-width: var(--sidebar-width, 240px); +``` + +### ❌ 错误示例 + +**直接硬编码颜色值:** +```scss +// ❌ 不要直接使用十六进制颜色 +.my-component { + color: #094edc; + background-color: #fdfdfd; +} +``` + +**在 SCSS 中直接使用 CSS 变量而不经过 variable.scss 桥接:** +```scss +// ❌ 缺少 fallback 值,且没有统一管理 +.my-component { + color: var(--primary-color); +} +``` + +**在 JS 中手动读取 CSS 变量而不用 CSSUtils:** +```typescript +// ❌ 不要手动操作 DOM API +const color = getComputedStyle(document.documentElement) + .getPropertyValue('--primary-color'); +``` + +**在组件内联样式中硬编码主题色:** +```tsx +// ❌ 内联样式无法响应主题变更 +
标题
+``` + +**mobile-ui 忘记注入 antd-mobile 主题变量:** +```scss +// ❌ mobile-ui 的 index.scss 中缺少以下配置会导致 antd-mobile 组件颜色不一致 +// 必须包含: +:root:root { + --adm-color-primary: var(--primary-color); +} +``` diff --git a/docs/conventions/frontend-http-client.md b/docs/conventions/frontend-http-client.md new file mode 100644 index 000000000..ac936a02b --- /dev/null +++ b/docs/conventions/frontend-http-client.md @@ -0,0 +1,212 @@ +--- +name: frontend-http-client +description: 前端 HTTP 请求封装规范 +status: 已实现 +scope: 前端 +source: 项目自有 +--- + +## 解决什么问题 + +前端应用需要与后端 REST API 交互,如果直接使用 axios 或 fetch,会面临以下问题: + +1. **错误处理分散**:每个请求都要手动 try/catch 并显示错误提示,代码重复且容易遗漏 +2. **反馈方式不一致**:不同页面使用不同的消息提示组件(message、notification、Toast),用户体验不统一 +3. **API 调用无结构**:请求散落在组件中,没有按业务领域组织,难以维护和复用 +4. **超时和认证缺失**:缺少统一的超时配置和 Token 注入机制 + +本项目通过 `@codingapi/ui-framework` 提供的 `HttpClient` 类进行统一封装,结合按领域组织的 API 模块文件,实现标准化的 HTTP 请求管理。 + +## 如何使用 + +### 1. 创建 HttpClient 实例 + +在 `src/api/index.ts` 中创建全局单例的 `httpClient`,配置超时时间和消息反馈回调: + +**admin-ui — `src/api/index.ts`:** +```typescript +import {message} from "antd"; +import {HttpClient} from "@codingapi/ui-framework"; + +export const httpClient = new HttpClient(10000, { + success: (msg: string) => { + message.success(msg).then(); + }, + error: (msg: string) => { + message.error(msg).then(); + } +}) +``` + +**mobile-ui — `src/api/index.ts`:** +```typescript +import {Toast} from "antd-mobile"; +import {HttpClient} from "@codingapi/ui-framework"; + +export const httpClient = new HttpClient(10000, { + success: (msg: string) => { + Toast.show({ + content: msg, + }) + }, + error: (msg: string) => { + Toast.show({ + icon: 'fail', + content: msg, + }) + } +}) +``` + +> `HttpClient` 构造函数的第一个参数是超时时间(毫秒),第二个参数是消息回调对象,包含 `success` 和 `error` 两个方法。 + +### 2. 按业务领域创建 API 模块文件 + +在 `src/api/` 目录下按后端领域划分文件,每个文件导出该领域的 API 函数: + +``` +src/api/ +├── index.ts # HttpClient 实例 +├── account.ts # 登录、验证码 +├── user.ts # 用户管理 +├── flow.ts # 工作流 +├── leave.ts # 请假 +├── oss.ts # 文件上传 +└── jar.ts # 部署管理 +``` + +### 3. API 函数命名和签名约定 + +- **查询列表**:使用 `list` 作为函数名,接受 `params`、`sort`、`filter`、`match` 四个参数,调用 `httpClient.page()` +- **查询详情**:使用描述性名称如 `detail`、`users`,调用 `httpClient.get()` +- **命令操作**:使用动词如 `save`、`remove`、`startFlow`,调用 `httpClient.post()` +- 所有 API 函数都应该是 `async function`,返回 `httpClient` 方法的 Promise + +### 4. HttpClient 提供的方法 + +| 方法 | 用途 | 示例 | +|------|------|------| +| `get(url, params?)` | GET 请求 | `httpClient.get('/api/query/user/list', {current:1, pageSize:999999})` | +| `post(url, body)` | POST 请求 | `httpClient.post('/api/cmd/user/save', body)` | +| `page(url, params, sort, filter, match)` | 分页查询 | `httpClient.page('/api/query/flowWork/list', params, sort, filter, match)` | + +## 使用实例 + +### ✅ 正确示例 + +**标准 API 模块文件:** +```typescript +// src/api/user.ts +import {httpClient} from "@/api/index"; + +export async function list( + params: any, + sort: any, + filter: any, + match: { key: string, type: string }[] +) { + return httpClient.page('/api/query/user/list', params, sort, filter, match); +} + +export async function save(body: any) { + return httpClient.post('/api/cmd/user/save', body); +} + +export async function remove(id: any) { + return httpClient.post('/api/cmd/user/remove', {id}); +} +``` + +**在组件中调用 API:** +```tsx +import {list, save} from "@/api/user"; + +const UserPage = () => { + const loadData = async () => { + const result = await list( + {current: 1, pageSize: 20}, + {}, + {}, + [] + ); + // result 已经过 HttpClient 统一处理 + }; + + const handleSave = async (values: any) => { + await save(values); // ✅ 成功/失败消息由 HttpClient 自动展示 + }; +}; +``` + +**登录 API 示例:** +```typescript +// src/api/account.ts +import {httpClient} from "@/api/index"; + +export async function login(body: Account.LoginRequest) { + return httpClient.post('/user/login', body); +} + +export async function captcha() { + return httpClient.get('/open/captcha'); +} +``` + +### ❌ 错误示例 + +**直接在组件中使用 axios/fetch:** +```tsx +// ❌ 不要绕过 HttpClient 直接发请求 +import axios from "axios"; + +const loadData = async () => { + const res = await axios.get('/api/query/user/list'); + // 缺少统一错误处理、超时配置、Token 注入 +}; +``` + +**在组件中内联写请求逻辑:** +```tsx +// ❌ 请求逻辑不应写在组件内部 +const UserPage = () => { + const handleSave = async () => { + const res = await httpClient.post('/api/cmd/user/save', formValues); + if (res.success) { + message.success('保存成功'); + } else { + message.error(res.errMessage); + } + }; +}; +``` + +**API 文件不按领域组织:** +```typescript +// ❌ 不要把所有 API 放在一个大文件中 +// api/all.ts +export async function userList() { ... } +export async function userSave() { ... } +export async function flowList() { ... } +export async function flowSave() { ... } +export async function leaveStart() { ... } +// 几百个函数混在一起,无法维护 +``` + +**自行封装新的 HTTP 工具类:** +```typescript +// ❌ 不要重复造轮子 +class MyHttpClient { + async get(url: string) { + const res = await fetch(url); + return res.json(); + } +} +// 已有 @codingapi/ui-framework 的 HttpClient,不需要自建 +``` + +**忽略消息回调的配置:** +```typescript +// ❌ 不传消息回调会导致用户看不到操作反馈 +export const httpClient = new HttpClient(10000); +// 应该始终提供 success 和 error 回调 +``` diff --git a/docs/conventions/frontend-module-federation.md b/docs/conventions/frontend-module-federation.md new file mode 100644 index 000000000..9aab638d7 --- /dev/null +++ b/docs/conventions/frontend-module-federation.md @@ -0,0 +1,295 @@ +--- +name: frontend-module-federation +description: Module Federation 微前端集成规范 +status: 已实现 +scope: 前端 +source: 项目自有 +--- + +## 解决什么问题 + +本项目包含多个前端应用(admin-ui、mobile-ui),它们需要共享基础依赖(React、ReactDOM)并具备独立部署、运行时集成的能力。传统的 monorepo 或 iframe 方案存在以下问题: + +1. **依赖重复打包**:每个应用都打包一份 React,用户加载多个应用时浪费带宽 +2. **版本不一致**:不同应用使用不同版本的 React 会导致 hooks 报错和运行时异常 +3. **无法运行时组合**:构建时就确定了所有依赖,无法在运行时动态加载其他应用的模块 +4. **部署耦合**:修改一个子应用需要重新构建和部署整个主应用 + +本项目使用 **@module-federation/enhanced + @module-federation/rsbuild-plugin** 配合 Rsbuild 构建工具,实现基于 Module Federation 的微前端架构。 + +## 如何使用 + +### 1. 安装依赖 + +确保 `package.json` 中包含以下依赖: + +```json +{ + "dependencies": { + "@module-federation/enhanced": "^0.17.0" + }, + "devDependencies": { + "@module-federation/rsbuild-plugin": "^0.17.0", + "@rsbuild/core": "1.4.7", + "@rsbuild/plugin-react": "1.3.4", + "@rsbuild/plugin-sass": "..." + } +} +``` + +### 2. 配置 Rsbuild + Module Federation + +在 `rsbuild.config.ts` 中通过 `pluginModuleFederation` 插件配置联邦模块: + +**admin-ui/rsbuild.config.ts:** +```typescript +import * as path from 'path'; +import {defineConfig} from '@rsbuild/core'; +import {pluginReact} from '@rsbuild/plugin-react'; +import {pluginSass} from '@rsbuild/plugin-sass'; +import {pluginModuleFederation} from '@module-federation/rsbuild-plugin'; + +export default defineConfig({ + plugins: [ + pluginReact(), + pluginSass(), + pluginModuleFederation({ + name: "AdminUI", + shared: { + react: { + requiredVersion: '^18.3.1', + singleton: true, + strictVersion: false, + }, + 'react-dom': { + requiredVersion: '^18.3.1', + singleton: true, + strictVersion: false, + }, + } + }, { + ssr: false, + ssrDir: path.resolve(__dirname, 'ssr'), + environment: 'development', + }), + ], + server: { + port: 8000, + }, + source: { + entry: { + index: './src/entry.tsx', + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + react: path.resolve(__dirname, 'node_modules/react'), + 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), + } + }, +}); +``` + +**mobile-ui/rsbuild.config.ts:** +```typescript +import {pluginModuleFederation} from '@module-federation/rsbuild-plugin'; + +export default defineConfig({ + plugins: [ + pluginReact(), + pluginSass(), + pluginModuleFederation({ + name: "MobileUI", // ✅ 每个应用必须有唯一的 name + shared: { + react: { + requiredVersion: '^18.3.1', + singleton: true, + strictVersion: false, + }, + 'react-dom': { + requiredVersion: '^18.3.1', + singleton: true, + strictVersion: false, + }, + } + }, { + ssr: false, + ssrDir: path.resolve(__dirname, 'ssr'), + environment: 'development', + }), + ], + // ...其余配置相同 +}); +``` + +### 3. 关键配置说明 + +| 配置项 | 说明 | +|--------|------| +| `name` | 联邦模块的唯一标识名,如 `"AdminUI"`、`"MobileUI"` | +| `shared.react.singleton: true` | 确保整个运行时只加载一个 React 实例,避免多实例导致 hooks 报错 | +| `shared.react.strictVersion: false` | 允许在一定范围内兼容不同小版本,避免因严格版本匹配导致加载失败 | +| `shared.react.requiredVersion` | 声明所需的 React 版本范围 | +| `ssr: false` | 当前项目不使用 SSR,设为 false | +| `source.entry.index` | 入口文件为 `./src/entry.tsx`,而非直接指向 `index.tsx` | + +### 4. 入口文件异步化 + +Module Federation 要求入口文件使用异步导入,确保共享依赖在应用初始化前完成协商: + +```typescript +// src/entry.tsx +import("./index"); +``` + +### 5. 开发环境代理配置 + +通过 `rsbuild.config.dev.ts` 扩展基础配置,添加后端 API 代理: + +```typescript +import {defineConfig} from '@rsbuild/core'; +import commonConfig from './rsbuild.config'; + +export default defineConfig({ + ...commonConfig, + server: { + port: 8000, + proxy: { + '/api': 'http://127.0.0.1:8090', + '/open': 'http://127.0.0.1:8090', + '/user': 'http://127.0.0.1:8090', + }, + }, +}) +``` + +### 6. Mock 模式配置 + +通过 `rsbuild.config.mock.ts` 提供无后端的开发模式: + +```typescript +import {defineConfig} from '@rsbuild/core'; +import commonConfig from './rsbuild.config'; +import {usersHandler} from "./mocks/user"; +import {productsHandler} from "./mocks/product"; + +export default defineConfig({ + ...commonConfig, + server: { + port: 8000, + }, + dev: { + setupMiddlewares: (middlewares, devServer) => { + if (!devServer) { + throw new Error('webpack-dev-server is not defined'); + } + console.log('mock server is running'); + middlewares.unshift(usersHandler, productsHandler); + } + } +}) +``` + +## 使用实例 + +### ✅ 正确示例 + +**配置 shared 依赖时使用 singleton:** +```typescript +pluginModuleFederation({ + name: "AdminUI", + shared: { + react: { + requiredVersion: '^18.3.1', + singleton: true, // ✅ 保证单例 + strictVersion: false, // ✅ 允许版本弹性 + }, + 'react-dom': { + requiredVersion: '^18.3.1', + singleton: true, + strictVersion: false, + }, + } +}) +``` + +**入口文件使用异步导入:** +```typescript +// ✅ entry.tsx - 异步加载确保共享依赖先协商 +import("./index"); +``` + +**新增一个联邦应用时保持配置一致:** +```typescript +// new-app/rsbuild.config.ts +pluginModuleFederation({ + name: "NewApp", // ✅ 唯一名称 + shared: { + react: { + requiredVersion: '^18.3.1', // ✅ 与其他应用保持一致 + singleton: true, + strictVersion: false, + }, + 'react-dom': { + requiredVersion: '^18.3.1', + singleton: true, + strictVersion: false, + }, + } +}) +``` + +### ❌ 错误示例 + +**忘记设置 singleton: true:** +```typescript +// ❌ 缺少 singleton 可能导致多个 React 实例 +shared: { + react: { + requiredVersion: '^18.3.1', + // singleton: true ← 遗漏! + strictVersion: false, + }, +} +``` + +**入口文件使用同步导入:** +```typescript +// ❌ entry.tsx 不能直接写业务代码 +import React from 'react'; +import ReactDOM from 'react-dom/client'; +// Module Federation 需要先完成共享依赖协商 +// 必须使用 import("./index") 的异步形式 +``` + +**不同应用使用不同的 React 版本范围:** +```typescript +// ❌ AdminUI 使用 ^18.3.1 +shared: { react: { requiredVersion: '^18.3.1' } } + +// ❌ MobileUI 使用 ^17.0.0 — 版本不兼容! +shared: { react: { requiredVersion: '^17.0.0' } } +``` + +**将 rsbuild.config.ts 与 rsbuild.config.dev.ts 混为一谈:** +```typescript +// ❌ 不要在基础配置中写死代理地址 +export default defineConfig({ + server: { + proxy: { + '/api': 'http://127.0.0.1:8090', // 这应该在 dev 配置中 + }, + }, +}) +``` + +**Module Federation 名称重复:** +```typescript +// ❌ 两个应用使用了相同的 name +// admin-ui/rsbuild.config.ts +pluginModuleFederation({ name: "App" }) + +// mobile-ui/rsbuild.config.ts +pluginModuleFederation({ name: "App" }) // 冲突!应改为 "MobileUI" +``` diff --git a/docs/conventions/frontend-state-management.md b/docs/conventions/frontend-state-management.md new file mode 100644 index 000000000..55ddd3f1e --- /dev/null +++ b/docs/conventions/frontend-state-management.md @@ -0,0 +1,286 @@ +--- +name: frontend-state-management +description: Redux Toolkit 状态管理规范 +status: 已实现 +scope: 前端 +source: 项目自有 +--- + +## 解决什么问题 + +前端应用中存在需要跨组件共享的状态(如菜单刷新、页面标题、用户会话信息等)。如果没有统一的状态管理方案,会导致: + +1. **Prop Drilling**:状态需要通过多层组件逐级传递,增加耦合度 +2. **状态散落**:不同开发者用不同方式管理全局状态(Context、localStorage、全局变量等),难以维护 +3. **不可预测的更新**:缺少统一的更新机制,状态变更难以追踪和调试 + +本项目使用 **@reduxjs/toolkit + react-redux** 作为统一的状态管理方案,通过 Slice 模式组织状态逻辑。 + +## 如何使用 + +### 1. 创建 Slice 文件 + +每个独立的状态域创建一个 Slice 文件,放在 `src/store/` 目录下。Slice 文件需要包含: + +- **State 接口定义**:明确状态的类型结构 +- **Action 类型定义**:声明 reducer 函数的签名 +- **createSlice 调用**:定义 name、initialState 和 reducers +- **导出 actions**:从 slice 中解构并导出具名 action + +**admin-ui 示例 — `store/MenuSlice.ts`:** +```typescript +import {createSlice} from '@reduxjs/toolkit'; + +export interface MenuStore { + version: number; +} + +export type MenuStoreAction = { + refresh: (state: MenuStore) => void; +} + +export const menuSlice = createSlice({ + name: 'menu', + initialState: { + version: 0, + }, + reducers: { + refresh: (state) => { + state.version = Math.random(); + }, + }, +}); + +export const {refresh} = menuSlice.actions; +``` + +**mobile-ui 示例 — `sotre/LayoutSlice.ts`:** +```typescript +import {configureStore, createSlice, PayloadAction} from '@reduxjs/toolkit'; + +export interface LayoutStore { + title: string; + backHome: boolean; +} + +export type MenuStoreAction = { + changeTitle: (state: LayoutStore, action: PayloadAction) => void; +} + +export const layoutSlice = createSlice({ + name: 'layout', + initialState: { + title: '首页', + backHome: true + }, + reducers: { + changeTitle: (state, action) => { + const title = action.payload; + state.title = title; + state.backHome = '首页' === title; + } + }, +}); + +export const {changeTitle} = layoutSlice.actions; +``` + +### 2. 注册 Slice 到 Store + +在 `store/Redux.tsx`(admin-ui)或 Slice 文件内(mobile-ui)使用 `configureStore` 组合所有 reducer: + +**admin-ui — `store/Redux.tsx`:** +```typescript +import {configureStore} from '@reduxjs/toolkit'; +import {counterSlice} from './CounterSlice'; +import {menuSlice} from './MenuSlice'; + +const store = configureStore({ + reducer: { + counter: counterSlice.reducer, + menu: menuSlice.reducer, + }, +}); + +export type RootState = ReturnType; +export default store; +``` + +**mobile-ui — 在 `LayoutSlice.ts` 中直接创建 store:** +```typescript +export const layoutStore = configureStore({ + reducer: { + layout: layoutSlice.reducer + }, +}); + +export type LayoutState = ReturnType; +``` + +### 3. 在应用入口挂载 Provider + +**admin-ui — `index.tsx`:** +```tsx +import {Provider} from "react-redux"; +import store from "@/store/Redux"; + +root.render( + + + + + + + + + +); +``` + +**mobile-ui — `layout/index.tsx`:** +```tsx +import {Provider} from "react-redux"; +import {layoutStore} from "@/sotre/LayoutSlice"; + +const Layout = () => { + return ( + + <$Layout/> + + ) +} +``` + +### 4. 在组件中使用 useSelector / useDispatch + +```tsx +import {useSelector} from "react-redux"; +import {RootState} from "@/store/Redux"; + +const Layout = () => { + const menuVersion = useSelector((state: RootState) => state.menu.version); + + useEffect(() => { + // menuVersion 变化时重新加载菜单 + MenuRouteManager.getInstance().refresh(); + actionRef.current?.reload(); + }, [menuVersion]); +}; +``` + +## 使用实例 + +### ✅ 正确示例 + +**创建新的 Slice:** +```typescript +// store/UserSlice.ts +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; + +export interface UserStore { + username: string; + isLoggedIn: boolean; +} + +export type UserStoreAction = { + login: (state: UserStore, action: PayloadAction) => void; + logout: (state: UserStore) => void; +} + +export const userSlice = createSlice({ + name: 'user', + initialState: { + username: '', + isLoggedIn: false, + }, + reducers: { + login: (state, action) => { + state.username = action.payload; + state.isLoggedIn = true; + }, + logout: (state) => { + state.username = ''; + state.isLoggedIn = false; + }, + }, +}); + +export const {login, logout} = userSlice.actions; +``` + +**在组件中 dispatch action:** +```tsx +import {useDispatch} from "react-redux"; +import {refresh} from "@/store/MenuSlice"; + +const SomeComponent = () => { + const dispatch = useDispatch(); + + const handleSave = async () => { + await saveData(); + dispatch(refresh()); // ✅ 触发菜单刷新 + }; +}; +``` + +**带 payload 的 action:** +```tsx +import {useDispatch} from "react-redux"; +import {changeTitle} from "@/sotre/LayoutSlice"; + +const Page = () => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(changeTitle('请假申请')); // ✅ PayloadAction + }, []); +}; +``` + +### ❌ 错误示例 + +**直接修改 state 而不通过 reducer:** +```typescript +// ❌ 永远不要直接修改 store 中的状态 +import store from "@/store/Redux"; +const state = store.getState(); +state.menu.version = 999; // 违反 Redux 不可变原则 +``` + +**在 Slice 外部定义 mutable 全局变量代替 Redux:** +```typescript +// ❌ 不要用全局变量代替状态管理 +let globalMenuVersion = 0; +export const refreshMenu = () => { globalMenuVersion++; }; +``` + +**Slice 文件中不导出 Action 类型:** +```typescript +// ❌ 缺少类型定义,后续维护困难 +export const someSlice = createSlice({ + name: 'some', + initialState: { value: 0 }, + reducers: { + update: (state) => { state.value += 1; }, + }, +}); +// 没有 interface、没有 type alias、没有显式泛型参数 +``` + +**忘记将新 Slice 注册到 store:** +```typescript +// ❌ 创建了 UserSlice 但忘记添加到 configureStore +const store = configureStore({ + reducer: { + counter: counterSlice.reducer, + menu: menuSlice.reducer, + // 缺少 user: userSlice.reducer + }, +}); +``` + +**在组件中硬编码 state 路径而不使用 RootState 类型:** +```tsx +// ❌ 没有类型安全,重构时不会报错 +const version = useSelector((state: any) => state.menu.version); +``` diff --git a/docs/conventions/global-exception-handling.md b/docs/conventions/global-exception-handling.md new file mode 100644 index 000000000..3f8ef53fc --- /dev/null +++ b/docs/conventions/global-exception-handling.md @@ -0,0 +1,178 @@ +--- +name: global-exception-handling +description: 全局异常处理与国际化规范 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +全局异常处理机制解决了以下问题: + +- **统一错误格式**:所有异常自动转换为标准 `Response` 格式(`success=false, errCode, errMessage`) +- **国际化支持**:业务异常通过 `LocaleMessageException` + `messages.properties` 实现多语言错误消息 +- **占位符参数化**:错误消息支持 `{0}`, `{1}` 等占位符,动态填充上下文信息 +- **Controller 无需 try-catch**:业务代码只需抛出异常,由框架统一拦截和转换 + +## 如何使用 + +### 核心组件 + +```java +// 1. ExceptionConfiguration —— 注册国际化消息源 +@Configuration +public class ExceptionConfiguration { + @Bean(initMethod = "init") + public LocaleMessage exceptionLocaleMessage(MessageSource messageSource) { + return new LocaleMessage(messageSource); + } +} + +// 2. BasicHandlerExceptionResolverConfiguration —— 全局异常拦截器 +@Configuration +public class BasicHandlerExceptionResolverConfiguration { + @Bean + public HandlerExceptionResolver servletExceptionHandler() { + return new ServletExceptionHandler(); + } +} + +// 3. LocaleMessageException —— 业务异常类 +public class LocaleMessageException extends RuntimeException { + String errCode; + String errMessage; + + // 从 messages.properties 读取消息 + LocaleMessageException(String errCode); + // 带参数的国际化消息 + LocaleMessageException(String errCode, Object[] args); + // 直接指定消息 + LocaleMessageException(String errCode, String errMessage); + // 静态工厂方法(推荐) + static LocaleMessageException of(String errCode, Object... args); +} +``` + +### 国际化消息配置 + +在 `src/main/resources/messages.properties` 中定义错误码对应的消息: + +```properties +error1=操作失败,请重试 +error2=用户不存在 +error3=参数错误:{0} +system.err=系统内部错误 +``` + +### 异常类型说明 + +| 异常类 | 用途 | 处理方式 | +|--------|------|---------| +| `LocaleMessageException` | 业务异常,支持国际化 | 提取 errCode + errMessage 返回 | +| `EventException` | 事件处理器执行异常聚合 | 包含多个子异常列表 | +| `EventLoopException` | 事件循环调用检测 | 立即中断事件链 | +| 其他 `RuntimeException` | 未预期异常 | 使用 `system.err` 作为默认错误码 | + +## 使用实例 + +### ✅ 正确示例:使用国际化错误码 + +```java +@GetMapping("/error1") +public Response error1() { + // 从 messages.properties 读取 error1 对应的消息 + throw new LocaleMessageException("error1"); +} +``` + +### ✅ 正确示例:带参数的国际化异常 + +```java +@GetMapping("/error3") +public Response error3() { + // messages.properties: error3=参数错误:{0} + // 最终消息:参数错误:my is arg + throw LocaleMessageException.of("error3", "my is arg"); +} +``` + +### ✅ 正确示例:直接指定错误消息(无需国际化时) + +```java +@GetMapping("/error2") +public Response error2() { + throw new LocaleMessageException("error2", "error 2 message"); +} +``` + +### ✅ 正确示例:在 Domain Service 中抛出业务异常 + +```java +@Service +@AllArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public User getUser(long id) { + User user = userRepository.getUserById(id); + if (user == null) { + throw LocaleMessageException.of("user.not.found", id); + } + return user; + } +} +``` + +### ❌ 错误示例:在 Controller 中手动捕获异常 + +```java +@PostMapping("/save") +public Response save(@RequestBody UserCmd cmd) { + try { + userRouter.createOrUpdate(cmd); + return Response.buildSuccess(); + } catch (LocaleMessageException e) { + // 禁止!全局异常处理器会自动处理 + return Response.buildFailure(e.getErrCode(), e.getErrMessage()); + } +} +``` + +### ❌ 错误示例:直接使用 RuntimeException 而非 LocaleMessageException + +```java +public void validate(String name) { + if (name == null) { + // 禁止!缺少 errCode,无法被前端识别和处理 + throw new RuntimeException("名称不能为空"); + } +} +``` + +### ❌ 错误示例:硬编码中文错误消息而不使用国际化 + +```java +// 不推荐:消息写死在代码中,无法切换语言 +throw new LocaleMessageException("error.hardcoded", "用户名已存在"); + +// 推荐:使用错误码 + messages.properties +throw new LocaleMessageException("user.name.duplicate"); +``` + +### ❌ 错误示例:忽略 EventException 中的子异常信息 + +```java +try { + EventPusher.push(event); +} catch (EventException e) { + // 禁止!丢失了具体的子异常详情 + log.error("事件处理失败"); + + // 正确做法:遍历子异常 + for (Exception sub : e.getError()) { + log.error("子异常: {}", sub.getMessage(), sub); + } +} +``` diff --git a/docs/conventions/index.md b/docs/conventions/index.md new file mode 100644 index 000000000..73b2838df --- /dev/null +++ b/docs/conventions/index.md @@ -0,0 +1,20 @@ +# 规范(Conventions) + +> 🔄 此文件由 `rebuild_pkr_index.py` 自动生成,请勿手动编辑。 + +## ✅ 已实现 + +| 名称 | 描述 | 范围 | 来源 | +|------|------|------|------| +| [ddd-layered-architecture](./ddd-layered-architecture.md) | DDD 分层架构与依赖规范 | 后端 | 项目自有 | +| [event-handler-pattern](./event-handler-pattern.md) | 事件处理器自动注册规范 | 后端 | 项目自有 | +| [frontend-css-variables](./frontend-css-variables.md) | 前端 CSS 变量定义与使用规范 | 前端 | 项目自有 | +| [frontend-http-client](./frontend-http-client.md) | 前端 HTTP 请求封装规范 | 前端 | 项目自有 | +| [frontend-module-federation](./frontend-module-federation.md) | Module Federation 微前端集成规范 | 前端 | 项目自有 | +| [frontend-state-management](./frontend-state-management.md) | Redux Toolkit 状态管理规范 | 前端 | 项目自有 | +| [global-exception-handling](./global-exception-handling.md) | 全局异常处理与国际化规范 | 后端 | 项目自有 | +| [unified-response-convention](./unified-response-convention.md) | API 统一响应返回规范 | 后端 | 项目自有 | + +--- + +**统计**: 共 8 篇 — 已实现 8 / 计划中 0 / 已废弃 0 diff --git a/docs/conventions/unified-response-convention.md b/docs/conventions/unified-response-convention.md new file mode 100644 index 000000000..d5b7d4871 --- /dev/null +++ b/docs/conventions/unified-response-convention.md @@ -0,0 +1,161 @@ +--- +name: unified-response-convention +description: API 统一响应返回规范 +status: 已实现 +scope: 后端 +source: 项目自有 +--- + +## 解决什么问题 + +统一的响应格式解决了以下问题: + +- **前后端契约**:所有 API 返回结构一致,前端可以用统一的拦截器处理成功/失败 +- **错误标准化**:错误信息包含 `errCode` + `errMessage`,支持国际化和本地化展示 +- **类型安全**:通过泛型 Response 子类明确每个接口的返回数据类型 +- **分页统一**:列表接口自带 `total` 字段,无需额外约定分页元数据格式 + +## 如何使用 + +### Response 体系 + +```java +// 基础响应 —— 仅表示成功/失败 +public class Response { + boolean success; + String errCode; + String errMessage; + + static Response buildSuccess(); + static Response buildFailure(String errCode, String errMessage); +} + +// 单对象响应 +public class SingleResponse extends Response { + T data; + static SingleResponse of(T data); + static SingleResponse empty(); +} + +// 列表响应(含分页) +public class MultiResponse extends Response { + Content data; // { total: long, list: Collection } + static MultiResponse of(Collection data); + static MultiResponse of(Collection data, long total); + static MultiResponse of(Page page); + static MultiResponse empty(); +} + +// Map 响应 +public class MapResponse extends Response { + Map data; + static MapResponse create(); + MapResponse add(String key, Object value); +} +``` + +### 使用规则 + +| 场景 | 返回类型 | 工厂方法 | +|------|---------|---------| +| 无返回数据的操作 | `Response` | `Response.buildSuccess()` | +| 返回单个对象 | `SingleResponse` | `SingleResponse.of(data)` | +| 返回列表/分页 | `MultiResponse` | `MultiResponse.of(list)` / `MultiResponse.of(page)` | +| 返回键值对 | `MapResponse` | `MapResponse.create().add(k, v)` | +| 业务错误 | `Response` | `Response.buildFailure(code, msg)` 或抛出 `LocaleMessageException` | + +## 使用实例 + +### ✅ 正确示例:命令接口返回 Response + +```java +@PostMapping("/save") +public Response save(@RequestBody UserCmd.UpdateRequest request) { + userRouter.createOrUpdate(request); + return Response.buildSuccess(); +} + +@PostMapping("/remove") +public Response remove(@RequestBody IdRequest request) { + userRouter.removeUser(request.getLongId()); + return Response.buildSuccess(); +} +``` + +### ✅ 正确示例:查询接口返回 MultiResponse + +```java +@GetMapping("/list") +public MultiResponse list(SearchRequest searchRequest) { + return MultiResponse.of(userQueryService.list(searchRequest)); +} +``` + +### ✅ 正确示例:返回单个对象用 SingleResponse + +```java +@GetMapping("/detail") +public SingleResponse getFlowStep(@RequestBody IdRequest request) { + FlowWork work = flowAppQueryService.getFlowStep(request.getLongId()); + return SingleResponse.of(work); +} +``` + +### ✅ 正确示例:返回 Map 数据 + +```java +@GetMapping("/stats") +public MapResponse stats() { + return MapResponse.create() + .add("totalUsers", userService.count()) + .add("activeUsers", userService.countActive()); +} +``` + +### ❌ 错误示例:直接返回 Entity 而非 Response 包装 + +```java +@GetMapping("/user") +public User getUser(@RequestParam long id) { + // 禁止!缺少 success/errCode/errMessage 标准字段 + return userService.getById(id); +} +``` + +### ❌ 错误示例:手动构造 JSON 字符串 + +```java +@GetMapping("/list") +public String list() { + // 禁止!绕过 Response 体系,前端无法统一处理 + return "{\"success\":true,\"data\":[...]}"; +} +``` + +### ❌ 错误示例:用 null 代替正确的空响应 + +```java +@GetMapping("/detail") +public SingleResponse detail(@RequestParam long id) { + User user = userService.getById(id); + if (user == null) { + return null; // 禁止!应使用 SingleResponse.empty() + } + return SingleResponse.of(user); +} +``` + +### ❌ 错误示例:在 Controller 中捕获异常并手动构建错误响应 + +```java +@PostMapping("/save") +public Response save(@RequestBody UserCmd cmd) { + try { + userRouter.createOrUpdate(cmd); + return Response.buildSuccess(); + } catch (Exception e) { + // 禁止!应由全局异常处理器统一处理 + return Response.buildFailure("error", e.getMessage()); + } +} +``` From e24a00c4af5a386b2a9887cbf779e3791a875c1f Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 8 Jun 2026 18:12:54 +0800 Subject: [PATCH 06/10] update docs --- .claude/settings.json | 3 + CLAUDE.md | 11 +- docs/capabilities/apache-groovy.md | 86 ----- docs/capabilities/bouncycastle.md | 128 -------- docs/capabilities/byte-buddy.md | 136 -------- docs/capabilities/command-executor.md | 279 ----------------- docs/capabilities/commons-crypto.md | 110 ------- docs/capabilities/commons-io.md | 100 ------ docs/capabilities/commons-text.md | 87 ------ docs/capabilities/data-authorization.md | 206 ------------ .../capabilities/domain-change-interceptor.md | 165 ++++++++++ docs/capabilities/domain-event.md | 202 ------------ docs/capabilities/event-system.md | 238 +++++--------- docs/capabilities/exception-handling.md | 254 --------------- docs/capabilities/fast-repository.md | 259 ++++++--------- docs/capabilities/fastjson.md | 121 ------- docs/capabilities/flow-postpone-event.md | 77 +++++ docs/capabilities/groovy-script-engine.md | 280 ++++++----------- docs/capabilities/http-client-factory.md | 165 ---------- docs/capabilities/httpclient5.md | 119 ------- docs/capabilities/index.md | 54 ++-- docs/capabilities/jackson-databind.md | 124 -------- docs/capabilities/jackson.md | 122 ++++++++ docs/capabilities/jdbc-proxy-chain.md | 95 ++++++ docs/capabilities/jjwt.md | 146 --------- docs/capabilities/jsqlparser.md | 78 ----- docs/capabilities/kryo.md | 87 ------ docs/capabilities/locale-exception.md | 156 +++++++++ docs/capabilities/page-request-filter.md | 219 ------------- docs/capabilities/rest-template-context.md | 113 ------- docs/capabilities/security-auth.md | 263 ---------------- docs/capabilities/spring-data-jpa.md | 115 ++++--- docs/capabilities/spring-data-redis.md | 105 +++++-- docs/capabilities/spring-framework.md | 110 +++++++ docs/capabilities/spring-ioc-container.md | 85 ----- docs/capabilities/spring-security.md | 109 +++++-- docs/capabilities/spring-web-mvc.md | 109 ------- docs/capabilities/sql-interceptor.md | 186 +++++++++++ docs/capabilities/token-gateway.md | 101 ++++++ .../transaction-manager-context.md | 141 +++++++++ docs/capabilities/trigger-system.md | 230 ++++++++++++++ docs/capabilities/unified-response.md | 208 ------------ docs/capabilities/workflow-engine.md | 284 ++++++----------- docs/conventions/autoconfiguration-pattern.md | 134 ++++++++ docs/conventions/context-singleton.md | 124 ++++++++ docs/conventions/ddd-layered-architecture.md | 182 ----------- docs/conventions/event-handler-pattern.md | 142 --------- docs/conventions/frontend-css-variables.md | 194 ------------ docs/conventions/frontend-http-client.md | 212 ------------- .../conventions/frontend-module-federation.md | 295 ------------------ docs/conventions/frontend-state-management.md | 286 ----------------- docs/conventions/global-exception-handling.md | 178 ----------- docs/conventions/index.md | 15 +- docs/conventions/locale-exception-handling.md | 127 ++++++++ docs/conventions/slf4j-logging.md | 118 +++++++ .../unified-response-convention.md | 161 ---------- docs/conventions/unified-response.md | 117 +++++++ 57 files changed, 2616 insertions(+), 5935 deletions(-) delete mode 100644 docs/capabilities/apache-groovy.md delete mode 100644 docs/capabilities/bouncycastle.md delete mode 100644 docs/capabilities/byte-buddy.md delete mode 100644 docs/capabilities/command-executor.md delete mode 100644 docs/capabilities/commons-crypto.md delete mode 100644 docs/capabilities/commons-io.md delete mode 100644 docs/capabilities/commons-text.md delete mode 100644 docs/capabilities/data-authorization.md create mode 100644 docs/capabilities/domain-change-interceptor.md delete mode 100644 docs/capabilities/domain-event.md delete mode 100644 docs/capabilities/exception-handling.md delete mode 100644 docs/capabilities/fastjson.md create mode 100644 docs/capabilities/flow-postpone-event.md delete mode 100644 docs/capabilities/http-client-factory.md delete mode 100644 docs/capabilities/httpclient5.md delete mode 100644 docs/capabilities/jackson-databind.md create mode 100644 docs/capabilities/jackson.md create mode 100644 docs/capabilities/jdbc-proxy-chain.md delete mode 100644 docs/capabilities/jjwt.md delete mode 100644 docs/capabilities/jsqlparser.md delete mode 100644 docs/capabilities/kryo.md create mode 100644 docs/capabilities/locale-exception.md delete mode 100644 docs/capabilities/page-request-filter.md delete mode 100644 docs/capabilities/rest-template-context.md delete mode 100644 docs/capabilities/security-auth.md create mode 100644 docs/capabilities/spring-framework.md delete mode 100644 docs/capabilities/spring-ioc-container.md delete mode 100644 docs/capabilities/spring-web-mvc.md create mode 100644 docs/capabilities/sql-interceptor.md create mode 100644 docs/capabilities/token-gateway.md create mode 100644 docs/capabilities/transaction-manager-context.md create mode 100644 docs/capabilities/trigger-system.md delete mode 100644 docs/capabilities/unified-response.md create mode 100644 docs/conventions/autoconfiguration-pattern.md create mode 100644 docs/conventions/context-singleton.md delete mode 100644 docs/conventions/ddd-layered-architecture.md delete mode 100644 docs/conventions/event-handler-pattern.md delete mode 100644 docs/conventions/frontend-css-variables.md delete mode 100644 docs/conventions/frontend-http-client.md delete mode 100644 docs/conventions/frontend-module-federation.md delete mode 100644 docs/conventions/frontend-state-management.md delete mode 100644 docs/conventions/global-exception-handling.md create mode 100644 docs/conventions/locale-exception-handling.md create mode 100644 docs/conventions/slf4j-logging.md delete mode 100644 docs/conventions/unified-response-convention.md create mode 100644 docs/conventions/unified-response.md diff --git a/.claude/settings.json b/.claude/settings.json index 19959dfbb..ca30ed8e6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -3,5 +3,8 @@ "allow": [ "Bash(echo \"exit: $?\")" ] + }, + "enabledPlugins": { + "pkr@pkr-marketplace": true } } diff --git a/CLAUDE.md b/CLAUDE.md index ba71046ef..fd27cbc44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -170,14 +170,15 @@ codingapi.framework.handler-thread-pool-size=20 # 事件异步线程池大小 计划方案中必须包含: 1. **复用了哪些已有能力** — 列出从 PKR 中找到并复用的 Capability 2. **遵循了哪些规范** — 列出遵守的 Convention -3. **是否有新增能力** — 如果本次开发产生了可复用的新能力,完成后通过 `/pkr-scan add` 注册 +3. **是否有新增能力** — 如果本次开发产生了可复用的新能力,完成后通过 `/pkr-add` 注册 ### 知识管理命令 | 命令 | 用途 | |------|------| -| `/pkr-scan init` | 首次扫描项目,发现候选能力和规范 | -| `/pkr-scan sync` | 全量同步,对比代码变更 | -| `/pkr-scan update ` | 单项更新指定能力或规范 | -| `/pkr-scan add` | 手动注册新的能力或规范 | +| `/pkr-init` | 首次扫描项目,发现候选能力和规范 | +| `/pkr-sync` | 全量同步,对比代码变更 | +| `/pkr-update [desc]` | 单项更新,可带描述指导更新 | +| `/pkr-add [name] ` | 从代码/框架扫描注册(名称可省略) | +| `/pkr-add plan [name] ` | 注册计划中的能力(名称可省略) | diff --git a/docs/capabilities/apache-groovy.md b/docs/capabilities/apache-groovy.md deleted file mode 100644 index 785bfa7cd..000000000 --- a/docs/capabilities/apache-groovy.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: apache-groovy -description: 动态语言运行时,支持脚本编译与执行 -status: 已实现 -scope: 后端 -source: 框架:Apache Groovy ---- - -## 解决什么问题 - -Apache Groovy 为项目提供运行时脚本引擎能力,解决了以下问题: - -- **动态逻辑**:允许在不重启应用的情况下修改业务逻辑,适用于规则引擎、动态报表等场景 -- **热更新**:脚本存储在数据库中,支持运行时编译和缓存,修改即时生效 -- **REST API 驱动**:通过 REST 接口管理脚本的编译和执行,便于运维操作 -- **与工作流集成**:flow 模块使用 GroovyShell 执行流程条件表达式 - -## 如何使用 - -项目使用 Groovy 4.0.24,在 `springboot-starter-script` 和 `springboot-starter-flow` 两个模块中使用: - -1. **GroovyScriptRuntime**:核心运行时,基于 `GroovyShell` + `GroovyClassLoader`,内置 LRU 缓存避免重复编译 -2. **编译模式**:`compile(script, cache)` 支持缓存和非缓存两种模式,缓存 key 为脚本 SHA256 哈希 -3. **函数调用**:`invoke(method, script, returnType, binds, args)` 支持调用脚本中定义的函数并绑定变量 -4. **启动加载**:`GroovyScriptEngineRunner` 在应用启动时从数据库加载临时脚本到缓存 -5. **REST API**:`GroovyScriptController` 提供脚本编译和执行的 HTTP 接口 - -## 使用实例 - -### GroovyScriptRuntime 编译与缓存 - -```java -public class GroovyScriptRuntime { - - private final GroovyShell shell; - private final Map cache; // LRU 缓存 - - public void compile(String script, boolean cache) { - if (cache) { - String key = SHA256.sha256(script); - Script runtime = this.cache.get(key); - if (runtime == null) { - runtime = this.shell.parse(script); - this.cache.put(key, runtime); - } - } else { - this.shell.parse(script); - } - } - - public T invoke(String method, String script, Class returnType, - Map binds, Object... args) { - // 执行脚本中的指定函数 - } -} -``` - -### 启动时自动加载脚本 - -```java -public class GroovyScriptEngineRunner implements InitializingBean, DisposableBean { - - @Override - public void afterPropertiesSet() throws Exception { - // 从数据库分页加载所有临时脚本到缓存 - PageRequest request = PageRequest.of(0, 100); - Page page = TempGroovyScriptRepositoryContext.getInstance().find(request); - while (page.hasNext()) { - TempGroovyScriptContext.getInstance().loadAll(page.getContent()); - request = PageRequest.of(request.getCurrent() + 1, 100); - page = TempGroovyScriptRepositoryContext.getInstance().find(request); - } - } -} -``` - -### Flow 模块中的 GroovyShell 使用 - -```java -// springboot-starter-flow 中使用 GroovyShell 执行流程条件脚本 -public class GroovyShellContext { - public T run(String script, Class returnType, Object... args) { - // 执行流程节点的条件判断脚本 - } -} -``` diff --git a/docs/capabilities/bouncycastle.md b/docs/capabilities/bouncycastle.md deleted file mode 100644 index 35295d753..000000000 --- a/docs/capabilities/bouncycastle.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -name: bouncycastle -description: 密码学算法库,提供 AES/RSA 等加密算法支持 -status: 已实现 -scope: 后端 -source: 框架:BouncyCastle ---- - -## 解决什么问题 - -框架需要提供数据加解密能力,用于: -- 安全模块中 Token IV(初始化向量)的 AES 加密/解密 -- RSA 非对称加密用于数据传输安全 -- 作为 JCA/JCE Provider 注册,增强 JDK 默认的密码学算法支持 - -BouncyCastle 作为 Java 密码学扩展提供者(Provider),通过 `Security.addProvider()` 注册后,JDK 标准 Cipher API 即可使用更丰富的算法实现。 - -## 如何使用 - -### 依赖引入 - -```xml - - - org.bouncycastle - bcprov-jdk18on - 1.79 - -``` - -### 核心类 - -- `AES` — AES 对称加密封装类(支持 CBC/PKCS5Padding) -- `RSA` — RSA 非对称加密封装类(2048 位密钥对) -- `AESUtils` — AES 工具类(单例模式) -- `RSAUtils` — RSA 工具类(单例模式,内置默认密钥对) - -## 使用实例 - -### 1. AES 对称加密 - -```java -// AES.java - 使用 BouncyCastle Provider -public class AES { - public static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; - - public AES() throws Exception { - this(CIPHER_ALGORITHM, 256); - } - - public AES(String algorithm, int generateKeySize) throws Exception { - // 注册 BouncyCastle Provider - Security.addProvider(new BouncyCastleProvider()); - this.algorithm = algorithm; - this.key = generateKey(generateKeySize); - this.iv = generateIV(randomIv()); - } - - public byte[] encrypt(byte[] data) throws Exception { - Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.ENCRYPT_MODE, key, iv); - return cipher.doFinal(data); - } - - public byte[] decrypt(byte[] encryptedData) throws Exception { - Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.DECRYPT_MODE, key, iv); - return cipher.doFinal(encryptedData); - } -} -``` - -### 2. RSA 非对称加密 - -```java -// RSA.java - 使用 BouncyCastle Provider -public class RSA { - public RSA() throws NoSuchAlgorithmException { - Security.addProvider(new BouncyCastleProvider()); - KeyPair keyPair = generateKey(); - this.privateKey = keyPair.getPrivate(); - this.publicKey = keyPair.getPublic(); - } - - public KeyPair generateKey() throws NoSuchAlgorithmException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); - keyGen.initialize(2048); - return keyGen.generateKeyPair(); - } - - public byte[] encrypt(byte[] text) throws Exception { - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - return cipher.doFinal(text); - } - - public byte[] decrypt(byte[] text) throws Exception { - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(Cipher.DECRYPT_MODE, privateKey); - return cipher.doFinal(text); - } -} -``` - -### 3. Security 模块中的 AES 工具 - -```java -// AESTools.java - Security 模块中使用 AES 加密 Token IV -public class AESTools { - private AES aes; - - public void init(AES aes) { - this.aes = aes; - } - - // Token 创建时加密 IV - // Token.java: this.iv = AESTools.getInstance().encode(iv); - // Token 解析时解密 IV - // Token.java: return AESTools.getInstance().decode(iv); -} -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter | `AES`、`RSA`、`AESUtils`、`RSAUtils` 加密工具类 | -| springboot-starter-security | `AESTools` Token IV 加解密 | diff --git a/docs/capabilities/byte-buddy.md b/docs/capabilities/byte-buddy.md deleted file mode 100644 index 64bcd3801..000000000 --- a/docs/capabilities/byte-buddy.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -name: byte-buddy -description: 字节码操作库,用于运行时动态生成 JPA 实体类 -status: 已实现 -scope: 后端 -source: 框架:Byte Buddy ---- - -## 解决什么问题 - -在 springboot-starter-data-fast 模块中,需要支持**动态表结构**场景: -- 根据元数据(`TableEntityMetadata`)在运行时动态生成 JPA 实体类 -- 生成的实体类需要携带 `@Entity`、`@Table`、`@Column`、`@Id` 等 JPA 注解 -- 生成的类需要有标准的 getter/setter 方法 -- 生成的类需要注册到自定义 ClassLoader 中以被 Hibernate/JPA 识别 - -Byte Buddy 提供了类型安全的字节码操作 API,可以在运行时创建带有注解和方法的 Java 类,无需编译期代码生成。 - -## 如何使用 - -### 依赖引入 - -```xml - - - net.bytebuddy - byte-buddy - -``` - -### 核心类 - -- `TableEntityClassBuilder` — 基于 Byte Buddy 的动态实体类构建器 -- `TableEntityMetadata` — 实体元数据定义(表名、列信息、注解等) -- `DynamicTableClassLoader` — 动态类加载器,注册运行时生成的类 - -## 使用实例 - -### 1. 动态构建 JPA 实体类 - -```java -// TableEntityClassBuilder.java -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.description.annotation.AnnotationDescription; -import net.bytebuddy.description.modifier.Visibility; -import net.bytebuddy.dynamic.DynamicType; -import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; -import net.bytebuddy.implementation.FieldAccessor; - -class TableEntityClassBuilder { - - private final TableEntityMetadata metadata; - private DynamicType.Builder builder; - - public TableEntityClassBuilder(TableEntityMetadata metadata) { - this.metadata = metadata; - // 创建子类并添加 @Entity、@Table 注解 - this.builder = new ByteBuddy() - .subclass(Object.class) - .name(metadata.getClassName()) - .implement(Serializable.class) - .annotateType(buildEntityAnnotations()); - } - - public Class build() { - this.buildColumns(); - Class clazz = builder.make() - .load(TableEntityClassBuilder.class.getClassLoader(), - ClassLoadingStrategy.Default.WRAPPER) - .getLoaded(); - // 注册到动态类加载器 - DynamicTableClassLoader.getInstance().registerClass(clazz); - return clazz; - } -} -``` - -### 2. 动态添加字段和注解 - -```java -// TableEntityClassBuilder.java - 添加带 JPA 注解的字段 -private void addColumnField(TableEntityMetadata.ColumnMeta columnMeta) { - String fieldName = columnMeta.getFieldName(); - Class fieldType = columnMeta.getType(); - - // 构建字段注解 (@Column, @Id, @GeneratedValue 等) - List fieldAnnotations = buildFieldAnnotations(columnMeta); - - // 定义私有字段并添加注解 - builder = builder.defineField(fieldName, fieldType, Visibility.PRIVATE) - .annotateField(fieldAnnotations.toArray(new AnnotationDescription[0])); - - // 自动生成 getter/setter 方法 - String capitalizedFieldName = capitalize(fieldName); - builder = builder - .defineMethod("get" + capitalizedFieldName, fieldType, Visibility.PUBLIC) - .intercept(FieldAccessor.ofField(fieldName)) - .defineMethod("set" + capitalizedFieldName, void.class, Visibility.PUBLIC) - .withParameter(fieldType) - .intercept(FieldAccessor.ofField(fieldName)); -} -``` - -### 3. 构建实体类注解 - -```java -// TableEntityClassBuilder.java - 动态构建 @Entity + @Table 注解 -private AnnotationDescription[] buildEntityAnnotations() { - List annotations = new ArrayList<>(); - - // @Entity - annotations.add(AnnotationDescription.Builder.ofType(Entity.class).build()); - - // @Table(name = "xxx") - if (metadata.getTable() != null) { - AnnotationDescription.Builder tableBuilder = - AnnotationDescription.Builder.ofType(Table.class); - tableBuilder = tableBuilder.define("name", metadata.getTable().getName()); - annotations.add(tableBuilder.build()); - } - - // @Comment - if (StringUtils.hasText(metadata.getTable().getComment())) { - annotations.add(AnnotationDescription.Builder.ofType(Comment.class) - .define("value", metadata.getTable().getComment()).build()); - } - - return annotations.toArray(new AnnotationDescription[0]); -} -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter-data-fast | `TableEntityClassBuilder` 运行时动态生成 JPA 实体类 | diff --git a/docs/capabilities/command-executor.md b/docs/capabilities/command-executor.md deleted file mode 100644 index 8ab8c386d..000000000 --- a/docs/capabilities/command-executor.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -name: command-executor -description: CQRS 命令执行器接口,定义标准化的命令-响应契约,支持带参和无参两种执行模式 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在 CQRS(命令查询职责分离)架构中,写操作(Command)和读操作(Query)需要明确分离。实际开发中常见以下问题: - -1. **Service 方法签名不统一**:有的方法返回 void,有的返回 DTO,有的返回 Response,调用方无法预期 -2. **命令对象缺乏规范**:不同开发者定义的命令参数格式各异,缺少统一的输入输出契约 -3. **响应与命令脱节**:命令执行后的结果没有标准化的封装方式,成功/失败判断逻辑分散 -4. **DDD 应用层编排困难**:Application Service 需要协调多个领域服务,但缺乏清晰的命令执行抽象 - -`IExecutor` 接口定义了两种命令执行器契约: -- `IExecutor.Command` — 接收命令对象 C,返回响应 R -- `IExecutor.Void` — 无参执行,返回响应 R - -其中 R 必须继承自 `Response`,确保所有命令执行结果都遵循统一响应格式。 - -## 如何使用 - -### 1. 定义命令对象 - -命令对象是纯数据载体,描述"要做什么": - -```java -@Getter -@Setter -public class CreateUserCommand { - private String name; - private String email; - private Integer age; -} -``` - -### 2. 实现 Command 执行器 - -```java -@Component -public class CreateUserExecutor implements IExecutor.Command, CreateUserCommand> { - - @Autowired - private UserRepository userRepository; - - @Override - public SingleResponse execute(CreateUserCommand command) { - User user = new User(); - user.setName(command.getName()); - user.setEmail(command.getEmail()); - user.setAge(command.getAge()); - - userRepository.save(user); - - UserDTO dto = convertToDTO(user); - return SingleResponse.of(dto); - } -} -``` - -### 3. 实现 Void 执行器 - -适用于不需要传入命令对象的场景: - -```java -@Component -public class SystemCleanupExecutor implements IExecutor.Void { - - @Autowired - private CacheService cacheService; - - @Override - public Response execute() { - cacheService.clearAll(); - return Response.buildSuccess(); - } -} -``` - -### 4. 在 Controller 或 Application Service 中调用 - -```java -@RestController -@RequestMapping("/api/users") -public class UserController { - - @Autowired - private CreateUserExecutor createUserExecutor; - - @PostMapping - public SingleResponse createUser(@RequestBody CreateUserCommand cmd) { - return createUserExecutor.execute(cmd); - } -} -``` - -## 使用实例 - -### DDD 应用层中的命令编排 - -```java -// ===== 命令定义 ===== - -@Getter @Setter -public class TransferMoneyCommand { - private Long fromAccountId; - private Long toAccountId; - private BigDecimal amount; - private String remark; -} - -// ===== 执行器实现 ===== - -@Component -public class TransferMoneyExecutor - implements IExecutor.Command, TransferMoneyCommand> { - - @Autowired - private AccountRepository accountRepository; - - @Autowired - private TransactionRepository transactionRepository; - - @Override - @Transactional - public SingleResponse execute(TransferMoneyCommand cmd) { - // 1. 加载聚合根 - Account fromAccount = accountRepository.findById(cmd.getFromAccountId()) - .orElseThrow(() -> LocaleMessageException.of("ACCOUNT_NOT_FOUND", cmd.getFromAccountId())); - Account toAccount = accountRepository.findById(cmd.getToAccountId()) - .orElseThrow(() -> LocaleMessageException.of("ACCOUNT_NOT_FOUND", cmd.getToAccountId())); - - // 2. 执行业务逻辑 - fromAccount.debit(cmd.getAmount()); - toAccount.credit(cmd.getAmount()); - - // 3. 保存状态 - accountRepository.save(fromAccount); - accountRepository.save(toAccount); - - // 4. 创建交易记录 - Transaction tx = new Transaction(fromAccount, toAccount, cmd.getAmount(), cmd.getRemark()); - transactionRepository.save(tx); - - // 5. 推送领域事件 - EventPusher.push(new MoneyTransferredEvent(tx.getId())); - - return SingleResponse.of(convertToDTO(tx)); - } -} - -// ===== Controller ===== - -@RestController -@RequestMapping("/api/transfers") -public class TransferController { - - @Autowired - private TransferMoneyExecutor transferExecutor; - - @PostMapping - public SingleResponse transfer(@RequestBody TransferMoneyCommand cmd) { - return transferExecutor.execute(cmd); - } -} -``` - -### 批量命令执行器 - -```java -@Getter @Setter -public class BatchImportUsersCommand { - private List users; -} - -@Component -public class BatchImportUsersExecutor - implements IExecutor.Command, BatchImportUsersCommand> { - - @Autowired - private UserRepository userRepository; - - @Override - @Transactional - public MultiResponse execute(BatchImportUsersCommand cmd) { - List results = new ArrayList<>(); - - for (UserImportItem item : cmd.getUsers()) { - User user = new User(); - user.setName(item.getName()); - user.setEmail(item.getEmail()); - userRepository.save(user); - results.add(convertToDTO(user)); - } - - return MultiResponse.of(results, results.size()); - } -} -``` - -### 在 Application Service 中组合多个执行器 - -```java -@Service -public class OrderApplicationService { - - @Autowired - private CreateOrderExecutor createOrderExecutor; - - @Autowired - private DeductInventoryExecutor deductInventoryExecutor; - - @Autowired - private SendNotificationExecutor sendNotificationExecutor; - - @Transactional - public SingleResponse placeOrder(CreateOrderCommand cmd) { - // 1. 创建订单 - SingleResponse orderResponse = createOrderExecutor.execute(cmd); - if (!orderResponse.isSuccess()) { - return orderResponse; - } - - // 2. 扣减库存 - DeductInventoryCommand inventoryCmd = new DeductInventoryCommand(); - inventoryCmd.setOrderId(orderResponse.getData().getId()); - inventoryCmd.setItems(cmd.getItems()); - Response inventoryResp = deductInventoryExecutor.execute(inventoryCmd); - if (!inventoryResp.isSuccess()) { - throw LocaleMessageException.of("INVENTORY_DEDUCT_FAILED"); - } - - // 3. 发送通知(异步,不影响主流程) - try { - sendNotificationExecutor.execute( - new SendOrderNotificationCommand(orderResponse.getData().getId())); - } catch (Exception e) { - log.warn("Failed to send order notification", e); - } - - return orderResponse; - } -} -``` - -### IExecutor 接口结构 - -```java -public interface IExecutor { - - /** - * 带命令参数的执行器 - * @param 响应类型,必须继承 Response - * @param 命令类型 - */ - interface Command { - R execute(C command); - } - - /** - * 无参数的执行器 - * @param 响应类型,必须继承 Response - */ - interface Void { - R execute(); - } -} -``` - -### 设计原则 - -1. **单一职责**:每个执行器只负责一个业务命令 -2. **响应标准化**:返回值必须是 `Response` 或其子类 -3. **命令不可变**:命令对象作为输入不应在执行过程中被修改 -4. **事务边界清晰**:`@Transactional` 加在执行器方法上,而非 Controller -5. **异常即响应**:业务异常通过 `LocaleMessageException` 抛出,由全局处理器转为标准错误响应 diff --git a/docs/capabilities/commons-crypto.md b/docs/capabilities/commons-crypto.md deleted file mode 100644 index c67c7c6ba..000000000 --- a/docs/capabilities/commons-crypto.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: commons-crypto -description: Apache 加密工具封装,提供 AES 等加密操作 -status: 已实现 -scope: 后端 -source: 框架:Apache Commons Crypto ---- - -## 解决什么问题 - -框架在 springboot-starter 模块中提供了统一的加密工具抽象,用于: -- AES 对称加密的便捷封装 -- 安全模块中 Token IV 向量的加解密 -- 为上层业务提供简洁的加密/解密 API - -项目中通过 `AES`、`AESUtils`、`AESTools` 等类对加密操作进行了封装,结合 BouncyCastle Provider 提供完整的加密能力。 - -## 如何使用 - -### 依赖引入 - -```xml - - - org.apache.commons - commons-crypto - 1.2.0 - -``` - -### 核心类 - -- `AES` — AES 加密封装(位于 `com.codingapi.springboot.framework.crypto`) -- `AESUtils` — AES 单例工具类 -- `AESTools` — Security 模块的 AES 工具(自动注入配置密钥) - -## 使用实例 - -### 1. AESUtils 单例使用 - -```java -// AESUtils.java - 框架级 AES 工具 -public class AESUtils { - private static final AESUtils instance = new AESUtils(); - private final AES aes; - - private AESUtils() { - // 初始化默认 AES 实例 - this.aes = new AES(); - } - - public static AESUtils getInstance() { - return instance; - } - - // 提供 encode/decode 方法供业务调用 -} -``` - -### 2. Security 模块 AESTools - -```java -// AESTools.java - 安全模块中的 AES 工具 -@Component -public class AESTools { - private static AESTools instance; - private AES aes; - - @PostConstruct - public void init(AES aes) { - this.aes = aes; - instance = this; - } - - public static AESTools getInstance() { - return instance; - } - - // 在 Token 中使用: - // 加密: AESTools.getInstance().encode(iv) - // 解密: AESTools.getInstance().decode(iv) -} -``` - -### 3. Security 加密配置 - -```java -// SecurityCryptoConfiguration.java -@Configuration -public class SecurityCryptoConfiguration { - - @Bean - public AES aes(CodingApiSecurityProperties properties) { - // 根据配置创建 AES 实例 - return new AES(properties.getAesKey(), properties.getAesIv()); - } - - @Bean - public AESTools aesTools() { - return new AESTools(); - } -} -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter | `AES`、`AESUtils` 基础加密工具 | -| springboot-starter-security | `AESTools`、`SecurityCryptoConfiguration` 安全加密配置 | diff --git a/docs/capabilities/commons-io.md b/docs/capabilities/commons-io.md deleted file mode 100644 index 2c42bdb88..000000000 --- a/docs/capabilities/commons-io.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -name: commons-io -description: IO 工具类库,简化文件/流操作 -status: 已实现 -scope: 后端 -source: 框架:Apache Commons IO ---- - -## 解决什么问题 - -在 Spring Security Filter 链中,需要从 HTTP 请求/响应中读取和写入数据流: -- 从 `HttpServletRequest` 中读取请求体内容 -- 向 `HttpServletResponse` 中写入 JSON 响应 -- 测试中读取文件资源 - -Apache Commons IO 的 `IOUtils` 提供了简洁的流读写 API,避免了手动处理流的样板代码。 - -## 如何使用 - -### 依赖引入 - -```xml - - - commons-io - commons-io - 2.17.0 - -``` - -### 核心用法 - -主要使用 `org.apache.commons.io.IOUtils` 进行流的读写操作。 - -## 使用实例 - -### 1. Security Filter 中读取请求体 - -```java -// MyLoginFilter.java - 读取登录请求体 -import org.apache.commons.io.IOUtils; - -@Override -public Authentication attemptAuthentication(HttpServletRequest request, - HttpServletResponse response) { - // 从请求输入流中读取 JSON 数据 - String body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); - JSONObject json = JSONObject.parseObject(body); - String username = json.getString("username"); - String password = json.getString("password"); - // ... 执行认证逻辑 -} -``` - -### 2. Security Filter 中写入响应 - -```java -// MyAccessDeniedHandler.java / MyUnAuthenticationEntryPoint.java -import org.apache.commons.io.IOUtils; - -@Override -public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException { - response.setContentType("application/json;charset=UTF-8"); - JSONObject json = new JSONObject(); - json.put("errCode", "403"); - json.put("errMessage", "访问被拒绝"); - // 使用 IOUtils 将 JSON 字符串写入响应输出流 - IOUtils.write(json.toJSONString(), response.getOutputStream(), StandardCharsets.UTF_8); -} -``` - -### 3. MyAuthenticationFilter 中的 Token 读取 - -```java -// MyAuthenticationFilter.java - 从请求头或请求体中读取 Token -import org.apache.commons.io.IOUtils; - -// 读取请求内容进行 Token 验证 -String content = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); -``` - -### 4. 测试中的文件读取 - -```java -// SearchRequestTest.java - 测试中读取测试资源文件 -import org.apache.commons.io.FileUtils; - -String content = FileUtils.readFileToString( - new File("src/test/resources/test-data.json"), - StandardCharsets.UTF_8 -); -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter-security | Filter 链中请求体读取、响应体写入(IOUtils) | -| springboot-starter | 测试中文件操作(FileUtils) | diff --git a/docs/capabilities/commons-text.md b/docs/capabilities/commons-text.md deleted file mode 100644 index 1960739de..000000000 --- a/docs/capabilities/commons-text.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: commons-text -description: 文本处理工具库,提供字符串命名转换等功能 -status: 已实现 -scope: 后端 -source: 框架:Apache Commons Text ---- - -## 解决什么问题 - -在数据查询模块(springboot-starter-data-fast)中,JDBC 查询返回的结果集列名通常是数据库风格的下划线命名(如 `user_name`),而 Java Bean 属性使用驼峰命名(如 `userName`)。需要自动将列名转换为驼峰格式以匹配 Bean 属性。 - -Apache Commons Text 的 `CaseUtils.toCamelCase()` 提供了开箱即用的命名风格转换功能。 - -## 如何使用 - -### 依赖引入 - -```xml - - - org.apache.commons - commons-text - 1.12.0 - -``` - -### 核心用法 - -主要使用 `org.apache.commons.text.CaseUtils` 进行命名风格转换。 - -## 使用实例 - -### JdbcQuery 中的驼峰命名映射 - -```java -// JdbcQuery.java - JDBC 查询结果集的列名转驼峰 -import org.apache.commons.text.CaseUtils; - -public class JdbcQuery { - - /** - * 自定义 RowMapper,将下划线列名自动转为驼峰命名 - */ - private static class CamelCaseRowMapper implements RowMapper> { - - @Override - public Map mapRow(ResultSet rs, int rowNum) throws SQLException { - ResultSetMetaData metaData = rs.getMetaData(); - int columnCount = metaData.getColumnCount(); - Map map = new HashMap<>(columnCount); - for (int i = 1; i <= columnCount; i++) { - String columnName = metaData.getColumnLabel(i); - // 将 user_name → userName, create_time → createTime - map.put(CaseUtils.toCamelCase(columnName, false), rs.getObject(i)); - } - return map; - } - } - - /** - * 查询返回 Map 列表(自动驼峰转换) - */ - public List> queryForMapList(String sql, Object... params) { - return jdbcTemplate.query(sql, new CamelCaseRowMapper(), params); - } - - /** - * 分页查询返回 Map 页面(自动驼峰转换) - */ - public Page> queryForMapPage(String sql, String countSql, - PageRequest pageRequest, - Object... params) { - List> list = jdbcTemplate.query( - sql, new CamelCaseRowMapper(), params - ); - long count = this.countQuery(countSql, params); - return new PageImpl<>(list, pageRequest, count); - } -} -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter-data-fast | `JdbcQuery.CamelCaseRowMapper` 列名下划线转驼峰 | diff --git a/docs/capabilities/data-authorization.md b/docs/capabilities/data-authorization.md deleted file mode 100644 index 22dc8ca09..000000000 --- a/docs/capabilities/data-authorization.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -name: data-authorization -description: 基于 JDBC 代理链的透明数据权限过滤,支持行级和列级权限控制 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在企业级多租户或多部门系统中,不同用户只能查看和操作其权限范围内的数据。传统做法需要在每个业务查询中手动拼接权限条件,代码侵入性强且容易遗漏。 - -`springboot-starter-data-authorization` 通过 JDBC 驱动层代理拦截所有 SQL 执行,在 SQL 到达数据库之前自动注入行级过滤条件(WHERE / JOIN),并在结果集返回时提供列级权限处理能力。业务代码无需任何修改即可获得透明的数据权限控制。 - -核心设计思路: -- **JDBC 驱动代理**:`AuthorizationJdbcDriver` 作为 JDBC Driver 注册,将所有 `Connection` 包装为 `ConnectionProxy` -- **SQL 拦截改写**:通过 `SQLRunningContext.intercept(sql)` 在 `prepareStatement` / `prepareCall` / `createStatement` 时解析并改写 SQL -- **行级权限**:`RowHandler` + `DataAuthorizationFilter` 为每张表动态注入 WHERE 条件或 JOIN 关联 -- **列级权限**:`ColumnHandler` 在 `ResultSetProxy` 读取字段值时进行脱敏或过滤 -- **跳过机制**:`SQLRunningContext.skipDataAuthorization()` 允许特定操作绕过权限拦截 - -## 如何使用 - -### 1. 引入依赖 - -```xml - - com.codingapi.springboot - springboot-starter-data-authorization - -``` - -### 2. 配置 JDBC 驱动 - -将 JDBC URL 中的 driver 替换为 `com.codingapi.springboot.authorization.jdbc.AuthorizationJdbcDriver`,该驱动会自动查找并代理真实的数据库驱动: - -```properties -spring.datasource.driver-class-name=com.codingapi.springboot.authorization.jdbc.AuthorizationJdbcDriver -``` - -### 3. 注册数据权限过滤器 - -实现 `DataAuthorizationFilter` 接口并注册到 `DataAuthorizationContext`: - -```java -// 添加行级权限过滤器 -DataAuthorizationContext.getInstance().addDataAuthorizationFilter(filter); -``` - -`DataAuthorizationFilter` 需要实现以下方法: -- `supportRowAuthorization(tableName, tableAlias)` — 判断是否对指定表启用行级权限 -- `rowAuthorization(tableName, tableAlias)` — 返回 `Condition` 对象,包含要注入的 WHERE 或 JOIN 条件 -- `supportColumnAuthorization(tableName, columnName, value)` — 判断是否对指定列启用列级权限 -- `columnAuthorization(tableName, columnName, value)` — 返回处理后的列值 - -### 4. 构建行级权限条件 - -使用 `Condition` 类的静态工厂方法构建权限条件: - -```java -// WHERE 条件 -Condition.customCondition("department_id = 10"); - -// 格式化条件 -Condition.formatCondition("department_id = %d", deptId); - -// 默认放行条件 -Condition.defaultCondition(); // 等价于 "1=1" - -// 无权限(返回 null) -Condition.emptyCondition(); -``` - -对于 JOIN 类型的权限条件,框架内置了 `JoinConditionSQLHandler`,支持 INNER / LEFT / RIGHT JOIN 的自动注入。 - -### 5. 自定义 RowHandler - -如需自定义行权限处理逻辑,可实现 `RowHandler` 接口并通过 `RowHandlerContext` 设置: - -```java -RowHandlerContext.getInstance().setRowHandler((subSql, tableName, tableAlias) -> { - return Condition.customCondition("tenant_id = '" + getCurrentTenantId() + "'"); -}); -``` - -### 6. 自定义列级权限处理器 - -实现 `ColumnHandler` 接口以对特定列的值进行脱敏或转换: - -```java -// ColumnHandler 接口提供了所有 JDBC 类型的 get 方法 -String getString(SQLExecuteState interceptState, int columnIndex, - String tableName, String columnName, String value); -int getInt(SQLExecuteState interceptState, int columnIndex, - String tableName, String columnName, int value); -// ... 其他类型同理 -``` - -### 7. 跳过数据权限拦截 - -在某些管理操作或系统内部查询中需要绕过权限过滤: - -```java -// 方式一:使用 Supplier -List users = SQLRunningContext.getInstance() - .skipDataAuthorization(() -> userRepository.findAll()); - -// 方式二:使用 Runnable -SQLRunningContext.getInstance() - .skipDataAuthorization(() -> userRepository.deleteAll()); -``` - -### 8. 自定义跳过拦截时的 SQL 处理 - -实现 `SkipAuthorizationFilter` 接口,在跳过权限拦截时对 SQL 进行额外处理: - -```java -DataAuthorizationContext.getInstance() - .setSkipAuthorizationFilter(sql -> { - // 对跳过拦截的 SQL 做特殊处理 - return sql; - }); -``` - -## 使用实例 - -### 完整的多租户数据权限示例 - -```java -@Configuration -public class DataPermissionConfig { - - @PostConstruct - public void initDataPermission() { - // 注册数据权限过滤器 - DataAuthorizationContext.getInstance().addDataAuthorizationFilter( - new DataAuthorizationFilter() { - - @Override - public boolean supportRowAuthorization(String tableName, String tableAlias) { - // 对订单表和员工表启用行级权限 - return "t_order".equals(tableName) || "t_employee".equals(tableName); - } - - @Override - public Condition rowAuthorization(String tableName, String tableAlias) { - Long deptId = SecurityUtils.getCurrentDeptId(); - if (deptId == null) { - return Condition.emptyCondition(); - } - return Condition.formatCondition("%s.dept_id = %d", tableAlias, deptId); - } - - @Override - public boolean supportColumnAuthorization( - String tableName, String columnName, T value) { - // 对员工表的手机号列启用脱敏 - return "t_employee".equals(tableName) && "phone".equals(columnName); - } - - @Override - public T columnAuthorization( - String tableName, String columnName, T value) { - if (value instanceof String phone) { - return (T) phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); - } - return value; - } - } - ); - } -} -``` - -### 代理链工作原理 - -``` -应用代码调用 prepareStatement(sql) - ↓ -ConnectionProxy.prepareStatement(sql) - ↓ -SQLRunningContext.intercept(sql) - ├── skipInterceptor=true → 直接返回原始 SQL - └── SQLInterceptor.beforeHandler(sql) → true - ↓ - SQLInterceptor.postHandler(sql) → DataPermissionSQL(newSql, aliasContext) - ↓ - connection.prepareStatement(newSql) → PreparedStatementProxy - ↓ - executeQuery() → ResultSetProxy(resultSet, interceptState) - ↓ - ResultSetProxy.next()/getXxx() → ColumnHandler 列级权限处理 -``` - -### 异常处理 - -当权限校验失败时,框架抛出 `NotAuthorizationException`(继承自 `SQLException`),可在上层统一捕获处理: - -```java -try { - List orders = orderRepository.findAll(); -} catch (NotAuthorizationException e) { - // 处理无权限异常 - throw new BusinessException("无权访问该数据"); -} -``` diff --git a/docs/capabilities/domain-change-interceptor.md b/docs/capabilities/domain-change-interceptor.md new file mode 100644 index 000000000..f2b482e0d --- /dev/null +++ b/docs/capabilities/domain-change-interceptor.md @@ -0,0 +1,165 @@ +--- +name: domain-change-interceptor +description: 通过 CGLIB 代理拦截领域实体字段的 setter 方法,自动检测变更并发布 DomainChangeEvent +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 87c449a7 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/proxy/DomainChangeInterceptor.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/proxy/DomainProxyFactory.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainChangeEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainCreateEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainDeleteEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/IDomain.java +--- + +## 解决什么问题 + +在 DDD(领域驱动设计)实践中,领域实体的状态变更需要被精确追踪并作为领域事件发布,以便触发审计日志、通知下游系统、维护事件溯源等。传统做法是在每个 setter 方法中手动编写变更检测和事件发布代码,导致领域模型充斥大量样板代码,且容易遗漏。 + +本能力通过 CGLIB 动态代理技术,在运行时拦截领域实体的所有带参数的方法调用(主要是 setter),自动对比字段变更前后的值,当检测到差异时自动发布 `DomainChangeEvent`。核心优势: + +- **零侵入领域模型**:实体类无需继承特定基类或添加注解,保持纯 POJO +- **自动变更检测**:支持基本类型和嵌套对象的深层字段比较 +- **完整生命周期事件**:覆盖创建(`DomainCreateEvent`)、变更(`DomainChangeEvent`)、删除(`DomainDeleteEvent`)、持久化(`DomainPersistEvent`)四种领域事件 +- **与框架事件系统集成**:变更事件通过 `EventPusher` 发布,可被同步/异步 Handler 消费 + +## 如何使用 + +### 核心组件 + +#### IDomain — 领域对象接口 + +```java +public interface IDomain { + // 发布持久化事件 + default void persist() { EventPusher.push(new DomainPersistEvent(this)); } + + // 发布删除事件 + default void delete() { EventPusher.push(new DomainDeleteEvent(this)); } +} +``` + +领域实体可选择性实现此接口以获得便捷的持久化和删除事件发布能力。 + +#### DomainProxyFactory — 代理工厂 + +```java +// 创建带有变更拦截的领域实体代理 +public static T create(Class entityClass, Object... args) +``` + +调用 `create()` 会: +1. 通过反射调用目标类的构造函数创建实例 +2. 使用 CGLIB 生成代理对象 +3. 自动发布 `DomainCreateEvent` +4. 返回代理对象 + +#### DomainChangeInterceptor — CGLIB 方法拦截器 + +内部工作机制: +1. **首次拦截时**:通过 `BeanUtils.getPropertyDescriptors()` 读取实体所有属性,递归快照当前字段值到内存 Map +2. **每次 setter 调用后**:重新读取字段值并与快照对比 +3. **发现差异时**:通过 `EventPusher.push()` 发布 `DomainChangeEvent`,包含字段名、旧值、新值 +4. **更新快照**:将新值写入快照 Map,为下次比较做准备 + +#### 领域事件体系 + +``` +DomainEvent (抽象基类,实现 IEvent) +├── entity — 关联的实体对象 +├── entityClass — 实体类型 +└── timestamp — 事件时间戳 + +DomainCreateEvent extends DomainEvent — 实体创建 +DomainChangeEvent extends DomainEvent — 字段变更(含 fieldName, oldValue, newValue) +DomainDeleteEvent extends DomainEvent — 实体删除 +DomainPersistEvent extends DomainEvent — 实体持久化 +``` + +### 注意事项 + +- 代理对象通过 CGLIB 子类化实现,要求目标类不能是 final 类,且需要有可访问的构造函数 +- 变更检测基于 `equals()` 比较,自定义对象需正确实现 `equals()` 方法 +- 仅对带参数的方法调用进行变更检测(即 setter),无参方法(getter)直接透传 +- 支持嵌套对象的深层比较,但仅限基本类型属性的递归展开 + +## 使用实例 + +### 1. 定义领域实体 + +```java +public class UserEntity implements IDomain { + private String name; + private Integer age; + private Address address; + + public UserEntity() {} + + public UserEntity(String name, Integer age) { + this.name = name; + this.age = age; + } + + // getter/setter ... +} +``` + +### 2. 通过代理工厂创建实体 + +```java +// 创建代理对象,自动发布 DomainCreateEvent +UserEntity user = DomainProxyFactory.create(UserEntity.class, "张三", 25); + +// 修改字段时自动检测变更并发布 DomainChangeEvent +user.setName("李四"); // → DomainChangeEvent(fieldName="name", oldValue="张三", newValue="李四") +user.setAge(30); // → DomainChangeEvent(fieldName="age", oldValue=25, newValue=30) + +// 持久化时发布 DomainPersistEvent +user.persist(); + +// 删除时发布 DomainDeleteEvent +user.delete(); +``` + +### 3. 监听领域变更事件 + +```java +@Component +public class UserChangeHandler implements IHandler { + + @Override + public void handler(DomainChangeEvent event) { + if (event.getEntity() instanceof UserEntity) { + log.info("用户字段变更: {} = {} → {}", + event.getFieldName(), + event.getOldValue(), + event.getNewValue()); + } + } +} +``` + +### 4. 监听实体创建事件 + +```java +@Component +public class UserCreateHandler implements IHandler { + + @Override + public void handler(DomainCreateEvent event) { + log.info("新用户创建: {}", event.getEntity().getClass().getSimpleName()); + } +} +``` + +### 5. 嵌套对象变更检测 + +```java +// 假设 UserEntity 中包含 Address 嵌套对象 +user.getAddress().setCity("北京"); +// → DomainChangeEvent(fieldName="address.city", oldValue="上海", newValue="北京") +``` + +嵌套对象的字段变更会以 `父字段.子字段` 的格式记录字段路径,便于精确定位变更位置。 diff --git a/docs/capabilities/domain-event.md b/docs/capabilities/domain-event.md deleted file mode 100644 index 01964314e..000000000 --- a/docs/capabilities/domain-event.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -name: domain-event -description: 领域实体事件自动追踪机制,支持创建、变更、删除事件的自动推送与字段级变更检测 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在 DDD 实践中,领域实体的生命周期事件(创建、修改、删除)是业务流转的关键信号。手动在每个业务方法中调用 `EventPusher.push()` 存在以下痛点: - -1. **侵入性强**:每个 setter 或状态变更方法都需要显式推送事件,代码冗余 -2. **遗漏风险**:开发者可能忘记在某些字段变更后推送事件 -3. **变更详情缺失**:普通事件只能知道"实体变了",无法知道"哪个字段从什么值变成了什么值" -4. **实体创建事件易遗漏**:新创建的实体如果没有及时推送事件,下游监听器将无法感知 - -框架通过 `IDomain` 接口提供标准的持久化和删除事件入口,并通过 `DomainProxyFactory` + CGLIB 代理实现字段级变更拦截,自动推送 `DomainChangeEvent`,携带字段名、旧值、新值信息。 - -## 如何使用 - -### 1. 实体类实现 IDomain 接口 - -```java -public class User implements IDomain { - private Long id; - private String name; - private Integer age; - - // getter/setter... -} -``` - -### 2. 使用 DomainProxyFactory 创建代理实例 - -通过工厂创建的实例会自动: -- 推送 `DomainCreateEvent`(创建时) -- 拦截所有带参数的 setter 方法,自动对比字段变化并推送 `DomainChangeEvent` - -```java -// 创建代理实例(自动推送 DomainCreateEvent) -User user = DomainProxyFactory.create(User.class); -user.setName("张三"); // 自动推送 DomainChangeEvent(fieldName="name", oldValue=null, newValue="张三") -``` - -### 3. 手动触发持久化或删除事件 - -```java -// 保存后推送持久化事件 -user.persist(); // 推送 DomainPersistEvent - -// 删除前推送删除事件 -user.delete(); // 推送 DomainDeleteEvent -``` - -### 4. 订阅领域事件 - -```java -@Component -public class UserChangeLogger implements IHandler { - @Override - public void handler(DomainChangeEvent event) { - log.info("Entity {} field '{}' changed from {} to {}", - event.getEntityClass().getSimpleName(), - event.getFieldName(), - event.getOldValue(), - event.getNewValue() - ); - } -} - -@Component -public class UserCreatedNotifier implements IHandler { - @Override - public void handler(DomainCreateEvent event) { - log.info("New entity created: {}", event.getEntityClass().getSimpleName()); - } -} -``` - -## 使用实例 - -### 完整示例:用户信息管理 - -```java -// 1. 定义用户实体 -public class User implements IDomain { - private Long id; - private String name; - private String email; - private Integer status; - - // 必须有无参构造或有参构造供代理使用 - public User() {} -} - -// 2. 监听创建事件 -@Component -public class UserCreatedHandler implements IHandler { - @Override - public int order() { return 0; } - - @Override - public void handler(DomainCreateEvent event) { - if (event.getEntity() instanceof User user) { - log.info("User created: {}", user.getId()); - // 初始化默认配置等 - } - } -} - -// 3. 监听字段变更事件 -@Component -public class UserChangeAuditHandler implements IHandler { - @Override - public void handler(DomainChangeEvent event) { - if (event.getEntity() instanceof User) { - auditLogService.record( - event.getEntityClass().getName(), - event.getFieldName(), - String.valueOf(event.getOldValue()), - String.valueOf(event.getNewValue()) - ); - } - } -} - -// 4. 监听删除事件 -@Component -public class UserDeletedHandler implements IHandler { - @Override - public void handler(DomainDeleteEvent event) { - if (event.getEntity() instanceof User user) { - cleanupService.cleanUserData(user.getId()); - } - } -} - -// 5. Service 中使用 -@Service -public class UserService { - - public User createUser(String name, String email) { - // 通过工厂创建,自动推送 DomainCreateEvent - User user = DomainProxyFactory.create(User.class); - user.setName(name); // 自动推送 DomainChangeEvent - user.setEmail(email); // 自动推送 DomainChangeEvent - - userRepository.save(user); - user.persist(); // 推送 DomainPersistEvent - return user; - } - - public void updateUserName(Long userId, String newName) { - User user = userRepository.findById(userId).orElseThrow(); - // 注意:直接从 Repository 获取的对象不会自动追踪变更 - // 如需追踪,需重新通过代理包装或使用其他 AOP 方案 - user.setName(newName); - userRepository.save(user); - } - - public void deleteUser(Long userId) { - User user = userRepository.findById(userId).orElseThrow(); - user.delete(); // 推送 DomainDeleteEvent - userRepository.delete(user); - } -} -``` - -### 嵌套对象字段变更检测 - -`DomainChangeInterceptor` 支持嵌套对象的字段级变更检测: - -```java -public class Address { - private String city; - private String street; - // getter/setter... -} - -public class User implements IDomain { - private String name; - private Address address; - // getter/setter... -} - -// 当 address.city 发生变化时,fieldName 为 "address.city" -// DomainChangeEvent.getFieldName() = "address.city" -// DomainChangeEvent.getOldValue() = "北京" -// DomainChangeEvent.getNewValue() = "上海" -``` - -### 事件类型继承关系 - -``` -IEvent - └── DomainEvent (com.codingapi.springboot.framework.domain.event.DomainEvent) - ├── DomainCreateEvent — 实体创建 - ├── DomainChangeEvent — 字段变更(含 fieldName, oldValue, newValue) - ├── DomainDeleteEvent — 实体删除 - └── DomainPersistEvent — 实体持久化 -``` diff --git a/docs/capabilities/event-system.md b/docs/capabilities/event-system.md index ad694dcc7..42df04b12 100644 --- a/docs/capabilities/event-system.md +++ b/docs/capabilities/event-system.md @@ -1,211 +1,137 @@ --- name: event-system -description: 基于发布-订阅模式的事件系统,支持同步/异步事件处理、事件排序和异常隔离 +description: 自建的领域事件发布-订阅系统,支持同步/异步事件、事务后提交、Handler自动注册排序 status: 已实现 scope: 后端 source: 项目自有 +last_commit: 145aada7 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/EventPusher.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/IHandler.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/IEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/ISyncEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/IAsyncEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/DomainEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/DomainEventContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/SpringDefaultEventHandler.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/SpringTransactionEventHandler.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/HandlerBeanDefinitionRegistrar.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/ApplicationHandlerUtils.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainCreateEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainChangeEvent.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainDeleteEvent.java --- ## 解决什么问题 -在领域驱动设计(DDD)中,领域事件是实现业务解耦的核心机制。传统 Spring Event 存在以下问题: +在 DDD(领域驱动设计)架构中,领域实体状态变更时需要通知其他模块执行联动逻辑(如发送通知、更新缓存、触发下游流程等)。如果直接调用会导致模块间强耦合,且主业务与分支逻辑的事务边界不清晰。 -1. **事务耦合**:Spring Event 默认与主业务事务绑定,事件失败会影响主流程 -2. **缺乏类型安全**:事件处理器需要手动判断事件类型 -3. **缺少执行顺序控制**:多个处理器无法保证执行顺序 -4. **异常传播**:一个处理器异常会中断其他处理器的执行 -5. **循环调用检测缺失**:事件之间可能形成循环依赖导致栈溢出 +本能力提供了一套自建的发布-订阅事件系统,解决以下痛点: -框架自建的事件系统通过 `IEvent` + `IHandler` 的泛型绑定、`order()` 排序机制、`error()` 异常回调以及 `EventLoopException` 循环检测,解决了上述所有问题。同时支持同步(`ISyncEvent`)和异步(`IAsyncEvent`)两种执行模式,异步事件可配置线程池大小。 +- **解耦**:事件发布者无需知道有哪些订阅者,通过 `IEvent` + `IHandler` 接口完全解耦 +- **同步/异步分离**:通过 `ISyncEvent` / `IAsyncEvent` 标记接口区分执行模式,异步事件由独立线程池处理 +- **事务后提交**:`SpringTransactionEventHandler` 使用 `@TransactionalEventListener(AFTER_COMMIT)` 确保事件在主事务提交后才触发,避免脏数据 +- **Handler 自动注册与排序**:所有 `IHandler` 实现类通过 Spring Bean 自动扫描注册,支持 `order()` 控制执行顺序 +- **循环检测**:内置 `EventTraceContext` 追踪事件链,防止事件循环触发 +- **异常隔离**:多 Handler 场景下,单个 Handler 异常通过 `error()` 回调处理,不影响其他 Handler 执行 ## 如何使用 -### 1. 定义事件 +### 核心接口 -实现 `IEvent` 接口或其子接口: +| 接口/类 | 说明 | +|---------|------| +| `IEvent` | 事件标记接口,继承 `Serializable` | +| `ISyncEvent extends IEvent` | 同步事件标记 | +| `IAsyncEvent extends IEvent` | 异步事件标记 | +| `IHandler` | 事件处理器,泛型绑定事件类型 | +| `EventPusher` | 静态工具类,事件推送入口 | +| `DomainEvent` (domain包) | 领域实体事件基类,携带 entity、entityClass、timestamp | +| `DomainCreateEvent` | 实体创建事件 | +| `DomainChangeEvent` | 实体字段变更事件(含 fieldName、oldValue、newValue) | +| `DomainDeleteEvent` | 实体删除事件 | -```java -// 同步事件 - 在当前线程立即执行 -public class UserCreatedEvent implements ISyncEvent { - private final Long userId; - - public UserCreatedEvent(Long userId) { - this.userId = userId; - } -} - -// 异步事件 - 在线程池中异步执行 -public class NotificationEvent implements IAsyncEvent { - private final String message; - - public NotificationEvent(String message) { - this.message = message; - } -} -``` - -### 2. 实现事件处理器 - -实现 `IHandler` 接口,泛型参数指定要订阅的事件类型: - -```java -@Component -public class UserCreatedHandler implements IHandler { - - // 可选:指定执行顺序,数值越小优先级越高 - @Override - public int order() { - return 0; - } - - @Override - public void handler(UserCreatedEvent event) { - // 处理用户创建逻辑 - System.out.println("User created: " + event.getUserId()); - } - - // 可选:自定义异常处理,避免中断其他处理器 - @Override - public void error(Exception exception) throws Exception { - log.error("Failed to handle user created event", exception); - // 不抛出异常,允许后续处理器继续执行 - } -} -``` - -### 3. 推送事件 - -使用静态方法 `EventPusher.push()` 推送事件: - -```java -// 推送同步事件 -EventPusher.push(new UserCreatedEvent(1L)); +### 注入方式 -// 推送异步事件 -EventPusher.push(new NotificationEvent("Welcome!")); +无需手动注入。`HandlerBeanDefinitionRegistrar` 在 Spring 启动时自动扫描所有标注 `@Handler` 的 Bean 并注册到 `ApplicationHandlerUtils`。 -// 禁用循环检测(仅在明确知道无循环风险时使用) -EventPusher.push(event, true); -``` - -### 4. 配置异步线程池 - -在 `application.properties` 中配置: +### 配置项 ```properties +# 异步事件线程池大小(默认20) codingapi.framework.handler-thread-pool-size=20 ``` -### 5. 启用事务后提交触发 +### 两种事件分发器 -如需在事务提交后再触发事件处理器,启用事务事件监听器: - -```properties -codingapi.framework.event.transaction.enable=true -``` - -启用后将使用 `@TransactionalEventListener(phase = AFTER_COMMIT)` 替代默认的 `@EventListener`。 +- **`SpringDefaultEventHandler`**:使用 `@EventListener`,事件立即触发 +- **`SpringTransactionEventHandler`**:使用 `@TransactionalEventListener(AFTER_COMMIT, fallbackExecution=true)`,事务提交后触发 ## 使用实例 -### 完整示例:订单创建流程 +### 1. 定义自定义事件 ```java -// 1. 定义订单创建事件 +// 同步事件 public class OrderCreatedEvent implements ISyncEvent { private final String orderId; - private final BigDecimal amount; - public OrderCreatedEvent(String orderId, BigDecimal amount) { + public OrderCreatedEvent(String orderId) { this.orderId = orderId; - this.amount = amount; - } -} - -// 2. 库存扣减处理器(优先执行) -@Component -public class InventoryDeductHandler implements IHandler { - @Override - public int order() { - return 0; // 最高优先级 } - @Override - public void handler(OrderCreatedEvent event) { - inventoryService.deduct(event.getOrderId(), event.getAmount()); - } + public String getOrderId() { return orderId; } } +``` -// 3. 积分累加处理器(次优先) -@Component -public class PointsAccumulateHandler implements IHandler { - @Override - public int order() { - return 10; - } - - @Override - public void handler(OrderCreatedEvent event) { - pointsService.addPoints(event.getOrderId()); - } - - @Override - public void error(Exception exception) throws Exception { - // 积分失败不影响主流程,仅记录日志 - log.warn("Points accumulation failed for order: {}", - event.getOrderId(), exception); - } -} +### 2. 编写 Handler -// 4. 发送通知处理器(最后执行) +```java +@Handler @Component -public class NotificationSendHandler implements IHandler { +public class OrderNotificationHandler implements IHandler { + @Override public int order() { - return 20; + return 10; // 排序值,越小越先执行 } - + @Override public void handler(OrderCreatedEvent event) { - notificationService.sendOrderConfirmation(event.getOrderId()); + // 发送订单创建通知 + System.out.println("Order created: " + event.getOrderId()); } -} -// 5. 在 Service 中推送事件 -@Service -public class OrderService { - public void createOrder(CreateOrderCommand cmd) { - Order order = new Order(cmd); - orderRepository.save(order); - - // 推送事件,三个 Handler 按 order 顺序依次执行 - EventPusher.push(new OrderCreatedEvent(order.getId(), order.getAmount())); + @Override + public void error(Exception exception) throws Exception { + // 异常回调,记录日志但不阻止后续 Handler + log.error("Notification failed", exception); } } ``` -### 循环调用防护 - -当检测到事件循环时,框架会自动抛出 `EventLoopException`: +### 3. 推送事件 ```java -// 错误示例:A 触发 B,B 又触发 A -public class EventA implements ISyncEvent {} -public class EventB implements ISyncEvent {} +// 推送同步事件 +EventPusher.push(new OrderCreatedEvent("ORD-001")); -@Component -public class HandlerA implements IHandler { - @Override - public void handler(EventA event) { - EventPusher.push(new EventB()); // 触发 B - } -} +// 推送异步事件(需事件类实现 IAsyncEvent) +EventPusher.push(new AsyncLogEvent("log message")); -@Component -public class HandlerB implements IHandler { - @Override - public void handler(EventB event) { - EventPusher.push(new EventA()); // 再次触发 A → 抛出 EventLoopException - } -} +// 允许循环事件(跳过循环检测) +EventPusher.push(event, true); +``` + +### 4. 使用领域实体事件 + +```java +// 实体变更事件携带字段级别信息 +DomainChangeEvent changeEvent = new DomainChangeEvent( + userEntity, // 实体对象 + "status", // 字段名 + "ACTIVE", // 旧值 + "INACTIVE" // 新值 +); +EventPusher.push(changeEvent); ``` diff --git a/docs/capabilities/exception-handling.md b/docs/capabilities/exception-handling.md deleted file mode 100644 index 533da47e9..000000000 --- a/docs/capabilities/exception-handling.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -name: exception-handling -description: 国际化异常体系与事件异常处理,支持多语言错误消息、占位符参数和事件循环检测 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -企业级应用的异常处理面临以下挑战: - -1. **错误消息硬编码**:异常信息写死在代码中,无法根据用户语言环境切换 -2. **错误码管理混乱**:不同模块使用不同的错误码格式,缺乏统一管理 -3. **占位符支持缺失**:类似"用户 {userId} 不存在"的消息需要手动拼接字符串 -4. **事件异常丢失**:多个事件处理器并发执行时,部分异常可能被吞掉 -5. **循环调用难排查**:事件之间形成循环依赖时,缺少清晰的堆栈信息 - -框架提供三层异常基础设施: -- `LocaleMessageException` — 基于 Spring MessageSource 的国际化业务异常 -- `EventException` — 聚合多个事件处理器异常的容器异常 -- `EventLoopException` — 事件循环调用的专用检测异常 - -配合 `ExceptionConfiguration` 自动装配和 `MessageContext` 全局上下文,实现开箱即用的国际化异常体系。 - -## 如何使用 - -### 1. 配置消息资源文件 - -在 `src/main/resources/` 下创建 messages 属性文件: - -```properties -# messages.properties (默认/中文) -USER_NOT_FOUND=用户不存在 -USER_DUPLICATE=用户名 {0} 已存在 -ORDER_AMOUNT_INVALID=订单金额 {0} 超出限制,最大允许 {1} -ACCOUNT_INSUFFICIENT=账户余额不足,当前余额 {0},需要 {1} - -# messages_en.properties (英文) -USER_NOT_FOUND=User not found -USER_DUPLICATE=Username {0} already exists -ORDER_AMOUNT_INVALID=Order amount {0} exceeds limit, maximum allowed {1} -ACCOUNT_INSUFFICIENT=Insufficient balance, current {0}, required {1} -``` - -### 2. 抛出 LocaleMessageException - -```java -// 简单错误码 -throw new LocaleMessageException("USER_NOT_FOUND"); - -// 带占位符参数 -throw LocaleMessageException.of("USER_DUPLICATE", username); -throw LocaleMessageException.of("ORDER_AMOUNT_INVALID", amount, maxAmount); - -// 带原始异常 -throw new LocaleMessageException("SYSTEM_ERROR", cause); - -// 直接指定消息(不走国际化) -throw new LocaleMessageException("CUSTOM_ERROR", "自定义错误消息"); -``` - -### 3. 获取错误码和消息 - -```java -try { - userService.create(cmd); -} catch (LocaleMessageException e) { - String errCode = e.getErrCode(); // "USER_DUPLICATE" - String errMsg = e.getErrMessage(); // "用户名 admin 已存在" -} -``` - -### 4. 处理 EventException - -当多个事件处理器中有多个抛出异常时,框架会将它们收集到 `EventException` 中: - -```java -try { - EventPusher.push(new OrderCreatedEvent(orderId)); -} catch (EventException e) { - List errors = e.getError(); - log.error("Event processing failed with {} errors", errors.size()); - for (Exception error : errors) { - log.error(" - {}", error.getMessage()); - } -} -``` - -### 5. 处理 EventLoopException - -当事件形成循环调用时自动抛出: - -```java -try { - EventPusher.push(new EventA()); -} catch (EventLoopException e) { - List> stack = e.getStack(); - log.error("Event loop detected! Stack: {}", stack); - // 输出: event loop error current event class:class EventA, - // history event stack:[class EventA, class EventB] -} -``` - -## 使用实例 - -### 完整的 Service 层异常处理 - -```java -@Service -public class UserService { - - @Autowired - private UserRepository userRepository; - - public User createUser(CreateUserCommand cmd) { - // 1. 唯一性校验 - 带占位符的国际化异常 - if (userRepository.existsByName(cmd.getName())) { - throw LocaleMessageException.of("USER_DUPLICATE", cmd.getName()); - } - - // 2. 业务规则校验 - if (cmd.getAge() != null && cmd.getAge() < 0) { - throw LocaleMessageException.of("AGE_INVALID", cmd.getAge()); - } - - // 3. 创建用户 - User user = new User(); - user.setName(cmd.getName()); - user.setAge(cmd.getAge()); - - try { - userRepository.save(user); - } catch (DataAccessException e) { - // 包装底层异常为业务异常 - throw new LocaleMessageException("USER_CREATE_FAILED", e); - } - - return user; - } - - public User findById(Long id) { - return userRepository.findById(id) - .orElseThrow(() -> new LocaleMessageException("USER_NOT_FOUND")); - } - - public void transferPoints(Long fromUserId, Long toUserId, int points) { - User fromUser = findById(fromUserId); - User toUser = findById(toUserId); - - if (fromUser.getPoints() < points) { - throw LocaleMessageException.of("POINTS_INSUFFICIENT", - fromUser.getPoints(), points); - } - - fromUser.setPoints(fromUser.getPoints() - points); - toUser.setPoints(toUser.getPoints() + points); - - userRepository.save(fromUser); - userRepository.save(toUser); - } -} -``` - -### Controller 层的全局异常处理配合 - -框架的 `LocaleMessageException` 通常由全局异常处理器捕获并转为标准响应: - -```java -@RestControllerAdvice -public class GlobalExceptionHandler { - - /** - * 处理国际化业务异常 - */ - @ExceptionHandler(LocaleMessageException.class) - public Response handleLocaleMessageException(LocaleMessageException e) { - return Response.buildFailure(e.getErrCode(), e.getErrMessage()); - } - - /** - * 处理事件聚合异常 - */ - @ExceptionHandler(EventException.class) - public Response handleEventException(EventException e) { - log.error("Event processing errors", e); - return Response.buildFailure("EVENT_PROCESSING_ERROR", - "事件处理异常,共 " + e.getError().size() + " 个错误"); - } - - /** - * 处理事件循环异常 - */ - @ExceptionHandler(EventLoopException.class) - public Response handleEventLoopException(EventLoopException e) { - log.error("Event loop detected", e); - return Response.buildFailure("EVENT_LOOP_ERROR", - "检测到事件循环调用"); - } -} -``` - -### messages.properties 完整示例 - -```properties -# ===== 用户模块 ===== -USER_NOT_FOUND=用户不存在 -USER_DUPLICATE=用户名 {0} 已存在 -USER_DISABLED=用户 {0} 已被禁用 -USER_CREATE_FAILED=用户创建失败 -AGE_INVALID=年龄 {0} 无效,必须大于等于 0 - -# ===== 订单模块 ===== -ORDER_NOT_FOUND=订单 {0} 不存在 -ORDER_STATUS_INVALID=订单状态不允许此操作,当前状态: {0} -ORDER_AMOUNT_INVALID=订单金额 {0} 超出限制,最大允许 {1} -ORDER_ALREADY_PAID=订单 {0} 已支付,请勿重复操作 - -# ===== 账户模块 ===== -ACCOUNT_NOT_FOUND=账户 {0} 不存在 -ACCOUNT_INSUFFICIENT=账户余额不足,当前余额 {0},需要 {1} -ACCOUNT_FROZEN=账户 {0} 已冻结 - -# ===== 系统模块 ===== -SYSTEM_ERROR=系统内部错误 -PARAM_MISSING=缺少必要参数: {0} -PARAM_INVALID=参数 {0} 格式不正确 -UNAUTHORIZED=未授权访问 -FORBIDDEN=无权执行此操作 -``` - -### 异常类层次结构 - -``` -RuntimeException - ├── LocaleMessageException — 国际化业务异常 - │ └── getErrCode() — 错误码 - │ └── getErrMessage() — 国际化后的错误消息 - │ - ├── EventException — 事件处理器聚合异常 - │ └── getError(): List — 所有处理器抛出的异常列表 - │ - └── EventLoopException — 事件循环调用异常 - └── getStack(): List> — 事件调用栈(按触发顺序) -``` - -### 关键设计要点 - -1. **errCode 是稳定的标识符**:前端应根据 errCode 而非 errMessage 做逻辑判断,因为 errMessage 会随语言变化 -2. **占位符从 {0} 开始**:遵循 Java MessageFormat 规范 -3. **LocaleMessage 自动初始化**:通过 `ExceptionConfiguration` 的 `@Bean(initMethod = "init")` 自动注册到 `MessageContext` -4. **EventException 保留原始异常链**:通过 `addSuppressed()` 将所有子异常附加到主异常上,确保日志可追溯 -5. **EventLoopException 包含完整调用栈**:stack 列表按事件触发顺序排列,便于定位循环点 -6. **线程安全**:`MessageContext` 使用单例模式 + synchronized 保证初始化安全;`LocaleContextHolder` 保证多语言请求隔离 diff --git a/docs/capabilities/fast-repository.md b/docs/capabilities/fast-repository.md index 442b7f783..7b7805502 100644 --- a/docs/capabilities/fast-repository.md +++ b/docs/capabilities/fast-repository.md @@ -1,213 +1,140 @@ --- name: fast-repository -description: JPA Repository 增强,支持基于 PageRequest 的动态过滤查询、HQL 构建和 SearchRequest 自动解析 +description: JPA Repository 增强,支持动态过滤查询(RequestFilter + Relation 操作符)、HQL 自动构建、排序 status: 已实现 scope: 后端 source: 项目自有 +last_commit: 44ba20df +code_files: + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/FastRepository.java + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/BaseRepository.java + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/DynamicRepository.java + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/SortRepository.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/request/PageRequest.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/request/RequestFilter.java --- ## 解决什么问题 -Spring Data JPA 的 `JpaRepository` 提供了基础的 CRUD 操作,但在实际业务中存在以下不足: +Spring Data JPA 原生的 `JpaRepository` 在处理复杂查询时存在以下痛点: -1. **动态条件查询繁琐**:需要手动编写 `Specification` 或 `@Query`,每个查询场景都要新建方法 -2. **前端过滤条件无法直传**:前端传来的 JSON 过滤参数需要逐字段解析再拼装查询 -3. **Example 查询局限**:Spring Data Example 只支持等值和部分模糊匹配,不支持 BETWEEN、IN、大于小于等关系 -4. **分页与过滤割裂**:分页参数和过滤条件分散在不同对象中,缺乏统一封装 -5. **原生 SQL 构建困难**:复杂动态查询需要手写字符串拼接,容易出错且不安全 +- **动态条件拼接繁琐**:需要手动编写 `Specification` 或 `@Query`,无法根据前端传入的过滤参数自动构建查询 +- **分页与过滤分离**:Spring 原生 `PageRequest` 不支持携带过滤条件,业务代码需要在 Controller/Service 层反复组装查询逻辑 +- **缺少丰富的比较操作符**:LIKE、BETWEEN、IN、IS_NULL 等常用操作符没有统一的抽象 -`FastRepository` 扩展了 `JpaRepository` + `JpaSpecificationExecutor`,内置三种查询模式: -- `findAll(PageRequest)` — 使用 Spring Data Example 进行简单等值/模糊查询 -- `pageRequest(PageRequest)` — 使用 HQL 动态构建,支持全部 `Relation` 关系 -- `searchRequest(SearchRequest)` — 直接从 HTTP 请求参数解析并执行查询 +本能力通过扩展 `PageRequest` 和提供 `FastRepository` 接口,实现了: + +- `RequestFilter` + `Relation` 枚举统一表达 14 种查询操作符 +- `FastRepository.findAll(PageRequest)` 自动根据 Filter 构建 Example 查询 +- `FastRepository.pageRequest(PageRequest)` 自动构建 HQL 动态查询 +- `DynamicRepository` 支持原生 SQL / SQLBuilder 的动态列表和分页查询 +- `SortRepository` 提供拖拽排序的 `reSort()` 方法 ## 如何使用 -### 1. 定义 Repository 接口 +### 核心接口 + +| 接口/类 | 说明 | +|---------|------| +| `FastRepository` | 增强型 Repository,继承 JpaRepository + JpaSpecificationExecutor + DynamicRepository | +| `BaseRepository` | 基础 Repository,提供 `getEntityClass()` 反射获取泛型类型 | +| `DynamicRepository` | 动态查询 Repository,支持 SQLBuilder 和原生 SQL 查询 | +| `SortRepository` | 排序 Repository,提供 `reSort(SortRequest)` 拖拽排序 | +| `PageRequest` | 扩展 Spring Data PageRequest,内置 `RequestFilter` | +| `RequestFilter` | 过滤条件容器,管理多个 `Filter` | +| `Filter` | 单个过滤条件,包含 key、relation、value | +| `Relation` | 查询操作符枚举 | + +### Relation 操作符 + +``` +EQUAL, NOT_EQUAL, LIKE, LEFT_LIKE, RIGHT_LIKE, +BETWEEN, IN, NOT_IN, IS_NULL, IS_NOT_NULL, +GREATER_THAN, LESS_THAN, GREATER_THAN_EQUAL, LESS_THAN_EQUAL +``` -只需继承 `FastRepository`,无需额外代码: +### 注入方式 + +让实体 Repository 继承 `FastRepository` 即可: ```java public interface UserRepository extends FastRepository { - // 自动获得 findAll(PageRequest)、pageRequest(PageRequest)、searchRequest(SearchRequest) 等方法 } ``` -### 2. 使用 findAll(PageRequest) — Example 模式 +## 使用实例 -适用于简单的等值和 LIKE 查询,底层使用 Spring Data Example: +### 1. 基础动态过滤查询(Example 模式) ```java +// 创建分页请求并添加过滤条件 PageRequest request = PageRequest.of(0, 20); -request.addFilter("name", "张三"); // EQUAL -request.addFilter("email", Relation.LIKE, "@example.com"); +request.addFilter("name", "张三"); // 默认 EQUAL +request.addFilter("age", Relation.GREATER_THAN, 18); // 大于 +request.addFilter("email", Relation.LIKE, "gmail"); // 模糊匹配 Page page = userRepository.findAll(request); ``` -> **注意**:Example 模式仅支持有限的 Relation 类型。如需完整关系支持,请使用 `pageRequest()` 方法。 - -### 3. 使用 pageRequest(PageRequest) — HQL 模式 - -支持所有 `Relation` 枚举定义的比较关系,底层动态生成 HQL: +### 2. HQL 动态查询 ```java -PageRequest request = PageRequest.of(0, 20) - .addFilter("age", Relation.GREATER_THAN, 18) - .addFilter("createTime", Relation.BETWEEN, startDate, endDate) - .addFilter("status", Relation.IN, 1, 2, 3) - .addSort(Sort.by("createTime").descending()); +PageRequest request = PageRequest.of(0, 20); +request.addFilter("status", Relation.IN, "ACTIVE", "PENDING"); +request.addFilter("createTime", Relation.BETWEEN, startDate, endDate); +// pageRequest() 使用 HQL 构建查询,适合复杂条件 Page page = userRepository.pageRequest(request); ``` -### 4. 使用 searchRequest(SearchRequest) — HTTP 自动解析模式 - -直接将 HTTP 请求参数转为查询条件,适合前后端联调: +### 3. AND/OR 组合条件 ```java -@GetMapping("/users") -public MultiResponse list(SearchRequest searchRequest) { - Page page = userRepository.searchRequest(searchRequest); - return MultiResponse.of(page); -} -``` +PageRequest request = PageRequest.of(0, 20); -前端可通过 URL 参数传递过滤条件: +// OR 组合:name='张三' OR name='李四' +request.orFilters( + Filter.as("name", "张三"), + Filter.as("name", "李四") +); + +// AND 组合 +request.andFilters( + Filter.as("age", Relation.GREATER_THAN_EQUAL, 18), + Filter.as("status", "ACTIVE") +); ``` -GET /api/users?current=0&pageSize=20&name=张三&age=18 -``` - -或通过 Base64 编码的 filter/sort 参数传递复杂条件。 -### 5. 结合 DynamicNativeRepository +### 4. 从请求中读取过滤值 -`FastRepository` 同时继承了 `DynamicNativeRepository`,支持原生 SQL 动态查询(具体用法参见相关文档)。 - -## 使用实例 +```java +// 在 Service 层获取前端传入的过滤参数 +String name = request.getStringFilter("name", "default"); +int age = request.getIntFilter("age", 0); +boolean hasFilter = request.hasFilter(); +``` -### 完整的用户管理 CRUD + 动态查询 +### 5. 原生 SQL 动态查询 ```java -// Entity -@Entity -@Table(name = "t_user") -public class User { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - private String email; - private Integer age; - private Integer status; - private LocalDateTime createTime; -} - -// Repository -public interface UserRepository extends FastRepository { - // 可继续添加自定义查询方法 - List findByStatus(Integer status); -} +// 使用 DynamicRepository 的原生 SQL 查询 +List users = userRepository.dynamicListQuery( + "SELECT * FROM user WHERE status = ?", "ACTIVE" +); + +// 分页查询 +Page page = userRepository.dynamicPageQuery( + "SELECT * FROM user WHERE dept_id = ?", + "SELECT COUNT(*) FROM user WHERE dept_id = ?", + PageRequest.of(0, 20), + deptId +); +``` -// Service -@Service -public class UserService { - - @Autowired - private UserRepository userRepository; - - /** - * 简单等值查询 - */ - public Page findByName(String name, int page, int size) { - PageRequest request = PageRequest.of(page, size); - request.addFilter("name", name); - return userRepository.findAll(request); - } - - /** - * 复杂条件查询 - */ - public Page advancedSearch(UserSearchCriteria criteria) { - PageRequest request = PageRequest.of(criteria.getPage(), criteria.getSize()) - .addSort(Sort.by("createTime").descending()); - - if (StringUtils.hasText(criteria.getName())) { - request.addFilter("name", Relation.LIKE, criteria.getName()); - } - if (criteria.getMinAge() != null) { - request.addFilter("age", Relation.GREATER_THAN_EQUAL, criteria.getMinAge()); - } - if (criteria.getMaxAge() != null) { - request.addFilter("age", Relation.LESS_THAN_EQUAL, criteria.getMaxAge()); - } - if (criteria.getStatuses() != null && !criteria.getStatuses().isEmpty()) { - request.addFilter("status", Relation.IN, - criteria.getStatuses().toArray()); - } - if (criteria.getStartDate() != null && criteria.getEndDate() != null) { - request.addFilter("createTime", Relation.BETWEEN, - criteria.getStartDate(), criteria.getEndDate()); - } - - return userRepository.pageRequest(request); - } - - /** - * HTTP 参数自动解析查询 - */ - public Page searchFromHttp(SearchRequest searchRequest) { - return userRepository.searchRequest(searchRequest); - } -} +### 6. 拖拽排序 -// Controller -@RestController -@RequestMapping("/api/users") -public class UserController { - - @Autowired - private UserService userService; - - @GetMapping("/simple") - public MultiResponse simpleSearch( - @RequestParam(required = false) String name, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - Page result = userService.findByName(name, page, size); - return MultiResponse.of(result); - } - - @GetMapping("/advanced") - public MultiResponse advancedSearch(UserSearchCriteria criteria) { - Page result = userService.advancedSearch(criteria); - return MultiResponse.of(result); - } - - @GetMapping - public MultiResponse autoSearch(SearchRequest searchRequest) { - Page result = userService.searchFromHttp(searchRequest); - return MultiResponse.of(result); - } -} +```java +// 实体需实现 ISort 接口 +SortRequest sortRequest = new SortRequest(List.of(3L, 1L, 2L)); +userRepository.reSort(sortRequest); ``` - -### 三种查询模式对比 - -| 特性 | `findAll(PageRequest)` | `pageRequest(PageRequest)` | `searchRequest(SearchRequest)` | -|------|------------------------|----------------------------|--------------------------------| -| 底层实现 | Spring Data Example | 动态 HQL | 转 PageRequest → HQL | -| 支持的 Relation | EQUAL, LIKE (有限) | 全部 14 种 | 全部 14 种 | -| 组合条件 (AND/OR) | ❌ | ✅ | ✅ | -| HTTP 参数自动解析 | ❌ | ❌ | ✅ | -| 嵌套属性查询 | 有限支持 | ✅ | ✅ | -| 性能 | 较好 | 良好 | 良好 | -| 适用场景 | 简单等值筛选 | 复杂业务查询 | 前后端联调/通用列表页 | - -### 注意事项 - -1. **实体类要求**:过滤字段名必须与实体类的属性名一致(不是数据库列名) -2. **类型匹配**:过滤值的类型应与实体字段类型兼容,框架会尝试自动转换 -3. **空值处理**:当过滤值为 null 或空字符串时,该条件会被跳过 -4. **SQL 注入防护**:HQL 模式使用参数化查询,不存在 SQL 注入风险 -5. **分页起始页**:`PageRequest.of(page, size)` 中 page 从 0 开始 diff --git a/docs/capabilities/fastjson.md b/docs/capabilities/fastjson.md deleted file mode 100644 index d55242be6..000000000 --- a/docs/capabilities/fastjson.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: fastjson -description: 高性能 JSON 序列化/反序列化库 -status: 已实现 -scope: 后端 -source: 框架:Fastjson ---- - -## 解决什么问题 - -在框架中需要频繁进行 Java 对象与 JSON 字符串之间的转换,包括: -- Response 统一响应体的序列化 -- Token 对象的 JSON 序列化/反序列化 -- DTO 数据的 Map 转换 -- Security Filter 中的请求/响应体 JSON 处理 -- Flow 工作流模块的数据快照绑定 - -Fastjson 提供了比 Jackson 更简洁的 API(`JSONObject.toJSONString()` / `JSONObject.parseObject()`),适合框架内部轻量级序列化场景。 - -## 如何使用 - -### 依赖引入 - -```xml - - - com.alibaba.fastjson2 - fastjson2 - 2.0.53 - -``` - -### 核心接口:JsonSerializable - -框架定义了 `JsonSerializable` 接口,所有需要 JSON 序列化的 DTO/实体类实现此接口即可获得 `toJson()` 方法: - -```java -public interface JsonSerializable extends Serializable { - default String toJson() { - return JSONObject.toJSONString(this); - } -} -``` - -### 核心接口:MapSerializable - -提供对象到 Map 的转换能力: - -```java -public interface MapSerializable { - default Map toMap() { - return JSON.parseObject(JSON.toJSONString(this)); - } -} -``` - -## 使用实例 - -### 1. Token 序列化(JWT 模式) - -```java -// JwtTokenGateway.java - 创建 JWT Token -public Token create(String username, String iv, List authorities, String extra) { - Token token = new Token(username, iv, extra, authorities, validTime, restTime); - // 将 Token 对象序列化为 JSON 作为 JWT subject - String jwt = Jwts.builder().subject(token.toJson()).signWith(key).compact(); - token.setToken(jwt); - return token; -} - -// 解析 JWT Token -public Token parser(String sign) { - Jws jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(sign); - String subject = jws.getPayload().getSubject(); - // 从 JSON 反序列化为 Token 对象 - return JSONObject.parseObject(subject, Token.class); -} -``` - -### 2. Security Filter 中的 JSON 响应 - -```java -// MyAuthenticationFilter.java / MyLoginFilter.java 等 -// 使用 JSONObject 构建错误响应 -JSONObject json = new JSONObject(); -json.put("errCode", "401"); -json.put("errMessage", "认证失败"); -response.getWriter().write(json.toJSONString()); -``` - -### 3. Redis Token 存储 - -```java -// RedisTokenGateway.java - 使用 fastjson2 进行 Token 持久化 -import com.alibaba.fastjson2.JSONObject; - -// Token 序列化存入 Redis -String json = token.toJson(); -redisTemplate.opsForValue().set(key, json); - -// 从 Redis 读取并反序列化 -String json = redisTemplate.opsForValue().get(key); -Token token = JSONObject.parseObject(json, Token.class); -``` - -### 4. Flow 数据快照绑定 - -```java -// BindDataSnapshot.java / FlowMapBindData.java -// 使用 JSONObject 处理流程数据快照 -JSONObject data = new JSONObject(); -data.put("key", value); -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter | `JsonSerializable`、`MapSerializable` 接口定义 | -| springboot-starter-security | Token 序列化、Filter JSON 响应、JWT/Redis 网关 | -| springboot-starter-flow | 数据快照绑定、Schema 解析 | diff --git a/docs/capabilities/flow-postpone-event.md b/docs/capabilities/flow-postpone-event.md new file mode 100644 index 000000000..84d7d84dc --- /dev/null +++ b/docs/capabilities/flow-postpone-event.md @@ -0,0 +1,77 @@ +--- +name: flow-postpone-event +description: 延期流程操作的事件通知 — 当前 FlowPostponedService 未发送 FlowApprovalEvent +status: 计划中 +scope: 后端 +source: 计划 +--- + +## 解决什么问题 + +当前工作流引擎的所有操作(发起、提交、驳回、撤回、删除、作废、退回、完成、转办、催办、终止)都通过 `EventPusher` 发送 `FlowApprovalEvent`,业务侧可通过 `IHandler` 监听并联动处理。 + +**但延期(Postpone)操作是唯一的例外**:`FlowPostponedService.postponed()` 仅更新了 `FlowRecord` 的截止时间,没有发送任何事件。这导致: + +- 业务系统无法感知延期操作,无法触发提醒、日志、通知等联动 +- 与其他流程操作的契约不一致 — Handler 中 `isXxx()` 判断覆盖了所有操作,唯独缺少 `isPostponed()` +- 审计日志中延期操作无事件记录 + +## 如何使用 + +### 计划实现 + +1. 在 `FlowApprovalEvent` 中新增状态常量: + ```java + public static final int STATE_POSTPONED = 15; + ``` + +2. 新增便捷判断方法: + ```java + public boolean isPostponed() { + return state == STATE_POSTPONED; + } + ``` + +3. 在 `FlowPostponedService.postponed()` 末尾发送事件: + ```java + EventPusher.push(new FlowApprovalEvent( + FlowApprovalEvent.STATE_POSTPONED, + flowRecord, currentOperator, flowWork, bindData + )); + ``` + +4. 在 `event.md` 规范文档中将"延期流程"事件更新为 `POSTPONED` + +### 业务监听 + +```java +@Component +public class LeavePostponedHandler implements IHandler { + @Override + public void handler(FlowApprovalEvent event) { + if (event.isPostponed()) { + FlowRecord record = event.getFlowRecord(); + // 发送延期提醒通知给申请人 + notifyService.sendDelayNotice( + record.getCreateOperatorId(), + record.getPostponedTime() + ); + } + } +} +``` + +## 使用实例 + +```java +// 当前状态(无事件): +flowPostponedService.postponed(recordId, operator, delayMillis); +// → FlowRecord 截止时间已更新 +// → ❌ 无 FlowApprovalEvent 发出 + +// 实现后(有事件): +flowPostponedService.postponed(recordId, operator, delayMillis); +// → FlowRecord 截止时间已更新 +// → ✅ FlowApprovalEvent(STATE_POSTPONED) 通过 EventPusher 发出 +// → ✅ LeavePostponedHandler.handler() 被调用 +``` diff --git a/docs/capabilities/groovy-script-engine.md b/docs/capabilities/groovy-script-engine.md index 915286669..451c399e0 100644 --- a/docs/capabilities/groovy-script-engine.md +++ b/docs/capabilities/groovy-script-engine.md @@ -1,237 +1,133 @@ --- name: groovy-script-engine -description: Groovy 脚本运行时编译、缓存与热更新引擎,提供 REST API 管理脚本生命周期 +description: Groovy 动态脚本引擎,支持运行时编译执行、LRU 缓存、热更新和 REST API status: 已实现 scope: 后端 source: 项目自有 +last_commit: fbfb901b +code_files: + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/GroovyScriptRuntime.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/GroovyScriptRuntimeContext.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/cache/GroovyScriptCacheContext.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/repository/GroovyScriptRepository.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/repository/TempGroovyScriptRepository.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/strategy/GroovyTypeFixStrategy.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/strategy/GroovyMetadataGenerateStrategy.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/controller/GroovyScriptController.java + - springboot-starter-script/src/main/java/com/codingapi/springboot/script/runner/GroovyScriptEngineRunner.java --- ## 解决什么问题 -在企业应用中,部分业务规则(如流程节点的标题生成、操作者匹配、数据校验等)需要在不重启服务的情况下动态调整。硬编码这些规则会导致每次变更都需要重新部署,而使用配置化方案又难以表达复杂逻辑。 +业务规则频繁变更时,硬编码需要重新编译部署。Groovy 脚本引擎提供运行时动态编译执行能力,让业务规则可以通过脚本形式热加载,无需重启应用。典型场景: -`springboot-starter-script` 提供了一个基于 Groovy 的脚本引擎,支持: - -- **运行时编译**:通过 `GroovyScript.compile(cache)` 在运行时编译 Groovy 脚本 -- **LRU 缓存**:编译后的脚本对象通过 `GroovyScriptRuntimeContext` 缓存,避免重复编译 -- **热更新**:修改脚本后重新调用 `compile(true)` 即可生效,无需重启 -- **元数据扫描**:通过注解(`@ScriptType`、`@ScriptField`、`@ScriptFunction`、`@ScriptParameter`)自动提取脚本的类型、字段、方法等元数据信息 -- **策略扩展**:通过 `GroovyTypeFixStrategy`、`GroovyMetadataGenerateStrategy`、`ScriptTypeMapping` 三种策略接口扩展脚本处理逻辑 -- **REST API**:内置 `GroovyScriptController` 提供脚本编译、查询、保存等 HTTP 接口 -- **临时脚本**:支持临时存储和定时清理机制,方便开发调试 +- 动态表单校验规则 +- 工作流节点审批脚本 +- 数据转换/映射规则 +- 临时数据处理脚本 ## 如何使用 -### 1. 引入依赖 - -```xml - - com.codingapi.springboot - springboot-starter-script - -``` - -### 2. 配置属性 - -在 `application.properties` 中配置脚本引擎参数: - -```properties -# 脚本引擎配置前缀 -codingapi.script.*=... -``` - -配置通过 `GroovyScriptProperties` 绑定,由 `AutoConfiguration` 自动注册。 - -### 3. 创建和执行脚本 - -使用 `GroovyScript.Builder` 构建脚本对象: +### 核心运行时(GroovyScriptRuntime) ```java -GroovyScript script = GroovyScript.builder("my_script_key") - .script("return 'Hello, ' + name") - .description("问候脚本") - .build(); - -// 编译脚本(启用缓存) -script.compile(true); - -// 执行脚本 -Map binds = Map.of("name", "World"); -String result = script.invoke(TransactionMode.NONE, binds); -``` - -核心 API: -- `GroovyScript.builder(key)` — 创建构建器 -- `builder.script(String)` — 设置脚本内容 -- `builder.description(String)` — 设置描述 -- `script.compile(boolean cache)` — 编译脚本,`cache=true` 时缓存编译结果 -- `script.invoke(TransactionMode, Map binds, Object... requests)` — 执行脚本 -- `script.save()` — 持久化脚本到仓储 -- `script.temp()` — 临时存储脚本(会被定时清理) -- `script.toMetadata()` — 提取脚本元数据 +// 初始化运行时(指定 LRU 缓存最大容量) +GroovyScriptRuntime runtime = new GroovyScriptRuntime(100); -### 4. 脚本元数据注解 +// 编译脚本(带缓存 — 以 SHA256 哈希为 Key) +runtime.compile("def add(a, b) { return a + b }", true); -通过注解为脚本添加结构化元数据: +// 编译脚本(不带缓存) +runtime.compile("println 'hello'", false); -```groovy -@ScriptType(name = "LeaveApproval", description = "请假审批脚本") -class LeaveApproval { - - @ScriptField(description = "请假天数") - int days +// 执行脚本 +Object result = runtime.execute("return 1 + 2"); - @ScriptFunction(description = "计算审批级别") - int calculateLevel( - @ScriptParameter(description = "员工职级") int rank - ) { - return rank > 5 ? 2 : 1 - } -} +// 执行脚本并绑定变量 +Map bindings = new HashMap<>(); +bindings.put("name", "张三"); +Object result = runtime.execute("return 'Hello, ' + name", bindings); ``` -框架通过 `GroovyMetadataScannerUtils.scanner(groovyScript)` 自动扫描这些注解,生成 `GroovyMetadata` 对象,包含 `GroovyType`、`GroovyField`、`GroovyFunction` 等信息。 - -### 5. 扩展策略 +### LRU 缓存机制 -#### 类型修复策略 (GroovyTypeFixStrategy) +- 缓存 Key 为脚本内容的 SHA256 哈希值 +- 采用 `LinkedHashMap` 实现 LRU 淘汰策略 +- 超过 `maxCacheSize` 时自动移除最久未使用的条目 +- 通过 `clearCache()` 手动清空缓存 +- 通过 `cacheSize()` 查看当前缓存数量 -当需要对特定类型的脚本进行额外处理时: +### 脚本仓库(GroovyScriptRepository) +持久化存储脚本,支持 CRUD 和按名称查找: ```java -GroovyTypeFixStrategyContext.getInstance().addFixStrategy( - new GroovyTypeFixStrategy() { - @Override - public boolean support(Class clazz) { - return MyInterface.class.isAssignableFrom(clazz); - } - - @Override - public void fix(GroovyScript groovyScript, GroovyType groovyType) { - // 对 groovyType 进行修复或增强 - } - } -); -``` +// 保存脚本 +repository.save("calc_discount", scriptContent); -#### 元数据生成策略 (GroovyMetadataGenerateStrategy) +// 按名称执行 +Object result = runtime.executeByName("calc_discount", bindings); +``` -自定义元数据生成逻辑(优先于注解扫描): +### 临时脚本(TempGroovyScriptRepository) +一次性执行的临时脚本,执行后自动清理: ```java -GroovyMetadataGenerateStrategyContext.getInstance().addGenerateStrategy( - new GroovyMetadataGenerateStrategy() { - @Override - public boolean support(GroovyScript groovyScript) { - return "special_key".equals(groovyScript.getKey()); - } - - @Override - public GroovyMetadata generate(GroovyScript groovyScript) { - // 手动构建元数据 - return customMetadata; - } - } -); +// 提交临时脚本 +tempRepo.submit("return data.collect { it.name }", dataBindings); ``` -#### 类型映射策略 (ScriptTypeMapping) - -将脚本中的类型映射为目标类型: +### 元数据扫描 +通过注解扫描提取脚本元数据,用于前端展示: ```java -ScriptTypeMappingContext.getInstance().addMapping( - new ScriptTypeMapping() { - @Override - public boolean support(Class target) { - return target == OriginalType.class; - } - - @Override - public Class mapping(Class target) { - return MappedType.class; - } +@GroovyScript +class DiscountCalc { + @ScriptFunction(description = "计算折扣") + static BigDecimal calc(@ScriptParameter("金额") BigDecimal amount) { + return amount.multiply(new BigDecimal("0.9")) } -); +} ``` -### 6. REST API +### 策略扩展点 -框架内置了 `GroovyScriptController`,提供以下端点: +- `GroovyMetadataGenerateStrategy` — 自定义元数据生成逻辑 +- `GroovyTypeFixStrategy` — 自定义类型元数据修正(如补充泛型信息) -| 方法 | 路径 | 说明 | -|------|------|------| -| POST | `/api/groovy-script/compile` | 编译脚本 | -| GET | `/api/groovy-script/getScript?key=xxx` | 获取脚本内容 | -| GET | `/api/groovy-script/getMetadata?key=xxx` | 获取脚本元数据 | -| POST | `/api/groovy-script/save` | 保存脚本 | - -编译请求体 (`ScriptCompileRequest`): -```json -{ - "script": "return 'hello'", - "cache": true -} -``` +### REST API(GroovyScriptController) -保存请求体 (`ScriptSaveRequest`): -```json -{ - "key": "my_script_key", - "script": "return 'updated content'" -} -``` +| 端点 | 方法 | 说明 | +|------|------|------| +| `/open/script/save` | POST | 保存脚本 | +| `/open/script/compile` | POST | 编译检查(不执行) | +| `/open/script/run` | POST | 执行脚本 | +| `/open/script/list` | GET | 列出所有脚本 | ## 使用实例 -### 脚本生命周期管理 - ```java -// 1. 创建脚本 -GroovyScript script = GroovyScript.builder("order_validator") - .script(""" - boolean validate(Map order) { - return order.amount > 0 && order.items != null - } - """) - .description("订单校验脚本") - .build(); - -// 2. 临时存储(用于开发调试,会被定时清理) -script.temp(); - -// 3. 编译并缓存 -script.compile(true); - -// 4. 执行 -Map binds = Map.of("order", orderData); -Boolean valid = script.invoke(TransactionMode.NONE, binds); - -// 5. 确认无误后持久化 -script.save(); -``` - -### 引擎启动与关闭 - -`GroovyScriptEngineRunner` 实现了 `InitializingBean` 和 `DisposableBean`: - -- **启动时**(`afterPropertiesSet`):从 `TempGroovyScriptRepository` 加载所有临时脚本到内存缓存 -- **关闭时**(`destroy`):将内存中的临时脚本持久化到 `TempGroovyScriptRepository` - -这确保了服务重启后临时脚本不会丢失。 - -### 策略上下文清理 - -所有策略上下文都提供了 `clear()` 方法,可在测试或重新初始化时清空已注册的策略: +// 1. 基本执行 +GroovyScriptRuntime runtime = new GroovyScriptRuntime(50); +Object result = runtime.execute("return [1,2,3].sum()"); +assert result.equals(6); + +// 2. 缓存模式 — 相同内容只编译一次 +String script = "def discount(amount) { return amount * 0.85 }"; +runtime.compile(script, true); // 首次编译并缓存 +Object r1 = runtime.execute(script); // 命中缓存 +Object r2 = runtime.execute(script); // 命中缓存 + +// 3. 绑定变量执行 +Map vars = new HashMap<>(); +vars.put("price", new BigDecimal("100")); +vars.put("quantity", 3); +Object total = runtime.execute( + "return price.multiply(new BigDecimal(quantity))", + vars +); +// total = 300 -```java -GroovyTypeFixStrategyContext.getInstance().clear(); -GroovyMetadataGenerateStrategyContext.getInstance().clear(); -ScriptTypeMappingContext.getInstance().clear(); +// 4. 清空缓存 +runtime.clearCache(); +assert runtime.cacheSize() == 0; ``` - -### 自动配置 - -`AutoConfiguration` 类通过 `@ComponentScan(basePackages = "com.codingapi.springboot.script")` 自动扫描脚本模块的所有组件,并注册: -- `GroovyScriptEngineRunner` Bean — 负责引擎生命周期管理 -- `GroovyScriptProperties` Bean — 绑定 `codingapi.script.*` 配置项 diff --git a/docs/capabilities/http-client-factory.md b/docs/capabilities/http-client-factory.md deleted file mode 100644 index e2b05e6db..000000000 --- a/docs/capabilities/http-client-factory.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -name: http-client-factory -description: 信任所有证书的 Apache HttpClient 5 工厂,用于内部 HTTPS 调用跳过证书验证 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在微服务架构或企业内部系统集成中,服务间通信常使用自签名证书或内部 CA 签发的证书。标准的 Apache HttpClient 会严格校验 SSL 证书链,导致请求失败并抛出 `SSLHandshakeException`。 - -`TrustAnyHttpClientFactory` 提供了一个静态工厂方法,创建跳过 SSL 证书验证的 Apache HttpClient 5 实例。它同时被 `RestTemplateContext` 作为底层 HttpClient 使用,也可以独立用于任何需要信任所有证书的场景。 - -核心设计: -- **内部 TrustManager**:`TrustAnyTrustManager` 实现了 `X509TrustManager`,`checkClientTrusted` 和 `checkServerTrusted` 均为空实现 -- **NoopHostnameVerifier**:不校验服务器主机名与证书的匹配 -- **双协议支持**:通过 `Registry` 同时注册 HTTP(`PlainConnectionSocketFactory`)和 HTTPS(`SSLConnectionSocketFactory`) -- **循环重定向**:默认开启 `circularRedirectsAllowed` - -## 如何使用 - -### 1. 创建信任所有证书的 HttpClient - -```java -import com.codingapi.springboot.framework.utils.TrustAnyHttpClientFactory; -import org.apache.hc.client5.http.classic.HttpClient; - -HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); -``` - -### 2. 配合 RestTemplate 使用 - -```java -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -HttpComponentsClientHttpRequestFactory factory = - new HttpComponentsClientHttpRequestFactory( - TrustAnyHttpClientFactory.createTrustAnyHttpClient() - ); -RestTemplate restTemplate = new RestTemplate(factory); -``` - -### 3. 直接使用 HttpClient 执行请求 - -```java -HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); - -// 使用 Apache HC5 API 执行请求 -httpClient.execute(new HttpGet("https://internal-service.example.com/api/data"), response -> { - int statusCode = response.getCode(); - String body = EntityUtils.toString(response.getEntity()); - // 处理响应... - return body; -}); -``` - -## 使用实例 - -### 自定义 HttpClient 配置 - -如需在信任所有证书的基础上添加额外配置(如超时、连接池等),可参考 `TrustAnyHttpClientFactory` 的实现模式自行扩展: - -```java -@SneakyThrows -public static HttpClient createCustomTrustAnyHttpClient() { - // 1. 创建信任所有证书的 SSL 上下文 - SSLContext sslContext = SSLContext.getInstance("TLS"); - X509TrustManager trustManager = new X509TrustManager() { - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) {} - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) {} - @Override - public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } - }; - sslContext.init(null, new TrustManager[]{trustManager}, null); - - // 2. 创建 SSL 连接工厂 - SSLConnectionSocketFactory sslFactory = - new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); - - // 3. 注册协议 - Registry registry = RegistryBuilder.create() - .register("https", sslFactory) - .register("http", new PlainConnectionSocketFactory()) - .build(); - - // 4. 配置请求参数 - RequestConfig requestConfig = RequestConfig.custom() - .setCircularRedirectsAllowed(true) - .setResponseTimeout(Timeout.ofSeconds(30)) - .setConnectTimeout(Timeout.ofSeconds(10)) - .build(); - - // 5. 构建 HttpClient - BasicHttpClientConnectionManager connManager = - new BasicHttpClientConnectionManager(registry); - - return HttpClients.custom() - .setDefaultRequestConfig(requestConfig) - .setConnectionManager(connManager) - .build(); -} -``` - -### 内部类 TrustAnyTrustManager - -`TrustAnyTrustManager` 是 `TrustAnyHttpClientFactory` 的内部私有类,实现了 `X509TrustManager` 接口: - -```java -private static class TrustAnyTrustManager implements X509TrustManager { - @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s) - throws CertificateException { - // 空实现:信任所有客户端证书 - } - - @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) - throws CertificateException { - // 空实现:信任所有服务端证书 - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } -} -``` - -### 与其他模块的关系 - -`TrustAnyHttpClientFactory` 被 `RestTemplateContext` 直接引用作为底层 HttpClient 的创建方式: - -```java -// RestTemplateContext 构造函数 -private RestTemplateContext() { - HttpComponentsClientHttpRequestFactory requestFactory = - new HttpComponentsClientHttpRequestFactory( - TrustAnyHttpClientFactory.createTrustAnyHttpClient() - ); - this.restTemplate = restTemplate(requestFactory); -} -``` - -如果只需要底层的 HttpClient 而不需要 RestTemplate 封装,可以直接使用 `TrustAnyHttpClientFactory`。 - -### ⚠️ 安全提示 - -此工厂创建的 HttpClient **完全跳过了 SSL/TLS 证书验证**,包括: -- 不验证证书链是否由受信任的 CA 签发 -- 不验证证书是否过期 -- 不验证服务器主机名是否与证书中的 CN/SAN 匹配 - -**仅应在以下场景使用:** -- 内部微服务间的 HTTPS 通信(自签名证书) -- 开发/测试环境调试 -- 对接已知可信但证书不规范的第三方系统 - -**不应在以下场景使用:** -- 面向公网的用户数据传输 -- 涉及敏感数据的跨组织通信 -- 任何需要保证传输安全性的生产环境外部调用 diff --git a/docs/capabilities/httpclient5.md b/docs/capabilities/httpclient5.md deleted file mode 100644 index fa5f4d0bf..000000000 --- a/docs/capabilities/httpclient5.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: httpclient5 -description: HTTP 客户端库,支持 HTTPS 信任所有证书的请求 -status: 已实现 -scope: 后端 -source: 框架:Apache HttpClient 5 ---- - -## 解决什么问题 - -框架需要发起外部 HTTP/HTTPS 请求的场景: -- 对接第三方服务时需要跳过 SSL 证书验证(开发/测试环境) -- 通过 `RestTemplate` 发送 HTTP 请求时底层使用高性能连接管理 -- 支持代理配置的企业内网环境 - -Apache HttpClient 5 提供了成熟的 HTTP 客户端实现,项目中主要用于创建信任所有证书的 `HttpClient` 实例,作为 Spring `RestTemplate` 的底层传输层。 - -## 如何使用 - -### 依赖引入 - -```xml - - - org.apache.httpcomponents.client5 - httpclient5 - -``` - -### 核心类 - -- `TrustAnyHttpClientFactory` — 创建信任所有证书的 HttpClient 工厂 -- `RestTemplateContext` — 基于 HttpClient 5 的 RestTemplate 单例上下文 -- `HttpClient` — 框架封装的 HTTP 客户端(GET/POST) -- `HttpRequest` — 底层请求执行器(支持代理、重定向) - -## 使用实例 - -### 1. 创建信任所有证书的 HttpClient - -```java -// TrustAnyHttpClientFactory.java -import org.apache.hc.client5.http.classic.HttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; -import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; - -public class TrustAnyHttpClientFactory { - - public static HttpClient createTrustAnyHttpClient() { - // 创建信任所有证书的 SSLContext - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, new TrustManager[]{new TrustAnyTrustManager()}, null); - - // 使用 NoopHostnameVerifier 跳过主机名验证 - SSLConnectionSocketFactory sslSocketFactory = - new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); - - // 注册 HTTP 和 HTTPS 协议 - Registry registry = RegistryBuilder - .create() - .register("https", sslSocketFactory) - .register("http", new PlainConnectionSocketFactory()) - .build(); - - BasicHttpClientConnectionManager connectionManager = - new BasicHttpClientConnectionManager(registry); - - return HttpClients.custom() - .setDefaultRequestConfig(RequestConfig.custom() - .setCircularRedirectsAllowed(true).build()) - .setConnectionManager(connectionManager) - .build(); - } -} -``` - -### 2. RestTemplate 集成 - -```java -// RestTemplateContext.java - 使用 HttpClient 5 作为底层传输 -public class RestTemplateContext { - private static RestTemplateContext instance; - private final RestTemplate restTemplate; - - private RestTemplateContext() { - // 使用 Apache HttpClient 5 创建请求工厂 - HttpComponentsClientHttpRequestFactory requestFactory = - new HttpComponentsClientHttpRequestFactory( - TrustAnyHttpClientFactory.createTrustAnyHttpClient() - ); - this.restTemplate = new RestTemplate(requestFactory); - } - - public static RestTemplateContext getInstance() { /* 双重检查锁单例 */ } -} -``` - -### 3. 框架封装的 HttpClient - -```java -// HttpClient.java - 简洁的 HTTP 调用 API -HttpClient client = new HttpClient(); - -// POST JSON 请求 -String result = client.post(url, headers, jsonObject); - -// POST 表单请求 -String result = client.post(url, headers, formData); - -// GET 请求 -String result = client.get(url, headers, uriVariables); -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter | `TrustAnyHttpClientFactory`、`RestTemplateContext`、`HttpClient`、`HttpRequest` | diff --git a/docs/capabilities/index.md b/docs/capabilities/index.md index 3af101158..236420d95 100644 --- a/docs/capabilities/index.md +++ b/docs/capabilities/index.md @@ -6,37 +6,29 @@ | 名称 | 描述 | 范围 | 来源 | |------|------|------|------| -| [apache-groovy](./apache-groovy.md) | 动态语言运行时,支持脚本编译与执行 | 后端 | 框架:Apache Groovy | -| [bouncycastle](./bouncycastle.md) | 密码学算法库,提供 AES/RSA 等加密算法支持 | 后端 | 框架:BouncyCastle | -| [byte-buddy](./byte-buddy.md) | 字节码操作库,用于运行时动态生成 JPA 实体类 | 后端 | 框架:Byte Buddy | -| [command-executor](./command-executor.md) | CQRS 命令执行器接口,定义标准化的命令-响应契约,支持带参和无参两种执行模式 | 后端 | 项目自有 | -| [commons-crypto](./commons-crypto.md) | Apache 加密工具封装,提供 AES 等加密操作 | 后端 | 框架:Apache Commons Crypto | -| [commons-io](./commons-io.md) | IO 工具类库,简化文件/流操作 | 后端 | 框架:Apache Commons IO | -| [commons-text](./commons-text.md) | 文本处理工具库,提供字符串命名转换等功能 | 后端 | 框架:Apache Commons Text | -| [data-authorization](./data-authorization.md) | 基于 JDBC 代理链的透明数据权限过滤,支持行级和列级权限控制 | 后端 | 项目自有 | -| [domain-event](./domain-event.md) | 领域实体事件自动追踪机制,支持创建、变更、删除事件的自动推送与字段级变更检测 | 后端 | 项目自有 | -| [event-system](./event-system.md) | 基于发布-订阅模式的事件系统,支持同步/异步事件处理、事件排序和异常隔离 | 后端 | 项目自有 | -| [exception-handling](./exception-handling.md) | 国际化异常体系与事件异常处理,支持多语言错误消息、占位符参数和事件循环检测 | 后端 | 项目自有 | -| [fast-repository](./fast-repository.md) | JPA Repository 增强,支持基于 PageRequest 的动态过滤查询、HQL 构建和 SearchRequest 自动解析 | 后端 | 项目自有 | -| [fastjson](./fastjson.md) | 高性能 JSON 序列化/反序列化库 | 后端 | 框架:Fastjson | -| [groovy-script-engine](./groovy-script-engine.md) | Groovy 脚本运行时编译、缓存与热更新引擎,提供 REST API 管理脚本生命周期 | 后端 | 项目自有 | -| [http-client-factory](./http-client-factory.md) | 信任所有证书的 Apache HttpClient 5 工厂,用于内部 HTTPS 调用跳过证书验证 | 后端 | 项目自有 | -| [httpclient5](./httpclient5.md) | HTTP 客户端库,支持 HTTPS 信任所有证书的请求 | 后端 | 框架:Apache HttpClient 5 | -| [jackson-databind](./jackson-databind.md) | JSON 数据绑定库,Spring Boot 默认 JSON 处理器 | 后端 | 框架:Jackson Databind | -| [jjwt](./jjwt.md) | JWT 令牌生成与解析库 | 后端 | 框架:JJWT | -| [jsqlparser](./jsqlparser.md) | SQL 解析与改写库,用于数据权限 SQL 注入 | 后端 | 框架:JSqlParser | -| [kryo](./kryo.md) | 高性能 Java 序列化框架,用于数据快照 | 后端 | 框架:Kryo | -| [page-request-filter](./page-request-filter.md) | 动态查询过滤请求封装,支持多种比较关系、组合条件、HTTP 参数自动解析 | 后端 | 项目自有 | -| [rest-template-context](./rest-template-context.md) | 预配置的 RestTemplate 单例上下文,内置信任所有证书的 HttpClient,简化内部服务间 HTTP 调用 | 后端 | 项目自有 | -| [security-auth](./security-auth.md) | 基于 Spring Security 的 JWT/Redis 双模式认证框架,支持 Token 自动续期与登录扩展 | 后端 | 项目自有 | -| [spring-data-jpa](./spring-data-jpa.md) | ORM 映射、Repository 抽象、自动分页 | 后端 | 框架:Spring Data JPA | -| [spring-data-redis](./spring-data-redis.md) | Redis 客户端抽象,支持缓存和有状态认证 Token 存储 | 后端 | 框架:Spring Data Redis | -| [spring-ioc-container](./spring-ioc-container.md) | Spring IoC 容器提供依赖注入、Bean 生命周期管理、AOP 支持 | 后端 | 框架:Spring Framework | -| [spring-security](./spring-security.md) | 认证授权框架,提供 Filter Chain、CSRF 防护 | 后端 | 框架:Spring Security | -| [spring-web-mvc](./spring-web-mvc.md) | Web MVC 框架,提供 RESTful API 路由、请求参数绑定、响应序列化 | 后端 | 框架:Spring Web MVC | -| [unified-response](./unified-response.md) | 统一 API 响应封装,提供 Response、SingleResponse、MultiResponse、MapResponse 四种标准响应格式 | 后端 | 项目自有 | -| [workflow-engine](./workflow-engine.md) | 内置工作流引擎,支持流程定义、节点流转、审批、退回、会签、抄送、委托与数据快照 | 后端 | 项目自有 | +| [domain-change-interceptor](./domain-change-interceptor.md) | 通过 CGLIB 代理拦截领域实体字段的 setter 方法,自动检测变更并发布 DomainChangeEvent | 后端 | 项目自有 | +| [event-system](./event-system.md) | 自建的领域事件发布-订阅系统,支持同步/异步事件、事务后提交、Handler自动注册排序 | 后端 | 项目自有 | +| [fast-repository](./fast-repository.md) | JPA Repository 增强,支持动态过滤查询(RequestFilter + Relation 操作符)、HQL 自动构建、排序 | 后端 | 项目自有 | +| [groovy-script-engine](./groovy-script-engine.md) | Groovy 动态脚本引擎,支持运行时编译执行、LRU 缓存、热更新和 REST API | 后端 | 项目自有 | +| [jackson](./jackson.md) | Jackson JSON 序列化/反序列化能力,由 Spring Boot Web 自动配置,框架中用于 REST API 响应序列化、请求参数绑定等场景 | 后端 | 框架:jackson | +| [jdbc-proxy-chain](./jdbc-proxy-chain.md) | 完整的 JDBC 装饰器代理链,透明拦截 SQL 执行以实现数据权限控制 | 后端 | 项目自有 | +| [locale-exception](./locale-exception.md) | 支持 i18n 的业务异常体系,errCode + errMessage,通过 MessageSource 实现国际化 | 后端 | 项目自有 | +| [spring-data-jpa](./spring-data-jpa.md) | Spring Data JPA 数据访问能力,提供 ORM 映射、Repository 抽象、分页查询等基础设施,框架在此基础上扩展了 FastRepos... | 后端 | 框架:spring-data-jpa | +| [spring-data-redis](./spring-data-redis.md) | Spring Data Redis 数据存储能力,框架中主要用于 Token/Session 的有状态存储,支持服务端主动失效和多端登录管理 | 后端 | 框架:spring-data-redis | +| [spring-framework](./spring-framework.md) | Spring Framework / Spring Boot 核心基础能力,提供 IoC 容器、事件发布、AOP、Web MVC、事务管理等基础设施 | 后端 | 框架:spring-framework | +| [spring-security](./spring-security.md) | Spring Security 安全认证与授权能力,框架在此基础上封装了 JWT 无状态认证和 Redis 有状态认证两种模式 | 后端 | 框架:spring-security | +| [sql-interceptor](./sql-interceptor.md) | 基于 JSqlParser 的 SQL 解析改写拦截器,在 SQL 执行前透明注入数据权限条件 | 后端 | 项目自有 | +| [token-gateway](./token-gateway.md) | JWT/Redis 双模式 Token 认证网关,统一创建、解析和管理认证令牌 | 后端 | 项目自有 | +| [transaction-manager-context](./transaction-manager-context.md) | 单例编程式事务上下文,支持 REQUIRES_NEW 语义,在非 Spring 管理上下文中也能使用事务 | 后端 | 项目自有 | +| [trigger-system](./trigger-system.md) | 触发器框架,支持定时/条件触发,提供统一的触发上下文管理 | 后端 | 项目自有 | +| [workflow-engine](./workflow-engine.md) | 工作流引擎,支持流程定义、节点流转、审批、委托、会签、数据快照和事件通知 | 后端 | 项目自有 | + +## 🗓️ 计划中 + +| 名称 | 描述 | 范围 | 来源 | +|------|------|------|------| +| [flow-postpone-event](./flow-postpone-event.md) | 延期流程操作的事件通知 — 当前 FlowPostponedService 未发送 FlowApprovalEvent | 后端 | 计划 | --- -**统计**: 共 30 篇 — 已实现 30 / 计划中 0 / 已废弃 0 +**统计**: 共 17 篇 — 已实现 16 / 计划中 1 / 已废弃 0 diff --git a/docs/capabilities/jackson-databind.md b/docs/capabilities/jackson-databind.md deleted file mode 100644 index 5992e9b9a..000000000 --- a/docs/capabilities/jackson-databind.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: jackson-databind -description: JSON 数据绑定库,Spring Boot 默认 JSON 处理器 -status: 已实现 -scope: 后端 -source: 框架:Jackson Databind ---- - -## 解决什么问题 - -作为 Spring Boot 默认的 JSON 处理库,Jackson Databind 在框架中承担: -- Controller 层请求体(`@RequestBody`)的自动反序列化 -- Controller 层响应体的自动序列化 -- 自定义枚举序列化(将 `IEnum` 接口实现类序列化为 code 值而非名称) -- 与 Spring MVC 的消息转换器(`MappingJackson2HttpMessageConverter`)集成 - -虽然框架内部使用 Fastjson 做轻量级序列化,但 Jackson 仍然是 Spring MVC 层面的默认 JSON 处理器,负责所有 REST API 的请求/响应转换。 - -## 如何使用 - -### 依赖引入 - -```xml - - - org.springframework.boot - spring-boot-starter-web - -``` - -### 核心用法 - -#### 自定义枚举序列化器 - -框架定义了 `EnumSerializer`,将实现 `IEnum` 接口的枚举序列化为数字 code 值: - -```java -// EnumSerializer.java -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -public class EnumSerializer extends JsonSerializer { - - @Override - public void serialize(IEnum iEnum, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - // 将枚举序列化为 code 数字,而非枚举名称字符串 - jsonGenerator.writeNumber(iEnum.getCode()); - } -} -``` - -## 使用实例 - -### 1. IEnum 接口定义 - -```java -// IEnum.java - 枚举编码接口 -public interface IEnum { - int getCode(); -} - -// 使用示例 -public enum ApprovalType implements IEnum { - APPROVE(1), - REJECT(2), - WITHDRAW(3); - - private final int code; - ApprovalType(int code) { this.code = code; } - - @Override - public int getCode() { return code; } -} -``` - -### 2. 配置 Jackson 使用自定义序列化器 - -```java -// 在 Jackson 配置中注册 EnumSerializer -@Configuration -public class JacksonConfiguration { - - @Bean - public ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(IEnum.class, new EnumSerializer()); - mapper.registerModule(module); - return mapper; - } -} -``` - -### 3. Controller 中的自动序列化 - -```java -// 当 Controller 返回包含 IEnum 字段的对象时, -// Jackson 自动使用 EnumSerializer 将枚举序列化为数字 -@GetMapping("/flow/nodes") -public MultiResponse getNodes() { - // FlowNodeDTO.approvalType 字段如果实现了 IEnum, - // 会被序列化为 {"approvalType": 1} 而非 {"approvalType": "APPROVE"} - return Response.buildSuccess(nodeService.getNodes()); -} -``` - -### 4. 与 Fastjson 的分工 - -| 场景 | 使用的库 | 说明 | -|------|----------|------| -| Spring MVC 请求/响应 | Jackson | `@RequestBody` / 返回值自动序列化 | -| 框架内部 DTO 序列化 | Fastjson | `JsonSerializable.toJson()` | -| Token JWT payload | Fastjson | `JSONObject.toJSONString(token)` | -| 枚举 code 序列化 | Jackson | `EnumSerializer` 自定义序列化器 | -| Security Filter JSON 响应 | Fastjson | 手动构建 JSONObject | - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter | `EnumSerializer` 自定义枚举序列化 | -| 所有 Web 模块 | Spring MVC 默认 JSON 消息转换 | diff --git a/docs/capabilities/jackson.md b/docs/capabilities/jackson.md new file mode 100644 index 000000000..5170e6ccc --- /dev/null +++ b/docs/capabilities/jackson.md @@ -0,0 +1,122 @@ +--- +name: jackson +description: Jackson JSON 序列化/反序列化能力,由 Spring Boot Web 自动配置,框架中用于 REST API 响应序列化、请求参数绑定等场景 +status: 已实现 +scope: 后端 +source: 框架:jackson +framework_version: Managed by Spring Boot BOM (3.3.5) +--- + +## 解决什么问题 + +Jackson 是 Spring Boot Web 默认的 JSON 处理库,在框架中承担以下职责: + +- **REST API 响应序列化**:所有 Controller 返回的 `Response` / `SingleResponse` / `MultiResponse` / `MapResponse` 对象均通过 Jackson 自动序列化为 JSON 输出 +- **请求参数反序列化**:`@RequestBody` 注解的请求体自动由 Jackson 反序列化为 Java 对象,包括 `PageRequest` 中的过滤条件 +- **枚举序列化**:框架提供了自定义 `EnumSerializer`,统一枚举值的 JSON 输出格式 +- **日期/时间格式化**:通过 Spring Boot 的 `spring.jackson.*` 配置项统一日期格式 +- **字段命名策略**:支持驼峰/下划线等命名转换,前后端字段名对齐 + +> **注意**:框架内部部分场景(如 Token 序列化、事件数据传输)使用了 Fastjson(`com.alibaba.fastjson.JSONObject`)作为补充,通过 `JsonSerializable` 接口封装。Jackson 主要负责 HTTP 层的 JSON 处理。 + +## 如何使用 + +### 依赖引入 + +Jackson 由 `spring-boot-starter-web` 自动引入,无需单独声明: + +```xml + + + com.codingapi.springboot + springboot-starter + +``` + +### 全局配置 + +在 `application.properties` 中统一配置 Jackson 行为: + +```properties +# 日期格式 +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=GMT+8 + +# 序列化选项 +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.write-dates-as-timestamps=false + +# 反序列化选项 +spring.jackson.deserialization.fail-on-unknown-properties=false +``` + +### 自定义序列化器 + +框架已提供 `EnumSerializer`,可按需注册更多自定义序列化器: + +```java +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addSerializer(MyEnum.class, new EnumSerializer()); + mapper.registerModule(module); + return mapper; + } +} +``` + +## 使用实例 + +### REST API 响应(自动序列化) + +```java +@RestController +@RequestMapping("/api/users") +@AllArgsConstructor +public class UserController { + + private final UserQueryService userQueryService; + + // Jackson 自动将 SingleResponse 序列化为 JSON + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + return SingleResponse.of(userQueryService.findById(id)); + } + + // 分页响应包含 total、content 等字段 + @GetMapping + public MultiResponse list(PageRequest request) { + Page page = userQueryService.findAll(request); + return MultiResponse.of(page.getContent(), page.getTotalElements()); + } +} +``` + +输出示例: + +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": [ + { "id": 1, "name": "张三", "age": 28 } + ], + "total": 1 +} +``` + +### 请求体反序列化 + +```java +@PostMapping +public Response createUser(@RequestBody CreateUserCmd cmd) { + // Jackson 自动将 JSON 请求体反序列化为 CreateUserCmd + userService.create(cmd); + return Response.buildSuccess(); +} +``` diff --git a/docs/capabilities/jdbc-proxy-chain.md b/docs/capabilities/jdbc-proxy-chain.md new file mode 100644 index 000000000..9c21d2f7a --- /dev/null +++ b/docs/capabilities/jdbc-proxy-chain.md @@ -0,0 +1,95 @@ +--- +name: jdbc-proxy-chain +description: 完整的 JDBC 装饰器代理链,透明拦截 SQL 执行以实现数据权限控制 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 67895fda +code_files: + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/ConnectionProxy.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/StatementProxy.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/PreparedStatementProxy.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/CallableStatementProxy.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/ResultSetProxy.java +--- + +## 解决什么问题 + +数据权限要求在不修改业务 SQL 的前提下,透明地注入行级/列级过滤条件。手动在每个 Repository 中拼接权限条件会导致: +- 业务代码与权限逻辑强耦合 +- 遗漏某个查询导致数据泄露 +- 难以统一管理和审计权限规则 + +JDBC 代理链通过装饰器模式包装整个 JDBC 调用链,在 SQL 执行前自动拦截和改写,对业务代码完全透明。 + +## 如何使用 + +### 代理链架构 + +``` +ConnectionProxy(包装 DataSource 返回的 Connection) + │ + ├── createStatement() → StatementProxy + │ └── executeQuery(sql) → SQLRunningContext.intercept(sql) → ResultSetProxy + │ + ├── prepareStatement(sql) → SQLRunningContext.intercept(sql) → PreparedStatementProxy + │ └── executeQuery() → ResultSetProxy + │ + └── prepareCall(sql) → SQLRunningContext.intercept(sql) → CallableStatementProxy + └── executeQuery() → ResultSetProxy +``` + +### 各代理职责 + +| 代理类 | 职责 | +|--------|------| +| `ConnectionProxy` | 拦截 `prepareStatement`/`prepareCall`,在 SQL 创建时调用 `SQLRunningContext.intercept()` | +| `StatementProxy` | 拦截 `executeQuery`/`executeUpdate`,动态改写 SQL 并包装返回值 | +| `PreparedStatementProxy` | 包装已拦截的 PreparedStatement,对返回的 ResultSet 应用列级权限 | +| `CallableStatementProxy` | 委托底层 CallableStatement,应用授权上下文 | +| `ResultSetProxy` | 拦截列级数据读取,通过 `ColumnHandlerContext` 对字段值进行脱敏或过滤 | + +### 拦截流程 + +1. `ConnectionProxy.prepareStatement(sql)` 调用 `SQLRunningContext.getInstance().intercept(sql)` +2. `SQLRunningContext` 返回 `SQLExecuteState`(包含改写后的 SQL 和拦截状态) +3. 改写后的 SQL 传给底层 `Connection.prepareStatement()` +4. 返回的 `ResultSet` 被 `ResultSetProxy` 包装 +5. 读取每列时,`ColumnHandlerContext` 根据表名和列名决定是否脱敏 + +### 自动装配 + +代理链通过 `DataAuthorizationConfiguration` 自动注入到 DataSource 中,业务代码无需感知: + +```java +// 框架自动将 DataSource.getConnection() 替换为 ConnectionProxy +// 所有 JPA/JDBC 查询自动走代理链 +``` + +## 使用实例 + +```java +// 自定义 SQL 拦截器 — 为特定表注入租户条件 +public class TenantSQLInterceptor implements SQLInterceptor { + @Override + public SQLExecuteState intercept(String sql) { + if (sql.contains("FROM orders")) { + String tenantId = UserContext.getCurrentUser().getTenantId(); + String modifiedSql = sql + " AND tenant_id = '" + tenantId + "'"; + return new SQLExecuteState(modifiedSql, true); + } + return new SQLExecuteState(sql, false); + } +} + +// 自定义列级处理器 — 对手机号脱敏 +public class PhoneColumnHandler implements ColumnHandler { + @Override + public String handle(String tableName, String columnName, String value) { + if ("user".equals(tableName) && "phone".equals(columnName)) { + return value.substring(0, 3) + "****" + value.substring(7); + } + return value; + } +} +``` diff --git a/docs/capabilities/jjwt.md b/docs/capabilities/jjwt.md deleted file mode 100644 index 8cad670f0..000000000 --- a/docs/capabilities/jjwt.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -name: jjwt -description: JWT 令牌生成与解析库 -status: 已实现 -scope: 后端 -source: 框架:JJWT ---- - -## 解决什么问题 - -在安全认证模块中,需要实现无状态的 JWT(JSON Web Token)认证机制: -- 用户登录后生成签名的 JWT Token -- 后续请求中验证 Token 签名和有效期 -- 将用户信息(用户名、权限、扩展数据)嵌入 Token payload - -JJWT 提供了类型安全的 JWT 构建器和解析器 API,支持 HMAC-SHA 签名算法。 - -## 如何使用 - -### 依赖引入 - -```xml - - - io.jsonwebtoken - jjwt-api - 0.12.6 - - - io.jsonwebtoken - jjwt-impl - 0.12.6 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.12.6 - runtime - -``` - -### 配置项 - -```properties -# application.properties -codingapi.security.jwt.enable=true -codingapi.security.jwt.secret-key=your-secret-key-at-least-256-bits -codingapi.security.jwt.valid-time=3600000 -codingapi.security.jwt.rest-time=1800000 -``` - -### 核心类 - -- `JwtTokenGateway` — JWT Token 的创建与解析核心逻辑 -- `JWTTokenGatewayImpl` — 实现 `TokenGateway` 接口的适配器 -- `JWTSecurityConfiguration` — Spring Boot 自动配置类 - -## 使用实例 - -### 1. 创建 JWT Token - -```java -// JwtTokenGateway.java -public class JwtTokenGateway { - private final SecretKey key; - private final int validTime; - private final int restTime; - - public JwtTokenGateway(SecurityJWTProperties properties) { - // 使用 HMAC-SHA 密钥 - this.key = Keys.hmacShaKeyFor( - properties.getSecretKey().getBytes(StandardCharsets.UTF_8) - ); - this.validTime = properties.getValidTime(); - this.restTime = properties.getRestTime(); - } - - public Token create(String username, String iv, List authorities, String extra) { - Token token = new Token(username, iv, extra, authorities, validTime, restTime); - // 将 Token 对象序列化为 JSON 作为 JWT subject - String jwt = Jwts.builder() - .subject(token.toJson()) - .signWith(key) - .compact(); - token.setToken(jwt); - return token; - } -} -``` - -### 2. 解析 JWT Token - -```java -// JwtTokenGateway.java -public Token parser(String sign) { - try { - // 验证签名并解析 Claims - Jws jws = Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(sign); - if (jws != null) { - String subject = jws.getPayload().getSubject(); - return JSONObject.parseObject(subject, Token.class); - } - throw new LocaleMessageException("token.error", "token失效,请重新登录."); - } catch (Exception exp) { - throw new LocaleMessageException("token.error", exp.getMessage()); - } -} -``` - -### 3. 自动配置注册 - -```java -// JWTSecurityConfiguration.java -@Configuration -@ConditionalOnProperty(prefix = "codingapi.security.jwt", name = "enable", havingValue = "true") -public class JWTSecurityConfiguration { - - @Bean - @ConfigurationProperties(prefix = "codingapi.security.jwt") - public SecurityJWTProperties securityJWTProperties() { - return new SecurityJWTProperties(); - } - - @Bean - @ConditionalOnMissingBean - public JwtTokenGateway jwtTokenGateway(SecurityJWTProperties properties) { - return new JwtTokenGateway(properties); - } - - @Bean - @ConditionalOnMissingBean - public TokenGateway jwtTokenGatewayImpl(JwtTokenGateway jwtTokenGateway) { - return new JWTTokenGatewayImpl(jwtTokenGateway); - } -} -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter-security | JWT Token 生成/解析、自动配置、TokenGateway 实现 | diff --git a/docs/capabilities/jsqlparser.md b/docs/capabilities/jsqlparser.md deleted file mode 100644 index 73040ad3c..000000000 --- a/docs/capabilities/jsqlparser.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: jsqlparser -description: SQL 解析与改写库,用于数据权限 SQL 注入 -status: 已实现 -scope: 后端 -source: 框架:JSqlParser ---- - -## 解决什么问题 - -JSqlParser 为项目的数据权限模块提供 SQL 解析和改写能力,解决了以下问题: - -- **SQL 解析**:将原始 SQL 字符串解析为结构化的 AST(抽象语法树),理解表名、字段、WHERE 条件等 -- **SQL 改写**:在 SELECT 语句中自动注入数据权限过滤条件,实现透明的行级数据隔离 -- **零侵入**:业务代码无需修改,权限条件通过 JDBC 代理层自动注入 - -## 如何使用 - -项目使用 JSqlParser 5.0,仅在 `springboot-starter-data-authorization` 模块中使用: - -1. **SQL 解析**:通过 `CCJSqlParserUtil.parse(sql)` 将 SQL 解析为 `Statement` 对象 -2. **条件注入**:`DataPermissionSQLEnhancer` 遍历 SELECT 语句,根据 `RowHandler` 定义的权限规则向 WHERE 子句追加条件 -3. **代理链拦截**:`ConnectionProxy` → `PreparedStatementProxy` / `StatementProxy` 在 SQL 执行前调用 `SQLRunningContext.intercept(sql)` 完成改写 -4. **跳过拦截**:通过 `SQLRunningContext.skipDataAuthorization()` 可临时跳过权限过滤 - -## 使用实例 - -### DataPermissionSQLEnhancer 核心逻辑 - -```java -public class DataPermissionSQLEnhancer { - - public DataPermissionSQLEnhancer(String sql, RowHandler rowHandler) throws SQLException { - this.sql = sql.replaceAll("\\?", " ? "); - this.rowHandler = rowHandler; - // 使用 JSqlParser 解析 SQL - this.statement = CCJSqlParserUtil.parse(this.sql); - this.tableColumnAliasHolder = new TableColumnAliasHolder(statement); - } - - public String getNewSQL() throws SQLException { - if (statement instanceof Select) { - tableColumnAliasHolder.holderAlias(); - Select select = (Select) statement; - this.deepMatch(select); // 递归注入权限条件 - return statement.toString(); // 返回改写后的 SQL - } - return sql; - } -} -``` - -### ConnectionProxy 中的拦截点 - -```java -public class ConnectionProxy implements Connection { - - @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { - // 在创建 PreparedStatement 前拦截并改写 SQL - this.interceptState = SQLRunningContext.getInstance().intercept(sql); - return new PreparedStatementProxy( - connection.prepareStatement(interceptState.getSql()), - interceptState - ); - } -} -``` - -### 临时跳过数据权限 - -```java -// 某些管理操作需要查询全量数据,跳过权限过滤 -Object result = SQLRunningContext.getInstance() - .skipDataAuthorization(() -> { - return userRepository.findAll(); - }); -``` diff --git a/docs/capabilities/kryo.md b/docs/capabilities/kryo.md deleted file mode 100644 index 7d5133d17..000000000 --- a/docs/capabilities/kryo.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: kryo -description: 高性能 Java 序列化框架,用于数据快照 -status: 已实现 -scope: 后端 -source: 框架:Kryo ---- - -## 解决什么问题 - -Kryo 为项目的工作流模块提供高性能序列化能力,解决了以下问题: - -- **流程快照**:将完整的流程定义(FlowWork)序列化为字节数组存储,支持版本回溯 -- **存储优化**:相比 Java 原生序列化,Kryo 生成的字节流更小、速度更快,降低数据库存储压力 -- **版本去重**:自动判断是否存在相同版本的流程快照,避免重复存储 - -## 如何使用 - -项目使用 Kryo 5.6.2,仅在 `springboot-starter-flow` 模块的 `FlowWorkSerializable` 类中使用: - -1. **序列化**:`FlowWorkSerializable.toSerializable()` 将流程对象转为 `byte[]` -2. **反序列化**:`FlowWorkSerializable.fromSerializable(byte[])` 从字节数组恢复流程对象 -3. **类型注册**:需要预先注册所有参与序列化的类(包括枚举、嵌套对象等) -4. **与 FlowBackup 集成**:`FlowBackup` 持有序列化后的字节数组,通过 `resume()` 方法恢复完整流程 - -## 使用实例 - -### 流程序列化 - -```java -public class FlowWorkSerializable implements Serializable { - - public byte[] toSerializable() { - Kryo kryo = new Kryo(); - // 注册所有需要序列化的类型 - kryo.register(ArrayList.class); - kryo.register(FlowNodeSerializable.class); - kryo.register(FlowRelationSerializable.class); - kryo.register(FlowWorkSerializable.class); - kryo.register(ApprovalType.class); - kryo.register(NodeType.class); - kryo.register(FlowButton.class); - kryo.register(FlowButtonType.class); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Output output = new Output(outputStream); - kryo.writeObject(output, this); - output.close(); - return outputStream.toByteArray(); - } -} -``` - -### 流程反序列化 - -```java -public static FlowWorkSerializable fromSerializable(byte[] bytes) { - Kryo kryo = new Kryo(); - kryo.register(ArrayList.class); - kryo.register(FlowNodeSerializable.class); - kryo.register(FlowRelationSerializable.class); - kryo.register(FlowWorkSerializable.class); - kryo.register(ApprovalType.class); - kryo.register(NodeType.class); - kryo.register(FlowButton.class); - kryo.register(FlowButtonType.class); - return kryo.readObject(new Input(bytes), FlowWorkSerializable.class); -} -``` - -### FlowBackup 中的使用 - -```java -// 创建快照:将流程定义序列化后存储 -public FlowBackup(FlowWork flowWork) { - this.bytes = flowWork.toSerializable().toSerializable(); // Kryo 序列化 - this.workVersion = flowWork.getUpdateTime(); - this.workId = flowWork.getId(); - this.createTime = System.currentTimeMillis(); -} - -// 恢复快照:从字节数组反序列化并重建流程对象 -public FlowWork resume(FlowOperatorRepository flowOperatorRepository) { - return FlowWorkSerializable.fromSerializable(this.bytes) - .toFlowWork(flowOperatorRepository); -} -``` diff --git a/docs/capabilities/locale-exception.md b/docs/capabilities/locale-exception.md new file mode 100644 index 000000000..ee843ff5d --- /dev/null +++ b/docs/capabilities/locale-exception.md @@ -0,0 +1,156 @@ +--- +name: locale-exception +description: 支持 i18n 的业务异常体系,errCode + errMessage,通过 MessageSource 实现国际化 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 0c4299a1 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/LocaleMessageException.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/LocaleMessage.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/MessageContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/ExceptionConfiguration.java +--- + +## 解决什么问题 + +在企业级应用中,业务异常需要同时满足以下需求: + +1. **结构化错误信息**:异常需携带机器可读的错误码(errCode)和人类可读的错误消息(errMessage),便于前端根据 errCode 做差异化处理 +2. **国际化支持**:同一错误码在不同语言环境下返回对应语言的错误消息,而非硬编码中文字符串 +3. **参数化消息**:错误消息支持占位符(如 `{0}`、`{1}`),运行时动态填充具体值 +4. **与统一响应对接**:异常的 errCode/errMessage 可直接映射到框架的 `Response` DTO,由全局异常处理器统一返回 + +本能力基于 Spring `MessageSource` 构建了国际化异常体系,开发者只需在 `messages.properties` 中定义错误码与消息的映射关系,代码中通过错误码抛出异常即可自动解析当前 Locale 对应的消息。 + +## 如何使用 + +### 核心组件 + +#### LocaleMessageException — 国际化业务异常 + +```java +public class LocaleMessageException extends RuntimeException { + String getErrCode() // 错误码 + String getErrMessage() // 解析后的错误消息(当前 Locale) +} +``` + +提供多种构造方式: + +| 构造函数 | 说明 | +|---------|------| +| `LocaleMessageException(errCode)` | 仅错误码,从 MessageSource 解析消息 | +| `LocaleMessageException(errCode, args)` | 错误码 + 占位符参数 | +| `LocaleMessageException(errCode, errMessage)` | 错误码 + 自定义消息(不走 i18n) | +| `LocaleMessageException(errCode, args, cause)` | 错误码 + 参数 + 原始异常 | +| `LocaleMessageException.of(errCode, args...)` | 静态工厂方法(推荐) | + +#### MessageContext — 消息上下文(单例) + +内部持有 `LocaleMessage` 实例,提供 `getErrorMsg(errCode)` 和 `getErrorMsg(errCode, args)` 方法。`LocaleMessageException` 的无消息构造函数通过此上下文获取国际化消息。 + +#### LocaleMessage — 消息解析器 + +封装 Spring `MessageSource`,根据 `LocaleContextHolder.getLocale()` 自动获取当前请求的语言环境并解析消息。 + +### 自动配置 + +`ExceptionConfiguration` 自动注册 `LocaleMessage` Bean: + +```java +@Configuration +public class ExceptionConfiguration { + @Bean(initMethod = "init") + public LocaleMessage exceptionLocaleMessage(MessageSource messageSource) { + return new LocaleMessage(messageSource); + } +} +``` + +`init()` 方法将 `LocaleMessage` 注入到 `MessageContext` 单例中。只要 Spring 容器中存在 `MessageSource`(通常由 `messages.properties` 自动配置),即可直接使用。 + +### 消息资源文件 + +在 `src/main/resources/` 下创建消息资源文件: + +```properties +# messages.properties (默认/中文) +user.not.found=用户不存在 +order.amount.exceeded=订单金额超出限制,最大金额为 {0} 元 +account.locked=账户 {0} 已被锁定,请联系管理员 + +# messages_en.properties (英文) +user.not.found=User not found +order.amount.exceeded=Order amount exceeded, maximum is {0} yuan +account.locked=Account {0} has been locked, please contact administrator +``` + +## 使用实例 + +### 1. 基本用法 — 仅错误码 + +```java +// 从 messages.properties 中解析 "user.not.found" 对应的消息 +throw new LocaleMessageException("user.not.found"); +// 中文环境 → errMessage: "用户不存在" +// 英文环境 → errMessage: "User not found" +``` + +### 2. 带参数的消息 + +```java +// 使用静态工厂方法(推荐) +throw LocaleMessageException.of("order.amount.exceeded", 10000); +// → errMessage: "订单金额超出限制,最大金额为 10000 元" + +// 或使用构造函数 +throw new LocaleMessageException("account.locked", new Object[]{"admin@example.com"}); +// → errMessage: "账户 admin@example.com 已被锁定,请联系管理员" +``` + +### 3. 携带原始异常 + +```java +try { + userRepository.findById(userId); +} catch (DataAccessException e) { + throw new LocaleMessageException("user.not.found", e); +} +``` + +### 4. 自定义消息(跳过 i18n) + +```java +// 直接指定消息文本,不从 MessageSource 解析 +throw new LocaleMessageException("custom.error", "这是一条自定义错误消息"); +``` + +### 5. 与全局异常处理器配合 + +在全局异常处理器中捕获 `LocaleMessageException`,提取 errCode 和 errMessage 构建统一响应: + +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(LocaleMessageException.class) + public Response handleLocaleException(LocaleMessageException e) { + return Response.buildFailure(e.getErrCode(), e.getErrMessage()); + } +} +``` + +返回的 JSON 响应: + +```json +{ + "success": false, + "errCode": "order.amount.exceeded", + "errMessage": "订单金额超出限制,最大金额为 10000 元" +} +``` + +### 6. Locale 切换 + +框架通过 `LocaleContextHolder` 获取当前 Locale,与 Spring MVC 的 `Accept-Language` 请求头或自定义 LocaleResolver 集成。无需额外配置,客户端发送不同的 `Accept-Language` 头即可获得对应语言的错误消息。 diff --git a/docs/capabilities/page-request-filter.md b/docs/capabilities/page-request-filter.md deleted file mode 100644 index 2aa4e788e..000000000 --- a/docs/capabilities/page-request-filter.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -name: page-request-filter -description: 动态查询过滤请求封装,支持多种比较关系、组合条件、HTTP 参数自动解析 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在企业级应用的列表查询场景中,前端通常需要传递多个筛选条件。传统做法存在以下问题: - -1. **Controller 参数爆炸**:每个查询字段都需要一个 `@RequestParam`,方法签名冗长 -2. **条件拼接复杂**:需要手动构建 JPA Specification 或 HQL,代码重复度高 -3. **前后端协议不统一**:不同项目的分页参数命名(page/pageNo/current)和过滤格式各异 -4. **动态条件难维护**:新增查询字段需要同时修改 Controller、Service、Repository 三层 -5. **类型转换繁琐**:字符串参数需要手动转为对应的实体字段类型 - -框架通过 `PageRequest` + `RequestFilter` + `Relation` 三件套提供统一的动态查询抽象。`SearchRequest` 进一步封装了 HTTP 请求参数的自动解析,支持 Base64 编码的 filter/sort/params JSON 参数,实现前端零配置对接。 - -## 如何使用 - -### 1. PageRequest — 编程式构建查询条件 - -```java -// 基础分页 -PageRequest request = PageRequest.of(0, 20); - -// 添加等值过滤 -request.addFilter("name", "张三"); - -// 添加指定关系的过滤 -request.addFilter("age", Relation.GREATER_THAN, 18); -request.addFilter("createTime", Relation.BETWEEN, startDate, endDate); -request.addFilter("status", Relation.IN, 1, 2, 3); - -// 添加排序 -request.addSort(Sort.by("createTime").descending()); - -// 链式调用 -PageRequest req = PageRequest.of(0, 20) - .addFilter("name", Relation.LIKE, "张") - .addFilter("age", Relation.GREATER_THAN_EQUAL, 18); -``` - -### 2. Relation — 支持的比较关系 - -| 枚举值 | SQL 等价 | 说明 | -|--------|----------|------| -| `EQUAL` | `= value` | 等值匹配(默认) | -| `NOT_EQUAL` | `!= value` | 不等 | -| `LIKE` | `LIKE %value%` | 模糊匹配 | -| `LEFT_LIKE` | `LIKE %value` | 左模糊 | -| `RIGHT_LIKE` | `LIKE value%` | 右模糊 | -| `BETWEEN` | `BETWEEN v1 AND v2` | 区间 | -| `IN` | `IN (v1, v2, ...)` | 包含 | -| `NOT_IN` | `NOT IN (v1, v2, ...)` | 不包含 | -| `IS_NULL` | `IS NULL` | 为空 | -| `IS_NOT_NULL` | `IS NOT NULL` | 非空 | -| `GREATER_THAN` | `> value` | 大于 | -| `LESS_THAN` | `< value` | 小于 | -| `GREATER_THAN_EQUAL` | `>= value` | 大于等于 | -| `LESS_THAN_EQUAL` | `<= value` | 小于等于 | - -### 3. Filter — 组合条件 - -使用 `Filter.and()` 和 `Filter.or()` 构建复合条件: - -```java -// AND 组合:name LIKE '%张%' AND age > 18 -request.andFilter( - Filter.as("name", Relation.LIKE, "张"), - Filter.as("age", Relation.GREATER_THAN, 18) -); - -// OR 组合:status = 1 OR status = 2 -request.orFilters( - Filter.as("status", Relation.EQUAL, 1), - Filter.as("status", Relation.EQUAL, 2) -); -``` - -### 4. SearchRequest — HTTP 参数自动解析 - -在 Controller 中直接使用 `SearchRequest`,自动从 HTTP 请求中提取分页和过滤参数: - -```java -@GetMapping("/users") -public MultiResponse list(SearchRequest searchRequest) { - // 传入实体类用于类型推断 - PageRequest pageRequest = searchRequest.toPageRequest(User.class); - Page page = userRepository.pageRequest(pageRequest); - return MultiResponse.of(page); -} -``` - -**前端传参方式:** - -- 直接参数:`?current=0&pageSize=20&name=张三&age=18` -- Base64 编码 filter:`?filter=eyJuYW1lIjpbIuW8oCJdfQ==`(解码后为 `{"name":["张"]}` ) -- Base64 编码 sort:`?sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9` -- Base64 编码 params(指定操作符):`?params=W3sia2V5IjoiYWdlIiwidHlwZSI6IkdSRUFURVJfVEhBTiJ9XQ==` - -### 5. RequestFilter — 读取已设置的过滤条件 - -```java -// 检查是否有过滤条件 -if (request.hasFilter()) { ... } - -// 获取特定过滤值 -String name = request.getStringFilter("name"); -int age = request.getIntFilter("age", 0); // 带默认值 - -// 移除某个过滤条件 -request.removeFilter("name"); -``` - -## 使用实例 - -### 完整的用户列表查询 - -```java -// Entity -@Entity -public class User { - @Id @GeneratedValue - private Long id; - private String name; - private Integer age; - private String email; - private LocalDateTime createTime; - private Integer status; -} - -// Repository -public interface UserRepository extends FastRepository { -} - -// Controller -@RestController -@RequestMapping("/api/users") -public class UserController { - - @Autowired - private UserRepository userRepository; - - /** - * 方式一:编程式构建 - */ - @GetMapping("/search") - public MultiResponse search( - @RequestParam(required = false) String name, - @RequestParam(required = false) Integer minAge, - @RequestParam(defaultValue = "0") int current, - @RequestParam(defaultValue = "20") int pageSize) { - - PageRequest request = PageRequest.of(current, pageSize) - .addSort(Sort.by("createTime").descending()); - - if (StringUtils.hasText(name)) { - request.addFilter("name", Relation.LIKE, name); - } - if (minAge != null) { - request.addFilter("age", Relation.GREATER_THAN_EQUAL, minAge); - } - - Page page = userRepository.findAll(request); - return MultiResponse.of(page); - } - - /** - * 方式二:SearchRequest 自动解析 - */ - @GetMapping - public MultiResponse list(SearchRequest searchRequest) { - PageRequest pageRequest = searchRequest.toPageRequest(User.class); - Page page = userRepository.searchRequest(searchRequest); - return MultiResponse.of(page); - } -} -``` - -### 复杂组合查询示例 - -```java -// 查询:(name LIKE '%张%' AND age >= 18) OR status = 1 -PageRequest request = PageRequest.of(0, 20); - -request.orFilters( - Filter.and( - Filter.as("name", Relation.LIKE, "张"), - Filter.as("age", Relation.GREATER_THAN_EQUAL, 18) - ), - Filter.as("status", Relation.EQUAL, 1) -); - -Page page = userRepository.pageRequest(request); -``` - -### Filter 工具方法速查 - -```java -// 快捷创建 -Filter.as("key", value) // EQUAL -Filter.as("key", Relation.LIKE, val) // 指定关系 -Filter.and(f1, f2, ...) // AND 组合 -Filter.or(f1, f2, ...) // OR 组合 - -// 判断类型 -filter.isEqual() -filter.isLike() -filter.isBetween() -filter.isIn() -filter.isAndFilters() -filter.isOrFilters() - -// 获取值(自动类型转换) -Object val = filter.getFilterValue(Integer.class); -``` diff --git a/docs/capabilities/rest-template-context.md b/docs/capabilities/rest-template-context.md deleted file mode 100644 index 8c04c4c46..000000000 --- a/docs/capabilities/rest-template-context.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -name: rest-template-context -description: 预配置的 RestTemplate 单例上下文,内置信任所有证书的 HttpClient,简化内部服务间 HTTP 调用 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在企业内部系统集成场景中,服务间通过 HTTPS 通信时经常遇到自签名证书或内部 CA 签发的证书不被信任的问题。每次创建 `RestTemplate` 都需要手动配置 SSL 信任策略,代码重复且容易出错。 - -`RestTemplateContext` 提供了一个全局单例的 `RestTemplate` 实例,内置了信任所有证书的 `HttpClient`(基于 Apache HttpComponents 5),适用于内部服务间调用、第三方接口对接等不需要严格证书校验的场景。 - -核心特点: -- **单例模式**:通过双重检查锁确保线程安全的懒初始化 -- **信任所有证书**:底层使用 `TrustAnyHttpClientFactory` 创建的 HttpClient,跳过 SSL 证书验证 -- **支持 HTTP/HTTPS**:同时注册了 HTTP 和 HTTPS 的连接工厂 -- **允许循环重定向**:默认开启 `circularRedirectsAllowed` - -## 如何使用 - -### 1. 获取 RestTemplate 实例 - -```java -RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); -``` - -### 2. 发起 HTTP 请求 - -```java -RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); - -// GET 请求 -String result = restTemplate.getForObject("https://internal-api.example.com/data", String.class); - -// POST 请求 -Map body = Map.of("key", "value"); -ResponseEntity response = restTemplate.postForEntity( - "https://internal-api.example.com/submit", - body, - String.class -); - -// 带 Header 的请求 -HttpHeaders headers = new HttpHeaders(); -headers.setContentType(MediaType.APPLICATION_JSON); -headers.setBearerAuth(token); -HttpEntity> entity = new HttpEntity<>(body, headers); -ResponseEntity resp = restTemplate.exchange( - "https://api.example.com/resource", - HttpMethod.GET, - entity, - String.class -); -``` - -### 3. 自定义 ClientHttpRequestFactory - -如需使用不同的请求工厂创建新的 RestTemplate: - -```java -ClientHttpRequestFactory customFactory = new HttpComponentsClientHttpRequestFactory(customHttpClient); -RestTemplate customTemplate = RestTemplateContext.getInstance().restTemplate(customFactory); -``` - -## 使用实例 - -### 内部服务调用示例 - -```java -@Service -public class ExternalServiceClient { - - private final RestTemplate restTemplate; - - public ExternalServiceClient() { - this.restTemplate = RestTemplateContext.getInstance().getRestTemplate(); - } - - public UserInfo getUserFromRemote(Long userId) { - String url = "https://user-service.internal/api/users/" + userId; - return restTemplate.getForObject(url, UserInfo.class); - } - - public void notifyRemote(String message) { - String url = "https://notification-service.internal/api/notify"; - Map payload = Map.of("message", message); - restTemplate.postForEntity(url, payload, Void.class); - } -} -``` - -### 架构说明 - -``` -RestTemplateContext (Singleton) - └── RestTemplate - └── HttpComponentsClientHttpRequestFactory - └── HttpClient (Apache HC5) - ├── SSLConnectionSocketFactory (TLS, TrustAnyTrustManager, NoopHostnameVerifier) - └── PlainConnectionSocketFactory (HTTP) -``` - -`TrustAnyHttpClientFactory.createTrustAnyHttpClient()` 创建的 HttpClient 具有以下特性: -- SSL 上下文使用 `TrustAnyTrustManager`,对所有服务端证书返回信任 -- 主机名验证使用 `NoopHostnameVerifier`,不校验主机名 -- 连接管理器使用 `BasicHttpClientConnectionManager` -- 请求配置允许循环重定向 - -### ⚠️ 安全提示 - -此 RestTemplate **仅适用于受信任的内部网络环境**。由于跳过了 SSL 证书验证,在公网环境下使用存在中间人攻击风险。对于面向公网的外部 API 调用,应使用标准配置的 RestTemplate 并正确配置证书信任链。 diff --git a/docs/capabilities/security-auth.md b/docs/capabilities/security-auth.md deleted file mode 100644 index 20179477d..000000000 --- a/docs/capabilities/security-auth.md +++ /dev/null @@ -1,263 +0,0 @@ ---- -name: security-auth -description: 基于 Spring Security 的 JWT/Redis 双模式认证框架,支持 Token 自动续期与登录扩展 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -Spring Security 默认的表单登录和 Session 机制不适合前后端分离的 RESTful API 场景。企业应用通常需要: -- 无状态的 JWT Token 认证或有状态的 Redis Token 认证 -- JSON 格式的统一登录/登出响应(而非重定向) -- Token 自动续期机制 -- 可扩展的登录前置/后置处理逻辑 -- 统一的未认证、无权限错误响应 - -`springboot-starter-security` 在 Spring Security 基础上封装了完整的认证框架,提供开箱即用的 JWT / Redis 双模式认证,所有安全事件均以 JSON 格式返回统一的 `Response` 结构。 - -## 如何使用 - -### 1. 引入依赖 - -```xml - - com.codingapi.springboot - springboot-starter-security - -``` - -### 2. 配置安全属性 - -在 `application.properties` 中配置: - -```properties -# 安全模块配置前缀 -codingapi.security.login-processing-url=/login # 登录接口路径 -codingapi.security.logout-url=/logout # 登出接口路径 -codingapi.security.ignore-urls=/open/**,/#/** # 免认证 URL 列表 -codingapi.security.authenticated-urls=/api/** # 需认证的 URL 列表 -codingapi.security.disable-cors=false # 是否禁用 CORS(false=启用默认 CORS) - -# JWT 模式 -codingapi.security.jwt.enable=true # 启用 JWT 认证 - -# Redis 模式 -codingapi.security.redis.enable=true # 启用 Redis 有状态认证 -``` - -配置通过 `CodingApiSecurityProperties` 绑定。 - -### 3. Token 网关 (TokenGateway) - -框架定义了 `TokenGateway` 接口作为 Token 操作的抽象层: - -```java -public interface TokenGateway { - // 创建 Token - Token create(String username, String iv, List authorities, String extra); - - // 解析 Token - Token parser(String sign); -} -``` - -框架提供了两种实现: -- **`JWTTokenGatewayImpl`**:基于 JJWT 的无状态 JWT Token -- **`RedisTokenGatewayImpl`**:基于 Redis 的有状态 Token - -根据配置自动选择实现。 - -### 4. Token 对象 - -`Token` 类封装了认证信息: - -```java -public class Token implements JsonSerializable { - private String username; // 用户名 - private String extra; // 额外信息(JSON 字符串) - private String iv; // 加密向量(AES 编码) - private String token; // Token 值 - private List authorities; // 权限列表 - private long expireTime; // 过期时间戳 - private long remindTime; // 提醒续期时间戳 - - // 验证 Token 是否过期 - void verify() throws TokenExpiredException; - - // 是否需要续期(未过期且已过提醒时间) - boolean canRestToken(); - - // 解析额外信息为指定类型 - T parseExtra(Class clazz); - - // 转换为 Spring Security Authentication - UsernamePasswordAuthenticationToken getAuthenticationToken(); -} -``` - -### 5. 获取当前用户信息 - -通过 `TokenContext` 获取当前登录用户的 Token: - -```java -// 获取当前 Token -Token token = TokenContext.current(); -String username = token.getUsername(); -List authorities = token.getAuthorities(); - -// 获取额外信息 -String extra = TokenContext.getExtra(); -UserInfo userInfo = token.parseExtra(UserInfo.class); -``` - -### 6. 自定义登录处理 - -实现 `SecurityLoginHandler` 接口以扩展登录逻辑: - -```java -@Bean -public SecurityLoginHandler securityLoginHandler() { - return new SecurityLoginHandler() { - @Override - public void preHandle(HttpServletRequest request, - HttpServletResponse response, - LoginRequest loginRequest) throws Exception { - // 登录前置处理:验证码校验、IP 黑名单检查等 - if (!captchaService.verify(loginRequest.getCaptcha())) { - throw new Exception("验证码错误"); - } - } - - @Override - public LoginResponse postHandle(HttpServletRequest request, - HttpServletResponse response, - LoginRequest loginRequest, - UserDetails user, - Token token) { - // 登录后置处理:记录登录日志、附加额外信息等 - LoginResponse loginResponse = new LoginResponse(); - loginResponse.setToken(token.getToken()); - loginResponse.setUsername(token.getUsername()); - loginResponse.setAuthorities(token.getAuthorities()); - loginResponse.setData(Map.of("lastLoginTime", System.currentTimeMillis())); - return loginResponse; - } - }; -} -``` - -`LoginResponse` 结构: -```java -public class LoginResponse { - private String username; - private String token; - private List authorities; - private Object data; // 自定义附加数据 -} -``` - -### 7. 自定义 Token 过滤 - -实现 `AuthenticationTokenFilter` 在每次请求认证通过后执行额外逻辑: - -```java -@Bean -public AuthenticationTokenFilter authenticationTokenFilter() { - return (request, response) -> { - // 例如:更新最后登录时间、审计日志等 - }; -} -``` - -### 8. 自定义 HttpSecurity 配置 - -实现 `HttpSecurityCustomer` 接口以进一步定制 Spring Security 配置: - -```java -@Bean -public HttpSecurityCustomer httpSecurityCustomer(CodingApiSecurityProperties properties) { - return new DefaultHttpSecurityCustomer(properties); - // 或自定义实现 -} -``` - -## 使用实例 - -### 认证过滤器链工作原理 - -``` -HTTP Request - ↓ -MyUnAuthenticationEntryPoint ← 未认证时返回 {"errCode":"not.login","errMessage":"please to login."} - ↓ -MyAccessDeniedHandler ← 无权限时返回 {"errCode":"not.access","errMessage":"please check user authentication."} - ↓ -MyLoginFilter (POST /login) ← 登录认证 - ├── attemptAuthentication() → 读取 JSON body → SecurityLoginHandler.preHandle() → authenticate() - ├── successfulAuthentication() → TokenGateway.create() → SecurityLoginHandler.postHandle() → 返回 LoginResponse - └── unsuccessfulAuthentication() → 返回 {"errCode":"login.error","errMessage":"..."} - ↓ -MyAuthenticationFilter ← Token 认证 - ├── 匹配 authenticatedUrls - ├── 从 Header "Authorization" 读取 Token - ├── TokenGateway.parser(sign) → Token.verify() - ├── Token.canRestToken() → 自动续期,新 Token 写入响应头 - └── SecurityContextHolder.setAuthentication() - ↓ -MyLogoutSuccessHandler (POST /logout) ← 登出成功返回 {"success":true} -``` - -### 统一错误响应格式 - -所有安全相关的错误都以统一的 `Response` JSON 格式返回: - -| 场景 | errCode | errMessage | -|------|---------|------------| -| 未登录 | `not.login` | `please to login.` | -| 无权限 | `not.access` | `please check user authentication.` | -| 登录失败 | `login.error` | 具体异常信息 | -| Token 为空 | `token.error` | `token must not null.` | -| Token 过期 | `token.expire` | `token expire.` | - -### Token 自动续期 - -当 Token 未过期但已超过 `remindTime` 时,`MyAuthenticationFilter` 会自动创建新 Token 并通过响应头 `Authorization` 返回: - -```java -// MyAuthenticationFilter.doFilterInternal() 中的关键逻辑 -if (token.canRestToken()) { - Token newSign = tokenGateway.create( - token.getUsername(), - token.decodeIv(), - token.getAuthorities(), - token.getExtra() - ); - response.setHeader("Authorization", newSign.getToken()); -} -``` - -客户端应在每次响应后检查 `Authorization` 响应头,如有新值则替换本地存储的 Token。 - -### 密码编码器 - -框架默认使用 Spring Security 的 `DelegatingPasswordEncoder`,支持多种编码格式的自动识别: - -```java -@Bean -@ConditionalOnMissingBean -public PasswordEncoder passwordEncoder() { - return PasswordEncoderFactories.createDelegatingPasswordEncoder(); -} -``` - -如需自定义密码编码器,只需声明自己的 `PasswordEncoder` Bean 即可覆盖默认实现。 - -### 默认用户(开发环境) - -当没有自定义 `UserDetailsService` 时,框架自动创建内存用户: -- `admin / admin` — ROLE_ADMIN -- `user / admin` — ROLE_USER - -生产环境应提供自己的 `UserDetailsService` Bean 来覆盖此默认行为。 diff --git a/docs/capabilities/spring-data-jpa.md b/docs/capabilities/spring-data-jpa.md index 3036331d2..9463ee2c3 100644 --- a/docs/capabilities/spring-data-jpa.md +++ b/docs/capabilities/spring-data-jpa.md @@ -1,77 +1,106 @@ --- name: spring-data-jpa -description: ORM 映射、Repository 抽象、自动分页 +description: Spring Data JPA 数据访问能力,提供 ORM 映射、Repository 抽象、分页查询等基础设施,框架在此基础上扩展了 FastRepository 动态查询 status: 已实现 scope: 后端 -source: 框架:Spring Data JPA +source: 框架:spring-data-jpa +framework_version: Managed by Spring Boot BOM (3.3.5) --- ## 解决什么问题 -Spring Data JPA 是项目中数据持久化的基础层,解决了以下问题: +Spring Data JPA 是框架数据持久层的基石,解决了以下问题: -- **ORM 映射**:将数据库表映射为 Java Entity,减少手写 SQL -- **Repository 抽象**:通过接口定义数据访问方法,由框架自动生成实现 -- **自动分页**:内置 `Pageable` / `Page` 支持,简化分页查询逻辑 -- **作为 FastRepository 的底层**:项目的 `springboot-starter-data-fast` 模块在此基础上扩展了动态过滤查询能力 +- **ORM 映射**:通过 JPA 注解将领域实体映射到关系数据库表,框架中所有领域对象(如 `FlowWork`、`FlowRecord`、`FlowNode` 等)均基于 JPA Entity 定义 +- **Repository 抽象**:提供 `JpaRepository` 接口,自动生成 CRUD 实现,消除样板代码 +- **分页查询**:原生支持 `Pageable` / `Page` 分页抽象,框架在此基础上扩展了 `PageRequest` + `RequestFilter` 动态过滤能力 -## 如何使用 +框架的 `springboot-starter-data-fast` 模块在 Spring Data JPA 之上构建了 `FastRepository`,支持基于 Filter 的动态 Example 查询和 HQL 构建,大幅简化复杂条件查询的开发。 -项目使用 `spring-boot-starter-data-jpa`(随 Spring Boot 3.3.5),在 DDD 架构中处于 infra 层: +## 如何使用 -1. **Entity 定义**:在 `example-infra-jpa` 模块中定义 JPA Entity(如 `UserEntity`) -2. **JPA Repository**:继承 `JpaRepository` 或 `FastRepository` 获得 CRUD + 动态查询能力 -3. **Domain Repository 实现**:在 infra 层实现 domain 层定义的 Repository 接口,内部委托给 JPA Repository -4. **Convertor 转换**:使用 Convertor 类在 Entity 与 Domain Entity 之间转换,隔离持久化细节 +### 依赖引入 -## 使用实例 +```xml + + com.codingapi.springboot + springboot-starter-data-fast + +``` -### JPA Repository 定义 +该模块已传递依赖 `spring-boot-starter-data-jpa`,无需单独引入。 -```java -// example-infra-jpa 模块 -public interface UserEntityRepository extends FastRepository { +### Repository 定义 - UserEntity getUserEntityByUsername(String username); +继承 `FastRepository` 而非原生的 `JpaRepository`,即可获得动态过滤查询能力: - UserEntity getUserEntityById(long id); +```java +public interface UserRepository extends FastRepository { + // 自动拥有 findAll(PageRequest)、pageRequest(PageRequest)、searchRequest(SearchRequest) 等方法 } ``` -### Domain Repository 接口(domain 层) +### 动态过滤查询 + +`PageRequest` 扩展了 Spring Data 的 `org.springframework.data.domain.PageRequest`,增加了 `RequestFilter`: ```java -// example-domain-user 模块 —— 仅定义接口,不依赖 JPA -public interface UserRepository { - User getUserByUsername(String username); - User getUserById(long id); - void save(User user); - void delete(long id); -} +PageRequest request = PageRequest.of(0, 20); +request.addFilter("name", "张三"); +request.addFilter("age", Relation.GT, 18); +Page page = userRepository.findAll(request); ``` -### Infra 层实现(桥接 Domain 与 JPA) +当 Filter 存在时,`findAll` 自动构建 Example 查询;`pageRequest` 方法则构建 HQL 动态 SQL,支持更复杂的关联查询。 + +## 使用实例 + +### 基础 CRUD + 动态过滤 ```java -@Repository +@Service @AllArgsConstructor -public class UserRepositoryImpl implements UserRepository { +public class UserQueryService { - private final UserEntityRepository userEntityRepository; + private final UserRepository userRepository; - @Override - public User getUserByUsername(String username) { - return UserConvertor.convert( - userEntityRepository.getUserEntityByUsername(username), - userEntityRepository - ); + // 简单分页(无过滤) + public Page listUsers(int page, int size) { + return userRepository.findAll(PageRequest.of(page, size)); } - @Override - public void save(User user) { - UserEntity entity = UserConvertor.convert(user); - entity = userEntityRepository.save(entity); - user.setId(entity.getId()); + // 带动态过滤的分页查询 + public Page searchUsers(String name, Integer minAge) { + PageRequest request = PageRequest.of(0, 20); + if (name != null) { + request.addFilter("name", name); + } + if (minAge != null) { + request.addFilter("age", Relation.GTE, minAge); + } + return userRepository.findAll(request); } + + // 使用 HQL 动态查询(支持关联字段、OR 条件等) + public Page advancedSearch(SearchRequest searchRequest) { + return userRepository.searchRequest(searchRequest); + } +} +``` + +### JPA Entity 定义 + +```java +@Entity +@Table(name = "t_user") +@Getter @Setter +public class UserEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private Integer age; + private String email; } ``` diff --git a/docs/capabilities/spring-data-redis.md b/docs/capabilities/spring-data-redis.md index 7338ff9a4..bc77f3d0c 100644 --- a/docs/capabilities/spring-data-redis.md +++ b/docs/capabilities/spring-data-redis.md @@ -1,58 +1,105 @@ --- name: spring-data-redis -description: Redis 客户端抽象,支持缓存和有状态认证 Token 存储 +description: Spring Data Redis 数据存储能力,框架中主要用于 Token/Session 的有状态存储,支持服务端主动失效和多端登录管理 status: 已实现 scope: 后端 -source: 框架:Spring Data Redis +source: 框架:spring-data-redis +framework_version: Managed by Spring Boot BOM (3.3.5) --- ## 解决什么问题 -Spring Data Redis 为项目提供 Redis 访问能力,解决了以下问题: +Spring Data Redis 在框架中承担了有状态认证场景下的核心存储职责,解决了以下问题: -- **有状态认证**:在 Redis 模式下存储用户 Token 和会话信息,支持分布式部署 -- **缓存**:为热点数据提供高速缓存层,减少数据库压力 -- **分布式协调**:可作为分布式锁、消息队列等基础设施的底层支撑 +- **Token 服务端存储**:`RedisTokenGateway` 使用 `RedisTemplate` 将 Token 以 JSON 格式存入 Redis,Key 格式为 `{encodedUsername}:{uuid}`,支持按用户名批量查询和删除 +- **Token 生命周期管理**:通过 Redis TTL 机制自动过期,同时支持服务端主动调用 `removeToken()` / `resetToken()` 实现即时失效和续期 +- **多端登录管理**:`getTokensByUsername()` 获取用户所有在线 Token,`removeUsername()` 一键踢出所有设备,还支持按条件(`Predicate`)选择性踢出 +- **缓存抽象**:可作为通用缓存层,用于脚本编译结果缓存、会话数据暂存等场景 ## 如何使用 -项目使用 `spring-boot-starter-data-redis`(随 Spring Boot 3.3.5),主要在安全模块中使用: +### 依赖引入 -1. **启用 Redis 认证**:配置 `codingapi.security.redis.enable=true` -2. **Token 存储**:登录成功后将 Token 存入 Redis,后续请求从 Redis 验证 Token 有效性 -3. **与 JWT 互斥**:JWT 模式(无状态)和 Redis 模式(有状态)二选一 -4. **连接配置**:通过标准 Spring Boot Redis 配置项指定连接信息 +```xml + + com.codingapi.springboot + springboot-starter-security + +``` -## 使用实例 +`spring-boot-starter-data-redis` 作为 `provided` 依赖包含在安全模块中,启用 Redis 认证模式时需确保运行时存在 Redis 连接配置。 -### 启用 Redis 有状态认证 +### 配置 Redis 连接 ```properties -# application.properties -codingapi.security.redis.enable=true -codingapi.security.jwt.enable=false - # Redis 连接配置 spring.data.redis.host=localhost spring.data.redis.port=6379 spring.data.redis.password=your-password + +# 启用 Redis 有状态认证 +codingapi.security.redis.enable=true +codingapi.security.redis.valid-time=86400000 # Token 有效期(毫秒) +codingapi.security.redis.rest-time=3600000 # 刷新提醒时间(毫秒) ``` -### 免认证 URL 配置 +### RedisTokenGateway 核心 API -```properties -# 这些路径不需要 Token 验证 -codingapi.security.ignore-urls=/open/**,/#/**,/api/query/** -``` +框架封装了 `RedisTokenGateway`,提供面向业务的 Token 操作方法: -### 架构说明 +| 方法 | 说明 | +|------|------| +| `create(username, iv, authorities, extra)` | 创建 Token 并存入 Redis | +| `getToken(token)` | 根据 Token 字符串获取用户信息 | +| `removeToken(token)` | 删除指定 Token | +| `resetToken(token)` | 重置 Token 有效期 | +| `removeUsername(username)` | 删除用户所有 Token(踢出所有设备) | +| `getTokensByUsername(username)` | 获取用户所有在线 Token Key | +| `removeUsername(username, predicate)` | 按条件选择性删除 Token | +## 使用实例 + +### 管理员强制下线用户 + +```java +@Service +@AllArgsConstructor +public class UserAdminService { + + private final RedisTokenGateway redisTokenGateway; + + /** + * 强制下线指定用户的所有设备 + */ + public void forceLogout(String username) { + redisTokenGateway.removeUsername(username); + } + + /** + * 选择性下线:仅移除特定设备的 Token + */ + public void logoutDevice(String username, String targetToken) { + redisTokenGateway.removeUsername(username, token -> + targetToken.equals(token.getToken()) + ); + } + + /** + * 查看用户在线设备数 + */ + public int getOnlineDeviceCount(String username) { + return redisTokenGateway.getTokensByUsername(username).size(); + } +} ``` -用户登录 → MyLoginFilter 验证凭据 - → 生成 Token 存入 Redis - → 返回 Token 给客户端 -后续请求 → MyAuthenticationFilter 拦截 - → 从 Redis 读取 Token 验证 - → 验证通过则放行,否则返回 401 +### Token 续期 + +```java +// 在认证过滤器中检测 Token 是否需要续期 +Token token = tokenGateway.parser(tokenString); +if (token != null && token.canRestToken()) { + // canRestToken() = !isExpire() && remindTime <= currentTimeMillis + redisTokenGateway.resetToken(token); +} ``` diff --git a/docs/capabilities/spring-framework.md b/docs/capabilities/spring-framework.md new file mode 100644 index 000000000..566c8e834 --- /dev/null +++ b/docs/capabilities/spring-framework.md @@ -0,0 +1,110 @@ +--- +name: spring-framework +description: Spring Framework / Spring Boot 核心基础能力,提供 IoC 容器、事件发布、AOP、Web MVC、事务管理等基础设施 +status: 已实现 +scope: 后端 +source: 框架:spring-framework +framework_version: 3.3.5 +--- + +## 解决什么问题 + +Spring Framework / Spring Boot 是整个框架的运行时基座,解决了以下核心问题: + +- **IoC 容器**:管理所有 Bean 的生命周期与依赖注入,框架中所有 Starter 模块的自动配置均基于 Spring Boot AutoConfiguration 机制(`spring.factories` + `AutoConfiguration.imports`) +- **事件发布(ApplicationEventPublisher)**:框架自建的事件系统(`DomainEvent`、`IHandler`)底层通过 Spring 的 `@EventListener` 桥接,由 `SpringDefaultEventHandler` / `SpringTransactionEventHandler` 订阅 `DomainEvent` 并分发到业务 Handler +- **AOP**:支持声明式事务(`@Transactional`)、数据权限 SQL 拦截等切面能力 +- **Web MVC**:统一 REST API 响应封装(`Response` / `SingleResponse` / `MultiResponse`)、全局异常处理、参数绑定 +- **事务管理**:工作流引擎、领域事件持久化等场景均依赖 Spring 声明式事务 + +## 如何使用 + +### 依赖引入 + +框架根 POM 继承自 `spring-boot-starter-parent:3.3.5`,所有子模块自动获得 Spring Boot 依赖管理。 + +```xml + + + com.codingapi.springboot + springboot-starter + +``` + +### 事件系统桥接 + +框架通过 `SpringEventConfiguration` 注册 `SpringEventInitializer`,在容器启动时将 `ApplicationContext` 注入 `DomainEventContext`,使 `EventPusher.push()` 能够发布 Spring `DomainEvent`: + +```java +// 框架内部自动配置,无需手动操作 +@Bean +public SpringEventInitializer springEventInitializer(ApplicationContext context) { + return new SpringEventInitializer(context); +} +``` + +业务侧只需实现 `IHandler` 接口即可订阅事件,Handler 由 `HandlerBeanDefinitionRegistrar` 自动扫描注册。 + +### 事务管理 + +使用标准 Spring `@Transactional` 注解: + +```java +@Transactional +public void startFlow(...) { ... } +``` + +可通过 `codingapi.framework.event.transaction.enable=true` 启用事务内事件处理模式(`SpringTransactionEventHandler`),确保事件在事务提交后触发。 + +## 使用实例 + +### 定义并订阅领域事件 + +```java +// 1. 定义事件 +public class OrderCreatedEvent implements ISyncEvent { + private final String orderId; + // constructor, getters... +} + +// 2. 实现处理器(自动注册为 Spring Bean) +@Component +public class OrderCreatedHandler implements IHandler { + @Override + public int order() { return 0; } + + @Override + public void handler(OrderCreatedEvent event) { + // 处理订单创建逻辑 + } + + @Override + public void error(OrderCreatedEvent event, Exception exception) { + // 异常回调 + } +} + +// 3. 发布事件 +EventPusher.push(new OrderCreatedEvent("ORD-001"), true); +``` + +### 统一响应封装 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + UserDTO user = userService.findById(id); + return SingleResponse.of(user); + } + + @GetMapping + public MultiResponse listUsers(PageRequest request) { + Page page = userService.findAll(request); + return MultiResponse.of(page.getContent(), page.getTotalElements()); + } +} +``` diff --git a/docs/capabilities/spring-ioc-container.md b/docs/capabilities/spring-ioc-container.md deleted file mode 100644 index 58bffabe2..000000000 --- a/docs/capabilities/spring-ioc-container.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: spring-ioc-container -description: Spring IoC 容器提供依赖注入、Bean 生命周期管理、AOP 支持 -status: 已实现 -scope: 后端 -source: 框架:Spring Framework ---- - -## 解决什么问题 - -Spring IoC(控制反转)容器是项目的核心基础设施,解决了以下问题: - -- **对象创建与组装**:自动管理 Bean 的创建、依赖注入和生命周期,避免手动 new 对象和硬编码依赖 -- **模块解耦**:通过接口注入而非具体类依赖,使各 starter 模块可以独立开发和测试 -- **横切关注点**:通过 AOP 支持事务管理、日志记录等横切逻辑的统一处理 -- **自动配置**:Spring Boot 的条件化自动配置机制使框架的各 starter 模块可按需激活 - -## 如何使用 - -项目基于 Spring Boot 3.3.5(Java 17),使用方式遵循标准 Spring Boot 约定: - -1. **依赖注入**:使用构造器注入(推荐 `@AllArgsConstructor` + `final` 字段)或 `@Autowired` -2. **Bean 声明**:使用 `@Component`、`@Service`、`@Repository`、`@Configuration` 等注解 -3. **自动配置注册**:各 starter 模块通过 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 和 `META-INF/spring.factories` 双重注册 -4. **ImportBeanDefinitionRegistrar**:框架使用此扩展点动态注册 Bean,如 `HandlerBeanDefinitionRegistrar` 自动扫描事件处理器 - -## 使用实例 - -### 构造器注入(项目推荐方式) - -```java -@Slf4j -@Service -@AllArgsConstructor -public class LeaveHandler implements IHandler { - - private final LeaveService leaveService; // 自动注入 - - @Override - public void handler(FlowApprovalEvent event) { - if (event.isFinish() && event.match(LeaveForm.class)) { - LeaveForm form = (LeaveForm) event.getBindData(); - leaveService.create(form.toLeave()); - } - } -} -``` - -### Repository 实现注入 - -```java -@Repository -@AllArgsConstructor -public class UserRepositoryImpl implements UserRepository { - - private final UserEntityRepository userEntityRepository; // JPA Repository 自动注入 - - @Override - public User getUserByUsername(String username) { - return UserConvertor.convert( - userEntityRepository.getUserEntityByUsername(username), - userEntityRepository - ); - } -} -``` - -### 自定义 Bean 注册(ImportBeanDefinitionRegistrar) - -```java -@Configuration -public class HandlerBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { - - @SneakyThrows - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, - BeanDefinitionRegistry registry) { - RegisterBeanScanner scanner = new RegisterBeanScanner(importingClassMetadata, Handler.class); - List beanDefinitions = scanner.findBeanDefinitions(); - for (BeanDefinition candidate : beanDefinitions) { - registry.registerBeanDefinition(candidate.getBeanClassName(), candidate); - } - } -} -``` diff --git a/docs/capabilities/spring-security.md b/docs/capabilities/spring-security.md index 2297378a8..407c72f47 100644 --- a/docs/capabilities/spring-security.md +++ b/docs/capabilities/spring-security.md @@ -1,66 +1,109 @@ --- name: spring-security -description: 认证授权框架,提供 Filter Chain、CSRF 防护 +description: Spring Security 安全认证与授权能力,框架在此基础上封装了 JWT 无状态认证和 Redis 有状态认证两种模式 status: 已实现 scope: 后端 -source: 框架:Spring Security +source: 框架:spring-security +framework_version: Managed by Spring Boot BOM (3.3.5) --- ## 解决什么问题 -Spring Security 为项目提供安全基础设施,解决了以下问题: +Spring Security 为框架提供了完整的安全基础设施,解决了以下问题: -- **认证**:验证用户身份,支持 JWT 无状态认证和 Redis 有状态认证两种模式 -- **授权**:基于 URL 模式和角色的访问控制 -- **Filter Chain**:通过过滤器链拦截请求,统一处理安全逻辑 -- **密码加密**:提供 PasswordEncoder 接口进行安全的密码哈希 +- **认证(Authentication)**:框架的 `springboot-starter-security` 模块基于 Spring Security 过滤器链,实现了两种 Token 认证模式: + - **JWT 无状态模式**:通过 `JwtTokenGateway` + JJWT 库签发/验证 JWT Token,适用于分布式部署 + - **Redis 有状态模式**:通过 `RedisTokenGateway` + `RedisTemplate` 将 Token 存储在 Redis 中,支持服务端主动失效、单点登录等场景 +- **授权(Authorization)**:基于 Spring Security 的权限模型,`Token.authorities` 携带用户角色/权限列表,通过 `UsernamePasswordAuthenticationToken` 注入 SecurityContext +- **CSRF 防护**:REST API 场景下默认禁用 CSRF(无状态 Token 认证不需要),表单场景可按需启用 +- **统一安全过滤链**:`MyAuthenticationFilter` 拦截请求解析 Token,`MyLoginFilter` 处理登录请求生成 Token,`SecurityLoginHandler` 统一登录成功/失败响应 ## 如何使用 -项目使用 `spring-boot-starter-security`(随 Spring Boot 3.3.5),被 `springboot-starter-security` 模块封装: +### 依赖引入 -1. **JWT 模式**:配置 `codingapi.security.jwt.enable=true`,使用 `JwtTokenGateway` 签发和验证 Token -2. **Redis 模式**:配置 `codingapi.security.redis.enable=true`,Token 存储在 Redis 中 -3. **免认证 URL**:通过 `codingapi.security.ignore-urls` 配置白名单路径 -4. **自定义 Filter**:框架提供 `MyAuthenticationFilter`、`MyLoginFilter`、`MyLogoutSuccessHandler` 等组件 -5. **异常处理**:`MyAccessDeniedHandler` 和 `MyUnAuthenticationEntryPoint` 统一返回 `Response` 格式的错误响应 +```xml + + com.codingapi.springboot + springboot-starter-security + +``` -## 使用实例 +### 配置认证模式 -### 安全配置项 +在 `application.properties` 中选择认证方式: ```properties -# application.properties +# JWT 无状态认证 codingapi.security.jwt.enable=true -codingapi.security.redis.enable=false -codingapi.security.ignore-urls=/open/**,/#/**,/api/query/** +codingapi.security.jwt.secret-key=your-secret-key-at-least-256-bits +codingapi.security.jwt.valid-time=86400000 # Token 有效期(毫秒) +codingapi.security.jwt.rest-time=3600000 # Token 刷新提醒时间(毫秒) + +# Redis 有状态认证(二选一) +codingapi.security.redis.enable=true +codingapi.security.redis.valid-time=86400000 +codingapi.security.redis.rest-time=3600000 + +# 免认证 URL 列表 +codingapi.security.ignore-urls=/open/**,/#/**,/api/login ``` -### Controller 中使用认证信息 +### TokenGateway 接口 + +两种模式均实现统一的 `TokenGateway` 接口,业务代码无需关心底层存储: ```java -@RestController -@RequestMapping("/api/cmd/user") +public interface TokenGateway { + Token create(String username, String iv, List authorities, String extra); + Token parser(String sign); +} +``` + +## 使用实例 + +### 自定义登录逻辑 + +```java +@Service @AllArgsConstructor -public class UserDomainCmdController { +public class LoginService { + + private final TokenGateway tokenGateway; - private final UserRouter userRouter; + public Token login(String username, String password) { + // 1. 验证用户名密码(自行实现) + UserDetails user = authenticate(username, password); - @PostMapping("/save") - public Response save(@RequestBody UserCmd.UpdateRequest request) { - // 请求已通过 Security Filter Chain 认证 - userRouter.createOrUpdate(request); - return Response.buildSuccess(); + // 2. 生成 Token(自动根据配置选择 JWT 或 Redis 模式) + return tokenGateway.create( + user.getUsername(), + null, // iv(AES 加密向量,可选) + user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()), + "{\"userId\":1}" // extra 扩展信息 + ); } } ``` -### Domain 层定义密码加密网关 +### 获取当前用户信息 ```java -// domain 层定义接口,不依赖 Spring Security -public interface PasswordEncoder { - String encode(String rawPassword); - boolean matches(String rawPassword, String encodedPassword); +@RestController +public class ProfileController { + + @GetMapping("/api/profile") + public SingleResponse> profile() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + Token token = (Token) auth.getPrincipal(); + + Map info = new HashMap<>(); + info.put("username", token.getUsername()); + info.put("authorities", token.getAuthorities()); + info.put("extra", token.parseExtra(Map.class)); + return SingleResponse.of(info); + } } ``` diff --git a/docs/capabilities/spring-web-mvc.md b/docs/capabilities/spring-web-mvc.md deleted file mode 100644 index 60015788a..000000000 --- a/docs/capabilities/spring-web-mvc.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: spring-web-mvc -description: Web MVC 框架,提供 RESTful API 路由、请求参数绑定、响应序列化 -status: 已实现 -scope: 后端 -source: 框架:Spring Web MVC ---- - -## 解决什么问题 - -框架需要提供 RESTful API 能力: -- 安全模块暴露版本查询接口 -- Groovy 脚本模块暴露脚本编译/保存/查询接口 -- 统一响应体(Response/SingleResponse/MultiResponse)的自动 JSON 序列化 -- 请求参数绑定(@RequestBody、@RequestParam) - -Spring Web MVC 是 Spring Boot Web 的核心组件,通过 `spring-boot-starter-web` 依赖引入。 - -## 如何使用 - -### 依赖引入 - -```xml - - - org.springframework.boot - spring-boot-starter-web - -``` - -### 核心注解 - -- `@RestController` — 声明 REST 控制器 -- `@GetMapping` / `@PostMapping` — 路由映射 -- `@RequestBody` — 请求体 JSON 反序列化 -- `@RequestParam` — 查询参数绑定 - -## 使用实例 - -### 1. Security 版本查询接口 - -```java -// VersionController.java -@RestController -public class VersionController { - - @GetMapping("/open/version") - public SingleResponse version() { - return Response.buildSuccess("3.4.54"); - } -} -``` - -### 2. Groovy 脚本管理接口 - -```java -// GroovyScriptController.java -@RestController -@RequestMapping("/api/groovy-script") -public class GroovyScriptController { - - @PostMapping("/compile") - public Response compile(@RequestBody ScriptCompileRequest request) { - // 编译 Groovy 脚本 - return Response.buildSuccess(); - } - - @GetMapping("/getScript") - public SingleResponse getScript(@RequestParam(name = "key") String key) { - // 获取脚本内容 - return Response.buildSuccess(script); - } - - @GetMapping("/getMetadata") - public SingleResponse getMetadata( - @RequestParam(name = "key") String key) { - // 获取脚本元数据 - return Response.buildSuccess(metadata); - } - - @PostMapping("/save") - public Response save(@RequestBody ScriptSaveRequest request) { - // 保存脚本 - return Response.buildSuccess(); - } -} -``` - -### 3. 统一响应体与 Spring MVC 集成 - -框架定义的 `Response`、`SingleResponse`、`MultiResponse` 等响应类通过 Jackson(Spring Boot 默认)自动序列化为 JSON: - -```java -// Controller 返回统一响应格式 -@GetMapping("/users") -public MultiResponse listUsers(PageRequest request) { - Page page = userRepository.findAll(request); - return Response.buildSuccess(page.getContent()); -} -// 自动序列化为: {"success": true, "data": [...]} -``` - -### 涉及模块 - -| 模块 | 使用场景 | -|------|----------| -| springboot-starter-security | `VersionController` 版本查询 API | -| springboot-starter-script | `GroovyScriptController` 脚本管理 API | -| springboot-starter-data-fast | 动态表管理的 REST 接口(example 中使用) | diff --git a/docs/capabilities/sql-interceptor.md b/docs/capabilities/sql-interceptor.md new file mode 100644 index 000000000..70d9a7c57 --- /dev/null +++ b/docs/capabilities/sql-interceptor.md @@ -0,0 +1,186 @@ +--- +name: sql-interceptor +description: 基于 JSqlParser 的 SQL 解析改写拦截器,在 SQL 执行前透明注入数据权限条件 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 67895fda +code_files: + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLInterceptor.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLRunningContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLInterceptorContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/DefaultSQLInterceptor.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/register/SQLInterceptorRegister.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DataAuthorizationFilter.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DefaultDataAuthorizationFilter.java +--- + +## 解决什么问题 + +在企业级多租户或行级数据权限场景中,业务查询需要根据当前用户的角色、部门、数据范围等条件自动追加 WHERE 过滤条件。如果由每个开发者手动拼接权限 SQL,容易出现遗漏、不一致和安全漏洞。 + +本能力通过 JDBC Connection/Statement 代理层拦截所有 SQL 执行,利用 JSqlParser 解析 SQL AST,在不修改任何业务代码的前提下,自动向 SELECT 语句注入行级和列级数据权限条件。核心优势: + +- **零侵入**:业务代码无需感知权限逻辑,SQL 改写对上层完全透明 +- **可扩展**:通过 `SQLInterceptor` 接口自定义拦截策略,通过 `DataAuthorizationFilter` 定义具体的权限规则 +- **防递归**:拦截器内部执行的查询自动跳过拦截,避免无限循环 +- **支持复杂 SQL**:处理子查询、JOIN、UNION、IN 子句中的嵌套 SELECT 等场景 + +## 如何使用 + +### 核心接口 + +#### SQLInterceptor — SQL 拦截器接口 + +```java +public interface SQLInterceptor { + // 前置判断:是否需要拦截该 SQL(默认仅拦截查询语句) + boolean beforeHandler(String sql); + + // 核心处理:解析并改写 SQL,返回 DataPermissionSQL(含新 SQL 和表别名上下文) + DataPermissionSQL postHandler(String sql) throws SQLException; + + // 后置回调:用于日志记录或异常处理 + void afterHandler(String sql, String newSql, SQLException exception); +} +``` + +#### DataAuthorizationFilter — 数据权限过滤器接口 + +```java +public interface DataAuthorizationFilter { + // 行级权限:返回需要追加到 WHERE/JOIN 的条件 + Condition rowAuthorization(String tableName, String tableAlias); + + // 列级权限:对查询结果中的特定列值进行脱敏/过滤 + T columnAuthorization(String tableName, String columnName, T value); + + // 是否支持对该表/列进行权限过滤 + boolean supportRowAuthorization(String tableName, String tableAlias); + boolean supportColumnAuthorization(String tableName, String columnName, Object value); +} +``` + +#### SQLRunningContext — SQL 执行上下文(单例) + +```java +// 获取单例 +SQLRunningContext.getInstance() + +// 拦截 SQL 并返回执行状态(包含改写后的 SQL) +SQLExecuteState intercept(String sql) + +// 临时跳过数据权限拦截执行代码块 + T skipDataAuthorization(Supplier supplier) +void skipDataAuthorization(Runnable runnable) +``` + +### 自定义拦截器注册 + +实现 `SQLInterceptor` 接口后,通过 Spring Bean 方式注册: + +```java +@Bean +public SQLInterceptorRegister sqlInterceptorRegister(MyCustomSQLInterceptor interceptor) { + return new SQLInterceptorRegister(interceptor); +} +``` + +`SQLInterceptorRegister` 会在构造时将自定义拦截器设置到 `SQLInterceptorContext` 中,替换默认的 `DefaultSQLInterceptor`。 + +### 自定义数据权限过滤器 + +实现 `DataAuthorizationFilter` 接口,并通过 `DataAuthorizationContext.getInstance().addDataAuthorizationFilter(filter)` 注册。可注册多个过滤器,按注册顺序依次匹配。 + +### 配置项 + +在 `application.properties` 中开启 SQL 日志输出: + +```properties +# 开启后会在 afterHandler 中打印改写后的 SQL +codingapi.authorization.show-sql=true +``` + +## 使用实例 + +### 1. 实现行级数据权限过滤器 + +```java +@Component +public class DepartmentDataFilter implements DataAuthorizationFilter { + + @Override + public boolean supportRowAuthorization(String tableName, String tableAlias) { + // 仅对 employee 表生效 + return "employee".equalsIgnoreCase(tableName); + } + + @Override + public Condition rowAuthorization(String tableName, String tableAlias) { + // 根据当前用户部门追加过滤条件 + Long deptId = SecurityContext.getCurrentDeptId(); + return Condition.formatCondition("%s.dept_id = %d", tableAlias, deptId); + } + + @Override + public boolean supportColumnAuthorization(String tableName, String columnName, Object value) { + return false; + } + + @Override + public T columnAuthorization(String tableName, String columnName, T value) { + return value; + } +} +``` + +### 2. 临时跳过数据权限拦截 + +```java +// 在管理后台导出全量数据时,跳过行级权限 +List allEmployees = SQLRunningContext.getInstance() + .skipDataAuthorization(() -> employeeRepository.findAll()); +``` + +### 3. 自定义 SQL 拦截器 + +```java +@Component +public class AuditSQLInterceptor implements SQLInterceptor { + + @Override + public boolean beforeHandler(String sql) { + // 仅拦截包含敏感表的查询 + return SQLUtils.isQuerySql(sql) && sql.contains("salary"); + } + + @Override + public DataPermissionSQL postHandler(String sql) throws SQLException { + RowHandler rowHandler = RowHandlerContext.getInstance().getRowHandler(); + DataPermissionSQLEnhancer enhancer = new DataPermissionSQLEnhancer(sql, rowHandler); + return new DataPermissionSQL(sql, enhancer.getNewSQL(), enhancer.getTableAlias()); + } + + @Override + public void afterHandler(String sql, String newSql, SQLException exception) { + if (exception != null) { + log.error("SQL 改写失败: {}", sql, exception); + } else { + auditLog.record(sql, newSql); + } + } +} +``` + +### 4. 拦截流程说明 + +``` +原始 SQL → SQLRunningContext.intercept() + → SQLInterceptor.beforeHandler() // 判断是否需要拦截 + → SQLInterceptor.postHandler() // JSqlParser 解析 + 注入权限条件 + → SQLInterceptor.afterHandler() // 日志/审计回调 + → 返回 SQLExecuteState(含改写后的 SQL) + → JDBC 代理使用改写后的 SQL 执行查询 +``` + +拦截器内部的查询操作会自动设置 `skipInterceptor=true`,防止递归拦截。执行完毕后自动重置状态。 diff --git a/docs/capabilities/token-gateway.md b/docs/capabilities/token-gateway.md new file mode 100644 index 000000000..aa2ae7bd5 --- /dev/null +++ b/docs/capabilities/token-gateway.md @@ -0,0 +1,101 @@ +--- +name: token-gateway +description: JWT/Redis 双模式 Token 认证网关,统一创建、解析和管理认证令牌 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 327e5420 +code_files: + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/TokenGateway.java + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/Token.java + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/jwt/JwtTokenGateway.java + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/redis/RedisTokenGateway.java + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/TokenContext.java +--- + +## 解决什么问题 + +在多端登录、无状态/有状态认证场景中,业务层需要一个统一的 Token 抽象来屏蔽底层存储差异。`TokenGateway` 提供了统一的令牌创建、解析和生命周期管理接口,支持: + +- **JWT 无状态模式**:Token 信息自包含在签名载荷中,适合微服务和分布式场景 +- **Redis 有状态模式**:Token 存储在 Redis 中,支持多端登录管理、强制下线、按需踢出 + +## 如何使用 + +### 核心接口 + +```java +public interface TokenGateway { + // 创建令牌(用户名 + IV加密密钥 + 权限列表 + 扩展信息) + Token create(String username, String iv, List authorities, String extra); + + // 解析令牌,返回 Token 对象 + Token parser(String sign); +} +``` + +### Token 对象 + +`Token` 包含以下字段: +- `username` — 用户名 +- `token` — 令牌字符串(JWT 签名或 Redis Key) +- `authorities` — 权限列表 +- `extra` — 扩展信息(JSON 字符串,可通过 `parseExtra(Class)` 反序列化) +- `iv` — AES 加密密钥(经 AESTools 编码) +- `expireTime` — 过期时间戳 +- `remindTime` — 续期提醒时间戳 + +关键方法: +- `token.verify()` — 校验是否过期,过期抛出 `TokenExpiredException` +- `token.canRestToken()` — 判断是否需要续期(未过期且已过提醒时间) +- `token.getAuthenticationToken()` — 转换为 Spring Security 的 `UsernamePasswordAuthenticationToken` + +### JWT 模式(JwtTokenGateway) + +通过 HMAC-SHA 签名生成自包含 JWT,配置项: +```properties +codingapi.security.jwt.secret-key=your-secret-key +codingapi.security.jwt.valid-time=3600000 # 有效期(毫秒) +codingapi.security.jwt.rest-time=1800000 # 续期提醒时间(毫秒) +``` + +### Redis 模式(RedisTokenGateway) + +Token 以 `AES(用户名):UUID` 为 Key 存储在 Redis 中,支持: +- `getToken(token)` — 根据令牌获取用户信息 +- `removeToken(token)` — 删除单个令牌 +- `removeUsername(username)` — 删除用户所有令牌(强制下线) +- `removeUsername(username, Predicate)` — 按条件选择性踢出 +- `getTokensByUsername(username)` — 获取用户所有令牌(多端管理) +- `resetToken(token)` — 续期令牌 + +```properties +codingapi.security.redis.valid-time=3600000 +codingapi.security.redis.rest-time=1800000 +``` + +## 使用实例 + +```java +// JWT 模式 — 创建并解析令牌 +JwtTokenGateway jwtGateway = new JwtTokenGateway(jwtProperties); +Token token = jwtGateway.create("admin", Arrays.asList("ROLE_USER", "ROLE_ADMIN")); +System.out.println(token.getToken()); // eyJhbGciOiJIUz... + +Token parsed = jwtGateway.parser(token.getToken()); +parsed.verify(); // 校验过期时间 +System.out.println(parsed.getUsername()); // "admin" + +// Redis 模式 — 多端登录管理 +RedisTokenGateway redisGateway = new RedisTokenGateway(redisTemplate, redisProperties); +Token token1 = redisGateway.create("user1", "iv123", Arrays.asList("ROLE_USER"), null); + +// 获取用户所有在线会话 +List tokens = redisGateway.getTokensByUsername("user1"); + +// 按条件踢出(如踢出非当前设备的会话) +redisGateway.removeUsername("user1", t -> !t.getToken().equals(currentToken)); + +// 强制下线 +redisGateway.removeUsername("user1"); +``` diff --git a/docs/capabilities/transaction-manager-context.md b/docs/capabilities/transaction-manager-context.md new file mode 100644 index 000000000..f1e5be8c9 --- /dev/null +++ b/docs/capabilities/transaction-manager-context.md @@ -0,0 +1,141 @@ +--- +name: transaction-manager-context +description: 单例编程式事务上下文,支持 REQUIRES_NEW 语义,在非 Spring 管理上下文中也能使用事务 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 398bf849 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextConfiguration.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextRegister.java +--- + +## 解决什么问题 + +在以下场景中,标准的 Spring `@Transactional` 声明式事务无法满足需求: + +1. **非 Spring 管理的代码**:工具类、静态方法、回调函数中无法使用 `@Transactional` 注解 +2. **需要独立新事务**:当前已处于一个事务中,但某段逻辑需要在独立的新事务中执行(`REQUIRES_NEW`),且不受外层事务回滚影响 +3. **动态事务控制**:需要根据运行时条件决定是否开启事务、使用只读事务还是读写事务 +4. **框架内部基础设施**:事件处理器、脚本引擎等框架组件需要在自身逻辑中管理事务边界 + +本能力提供了一个全局单例的编程式事务上下文,封装了 Spring `PlatformTransactionManager`,使得在任何位置都能以简洁的 Lambda 方式执行事务操作。核心特点: + +- **全局单例访问**:通过 `TransactionManagerContext.getInstance()` 在任何位置获取 +- **REQUIRES_NEW 语义**:每次调用都开启独立新事务,不与调用方共享事务 +- **自动初始化**:Spring 容器启动时自动注入 `PlatformTransactionManager` +- **优雅降级**:未配置事务管理器时直接执行业务逻辑,不抛异常 + +## 如何使用 + +### 核心 API + +#### TransactionManagerContext — 事务上下文(单例) + +```java +// 获取单例实例 +TransactionManagerContext ctx = TransactionManagerContext.getInstance(); + +// 在读写事务中执行(REQUIRES_NEW) + T commit(Supplier supplier) + +// 在只读事务中执行(REQUIRES_NEW + readOnly=true) + T readOnly(Supplier supplier) +``` + +### 自动配置 + +框架通过 `TransactionManagerContextConfiguration` 自动完成初始化: + +1. `TransactionManagerContextConfiguration` 注册 `TransactionManagerContextRegister` Bean +2. `TransactionManagerContextRegister` 实现 `InitializingBean`,在 Bean 属性设置完成后将 Spring 容器中的 `PlatformTransactionManager` 注入到 `TransactionManagerContext` 单例中 +3. 如果容器中不存在 `PlatformTransactionManager`(`@Autowired(required = false)`),则不注入,事务操作会降级为直接执行 + +无需任何额外配置,只要项目中存在数据源和事务管理器即可自动生效。 + +### 事务行为说明 + +| 方法 | 传播行为 | 只读 | 正常结束 | 异常时 | +|------|---------|------|---------|--------| +| `commit()` | REQUIRES_NEW | 否 | 提交事务 | 回滚事务并抛出异常 | +| `readOnly()` | REQUIRES_NEW | 是 | 回滚事务(只读无需提交) | 回滚事务并抛出异常 | + +> **注意**:`readOnly()` 方法在正常结束时也会调用 `rollback()` 而非 `commit()`,因为只读事务没有数据变更,回滚比提交更高效。 + +## 使用实例 + +### 1. 在工具类中使用事务 + +```java +public class DataMigrationUtil { + + public static void migrateData(List records) { + TransactionManagerContext.getInstance().commit(() -> { + // 这些操作在独立的新事务中执行 + for (Record record : records) { + recordRepository.save(record); + } + return null; + }); + } +} +``` + +### 2. 在事件处理器中使用独立事务 + +```java +@Component +public class OrderCreatedHandler implements IHandler { + + @Override + public void handler(OrderCreatedEvent event) { + // 即使外层事务回滚,库存扣减也在独立事务中完成 + TransactionManagerContext.getInstance().commit(() -> { + inventoryService.deduct(event.getProductId(), event.getQuantity()); + return null; + }); + } +} +``` + +### 3. 使用只读事务查询 + +```java +public List generateReport() { + return TransactionManagerContext.getInstance().readOnly(() -> { + // 只读事务,数据库可优化查询性能 + List orders = orderRepository.findByMonth(currentMonth); + List products = productRepository.findAll(); + return reportBuilder.build(orders, products); + }); +} +``` + +### 4. 无事务管理器时的降级行为 + +```java +// 当 PlatformTransactionManager 未注入时,supplier 直接执行,不包裹事务 +// 适用于测试环境或无数据库的场景 +String result = TransactionManagerContext.getInstance().commit(() -> { + return "direct execution without transaction"; +}); +``` + +### 5. 异常自动回滚 + +```java +try { + TransactionManagerContext.getInstance().commit(() -> { + accountService.debit(fromAccount, amount); + accountService.credit(toAccount, amount); + if (balanceCheckFailed) { + throw new IllegalStateException("余额校验失败"); + } + return null; + }); +} catch (IllegalStateException e) { + // 事务已自动回滚,debit 和 credit 操作均已撤销 + log.error("转账失败,事务已回滚", e); +} +``` diff --git a/docs/capabilities/trigger-system.md b/docs/capabilities/trigger-system.md new file mode 100644 index 000000000..79e8ca7ca --- /dev/null +++ b/docs/capabilities/trigger-system.md @@ -0,0 +1,230 @@ +--- +name: trigger-system +description: 触发器框架,支持定时/条件触发,提供统一的触发上下文管理 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 70be0d38 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/Trigger.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/TriggerHandler.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/TriggerContext.java +--- + +## 解决什么问题 + +在业务流程中,存在一类"等待特定条件满足后执行"的场景: + +- 订单超时未支付自动取消 +- 审批流程中等待上级审批后再触发下一步 +- 数据采集达到阈值后触发告警 +- 临时性的条件监听,触发一次后即失效 + +这类需求与事件系统(Event System)的区别在于:**Event 是消息驱动**(确定了消息内容,不确定谁消费),而 **Trigger 是订阅驱动**(先注册订阅规则,再等待匹配的消息到来触发执行)。Trigger 模式允许精确控制: + +- **是否进入触发逻辑**:通过 `preTrigger()` 前置判断 +- **触发后是否移除**:通过 `remove()` 控制一次性触发还是持续监听 +- **按类型隔离**:不同 Trigger 类型的 Handler 互不干扰 + +本能力提供了一个轻量级的触发器框架,支持运行时动态注册/移除触发器,适用于需要条件触发、一次性触发或临时监听的业务场景。 + +## 如何使用 + +### 核心接口 + +#### Trigger — 触发器数据对象标记接口 + +```java +public interface Trigger { + // 标记接口,具体触发器需定义自己的数据结构 +} +``` + +所有触发器数据对象必须实现此接口,作为泛型约束的基础类型。 + +#### TriggerHandler\ — 触发处理器 + +```java +public interface TriggerHandler { + // 前置判断:是否满足触发条件 + boolean preTrigger(T trigger); + + // 触发执行逻辑 + void trigger(T trigger); + + // 执行完成后是否移除此 Handler(默认不移除) + default boolean remove(T trigger, boolean canTrigger) { + return false; + } +} +``` + +- `preTrigger()` 返回 `true` 时才会执行 `trigger()` 方法 +- `remove()` 的 `canTrigger` 参数表示本次是否实际执行了 `trigger()` 逻辑 +- `remove()` 返回 `true` 时,该 Handler 会从触发器列表中移除(一次性触发器) + +#### TriggerContext — 触发器上下文(单例) + +```java +// 获取单例 +TriggerContext ctx = TriggerContext.getInstance(); + +// 注册触发处理器 +void addTrigger(TriggerHandler handler) + +// 触发指定类型的触发器 +void trigger(Trigger trigger) + +// 清空某类型的所有触发器 +void clear(Class clazz) + +// 判断某类型的触发器是否为空 +boolean isEmpty(Class clazz) +``` + +### 工作机制 + +1. **注册阶段**:调用 `addTrigger(handler)` 时,框架通过反射读取 Handler 实现的 `TriggerHandler` 泛型参数,自动推断其关联的 Trigger 类型,并按类型分组存储 +2. **触发阶段**:调用 `trigger(triggerObj)` 时,根据 triggerObj 的实际类型查找对应的 Handler 列表,依次执行: + - 调用 `preTrigger()` 判断是否满足条件 + - 满足条件则调用 `trigger()` 执行业务逻辑 + - 调用 `remove()` 判断是否需要移除该 Handler +3. **异常隔离**:单个 Handler 执行异常不会影响其他 Handler 的执行,异常会被捕获并记录 warn 日志 + +### 线程安全 + +- 触发器 Map 使用 `ConcurrentHashMap` 保证并发注册安全 +- 每个类型的 Handler 列表使用 `CopyOnWriteArrayList` 保证遍历时的并发安全 + +## 使用实例 + +### 1. 定义触发器数据对象 + +```java +// 订单超时触发器 +public class OrderTimeoutTrigger implements Trigger { + private final String orderId; + private final LocalDateTime deadline; + + public OrderTimeoutTrigger(String orderId, LocalDateTime deadline) { + this.orderId = orderId; + this.deadline = deadline; + } + + public String getOrderId() { return orderId; } + public LocalDateTime getDeadline() { return deadline; } +} +``` + +### 2. 实现一次性触发处理器 + +```java +// 订单超时自动取消(触发一次后自动移除) +public class OrderTimeoutCancelHandler implements TriggerHandler { + + @Override + public boolean preTrigger(OrderTimeoutTrigger trigger) { + // 仅在超过截止时间时才触发 + return LocalDateTime.now().isAfter(trigger.getDeadline()); + } + + @Override + public void trigger(OrderTimeoutTrigger trigger) { + log.info("订单 {} 超时未支付,自动取消", trigger.getOrderId()); + orderService.cancelOrder(trigger.getOrderId(), "超时未支付"); + } + + @Override + public boolean remove(OrderTimeoutTrigger trigger, boolean canTrigger) { + // 无论是否触发成功,都移除该 Handler(一次性) + return true; + } +} +``` + +### 3. 实现持续性触发处理器 + +```java +// 库存告警(持续监听,每次低于阈值都触发) +public class StockAlertTrigger implements Trigger { + private final String productId; + private final int currentStock; + + public StockAlertTrigger(String productId, int currentStock) { + this.productId = productId; + this.currentStock = currentStock; + } + + // getter ... +} + +public class StockAlertHandler implements TriggerHandler { + + private static final int THRESHOLD = 10; + + @Override + public boolean preTrigger(StockAlertTrigger trigger) { + return trigger.getCurrentStock() < THRESHOLD; + } + + @Override + public void trigger(StockAlertTrigger trigger) { + notificationService.sendAlert( + "商品 " + trigger.getProductId() + " 库存不足,当前: " + trigger.getCurrentStock() + ); + } + + // 使用默认 remove(),返回 false,持续监听 +} +``` + +### 4. 注册和触发 + +```java +@Service +public class OrderService { + + // 创建订单时注册超时触发器 + public void createOrder(String orderId) { + // ... 创建订单逻辑 ... + + // 注册 30 分钟超时触发器 + OrderTimeoutTrigger trigger = new OrderTimeoutTrigger( + orderId, LocalDateTime.now().plusMinutes(30)); + TriggerContext.getInstance().addTrigger(new OrderTimeoutCancelHandler()); + + // 在实际场景中,通常会结合定时任务定期调用 trigger() + } + + // 定时检查(由 ScheduledTask 调用) + @Scheduled(fixedRate = 60000) + public void checkOrderTimeout() { + OrderTimeoutTrigger trigger = new OrderTimeoutTrigger(null, null); + // 遍历待检查订单,逐个触发 + for (String orderId : pendingOrders) { + OrderTimeoutTrigger t = new OrderTimeoutTrigger(orderId, getOrderDeadline(orderId)); + TriggerContext.getInstance().trigger(t); + } + } +} +``` + +### 5. 清空触发器 + +```java +// 当订单被手动取消或支付成功时,清空该类型的所有触发器 +TriggerContext.getInstance().clear(OrderTimeoutTrigger.class); + +// 检查是否还有活跃的触发器 +boolean hasTriggers = !TriggerContext.getInstance().isEmpty(OrderTimeoutTrigger.class); +``` + +### 6. Event vs Trigger 选择指南 + +| 场景 | 推荐 | 原因 | +|------|------|------| +| 实体状态变更后通知多个下游 | Event | 消息确定,消费者不确定 | +| 等待特定条件后执行一次性操作 | Trigger | 先订阅再等待,触发后可自移除 | +| 跨模块解耦通信 | Event | 发布-订阅天然解耦 | +| 临时性条件监听 | Trigger | 可动态注册/移除,生命周期可控 | +| 定时轮询检查条件 | Trigger | 配合 preTrigger 做条件判断 | diff --git a/docs/capabilities/unified-response.md b/docs/capabilities/unified-response.md deleted file mode 100644 index 8b376026e..000000000 --- a/docs/capabilities/unified-response.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -name: unified-response -description: 统一 API 响应封装,提供 Response、SingleResponse、MultiResponse、MapResponse 四种标准响应格式 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -在企业级应用中,API 响应格式不统一会导致以下问题: - -1. **前端解析困难**:不同接口返回不同的 JSON 结构,前端需要针对每个接口做适配 -2. **错误处理不一致**:有些接口用 HTTP 状态码表示错误,有些用业务字段,缺乏统一规范 -3. **分页数据格式混乱**:列表接口的分页信息(总数、页码、列表)命名和结构各不相同 -4. **国际化支持缺失**:错误消息硬编码在代码中,无法根据用户语言动态切换 -5. **序列化兼容性**:不同 DTO 的 JSON 序列化行为不可预测 - -框架提供四种标准响应类型,所有响应都继承自 `Response` 基类,包含统一的 `success`、`errCode`、`errMessage` 字段。结合 `LocaleMessageException` 可实现错误的国际化输出。 - -## 如何使用 - -### 1. Response — 基础响应 - -仅表示操作成功或失败,不携带数据: - -```java -// 成功响应 -return Response.buildSuccess(); - -// 失败响应 -return Response.buildFailure("USER_NOT_FOUND", "用户不存在"); -``` - -### 2. SingleResponse\ — 单对象响应 - -返回单个业务对象: - -```java -// 返回数据 -User user = userService.findById(id); -return SingleResponse.of(user); - -// 返回空数据(表示查询无结果但不是错误) -return SingleResponse.empty(); -``` - -### 3. MultiResponse\ — 列表响应 - -返回集合数据,支持分页信息: - -```java -// 从 Spring Data Page 构建(自动提取 total 和 content) -Page page = userRepository.findAll(pageable); -return MultiResponse.of(page); - -// 手动指定数据和总数 -List users = userService.search(keyword); -long total = userService.countByKeyword(keyword); -return MultiResponse.of(users, total); - -// 仅传集合(total 自动设为集合大小) -return MultiResponse.of(users); - -// 空列表 -return MultiResponse.empty(); -``` - -### 4. MapResponse — Map 响应 - -返回键值对结构的灵活数据: - -```java -return MapResponse.create() - .add("userName", "张三") - .add("roleCount", 3) - .add("permissions", Arrays.asList("READ", "WRITE")); -``` - -## 使用实例 - -### Controller 中的完整使用示例 - -```java -@RestController -@RequestMapping("/api/users") -public class UserController { - - @Autowired - private UserService userService; - - /** - * 查询单个用户 - */ - @GetMapping("/{id}") - public SingleResponse getUser(@PathVariable Long id) { - UserDTO user = userService.findById(id); - if (user == null) { - // 抛出国际化异常,由全局异常处理器转为 Response - throw LocaleMessageException.of("USER_NOT_FOUND", id); - } - return SingleResponse.of(user); - } - - /** - * 分页查询用户列表 - */ - @GetMapping - public MultiResponse listUsers(PageRequest request) { - Page page = userService.findAll(request); - return MultiResponse.of(page); - } - - /** - * 创建用户 - */ - @PostMapping - public SingleResponse createUser(@RequestBody CreateUserCommand cmd) { - UserDTO user = userService.create(cmd); - return SingleResponse.of(user); - } - - /** - * 删除用户 - */ - @DeleteMapping("/{id}") - public Response deleteUser(@PathVariable Long id) { - userService.delete(id); - return Response.buildSuccess(); - } - - /** - * 获取用户统计信息 - */ - @GetMapping("/stats") - public MapResponse getStats() { - return MapResponse.create() - .add("totalUsers", userService.count()) - .add("activeUsers", userService.countActive()) - .add("todayNewUsers", userService.countTodayNew()); - } -} -``` - -### 响应 JSON 结构示例 - -**SingleResponse 成功:** -```json -{ - "success": true, - "errCode": null, - "errMessage": null, - "data": { - "id": 1, - "name": "张三", - "email": "zhangsan@example.com" - } -} -``` - -**MultiResponse 分页:** -```json -{ - "success": true, - "errCode": null, - "errMessage": null, - "data": { - "total": 100, - "list": [ - {"id": 1, "name": "张三"}, - {"id": 2, "name": "李四"} - ] - } -} -``` - -**Response 失败:** -```json -{ - "success": false, - "errCode": "USER_NOT_FOUND", - "errMessage": "用户不存在" -} -``` - -### 与全局异常处理的配合 - -当 Controller 抛出 `LocaleMessageException` 时,全局异常处理器会自动将其转换为标准的 `Response` 格式: - -```java -// Service 层抛出异常 -if (user == null) { - throw LocaleMessageException.of("USER_NOT_FOUND", userId); -} - -// 框架自动捕获并返回: -// {"success": false, "errCode": "USER_NOT_FOUND", "errMessage": "用户不存在"} -``` - -### 响应类型选择指南 - -| 场景 | 推荐类型 | 说明 | -|------|----------|------| -| 增删改操作 | `Response` | 只需告知成功/失败 | -| 查询单个对象 | `SingleResponse` | 可能为 null,用 `empty()` 表示 | -| 查询列表/分页 | `MultiResponse` | 携带 total 和 list | -| 聚合统计/灵活结构 | `MapResponse` | 键值对形式 | -| 操作失败 | `Response.buildFailure()` 或抛异常 | 推荐抛 `LocaleMessageException` | diff --git a/docs/capabilities/workflow-engine.md b/docs/capabilities/workflow-engine.md index 84c9043f7..763b690e6 100644 --- a/docs/capabilities/workflow-engine.md +++ b/docs/capabilities/workflow-engine.md @@ -1,243 +1,135 @@ --- name: workflow-engine -description: 内置工作流引擎,支持流程定义、节点流转、审批、退回、会签、抄送、委托与数据快照 +description: 工作流引擎,支持流程定义、节点流转、审批、委托、会签、数据快照和事件通知 status: 已实现 scope: 后端 source: 项目自有 +last_commit: 0fc02aca +code_files: + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/FlowConfiguration.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowProcess.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowRecord.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowBackup.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowMerge.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/FlowService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/FlowNodeService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowStartService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowSubmitService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowRecallService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowStopService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowTrySubmitService.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowWorkRepository.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowProcessRepository.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowRecordRepository.java + - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowBackupRepository.java --- ## 解决什么问题 -企业应用中审批流程是核心业务场景,但引入外部工作流引擎(如 Activiti、Camunda)往往带来过重的依赖和复杂的 BPMN 学习成本。许多中小规模审批场景只需要一个轻量、可编程、与 DDD 架构自然融合的流程引擎。 - -`springboot-starter-flow` 提供了一个自研的轻量级工作流引擎,具备以下特点: - -- **Schema 驱动**:流程定义通过 JSON Schema 描述,支持可视化编辑器生成,运行时由 `SchemaReader` 解析为 `FlowNode` + `FlowRelation` 对象图 -- **完整的审批语义**:支持通过、退回、转办、委托、会签、抄送、催办、撤销、作废、停止等操作 -- **数据快照**:流程发起时对绑定数据进行快照(`BindDataSnapshot`),确保审批过程中数据一致性 -- **版本管理**:通过 `FlowBackup` 对流程定义进行版本快照,运行中的流程始终使用发起时的版本 -- **事件驱动**:每个状态变更推送 `FlowApprovalEvent`(同步事件),业务层通过 `IHandler` 响应 -- **脚本化扩展**:节点的标题生成(`TitleGenerator`)、操作者匹配(`OperatorMatcher`)、异常触发(`ErrTrigger`)均支持脚本表达式 +企业级审批场景(请假、报销、采购等)需要标准化的流程引擎来管理: +- **流程定义与版本管理**:流程模板可修改、可版本化,运行中流程不受定义变更影响 +- **节点流转规则**:支持串行、并行(会签)、条件分支等多种流转模式 +- **审批操作**:通过、驳回、退回、撤回、转办、催办、终止等完整操作集 +- **数据快照**:发起时锁定流程定义版本,确保审批过程一致性 +- **事件通知**:每个状态变更自动推送 `FlowApprovalEvent`,驱动业务联动 ## 如何使用 -### 1. 引入依赖 +### 核心数据模型 -```xml - - com.codingapi.springboot - springboot-starter-flow - ``` +FlowWork(流程定义) + └── FlowNode(节点) + └── FlowSource(连线/分支条件) -### 2. 实现 Repository 接口 - -框架定义了以下仓储接口,需由基础设施层提供持久化实现: +FlowProcess(流程实例) + └── FlowRecord(审批记录) + └── FlowBackup(版本快照) +``` -| 接口 | 职责 | +| 模型 | 说明 | |------|------| -| `FlowWorkRepository` | 流程定义的增删改查 | -| `FlowRecordRepository` | 审批记录的保存与查询 | -| `FlowBackupRepository` | 流程版本快照的存储与恢复 | -| `FlowProcessRepository` | 流程实例的管理 | -| `FlowOperatorRepository` | 流程参与者的查询(`findByIds`) | -| `FlowBindDataRepository` | 绑定数据快照的存储 | +| `FlowWork` | 流程模板定义,包含节点列表和连线关系 | +| `FlowProcess` | 流程实例(由 `FlowStartService` 创建),关联 backupId 锁定版本 | +| `FlowRecord` | 审批记录,记录每个节点的审批人、结果、意见、时间 | +| `FlowBackup` | 流程定义快照(Kryo 序列化),确保运行中流程不受模板修改影响 | + +### 服务层 API + +| 服务 | 方法 | 说明 | +|------|------|------| +| `FlowStartService` | `startFlow(...)` | 发起流程,创建 FlowProcess + FlowBackup + 初始 FlowRecord | +| `FlowSubmitService` | `submitFlow(...)` | 提交审批(通过/驳回) | +| `FlowRecallService` | `recallFlow(...)` | 撤回已提交的流程 | +| `FlowTrySubmitService` | `trySubmitFlow(...)` | 试提交 — 获取下一节点审批人(不实际流转) | +| `FlowStopService` | `stopFlow(...)` | 终止流程 | +| `FlowUrgeService` | `urgeFlow(...)` | 催办 | +| `FlowNodeService` | `getFlowRecords(...)` | 获取流程审批记录 | +| `FlowStepService` | `getFlowSteps(...)` | 获取流程步骤图 | +| `FlowCustomEventService` | `customEvent(...)` | 触发自定义按钮事件 | + +### 流程状态(FlowStatus) -### 3. 实现 IFlowOperator 接口 - -所有流程参与者需实现 `IFlowOperator` 接口: - -```java -public interface IFlowOperator { - long getUserId(); // 用户ID - String getName(); // 用户名称 - boolean isFlowManager(); // 是否流程管理员(可强制干预流程) - IFlowOperator entrustOperator(); // 委托操作者(不为空时由委托人执行) -} ``` - -### 4. 设计流程 - -通过 `FlowWork` 构建流程定义: - -```java -FlowWork flowWork = new FlowWork(createUser); -flowWork.setTitle("请假审批"); -flowWork.setCode("leave_approval"); -flowWork.setDescription("员工请假审批流程"); - -// 通过 Schema 解析节点和关系 -flowWork.schema(schemaJson); - -// 验证并启用 -flowWork.verify(); -flowWork.enable(); +RUNNING → FINISH / VOIDED / STOP ``` -核心领域对象: -- **`FlowWork`**:流程定义,包含节点列表(`nodes`)和关系列表(`relations`),支持 `copy()` 复制、`schema()` 解析、`verify()` 校验 -- **`FlowNode`**:流程节点,具有 `code`、`type`(NodeType)、`approvalType`(ApprovalType)、`titleGenerator`、`operatorMatcher`、`errTrigger`、`buttons`、`timeout` 等属性 -- **`FlowRelation`**:节点间关系,定义源节点到目标节点的流转规则,支持条件触发(`OutTrigger`)和回退标记(`isBack`) -- **`FlowRecord`**:审批记录,记录每个节点的审批状态、操作者、意见等 - -### 5. 发起流程 - -使用 `FlowStartService` 发起流程: - -```java -FlowStartService startService = new FlowStartService( - workCode, // 流程编码 - operator, // 发起人 (IFlowOperator) - bindData, // 绑定数据 (IBindData) - advice, // 发起意见 - repositoryHolder // 仓储持有者 -); - -FlowResult result = startService.startFlow(); -``` +### 审批操作类型(FlowType) -`startFlow()` 的执行流程: -1. 加载并验证流程定义(`loadFlowWork`) -2. 创建或获取流程版本快照(`loadFlowBackup`) -3. 保存流程实例(`saveFlowProcess`) -4. 保存绑定数据快照(`saveBindDataSnapshot`) -5. 构建节点服务并创建待办记录(`buildFlowNodeService` → `createRecord`) -6. 推送 `FlowApprovalEvent` 事件(STATE_CREATE / STATE_TODO / STATE_SAVE / STATE_FINISH) +CREATE、SAVE、PASS、REJECT、RECALL、DELETE、VOIDED、BACK、FINISH、TRANSFER、URGE、STOP -### 6. 提交审批 +### 事件通知 -使用 `FlowSubmitService` 处理审批操作: - -```java -FlowSubmitService submitService = new FlowSubmitService( - recordId, // 当前审批记录ID - currentOperator, // 当前操作者 - bindData, // 绑定数据 - opinion, // 审批意见 (Opinion) - repositoryHolder // 仓储持有者 -); -``` - -`Opinion` 封装了审批动作: -- `Opinion.pass(advice)` — 通过 -- `Opinion.reject(advice)` — 拒绝 -- `Opinion.transfer(operatorIds)` — 转办 -- 支持设置自定义操作者 `opinion.getOperatorIds()` - -### 7. 查看流程步骤预览 - -使用 `FlowStepService` 获取流程步骤预览信息: - -```java -FlowStepService stepService = new FlowStepService( - recordId, // 记录ID(0 表示新流程预览) - workCode, // 流程编码 - currentOperator, // 当前操作者 - bindData, // 绑定数据 - repositoryHolder // 仓储持有者 -); - -FlowStepResult stepResult = stepService.getFlowStep(); -``` - -### 8. 监听流程事件 - -实现 `IHandler` 响应流程状态变更: +每次状态变更通过 `EventPusher` 推送 `FlowApprovalEvent`,业务侧通过 `IHandler` 监听: ```java @Component -public class FlowEventHandler implements IHandler { - +public class LeaveHandler implements IHandler { @Override public void handler(FlowApprovalEvent event) { - if (event.isCreate()) { - // 流程创建 - } else if (event.isTodo()) { - // 新待办产生,发送通知 - } else if (event.isPass()) { - // 审批通过 - } else if (event.isReject()) { - // 审批拒绝 - } else if (event.isFinish()) { - // 流程结束 - } else if (event.isTransfer()) { - // 转办 - } else if (event.isCirculate()) { - // 抄送 + if (event.getFlowType() == FlowType.FINISH) { + // 审批完成 — 更新请假状态 } - // ... 其他状态 } } ``` -`FlowApprovalEvent` 支持的状态常量: - -| 常量 | 值 | 含义 | -|------|-----|------| -| `STATE_CREATE` | 1 | 创建流程 | -| `STATE_PASS` | 2 | 审批通过 | -| `STATE_REJECT` | 3 | 审批拒绝 | -| `STATE_TRANSFER` | 4 | 转办 | -| `STATE_RECALL` | 5 | 撤销 | -| `STATE_FINISH` | 6 | 流程完成 | -| `STATE_TODO` | 7 | 创建待办 | -| `STATE_URGE` | 8 | 催办 | -| `STATE_CIRCULATE` | 9 | 抄送 | -| `STATE_SAVE` | 10 | 保存 | -| `STATE_DELETE` | 11 | 删除 | -| `STATE_BACK` | 12 | 退回 | -| `STATE_VOIDED` | 13 | 作废 | -| `STATE_STOP` | 14 | 停止 | - -## 使用实例 - -### 流程流转方向控制 - -`FlowDirectionService` 负责解析审批方向: +### 配置 ```java -FlowDirectionService directionService = - new FlowDirectionService(flowNode, flowWork, opinion); -directionService.bindHistoryRecords(historyRecords); -directionService.loadFlowSourceDirection(); - -// 判断是否为通过方向 -if (directionService.isPassRecord()) { - flowNodeService.loadNextPassNode(currentNode); -} else { - flowNodeService.loadDefaultBackNode(currentNode, currentRecord); +@Configuration +public class FlowConfiguration { + // 自动注册所有 Flow Repository 和 Service + // 通过 FlowFrameworkRegister 注册框架级触发器 } ``` -### 会签处理 - -对于会签节点(`flowNode.isSign() == true`),`FlowDirectionService.reloadFlowSourceDirection()` 会根据历史记录中所有人的提交状态重新计算审批方向——只有所有人通过后才会继续流转,否则转为拒绝方向。 - -### 流程版本快照 +## 使用实例 ```java -// 自动备份(在 FlowStartService 中调用) -FlowBackup backup = flowBackupRepository.backup(flowWork); +// 1. 发起请假流程 +FlowProcess process = flowStartService.startFlow( + workId, // 流程定义ID + operatorId, // 发起人ID + bindDataId // 关联业务数据ID +); -// 从快照恢复流程定义 -FlowWork restoredWork = backup.resume(flowOperatorRepository); -``` +// 2. 试提交 — 查看下一节点审批人 +List nextApprovers = flowTrySubmitService.trySubmitFlow( + process.getProcessId(), operatorId, "同意请假" +); -`FlowBackup` 将 `FlowWork` 序列化为字节数组存储,恢复时通过 `FlowWorkSerializable.fromSerializable(bytes).toFlowWork()` 反序列化,确保运行中的流程不受后续流程定义修改的影响。 +// 3. 正式提交审批 +flowSubmitService.submitFlow( + process.getProcessId(), operatorId, "同意请假", ApprovalType.PASS +); -### FlowSession 上下文 +// 4. 撤回(仅在下一节点未审批前可撤回) +flowRecallService.recallFlow(process.getProcessId(), operatorId); -`FlowSession` 封装了流程运行时的完整上下文,供 `TitleGenerator`、`OperatorMatcher`、`ErrTrigger`、`FlowRelation.trigger()` 等脚本化组件使用: +// 5. 查询审批记录 +List records = flowNodeService.getFlowRecords(process.getProcessId()); -```java -FlowSession session = new FlowSession( - flowRecord, // 当前审批记录 - flowWork, // 流程定义 - flowNode, // 当前节点 - createOperator, // 发起人 - currentOperator, // 当前操作者 - bindData, // 绑定数据 - opinion, // 审批意见 - historyRecords // 历史记录 -); +// 6. 终止流程 +flowStopService.stopFlow(process.getProcessId(), operatorId, "业务取消"); ``` diff --git a/docs/conventions/autoconfiguration-pattern.md b/docs/conventions/autoconfiguration-pattern.md new file mode 100644 index 000000000..e33758ace --- /dev/null +++ b/docs/conventions/autoconfiguration-pattern.md @@ -0,0 +1,134 @@ +--- +name: autoconfiguration-pattern +description: 每个 starter 模块必须同时注册 spring.factories(旧版兼容)和 AutoConfiguration.imports(Spring Boot 3.x 标准),使用 @ConfigurationProperties + Context 单例模式 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 303b377f +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/properties/PropertiesContext.java + - springboot-starter/src/main/resources/META-INF/spring.factories + - springboot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +--- + +## 解决什么问题 + +创建 Spring Boot Starter 时,如果不遵循标准的自动配置注册规范,会导致以下问题: + +1. **版本兼容性断裂**:Spring Boot 3.x 废弃了 `spring.factories` 中的 `EnableAutoConfiguration` 键,仅支持 `AutoConfiguration.imports`;但部分运行环境或工具链仍读取旧格式,缺失任一都会导致自动配置不生效 +2. **配置属性无法绑定**:不使用 `@ConfigurationProperties` 注解则无法将 `application.properties` 中的配置自动映射到 Java 对象 +3. **非 Spring 上下文无法访问配置**:在工具类、拦截器、代理等非 Bean 环境中,无法通过 `@Autowired` 注入配置,需要额外的 Context 单例桥接 +4. **新模块遗漏注册**:没有统一的注册约定,新增 starter 时容易忘记配置文件导致功能静默失效 + +## 如何使用 + +### 1. 双文件注册(必须同时维护) + +在每个 starter 模块的 `src/main/resources/META-INF/` 下创建两个文件: + +**`spring.factories`**(兼容 Spring Boot 2.x 及旧工具链): +```properties +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.example.mystarter.MyAutoConfiguration,\ +com.example.mystarter.config.SomeConfiguration +``` + +**`spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`**(Spring Boot 3.x 标准): +``` +com.example.mystarter.MyAutoConfiguration +com.example.mystarter.config.SomeConfiguration +``` + +> ⚠️ 两个文件中列出的配置类**必须完全一致**,仅格式不同。 + +### 2. ConfigurationProperties + Context 模式 + +对于需要在非 Spring Bean 中访问的配置: + +1. 定义 Properties 类并标注 `@ConfigurationProperties(prefix = "...")` +2. 定义对应的 `*Context` 单例类持有该 Properties +3. 在 `@Configuration` 类的 `@Bean` 方法中创建 Properties 并同步设置到 Context + +### 3. 规则 + +1. **新增 starter 模块时必须同时创建两个注册文件** +2. 修改自动配置类列表时,两个文件必须同步更新 +3. 配置类使用 `@Configuration` 注解,可配合 `@ConditionalOnClass` / `@ConditionalOnProperty` 做条件加载 +4. 需要暴露给非 Bean 环境的配置必须通过 Context 单例桥接 + +## 使用实例 + +### ✅ 正确示例 + +**目录结构:** +``` +springboot-starter-mymodule/ + src/main/ + java/com/example/mymodule/ + AutoConfiguration.java + MyModuleProperties.java + MyModuleContext.java + resources/META-INF/ + spring.factories + spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +``` + +**spring.factories:** +```properties +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.example.mymodule.AutoConfiguration +``` + +**AutoConfiguration.imports:** +``` +com.example.mymodule.AutoConfiguration +``` + +**AutoConfiguration.java:** +```java +@Configuration +public class AutoConfiguration implements InitializingBean { + + @Bean + @ConfigurationProperties(prefix = "codingapi.mymodule") + public MyModuleProperties myModuleProperties() { + MyModuleProperties properties = new MyModuleProperties(); + MyModuleContext.getInstance().setProperties(properties); + return properties; + } + + @Override + public void afterPropertiesSet() throws Exception { + // 启动时的初始化逻辑 + } +} +``` + +### ❌ 错误示例 + +```properties +# 错误:仅有 spring.factories,缺少 AutoConfiguration.imports +# Spring Boot 3.x 环境下自动配置不会生效 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.example.mymodule.AutoConfiguration +``` + +```java +// 错误:直接使用 @Value 注入配置,非 Bean 环境无法获取 +@Component +public class MyService { + @Value("${codingapi.mymodule.timeout}") + private int timeout; +} + +// 错误:两个文件内容不一致 +// spring.factories 列出了 A, B +// AutoConfiguration.imports 只列出了 A +// → 某些环境下 B 配置不生效,难以排查 + +// 错误:配置类未使用 @ConfigurationProperties +@Bean +public MyModuleProperties myModuleProperties() { + return new MyModuleProperties(); // 属性不会被自动绑定 +} +``` diff --git a/docs/conventions/context-singleton.md b/docs/conventions/context-singleton.md new file mode 100644 index 000000000..7489a39c4 --- /dev/null +++ b/docs/conventions/context-singleton.md @@ -0,0 +1,124 @@ +--- +name: context-singleton +description: 全局状态通过单例 Context 类持有,由 Register 类在启动时注入依赖 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 303b377f +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/properties/PropertiesContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/MessageContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextRegister.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/user/UserContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/RestTemplateContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/DomainEventContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/TriggerContext.java + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/TokenContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLInterceptorContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationContext.java +--- + +## 解决什么问题 + +框架层工具类(如事件推送、事务管理、异常国际化)需要在非 Spring 管理的上下文中使用(例如 Groovy 脚本运行时、静态方法调用、线程池异步任务)。如果直接依赖 Spring Bean 注入,在这些场景中将无法获取实例。 + +单例 Context 模式解决了这个问题:通过静态单例持有全局状态,由 Spring 管理的 `*Register` 类在应用启动时注入所需依赖,使得框架能力在任何地方都能通过 `XxxContext.getInstance()` 访问。 + +## 如何使用 + +### 命名规范 + +| 类后缀 | 职责 | 示例 | +|--------|------|------| +| `*Context` | 持有全局状态的单例类 | `PropertiesContext`, `UserContext`, `TransactionManagerContext` | +| `*Register` | `InitializingBean` 实现,启动时注入依赖到 Context | `TransactionManagerContextRegister` | +| `*Configuration` | `@Configuration` 类,注册 Context Bean 和 Register Bean | `TransactionManagerContextConfiguration` | + +### Context 类结构 + +```java +public class XxxContext { + // 饿汉式单例(推荐)或懒汉式(需双重检查锁) + @Getter + private static final XxxContext instance = new XxxContext(); + + private XxxContext() {} + + // 注入的依赖(由 Register 设置) + private SomeDependency dependency; + + public void setDependency(SomeDependency dependency) { + this.dependency = dependency; + } + + // 对外 API + public void doSomething() { + dependency.execute(); + } +} +``` + +### Register 类结构 + +```java +public class XxxContextRegister implements InitializingBean { + private final SomeDependency dependency; + + public XxxContextRegister(SomeDependency dependency) { + this.dependency = dependency; + } + + @Override + public void afterPropertiesSet() { + XxxContext.getInstance().setDependency(dependency); + } +} +``` + +### 已注册的 Context 类 + +| Context | 持有状态 | 用途 | +|---------|---------|------| +| `PropertiesContext` | `FrameworkProperties` | 框架配置属性 | +| `MessageContext` | `MessageSource` | 国际化消息 | +| `TransactionManagerContext` | `PlatformTransactionManager` | 编程式事务 | +| `UserContext` | `ThreadLocal` | 当前登录用户 | +| `RestTemplateContext` | `RestTemplate` | HTTP 客户端 | +| `DomainEventContext` | 领域事件上下文 | 事件变更追踪 | +| `TokenContext` | `ThreadLocal` | 当前 Token 扩展信息 | +| `SQLInterceptorContext` | SQL 拦截状态 | 数据权限上下文 | +| `TriggerContext` | 触发器注册表 | 触发器管理 | + +## 使用实例 + +```java +// ✅ 正确 — 通过 Context 单例访问 +public class GroovyScriptRuntime { + public Object execute(String script) { + // 在 Groovy 脚本中使用框架事务 + return TransactionManagerContext.getInstance().commit(() -> { + return runScript(script); + }); + } +} + +// ✅ 正确 — Register 在启动时注入 +@Bean +public TransactionManagerContextRegister transactionManagerContextRegister( + PlatformTransactionManager transactionManager) { + return new TransactionManagerContextRegister(transactionManager); +} + +// ❌ 错误 — 在非 Spring 管理的类中直接注入 Bean +public class GroovyScriptRuntime { + @Autowired // 无法注入!此类不是 Spring Bean + private PlatformTransactionManager transactionManager; +} + +// ❌ 错误 — 在静态工具类中使用 ThreadLocal 之外的 Spring Bean +public class EventPusher { + @Autowired // 静态上下文无法注入 + private static ApplicationEventPublisher publisher; +} +``` diff --git a/docs/conventions/ddd-layered-architecture.md b/docs/conventions/ddd-layered-architecture.md deleted file mode 100644 index c654008b6..000000000 --- a/docs/conventions/ddd-layered-architecture.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -name: ddd-layered-architecture -description: DDD 分层架构与依赖规范 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -项目采用领域驱动设计(DDD)分层架构,解决了以下问题: - -- **关注点分离**:将接口、应用、领域、基础设施四层职责清晰划分 -- **领域纯净**:domain 层不依赖任何外部框架和基础设施实现,只包含纯业务逻辑 -- **可替换性**:infra 层的实现可以替换(如 JPA → MyBatis),不影响 domain 层 -- **CQRS 分离**:查询侧(Query)和命令侧(Command)独立演进,避免读写耦合 - -## 如何使用 - -### 分层结构与依赖方向 - -``` -interface 层(Controller + Handler + Runner) - ↓ 依赖 -app 层(Query Service / Cmd Router) - ↓ 依赖 -domain 层(Entity / Repository接口 / Domain Service / Event / Gateway接口) - ↑ 实现(反向依赖) -infra 层(JPA Entity / Repository实现 / Convertor / Flow集成 / Security配置) -``` - -**核心规则**:依赖只能从上到下,domain 层绝不依赖 infra 层。infra 层通过实现 domain 层定义的接口来注入。 - -### 模块命名规范 - -| 层级 | 模块命名 | 示例 | -|------|---------|------| -| 接口层 | `example-interface` | Controller、Handler、Runner | -| 应用查询 | `example-app-query` | QueryService | -| 应用命令 | `example-app-cmd-domain` | Router / CmdService | -| 应用元数据 | `example-app-cmd-meta` | 元数据相关命令 | -| 领域 | `example-domain-{聚合名}` | Entity、Repository接口、Service | -| 基础设施 | `example-infra-{技术}` | JPA实现、Flow集成、Security配置 | - -### 包结构规范 - -``` -com.codingapi.example. -├── api.domain/ # interface 层 - 命令 Controller -├── api.query/ # interface 层 - 查询 Controller -├── handler/ # interface 层 - 事件 Handler -├── runner/ # interface 层 - 启动 Runner -├── app.query.service/ # app 层 - 查询服务 -├── app.cmd.domain/ # app 层 - 命令路由 -├── domain.{聚合}.entity/ # domain 层 - 实体 -├── domain.{聚合}.repository/ # domain 层 - 仓库接口 -├── domain.{聚合}.service/ # domain 层 - 领域服务 -├── domain.{聚合}.event/ # domain 层 - 领域事件 -├── domain.{聚合}.gateway/ # domain 层 - 外部网关接口 -├── infra.db.entity/ # infra 层 - JPA Entity -├── infra.db.jpa/ # infra 层 - JPA Repository -├── infra.db.repository/ # infra 层 - Domain Repository 实现 -└── infra.db.convert/ # infra 层 - Entity 转换器 -``` - -## 使用实例 - -### ✅ 正确示例:Domain 层定义 Repository 接口 - -```java -// example-domain-user —— 纯 Java 接口,无任何框架依赖 -package com.codingapi.example.domain.user.repository; - -public interface UserRepository { - User getUserByUsername(String username); - User getUserById(long id); - void save(User user); - void delete(long id); -} -``` - -### ✅ 正确示例:Infra 层实现 Repository - -```java -// example-infra-jpa —— 实现 domain 层接口,内部委托 JPA -@Repository -@AllArgsConstructor -public class UserRepositoryImpl implements UserRepository { - - private final UserEntityRepository userEntityRepository; - - @Override - public User getUserByUsername(String username) { - return UserConvertor.convert( - userEntityRepository.getUserEntityByUsername(username), - userEntityRepository - ); - } - - @Override - public void save(User user) { - UserEntity entity = UserConvertor.convert(user); - entity = userEntityRepository.save(entity); - user.setId(entity.getId()); - } -} -``` - -### ✅ 正确示例:Interface 层按 CQRS 分离 Controller - -```java -// 查询 Controller —— 调用 app-query 服务 -@RestController -@RequestMapping("/api/query/user") -@AllArgsConstructor -public class UserQueryController { - private final UserQueryService userQueryService; - - @GetMapping("/list") - public MultiResponse list(SearchRequest searchRequest) { - return MultiResponse.of(userQueryService.list(searchRequest)); - } -} - -// 命令 Controller —— 调用 app-cmd 路由 -@RestController -@RequestMapping("/api/cmd/user") -@AllArgsConstructor -public class UserDomainCmdController { - private final UserRouter userRouter; - - @PostMapping("/save") - public Response save(@RequestBody UserCmd.UpdateRequest request) { - userRouter.createOrUpdate(request); - return Response.buildSuccess(); - } -} -``` - -### ❌ 错误示例:Domain 层直接依赖 JPA - -```java -// domain 层不应引入 javax.persistence / jakarta.persistence -@Entity // ← 禁止!domain 层不能使用 JPA 注解 -@Table(name = "t_user") -public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; -} -``` - -### ❌ 错误示例:Domain 层依赖 Infra 层的具体类 - -```java -package com.codingapi.example.domain.user.service; - -import com.codingapi.example.infra.db.jpa.UserEntityRepository; // ← 禁止! - -public class UserService { - private final UserEntityRepository repository; // 应使用 UserRepository 接口 -} -``` - -### ❌ 错误示例:查询和命令混在同一个 Controller - -```java -@RestController -@RequestMapping("/api/user") -public class UserController { - // 查询方法 - @GetMapping("/list") - public MultiResponse list() { ... } - - // 命令方法 —— 不应与查询混在一起 - @PostMapping("/save") - public Response save(@RequestBody UserCmd cmd) { ... } - - @DeleteMapping("/remove") - public Response remove(@RequestBody IdRequest req) { ... } -} -``` diff --git a/docs/conventions/event-handler-pattern.md b/docs/conventions/event-handler-pattern.md deleted file mode 100644 index b00f4aba1..000000000 --- a/docs/conventions/event-handler-pattern.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -name: event-handler-pattern -description: 事件处理器自动注册规范 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -框架自建的事件系统需要一套统一的 Handler 编写和注册规范,以确保: - -- **自动发现**:新增 Handler 无需手动注册,Spring 容器启动时自动扫描并挂载 -- **类型安全**:通过泛型 `IHandler` 确保 Handler 只接收匹配的事件类型 -- **执行顺序可控**:同一事件的多个 Handler 可通过 `order()` 控制执行顺序 -- **异常隔离**:单个 Handler 的异常不会阻断其他 Handler 的执行(除非主动抛出) -- **循环检测**:自动检测事件链中的循环调用,防止无限递归 - -## 如何使用 - -### 核心接口 - -```java -// 事件标记接口 -public interface IEvent extends Serializable {} -public interface ISyncEvent extends IEvent {} // 同步事件 -public interface IAsyncEvent extends IEvent {} // 异步事件 - -// 事件处理器 -public interface IHandler { - default int order() { return 0; } // 执行顺序,数值越小越先执行 - void handler(T event); // 事件处理逻辑 - default void error(Exception exception) throws Exception { - throw exception; // 默认重新抛出异常 - } -} -``` - -### 注册流程 - -1. 编写 Handler 类,实现 `IHandler` 接口 -2. 标记为 Spring Bean(`@Service`、`@Component` 等) -3. `HandlerBeanDefinitionRegistrar` 在启动时自动扫描所有带 `@Handler` 注解的类 -4. `SpringDefaultEventHandler` / `SpringTransactionEventHandler` 构造函数中收集所有 `IHandler` Bean,注册到 `ApplicationHandlerUtils` -5. 事件推送时由 `ApplicationHandlerUtils.handler()` 按类型匹配 + order 排序后依次调用 - -### 推送事件 - -```java -// 同步推送(默认) -EventPusher.push(event); - -// 指定是否允许循环事件 -EventPusher.push(event, hasLoopEvent); -``` - -## 使用实例 - -### ✅ 正确示例:标准 Handler 编写 - -```java -@Slf4j -@Service -@AllArgsConstructor -public class LeaveHandler implements IHandler { - - private final LeaveService leaveService; - - @Override - public void handler(FlowApprovalEvent event) { - log.info("LeaveHandler: {}", event); - if (event.isFinish() && event.match(LeaveForm.class)) { - LeaveForm form = (LeaveForm) event.getBindData(); - leaveService.create(form.toLeave()); - } - } - - @Override - public int order() { - return 10; // 明确指定执行顺序 - } - - @Override - public void error(Exception exception) throws Exception { - log.error("LeaveHandler error", exception); - // 自定义异常处理,不重新抛出则不会阻断后续 Handler - } -} -``` - -### ✅ 正确示例:定义同步/异步事件 - -```java -// 同步事件 —— 在当前线程中执行 -public class FlowApprovalEvent implements ISyncEvent { - // ... -} - -// 异步事件 —— 在线程池中执行 -public class UserNotificationEvent implements IAsyncEvent { - // ... -} - -// 默认同步(未实现 ISyncEvent 或 IAsyncEvent 时视为同步) -public class UserEvent implements IEvent { - // ... -} -``` - -### ❌ 错误示例:忘记标记为 Spring Bean - -```java -// 没有 @Service/@Component,不会被自动发现和注册 -public class LeaveHandler implements IHandler { - @Override - public void handler(FlowApprovalEvent event) { - // 永远不会被调用 - } -} -``` - -### ❌ 错误示例:在 Handler 中触发相同类型的事件导致循环 - -```java -@Service -public class RecursiveHandler implements IHandler { - @Override - public void handler(UserEvent event) { - // 再次推送同类型事件 → 触发 EventLoopException - EventPusher.push(new UserEvent()); - } -} -``` - -### ❌ 错误示例:在 error() 中静默吞掉异常且不记录 - -```java -@Override -public void error(Exception exception) throws Exception { - // 既不记录日志也不重新抛出,问题被完全隐藏 -} -``` diff --git a/docs/conventions/frontend-css-variables.md b/docs/conventions/frontend-css-variables.md deleted file mode 100644 index 353d3c9a3..000000000 --- a/docs/conventions/frontend-css-variables.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -name: frontend-css-variables -description: 前端 CSS 变量定义与使用规范 -status: 已实现 -scope: 前端 -source: 项目自有 ---- - -## 解决什么问题 - -在前端项目中,主题颜色、字体大小、背景色等设计令牌(Design Tokens)需要在多处复用,且需要与 Ant Design / Ant Design Mobile 组件库的主题配置保持同步。如果直接在样式文件中硬编码颜色值或尺寸,会导致: - -1. **主题不一致**:修改主色时需要逐个文件查找替换,容易遗漏 -2. **JS/CSS 不同步**:Ant Design 主题 token 和 CSS 样式使用不同的颜色来源 -3. **多端适配困难**:admin-ui 和 mobile-ui 有不同的设计令牌,但缺乏统一的管理方式 - -本规范通过 CSS Custom Properties(CSS 变量)+ SCSS 变量桥接 + `CSSUtils.getRootVariable()` 运行时读取三层机制,实现设计令牌的单一来源管理。 - -## 如何使用 - -### 1. 在 `index.scss` 的 `:root` 中定义 CSS 变量 - -所有 CSS 变量的**实际值**必须在 `src/styles/index.scss` 的 `:root` 选择器中定义。这是唯一的设计令牌来源。 - -**admin-ui/src/styles/index.scss** 定义的变量: -```scss -:root { - --primary-color: #094edc; - --body-background-color: #fdfdfd; - - --content-font-size-large: 24px; - --content-font-size-middle: 16px; - --content-font-size-small: 12px; - - --content-font-size: var(--content-font-size-middle); -} -``` - -**mobile-ui/src/styles/index.scss** 定义的变量: -```scss -:root { - --primary-color: #0f58ea; - --body-background-color: #e6e7ea; - - --content-font-size-large: 24px; - --content-font-size-middle: 16px; - --content-font-size-small: 12px; - - --content-font-size: var(--content-font-size-middle); -} - -// 移动端额外:将主色注入 antd-mobile 主题 -:root:root { - --adm-color-primary: var(--primary-color); -} -``` - -### 2. 在 `variable.scss` 中创建 SCSS 变量桥接 - -为了让 SCSS 文件也能使用这些变量,在 `src/styles/variable.scss` 中用 SCSS 变量包装 CSS 变量,并提供 fallback 值: - -```scss -// 主题颜色 -$theme-primary-color: var(--primary-color, #4a79d8); -// 背景颜色 -$body-background-color: var(--body-background-color, #fdfdfd); -// 标题字体大小 -$title-font-size: var(--content-font-size-middle, 16px); -// 内容字体大小 -$content-font-size: var(--content-font-size, 12px); -``` - -> mobile-ui 的 `variable.scss` 还定义了移动端专属变量(如 `$page-header-height`、`$page-footer-height`),这些不需要 CSS 变量形式,可直接使用固定值。 - -### 3. 在 SCSS 文件中通过 `@use` 引入变量 - -```scss -@use "@/styles/variable" as *; - -.page-header { - background-color: white; - height: $page-header-height !important; - box-shadow: inset 0 0 0 1px $body-background-color; -} -``` - -### 4. 在 TypeScript/React 中通过 `CSSUtils.getRootVariable()` 读取 - -当需要在 JS/TS 代码中获取 CSS 变量值时(如配置 Ant Design 主题),使用 `@codingapi/ui-framework` 提供的 `CSSUtils`: - -```typescript -import {CSSUtils, ThemeConfig, ThemeProvider} from "@codingapi/ui-framework"; - -const theme = { - token: { - colorPrimary: CSSUtils.getRootVariable('--primary-color'), - contentFontSize: CSSUtils.getRootVariable('--content-font-size'), - } -} as ThemeConfig; -``` - -然后将 `theme` 同时传给 `` 和 Ant Design 的 ``,确保框架组件和 UI 库使用同一套主题。 - -## 使用实例 - -### ✅ 正确示例 - -**在 SCSS 中使用变量:** -```scss -@use "@/styles/variable" as *; - -.my-component { - color: $theme-primary-color; - font-size: $content-font-size; - background-color: $body-background-color; -} -``` - -**在 TSX 中注入 Ant Design 主题:** -```tsx -import {CSSUtils, ThemeConfig, ThemeProvider} from "@codingapi/ui-framework"; -import {ConfigProvider} from "antd"; - -const theme = { - token: { - colorPrimary: CSSUtils.getRootVariable('--primary-color'), - contentFontSize: CSSUtils.getRootVariable('--content-font-size'), - } -} as ThemeConfig; - - - - - - -``` - -**在 `:root` 中添加新的 CSS 变量:** -```scss -// index.scss -:root { - --primary-color: #094edc; - --body-background-color: #fdfdfd; - // ✅ 新增变量放在 :root 中 - --sidebar-width: 240px; -} -``` - -```scss -// variable.scss -// ✅ 对应添加 SCSS 桥接变量 -$sidebar-width: var(--sidebar-width, 240px); -``` - -### ❌ 错误示例 - -**直接硬编码颜色值:** -```scss -// ❌ 不要直接使用十六进制颜色 -.my-component { - color: #094edc; - background-color: #fdfdfd; -} -``` - -**在 SCSS 中直接使用 CSS 变量而不经过 variable.scss 桥接:** -```scss -// ❌ 缺少 fallback 值,且没有统一管理 -.my-component { - color: var(--primary-color); -} -``` - -**在 JS 中手动读取 CSS 变量而不用 CSSUtils:** -```typescript -// ❌ 不要手动操作 DOM API -const color = getComputedStyle(document.documentElement) - .getPropertyValue('--primary-color'); -``` - -**在组件内联样式中硬编码主题色:** -```tsx -// ❌ 内联样式无法响应主题变更 -
标题
-``` - -**mobile-ui 忘记注入 antd-mobile 主题变量:** -```scss -// ❌ mobile-ui 的 index.scss 中缺少以下配置会导致 antd-mobile 组件颜色不一致 -// 必须包含: -:root:root { - --adm-color-primary: var(--primary-color); -} -``` diff --git a/docs/conventions/frontend-http-client.md b/docs/conventions/frontend-http-client.md deleted file mode 100644 index ac936a02b..000000000 --- a/docs/conventions/frontend-http-client.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -name: frontend-http-client -description: 前端 HTTP 请求封装规范 -status: 已实现 -scope: 前端 -source: 项目自有 ---- - -## 解决什么问题 - -前端应用需要与后端 REST API 交互,如果直接使用 axios 或 fetch,会面临以下问题: - -1. **错误处理分散**:每个请求都要手动 try/catch 并显示错误提示,代码重复且容易遗漏 -2. **反馈方式不一致**:不同页面使用不同的消息提示组件(message、notification、Toast),用户体验不统一 -3. **API 调用无结构**:请求散落在组件中,没有按业务领域组织,难以维护和复用 -4. **超时和认证缺失**:缺少统一的超时配置和 Token 注入机制 - -本项目通过 `@codingapi/ui-framework` 提供的 `HttpClient` 类进行统一封装,结合按领域组织的 API 模块文件,实现标准化的 HTTP 请求管理。 - -## 如何使用 - -### 1. 创建 HttpClient 实例 - -在 `src/api/index.ts` 中创建全局单例的 `httpClient`,配置超时时间和消息反馈回调: - -**admin-ui — `src/api/index.ts`:** -```typescript -import {message} from "antd"; -import {HttpClient} from "@codingapi/ui-framework"; - -export const httpClient = new HttpClient(10000, { - success: (msg: string) => { - message.success(msg).then(); - }, - error: (msg: string) => { - message.error(msg).then(); - } -}) -``` - -**mobile-ui — `src/api/index.ts`:** -```typescript -import {Toast} from "antd-mobile"; -import {HttpClient} from "@codingapi/ui-framework"; - -export const httpClient = new HttpClient(10000, { - success: (msg: string) => { - Toast.show({ - content: msg, - }) - }, - error: (msg: string) => { - Toast.show({ - icon: 'fail', - content: msg, - }) - } -}) -``` - -> `HttpClient` 构造函数的第一个参数是超时时间(毫秒),第二个参数是消息回调对象,包含 `success` 和 `error` 两个方法。 - -### 2. 按业务领域创建 API 模块文件 - -在 `src/api/` 目录下按后端领域划分文件,每个文件导出该领域的 API 函数: - -``` -src/api/ -├── index.ts # HttpClient 实例 -├── account.ts # 登录、验证码 -├── user.ts # 用户管理 -├── flow.ts # 工作流 -├── leave.ts # 请假 -├── oss.ts # 文件上传 -└── jar.ts # 部署管理 -``` - -### 3. API 函数命名和签名约定 - -- **查询列表**:使用 `list` 作为函数名,接受 `params`、`sort`、`filter`、`match` 四个参数,调用 `httpClient.page()` -- **查询详情**:使用描述性名称如 `detail`、`users`,调用 `httpClient.get()` -- **命令操作**:使用动词如 `save`、`remove`、`startFlow`,调用 `httpClient.post()` -- 所有 API 函数都应该是 `async function`,返回 `httpClient` 方法的 Promise - -### 4. HttpClient 提供的方法 - -| 方法 | 用途 | 示例 | -|------|------|------| -| `get(url, params?)` | GET 请求 | `httpClient.get('/api/query/user/list', {current:1, pageSize:999999})` | -| `post(url, body)` | POST 请求 | `httpClient.post('/api/cmd/user/save', body)` | -| `page(url, params, sort, filter, match)` | 分页查询 | `httpClient.page('/api/query/flowWork/list', params, sort, filter, match)` | - -## 使用实例 - -### ✅ 正确示例 - -**标准 API 模块文件:** -```typescript -// src/api/user.ts -import {httpClient} from "@/api/index"; - -export async function list( - params: any, - sort: any, - filter: any, - match: { key: string, type: string }[] -) { - return httpClient.page('/api/query/user/list', params, sort, filter, match); -} - -export async function save(body: any) { - return httpClient.post('/api/cmd/user/save', body); -} - -export async function remove(id: any) { - return httpClient.post('/api/cmd/user/remove', {id}); -} -``` - -**在组件中调用 API:** -```tsx -import {list, save} from "@/api/user"; - -const UserPage = () => { - const loadData = async () => { - const result = await list( - {current: 1, pageSize: 20}, - {}, - {}, - [] - ); - // result 已经过 HttpClient 统一处理 - }; - - const handleSave = async (values: any) => { - await save(values); // ✅ 成功/失败消息由 HttpClient 自动展示 - }; -}; -``` - -**登录 API 示例:** -```typescript -// src/api/account.ts -import {httpClient} from "@/api/index"; - -export async function login(body: Account.LoginRequest) { - return httpClient.post('/user/login', body); -} - -export async function captcha() { - return httpClient.get('/open/captcha'); -} -``` - -### ❌ 错误示例 - -**直接在组件中使用 axios/fetch:** -```tsx -// ❌ 不要绕过 HttpClient 直接发请求 -import axios from "axios"; - -const loadData = async () => { - const res = await axios.get('/api/query/user/list'); - // 缺少统一错误处理、超时配置、Token 注入 -}; -``` - -**在组件中内联写请求逻辑:** -```tsx -// ❌ 请求逻辑不应写在组件内部 -const UserPage = () => { - const handleSave = async () => { - const res = await httpClient.post('/api/cmd/user/save', formValues); - if (res.success) { - message.success('保存成功'); - } else { - message.error(res.errMessage); - } - }; -}; -``` - -**API 文件不按领域组织:** -```typescript -// ❌ 不要把所有 API 放在一个大文件中 -// api/all.ts -export async function userList() { ... } -export async function userSave() { ... } -export async function flowList() { ... } -export async function flowSave() { ... } -export async function leaveStart() { ... } -// 几百个函数混在一起,无法维护 -``` - -**自行封装新的 HTTP 工具类:** -```typescript -// ❌ 不要重复造轮子 -class MyHttpClient { - async get(url: string) { - const res = await fetch(url); - return res.json(); - } -} -// 已有 @codingapi/ui-framework 的 HttpClient,不需要自建 -``` - -**忽略消息回调的配置:** -```typescript -// ❌ 不传消息回调会导致用户看不到操作反馈 -export const httpClient = new HttpClient(10000); -// 应该始终提供 success 和 error 回调 -``` diff --git a/docs/conventions/frontend-module-federation.md b/docs/conventions/frontend-module-federation.md deleted file mode 100644 index 9aab638d7..000000000 --- a/docs/conventions/frontend-module-federation.md +++ /dev/null @@ -1,295 +0,0 @@ ---- -name: frontend-module-federation -description: Module Federation 微前端集成规范 -status: 已实现 -scope: 前端 -source: 项目自有 ---- - -## 解决什么问题 - -本项目包含多个前端应用(admin-ui、mobile-ui),它们需要共享基础依赖(React、ReactDOM)并具备独立部署、运行时集成的能力。传统的 monorepo 或 iframe 方案存在以下问题: - -1. **依赖重复打包**:每个应用都打包一份 React,用户加载多个应用时浪费带宽 -2. **版本不一致**:不同应用使用不同版本的 React 会导致 hooks 报错和运行时异常 -3. **无法运行时组合**:构建时就确定了所有依赖,无法在运行时动态加载其他应用的模块 -4. **部署耦合**:修改一个子应用需要重新构建和部署整个主应用 - -本项目使用 **@module-federation/enhanced + @module-federation/rsbuild-plugin** 配合 Rsbuild 构建工具,实现基于 Module Federation 的微前端架构。 - -## 如何使用 - -### 1. 安装依赖 - -确保 `package.json` 中包含以下依赖: - -```json -{ - "dependencies": { - "@module-federation/enhanced": "^0.17.0" - }, - "devDependencies": { - "@module-federation/rsbuild-plugin": "^0.17.0", - "@rsbuild/core": "1.4.7", - "@rsbuild/plugin-react": "1.3.4", - "@rsbuild/plugin-sass": "..." - } -} -``` - -### 2. 配置 Rsbuild + Module Federation - -在 `rsbuild.config.ts` 中通过 `pluginModuleFederation` 插件配置联邦模块: - -**admin-ui/rsbuild.config.ts:** -```typescript -import * as path from 'path'; -import {defineConfig} from '@rsbuild/core'; -import {pluginReact} from '@rsbuild/plugin-react'; -import {pluginSass} from '@rsbuild/plugin-sass'; -import {pluginModuleFederation} from '@module-federation/rsbuild-plugin'; - -export default defineConfig({ - plugins: [ - pluginReact(), - pluginSass(), - pluginModuleFederation({ - name: "AdminUI", - shared: { - react: { - requiredVersion: '^18.3.1', - singleton: true, - strictVersion: false, - }, - 'react-dom': { - requiredVersion: '^18.3.1', - singleton: true, - strictVersion: false, - }, - } - }, { - ssr: false, - ssrDir: path.resolve(__dirname, 'ssr'), - environment: 'development', - }), - ], - server: { - port: 8000, - }, - source: { - entry: { - index: './src/entry.tsx', - }, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, 'src'), - react: path.resolve(__dirname, 'node_modules/react'), - 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), - } - }, -}); -``` - -**mobile-ui/rsbuild.config.ts:** -```typescript -import {pluginModuleFederation} from '@module-federation/rsbuild-plugin'; - -export default defineConfig({ - plugins: [ - pluginReact(), - pluginSass(), - pluginModuleFederation({ - name: "MobileUI", // ✅ 每个应用必须有唯一的 name - shared: { - react: { - requiredVersion: '^18.3.1', - singleton: true, - strictVersion: false, - }, - 'react-dom': { - requiredVersion: '^18.3.1', - singleton: true, - strictVersion: false, - }, - } - }, { - ssr: false, - ssrDir: path.resolve(__dirname, 'ssr'), - environment: 'development', - }), - ], - // ...其余配置相同 -}); -``` - -### 3. 关键配置说明 - -| 配置项 | 说明 | -|--------|------| -| `name` | 联邦模块的唯一标识名,如 `"AdminUI"`、`"MobileUI"` | -| `shared.react.singleton: true` | 确保整个运行时只加载一个 React 实例,避免多实例导致 hooks 报错 | -| `shared.react.strictVersion: false` | 允许在一定范围内兼容不同小版本,避免因严格版本匹配导致加载失败 | -| `shared.react.requiredVersion` | 声明所需的 React 版本范围 | -| `ssr: false` | 当前项目不使用 SSR,设为 false | -| `source.entry.index` | 入口文件为 `./src/entry.tsx`,而非直接指向 `index.tsx` | - -### 4. 入口文件异步化 - -Module Federation 要求入口文件使用异步导入,确保共享依赖在应用初始化前完成协商: - -```typescript -// src/entry.tsx -import("./index"); -``` - -### 5. 开发环境代理配置 - -通过 `rsbuild.config.dev.ts` 扩展基础配置,添加后端 API 代理: - -```typescript -import {defineConfig} from '@rsbuild/core'; -import commonConfig from './rsbuild.config'; - -export default defineConfig({ - ...commonConfig, - server: { - port: 8000, - proxy: { - '/api': 'http://127.0.0.1:8090', - '/open': 'http://127.0.0.1:8090', - '/user': 'http://127.0.0.1:8090', - }, - }, -}) -``` - -### 6. Mock 模式配置 - -通过 `rsbuild.config.mock.ts` 提供无后端的开发模式: - -```typescript -import {defineConfig} from '@rsbuild/core'; -import commonConfig from './rsbuild.config'; -import {usersHandler} from "./mocks/user"; -import {productsHandler} from "./mocks/product"; - -export default defineConfig({ - ...commonConfig, - server: { - port: 8000, - }, - dev: { - setupMiddlewares: (middlewares, devServer) => { - if (!devServer) { - throw new Error('webpack-dev-server is not defined'); - } - console.log('mock server is running'); - middlewares.unshift(usersHandler, productsHandler); - } - } -}) -``` - -## 使用实例 - -### ✅ 正确示例 - -**配置 shared 依赖时使用 singleton:** -```typescript -pluginModuleFederation({ - name: "AdminUI", - shared: { - react: { - requiredVersion: '^18.3.1', - singleton: true, // ✅ 保证单例 - strictVersion: false, // ✅ 允许版本弹性 - }, - 'react-dom': { - requiredVersion: '^18.3.1', - singleton: true, - strictVersion: false, - }, - } -}) -``` - -**入口文件使用异步导入:** -```typescript -// ✅ entry.tsx - 异步加载确保共享依赖先协商 -import("./index"); -``` - -**新增一个联邦应用时保持配置一致:** -```typescript -// new-app/rsbuild.config.ts -pluginModuleFederation({ - name: "NewApp", // ✅ 唯一名称 - shared: { - react: { - requiredVersion: '^18.3.1', // ✅ 与其他应用保持一致 - singleton: true, - strictVersion: false, - }, - 'react-dom': { - requiredVersion: '^18.3.1', - singleton: true, - strictVersion: false, - }, - } -}) -``` - -### ❌ 错误示例 - -**忘记设置 singleton: true:** -```typescript -// ❌ 缺少 singleton 可能导致多个 React 实例 -shared: { - react: { - requiredVersion: '^18.3.1', - // singleton: true ← 遗漏! - strictVersion: false, - }, -} -``` - -**入口文件使用同步导入:** -```typescript -// ❌ entry.tsx 不能直接写业务代码 -import React from 'react'; -import ReactDOM from 'react-dom/client'; -// Module Federation 需要先完成共享依赖协商 -// 必须使用 import("./index") 的异步形式 -``` - -**不同应用使用不同的 React 版本范围:** -```typescript -// ❌ AdminUI 使用 ^18.3.1 -shared: { react: { requiredVersion: '^18.3.1' } } - -// ❌ MobileUI 使用 ^17.0.0 — 版本不兼容! -shared: { react: { requiredVersion: '^17.0.0' } } -``` - -**将 rsbuild.config.ts 与 rsbuild.config.dev.ts 混为一谈:** -```typescript -// ❌ 不要在基础配置中写死代理地址 -export default defineConfig({ - server: { - proxy: { - '/api': 'http://127.0.0.1:8090', // 这应该在 dev 配置中 - }, - }, -}) -``` - -**Module Federation 名称重复:** -```typescript -// ❌ 两个应用使用了相同的 name -// admin-ui/rsbuild.config.ts -pluginModuleFederation({ name: "App" }) - -// mobile-ui/rsbuild.config.ts -pluginModuleFederation({ name: "App" }) // 冲突!应改为 "MobileUI" -``` diff --git a/docs/conventions/frontend-state-management.md b/docs/conventions/frontend-state-management.md deleted file mode 100644 index 55ddd3f1e..000000000 --- a/docs/conventions/frontend-state-management.md +++ /dev/null @@ -1,286 +0,0 @@ ---- -name: frontend-state-management -description: Redux Toolkit 状态管理规范 -status: 已实现 -scope: 前端 -source: 项目自有 ---- - -## 解决什么问题 - -前端应用中存在需要跨组件共享的状态(如菜单刷新、页面标题、用户会话信息等)。如果没有统一的状态管理方案,会导致: - -1. **Prop Drilling**:状态需要通过多层组件逐级传递,增加耦合度 -2. **状态散落**:不同开发者用不同方式管理全局状态(Context、localStorage、全局变量等),难以维护 -3. **不可预测的更新**:缺少统一的更新机制,状态变更难以追踪和调试 - -本项目使用 **@reduxjs/toolkit + react-redux** 作为统一的状态管理方案,通过 Slice 模式组织状态逻辑。 - -## 如何使用 - -### 1. 创建 Slice 文件 - -每个独立的状态域创建一个 Slice 文件,放在 `src/store/` 目录下。Slice 文件需要包含: - -- **State 接口定义**:明确状态的类型结构 -- **Action 类型定义**:声明 reducer 函数的签名 -- **createSlice 调用**:定义 name、initialState 和 reducers -- **导出 actions**:从 slice 中解构并导出具名 action - -**admin-ui 示例 — `store/MenuSlice.ts`:** -```typescript -import {createSlice} from '@reduxjs/toolkit'; - -export interface MenuStore { - version: number; -} - -export type MenuStoreAction = { - refresh: (state: MenuStore) => void; -} - -export const menuSlice = createSlice({ - name: 'menu', - initialState: { - version: 0, - }, - reducers: { - refresh: (state) => { - state.version = Math.random(); - }, - }, -}); - -export const {refresh} = menuSlice.actions; -``` - -**mobile-ui 示例 — `sotre/LayoutSlice.ts`:** -```typescript -import {configureStore, createSlice, PayloadAction} from '@reduxjs/toolkit'; - -export interface LayoutStore { - title: string; - backHome: boolean; -} - -export type MenuStoreAction = { - changeTitle: (state: LayoutStore, action: PayloadAction) => void; -} - -export const layoutSlice = createSlice({ - name: 'layout', - initialState: { - title: '首页', - backHome: true - }, - reducers: { - changeTitle: (state, action) => { - const title = action.payload; - state.title = title; - state.backHome = '首页' === title; - } - }, -}); - -export const {changeTitle} = layoutSlice.actions; -``` - -### 2. 注册 Slice 到 Store - -在 `store/Redux.tsx`(admin-ui)或 Slice 文件内(mobile-ui)使用 `configureStore` 组合所有 reducer: - -**admin-ui — `store/Redux.tsx`:** -```typescript -import {configureStore} from '@reduxjs/toolkit'; -import {counterSlice} from './CounterSlice'; -import {menuSlice} from './MenuSlice'; - -const store = configureStore({ - reducer: { - counter: counterSlice.reducer, - menu: menuSlice.reducer, - }, -}); - -export type RootState = ReturnType; -export default store; -``` - -**mobile-ui — 在 `LayoutSlice.ts` 中直接创建 store:** -```typescript -export const layoutStore = configureStore({ - reducer: { - layout: layoutSlice.reducer - }, -}); - -export type LayoutState = ReturnType; -``` - -### 3. 在应用入口挂载 Provider - -**admin-ui — `index.tsx`:** -```tsx -import {Provider} from "react-redux"; -import store from "@/store/Redux"; - -root.render( - - - - - - - - - -); -``` - -**mobile-ui — `layout/index.tsx`:** -```tsx -import {Provider} from "react-redux"; -import {layoutStore} from "@/sotre/LayoutSlice"; - -const Layout = () => { - return ( - - <$Layout/> - - ) -} -``` - -### 4. 在组件中使用 useSelector / useDispatch - -```tsx -import {useSelector} from "react-redux"; -import {RootState} from "@/store/Redux"; - -const Layout = () => { - const menuVersion = useSelector((state: RootState) => state.menu.version); - - useEffect(() => { - // menuVersion 变化时重新加载菜单 - MenuRouteManager.getInstance().refresh(); - actionRef.current?.reload(); - }, [menuVersion]); -}; -``` - -## 使用实例 - -### ✅ 正确示例 - -**创建新的 Slice:** -```typescript -// store/UserSlice.ts -import {createSlice, PayloadAction} from '@reduxjs/toolkit'; - -export interface UserStore { - username: string; - isLoggedIn: boolean; -} - -export type UserStoreAction = { - login: (state: UserStore, action: PayloadAction) => void; - logout: (state: UserStore) => void; -} - -export const userSlice = createSlice({ - name: 'user', - initialState: { - username: '', - isLoggedIn: false, - }, - reducers: { - login: (state, action) => { - state.username = action.payload; - state.isLoggedIn = true; - }, - logout: (state) => { - state.username = ''; - state.isLoggedIn = false; - }, - }, -}); - -export const {login, logout} = userSlice.actions; -``` - -**在组件中 dispatch action:** -```tsx -import {useDispatch} from "react-redux"; -import {refresh} from "@/store/MenuSlice"; - -const SomeComponent = () => { - const dispatch = useDispatch(); - - const handleSave = async () => { - await saveData(); - dispatch(refresh()); // ✅ 触发菜单刷新 - }; -}; -``` - -**带 payload 的 action:** -```tsx -import {useDispatch} from "react-redux"; -import {changeTitle} from "@/sotre/LayoutSlice"; - -const Page = () => { - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(changeTitle('请假申请')); // ✅ PayloadAction - }, []); -}; -``` - -### ❌ 错误示例 - -**直接修改 state 而不通过 reducer:** -```typescript -// ❌ 永远不要直接修改 store 中的状态 -import store from "@/store/Redux"; -const state = store.getState(); -state.menu.version = 999; // 违反 Redux 不可变原则 -``` - -**在 Slice 外部定义 mutable 全局变量代替 Redux:** -```typescript -// ❌ 不要用全局变量代替状态管理 -let globalMenuVersion = 0; -export const refreshMenu = () => { globalMenuVersion++; }; -``` - -**Slice 文件中不导出 Action 类型:** -```typescript -// ❌ 缺少类型定义,后续维护困难 -export const someSlice = createSlice({ - name: 'some', - initialState: { value: 0 }, - reducers: { - update: (state) => { state.value += 1; }, - }, -}); -// 没有 interface、没有 type alias、没有显式泛型参数 -``` - -**忘记将新 Slice 注册到 store:** -```typescript -// ❌ 创建了 UserSlice 但忘记添加到 configureStore -const store = configureStore({ - reducer: { - counter: counterSlice.reducer, - menu: menuSlice.reducer, - // 缺少 user: userSlice.reducer - }, -}); -``` - -**在组件中硬编码 state 路径而不使用 RootState 类型:** -```tsx -// ❌ 没有类型安全,重构时不会报错 -const version = useSelector((state: any) => state.menu.version); -``` diff --git a/docs/conventions/global-exception-handling.md b/docs/conventions/global-exception-handling.md deleted file mode 100644 index 3f8ef53fc..000000000 --- a/docs/conventions/global-exception-handling.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -name: global-exception-handling -description: 全局异常处理与国际化规范 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -全局异常处理机制解决了以下问题: - -- **统一错误格式**:所有异常自动转换为标准 `Response` 格式(`success=false, errCode, errMessage`) -- **国际化支持**:业务异常通过 `LocaleMessageException` + `messages.properties` 实现多语言错误消息 -- **占位符参数化**:错误消息支持 `{0}`, `{1}` 等占位符,动态填充上下文信息 -- **Controller 无需 try-catch**:业务代码只需抛出异常,由框架统一拦截和转换 - -## 如何使用 - -### 核心组件 - -```java -// 1. ExceptionConfiguration —— 注册国际化消息源 -@Configuration -public class ExceptionConfiguration { - @Bean(initMethod = "init") - public LocaleMessage exceptionLocaleMessage(MessageSource messageSource) { - return new LocaleMessage(messageSource); - } -} - -// 2. BasicHandlerExceptionResolverConfiguration —— 全局异常拦截器 -@Configuration -public class BasicHandlerExceptionResolverConfiguration { - @Bean - public HandlerExceptionResolver servletExceptionHandler() { - return new ServletExceptionHandler(); - } -} - -// 3. LocaleMessageException —— 业务异常类 -public class LocaleMessageException extends RuntimeException { - String errCode; - String errMessage; - - // 从 messages.properties 读取消息 - LocaleMessageException(String errCode); - // 带参数的国际化消息 - LocaleMessageException(String errCode, Object[] args); - // 直接指定消息 - LocaleMessageException(String errCode, String errMessage); - // 静态工厂方法(推荐) - static LocaleMessageException of(String errCode, Object... args); -} -``` - -### 国际化消息配置 - -在 `src/main/resources/messages.properties` 中定义错误码对应的消息: - -```properties -error1=操作失败,请重试 -error2=用户不存在 -error3=参数错误:{0} -system.err=系统内部错误 -``` - -### 异常类型说明 - -| 异常类 | 用途 | 处理方式 | -|--------|------|---------| -| `LocaleMessageException` | 业务异常,支持国际化 | 提取 errCode + errMessage 返回 | -| `EventException` | 事件处理器执行异常聚合 | 包含多个子异常列表 | -| `EventLoopException` | 事件循环调用检测 | 立即中断事件链 | -| 其他 `RuntimeException` | 未预期异常 | 使用 `system.err` 作为默认错误码 | - -## 使用实例 - -### ✅ 正确示例:使用国际化错误码 - -```java -@GetMapping("/error1") -public Response error1() { - // 从 messages.properties 读取 error1 对应的消息 - throw new LocaleMessageException("error1"); -} -``` - -### ✅ 正确示例:带参数的国际化异常 - -```java -@GetMapping("/error3") -public Response error3() { - // messages.properties: error3=参数错误:{0} - // 最终消息:参数错误:my is arg - throw LocaleMessageException.of("error3", "my is arg"); -} -``` - -### ✅ 正确示例:直接指定错误消息(无需国际化时) - -```java -@GetMapping("/error2") -public Response error2() { - throw new LocaleMessageException("error2", "error 2 message"); -} -``` - -### ✅ 正确示例:在 Domain Service 中抛出业务异常 - -```java -@Service -@AllArgsConstructor -public class UserService { - - private final UserRepository userRepository; - - public User getUser(long id) { - User user = userRepository.getUserById(id); - if (user == null) { - throw LocaleMessageException.of("user.not.found", id); - } - return user; - } -} -``` - -### ❌ 错误示例:在 Controller 中手动捕获异常 - -```java -@PostMapping("/save") -public Response save(@RequestBody UserCmd cmd) { - try { - userRouter.createOrUpdate(cmd); - return Response.buildSuccess(); - } catch (LocaleMessageException e) { - // 禁止!全局异常处理器会自动处理 - return Response.buildFailure(e.getErrCode(), e.getErrMessage()); - } -} -``` - -### ❌ 错误示例:直接使用 RuntimeException 而非 LocaleMessageException - -```java -public void validate(String name) { - if (name == null) { - // 禁止!缺少 errCode,无法被前端识别和处理 - throw new RuntimeException("名称不能为空"); - } -} -``` - -### ❌ 错误示例:硬编码中文错误消息而不使用国际化 - -```java -// 不推荐:消息写死在代码中,无法切换语言 -throw new LocaleMessageException("error.hardcoded", "用户名已存在"); - -// 推荐:使用错误码 + messages.properties -throw new LocaleMessageException("user.name.duplicate"); -``` - -### ❌ 错误示例:忽略 EventException 中的子异常信息 - -```java -try { - EventPusher.push(event); -} catch (EventException e) { - // 禁止!丢失了具体的子异常详情 - log.error("事件处理失败"); - - // 正确做法:遍历子异常 - for (Exception sub : e.getError()) { - log.error("子异常: {}", sub.getMessage(), sub); - } -} -``` diff --git a/docs/conventions/index.md b/docs/conventions/index.md index 73b2838df..52c10205b 100644 --- a/docs/conventions/index.md +++ b/docs/conventions/index.md @@ -6,15 +6,12 @@ | 名称 | 描述 | 范围 | 来源 | |------|------|------|------| -| [ddd-layered-architecture](./ddd-layered-architecture.md) | DDD 分层架构与依赖规范 | 后端 | 项目自有 | -| [event-handler-pattern](./event-handler-pattern.md) | 事件处理器自动注册规范 | 后端 | 项目自有 | -| [frontend-css-variables](./frontend-css-variables.md) | 前端 CSS 变量定义与使用规范 | 前端 | 项目自有 | -| [frontend-http-client](./frontend-http-client.md) | 前端 HTTP 请求封装规范 | 前端 | 项目自有 | -| [frontend-module-federation](./frontend-module-federation.md) | Module Federation 微前端集成规范 | 前端 | 项目自有 | -| [frontend-state-management](./frontend-state-management.md) | Redux Toolkit 状态管理规范 | 前端 | 项目自有 | -| [global-exception-handling](./global-exception-handling.md) | 全局异常处理与国际化规范 | 后端 | 项目自有 | -| [unified-response-convention](./unified-response-convention.md) | API 统一响应返回规范 | 后端 | 项目自有 | +| [autoconfiguration-pattern](./autoconfiguration-pattern.md) | 每个 starter 模块必须同时注册 spring.factories(旧版兼容)和 AutoConfiguration.imports(Spring ... | 后端 | 项目自有 | +| [context-singleton](./context-singleton.md) | 全局状态通过单例 Context 类持有,由 Register 类在启动时注入依赖 | 后端 | 项目自有 | +| [locale-exception-handling](./locale-exception-handling.md) | 所有业务异常必须使用 LocaleMessageException,通过 errCode 实现国际化,由全局 HandlerExceptionResolv... | 后端 | 项目自有 | +| [slf4j-logging](./slf4j-logging.md) | 所有 Java 类必须使用 Lombok @Slf4j 注解进行日志输出,禁止直接使用 LoggerFactory.getLogger() | 全栈 | 项目自有 | +| [unified-response](./unified-response.md) | 所有 API 返回值必须使用统一响应封装(Response → SingleResponse / MultiResponse / MapResponse)... | 后端 | 项目自有 | --- -**统计**: 共 8 篇 — 已实现 8 / 计划中 0 / 已废弃 0 +**统计**: 共 5 篇 — 已实现 5 / 计划中 0 / 已废弃 0 diff --git a/docs/conventions/locale-exception-handling.md b/docs/conventions/locale-exception-handling.md new file mode 100644 index 000000000..d47af6441 --- /dev/null +++ b/docs/conventions/locale-exception-handling.md @@ -0,0 +1,127 @@ +--- +name: locale-exception-handling +description: 所有业务异常必须使用 LocaleMessageException,通过 errCode 实现国际化,由全局 HandlerExceptionResolver 统一处理 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 0c4299a1 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/LocaleMessageException.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/servlet/BasicHandlerExceptionResolverConfiguration.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/ExceptionConfiguration.java +--- + +## 解决什么问题 + +如果业务代码中随意抛出 `RuntimeException`、`IllegalArgumentException` 等原生异常,会导致以下问题: + +1. **错误信息无法国际化**:硬编码的中文字符串无法根据用户语言环境切换 +2. **缺少结构化错误码**:前端无法通过 `errCode` 精确判断错误类型并做差异化处理 +3. **异常响应格式不统一**:不同异常类型被 Spring 默认处理器转为不同的 HTTP 响应结构 +4. **敏感信息泄露**:未捕获的异常堆栈可能暴露内部实现细节给客户端 + +框架通过 `LocaleMessageException` + `BasicHandlerExceptionResolverConfiguration` 实现统一的国际化异常处理链: +- `LocaleMessageException` 携带 `errCode`,可选携带占位符参数 +- `LocaleMessage` 从 Spring `MessageSource`(`messages.properties`)读取国际化消息 +- `ServletExceptionHandler` 将所有异常统一转为 `{success, errCode, errMessage}` JSON 响应 + +## 如何使用 + +### 1. 定义国际化消息 + +在 `src/main/resources/messages.properties` 和 `messages_{locale}.properties` 中配置错误码对应的消息: + +```properties +# messages.properties (默认/英文) +user.not.found=User not found +order.amount.invalid=Order amount must be greater than {0} + +# messages_zh_CN.properties (中文) +user.not.found=用户不存在 +order.amount.invalid=订单金额必须大于 {0} +``` + +### 2. 抛出 LocaleMessageException + +| 构造方式 | 说明 | +|----------|------| +| `new LocaleMessageException("err.code")` | 仅指定错误码,自动从 MessageSource 查找消息 | +| `new LocaleMessageException("err.code", "fallback message")` | 指定错误码和回退消息(不走国际化) | +| `LocaleMessageException.of("err.code", arg1, arg2)` | 带占位符参数的国际化消息(推荐) | +| `new LocaleMessageException("err.code", new Object[]{arg1}, cause)` | 带参数和原始异常 | + +### 3. 规则 + +1. **所有业务异常必须使用 `LocaleMessageException`**,禁止直接抛出 `RuntimeException` 或其他原生异常 +2. 错误码采用点分隔的小写命名(如 `user.not.found`、`order.amount.invalid`) +3. 需要动态内容时使用占位符 `{0}`、`{1}`,通过 `LocaleMessageException.of()` 传入参数 +4. Controller 方法无需 try-catch,由全局 `ServletExceptionHandler` 统一处理并返回标准错误响应 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. 在 Service 层抛出国际化异常 +@Service +public class UserService { + + public UserEntity getById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new LocaleMessageException("user.not.found")); + } + + public void createOrder(BigDecimal amount) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw LocaleMessageException.of("order.amount.invalid", amount); + } + } +} + +// 2. Controller 无需手动处理异常,返回类型仍为统一响应 +@RestController +@RequestMapping("/api/users") +public class UserController { + + @GetMapping("/{id}") + public SingleResponse getById(@PathVariable Long id) { + return SingleResponse.of(userService.getById(id)); + } +} + +// 3. 当 user.not.found 触发时,框架自动返回: +// {"success": false, "errCode": "user.not.found", "errMessage": "用户不存在"} +``` + +### ❌ 错误示例 + +```java +// 错误:直接抛出 RuntimeException,无错误码、不支持国际化 +public UserEntity getById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("用户不存在")); +} + +// 错误:使用 IllegalArgumentException,前端无法识别错误类型 +public void createOrder(BigDecimal amount) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("金额无效"); + } +} + +// 错误:在 Controller 中手动 try-catch 构建错误响应 +@GetMapping("/{id}") +public Response getById(@PathVariable Long id) { + try { + UserEntity user = userService.getById(id); + return SingleResponse.of(user); + } catch (Exception e) { + return Response.buildFailure("error", e.getMessage()); + } +} + +// 错误:自定义异常类但未继承 LocaleMessageException +public class BusinessException extends RuntimeException { + // 不会被全局异常处理器识别为业务异常 +} +``` diff --git a/docs/conventions/slf4j-logging.md b/docs/conventions/slf4j-logging.md new file mode 100644 index 000000000..7bab85aff --- /dev/null +++ b/docs/conventions/slf4j-logging.md @@ -0,0 +1,118 @@ +--- +name: slf4j-logging +description: 所有 Java 类必须使用 Lombok @Slf4j 注解进行日志输出,禁止直接使用 LoggerFactory.getLogger() +status: 已实现 +scope: 全栈 +source: 项目自有 +last_commit: N/A +code_files: [] +--- + +## 解决什么问题 + +如果每个类都手动创建 Logger 实例,会导致以下问题: + +1. **样板代码冗余**:每个类都需要声明 `private static final Logger log = LoggerFactory.getLogger(XxxClass.class)`,增加无意义的代码量 +2. **类名引用易出错**:复制粘贴时忘记修改 `getLogger()` 中的类名参数,导致日志输出错误的来源标识 +3. **重构风险**:重命名类后若未同步修改 Logger 声明,日志追踪将指向旧类名 +4. **风格不一致**:部分开发者使用 `Logger`、部分使用 `Log`、部分使用 `logger` 等不同的变量命名 + +Lombok 的 `@Slf4j` 注解在编译期自动生成 `private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(当前类.class);`,消除上述所有问题。 + +## 如何使用 + +### 规则 + +1. **所有需要日志输出的 Java 类必须在类级别添加 `@Slf4j` 注解** +2. 使用生成的 `log` 变量进行日志输出(`log.info()`、`log.warn()`、`log.error()` 等) +3. **禁止**在代码中出现 `LoggerFactory.getLogger()` 调用 +4. **禁止**手动声明 `Logger` / `Log` 字段 +5. 日志消息使用 SLF4J 占位符 `{}` 格式,不使用字符串拼接 +6. 异常日志应将异常对象作为最后一个参数传入,而非调用 `e.getMessage()` + +### 注意事项 + +- `@Slf4j` 是 Lombok 注解,需确保项目中已引入 Lombok 依赖且 IDE 已安装 Lombok 插件 +- 对于内部静态类,Lombok 会自动生成对应内部类的 Logger,无需额外处理 +- 如需自定义 Logger 名称(极少数场景),可使用 `@Slf4j(topic = "custom.topic")` + +## 使用实例 + +### ✅ 正确示例 + +```java +@Slf4j +@Service +public class UserService { + + public UserEntity getById(Long id) { + log.info("查询用户, id={}", id); + UserEntity user = userRepository.findById(id) + .orElseThrow(() -> new LocaleMessageException("user.not.found")); + log.debug("用户查询成功, id={}, name={}", id, user.getName()); + return user; + } + + public void deleteUser(Long id) { + try { + userRepository.deleteById(id); + log.info("用户删除成功, id={}", id); + } catch (Exception e) { + // 异常对象作为最后一个参数,SLF4J 会自动打印堆栈 + log.error("用户删除失败, id={}", id, e); + throw e; + } + } +} + +// 内部静态类也可正常使用 +@Configuration +public class BasicHandlerExceptionResolverConfiguration { + + @Slf4j + public static class ServletExceptionHandler implements HandlerExceptionResolver { + @Override + public ModelAndView resolveException(...) { + log.warn("controller exception:{}", ex.getLocalizedMessage(), ex); + // ... + } + } +} +``` + +### ❌ 错误示例 + +```java +// 错误:手动创建 Logger,存在样板代码和类名引用风险 +@Service +public class UserService { + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + public UserEntity getById(Long id) { + logger.info("查询用户, id=" + id); // 还使用了字符串拼接 + // ... + } +} + +// 错误:使用不同变量名,风格不统一 +@Service +public class OrderService { + private static final Log LOG = LogFactory.getLog(OrderService.class); + // ... +} + +// 错误:异常日志未传入异常对象 +try { + doSomething(); +} catch (Exception e) { + log.error("操作失败: " + e.getMessage()); // 丢失堆栈信息 +} + +// 错误:缺少 @Slf4j 注解却直接使用 log 变量(编译报错) +@Service +public class PaymentService { + public void pay() { + log.info("开始支付"); // 编译错误:找不到符号 log + } +} +``` diff --git a/docs/conventions/unified-response-convention.md b/docs/conventions/unified-response-convention.md deleted file mode 100644 index d5b7d4871..000000000 --- a/docs/conventions/unified-response-convention.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -name: unified-response-convention -description: API 统一响应返回规范 -status: 已实现 -scope: 后端 -source: 项目自有 ---- - -## 解决什么问题 - -统一的响应格式解决了以下问题: - -- **前后端契约**:所有 API 返回结构一致,前端可以用统一的拦截器处理成功/失败 -- **错误标准化**:错误信息包含 `errCode` + `errMessage`,支持国际化和本地化展示 -- **类型安全**:通过泛型 Response 子类明确每个接口的返回数据类型 -- **分页统一**:列表接口自带 `total` 字段,无需额外约定分页元数据格式 - -## 如何使用 - -### Response 体系 - -```java -// 基础响应 —— 仅表示成功/失败 -public class Response { - boolean success; - String errCode; - String errMessage; - - static Response buildSuccess(); - static Response buildFailure(String errCode, String errMessage); -} - -// 单对象响应 -public class SingleResponse extends Response { - T data; - static SingleResponse of(T data); - static SingleResponse empty(); -} - -// 列表响应(含分页) -public class MultiResponse extends Response { - Content data; // { total: long, list: Collection } - static MultiResponse of(Collection data); - static MultiResponse of(Collection data, long total); - static MultiResponse of(Page page); - static MultiResponse empty(); -} - -// Map 响应 -public class MapResponse extends Response { - Map data; - static MapResponse create(); - MapResponse add(String key, Object value); -} -``` - -### 使用规则 - -| 场景 | 返回类型 | 工厂方法 | -|------|---------|---------| -| 无返回数据的操作 | `Response` | `Response.buildSuccess()` | -| 返回单个对象 | `SingleResponse` | `SingleResponse.of(data)` | -| 返回列表/分页 | `MultiResponse` | `MultiResponse.of(list)` / `MultiResponse.of(page)` | -| 返回键值对 | `MapResponse` | `MapResponse.create().add(k, v)` | -| 业务错误 | `Response` | `Response.buildFailure(code, msg)` 或抛出 `LocaleMessageException` | - -## 使用实例 - -### ✅ 正确示例:命令接口返回 Response - -```java -@PostMapping("/save") -public Response save(@RequestBody UserCmd.UpdateRequest request) { - userRouter.createOrUpdate(request); - return Response.buildSuccess(); -} - -@PostMapping("/remove") -public Response remove(@RequestBody IdRequest request) { - userRouter.removeUser(request.getLongId()); - return Response.buildSuccess(); -} -``` - -### ✅ 正确示例:查询接口返回 MultiResponse - -```java -@GetMapping("/list") -public MultiResponse list(SearchRequest searchRequest) { - return MultiResponse.of(userQueryService.list(searchRequest)); -} -``` - -### ✅ 正确示例:返回单个对象用 SingleResponse - -```java -@GetMapping("/detail") -public SingleResponse getFlowStep(@RequestBody IdRequest request) { - FlowWork work = flowAppQueryService.getFlowStep(request.getLongId()); - return SingleResponse.of(work); -} -``` - -### ✅ 正确示例:返回 Map 数据 - -```java -@GetMapping("/stats") -public MapResponse stats() { - return MapResponse.create() - .add("totalUsers", userService.count()) - .add("activeUsers", userService.countActive()); -} -``` - -### ❌ 错误示例:直接返回 Entity 而非 Response 包装 - -```java -@GetMapping("/user") -public User getUser(@RequestParam long id) { - // 禁止!缺少 success/errCode/errMessage 标准字段 - return userService.getById(id); -} -``` - -### ❌ 错误示例:手动构造 JSON 字符串 - -```java -@GetMapping("/list") -public String list() { - // 禁止!绕过 Response 体系,前端无法统一处理 - return "{\"success\":true,\"data\":[...]}"; -} -``` - -### ❌ 错误示例:用 null 代替正确的空响应 - -```java -@GetMapping("/detail") -public SingleResponse detail(@RequestParam long id) { - User user = userService.getById(id); - if (user == null) { - return null; // 禁止!应使用 SingleResponse.empty() - } - return SingleResponse.of(user); -} -``` - -### ❌ 错误示例:在 Controller 中捕获异常并手动构建错误响应 - -```java -@PostMapping("/save") -public Response save(@RequestBody UserCmd cmd) { - try { - userRouter.createOrUpdate(cmd); - return Response.buildSuccess(); - } catch (Exception e) { - // 禁止!应由全局异常处理器统一处理 - return Response.buildFailure("error", e.getMessage()); - } -} -``` diff --git a/docs/conventions/unified-response.md b/docs/conventions/unified-response.md new file mode 100644 index 000000000..35c9292bc --- /dev/null +++ b/docs/conventions/unified-response.md @@ -0,0 +1,117 @@ +--- +name: unified-response +description: 所有 API 返回值必须使用统一响应封装(Response → SingleResponse / MultiResponse / MapResponse),禁止直接返回裸对象 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 841c49b8 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/Response.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/SingleResponse.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/MultiResponse.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/MapResponse.java +--- + +## 解决什么问题 + +如果 Controller 直接返回裸对象(如 `UserEntity`、`List`、`Map`),会导致以下问题: + +1. **前后端协议不一致**:不同接口返回结构各异,前端无法统一解析成功/失败状态 +2. **错误信息缺失**:裸对象不包含 `errCode` / `errMessage`,异常处理与正常响应格式不统一 +3. **分页元数据丢失**:直接返回 `List` 时缺少 `total` 等分页信息,前端无法正确渲染分页组件 +4. **扩展困难**:后续如需增加请求 ID、时间戳等通用字段,需逐个修改所有接口 + +## 如何使用 + +框架提供四种响应类型,均继承自 `Response` 基类: + +| 类型 | 用途 | 工厂方法 | +|------|------|----------| +| `Response` | 无数据的操作结果(仅 success/errCode/errMessage) | `Response.buildSuccess()` / `Response.buildFailure(errCode, errMessage)` | +| `SingleResponse` | 返回单个对象 | `SingleResponse.of(data)` / `SingleResponse.empty()` | +| `MultiResponse` | 返回列表(支持分页) | `MultiResponse.of(collection, total)` / `MultiResponse.of(page)` / `MultiResponse.of(collection)` / `MultiResponse.empty()` | +| `MapResponse` | 返回键值对数据 | `MapResponse.create().add(key, value)` / `MapResponse.empty()` | + +### 规则 + +1. **Controller 方法的返回类型必须是上述四种之一**,不得返回实体类、集合或 Map 裸对象 +2. 成功时使用 `of()` / `buildSuccess()` 等静态工厂方法创建响应 +3. 失败时优先通过抛出 `LocaleMessageException` 由全局异常处理器自动构建错误响应;仅在非异常场景下手动调用 `Response.buildFailure()` +4. `MultiResponse` 接收 Spring Data `Page` 对象时,使用 `MultiResponse.of(page)` 自动提取 `content` 和 `totalElements` + +## 使用实例 + +### ✅ 正确示例 + +```java +@RestController +@RequestMapping("/api/users") +public class UserQueryController { + + @Autowired + private UserQueryService userQueryService; + + // 返回单个对象 + @GetMapping("/{id}") + public SingleResponse getById(@PathVariable Long id) { + return SingleResponse.of(userQueryService.getById(id)); + } + + // 返回列表(带分页) + @PostMapping("/list") + public MultiResponse list(@RequestBody SearchRequest searchRequest) { + return MultiResponse.of(userQueryService.list(searchRequest)); + } + + // 返回键值对 + @GetMapping("/stats") + public MapResponse stats() { + return MapResponse.create() + .add("totalUsers", 100) + .add("activeUsers", 85); + } + + // 无数据操作 + @DeleteMapping("/{id}") + public Response delete(@PathVariable Long id) { + userCommandService.delete(id); + return Response.buildSuccess(); + } +} +``` + +### ❌ 错误示例 + +```java +@RestController +@RequestMapping("/api/users") +public class UserQueryController { + + // 错误:直接返回实体类 + @GetMapping("/{id}") + public UserEntity getById(@PathVariable Long id) { + return userQueryService.getById(id); + } + + // 错误:直接返回 List,缺少 total 和统一包装 + @GetMapping("/list") + public List list() { + return userQueryService.listAll(); + } + + // 错误:直接返回 Map + @GetMapping("/stats") + public Map stats() { + Map map = new HashMap<>(); + map.put("totalUsers", 100); + return map; + } + + // 错误:使用 ResponseEntity 包装裸对象 + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + userCommandService.delete(id); + return ResponseEntity.ok().build(); + } +} +``` From d820bfee69b046a668ccbc815e96d9a5c579e1a2 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 8 Jun 2026 18:23:01 +0800 Subject: [PATCH 07/10] update docs --- docs/capabilities/crypto-tools.md | 247 ++++++++++++++++++ docs/capabilities/data-authorization.md | 228 ++++++++++++++++ docs/capabilities/dynamic-application.md | 132 ++++++++++ docs/capabilities/dynamic-mvc-mapping.md | 168 ++++++++++++ docs/capabilities/index.md | 8 +- docs/capabilities/rest-client.md | 226 ++++++++++++++++ docs/capabilities/user-context.md | 142 ++++++++++ docs/conventions/context-register-pairing.md | 222 ++++++++++++++++ docs/conventions/index.md | 5 +- docs/conventions/meta-table-annotation.md | 146 +++++++++++ .../conventions/servlet-exception-resolver.md | 111 ++++++++ 11 files changed, 1633 insertions(+), 2 deletions(-) create mode 100644 docs/capabilities/crypto-tools.md create mode 100644 docs/capabilities/data-authorization.md create mode 100644 docs/capabilities/dynamic-application.md create mode 100644 docs/capabilities/dynamic-mvc-mapping.md create mode 100644 docs/capabilities/rest-client.md create mode 100644 docs/capabilities/user-context.md create mode 100644 docs/conventions/context-register-pairing.md create mode 100644 docs/conventions/meta-table-annotation.md create mode 100644 docs/conventions/servlet-exception-resolver.md diff --git a/docs/capabilities/crypto-tools.md b/docs/capabilities/crypto-tools.md new file mode 100644 index 000000000..b8791dcb3 --- /dev/null +++ b/docs/capabilities/crypto-tools.md @@ -0,0 +1,247 @@ +--- +name: crypto-tools +description: 密码学工具套件,AES(CBC 模式)/RSA(BouncyCastle)/DES 加解密 + SHA256/HmacSHA256 哈希签名,提供实例化工具类和静态方法两种 API +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 36eae41d +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/AES.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/AESUtils.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/DES.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/DESUtils.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/RSA.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/RSAUtils.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/SHA256.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/HmacSHA256.java + - springboot-starter-security/src/main/java/com/codingapi/springboot/security/crypto/AESTools.java +--- + +## 解决什么问题 + +企业应用中大量场景需要密码学支持:敏感数据存储加密、API 通信签名验证、Token 加解密、文件完整性校验等。直接使用 JDK 原生 `javax.crypto` / `java.security` API 存在以下问题: + +- **API 繁琐**:每次加解密都需要手动创建 Cipher、KeySpec、AlgorithmParameters,代码冗长且容易出错 +- **Provider 管理**:BouncyCastle 等第三方 Provider 需要手动注册,遗漏会导致运行时异常 +- **密钥格式混乱**:byte[]、String、Base64 之间的转换散落在各处 +- **缺少统一入口**:不同算法的使用方式不一致,增加学习成本 + +本能力封装了五种常用密码学算法,提供两种使用风格: + +- **实例化 API**(`AES`/`DES`/`RSA`):适合自定义密钥、动态生成密钥对的场景 +- **单例工具类**(`AESUtils`/`DESUtils`/`RSAUtils`/`AESTools`):内置预置密钥,开箱即用,适合应用内统一加解密 +- **静态方法**(`SHA256`/`HmacSHA256`):无状态哈希/签名,直接调用 + +## 如何使用 + +### AES 加解密(CBC/PKCS5Padding) + +基于 BouncyCastle Provider,默认使用 `AES/CBC/PKCS5Padding` 算法。 + +```java +// 自动生成 256 位密钥 + 随机 IV +AES aes = new AES(); + +// 指定密钥长度 +AES aes = new AES(128); + +// 使用指定密钥和 IV(byte[]) +AES aes = new AES(keyBytes, ivBytes); + +// 使用字符串密钥和 IV +AES aes = new AES("mySecretKey12345", "myIvParam1234567"); + +// 加解密 +byte[] encrypted = aes.encrypt("Hello".getBytes(StandardCharsets.UTF_8)); +byte[] decrypted = aes.decrypt(encrypted); + +// 获取密钥和 IV(用于传输或存储) +byte[] key = aes.getKey(); +byte[] iv = aes.getIv(); +``` + +#### AESUtils — 预置密钥单例 + +内置 Base64 编码的预置密钥和 IV,适用于应用内部数据加密: + +```java +AESUtils utils = AESUtils.getInstance(); + +// 字符串加解密(自动 Base64 编解码) +String cipher = utils.encode("敏感数据"); +String plain = utils.decode(cipher); + +// byte[] 加解密(原始字节,不做 Base64) +byte[] encrypted = utils.encode(rawBytes); +byte[] decrypted = utils.decode(encrypted); +``` + +#### AESTools — Security 模块的 AES 封装 + +位于 `springboot-starter-security` 模块,通过 `init(AES)` 方法注入自定义 AES 实例,适合安全模块统一管理密钥: + +```java +// 由安全模块配置类初始化 +AESTools.getInstance().init(new AES(customKey, customIv)); + +// 使用方式与 AESUtils 一致 +String cipher = AESTools.getInstance().encode("data"); +String plain = AESTools.getInstance().decode(cipher); +``` + +### DES 加解密 + +标准 DES 算法,ECB 模式。 + +```java +// 自动生成密钥 +DES des = new DES(); + +// 使用指定密钥 +DES des = new DES("myKey123"); +DES des = new DES(keyBytes); + +// 加解密 +byte[] encrypted = des.encrypt(data); +byte[] decrypted = des.decrypt(encrypted); +``` + +#### DESUtils — 预置密钥单例 + +```java +DESUtils utils = DESUtils.getInstance(); +String cipher = utils.encode("明文"); +String plain = utils.decode(cipher); +``` + +### RSA 加解密(2048 位) + +基于 BouncyCastle Provider,使用 `RSA/ECB/PKCS1Padding`,密钥长度 2048 位。 + +```java +// 自动生成密钥对 +RSA rsa = new RSA(); + +// 从已有密钥构造 +RSA rsa = new RSA(privateKeyBytes, publicKeyBytes); +RSA rsa = new RSA(publicKeyBytes); // 仅公钥(只能加密) +RSA rsa = new RSA(keyPair); + +// 公钥加密 / 私钥解密 +byte[] encrypted = rsa.encrypt("Hello".getBytes(StandardCharsets.UTF_8)); +byte[] decrypted = rsa.decrypt(encrypted); + +// 导出密钥 +byte[] pubKey = rsa.getPublicKey(); +byte[] priKey = rsa.getPrivateKey(); +``` + +#### RSAUtils — 预置密钥单例 + +内置 Base64 编码的 2048 位 RSA 密钥对: + +```java +RSAUtils utils = RSAUtils.getInstance(); +String cipher = utils.encode("敏感数据"); +String plain = utils.decode(cipher); +``` + +### SHA256 哈希 + +无状态静态方法,返回十六进制小写字符串: + +```java +String hash = SHA256.sha256("Hello World"); +// 输出: "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" +``` + +### HmacSHA256 签名 + +无状态静态方法,返回 HMAC-SHA256 签名字节数组: + +```java +byte[] signature = HmacSHA256.sha256( + "message".getBytes(StandardCharsets.UTF_8), + "secret-key".getBytes(StandardCharsets.UTF_8) +); +String hexSign = HexFormat.of().formatHex(signature); +``` + +## 使用实例 + +### 1. API 请求签名验证 + +```java +// 客户端签名 +String timestamp = String.valueOf(System.currentTimeMillis()); +String payload = userId + timestamp; +byte[] sign = HmacSHA256.sha256( + payload.getBytes(StandardCharsets.UTF_8), + appSecret.getBytes(StandardCharsets.UTF_8) +); +String signature = HexFormat.of().formatHex(sign); + +// 服务端验签 +byte[] expected = HmacSHA256.sha256( + payload.getBytes(StandardCharsets.UTF_8), + appSecret.getBytes(StandardCharsets.UTF_8) +); +boolean valid = Arrays.equals(sign, expected); +``` + +### 2. 用户敏感字段存储加密 + +```java +// 入库时加密 +String encryptedPhone = AESUtils.getInstance().encode(user.getPhone()); +user.setPhone(encryptedPhone); +userRepository.save(user); + +// 查询时解密 +String phone = AESUtils.getInstance().decode(user.getPhone()); +``` + +### 3. RSA 密钥交换 + AES 数据加密 + +```java +// 发送方:用接收方公钥加密 AES 密钥 +RSA rsa = new RSA(receiverPublicKeyBytes); +AES aes = new AES(); +byte[] encryptedAesKey = rsa.encrypt(aes.getKey()); + +// 用 AES 加密实际数据 +byte[] encryptedData = aes.encrypt(sensitiveData); + +// 接收方:用私钥解密 AES 密钥,再解密数据 +RSA rsaReceiver = new RSA(receiverPrivateKeyBytes, receiverPublicKeyBytes); +byte[] aesKey = rsaReceiver.decrypt(encryptedAesKey); +AES aesDecrypt = new AES(aesKey, aes.getIv()); +byte[] plainData = aesDecrypt.decrypt(encryptedData); +``` + +### 4. 密码哈希存储 + +```java +// 注册时 +String passwordHash = SHA256.sha256(rawPassword + salt); +user.setPassword(passwordHash); + +// 登录验证 +String inputHash = SHA256.sha256(inputPassword + salt); +boolean match = passwordHash.equals(inputHash); +``` + +### 5. 安全模块中自定义 AES 密钥 + +```java +@Configuration +public class SecurityCryptoConfig { + + @PostConstruct + public void initAESTools() throws Exception { + String key = environment.getProperty("security.aes.key"); + String iv = environment.getProperty("security.aes.iv"); + AESTools.getInstance().init(new AES(key, iv)); + } +} +``` diff --git a/docs/capabilities/data-authorization.md b/docs/capabilities/data-authorization.md new file mode 100644 index 000000000..e037b63c7 --- /dev/null +++ b/docs/capabilities/data-authorization.md @@ -0,0 +1,228 @@ +--- +name: data-authorization +description: 完整的数据权限框架,包含行级权限(WHERE/JOIN 条件注入)和列级权限(数据脱敏),基于 JSqlParser 的 SQL 增强引擎,提供 ColumnMask SPI、RowHandler/ColumnHandler SPI、SkipAuthorizationFilter 跳过授权机制 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 67895fda +code_files: + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationConfiguration.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/enhancer/DataPermissionSQLEnhancer.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DataAuthorizationFilter.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DefaultDataAuthorizationFilter.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/SkipAuthorizationFilter.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/handler/RowHandler.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/handler/ColumnHandler.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/handler/ColumnHandlerContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/ColumnMask.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/ColumnMaskContext.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/impl/PhoneMask.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/impl/BankCardMask.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/impl/IDCardMask.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/condition/IConditionSQL.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/condition/WhereConditionSQL.java + - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/condition/JoinConditionSQL.java +--- + +## 解决什么问题 + +在企业级应用中,不同角色/部门的用户只能看到其权限范围内的数据行,且敏感字段(手机号、银行卡号、身份证号等)需要脱敏展示。如果由每个业务模块自行实现权限过滤和数据脱敏,会导致大量重复代码、规则分散难以维护、容易遗漏造成数据泄露。 + +本能力提供一套完整的数据权限框架,从两个维度保障数据安全: + +- **行级权限**:通过 `RowHandler` SPI 定义过滤规则,`DataPermissionSQLEnhancer` 基于 JSqlParser 解析 SQL AST,自动向 SELECT 语句注入 WHERE 条件或 JOIN 关联表,支持子查询、UNION、嵌套 SELECT 等复杂场景 +- **列级权限**:通过 `ColumnHandler` SPI 拦截 ResultSet 读取,结合 `ColumnMask` SPI 对敏感数据自动脱敏。内置手机号、银行卡号、身份证号三种脱敏策略,可按需扩展 +- **跳过授权机制**:通过 `SkipAuthorizationFilter` 接口,允许特定 SQL 或场景(如管理后台全量导出)绕过数据权限拦截 +- **Spring Boot 自动装配**:通过 `DataAuthorizationConfiguration` 自动注册所有组件,业务方只需实现 SPI 接口并声明为 Bean 即可生效 + +## 如何使用 + +### 核心架构 + +``` +DataAuthorizationContext (单例入口) +├── DataAuthorizationFilter[] — 权限过滤器链(行+列) +├── SkipAuthorizationFilter — 跳过授权判断 +├── RowHandler — 行权限条件提供者 +├── ColumnHandler — 列权限结果处理器 +└── ColumnMask[] — 数据脱敏策略链 +``` + +### 行级权限 — RowHandler + +实现 `RowHandler` 接口,根据表名返回需要注入的 SQL 条件: + +```java +public interface RowHandler { + Condition handler(String subSql, String tableName, String tableAlias); +} +``` + +`Condition` 包含 `List` 条件列表,支持两种条件类型: + +- **WhereConditionSQL**:追加 WHERE 条件,如 `"t.dept_id = %d"` +- **JoinConditionSQL**:追加 JOIN 关联表,支持 INNER/LEFT/RIGHT 三种类型,指定表名、别名和 ON 条件 + +将 `RowHandler` 实现声明为 Spring Bean,`ConditionHandlerRegister` 会自动将其注册到框架中。 + +### 列级权限 — ColumnHandler + +实现 `ColumnHandler` 接口,拦截 ResultSet 中各类型字段的读取: + +```java +public interface ColumnHandler { + String getString(SQLExecuteState state, int columnIndex, + String tableName, String columnName, String value); + int getInt(SQLExecuteState state, int columnIndex, + String tableName, String columnName, int value); + // ... 覆盖所有 JDBC ResultSet.getXxx() 方法 +} +``` + +`ColumnHandlerContext` 是单例委托器,框架内部通过它调用 `ColumnHandler`。将 `ColumnHandler` 实现声明为 Spring Bean,`ResultSetHandlerRegister` 会自动注册。 + +### 数据脱敏 — ColumnMask SPI + +```java +public interface ColumnMask { + boolean support(Object value); // 判断是否匹配该脱敏规则 + Object mask(Object value); // 执行脱敏 +} +``` + +内置三种脱敏实现,通过正则匹配自动识别: + +| 实现类 | 匹配规则 | 脱敏效果 | +|--------|----------|----------| +| `PhoneMask` | `^1[3-9]\d{9}$` | `138****1234` | +| `BankCardMask` | `^\d{13,19}$` | `622202******1234` | +| `IDCardMask` | 15/18位身份证 | `110101********1234` | + +通过 `ColumnMaskContext.getInstance().addColumnMask(mask)` 注册自定义脱敏策略,按注册顺序依次匹配,首个命中即返回。 + +### 跳过授权 — SkipAuthorizationFilter + +```java +public interface SkipAuthorizationFilter { + String filter(String sql); // 返回处理后的 SQL,或 null 表示不跳过 +} +``` + +通过 `DataAuthorizationContext.getInstance().setSkipAuthorizationFilter(filter)` 设置。默认使用 `DefaultSkipAuthorizationFilter`(不跳过任何 SQL)。 + +### 自动配置 + +`DataAuthorizationConfiguration` 通过 `@Configuration` 自动注册以下 Bean: + +- `DataAuthorizationProperties`(配置前缀 `codingapi.data-authorization`) +- `ConditionHandlerRegister`(注入 `RowHandler`) +- `ResultSetHandlerRegister`(注入 `ColumnHandler`) +- `SQLInterceptorRegister`(注入 `SQLInterceptor`) +- `DataAuthorizationContextRegister`(注入所有 `DataAuthorizationFilter` Bean) + +所有 Handler/Filter 均为 `@Autowired(required = false)`,未提供时使用默认空实现。 + +## 使用实例 + +### 1. 实现行级数据权限 + +```java +@Component +public class DeptRowHandler implements RowHandler { + + @Override + public Condition handler(String subSql, String tableName, String tableAlias) { + if ("employee".equalsIgnoreCase(tableName)) { + Long deptId = SecurityContext.getCurrentDeptId(); + WhereConditionSQL where = new WhereConditionSQL( + "%s.dept_id = %d", tableAlias, deptId + ); + return Condition.of(where); + } + return null; + } +} +``` + +### 2. 实现列级数据脱敏 + +```java +@Component +public class SensitiveColumnHandler extends DefaultColumnHandler { + + @Override + public String getString(SQLExecuteState state, int columnIndex, + String tableName, String columnName, String value) { + // 委托给 ColumnMaskContext 进行自动脱敏 + return ColumnMaskContext.getInstance().mask(value); + } +} +``` + +### 3. 注册自定义脱敏策略 + +```java +@Component +public class EmailMask implements ColumnMask { + + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[\\w.-]+@[\\w.-]+\\.\\w+$"); + + @Override + public boolean support(Object value) { + return value instanceof String + && EMAIL_PATTERN.matcher((String) value).matches(); + } + + @Override + public Object mask(Object value) { + String email = (String) value; + int atIndex = email.indexOf('@'); + return email.charAt(0) + "***" + email.substring(atIndex); + } +} + +// 在初始化时注册 +@PostConstruct +public void init() { + ColumnMaskContext.getInstance().addColumnMask(new EmailMask()); +} +``` + +### 4. 使用 JOIN 条件实现跨表行权限 + +```java +@Component +public class ProjectRowHandler implements RowHandler { + + @Override + public Condition handler(String subSql, String tableName, String tableAlias) { + if ("task".equalsIgnoreCase(tableName)) { + JoinConditionSQL join = new JoinConditionSQL( + JoinConditionSQL.Type.INNER, + "project_member", + "pm", + String.format("%s.project_id = pm.project_id AND pm.user_id = %d", + tableAlias, SecurityContext.getCurrentUserId()) + ); + return Condition.of(join); + } + return null; + } +} +``` + +### 5. 临时跳过数据权限 + +```java +// 管理后台导出全量数据 +DataAuthorizationContext.getInstance() + .setSkipAuthorizationFilter(sql -> sql); // 放行所有 SQL + +List all = employeeRepository.findAll(); + +// 恢复正常权限 +DataAuthorizationContext.getInstance() + .setSkipAuthorizationFilter(new DefaultSkipAuthorizationFilter()); +``` diff --git a/docs/capabilities/dynamic-application.md b/docs/capabilities/dynamic-application.md new file mode 100644 index 000000000..2b59e906c --- /dev/null +++ b/docs/capabilities/dynamic-application.md @@ -0,0 +1,132 @@ +--- +name: dynamic-application +description: 支持外部 JAR 加载和 Spring 上下文热重启的应用启动器 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: e8583c68 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/boot/DynamicApplication.java +--- + +## 解决什么问题 + +标准 Spring Boot 应用的类路径在启动时确定,运行期间无法动态加载新的 JAR 包或模块。当业务需要以下场景时,传统启动方式无法满足: + +- **插件化架构**:核心应用运行时加载外部插件 JAR,无需重新打包主应用 +- **热部署/热更新**:开发或运维过程中,替换 JAR 后自动重启 Spring 上下文,减少停机时间 +- **模块化分发**:不同客户/环境使用不同的 JAR 组合,通过外部目录配置而非 Maven 依赖 + +本能力提供了 `DynamicApplication` 启动器,替代标准的 `SpringApplication.run()`,在启动前扫描指定目录下的 JAR 文件并注入到类加载器中,同时支持运行时关闭并重建 Spring 上下文(热重启)。 + +## 如何使用 + +### 核心类 + +| 类 | 说明 | +|----|------| +| `DynamicApplication` | 单例应用启动器,管理外部 JAR 加载和 Spring 上下文生命周期 | + +### API + +```java +// 启动应用(替代 SpringApplication.run) +DynamicApplication.run(Class applicationClass, String[] args) + +// 热重启(关闭当前上下文 → 重新加载 JAR → 重新启动) +DynamicApplication.restart() + +// 获取单例实例 +DynamicApplication app = DynamicApplication.getInstance() + +// 设置外部 JAR 目录(默认 ./jars) +app.setJarsFolder("/path/to/plugins") +``` + +### 启动流程 + +1. 调用 `DynamicApplication.run(App.class, args)` +2. 扫描 `jarsFolder` 目录下所有 `.jar` 文件 +3. 创建 `URLClassLoader` 并将这些 JAR 加入类路径 +4. 将当前线程的 ContextClassLoader 设置为该 URLClassLoader +5. 调用 `SpringApplication.run()` 启动 Spring Boot 应用 + +### 热重启流程 + +1. 调用 `DynamicApplication.restart()` +2. 在新线程中执行(非守护线程,确保 JVM 不会提前退出) +3. 关闭当前 `ConfigurableApplicationContext` +4. 重新扫描 JAR 目录并更新 ClassLoader +5. 使用原始启动类和参数重新启动 Spring Boot + +### 配置 + +```java +// 修改外部 JAR 目录(默认为工作目录下的 ./jars) +DynamicApplication.getInstance().setJarsFolder("/opt/app/plugins"); +``` + +### 注意事项 + +- 外部 JAR 目录不存在或为空时,不加载任何额外类路径,应用正常启动 +- 热重启会完全销毁并重建 Spring 上下文,所有 Bean 会被重新初始化 +- 热重启在新线程中执行,原调用线程立即返回 +- `URLClassLoader` 以系统 ClassLoader 为父加载器,外部 JAR 中的类优先级高于系统类路径中的同名类 + +## 使用实例 + +### 1. 基本启动(替代 SpringApplication.run) + +```java +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + // 替代 SpringApplication.run(MyApplication.class, args); + DynamicApplication.run(MyApplication.class, args); + } +} +``` + +### 2. 自定义 JAR 目录启动 + +```java +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + DynamicApplication.getInstance().setJarsFolder("/opt/myapp/extensions"); + DynamicApplication.run(MyApplication.class, args); + } +} +``` + +### 3. 通过 REST API 触发重启 + +```java +@RestController +@RequestMapping("/admin") +public class AdminController { + + @PostMapping("/restart") + public Response restart() { + // 异步重启,接口立即返回 + DynamicApplication.restart(); + return Response.success(); + } +} +``` + +### 4. 目录结构示例 + +``` +project-root/ +├── jars/ ← 默认外部 JAR 目录 +│ ├── plugin-a.jar ← 自动加载 +│ ├── plugin-b.jar ← 自动加载 +│ └── readme.txt ← 非 .jar 文件,忽略 +├── src/ +└── pom.xml +``` + +将新的插件 JAR 放入 `jars/` 目录后,调用 `DynamicApplication.restart()` 即可加载新插件并重启应用。 diff --git a/docs/capabilities/dynamic-mvc-mapping.md b/docs/capabilities/dynamic-mvc-mapping.md new file mode 100644 index 000000000..0511cf1cc --- /dev/null +++ b/docs/capabilities/dynamic-mvc-mapping.md @@ -0,0 +1,168 @@ +--- +name: dynamic-mvc-mapping +description: 运行时动态注册/注销 Spring MVC REST 端点,支持将 Groovy 脚本绑定到动态 API 路径 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: d5569b1d +code_files: + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/mapping/FastMvcMappingRegister.java + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/script/FastScriptMappingRegister.java + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/script/ScriptMapping.java + - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/script/ScriptRuntime.java +--- + +## 解决什么问题 + +传统 Spring MVC 的 REST 端点在编译期通过注解(`@RequestMapping`)静态声明,无法在运行时按需增减。当业务需要以下场景时,静态映射无法满足: + +- **低代码/无代码平台**:用户通过界面配置 API,后端需动态生成对应的 REST 端点 +- **Groovy 脚本绑定 API**:将动态编写的 Groovy 脚本挂载到指定 URL 路径,作为即时可用的 REST 接口 +- **插件化扩展**:第三方模块在运行时注册自己的 API,卸载时自动注销 +- **灰度/临时接口**:在不重启应用的前提下快速上线或下线调试端点 + +本能力封装了 Spring `RequestMappingHandlerMapping` 的底层 API,提供安全的运行时端点注册/注销操作,并与 Groovy 脚本引擎集成,实现"脚本即 API"的动态绑定。 + +## 如何使用 + +### 核心组件 + +| 类 | 说明 | +|----|------| +| `FastMvcMappingRegister` | 底层 MVC 映射注册器,直接操作 `RequestMappingHandlerMapping` | +| `FastScriptMappingRegister` | 上层脚本映射注册器,将 `ScriptMapping` 绑定为 REST 端点 | +| `ScriptMapping` | 脚本与 URL 的绑定关系,持有映射路径、HTTP 方法和 Groovy 脚本内容 | +| `ScriptRuntime` | 脚本执行运行时(单例),向脚本注入 `$request`、`$jpa`、`$jdbc` 上下文变量 | +| `ScriptMethod` | HTTP 方法枚举(GET / POST),转换为 Spring `RequestMethod` | +| `ScriptRequest` | 对 `HttpServletRequest` 的类型安全封装,提供带默认值的参数获取方法 | + +### 自动配置 + +`DataFastConfiguration` 在检测到 `WebMvcConfigurer` 类存在时自动注册以下 Bean: + +- `FastMvcMappingRegister` — 注入 Spring 容器的 `requestMappingHandlerMapping` +- `FastScriptMappingRegister` — 依赖 `FastMvcMappingRegister` + +无需手动配置,引入 `springboot-starter-data-fast` 模块即可使用。 + +### FastMvcMappingRegister API + +```java +// 注册端点 +void addMapping(String url, RequestMethod requestMethod, Object handler, Method method) + +// 注销端点 +void removeMapping(String url, RequestMethod requestMethod) +``` + +所有端点默认产出 `application/json` 媒体类型,使用 `PathPatternParser` 解析路径模式。 + +### FastScriptMappingRegister API + +```java +// 将脚本映射注册为 REST 端点 +void addMapping(ScriptMapping scriptMapping) + +// 测试脚本映射(不注册端点,直接执行并返回结果) +Response test(ScriptMapping scriptMapping) + +// 注销脚本映射对应的端点 +void removeMapping(String url, ScriptMethod scriptMethod) +``` + +### ScriptRuntime 注入变量 + +脚本执行时自动绑定以下变量: + +| 变量名 | 类型 | 说明 | +|--------|------|------| +| `$request` | `ScriptRequest` | 当前 HTTP 请求的类型安全封装 | +| `$jpa` | `JpaQuery` | JPA 查询工具 | +| `$jdbc` | `JdbcQuery` | JDBC 查询工具 | + +### ScriptRequest 参数获取 + +```java +String getParameter(String key, String defaultValue) +int getParameter(String key, int defaultValue) +float getParameter(String key, float defaultValue) +double getParameter(String key, double defaultValue) +long getParameter(String key, long defaultValue) +boolean getParameter(String key, boolean defaultValue) +PageRequest pageRequest(int pageNumber, int pageSize) +``` + +## 使用实例 + +### 1. 注册一个动态 GET 端点 + +```java +@Autowired +private FastScriptMappingRegister scriptMappingRegister; + +// 创建脚本映射:GET /api/dynamic/hello → 返回字符串 +ScriptMapping mapping = new ScriptMapping( + "/api/dynamic/hello", + ScriptMethod.GET, + "return 'Hello, Dynamic API!'" +); + +// 注册为 REST 端点 +scriptMappingRegister.addMapping(mapping); +// 此时 GET /api/dynamic/hello 已可用,返回 SingleResponse{"Hello, Dynamic API!"} +``` + +### 2. 注册一个带参数的 POST 端点 + +```java +String script = """ + def name = $request.getParameter('name', 'World') + def age = $request.getParameter('age', 0) + return [name: name, age: age] +"""; + +ScriptMapping mapping = new ScriptMapping( + "/api/dynamic/greet", + ScriptMethod.POST, + script +); +scriptMappingRegister.addMapping(mapping); +``` + +### 3. 测试脚本(不注册端点) + +```java +ScriptMapping mapping = new ScriptMapping( + "/test", + ScriptMethod.GET, + "return [1, 2, 3]" +); + +// 直接执行脚本并获取结果,不会注册到 MVC +Response result = scriptMappingRegister.test(mapping); +``` + +### 4. 注销动态端点 + +```java +// 移除之前注册的端点 +scriptMappingRegister.removeMapping("/api/dynamic/hello", ScriptMethod.GET); +// GET /api/dynamic/hello 不再可用 +``` + +### 5. 脚本中使用 JPA/JDBC 查询 + +```java +String script = """ + def page = $request.pageRequest(0, 20) + def users = $jpa.query('select u from User u', page) + return users +"""; + +ScriptMapping mapping = new ScriptMapping( + "/api/dynamic/users", + ScriptMethod.GET, + script +); +scriptMappingRegister.addMapping(mapping); +``` diff --git a/docs/capabilities/index.md b/docs/capabilities/index.md index 236420d95..0a3e6bc19 100644 --- a/docs/capabilities/index.md +++ b/docs/capabilities/index.md @@ -6,13 +6,18 @@ | 名称 | 描述 | 范围 | 来源 | |------|------|------|------| +| [crypto-tools](./crypto-tools.md) | 密码学工具套件,AES(CBC 模式)/RSA(BouncyCastle)/DES 加解密 + SHA256/HmacSHA256 哈希签名,提供实例化工... | 后端 | 项目自有 | +| [data-authorization](./data-authorization.md) | 完整的数据权限框架,包含行级权限(WHERE/JOIN 条件注入)和列级权限(数据脱敏),基于 JSqlParser 的 SQL 增强引擎,提供 Colu... | 后端 | 项目自有 | | [domain-change-interceptor](./domain-change-interceptor.md) | 通过 CGLIB 代理拦截领域实体字段的 setter 方法,自动检测变更并发布 DomainChangeEvent | 后端 | 项目自有 | +| [dynamic-application](./dynamic-application.md) | 支持外部 JAR 加载和 Spring 上下文热重启的应用启动器 | 后端 | 项目自有 | +| [dynamic-mvc-mapping](./dynamic-mvc-mapping.md) | 运行时动态注册/注销 Spring MVC REST 端点,支持将 Groovy 脚本绑定到动态 API 路径 | 后端 | 项目自有 | | [event-system](./event-system.md) | 自建的领域事件发布-订阅系统,支持同步/异步事件、事务后提交、Handler自动注册排序 | 后端 | 项目自有 | | [fast-repository](./fast-repository.md) | JPA Repository 增强,支持动态过滤查询(RequestFilter + Relation 操作符)、HQL 自动构建、排序 | 后端 | 项目自有 | | [groovy-script-engine](./groovy-script-engine.md) | Groovy 动态脚本引擎,支持运行时编译执行、LRU 缓存、热更新和 REST API | 后端 | 项目自有 | | [jackson](./jackson.md) | Jackson JSON 序列化/反序列化能力,由 Spring Boot Web 自动配置,框架中用于 REST API 响应序列化、请求参数绑定等场景 | 后端 | 框架:jackson | | [jdbc-proxy-chain](./jdbc-proxy-chain.md) | 完整的 JDBC 装饰器代理链,透明拦截 SQL 执行以实现数据权限控制 | 后端 | 项目自有 | | [locale-exception](./locale-exception.md) | 支持 i18n 的业务异常体系,errCode + errMessage,通过 MessageSource 实现国际化 | 后端 | 项目自有 | +| [rest-client](./rest-client.md) | 出站 HTTP 客户端框架,提供 HttpClient(底层 HTTP 封装)、RestClient(REST 高级客户端)、SessionClient(... | 后端 | 项目自有 | | [spring-data-jpa](./spring-data-jpa.md) | Spring Data JPA 数据访问能力,提供 ORM 映射、Repository 抽象、分页查询等基础设施,框架在此基础上扩展了 FastRepos... | 后端 | 框架:spring-data-jpa | | [spring-data-redis](./spring-data-redis.md) | Spring Data Redis 数据存储能力,框架中主要用于 Token/Session 的有状态存储,支持服务端主动失效和多端登录管理 | 后端 | 框架:spring-data-redis | | [spring-framework](./spring-framework.md) | Spring Framework / Spring Boot 核心基础能力,提供 IoC 容器、事件发布、AOP、Web MVC、事务管理等基础设施 | 后端 | 框架:spring-framework | @@ -21,6 +26,7 @@ | [token-gateway](./token-gateway.md) | JWT/Redis 双模式 Token 认证网关,统一创建、解析和管理认证令牌 | 后端 | 项目自有 | | [transaction-manager-context](./transaction-manager-context.md) | 单例编程式事务上下文,支持 REQUIRES_NEW 语义,在非 Spring 管理上下文中也能使用事务 | 后端 | 项目自有 | | [trigger-system](./trigger-system.md) | 触发器框架,支持定时/条件触发,提供统一的触发上下文管理 | 后端 | 项目自有 | +| [user-context](./user-context.md) | 线程级用户身份上下文,通过 ThreadLocal 持有当前登录用户,支持跨层传递 | 后端 | 项目自有 | | [workflow-engine](./workflow-engine.md) | 工作流引擎,支持流程定义、节点流转、审批、委托、会签、数据快照和事件通知 | 后端 | 项目自有 | ## 🗓️ 计划中 @@ -31,4 +37,4 @@ --- -**统计**: 共 17 篇 — 已实现 16 / 计划中 1 / 已废弃 0 +**统计**: 共 23 篇 — 已实现 22 / 计划中 1 / 已废弃 0 diff --git a/docs/capabilities/rest-client.md b/docs/capabilities/rest-client.md new file mode 100644 index 000000000..986f5412a --- /dev/null +++ b/docs/capabilities/rest-client.md @@ -0,0 +1,226 @@ +--- +name: rest-client +description: 出站 HTTP 客户端框架,提供 HttpClient(底层 HTTP 封装)、RestClient(REST 高级客户端)、SessionClient(Cookie/Session 感知)、HttpRequest(请求构建器 + 代理支持) +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: dc5e04ef +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/HttpClient.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/RestClient.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/SessionClient.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/HttpRequest.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/Request.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/RestTemplateContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/param/IRestParam.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/param/RestParam.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/properties/HttpProxyProperties.java +--- + +## 解决什么问题 + +在微服务架构中,服务间调用、第三方 API 对接、爬虫数据采集等场景需要频繁发起出站 HTTP 请求。直接使用 Spring `RestTemplate` 存在以下痛点: + +- **缺少重试机制**:网络抖动时直接抛异常,需要手动包装重试逻辑 +- **Session/Cookie 管理繁琐**:模拟登录等有状态交互需手动维护 Cookie +- **代理配置分散**:HTTP 代理设置需要在每处创建 RestTemplate 时重复配置 +- **参数构建不便**:GET 查询参数和 POST 表单/JSON 的参数组装代码冗长 + +本能力提供三层 HTTP 客户端抽象: + +- **HttpClient**:轻量级底层封装,适合单次无状态调用 +- **RestClient**:REST 高级客户端,内置 baseUrl 拼接、自动重试、默认 JSON Content-Type +- **SessionClient**:Cookie/Session 感知客户端,自动跟踪 Set-Cookie 响应头并回传,支持 302 重定向跟随 + +所有客户端共享 `RestTemplateContext` 单例(基于 Apache HttpComponents,信任所有证书),统一支持 HTTP 代理配置。 + +## 如何使用 + +### 核心类层次 + +``` +RestTemplateContext (单例, Apache HttpComponents) +└── HttpRequest (请求构建器, 代理支持, 拦截器钩子) + ├── HttpClient (基础 GET/POST) + ├── RestClient (baseUrl + 重试 + REST 风格) + └── SessionClient (Cookie 跟踪 + 重定向) +``` + +### HttpRequest — 请求构建器 + +底层请求引擎,封装 `RestTemplate.exchange()` 调用,提供两个扩展点: + +```java +// 请求拦截器:可修改 URL、Header、Body +interface IHttpRequestHandler { + String handler(HttpRequest client, String uri, HttpMethod method, + HttpHeaders headers, HttpEntity httpEntity); +} + +// 响应拦截器:可处理状态码、重定向、错误 +interface IHttpResponseHandler { + String handler(HttpRequest client, String uri, ResponseEntity response); +} +``` + +默认行为:请求拦截器透传 URL;响应拦截器处理 200/404 返回 body,302 自动跟随重定向。连接超时固定为 3000ms。 + +### HttpClient — 基础客户端 + +```java +// 无代理 +HttpClient client = new HttpClient(); + +// 带代理 +HttpProxyProperties proxy = new HttpProxyProperties(); +proxy.setEnableProxy(true); +proxy.setProxyHost("proxy.example.com"); +proxy.setProxyPort(8080); +proxy.setProxyType(Proxy.Type.HTTP); +HttpClient client = new HttpClient(proxy); + +// 自定义请求/响应处理器 +HttpClient client = new HttpClient(proxy, requestHandler, responseHandler); +``` + +提供三个便捷方法: +- `post(url, headers, JSON)` — POST JSON 请求 +- `post(url, headers, MultiValueMap)` — POST 表单请求 +- `get(url, headers, uriVariables)` — GET 请求 + +### RestClient — REST 高级客户端 + +```java +// 最简构造:指定 baseUrl,默认重试 5 次,空响应 "{}" +RestClient rest = new RestClient("https://api.example.com"); + +// 完整构造 +RestClient rest = new RestClient(proxyProperties, baseUrl, retryCount, + emptyResponse, requestHandler, responseHandler); +``` + +特性: +- **baseUrl 自动拼接**:`rest.get("/users")` → `https://api.example.com/users` +- **自动重试**:请求失败时按 `retryCount` 重试,每次间隔 1 秒 +- **默认 JSON**:初始化时自动设置 `Content-Type: application/json` +- **优雅降级**:重试耗尽后返回 `emptyResponse` 而非抛异常 + +### SessionClient — Session 感知客户端 + +```java +SessionClient session = new SessionClient(); // 无代理 +SessionClient session = new SessionClient(proxyProperties); // 带代理 + +// 链式添加 Header +session.addHeader("Authorization", "Bearer xxx"); + +// 表单提交(自动设置 APPLICATION_FORM_URLENCODED) +session.postForm("https://example.com/login", restParam); + +// JSON 提交 +session.postJson("https://example.com/api/data", restParam); + +// GET 请求 +session.getJson("https://example.com/api/users"); +session.getHtml("https://example.com/page"); +``` + +内部通过自定义 `IHttpResponseHandler` 自动将响应中的 `Set-Cookie` 转为后续请求的 `Cookie` 头,并在遇到 302 时自动跟随重定向。 + +### RestParam — 统一参数构建器 + +```java +// 手动构建 +RestParam param = RestParam.create() + .add("username", "admin") + .add("password", "secret"); + +// 从对象解析 +RestParam param = RestParam.parser(myObject); + +// 转换为不同格式 +JSONObject json = param.toJsonRequest(); // POST JSON +MultiValueMap form = param.toFormRequest(); // POST 表单 +MultiValueMap query = param.toGetRequest(); // GET 参数 +``` + +实现 `IRestParam` 接口的 POJO 可直接调用 `toParameters()` 转为 `RestParam`。 + +### HttpProxyProperties — 代理配置 + +```java +@Setter @Getter +public class HttpProxyProperties { + private boolean enableProxy; + private String proxyHost; + private int proxyPort; + private Proxy.Type proxyType; +} +``` + +## 使用实例 + +### 1. 调用第三方 REST API(带重试) + +```java +RestClient api = new RestClient("https://api.third-party.com"); + +// GET 请求 +String users = api.get("/v1/users"); + +// POST JSON 请求 +RestParam param = RestParam.create() + .add("name", "张三") + .add("age", 25); +String result = api.post("/v1/users", param); + +// 带自定义 Header +HttpHeaders headers = new HttpHeaders(); +headers.set("X-API-Key", "my-key"); +String data = api.get("/v1/data", headers); +``` + +### 2. 模拟登录并保持会话 + +```java +SessionClient session = new SessionClient(); + +// 登录(表单提交,自动保存 Cookie) +RestParam loginParam = RestParam.create() + .add("username", "admin") + .add("password", "password123"); +session.postForm("https://app.example.com/login", loginParam); + +// 后续请求自动携带 Cookie +String dashboard = session.getHtml("https://app.example.com/dashboard"); +String userInfo = session.getJson("https://app.example.com/api/user/info"); +``` + +### 3. 通过代理访问外部服务 + +```java +HttpProxyProperties proxy = new HttpProxyProperties(); +proxy.setEnableProxy(true); +proxy.setProxyHost("proxy.corp.com"); +proxy.setProxyPort(3128); +proxy.setProxyType(Proxy.Type.HTTP); + +RestClient client = new RestClient(proxy, "https://external-api.com", + 3, "{}", null, null); +String response = client.get("/data"); +``` + +### 4. 自定义请求/响应拦截 + +```java +// 请求签名拦截器 +HttpRequest.IHttpRequestHandler signer = (client, uri, method, headers, entity) -> { + String timestamp = String.valueOf(System.currentTimeMillis()); + headers.set("X-Timestamp", timestamp); + headers.set("X-Signature", HmacSHA256.sign(uri + timestamp, secretKey)); + return uri; +}; + +HttpClient httpClient = new HttpClient(signer, null); +String result = httpClient.get("https://secure-api.com/data", headers, null); +``` diff --git a/docs/capabilities/user-context.md b/docs/capabilities/user-context.md new file mode 100644 index 000000000..3e36d7657 --- /dev/null +++ b/docs/capabilities/user-context.md @@ -0,0 +1,142 @@ +--- +name: user-context +description: 线程级用户身份上下文,通过 ThreadLocal 持有当前登录用户,支持跨层传递 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 719167a0 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/user/UserContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/user/IUser.java +--- + +## 解决什么问题 + +在企业应用中,几乎所有业务操作都需要知道"当前操作用户是谁"。传统做法是在方法签名中逐层传递用户对象(Controller → Service → Repository),导致: + +- **参数污染**:每个业务方法都需要额外接收用户参数,签名臃肿 +- **跨层耦合**:中间层即使不使用用户信息也必须透传 +- **框架集成困难**:事件处理器、异步任务、AOP 切面等非调用链场景无法方便地获取用户身份 + +本能力通过 `ThreadLocal` 在当前线程中绑定用户身份对象,使任意层级的代码都能直接获取当前登录用户,无需显式传参。配合安全模块(`springboot-starter-security`)的认证过滤器,在请求入口处自动设置、请求结束时自动清理。 + +## 如何使用 + +### 核心接口与类 + +| 类型 | 说明 | +|------|------| +| `IUser` | 用户身份标记接口,业务模块实现此接口定义自己的用户实体 | +| `UserContext` | 单例用户上下文管理器,基于 `ThreadLocal` 存储当前线程的用户身份 | + +### API + +```java +// 获取全局单例 +UserContext ctx = UserContext.getInstance(); + +// 设置当前线程的用户身份 +ctx.setCurrent(IUser user); + +// 获取当前线程的用户身份 +IUser user = ctx.current(); +``` + +### 实现 IUser + +`IUser` 是一个纯标记接口(无方法声明),业务模块自行定义用户属性: + +```java +public class LoginUser implements IUser { + private String userId; + private String username; + private List roles; + // getter/setter... +} +``` + +### 与安全模块集成 + +在认证过滤器中设置用户上下文: + +```java +// 认证通过后设置 +UserContext.getInstance().setCurrent(loginUser); + +// 请求结束后清理(防止线程池复用导致上下文泄漏) +UserContext.getInstance().setCurrent(null); +``` + +### 注意事项 + +- `UserContext` 基于 `ThreadLocal`,仅在同一个线程内有效 +- 异步场景(`@Async`、线程池)需要手动传递或配合 `TaskDecorator` 复制上下文 +- 务必在请求结束时清理(设为 `null`),避免线程池场景下的用户信息泄漏 + +## 使用实例 + +### 1. 在 Service 层获取当前用户 + +```java +@Service +public class OrderService { + + public Order createOrder(CreateOrderCmd cmd) { + // 无需方法参数传入用户,直接从上下文获取 + IUser currentUser = UserContext.getInstance().current(); + LoginUser loginUser = (LoginUser) currentUser; + + Order order = new Order(); + order.setCreatorId(loginUser.getUserId()); + order.setCreatorName(loginUser.getUsername()); + // ... + return orderRepository.save(order); + } +} +``` + +### 2. 在事件 Handler 中获取操作用户 + +```java +@Handler +@Component +public class AuditLogHandler implements IHandler { + + @Override + public void handler(DomainCreateEvent event) { + IUser user = UserContext.getInstance().current(); + // 记录审计日志:谁在什么时候创建了什么 + auditLogRepository.save(new AuditLog( + user != null ? user.toString() : "system", + event.getEntityClass().getSimpleName(), + LocalDateTime.now() + )); + } +} +``` + +### 3. 在认证过滤器中设置与清理 + +```java +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) + throws ServletException, IOException { + try { + // 解析 Token → 构建用户对象 + LoginUser loginUser = tokenGateway.parseAndBuildUser(request); + if (loginUser != null) { + UserContext.getInstance().setCurrent(loginUser); + } + chain.doFilter(request, response); + } finally { + // 请求结束必须清理,防止线程池复用导致上下文污染 + UserContext.getInstance().setCurrent(null); + } + } +} +``` diff --git a/docs/conventions/context-register-pairing.md b/docs/conventions/context-register-pairing.md new file mode 100644 index 000000000..fdcbd0de3 --- /dev/null +++ b/docs/conventions/context-register-pairing.md @@ -0,0 +1,222 @@ +--- +name: context-register-pairing +description: 每个 Context 单例必须配套 Register 类和 Configuration 类,形成三件套:Context(持有状态)+ Register(注入依赖)+ Configuration(注册 Bean) +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 303b377f +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContext.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextRegister.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextConfiguration.java +--- + +## 解决什么问题 + +本规范是对已有 `context-singleton` 规范的补充细化。`context-singleton` 定义了 Context 单例的基本结构,但在实际开发中经常出现以下问题: + +1. **Register 缺失**:开发者创建了 Context 单例,但忘记编写对应的 Register 类来注入 Spring Bean 依赖,导致 Context 中的依赖为 null,运行时才暴露 NPE。 +2. **Configuration 遗漏**:Register 类虽然写了,但没有通过 `@Configuration` + `@Bean` 注册到 Spring 容器中,Register 不会被实例化,`afterPropertiesSet()` 永远不会被调用。 +3. **三者分散在不同模块**:Context、Register、Configuration 散落在不同的包或模块中,新增功能时难以找到完整的配对关系,维护成本高。 +4. **依赖注入方式不一致**:有的用构造函数注入,有的用 `@Autowired` 字段注入,有的直接在 Configuration 中调用 setter,缺乏统一模式。 + +本规范要求:**每创建一个 Context 单例,必须同时创建配套的 Register 和 Configuration 类,三者缺一不可,且放在同一个包下。** + +## 如何使用 + +### 三件套命名规范 + +以 `Xxx` 为业务前缀,三个类的命名固定为: + +| 类名 | 职责 | 关键特征 | +|------|------|---------| +| `XxxContext` | 持有全局状态的饿汉式单例 | `private static final XxxContext instance = new XxxContext()`;私有构造函数;提供 `getInstance()` | +| `XxxContextRegister` | 实现 `InitializingBean`,在 Bean 初始化完成后将依赖注入到 Context | 构造函数接收所需依赖;`afterPropertiesSet()` 中调用 Context 的 setter | +| `XxxContextConfiguration` | `@Configuration` 类,通过 `@Bean` 方法注册 Register 实例 | 可声明 `@ConditionalOnClass` 等条件注解;依赖参数使用 `@Autowired(required = false)` 避免启动失败 | + +### 文件组织 + +三个类必须放在**同一个 Java 包**下,文件名与类名一致: + +``` +com.codingapi.springboot.framework.transaction/ +├── TransactionManagerContext.java +├── TransactionManagerContextRegister.java +└── TransactionManagerContextConfiguration.java +``` + +### Context 类模板 + +```java +@Slf4j +public class XxxContext { + + @Getter + private static final XxxContext instance = new XxxContext(); + + private SomeDependency dependency; + + private XxxContext() {} + + public void setDependency(SomeDependency dependency) { + this.dependency = dependency; + if (dependency != null) { + log.info("{} load success", dependency); + } + } + + // 对外 API 方法 + public T execute(Supplier supplier) { + if (dependency != null) { + // 使用依赖执行业务逻辑 + } + return supplier.get(); + } +} +``` + +### Register 类模板 + +```java +public class XxxContextRegister implements InitializingBean { + + private final SomeDependency dependency; + + public XxxContextRegister(SomeDependency dependency) { + this.dependency = dependency; + } + + @Override + public void afterPropertiesSet() throws Exception { + XxxContext.getInstance().setDependency(dependency); + } +} +``` + +### Configuration 类模板 + +```java +@Configuration +public class XxxContextConfiguration { + + @Bean + public XxxContextRegister xxxContextRegister( + @Autowired(required = false) SomeDependency dependency) { + return new XxxContextRegister(dependency); + } +} +``` + +### 关键规则 + +1. **Register 只负责注入**:`afterPropertiesSet()` 中只做 setter 调用,不包含业务逻辑。 +2. **Configuration 只负责注册**:`@Bean` 方法只构造 Register 实例,不做其他操作。 +3. **可选依赖使用 `required = false`**:当依赖可能不存在时(如 `PlatformTransactionManager` 在某些测试环境中),使用 `@Autowired(required = false)` 避免启动失败。Context 的 API 方法内部需对 null 做防御性检查。 +4. **Context 的 setter 应有日志**:注入成功时打印 INFO 日志,便于排查启动问题。 +5. **自动配置注册**:Configuration 类需在 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 和 `META-INF/spring.factories` 中同时注册。 + +## 使用实例 + +```java +// ✅ 正确 — 完整的三件套(以 TransactionManager 为例) + +// 1. Context:持有 PlatformTransactionManager,提供编程式事务 API +public class TransactionManagerContext { + @Getter + private static final TransactionManagerContext instance = new TransactionManagerContext(); + @Getter + private PlatformTransactionManager platformTransactionManager; + + private TransactionManagerContext() {} + + public void setPlatformTransactionManager(PlatformTransactionManager ptm) { + this.platformTransactionManager = ptm; + if (ptm != null) { + log.info("platformTransactionManager:{} load success", ptm); + } + } + + public T commit(Supplier supplier) { + if (platformTransactionManager != null) { + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionStatus status = platformTransactionManager.getTransaction(def); + try { + T result = supplier.get(); + platformTransactionManager.commit(status); + return result; + } catch (Exception e) { + platformTransactionManager.rollback(status); + throw e; + } + } + return supplier.get(); // 无事务管理器时降级执行 + } +} + +// 2. Register:启动时将 TransactionManager 注入到 Context +public class TransactionManagerContextRegister implements InitializingBean { + private final PlatformTransactionManager transactionManager; + + public TransactionManagerContextRegister(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public void afterPropertiesSet() { + TransactionManagerContext.getInstance() + .setPlatformTransactionManager(transactionManager); + } +} + +// 3. Configuration:注册 Register Bean +@Configuration +public class TransactionManagerContextConfiguration { + @Bean + public TransactionManagerContextRegister transactionManagerContextRegister( + @Autowired(required = false) PlatformTransactionManager ptm) { + return new TransactionManagerContextRegister(ptm); + } +} + +// ✅ 正确 — 在非 Spring 管理的代码中使用 Context +public class GroovyScriptRuntime { + public T invoke(String method, String script, TransactionMode mode) { + if (mode == TransactionMode.COMMIT) { + return TransactionManagerContext.getInstance().commit(() -> { + return (T) runtime.invokeMethod(method, args); + }); + } + // ... + } +} + +// ❌ 错误 — 只有 Context,缺少 Register 和 Configuration +public class CacheContext { + @Getter + private static final CacheContext instance = new CacheContext(); + private CacheManager cacheManager; + + private CacheContext() {} + + // 没有 Register 注入 cacheManager,永远为 null! + public void put(String key, Object value) { + cacheManager.put(key, value); // NPE! + } +} + +// ❌ 错误 — Register 存在但没有 Configuration 注册 +// TransactionManagerContextRegister 写了,但没有 @Configuration 类将其注册为 Bean +// afterPropertiesSet() 永远不会被调用 + +// ❌ 错误 — 在 Configuration 中直接操作 Context,跳过 Register +@Configuration +public class BadConfiguration { + @Bean + public SomeBean someBean(PlatformTransactionManager ptm) { + // 不应在普通 Bean 定义中直接操作 Context + TransactionManagerContext.getInstance().setPlatformTransactionManager(ptm); + return new SomeBean(); + } +} +``` diff --git a/docs/conventions/index.md b/docs/conventions/index.md index 52c10205b..647519f8e 100644 --- a/docs/conventions/index.md +++ b/docs/conventions/index.md @@ -7,11 +7,14 @@ | 名称 | 描述 | 范围 | 来源 | |------|------|------|------| | [autoconfiguration-pattern](./autoconfiguration-pattern.md) | 每个 starter 模块必须同时注册 spring.factories(旧版兼容)和 AutoConfiguration.imports(Spring ... | 后端 | 项目自有 | +| [context-register-pairing](./context-register-pairing.md) | 每个 Context 单例必须配套 Register 类和 Configuration 类,形成三件套:Context(持有状态)+ Register(注... | 后端 | 项目自有 | | [context-singleton](./context-singleton.md) | 全局状态通过单例 Context 类持有,由 Register 类在启动时注入依赖 | 后端 | 项目自有 | | [locale-exception-handling](./locale-exception-handling.md) | 所有业务异常必须使用 LocaleMessageException,通过 errCode 实现国际化,由全局 HandlerExceptionResolv... | 后端 | 项目自有 | +| [meta-table-annotation](./meta-table-annotation.md) | 通过 @MetaTable/@MetaColumn/@MetaRelation 注解声明表的元数据描述,驱动动态实体生成和元数据查询 | 后端 | 项目自有 | +| [servlet-exception-resolver](./servlet-exception-resolver.md) | 使用 HandlerExceptionResolver(而非 @ControllerAdvice)作为全局异常处理机制,返回统一 JSON 响应格式 | 后端 | 项目自有 | | [slf4j-logging](./slf4j-logging.md) | 所有 Java 类必须使用 Lombok @Slf4j 注解进行日志输出,禁止直接使用 LoggerFactory.getLogger() | 全栈 | 项目自有 | | [unified-response](./unified-response.md) | 所有 API 返回值必须使用统一响应封装(Response → SingleResponse / MultiResponse / MapResponse)... | 后端 | 项目自有 | --- -**统计**: 共 5 篇 — 已实现 5 / 计划中 0 / 已废弃 0 +**统计**: 共 8 篇 — 已实现 8 / 计划中 0 / 已废弃 0 diff --git a/docs/conventions/meta-table-annotation.md b/docs/conventions/meta-table-annotation.md new file mode 100644 index 000000000..0cc14b1dc --- /dev/null +++ b/docs/conventions/meta-table-annotation.md @@ -0,0 +1,146 @@ +--- +name: meta-table-annotation +description: 通过 @MetaTable/@MetaColumn/@MetaRelation 注解声明表的元数据描述,驱动动态实体生成和元数据查询 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: a59736ac +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/annotation/MetaTable.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/annotation/MetaColumn.java + - springboot-starter/src/main/java/com/codingapi/springboot/framework/annotation/MetaRelation.java +--- + +## 解决什么问题 + +在 fast-repository 模块的动态实体生成场景中(如 Groovy 脚本定义的数据表、运行时创建的临时查询实体),需要在没有 JPA Entity 类的情况下描述一张数据库表的结构。传统的做法是手动构建 `TableEntityMetadata` 对象并逐个添加列信息,代码冗长且容易出错: + +```java +// 手动构建元数据 — 繁琐、易错 +TableEntityMetadata metadata = new TableEntityMetadata("com.example.DynamicUser"); +metadata.setTable("t_user", "用户表"); +metadata.addPrimaryKeyColumn(Long.class, "id", "id", GenerationType.IDENTITY, ...); +metadata.addColumn(String.class, "name", "name", "姓名", false, false, true, ...); +``` + +`@MetaTable` / `@MetaColumn` / `@MetaRelation` 注解体系提供了一种**声明式**的元数据描述方式,将表结构信息直接标注在 POJO 类上,由框架自动扫描并转换为 `TableEntityMetadata`,大幅简化了动态实体的定义流程。 + +## 如何使用 + +### 注解说明 + +#### @MetaTable(类级别) + +标注在类上,声明该类对应的数据库表信息。 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | String | 是 | 数据库表名称 | +| `desc` | String | 是 | 表的中文说明/备注 | + +#### @MetaColumn(字段级别) + +标注在字段上,声明该字段对应的数据库列信息。 + +| 属性 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `name` | String | 是 | — | 数据库列名称 | +| `desc` | String | 是 | — | 列的中文说明/备注 | +| `primaryKey` | boolean | 否 | `false` | 是否为主键 | +| `type` | ColumnType | 否 | `ColumnType.String` | 字段数据类型 | +| `format` | String | 否 | `""` | 格式化模板(如日期格式) | +| `dependent` | MetaRelation | 否 | 空关联 | 外键依赖关系 | + +#### @MetaRelation(嵌套在 @MetaColumn 中) + +声明字段的外键关联关系,作为 `@MetaColumn.dependent` 的值使用。 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tableName` | String | 是 | 关联的目标表名 | +| `columnName` | String | 是 | 关联的目标列名 | + +#### ColumnType 枚举 + +| 值 | 说明 | +|----|------| +| `Number` | 整数 | +| `Float` | 浮点数 | +| `String` | 字符串 | +| `Date` | 日期 | +| `File` | 文件 | +| `Boolean` | 布尔 | +| `Bytes` | 字节数组 | +| `JSON` | JSON 对象 | +| `Any` | 任意类型 | + +### 命名规范 + +- 注解类统一放在 `com.codingapi.springboot.framework.annotation` 包下 +- `@MetaTable` 只能用在类上(`@Target(ElementType.TYPE)`) +- `@MetaColumn` 只能用在字段上(`@Target(ElementType.FIELD)`) +- 所有注解均为 `@Retention(RetentionPolicy.RUNTIME)`,支持运行时反射读取 + +## 使用实例 + +```java +// ✅ 正确 — 完整的元数据注解声明 +@MetaTable(name = "t_leave_request", desc = "请假申请表") +public class LeaveRequestMeta { + + @MetaColumn(name = "id", desc = "主键ID", primaryKey = true, type = ColumnType.Number) + private Long id; + + @MetaColumn(name = "user_id", desc = "申请人ID", type = ColumnType.Number, + dependent = @MetaRelation(tableName = "t_user", columnName = "id")) + private Long userId; + + @MetaColumn(name = "start_date", desc = "开始日期", type = ColumnType.Date, format = "yyyy-MM-dd") + private String startDate; + + @MetaColumn(name = "end_date", desc = "结束日期", type = ColumnType.Date, format = "yyyy-MM-dd") + private String endDate; + + @MetaColumn(name = "reason", desc = "请假原因", type = ColumnType.String) + private String reason; + + @MetaColumn(name = "approved", desc = "是否批准", type = ColumnType.Boolean) + private Boolean approved; +} + +// ✅ 正确 — 最简声明(使用默认值) +@MetaTable(name = "t_dict", desc = "数据字典") +public class DictMeta { + + @MetaColumn(name = "id", desc = "主键", primaryKey = true) + private Long id; + + @MetaColumn(name = "code", desc = "编码") + private String code; + + @MetaColumn(name = "label", desc = "显示文本") + private String label; +} + +// ❌ 错误 — 缺少 @MetaTable 注解 +public class BadMeta { + @MetaColumn(name = "id", desc = "主键", primaryKey = true) + private Long id; +} + +// ❌ 错误 — @MetaColumn 缺少必填属性 name 和 desc +@MetaTable(name = "t_test", desc = "测试表") +public class IncompleteMeta { + @MetaColumn(desc = "主键") // 缺少 name!编译报错 + private Long id; +} + +// ❌ 错误 — 不要混用 JPA 注解代替 Meta 注解 +@Entity // 这是 JPA 注解,不是元数据注解 +@Table(name = "t_user") +public class UserMeta { + @Id + @Column(name = "id") + private Long id; +} +``` diff --git a/docs/conventions/servlet-exception-resolver.md b/docs/conventions/servlet-exception-resolver.md new file mode 100644 index 000000000..6ca773d9c --- /dev/null +++ b/docs/conventions/servlet-exception-resolver.md @@ -0,0 +1,111 @@ +--- +name: servlet-exception-resolver +description: 使用 HandlerExceptionResolver(而非 @ControllerAdvice)作为全局异常处理机制,返回统一 JSON 响应格式 +status: 已实现 +scope: 后端 +source: 项目自有 +last_commit: 9c0b4ac5 +code_files: + - springboot-starter/src/main/java/com/codingapi/springboot/framework/servlet/BasicHandlerExceptionResolverConfiguration.java +--- + +## 解决什么问题 + +Spring MVC 中常见的全局异常处理方式是通过 `@ControllerAdvice` + `@ExceptionHandler` 注解实现。这种方式存在以下局限: + +1. **与 Controller 层强耦合**:`@ControllerAdvice` 本质上是一个增强型 Controller,其生命周期和优先级受 DispatcherServlet 的 HandlerAdapter 调度影响,在某些边界场景(如 Filter 链异常、非标准 Handler 类型)下可能无法捕获所有异常。 +2. **多 Advice 优先级冲突**:当项目中引入多个第三方库各自提供 `@ControllerAdvice` 时,需要通过 `@Order` 手动协调优先级,容易产生遗漏或覆盖。 +3. **响应格式不统一**:各 `@ExceptionHandler` 方法可能返回不同的响应结构,导致前端需要适配多种错误格式。 + +本框架采用 Servlet 层面的 `HandlerExceptionResolver` 接口作为全局异常处理入口,直接注册为 Spring Bean,由 DispatcherServlet 在异常解析阶段统一调用,确保所有 Controller 异常都能被拦截并输出统一的 `{success, errCode, errMessage}` JSON 格式。 + +## 如何使用 + +### 核心实现类 + +`BasicHandlerExceptionResolverConfiguration` 是框架内置的自动配置类,通过 `@Bean` 注册一个 `HandlerExceptionResolver` 实例(内部类 `ServletExceptionHandler`)。 + +### 异常处理规则 + +| 异常类型 | errCode | errMessage | 日志级别 | +|---------|---------|------------|---------| +| `LocaleMessageException` | 异常自带的 `errCode` | 国际化消息内容 | WARN | +| 其他异常 | `"system.err"` | `ex.getMessage()` | WARN(含堆栈) | + +### 响应格式 + +所有异常均通过 `MappingJackson2JsonView` 输出以下 JSON 结构: + +```json +{ + "success": false, + "errCode": "错误码", + "errMessage": "错误描述" +} +``` + +### 业务异常抛出方式 + +业务代码应使用 `LocaleMessageException` 抛出异常,支持国际化错误码: + +```java +// 直接使用错误码(从 message.properties 读取消息) +throw new LocaleMessageException("user.not.found"); + +// 带参数的错误码 +throw LocaleMessageException.of("field.invalid", fieldName); + +// 自定义错误消息 +throw new LocaleMessageException("custom.error", "自定义错误信息"); +``` + +### 扩展须知 + +如需新增自定义异常处理逻辑,应修改 `ServletExceptionHandler.resolveException()` 方法中的判断分支,而不是新建额外的 `@ControllerAdvice` 类。这保证了异常处理入口的唯一性。 + +## 使用实例 + +```java +// ✅ 正确 — 业务代码抛出 LocaleMessageException,由框架统一处理 +@GetMapping("/users/{id}") +public SingleResponse getUser(@PathVariable Long id) { + User user = userRepository.findById(id).orElse(null); + if (user == null) { + throw new LocaleMessageException("user.not.found"); + } + return Response.of(user); +} +// 异常被 ServletExceptionHandler 捕获,返回: +// {"success": false, "errCode": "user.not.found", "errMessage": "用户不存在"} + +// ✅ 正确 — 未预期异常也会被捕获 +@PostMapping("/orders") +public SingleResponse createOrder(@RequestBody OrderRequest request) { + // 某个 NPE 或 IllegalArgumentException + return Response.of(orderService.create(request)); +} +// 返回:{"success": false, "errCode": "system.err", "errMessage": "..."} + +// ❌ 错误 — 不要使用 @ControllerAdvice 处理全局异常 +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception ex) { + Map body = new HashMap<>(); + body.put("code", 500); // 格式与框架不一致! + body.put("msg", ex.getMessage()); + return ResponseEntity.status(500).body(body); + } +} + +// ❌ 错误 — 不要在 Controller 中自行 try-catch 并构造响应 +@GetMapping("/users/{id}") +public Map getUser(@PathVariable Long id) { + try { + User user = userService.findById(id); + return Map.of("data", user); + } catch (Exception e) { + return Map.of("error", e.getMessage()); // 绕过了框架异常处理! + } +} +``` From d3a806033a496572b15a0f9c820e4a32cded21e4 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 8 Jun 2026 20:12:15 +0800 Subject: [PATCH 08/10] =?UTF-8?q?docs:=20=E9=87=8D=E5=BB=BA=20PKR=20?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E6=96=87=E6=A1=A3=EF=BC=8821?= =?UTF-8?q?=E7=AF=87=E8=83=BD=E5=8A=9B=20+=204=E7=AF=87=E8=A7=84=E8=8C=83?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增能力文档:domain-proxy, dynamic-data-query, global-exception-handler, groovy-runtime, jjwt, jsqlparser, jwt-auth-gateway, kryo, unified-response - 更新能力文档:data-authorization, event-system, groovy-script-engine, rest-client, spring-data-jpa, spring-framework, spring-security, workflow-engine - 删除旧文档:crypto-tools, domain-change-interceptor, dynamic-application 等 - 新增规范文档:ddd-layered-architecture, event-handler-standard, global-exception-handling, unified-response-format - 删除旧规范:autoconfiguration-pattern, context-register-pairing 等 - CLAUDE.md 添加 PKR 知识查阅约束 Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 22 +- docs/capabilities/crypto-tools.md | 247 ----------------- docs/capabilities/data-authorization.md | 255 +++++------------- .../capabilities/domain-change-interceptor.md | 165 ------------ docs/capabilities/domain-proxy.md | 79 ++++++ docs/capabilities/dynamic-application.md | 132 --------- docs/capabilities/dynamic-data-query.md | 106 ++++++++ docs/capabilities/dynamic-mvc-mapping.md | 168 ------------ docs/capabilities/event-system.md | 157 +++++------ docs/capabilities/fast-repository.md | 140 ---------- docs/capabilities/flow-postpone-event.md | 77 ------ docs/capabilities/global-exception-handler.md | 64 +++++ docs/capabilities/groovy-runtime.md | 57 ++++ docs/capabilities/groovy-script-engine.md | 160 +++++------ docs/capabilities/index.md | 47 ++-- docs/capabilities/jackson.md | 122 --------- docs/capabilities/jdbc-proxy-chain.md | 95 ------- docs/capabilities/jjwt.md | 69 +++++ docs/capabilities/jsqlparser.md | 62 +++++ docs/capabilities/jwt-auth-gateway.md | 96 +++++++ docs/capabilities/kryo.md | 62 +++++ docs/capabilities/locale-exception.md | 156 ----------- docs/capabilities/rest-client.md | 242 ++++------------- docs/capabilities/spring-data-jpa.md | 108 +++----- docs/capabilities/spring-data-redis.md | 105 -------- docs/capabilities/spring-framework.md | 115 +++----- docs/capabilities/spring-security.md | 118 +++----- docs/capabilities/sql-interceptor.md | 186 ------------- docs/capabilities/token-gateway.md | 101 ------- .../transaction-manager-context.md | 141 ---------- docs/capabilities/trigger-system.md | 230 ---------------- docs/capabilities/unified-response.md | 87 ++++++ docs/capabilities/user-context.md | 142 ---------- docs/capabilities/workflow-engine.md | 173 +++++------- docs/conventions/autoconfiguration-pattern.md | 134 --------- docs/conventions/context-register-pairing.md | 222 --------------- docs/conventions/context-singleton.md | 124 --------- docs/conventions/ddd-layered-architecture.md | 129 +++++++++ docs/conventions/event-handler-standard.md | 130 +++++++++ docs/conventions/global-exception-handling.md | 99 +++++++ docs/conventions/index.md | 14 +- docs/conventions/locale-exception-handling.md | 127 --------- docs/conventions/meta-table-annotation.md | 146 ---------- .../conventions/servlet-exception-resolver.md | 111 -------- docs/conventions/slf4j-logging.md | 118 -------- docs/conventions/unified-response-format.md | 94 +++++++ docs/conventions/unified-response.md | 117 -------- 47 files changed, 1588 insertions(+), 4263 deletions(-) delete mode 100644 docs/capabilities/crypto-tools.md delete mode 100644 docs/capabilities/domain-change-interceptor.md create mode 100644 docs/capabilities/domain-proxy.md delete mode 100644 docs/capabilities/dynamic-application.md create mode 100644 docs/capabilities/dynamic-data-query.md delete mode 100644 docs/capabilities/dynamic-mvc-mapping.md delete mode 100644 docs/capabilities/fast-repository.md delete mode 100644 docs/capabilities/flow-postpone-event.md create mode 100644 docs/capabilities/global-exception-handler.md create mode 100644 docs/capabilities/groovy-runtime.md delete mode 100644 docs/capabilities/jackson.md delete mode 100644 docs/capabilities/jdbc-proxy-chain.md create mode 100644 docs/capabilities/jjwt.md create mode 100644 docs/capabilities/jsqlparser.md create mode 100644 docs/capabilities/jwt-auth-gateway.md create mode 100644 docs/capabilities/kryo.md delete mode 100644 docs/capabilities/locale-exception.md delete mode 100644 docs/capabilities/spring-data-redis.md delete mode 100644 docs/capabilities/sql-interceptor.md delete mode 100644 docs/capabilities/token-gateway.md delete mode 100644 docs/capabilities/transaction-manager-context.md delete mode 100644 docs/capabilities/trigger-system.md create mode 100644 docs/capabilities/unified-response.md delete mode 100644 docs/capabilities/user-context.md delete mode 100644 docs/conventions/autoconfiguration-pattern.md delete mode 100644 docs/conventions/context-register-pairing.md delete mode 100644 docs/conventions/context-singleton.md create mode 100644 docs/conventions/ddd-layered-architecture.md create mode 100644 docs/conventions/event-handler-standard.md create mode 100644 docs/conventions/global-exception-handling.md delete mode 100644 docs/conventions/locale-exception-handling.md delete mode 100644 docs/conventions/meta-table-annotation.md delete mode 100644 docs/conventions/servlet-exception-resolver.md delete mode 100644 docs/conventions/slf4j-logging.md create mode 100644 docs/conventions/unified-response-format.md delete mode 100644 docs/conventions/unified-response.md diff --git a/CLAUDE.md b/CLAUDE.md index fd27cbc44..c7d4461e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,20 +156,30 @@ codingapi.framework.handler-thread-pool-size=20 # 事件异步线程池大小 见根 `pom.xml` 的 `` 区块。主要版本:Groovy 4.0.24、JSqlParser 5.0、Fastjson 2.0.53、JJWT 0.12.6、H2 2.3.232、Kryo 5.6.2。 + ## PKR 知识查阅(编码前必须) -进入计划模式或实现功能前,必须查阅: -1. [docs/capabilities/index.md](./docs/capabilities/index.md) — 已有可复用能力 -2. [docs/conventions/index.md](./docs/conventions/index.md) — 开发规范 +进入计划模式或实现功能前,**必须按以下优先级查阅**: + +### ⚠️ 开发规范(最高优先级,必须严格遵守) + +1. [docs/conventions/index.md](./docs/conventions/index.md) — 项目开发规范 + +**规范具有最高优先级。** 所有代码必须遵循已注册的 Convention,违反规范的代码视为缺陷。 +编码前必须逐条检查相关规范,确保命名、结构、模式完全符合要求。 + +### 已有能力(必须复用,禁止重复实现) + +2. [docs/capabilities/index.md](./docs/capabilities/index.md) — 已有可复用能力 -已有能力必须复用,禁止重新实现。编码必须遵循已注册的规范。 +已有能力必须复用,禁止重新实现。优先组合已有能力解决问题。 ### 计划模式约束 计划方案中必须包含: -1. **复用了哪些已有能力** — 列出从 PKR 中找到并复用的 Capability -2. **遵循了哪些规范** — 列出遵守的 Convention +1. **遵循了哪些规范** — 列出遵守的 Convention(必须首先说明) +2. **复用了哪些已有能力** — 列出从 PKR 中找到并复用的 Capability 3. **是否有新增能力** — 如果本次开发产生了可复用的新能力,完成后通过 `/pkr-add` 注册 ### 知识管理命令 diff --git a/docs/capabilities/crypto-tools.md b/docs/capabilities/crypto-tools.md deleted file mode 100644 index b8791dcb3..000000000 --- a/docs/capabilities/crypto-tools.md +++ /dev/null @@ -1,247 +0,0 @@ ---- -name: crypto-tools -description: 密码学工具套件,AES(CBC 模式)/RSA(BouncyCastle)/DES 加解密 + SHA256/HmacSHA256 哈希签名,提供实例化工具类和静态方法两种 API -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 36eae41d -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/AES.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/AESUtils.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/DES.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/DESUtils.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/RSA.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/RSAUtils.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/SHA256.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/crypto/HmacSHA256.java - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/crypto/AESTools.java ---- - -## 解决什么问题 - -企业应用中大量场景需要密码学支持:敏感数据存储加密、API 通信签名验证、Token 加解密、文件完整性校验等。直接使用 JDK 原生 `javax.crypto` / `java.security` API 存在以下问题: - -- **API 繁琐**:每次加解密都需要手动创建 Cipher、KeySpec、AlgorithmParameters,代码冗长且容易出错 -- **Provider 管理**:BouncyCastle 等第三方 Provider 需要手动注册,遗漏会导致运行时异常 -- **密钥格式混乱**:byte[]、String、Base64 之间的转换散落在各处 -- **缺少统一入口**:不同算法的使用方式不一致,增加学习成本 - -本能力封装了五种常用密码学算法,提供两种使用风格: - -- **实例化 API**(`AES`/`DES`/`RSA`):适合自定义密钥、动态生成密钥对的场景 -- **单例工具类**(`AESUtils`/`DESUtils`/`RSAUtils`/`AESTools`):内置预置密钥,开箱即用,适合应用内统一加解密 -- **静态方法**(`SHA256`/`HmacSHA256`):无状态哈希/签名,直接调用 - -## 如何使用 - -### AES 加解密(CBC/PKCS5Padding) - -基于 BouncyCastle Provider,默认使用 `AES/CBC/PKCS5Padding` 算法。 - -```java -// 自动生成 256 位密钥 + 随机 IV -AES aes = new AES(); - -// 指定密钥长度 -AES aes = new AES(128); - -// 使用指定密钥和 IV(byte[]) -AES aes = new AES(keyBytes, ivBytes); - -// 使用字符串密钥和 IV -AES aes = new AES("mySecretKey12345", "myIvParam1234567"); - -// 加解密 -byte[] encrypted = aes.encrypt("Hello".getBytes(StandardCharsets.UTF_8)); -byte[] decrypted = aes.decrypt(encrypted); - -// 获取密钥和 IV(用于传输或存储) -byte[] key = aes.getKey(); -byte[] iv = aes.getIv(); -``` - -#### AESUtils — 预置密钥单例 - -内置 Base64 编码的预置密钥和 IV,适用于应用内部数据加密: - -```java -AESUtils utils = AESUtils.getInstance(); - -// 字符串加解密(自动 Base64 编解码) -String cipher = utils.encode("敏感数据"); -String plain = utils.decode(cipher); - -// byte[] 加解密(原始字节,不做 Base64) -byte[] encrypted = utils.encode(rawBytes); -byte[] decrypted = utils.decode(encrypted); -``` - -#### AESTools — Security 模块的 AES 封装 - -位于 `springboot-starter-security` 模块,通过 `init(AES)` 方法注入自定义 AES 实例,适合安全模块统一管理密钥: - -```java -// 由安全模块配置类初始化 -AESTools.getInstance().init(new AES(customKey, customIv)); - -// 使用方式与 AESUtils 一致 -String cipher = AESTools.getInstance().encode("data"); -String plain = AESTools.getInstance().decode(cipher); -``` - -### DES 加解密 - -标准 DES 算法,ECB 模式。 - -```java -// 自动生成密钥 -DES des = new DES(); - -// 使用指定密钥 -DES des = new DES("myKey123"); -DES des = new DES(keyBytes); - -// 加解密 -byte[] encrypted = des.encrypt(data); -byte[] decrypted = des.decrypt(encrypted); -``` - -#### DESUtils — 预置密钥单例 - -```java -DESUtils utils = DESUtils.getInstance(); -String cipher = utils.encode("明文"); -String plain = utils.decode(cipher); -``` - -### RSA 加解密(2048 位) - -基于 BouncyCastle Provider,使用 `RSA/ECB/PKCS1Padding`,密钥长度 2048 位。 - -```java -// 自动生成密钥对 -RSA rsa = new RSA(); - -// 从已有密钥构造 -RSA rsa = new RSA(privateKeyBytes, publicKeyBytes); -RSA rsa = new RSA(publicKeyBytes); // 仅公钥(只能加密) -RSA rsa = new RSA(keyPair); - -// 公钥加密 / 私钥解密 -byte[] encrypted = rsa.encrypt("Hello".getBytes(StandardCharsets.UTF_8)); -byte[] decrypted = rsa.decrypt(encrypted); - -// 导出密钥 -byte[] pubKey = rsa.getPublicKey(); -byte[] priKey = rsa.getPrivateKey(); -``` - -#### RSAUtils — 预置密钥单例 - -内置 Base64 编码的 2048 位 RSA 密钥对: - -```java -RSAUtils utils = RSAUtils.getInstance(); -String cipher = utils.encode("敏感数据"); -String plain = utils.decode(cipher); -``` - -### SHA256 哈希 - -无状态静态方法,返回十六进制小写字符串: - -```java -String hash = SHA256.sha256("Hello World"); -// 输出: "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" -``` - -### HmacSHA256 签名 - -无状态静态方法,返回 HMAC-SHA256 签名字节数组: - -```java -byte[] signature = HmacSHA256.sha256( - "message".getBytes(StandardCharsets.UTF_8), - "secret-key".getBytes(StandardCharsets.UTF_8) -); -String hexSign = HexFormat.of().formatHex(signature); -``` - -## 使用实例 - -### 1. API 请求签名验证 - -```java -// 客户端签名 -String timestamp = String.valueOf(System.currentTimeMillis()); -String payload = userId + timestamp; -byte[] sign = HmacSHA256.sha256( - payload.getBytes(StandardCharsets.UTF_8), - appSecret.getBytes(StandardCharsets.UTF_8) -); -String signature = HexFormat.of().formatHex(sign); - -// 服务端验签 -byte[] expected = HmacSHA256.sha256( - payload.getBytes(StandardCharsets.UTF_8), - appSecret.getBytes(StandardCharsets.UTF_8) -); -boolean valid = Arrays.equals(sign, expected); -``` - -### 2. 用户敏感字段存储加密 - -```java -// 入库时加密 -String encryptedPhone = AESUtils.getInstance().encode(user.getPhone()); -user.setPhone(encryptedPhone); -userRepository.save(user); - -// 查询时解密 -String phone = AESUtils.getInstance().decode(user.getPhone()); -``` - -### 3. RSA 密钥交换 + AES 数据加密 - -```java -// 发送方:用接收方公钥加密 AES 密钥 -RSA rsa = new RSA(receiverPublicKeyBytes); -AES aes = new AES(); -byte[] encryptedAesKey = rsa.encrypt(aes.getKey()); - -// 用 AES 加密实际数据 -byte[] encryptedData = aes.encrypt(sensitiveData); - -// 接收方:用私钥解密 AES 密钥,再解密数据 -RSA rsaReceiver = new RSA(receiverPrivateKeyBytes, receiverPublicKeyBytes); -byte[] aesKey = rsaReceiver.decrypt(encryptedAesKey); -AES aesDecrypt = new AES(aesKey, aes.getIv()); -byte[] plainData = aesDecrypt.decrypt(encryptedData); -``` - -### 4. 密码哈希存储 - -```java -// 注册时 -String passwordHash = SHA256.sha256(rawPassword + salt); -user.setPassword(passwordHash); - -// 登录验证 -String inputHash = SHA256.sha256(inputPassword + salt); -boolean match = passwordHash.equals(inputHash); -``` - -### 5. 安全模块中自定义 AES 密钥 - -```java -@Configuration -public class SecurityCryptoConfig { - - @PostConstruct - public void initAESTools() throws Exception { - String key = environment.getProperty("security.aes.key"); - String iv = environment.getProperty("security.aes.iv"); - AESTools.getInstance().init(new AES(key, iv)); - } -} -``` diff --git a/docs/capabilities/data-authorization.md b/docs/capabilities/data-authorization.md index e037b63c7..2ba9ed11e 100644 --- a/docs/capabilities/data-authorization.md +++ b/docs/capabilities/data-authorization.md @@ -1,228 +1,95 @@ --- name: data-authorization -description: 完整的数据权限框架,包含行级权限(WHERE/JOIN 条件注入)和列级权限(数据脱敏),基于 JSqlParser 的 SQL 增强引擎,提供 ColumnMask SPI、RowHandler/ColumnHandler SPI、SkipAuthorizationFilter 跳过授权机制 +description: 数据权限 SQL 拦截器,通过 JDBC 代理链在 SQL 执行前透明注入权限条件,支持行级过滤与列级脱敏 status: 已实现 scope: 后端 source: 项目自有 -last_commit: 67895fda -code_files: - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationConfiguration.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/enhancer/DataPermissionSQLEnhancer.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DataAuthorizationFilter.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DefaultDataAuthorizationFilter.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/SkipAuthorizationFilter.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/handler/RowHandler.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/handler/ColumnHandler.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/handler/ColumnHandlerContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/ColumnMask.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/ColumnMaskContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/impl/PhoneMask.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/impl/BankCardMask.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/mask/impl/IDCardMask.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/condition/IConditionSQL.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/condition/WhereConditionSQL.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/condition/JoinConditionSQL.java +import: com.codingapi.springboot:springboot-starter-data-authorization +symbols: + - ConnectionProxy + - PreparedStatementProxy + - StatementProxy + - CallableStatementProxy + - ResultSetProxy + - SQLRunningContext + - SQLInterceptor + - DefaultSQLInterceptor + - SQLInterceptorContext + - SQLExecuteState + - DataPermissionSQL + - DataAuthorizationContext + - DataAuthorizationFilter + - DefaultDataAuthorizationFilter + - AuthorizationJdbcDriver + - DataPermissionSQLEnhancer + - ColumnHandlerContext + - ColumnMask + - RowHandler + - WhereConditionSQL + - JoinConditionSQL +content_hash: 213024d85431e425f67a13790847dc638a8d99a30b1013a7992236c3d1dadf8f --- ## 解决什么问题 -在企业级应用中,不同角色/部门的用户只能看到其权限范围内的数据行,且敏感字段(手机号、银行卡号、身份证号等)需要脱敏展示。如果由每个业务模块自行实现权限过滤和数据脱敏,会导致大量重复代码、规则分散难以维护、容易遗漏造成数据泄露。 +企业系统中,不同用户只能查看权限范围内的数据(如部门经理只看本部门数据)。传统做法需要在每个查询中手动拼接权限条件,代码侵入性大。本能力通过 JDBC 代理层实现透明的 SQL 拦截与改写,解决以下问题: -本能力提供一套完整的数据权限框架,从两个维度保障数据安全: - -- **行级权限**:通过 `RowHandler` SPI 定义过滤规则,`DataPermissionSQLEnhancer` 基于 JSqlParser 解析 SQL AST,自动向 SELECT 语句注入 WHERE 条件或 JOIN 关联表,支持子查询、UNION、嵌套 SELECT 等复杂场景 -- **列级权限**:通过 `ColumnHandler` SPI 拦截 ResultSet 读取,结合 `ColumnMask` SPI 对敏感数据自动脱敏。内置手机号、银行卡号、身份证号三种脱敏策略,可按需扩展 -- **跳过授权机制**:通过 `SkipAuthorizationFilter` 接口,允许特定 SQL 或场景(如管理后台全量导出)绕过数据权限拦截 -- **Spring Boot 自动装配**:通过 `DataAuthorizationConfiguration` 自动注册所有组件,业务方只需实现 SPI 接口并声明为 Bean 即可生效 +- **透明权限注入**:在 SQL 执行前自动注入 WHERE/JOIN 条件,业务代码无感知 +- **行级数据过滤**:根据用户权限动态追加行过滤条件 +- **列级数据脱敏**:通过 ColumnMask 对敏感字段(手机号、身份证、银行卡)进行脱敏 +- **跳过权限控制**:提供 `skipDataAuthorization()` 方法在特定场景下绕过权限拦截 ## 如何使用 -### 核心架构 - -``` -DataAuthorizationContext (单例入口) -├── DataAuthorizationFilter[] — 权限过滤器链(行+列) -├── SkipAuthorizationFilter — 跳过授权判断 -├── RowHandler — 行权限条件提供者 -├── ColumnHandler — 列权限结果处理器 -└── ColumnMask[] — 数据脱敏策略链 -``` - -### 行级权限 — RowHandler - -实现 `RowHandler` 接口,根据表名返回需要注入的 SQL 条件: - -```java -public interface RowHandler { - Condition handler(String subSql, String tableName, String tableAlias); -} -``` - -`Condition` 包含 `List` 条件列表,支持两种条件类型: - -- **WhereConditionSQL**:追加 WHERE 条件,如 `"t.dept_id = %d"` -- **JoinConditionSQL**:追加 JOIN 关联表,支持 INNER/LEFT/RIGHT 三种类型,指定表名、别名和 ON 条件 - -将 `RowHandler` 实现声明为 Spring Bean,`ConditionHandlerRegister` 会自动将其注册到框架中。 - -### 列级权限 — ColumnHandler - -实现 `ColumnHandler` 接口,拦截 ResultSet 中各类型字段的读取: +### 配置数据权限过滤器 ```java -public interface ColumnHandler { - String getString(SQLExecuteState state, int columnIndex, - String tableName, String columnName, String value); - int getInt(SQLExecuteState state, int columnIndex, - String tableName, String columnName, int value); - // ... 覆盖所有 JDBC ResultSet.getXxx() 方法 +@Bean +public DataAuthorizationFilter dataAuthorizationFilter() { + return (tableName, aliasContext) -> { + if ("sys_user".equals(tableName)) { + return new WhereConditionSQL("dept_id", + Relation.IN, getCurrentUserDeptIds()); + } + return null; // 不过滤 + }; } ``` -`ColumnHandlerContext` 是单例委托器,框架内部通过它调用 `ColumnHandler`。将 `ColumnHandler` 实现声明为 Spring Bean,`ResultSetHandlerRegister` 会自动注册。 - -### 数据脱敏 — ColumnMask SPI +### 配置 SQL 拦截器 ```java -public interface ColumnMask { - boolean support(Object value); // 判断是否匹配该脱敏规则 - Object mask(Object value); // 执行脱敏 +@Bean +public SQLInterceptor sqlInterceptor() { + return new DefaultSQLInterceptor(dataAuthorizationFilter); } ``` -内置三种脱敏实现,通过正则匹配自动识别: - -| 实现类 | 匹配规则 | 脱敏效果 | -|--------|----------|----------| -| `PhoneMask` | `^1[3-9]\d{9}$` | `138****1234` | -| `BankCardMask` | `^\d{13,19}$` | `622202******1234` | -| `IDCardMask` | 15/18位身份证 | `110101********1234` | +### 使用 JDBC 驱动代理 -通过 `ColumnMaskContext.getInstance().addColumnMask(mask)` 注册自定义脱敏策略,按注册顺序依次匹配,首个命中即返回。 +将数据库驱动替换为 `AuthorizationJdbcDriver`,所有通过 JDBC 执行的 SQL 将自动经过权限拦截。 -### 跳过授权 — SkipAuthorizationFilter +### 跳过权限拦截 ```java -public interface SkipAuthorizationFilter { - String filter(String sql); // 返回处理后的 SQL,或 null 表示不跳过 -} +// 在特定查询中跳过数据权限 +Object result = SQLRunningContext.getInstance() + .skipDataAuthorization(() -> { + return jdbcTemplate.queryForObject("SELECT count(*) FROM sys_user", Long.class); + }); ``` -通过 `DataAuthorizationContext.getInstance().setSkipAuthorizationFilter(filter)` 设置。默认使用 `DefaultSkipAuthorizationFilter`(不跳过任何 SQL)。 - -### 自动配置 - -`DataAuthorizationConfiguration` 通过 `@Configuration` 自动注册以下 Bean: - -- `DataAuthorizationProperties`(配置前缀 `codingapi.data-authorization`) -- `ConditionHandlerRegister`(注入 `RowHandler`) -- `ResultSetHandlerRegister`(注入 `ColumnHandler`) -- `SQLInterceptorRegister`(注入 `SQLInterceptor`) -- `DataAuthorizationContextRegister`(注入所有 `DataAuthorizationFilter` Bean) - -所有 Handler/Filter 均为 `@Autowired(required = false)`,未提供时使用默认空实现。 - ## 使用实例 -### 1. 实现行级数据权限 - -```java -@Component -public class DeptRowHandler implements RowHandler { - - @Override - public Condition handler(String subSql, String tableName, String tableAlias) { - if ("employee".equalsIgnoreCase(tableName)) { - Long deptId = SecurityContext.getCurrentDeptId(); - WhereConditionSQL where = new WhereConditionSQL( - "%s.dept_id = %d", tableAlias, deptId - ); - return Condition.of(where); - } - return null; - } -} -``` - -### 2. 实现列级数据脱敏 - -```java -@Component -public class SensitiveColumnHandler extends DefaultColumnHandler { - - @Override - public String getString(SQLExecuteState state, int columnIndex, - String tableName, String columnName, String value) { - // 委托给 ColumnMaskContext 进行自动脱敏 - return ColumnMaskContext.getInstance().mask(value); - } -} -``` - -### 3. 注册自定义脱敏策略 - -```java -@Component -public class EmailMask implements ColumnMask { - - private static final Pattern EMAIL_PATTERN = - Pattern.compile("^[\\w.-]+@[\\w.-]+\\.\\w+$"); - - @Override - public boolean support(Object value) { - return value instanceof String - && EMAIL_PATTERN.matcher((String) value).matches(); - } - - @Override - public Object mask(Object value) { - String email = (String) value; - int atIndex = email.indexOf('@'); - return email.charAt(0) + "***" + email.substring(atIndex); - } -} - -// 在初始化时注册 -@PostConstruct -public void init() { - ColumnMaskContext.getInstance().addColumnMask(new EmailMask()); -} -``` - -### 4. 使用 JOIN 条件实现跨表行权限 - ```java -@Component -public class ProjectRowHandler implements RowHandler { - - @Override - public Condition handler(String subSql, String tableName, String tableAlias) { - if ("task".equalsIgnoreCase(tableName)) { - JoinConditionSQL join = new JoinConditionSQL( - JoinConditionSQL.Type.INNER, - "project_member", - "pm", - String.format("%s.project_id = pm.project_id AND pm.user_id = %d", - tableAlias, SecurityContext.getCurrentUserId()) - ); - return Condition.of(join); - } - return null; - } +// 业务代码正常写查询,无需关心权限 +Page users = userRepository.findAll(PageRequest.of(0, 20)); +// SQL: SELECT * FROM sys_user LIMIT 20 +// 实际执行: SELECT * FROM sys_user WHERE dept_id IN (1,2,3) LIMIT 20 + +// 列脱敏配置 +@Bean +public ColumnMask phoneMask() { + return new PhoneMask(); // 138****8888 } ``` - -### 5. 临时跳过数据权限 - -```java -// 管理后台导出全量数据 -DataAuthorizationContext.getInstance() - .setSkipAuthorizationFilter(sql -> sql); // 放行所有 SQL - -List all = employeeRepository.findAll(); - -// 恢复正常权限 -DataAuthorizationContext.getInstance() - .setSkipAuthorizationFilter(new DefaultSkipAuthorizationFilter()); -``` diff --git a/docs/capabilities/domain-change-interceptor.md b/docs/capabilities/domain-change-interceptor.md deleted file mode 100644 index f2b482e0d..000000000 --- a/docs/capabilities/domain-change-interceptor.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -name: domain-change-interceptor -description: 通过 CGLIB 代理拦截领域实体字段的 setter 方法,自动检测变更并发布 DomainChangeEvent -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 87c449a7 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/proxy/DomainChangeInterceptor.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/proxy/DomainProxyFactory.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainChangeEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainCreateEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainDeleteEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/IDomain.java ---- - -## 解决什么问题 - -在 DDD(领域驱动设计)实践中,领域实体的状态变更需要被精确追踪并作为领域事件发布,以便触发审计日志、通知下游系统、维护事件溯源等。传统做法是在每个 setter 方法中手动编写变更检测和事件发布代码,导致领域模型充斥大量样板代码,且容易遗漏。 - -本能力通过 CGLIB 动态代理技术,在运行时拦截领域实体的所有带参数的方法调用(主要是 setter),自动对比字段变更前后的值,当检测到差异时自动发布 `DomainChangeEvent`。核心优势: - -- **零侵入领域模型**:实体类无需继承特定基类或添加注解,保持纯 POJO -- **自动变更检测**:支持基本类型和嵌套对象的深层字段比较 -- **完整生命周期事件**:覆盖创建(`DomainCreateEvent`)、变更(`DomainChangeEvent`)、删除(`DomainDeleteEvent`)、持久化(`DomainPersistEvent`)四种领域事件 -- **与框架事件系统集成**:变更事件通过 `EventPusher` 发布,可被同步/异步 Handler 消费 - -## 如何使用 - -### 核心组件 - -#### IDomain — 领域对象接口 - -```java -public interface IDomain { - // 发布持久化事件 - default void persist() { EventPusher.push(new DomainPersistEvent(this)); } - - // 发布删除事件 - default void delete() { EventPusher.push(new DomainDeleteEvent(this)); } -} -``` - -领域实体可选择性实现此接口以获得便捷的持久化和删除事件发布能力。 - -#### DomainProxyFactory — 代理工厂 - -```java -// 创建带有变更拦截的领域实体代理 -public static T create(Class entityClass, Object... args) -``` - -调用 `create()` 会: -1. 通过反射调用目标类的构造函数创建实例 -2. 使用 CGLIB 生成代理对象 -3. 自动发布 `DomainCreateEvent` -4. 返回代理对象 - -#### DomainChangeInterceptor — CGLIB 方法拦截器 - -内部工作机制: -1. **首次拦截时**:通过 `BeanUtils.getPropertyDescriptors()` 读取实体所有属性,递归快照当前字段值到内存 Map -2. **每次 setter 调用后**:重新读取字段值并与快照对比 -3. **发现差异时**:通过 `EventPusher.push()` 发布 `DomainChangeEvent`,包含字段名、旧值、新值 -4. **更新快照**:将新值写入快照 Map,为下次比较做准备 - -#### 领域事件体系 - -``` -DomainEvent (抽象基类,实现 IEvent) -├── entity — 关联的实体对象 -├── entityClass — 实体类型 -└── timestamp — 事件时间戳 - -DomainCreateEvent extends DomainEvent — 实体创建 -DomainChangeEvent extends DomainEvent — 字段变更(含 fieldName, oldValue, newValue) -DomainDeleteEvent extends DomainEvent — 实体删除 -DomainPersistEvent extends DomainEvent — 实体持久化 -``` - -### 注意事项 - -- 代理对象通过 CGLIB 子类化实现,要求目标类不能是 final 类,且需要有可访问的构造函数 -- 变更检测基于 `equals()` 比较,自定义对象需正确实现 `equals()` 方法 -- 仅对带参数的方法调用进行变更检测(即 setter),无参方法(getter)直接透传 -- 支持嵌套对象的深层比较,但仅限基本类型属性的递归展开 - -## 使用实例 - -### 1. 定义领域实体 - -```java -public class UserEntity implements IDomain { - private String name; - private Integer age; - private Address address; - - public UserEntity() {} - - public UserEntity(String name, Integer age) { - this.name = name; - this.age = age; - } - - // getter/setter ... -} -``` - -### 2. 通过代理工厂创建实体 - -```java -// 创建代理对象,自动发布 DomainCreateEvent -UserEntity user = DomainProxyFactory.create(UserEntity.class, "张三", 25); - -// 修改字段时自动检测变更并发布 DomainChangeEvent -user.setName("李四"); // → DomainChangeEvent(fieldName="name", oldValue="张三", newValue="李四") -user.setAge(30); // → DomainChangeEvent(fieldName="age", oldValue=25, newValue=30) - -// 持久化时发布 DomainPersistEvent -user.persist(); - -// 删除时发布 DomainDeleteEvent -user.delete(); -``` - -### 3. 监听领域变更事件 - -```java -@Component -public class UserChangeHandler implements IHandler { - - @Override - public void handler(DomainChangeEvent event) { - if (event.getEntity() instanceof UserEntity) { - log.info("用户字段变更: {} = {} → {}", - event.getFieldName(), - event.getOldValue(), - event.getNewValue()); - } - } -} -``` - -### 4. 监听实体创建事件 - -```java -@Component -public class UserCreateHandler implements IHandler { - - @Override - public void handler(DomainCreateEvent event) { - log.info("新用户创建: {}", event.getEntity().getClass().getSimpleName()); - } -} -``` - -### 5. 嵌套对象变更检测 - -```java -// 假设 UserEntity 中包含 Address 嵌套对象 -user.getAddress().setCity("北京"); -// → DomainChangeEvent(fieldName="address.city", oldValue="上海", newValue="北京") -``` - -嵌套对象的字段变更会以 `父字段.子字段` 的格式记录字段路径,便于精确定位变更位置。 diff --git a/docs/capabilities/domain-proxy.md b/docs/capabilities/domain-proxy.md new file mode 100644 index 000000000..d28883dbe --- /dev/null +++ b/docs/capabilities/domain-proxy.md @@ -0,0 +1,79 @@ +--- +name: domain-proxy +description: 基于 CGLIB 的领域实体代理,自动拦截字段变更并推送 DomainChangeEvent 领域事件 +status: 已实现 +scope: 后端 +source: 项目自有 +import: com.codingapi.springboot:springboot-starter +symbols: + - DomainProxyFactory + - DomainChangeInterceptor + - DomainChangeEvent + - DomainCreateEvent + - DomainDeleteEvent + - DomainPersistEvent + - DomainEvent + - IDomain +content_hash: dc7a9d76224be09498f0278ec05c56ea34b0c5e2dbb80850ccb826fabe721da6 +--- + +## 解决什么问题 + +在 DDD 中,领域实体的变更需要通知相关组件(如记录变更日志、触发联动更新)。手动编写变更检测代码繁琐且易遗漏。本能力通过代理模式自动拦截实体字段变更,解决以下问题: + +- **自动变更检测**:代理实体的 setter 方法,自动对比新旧值 +- **变更事件推送**:检测到字段变更时自动推送 `DomainChangeEvent` +- **创建事件**:通过 `DomainProxyFactory.create()` 创建实体时自动推送 `DomainCreateEvent` +- **嵌套字段支持**:递归读取和对比嵌套对象的字段变更 + +## 如何使用 + +### 创建代理实体 + +```java +// 通过工厂创建代理实体,自动推送 DomainCreateEvent +User user = DomainProxyFactory.create(User.class, "张三", 25); +``` + +### 变更自动检测 + +```java +// 调用 setter 方法时,自动对比字段变更并推送 DomainChangeEvent +user.setName("李四"); // 自动推送 DomainChangeEvent(user, "name", "张三", "李四") +user.setAge(30); // 自动推送 DomainChangeEvent(user, "age", 25, 30) +``` + +### 订阅变更事件 + +```java +@Service +public class ChangeLogHandler implements IHandler { + @Override + public void handler(DomainChangeEvent event) { + log.info("字段变更: {} {} -> {}", + event.getFieldName(), event.getOldValue(), event.getNewValue()); + } +} +``` + +## 使用实例 + +```java +// 创建代理实体 +Order order = DomainProxyFactory.create(Order.class, orderId, amount); + +// 修改字段 - 自动触发变更事件 +order.setStatus("PAID"); +// → DomainChangeEvent(order, "status", "PENDING", "PAID") + +// 订阅处理 +@Service +public class OrderChangeHandler implements IHandler { + @Override + public void handler(DomainChangeEvent event) { + if ("status".equals(event.getFieldName())) { + auditLog.record(event); + } + } +} +``` diff --git a/docs/capabilities/dynamic-application.md b/docs/capabilities/dynamic-application.md deleted file mode 100644 index 2b59e906c..000000000 --- a/docs/capabilities/dynamic-application.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -name: dynamic-application -description: 支持外部 JAR 加载和 Spring 上下文热重启的应用启动器 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: e8583c68 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/boot/DynamicApplication.java ---- - -## 解决什么问题 - -标准 Spring Boot 应用的类路径在启动时确定,运行期间无法动态加载新的 JAR 包或模块。当业务需要以下场景时,传统启动方式无法满足: - -- **插件化架构**:核心应用运行时加载外部插件 JAR,无需重新打包主应用 -- **热部署/热更新**:开发或运维过程中,替换 JAR 后自动重启 Spring 上下文,减少停机时间 -- **模块化分发**:不同客户/环境使用不同的 JAR 组合,通过外部目录配置而非 Maven 依赖 - -本能力提供了 `DynamicApplication` 启动器,替代标准的 `SpringApplication.run()`,在启动前扫描指定目录下的 JAR 文件并注入到类加载器中,同时支持运行时关闭并重建 Spring 上下文(热重启)。 - -## 如何使用 - -### 核心类 - -| 类 | 说明 | -|----|------| -| `DynamicApplication` | 单例应用启动器,管理外部 JAR 加载和 Spring 上下文生命周期 | - -### API - -```java -// 启动应用(替代 SpringApplication.run) -DynamicApplication.run(Class applicationClass, String[] args) - -// 热重启(关闭当前上下文 → 重新加载 JAR → 重新启动) -DynamicApplication.restart() - -// 获取单例实例 -DynamicApplication app = DynamicApplication.getInstance() - -// 设置外部 JAR 目录(默认 ./jars) -app.setJarsFolder("/path/to/plugins") -``` - -### 启动流程 - -1. 调用 `DynamicApplication.run(App.class, args)` -2. 扫描 `jarsFolder` 目录下所有 `.jar` 文件 -3. 创建 `URLClassLoader` 并将这些 JAR 加入类路径 -4. 将当前线程的 ContextClassLoader 设置为该 URLClassLoader -5. 调用 `SpringApplication.run()` 启动 Spring Boot 应用 - -### 热重启流程 - -1. 调用 `DynamicApplication.restart()` -2. 在新线程中执行(非守护线程,确保 JVM 不会提前退出) -3. 关闭当前 `ConfigurableApplicationContext` -4. 重新扫描 JAR 目录并更新 ClassLoader -5. 使用原始启动类和参数重新启动 Spring Boot - -### 配置 - -```java -// 修改外部 JAR 目录(默认为工作目录下的 ./jars) -DynamicApplication.getInstance().setJarsFolder("/opt/app/plugins"); -``` - -### 注意事项 - -- 外部 JAR 目录不存在或为空时,不加载任何额外类路径,应用正常启动 -- 热重启会完全销毁并重建 Spring 上下文,所有 Bean 会被重新初始化 -- 热重启在新线程中执行,原调用线程立即返回 -- `URLClassLoader` 以系统 ClassLoader 为父加载器,外部 JAR 中的类优先级高于系统类路径中的同名类 - -## 使用实例 - -### 1. 基本启动(替代 SpringApplication.run) - -```java -@SpringBootApplication -public class MyApplication { - - public static void main(String[] args) { - // 替代 SpringApplication.run(MyApplication.class, args); - DynamicApplication.run(MyApplication.class, args); - } -} -``` - -### 2. 自定义 JAR 目录启动 - -```java -@SpringBootApplication -public class MyApplication { - - public static void main(String[] args) { - DynamicApplication.getInstance().setJarsFolder("/opt/myapp/extensions"); - DynamicApplication.run(MyApplication.class, args); - } -} -``` - -### 3. 通过 REST API 触发重启 - -```java -@RestController -@RequestMapping("/admin") -public class AdminController { - - @PostMapping("/restart") - public Response restart() { - // 异步重启,接口立即返回 - DynamicApplication.restart(); - return Response.success(); - } -} -``` - -### 4. 目录结构示例 - -``` -project-root/ -├── jars/ ← 默认外部 JAR 目录 -│ ├── plugin-a.jar ← 自动加载 -│ ├── plugin-b.jar ← 自动加载 -│ └── readme.txt ← 非 .jar 文件,忽略 -├── src/ -└── pom.xml -``` - -将新的插件 JAR 放入 `jars/` 目录后,调用 `DynamicApplication.restart()` 即可加载新插件并重启应用。 diff --git a/docs/capabilities/dynamic-data-query.md b/docs/capabilities/dynamic-data-query.md new file mode 100644 index 000000000..c6ee76d60 --- /dev/null +++ b/docs/capabilities/dynamic-data-query.md @@ -0,0 +1,106 @@ +--- +name: dynamic-data-query +description: 动态数据查询体系,基于 FastRepository + PageRequest/Filter/SearchRequest 自动构建 Example 或 HQL 查询 +status: 已实现 +scope: 后端 +source: 项目自有 +import: com.codingapi.springboot:springboot-starter-data-fast +symbols: + - FastRepository + - DynamicRepository + - DynamicNativeRepository + - DynamicSQLBuilder + - ExampleBuilder + - BaseRepository + - SortRepository + - PageRequest + - SearchRequest + - RequestFilter + - Filter + - Relation + - QueryColumns + - MapViewResult + - JpaQuery + - JdbcQuery + - ICurrentOffset + - CurrentPageOffsetContext + - DynamicTableGenerator + - DynamicTableClassLoader + - TableEntityClassBuilder +content_hash: 205a681871886afbc38ec401a4a24474e11d3ef833dbb9c66d910950404fba22 +--- + +## 解决什么问题 + +后台管理系统的列表查询通常需要支持动态过滤、排序和分页。传统做法需要大量 if-else 拼接查询条件。本能力通过扩展 Spring Data JPA,提供了声明式的动态查询方案: + +- **动态过滤条件**:通过 `RequestFilter` 声明式添加过滤条件,自动构建查询 +- **前端参数直连**:`SearchRequest` 直接解析 HttpServletRequest 参数为 PageRequest +- **多种查询模式**:支持 Example 查询(简单过滤)和 HQL 查询(复杂关联) +- **Map 结果映射**:支持自定义列选择和 Map 结果返回 + +## 如何使用 + +### 基础分页查询 + +```java +// Repository 继承 FastRepository +public interface UserRepository extends FastRepository {} + +// 分页 + 过滤 +PageRequest request = PageRequest.of(0, 20); +request.addFilter("name", "张三"); +request.addFilter("age", Relation.GT, 18); +request.addFilter("status", Relation.IN, "ACTIVE", "PENDING"); +Page page = userRepository.findAll(request); +``` + +### 复杂 HQL 查询 + +```java +// 使用 pageRequest 触发 HQL 构建(支持关联查询过滤) +PageRequest request = PageRequest.of(0, 20); +request.addFilter("dept.name", "技术部"); // 关联字段 +request.addSort(Sort.by("createTime").descending()); +Page page = userRepository.pageRequest(request); +``` + +### 从 HTTP 请求自动构建 + +```java +// 自动解析前端传递的 filter/sort/params 参数 +SearchRequest searchRequest = new SearchRequest(); +searchRequest.setCurrent(0); +searchRequest.setPageSize(20); +PageRequest pageRequest = searchRequest.toPageRequest(User.class); +Page page = userRepository.searchRequest(searchRequest); +``` + +### 支持的 Relation 操作 + +| Relation | 含义 | 示例 | +|----------|------|------| +| `EQUAL` | 等于 | `addFilter("name", "张三")` | +| `NOT_EQUAL` | 不等于 | `addFilter("status", Relation.NOT_EQUAL, "DELETED")` | +| `GT` / `GTE` | 大于/大于等于 | `addFilter("age", Relation.GT, 18)` | +| `LT` / `LTE` | 小于/小于等于 | `addFilter("price", Relation.LTE, 100)` | +| `LIKE` | 模糊匹配 | `addFilter("name", Relation.LIKE, "张")` | +| `IN` | 包含 | `addFilter("status", Relation.IN, "A", "B")` | + +## 使用实例 + +```java +// Controller 层 +@GetMapping("/users") +public MultiResponse list() { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setCurrent(0); + searchRequest.setPageSize(20); + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page); +} + +// 前端请求参数: +// ?current=0&pageSize=20&filter=eyJuYW1lIjpbIuW8oCJdfQ==&sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9 +// filter 和 sort 参数为 Base64 编码的 JSON +``` diff --git a/docs/capabilities/dynamic-mvc-mapping.md b/docs/capabilities/dynamic-mvc-mapping.md deleted file mode 100644 index 0511cf1cc..000000000 --- a/docs/capabilities/dynamic-mvc-mapping.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -name: dynamic-mvc-mapping -description: 运行时动态注册/注销 Spring MVC REST 端点,支持将 Groovy 脚本绑定到动态 API 路径 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: d5569b1d -code_files: - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/mapping/FastMvcMappingRegister.java - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/script/FastScriptMappingRegister.java - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/script/ScriptMapping.java - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/script/ScriptRuntime.java ---- - -## 解决什么问题 - -传统 Spring MVC 的 REST 端点在编译期通过注解(`@RequestMapping`)静态声明,无法在运行时按需增减。当业务需要以下场景时,静态映射无法满足: - -- **低代码/无代码平台**:用户通过界面配置 API,后端需动态生成对应的 REST 端点 -- **Groovy 脚本绑定 API**:将动态编写的 Groovy 脚本挂载到指定 URL 路径,作为即时可用的 REST 接口 -- **插件化扩展**:第三方模块在运行时注册自己的 API,卸载时自动注销 -- **灰度/临时接口**:在不重启应用的前提下快速上线或下线调试端点 - -本能力封装了 Spring `RequestMappingHandlerMapping` 的底层 API,提供安全的运行时端点注册/注销操作,并与 Groovy 脚本引擎集成,实现"脚本即 API"的动态绑定。 - -## 如何使用 - -### 核心组件 - -| 类 | 说明 | -|----|------| -| `FastMvcMappingRegister` | 底层 MVC 映射注册器,直接操作 `RequestMappingHandlerMapping` | -| `FastScriptMappingRegister` | 上层脚本映射注册器,将 `ScriptMapping` 绑定为 REST 端点 | -| `ScriptMapping` | 脚本与 URL 的绑定关系,持有映射路径、HTTP 方法和 Groovy 脚本内容 | -| `ScriptRuntime` | 脚本执行运行时(单例),向脚本注入 `$request`、`$jpa`、`$jdbc` 上下文变量 | -| `ScriptMethod` | HTTP 方法枚举(GET / POST),转换为 Spring `RequestMethod` | -| `ScriptRequest` | 对 `HttpServletRequest` 的类型安全封装,提供带默认值的参数获取方法 | - -### 自动配置 - -`DataFastConfiguration` 在检测到 `WebMvcConfigurer` 类存在时自动注册以下 Bean: - -- `FastMvcMappingRegister` — 注入 Spring 容器的 `requestMappingHandlerMapping` -- `FastScriptMappingRegister` — 依赖 `FastMvcMappingRegister` - -无需手动配置,引入 `springboot-starter-data-fast` 模块即可使用。 - -### FastMvcMappingRegister API - -```java -// 注册端点 -void addMapping(String url, RequestMethod requestMethod, Object handler, Method method) - -// 注销端点 -void removeMapping(String url, RequestMethod requestMethod) -``` - -所有端点默认产出 `application/json` 媒体类型,使用 `PathPatternParser` 解析路径模式。 - -### FastScriptMappingRegister API - -```java -// 将脚本映射注册为 REST 端点 -void addMapping(ScriptMapping scriptMapping) - -// 测试脚本映射(不注册端点,直接执行并返回结果) -Response test(ScriptMapping scriptMapping) - -// 注销脚本映射对应的端点 -void removeMapping(String url, ScriptMethod scriptMethod) -``` - -### ScriptRuntime 注入变量 - -脚本执行时自动绑定以下变量: - -| 变量名 | 类型 | 说明 | -|--------|------|------| -| `$request` | `ScriptRequest` | 当前 HTTP 请求的类型安全封装 | -| `$jpa` | `JpaQuery` | JPA 查询工具 | -| `$jdbc` | `JdbcQuery` | JDBC 查询工具 | - -### ScriptRequest 参数获取 - -```java -String getParameter(String key, String defaultValue) -int getParameter(String key, int defaultValue) -float getParameter(String key, float defaultValue) -double getParameter(String key, double defaultValue) -long getParameter(String key, long defaultValue) -boolean getParameter(String key, boolean defaultValue) -PageRequest pageRequest(int pageNumber, int pageSize) -``` - -## 使用实例 - -### 1. 注册一个动态 GET 端点 - -```java -@Autowired -private FastScriptMappingRegister scriptMappingRegister; - -// 创建脚本映射:GET /api/dynamic/hello → 返回字符串 -ScriptMapping mapping = new ScriptMapping( - "/api/dynamic/hello", - ScriptMethod.GET, - "return 'Hello, Dynamic API!'" -); - -// 注册为 REST 端点 -scriptMappingRegister.addMapping(mapping); -// 此时 GET /api/dynamic/hello 已可用,返回 SingleResponse{"Hello, Dynamic API!"} -``` - -### 2. 注册一个带参数的 POST 端点 - -```java -String script = """ - def name = $request.getParameter('name', 'World') - def age = $request.getParameter('age', 0) - return [name: name, age: age] -"""; - -ScriptMapping mapping = new ScriptMapping( - "/api/dynamic/greet", - ScriptMethod.POST, - script -); -scriptMappingRegister.addMapping(mapping); -``` - -### 3. 测试脚本(不注册端点) - -```java -ScriptMapping mapping = new ScriptMapping( - "/test", - ScriptMethod.GET, - "return [1, 2, 3]" -); - -// 直接执行脚本并获取结果,不会注册到 MVC -Response result = scriptMappingRegister.test(mapping); -``` - -### 4. 注销动态端点 - -```java -// 移除之前注册的端点 -scriptMappingRegister.removeMapping("/api/dynamic/hello", ScriptMethod.GET); -// GET /api/dynamic/hello 不再可用 -``` - -### 5. 脚本中使用 JPA/JDBC 查询 - -```java -String script = """ - def page = $request.pageRequest(0, 20) - def users = $jpa.query('select u from User u', page) - return users -"""; - -ScriptMapping mapping = new ScriptMapping( - "/api/dynamic/users", - ScriptMethod.GET, - script -); -scriptMappingRegister.addMapping(mapping); -``` diff --git a/docs/capabilities/event-system.md b/docs/capabilities/event-system.md index 42df04b12..9a533528a 100644 --- a/docs/capabilities/event-system.md +++ b/docs/capabilities/event-system.md @@ -1,137 +1,104 @@ --- name: event-system -description: 自建的领域事件发布-订阅系统,支持同步/异步事件、事务后提交、Handler自动注册排序 +description: 发布-订阅事件系统,支持同步/异步事件、Handler排序、循环检测与事务集成 status: 已实现 scope: 后端 source: 项目自有 -last_commit: 145aada7 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/EventPusher.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/IHandler.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/IEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/ISyncEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/IAsyncEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/DomainEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/DomainEventContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/SpringDefaultEventHandler.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/SpringTransactionEventHandler.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/HandlerBeanDefinitionRegistrar.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/ApplicationHandlerUtils.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainCreateEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainChangeEvent.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/domain/event/DomainDeleteEvent.java +import: com.codingapi.springboot:springboot-starter +symbols: + - EventPusher + - IEvent + - ISyncEvent + - IAsyncEvent + - IHandler + - ApplicationHandlerUtils + - DomainEventContext + - EventStackContext + - EventTraceContext + - DomainEvent + - SpringEventHandler + - SpringDefaultEventHandler + - SpringTransactionEventHandler + - HandlerBeanDefinitionRegistrar +content_hash: 0703329f337a48ff76c084efb93b0c0fcb3ef426ba4b201f81e0c28b646c7405 --- ## 解决什么问题 -在 DDD(领域驱动设计)架构中,领域实体状态变更时需要通知其他模块执行联动逻辑(如发送通知、更新缓存、触发下游流程等)。如果直接调用会导致模块间强耦合,且主业务与分支逻辑的事务边界不清晰。 +在 DDD 架构中,领域事件是解耦聚合根之间通信的核心机制。本事件系统解决了以下问题: -本能力提供了一套自建的发布-订阅事件系统,解决以下痛点: - -- **解耦**:事件发布者无需知道有哪些订阅者,通过 `IEvent` + `IHandler` 接口完全解耦 -- **同步/异步分离**:通过 `ISyncEvent` / `IAsyncEvent` 标记接口区分执行模式,异步事件由独立线程池处理 -- **事务后提交**:`SpringTransactionEventHandler` 使用 `@TransactionalEventListener(AFTER_COMMIT)` 确保事件在主事务提交后才触发,避免脏数据 -- **Handler 自动注册与排序**:所有 `IHandler` 实现类通过 Spring Bean 自动扫描注册,支持 `order()` 控制执行顺序 -- **循环检测**:内置 `EventTraceContext` 追踪事件链,防止事件循环触发 -- **异常隔离**:多 Handler 场景下,单个 Handler 异常通过 `error()` 回调处理,不影响其他 Handler 执行 +- **领域事件解耦**:聚合根通过事件通信,无需直接依赖 +- **同步/异步分离**:通过接口标记区分事件类型,由框架自动调度 +- **循环事件检测**:自动检测事件嵌套推送中的循环引用,防止无限递归 +- **Handler 排序**:多个 Handler 订阅同一事件时,可通过 `order()` 控制执行顺序 +- **异常隔离**:每个 Handler 的异常通过独立回调处理,不影响其他 Handler ## 如何使用 ### 核心接口 -| 接口/类 | 说明 | -|---------|------| -| `IEvent` | 事件标记接口,继承 `Serializable` | -| `ISyncEvent extends IEvent` | 同步事件标记 | -| `IAsyncEvent extends IEvent` | 异步事件标记 | -| `IHandler` | 事件处理器,泛型绑定事件类型 | -| `EventPusher` | 静态工具类,事件推送入口 | -| `DomainEvent` (domain包) | 领域实体事件基类,携带 entity、entityClass、timestamp | -| `DomainCreateEvent` | 实体创建事件 | -| `DomainChangeEvent` | 实体字段变更事件(含 fieldName、oldValue、newValue) | -| `DomainDeleteEvent` | 实体删除事件 | - -### 注入方式 - -无需手动注入。`HandlerBeanDefinitionRegistrar` 在 Spring 启动时自动扫描所有标注 `@Handler` 的 Bean 并注册到 `ApplicationHandlerUtils`。 - -### 配置项 - -```properties -# 异步事件线程池大小(默认20) -codingapi.framework.handler-thread-pool-size=20 -``` +- `IEvent` — 事件标记接口(extends Serializable) +- `ISyncEvent extends IEvent` — 同步事件标记 +- `IAsyncEvent extends IEvent` — 异步事件标记 +- `IHandler` — 事件处理器,泛型绑定事件类型 -### 两种事件分发器 - -- **`SpringDefaultEventHandler`**:使用 `@EventListener`,事件立即触发 -- **`SpringTransactionEventHandler`**:使用 `@TransactionalEventListener(AFTER_COMMIT, fallbackExecution=true)`,事务提交后触发 - -## 使用实例 - -### 1. 定义自定义事件 +### 推送事件 ```java -// 同步事件 -public class OrderCreatedEvent implements ISyncEvent { - private final String orderId; - - public OrderCreatedEvent(String orderId) { - this.orderId = orderId; - } - - public String getOrderId() { return orderId; } -} +// 推送事件(默认同步,自动检测循环) +EventPusher.push(new MyEvent()); + +// 允许循环事件 +EventPusher.push(new MyEvent(), true); ``` -### 2. 编写 Handler +### 订阅事件 ```java -@Handler -@Component -public class OrderNotificationHandler implements IHandler { +@Service +public class MyHandler implements IHandler { @Override public int order() { - return 10; // 排序值,越小越先执行 + return 0; // 排序,默认0 } @Override - public void handler(OrderCreatedEvent event) { - // 发送订单创建通知 - System.out.println("Order created: " + event.getOrderId()); + public void handler(MyEvent event) { + // 处理事件 } @Override public void error(Exception exception) throws Exception { - // 异常回调,记录日志但不阻止后续 Handler - log.error("Notification failed", exception); + // 异常回调 + throw exception; } } ``` -### 3. 推送事件 +### 注册方式 -```java -// 推送同步事件 -EventPusher.push(new OrderCreatedEvent("ORD-001")); +Handler 只需声明为 Spring Bean(`@Service` / `@Component`),框架通过 `HandlerBeanDefinitionRegistrar` 自动扫描所有 `IHandler` 实现并注册到 `ApplicationHandlerUtils`。 -// 推送异步事件(需事件类实现 IAsyncEvent) -EventPusher.push(new AsyncLogEvent("log message")); +## 使用实例 -// 允许循环事件(跳过循环检测) -EventPusher.push(event, true); -``` +```java +// 定义事件 +public class UserCreatedEvent implements ISyncEvent { + private final Long userId; + public UserCreatedEvent(Long userId) { this.userId = userId; } + public Long getUserId() { return userId; } +} -### 4. 使用领域实体事件 +// 推送 +EventPusher.push(new UserCreatedEvent(user.getId())); -```java -// 实体变更事件携带字段级别信息 -DomainChangeEvent changeEvent = new DomainChangeEvent( - userEntity, // 实体对象 - "status", // 字段名 - "ACTIVE", // 旧值 - "INACTIVE" // 新值 -); -EventPusher.push(changeEvent); +// 订阅 +@Service +public class NotifyHandler implements IHandler { + @Override + public void handler(UserCreatedEvent event) { + // 发送通知 + } +} ``` diff --git a/docs/capabilities/fast-repository.md b/docs/capabilities/fast-repository.md deleted file mode 100644 index 7b7805502..000000000 --- a/docs/capabilities/fast-repository.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -name: fast-repository -description: JPA Repository 增强,支持动态过滤查询(RequestFilter + Relation 操作符)、HQL 自动构建、排序 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 44ba20df -code_files: - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/FastRepository.java - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/BaseRepository.java - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/DynamicRepository.java - - springboot-starter-data-fast/src/main/java/com/codingapi/springboot/fast/jpa/repository/SortRepository.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/request/PageRequest.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/request/RequestFilter.java ---- - -## 解决什么问题 - -Spring Data JPA 原生的 `JpaRepository` 在处理复杂查询时存在以下痛点: - -- **动态条件拼接繁琐**:需要手动编写 `Specification` 或 `@Query`,无法根据前端传入的过滤参数自动构建查询 -- **分页与过滤分离**:Spring 原生 `PageRequest` 不支持携带过滤条件,业务代码需要在 Controller/Service 层反复组装查询逻辑 -- **缺少丰富的比较操作符**:LIKE、BETWEEN、IN、IS_NULL 等常用操作符没有统一的抽象 - -本能力通过扩展 `PageRequest` 和提供 `FastRepository` 接口,实现了: - -- `RequestFilter` + `Relation` 枚举统一表达 14 种查询操作符 -- `FastRepository.findAll(PageRequest)` 自动根据 Filter 构建 Example 查询 -- `FastRepository.pageRequest(PageRequest)` 自动构建 HQL 动态查询 -- `DynamicRepository` 支持原生 SQL / SQLBuilder 的动态列表和分页查询 -- `SortRepository` 提供拖拽排序的 `reSort()` 方法 - -## 如何使用 - -### 核心接口 - -| 接口/类 | 说明 | -|---------|------| -| `FastRepository` | 增强型 Repository,继承 JpaRepository + JpaSpecificationExecutor + DynamicRepository | -| `BaseRepository` | 基础 Repository,提供 `getEntityClass()` 反射获取泛型类型 | -| `DynamicRepository` | 动态查询 Repository,支持 SQLBuilder 和原生 SQL 查询 | -| `SortRepository` | 排序 Repository,提供 `reSort(SortRequest)` 拖拽排序 | -| `PageRequest` | 扩展 Spring Data PageRequest,内置 `RequestFilter` | -| `RequestFilter` | 过滤条件容器,管理多个 `Filter` | -| `Filter` | 单个过滤条件,包含 key、relation、value | -| `Relation` | 查询操作符枚举 | - -### Relation 操作符 - -``` -EQUAL, NOT_EQUAL, LIKE, LEFT_LIKE, RIGHT_LIKE, -BETWEEN, IN, NOT_IN, IS_NULL, IS_NOT_NULL, -GREATER_THAN, LESS_THAN, GREATER_THAN_EQUAL, LESS_THAN_EQUAL -``` - -### 注入方式 - -让实体 Repository 继承 `FastRepository` 即可: - -```java -public interface UserRepository extends FastRepository { -} -``` - -## 使用实例 - -### 1. 基础动态过滤查询(Example 模式) - -```java -// 创建分页请求并添加过滤条件 -PageRequest request = PageRequest.of(0, 20); -request.addFilter("name", "张三"); // 默认 EQUAL -request.addFilter("age", Relation.GREATER_THAN, 18); // 大于 -request.addFilter("email", Relation.LIKE, "gmail"); // 模糊匹配 - -Page page = userRepository.findAll(request); -``` - -### 2. HQL 动态查询 - -```java -PageRequest request = PageRequest.of(0, 20); -request.addFilter("status", Relation.IN, "ACTIVE", "PENDING"); -request.addFilter("createTime", Relation.BETWEEN, startDate, endDate); - -// pageRequest() 使用 HQL 构建查询,适合复杂条件 -Page page = userRepository.pageRequest(request); -``` - -### 3. AND/OR 组合条件 - -```java -PageRequest request = PageRequest.of(0, 20); - -// OR 组合:name='张三' OR name='李四' -request.orFilters( - Filter.as("name", "张三"), - Filter.as("name", "李四") -); - -// AND 组合 -request.andFilters( - Filter.as("age", Relation.GREATER_THAN_EQUAL, 18), - Filter.as("status", "ACTIVE") -); -``` - -### 4. 从请求中读取过滤值 - -```java -// 在 Service 层获取前端传入的过滤参数 -String name = request.getStringFilter("name", "default"); -int age = request.getIntFilter("age", 0); -boolean hasFilter = request.hasFilter(); -``` - -### 5. 原生 SQL 动态查询 - -```java -// 使用 DynamicRepository 的原生 SQL 查询 -List users = userRepository.dynamicListQuery( - "SELECT * FROM user WHERE status = ?", "ACTIVE" -); - -// 分页查询 -Page page = userRepository.dynamicPageQuery( - "SELECT * FROM user WHERE dept_id = ?", - "SELECT COUNT(*) FROM user WHERE dept_id = ?", - PageRequest.of(0, 20), - deptId -); -``` - -### 6. 拖拽排序 - -```java -// 实体需实现 ISort 接口 -SortRequest sortRequest = new SortRequest(List.of(3L, 1L, 2L)); -userRepository.reSort(sortRequest); -``` diff --git a/docs/capabilities/flow-postpone-event.md b/docs/capabilities/flow-postpone-event.md deleted file mode 100644 index 84d7d84dc..000000000 --- a/docs/capabilities/flow-postpone-event.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: flow-postpone-event -description: 延期流程操作的事件通知 — 当前 FlowPostponedService 未发送 FlowApprovalEvent -status: 计划中 -scope: 后端 -source: 计划 ---- - -## 解决什么问题 - -当前工作流引擎的所有操作(发起、提交、驳回、撤回、删除、作废、退回、完成、转办、催办、终止)都通过 `EventPusher` 发送 `FlowApprovalEvent`,业务侧可通过 `IHandler` 监听并联动处理。 - -**但延期(Postpone)操作是唯一的例外**:`FlowPostponedService.postponed()` 仅更新了 `FlowRecord` 的截止时间,没有发送任何事件。这导致: - -- 业务系统无法感知延期操作,无法触发提醒、日志、通知等联动 -- 与其他流程操作的契约不一致 — Handler 中 `isXxx()` 判断覆盖了所有操作,唯独缺少 `isPostponed()` -- 审计日志中延期操作无事件记录 - -## 如何使用 - -### 计划实现 - -1. 在 `FlowApprovalEvent` 中新增状态常量: - ```java - public static final int STATE_POSTPONED = 15; - ``` - -2. 新增便捷判断方法: - ```java - public boolean isPostponed() { - return state == STATE_POSTPONED; - } - ``` - -3. 在 `FlowPostponedService.postponed()` 末尾发送事件: - ```java - EventPusher.push(new FlowApprovalEvent( - FlowApprovalEvent.STATE_POSTPONED, - flowRecord, currentOperator, flowWork, bindData - )); - ``` - -4. 在 `event.md` 规范文档中将"延期流程"事件更新为 `POSTPONED` - -### 业务监听 - -```java -@Component -public class LeavePostponedHandler implements IHandler { - @Override - public void handler(FlowApprovalEvent event) { - if (event.isPostponed()) { - FlowRecord record = event.getFlowRecord(); - // 发送延期提醒通知给申请人 - notifyService.sendDelayNotice( - record.getCreateOperatorId(), - record.getPostponedTime() - ); - } - } -} -``` - -## 使用实例 - -```java -// 当前状态(无事件): -flowPostponedService.postponed(recordId, operator, delayMillis); -// → FlowRecord 截止时间已更新 -// → ❌ 无 FlowApprovalEvent 发出 - -// 实现后(有事件): -flowPostponedService.postponed(recordId, operator, delayMillis); -// → FlowRecord 截止时间已更新 -// → ✅ FlowApprovalEvent(STATE_POSTPONED) 通过 EventPusher 发出 -// → ✅ LeavePostponedHandler.handler() 被调用 -``` diff --git a/docs/capabilities/global-exception-handler.md b/docs/capabilities/global-exception-handler.md new file mode 100644 index 000000000..3b5705d5f --- /dev/null +++ b/docs/capabilities/global-exception-handler.md @@ -0,0 +1,64 @@ +--- +name: global-exception-handler +description: 全局异常处理器,统一捕获 Controller 层异常并转换为标准 Response 格式返回 +status: 已实现 +scope: 后端 +source: 项目自有 +import: com.codingapi.springboot:springboot-starter +symbols: + - BasicHandlerExceptionResolverConfiguration + - ServletExceptionHandler + - LocaleMessageException +content_hash: e3029dc411b34af5ea488d45c2ac414e2ea7578b571e681e0d7dc938ae75f052 +--- + +## 解决什么问题 + +Controller 层的未捕获异常需要统一处理,避免将堆栈信息暴露给前端。本能力通过 Spring `HandlerExceptionResolver` 机制实现全局异常拦截: + +- **统一错误格式**:所有异常都转换为 `{success: false, errCode, errMessage}` 格式 +- **国际化异常支持**:`LocaleMessageException` 携带错误码,支持国际化消息 +- **兜底处理**:未识别的异常统一返回 `system.err` 错误码 +- **日志记录**:异常信息自动记录到日志 + +## 如何使用 + +### 抛出业务异常 + +```java +// 使用 LocaleMessageException 抛出带错误码的异常 +throw new LocaleMessageException("user.not.found", "用户不存在"); +throw new LocaleMessageException("order.invalid", "订单状态无效"); +``` + +### 自动处理 + +框架自动注册 `ServletExceptionHandler`,所有 Controller 层的异常都会被拦截并转换为标准响应: + +- `LocaleMessageException` → `{success: false, errCode: "user.not.found", errMessage: "用户不存在"}` +- 其他 `Exception` → `{success: false, errCode: "system.err", errMessage: "异常消息"}` + +### 自动配置 + +无需手动配置,`BasicHandlerExceptionResolverConfiguration` 在 Spring Web 环境下自动生效(`@ConditionalOnClass`)。 + +## 使用实例 + +```java +@RestController +public class UserController { + + @GetMapping("/users/{id}") + public SingleResponse get(@PathVariable Long id) { + User user = userService.findById(id); + if (user == null) { + // 抛出国际化异常,自动转换为错误响应 + throw new LocaleMessageException("user.not.found", "用户不存在"); + } + return SingleResponse.of(user); + } +} + +// 前端收到的响应: +// {"success": false, "errCode": "user.not.found", "errMessage": "用户不存在"} +``` diff --git a/docs/capabilities/groovy-runtime.md b/docs/capabilities/groovy-runtime.md new file mode 100644 index 000000000..dbdf9ddb6 --- /dev/null +++ b/docs/capabilities/groovy-runtime.md @@ -0,0 +1,57 @@ +--- +name: groovy-runtime +description: Apache Groovy 运行时 — 动态脚本编译、执行与类型系统桥接 +status: 已实现 +scope: 后端 +source: 框架:Groovy +import: org.apache.groovy:groovy +framework_version: 4.0.24 +--- + +## 解决什么问题 + +Groovy 作为 JVM 上的动态语言,在本框架中被广泛使用: + +- **工作流表达式**:FlowNode 的 `TitleGenerator`、`OperatorMatcher`、`OutTrigger` 使用 Groovy 脚本 +- **动态脚本引擎**:`springboot-starter-script` 提供完整的 Groovy 脚本运行时 +- **类型桥接**:Groovy 与 Java 之间的自动类型转换 +- **JSON/XML 处理**:通过 `groovy-json` 和 `groovy-xml` 模块处理结构化数据 + +## 如何使用 + +### Groovy 脚本执行 + +```groovy +// Groovy 脚本可以直接使用 Java 类型 +def greeting(String name) { + return "Hello, ${name}!" +} +``` + +### GroovyShell 使用 + +```java +GroovyShell shell = new GroovyShell(); +Script script = shell.parse("return a + b"); +Binding binding = new Binding(); +binding.setVariable("a", 10); +binding.setVariable("b", 20); +script.setBinding(binding); +Object result = script.run(); // 30 +``` + +### 本框架中的使用场景 + +- **工作流引擎**:节点标题生成、审批人匹配、条件分支触发器 +- **脚本引擎**:动态业务规则、表单校验、报表查询 +- **数据查询**:FastRepository 中的动态 HQL 构建辅助 + +## 使用实例 + +```java +// 工作流中使用 Groovy 匹配审批人 +OperatorMatcher matcher = new OperatorMatcher( + "def matcher(session) { return [session.createOperatorId] }" +); +List operatorIds = matcher.matcher(flowSession); +``` diff --git a/docs/capabilities/groovy-script-engine.md b/docs/capabilities/groovy-script-engine.md index 451c399e0..e7dd9459a 100644 --- a/docs/capabilities/groovy-script-engine.md +++ b/docs/capabilities/groovy-script-engine.md @@ -1,133 +1,93 @@ --- name: groovy-script-engine -description: Groovy 动态脚本引擎,支持运行时编译执行、LRU 缓存、热更新和 REST API +description: Groovy 脚本运行时引擎,支持动态编译、LRU 缓存、类型映射、元数据扫描与临时脚本持久化 status: 已实现 scope: 后端 source: 项目自有 -last_commit: fbfb901b -code_files: - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/GroovyScriptRuntime.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/GroovyScriptRuntimeContext.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/cache/GroovyScriptCacheContext.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/repository/GroovyScriptRepository.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/repository/TempGroovyScriptRepository.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/strategy/GroovyTypeFixStrategy.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/strategy/GroovyMetadataGenerateStrategy.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/controller/GroovyScriptController.java - - springboot-starter-script/src/main/java/com/codingapi/springboot/script/runner/GroovyScriptEngineRunner.java +import: com.codingapi.springboot:springboot-starter-script +symbols: + - GroovyScript + - GroovyScriptRuntimeContext + - GroovyScriptRuntime + - GroovyScriptEngineRunner + - GroovyScriptCacheContext + - GroovyMetadataScannerUtils + - GroovyMetadata + - GroovyType + - GroovyField + - GroovyFunction + - ScriptTypeMappingContext + - ScriptTypeMapping + - GroovyTypeFixStrategyContext + - GroovyTypeFixStrategy + - GroovyMetadataGenerateStrategyContext + - GroovyScriptController + - GroovyScriptRepository + - TempGroovyScriptContext + - TransactionMode +content_hash: 913fcdb49caf0b894a3d6cbff60bb519566caf888e292724c652766cc4fa9c90 --- ## 解决什么问题 -业务规则频繁变更时,硬编码需要重新编译部署。Groovy 脚本引擎提供运行时动态编译执行能力,让业务规则可以通过脚本形式热加载,无需重启应用。典型场景: +在企业应用中,某些业务规则需要动态调整而不想重新部署(如工作流条件表达式、表单校验规则、动态报表查询)。本 Groovy 脚本引擎解决了以下问题: -- 动态表单校验规则 -- 工作流节点审批脚本 -- 数据转换/映射规则 -- 临时数据处理脚本 +- **运行时编译执行**:Groovy 脚本在运行时编译为 Java 字节码并执行 +- **LRU 编译缓存**:编译后的 Class 缓存在 LRU Cache 中,避免重复编译 +- **类型映射**:解决 Groovy 与 Java 之间的类型差异(如 `int` → `Integer`) +- **元数据扫描**:通过注解扫描脚本的字段、函数、参数信息 +- **临时脚本持久化**:临时脚本在应用重启时自动持久化到数据库并恢复 ## 如何使用 -### 核心运行时(GroovyScriptRuntime) +### 构建与执行脚本 ```java -// 初始化运行时(指定 LRU 缓存最大容量) -GroovyScriptRuntime runtime = new GroovyScriptRuntime(100); - -// 编译脚本(带缓存 — 以 SHA256 哈希为 Key) -runtime.compile("def add(a, b) { return a + b }", true); - -// 编译脚本(不带缓存) -runtime.compile("println 'hello'", false); +GroovyScript script = GroovyScript.builder("calc-discount") + .script("def calc(BigDecimal price, int level) { return price * (1 - level * 0.1) }") + .description("计算折扣价") + .returnType(BigDecimal.class) + .build(); // 执行脚本 -Object result = runtime.execute("return 1 + 2"); - -// 执行脚本并绑定变量 -Map bindings = new HashMap<>(); -bindings.put("name", "张三"); -Object result = runtime.execute("return 'Hello, ' + name", bindings); +BigDecimal result = script.invoke(TransactionMode.none, + Map.of(), new BigDecimal("100"), 2); +// result = 80.0 ``` -### LRU 缓存机制 +### 带事务执行 -- 缓存 Key 为脚本内容的 SHA256 哈希值 -- 采用 `LinkedHashMap` 实现 LRU 淘汰策略 -- 超过 `maxCacheSize` 时自动移除最久未使用的条目 -- 通过 `clearCache()` 手动清空缓存 -- 通过 `cacheSize()` 查看当前缓存数量 - -### 脚本仓库(GroovyScriptRepository) - -持久化存储脚本,支持 CRUD 和按名称查找: ```java -// 保存脚本 -repository.save("calc_discount", scriptContent); - -// 按名称执行 -Object result = runtime.executeByName("calc_discount", bindings); +// 在 Spring 事务中执行脚本 +Object result = script.invoke(TransactionMode.required, bindMap, arg1, arg2); ``` -### 临时脚本(TempGroovyScriptRepository) +### REST API 管理脚本 -一次性执行的临时脚本,执行后自动清理: -```java -// 提交临时脚本 -tempRepo.submit("return data.collect { it.name }", dataBindings); -``` +引擎提供 `GroovyScriptController`,通过 HTTP 接口管理脚本的编译、保存和执行。 -### 元数据扫描 +### 类型映射扩展 -通过注解扫描提取脚本元数据,用于前端展示: ```java -@GroovyScript -class DiscountCalc { - @ScriptFunction(description = "计算折扣") - static BigDecimal calc(@ScriptParameter("金额") BigDecimal amount) { - return amount.multiply(new BigDecimal("0.9")) - } -} +// 注册自定义类型映射 +ScriptTypeMappingContext.getInstance().addMapping(new ScriptTypeMapping() { + public boolean support(Class target) { return target == PageRequest.class; } + public Class mapping(Class target) { return CustomPageRequest.class; } +}); ``` -### 策略扩展点 - -- `GroovyMetadataGenerateStrategy` — 自定义元数据生成逻辑 -- `GroovyTypeFixStrategy` — 自定义类型元数据修正(如补充泛型信息) - -### REST API(GroovyScriptController) - -| 端点 | 方法 | 说明 | -|------|------|------| -| `/open/script/save` | POST | 保存脚本 | -| `/open/script/compile` | POST | 编译检查(不执行) | -| `/open/script/run` | POST | 执行脚本 | -| `/open/script/list` | GET | 列出所有脚本 | - ## 使用实例 ```java -// 1. 基本执行 -GroovyScriptRuntime runtime = new GroovyScriptRuntime(50); -Object result = runtime.execute("return [1,2,3].sum()"); -assert result.equals(6); - -// 2. 缓存模式 — 相同内容只编译一次 -String script = "def discount(amount) { return amount * 0.85 }"; -runtime.compile(script, true); // 首次编译并缓存 -Object r1 = runtime.execute(script); // 命中缓存 -Object r2 = runtime.execute(script); // 命中缓存 - -// 3. 绑定变量执行 -Map vars = new HashMap<>(); -vars.put("price", new BigDecimal("100")); -vars.put("quantity", 3); -Object total = runtime.execute( - "return price.multiply(new BigDecimal(quantity))", - vars -); -// total = 300 - -// 4. 清空缓存 -runtime.clearCache(); -assert runtime.cacheSize() == 0; +// 工作流中使用 Groovy 脚本匹配审批人 +GroovyScript script = GroovyScript.builder("match-operator") + .script(""" + def matcher(session) { + if (session.amount > 10000) return [1001L] // 总经理审批 + return [session.createOperatorId] // 直属上级 + } + """) + .build(); + +List operatorIds = script.invoke(TransactionMode.none, Map.of(), session); ``` diff --git a/docs/capabilities/index.md b/docs/capabilities/index.md index 0a3e6bc19..1a0732dcf 100644 --- a/docs/capabilities/index.md +++ b/docs/capabilities/index.md @@ -6,35 +6,24 @@ | 名称 | 描述 | 范围 | 来源 | |------|------|------|------| -| [crypto-tools](./crypto-tools.md) | 密码学工具套件,AES(CBC 模式)/RSA(BouncyCastle)/DES 加解密 + SHA256/HmacSHA256 哈希签名,提供实例化工... | 后端 | 项目自有 | -| [data-authorization](./data-authorization.md) | 完整的数据权限框架,包含行级权限(WHERE/JOIN 条件注入)和列级权限(数据脱敏),基于 JSqlParser 的 SQL 增强引擎,提供 Colu... | 后端 | 项目自有 | -| [domain-change-interceptor](./domain-change-interceptor.md) | 通过 CGLIB 代理拦截领域实体字段的 setter 方法,自动检测变更并发布 DomainChangeEvent | 后端 | 项目自有 | -| [dynamic-application](./dynamic-application.md) | 支持外部 JAR 加载和 Spring 上下文热重启的应用启动器 | 后端 | 项目自有 | -| [dynamic-mvc-mapping](./dynamic-mvc-mapping.md) | 运行时动态注册/注销 Spring MVC REST 端点,支持将 Groovy 脚本绑定到动态 API 路径 | 后端 | 项目自有 | -| [event-system](./event-system.md) | 自建的领域事件发布-订阅系统,支持同步/异步事件、事务后提交、Handler自动注册排序 | 后端 | 项目自有 | -| [fast-repository](./fast-repository.md) | JPA Repository 增强,支持动态过滤查询(RequestFilter + Relation 操作符)、HQL 自动构建、排序 | 后端 | 项目自有 | -| [groovy-script-engine](./groovy-script-engine.md) | Groovy 动态脚本引擎,支持运行时编译执行、LRU 缓存、热更新和 REST API | 后端 | 项目自有 | -| [jackson](./jackson.md) | Jackson JSON 序列化/反序列化能力,由 Spring Boot Web 自动配置,框架中用于 REST API 响应序列化、请求参数绑定等场景 | 后端 | 框架:jackson | -| [jdbc-proxy-chain](./jdbc-proxy-chain.md) | 完整的 JDBC 装饰器代理链,透明拦截 SQL 执行以实现数据权限控制 | 后端 | 项目自有 | -| [locale-exception](./locale-exception.md) | 支持 i18n 的业务异常体系,errCode + errMessage,通过 MessageSource 实现国际化 | 后端 | 项目自有 | -| [rest-client](./rest-client.md) | 出站 HTTP 客户端框架,提供 HttpClient(底层 HTTP 封装)、RestClient(REST 高级客户端)、SessionClient(... | 后端 | 项目自有 | -| [spring-data-jpa](./spring-data-jpa.md) | Spring Data JPA 数据访问能力,提供 ORM 映射、Repository 抽象、分页查询等基础设施,框架在此基础上扩展了 FastRepos... | 后端 | 框架:spring-data-jpa | -| [spring-data-redis](./spring-data-redis.md) | Spring Data Redis 数据存储能力,框架中主要用于 Token/Session 的有状态存储,支持服务端主动失效和多端登录管理 | 后端 | 框架:spring-data-redis | -| [spring-framework](./spring-framework.md) | Spring Framework / Spring Boot 核心基础能力,提供 IoC 容器、事件发布、AOP、Web MVC、事务管理等基础设施 | 后端 | 框架:spring-framework | -| [spring-security](./spring-security.md) | Spring Security 安全认证与授权能力,框架在此基础上封装了 JWT 无状态认证和 Redis 有状态认证两种模式 | 后端 | 框架:spring-security | -| [sql-interceptor](./sql-interceptor.md) | 基于 JSqlParser 的 SQL 解析改写拦截器,在 SQL 执行前透明注入数据权限条件 | 后端 | 项目自有 | -| [token-gateway](./token-gateway.md) | JWT/Redis 双模式 Token 认证网关,统一创建、解析和管理认证令牌 | 后端 | 项目自有 | -| [transaction-manager-context](./transaction-manager-context.md) | 单例编程式事务上下文,支持 REQUIRES_NEW 语义,在非 Spring 管理上下文中也能使用事务 | 后端 | 项目自有 | -| [trigger-system](./trigger-system.md) | 触发器框架,支持定时/条件触发,提供统一的触发上下文管理 | 后端 | 项目自有 | -| [user-context](./user-context.md) | 线程级用户身份上下文,通过 ThreadLocal 持有当前登录用户,支持跨层传递 | 后端 | 项目自有 | -| [workflow-engine](./workflow-engine.md) | 工作流引擎,支持流程定义、节点流转、审批、委托、会签、数据快照和事件通知 | 后端 | 项目自有 | - -## 🗓️ 计划中 - -| 名称 | 描述 | 范围 | 来源 | -|------|------|------|------| -| [flow-postpone-event](./flow-postpone-event.md) | 延期流程操作的事件通知 — 当前 FlowPostponedService 未发送 FlowApprovalEvent | 后端 | 计划 | +| [data-authorization](./data-authorization.md) | 数据权限 SQL 拦截器,通过 JDBC 代理链在 SQL 执行前透明注入权限条件,支持行级过滤与列级脱敏 | 后端 | 项目自有 | +| [domain-proxy](./domain-proxy.md) | 基于 CGLIB 的领域实体代理,自动拦截字段变更并推送 DomainChangeEvent 领域事件 | 后端 | 项目自有 | +| [dynamic-data-query](./dynamic-data-query.md) | 动态数据查询体系,基于 FastRepository + PageRequest/Filter/SearchRequest 自动构建 Example 或 ... | 后端 | 项目自有 | +| [event-system](./event-system.md) | 发布-订阅事件系统,支持同步/异步事件、Handler排序、循环检测与事务集成 | 后端 | 项目自有 | +| [global-exception-handler](./global-exception-handler.md) | 全局异常处理器,统一捕获 Controller 层异常并转换为标准 Response 格式返回 | 后端 | 项目自有 | +| [groovy-runtime](./groovy-runtime.md) | Apache Groovy 运行时 — 动态脚本编译、执行与类型系统桥接 | 后端 | 框架:Groovy | +| [groovy-script-engine](./groovy-script-engine.md) | Groovy 脚本运行时引擎,支持动态编译、LRU 缓存、类型映射、元数据扫描与临时脚本持久化 | 后端 | 项目自有 | +| [jjwt](./jjwt.md) | JJWT 库 — JWT Token 的创建、签名、解析与验证 | 后端 | 框架:JJWT | +| [jsqlparser](./jsqlparser.md) | JSqlParser SQL 解析库 — 解析和改写 SQL 语句,用于数据权限条件注入 | 后端 | 框架:JSqlParser | +| [jwt-auth-gateway](./jwt-auth-gateway.md) | JWT 认证网关,集成 Spring Security 与 JJWT,支持 Token 创建、解析、Redis 有状态/无状态双模式 | 后端 | 项目自有 | +| [kryo](./kryo.md) | Kryo 高性能序列化库 — 用于工作流数据快照的深拷贝与持久化 | 后端 | 框架:Kryo | +| [rest-client](./rest-client.md) | REST HTTP 客户端封装,支持自动重试、代理配置、请求/响应拦截器与信任所有证书模式 | 后端 | 项目自有 | +| [spring-data-jpa](./spring-data-jpa.md) | Spring Data JPA — ORM 映射、Repository 抽象、分页查询、Specification 动态查询 | 后端 | 框架:Spring Data JPA | +| [spring-framework](./spring-framework.md) | Spring Framework / Spring Boot 核心能力 — IoC 容器、自动配置、事件发布、AOP、Web MVC | 后端 | 框架:Spring Boot | +| [spring-security](./spring-security.md) | Spring Security 认证与授权框架 — 提供 Filter 链、CSRF 防护、密码编码、权限控制 | 后端 | 框架:Spring Security | +| [unified-response](./unified-response.md) | 统一响应封装体系(Response / SingleResponse / MultiResponse / MapResponse),标准化 API 返回格式 | 后端 | 项目自有 | +| [workflow-engine](./workflow-engine.md) | 轻量级工作流引擎,支持流程定义、节点流转、审批、退回、委托、会签、抄送、数据快照与事件通知 | 后端 | 项目自有 | --- -**统计**: 共 23 篇 — 已实现 22 / 计划中 1 / 已废弃 0 +**统计**: 共 17 篇 — 已实现 17 / 计划中 0 / 已废弃 0 diff --git a/docs/capabilities/jackson.md b/docs/capabilities/jackson.md deleted file mode 100644 index 5170e6ccc..000000000 --- a/docs/capabilities/jackson.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -name: jackson -description: Jackson JSON 序列化/反序列化能力,由 Spring Boot Web 自动配置,框架中用于 REST API 响应序列化、请求参数绑定等场景 -status: 已实现 -scope: 后端 -source: 框架:jackson -framework_version: Managed by Spring Boot BOM (3.3.5) ---- - -## 解决什么问题 - -Jackson 是 Spring Boot Web 默认的 JSON 处理库,在框架中承担以下职责: - -- **REST API 响应序列化**:所有 Controller 返回的 `Response` / `SingleResponse` / `MultiResponse` / `MapResponse` 对象均通过 Jackson 自动序列化为 JSON 输出 -- **请求参数反序列化**:`@RequestBody` 注解的请求体自动由 Jackson 反序列化为 Java 对象,包括 `PageRequest` 中的过滤条件 -- **枚举序列化**:框架提供了自定义 `EnumSerializer`,统一枚举值的 JSON 输出格式 -- **日期/时间格式化**:通过 Spring Boot 的 `spring.jackson.*` 配置项统一日期格式 -- **字段命名策略**:支持驼峰/下划线等命名转换,前后端字段名对齐 - -> **注意**:框架内部部分场景(如 Token 序列化、事件数据传输)使用了 Fastjson(`com.alibaba.fastjson.JSONObject`)作为补充,通过 `JsonSerializable` 接口封装。Jackson 主要负责 HTTP 层的 JSON 处理。 - -## 如何使用 - -### 依赖引入 - -Jackson 由 `spring-boot-starter-web` 自动引入,无需单独声明: - -```xml - - - com.codingapi.springboot - springboot-starter - -``` - -### 全局配置 - -在 `application.properties` 中统一配置 Jackson 行为: - -```properties -# 日期格式 -spring.jackson.date-format=yyyy-MM-dd HH:mm:ss -spring.jackson.time-zone=GMT+8 - -# 序列化选项 -spring.jackson.default-property-inclusion=non_null -spring.jackson.serialization.write-dates-as-timestamps=false - -# 反序列化选项 -spring.jackson.deserialization.fail-on-unknown-properties=false -``` - -### 自定义序列化器 - -框架已提供 `EnumSerializer`,可按需注册更多自定义序列化器: - -```java -@Configuration -public class JacksonConfig { - - @Bean - public ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(MyEnum.class, new EnumSerializer()); - mapper.registerModule(module); - return mapper; - } -} -``` - -## 使用实例 - -### REST API 响应(自动序列化) - -```java -@RestController -@RequestMapping("/api/users") -@AllArgsConstructor -public class UserController { - - private final UserQueryService userQueryService; - - // Jackson 自动将 SingleResponse 序列化为 JSON - @GetMapping("/{id}") - public SingleResponse getUser(@PathVariable Long id) { - return SingleResponse.of(userQueryService.findById(id)); - } - - // 分页响应包含 total、content 等字段 - @GetMapping - public MultiResponse list(PageRequest request) { - Page page = userQueryService.findAll(request); - return MultiResponse.of(page.getContent(), page.getTotalElements()); - } -} -``` - -输出示例: - -```json -{ - "success": true, - "errCode": null, - "errMessage": null, - "data": [ - { "id": 1, "name": "张三", "age": 28 } - ], - "total": 1 -} -``` - -### 请求体反序列化 - -```java -@PostMapping -public Response createUser(@RequestBody CreateUserCmd cmd) { - // Jackson 自动将 JSON 请求体反序列化为 CreateUserCmd - userService.create(cmd); - return Response.buildSuccess(); -} -``` diff --git a/docs/capabilities/jdbc-proxy-chain.md b/docs/capabilities/jdbc-proxy-chain.md deleted file mode 100644 index 9c21d2f7a..000000000 --- a/docs/capabilities/jdbc-proxy-chain.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -name: jdbc-proxy-chain -description: 完整的 JDBC 装饰器代理链,透明拦截 SQL 执行以实现数据权限控制 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 67895fda -code_files: - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/ConnectionProxy.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/StatementProxy.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/PreparedStatementProxy.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/CallableStatementProxy.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/jdbc/proxy/ResultSetProxy.java ---- - -## 解决什么问题 - -数据权限要求在不修改业务 SQL 的前提下,透明地注入行级/列级过滤条件。手动在每个 Repository 中拼接权限条件会导致: -- 业务代码与权限逻辑强耦合 -- 遗漏某个查询导致数据泄露 -- 难以统一管理和审计权限规则 - -JDBC 代理链通过装饰器模式包装整个 JDBC 调用链,在 SQL 执行前自动拦截和改写,对业务代码完全透明。 - -## 如何使用 - -### 代理链架构 - -``` -ConnectionProxy(包装 DataSource 返回的 Connection) - │ - ├── createStatement() → StatementProxy - │ └── executeQuery(sql) → SQLRunningContext.intercept(sql) → ResultSetProxy - │ - ├── prepareStatement(sql) → SQLRunningContext.intercept(sql) → PreparedStatementProxy - │ └── executeQuery() → ResultSetProxy - │ - └── prepareCall(sql) → SQLRunningContext.intercept(sql) → CallableStatementProxy - └── executeQuery() → ResultSetProxy -``` - -### 各代理职责 - -| 代理类 | 职责 | -|--------|------| -| `ConnectionProxy` | 拦截 `prepareStatement`/`prepareCall`,在 SQL 创建时调用 `SQLRunningContext.intercept()` | -| `StatementProxy` | 拦截 `executeQuery`/`executeUpdate`,动态改写 SQL 并包装返回值 | -| `PreparedStatementProxy` | 包装已拦截的 PreparedStatement,对返回的 ResultSet 应用列级权限 | -| `CallableStatementProxy` | 委托底层 CallableStatement,应用授权上下文 | -| `ResultSetProxy` | 拦截列级数据读取,通过 `ColumnHandlerContext` 对字段值进行脱敏或过滤 | - -### 拦截流程 - -1. `ConnectionProxy.prepareStatement(sql)` 调用 `SQLRunningContext.getInstance().intercept(sql)` -2. `SQLRunningContext` 返回 `SQLExecuteState`(包含改写后的 SQL 和拦截状态) -3. 改写后的 SQL 传给底层 `Connection.prepareStatement()` -4. 返回的 `ResultSet` 被 `ResultSetProxy` 包装 -5. 读取每列时,`ColumnHandlerContext` 根据表名和列名决定是否脱敏 - -### 自动装配 - -代理链通过 `DataAuthorizationConfiguration` 自动注入到 DataSource 中,业务代码无需感知: - -```java -// 框架自动将 DataSource.getConnection() 替换为 ConnectionProxy -// 所有 JPA/JDBC 查询自动走代理链 -``` - -## 使用实例 - -```java -// 自定义 SQL 拦截器 — 为特定表注入租户条件 -public class TenantSQLInterceptor implements SQLInterceptor { - @Override - public SQLExecuteState intercept(String sql) { - if (sql.contains("FROM orders")) { - String tenantId = UserContext.getCurrentUser().getTenantId(); - String modifiedSql = sql + " AND tenant_id = '" + tenantId + "'"; - return new SQLExecuteState(modifiedSql, true); - } - return new SQLExecuteState(sql, false); - } -} - -// 自定义列级处理器 — 对手机号脱敏 -public class PhoneColumnHandler implements ColumnHandler { - @Override - public String handle(String tableName, String columnName, String value) { - if ("user".equals(tableName) && "phone".equals(columnName)) { - return value.substring(0, 3) + "****" + value.substring(7); - } - return value; - } -} -``` diff --git a/docs/capabilities/jjwt.md b/docs/capabilities/jjwt.md new file mode 100644 index 000000000..74cb4f2ff --- /dev/null +++ b/docs/capabilities/jjwt.md @@ -0,0 +1,69 @@ +--- +name: jjwt +description: JJWT 库 — JWT Token 的创建、签名、解析与验证 +status: 已实现 +scope: 后端 +source: 框架:JJWT +import: io.jsonwebtoken:jjwt-api +framework_version: 0.12.6 +--- + +## 解决什么问题 + +JJWT(Java JWT)是 Java 生态最主流的 JWT 库,本框架在 `springboot-starter-security` 中使用 JJWT 实现 Token 认证: + +- **Token 创建**:`Jwts.builder()` 构建带签名的 JWT +- **Token 解析**:`Jwts.parser()` 验证签名并提取 Claims +- **HMAC 签名**:使用 `Keys.hmacShaKeyFor()` 生成对称密钥 +- **Subject 载荷**:将用户信息序列化到 Token 的 subject 字段 + +## 如何使用 + +### 创建 Token + +```java +SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); +String jwt = Jwts.builder() + .subject(tokenPayload) + .signWith(key) + .compact(); +``` + +### 解析 Token + +```java +Jws jws = Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(jwtString); +String subject = jws.getPayload().getSubject(); +``` + +### 本框架封装 + +```java +// 通过 JwtTokenGateway 封装 +JwtTokenGateway gateway = new JwtTokenGateway(securityJWTProperties); +Token token = gateway.create(username, authorities); +Token parsed = gateway.parser(jwtString); +``` + +## 使用实例 + +```java +// 手动使用 JJWT +SecretKey key = Keys.hmacShaKeyFor("my-secret-key-at-least-32-chars".getBytes()); + +// 创建 +String jwt = Jwts.builder() + .subject("{\"userId\":1,\"username\":\"admin\"}") + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + 7200000)) + .signWith(key) + .compact(); + +// 验证 +Claims claims = Jwts.parser().verifyWith(key).build() + .parseSignedClaims(jwt).getPayload(); +String payload = claims.getSubject(); +``` diff --git a/docs/capabilities/jsqlparser.md b/docs/capabilities/jsqlparser.md new file mode 100644 index 000000000..0bf67c154 --- /dev/null +++ b/docs/capabilities/jsqlparser.md @@ -0,0 +1,62 @@ +--- +name: jsqlparser +description: JSqlParser SQL 解析库 — 解析和改写 SQL 语句,用于数据权限条件注入 +status: 已实现 +scope: 后端 +source: 框架:JSqlParser +import: com.github.jsqlparser:jsqlparser +framework_version: 5.0 +--- + +## 解决什么问题 + +数据权限模块需要在 SQL 执行前动态注入权限条件(WHERE/JOIN 子句),手动拼接 SQL 既不安全也不灵活。JSqlParser 提供了 SQL 语法树解析和改写能力: + +- **SQL 解析**:将 SQL 字符串解析为语法树(AST) +- **条件注入**:在 SELECT/UPDATE/DELETE 语句中注入 WHERE 条件 +- **JOIN 注入**:为关联查询添加权限 JOIN 子句 +- **别名管理**:处理表别名与列别名的映射关系 + +## 如何使用 + +### 解析 SQL + +```java +Statement stmt = CCJSqlParserUtil.parse("SELECT * FROM users WHERE status = 'active'"); +Select select = (Select) stmt; +PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); +``` + +### 注入 WHERE 条件 + +```java +Expression where = CCJSqlParserUtil.parseCondExpression("dept_id IN (1, 2, 3)"); +Expression existingWhere = plainSelect.getWhere(); +if (existingWhere != null) { + plainSelect.setWhere(new AndExpression(existingWhere, where)); +} else { + plainSelect.setWhere(where); +} +String newSql = plainSelect.toString(); +// SELECT * FROM users WHERE status = 'active' AND dept_id IN (1, 2, 3) +``` + +### 本框架中的使用 + +`DataPermissionSQLEnhancer` 封装了 JSqlParser 的 SQL 改写逻辑,在 `SQLRunningContext.intercept()` 中自动调用。 + +## 使用实例 + +```java +// 数据权限自动改写 SQL +// 原始 SQL: SELECT * FROM orders WHERE status = 'PENDING' +// 改写后: SELECT * FROM orders WHERE status = 'PENDING' AND dept_id IN (1,2) + +// 直接调用 +Statement stmt = CCJSqlParserUtil.parse(sql); +if (stmt instanceof Select) { + // 注入权限条件 + enhancer.enhance((Select) stmt, tableName, condition); +} +String securedSql = stmt.toString(); +``` diff --git a/docs/capabilities/jwt-auth-gateway.md b/docs/capabilities/jwt-auth-gateway.md new file mode 100644 index 000000000..32b3e02ea --- /dev/null +++ b/docs/capabilities/jwt-auth-gateway.md @@ -0,0 +1,96 @@ +--- +name: jwt-auth-gateway +description: JWT 认证网关,集成 Spring Security 与 JJWT,支持 Token 创建、解析、Redis 有状态/无状态双模式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: com.codingapi.springboot:springboot-starter-security +symbols: + - JwtTokenGateway + - JWTTokenGatewayImpl + - JWTSecurityConfiguration + - SecurityJWTProperties + - Token + - TokenContext + - TokenGateway + - RedisTokenGateway + - RedisTokenGatewayImpl + - AuthenticationTokenFilter + - HttpSecurityConfigurer + - WebSecurityConfigurer + - LoginRequest + - LoginResponse +content_hash: 2dfa771d38597c44f2723f8f37c86531e625dc39fc7d352eae74844d43c949f3 +--- + +## 解决什么问题 + +Web 应用需要标准化的用户认证机制。本能力在 Spring Security 基础上封装了 JWT 认证体系: + +- **无状态认证(JWT 模式)**:Token 自包含用户信息,适合微服务架构 +- **有状态认证(Redis 模式)**:Token 存储在 Redis 中,支持主动注销和续期 +- **统一网关抽象**:`TokenGateway` 接口屏蔽 JWT/Redis 两种模式的差异 +- **Spring Security 集成**:自动配置 Filter 链,处理登录、登出、Token 校验 + +## 如何使用 + +### 配置 + +```properties +# 启用 JWT 无状态认证 +codingapi.security.jwt.enable=true +codingapi.security.jwt.secret-key=your-secret-key-at-least-32-chars +codingapi.security.jwt.valid-time=7200 + +# 或启用 Redis 有状态认证 +codingapi.security.redis.enable=true +codingapi.security.redis.valid-time=7200 +``` + +### 创建 Token + +```java +@Autowired +private TokenGateway tokenGateway; + +// 登录成功后创建 Token +Token token = tokenGateway.create(username, authorities); +// 或通过 JwtTokenGateway 创建 +Token token = jwtTokenGateway.create(username, iv, authorities, extra); +``` + +### 解析 Token + +```java +Token token = jwtTokenGateway.parser(jwtString); +String username = token.getUsername(); +List authorities = token.getAuthorities(); +``` + +### 配置免认证 URL + +```properties +codingapi.security.ignore-urls=/open/**,/#/**,/api/version +``` + +## 使用实例 + +```java +// 自定义登录处理 +@Service +public class LoginService { + @Autowired + private TokenGateway tokenGateway; + + public LoginResponse login(String username, String password) { + // 验证用户名密码... + UserDetails user = authenticate(username, password); + Token token = tokenGateway.create( + user.getUsername(), + user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority).toList() + ); + return new LoginResponse(token.getToken(), token.getValidTime()); + } +} +``` diff --git a/docs/capabilities/kryo.md b/docs/capabilities/kryo.md new file mode 100644 index 000000000..beebd2b66 --- /dev/null +++ b/docs/capabilities/kryo.md @@ -0,0 +1,62 @@ +--- +name: kryo +description: Kryo 高性能序列化库 — 用于工作流数据快照的深拷贝与持久化 +status: 已实现 +scope: 后端 +source: 框架:Kryo +import: com.esotericsoftware:kryo +framework_version: 5.6.2 +--- + +## 解决什么问题 + +工作流引擎在审批过程中需要保存业务数据的快照(`BindDataSnapshot`),以便后续审批人能看到提交时的数据状态。Kryo 提供了高效的二进制序列化方案: + +- **深拷贝**:将绑定数据深拷贝为快照,避免引用污染 +- **高性能**:比 Java 原生序列化快 10 倍以上 +- **紧凑**:序列化后的二进制数据体积小,适合数据库存储 +- **类型注册**:通过预注册类型优化序列化效率 + +## 如何使用 + +### 基本序列化 + +```java +Kryo kryo = new Kryo(); +kryo.setRegistrationRequired(false); + +// 序列化 +Output output = new Output(4096, -1); +kryo.writeObject(output, myObject); +byte[] bytes = output.toBytes(); + +// 反序列化 +Input input = new Input(bytes); +MyObject restored = kryo.readObject(input, MyObject.class); +``` + +### 深拷贝 + +```java +Kryo kryo = new Kryo(); +MyObject copy = kryo.copy(original); +``` + +### 本框架中的使用 + +工作流引擎的 `BindDataSnapshot` 使用 Kryo 保存审批时的业务数据快照: + +```java +// 提交审批时自动保存快照 +FlowResult result = flowService.submit(processId, opinion, operator); +// 内部:kryo.copy(bindData) → 快照存储 +``` + +## 使用实例 + +```java +// 工作流快照恢复 +FlowDetail detail = flowService.detail(processId); +IBindData snapshot = detail.getSnapshot(); +// snapshot 保存的是提交审批时的数据状态,而非当前最新数据 +``` diff --git a/docs/capabilities/locale-exception.md b/docs/capabilities/locale-exception.md deleted file mode 100644 index ee843ff5d..000000000 --- a/docs/capabilities/locale-exception.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -name: locale-exception -description: 支持 i18n 的业务异常体系,errCode + errMessage,通过 MessageSource 实现国际化 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 0c4299a1 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/LocaleMessageException.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/LocaleMessage.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/MessageContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/ExceptionConfiguration.java ---- - -## 解决什么问题 - -在企业级应用中,业务异常需要同时满足以下需求: - -1. **结构化错误信息**:异常需携带机器可读的错误码(errCode)和人类可读的错误消息(errMessage),便于前端根据 errCode 做差异化处理 -2. **国际化支持**:同一错误码在不同语言环境下返回对应语言的错误消息,而非硬编码中文字符串 -3. **参数化消息**:错误消息支持占位符(如 `{0}`、`{1}`),运行时动态填充具体值 -4. **与统一响应对接**:异常的 errCode/errMessage 可直接映射到框架的 `Response` DTO,由全局异常处理器统一返回 - -本能力基于 Spring `MessageSource` 构建了国际化异常体系,开发者只需在 `messages.properties` 中定义错误码与消息的映射关系,代码中通过错误码抛出异常即可自动解析当前 Locale 对应的消息。 - -## 如何使用 - -### 核心组件 - -#### LocaleMessageException — 国际化业务异常 - -```java -public class LocaleMessageException extends RuntimeException { - String getErrCode() // 错误码 - String getErrMessage() // 解析后的错误消息(当前 Locale) -} -``` - -提供多种构造方式: - -| 构造函数 | 说明 | -|---------|------| -| `LocaleMessageException(errCode)` | 仅错误码,从 MessageSource 解析消息 | -| `LocaleMessageException(errCode, args)` | 错误码 + 占位符参数 | -| `LocaleMessageException(errCode, errMessage)` | 错误码 + 自定义消息(不走 i18n) | -| `LocaleMessageException(errCode, args, cause)` | 错误码 + 参数 + 原始异常 | -| `LocaleMessageException.of(errCode, args...)` | 静态工厂方法(推荐) | - -#### MessageContext — 消息上下文(单例) - -内部持有 `LocaleMessage` 实例,提供 `getErrorMsg(errCode)` 和 `getErrorMsg(errCode, args)` 方法。`LocaleMessageException` 的无消息构造函数通过此上下文获取国际化消息。 - -#### LocaleMessage — 消息解析器 - -封装 Spring `MessageSource`,根据 `LocaleContextHolder.getLocale()` 自动获取当前请求的语言环境并解析消息。 - -### 自动配置 - -`ExceptionConfiguration` 自动注册 `LocaleMessage` Bean: - -```java -@Configuration -public class ExceptionConfiguration { - @Bean(initMethod = "init") - public LocaleMessage exceptionLocaleMessage(MessageSource messageSource) { - return new LocaleMessage(messageSource); - } -} -``` - -`init()` 方法将 `LocaleMessage` 注入到 `MessageContext` 单例中。只要 Spring 容器中存在 `MessageSource`(通常由 `messages.properties` 自动配置),即可直接使用。 - -### 消息资源文件 - -在 `src/main/resources/` 下创建消息资源文件: - -```properties -# messages.properties (默认/中文) -user.not.found=用户不存在 -order.amount.exceeded=订单金额超出限制,最大金额为 {0} 元 -account.locked=账户 {0} 已被锁定,请联系管理员 - -# messages_en.properties (英文) -user.not.found=User not found -order.amount.exceeded=Order amount exceeded, maximum is {0} yuan -account.locked=Account {0} has been locked, please contact administrator -``` - -## 使用实例 - -### 1. 基本用法 — 仅错误码 - -```java -// 从 messages.properties 中解析 "user.not.found" 对应的消息 -throw new LocaleMessageException("user.not.found"); -// 中文环境 → errMessage: "用户不存在" -// 英文环境 → errMessage: "User not found" -``` - -### 2. 带参数的消息 - -```java -// 使用静态工厂方法(推荐) -throw LocaleMessageException.of("order.amount.exceeded", 10000); -// → errMessage: "订单金额超出限制,最大金额为 10000 元" - -// 或使用构造函数 -throw new LocaleMessageException("account.locked", new Object[]{"admin@example.com"}); -// → errMessage: "账户 admin@example.com 已被锁定,请联系管理员" -``` - -### 3. 携带原始异常 - -```java -try { - userRepository.findById(userId); -} catch (DataAccessException e) { - throw new LocaleMessageException("user.not.found", e); -} -``` - -### 4. 自定义消息(跳过 i18n) - -```java -// 直接指定消息文本,不从 MessageSource 解析 -throw new LocaleMessageException("custom.error", "这是一条自定义错误消息"); -``` - -### 5. 与全局异常处理器配合 - -在全局异常处理器中捕获 `LocaleMessageException`,提取 errCode 和 errMessage 构建统一响应: - -```java -@RestControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(LocaleMessageException.class) - public Response handleLocaleException(LocaleMessageException e) { - return Response.buildFailure(e.getErrCode(), e.getErrMessage()); - } -} -``` - -返回的 JSON 响应: - -```json -{ - "success": false, - "errCode": "order.amount.exceeded", - "errMessage": "订单金额超出限制,最大金额为 10000 元" -} -``` - -### 6. Locale 切换 - -框架通过 `LocaleContextHolder` 获取当前 Locale,与 Spring MVC 的 `Accept-Language` 请求头或自定义 LocaleResolver 集成。无需额外配置,客户端发送不同的 `Accept-Language` 头即可获得对应语言的错误消息。 diff --git a/docs/capabilities/rest-client.md b/docs/capabilities/rest-client.md index 986f5412a..44a88b946 100644 --- a/docs/capabilities/rest-client.md +++ b/docs/capabilities/rest-client.md @@ -1,226 +1,86 @@ --- name: rest-client -description: 出站 HTTP 客户端框架,提供 HttpClient(底层 HTTP 封装)、RestClient(REST 高级客户端)、SessionClient(Cookie/Session 感知)、HttpRequest(请求构建器 + 代理支持) +description: REST HTTP 客户端封装,支持自动重试、代理配置、请求/响应拦截器与信任所有证书模式 status: 已实现 scope: 后端 source: 项目自有 -last_commit: dc5e04ef -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/HttpClient.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/RestClient.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/SessionClient.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/HttpRequest.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/Request.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/RestTemplateContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/param/IRestParam.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/param/RestParam.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/properties/HttpProxyProperties.java +import: com.codingapi.springboot:springboot-starter +symbols: + - RestClient + - HttpClient + - HttpRequest + - Request + - RestTemplateContext + - SessionClient + - IRestParam + - RestParam + - HttpProxyProperties + - TrustAnyHttpClientFactory +content_hash: 7c8d0b2743ca064f6b334c28b0fc2565f210b4898903f3cf030cf8613ff95854 --- ## 解决什么问题 -在微服务架构中,服务间调用、第三方 API 对接、爬虫数据采集等场景需要频繁发起出站 HTTP 请求。直接使用 Spring `RestTemplate` 存在以下痛点: +微服务之间或调用第三方 API 时,需要封装 HTTP 请求。Spring RestTemplate 原生使用不够便捷。本能力提供了更友好的 REST 客户端: -- **缺少重试机制**:网络抖动时直接抛异常,需要手动包装重试逻辑 -- **Session/Cookie 管理繁琐**:模拟登录等有状态交互需手动维护 Cookie -- **代理配置分散**:HTTP 代理设置需要在每处创建 RestTemplate 时重复配置 -- **参数构建不便**:GET 查询参数和 POST 表单/JSON 的参数组装代码冗长 - -本能力提供三层 HTTP 客户端抽象: - -- **HttpClient**:轻量级底层封装,适合单次无状态调用 -- **RestClient**:REST 高级客户端,内置 baseUrl 拼接、自动重试、默认 JSON Content-Type -- **SessionClient**:Cookie/Session 感知客户端,自动跟踪 Set-Cookie 响应头并回传,支持 302 重定向跟随 - -所有客户端共享 `RestTemplateContext` 单例(基于 Apache HttpComponents,信任所有证书),统一支持 HTTP 代理配置。 +- **自动重试**:`RestClient` 内置重试机制,失败后自动重试(默认5次) +- **请求拦截器**:支持请求前/响应后的钩子处理(如自动添加 Header、日志记录) +- **代理支持**:通过 `HttpProxyProperties` 配置 HTTP 代理 +- **信任所有证书**:`TrustAnyHttpClientFactory` 支持 HTTPS 免证书验证(开发环境) +- **参数构建器**:`RestParam` 链式构建请求参数 ## 如何使用 -### 核心类层次 - -``` -RestTemplateContext (单例, Apache HttpComponents) -└── HttpRequest (请求构建器, 代理支持, 拦截器钩子) - ├── HttpClient (基础 GET/POST) - ├── RestClient (baseUrl + 重试 + REST 风格) - └── SessionClient (Cookie 跟踪 + 重定向) -``` - -### HttpRequest — 请求构建器 - -底层请求引擎,封装 `RestTemplate.exchange()` 调用,提供两个扩展点: +### RestClient(带重试) ```java -// 请求拦截器:可修改 URL、Header、Body -interface IHttpRequestHandler { - String handler(HttpRequest client, String uri, HttpMethod method, - HttpHeaders headers, HttpEntity httpEntity); -} +// 简单使用 +RestClient client = new RestClient("https://api.example.com"); +String result = client.get("/users/1"); +String result = client.post("/users", jsonObject); -// 响应拦截器:可处理状态码、重定向、错误 -interface IHttpResponseHandler { - String handler(HttpRequest client, String uri, ResponseEntity response); -} +// 带自定义 Header +HttpHeaders headers = new HttpHeaders(); +headers.set("Authorization", "Bearer " + token); +String result = client.get("/protected", headers); ``` -默认行为:请求拦截器透传 URL;响应拦截器处理 200/404 返回 body,302 自动跟随重定向。连接超时固定为 3000ms。 - -### HttpClient — 基础客户端 +### HttpClient(无重试) ```java -// 无代理 HttpClient client = new HttpClient(); - -// 带代理 -HttpProxyProperties proxy = new HttpProxyProperties(); -proxy.setEnableProxy(true); -proxy.setProxyHost("proxy.example.com"); -proxy.setProxyPort(8080); -proxy.setProxyType(Proxy.Type.HTTP); -HttpClient client = new HttpClient(proxy); - -// 自定义请求/响应处理器 -HttpClient client = new HttpClient(proxy, requestHandler, responseHandler); -``` - -提供三个便捷方法: -- `post(url, headers, JSON)` — POST JSON 请求 -- `post(url, headers, MultiValueMap)` — POST 表单请求 -- `get(url, headers, uriVariables)` — GET 请求 - -### RestClient — REST 高级客户端 - -```java -// 最简构造:指定 baseUrl,默认重试 5 次,空响应 "{}" -RestClient rest = new RestClient("https://api.example.com"); - -// 完整构造 -RestClient rest = new RestClient(proxyProperties, baseUrl, retryCount, - emptyResponse, requestHandler, responseHandler); -``` - -特性: -- **baseUrl 自动拼接**:`rest.get("/users")` → `https://api.example.com/users` -- **自动重试**:请求失败时按 `retryCount` 重试,每次间隔 1 秒 -- **默认 JSON**:初始化时自动设置 `Content-Type: application/json` -- **优雅降级**:重试耗尽后返回 `emptyResponse` 而非抛异常 - -### SessionClient — Session 感知客户端 - -```java -SessionClient session = new SessionClient(); // 无代理 -SessionClient session = new SessionClient(proxyProperties); // 带代理 - -// 链式添加 Header -session.addHeader("Authorization", "Bearer xxx"); - -// 表单提交(自动设置 APPLICATION_FORM_URLENCODED) -session.postForm("https://example.com/login", restParam); - -// JSON 提交 -session.postJson("https://example.com/api/data", restParam); - -// GET 请求 -session.getJson("https://example.com/api/users"); -session.getHtml("https://example.com/page"); +String result = client.post("https://api.example.com/data", headers, jsonObject); +String result = client.get("https://api.example.com/data", headers, params); ``` -内部通过自定义 `IHttpResponseHandler` 自动将响应中的 `Set-Cookie` 转为后续请求的 `Cookie` 头,并在遇到 302 时自动跟随重定向。 - -### RestParam — 统一参数构建器 +### RestTemplateContext(单例) ```java -// 手动构建 -RestParam param = RestParam.create() - .add("username", "admin") - .add("password", "secret"); - -// 从对象解析 -RestParam param = RestParam.parser(myObject); - -// 转换为不同格式 -JSONObject json = param.toJsonRequest(); // POST JSON -MultiValueMap form = param.toFormRequest(); // POST 表单 -MultiValueMap query = param.toGetRequest(); // GET 参数 +// 全局共享的 RestTemplate(信任所有证书) +RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); ``` -实现 `IRestParam` 接口的 POJO 可直接调用 `toParameters()` 转为 `RestParam`。 - -### HttpProxyProperties — 代理配置 +### 请求/响应拦截器 ```java -@Setter @Getter -public class HttpProxyProperties { - private boolean enableProxy; - private String proxyHost; - private int proxyPort; - private Proxy.Type proxyType; -} +RestClient client = new RestClient( + proxyProperties, + "https://api.example.com", + 5, // 重试次数 + "{}", // 失败时的默认响应 + request -> { /* 请求前处理 */ }, + response -> { /* 响应后处理 */ } +); ``` ## 使用实例 -### 1. 调用第三方 REST API(带重试) - ```java -RestClient api = new RestClient("https://api.third-party.com"); - -// GET 请求 -String users = api.get("/v1/users"); - -// POST JSON 请求 -RestParam param = RestParam.create() - .add("name", "张三") - .add("age", 25); -String result = api.post("/v1/users", param); - -// 带自定义 Header -HttpHeaders headers = new HttpHeaders(); -headers.set("X-API-Key", "my-key"); -String data = api.get("/v1/data", headers); -``` - -### 2. 模拟登录并保持会话 - -```java -SessionClient session = new SessionClient(); - -// 登录(表单提交,自动保存 Cookie) -RestParam loginParam = RestParam.create() - .add("username", "admin") - .add("password", "password123"); -session.postForm("https://app.example.com/login", loginParam); - -// 后续请求自动携带 Cookie -String dashboard = session.getHtml("https://app.example.com/dashboard"); -String userInfo = session.getJson("https://app.example.com/api/user/info"); -``` - -### 3. 通过代理访问外部服务 - -```java -HttpProxyProperties proxy = new HttpProxyProperties(); -proxy.setEnableProxy(true); -proxy.setProxyHost("proxy.corp.com"); -proxy.setProxyPort(3128); -proxy.setProxyType(Proxy.Type.HTTP); - -RestClient client = new RestClient(proxy, "https://external-api.com", - 3, "{}", null, null); -String response = client.get("/data"); -``` - -### 4. 自定义请求/响应拦截 - -```java -// 请求签名拦截器 -HttpRequest.IHttpRequestHandler signer = (client, uri, method, headers, entity) -> { - String timestamp = String.valueOf(System.currentTimeMillis()); - headers.set("X-Timestamp", timestamp); - headers.set("X-Signature", HmacSHA256.sign(uri + timestamp, secretKey)); - return uri; -}; - -HttpClient httpClient = new HttpClient(signer, null); -String result = httpClient.get("https://secure-api.com/data", headers, null); +// 调用外部 API +RestClient client = new RestClient("https://api.weather.com"); +RestParam params = new RestParam() + .add("city", "北京") + .add("unit", "celsius"); +String weather = client.get("/forecast", params); +JSONObject data = JSON.parseObject(weather); ``` diff --git a/docs/capabilities/spring-data-jpa.md b/docs/capabilities/spring-data-jpa.md index 9463ee2c3..c1c68ff88 100644 --- a/docs/capabilities/spring-data-jpa.md +++ b/docs/capabilities/spring-data-jpa.md @@ -1,106 +1,68 @@ --- name: spring-data-jpa -description: Spring Data JPA 数据访问能力,提供 ORM 映射、Repository 抽象、分页查询等基础设施,框架在此基础上扩展了 FastRepository 动态查询 +description: Spring Data JPA — ORM 映射、Repository 抽象、分页查询、Specification 动态查询 status: 已实现 scope: 后端 -source: 框架:spring-data-jpa -framework_version: Managed by Spring Boot BOM (3.3.5) +source: 框架:Spring Data JPA +import: org.springframework.boot:spring-boot-starter-data-jpa +framework_version: (由 Spring Boot 3.3.5 管理) --- ## 解决什么问题 -Spring Data JPA 是框架数据持久层的基石,解决了以下问题: +Spring Data JPA 是项目持久层的标准基础: -- **ORM 映射**:通过 JPA 注解将领域实体映射到关系数据库表,框架中所有领域对象(如 `FlowWork`、`FlowRecord`、`FlowNode` 等)均基于 JPA Entity 定义 -- **Repository 抽象**:提供 `JpaRepository` 接口,自动生成 CRUD 实现,消除样板代码 -- **分页查询**:原生支持 `Pageable` / `Page` 分页抽象,框架在此基础上扩展了 `PageRequest` + `RequestFilter` 动态过滤能力 - -框架的 `springboot-starter-data-fast` 模块在 Spring Data JPA 之上构建了 `FastRepository`,支持基于 Filter 的动态 Example 查询和 HQL 构建,大幅简化复杂条件查询的开发。 +- **ORM 映射**:JPA 注解定义实体与表的映射关系 +- **Repository 抽象**:`JpaRepository` 提供标准 CRUD 操作 +- **分页查询**:`Pageable` / `Page` 标准化分页接口 +- **Specification**:`JpaSpecificationExecutor` 支持动态条件查询 +- **命名查询**:通过方法名自动推导 SQL(`findByUsername`) ## 如何使用 -### 依赖引入 - -```xml - - com.codingapi.springboot - springboot-starter-data-fast - -``` - -该模块已传递依赖 `spring-boot-starter-data-jpa`,无需单独引入。 - -### Repository 定义 - -继承 `FastRepository` 而非原生的 `JpaRepository`,即可获得动态过滤查询能力: +### 基础 Repository ```java -public interface UserRepository extends FastRepository { - // 自动拥有 findAll(PageRequest)、pageRequest(PageRequest)、searchRequest(SearchRequest) 等方法 +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + List findByStatus(String status); } ``` -### 动态过滤查询 +### 与本框架集成 -`PageRequest` 扩展了 Spring Data 的 `org.springframework.data.domain.PageRequest`,增加了 `RequestFilter`: +本框架的 `FastRepository` 扩展了 Spring Data JPA: ```java -PageRequest request = PageRequest.of(0, 20); -request.addFilter("name", "张三"); -request.addFilter("age", Relation.GT, 18); -Page page = userRepository.findAll(request); +// FastRepository 继承 JpaRepository + JpaSpecificationExecutor +public interface UserRepository extends FastRepository { + // 继承 findAll(PageRequest) — 自动构建 Example/HQL +} ``` -当 Filter 存在时,`findAll` 自动构建 Example 查询;`pageRequest` 方法则构建 HQL 动态 SQL,支持更复杂的关联查询。 - -## 使用实例 - -### 基础 CRUD + 动态过滤 +### 分页查询 ```java -@Service -@AllArgsConstructor -public class UserQueryService { - - private final UserRepository userRepository; - - // 简单分页(无过滤) - public Page listUsers(int page, int size) { - return userRepository.findAll(PageRequest.of(page, size)); - } - - // 带动态过滤的分页查询 - public Page searchUsers(String name, Integer minAge) { - PageRequest request = PageRequest.of(0, 20); - if (name != null) { - request.addFilter("name", name); - } - if (minAge != null) { - request.addFilter("age", Relation.GTE, minAge); - } - return userRepository.findAll(request); - } - - // 使用 HQL 动态查询(支持关联字段、OR 条件等) - public Page advancedSearch(SearchRequest searchRequest) { - return userRepository.searchRequest(searchRequest); - } -} +Page page = userRepository.findAll( + org.springframework.data.domain.PageRequest.of(0, 20, Sort.by("id")) +); ``` -### JPA Entity 定义 +## 使用实例 ```java @Entity -@Table(name = "t_user") -@Getter @Setter -public class UserEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) +@Table(name = "sys_user") +public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - private String name; - private Integer age; + private String username; private String email; + private String status; } + +// 标准 CRUD +userRepository.save(user); +userRepository.findById(id); +userRepository.delete(user); ``` diff --git a/docs/capabilities/spring-data-redis.md b/docs/capabilities/spring-data-redis.md deleted file mode 100644 index bc77f3d0c..000000000 --- a/docs/capabilities/spring-data-redis.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -name: spring-data-redis -description: Spring Data Redis 数据存储能力,框架中主要用于 Token/Session 的有状态存储,支持服务端主动失效和多端登录管理 -status: 已实现 -scope: 后端 -source: 框架:spring-data-redis -framework_version: Managed by Spring Boot BOM (3.3.5) ---- - -## 解决什么问题 - -Spring Data Redis 在框架中承担了有状态认证场景下的核心存储职责,解决了以下问题: - -- **Token 服务端存储**:`RedisTokenGateway` 使用 `RedisTemplate` 将 Token 以 JSON 格式存入 Redis,Key 格式为 `{encodedUsername}:{uuid}`,支持按用户名批量查询和删除 -- **Token 生命周期管理**:通过 Redis TTL 机制自动过期,同时支持服务端主动调用 `removeToken()` / `resetToken()` 实现即时失效和续期 -- **多端登录管理**:`getTokensByUsername()` 获取用户所有在线 Token,`removeUsername()` 一键踢出所有设备,还支持按条件(`Predicate`)选择性踢出 -- **缓存抽象**:可作为通用缓存层,用于脚本编译结果缓存、会话数据暂存等场景 - -## 如何使用 - -### 依赖引入 - -```xml - - com.codingapi.springboot - springboot-starter-security - -``` - -`spring-boot-starter-data-redis` 作为 `provided` 依赖包含在安全模块中,启用 Redis 认证模式时需确保运行时存在 Redis 连接配置。 - -### 配置 Redis 连接 - -```properties -# Redis 连接配置 -spring.data.redis.host=localhost -spring.data.redis.port=6379 -spring.data.redis.password=your-password - -# 启用 Redis 有状态认证 -codingapi.security.redis.enable=true -codingapi.security.redis.valid-time=86400000 # Token 有效期(毫秒) -codingapi.security.redis.rest-time=3600000 # 刷新提醒时间(毫秒) -``` - -### RedisTokenGateway 核心 API - -框架封装了 `RedisTokenGateway`,提供面向业务的 Token 操作方法: - -| 方法 | 说明 | -|------|------| -| `create(username, iv, authorities, extra)` | 创建 Token 并存入 Redis | -| `getToken(token)` | 根据 Token 字符串获取用户信息 | -| `removeToken(token)` | 删除指定 Token | -| `resetToken(token)` | 重置 Token 有效期 | -| `removeUsername(username)` | 删除用户所有 Token(踢出所有设备) | -| `getTokensByUsername(username)` | 获取用户所有在线 Token Key | -| `removeUsername(username, predicate)` | 按条件选择性删除 Token | - -## 使用实例 - -### 管理员强制下线用户 - -```java -@Service -@AllArgsConstructor -public class UserAdminService { - - private final RedisTokenGateway redisTokenGateway; - - /** - * 强制下线指定用户的所有设备 - */ - public void forceLogout(String username) { - redisTokenGateway.removeUsername(username); - } - - /** - * 选择性下线:仅移除特定设备的 Token - */ - public void logoutDevice(String username, String targetToken) { - redisTokenGateway.removeUsername(username, token -> - targetToken.equals(token.getToken()) - ); - } - - /** - * 查看用户在线设备数 - */ - public int getOnlineDeviceCount(String username) { - return redisTokenGateway.getTokensByUsername(username).size(); - } -} -``` - -### Token 续期 - -```java -// 在认证过滤器中检测 Token 是否需要续期 -Token token = tokenGateway.parser(tokenString); -if (token != null && token.canRestToken()) { - // canRestToken() = !isExpire() && remindTime <= currentTimeMillis - redisTokenGateway.resetToken(token); -} -``` diff --git a/docs/capabilities/spring-framework.md b/docs/capabilities/spring-framework.md index 566c8e834..7388d1491 100644 --- a/docs/capabilities/spring-framework.md +++ b/docs/capabilities/spring-framework.md @@ -1,110 +1,65 @@ --- name: spring-framework -description: Spring Framework / Spring Boot 核心基础能力,提供 IoC 容器、事件发布、AOP、Web MVC、事务管理等基础设施 +description: Spring Framework / Spring Boot 核心能力 — IoC 容器、自动配置、事件发布、AOP、Web MVC status: 已实现 scope: 后端 -source: 框架:spring-framework +source: 框架:Spring Boot +import: org.springframework.boot:spring-boot-starter framework_version: 3.3.5 --- ## 解决什么问题 -Spring Framework / Spring Boot 是整个框架的运行时基座,解决了以下核心问题: +Spring Boot 作为项目的基础框架,提供以下核心能力: -- **IoC 容器**:管理所有 Bean 的生命周期与依赖注入,框架中所有 Starter 模块的自动配置均基于 Spring Boot AutoConfiguration 机制(`spring.factories` + `AutoConfiguration.imports`) -- **事件发布(ApplicationEventPublisher)**:框架自建的事件系统(`DomainEvent`、`IHandler`)底层通过 Spring 的 `@EventListener` 桥接,由 `SpringDefaultEventHandler` / `SpringTransactionEventHandler` 订阅 `DomainEvent` 并分发到业务 Handler -- **AOP**:支持声明式事务(`@Transactional`)、数据权限 SQL 拦截等切面能力 -- **Web MVC**:统一 REST API 响应封装(`Response` / `SingleResponse` / `MultiResponse`)、全局异常处理、参数绑定 -- **事务管理**:工作流引擎、领域事件持久化等场景均依赖 Spring 声明式事务 +- **IoC 容器**:依赖注入(DI)管理所有 Bean 的生命周期 +- **自动配置**:`@SpringBootApplication` 自动装配 Starter 组件 +- **事件发布**:`ApplicationEventPublisher` 发布 Spring 原生事件 +- **AOP 切面**:声明式事务(`@Transactional`)、日志切面等 +- **Web MVC**:REST 控制器、参数绑定、拦截器、异常处理 +- **条件装配**:`@ConditionalOnClass` / `@ConditionalOnProperty` 按需加载 ## 如何使用 -### 依赖引入 +### 自动配置注册 -框架根 POM 继承自 `spring-boot-starter-parent:3.3.5`,所有子模块自动获得 Spring Boot 依赖管理。 +各 Starter 模块通过以下文件注册自动配置: +- `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`(Spring Boot 3.x) +- `META-INF/spring.factories`(兼容旧版) -```xml - - - com.codingapi.springboot - springboot-starter - -``` - -### 事件系统桥接 - -框架通过 `SpringEventConfiguration` 注册 `SpringEventInitializer`,在容器启动时将 `ApplicationContext` 注入 `DomainEventContext`,使 `EventPusher.push()` 能够发布 Spring `DomainEvent`: +### Spring 事件集成 +本框架的事件系统底层桥接了 Spring 的 `ApplicationEventPublisher`: ```java -// 框架内部自动配置,无需手动操作 -@Bean -public SpringEventInitializer springEventInitializer(ApplicationContext context) { - return new SpringEventInitializer(context); -} +// DomainEventContext 内部使用 Spring 事件发布 +context.publishEvent(new DomainEvent(event, sync, traceId)); ``` -业务侧只需实现 `IHandler` 接口即可订阅事件,Handler 由 `HandlerBeanDefinitionRegistrar` 自动扫描注册。 - -### 事务管理 - -使用标准 Spring `@Transactional` 注解: +### 配置属性 -```java -@Transactional -public void startFlow(...) { ... } +```properties +# Spring Boot 基础配置 +server.port=8090 +spring.application.name=my-app ``` -可通过 `codingapi.framework.event.transaction.enable=true` 启用事务内事件处理模式(`SpringTransactionEventHandler`),确保事件在事务提交后触发。 - ## 使用实例 -### 定义并订阅领域事件 - ```java -// 1. 定义事件 -public class OrderCreatedEvent implements ISyncEvent { - private final String orderId; - // constructor, getters... -} - -// 2. 实现处理器(自动注册为 Spring Bean) -@Component -public class OrderCreatedHandler implements IHandler { - @Override - public int order() { return 0; } - - @Override - public void handler(OrderCreatedEvent event) { - // 处理订单创建逻辑 - } - - @Override - public void error(OrderCreatedEvent event, Exception exception) { - // 异常回调 +// 自定义自动配置 +@Configuration +@ConditionalOnClass(name = "org.springframework.web.servlet.HandlerExceptionResolver") +public class BasicHandlerExceptionResolverConfiguration { + @Bean + public HandlerExceptionResolver servletExceptionHandler() { + return new ServletExceptionHandler(); } } -// 3. 发布事件 -EventPusher.push(new OrderCreatedEvent("ORD-001"), true); -``` - -### 统一响应封装 - -```java -@RestController -@RequestMapping("/api/users") -public class UserController { - - @GetMapping("/{id}") - public SingleResponse getUser(@PathVariable Long id) { - UserDTO user = userService.findById(id); - return SingleResponse.of(user); - } - - @GetMapping - public MultiResponse listUsers(PageRequest request) { - Page page = userService.findAll(request); - return MultiResponse.of(page.getContent(), page.getTotalElements()); - } +// 声明式事务 +@Transactional +public void createUser(User user) { + userRepository.save(user); + EventPusher.push(new UserCreatedEvent(user.getId())); } ``` diff --git a/docs/capabilities/spring-security.md b/docs/capabilities/spring-security.md index 407c72f47..a93d02a4e 100644 --- a/docs/capabilities/spring-security.md +++ b/docs/capabilities/spring-security.md @@ -1,109 +1,71 @@ --- name: spring-security -description: Spring Security 安全认证与授权能力,框架在此基础上封装了 JWT 无状态认证和 Redis 有状态认证两种模式 +description: Spring Security 认证与授权框架 — 提供 Filter 链、CSRF 防护、密码编码、权限控制 status: 已实现 scope: 后端 -source: 框架:spring-security -framework_version: Managed by Spring Boot BOM (3.3.5) +source: 框架:Spring Security +import: org.springframework.boot:spring-boot-starter-security +framework_version: (由 Spring Boot 3.3.5 管理) --- ## 解决什么问题 -Spring Security 为框架提供了完整的安全基础设施,解决了以下问题: +Spring Security 为 Web 应用提供完整的安全基础设施: -- **认证(Authentication)**:框架的 `springboot-starter-security` 模块基于 Spring Security 过滤器链,实现了两种 Token 认证模式: - - **JWT 无状态模式**:通过 `JwtTokenGateway` + JJWT 库签发/验证 JWT Token,适用于分布式部署 - - **Redis 有状态模式**:通过 `RedisTokenGateway` + `RedisTemplate` 将 Token 存储在 Redis 中,支持服务端主动失效、单点登录等场景 -- **授权(Authorization)**:基于 Spring Security 的权限模型,`Token.authorities` 携带用户角色/权限列表,通过 `UsernamePasswordAuthenticationToken` 注入 SecurityContext -- **CSRF 防护**:REST API 场景下默认禁用 CSRF(无状态 Token 认证不需要),表单场景可按需启用 -- **统一安全过滤链**:`MyAuthenticationFilter` 拦截请求解析 Token,`MyLoginFilter` 处理登录请求生成 Token,`SecurityLoginHandler` 统一登录成功/失败响应 +- **Filter 链**:请求级别的认证与授权过滤 +- **CSRF 防护**:跨站请求伪造保护(可通过配置关闭) +- **密码编码**:`PasswordEncoder` 安全存储密码 +- **会话管理**:有状态(Session)与无状态(JWT)两种模式 +- **权限注解**:`@PreAuthorize` / `@Secured` 方法级权限控制 ## 如何使用 -### 依赖引入 +### 与本框架集成 -```xml - - com.codingapi.springboot - springboot-starter-security - -``` - -### 配置认证模式 +本框架的 `springboot-starter-security` 模块已封装 Spring Security 配置: -在 `application.properties` 中选择认证方式: +```java +// HttpSecurityConfigurer — 自定义 Security 配置 +public interface HttpSecurityCustomer { + void customer(HttpSecurity http) throws Exception; +} -```properties -# JWT 无状态认证 -codingapi.security.jwt.enable=true -codingapi.security.jwt.secret-key=your-secret-key-at-least-256-bits -codingapi.security.jwt.valid-time=86400000 # Token 有效期(毫秒) -codingapi.security.jwt.rest-time=3600000 # Token 刷新提醒时间(毫秒) +// 注入自定义配置 +@Bean +public HttpSecurityCustomer customSecurity() { + return http -> http.authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ); +} +``` -# Redis 有状态认证(二选一) -codingapi.security.redis.enable=true -codingapi.security.redis.valid-time=86400000 -codingapi.security.redis.rest-time=3600000 +### 免认证 URL -# 免认证 URL 列表 -codingapi.security.ignore-urls=/open/**,/#/**,/api/login +```properties +codingapi.security.ignore-urls=/open/**,/#/**,/api/version ``` -### TokenGateway 接口 - -两种模式均实现统一的 `TokenGateway` 接口,业务代码无需关心底层存储: +### 权限注解 ```java -public interface TokenGateway { - Token create(String username, String iv, List authorities, String extra); - Token parser(String sign); -} +@PreAuthorize("hasRole('ADMIN')") +public void deleteUser(Long id) { ... } ``` ## 使用实例 -### 自定义登录逻辑 - ```java +// 自定义 UserDetailsService @Service -@AllArgsConstructor -public class LoginService { - - private final TokenGateway tokenGateway; - - public Token login(String username, String password) { - // 1. 验证用户名密码(自行实现) - UserDetails user = authenticate(username, password); - - // 2. 生成 Token(自动根据配置选择 JWT 或 Redis 模式) - return tokenGateway.create( - user.getUsername(), - null, // iv(AES 加密向量,可选) - user.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()), - "{\"userId\":1}" // extra 扩展信息 +public class UserDetailsServiceImpl implements UserDetailsService { + @Override + public UserDetails loadUserByUsername(String username) { + User user = userService.findByUsername(username); + return new org.springframework.security.core.userdetails.User( + user.getUsername(), user.getPassword(), + user.getAuthorities() ); } } ``` - -### 获取当前用户信息 - -```java -@RestController -public class ProfileController { - - @GetMapping("/api/profile") - public SingleResponse> profile() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - Token token = (Token) auth.getPrincipal(); - - Map info = new HashMap<>(); - info.put("username", token.getUsername()); - info.put("authorities", token.getAuthorities()); - info.put("extra", token.parseExtra(Map.class)); - return SingleResponse.of(info); - } -} -``` diff --git a/docs/capabilities/sql-interceptor.md b/docs/capabilities/sql-interceptor.md deleted file mode 100644 index 70d9a7c57..000000000 --- a/docs/capabilities/sql-interceptor.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -name: sql-interceptor -description: 基于 JSqlParser 的 SQL 解析改写拦截器,在 SQL 执行前透明注入数据权限条件 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 67895fda -code_files: - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLInterceptor.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLRunningContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLInterceptorContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/DefaultSQLInterceptor.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/register/SQLInterceptorRegister.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DataAuthorizationFilter.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/filter/DefaultDataAuthorizationFilter.java ---- - -## 解决什么问题 - -在企业级多租户或行级数据权限场景中,业务查询需要根据当前用户的角色、部门、数据范围等条件自动追加 WHERE 过滤条件。如果由每个开发者手动拼接权限 SQL,容易出现遗漏、不一致和安全漏洞。 - -本能力通过 JDBC Connection/Statement 代理层拦截所有 SQL 执行,利用 JSqlParser 解析 SQL AST,在不修改任何业务代码的前提下,自动向 SELECT 语句注入行级和列级数据权限条件。核心优势: - -- **零侵入**:业务代码无需感知权限逻辑,SQL 改写对上层完全透明 -- **可扩展**:通过 `SQLInterceptor` 接口自定义拦截策略,通过 `DataAuthorizationFilter` 定义具体的权限规则 -- **防递归**:拦截器内部执行的查询自动跳过拦截,避免无限循环 -- **支持复杂 SQL**:处理子查询、JOIN、UNION、IN 子句中的嵌套 SELECT 等场景 - -## 如何使用 - -### 核心接口 - -#### SQLInterceptor — SQL 拦截器接口 - -```java -public interface SQLInterceptor { - // 前置判断:是否需要拦截该 SQL(默认仅拦截查询语句) - boolean beforeHandler(String sql); - - // 核心处理:解析并改写 SQL,返回 DataPermissionSQL(含新 SQL 和表别名上下文) - DataPermissionSQL postHandler(String sql) throws SQLException; - - // 后置回调:用于日志记录或异常处理 - void afterHandler(String sql, String newSql, SQLException exception); -} -``` - -#### DataAuthorizationFilter — 数据权限过滤器接口 - -```java -public interface DataAuthorizationFilter { - // 行级权限:返回需要追加到 WHERE/JOIN 的条件 - Condition rowAuthorization(String tableName, String tableAlias); - - // 列级权限:对查询结果中的特定列值进行脱敏/过滤 - T columnAuthorization(String tableName, String columnName, T value); - - // 是否支持对该表/列进行权限过滤 - boolean supportRowAuthorization(String tableName, String tableAlias); - boolean supportColumnAuthorization(String tableName, String columnName, Object value); -} -``` - -#### SQLRunningContext — SQL 执行上下文(单例) - -```java -// 获取单例 -SQLRunningContext.getInstance() - -// 拦截 SQL 并返回执行状态(包含改写后的 SQL) -SQLExecuteState intercept(String sql) - -// 临时跳过数据权限拦截执行代码块 - T skipDataAuthorization(Supplier supplier) -void skipDataAuthorization(Runnable runnable) -``` - -### 自定义拦截器注册 - -实现 `SQLInterceptor` 接口后,通过 Spring Bean 方式注册: - -```java -@Bean -public SQLInterceptorRegister sqlInterceptorRegister(MyCustomSQLInterceptor interceptor) { - return new SQLInterceptorRegister(interceptor); -} -``` - -`SQLInterceptorRegister` 会在构造时将自定义拦截器设置到 `SQLInterceptorContext` 中,替换默认的 `DefaultSQLInterceptor`。 - -### 自定义数据权限过滤器 - -实现 `DataAuthorizationFilter` 接口,并通过 `DataAuthorizationContext.getInstance().addDataAuthorizationFilter(filter)` 注册。可注册多个过滤器,按注册顺序依次匹配。 - -### 配置项 - -在 `application.properties` 中开启 SQL 日志输出: - -```properties -# 开启后会在 afterHandler 中打印改写后的 SQL -codingapi.authorization.show-sql=true -``` - -## 使用实例 - -### 1. 实现行级数据权限过滤器 - -```java -@Component -public class DepartmentDataFilter implements DataAuthorizationFilter { - - @Override - public boolean supportRowAuthorization(String tableName, String tableAlias) { - // 仅对 employee 表生效 - return "employee".equalsIgnoreCase(tableName); - } - - @Override - public Condition rowAuthorization(String tableName, String tableAlias) { - // 根据当前用户部门追加过滤条件 - Long deptId = SecurityContext.getCurrentDeptId(); - return Condition.formatCondition("%s.dept_id = %d", tableAlias, deptId); - } - - @Override - public boolean supportColumnAuthorization(String tableName, String columnName, Object value) { - return false; - } - - @Override - public T columnAuthorization(String tableName, String columnName, T value) { - return value; - } -} -``` - -### 2. 临时跳过数据权限拦截 - -```java -// 在管理后台导出全量数据时,跳过行级权限 -List allEmployees = SQLRunningContext.getInstance() - .skipDataAuthorization(() -> employeeRepository.findAll()); -``` - -### 3. 自定义 SQL 拦截器 - -```java -@Component -public class AuditSQLInterceptor implements SQLInterceptor { - - @Override - public boolean beforeHandler(String sql) { - // 仅拦截包含敏感表的查询 - return SQLUtils.isQuerySql(sql) && sql.contains("salary"); - } - - @Override - public DataPermissionSQL postHandler(String sql) throws SQLException { - RowHandler rowHandler = RowHandlerContext.getInstance().getRowHandler(); - DataPermissionSQLEnhancer enhancer = new DataPermissionSQLEnhancer(sql, rowHandler); - return new DataPermissionSQL(sql, enhancer.getNewSQL(), enhancer.getTableAlias()); - } - - @Override - public void afterHandler(String sql, String newSql, SQLException exception) { - if (exception != null) { - log.error("SQL 改写失败: {}", sql, exception); - } else { - auditLog.record(sql, newSql); - } - } -} -``` - -### 4. 拦截流程说明 - -``` -原始 SQL → SQLRunningContext.intercept() - → SQLInterceptor.beforeHandler() // 判断是否需要拦截 - → SQLInterceptor.postHandler() // JSqlParser 解析 + 注入权限条件 - → SQLInterceptor.afterHandler() // 日志/审计回调 - → 返回 SQLExecuteState(含改写后的 SQL) - → JDBC 代理使用改写后的 SQL 执行查询 -``` - -拦截器内部的查询操作会自动设置 `skipInterceptor=true`,防止递归拦截。执行完毕后自动重置状态。 diff --git a/docs/capabilities/token-gateway.md b/docs/capabilities/token-gateway.md deleted file mode 100644 index aa2ae7bd5..000000000 --- a/docs/capabilities/token-gateway.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -name: token-gateway -description: JWT/Redis 双模式 Token 认证网关,统一创建、解析和管理认证令牌 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 327e5420 -code_files: - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/TokenGateway.java - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/Token.java - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/jwt/JwtTokenGateway.java - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/redis/RedisTokenGateway.java - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/TokenContext.java ---- - -## 解决什么问题 - -在多端登录、无状态/有状态认证场景中,业务层需要一个统一的 Token 抽象来屏蔽底层存储差异。`TokenGateway` 提供了统一的令牌创建、解析和生命周期管理接口,支持: - -- **JWT 无状态模式**:Token 信息自包含在签名载荷中,适合微服务和分布式场景 -- **Redis 有状态模式**:Token 存储在 Redis 中,支持多端登录管理、强制下线、按需踢出 - -## 如何使用 - -### 核心接口 - -```java -public interface TokenGateway { - // 创建令牌(用户名 + IV加密密钥 + 权限列表 + 扩展信息) - Token create(String username, String iv, List authorities, String extra); - - // 解析令牌,返回 Token 对象 - Token parser(String sign); -} -``` - -### Token 对象 - -`Token` 包含以下字段: -- `username` — 用户名 -- `token` — 令牌字符串(JWT 签名或 Redis Key) -- `authorities` — 权限列表 -- `extra` — 扩展信息(JSON 字符串,可通过 `parseExtra(Class)` 反序列化) -- `iv` — AES 加密密钥(经 AESTools 编码) -- `expireTime` — 过期时间戳 -- `remindTime` — 续期提醒时间戳 - -关键方法: -- `token.verify()` — 校验是否过期,过期抛出 `TokenExpiredException` -- `token.canRestToken()` — 判断是否需要续期(未过期且已过提醒时间) -- `token.getAuthenticationToken()` — 转换为 Spring Security 的 `UsernamePasswordAuthenticationToken` - -### JWT 模式(JwtTokenGateway) - -通过 HMAC-SHA 签名生成自包含 JWT,配置项: -```properties -codingapi.security.jwt.secret-key=your-secret-key -codingapi.security.jwt.valid-time=3600000 # 有效期(毫秒) -codingapi.security.jwt.rest-time=1800000 # 续期提醒时间(毫秒) -``` - -### Redis 模式(RedisTokenGateway) - -Token 以 `AES(用户名):UUID` 为 Key 存储在 Redis 中,支持: -- `getToken(token)` — 根据令牌获取用户信息 -- `removeToken(token)` — 删除单个令牌 -- `removeUsername(username)` — 删除用户所有令牌(强制下线) -- `removeUsername(username, Predicate)` — 按条件选择性踢出 -- `getTokensByUsername(username)` — 获取用户所有令牌(多端管理) -- `resetToken(token)` — 续期令牌 - -```properties -codingapi.security.redis.valid-time=3600000 -codingapi.security.redis.rest-time=1800000 -``` - -## 使用实例 - -```java -// JWT 模式 — 创建并解析令牌 -JwtTokenGateway jwtGateway = new JwtTokenGateway(jwtProperties); -Token token = jwtGateway.create("admin", Arrays.asList("ROLE_USER", "ROLE_ADMIN")); -System.out.println(token.getToken()); // eyJhbGciOiJIUz... - -Token parsed = jwtGateway.parser(token.getToken()); -parsed.verify(); // 校验过期时间 -System.out.println(parsed.getUsername()); // "admin" - -// Redis 模式 — 多端登录管理 -RedisTokenGateway redisGateway = new RedisTokenGateway(redisTemplate, redisProperties); -Token token1 = redisGateway.create("user1", "iv123", Arrays.asList("ROLE_USER"), null); - -// 获取用户所有在线会话 -List tokens = redisGateway.getTokensByUsername("user1"); - -// 按条件踢出(如踢出非当前设备的会话) -redisGateway.removeUsername("user1", t -> !t.getToken().equals(currentToken)); - -// 强制下线 -redisGateway.removeUsername("user1"); -``` diff --git a/docs/capabilities/transaction-manager-context.md b/docs/capabilities/transaction-manager-context.md deleted file mode 100644 index f1e5be8c9..000000000 --- a/docs/capabilities/transaction-manager-context.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: transaction-manager-context -description: 单例编程式事务上下文,支持 REQUIRES_NEW 语义,在非 Spring 管理上下文中也能使用事务 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 398bf849 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextConfiguration.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextRegister.java ---- - -## 解决什么问题 - -在以下场景中,标准的 Spring `@Transactional` 声明式事务无法满足需求: - -1. **非 Spring 管理的代码**:工具类、静态方法、回调函数中无法使用 `@Transactional` 注解 -2. **需要独立新事务**:当前已处于一个事务中,但某段逻辑需要在独立的新事务中执行(`REQUIRES_NEW`),且不受外层事务回滚影响 -3. **动态事务控制**:需要根据运行时条件决定是否开启事务、使用只读事务还是读写事务 -4. **框架内部基础设施**:事件处理器、脚本引擎等框架组件需要在自身逻辑中管理事务边界 - -本能力提供了一个全局单例的编程式事务上下文,封装了 Spring `PlatformTransactionManager`,使得在任何位置都能以简洁的 Lambda 方式执行事务操作。核心特点: - -- **全局单例访问**:通过 `TransactionManagerContext.getInstance()` 在任何位置获取 -- **REQUIRES_NEW 语义**:每次调用都开启独立新事务,不与调用方共享事务 -- **自动初始化**:Spring 容器启动时自动注入 `PlatformTransactionManager` -- **优雅降级**:未配置事务管理器时直接执行业务逻辑,不抛异常 - -## 如何使用 - -### 核心 API - -#### TransactionManagerContext — 事务上下文(单例) - -```java -// 获取单例实例 -TransactionManagerContext ctx = TransactionManagerContext.getInstance(); - -// 在读写事务中执行(REQUIRES_NEW) - T commit(Supplier supplier) - -// 在只读事务中执行(REQUIRES_NEW + readOnly=true) - T readOnly(Supplier supplier) -``` - -### 自动配置 - -框架通过 `TransactionManagerContextConfiguration` 自动完成初始化: - -1. `TransactionManagerContextConfiguration` 注册 `TransactionManagerContextRegister` Bean -2. `TransactionManagerContextRegister` 实现 `InitializingBean`,在 Bean 属性设置完成后将 Spring 容器中的 `PlatformTransactionManager` 注入到 `TransactionManagerContext` 单例中 -3. 如果容器中不存在 `PlatformTransactionManager`(`@Autowired(required = false)`),则不注入,事务操作会降级为直接执行 - -无需任何额外配置,只要项目中存在数据源和事务管理器即可自动生效。 - -### 事务行为说明 - -| 方法 | 传播行为 | 只读 | 正常结束 | 异常时 | -|------|---------|------|---------|--------| -| `commit()` | REQUIRES_NEW | 否 | 提交事务 | 回滚事务并抛出异常 | -| `readOnly()` | REQUIRES_NEW | 是 | 回滚事务(只读无需提交) | 回滚事务并抛出异常 | - -> **注意**:`readOnly()` 方法在正常结束时也会调用 `rollback()` 而非 `commit()`,因为只读事务没有数据变更,回滚比提交更高效。 - -## 使用实例 - -### 1. 在工具类中使用事务 - -```java -public class DataMigrationUtil { - - public static void migrateData(List records) { - TransactionManagerContext.getInstance().commit(() -> { - // 这些操作在独立的新事务中执行 - for (Record record : records) { - recordRepository.save(record); - } - return null; - }); - } -} -``` - -### 2. 在事件处理器中使用独立事务 - -```java -@Component -public class OrderCreatedHandler implements IHandler { - - @Override - public void handler(OrderCreatedEvent event) { - // 即使外层事务回滚,库存扣减也在独立事务中完成 - TransactionManagerContext.getInstance().commit(() -> { - inventoryService.deduct(event.getProductId(), event.getQuantity()); - return null; - }); - } -} -``` - -### 3. 使用只读事务查询 - -```java -public List generateReport() { - return TransactionManagerContext.getInstance().readOnly(() -> { - // 只读事务,数据库可优化查询性能 - List orders = orderRepository.findByMonth(currentMonth); - List products = productRepository.findAll(); - return reportBuilder.build(orders, products); - }); -} -``` - -### 4. 无事务管理器时的降级行为 - -```java -// 当 PlatformTransactionManager 未注入时,supplier 直接执行,不包裹事务 -// 适用于测试环境或无数据库的场景 -String result = TransactionManagerContext.getInstance().commit(() -> { - return "direct execution without transaction"; -}); -``` - -### 5. 异常自动回滚 - -```java -try { - TransactionManagerContext.getInstance().commit(() -> { - accountService.debit(fromAccount, amount); - accountService.credit(toAccount, amount); - if (balanceCheckFailed) { - throw new IllegalStateException("余额校验失败"); - } - return null; - }); -} catch (IllegalStateException e) { - // 事务已自动回滚,debit 和 credit 操作均已撤销 - log.error("转账失败,事务已回滚", e); -} -``` diff --git a/docs/capabilities/trigger-system.md b/docs/capabilities/trigger-system.md deleted file mode 100644 index 79e8ca7ca..000000000 --- a/docs/capabilities/trigger-system.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -name: trigger-system -description: 触发器框架,支持定时/条件触发,提供统一的触发上下文管理 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 70be0d38 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/Trigger.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/TriggerHandler.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/TriggerContext.java ---- - -## 解决什么问题 - -在业务流程中,存在一类"等待特定条件满足后执行"的场景: - -- 订单超时未支付自动取消 -- 审批流程中等待上级审批后再触发下一步 -- 数据采集达到阈值后触发告警 -- 临时性的条件监听,触发一次后即失效 - -这类需求与事件系统(Event System)的区别在于:**Event 是消息驱动**(确定了消息内容,不确定谁消费),而 **Trigger 是订阅驱动**(先注册订阅规则,再等待匹配的消息到来触发执行)。Trigger 模式允许精确控制: - -- **是否进入触发逻辑**:通过 `preTrigger()` 前置判断 -- **触发后是否移除**:通过 `remove()` 控制一次性触发还是持续监听 -- **按类型隔离**:不同 Trigger 类型的 Handler 互不干扰 - -本能力提供了一个轻量级的触发器框架,支持运行时动态注册/移除触发器,适用于需要条件触发、一次性触发或临时监听的业务场景。 - -## 如何使用 - -### 核心接口 - -#### Trigger — 触发器数据对象标记接口 - -```java -public interface Trigger { - // 标记接口,具体触发器需定义自己的数据结构 -} -``` - -所有触发器数据对象必须实现此接口,作为泛型约束的基础类型。 - -#### TriggerHandler\ — 触发处理器 - -```java -public interface TriggerHandler { - // 前置判断:是否满足触发条件 - boolean preTrigger(T trigger); - - // 触发执行逻辑 - void trigger(T trigger); - - // 执行完成后是否移除此 Handler(默认不移除) - default boolean remove(T trigger, boolean canTrigger) { - return false; - } -} -``` - -- `preTrigger()` 返回 `true` 时才会执行 `trigger()` 方法 -- `remove()` 的 `canTrigger` 参数表示本次是否实际执行了 `trigger()` 逻辑 -- `remove()` 返回 `true` 时,该 Handler 会从触发器列表中移除(一次性触发器) - -#### TriggerContext — 触发器上下文(单例) - -```java -// 获取单例 -TriggerContext ctx = TriggerContext.getInstance(); - -// 注册触发处理器 -void addTrigger(TriggerHandler handler) - -// 触发指定类型的触发器 -void trigger(Trigger trigger) - -// 清空某类型的所有触发器 -void clear(Class clazz) - -// 判断某类型的触发器是否为空 -boolean isEmpty(Class clazz) -``` - -### 工作机制 - -1. **注册阶段**:调用 `addTrigger(handler)` 时,框架通过反射读取 Handler 实现的 `TriggerHandler` 泛型参数,自动推断其关联的 Trigger 类型,并按类型分组存储 -2. **触发阶段**:调用 `trigger(triggerObj)` 时,根据 triggerObj 的实际类型查找对应的 Handler 列表,依次执行: - - 调用 `preTrigger()` 判断是否满足条件 - - 满足条件则调用 `trigger()` 执行业务逻辑 - - 调用 `remove()` 判断是否需要移除该 Handler -3. **异常隔离**:单个 Handler 执行异常不会影响其他 Handler 的执行,异常会被捕获并记录 warn 日志 - -### 线程安全 - -- 触发器 Map 使用 `ConcurrentHashMap` 保证并发注册安全 -- 每个类型的 Handler 列表使用 `CopyOnWriteArrayList` 保证遍历时的并发安全 - -## 使用实例 - -### 1. 定义触发器数据对象 - -```java -// 订单超时触发器 -public class OrderTimeoutTrigger implements Trigger { - private final String orderId; - private final LocalDateTime deadline; - - public OrderTimeoutTrigger(String orderId, LocalDateTime deadline) { - this.orderId = orderId; - this.deadline = deadline; - } - - public String getOrderId() { return orderId; } - public LocalDateTime getDeadline() { return deadline; } -} -``` - -### 2. 实现一次性触发处理器 - -```java -// 订单超时自动取消(触发一次后自动移除) -public class OrderTimeoutCancelHandler implements TriggerHandler { - - @Override - public boolean preTrigger(OrderTimeoutTrigger trigger) { - // 仅在超过截止时间时才触发 - return LocalDateTime.now().isAfter(trigger.getDeadline()); - } - - @Override - public void trigger(OrderTimeoutTrigger trigger) { - log.info("订单 {} 超时未支付,自动取消", trigger.getOrderId()); - orderService.cancelOrder(trigger.getOrderId(), "超时未支付"); - } - - @Override - public boolean remove(OrderTimeoutTrigger trigger, boolean canTrigger) { - // 无论是否触发成功,都移除该 Handler(一次性) - return true; - } -} -``` - -### 3. 实现持续性触发处理器 - -```java -// 库存告警(持续监听,每次低于阈值都触发) -public class StockAlertTrigger implements Trigger { - private final String productId; - private final int currentStock; - - public StockAlertTrigger(String productId, int currentStock) { - this.productId = productId; - this.currentStock = currentStock; - } - - // getter ... -} - -public class StockAlertHandler implements TriggerHandler { - - private static final int THRESHOLD = 10; - - @Override - public boolean preTrigger(StockAlertTrigger trigger) { - return trigger.getCurrentStock() < THRESHOLD; - } - - @Override - public void trigger(StockAlertTrigger trigger) { - notificationService.sendAlert( - "商品 " + trigger.getProductId() + " 库存不足,当前: " + trigger.getCurrentStock() - ); - } - - // 使用默认 remove(),返回 false,持续监听 -} -``` - -### 4. 注册和触发 - -```java -@Service -public class OrderService { - - // 创建订单时注册超时触发器 - public void createOrder(String orderId) { - // ... 创建订单逻辑 ... - - // 注册 30 分钟超时触发器 - OrderTimeoutTrigger trigger = new OrderTimeoutTrigger( - orderId, LocalDateTime.now().plusMinutes(30)); - TriggerContext.getInstance().addTrigger(new OrderTimeoutCancelHandler()); - - // 在实际场景中,通常会结合定时任务定期调用 trigger() - } - - // 定时检查(由 ScheduledTask 调用) - @Scheduled(fixedRate = 60000) - public void checkOrderTimeout() { - OrderTimeoutTrigger trigger = new OrderTimeoutTrigger(null, null); - // 遍历待检查订单,逐个触发 - for (String orderId : pendingOrders) { - OrderTimeoutTrigger t = new OrderTimeoutTrigger(orderId, getOrderDeadline(orderId)); - TriggerContext.getInstance().trigger(t); - } - } -} -``` - -### 5. 清空触发器 - -```java -// 当订单被手动取消或支付成功时,清空该类型的所有触发器 -TriggerContext.getInstance().clear(OrderTimeoutTrigger.class); - -// 检查是否还有活跃的触发器 -boolean hasTriggers = !TriggerContext.getInstance().isEmpty(OrderTimeoutTrigger.class); -``` - -### 6. Event vs Trigger 选择指南 - -| 场景 | 推荐 | 原因 | -|------|------|------| -| 实体状态变更后通知多个下游 | Event | 消息确定,消费者不确定 | -| 等待特定条件后执行一次性操作 | Trigger | 先订阅再等待,触发后可自移除 | -| 跨模块解耦通信 | Event | 发布-订阅天然解耦 | -| 临时性条件监听 | Trigger | 可动态注册/移除,生命周期可控 | -| 定时轮询检查条件 | Trigger | 配合 preTrigger 做条件判断 | diff --git a/docs/capabilities/unified-response.md b/docs/capabilities/unified-response.md new file mode 100644 index 000000000..7c7ca82e1 --- /dev/null +++ b/docs/capabilities/unified-response.md @@ -0,0 +1,87 @@ +--- +name: unified-response +description: 统一响应封装体系(Response / SingleResponse / MultiResponse / MapResponse),标准化 API 返回格式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: com.codingapi.springboot:springboot-starter +symbols: + - Response + - SingleResponse + - MultiResponse + - MapResponse +content_hash: 1dfe2559a6b514effb51735ca8ca49aba308959e02814c5862b842eed3b04fb1 +--- + +## 解决什么问题 + +前后端分离项目中,API 返回格式需要统一规范,便于前端统一处理。本能力提供了标准化的响应封装: + +- **统一结构**:所有 API 返回 `{success, errCode, errMessage, data}` 标准格式 +- **泛型支持**:`SingleResponse` 和 `MultiResponse` 支持任意数据类型 +- **分页集成**:`MultiResponse.of(Page)` 直接从 Spring Data Page 构建响应 +- **失败标准化**:`Response.buildFailure(code, message)` 统一错误响应 + +## 如何使用 + +### 成功响应 + +```java +// 无数据 +return Response.buildSuccess(); + +// 单对象 +return SingleResponse.of(user); + +// 列表 + 总数 +return MultiResponse.of(userList, totalCount); + +// 从 Spring Data Page 构建 +Page page = userRepository.findAll(PageRequest.of(0, 20)); +return MultiResponse.of(page); +``` + +### 失败响应 + +```java +return Response.buildFailure("user.not.found", "用户不存在"); +``` + +### 响应结构 + +```json +// SingleResponse +{ "success": true, "errCode": null, "errMessage": null, "data": { "id": 1, "name": "张三" } } + +// MultiResponse +{ "success": true, "data": { "total": 100, "list": [...] } } + +// 失败响应 +{ "success": false, "errCode": "user.not.found", "errMessage": "用户不存在" } +``` + +## 使用实例 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @GetMapping("/{id}") + public SingleResponse get(@PathVariable Long id) { + User user = userService.findById(id); + if (user == null) { + throw new LocaleMessageException("user.not.found", "用户不存在"); + } + return SingleResponse.of(user); + } + + @GetMapping + public MultiResponse list() { + Page page = userRepository.findAll( + PageRequest.of(0, 20).addFilter("status", "ACTIVE") + ); + return MultiResponse.of(page); + } +} +``` diff --git a/docs/capabilities/user-context.md b/docs/capabilities/user-context.md deleted file mode 100644 index 3e36d7657..000000000 --- a/docs/capabilities/user-context.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -name: user-context -description: 线程级用户身份上下文,通过 ThreadLocal 持有当前登录用户,支持跨层传递 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 719167a0 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/user/UserContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/user/IUser.java ---- - -## 解决什么问题 - -在企业应用中,几乎所有业务操作都需要知道"当前操作用户是谁"。传统做法是在方法签名中逐层传递用户对象(Controller → Service → Repository),导致: - -- **参数污染**:每个业务方法都需要额外接收用户参数,签名臃肿 -- **跨层耦合**:中间层即使不使用用户信息也必须透传 -- **框架集成困难**:事件处理器、异步任务、AOP 切面等非调用链场景无法方便地获取用户身份 - -本能力通过 `ThreadLocal` 在当前线程中绑定用户身份对象,使任意层级的代码都能直接获取当前登录用户,无需显式传参。配合安全模块(`springboot-starter-security`)的认证过滤器,在请求入口处自动设置、请求结束时自动清理。 - -## 如何使用 - -### 核心接口与类 - -| 类型 | 说明 | -|------|------| -| `IUser` | 用户身份标记接口,业务模块实现此接口定义自己的用户实体 | -| `UserContext` | 单例用户上下文管理器,基于 `ThreadLocal` 存储当前线程的用户身份 | - -### API - -```java -// 获取全局单例 -UserContext ctx = UserContext.getInstance(); - -// 设置当前线程的用户身份 -ctx.setCurrent(IUser user); - -// 获取当前线程的用户身份 -IUser user = ctx.current(); -``` - -### 实现 IUser - -`IUser` 是一个纯标记接口(无方法声明),业务模块自行定义用户属性: - -```java -public class LoginUser implements IUser { - private String userId; - private String username; - private List roles; - // getter/setter... -} -``` - -### 与安全模块集成 - -在认证过滤器中设置用户上下文: - -```java -// 认证通过后设置 -UserContext.getInstance().setCurrent(loginUser); - -// 请求结束后清理(防止线程池复用导致上下文泄漏) -UserContext.getInstance().setCurrent(null); -``` - -### 注意事项 - -- `UserContext` 基于 `ThreadLocal`,仅在同一个线程内有效 -- 异步场景(`@Async`、线程池)需要手动传递或配合 `TaskDecorator` 复制上下文 -- 务必在请求结束时清理(设为 `null`),避免线程池场景下的用户信息泄漏 - -## 使用实例 - -### 1. 在 Service 层获取当前用户 - -```java -@Service -public class OrderService { - - public Order createOrder(CreateOrderCmd cmd) { - // 无需方法参数传入用户,直接从上下文获取 - IUser currentUser = UserContext.getInstance().current(); - LoginUser loginUser = (LoginUser) currentUser; - - Order order = new Order(); - order.setCreatorId(loginUser.getUserId()); - order.setCreatorName(loginUser.getUsername()); - // ... - return orderRepository.save(order); - } -} -``` - -### 2. 在事件 Handler 中获取操作用户 - -```java -@Handler -@Component -public class AuditLogHandler implements IHandler { - - @Override - public void handler(DomainCreateEvent event) { - IUser user = UserContext.getInstance().current(); - // 记录审计日志:谁在什么时候创建了什么 - auditLogRepository.save(new AuditLog( - user != null ? user.toString() : "system", - event.getEntityClass().getSimpleName(), - LocalDateTime.now() - )); - } -} -``` - -### 3. 在认证过滤器中设置与清理 - -```java -@Component -public class JwtAuthFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain chain) - throws ServletException, IOException { - try { - // 解析 Token → 构建用户对象 - LoginUser loginUser = tokenGateway.parseAndBuildUser(request); - if (loginUser != null) { - UserContext.getInstance().setCurrent(loginUser); - } - chain.doFilter(request, response); - } finally { - // 请求结束必须清理,防止线程池复用导致上下文污染 - UserContext.getInstance().setCurrent(null); - } - } -} -``` diff --git a/docs/capabilities/workflow-engine.md b/docs/capabilities/workflow-engine.md index 763b690e6..c5cda07c1 100644 --- a/docs/capabilities/workflow-engine.md +++ b/docs/capabilities/workflow-engine.md @@ -1,135 +1,104 @@ --- name: workflow-engine -description: 工作流引擎,支持流程定义、节点流转、审批、委托、会签、数据快照和事件通知 +description: 轻量级工作流引擎,支持流程定义、节点流转、审批、退回、委托、会签、抄送、数据快照与事件通知 status: 已实现 scope: 后端 source: 项目自有 -last_commit: 0fc02aca -code_files: - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/FlowConfiguration.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowProcess.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowRecord.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowBackup.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/record/FlowMerge.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/FlowService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/FlowNodeService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowStartService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowSubmitService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowRecallService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowStopService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/service/impl/FlowTrySubmitService.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowWorkRepository.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowProcessRepository.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowRecordRepository.java - - springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/repository/FlowBackupRepository.java +import: com.codingapi.springboot:springboot-starter-flow +symbols: + - FlowWork + - FlowNode + - FlowRelation + - FlowRecord + - FlowBackup + - FlowProcess + - FlowWorkBuilder + - SchemaReader + - FlowNodeService + - FlowStartService + - FlowStepService + - FlowSubmitService + - FlowBackService + - FlowRecallService + - FlowTransferService + - FlowSession + - FlowApprovalEvent + - TitleGenerator + - OperatorMatcher + - OutTrigger + - IFlowOperator + - FlowRecordRepository + - FlowWorkRepository + - FlowOperatorRepository + - BindDataSnapshot + - IBindData +content_hash: 42954fc1cb16f19b5ed4693dfa2ba2887cc9a90673dbb5bafd1dd7c291631712 --- ## 解决什么问题 -企业级审批场景(请假、报销、采购等)需要标准化的流程引擎来管理: -- **流程定义与版本管理**:流程模板可修改、可版本化,运行中流程不受定义变更影响 -- **节点流转规则**:支持串行、并行(会签)、条件分支等多种流转模式 -- **审批操作**:通过、驳回、退回、撤回、转办、催办、终止等完整操作集 -- **数据快照**:发起时锁定流程定义版本,确保审批过程一致性 -- **事件通知**:每个状态变更自动推送 `FlowApprovalEvent`,驱动业务联动 +企业应用中审批流程(请假、报销、合同审批等)是高频需求。本工作流引擎解决了以下问题: + +- **流程定义与构建**:通过 `FlowWorkBuilder` 或 JSON Schema 定义流程节点和流转关系 +- **灵活的节点流转**:支持条件分支、退回、委托、抄送、会签等多种流转模式 +- **操作者匹配**:通过 Groovy 脚本动态匹配节点审批人 +- **数据快照**:使用 Kryo 深拷贝保存审批时的业务数据快照 +- **事件驱动**:每个状态变更推送 `FlowApprovalEvent`,与事件系统集成 ## 如何使用 -### 核心数据模型 +### 核心领域模型 -``` -FlowWork(流程定义) - └── FlowNode(节点) - └── FlowSource(连线/分支条件) +- `FlowWork` — 流程定义(包含节点和关系) +- `FlowNode` — 流程节点(开始/审批/传阅/结束) +- `FlowRelation` — 节点间关系(含条件触发器) +- `FlowRecord` — 审批记录 +- `FlowBackup` — 流程版本快照 -FlowProcess(流程实例) - └── FlowRecord(审批记录) - └── FlowBackup(版本快照) -``` +### 流程发起 -| 模型 | 说明 | -|------|------| -| `FlowWork` | 流程模板定义,包含节点列表和连线关系 | -| `FlowProcess` | 流程实例(由 `FlowStartService` 创建),关联 backupId 锁定版本 | -| `FlowRecord` | 审批记录,记录每个节点的审批人、结果、意见、时间 | -| `FlowBackup` | 流程定义快照(Kryo 序列化),确保运行中流程不受模板修改影响 | - -### 服务层 API - -| 服务 | 方法 | 说明 | -|------|------|------| -| `FlowStartService` | `startFlow(...)` | 发起流程,创建 FlowProcess + FlowBackup + 初始 FlowRecord | -| `FlowSubmitService` | `submitFlow(...)` | 提交审批(通过/驳回) | -| `FlowRecallService` | `recallFlow(...)` | 撤回已提交的流程 | -| `FlowTrySubmitService` | `trySubmitFlow(...)` | 试提交 — 获取下一节点审批人(不实际流转) | -| `FlowStopService` | `stopFlow(...)` | 终止流程 | -| `FlowUrgeService` | `urgeFlow(...)` | 催办 | -| `FlowNodeService` | `getFlowRecords(...)` | 获取流程审批记录 | -| `FlowStepService` | `getFlowSteps(...)` | 获取流程步骤图 | -| `FlowCustomEventService` | `customEvent(...)` | 触发自定义按钮事件 | - -### 流程状态(FlowStatus) +```java +@Autowired +private FlowService flowService; -``` -RUNNING → FINISH / VOIDED / STOP +// 发起流程 +FlowResult result = flowService.start("leave-approval", bindData, createOperator); ``` -### 审批操作类型(FlowType) +### 流程审批 -CREATE、SAVE、PASS、REJECT、RECALL、DELETE、VOIDED、BACK、FINISH、TRANSFER、URGE、STOP +```java +// 提交审批意见 +Opinion opinion = Opinion.builder() + .pass(true) + .comment("同意") + .build(); +flowService.submit(processId, opinion, currentOperator); +``` -### 事件通知 +### 流程退回 -每次状态变更通过 `EventPusher` 推送 `FlowApprovalEvent`,业务侧通过 `IHandler` 监听: +```java +flowService.back(processId, opinion, currentOperator); +``` + +### 监听审批事件 ```java -@Component +@Service public class LeaveHandler implements IHandler { @Override public void handler(FlowApprovalEvent event) { - if (event.getFlowType() == FlowType.FINISH) { - // 审批完成 — 更新请假状态 + if (event.isFinish() && event.match(LeaveForm.class)) { + // 审批通过,执行业务逻辑 } } } ``` -### 配置 - -```java -@Configuration -public class FlowConfiguration { - // 自动注册所有 Flow Repository 和 Service - // 通过 FlowFrameworkRegister 注册框架级触发器 -} -``` - ## 使用实例 -```java -// 1. 发起请假流程 -FlowProcess process = flowStartService.startFlow( - workId, // 流程定义ID - operatorId, // 发起人ID - bindDataId // 关联业务数据ID -); - -// 2. 试提交 — 查看下一节点审批人 -List nextApprovers = flowTrySubmitService.trySubmitFlow( - process.getProcessId(), operatorId, "同意请假" -); - -// 3. 正式提交审批 -flowSubmitService.submitFlow( - process.getProcessId(), operatorId, "同意请假", ApprovalType.PASS -); - -// 4. 撤回(仅在下一节点未审批前可撤回) -flowRecallService.recallFlow(process.getProcessId(), operatorId); - -// 5. 查询审批记录 -List records = flowNodeService.getFlowRecords(process.getProcessId()); - -// 6. 终止流程 -flowStopService.stopFlow(process.getProcessId(), operatorId, "业务取消"); -``` +参考 `example` 模块中的请假流程示例: +- 流程定义:`FlowWorkCmd` 通过 `FlowWorkBuilder` 构建 +- 审批处理:`LeaveHandler` 监听 `FlowApprovalEvent` +- 数据绑定:`LeaveForm implements IBindData` diff --git a/docs/conventions/autoconfiguration-pattern.md b/docs/conventions/autoconfiguration-pattern.md deleted file mode 100644 index e33758ace..000000000 --- a/docs/conventions/autoconfiguration-pattern.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -name: autoconfiguration-pattern -description: 每个 starter 模块必须同时注册 spring.factories(旧版兼容)和 AutoConfiguration.imports(Spring Boot 3.x 标准),使用 @ConfigurationProperties + Context 单例模式 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 303b377f -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/properties/PropertiesContext.java - - springboot-starter/src/main/resources/META-INF/spring.factories - - springboot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ---- - -## 解决什么问题 - -创建 Spring Boot Starter 时,如果不遵循标准的自动配置注册规范,会导致以下问题: - -1. **版本兼容性断裂**:Spring Boot 3.x 废弃了 `spring.factories` 中的 `EnableAutoConfiguration` 键,仅支持 `AutoConfiguration.imports`;但部分运行环境或工具链仍读取旧格式,缺失任一都会导致自动配置不生效 -2. **配置属性无法绑定**:不使用 `@ConfigurationProperties` 注解则无法将 `application.properties` 中的配置自动映射到 Java 对象 -3. **非 Spring 上下文无法访问配置**:在工具类、拦截器、代理等非 Bean 环境中,无法通过 `@Autowired` 注入配置,需要额外的 Context 单例桥接 -4. **新模块遗漏注册**:没有统一的注册约定,新增 starter 时容易忘记配置文件导致功能静默失效 - -## 如何使用 - -### 1. 双文件注册(必须同时维护) - -在每个 starter 模块的 `src/main/resources/META-INF/` 下创建两个文件: - -**`spring.factories`**(兼容 Spring Boot 2.x 及旧工具链): -```properties -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.example.mystarter.MyAutoConfiguration,\ -com.example.mystarter.config.SomeConfiguration -``` - -**`spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`**(Spring Boot 3.x 标准): -``` -com.example.mystarter.MyAutoConfiguration -com.example.mystarter.config.SomeConfiguration -``` - -> ⚠️ 两个文件中列出的配置类**必须完全一致**,仅格式不同。 - -### 2. ConfigurationProperties + Context 模式 - -对于需要在非 Spring Bean 中访问的配置: - -1. 定义 Properties 类并标注 `@ConfigurationProperties(prefix = "...")` -2. 定义对应的 `*Context` 单例类持有该 Properties -3. 在 `@Configuration` 类的 `@Bean` 方法中创建 Properties 并同步设置到 Context - -### 3. 规则 - -1. **新增 starter 模块时必须同时创建两个注册文件** -2. 修改自动配置类列表时,两个文件必须同步更新 -3. 配置类使用 `@Configuration` 注解,可配合 `@ConditionalOnClass` / `@ConditionalOnProperty` 做条件加载 -4. 需要暴露给非 Bean 环境的配置必须通过 Context 单例桥接 - -## 使用实例 - -### ✅ 正确示例 - -**目录结构:** -``` -springboot-starter-mymodule/ - src/main/ - java/com/example/mymodule/ - AutoConfiguration.java - MyModuleProperties.java - MyModuleContext.java - resources/META-INF/ - spring.factories - spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports -``` - -**spring.factories:** -```properties -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.example.mymodule.AutoConfiguration -``` - -**AutoConfiguration.imports:** -``` -com.example.mymodule.AutoConfiguration -``` - -**AutoConfiguration.java:** -```java -@Configuration -public class AutoConfiguration implements InitializingBean { - - @Bean - @ConfigurationProperties(prefix = "codingapi.mymodule") - public MyModuleProperties myModuleProperties() { - MyModuleProperties properties = new MyModuleProperties(); - MyModuleContext.getInstance().setProperties(properties); - return properties; - } - - @Override - public void afterPropertiesSet() throws Exception { - // 启动时的初始化逻辑 - } -} -``` - -### ❌ 错误示例 - -```properties -# 错误:仅有 spring.factories,缺少 AutoConfiguration.imports -# Spring Boot 3.x 环境下自动配置不会生效 -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.example.mymodule.AutoConfiguration -``` - -```java -// 错误:直接使用 @Value 注入配置,非 Bean 环境无法获取 -@Component -public class MyService { - @Value("${codingapi.mymodule.timeout}") - private int timeout; -} - -// 错误:两个文件内容不一致 -// spring.factories 列出了 A, B -// AutoConfiguration.imports 只列出了 A -// → 某些环境下 B 配置不生效,难以排查 - -// 错误:配置类未使用 @ConfigurationProperties -@Bean -public MyModuleProperties myModuleProperties() { - return new MyModuleProperties(); // 属性不会被自动绑定 -} -``` diff --git a/docs/conventions/context-register-pairing.md b/docs/conventions/context-register-pairing.md deleted file mode 100644 index fdcbd0de3..000000000 --- a/docs/conventions/context-register-pairing.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -name: context-register-pairing -description: 每个 Context 单例必须配套 Register 类和 Configuration 类,形成三件套:Context(持有状态)+ Register(注入依赖)+ Configuration(注册 Bean) -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 303b377f -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextRegister.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextConfiguration.java ---- - -## 解决什么问题 - -本规范是对已有 `context-singleton` 规范的补充细化。`context-singleton` 定义了 Context 单例的基本结构,但在实际开发中经常出现以下问题: - -1. **Register 缺失**:开发者创建了 Context 单例,但忘记编写对应的 Register 类来注入 Spring Bean 依赖,导致 Context 中的依赖为 null,运行时才暴露 NPE。 -2. **Configuration 遗漏**:Register 类虽然写了,但没有通过 `@Configuration` + `@Bean` 注册到 Spring 容器中,Register 不会被实例化,`afterPropertiesSet()` 永远不会被调用。 -3. **三者分散在不同模块**:Context、Register、Configuration 散落在不同的包或模块中,新增功能时难以找到完整的配对关系,维护成本高。 -4. **依赖注入方式不一致**:有的用构造函数注入,有的用 `@Autowired` 字段注入,有的直接在 Configuration 中调用 setter,缺乏统一模式。 - -本规范要求:**每创建一个 Context 单例,必须同时创建配套的 Register 和 Configuration 类,三者缺一不可,且放在同一个包下。** - -## 如何使用 - -### 三件套命名规范 - -以 `Xxx` 为业务前缀,三个类的命名固定为: - -| 类名 | 职责 | 关键特征 | -|------|------|---------| -| `XxxContext` | 持有全局状态的饿汉式单例 | `private static final XxxContext instance = new XxxContext()`;私有构造函数;提供 `getInstance()` | -| `XxxContextRegister` | 实现 `InitializingBean`,在 Bean 初始化完成后将依赖注入到 Context | 构造函数接收所需依赖;`afterPropertiesSet()` 中调用 Context 的 setter | -| `XxxContextConfiguration` | `@Configuration` 类,通过 `@Bean` 方法注册 Register 实例 | 可声明 `@ConditionalOnClass` 等条件注解;依赖参数使用 `@Autowired(required = false)` 避免启动失败 | - -### 文件组织 - -三个类必须放在**同一个 Java 包**下,文件名与类名一致: - -``` -com.codingapi.springboot.framework.transaction/ -├── TransactionManagerContext.java -├── TransactionManagerContextRegister.java -└── TransactionManagerContextConfiguration.java -``` - -### Context 类模板 - -```java -@Slf4j -public class XxxContext { - - @Getter - private static final XxxContext instance = new XxxContext(); - - private SomeDependency dependency; - - private XxxContext() {} - - public void setDependency(SomeDependency dependency) { - this.dependency = dependency; - if (dependency != null) { - log.info("{} load success", dependency); - } - } - - // 对外 API 方法 - public T execute(Supplier supplier) { - if (dependency != null) { - // 使用依赖执行业务逻辑 - } - return supplier.get(); - } -} -``` - -### Register 类模板 - -```java -public class XxxContextRegister implements InitializingBean { - - private final SomeDependency dependency; - - public XxxContextRegister(SomeDependency dependency) { - this.dependency = dependency; - } - - @Override - public void afterPropertiesSet() throws Exception { - XxxContext.getInstance().setDependency(dependency); - } -} -``` - -### Configuration 类模板 - -```java -@Configuration -public class XxxContextConfiguration { - - @Bean - public XxxContextRegister xxxContextRegister( - @Autowired(required = false) SomeDependency dependency) { - return new XxxContextRegister(dependency); - } -} -``` - -### 关键规则 - -1. **Register 只负责注入**:`afterPropertiesSet()` 中只做 setter 调用,不包含业务逻辑。 -2. **Configuration 只负责注册**:`@Bean` 方法只构造 Register 实例,不做其他操作。 -3. **可选依赖使用 `required = false`**:当依赖可能不存在时(如 `PlatformTransactionManager` 在某些测试环境中),使用 `@Autowired(required = false)` 避免启动失败。Context 的 API 方法内部需对 null 做防御性检查。 -4. **Context 的 setter 应有日志**:注入成功时打印 INFO 日志,便于排查启动问题。 -5. **自动配置注册**:Configuration 类需在 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 和 `META-INF/spring.factories` 中同时注册。 - -## 使用实例 - -```java -// ✅ 正确 — 完整的三件套(以 TransactionManager 为例) - -// 1. Context:持有 PlatformTransactionManager,提供编程式事务 API -public class TransactionManagerContext { - @Getter - private static final TransactionManagerContext instance = new TransactionManagerContext(); - @Getter - private PlatformTransactionManager platformTransactionManager; - - private TransactionManagerContext() {} - - public void setPlatformTransactionManager(PlatformTransactionManager ptm) { - this.platformTransactionManager = ptm; - if (ptm != null) { - log.info("platformTransactionManager:{} load success", ptm); - } - } - - public T commit(Supplier supplier) { - if (platformTransactionManager != null) { - DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - TransactionStatus status = platformTransactionManager.getTransaction(def); - try { - T result = supplier.get(); - platformTransactionManager.commit(status); - return result; - } catch (Exception e) { - platformTransactionManager.rollback(status); - throw e; - } - } - return supplier.get(); // 无事务管理器时降级执行 - } -} - -// 2. Register:启动时将 TransactionManager 注入到 Context -public class TransactionManagerContextRegister implements InitializingBean { - private final PlatformTransactionManager transactionManager; - - public TransactionManagerContextRegister(PlatformTransactionManager transactionManager) { - this.transactionManager = transactionManager; - } - - @Override - public void afterPropertiesSet() { - TransactionManagerContext.getInstance() - .setPlatformTransactionManager(transactionManager); - } -} - -// 3. Configuration:注册 Register Bean -@Configuration -public class TransactionManagerContextConfiguration { - @Bean - public TransactionManagerContextRegister transactionManagerContextRegister( - @Autowired(required = false) PlatformTransactionManager ptm) { - return new TransactionManagerContextRegister(ptm); - } -} - -// ✅ 正确 — 在非 Spring 管理的代码中使用 Context -public class GroovyScriptRuntime { - public T invoke(String method, String script, TransactionMode mode) { - if (mode == TransactionMode.COMMIT) { - return TransactionManagerContext.getInstance().commit(() -> { - return (T) runtime.invokeMethod(method, args); - }); - } - // ... - } -} - -// ❌ 错误 — 只有 Context,缺少 Register 和 Configuration -public class CacheContext { - @Getter - private static final CacheContext instance = new CacheContext(); - private CacheManager cacheManager; - - private CacheContext() {} - - // 没有 Register 注入 cacheManager,永远为 null! - public void put(String key, Object value) { - cacheManager.put(key, value); // NPE! - } -} - -// ❌ 错误 — Register 存在但没有 Configuration 注册 -// TransactionManagerContextRegister 写了,但没有 @Configuration 类将其注册为 Bean -// afterPropertiesSet() 永远不会被调用 - -// ❌ 错误 — 在 Configuration 中直接操作 Context,跳过 Register -@Configuration -public class BadConfiguration { - @Bean - public SomeBean someBean(PlatformTransactionManager ptm) { - // 不应在普通 Bean 定义中直接操作 Context - TransactionManagerContext.getInstance().setPlatformTransactionManager(ptm); - return new SomeBean(); - } -} -``` diff --git a/docs/conventions/context-singleton.md b/docs/conventions/context-singleton.md deleted file mode 100644 index 7489a39c4..000000000 --- a/docs/conventions/context-singleton.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: context-singleton -description: 全局状态通过单例 Context 类持有,由 Register 类在启动时注入依赖 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 303b377f -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/properties/PropertiesContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/MessageContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/transaction/TransactionManagerContextRegister.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/user/UserContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/rest/RestTemplateContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/event/DomainEventContext.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/trigger/TriggerContext.java - - springboot-starter-security/src/main/java/com/codingapi/springboot/security/gateway/TokenContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/interceptor/SQLInterceptorContext.java - - springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationContext.java ---- - -## 解决什么问题 - -框架层工具类(如事件推送、事务管理、异常国际化)需要在非 Spring 管理的上下文中使用(例如 Groovy 脚本运行时、静态方法调用、线程池异步任务)。如果直接依赖 Spring Bean 注入,在这些场景中将无法获取实例。 - -单例 Context 模式解决了这个问题:通过静态单例持有全局状态,由 Spring 管理的 `*Register` 类在应用启动时注入所需依赖,使得框架能力在任何地方都能通过 `XxxContext.getInstance()` 访问。 - -## 如何使用 - -### 命名规范 - -| 类后缀 | 职责 | 示例 | -|--------|------|------| -| `*Context` | 持有全局状态的单例类 | `PropertiesContext`, `UserContext`, `TransactionManagerContext` | -| `*Register` | `InitializingBean` 实现,启动时注入依赖到 Context | `TransactionManagerContextRegister` | -| `*Configuration` | `@Configuration` 类,注册 Context Bean 和 Register Bean | `TransactionManagerContextConfiguration` | - -### Context 类结构 - -```java -public class XxxContext { - // 饿汉式单例(推荐)或懒汉式(需双重检查锁) - @Getter - private static final XxxContext instance = new XxxContext(); - - private XxxContext() {} - - // 注入的依赖(由 Register 设置) - private SomeDependency dependency; - - public void setDependency(SomeDependency dependency) { - this.dependency = dependency; - } - - // 对外 API - public void doSomething() { - dependency.execute(); - } -} -``` - -### Register 类结构 - -```java -public class XxxContextRegister implements InitializingBean { - private final SomeDependency dependency; - - public XxxContextRegister(SomeDependency dependency) { - this.dependency = dependency; - } - - @Override - public void afterPropertiesSet() { - XxxContext.getInstance().setDependency(dependency); - } -} -``` - -### 已注册的 Context 类 - -| Context | 持有状态 | 用途 | -|---------|---------|------| -| `PropertiesContext` | `FrameworkProperties` | 框架配置属性 | -| `MessageContext` | `MessageSource` | 国际化消息 | -| `TransactionManagerContext` | `PlatformTransactionManager` | 编程式事务 | -| `UserContext` | `ThreadLocal` | 当前登录用户 | -| `RestTemplateContext` | `RestTemplate` | HTTP 客户端 | -| `DomainEventContext` | 领域事件上下文 | 事件变更追踪 | -| `TokenContext` | `ThreadLocal` | 当前 Token 扩展信息 | -| `SQLInterceptorContext` | SQL 拦截状态 | 数据权限上下文 | -| `TriggerContext` | 触发器注册表 | 触发器管理 | - -## 使用实例 - -```java -// ✅ 正确 — 通过 Context 单例访问 -public class GroovyScriptRuntime { - public Object execute(String script) { - // 在 Groovy 脚本中使用框架事务 - return TransactionManagerContext.getInstance().commit(() -> { - return runScript(script); - }); - } -} - -// ✅ 正确 — Register 在启动时注入 -@Bean -public TransactionManagerContextRegister transactionManagerContextRegister( - PlatformTransactionManager transactionManager) { - return new TransactionManagerContextRegister(transactionManager); -} - -// ❌ 错误 — 在非 Spring 管理的类中直接注入 Bean -public class GroovyScriptRuntime { - @Autowired // 无法注入!此类不是 Spring Bean - private PlatformTransactionManager transactionManager; -} - -// ❌ 错误 — 在静态工具类中使用 ThreadLocal 之外的 Spring Bean -public class EventPusher { - @Autowired // 静态上下文无法注入 - private static ApplicationEventPublisher publisher; -} -``` diff --git a/docs/conventions/ddd-layered-architecture.md b/docs/conventions/ddd-layered-architecture.md new file mode 100644 index 000000000..b49a1024d --- /dev/null +++ b/docs/conventions/ddd-layered-architecture.md @@ -0,0 +1,129 @@ +--- +name: ddd-layered-architecture +description: DDD 分层架构规范 — 遵循 interface → app → domain ← infra 四层依赖规则,domain 层定义接口,infra 层提供实现 +status: 已实现 +scope: 后端 +source: 项目自有 +symbols: + - IDomain +content_hash: dc7a9d76224be09498f0278ec05c56ea34b0c5e2dbb80850ccb826fabe721da6 +--- + +## 解决什么问题 + +不遵守此规范会导致: +- 业务逻辑散落在 Controller 或基础设施层,难以测试和复用 +- 层间循环依赖,修改一处牵动全局 +- 领域模型贫血(只有 getter/setter,没有业务行为) +- 基础设施变更(如换数据库、换消息队列)影响业务代码 + +## 如何使用 + +### 四层架构 + +``` +example/ + example-interface/ ← 接口层:接收外部请求 + example-app/ ← 应用层:编排业务流程 + example-domain/ ← 领域层:核心业务逻辑 + example-infra/ ← 基础设施层:技术实现 +``` + +### 依赖规则 + +``` +interface → app → domain ← infra +``` + +- **interface → app**:接口层调用应用层服务 +- **app → domain**:应用层调用领域层服务和实体 +- **infra → domain**:基础设施层实现领域层定义的接口 +- **domain 不依赖任何层**:领域层是核心,独立存在 + +### 各层职责 + +| 层 | 职责 | 包含 | +|----|------|------| +| **interface** | 接收 HTTP 请求,参数转换,响应封装 | Controller、Handler(事件处理)、Runner | +| **app** | 编排业务流程,调用领域服务,管理事务 | 命令服务(cmd)、查询服务(query) | +| **domain** | 核心业务逻辑,定义 Repository 接口和 Gateway 接口 | Entity、Repository(接口)、Service、Event | +| **infra** | 技术实现:数据库、缓存、消息队列、外部 API | Repository(实现)、Entity(JPA)、Gateway(实现) | + +### CQRS 模式 + +应用层按读写分离: +- **cmd(命令侧)**:写操作,调用领域服务 +- **query(查询侧)**:读操作,直接查询数据库 + +## 使用实例 + +✅ **正确示例**: + +```java +// domain 层 — 定义接口 +public interface UserRepository { + Optional findById(Long id); + User save(User user); +} + +// infra 层 — 提供实现 +@Repository +public class UserRepositoryImpl implements UserRepository { + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id).map(UserConvertor::toDomain); + } +} + +// app 层 — 编排 +@Service +public class UserCommandService { + @Autowired + private UserRepository userRepository; // 依赖 domain 层接口 + + @Transactional + public void registerUser(RegisterCmd cmd) { + User user = new User(cmd.getUsername(), cmd.getEmail()); + userRepository.save(user); + EventPusher.push(new UserRegisteredEvent(user.getId())); + } +} + +// interface 层 — 接收请求 +@RestController +@RequestMapping("/api/users") +public class UserController { + @Autowired + private UserCommandService commandService; // 依赖 app 层 + + @PostMapping("/register") + public Response register(@RequestBody RegisterCmd cmd) { + commandService.registerUser(cmd); + return Response.buildSuccess(); + } +} +``` + +❌ **错误示例**: + +```java +// domain 层直接依赖 infra 实现(违反依赖方向) +public class User { + @Autowired + private UserRepositoryImpl repository; // ❌ 不应依赖实现类 +} + +// Controller 直接调用 Repository(跳过 app 层) +@RestController +public class UserController { + @Autowired + private UserRepository userRepository; // ❌ 应通过 app 层 +} + +// 业务逻辑在 Controller 中(应在 domain 层) +@PostMapping +public Response create(@RequestBody UserDTO dto) { + if (dto.getAge() < 18) { /* ❌ 业务逻辑不应在 Controller */ } + // ... +} +``` diff --git a/docs/conventions/event-handler-standard.md b/docs/conventions/event-handler-standard.md new file mode 100644 index 000000000..161b2b88b --- /dev/null +++ b/docs/conventions/event-handler-standard.md @@ -0,0 +1,130 @@ +--- +name: event-handler-standard +description: 事件处理规范 — Handler 必须实现 IHandler 接口并注册为 Spring Bean,事件必须实现 IEvent 接口 +status: 已实现 +scope: 后端 +source: 项目自有 +symbols: + - IHandler + - IEvent + - ISyncEvent + - IAsyncEvent + - EventPusher +content_hash: 0703329f337a48ff76c084efb93b0c0fcb3ef426ba4b201f81e0c28b646c7405 +--- + +## 解决什么问题 + +不遵守此规范会导致: +- Handler 无法被框架自动发现和注册 +- 事件类型混乱,同步/异步边界不清 +- 多个 Handler 执行顺序不可控 +- 事件循环引用导致系统崩溃 + +## 如何使用 + +### 事件定义规范 + +```java +// 事件必须实现 IEvent(或其子接口) +// 事件类应是不可变的数据载体 +public class OrderCreatedEvent implements ISyncEvent { + private final Long orderId; + private final Long userId; + + public OrderCreatedEvent(Long orderId, Long userId) { + this.orderId = orderId; + this.userId = userId; + } + + public Long getOrderId() { return orderId; } + public Long getUserId() { return userId; } +} +``` + +### 事件类型选择 + +| 接口 | 执行方式 | 适用场景 | +|------|----------|----------| +| `IEvent` | 同步(默认) | 一般业务事件 | +| `ISyncEvent` | 同步 | 需要保证执行顺序的事件 | +| `IAsyncEvent` | 异步(线程池) | 通知类、日志类非关键事件 | + +### Handler 规范 + +```java +// 1. 必须注册为 Spring Bean(@Service / @Component) +// 2. 泛型参数指定订阅的事件类型 +// 3. 不应在 Handler 中抛出未处理异常 +@Service +public class OrderNotifyHandler implements IHandler { + + @Override + public int order() { + return 10; // 排序值,数值小的先执行 + } + + @Override + public void handler(OrderCreatedEvent event) { + // 处理事件逻辑 + notifyService.sendOrderNotification(event.getOrderId()); + } + + @Override + public void error(Exception exception) throws Exception { + // 异常处理:记录日志但不影响其他 Handler + log.error("订单通知发送失败: {}", exception.getMessage()); + } +} +``` + +### 推送规范 + +```java +// ✅ 使用 EventPusher.push() 推送 +EventPusher.push(new OrderCreatedEvent(orderId, userId)); + +// ✅ 允许循环事件的场景(需谨慎) +EventPusher.push(event, true); + +// ❌ 不要直接使用 Spring ApplicationEventPublisher +applicationEventPublisher.publishEvent(event); // 绕过框架的事件管理 +``` + +## 使用实例 + +✅ **正确示例**: +```java +// 事件定义 — 不可变数据载体 +public class UserRegisteredEvent implements ISyncEvent { + private final Long userId; + public UserRegisteredEvent(Long userId) { this.userId = userId; } + public Long getUserId() { return userId; } +} + +// 推送 +EventPusher.push(new UserRegisteredEvent(user.getId())); + +// 订阅 — Spring Bean + IHandler 泛型 +@Service +public class WelcomeEmailHandler implements IHandler { + @Override + public void handler(UserRegisteredEvent event) { + emailService.sendWelcome(event.getUserId()); + } +} +``` + +❌ **错误示例**: +```java +// 事件包含可变状态 +public class BadEvent implements IEvent { + public List mutableList = new ArrayList<>(); // 不应暴露可变集合 +} + +// Handler 不是 Spring Bean — 不会被自动注册 +public class OrphanHandler implements IHandler { + @Override + public void handler(MyEvent event) { /* 永远不会被调用 */ } +} +``` diff --git a/docs/conventions/global-exception-handling.md b/docs/conventions/global-exception-handling.md new file mode 100644 index 000000000..baf628f78 --- /dev/null +++ b/docs/conventions/global-exception-handling.md @@ -0,0 +1,99 @@ +--- +name: global-exception-handling +description: 全局异常处理规范 — 业务异常必须使用 LocaleMessageException,禁止在 Controller 中 try-catch 吞没异常 +status: 已实现 +scope: 后端 +source: 项目自有 +symbols: + - LocaleMessageException + - BasicHandlerExceptionResolverConfiguration + - ServletExceptionHandler +content_hash: e3029dc411b34af5ea488d45c2ac414e2ea7578b571e681e0d7dc938ae75f052 +--- + +## 解决什么问题 + +不遵守此规范会导致: +- 异常堆栈信息直接暴露给前端,存在安全风险 +- 不同模块的错误响应格式不一致 +- 异常被 try-catch 吞没后,问题难以排查 +- 无法支持国际化错误消息 + +## 如何使用 + +### 异常类型规范 + +| 异常类型 | 使用场景 | +|----------|----------| +| `LocaleMessageException` | 所有业务异常(参数校验失败、数据不存在、状态错误等) | +| `IllegalArgumentException` | 框架级参数校验(不推荐在业务层使用) | +| `RuntimeException` | 不可预期的系统异常 | + +### 错误码命名规范 + +错误码采用 **英文点分隔** 的格式:`模块.实体.错误类型` + +```java +// ✅ 正确的错误码 +throw new LocaleMessageException("user.not.found", "用户不存在"); +throw new LocaleMessageException("order.status.invalid", "订单状态无效"); +throw new LocaleMessageException("auth.permission.denied", "权限不足"); + +// ❌ 错误的错误码 +throw new LocaleMessageException("USER_NOT_FOUND", "用户不存在"); // 不应大写 +throw new LocaleMessageException("userNotFound", "用户不存在"); // 不应驼峰 +``` + +### 异常处理规则 + +1. **Controller 层**:不 try-catch,直接抛出 `LocaleMessageException`,由全局处理器捕获 +2. **Service 层**:业务校验失败抛 `LocaleMessageException`,系统异常可包装后向上抛 +3. **基础设施层**:捕获底层异常,转换为 `LocaleMessageException` 向上传递 + +## 使用实例 + +✅ **正确示例**: +```java +@Service +public class UserService { + public User findById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new LocaleMessageException("user.not.found", "用户不存在")); + } + + public void delete(Long id) { + User user = findById(id); + if ("ADMIN".equals(user.getRole())) { + throw new LocaleMessageException("user.admin.cannot.delete", "管理员账户不可删除"); + } + userRepository.delete(user); + } +} + +@RestController +public class UserController { + @DeleteMapping("/{id}") + public Response delete(@PathVariable Long id) { + userService.delete(id); // 不 try-catch,异常由全局处理器捕获 + return Response.buildSuccess(); + } +} +``` + +❌ **错误示例**: +```java +// 在 Controller 中 try-catch 吞没异常 +@GetMapping("/{id}") +public SingleResponse get(@PathVariable Long id) { + try { + User user = userService.findById(id); + return SingleResponse.of(user); + } catch (Exception e) { + log.error("error", e); + return null; // 返回 null,前端无法识别错误 + } +} + +// 直接抛出原始异常 +throw new RuntimeException("数据库连接失败"); // 应包装为 LocaleMessageException +``` diff --git a/docs/conventions/index.md b/docs/conventions/index.md index 647519f8e..0b6ad7086 100644 --- a/docs/conventions/index.md +++ b/docs/conventions/index.md @@ -6,15 +6,11 @@ | 名称 | 描述 | 范围 | 来源 | |------|------|------|------| -| [autoconfiguration-pattern](./autoconfiguration-pattern.md) | 每个 starter 模块必须同时注册 spring.factories(旧版兼容)和 AutoConfiguration.imports(Spring ... | 后端 | 项目自有 | -| [context-register-pairing](./context-register-pairing.md) | 每个 Context 单例必须配套 Register 类和 Configuration 类,形成三件套:Context(持有状态)+ Register(注... | 后端 | 项目自有 | -| [context-singleton](./context-singleton.md) | 全局状态通过单例 Context 类持有,由 Register 类在启动时注入依赖 | 后端 | 项目自有 | -| [locale-exception-handling](./locale-exception-handling.md) | 所有业务异常必须使用 LocaleMessageException,通过 errCode 实现国际化,由全局 HandlerExceptionResolv... | 后端 | 项目自有 | -| [meta-table-annotation](./meta-table-annotation.md) | 通过 @MetaTable/@MetaColumn/@MetaRelation 注解声明表的元数据描述,驱动动态实体生成和元数据查询 | 后端 | 项目自有 | -| [servlet-exception-resolver](./servlet-exception-resolver.md) | 使用 HandlerExceptionResolver(而非 @ControllerAdvice)作为全局异常处理机制,返回统一 JSON 响应格式 | 后端 | 项目自有 | -| [slf4j-logging](./slf4j-logging.md) | 所有 Java 类必须使用 Lombok @Slf4j 注解进行日志输出,禁止直接使用 LoggerFactory.getLogger() | 全栈 | 项目自有 | -| [unified-response](./unified-response.md) | 所有 API 返回值必须使用统一响应封装(Response → SingleResponse / MultiResponse / MapResponse)... | 后端 | 项目自有 | +| [ddd-layered-architecture](./ddd-layered-architecture.md) | DDD 分层架构规范 — 遵循 interface → app → domain ← infra 四层依赖规则,domain 层定义接口,infra 层提供实现 | 后端 | 项目自有 | +| [event-handler-standard](./event-handler-standard.md) | 事件处理规范 — Handler 必须实现 IHandler 接口并注册为 Spring Bean,事件必须实现 IEvent 接口 | 后端 | 项目自有 | +| [global-exception-handling](./global-exception-handling.md) | 全局异常处理规范 — 业务异常必须使用 LocaleMessageException,禁止在 Controller 中 try-catch 吞没异常 | 后端 | 项目自有 | +| [unified-response-format](./unified-response-format.md) | 统一响应格式规范 — 所有 API 必须使用 Response/SingleResponse/MultiResponse 标准封装返回 | 后端 | 项目自有 | --- -**统计**: 共 8 篇 — 已实现 8 / 计划中 0 / 已废弃 0 +**统计**: 共 4 篇 — 已实现 4 / 计划中 0 / 已废弃 0 diff --git a/docs/conventions/locale-exception-handling.md b/docs/conventions/locale-exception-handling.md deleted file mode 100644 index d47af6441..000000000 --- a/docs/conventions/locale-exception-handling.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -name: locale-exception-handling -description: 所有业务异常必须使用 LocaleMessageException,通过 errCode 实现国际化,由全局 HandlerExceptionResolver 统一处理 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 0c4299a1 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/LocaleMessageException.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/servlet/BasicHandlerExceptionResolverConfiguration.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/exception/ExceptionConfiguration.java ---- - -## 解决什么问题 - -如果业务代码中随意抛出 `RuntimeException`、`IllegalArgumentException` 等原生异常,会导致以下问题: - -1. **错误信息无法国际化**:硬编码的中文字符串无法根据用户语言环境切换 -2. **缺少结构化错误码**:前端无法通过 `errCode` 精确判断错误类型并做差异化处理 -3. **异常响应格式不统一**:不同异常类型被 Spring 默认处理器转为不同的 HTTP 响应结构 -4. **敏感信息泄露**:未捕获的异常堆栈可能暴露内部实现细节给客户端 - -框架通过 `LocaleMessageException` + `BasicHandlerExceptionResolverConfiguration` 实现统一的国际化异常处理链: -- `LocaleMessageException` 携带 `errCode`,可选携带占位符参数 -- `LocaleMessage` 从 Spring `MessageSource`(`messages.properties`)读取国际化消息 -- `ServletExceptionHandler` 将所有异常统一转为 `{success, errCode, errMessage}` JSON 响应 - -## 如何使用 - -### 1. 定义国际化消息 - -在 `src/main/resources/messages.properties` 和 `messages_{locale}.properties` 中配置错误码对应的消息: - -```properties -# messages.properties (默认/英文) -user.not.found=User not found -order.amount.invalid=Order amount must be greater than {0} - -# messages_zh_CN.properties (中文) -user.not.found=用户不存在 -order.amount.invalid=订单金额必须大于 {0} -``` - -### 2. 抛出 LocaleMessageException - -| 构造方式 | 说明 | -|----------|------| -| `new LocaleMessageException("err.code")` | 仅指定错误码,自动从 MessageSource 查找消息 | -| `new LocaleMessageException("err.code", "fallback message")` | 指定错误码和回退消息(不走国际化) | -| `LocaleMessageException.of("err.code", arg1, arg2)` | 带占位符参数的国际化消息(推荐) | -| `new LocaleMessageException("err.code", new Object[]{arg1}, cause)` | 带参数和原始异常 | - -### 3. 规则 - -1. **所有业务异常必须使用 `LocaleMessageException`**,禁止直接抛出 `RuntimeException` 或其他原生异常 -2. 错误码采用点分隔的小写命名(如 `user.not.found`、`order.amount.invalid`) -3. 需要动态内容时使用占位符 `{0}`、`{1}`,通过 `LocaleMessageException.of()` 传入参数 -4. Controller 方法无需 try-catch,由全局 `ServletExceptionHandler` 统一处理并返回标准错误响应 - -## 使用实例 - -### ✅ 正确示例 - -```java -// 1. 在 Service 层抛出国际化异常 -@Service -public class UserService { - - public UserEntity getById(Long id) { - return userRepository.findById(id) - .orElseThrow(() -> new LocaleMessageException("user.not.found")); - } - - public void createOrder(BigDecimal amount) { - if (amount.compareTo(BigDecimal.ZERO) <= 0) { - throw LocaleMessageException.of("order.amount.invalid", amount); - } - } -} - -// 2. Controller 无需手动处理异常,返回类型仍为统一响应 -@RestController -@RequestMapping("/api/users") -public class UserController { - - @GetMapping("/{id}") - public SingleResponse getById(@PathVariable Long id) { - return SingleResponse.of(userService.getById(id)); - } -} - -// 3. 当 user.not.found 触发时,框架自动返回: -// {"success": false, "errCode": "user.not.found", "errMessage": "用户不存在"} -``` - -### ❌ 错误示例 - -```java -// 错误:直接抛出 RuntimeException,无错误码、不支持国际化 -public UserEntity getById(Long id) { - return userRepository.findById(id) - .orElseThrow(() -> new RuntimeException("用户不存在")); -} - -// 错误:使用 IllegalArgumentException,前端无法识别错误类型 -public void createOrder(BigDecimal amount) { - if (amount.compareTo(BigDecimal.ZERO) <= 0) { - throw new IllegalArgumentException("金额无效"); - } -} - -// 错误:在 Controller 中手动 try-catch 构建错误响应 -@GetMapping("/{id}") -public Response getById(@PathVariable Long id) { - try { - UserEntity user = userService.getById(id); - return SingleResponse.of(user); - } catch (Exception e) { - return Response.buildFailure("error", e.getMessage()); - } -} - -// 错误:自定义异常类但未继承 LocaleMessageException -public class BusinessException extends RuntimeException { - // 不会被全局异常处理器识别为业务异常 -} -``` diff --git a/docs/conventions/meta-table-annotation.md b/docs/conventions/meta-table-annotation.md deleted file mode 100644 index 0cc14b1dc..000000000 --- a/docs/conventions/meta-table-annotation.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -name: meta-table-annotation -description: 通过 @MetaTable/@MetaColumn/@MetaRelation 注解声明表的元数据描述,驱动动态实体生成和元数据查询 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: a59736ac -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/annotation/MetaTable.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/annotation/MetaColumn.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/annotation/MetaRelation.java ---- - -## 解决什么问题 - -在 fast-repository 模块的动态实体生成场景中(如 Groovy 脚本定义的数据表、运行时创建的临时查询实体),需要在没有 JPA Entity 类的情况下描述一张数据库表的结构。传统的做法是手动构建 `TableEntityMetadata` 对象并逐个添加列信息,代码冗长且容易出错: - -```java -// 手动构建元数据 — 繁琐、易错 -TableEntityMetadata metadata = new TableEntityMetadata("com.example.DynamicUser"); -metadata.setTable("t_user", "用户表"); -metadata.addPrimaryKeyColumn(Long.class, "id", "id", GenerationType.IDENTITY, ...); -metadata.addColumn(String.class, "name", "name", "姓名", false, false, true, ...); -``` - -`@MetaTable` / `@MetaColumn` / `@MetaRelation` 注解体系提供了一种**声明式**的元数据描述方式,将表结构信息直接标注在 POJO 类上,由框架自动扫描并转换为 `TableEntityMetadata`,大幅简化了动态实体的定义流程。 - -## 如何使用 - -### 注解说明 - -#### @MetaTable(类级别) - -标注在类上,声明该类对应的数据库表信息。 - -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `name` | String | 是 | 数据库表名称 | -| `desc` | String | 是 | 表的中文说明/备注 | - -#### @MetaColumn(字段级别) - -标注在字段上,声明该字段对应的数据库列信息。 - -| 属性 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `name` | String | 是 | — | 数据库列名称 | -| `desc` | String | 是 | — | 列的中文说明/备注 | -| `primaryKey` | boolean | 否 | `false` | 是否为主键 | -| `type` | ColumnType | 否 | `ColumnType.String` | 字段数据类型 | -| `format` | String | 否 | `""` | 格式化模板(如日期格式) | -| `dependent` | MetaRelation | 否 | 空关联 | 外键依赖关系 | - -#### @MetaRelation(嵌套在 @MetaColumn 中) - -声明字段的外键关联关系,作为 `@MetaColumn.dependent` 的值使用。 - -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `tableName` | String | 是 | 关联的目标表名 | -| `columnName` | String | 是 | 关联的目标列名 | - -#### ColumnType 枚举 - -| 值 | 说明 | -|----|------| -| `Number` | 整数 | -| `Float` | 浮点数 | -| `String` | 字符串 | -| `Date` | 日期 | -| `File` | 文件 | -| `Boolean` | 布尔 | -| `Bytes` | 字节数组 | -| `JSON` | JSON 对象 | -| `Any` | 任意类型 | - -### 命名规范 - -- 注解类统一放在 `com.codingapi.springboot.framework.annotation` 包下 -- `@MetaTable` 只能用在类上(`@Target(ElementType.TYPE)`) -- `@MetaColumn` 只能用在字段上(`@Target(ElementType.FIELD)`) -- 所有注解均为 `@Retention(RetentionPolicy.RUNTIME)`,支持运行时反射读取 - -## 使用实例 - -```java -// ✅ 正确 — 完整的元数据注解声明 -@MetaTable(name = "t_leave_request", desc = "请假申请表") -public class LeaveRequestMeta { - - @MetaColumn(name = "id", desc = "主键ID", primaryKey = true, type = ColumnType.Number) - private Long id; - - @MetaColumn(name = "user_id", desc = "申请人ID", type = ColumnType.Number, - dependent = @MetaRelation(tableName = "t_user", columnName = "id")) - private Long userId; - - @MetaColumn(name = "start_date", desc = "开始日期", type = ColumnType.Date, format = "yyyy-MM-dd") - private String startDate; - - @MetaColumn(name = "end_date", desc = "结束日期", type = ColumnType.Date, format = "yyyy-MM-dd") - private String endDate; - - @MetaColumn(name = "reason", desc = "请假原因", type = ColumnType.String) - private String reason; - - @MetaColumn(name = "approved", desc = "是否批准", type = ColumnType.Boolean) - private Boolean approved; -} - -// ✅ 正确 — 最简声明(使用默认值) -@MetaTable(name = "t_dict", desc = "数据字典") -public class DictMeta { - - @MetaColumn(name = "id", desc = "主键", primaryKey = true) - private Long id; - - @MetaColumn(name = "code", desc = "编码") - private String code; - - @MetaColumn(name = "label", desc = "显示文本") - private String label; -} - -// ❌ 错误 — 缺少 @MetaTable 注解 -public class BadMeta { - @MetaColumn(name = "id", desc = "主键", primaryKey = true) - private Long id; -} - -// ❌ 错误 — @MetaColumn 缺少必填属性 name 和 desc -@MetaTable(name = "t_test", desc = "测试表") -public class IncompleteMeta { - @MetaColumn(desc = "主键") // 缺少 name!编译报错 - private Long id; -} - -// ❌ 错误 — 不要混用 JPA 注解代替 Meta 注解 -@Entity // 这是 JPA 注解,不是元数据注解 -@Table(name = "t_user") -public class UserMeta { - @Id - @Column(name = "id") - private Long id; -} -``` diff --git a/docs/conventions/servlet-exception-resolver.md b/docs/conventions/servlet-exception-resolver.md deleted file mode 100644 index 6ca773d9c..000000000 --- a/docs/conventions/servlet-exception-resolver.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: servlet-exception-resolver -description: 使用 HandlerExceptionResolver(而非 @ControllerAdvice)作为全局异常处理机制,返回统一 JSON 响应格式 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 9c0b4ac5 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/servlet/BasicHandlerExceptionResolverConfiguration.java ---- - -## 解决什么问题 - -Spring MVC 中常见的全局异常处理方式是通过 `@ControllerAdvice` + `@ExceptionHandler` 注解实现。这种方式存在以下局限: - -1. **与 Controller 层强耦合**:`@ControllerAdvice` 本质上是一个增强型 Controller,其生命周期和优先级受 DispatcherServlet 的 HandlerAdapter 调度影响,在某些边界场景(如 Filter 链异常、非标准 Handler 类型)下可能无法捕获所有异常。 -2. **多 Advice 优先级冲突**:当项目中引入多个第三方库各自提供 `@ControllerAdvice` 时,需要通过 `@Order` 手动协调优先级,容易产生遗漏或覆盖。 -3. **响应格式不统一**:各 `@ExceptionHandler` 方法可能返回不同的响应结构,导致前端需要适配多种错误格式。 - -本框架采用 Servlet 层面的 `HandlerExceptionResolver` 接口作为全局异常处理入口,直接注册为 Spring Bean,由 DispatcherServlet 在异常解析阶段统一调用,确保所有 Controller 异常都能被拦截并输出统一的 `{success, errCode, errMessage}` JSON 格式。 - -## 如何使用 - -### 核心实现类 - -`BasicHandlerExceptionResolverConfiguration` 是框架内置的自动配置类,通过 `@Bean` 注册一个 `HandlerExceptionResolver` 实例(内部类 `ServletExceptionHandler`)。 - -### 异常处理规则 - -| 异常类型 | errCode | errMessage | 日志级别 | -|---------|---------|------------|---------| -| `LocaleMessageException` | 异常自带的 `errCode` | 国际化消息内容 | WARN | -| 其他异常 | `"system.err"` | `ex.getMessage()` | WARN(含堆栈) | - -### 响应格式 - -所有异常均通过 `MappingJackson2JsonView` 输出以下 JSON 结构: - -```json -{ - "success": false, - "errCode": "错误码", - "errMessage": "错误描述" -} -``` - -### 业务异常抛出方式 - -业务代码应使用 `LocaleMessageException` 抛出异常,支持国际化错误码: - -```java -// 直接使用错误码(从 message.properties 读取消息) -throw new LocaleMessageException("user.not.found"); - -// 带参数的错误码 -throw LocaleMessageException.of("field.invalid", fieldName); - -// 自定义错误消息 -throw new LocaleMessageException("custom.error", "自定义错误信息"); -``` - -### 扩展须知 - -如需新增自定义异常处理逻辑,应修改 `ServletExceptionHandler.resolveException()` 方法中的判断分支,而不是新建额外的 `@ControllerAdvice` 类。这保证了异常处理入口的唯一性。 - -## 使用实例 - -```java -// ✅ 正确 — 业务代码抛出 LocaleMessageException,由框架统一处理 -@GetMapping("/users/{id}") -public SingleResponse getUser(@PathVariable Long id) { - User user = userRepository.findById(id).orElse(null); - if (user == null) { - throw new LocaleMessageException("user.not.found"); - } - return Response.of(user); -} -// 异常被 ServletExceptionHandler 捕获,返回: -// {"success": false, "errCode": "user.not.found", "errMessage": "用户不存在"} - -// ✅ 正确 — 未预期异常也会被捕获 -@PostMapping("/orders") -public SingleResponse createOrder(@RequestBody OrderRequest request) { - // 某个 NPE 或 IllegalArgumentException - return Response.of(orderService.create(request)); -} -// 返回:{"success": false, "errCode": "system.err", "errMessage": "..."} - -// ❌ 错误 — 不要使用 @ControllerAdvice 处理全局异常 -@RestControllerAdvice -public class GlobalExceptionHandler { - @ExceptionHandler(Exception.class) - public ResponseEntity> handleException(Exception ex) { - Map body = new HashMap<>(); - body.put("code", 500); // 格式与框架不一致! - body.put("msg", ex.getMessage()); - return ResponseEntity.status(500).body(body); - } -} - -// ❌ 错误 — 不要在 Controller 中自行 try-catch 并构造响应 -@GetMapping("/users/{id}") -public Map getUser(@PathVariable Long id) { - try { - User user = userService.findById(id); - return Map.of("data", user); - } catch (Exception e) { - return Map.of("error", e.getMessage()); // 绕过了框架异常处理! - } -} -``` diff --git a/docs/conventions/slf4j-logging.md b/docs/conventions/slf4j-logging.md deleted file mode 100644 index 7bab85aff..000000000 --- a/docs/conventions/slf4j-logging.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -name: slf4j-logging -description: 所有 Java 类必须使用 Lombok @Slf4j 注解进行日志输出,禁止直接使用 LoggerFactory.getLogger() -status: 已实现 -scope: 全栈 -source: 项目自有 -last_commit: N/A -code_files: [] ---- - -## 解决什么问题 - -如果每个类都手动创建 Logger 实例,会导致以下问题: - -1. **样板代码冗余**:每个类都需要声明 `private static final Logger log = LoggerFactory.getLogger(XxxClass.class)`,增加无意义的代码量 -2. **类名引用易出错**:复制粘贴时忘记修改 `getLogger()` 中的类名参数,导致日志输出错误的来源标识 -3. **重构风险**:重命名类后若未同步修改 Logger 声明,日志追踪将指向旧类名 -4. **风格不一致**:部分开发者使用 `Logger`、部分使用 `Log`、部分使用 `logger` 等不同的变量命名 - -Lombok 的 `@Slf4j` 注解在编译期自动生成 `private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(当前类.class);`,消除上述所有问题。 - -## 如何使用 - -### 规则 - -1. **所有需要日志输出的 Java 类必须在类级别添加 `@Slf4j` 注解** -2. 使用生成的 `log` 变量进行日志输出(`log.info()`、`log.warn()`、`log.error()` 等) -3. **禁止**在代码中出现 `LoggerFactory.getLogger()` 调用 -4. **禁止**手动声明 `Logger` / `Log` 字段 -5. 日志消息使用 SLF4J 占位符 `{}` 格式,不使用字符串拼接 -6. 异常日志应将异常对象作为最后一个参数传入,而非调用 `e.getMessage()` - -### 注意事项 - -- `@Slf4j` 是 Lombok 注解,需确保项目中已引入 Lombok 依赖且 IDE 已安装 Lombok 插件 -- 对于内部静态类,Lombok 会自动生成对应内部类的 Logger,无需额外处理 -- 如需自定义 Logger 名称(极少数场景),可使用 `@Slf4j(topic = "custom.topic")` - -## 使用实例 - -### ✅ 正确示例 - -```java -@Slf4j -@Service -public class UserService { - - public UserEntity getById(Long id) { - log.info("查询用户, id={}", id); - UserEntity user = userRepository.findById(id) - .orElseThrow(() -> new LocaleMessageException("user.not.found")); - log.debug("用户查询成功, id={}, name={}", id, user.getName()); - return user; - } - - public void deleteUser(Long id) { - try { - userRepository.deleteById(id); - log.info("用户删除成功, id={}", id); - } catch (Exception e) { - // 异常对象作为最后一个参数,SLF4J 会自动打印堆栈 - log.error("用户删除失败, id={}", id, e); - throw e; - } - } -} - -// 内部静态类也可正常使用 -@Configuration -public class BasicHandlerExceptionResolverConfiguration { - - @Slf4j - public static class ServletExceptionHandler implements HandlerExceptionResolver { - @Override - public ModelAndView resolveException(...) { - log.warn("controller exception:{}", ex.getLocalizedMessage(), ex); - // ... - } - } -} -``` - -### ❌ 错误示例 - -```java -// 错误:手动创建 Logger,存在样板代码和类名引用风险 -@Service -public class UserService { - private static final Logger logger = LoggerFactory.getLogger(UserService.class); - - public UserEntity getById(Long id) { - logger.info("查询用户, id=" + id); // 还使用了字符串拼接 - // ... - } -} - -// 错误:使用不同变量名,风格不统一 -@Service -public class OrderService { - private static final Log LOG = LogFactory.getLog(OrderService.class); - // ... -} - -// 错误:异常日志未传入异常对象 -try { - doSomething(); -} catch (Exception e) { - log.error("操作失败: " + e.getMessage()); // 丢失堆栈信息 -} - -// 错误:缺少 @Slf4j 注解却直接使用 log 变量(编译报错) -@Service -public class PaymentService { - public void pay() { - log.info("开始支付"); // 编译错误:找不到符号 log - } -} -``` diff --git a/docs/conventions/unified-response-format.md b/docs/conventions/unified-response-format.md new file mode 100644 index 000000000..3b5dcbce6 --- /dev/null +++ b/docs/conventions/unified-response-format.md @@ -0,0 +1,94 @@ +--- +name: unified-response-format +description: 统一响应格式规范 — 所有 API 必须使用 Response/SingleResponse/MultiResponse 标准封装返回 +status: 已实现 +scope: 后端 +source: 项目自有 +symbols: + - Response + - SingleResponse + - MultiResponse + - MapResponse +content_hash: 1dfe2559a6b514effb51735ca8ca49aba308959e02814c5862b842eed3b04fb1 +--- + +## 解决什么问题 + +不遵守此规范会导致: +- 前端需要针对不同的 API 返回格式做多种适配 +- 错误响应格式不一致,前端无法统一处理错误提示 +- 分页数据结构不统一,增加前端列表组件的复杂度 + +## 如何使用 + +### 响应类型选择规则 + +| 场景 | 使用类型 | 说明 | +|------|----------|------| +| 无返回数据的操作 | `Response` | 创建/更新/删除操作 | +| 返回单个对象 | `SingleResponse` | 详情查询、单条记录 | +| 返回列表/分页 | `MultiResponse` | 列表查询、分页查询 | +| 返回 Map 结构 | `MapResponse` | 键值对数据 | + +### 分页数据格式 + +分页数据必须使用 `MultiResponse.of(Page)` 构建,结构为: +```json +{ + "success": true, + "data": { + "total": 100, + "list": [...] + } +} +``` + +### 错误响应格式 + +```json +{ + "success": false, + "errCode": "业务错误码(英文点分隔)", + "errMessage": "用户可读的错误描述" +} +``` + +## 使用实例 + +✅ **正确示例**: +```java +@GetMapping("/{id}") +public SingleResponse get(@PathVariable Long id) { + return SingleResponse.of(userService.findById(id)); +} + +@GetMapping +public MultiResponse list() { + Page page = userRepository.findAll(PageRequest.of(0, 20)); + return MultiResponse.of(page); +} + +@PostMapping +public Response create(@RequestBody UserDTO dto) { + userService.create(dto); + return Response.buildSuccess(); +} +``` + +❌ **错误示例**: +```java +// 直接返回实体 +@GetMapping("/{id}") +public User get(@PathVariable Long id) { + return userService.findById(id); +} + +// 自定义 Map 返回 +@GetMapping +public Map list() { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("data", users); + return result; +} +``` diff --git a/docs/conventions/unified-response.md b/docs/conventions/unified-response.md deleted file mode 100644 index 35c9292bc..000000000 --- a/docs/conventions/unified-response.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -name: unified-response -description: 所有 API 返回值必须使用统一响应封装(Response → SingleResponse / MultiResponse / MapResponse),禁止直接返回裸对象 -status: 已实现 -scope: 后端 -source: 项目自有 -last_commit: 841c49b8 -code_files: - - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/Response.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/SingleResponse.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/MultiResponse.java - - springboot-starter/src/main/java/com/codingapi/springboot/framework/dto/response/MapResponse.java ---- - -## 解决什么问题 - -如果 Controller 直接返回裸对象(如 `UserEntity`、`List`、`Map`),会导致以下问题: - -1. **前后端协议不一致**:不同接口返回结构各异,前端无法统一解析成功/失败状态 -2. **错误信息缺失**:裸对象不包含 `errCode` / `errMessage`,异常处理与正常响应格式不统一 -3. **分页元数据丢失**:直接返回 `List` 时缺少 `total` 等分页信息,前端无法正确渲染分页组件 -4. **扩展困难**:后续如需增加请求 ID、时间戳等通用字段,需逐个修改所有接口 - -## 如何使用 - -框架提供四种响应类型,均继承自 `Response` 基类: - -| 类型 | 用途 | 工厂方法 | -|------|------|----------| -| `Response` | 无数据的操作结果(仅 success/errCode/errMessage) | `Response.buildSuccess()` / `Response.buildFailure(errCode, errMessage)` | -| `SingleResponse` | 返回单个对象 | `SingleResponse.of(data)` / `SingleResponse.empty()` | -| `MultiResponse` | 返回列表(支持分页) | `MultiResponse.of(collection, total)` / `MultiResponse.of(page)` / `MultiResponse.of(collection)` / `MultiResponse.empty()` | -| `MapResponse` | 返回键值对数据 | `MapResponse.create().add(key, value)` / `MapResponse.empty()` | - -### 规则 - -1. **Controller 方法的返回类型必须是上述四种之一**,不得返回实体类、集合或 Map 裸对象 -2. 成功时使用 `of()` / `buildSuccess()` 等静态工厂方法创建响应 -3. 失败时优先通过抛出 `LocaleMessageException` 由全局异常处理器自动构建错误响应;仅在非异常场景下手动调用 `Response.buildFailure()` -4. `MultiResponse` 接收 Spring Data `Page` 对象时,使用 `MultiResponse.of(page)` 自动提取 `content` 和 `totalElements` - -## 使用实例 - -### ✅ 正确示例 - -```java -@RestController -@RequestMapping("/api/users") -public class UserQueryController { - - @Autowired - private UserQueryService userQueryService; - - // 返回单个对象 - @GetMapping("/{id}") - public SingleResponse getById(@PathVariable Long id) { - return SingleResponse.of(userQueryService.getById(id)); - } - - // 返回列表(带分页) - @PostMapping("/list") - public MultiResponse list(@RequestBody SearchRequest searchRequest) { - return MultiResponse.of(userQueryService.list(searchRequest)); - } - - // 返回键值对 - @GetMapping("/stats") - public MapResponse stats() { - return MapResponse.create() - .add("totalUsers", 100) - .add("activeUsers", 85); - } - - // 无数据操作 - @DeleteMapping("/{id}") - public Response delete(@PathVariable Long id) { - userCommandService.delete(id); - return Response.buildSuccess(); - } -} -``` - -### ❌ 错误示例 - -```java -@RestController -@RequestMapping("/api/users") -public class UserQueryController { - - // 错误:直接返回实体类 - @GetMapping("/{id}") - public UserEntity getById(@PathVariable Long id) { - return userQueryService.getById(id); - } - - // 错误:直接返回 List,缺少 total 和统一包装 - @GetMapping("/list") - public List list() { - return userQueryService.listAll(); - } - - // 错误:直接返回 Map - @GetMapping("/stats") - public Map stats() { - Map map = new HashMap<>(); - map.put("totalUsers", 100); - return map; - } - - // 错误:使用 ResponseEntity 包装裸对象 - @DeleteMapping("/{id}") - public ResponseEntity delete(@PathVariable Long id) { - userCommandService.delete(id); - return ResponseEntity.ok().build(); - } -} -``` From 25e74acecfe0ad3dabb3a7246efccba5e2b5bb30 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 8 Jun 2026 21:29:11 +0800 Subject: [PATCH 09/10] =?UTF-8?q?docs:=20=E9=87=8D=E5=BB=BA=20PKR=20?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E6=96=87=E6=A1=A3=EF=BC=8821?= =?UTF-8?q?=E7=AF=87=E8=83=BD=E5=8A=9B=20+=204=E7=AF=87=E8=A7=84=E8=8C=83?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 PKR 文档结构,从扁平目录改为模块化子目录: 能力文档(22篇): - springboot-starter (7): event-system, response-dto, domain-proxy, page-request, exception-handling, locale-message, rest-client - springboot-starter-security (1): auth-gateway - springboot-starter-data-fast (1): fast-repository - springboot-starter-data-authorization (1): sql-interception - springboot-starter-flow (1): workflow-engine - springboot-starter-script (1): groovy-runtime - 框架能力 (10): springboot/ioc-container, springboot/data-jpa, springboot/security, alibaba/fastjson, jsonwebtoken/jjwt, apache/groovy, jsqlparser/jsqlparser, esotericsoftware/kryo, react/react, redux/toolkit 规范文档(5篇): - event-driven-convention, response-convention, ddd-layered-architecture, exception-handling-convention, dynamic-query-convention 导出文档(17篇): - docs/agents/ 供下游项目导入使用 更新 CLAUDE.md:注入 PKR 知识查阅约束 Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 10 +- .../sql-interception.md | 215 ++++++++++++ .../fast-repository.md | 168 ++++++++++ .../workflow-engine.md | 147 +++++++++ .../groovy-runtime.md | 158 +++++++++ .../auth-gateway.md | 195 +++++++++++ .../springboot-starter/domain-proxy.md | 126 +++++++ .../springboot-starter/event-system.md | 272 ++++++++++++++++ .../springboot-starter/exception-handling.md | 139 ++++++++ .../springboot-starter/locale-message.md | 117 +++++++ .../springboot-starter/page-request.md | 177 ++++++++++ .../springboot-starter/response-dto.md | 172 ++++++++++ .../springboot-starter/rest-client.md | 108 ++++++ .../dynamic-query-convention.md | 148 +++++++++ .../ddd-layered-architecture.md | 225 +++++++++++++ .../event-driven-convention.md | 217 ++++++++++++ .../exception-handling-convention.md | 152 +++++++++ .../springboot-starter/response-convention.md | 156 +++++++++ docs/capabilities/alibaba/fastjson.md | 172 ++++++++++ docs/capabilities/apache/groovy.md | 178 ++++++++++ docs/capabilities/data-authorization.md | 95 ------ docs/capabilities/domain-proxy.md | 79 ----- docs/capabilities/dynamic-data-query.md | 106 ------ docs/capabilities/esotericsoftware/kryo.md | 167 ++++++++++ docs/capabilities/event-system.md | 104 ------ docs/capabilities/global-exception-handler.md | 64 ---- docs/capabilities/groovy-runtime.md | 57 ---- docs/capabilities/groovy-script-engine.md | 93 ------ docs/capabilities/index.md | 45 +-- docs/capabilities/jjwt.md | 69 ---- docs/capabilities/jsonwebtoken/jjwt.md | 188 +++++++++++ docs/capabilities/jsqlparser.md | 62 ---- docs/capabilities/jsqlparser/jsqlparser.md | 138 ++++++++ docs/capabilities/jwt-auth-gateway.md | 96 ------ docs/capabilities/kryo.md | 62 ---- docs/capabilities/react/react.md | 174 ++++++++++ docs/capabilities/redux/toolkit.md | 308 ++++++++++++++++++ docs/capabilities/rest-client.md | 86 ----- docs/capabilities/spring-data-jpa.md | 68 ---- docs/capabilities/spring-framework.md | 65 ---- docs/capabilities/spring-security.md | 71 ---- .../sql-interception.md | 221 +++++++++++++ .../fast-repository.md | 170 ++++++++++ .../workflow-engine.md | 154 +++++++++ .../groovy-runtime.md | 164 ++++++++++ .../auth-gateway.md | 201 ++++++++++++ .../springboot-starter/domain-proxy.md | 134 ++++++++ .../springboot-starter/event-system.md | 283 ++++++++++++++++ .../springboot-starter/exception-handling.md | 142 ++++++++ .../springboot-starter/locale-message.md | 121 +++++++ .../springboot-starter/page-request.md | 182 +++++++++++ .../springboot-starter/response-dto.md | 177 ++++++++++ .../springboot-starter/rest-client.md | 111 +++++++ docs/capabilities/springboot/data-jpa.md | 211 ++++++++++++ docs/capabilities/springboot/ioc-container.md | 218 +++++++++++++ docs/capabilities/springboot/security.md | 231 +++++++++++++ docs/capabilities/unified-response.md | 87 ----- docs/capabilities/workflow-engine.md | 104 ------ docs/conventions/ddd-layered-architecture.md | 129 -------- docs/conventions/event-handler-standard.md | 130 -------- docs/conventions/global-exception-handling.md | 99 ------ docs/conventions/index.md | 15 +- .../dynamic-query-convention.md | 148 +++++++++ .../ddd-layered-architecture.md | 224 +++++++++++++ .../event-driven-convention.md | 217 ++++++++++++ .../exception-handling-convention.md | 152 +++++++++ .../springboot-starter/response-convention.md | 156 +++++++++ docs/conventions/unified-response-format.md | 94 ------ 68 files changed, 7872 insertions(+), 1852 deletions(-) create mode 100644 docs/agents/capabilities/springboot-starter-data-authorization/sql-interception.md create mode 100644 docs/agents/capabilities/springboot-starter-data-fast/fast-repository.md create mode 100644 docs/agents/capabilities/springboot-starter-flow/workflow-engine.md create mode 100644 docs/agents/capabilities/springboot-starter-script/groovy-runtime.md create mode 100644 docs/agents/capabilities/springboot-starter-security/auth-gateway.md create mode 100644 docs/agents/capabilities/springboot-starter/domain-proxy.md create mode 100644 docs/agents/capabilities/springboot-starter/event-system.md create mode 100644 docs/agents/capabilities/springboot-starter/exception-handling.md create mode 100644 docs/agents/capabilities/springboot-starter/locale-message.md create mode 100644 docs/agents/capabilities/springboot-starter/page-request.md create mode 100644 docs/agents/capabilities/springboot-starter/response-dto.md create mode 100644 docs/agents/capabilities/springboot-starter/rest-client.md create mode 100644 docs/agents/conventions/springboot-starter-data-fast/dynamic-query-convention.md create mode 100644 docs/agents/conventions/springboot-starter/ddd-layered-architecture.md create mode 100644 docs/agents/conventions/springboot-starter/event-driven-convention.md create mode 100644 docs/agents/conventions/springboot-starter/exception-handling-convention.md create mode 100644 docs/agents/conventions/springboot-starter/response-convention.md create mode 100644 docs/capabilities/alibaba/fastjson.md create mode 100644 docs/capabilities/apache/groovy.md delete mode 100644 docs/capabilities/data-authorization.md delete mode 100644 docs/capabilities/domain-proxy.md delete mode 100644 docs/capabilities/dynamic-data-query.md create mode 100644 docs/capabilities/esotericsoftware/kryo.md delete mode 100644 docs/capabilities/event-system.md delete mode 100644 docs/capabilities/global-exception-handler.md delete mode 100644 docs/capabilities/groovy-runtime.md delete mode 100644 docs/capabilities/groovy-script-engine.md delete mode 100644 docs/capabilities/jjwt.md create mode 100644 docs/capabilities/jsonwebtoken/jjwt.md delete mode 100644 docs/capabilities/jsqlparser.md create mode 100644 docs/capabilities/jsqlparser/jsqlparser.md delete mode 100644 docs/capabilities/jwt-auth-gateway.md delete mode 100644 docs/capabilities/kryo.md create mode 100644 docs/capabilities/react/react.md create mode 100644 docs/capabilities/redux/toolkit.md delete mode 100644 docs/capabilities/rest-client.md delete mode 100644 docs/capabilities/spring-data-jpa.md delete mode 100644 docs/capabilities/spring-framework.md delete mode 100644 docs/capabilities/spring-security.md create mode 100644 docs/capabilities/springboot-starter-data-authorization/sql-interception.md create mode 100644 docs/capabilities/springboot-starter-data-fast/fast-repository.md create mode 100644 docs/capabilities/springboot-starter-flow/workflow-engine.md create mode 100644 docs/capabilities/springboot-starter-script/groovy-runtime.md create mode 100644 docs/capabilities/springboot-starter-security/auth-gateway.md create mode 100644 docs/capabilities/springboot-starter/domain-proxy.md create mode 100644 docs/capabilities/springboot-starter/event-system.md create mode 100644 docs/capabilities/springboot-starter/exception-handling.md create mode 100644 docs/capabilities/springboot-starter/locale-message.md create mode 100644 docs/capabilities/springboot-starter/page-request.md create mode 100644 docs/capabilities/springboot-starter/response-dto.md create mode 100644 docs/capabilities/springboot-starter/rest-client.md create mode 100644 docs/capabilities/springboot/data-jpa.md create mode 100644 docs/capabilities/springboot/ioc-container.md create mode 100644 docs/capabilities/springboot/security.md delete mode 100644 docs/capabilities/unified-response.md delete mode 100644 docs/capabilities/workflow-engine.md delete mode 100644 docs/conventions/ddd-layered-architecture.md delete mode 100644 docs/conventions/event-handler-standard.md delete mode 100644 docs/conventions/global-exception-handling.md create mode 100644 docs/conventions/springboot-starter-data-fast/dynamic-query-convention.md create mode 100644 docs/conventions/springboot-starter/ddd-layered-architecture.md create mode 100644 docs/conventions/springboot-starter/event-driven-convention.md create mode 100644 docs/conventions/springboot-starter/exception-handling-convention.md create mode 100644 docs/conventions/springboot-starter/response-convention.md delete mode 100644 docs/conventions/unified-response-format.md diff --git a/CLAUDE.md b/CLAUDE.md index c7d4461e2..551e19509 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,7 +156,6 @@ codingapi.framework.handler-thread-pool-size=20 # 事件异步线程池大小 见根 `pom.xml` 的 `` 区块。主要版本:Groovy 4.0.24、JSqlParser 5.0、Fastjson 2.0.53、JJWT 0.12.6、H2 2.3.232、Kryo 5.6.2。 - ## PKR 知识查阅(编码前必须) @@ -186,9 +185,10 @@ codingapi.framework.handler-thread-pool-size=20 # 事件异步线程池大小 | 命令 | 用途 | |------|------| -| `/pkr-init` | 首次扫描项目,发现候选能力和规范 | +| `/pkr-init` | 扫描项目,发现候选能力和规范(自动跳过已有文档) | | `/pkr-sync` | 全量同步,对比代码变更 | -| `/pkr-update [desc]` | 单项更新,可带描述指导更新 | -| `/pkr-add [name] ` | 从代码/框架扫描注册(名称可省略) | -| `/pkr-add plan [name] ` | 注册计划中的能力(名称可省略) | +| `/pkr-update / [desc]` | 单项更新,可带描述指导更新 | +| `/pkr-add / ` | 从代码/框架扫描注册 | +| `/pkr-add plan / ` | 注册计划中的能力 | +| `/pkr-export ...` | 导出模块文档供其他项目使用 | diff --git a/docs/agents/capabilities/springboot-starter-data-authorization/sql-interception.md b/docs/agents/capabilities/springboot-starter-data-authorization/sql-interception.md new file mode 100644 index 000000000..93abe1e4e --- /dev/null +++ b/docs/agents/capabilities/springboot-starter-data-authorization/sql-interception.md @@ -0,0 +1,215 @@ +--- +name: springboot-starter-data-authorization/sql-interception +module: springboot-starter-data-authorization +description: SQL 拦截数据权限,通过 JDBC 代理透明注入权限条件实现行级数据过滤 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter-data-authorization +import: "com.codingapi.springboot:springboot-starter-data-authorization" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级应用中,不同角色的用户只能看到自己有权限访问的数据(行级数据权限)。传统实现方式存在以下痛点: + +- **侵入性强**:需要在每个 Service/Repository 方法中手动拼接权限条件,代码散落各处 +- **容易遗漏**:新增查询接口时可能忘记添加权限过滤,导致数据泄露 +- **维护困难**:权限规则变更时需要修改大量业务代码 +- **与业务耦合**:权限逻辑与业务查询逻辑混杂,违反单一职责原则 + +SQL 拦截数据权限通过 JDBC 代理层实现了完全透明的权限注入: + +- **零侵入**:业务代码无需任何修改,所有 SELECT 查询自动注入权限条件 +- **全覆盖**:无论通过 JPA、MyBatis、原生 JDBC 还是其他 ORM 框架执行的 SQL,都会被统一拦截 +- **灵活配置**:通过 `RowHandler` 接口按表名自定义权限规则,支持 WHERE 条件和 JOIN 关联两种注入模式 +- **可跳过**:提供 `skipDataAuthorization()` API,允许特定场景下临时绕过权限检查 +- **SQL 解析增强**:使用 JSqlParser 解析 SQL AST,精确识别表名和别名,支持子查询、UNION、JOIN 等复杂 SQL 结构 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-data-authorization + +``` + +### 2. 架构概览 + +整个拦截链路由以下组件构成: + +``` +DataSource → ConnectionProxy → PreparedStatementProxy / StatementProxy + ↓ + SQLRunningContext.intercept(sql) + ↓ + SQLInterceptor (DefaultSQLInterceptor) + ↓ + DataPermissionSQLEnhancer (JSqlParser) + ↓ + RowHandler.handler(tableName, alias) + ↓ + Condition (WHERE / JOIN 条件注入) +``` + +### 3. 核心组件说明 + +#### ConnectionProxy + +JDBC `Connection` 的代理实现。在 `prepareStatement()`、`createStatement()`、`prepareCall()` 等方法中拦截 SQL,调用 `SQLRunningContext.intercept(sql)` 获取改写后的 SQL,并将 `SQLExecuteState` 传递给下游的 Statement 代理。 + +#### PreparedStatementProxy / StatementProxy + +JDBC `PreparedStatement` 和 `Statement` 的代理实现。在执行 `executeQuery()`、`execute()` 等方法时,确保使用经过权限改写的 SQL。对于直接传入 SQL 字符串的方法(如 `executeQuery(String sql)`),会再次调用 `SQLRunningContext.intercept()` 进行拦截。查询结果通过 `ResultSetProxy` 包装返回。 + +#### SQLRunningContext + +SQL 拦截的核心调度器(单例模式),负责: +- 从 `SQLInterceptorContext` 获取当前 `SQLInterceptor` 实例 +- 通过 `ThreadLocal skipInterceptor` 控制是否跳过拦截 +- 调用 `SQLInterceptor.beforeHandler()` 判断是否需要拦截(默认仅拦截 SELECT 语句) +- 调用 `SQLInterceptor.postHandler()` 执行 SQL 改写 +- 提供 `skipDataAuthorization(Supplier/Runnable)` API 临时跳过权限检查 + +#### DefaultSQLInterceptor + +默认的 SQL 拦截器实现,包含三个阶段的处理: +- `beforeHandler(sql)`:通过 `SQLUtils.isQuerySql()` 判断是否为查询语句,仅 SELECT 会被拦截 +- `postHandler(sql)`:创建 `DataPermissionSQLEnhancer`,使用 JSqlParser 解析 SQL 并通过 `RowHandler` 获取权限条件,返回增强后的 SQL +- `afterHandler(sql, newSql, exception)`:日志记录,当配置 `showSql=true` 时输出改写后的 SQL + +#### RowHandler + +行级权限处理器接口,由业务方实现: + +```java +public interface RowHandler { + Condition handler(String subSql, String tableName, String tableAlias); +} +``` + +返回值 `Condition` 支持两种注入模式: +- **WHERE 条件**:`Condition.customCondition("dept_id IN (1,2,3)")` — 在 WHERE 子句中追加 AND 条件 +- **JOIN 关联**:通过 `JoinConditionSQL` 添加 INNER/LEFT/RIGHT JOIN 关联表 + +### 4. 跳过数据权限 + +在某些管理操作或系统任务中需要绕过数据权限: + +```java +// 方式一:Lambda 表达式 +List allUsers = SQLRunningContext.getInstance() + .skipDataAuthorization(() -> userRepository.findAll()); + +// 方式二:Runnable +SQLRunningContext.getInstance() + .skipDataAuthorization(() -> { + reportService.generateMonthlyReport(); + }); +``` + +## 使用实例 + +### 示例一:按部门过滤数据 + +```java +@Component +public class DeptRowHandler implements RowHandler { + + @Override + public Condition handler(String subSql, String tableName, String tableAlias) { + // 仅对 employee 表注入权限条件 + if ("employee".equalsIgnoreCase(tableName)) { + List deptIds = SecurityContext.getCurrentDeptIds(); + if (deptIds == null || deptIds.isEmpty()) { + return Condition.emptyCondition(); // 无权限,返回 null 不注入 + } + String inClause = deptIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + return Condition.customCondition( + String.format("%s.dept_id IN (%s)", tableAlias, inClause) + ); + } + // 其他表不注入权限条件 + return Condition.emptyCondition(); + } +} +``` + +效果:原始 SQL `SELECT * FROM employee WHERE status = 'active'` 被改写为: +```sql +SELECT * FROM employee WHERE dept_id IN (1,2,3) AND status = 'active' +``` + +### 示例二:通过 JOIN 关联实现跨表权限 + +```java +@Component +public class ProjectRowHandler implements RowHandler { + + @Override + public Condition handler(String subSql, String tableName, String tableAlias) { + if ("project".equalsIgnoreCase(tableName)) { + Condition condition = new Condition(); + // 通过 JOIN 关联成员表,只查询当前用户参与的项目 + JoinConditionSQL joinSQL = new JoinConditionSQL( + "project_member pm", + JoinConditionSQL.Type.INNER, + String.format("pm.project_id = %s.id AND pm.user_id = %d", + tableAlias, SecurityContext.getCurrentUserId()) + ); + condition.addConditionSQL(joinSQL); + return condition; + } + return Condition.emptyCondition(); + } +} +``` + +效果:原始 SQL `SELECT * FROM project WHERE status = 'open'` 被改写为: +```sql +SELECT * FROM project +INNER JOIN project_member pm ON pm.project_id = project.id AND pm.user_id = 1001 +WHERE status = 'open' +``` + +### 示例三:自定义 SQLInterceptor + +如需替换默认的拦截逻辑(例如增加缓存或审计),可实现 `SQLInterceptor` 接口并注册为 Spring Bean: + +```java +@Component +public class AuditSQLInterceptor implements SQLInterceptor { + + @Override + public boolean beforeHandler(String sql) { + // 仅拦截 SELECT 且不包含系统表的查询 + return SQLUtils.isQuerySql(sql) && !sql.contains("sys_config"); + } + + @Override + public DataPermissionSQL postHandler(String sql) throws SQLException { + RowHandler rowHandler = RowHandlerContext.getInstance().getRowHandler(); + DataPermissionSQLEnhancer enhancer = new DataPermissionSQLEnhancer(sql, rowHandler); + return new DataPermissionSQL(sql, enhancer.getNewSQL(), enhancer.getTableAlias()); + } + + @Override + public void afterHandler(String sql, String newSql, SQLException exception) { + // 记录审计日志 + AuditLog.record(sql, newSql, exception); + } +} +``` + +### 内部工作原理 + +1. **连接代理**:DataSource 返回的 `Connection` 被包装为 `ConnectionProxy` +2. **SQL 拦截时机**:当调用 `connection.prepareStatement(sql)` 时,`ConnectionProxy` 立即调用 `SQLRunningContext.intercept(sql)` 对 SQL 进行改写 +3. **递归解析**:`DataPermissionSQLEnhancer` 使用 JSqlParser 解析 SQL AST,深度遍历 PlainSelect、SetOperationList(UNION)、子查询、JOIN 中的子 Select,对每个涉及的表调用 `RowHandler` +4. **条件注入**:`WhereConditionSQLHandler` 将 WHERE 条件通过 AND 拼接到原有 WHERE 子句;`JoinConditionSQLHandler` 向 FROM 子句追加 JOIN 关联 +5. **防重入**:`SQLRunningContext` 使用 ThreadLocal 标记,在拦截器内部执行的查询不会被二次拦截 diff --git a/docs/agents/capabilities/springboot-starter-data-fast/fast-repository.md b/docs/agents/capabilities/springboot-starter-data-fast/fast-repository.md new file mode 100644 index 000000000..22b7fbafa --- /dev/null +++ b/docs/agents/capabilities/springboot-starter-data-fast/fast-repository.md @@ -0,0 +1,168 @@ +--- +name: springboot-starter-data-fast/fast-repository +module: springboot-starter-data-fast +description: JPA 增强 Repository,支持 PageRequest 动态过滤查询和 HQL 构建 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter-data-fast +import: "com.codingapi.springboot:springboot-starter-data-fast" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在标准 Spring Data JPA 开发中,动态条件查询通常需要手动编写 `Specification`、`QueryDSL` 或拼接 HQL,代码冗长且难以维护。当业务列表页面需要支持多字段组合筛选(等于、模糊、范围、IN 等)时,开发者往往要为每种查询场景编写独立的 Repository 方法或复杂的 Specification 构建逻辑。 + +`FastRepository` 通过扩展 `JpaRepository` 和 `JpaSpecificationExecutor`,提供了基于 `PageRequest` 的声明式动态查询能力: + +- **自动 Example 查询**:当 Filter 条件均为简单等值匹配时,自动转换为 Spring Data `Example` 查询,零额外代码 +- **HQL 动态构建**:当包含模糊、范围、IN 等复杂条件时,自动构建参数化 HQL,避免 SQL 注入风险 +- **SearchRequest 集成**:支持从 HTTP 请求参数中自动解析 filter、sort 条件,适用于前端列表页的通用查询接口 +- **OR/AND 组合过滤**:支持嵌套的 OR/AND 条件组合,满足复杂业务筛选需求 + +## 如何使用 + +### 1. 定义 Repository 接口 + +继承 `FastRepository` 即可获得全部动态查询能力: + +```java +public interface UserEntityRepository extends FastRepository { + // 标准 JpaRepository 方法仍然可用 + UserEntity getUserEntityByUsername(String username); +} +``` + +### 2. 使用 PageRequest 进行动态过滤查询 + +```java +// 创建分页请求并添加过滤条件 +PageRequest request = PageRequest.of(0, 20); +request.addFilter("name", "张三"); // 等值匹配 +request.addFilter("age", Relation.GT, 18); // 大于 +request.addFilter("email", Relation.LIKE, "gmail"); // 模糊查询 + +// 方式一:自动选择 Example 或 HQL(推荐) +Page page = repository.findAll(request); + +// 方式二:强制使用 HQL 查询(适合复杂条件) +Page page2 = repository.pageRequest(request); +``` + +### 3. 支持的过滤关系(Relation) + +| Relation | 说明 | HQL 示例 | +|----------|------|----------| +| EQ(默认) | 等于 | `name = ?1` | +| NEQ | 不等于 | `name != ?1` | +| GT | 大于 | `age > ?1` | +| LT | 小于 | `age < ?1` | +| GTE | 大于等于 | `age >= ?1` | +| LTE | 小于等于 | `age <= ?1` | +| LIKE | 全模糊 | `name LIKE ?1`(自动加 `%value%`) | +| LEFT_LIKE | 左模糊 | `name LIKE ?1`(自动加 `%value`) | +| RIGHT_LIKE | 右模糊 | `name LIKE ?1`(自动加 `value%`) | +| IN | 包含 | `id IN (?1)` | +| NOT_IN | 不包含 | `id NOT IN (?1)` | +| BETWEEN | 区间 | `age BETWEEN ?1 AND ?2` | +| IS_NULL | 为空 | `name IS NULL` | +| IS_NOT_NULL | 非空 | `name IS NOT NULL` | + +### 4. 使用 SearchRequest 从 HTTP 请求自动解析 + +```java +// 在 Controller 中使用 SearchRequest,自动从 URL 参数解析 filter 和 sort +@GetMapping("/users") +public MultiResponse list() { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.addFilter("status", "active"); // 追加服务端固定条件 + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page.getContent()); +} +``` + +前端通过 URL 参数传递动态条件: +- `?filter=eyJuYW1lIjpbIuW8oCJdfQ==`(Base64 编码的 JSON:`{"name":["张"]}`) +- `?sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9`(Base64 编码的 JSON:`{"createTime":"descend"}`) + +### 5. OR / AND 组合条件 + +```java +PageRequest request = PageRequest.of(0, 20); + +// OR 条件:name = '张三' OR name = '李四' +request.orFilters( + new Filter("name", "张三"), + new Filter("name", "李四") +); + +// AND 条件组 +request.andFilter( + new Filter("age", Relation.GTE, 18), + new Filter("status", "active") +); +``` + +## 使用实例 + +### 完整示例:用户列表查询 + +```java +@Service +public class UserQueryService { + + @Resource + private UserEntityRepository userRepository; + + /** + * 基础动态查询 - 自动 Example/HQL + */ + public Page findUsers(String name, Integer minAge, String status) { + PageRequest request = PageRequest.of(0, 20); + if (name != null) { + request.addFilter("name", Relation.LIKE, name); + } + if (minAge != null) { + request.addFilter("age", Relation.GTE, minAge); + } + if (status != null) { + request.addFilter("status", status); + } + return userRepository.findAll(request); + } + + /** + * 复杂 HQL 查询 - 带排序 + */ + public Page findActiveUsersWithSort() { + PageRequest request = PageRequest.of(0, 20, Sort.by("createTime").descending()); + request.addFilter("status", "active"); + request.addFilter("age", Relation.BETWEEN, 18, 65); + return userRepository.pageRequest(request); + } + + /** + * 前端驱动的通用查询接口 + */ + public Page searchFromHttpRequest() { + SearchRequest searchRequest = new SearchRequest(); + // 追加服务端安全条件,防止越权查询 + searchRequest.addFilter("deleted", false); + return userRepository.searchRequest(searchRequest); + } +} +``` + +### 内部工作原理 + +`FastRepository.findAll(PageRequest)` 的执行流程: + +1. 检查 `request.hasFilter()` — 无过滤条件时直接委托给 Spring Data 的标准 `findAll(PageRequest)` +2. 有过滤条件时,通过 `ExampleBuilder` 尝试构建 `Example` 对象(仅处理等值匹配的属性) +3. 将 Example 与 PageRequest 一起传入 `findAll(Example, Pageable)` 执行查询 + +`FastRepository.pageRequest(PageRequest)` 的执行流程: + +1. 通过 `DynamicSQLBuilder` 根据 Filter 列表动态构建 HQL 语句和 COUNT 语句 +2. 所有值通过参数化绑定(`?1`, `?2`...),防止 SQL 注入 +3. 调用 `dynamicPageQuery(hql, countHql, request, params)` 执行分页查询 diff --git a/docs/agents/capabilities/springboot-starter-flow/workflow-engine.md b/docs/agents/capabilities/springboot-starter-flow/workflow-engine.md new file mode 100644 index 000000000..ce900011a --- /dev/null +++ b/docs/agents/capabilities/springboot-starter-flow/workflow-engine.md @@ -0,0 +1,147 @@ +--- +name: springboot-starter-flow/workflow-engine +module: springboot-starter-flow +description: 工作流引擎,支持流程定义、节点流转、审批、委托、会签和数据快照 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter-flow +import: "com.codingapi.springboot:springboot-starter-flow" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级应用中,审批流程(如请假、报销、采购等)是高频且复杂的业务场景。传统硬编码方式存在以下痛点: + +- **流程变更成本高**:每次调整审批节点或流转规则都需要修改代码并重新部署 +- **审批模式单一**:难以同时支持会签、或签、传阅、退回、委托等多种审批形态 +- **状态追踪困难**:流程实例的当前节点、历史审批记录、数据快照缺乏统一管理 +- **事件通知分散**:审批通过、拒绝、转办等状态变更需要在多处手动触发通知逻辑 + +`workflow-engine` 提供了一套轻量级的嵌入式工作流引擎,将流程定义与业务代码解耦。通过 Builder 模式声明式构建流程,引擎自动处理节点流转、操作者匹配、数据快照绑定和事件推送,让开发者专注于业务逻辑本身。 + +## 如何使用 + +### 核心概念 + +| 概念 | 类 | 说明 | +|------|-----|------| +| 流程定义 | `FlowWork` | 描述一个完整的审批流程,包含节点集合、关系集合、启用状态等 | +| 流程节点 | `FlowNode` | 流程中的单个环节(开始/审批/传阅/结束),配置审批类型、操作者匹配器、超时时间等 | +| 节点关系 | `FlowRelation` | 定义节点之间的流转规则,支持条件触发(`OutTrigger`)和退回标记 | +| 流程构建器 | `FlowWorkBuilder` | 链式 API 构建 `FlowWork`,内部自动校验节点和关系的完整性 | +| 发起服务 | `FlowStartService` | 发起新流程实例,创建流程备份、数据快照和首条待办记录 | +| 节点服务 | `FlowNodeService` | 驱动节点流转:加载下一节点、匹配操作者、创建审批记录、处理传阅跳过 | +| 审批事件 | `FlowApprovalEvent` | 同步事件,覆盖创建/待办/通过/拒绝/转办/撤回/完成/催办/抄送/退回等 14 种状态 | +| 流程操作者 | `IFlowOperator` | 用户接口,支持委托 (`entrustOperator()`) 和管理员强制干预 | + +### 构建流程 + +使用 `FlowWorkBuilder` 声明式定义流程: + +```java +FlowWork work = FlowWorkBuilder.builder(operator) + .title("请假审批流程") + .description("员工请假审批") + .skipIfSameApprover(true) // 相同审批人自动跳过 + .postponedMax(3) // 最大延期次数 + .nodes() + .node("发起", "start", "startView", ApprovalType.UN_SIGN, startMatcher) + .node("部门审批", "dept_approve", "approveView", ApprovalType.SIGN, deptMatcher, true, false) + .node("HR备案", "hr_record", "hrView", ApprovalType.UN_SIGN, hrMatcher) + .node("结束", "over", "overView", ApprovalType.UN_SIGN, endMatcher) + .relations() + .relation("发起->部门审批", "start", "dept_approve") + .relation("部门审批->HR备案", "dept_approve", "hr_record") + .relation("HR备案->结束", "hr_record", "over") + .build(); +``` + +构建时 `build()` 会自动调用 `enable()` → `verify()`,校验以下内容: +- 必须存在 `start` 和 `over` 节点及其关联关系 +- 节点 code 不能重复 +- 每个节点的 `titleGenerator` 和 `operatorMatcher` 不能为空 + +### 发起流程 + +通过 `FlowStartService` 发起流程实例: + +```java +FlowStartService startService = new FlowStartService( + workCode, operator, bindData, advice, repositoryHolder); +FlowResult result = startService.startFlow(); +``` + +启动过程依次执行:加载并校验流程定义 → 创建版本快照(`FlowBackup`)→ 保存流程实例 → 序列化绑定数据 → 从 start 节点创建待办记录 → 推送 `FlowApprovalEvent`。 + +### 监听审批事件 + +实现 `IHandler` 即可接收所有审批状态变更: + +```java +@Component +public class LeaveApprovalHandler implements IHandler { + @Override + public void handle(FlowApprovalEvent event) { + if (event.isTodo()) { + // 发送待办通知 + } + if (event.isPass()) { + // 审批通过处理 + } + if (event.isFinish()) { + // 流程结束归档 + } + } +} +``` + +事件通过框架的 `EventPusher` 同步推送,在同一个事务内完成。 + +## 使用实例 + +以下示例展示一个完整的请假审批流程定义与发起: + +```java +// 1. 定义操作者匹配器 +OperatorMatcher startMatcher = OperatorMatcher.any(); // 发起人 +OperatorMatcher deptMatcher = OperatorMatcher.script( // 部门负责人 + "session.bindData.deptLeaderId"); +OperatorMatcher hrMatcher = OperatorMatcher.script( // HR + "session.bindData.hrUserId"); +OperatorMatcher endMatcher = OperatorMatcher.any(); + +// 2. 构建流程 +FlowWork leaveFlow = FlowWorkBuilder.builder(currentUser) + .title("请假审批") + .skipIfSameApprover(true) + .nodes() + .node("发起申请", "start", "leaveStart", ApprovalType.UN_SIGN, startMatcher) + .node("部门审批", "dept", "leaveApprove", ApprovalType.SIGN, deptMatcher, true, false) + .node("HR确认", "hr", "leaveHr", ApprovalType.UN_SIGN, hrMatcher) + .node("结束", "over", "leaveEnd", ApprovalType.UN_SIGN, endMatcher) + .relations() + .relation("提交", "start", "dept") + .relation("批准", "dept", "hr") + .relation("确认", "hr", "over") + .build(); + +// 3. 准备业务数据 +LeaveRequest leave = new LeaveRequest(); +leave.setUserId(currentUser.getUserId()); +leave.setDays(3); +leave.setDeptLeaderId(deptLeaderId); +leave.setHrUserId(hrUserId); + +// 4. 发起流程 +FlowStartService service = new FlowStartService( + leaveFlow.getCode(), currentUser, leave, "申请年假3天", repoHolder); +FlowResult result = service.startFlow(); + +// 5. 获取待办记录 +List records = result.getRecords(); +records.forEach(r -> System.out.println( + "待办: " + r.getTitle() + " -> " + r.getCurrentOperator().getName())); +``` + +当部门审批人点击通过后,引擎自动流转至 HR 确认节点;若设置了退回关系,审批人可选择退回至指定节点。全程数据快照通过 `BindDataSnapshot` 持久化,确保审批过程中业务数据不可变。 diff --git a/docs/agents/capabilities/springboot-starter-script/groovy-runtime.md b/docs/agents/capabilities/springboot-starter-script/groovy-runtime.md new file mode 100644 index 000000000..b55477f56 --- /dev/null +++ b/docs/agents/capabilities/springboot-starter-script/groovy-runtime.md @@ -0,0 +1,158 @@ +--- +name: springboot-starter-script/groovy-runtime +module: springboot-starter-script +description: Groovy 脚本运行时引擎,支持运行时编译、LRU 缓存、热更新和 REST API +status: 已实现 +scope: 后端 +source: 框架:springboot-starter-script +import: "com.codingapi.springboot:springboot-starter-script" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业应用中,部分业务逻辑需要频繁调整但不希望每次都经历"改代码→编译→部署"的完整周期。典型场景包括: + +- **动态规则计算**:促销折扣、费率计算、风控阈值等业务规则经常变化 +- **自定义报表/导出**:不同租户或部门的报表格式差异大,用脚本比硬编码更灵活 +- **流程条件表达式**:工作流节点的条件判断、操作者匹配等需要运行时求值 +- **临时数据处理**:运维脚本、数据修复等一次性任务 + +`groovy-runtime` 提供了嵌入式的 Groovy 脚本执行引擎,具备以下特性: + +- **运行时编译执行**:无需重启应用即可加载和执行新脚本 +- **LRU 编译缓存**:基于 SHA256 指纹缓存已编译的 Script 对象,避免重复编译开销 +- **三级存储**:内存缓存 → 临时存储 → 持久化仓库,兼顾性能和可靠性 +- **事务集成**:支持只读(READONLY)和提交(COMMIT)两种事务模式 +- **REST API**:内置 Controller 提供脚本编译、查询、保存接口,方便管理端对接 + +## 如何使用 + +### 核心组件 + +| 组件 | 职责 | +|------|------| +| `GroovyScriptRuntime` | 底层执行引擎:GroovyShell 封装、LRU 编译缓存、invoke/run 方法、事务控制 | +| `GroovyScriptRuntimeContext` | Runtime 的单例上下文,从配置读取 `shellMaxCacheSize` 初始化 | +| `GroovyScript` | 脚本领域对象:封装 key/script/method/returnType/binds 等元信息,提供 compile/run/invoke 快捷方法 | +| `GroovyScriptCacheContext` | 脚本对象的 LRU 缓存(最大 10240 条),三级查找:缓存 → 临时存储 → Repository | +| `GroovyScriptController` | REST 端点 `/api/groovy-script/*`,提供 compile/getScript/getMetadata/save 接口 | + +### 直接执行脚本 + +通过 `GroovyScriptRuntimeContext` 单例直接运行脚本: + +```java +GroovyScriptRuntimeContext ctx = GroovyScriptRuntimeContext.getInstance(); + +// 简单执行 +String result = ctx.run("'hello ' + name", String.class, + TransactionMode.DEFAULT, Map.of("name", "world")); + +// 调用脚本中的函数 +int sum = ctx.invoke("add", "def add(a,b){ a + b }", int.class, + TransactionMode.DEFAULT, null, 3, 5); +``` + +### 使用 GroovyScript 对象 + +对于需要持久化和复用的脚本,使用 `GroovyScript` 封装: + +```java +GroovyScript script = GroovyScript.builder("discount_calc") + .script(""" + def calculate(price, rate) { + return price * rate + } + """) + .method("calculate") + .returnType(BigDecimal.class) + .description("折扣计算脚本") + .tag("pricing") + .build(); + +// 编译并缓存 +script.compile(true); + +// 执行 +BigDecimal result = script.invoke(Map.of(), new BigDecimal("100"), new BigDecimal("0.85")); + +// 持久化到仓库 +script.save(); +``` + +### 事务模式 + +`TransactionMode` 支持三种模式: + +| 模式 | 行为 | +|------|------| +| `DEFAULT` | 不参与事务,由调用方自行控制 | +| `READONLY` | 在只读事务中执行,适合查询类脚本 | +| `COMMIT` | 在可提交事务中执行,适合写入类脚本 | + +### REST API + +内置的 `GroovyScriptController` 提供以下端点: + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/groovy-script/compile` | 编译脚本(body: `{script, cache}`) | +| GET | `/api/groovy-script/getScript?key=` | 获取脚本内容 | +| GET | `/api/groovy-script/getMetadata?key=` | 获取脚本元数据 | +| POST | `/api/groovy-script/save` | 保存脚本(编译+持久化) | + +## 使用实例 + +以下示例展示一个完整的动态折扣计算场景: + +```java +// 1. 创建并保存折扣计算脚本 +GroovyScript discountScript = GroovyScript.builder("member_discount") + .script(""" + def calcDiscount(amount, memberLevel) { + switch(memberLevel) { + case 'GOLD': return amount * 0.8 + case 'SILVER': return amount * 0.9 + default: return amount + } + } + """) + .method("calcDiscount") + .returnType(BigDecimal.class) + .description("会员折扣计算") + .typeOne("pricing") + .typeTwo("discount") + .build(); + +// 编译并持久化 +discountScript.compile(true); +discountScript.save(); + +// 2. 后续使用时直接从缓存获取 +GroovyScript cached = GroovyScriptCacheContext.getInstance() + .getGroovyScript("member_discount"); + +// 3. 执行业务计算 +BigDecimal originalAmount = new BigDecimal("500"); +BigDecimal finalPrice = cached.invoke( + TransactionMode.READONLY, null, originalAmount, "GOLD"); +// finalPrice = 400.0 + +// 4. 热更新:修改脚本后重新保存即可生效 +cached.setScript(""" + def calcDiscount(amount, memberLevel) { + switch(memberLevel) { + case 'DIAMOND': return amount * 0.7 + case 'GOLD': return amount * 0.8 + case 'SILVER': return amount * 0.9 + default: return amount + } + } +"""); +cached.compile(true); +cached.save(); +// 下次 invoke 自动使用新版本 +``` + +缓存机制说明:`GroovyScriptRuntime` 内部以脚本内容的 SHA256 作为缓存 key,相同内容的脚本不会重复编译。当脚本内容变更后,SHA256 值改变,自动触发重新编译。LRU 策略确保内存占用可控,默认上限可通过配置项调整。 diff --git a/docs/agents/capabilities/springboot-starter-security/auth-gateway.md b/docs/agents/capabilities/springboot-starter-security/auth-gateway.md new file mode 100644 index 000000000..900b79df4 --- /dev/null +++ b/docs/agents/capabilities/springboot-starter-security/auth-gateway.md @@ -0,0 +1,195 @@ +--- +name: springboot-starter-security/auth-gateway +module: springboot-starter-security +description: 安全认证网关,支持 JWT 无状态认证和 Redis 有状态认证两种模式 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter-security +import: "com.codingapi.springboot:springboot-starter-security" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级 Web 应用中,用户认证是安全体系的第一道防线。Spring Security 原生提供的表单登录和 Session 机制不适合前后端分离架构,开发者通常需要自行实现以下能力: + +- **Token 生命周期管理**:创建、解析、过期校验、自动续期 +- **多认证模式切换**:JWT 无状态模式适合微服务和移动端,Redis 有状态模式适合需要服务端主动踢人的管理后台 +- **登录流程定制**:在认证前后插入自定义逻辑(验证码校验、登录日志、额外业务数据注入等) +- **统一 JSON 响应**:认证成功/失败均返回标准 `Response` 格式,而非 Spring Security 默认的重定向或 HTML 页面 + +auth-gateway 将上述能力封装为开箱即用的安全网关层,通过 `TokenGateway` 策略接口抽象 Token 的创建与解析,配合 `MyLoginFilter`(登录拦截)和 `MyAuthenticationFilter`(请求鉴权拦截)两个 Servlet Filter,实现完整的认证闭环。开发者只需通过配置选择 JWT 或 Redis 模式,即可零代码获得生产级认证能力。 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-security + +``` + +### 2. 选择认证模式 + +通过 `application.properties` 启用其中一种模式(二选一): + +**JWT 无状态模式:** + +```properties +codingapi.security.jwt.enable=true +codingapi.security.jwt.secret-key=your-secret-key-must-be-at-least-32-chars +codingapi.security.jwt.valid-time=900000 # Token 有效期 15 分钟(毫秒) +codingapi.security.jwt.rest-time=600000 # 10 分钟后自动续期(毫秒) +``` + +**Redis 有状态模式:** + +```properties +codingapi.security.redis.enable=true +codingapi.security.redis.valid-time=900000 +codingapi.security.redis.rest-time=600000 +``` + +### 3. 配置安全策略 + +```properties +# 需要认证的 URL 模式(逗号分隔) +codingapi.security.authenticated-urls=/api/** + +# 免认证 URL 模式 +codingapi.security.ignore-urls=/open/**,/#/** + +# 登录接口地址 +codingapi.security.login-processing-url=/user/login + +# 登出接口地址 +codingapi.security.logout-url=/user/logout +``` + +### 4. 核心 API + +| 类 | 说明 | +|---|---| +| `TokenGateway` | Token 策略接口,提供 `create()` 和 `parser()` 方法。框架根据配置自动注入 `JWTTokenGatewayImpl` 或 `RedisTokenGatewayImpl` | +| `Token` | Token 数据对象,包含 username、authorities、extra、expireTime、remindTime 等字段,支持 `verify()` 过期校验和 `canRestToken()` 自动续期判断 | +| `TokenContext` | 线程安全的 Token 上下文工具类。`TokenContext.current()` 获取当前登录用户的 Token;`TokenContext.pushExtra()` / `getExtra()` 传递额外业务数据 | +| `SecurityLoginHandler` | 登录扩展点接口。实现 `preHandle()` 可在认证前做自定义校验;实现 `postHandle()` 可定制登录成功响应 | +| `AuthenticationTokenFilter` | 鉴权后扩展点接口。每次请求通过 Token 验证后回调,可用于刷新用户缓存等场景 | + +### 5. 自定义 UserDetailsService + +框架默认提供内存用户(admin/user),生产环境需自行实现: + +```java +@Service +public class CustomUserDetailsService implements UserDetailsService { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 从数据库加载用户信息 + } +} +``` + +## 使用实例 + +### 示例 1:自定义登录处理器 + +在登录前校验验证码,登录后返回额外的用户信息: + +```java +@Component +public class CustomSecurityLoginHandler implements SecurityLoginHandler { + + @Override + public void preHandle(HttpServletRequest request, HttpServletResponse response, + LoginRequest loginRequest) throws Exception { + // 校验验证码 + String captcha = loginRequest.getString("captcha"); + if (captcha == null || !captchaService.verify(captcha)) { + throw new AuthenticationServiceException("验证码错误"); + } + } + + @Override + public LoginResponse postHandle(HttpServletRequest request, HttpServletResponse response, + LoginRequest loginRequest, UserDetails user, Token token) { + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setToken(token.getToken()); + loginResponse.setUsername(token.getUsername()); + loginResponse.setAuthorities(token.getAuthorities()); + // 附加用户头像等信息 + Map data = new HashMap<>(); + data.put("avatar", "/static/avatar/default.png"); + loginResponse.setData(data); + return loginResponse; + } +} +``` + +### 示例 2:在业务代码中获取当前用户 + +```java +@RestController +@RequestMapping("/api/profile") +public class ProfileController { + + @GetMapping + public SingleResponse> getProfile() { + // 从 SecurityContext 获取当前 Token + Token token = TokenContext.current(); + String username = token.getUsername(); + List roles = token.getAuthorities(); + + // 解析 extra 中的自定义数据 + UserInfo extra = token.parseExtra(UserInfo.class); + + Map profile = new HashMap<>(); + profile.put("username", username); + profile.put("roles", roles); + profile.put("department", extra != null ? extra.getDepartment() : null); + return SingleResponse.of(profile); + } +} +``` + +### 示例 3:鉴权后刷新用户缓存 + +```java +@Component +public class CacheRefreshFilter implements AuthenticationTokenFilter { + + private final UserService userService; + + public CacheRefreshFilter(UserService userService) { + this.userService = userService; + } + + @Override + public void doFilter(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + Token token = TokenContext.current(); + // 每次认证通过后刷新用户权限缓存 + userService.refreshPermissionCache(token.getUsername()); + } +} +``` + +### 前端调用示例 + +```javascript +// 登录 +const res = await fetch('/user/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'admin', password: 'admin' }) +}); +const { data } = await res.json(); +// data.token → 保存 Token + +// 携带 Token 访问受保护接口 +const profile = await fetch('/api/profile', { + headers: { 'Authorization': data.token } +}); +``` diff --git a/docs/agents/capabilities/springboot-starter/domain-proxy.md b/docs/agents/capabilities/springboot-starter/domain-proxy.md new file mode 100644 index 000000000..db21dd95e --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/domain-proxy.md @@ -0,0 +1,126 @@ +--- +name: springboot-starter/domain-proxy +module: springboot-starter +description: 领域实体变更代理,通过 CGLIB 代理拦截实体字段变更并自动推送领域事件 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在 DDD(领域驱动设计)实践中,领域实体的状态变更需要产生对应的领域事件(创建、变更、删除、持久化),以便下游处理器执行副作用操作(如发送通知、更新缓存、触发工作流等)。然而手动在每个 setter 方法中编写事件推送代码存在以下痛点: + +1. **侵入性强**:每个实体类的修改方法都需要显式调用 `EventPusher.push()`,业务逻辑与事件机制耦合。 +2. **容易遗漏**:开发者可能忘记在某些字段变更后推送事件,导致数据不一致。 +3. **变更追踪困难**:无法自动获取字段的旧值与新值,手动记录增加出错风险。 +4. **嵌套对象变更不可见**:当实体包含子对象时,子对象字段的变更更难被感知和追踪。 + +`domain-proxy` 能力通过 CGLIB 动态代理透明地拦截实体方法调用,自动比较字段值变化并推送 `DomainChangeEvent`,同时提供完整的实体生命周期事件(创建、持久化、删除),让领域事件的发布对业务代码零侵入。 + +## 如何使用 + +### 核心组件 + +| 组件 | 说明 | +|------|------| +| `IDomain` | 领域实体标记接口,提供 `persist()` 和 `delete()` 默认方法,分别推送 `DomainPersistEvent` 和 `DomainDeleteEvent` | +| `DomainProxyFactory` | 静态工厂类,通过 `create(Class, Object... args)` 创建代理实例,同时自动推送 `DomainCreateEvent` | +| `DomainChangeInterceptor` | CGLIB `MethodInterceptor` 实现,拦截带参数的方法调用,对比执行前后字段值差异,自动推送 `DomainChangeEvent` | +| `DomainEvent` | 领域事件基类,携带实体引用、实体类型和时间戳 | +| `DomainCreateEvent` | 实体创建事件,由 `DomainProxyFactory.create()` 自动推送 | +| `DomainChangeEvent` | 实体字段变更事件,包含 `fieldName`、`oldValue`、`newValue` | +| `DomainDeleteEvent` | 实体删除事件,通过 `IDomain.delete()` 推送 | +| `DomainPersistEvent` | 实体持久化事件,通过 `IDomain.persist()` 推送 | + +### 使用步骤 + +1. **定义领域实体**:创建实体类并实现 `IDomain` 接口。实体类需要有公开的构造函数供代理工厂反射调用。 + +2. **通过工厂创建实例**:使用 `DomainProxyFactory.create(EntityClass.class, constructorArgs...)` 代替 `new` 关键字创建实体。工厂会自动创建 CGLIB 代理并推送 `DomainCreateEvent`。 + +3. **正常调用业务方法**:对代理对象调用任何带参数的方法后,拦截器会自动比较所有字段(包括嵌套对象)的值变化,若有变更则推送 `DomainChangeEvent`。 + +4. **触发生命周期事件**:在适当时机调用 `entity.persist()` 推送持久化事件,或调用 `entity.delete()` 推送删除事件。 + +5. **注册事件处理器**:编写 `IHandler`、`IHandler` 等处理器 Bean,框架会自动扫描并注册。 + +### 注意事项 + +- 代理仅拦截**带参数**的方法调用,无参方法(如 getter)不会触发变更检测。 +- 支持基本类型(String、数值、布尔、枚举等)的直接比较,以及嵌套对象的递归字段比较。 +- 事件推送通过 `EventPusher` 进入框架的事件系统,遵循同步/异步分发和循环检测机制。 + +## 使用实例 + +### 定义领域实体 + +```java +public class User implements IDomain { + + @Getter + private final long id; + + @Getter + private String name; + + @Getter + private String email; + + public User(String name, String email) { + this.id = System.currentTimeMillis(); + this.name = name; + this.email = email; + } + + public void changeName(String name) { + this.name = name; + } + + public void changeEmail(String email) { + this.email = email; + } +} +``` + +### 创建代理并触发事件 + +```java +// 通过工厂创建代理实例,自动推送 DomainCreateEvent +User user = DomainProxyFactory.create(User.class, "张三", "zhangsan@example.com"); + +// 调用带参方法后,拦截器自动检测字段变更并推送 DomainChangeEvent +user.changeName("李四"); // → DomainChangeEvent(fieldName="name", oldValue="张三", newValue="李四") +user.changeEmail("li@example.com"); // → DomainChangeEvent(fieldName="email", ...) + +// 手动触发持久化事件 +user.persist(); // → DomainPersistEvent + +// 手动触发删除事件 +user.delete(); // → DomainDeleteEvent +``` + +### 编写事件处理器 + +```java +@Component +public class UserChangeHandler implements IHandler { + + @Override + public void handler(DomainChangeEvent event) { + log.info("用户字段变更: {} = {} → {}", + event.getFieldName(), event.getOldValue(), event.getNewValue()); + } +} + +@Component +public class UserCreateHandler implements IHandler { + + @Override + public void handler(DomainCreateEvent event) { + log.info("新用户创建: {}", event.getEntity().getClass().getSimpleName()); + } +} +``` diff --git a/docs/agents/capabilities/springboot-starter/event-system.md b/docs/agents/capabilities/springboot-starter/event-system.md new file mode 100644 index 000000000..5e7ea7ead --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/event-system.md @@ -0,0 +1,272 @@ +--- +name: springboot-starter/event-system +module: springboot-starter +description: 事件发布-订阅系统,支持同步/异步事件、事件循环检测、事务事件 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在 DDD(领域驱动设计)架构中,领域事件是实现聚合间解耦、触发副作用的核心机制。然而在实际开发中,事件系统面临以下痛点: + +1. **事件与主业务事务的边界模糊**:开发者容易将事件处理与主业务强绑定,导致事务范围膨胀、性能下降。框架需要明确"事件对主业务可成功可失败"的设计原则。 +2. **同步与异步事件的区分困难**:部分场景要求事件在同一事务内同步执行(如数据校验),部分场景则需异步解耦(如发送通知)。手动管理线程池和执行模式增加复杂度。 +3. **事件循环调用风险**:当 Handler A 处理 EventX 时又推送了 EventY,而 EventY 的 Handler 又推送了 EventX,就会形成无限循环。缺乏自动检测机制会导致栈溢出或系统挂起。 +4. **多 Handler 执行的异常隔离**:同一事件可能被多个 Handler 订阅,某个 Handler 抛异常不应影响其他 Handler 的执行,同时需要收集所有异常统一上报。 +5. **Handler 注册与排序繁琐**:手动注册处理器、维护执行顺序的代码分散且易出错。 + +本模块提供了一套完整的发布-订阅事件基础设施,通过 `EventPusher` 统一入口、`IEvent` 类型体系区分同步/异步、`EventTraceContext` + `EventStackContext` 实现事件链路追踪与循环检测、`ApplicationHandlerUtils` 自动匹配并按序分发 Handler,以及可选的事务提交后触发模式(`SpringTransactionEventHandler`)。 + +## 如何使用 + +### 核心接口 + +| 接口/类 | 说明 | +|---------|------| +| `IEvent` | 事件标记接口,继承 `Serializable`。默认视为同步事件 | +| `ISyncEvent` | 同步事件标记接口,继承 `IEvent`。事件在当前线程同步执行 | +| `IAsyncEvent` | 异步事件标记接口,继承 `IEvent`。事件在线程池中异步执行 | +| `IHandler` | 事件处理器接口,泛型 `T` 指定订阅的事件类型 | +| `EventPusher` | 事件推送静态工具类,是发布事件的唯一入口 | +| `@Handler` | 注解,标记一个类为事件处理器 Bean(也可使用 `@Component`/`@Service`) | + +### IHandler 接口方法 + +```java +public interface IHandler { + // 执行顺序,数值越小越先执行,默认为 0 + default int order() { return 0; } + + // 事件处理逻辑 + void handler(T event); + + // 异常回调,默认重新抛出异常(阻止后续 Handler 执行) + // 可覆盖此方法实现异常吞没或自定义处理 + default void error(Exception exception) throws Exception { + throw exception; + } +} +``` + +### 事件推送 + +```java +// 推送事件(自动根据 IEvent 子接口判断同步/异步) +EventPusher.push(new MyEvent(data)); + +// 显式声明允许循环事件(跳过循环检测) +EventPusher.push(new MyEvent(data), true); +``` + +事件类型的判定规则: +- 实现 `IAsyncEvent` → 异步执行(线程池) +- 实现 `ISyncEvent` → 同步执行(当前线程) +- 仅实现 `IEvent` → 默认同步执行 + +### 配置项 + +```properties +# 启用事务事件模式(事件在事务提交后触发) +codingapi.framework.event.transaction.enable=true + +# 异步事件线程池大小(默认值见 PropertiesContext) +codingapi.framework.handler-thread-pool-size=20 +``` + +### 事件分发模式 + +框架通过 `SpringHandlerConfiguration` 自动装配事件分发器: + +- **默认模式**(`SpringDefaultEventHandler`):使用 Spring `@EventListener` 即时触发,同步事件在当前线程执行,异步事件提交到固定大小线程池。 +- **事务模式**(`SpringTransactionEventHandler`):当配置 `codingapi.framework.event.transaction.enable=true` 时激活,使用 `@TransactionalEventListener(phase = AFTER_COMMIT)` 确保事件在事务提交后才触发,避免读取到未提交的数据。设置 `fallbackExecution = true` 保证无事务上下文时也能正常执行。 + +两种模式互斥,事务模式优先(`@ConditionalOnMissingBean` 兜底默认模式)。 + +### 事件循环检测 + +框架通过 `EventTraceContext` 和 `EventStackContext` 实现同一条事件链路内的循环检测: + +1. 每次推送事件时生成/复用 traceId,将事件类压入该 traceId 对应的事件栈。 +2. 若同一 traceId 下出现相同事件类,立即抛出 `EventLoopException`,并附带完整的事件调用栈信息。 +3. 事件处理完成后自动清理 traceId 和事件栈。 +4. 若确实需要允许循环(如状态机重试),可调用 `EventPusher.push(event, true)` 跳过检测。 + +### Handler 异常处理策略 + +当某个 Handler 抛出异常时: +1. 若异常为 `EventLoopException`,直接向上抛出,终止后续所有 Handler。 +2. 否则调用该 Handler 的 `error(exception)` 回调。 +3. 若 `error()` 也抛出异常,标记为"有异常"并继续执行下一个 Handler。 +4. 所有 Handler 执行完毕后,若存在异常,统一包装为 `EventException` 抛出。 + +## 使用实例 + +### 1. 定义事件 + +```java +package com.example.domain.order.event; + +import com.codingapi.springboot.framework.event.ISyncEvent; +import lombok.Getter; +import lombok.AllArgsConstructor; + +/** + * 订单创建事件(同步) + */ +@Getter +@AllArgsConstructor +public class OrderCreatedEvent implements ISyncEvent { + private final String orderId; + private final String userId; + private final long amount; +} +``` + +```java +package com.example.domain.order.event; + +import com.codingapi.springboot.framework.event.IAsyncEvent; +import lombok.Getter; +import lombok.AllArgsConstructor; + +/** + * 订单通知事件(异步) + */ +@Getter +@AllArgsConstructor +public class OrderNotifyEvent implements IAsyncEvent { + private final String orderId; + private final String message; +} +``` + +### 2. 编写事件处理器 + +```java +package com.example.handler; + +import com.codingapi.springboot.framework.event.IHandler; +import com.example.domain.order.event.OrderCreatedEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 订单创建后扣减库存(优先执行) + */ +@Slf4j +@Service +public class InventoryDeductHandler implements IHandler { + + @Override + public int order() { + return 1; // 最先执行 + } + + @Override + public void handler(OrderCreatedEvent event) { + log.info("扣减库存, orderId={}, amount={}", event.getOrderId(), event.getAmount()); + // inventoryService.deduct(event.getOrderId(), event.getAmount()); + } +} +``` + +```java +package com.example.handler; + +import com.codingapi.springboot.framework.event.IHandler; +import com.example.domain.order.event.OrderCreatedEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 订单创建后记录积分(其次执行) + */ +@Slf4j +@Service +public class PointsRecordHandler implements IHandler { + + @Override + public int order() { + return 2; + } + + @Override + public void handler(OrderCreatedEvent event) { + log.info("记录积分, userId={}", event.getUserId()); + // pointsService.record(event.getUserId(), event.getAmount()); + } + + @Override + public void error(Exception exception) { + // 积分记录失败不影响其他 Handler,仅记录日志 + log.error("积分记录失败, 但不阻断流程", exception); + } +} +``` + +```java +package com.example.handler; + +import com.codingapi.springboot.framework.event.IHandler; +import com.example.domain.order.event.OrderNotifyEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 异步发送订单通知 + */ +@Slf4j +@Service +public class OrderNotifyHandler implements IHandler { + + @Override + public void handler(OrderNotifyEvent event) { + log.info("发送通知, orderId={}, message={}", event.getOrderId(), event.getMessage()); + // notificationService.send(event.getOrderId(), event.getMessage()); + } +} +``` + +### 3. 在领域服务中推送事件 + +```java +package com.example.domain.order.service; + +import com.codingapi.springboot.framework.event.EventPusher; +import com.example.domain.order.event.OrderCreatedEvent; +import com.example.domain.order.event.OrderNotifyEvent; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class OrderService { + + @Transactional + public String createOrder(String userId, long amount) { + String orderId = generateOrderId(); + // ... 持久化订单实体 ... + + // 同步事件:在当前事务内执行库存扣减、积分记录 + EventPusher.push(new OrderCreatedEvent(orderId, userId, amount)); + + // 异步事件:事务外异步发送通知 + EventPusher.push(new OrderNotifyEvent(orderId, "您的订单已创建")); + + return orderId; + } +} +``` + +### 4. 启用事务事件模式(可选) + +若希望所有事件都在事务提交后再触发(避免 Handler 读到未提交数据),在 `application.properties` 中添加: + +```properties +codingapi.framework.event.transaction.enable=true +``` + +此时 `SpringTransactionEventHandler` 替代默认的 `SpringDefaultEventHandler`,所有事件将在 `AFTER_COMMIT` 阶段触发。 diff --git a/docs/agents/capabilities/springboot-starter/exception-handling.md b/docs/agents/capabilities/springboot-starter/exception-handling.md new file mode 100644 index 000000000..347de11b2 --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/exception-handling.md @@ -0,0 +1,139 @@ +--- +name: springboot-starter/exception-handling +module: springboot-starter +description: 全局异常处理,统一拦截 Controller 层异常并返回标准 Response +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在 Spring MVC 项目中,Controller 层抛出的异常如果不做统一处理,会导致以下问题: + +- **响应格式不一致**:不同接口返回的错误结构各异(有的返回 HTML 错误页、有的返回纯文本、有的返回自定义 JSON),前端难以统一解析。 +- **错误码缺失**:原生异常只有 message,没有业务错误码(errCode),前端无法根据错误码做多语言提示或差异化逻辑。 +- **国际化困难**:错误信息硬编码在代码中,无法根据用户 Locale 动态切换语言。 +- **敏感信息泄露**:未捕获的异常可能将堆栈信息直接暴露给客户端。 + +`springboot-starter` 的异常处理能力通过 `HandlerExceptionResolver` 机制,在 Servlet 层面统一拦截所有 Controller 异常,将其转换为标准的 `{success, errCode, errMessage}` JSON 响应,同时结合 `LocaleMessageException` 和 `MessageSource` 实现错误信息的国际化管理。 + +## 如何使用 + +### 自动生效 + +引入 `springboot-starter` 依赖后,异常处理自动配置即可生效,无需额外注解或配置: + +```xml + + com.codingapi.springboot + springboot-starter + +``` + +框架通过以下两个配置类自动注册: + +- **`ExceptionConfiguration`**:注册 `LocaleMessage` Bean,绑定 Spring `MessageSource`,在初始化时将自身注入 `MessageContext` 单例,为 `LocaleMessageException` 提供国际化消息解析能力。 +- **`BasicHandlerExceptionResolverConfiguration`**:当 classpath 中存在 `HandlerExceptionResolver`(即 Spring MVC 环境)时,注册 `ServletExceptionHandler` 作为全局异常解析器。 + +### 异常类型与响应映射 + +| 异常类型 | errCode | errMessage | 说明 | +|---------|---------|------------|------| +| `LocaleMessageException` | 异常中指定的 errCode | 国际化解析后的消息 | 业务异常,推荐使用 | +| 其他 `Exception` | `system.err` | `ex.getMessage()` | 未预期的系统异常 | + +### 抛出业务异常 + +在 Service 或 Controller 中抛出 `LocaleMessageException`,支持三种构造方式: + +```java +// 1. 直接使用错误码(从 messages.properties 中解析消息) +throw new LocaleMessageException("user.not.found"); + +// 2. 错误码 + 占位符参数 +throw LocaleMessageException.of("order.amount.exceed", maxAmount); + +// 3. 错误码 + 自定义消息(不走国际化) +throw new LocaleMessageException("custom.error", "自定义错误描述"); +``` + +### 配置国际化消息 + +在 `src/main/resources/messages.properties`(及对应的多语言文件)中定义错误消息: + +```properties +# messages.properties +user.not.found=用户不存在 +order.amount.exceed=订单金额超出限制,最大金额为 {0} + +# messages_en.properties +user.not.found=User not found +order.amount.exceed=Order amount exceeds limit, maximum is {0} +``` + +## 使用实例 + +### 完整示例:Controller 中抛出业务异常 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + User user = userService.findById(id); + if (user == null) { + // 抛出国际化业务异常 + throw new LocaleMessageException("user.not.found"); + } + return SingleResponse.of(user); + } + + @PostMapping + public Response createUser(@RequestBody CreateUserRequest request) { + if (request.getAmount().compareTo(MAX_AMOUNT) > 0) { + // 带占位符参数的异常 + throw LocaleMessageException.of("order.amount.exceed", MAX_AMOUNT); + } + userService.create(request); + return Response.buildSuccess(); + } +} +``` + +当请求触发异常时,框架自动返回统一格式的 JSON 响应: + +```json +{ + "success": false, + "errCode": "user.not.found", + "errMessage": "用户不存在" +} +``` + +若当前请求的 Locale 为英文,则自动返回: + +```json +{ + "success": false, + "errCode": "user.not.found", + "errMessage": "User not found" +} +``` + +对于未预期的系统异常(如 NullPointerException),返回: + +```json +{ + "success": false, + "errCode": "system.err", + "errMessage": "Cannot invoke method on null object" +} +``` diff --git a/docs/agents/capabilities/springboot-starter/locale-message.md b/docs/agents/capabilities/springboot-starter/locale-message.md new file mode 100644 index 000000000..fb106df5b --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/locale-message.md @@ -0,0 +1,117 @@ +--- +name: springboot-starter/locale-message +module: springboot-starter +description: 国际化异常消息,支持多语言异常信息和本地化消息解析 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级应用中,异常消息往往需要面向不同语言和地区的用户展示本地化内容。直接在代码中硬编码中文或英文错误提示存在以下痛点: + +- **多语言维护困难**:错误消息散落在业务代码各处,新增语言时需要逐一修改源码。 +- **异常与消息耦合**:`RuntimeException` 只接受字符串消息,无法携带错误码进行统一拦截和翻译。 +- **占位符参数化缺失**:部分错误消息需要动态插入变量(如用户名、金额),拼接字符串容易出错且不利于翻译。 + +`locale-message` 能力通过 `LocaleMessageException` + Spring `MessageSource` 的组合,将错误码作为异常的语义标识,在抛出时自动根据当前请求的 `Locale` 从 `messages.properties` 资源文件中解析出对应的本地化消息,从而实现异常消息与业务代码的解耦和多语言支持。 + +## 如何使用 + +### 核心组件 + +| 类 | 职责 | +|----|------| +| `LocaleMessageException` | 国际化异常,支持仅传错误码、错误码+占位符参数、错误码+自定义消息等多种构造方式 | +| `LocaleMessage` | 封装 Spring `MessageSource`,提供按 code + args + locale 解析消息的能力 | +| `MessageContext` | 单例上下文,持有 `LocaleMessage` 实例,供异常类在静态上下文中获取本地化消息 | + +### 配置消息资源文件 + +在 `src/main/resources/` 下创建标准 Spring 消息资源文件: + +```properties +# messages.properties(默认/回退) +error.user.not.found=User not found: {0} +error.param.invalid=Invalid parameter: {0}, expected {1} + +# messages_zh_CN.properties +error.user.not.found=用户不存在: {0} +error.param.invalid=参数无效: {0},期望值 {1} +``` + +Spring Boot 会自动加载这些文件作为 `MessageSource`。 + +### 抛出国际化异常 + +`LocaleMessageException` 提供多种构造方式和静态工厂方法: + +```java +// 1. 仅传错误码 —— 自动从 messages.properties 解析消息 +throw new LocaleMessageException("error.user.not.found"); + +// 2. 错误码 + 占位符参数 +throw LocaleMessageException.of("error.user.not.found", "zhangsan"); + +// 3. 错误码 + 多个占位符参数 +throw LocaleMessageException.of("error.param.invalid", "age", "Integer"); + +// 4. 错误码 + 自定义消息(不走 MessageSource) +throw new LocaleMessageException("ERR_CUSTOM", "自定义错误消息"); + +// 5. 携带原始异常 +throw new LocaleMessageException("error.user.not.found", cause); +``` + +### 自动装配 + +框架启动时,`LocaleMessage` Bean 会被创建并调用 `init()` 方法,将自身注册到 `MessageContext` 单例中。`LocaleMessageException` 在构造时通过 `MessageContext.getInstance().getErrorMsg(errCode, args)` 获取本地化消息,整个过程对业务代码透明,无需手动注入任何 Bean。 + +当前请求的 `Locale` 由 Spring 的 `LocaleContextHolder` 提供(通常通过 `Accept-Language` 请求头或 `LocaleChangeInterceptor` 设置)。 + +## 使用实例 + +### 完整示例:用户查询服务 + +```java +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + public User getUser(String username) { + return userRepository.findByUsername(username) + .orElseThrow(() -> + LocaleMessageException.of("error.user.not.found", username) + ); + } + + public void updateUserAge(String username, int age) { + if (age < 0 || age > 150) { + throw LocaleMessageException.of("error.param.invalid", "age", "0~150"); + } + // ... + } +} +``` + +当客户端发送 `Accept-Language: zh-CN` 请求时,若用户不存在,返回的异常消息为 `"用户不存在: zhangsan"`;若未指定语言或使用默认 locale,则返回 `"User not found: zhangsan"`。 + +### 在全局异常处理器中统一捕获 + +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(LocaleMessageException.class) + public Response handleLocaleException(LocaleMessageException e) { + return Response.buildFailure(e.getErrCode(), e.getErrMessage()); + } +} +``` + +由于异常对象已经携带了本地化后的 `errMessage`,全局处理器只需直接提取即可,无需再做二次翻译。 diff --git a/docs/agents/capabilities/springboot-starter/page-request.md b/docs/agents/capabilities/springboot-starter/page-request.md new file mode 100644 index 000000000..ebb742188 --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/page-request.md @@ -0,0 +1,177 @@ +--- +name: springboot-starter/page-request +module: springboot-starter +description: 动态分页查询请求,扩展 Spring PageRequest 支持 RequestFilter 动态过滤条件 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级后台管理系统中,列表查询是最常见的业务场景之一。Spring Data 原生的 `PageRequest` 仅支持分页参数(页码、大小、排序),无法携带动态过滤条件。开发者通常需要为每个查询接口手动编写 Specification、QueryDSL 或自定义 HQL,导致大量重复的查询构建代码。 + +`PageRequest` 能力解决了以下痛点: + +- **动态过滤**:在分页请求对象上直接附加任意字段、任意比较关系的过滤条件,无需为每种查询组合编写独立方法。 +- **前端驱动查询**:通过 `SearchRequest` 自动解析 HTTP 请求参数(包括 Base64 编码的 `filter`、`sort`、`params` JSON),将前端传入的筛选/排序规则转换为类型安全的过滤条件,减少 Controller 层样板代码。 +- **复杂条件组合**:支持 AND/OR 嵌套组合过滤,满足多条件联合查询需求。 +- **与 FastRepository 无缝集成**:`FastRepository.findAll(PageRequest)` 和 `pageRequest(PageRequest)` 自动根据过滤条件构建 Example 或 HQL 查询,开发者只需关注业务逻辑。 + +## 如何使用 + +### 核心类说明 + +| 类 | 职责 | +|---|------| +| `PageRequest` | 继承 Spring `PageRequest`,增加 `RequestFilter` 和链式 `addFilter` API | +| `RequestFilter` | 过滤条件容器,管理 `Filter` 列表并提供按 key 读取过滤值的快捷方法 | +| `Filter` | 单个过滤条件,包含字段名(key)、比较关系(Relation)和值 | +| `Relation` | 枚举,定义了 14 种比较关系:EQUAL、NOT_EQUAL、LIKE、LEFT_LIKE、RIGHT_LIKE、BETWEEN、IN、NOT_IN、IS_NULL、IS_NOT_NULL、GREATER_THAN、LESS_THAN、GREATER_THAN_EQUAL、LESS_THAN_EQUAL | +| `SearchRequest` | 从当前 `HttpServletRequest` 自动解析分页、排序、过滤参数并生成 `PageRequest` | + +### 创建 PageRequest + +```java +// 基本分页(第 0 页,每页 20 条) +PageRequest request = PageRequest.of(0, 20); + +// 带排序 +PageRequest request = PageRequest.of(0, 20, Sort.by("createTime").descending()); +``` + +### 添加过滤条件 + +```java +// 等值过滤(默认 EQUAL) +request.addFilter("name", "张三"); + +// 指定比较关系 +request.addFilter("age", Relation.GREATER_THAN, 18); +request.addFilter("createTime", Relation.BETWEEN, startDate, endDate); +request.addFilter("status", Relation.IN, "ACTIVE", "PENDING"); + +// AND / OR 组合 +request.andFilter( + Filter.as("deptId", 1), + Filter.as("roleId", 2) +); + +request.orFilters( + Filter.as("name", Relation.LIKE, "张"), + Filter.as("email", Relation.LIKE, "zhang") +); +``` + +### 读取过滤值 + +```java +String name = request.getStringFilter("name"); +int age = request.getIntFilter("age", 0); +boolean hasConditions = request.hasFilter(); +``` + +### 配合 FastRepository 使用 + +```java +// findAll — 简单过滤走 Example 查询 +Page page = userRepository.findAll(request); + +// pageRequest — 复杂过滤走 HQL 动态查询 +Page page = userRepository.pageRequest(request); + +// searchRequest — 从 HTTP 请求自动解析 +Page page = userRepository.searchRequest(searchRequest); +``` + +### SearchRequest 自动解析 + +`SearchRequest` 从当前 HTTP 请求中提取以下参数: + +| 参数 | 格式 | 说明 | +|------|------|------| +| `current` | int | 页码(从 0 开始) | +| `pageSize` | int | 每页大小 | +| `sort` | Base64(JSON) | 排序规则,如 `{"createTime":"descend"}` | +| `filter` | Base64(JSON) | 过滤条件,如 `{"status":["ACTIVE"]}` | +| `params` | Base64(JSON Array) | 指定字段的比较关系,如 `[{"key":"age","type":"GREATER_THAN"}]` | +| 其他参数 | string | 作为 EQUAL 过滤条件自动添加 | + +## 使用实例 + +### 示例 1:Service 层手动构建动态查询 + +```java +@Service +public class UserQueryService { + + @Autowired + private UserRepository userRepository; + + public Page searchUsers(String keyword, Integer minAge, String status) { + PageRequest request = PageRequest.of(0, 20); + request.addSort(Sort.by("createTime").descending()); + + if (keyword != null) { + request.addFilter("name", Relation.LIKE, keyword); + } + if (minAge != null) { + request.addFilter("age", Relation.GREATER_THAN_EQUAL, minAge); + } + if (status != null) { + request.addFilter("status", status); + } + + return userRepository.findAll(request); + } +} +``` + +### 示例 2:Controller 层使用 SearchRequest 自动绑定 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + @GetMapping + public MultiResponse list(SearchRequest searchRequest) { + // 自动从 HTTP 参数解析 current、pageSize、sort、filter + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page.getContent(), (int) page.getTotalElements()); + } +} +``` + +前端请求示例: +``` +GET /api/users?current=0&pageSize=20 + &sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9 + &filter=eyJzdGF0dXMiOlsiQUNUSVZFIiwiUEVORElORyJdfQ== + ¶ms=W3sia2V5IjoiYWdlIiwidHlwZSI6IkdSRUFURVJfVEhBTiJ9XQ== + &deptId=1 +``` + +其中 `filter` 解码后为 `{"status":["ACTIVE","PENDING"]}`,`params` 解码后为 `[{"key":"age","type":"GREATER_THAN"}]`,`deptId` 作为额外 EQUAL 条件自动加入。 + +### 示例 3:AND/OR 组合过滤 + +```java +PageRequest request = PageRequest.of(0, 20); + +// (deptId=1 AND roleId=2) OR (name LIKE '%admin%') +request.orFilters( + Filter.and( + Filter.as("deptId", 1), + Filter.as("roleId", 2) + ), + Filter.as("name", Relation.LIKE, "admin") +); + +Page page = userRepository.pageRequest(request); +``` diff --git a/docs/agents/capabilities/springboot-starter/response-dto.md b/docs/agents/capabilities/springboot-starter/response-dto.md new file mode 100644 index 000000000..e6a12f589 --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/response-dto.md @@ -0,0 +1,172 @@ +--- +name: springboot-starter/response-dto +module: springboot-starter +description: 统一响应封装,提供 Response/SingleResponse/MultiResponse/MapResponse 四种响应类型 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级 REST API 开发中,前后端交互需要统一的响应契约。若每个接口各自定义返回结构,会导致以下问题: + +- **前端解析复杂**:不同接口的成功/失败字段命名不一致,前端需逐接口适配 +- **错误处理分散**:缺乏统一的 `errCode` / `errMessage` 规范,全局异常拦截难以标准化 +- **分页结构不统一**:列表接口的总数、数据字段名各异,通用分页组件无法复用 +- **序列化行为不可控**:响应对象在缓存、消息队列等场景下需要可靠的 JSON 序列化能力 + +`response-dto` 提供了四种标准响应类型,覆盖无数据、单对象、列表/分页、键值对四类返回场景,使所有 API 输出遵循同一契约。基类 `Response` 实现了 `JsonSerializable` 接口,支持通过 Fastjson 进行一致的 JSON 序列化。 + +## 如何使用 + +### 类层次结构 + +``` +JsonSerializable (interface) + └── Response — 基础响应(success / errCode / errMessage) + ├── SingleResponse — 单对象响应,data 字段为泛型 T + ├── MultiResponse — 列表响应,data 字段包含 total + list + └── MapResponse — 键值对响应,data 字段为 Map +``` + +### 核心 API + +| 类 | 静态工厂方法 | 说明 | +|---|---|---| +| `Response` | `buildSuccess()` | 构建成功响应(无业务数据) | +| `Response` | `buildFailure(errCode, errMessage)` | 构建失败响应 | +| `SingleResponse` | `of(T data)` | 包装单个业务对象 | +| `SingleResponse` | `empty()` | 返回 data=null 的成功响应 | +| `MultiResponse` | `of(Collection data, long total)` | 包装集合 + 总数 | +| `MultiResponse` | `of(Page page)` | 直接包装 Spring Data Page 对象 | +| `MultiResponse` | `of(Collection data)` | 包装集合,total 自动取 size | +| `MultiResponse` | `empty()` | 返回空列表的成功响应 | +| `MapResponse` | `create()` | 创建空的键值对响应 | +| `MapResponse` | `empty()` | 返回 data=null 的键值对响应 | +| `MapResponse` | `add(key, value)` | 链式添加键值对 | + +### JSON 序列化 + +所有响应类均继承自 `Response`,而 `Response` 实现了 `JsonSerializable` 接口,可直接调用 `toJson()` 方法获取 JSON 字符串: + +```java +String json = response.toJson(); // 使用 Fastjson 序列化 +``` + +### Maven 依赖 + +```xml + + com.codingapi.springboot + springboot-starter + +``` + +## 使用实例 + +### 1. 无数据的操作响应 + +```java +@PostMapping("/save") +public Response save(@RequestBody UserRequest request) { + userService.save(request); + return Response.buildSuccess(); +} +``` + +返回 JSON: +```json +{ "success": true, "errCode": null, "errMessage": null } +``` + +### 2. 返回单个对象 + +```java +@GetMapping("/detail") +public SingleResponse detail(@RequestParam Long id) { + UserVO user = userQueryService.getById(id); + return SingleResponse.of(user); +} +``` + +返回 JSON: +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { "id": 1, "name": "张三", "email": "zhangsan@example.com" } +} +``` + +### 3. 返回列表(含分页) + +```java +@GetMapping("/list") +public MultiResponse list(SearchRequest searchRequest) { + Page page = userQueryService.page(searchRequest); + return MultiResponse.of(page); +} +``` + +返回 JSON: +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "total": 100, + "list": [ + { "id": 1, "name": "张三" }, + { "id": 2, "name": "李四" } + ] + } +} +``` + +### 4. 返回键值对数据 + +```java +@GetMapping("/dashboard/stats") +public MapResponse dashboardStats() { + return MapResponse.create() + .add("userCount", 1024) + .add("orderCount", 5678) + .add("todayRevenue", 99800.50); +} +``` + +返回 JSON: +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "userCount": 1024, + "orderCount": 5678, + "todayRevenue": 99800.50 + } +} +``` + +### 5. 返回错误响应 + +```java +@PostMapping("/login") +public Response login(@RequestBody LoginRequest request) { + if (!userService.authenticate(request)) { + return Response.buildFailure("AUTH_FAILED", "用户名或密码错误"); + } + return Response.buildSuccess(); +} +``` + +返回 JSON: +```json +{ "success": false, "errCode": "AUTH_FAILED", "errMessage": "用户名或密码错误" } +``` diff --git a/docs/agents/capabilities/springboot-starter/rest-client.md b/docs/agents/capabilities/springboot-starter/rest-client.md new file mode 100644 index 000000000..fa92c6fc0 --- /dev/null +++ b/docs/agents/capabilities/springboot-starter/rest-client.md @@ -0,0 +1,108 @@ +--- +name: springboot-starter/rest-client +module: springboot-starter +description: REST 客户端封装,提供 RestTemplate 上下文管理和 HTTPS 信任所有证书支持 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业内部系统集成和第三方 API 对接场景中,经常需要调用外部 HTTPS 服务。这些服务的证书可能由私有 CA 签发、已过期或主机名不匹配,导致标准 HTTP 客户端抛出 SSL 握手异常。同时,项目中多处使用 `RestTemplate` 时,如果每次都手动创建实例并配置底层 HttpClient,会导致代码重复且难以统一维护。 + +本能力通过两个核心类解决上述问题: + +- **TrustAnyHttpClientFactory** — 创建信任所有证书的 Apache HttpClient 5 实例,跳过服务端证书校验和主机名验证,适用于开发调试及内网可信环境下的 HTTPS 调用。 +- **RestTemplateContext** — 以单例模式持有预配置好的 `RestTemplate`(底层使用 TrustAnyHttpClientFactory),为全应用提供统一的 REST 客户端访问入口,避免重复创建和配置。 + +## 如何使用 + +### 依赖引入 + +在 Maven 项目中添加 springboot-starter 依赖即可: + +```xml + + com.codingapi.springboot + springboot-starter + +``` + +### 获取 RestTemplate 实例 + +通过 `RestTemplateContext` 的单例方法获取已配置好 HTTPS 信任策略的 `RestTemplate`: + +```java +RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); +``` + +该实例底层使用 Apache HttpComponents 5 作为传输层,并已启用以下特性: + +- TLS 协议下信任所有服务端证书(`TrustAnyTrustManager`) +- 禁用主机名验证(`NoopHostnameVerifier`) +- 允许循环重定向 + +### 自定义 ClientHttpRequestFactory + +如果需要替换默认的请求工厂,可以使用 `restTemplate(ClientHttpRequestFactory)` 方法创建新的 `RestTemplate` 实例: + +```java +ClientHttpRequestFactory customFactory = new HttpComponentsClientHttpRequestFactory(customHttpClient); +RestTemplate customTemplate = RestTemplateContext.getInstance().restTemplate(customFactory); +``` + +### 直接使用 TrustAnyHttpClientFactory + +若仅需创建信任所有证书的 HttpClient 而不使用 RestTemplateContext 封装,可直接调用静态工厂方法: + +```java +HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); +``` + +返回的 `HttpClient` 基于 Apache HttpClient 5,可用于任何需要绕过 SSL 校验的场景。 + +## 使用实例 + +### 示例 1:调用外部 HTTPS 接口 + +```java +@Service +public class ExternalApiService { + + private final RestTemplate restTemplate; + + public ExternalApiService() { + this.restTemplate = RestTemplateContext.getInstance().getRestTemplate(); + } + + public String fetchRemoteData(String url) { + ResponseEntity response = restTemplate.getForEntity(url, String.class); + return response.getBody(); + } + + public T postJson(String url, Object request, Class responseType) { + return restTemplate.postForObject(url, request, responseType); + } +} +``` + +### 示例 2:独立使用 TrustAnyHttpClientFactory + +```java +// 在不依赖 RestTemplateContext 的场景下,单独创建信任所有证书的 HttpClient +HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); + +// 配合 Spring 的 HttpComponentsClientHttpRequestFactory 构建自定义 RestTemplate +HttpComponentsClientHttpRequestFactory factory = + new HttpComponentsClientHttpRequestFactory(httpClient); +factory.setConnectTimeout(Duration.ofSeconds(5)); +factory.setReadTimeout(Duration.ofSeconds(30)); + +RestTemplate restTemplate = new RestTemplate(factory); +String result = restTemplate.getForObject("https://internal-api.example.com/data", String.class); +``` + +> ⚠️ **安全提示**:信任所有证书仅适用于开发调试和内网可信环境。在生产环境中应正确配置受信任的 CA 证书链,避免中间人攻击风险。 diff --git a/docs/agents/conventions/springboot-starter-data-fast/dynamic-query-convention.md b/docs/agents/conventions/springboot-starter-data-fast/dynamic-query-convention.md new file mode 100644 index 000000000..3ee7ca78c --- /dev/null +++ b/docs/agents/conventions/springboot-starter-data-fast/dynamic-query-convention.md @@ -0,0 +1,148 @@ +--- +name: springboot-starter-data-fast/dynamic-query-convention +module: springboot-starter-data-fast +description: 动态查询规范,定义 PageRequest + RequestFilter + FastRepository 的使用模式 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter-data-fast +import: "com.codingapi.springboot:springboot-starter-data-fast" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在企业级应用中,列表查询往往需要根据前端传入的动态条件进行过滤。如果不遵守本规范,会导致以下问题: + +1. **SQL 注入风险**:在 Service 层手动拼接 SQL 字符串实现动态查询,极易引入 SQL 注入漏洞。 +2. **代码重复与维护困难**:每个实体的动态查询都手写 `if-else` 判断和原生 SQL,导致大量重复代码,新增过滤字段需修改多处。 +3. **分页逻辑不一致**:各模块自行处理分页参数(偏移量计算、排序),缺乏统一标准,容易出现边界错误。 +4. **CQRS 职责混乱**:查询逻辑与命令逻辑耦合在同一 Service 中,违反读写分离原则,增加系统复杂度。 +5. **类型安全缺失**:使用 Map 或 JSON 传递查询条件时,缺乏编译期检查,运行时才发现字段名拼写错误或类型不匹配。 +6. **框架能力浪费**:不使用 `FastRepository` 提供的自动 Example/HQL 构建能力,绕过框架自建查询机制,失去统一的 SQL 拦截(如数据权限)支持。 + +## 如何使用 + +### 核心规则 + +1. **列表查询统一使用 `PageRequest` 构建分页参数** + - 通过 `PageRequest.of(page, size)` 或 `new PageRequest()` 创建请求对象。 + - 禁止直接使用 Spring Data 原生 `org.springframework.data.domain.PageRequest` 来承载业务过滤条件。 + +2. **动态过滤条件通过 `PageRequest.addFilter()` 方法添加** + - 简单等值过滤:`pageRequest.addFilter("name", "张三")`,默认使用 `Relation.EQ`。 + - 指定关系过滤:`pageRequest.addFilter("age", Relation.GT, 18)`。 + - 组合过滤:使用 `andFilter(Filter...)` 和 `orFilters(Filter...)` 构建复杂条件。 + +3. **过滤关系使用 `Relation` 枚举** + - 可用关系包括:`EQ`、`GT`、`LT`、`GTE`、`LTE`、`LIKE`、`IN` 等。 + - 所有过滤关系必须通过枚举表达,禁止硬编码字符串比较运算符。 + +4. **Repository 接口需继承 `FastRepository`** + - 实体 Repository 必须扩展 `FastRepository` 以获得动态查询能力。 + - `FastRepository` 同时继承了 `JpaRepository`、`JpaSpecificationExecutor`、`DynamicRepository` 和 `DynamicNativeRepository`。 + +5. **调用 `FastRepository.findAll(PageRequest)` 执行动态查询** + - 当 `PageRequest` 包含 Filter 时,自动通过 `ExampleBuilder` 构建 Example 查询。 + - 需要 HQL 级别查询时,使用 `pageRequest(PageRequest)` 方法,自动通过 `DynamicSQLBuilder` 生成 HQL。 + - 禁止绕过 `FastRepository` 提供的默认方法自行编写动态查询逻辑。 + +6. **禁止在 Service 层手写原生 SQL 实现动态查询** + - 不允许使用 `@Query(nativeQuery = true)` 或 `EntityManager.createNativeQuery()` 拼接动态条件。 + - 所有动态过滤必须委托给 `FastRepository` 的 Filter 机制完成。 + +7. **查询服务(CQRS Query 侧)应独立于命令服务** + - 查询服务放在 `*-app-query` 模块中,仅负责读取操作。 + - 命令服务放在 `*-app-cmd-*` 模块中,负责写入与领域编排。 + - 两者不得互相依赖或合并为同一个类。 + +### 命名约定 + +- 查询服务类命名:`{Entity}QueryService`,位于 `*.query` 包下。 +- 命令服务类命名:`{Entity}CmdService`,位于 `*.cmd` 包下。 +- Controller 中的列表查询方法参数类型统一为 `PageRequest`。 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. Repository 继承 FastRepository +public interface UserRepository extends FastRepository { +} + +// 2. 查询服务独立于命令服务 +@Service +public class UserQueryService { + + @Autowired + private UserRepository userRepository; + + public Page listUsers(String name, Integer minAge, int page, int size) { + PageRequest request = PageRequest.of(page, size); + + // 动态添加过滤条件 + if (StringUtils.hasText(name)) { + request.addFilter("name", Relation.LIKE, name); + } + if (minAge != null) { + request.addFilter("age", Relation.GTE, minAge); + } + + // 委托 FastRepository 自动构建查询 + return userRepository.findAll(request); + } +} + +// 3. Controller 接收 PageRequest +@GetMapping("/users") +public MultiResponse list( + @RequestParam(required = false) String name, + @RequestParam(required = false) Integer minAge, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + Page result = userQueryService.listUsers(name, minAge, page, size); + return ResponseUtils.toMultiResponse(result); +} +``` + +### ❌ 错误示例 + +```java +// 错误 1: Repository 未继承 FastRepository,丧失动态查询能力 +public interface UserRepository extends JpaRepository { +} + +// 错误 2: Service 中手写原生 SQL 拼接动态条件 +@Service +public class UserService { + + @PersistenceContext + private EntityManager entityManager; + + public List listUsers(String name, Integer minAge) { + StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1"); + if (name != null) { + sql.append(" AND name LIKE '%").append(name).append("%'"); // SQL 注入风险! + } + if (minAge != null) { + sql.append(" AND age >= ").append(minAge); + } + return entityManager.createNativeQuery(sql.toString(), User.class).getResultList(); + } +} + +// 错误 3: 查询与命令逻辑混合在同一个 Service 中 +@Service +public class UserService { + public Page list(...) { /* 查询逻辑 */ } + public void createUser(...) { /* 命令逻辑 */ } + public void deleteUser(...) { /* 命令逻辑 */ } +} + +// 错误 4: 使用 Spring Data 原生 PageRequest,无法承载 Filter +@GetMapping("/users") +public Page list(org.springframework.data.domain.PageRequest pageable) { + // 丢失了框架的动态过滤能力 + return userRepository.findAll(pageable); +} +``` diff --git a/docs/agents/conventions/springboot-starter/ddd-layered-architecture.md b/docs/agents/conventions/springboot-starter/ddd-layered-architecture.md new file mode 100644 index 000000000..8245f78c1 --- /dev/null +++ b/docs/agents/conventions/springboot-starter/ddd-layered-architecture.md @@ -0,0 +1,225 @@ +--- +name: springboot-starter/ddd-layered-architecture +module: springboot-starter +description: DDD 分层架构规范,定义 interface/app/domain/infra 四层的依赖规则和职责划分 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +不遵守 DDD 分层架构规范会导致以下问题: + +1. **领域逻辑污染**:domain 层直接引用 infra 层实现类(如 JPA Repository、Redis Client),导致业务规则与基础设施耦合,无法独立演进和测试。 +2. **依赖方向混乱**:若 domain 反向依赖 app 或 interface,修改一个 API 接口可能牵连整个领域模型重构,变更成本指数级增长。 +3. **CQRS 读写混淆**:查询服务与命令服务混杂在同一模块中,复杂查询被迫走聚合根加载全量数据,性能瓶颈难以定位和优化。 +4. **接口层职责膨胀**:Controller 中直接编排业务流程、拼装 DTO、处理事件,导致接口层成为"上帝类",复用性和可维护性急剧下降。 +5. **Repository 契约缺失**:domain 层直接使用具体持久化实现而非接口,切换存储方案(如从 MySQL 迁移到 MongoDB)需要改动所有调用方代码。 + +## 如何使用 + +### 四层架构与依赖方向 + +``` +interface → app → domain ← infra +``` + +| 层级 | 模块命名约定 | 核心职责 | 允许依赖 | +|------|-------------|---------|---------| +| **interface**(接口层) | `*-interface` | Controller(REST API)、Handler(事件处理器)、Runner(启动任务) | app | +| **app**(应用层) | `*-app-query` / `*-app-cmd-*` | query:CQRS 查询服务;cmd:命令服务与领域编排 | domain | +| **domain**(领域层) | `*-domain-*` | Entity、ValueObject、DomainService、Repository 接口、DomainEvent | 无外部框架 | +| **infra**(基础设施层) | `*-infra-*` | Repository 实现、Gateway 实现、外部服务适配、持久化配置 | domain | + +### 核心规则 + +1. **依赖方向严格单向**:`interface → app → domain ← infra`。禁止反向依赖和跨层依赖。 +2. **domain 层纯业务**:domain 层只包含领域模型和业务逻辑,不依赖 Spring、JPA、Redis 等任何外部框架。 +3. **Repository 接口在 domain,实现在 infra**:domain 层定义 Repository 接口,infra 层提供具体实现并通过 Spring Bean 注入。 +4. **app 层按 CQRS 拆分**:查询服务(query)与命令服务(cmd)分模块,query 侧可直接读取视图/DTO,cmd 侧通过聚合根执行业务操作。 +5. **interface 层仅做适配**:Controller 只做参数校验、权限检查和响应封装,业务编排委托给 app 层;Handler 只负责事件监听与转发。 +6. **禁止 domain 引用 infra 实现类**:domain 层不得 import infra 包下的任何类,包括具体的 Repository 实现、DAO、Client 等。 + +### 包结构约定 + +``` +{bounded-context}/ + {context}-interface/ # 接口层 + controller/ + handler/ + runner/ + {context}-app/ # 应用层 + {context}-app-query/ # CQRS Query 侧 + {context}-app-cmd-domain/ # CQRS Command 侧(领域编排) + {context}-domain/ # 领域层 + model/ # Entity, ValueObject + repository/ # Repository 接口 + service/ # DomainService + event/ # DomainEvent + {context}-infra/ # 基础设施层 + jpa/ # Repository 实现 + gateway/ # 外部服务适配 +``` + +## 使用实例 + +### ✅ 正确示例 + +**domain 层定义 Repository 接口:** + +```java +// example-domain-user/src/.../repository/UserRepository.java +package com.example.domain.user.repository; + +public interface UserRepository { + User findById(Long id); + void save(User user); +} +``` + +**infra 层提供 Repository 实现:** + +```java +// example-infra-jpa/src/.../jpa/UserRepositoryImpl.java +package com.example.infra.jpa; + +import com.example.domain.user.repository.UserRepository; +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepositoryImpl implements UserRepository { + private final UserJpaDao userJpaDao; + + @Override + public User findById(Long id) { + return userJpaDao.findById(id).map(UserConverter::toDomain).orElse(null); + } + + @Override + public void save(User user) { + userJpaDao.save(UserConverter.toEntity(user)); + } +} +``` + +**app 层按 CQRS 拆分:** + +```java +// example-app-query: 查询服务,直接返回 DTO +@Service +public class UserQueryService { + private final UserReadDao userReadDao; // 可直接读视图 + + public SingleResponse getUser(Long id) { + return SingleResponse.of(userReadDao.findUserDTO(id)); + } +} + +// example-app-cmd-domain: 命令服务,通过聚合根操作 +@Service +public class UserCommandService { + private final UserRepository userRepository; // domain 层接口 + + @Transactional + public Response createUser(CreateUserCmd cmd) { + User user = User.create(cmd.getName(), cmd.getEmail()); + userRepository.save(user); + return Response.success(); + } +} +``` + +**interface 层仅做适配:** + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + private final UserQueryService queryService; + private final UserCommandService commandService; + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + return queryService.getUser(id); // 委托 app 层 + } + + @PostMapping + public Response createUser(@Valid @RequestBody CreateUserCmd cmd) { + return commandService.createUser(cmd); // 委托 app 层 + } +} +``` + +### ❌ 错误示例 + +**domain 层直接引用 infra 实现类:** + +```java +// ❌ domain 层 import 了 infra 包的类 +package com.example.domain.user.service; + +import com.example.infra.jpa.UserJpaDao; // 违规!domain 不应依赖 infra + +@Service +public class UserService { + private final UserJpaDao userJpaDao; // 应使用 UserRepository 接口 + + public User findUser(Long id) { + return userJpaDao.findById(id).orElse(null); + } +} +``` + +**依赖方向反转:** + +```java +// ❌ domain 层反向依赖 app 层 +package com.example.domain.user.model; + +import com.example.app.cmd.domain.UserService; // 违规!domain 不应依赖 app + +public class User { + public void activate() { + UserService userService = ...; // 领域对象不应调用应用服务 + userService.sendActivationEmail(this); + } +} +``` + +**app 层未拆分 CQRS,查询走聚合根:** + +```java +// ❌ 查询和命令混在一起,列表查询加载完整聚合根 +@Service +public class UserService { + private final UserRepository userRepository; + + // 列表查询不应加载完整 User 聚合根 + public List listUsers(String keyword) { + return userRepository.findAll().stream() + .filter(u -> u.getName().contains(keyword)) + .collect(Collectors.toList()); // 内存过滤,性能灾难 + } +} +``` + +**Controller 中直接编排业务逻辑:** + +```java +// ❌ Controller 承担了本应属于 app 层的职责 +@PostMapping("/orders") +public Response createOrder(@RequestBody OrderCmd cmd) { + User user = userRepository.findById(cmd.getUserId()); // 应在 app 层 + Product product = productRepository.findById(cmd.getProductId()); // 应在 app 层 + if (product.getStock() < cmd.getQuantity()) { // 业务校验应在 domain + return Response.error("库存不足"); + } + Order order = new Order(user, product, cmd.getQuantity()); // 领域创建应在 app/cmd + orderRepository.save(order); // 应在 app 层 + eventPusher.push(new OrderCreatedEvent(order)); // 事件推送应在 app 层 + return Response.success(order.getId()); +} +``` diff --git a/docs/agents/conventions/springboot-starter/event-driven-convention.md b/docs/agents/conventions/springboot-starter/event-driven-convention.md new file mode 100644 index 000000000..c53b767cc --- /dev/null +++ b/docs/agents/conventions/springboot-starter/event-driven-convention.md @@ -0,0 +1,217 @@ +--- +name: springboot-starter/event-driven-convention +module: springboot-starter +description: 事件驱动开发规范,定义 IEvent/IHandler 的使用约定和事件处理模式 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +不遵守事件驱动开发规范会导致以下问题: + +1. **事务耦合风险**:将事件处理与主业务事务强绑定,导致分支业务的失败回滚整个主事务,破坏领域事件的独立性原则。当分支逻辑(如通知、日志、缓存更新)出错时,不应影响核心业务流程的提交。 + +2. **事件类型混乱**:不区分同步事件与异步事件,导致关键业务事件被异步执行产生时序问题,或非关键事件被同步阻塞影响主流程性能。 + +3. **Handler 注册失效**:未通过 `@Service` 注解将 Handler 注册为 Spring Bean,导致框架无法自动发现处理器,事件推送后无响应且难以排查。 + +4. **执行顺序不可控**:多个 Handler 订阅同一事件时未指定 `order()`,导致处理顺序依赖 Bean 加载顺序,在不同环境或重启后行为不一致。 + +5. **异常处理缺失**:未实现 `error()` 回调或直接在 `handler()` 中抛出未捕获异常,导致后续 Handler 被跳过且错误信息丢失,事件链中断而无法感知。 + +6. **绕过 EventPusher 直接调用**:手动实例化 Handler 或直接调用处理方法,绕过框架的循环检测、线程池调度和上下文传递机制,引发循环事件死锁或丢失链路追踪信息。 + +## 如何使用 + +### 规则 1:事件类必须实现 IEvent 接口 + +所有事件类必须实现 `IEvent` 接口。根据执行方式选择子接口: + +- **同步事件**:实现 `ISyncEvent`,在当前线程内按顺序执行所有 Handler +- **异步事件**:实现 `IAsyncEvent`,由框架线程池异步调度执行 + +事件类应为纯数据载体,不包含业务逻辑,并实现 `Serializable` 以支持序列化传输。 + +### 规则 2:事件处理器必须实现 IHandler 接口 + +Handler 通过泛型参数声明订阅的事件类型。框架在启动时通过 `HandlerBeanDefinitionRegistrar` 自动扫描所有 `IHandler` 实现并注册到 `ApplicationHandlerUtils`。 + +```java +public interface IHandler { + default int order() { return 0; } + void handler(T event); + default void error(Exception exception) throws Exception { throw exception; } +} +``` + +### 规则 3:事件处理器通过 @Service 注解注册为 Spring Bean + +Handler 必须标注 `@Service`(或 `@Component`)注解,确保被 Spring 容器管理。未注册为 Bean 的 Handler 不会被框架发现和调用。 + +### 规则 4:事件推送统一使用 EventPusher.push() 静态方法 + +所有事件推送必须通过 `EventPusher.push(event)` 发起,禁止手动调用 Handler。框架内置循环事件检测机制,当检测到事件循环推送时自动抛出异常。若确认需要允许循环事件,可使用 `EventPusher.push(event, true)` 关闭检测。 + +### 规则 5:事件不应与主业务强耦合事务绑定 + +事件处理的核心理念是**解耦**。事件对于主业务来说可成功可失败,成功与失败都不应强关联主体业务。若分支逻辑必须与主事务保持一致,应直接使用服务调用而非事件机制。 + +### 规则 6:多个 Handler 通过 order() 方法控制执行顺序 + +当多个 Handler 订阅同一事件时,通过重写 `order()` 方法指定执行优先级。数值越小越先执行,默认值为 0。相同 order 值的 Handler 执行顺序不确定。 + +### 规则 7:Handler 的 error() 回调处理异常 + +`error()` 方法接收 Handler 执行过程中抛出的异常。默认实现会重新抛出异常,这将**阻止后续 Handler 的执行**。若希望某个 Handler 的失败不影响其他 Handler,应在 `error()` 中记录日志而不重新抛出。 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. 定义同步事件 +public class UserCreatedEvent implements ISyncEvent { + private final String userId; + private final String username; + + public UserCreatedEvent(String userId, String username) { + this.userId = userId; + this.username = username; + } + + public String getUserId() { return userId; } + public String getUsername() { return username; } +} + +// 2. 定义异步事件 +public class UserNotificationEvent implements IAsyncEvent { + private final String userId; + private final String message; + + public UserNotificationEvent(String userId, String message) { + this.userId = userId; + this.message = message; + } + + public String getUserId() { return userId; } + public String getMessage() { return message; } +} + +// 3. 注册 Handler 并指定执行顺序 +@Service +public class UserCacheRefreshHandler implements IHandler { + + @Override + public int order() { + return 1; // 优先刷新缓存 + } + + @Override + public void handler(UserCreatedEvent event) { + cacheService.refreshUserCache(event.getUserId()); + } + + @Override + public void error(Exception exception) { + // 缓存刷新失败不影响后续 Handler,仅记录日志 + log.warn("缓存刷新失败: {}", exception.getMessage()); + } +} + +@Service +public class UserAuditLogHandler implements IHandler { + + @Override + public int order() { + return 2; // 缓存之后写审计日志 + } + + @Override + public void handler(UserCreatedEvent event) { + auditService.logUserCreation(event.getUserId(), event.getUsername()); + } +} + +// 4. 在业务中推送事件 +@Service +public class UserService { + + public User createUser(CreateUserCommand command) { + User user = userRepository.save(command.toEntity()); + // 通过 EventPusher 推送,不直接调用 Handler + EventPusher.push(new UserCreatedEvent(user.getId(), user.getUsername())); + return user; + } +} +``` + +### ❌ 错误示例 + +```java +// 错误 1:事件未实现 IEvent 接口 +public class UserCreatedEvent { // ❌ 缺少 ISyncEvent / IAsyncEvent + private String userId; +} + +// 错误 2:Handler 未注册为 Spring Bean +public class UserCacheHandler implements IHandler { // ❌ 缺少 @Service + @Override + public void handler(UserCreatedEvent event) { + cacheService.refreshUserCache(event.getUserId()); + } +} + +// 错误 3:在事务中强绑定事件结果 +@Transactional +public User createUser(CreateUserCommand command) { + User user = userRepository.save(command.toEntity()); + try { + EventPusher.push(new UserCreatedEvent(user.getId(), user.getUsername())); + } catch (Exception e) { + throw new RuntimeException("事件处理失败,回滚事务", e); // ❌ 事件失败不应回滚主事务 + } + return user; +} + +// 错误 4:手动调用 Handler 绕过框架 +@Service +public class UserService { + @Autowired + private UserCacheRefreshHandler cacheHandler; + + public User createUser(CreateUserCommand command) { + User user = userRepository.save(command.toEntity()); + cacheHandler.handler(new UserCreatedEvent(user.getId(), user.getUsername())); // ❌ 绕过 EventPusher + return user; + } +} + +// 错误 5:未处理异常导致事件链中断 +@Service +public class FragileHandler implements IHandler { + @Override + public void handler(UserCreatedEvent event) { + externalService.notify(event.getUserId()); // 可能抛出异常 + } + // ❌ 未重写 error(),默认重新抛出异常,阻止后续 Handler 执行 +} + +// 错误 6:多个 Handler 未指定 order,执行顺序不可预测 +@Service +public class HandlerA implements IHandler { + @Override + public void handler(UserCreatedEvent event) { /* ... */ } + // ❌ 未重写 order(),默认 0,与 HandlerB 顺序不确定 +} + +@Service +public class HandlerB implements IHandler { + @Override + public void handler(UserCreatedEvent event) { /* ... */ } + // ❌ 未重写 order(),默认 0,与 HandlerA 顺序不确定 +} +``` diff --git a/docs/agents/conventions/springboot-starter/exception-handling-convention.md b/docs/agents/conventions/springboot-starter/exception-handling-convention.md new file mode 100644 index 000000000..3f2d53ffa --- /dev/null +++ b/docs/agents/conventions/springboot-starter/exception-handling-convention.md @@ -0,0 +1,152 @@ +--- +name: springboot-starter/exception-handling-convention +module: springboot-starter +description: 全局异常处理规范,定义异常抛出和捕获的标准模式 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +不遵守此规范会导致以下问题: + +1. **错误响应格式不一致**:不同开发者在 Controller 中自行 try-catch 并返回自定义格式的错误信息,导致前端无法统一解析 `errCode` / `errMessage` 字段,增加联调和维护成本。 +2. **缺失国际化支持**:直接抛出 `RuntimeException` 或硬编码中文消息,无法根据请求 Locale 切换语言,多语言场景下用户体验差且需要大量改造。 +3. **异常被吞没**:Controller 层使用 try-catch 捕获异常后仅打印日志或返回 null,调用方无法感知业务失败原因,排查线上问题时缺少关键上下文。 +4. **错误码体系混乱**:各模块自行定义错误码常量或直接拼接字符串,缺乏统一的 key → message 映射机制,同一个业务含义可能出现多个不同的 errCode。 +5. **全局拦截失效**:绕过框架的 `HandlerExceptionResolver` 机制手动处理异常,导致日志记录、监控埋点等横切关注点被跳过。 + +## 如何使用 + +### 规则 1:业务异常统一使用 LocaleMessageException + +所有业务校验失败、参数非法、权限不足等场景,必须抛出 `LocaleMessageException`,禁止直接使用 `RuntimeException`、`IllegalArgumentException` 等原生异常。 + +```java +// 构造方式一:errCode + 默认消息(推荐用于简单场景) +throw new LocaleMessageException("user.not.found", "用户不存在"); + +// 构造方式二:仅 errCode,消息从 message.properties 自动解析 +throw new LocaleMessageException("user.not.found"); + +// 构造方式三:带占位符参数,对应 properties 中 user.duplicate=用户名 {0} 已存在 +throw LocaleMessageException.of("user.duplicate", username); +``` + +### 规则 2:异常消息采用 message key + 默认消息格式 + +- 第一个参数为 **errCode**(即 i18n message key),用于前端匹配和国际化查找。 +- 第二个参数为 **默认消息**(defaultMessage),当 message.properties 中未配置该 key 时作为兜底展示。 +- errCode 命名采用 **点分隔小写** 格式,如 `order.status.invalid`、`auth.token.expired`。 + +### 规则 3:依赖 ExceptionConfiguration 全局拦截 + +框架通过 `ExceptionConfiguration` 注册 `LocaleMessage` Bean,并由 `BasicHandlerExceptionResolverConfiguration` 中的 `ServletExceptionHandler` 统一拦截所有 Controller 层异常: + +- `LocaleMessageException` → 提取 `errCode` 和 `errMessage`,返回标准 Response 格式。 +- 其他未知异常 → 使用默认 errCode `system.err`,返回异常原始消息。 + +**无需在任何 Controller 中添加额外的 @ExceptionHandler。** + +### 规则 4:异常响应统一返回 Response 格式 + +全局拦截器返回的 JSON 结构如下: + +```json +{ + "success": false, + "errCode": "user.not.found", + "errMessage": "用户不存在" +} +``` + +前端只需判断 `success === false` 即可进入统一错误处理流程。 + +### 规则 5:禁止在 Controller 中使用 try-catch 吞掉异常 + +Controller 方法应保持简洁,让异常自然向上抛出由全局处理器接管。如需对特定异常做差异化处理(如参数校验),应在 Service 层完成转换后再抛出 `LocaleMessageException`。 + +### 规则 6:禁止直接抛出 RuntimeException + +`RuntimeException` 没有 `errCode` 字段,全局拦截器只能将其归入 `system.err`,丢失了业务语义。所有可预见的业务异常都必须使用 `LocaleMessageException`。 + +## 使用实例 + +### ✅ 正确示例 + +```java +@Service +public class UserService { + + public User getUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new LocaleMessageException("user.not.found", "用户不存在")); + } + + public void createUser(String username) { + if (userRepository.existsByUsername(username)) { + // 使用静态工厂方法 + 占位符参数 + throw LocaleMessageException.of("user.duplicate", username); + } + // ... + } +} + +@RestController +@RequestMapping("/users") +public class UserController { + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + // 不需要 try-catch,异常由全局处理器接管 + return SingleResponse.of(userService.getUser(id)); + } +} +``` + +对应的 `messages_zh_CN.properties`: + +```properties +user.not.found=用户不存在 +user.duplicate=用户名 {0} 已存在 +``` + +### ❌ 错误示例 + +```java +// 错误 1:直接抛出 RuntimeException,缺少 errCode +public User getUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("用户不存在")); +} + +// 错误 2:Controller 中 try-catch 吞掉异常 +@GetMapping("/{id}") +public SingleResponse getUser(@PathVariable Long id) { + try { + return SingleResponse.of(userService.getUser(id)); + } catch (Exception e) { + log.error("查询失败", e); + return null; // 前端收到 null,无法区分成功与失败 + } +} + +// 错误 3:Controller 中自行构建错误响应,绕过全局拦截 +@GetMapping("/{id}") +public ResponseEntity getUser(@PathVariable Long id) { + try { + return ResponseEntity.ok(SingleResponse.of(userService.getUser(id))); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("code", 500); + error.put("msg", e.getMessage()); + return ResponseEntity.status(500).body(error); + } +} + +// 错误 4:硬编码中文消息,不支持国际化 +throw new LocaleMessageException("err001", "这个用户找不到啊"); +``` diff --git a/docs/agents/conventions/springboot-starter/response-convention.md b/docs/agents/conventions/springboot-starter/response-convention.md new file mode 100644 index 000000000..4611f12ff --- /dev/null +++ b/docs/agents/conventions/springboot-starter/response-convention.md @@ -0,0 +1,156 @@ +--- +name: springboot-starter/response-convention +module: springboot-starter +description: 统一响应格式规范,定义 Controller 层返回值的标准格式 +status: 已实现 +scope: 后端 +source: 框架:springboot-starter +import: "com.codingapi.springboot:springboot-starter" +framework_version: "3.4.54" +--- + +## 解决什么问题 + +在 REST API 开发中,如果不对响应格式做统一约束,会导致以下问题: + +- **前端解析困难**:不同接口返回结构不一致(有的直接返回对象、有的返回 Map、有的包裹在自定义结构中),前端需要为每个接口单独适配,增加维护成本。 +- **错误处理碎片化**:没有统一的 `success` / `errCode` / `errMessage` 字段,前端无法用一套逻辑判断请求是否成功、展示错误提示。 +- **分页结构不统一**:列表接口各自定义分页字段名(`total` / `count` / `records` / `items`),前端分页组件难以复用。 +- **API 契约不稳定**:随意返回裸 Map 或临时 DTO,字段增减无感知,容易引发线上联调故障。 +- **国际化与监控缺失基础**:缺少标准化的错误码字段,后续接入国际化、链路追踪、告警分类时改造成本极高。 + +本规范通过强制使用框架提供的 Response 体系,确保所有 API 输出结构一致、可预测、可机器解析。 + +## 如何使用 + +### 核心规则 + +1. **Controller 方法返回值必须使用 Response 体系**:只能返回 `Response`、`SingleResponse`、`MultiResponse` 或 `MapResponse` 之一,禁止返回其他类型。 +2. **单个对象返回**:使用 `SingleResponse.of(data)`。 +3. **列表返回**:使用 `MultiResponse.of(list)` 或 `MultiResponse.of(page)`(接受 Spring Data `Page` 对象)。 +4. **无数据操作成功返回**:使用 `Response.buildSuccess()`。 +5. **失败返回**:使用 `Response.buildFailure(errCode, errMessage)`。 +6. **禁止直接返回 Map 或自定义 DTO 作为 API 响应**。 +7. **响应 JSON 结构固定包含**:`success`(boolean)、`errCode`(string)、`errMessage`(string)、`data`(业务数据,仅 SingleResponse/MultiResponse 携带)。 + +### 补充说明 + +- `SingleResponse.empty()` 用于查询可能为空但语义上成功的场景,返回 `{ success: true, data: null }`。 +- `MultiResponse.of(collection, total)` 用于手动分页场景;`MultiResponse.of(page)` 自动从 Spring Data Page 提取 total。 +- `MultiResponse.empty()` 返回空列表 `{ success: true, data: { total: 0, list: [] } }`。 +- 异常处理应通过全局异常处理器统一转换为 `Response.buildFailure(...)`,Controller 内不要 try-catch 后自行拼装错误响应。 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. 单对象查询 +@GetMapping("/users/{id}") +public SingleResponse getUser(@PathVariable Long id) { + UserDTO user = userService.findById(id); + return SingleResponse.of(user); +} + +// 2. 分页列表查询 +@GetMapping("/users") +public MultiResponse listUsers(PageRequest pageRequest) { + Page page = userService.findAll(pageRequest); + return MultiResponse.of(page); +} + +// 3. 非分页列表查询 +@GetMapping("/roles") +public MultiResponse listRoles() { + List roles = roleService.findAll(); + return MultiResponse.of(roles); +} + +// 4. 写操作成功(无返回数据) +@PostMapping("/users") +public Response createUser(@RequestBody CreateUserCmd cmd) { + userService.create(cmd); + return Response.buildSuccess(); +} + +// 5. 业务校验失败 +@PostMapping("/orders") +public Response createOrder(@RequestBody CreateOrderCmd cmd) { + if (cmd.getQuantity() <= 0) { + return Response.buildFailure("INVALID_QUANTITY", "订单数量必须大于0"); + } + orderService.create(cmd); + return Response.buildSuccess(); +} +``` + +正确返回的 JSON 结构: + +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { "id": 1, "name": "张三" } +} +``` + +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "total": 100, + "list": [{ "id": 1, "name": "张三" }] + } +} +``` + +### ❌ 错误示例 + +```java +// 错误1:直接返回实体对象 +@GetMapping("/users/{id}") +public UserDTO getUser(@PathVariable Long id) { + return userService.findById(id); +} + +// 错误2:返回裸 Map +@GetMapping("/users/{id}") +public Map getUser(@PathVariable Long id) { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("data", userService.findById(id)); + return result; +} + +// 错误3:自定义响应结构 +@GetMapping("/users/{id}") +public Result getUser(@PathVariable Long id) { + return new Result<>(userService.findById(id)); +} + +// 错误4:Controller 内 try-catch 自行拼装错误 +@PostMapping("/users") +public Response createUser(@RequestBody CreateUserCmd cmd) { + try { + userService.create(cmd); + return Response.buildSuccess(); + } catch (Exception e) { + // 不应在 Controller 中捕获并手动构建错误响应 + Response resp = new Response(); + resp.setSuccess(false); + resp.setErrMessage(e.getMessage()); + return resp; + } +} + +// 错误5:列表接口不使用 MultiResponse +@GetMapping("/users") +public List listUsers() { + return userService.findAll(); +} +``` + +以上错误写法会导致前端无法统一解析响应、错误处理逻辑分散、API 契约不可靠等问题。 diff --git a/docs/capabilities/alibaba/fastjson.md b/docs/capabilities/alibaba/fastjson.md new file mode 100644 index 000000000..14ff82970 --- /dev/null +++ b/docs/capabilities/alibaba/fastjson.md @@ -0,0 +1,172 @@ +--- +name: alibaba/fastjson +module: alibaba +description: Fastjson JSON 序列化库,提供高性能 JSON 解析和序列化 +status: 已实现 +scope: 后端 +source: 框架:alibaba +import: "com.alibaba:fastjson" +framework_version: 2.0.53 +--- + +## 解决什么问题 + +在企业级 Java 应用中,JSON 是最常用的数据交换格式。Fastjson 解决了以下核心痛点: + +- **高性能序列化/反序列化**:相比 JDK 原生或其他 JSON 库,Fastjson 在解析速度和内存占用上具有显著优势,适合高并发、大数据量的接口场景 +- **灵活的类型转换**:支持将 JSON 字符串直接转换为 Java Bean、Map、List 等复杂对象,也支持泛型集合的反序列化 +- **丰富的定制能力**:通过注解和过滤器机制,可以精确控制字段的序列化/反序列化行为(如字段重命名、忽略空值、日期格式化等) +- **与 Spring Boot 无缝集成**:可作为 Spring MVC 的 HttpMessageConverter,替代默认的 Jackson 进行请求/响应的自动 JSON 处理 + +在本框架中,Fastjson 2.x(2.0.53)被用作底层 JSON 工具,为统一响应封装、事件系统数据传递、脚本引擎参数绑定等模块提供高效的序列化支撑。 + +## 如何使用 + +### Maven 依赖 + +```xml + + com.alibaba.fastjson2 + fastjson2 + 2.0.53 + +``` + +### 核心 API + +| 方法 | 说明 | +|------|------| +| `JSON.toJSONString(object)` | 将 Java 对象序列化为 JSON 字符串 | +| `JSON.parseObject(json, Class)` | 将 JSON 字符串反序列化为指定类型的 Java 对象 | +| `JSON.parseArray(json, Class)` | 将 JSON 数组字符串反序列化为 List | +| `JSON.toJSONBytes(object)` | 将 Java 对象序列化为 byte[](适合网络传输/缓存存储) | +| `JSONObject.parseObject(json)` | 解析为 JSONObject(类似 Map 的动态访问) | + +### 常用特性控制 + +通过 `JSONWriter.Feature` 和 `JSONReader.Feature` 枚举控制序列化/反序列化行为: + +- `JSONWriter.Feature.WriteMapNullValue` — 序列化时保留 null 值字段 +- `JSONWriter.Feature.PrettyFormat` — 格式化输出(带缩进换行) +- `JSONWriter.Feature.WriteEnumsUsingToString` — 枚举使用 toString() 而非 ordinal +- `JSONReader.Feature.SupportSmartMatch` — 智能匹配(忽略大小写和下划线差异) +- `JSONReader.Feature.IgnoreCheckClose` — 宽松解析,容忍非标准 JSON + +### 注解定制 + +- `@JSONField(name = "...")` — 自定义 JSON 字段名 +- `@JSONField(format = "yyyy-MM-dd HH:mm:ss")` — 日期格式化 +- `@JSONField(serialize = false)` — 排除该字段不参与序列化 +- `@JSONField(deserialize = false)` — 排除该字段不参与反序列化 + +## 使用实例 + +### 基础序列化与反序列化 + +```java +import com.alibaba.fastjson2.JSON; + +// 序列化 +User user = new User(); +user.setId(1L); +user.setName("张三"); +user.setAge(28); + +String json = JSON.toJSONString(user); +// {"age":28,"id":1,"name":"张三"} + +// 反序列化 +User parsed = JSON.parseObject(json, User.class); +System.out.println(parsed.getName()); // 张三 +``` + +### 泛型集合反序列化 + +```java +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import java.util.List; + +String jsonArray = "[{\"id\":1,\"name\":\"张三\"},{\"id\":2,\"name\":\"李四\"}]"; + +// 使用 TypeReference 保留泛型信息 +List users = JSON.parseObject(jsonArray, new TypeReference>() {}); +System.out.println(users.size()); // 2 +``` + +### 带特性的序列化 + +```java +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; + +User user = new User(); +user.setId(1L); +user.setName(null); + +// 默认不输出 null 字段 +String compact = JSON.toJSONString(user); +// {"id":1} + +// 保留 null 值 + 格式化输出 +String pretty = JSON.toJSONString(user, + JSONWriter.Feature.WriteMapNullValue, + JSONWriter.Feature.PrettyFormat); +// { +// "age":null, +// "id":1, +// "name":null +// } +``` + +### 使用注解定制字段 + +```java +import com.alibaba.fastjson2.annotation.JSONField; +import java.time.LocalDateTime; + +public class Order { + + @JSONField(name = "order_id") + private Long orderId; + + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @JSONField(serialize = false) + private String internalCode; // 不参与序列化 + + // getter/setter ... +} + +Order order = new Order(); +order.setOrderId(1001L); +order.setCreateTime(LocalDateTime.now()); +order.setInternalCode("INTERNAL"); + +String json = JSON.toJSONString(order); +// {"createTime":"2026-06-08 14:30:00","order_id":1001} +// 注意:internalCode 未出现,orderId 被重命名为 order_id +``` + +### 动态 JSONObject 操作 + +```java +import com.alibaba.fastjson2.JSONObject; + +// 从字符串解析 +String json = "{\"name\":\"张三\",\"address\":{\"city\":\"杭州\",\"zip\":\"310000\"}}"; +JSONObject obj = JSONObject.parseObject(json); + +// 读取嵌套字段 +String city = obj.getJSONObject("address").getString("city"); +System.out.println(city); // 杭州 + +// 动态构建并输出 +JSONObject result = new JSONObject(); +result.put("code", 200); +result.put("message", "success"); +result.put("data", obj); + +System.out.println(result.toJSONString()); +``` diff --git a/docs/capabilities/apache/groovy.md b/docs/capabilities/apache/groovy.md new file mode 100644 index 000000000..cb666deda --- /dev/null +++ b/docs/capabilities/apache/groovy.md @@ -0,0 +1,178 @@ +--- +name: apache/groovy +module: apache +description: Groovy 动态脚本语言,运行在 JVM 上,支持动态类型和脚本化编程 +status: 已实现 +scope: 后端 +source: 框架:apache +import: "org.apache.groovy:groovy" +framework_version: 4.0.24 +--- + +## 解决什么问题 + +在企业级应用中,许多业务逻辑需要在不重启服务的前提下进行动态调整,例如: + +- **动态规则引擎**:促销折扣、风控策略、审批条件等频繁变更的业务规则,硬编码会导致频繁发版。 +- **可配置的计算逻辑**:报表字段计算、数据转换、自定义校验等逻辑需要由运营人员或实施人员灵活配置。 +- **热更新与灰度验证**:线上问题修复或新逻辑验证时,希望以脚本方式快速下发并即时生效,而非走完整的发布流程。 +- **降低扩展门槛**:让非核心开发人员(如实施顾问、数据分析师)也能通过简洁的 DSL 参与业务逻辑编写。 + +Groovy 作为运行在 JVM 上的动态语言,与 Java 完全兼容且语法更简洁。springboot-framework 的 `springboot-starter-script` 模块在此基础上封装了完整的脚本运行时,提供了编译缓存、LRU 淘汰、事务绑定、变量注入等企业级能力,使 Groovy 脚本能够安全、高效地嵌入到 Spring Boot 应用中。 + +## 如何使用 + +### 1. 引入依赖 + +在需要使用脚本能力的模块中添加 `springboot-starter-script` 依赖,该模块已包含 `org.apache.groovy:groovy:4.0.24`: + +```xml + + com.codingapi.springboot + springboot-starter-script + +``` + +### 2. 核心 API + +框架通过 `GroovyScriptRuntimeContext` 单例提供脚本运行时入口,主要方法如下: + +| 方法 | 说明 | +|------|------| +| `compile(script, cache)` | 编译脚本,`cache=true` 时使用 SHA256 作为 key 进行 LRU 缓存 | +| `run(script, returnType, transactionMode, binds)` | 执行整段脚本并返回结果 | +| `invoke(method, script, returnType, transactionMode, binds, args)` | 执行脚本中指定函数并返回结果 | +| `clearCache()` | 清空已编译的脚本缓存 | +| `cacheSize()` | 获取当前缓存中的脚本数量 | + +### 3. 高级封装:GroovyScript + +对于需要持久化管理的脚本,可使用 `GroovyScript` Builder 构建脚本对象,它集成了编译、执行、存储、元数据扫描等完整生命周期: + +```java +GroovyScript script = GroovyScript.builder("discount-rule-v1") + .script("def calculate(price, rate) { return price * rate }") + .method("calculate") + .returnType(BigDecimal.class) + .description("折扣计算脚本") + .tag("promotion") + .build(); +``` + +关键操作: + +- `script.compile(true)` — 预编译并缓存 +- `script.invoke(binds, args...)` — 调用指定函数 +- `script.run(binds)` — 执行整段脚本 +- `script.save()` — 持久化到 Repository +- `script.temp()` — 存入临时上下文(定时清理) +- `script.toMetadata()` — 提取脚本元数据信息 + +### 4. 事务模式 + +脚本执行支持三种事务模式,通过 `TransactionMode` 枚举控制: + +- `DEFAULT` — 不参与事务管理,直接执行 +- `READONLY` — 在只读事务中执行,适用于查询类脚本 +- `COMMIT` — 在可提交事务中执行,适用于涉及数据写入的脚本 + +### 5. 变量绑定 + +通过 `binds` Map 向脚本注入外部变量,脚本内可直接按变量名访问: + +```java +Map binds = new HashMap<>(); +binds.put("userService", userService); +binds.put("orderId", orderId); + +Object result = script.invoke(binds, param1, param2); +``` + +### 6. 缓存机制 + +- **编译缓存**:`GroovyScriptRuntime` 内部使用 `LinkedHashMap` 实现 LRU 缓存,最大容量通过配置项 `shellMaxCacheSize` 控制,超出时自动淘汰最久未使用的脚本。 +- **脚本对象缓存**:`GroovyScriptCacheContext` 维护脚本定义的 LRU 缓存(上限 10240),查找顺序为:内存缓存 → 临时上下文 → Repository 持久层。 + +## 使用实例 + +### 示例 1:执行简单计算脚本 + +```java +// 直接运行一段 Groovy 脚本 +String script = """ + def total = items.stream() + .mapToDouble { it.price * it.quantity } + .sum() + return BigDecimal.valueOf(total).setScale(2, RoundingMode.HALF_UP) + """; + +Map binds = Map.of("items", orderItems); +BigDecimal total = GroovyScriptRuntimeContext.getInstance() + .run(script, BigDecimal.class, TransactionMode.READONLY, binds); +``` + +### 示例 2:调用脚本中的函数 + +```java +String script = """ + def checkEligibility(user, minScore) { + return user.score >= minScore && user.status == 'ACTIVE' + } + """; + +Map binds = Map.of("minScore", 80); +Boolean eligible = GroovyScriptRuntimeContext.getInstance() + .invoke("checkEligibility", script, Boolean.class, + TransactionMode.DEFAULT, binds, currentUser); +``` + +### 示例 3:使用 GroovyScript 管理持久化脚本 + +```java +// 构建并保存脚本 +GroovyScript script = GroovyScript.builder("tax-calc-2024") + .script(""" + def calcTax(amount, region) { + def rate = region == 'CN' ? 0.13 : 0.08 + return (amount * rate).setScale(2, RoundingMode.HALF_UP) + } + """) + .method("calcTax") + .returnType(BigDecimal.class) + .description("2024年税率计算") + .tag("finance") + .build(); + +script.save(); // 持久化 +script.compile(true); // 预编译并缓存 + +// 后续使用时从缓存获取并执行 +GroovyScript cached = GroovyScriptCacheContext.getInstance() + .getGroovyScript("tax-calc-2024"); +BigDecimal tax = cached.invoke(TransactionMode.READONLY, null, + new BigDecimal("10000"), "CN"); +// tax = 1300.00 +``` + +### 示例 4:在事务中执行数据写入脚本 + +```java +String script = """ + def user = userRepository.findByUsername(username) + if (user != null) { + user.lastLoginTime = new Date() + user.loginCount += 1 + userRepository.save(user) + return true + } + return false + """; + +Map binds = Map.of( + "userRepository", userRepository, + "username", "admin" +); + +Boolean updated = GroovyScriptRuntimeContext.getInstance() + .run(script, Boolean.class, TransactionMode.COMMIT, binds); +``` diff --git a/docs/capabilities/data-authorization.md b/docs/capabilities/data-authorization.md deleted file mode 100644 index 2ba9ed11e..000000000 --- a/docs/capabilities/data-authorization.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -name: data-authorization -description: 数据权限 SQL 拦截器,通过 JDBC 代理链在 SQL 执行前透明注入权限条件,支持行级过滤与列级脱敏 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter-data-authorization -symbols: - - ConnectionProxy - - PreparedStatementProxy - - StatementProxy - - CallableStatementProxy - - ResultSetProxy - - SQLRunningContext - - SQLInterceptor - - DefaultSQLInterceptor - - SQLInterceptorContext - - SQLExecuteState - - DataPermissionSQL - - DataAuthorizationContext - - DataAuthorizationFilter - - DefaultDataAuthorizationFilter - - AuthorizationJdbcDriver - - DataPermissionSQLEnhancer - - ColumnHandlerContext - - ColumnMask - - RowHandler - - WhereConditionSQL - - JoinConditionSQL -content_hash: 213024d85431e425f67a13790847dc638a8d99a30b1013a7992236c3d1dadf8f ---- - -## 解决什么问题 - -企业系统中,不同用户只能查看权限范围内的数据(如部门经理只看本部门数据)。传统做法需要在每个查询中手动拼接权限条件,代码侵入性大。本能力通过 JDBC 代理层实现透明的 SQL 拦截与改写,解决以下问题: - -- **透明权限注入**:在 SQL 执行前自动注入 WHERE/JOIN 条件,业务代码无感知 -- **行级数据过滤**:根据用户权限动态追加行过滤条件 -- **列级数据脱敏**:通过 ColumnMask 对敏感字段(手机号、身份证、银行卡)进行脱敏 -- **跳过权限控制**:提供 `skipDataAuthorization()` 方法在特定场景下绕过权限拦截 - -## 如何使用 - -### 配置数据权限过滤器 - -```java -@Bean -public DataAuthorizationFilter dataAuthorizationFilter() { - return (tableName, aliasContext) -> { - if ("sys_user".equals(tableName)) { - return new WhereConditionSQL("dept_id", - Relation.IN, getCurrentUserDeptIds()); - } - return null; // 不过滤 - }; -} -``` - -### 配置 SQL 拦截器 - -```java -@Bean -public SQLInterceptor sqlInterceptor() { - return new DefaultSQLInterceptor(dataAuthorizationFilter); -} -``` - -### 使用 JDBC 驱动代理 - -将数据库驱动替换为 `AuthorizationJdbcDriver`,所有通过 JDBC 执行的 SQL 将自动经过权限拦截。 - -### 跳过权限拦截 - -```java -// 在特定查询中跳过数据权限 -Object result = SQLRunningContext.getInstance() - .skipDataAuthorization(() -> { - return jdbcTemplate.queryForObject("SELECT count(*) FROM sys_user", Long.class); - }); -``` - -## 使用实例 - -```java -// 业务代码正常写查询,无需关心权限 -Page users = userRepository.findAll(PageRequest.of(0, 20)); -// SQL: SELECT * FROM sys_user LIMIT 20 -// 实际执行: SELECT * FROM sys_user WHERE dept_id IN (1,2,3) LIMIT 20 - -// 列脱敏配置 -@Bean -public ColumnMask phoneMask() { - return new PhoneMask(); // 138****8888 -} -``` diff --git a/docs/capabilities/domain-proxy.md b/docs/capabilities/domain-proxy.md deleted file mode 100644 index d28883dbe..000000000 --- a/docs/capabilities/domain-proxy.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: domain-proxy -description: 基于 CGLIB 的领域实体代理,自动拦截字段变更并推送 DomainChangeEvent 领域事件 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter -symbols: - - DomainProxyFactory - - DomainChangeInterceptor - - DomainChangeEvent - - DomainCreateEvent - - DomainDeleteEvent - - DomainPersistEvent - - DomainEvent - - IDomain -content_hash: dc7a9d76224be09498f0278ec05c56ea34b0c5e2dbb80850ccb826fabe721da6 ---- - -## 解决什么问题 - -在 DDD 中,领域实体的变更需要通知相关组件(如记录变更日志、触发联动更新)。手动编写变更检测代码繁琐且易遗漏。本能力通过代理模式自动拦截实体字段变更,解决以下问题: - -- **自动变更检测**:代理实体的 setter 方法,自动对比新旧值 -- **变更事件推送**:检测到字段变更时自动推送 `DomainChangeEvent` -- **创建事件**:通过 `DomainProxyFactory.create()` 创建实体时自动推送 `DomainCreateEvent` -- **嵌套字段支持**:递归读取和对比嵌套对象的字段变更 - -## 如何使用 - -### 创建代理实体 - -```java -// 通过工厂创建代理实体,自动推送 DomainCreateEvent -User user = DomainProxyFactory.create(User.class, "张三", 25); -``` - -### 变更自动检测 - -```java -// 调用 setter 方法时,自动对比字段变更并推送 DomainChangeEvent -user.setName("李四"); // 自动推送 DomainChangeEvent(user, "name", "张三", "李四") -user.setAge(30); // 自动推送 DomainChangeEvent(user, "age", 25, 30) -``` - -### 订阅变更事件 - -```java -@Service -public class ChangeLogHandler implements IHandler { - @Override - public void handler(DomainChangeEvent event) { - log.info("字段变更: {} {} -> {}", - event.getFieldName(), event.getOldValue(), event.getNewValue()); - } -} -``` - -## 使用实例 - -```java -// 创建代理实体 -Order order = DomainProxyFactory.create(Order.class, orderId, amount); - -// 修改字段 - 自动触发变更事件 -order.setStatus("PAID"); -// → DomainChangeEvent(order, "status", "PENDING", "PAID") - -// 订阅处理 -@Service -public class OrderChangeHandler implements IHandler { - @Override - public void handler(DomainChangeEvent event) { - if ("status".equals(event.getFieldName())) { - auditLog.record(event); - } - } -} -``` diff --git a/docs/capabilities/dynamic-data-query.md b/docs/capabilities/dynamic-data-query.md deleted file mode 100644 index c6ee76d60..000000000 --- a/docs/capabilities/dynamic-data-query.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -name: dynamic-data-query -description: 动态数据查询体系,基于 FastRepository + PageRequest/Filter/SearchRequest 自动构建 Example 或 HQL 查询 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter-data-fast -symbols: - - FastRepository - - DynamicRepository - - DynamicNativeRepository - - DynamicSQLBuilder - - ExampleBuilder - - BaseRepository - - SortRepository - - PageRequest - - SearchRequest - - RequestFilter - - Filter - - Relation - - QueryColumns - - MapViewResult - - JpaQuery - - JdbcQuery - - ICurrentOffset - - CurrentPageOffsetContext - - DynamicTableGenerator - - DynamicTableClassLoader - - TableEntityClassBuilder -content_hash: 205a681871886afbc38ec401a4a24474e11d3ef833dbb9c66d910950404fba22 ---- - -## 解决什么问题 - -后台管理系统的列表查询通常需要支持动态过滤、排序和分页。传统做法需要大量 if-else 拼接查询条件。本能力通过扩展 Spring Data JPA,提供了声明式的动态查询方案: - -- **动态过滤条件**:通过 `RequestFilter` 声明式添加过滤条件,自动构建查询 -- **前端参数直连**:`SearchRequest` 直接解析 HttpServletRequest 参数为 PageRequest -- **多种查询模式**:支持 Example 查询(简单过滤)和 HQL 查询(复杂关联) -- **Map 结果映射**:支持自定义列选择和 Map 结果返回 - -## 如何使用 - -### 基础分页查询 - -```java -// Repository 继承 FastRepository -public interface UserRepository extends FastRepository {} - -// 分页 + 过滤 -PageRequest request = PageRequest.of(0, 20); -request.addFilter("name", "张三"); -request.addFilter("age", Relation.GT, 18); -request.addFilter("status", Relation.IN, "ACTIVE", "PENDING"); -Page page = userRepository.findAll(request); -``` - -### 复杂 HQL 查询 - -```java -// 使用 pageRequest 触发 HQL 构建(支持关联查询过滤) -PageRequest request = PageRequest.of(0, 20); -request.addFilter("dept.name", "技术部"); // 关联字段 -request.addSort(Sort.by("createTime").descending()); -Page page = userRepository.pageRequest(request); -``` - -### 从 HTTP 请求自动构建 - -```java -// 自动解析前端传递的 filter/sort/params 参数 -SearchRequest searchRequest = new SearchRequest(); -searchRequest.setCurrent(0); -searchRequest.setPageSize(20); -PageRequest pageRequest = searchRequest.toPageRequest(User.class); -Page page = userRepository.searchRequest(searchRequest); -``` - -### 支持的 Relation 操作 - -| Relation | 含义 | 示例 | -|----------|------|------| -| `EQUAL` | 等于 | `addFilter("name", "张三")` | -| `NOT_EQUAL` | 不等于 | `addFilter("status", Relation.NOT_EQUAL, "DELETED")` | -| `GT` / `GTE` | 大于/大于等于 | `addFilter("age", Relation.GT, 18)` | -| `LT` / `LTE` | 小于/小于等于 | `addFilter("price", Relation.LTE, 100)` | -| `LIKE` | 模糊匹配 | `addFilter("name", Relation.LIKE, "张")` | -| `IN` | 包含 | `addFilter("status", Relation.IN, "A", "B")` | - -## 使用实例 - -```java -// Controller 层 -@GetMapping("/users") -public MultiResponse list() { - SearchRequest searchRequest = new SearchRequest(); - searchRequest.setCurrent(0); - searchRequest.setPageSize(20); - Page page = userRepository.searchRequest(searchRequest); - return MultiResponse.of(page); -} - -// 前端请求参数: -// ?current=0&pageSize=20&filter=eyJuYW1lIjpbIuW8oCJdfQ==&sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9 -// filter 和 sort 参数为 Base64 编码的 JSON -``` diff --git a/docs/capabilities/esotericsoftware/kryo.md b/docs/capabilities/esotericsoftware/kryo.md new file mode 100644 index 000000000..b417d4fd1 --- /dev/null +++ b/docs/capabilities/esotericsoftware/kryo.md @@ -0,0 +1,167 @@ +--- +name: esotericsoftware/kryo +module: esotericsoftware +description: Kryo 高性能序列化框架,提供快速的 Java 对象序列化 +status: 已实现 +scope: 后端 +source: 框架:esotericsoftware +import: "com.esotericsoftware:kryo" +framework_version: 5.6.2 +--- + +## 解决什么问题 + +Kryo 是一个快速、高效的 Java 对象序列化框架,主要解决以下问题: + +1. **Java 原生序列化性能低下**:Java 内置的 `Serializable` 机制序列化后的字节流体积大、速度慢,不适合高性能场景。 +2. **存储与传输效率**:在需要将复杂对象持久化到数据库或通过网络传输时,Kryo 能显著减少数据体积并提升处理速度。 +3. **工作流快照存储**:在本框架的工作流引擎(`springboot-starter-flow`)中,流程定义数据需要以紧凑的二进制格式存储为版本快照,Kryo 提供了比 JSON/XML 更高效的序列化方案。 + +适用场景包括:缓存序列化、RPC 通信、对象深拷贝、数据库 BLOB 字段存储、分布式会话共享等对序列化性能敏感的场景。 + +## 如何使用 + +### Maven 依赖 + +```xml + + com.esotericsoftware + kryo + 5.6.2 + +``` + +### 核心 API + +| 类/接口 | 说明 | +|---------|------| +| `Kryo` | 序列化引擎核心类,负责注册类和执行序列化/反序列化操作 | +| `Output` | 输出流包装器,将序列化数据写入底层 OutputStream | +| `Input` | 输入流包装器,从底层 InputStream 读取序列化数据 | +| `Serializer` | 自定义序列化器接口,用于控制特定类型的序列化行为 | + +### 基本用法 + +1. 创建 `Kryo` 实例 +2. 注册需要序列化的类(可选但推荐,可减小输出体积) +3. 使用 `writeObject()` / `readObject()` 进行序列化/反序列化 + +> ⚠️ **注意**:`Kryo` 实例不是线程安全的,在多线程环境中应使用 `ThreadLocal` 或对象池进行管理。 + +## 使用实例 + +### 基础序列化与反序列化 + +```java +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +import java.io.ByteArrayOutputStream; + +// 定义实体类 +public class User implements Serializable { + private String name; + private int age; + + // getter/setter 省略 +} + +// 序列化 +Kryo kryo = new Kryo(); +kryo.register(User.class); + +ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); +Output output = new Output(outputStream); + +User user = new User("张三", 28); +kryo.writeObject(output, user); +output.close(); + +byte[] bytes = outputStream.toByteArray(); + +// 反序列化 +Input input = new Input(bytes); +User restored = kryo.readObject(input, User.class); +input.close(); + +System.out.println(restored.getName()); // 张三 +``` + +### 框架中的实际应用:工作流快照序列化 + +以下是 `springboot-starter-flow` 模块中使用 Kryo 对流程定义进行序列化的实际代码: + +```java +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +public class FlowWorkSerializable implements Serializable { + + private long id; + private String code; + private String title; + private List nodes; + private List relations; + // ... 其他字段 + + /** + * 将流程定义序列化为字节数组,用于持久化存储 + */ + public byte[] toSerializable() { + Kryo kryo = new Kryo(); + // 注册所有涉及的类型,减小序列化体积 + kryo.register(ArrayList.class); + kryo.register(FlowNodeSerializable.class); + kryo.register(FlowRelationSerializable.class); + kryo.register(FlowWorkSerializable.class); + kryo.register(ApprovalType.class); + kryo.register(NodeType.class); + kryo.register(FlowButton.class); + kryo.register(FlowButtonType.class); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Output output = new Output(outputStream); + kryo.writeObject(output, this); + output.close(); + return outputStream.toByteArray(); + } + + /** + * 从字节数组反序列化恢复流程定义 + */ + public static FlowWorkSerializable fromSerializable(byte[] bytes) { + Kryo kryo = new Kryo(); + kryo.register(ArrayList.class); + kryo.register(FlowNodeSerializable.class); + kryo.register(FlowRelationSerializable.class); + kryo.register(FlowWorkSerializable.class); + kryo.register(ApprovalType.class); + kryo.register(NodeType.class); + kryo.register(FlowButton.class); + kryo.register(FlowButtonType.class); + + return kryo.readObject(new Input(bytes), FlowWorkSerializable.class); + } +} +``` + +### 线程安全的使用方式 + +```java +// 使用 ThreadLocal 保证线程安全 +private static final ThreadLocal kryoThreadLocal = ThreadLocal.withInitial(() -> { + Kryo kryo = new Kryo(); + kryo.register(User.class); + kryo.register(ArrayList.class); + kryo.setReferences(true); // 支持循环引用 + kryo.setRegistrationRequired(false); // 允许未注册的类(开发阶段方便调试) + return kryo; +}); + +// 使用时获取线程本地实例 +Kryo kryo = kryoThreadLocal.get(); +Output output = new Output(new ByteArrayOutputStream()); +kryo.writeObject(output, user); +``` diff --git a/docs/capabilities/event-system.md b/docs/capabilities/event-system.md deleted file mode 100644 index 9a533528a..000000000 --- a/docs/capabilities/event-system.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: event-system -description: 发布-订阅事件系统,支持同步/异步事件、Handler排序、循环检测与事务集成 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter -symbols: - - EventPusher - - IEvent - - ISyncEvent - - IAsyncEvent - - IHandler - - ApplicationHandlerUtils - - DomainEventContext - - EventStackContext - - EventTraceContext - - DomainEvent - - SpringEventHandler - - SpringDefaultEventHandler - - SpringTransactionEventHandler - - HandlerBeanDefinitionRegistrar -content_hash: 0703329f337a48ff76c084efb93b0c0fcb3ef426ba4b201f81e0c28b646c7405 ---- - -## 解决什么问题 - -在 DDD 架构中,领域事件是解耦聚合根之间通信的核心机制。本事件系统解决了以下问题: - -- **领域事件解耦**:聚合根通过事件通信,无需直接依赖 -- **同步/异步分离**:通过接口标记区分事件类型,由框架自动调度 -- **循环事件检测**:自动检测事件嵌套推送中的循环引用,防止无限递归 -- **Handler 排序**:多个 Handler 订阅同一事件时,可通过 `order()` 控制执行顺序 -- **异常隔离**:每个 Handler 的异常通过独立回调处理,不影响其他 Handler - -## 如何使用 - -### 核心接口 - -- `IEvent` — 事件标记接口(extends Serializable) -- `ISyncEvent extends IEvent` — 同步事件标记 -- `IAsyncEvent extends IEvent` — 异步事件标记 -- `IHandler` — 事件处理器,泛型绑定事件类型 - -### 推送事件 - -```java -// 推送事件(默认同步,自动检测循环) -EventPusher.push(new MyEvent()); - -// 允许循环事件 -EventPusher.push(new MyEvent(), true); -``` - -### 订阅事件 - -```java -@Service -public class MyHandler implements IHandler { - - @Override - public int order() { - return 0; // 排序,默认0 - } - - @Override - public void handler(MyEvent event) { - // 处理事件 - } - - @Override - public void error(Exception exception) throws Exception { - // 异常回调 - throw exception; - } -} -``` - -### 注册方式 - -Handler 只需声明为 Spring Bean(`@Service` / `@Component`),框架通过 `HandlerBeanDefinitionRegistrar` 自动扫描所有 `IHandler` 实现并注册到 `ApplicationHandlerUtils`。 - -## 使用实例 - -```java -// 定义事件 -public class UserCreatedEvent implements ISyncEvent { - private final Long userId; - public UserCreatedEvent(Long userId) { this.userId = userId; } - public Long getUserId() { return userId; } -} - -// 推送 -EventPusher.push(new UserCreatedEvent(user.getId())); - -// 订阅 -@Service -public class NotifyHandler implements IHandler { - @Override - public void handler(UserCreatedEvent event) { - // 发送通知 - } -} -``` diff --git a/docs/capabilities/global-exception-handler.md b/docs/capabilities/global-exception-handler.md deleted file mode 100644 index 3b5705d5f..000000000 --- a/docs/capabilities/global-exception-handler.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -name: global-exception-handler -description: 全局异常处理器,统一捕获 Controller 层异常并转换为标准 Response 格式返回 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter -symbols: - - BasicHandlerExceptionResolverConfiguration - - ServletExceptionHandler - - LocaleMessageException -content_hash: e3029dc411b34af5ea488d45c2ac414e2ea7578b571e681e0d7dc938ae75f052 ---- - -## 解决什么问题 - -Controller 层的未捕获异常需要统一处理,避免将堆栈信息暴露给前端。本能力通过 Spring `HandlerExceptionResolver` 机制实现全局异常拦截: - -- **统一错误格式**:所有异常都转换为 `{success: false, errCode, errMessage}` 格式 -- **国际化异常支持**:`LocaleMessageException` 携带错误码,支持国际化消息 -- **兜底处理**:未识别的异常统一返回 `system.err` 错误码 -- **日志记录**:异常信息自动记录到日志 - -## 如何使用 - -### 抛出业务异常 - -```java -// 使用 LocaleMessageException 抛出带错误码的异常 -throw new LocaleMessageException("user.not.found", "用户不存在"); -throw new LocaleMessageException("order.invalid", "订单状态无效"); -``` - -### 自动处理 - -框架自动注册 `ServletExceptionHandler`,所有 Controller 层的异常都会被拦截并转换为标准响应: - -- `LocaleMessageException` → `{success: false, errCode: "user.not.found", errMessage: "用户不存在"}` -- 其他 `Exception` → `{success: false, errCode: "system.err", errMessage: "异常消息"}` - -### 自动配置 - -无需手动配置,`BasicHandlerExceptionResolverConfiguration` 在 Spring Web 环境下自动生效(`@ConditionalOnClass`)。 - -## 使用实例 - -```java -@RestController -public class UserController { - - @GetMapping("/users/{id}") - public SingleResponse get(@PathVariable Long id) { - User user = userService.findById(id); - if (user == null) { - // 抛出国际化异常,自动转换为错误响应 - throw new LocaleMessageException("user.not.found", "用户不存在"); - } - return SingleResponse.of(user); - } -} - -// 前端收到的响应: -// {"success": false, "errCode": "user.not.found", "errMessage": "用户不存在"} -``` diff --git a/docs/capabilities/groovy-runtime.md b/docs/capabilities/groovy-runtime.md deleted file mode 100644 index dbdf9ddb6..000000000 --- a/docs/capabilities/groovy-runtime.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: groovy-runtime -description: Apache Groovy 运行时 — 动态脚本编译、执行与类型系统桥接 -status: 已实现 -scope: 后端 -source: 框架:Groovy -import: org.apache.groovy:groovy -framework_version: 4.0.24 ---- - -## 解决什么问题 - -Groovy 作为 JVM 上的动态语言,在本框架中被广泛使用: - -- **工作流表达式**:FlowNode 的 `TitleGenerator`、`OperatorMatcher`、`OutTrigger` 使用 Groovy 脚本 -- **动态脚本引擎**:`springboot-starter-script` 提供完整的 Groovy 脚本运行时 -- **类型桥接**:Groovy 与 Java 之间的自动类型转换 -- **JSON/XML 处理**:通过 `groovy-json` 和 `groovy-xml` 模块处理结构化数据 - -## 如何使用 - -### Groovy 脚本执行 - -```groovy -// Groovy 脚本可以直接使用 Java 类型 -def greeting(String name) { - return "Hello, ${name}!" -} -``` - -### GroovyShell 使用 - -```java -GroovyShell shell = new GroovyShell(); -Script script = shell.parse("return a + b"); -Binding binding = new Binding(); -binding.setVariable("a", 10); -binding.setVariable("b", 20); -script.setBinding(binding); -Object result = script.run(); // 30 -``` - -### 本框架中的使用场景 - -- **工作流引擎**:节点标题生成、审批人匹配、条件分支触发器 -- **脚本引擎**:动态业务规则、表单校验、报表查询 -- **数据查询**:FastRepository 中的动态 HQL 构建辅助 - -## 使用实例 - -```java -// 工作流中使用 Groovy 匹配审批人 -OperatorMatcher matcher = new OperatorMatcher( - "def matcher(session) { return [session.createOperatorId] }" -); -List operatorIds = matcher.matcher(flowSession); -``` diff --git a/docs/capabilities/groovy-script-engine.md b/docs/capabilities/groovy-script-engine.md deleted file mode 100644 index e7dd9459a..000000000 --- a/docs/capabilities/groovy-script-engine.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: groovy-script-engine -description: Groovy 脚本运行时引擎,支持动态编译、LRU 缓存、类型映射、元数据扫描与临时脚本持久化 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter-script -symbols: - - GroovyScript - - GroovyScriptRuntimeContext - - GroovyScriptRuntime - - GroovyScriptEngineRunner - - GroovyScriptCacheContext - - GroovyMetadataScannerUtils - - GroovyMetadata - - GroovyType - - GroovyField - - GroovyFunction - - ScriptTypeMappingContext - - ScriptTypeMapping - - GroovyTypeFixStrategyContext - - GroovyTypeFixStrategy - - GroovyMetadataGenerateStrategyContext - - GroovyScriptController - - GroovyScriptRepository - - TempGroovyScriptContext - - TransactionMode -content_hash: 913fcdb49caf0b894a3d6cbff60bb519566caf888e292724c652766cc4fa9c90 ---- - -## 解决什么问题 - -在企业应用中,某些业务规则需要动态调整而不想重新部署(如工作流条件表达式、表单校验规则、动态报表查询)。本 Groovy 脚本引擎解决了以下问题: - -- **运行时编译执行**:Groovy 脚本在运行时编译为 Java 字节码并执行 -- **LRU 编译缓存**:编译后的 Class 缓存在 LRU Cache 中,避免重复编译 -- **类型映射**:解决 Groovy 与 Java 之间的类型差异(如 `int` → `Integer`) -- **元数据扫描**:通过注解扫描脚本的字段、函数、参数信息 -- **临时脚本持久化**:临时脚本在应用重启时自动持久化到数据库并恢复 - -## 如何使用 - -### 构建与执行脚本 - -```java -GroovyScript script = GroovyScript.builder("calc-discount") - .script("def calc(BigDecimal price, int level) { return price * (1 - level * 0.1) }") - .description("计算折扣价") - .returnType(BigDecimal.class) - .build(); - -// 执行脚本 -BigDecimal result = script.invoke(TransactionMode.none, - Map.of(), new BigDecimal("100"), 2); -// result = 80.0 -``` - -### 带事务执行 - -```java -// 在 Spring 事务中执行脚本 -Object result = script.invoke(TransactionMode.required, bindMap, arg1, arg2); -``` - -### REST API 管理脚本 - -引擎提供 `GroovyScriptController`,通过 HTTP 接口管理脚本的编译、保存和执行。 - -### 类型映射扩展 - -```java -// 注册自定义类型映射 -ScriptTypeMappingContext.getInstance().addMapping(new ScriptTypeMapping() { - public boolean support(Class target) { return target == PageRequest.class; } - public Class mapping(Class target) { return CustomPageRequest.class; } -}); -``` - -## 使用实例 - -```java -// 工作流中使用 Groovy 脚本匹配审批人 -GroovyScript script = GroovyScript.builder("match-operator") - .script(""" - def matcher(session) { - if (session.amount > 10000) return [1001L] // 总经理审批 - return [session.createOperatorId] // 直属上级 - } - """) - .build(); - -List operatorIds = script.invoke(TransactionMode.none, Map.of(), session); -``` diff --git a/docs/capabilities/index.md b/docs/capabilities/index.md index 1a0732dcf..0ee1ed796 100644 --- a/docs/capabilities/index.md +++ b/docs/capabilities/index.md @@ -4,26 +4,31 @@ ## ✅ 已实现 -| 名称 | 描述 | 范围 | 来源 | -|------|------|------|------| -| [data-authorization](./data-authorization.md) | 数据权限 SQL 拦截器,通过 JDBC 代理链在 SQL 执行前透明注入权限条件,支持行级过滤与列级脱敏 | 后端 | 项目自有 | -| [domain-proxy](./domain-proxy.md) | 基于 CGLIB 的领域实体代理,自动拦截字段变更并推送 DomainChangeEvent 领域事件 | 后端 | 项目自有 | -| [dynamic-data-query](./dynamic-data-query.md) | 动态数据查询体系,基于 FastRepository + PageRequest/Filter/SearchRequest 自动构建 Example 或 ... | 后端 | 项目自有 | -| [event-system](./event-system.md) | 发布-订阅事件系统,支持同步/异步事件、Handler排序、循环检测与事务集成 | 后端 | 项目自有 | -| [global-exception-handler](./global-exception-handler.md) | 全局异常处理器,统一捕获 Controller 层异常并转换为标准 Response 格式返回 | 后端 | 项目自有 | -| [groovy-runtime](./groovy-runtime.md) | Apache Groovy 运行时 — 动态脚本编译、执行与类型系统桥接 | 后端 | 框架:Groovy | -| [groovy-script-engine](./groovy-script-engine.md) | Groovy 脚本运行时引擎,支持动态编译、LRU 缓存、类型映射、元数据扫描与临时脚本持久化 | 后端 | 项目自有 | -| [jjwt](./jjwt.md) | JJWT 库 — JWT Token 的创建、签名、解析与验证 | 后端 | 框架:JJWT | -| [jsqlparser](./jsqlparser.md) | JSqlParser SQL 解析库 — 解析和改写 SQL 语句,用于数据权限条件注入 | 后端 | 框架:JSqlParser | -| [jwt-auth-gateway](./jwt-auth-gateway.md) | JWT 认证网关,集成 Spring Security 与 JJWT,支持 Token 创建、解析、Redis 有状态/无状态双模式 | 后端 | 项目自有 | -| [kryo](./kryo.md) | Kryo 高性能序列化库 — 用于工作流数据快照的深拷贝与持久化 | 后端 | 框架:Kryo | -| [rest-client](./rest-client.md) | REST HTTP 客户端封装,支持自动重试、代理配置、请求/响应拦截器与信任所有证书模式 | 后端 | 项目自有 | -| [spring-data-jpa](./spring-data-jpa.md) | Spring Data JPA — ORM 映射、Repository 抽象、分页查询、Specification 动态查询 | 后端 | 框架:Spring Data JPA | -| [spring-framework](./spring-framework.md) | Spring Framework / Spring Boot 核心能力 — IoC 容器、自动配置、事件发布、AOP、Web MVC | 后端 | 框架:Spring Boot | -| [spring-security](./spring-security.md) | Spring Security 认证与授权框架 — 提供 Filter 链、CSRF 防护、密码编码、权限控制 | 后端 | 框架:Spring Security | -| [unified-response](./unified-response.md) | 统一响应封装体系(Response / SingleResponse / MultiResponse / MapResponse),标准化 API 返回格式 | 后端 | 项目自有 | -| [workflow-engine](./workflow-engine.md) | 轻量级工作流引擎,支持流程定义、节点流转、审批、退回、委托、会签、抄送、数据快照与事件通知 | 后端 | 项目自有 | +| 名称 | 模块 | 描述 | 范围 | 来源 | +|------|------|------|------|------| +| [alibaba/fastjson](./alibaba/fastjson.md) | alibaba | Fastjson JSON 序列化库,提供高性能 JSON 解析和序列化 | 后端 | 框架:alibaba | +| [apache/groovy](./apache/groovy.md) | apache | Groovy 动态脚本语言,运行在 JVM 上,支持动态类型和脚本化编程 | 后端 | 框架:apache | +| [esotericsoftware/kryo](./esotericsoftware/kryo.md) | esotericsoftware | Kryo 高性能序列化框架,提供快速的 Java 对象序列化 | 后端 | 框架:esotericsoftware | +| [jsonwebtoken/jjwt](./jsonwebtoken/jjwt.md) | jsonwebtoken | JJWT 库,提供 JWT 令牌生成、验证和解析 | 后端 | 框架:jsonwebtoken | +| [jsqlparser/jsqlparser](./jsqlparser/jsqlparser.md) | jsqlparser | JSqlParser SQL 解析器,支持 SQL 语句解析和改写 | 后端 | 框架:jsqlparser | +| [react/react](./react/react.md) | react | React UI 框架配合 Ant Design 组件库,用于构建前端用户界面 | 前端 | 框架:react | +| [redux/toolkit](./redux/toolkit.md) | redux | Redux Toolkit 状态管理库,提供简化的 Redux 状态管理模式 | 前端 | 框架:redux | +| [springboot/data-jpa](./springboot/data-jpa.md) | springboot | Spring Data JPA,提供 ORM 映射、Repository 抽象和分页支持 | 后端 | 框架:springboot | +| [springboot/ioc-container](./springboot/ioc-container.md) | springboot | Spring IoC 容器,提供依赖注入、AOP、事件发布等核心功能 | 后端 | 框架:springboot | +| [springboot/security](./springboot/security.md) | springboot | Spring Security,提供认证、授权和 CSRF 防护 | 后端 | 框架:springboot | +| [springboot-starter/domain-proxy](./springboot-starter/domain-proxy.md) | springboot-starter | 领域实体变更代理,通过 CGLIB 代理拦截实体字段变更并自动推送领域事件 | 后端 | 项目自有 | +| [springboot-starter/event-system](./springboot-starter/event-system.md) | springboot-starter | 事件发布-订阅系统,支持同步/异步事件、事件循环检测、事务事件 | 后端 | 项目自有 | +| [springboot-starter/exception-handling](./springboot-starter/exception-handling.md) | springboot-starter | 全局异常处理,统一拦截 Controller 层异常并返回标准 Response | 后端 | 项目自有 | +| [springboot-starter/locale-message](./springboot-starter/locale-message.md) | springboot-starter | 国际化异常消息,支持多语言异常信息和本地化消息解析 | 后端 | 项目自有 | +| [springboot-starter/page-request](./springboot-starter/page-request.md) | springboot-starter | 动态分页查询请求,扩展 Spring PageRequest 支持 RequestFilter 动态过滤条件 | 后端 | 项目自有 | +| [springboot-starter/response-dto](./springboot-starter/response-dto.md) | springboot-starter | 统一响应封装,提供 Response/SingleResponse/MultiResponse/MapResponse 四种响应类型 | 后端 | 项目自有 | +| [springboot-starter/rest-client](./springboot-starter/rest-client.md) | springboot-starter | REST 客户端封装,提供 RestTemplate 上下文管理和 HTTPS 信任所有证书支持 | 后端 | 项目自有 | +| [springboot-starter-data-authorization/sql-interception](./springboot-starter-data-authorization/sql-interception.md) | springboot-starter-data-authorization | SQL 拦截数据权限,通过 JDBC 代理透明注入权限条件实现行级数据过滤 | 后端 | 项目自有 | +| [springboot-starter-data-fast/fast-repository](./springboot-starter-data-fast/fast-repository.md) | springboot-starter-data-fast | JPA 增强 Repository,支持 PageRequest 动态过滤查询和 HQL 构建 | 后端 | 项目自有 | +| [springboot-starter-flow/workflow-engine](./springboot-starter-flow/workflow-engine.md) | springboot-starter-flow | 工作流引擎,支持流程定义、节点流转、审批、委托、会签和数据快照 | 后端 | 项目自有 | +| [springboot-starter-script/groovy-runtime](./springboot-starter-script/groovy-runtime.md) | springboot-starter-script | Groovy 脚本运行时引擎,支持运行时编译、LRU 缓存、热更新和 REST API | 后端 | 项目自有 | +| [springboot-starter-security/auth-gateway](./springboot-starter-security/auth-gateway.md) | springboot-starter-security | 安全认证网关,支持 JWT 无状态认证和 Redis 有状态认证两种模式 | 后端 | 项目自有 | --- -**统计**: 共 17 篇 — 已实现 17 / 计划中 0 / 已废弃 0 +**统计**: 共 22 篇 — 已实现 22 / 计划中 0 / 已废弃 0 diff --git a/docs/capabilities/jjwt.md b/docs/capabilities/jjwt.md deleted file mode 100644 index 74cb4f2ff..000000000 --- a/docs/capabilities/jjwt.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: jjwt -description: JJWT 库 — JWT Token 的创建、签名、解析与验证 -status: 已实现 -scope: 后端 -source: 框架:JJWT -import: io.jsonwebtoken:jjwt-api -framework_version: 0.12.6 ---- - -## 解决什么问题 - -JJWT(Java JWT)是 Java 生态最主流的 JWT 库,本框架在 `springboot-starter-security` 中使用 JJWT 实现 Token 认证: - -- **Token 创建**:`Jwts.builder()` 构建带签名的 JWT -- **Token 解析**:`Jwts.parser()` 验证签名并提取 Claims -- **HMAC 签名**:使用 `Keys.hmacShaKeyFor()` 生成对称密钥 -- **Subject 载荷**:将用户信息序列化到 Token 的 subject 字段 - -## 如何使用 - -### 创建 Token - -```java -SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); -String jwt = Jwts.builder() - .subject(tokenPayload) - .signWith(key) - .compact(); -``` - -### 解析 Token - -```java -Jws jws = Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(jwtString); -String subject = jws.getPayload().getSubject(); -``` - -### 本框架封装 - -```java -// 通过 JwtTokenGateway 封装 -JwtTokenGateway gateway = new JwtTokenGateway(securityJWTProperties); -Token token = gateway.create(username, authorities); -Token parsed = gateway.parser(jwtString); -``` - -## 使用实例 - -```java -// 手动使用 JJWT -SecretKey key = Keys.hmacShaKeyFor("my-secret-key-at-least-32-chars".getBytes()); - -// 创建 -String jwt = Jwts.builder() - .subject("{\"userId\":1,\"username\":\"admin\"}") - .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + 7200000)) - .signWith(key) - .compact(); - -// 验证 -Claims claims = Jwts.parser().verifyWith(key).build() - .parseSignedClaims(jwt).getPayload(); -String payload = claims.getSubject(); -``` diff --git a/docs/capabilities/jsonwebtoken/jjwt.md b/docs/capabilities/jsonwebtoken/jjwt.md new file mode 100644 index 000000000..78f63f6fb --- /dev/null +++ b/docs/capabilities/jsonwebtoken/jjwt.md @@ -0,0 +1,188 @@ +--- +name: jsonwebtoken/jjwt +module: jsonwebtoken +description: JJWT 库,提供 JWT 令牌生成、验证和解析 +status: 已实现 +scope: 后端 +source: 框架:jsonwebtoken +import: "io.jsonwebtoken:jjwt-api" +framework_version: 0.12.6 +--- + +## 解决什么问题 + +在微服务与前后端分离架构中,服务端需要一种无状态的认证机制来识别用户身份。传统 Session 方案依赖服务端存储,难以水平扩展且不适合移动端场景。 + +JJWT(Java JSON Web Token)提供了标准的 JWT 令牌生成、签名与解析能力,解决以下核心问题: + +- **无状态认证**:将用户信息(用户名、权限、额外数据)编码进自包含的 JWT 令牌中,服务端无需维护会话存储即可验证请求身份。 +- **防篡改签名**:通过 HMAC-SHA 算法对令牌进行签名,确保令牌内容在传输过程中不被伪造或修改。 +- **令牌生命周期管理**:支持配置有效期与续期提醒时间,配合框架的 `Token.canRestToken()` 机制实现自动续期,避免用户在活跃操作中频繁重新登录。 +- **统一令牌抽象**:框架通过 `TokenGateway` 接口封装了 JWT 与 Redis 两种令牌策略,业务层只需面向接口编程,切换认证模式无需改动业务代码。 + +## 如何使用 + +### 依赖引入 + +JJWT 作为 `springboot-starter-security` 模块的传递依赖自动引入,无需单独声明: + +```xml + + com.codingapi.springboot + springboot-starter-security + +``` + +### 启用 JWT 认证 + +在 `application.properties` 中开启 JWT 并配置参数: + +```properties +# 启用 JWT 认证(默认 true) +codingapi.security.jwt.enable=true + +# HMAC 签名密钥(需大于 32 位字符串) +codingapi.security.jwt.secret-key=your-secret-key-at-least-32-characters + +# 令牌有效期(毫秒),默认 15 分钟 +codingapi.security.jwt.valid-time=900000 + +# 令牌续期提醒阈值(毫秒),默认 10 分钟后触发续期 +codingapi.security.jwt.rest-time=600000 +``` + +### 核心 API + +框架通过 `TokenGateway` 接口对外暴露令牌操作,JWT 模式下由 `JWTTokenGatewayImpl` → `JwtTokenGateway` 实现: + +| 方法 | 说明 | +|------|------| +| `create(username, authorities)` | 创建基础令牌,仅包含用户名和权限列表 | +| `create(username, authorities, extra)` | 创建带额外数据的令牌,extra 以 JSON 字符串存入 subject | +| `create(username, iv, authorities)` | 创建带加密向量(iv)的令牌,用于加解密场景 | +| `create(username, iv, authorities, extra)` | 完整创建方法,同时携带 iv 和额外数据 | +| `parser(sign)` | 解析并验证 JWT 签名,返回 `Token` 对象;签名无效或格式错误时抛出 `LocaleMessageException` | + +`Token` 对象提供以下辅助方法: + +| 方法 | 说明 | +|------|------| +| `verify()` | 校验令牌是否过期,过期则抛出 `TokenExpiredException` | +| `isExpire()` | 判断令牌是否已过期 | +| `canRestToken()` | 判断是否需要续期(未过期但已超过 restTime 阈值) | +| `parseExtra(Class)` | 将 extra 字段反序列化为指定类型 | +| `decodeIv()` | AES 解密 iv 字段 | +| `getAuthenticationToken()` | 转换为 Spring Security 的 `UsernamePasswordAuthenticationToken` | + +### 自动装配 + +当 `codingapi.security.jwt.enable=true` 时,`JWTSecurityConfiguration` 自动注册以下 Bean: + +- `SecurityJWTProperties` — JWT 配置属性 +- `JwtTokenGateway` — JWT 令牌生成与解析的核心组件 +- `TokenGateway`(`JWTTokenGatewayImpl`)— 统一的令牌网关接口实现 + +若项目中已存在自定义 `TokenGateway` Bean,自动装配不会覆盖(`@ConditionalOnMissingBean`)。 + +## 使用实例 + +### 注入 TokenGateway 创建与解析令牌 + +```java +@Service +public class AuthService { + + private final TokenGateway tokenGateway; + + public AuthService(TokenGateway tokenGateway) { + this.tokenGateway = tokenGateway; + } + + /** + * 用户登录后签发 JWT 令牌 + */ + public Token login(String username, List authorities) { + // 基础令牌 + return tokenGateway.create(username, authorities); + } + + /** + * 签发携带额外业务数据的令牌 + */ + public Token loginWithExtra(String username, List authorities, String tenantId) { + String extra = "{\"tenantId\":\"" + tenantId + "\"}"; + return tokenGateway.create(username, authorities, extra); + } + + /** + * 从请求头解析并验证令牌 + */ + public Token validateToken(String jwtSign) { + // 验签 + 解析,失败抛出 LocaleMessageException + Token token = tokenGateway.parser(jwtSign); + // 检查是否过期 + token.verify(); + return token; + } +} +``` + +### 在过滤器中使用令牌 + +```java +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + private final TokenGateway tokenGateway; + + public JwtAuthFilter(TokenGateway tokenGateway) { + this.tokenGateway = tokenGateway; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String sign = authHeader.substring(7); + try { + Token token = tokenGateway.parser(sign); + token.verify(); + + // 检查是否需要续期 + if (token.canRestToken()) { + // 重新签发令牌并通过响应头返回 + Token newToken = tokenGateway.create( + token.getUsername(), token.getAuthorities(), token.getExtra()); + response.setHeader("X-New-Token", newToken.getToken()); + } + + // 设置 Spring Security 认证上下文 + SecurityContextHolder.getContext() + .setAuthentication(token.getAuthenticationToken()); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + chain.doFilter(request, response); + } +} +``` + +### 解析令牌中的额外数据 + +```java +// 定义业务数据结构 +public class UserContext { + private String tenantId; + private String department; + // getter/setter +} + +// 从令牌中提取 +Token token = tokenGateway.parser(jwtSign); +UserContext ctx = token.parseExtra(UserContext.class); +String tenantId = ctx.getTenantId(); +``` diff --git a/docs/capabilities/jsqlparser.md b/docs/capabilities/jsqlparser.md deleted file mode 100644 index 0bf67c154..000000000 --- a/docs/capabilities/jsqlparser.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: jsqlparser -description: JSqlParser SQL 解析库 — 解析和改写 SQL 语句,用于数据权限条件注入 -status: 已实现 -scope: 后端 -source: 框架:JSqlParser -import: com.github.jsqlparser:jsqlparser -framework_version: 5.0 ---- - -## 解决什么问题 - -数据权限模块需要在 SQL 执行前动态注入权限条件(WHERE/JOIN 子句),手动拼接 SQL 既不安全也不灵活。JSqlParser 提供了 SQL 语法树解析和改写能力: - -- **SQL 解析**:将 SQL 字符串解析为语法树(AST) -- **条件注入**:在 SELECT/UPDATE/DELETE 语句中注入 WHERE 条件 -- **JOIN 注入**:为关联查询添加权限 JOIN 子句 -- **别名管理**:处理表别名与列别名的映射关系 - -## 如何使用 - -### 解析 SQL - -```java -Statement stmt = CCJSqlParserUtil.parse("SELECT * FROM users WHERE status = 'active'"); -Select select = (Select) stmt; -PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); -``` - -### 注入 WHERE 条件 - -```java -Expression where = CCJSqlParserUtil.parseCondExpression("dept_id IN (1, 2, 3)"); -Expression existingWhere = plainSelect.getWhere(); -if (existingWhere != null) { - plainSelect.setWhere(new AndExpression(existingWhere, where)); -} else { - plainSelect.setWhere(where); -} -String newSql = plainSelect.toString(); -// SELECT * FROM users WHERE status = 'active' AND dept_id IN (1, 2, 3) -``` - -### 本框架中的使用 - -`DataPermissionSQLEnhancer` 封装了 JSqlParser 的 SQL 改写逻辑,在 `SQLRunningContext.intercept()` 中自动调用。 - -## 使用实例 - -```java -// 数据权限自动改写 SQL -// 原始 SQL: SELECT * FROM orders WHERE status = 'PENDING' -// 改写后: SELECT * FROM orders WHERE status = 'PENDING' AND dept_id IN (1,2) - -// 直接调用 -Statement stmt = CCJSqlParserUtil.parse(sql); -if (stmt instanceof Select) { - // 注入权限条件 - enhancer.enhance((Select) stmt, tableName, condition); -} -String securedSql = stmt.toString(); -``` diff --git a/docs/capabilities/jsqlparser/jsqlparser.md b/docs/capabilities/jsqlparser/jsqlparser.md new file mode 100644 index 000000000..67392c83a --- /dev/null +++ b/docs/capabilities/jsqlparser/jsqlparser.md @@ -0,0 +1,138 @@ +--- +name: jsqlparser/jsqlparser +module: jsqlparser +description: JSqlParser SQL 解析器,支持 SQL 语句解析和改写 +status: 已实现 +scope: 后端 +source: 框架:jsqlparser +import: "com.github.jsqlparser:jsqlparser" +framework_version: 5.0 +--- + +## 解决什么问题 + +在企业级应用中,经常需要在运行时对 SQL 语句进行动态分析和改写,典型场景包括: + +- **数据权限过滤**:在 SQL 执行前透明注入行级权限条件(如 `WHERE dept_id IN (...)`),无需修改业务代码 +- **SQL 审计与监控**:解析 SQL 结构以提取表名、操作类型等元信息 +- **多租户隔离**:自动为查询追加租户过滤条件 +- **SQL 合法性校验**:判断传入的 SQL 是否为合法的 SELECT 查询,防止误操作 + +JSqlParser 提供了完整的 SQL 语法树解析能力,使上述场景可以在不依赖特定数据库方言的前提下,以统一的方式对 SQL 进行结构化访问和改写。在本框架中,它作为 `springboot-starter-data-authorization` 模块的核心依赖,支撑了数据权限 SQL 增强器的实现。 + +## 如何使用 + +### Maven 依赖 + +```xml + + com.github.jsqlparser + jsqlparser + 5.0 + +``` + +### 核心 API + +| 类 / 接口 | 说明 | +|-----------|------| +| `CCJSqlParserUtil.parse(sql)` | 将 SQL 字符串解析为 `Statement` 语法树对象 | +| `Statement` | SQL 语句的顶层抽象,可向下转型为 `Select`、`Insert`、`Update`、`Delete` 等 | +| `Select` / `PlainSelect` | 查询语句节点,提供 `getFromItem()`、`getWhere()`、`getJoins()` 等方法访问子句 | +| `Expression` | WHERE / ON 条件表达式树,支持 `AndExpression`、`OrExpression`、`InExpression` 等 | +| `Table` | 表引用节点,包含表名和别名信息 | +| `Join` | JOIN 子句节点,可通过 `getRightItem()` 获取关联表或子查询 | +| `statement.toString()` | 将修改后的语法树重新序列化为 SQL 字符串 | + +### 典型使用流程 + +1. 调用 `CCJSqlParserUtil.parse(sql)` 获得 `Statement` 对象 +2. 通过 `instanceof` 判断语句类型并向下转型 +3. 遍历语法树节点,读取或修改表名、条件、JOIN 等信息 +4. 调用 `toString()` 输出改写后的 SQL + +## 使用实例 + +### 示例 1:判断 SQL 是否为 SELECT 查询 + +```java +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.Select; + +public class SQLUtils { + + public static boolean isQuerySql(String sql) { + if (sql == null || sql.trim().isEmpty()) { + return false; + } + try { + Statement statement = CCJSqlParserUtil.parse(sql); + return statement instanceof Select; + } catch (Exception e) { + return false; + } + } +} +``` + +### 示例 2:解析 SQL 并向 WHERE 子句注入权限条件 + +以下示例展示了框架中 `DataPermissionSQLEnhancer` 的核心思路——解析 SQL 后遍历所有表和 JOIN,按需追加数据权限过滤条件: + +```java +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.*; + +public class DataPermissionExample { + + public String enhanceWithPermission(String sql, Expression permissionCondition) throws Exception { + // 1. 解析 SQL + Statement statement = CCJSqlParserUtil.parse(sql); + + if (statement instanceof Select) { + PlainSelect plainSelect = ((Select) statement).getPlainSelect(); + + // 2. 获取原有 WHERE 条件 + Expression existingWhere = plainSelect.getWhere(); + + // 3. 用 AND 拼接权限条件 + if (existingWhere != null) { + plainSelect.setWhere(new AndExpression(existingWhere, permissionCondition)); + } else { + plainSelect.setWhere(permissionCondition); + } + + // 4. 处理 JOIN 中的表(类似逻辑,此处省略) + } + + // 5. 返回改写后的 SQL + return statement.toString(); + } +} +``` + +### 示例 3:递归处理子查询和 UNION + +对于包含子查询或 `UNION` 的复杂 SQL,需要递归遍历语法树以确保权限条件被注入到每一层查询中: + +```java +private void deepMatch(Select select) throws Exception { + if (select instanceof PlainSelect) { + PlainSelect plainSelect = select.getPlainSelect(); + enhanceDataPermissionInSelect(plainSelect); + } + if (select instanceof SetOperationList) { + SetOperationList setOperationList = select.getSetOperationList(); + for (Select subSelect : setOperationList.getSelects()) { + deepMatch(subSelect.getPlainSelect()); + } + } +} +``` + +> **注意**:JSqlParser 5.0 对 API 进行了部分调整(如 `ParenthesedSelect` 替代旧版 `SubSelect`),升级时请参考官方迁移指南。 diff --git a/docs/capabilities/jwt-auth-gateway.md b/docs/capabilities/jwt-auth-gateway.md deleted file mode 100644 index 32b3e02ea..000000000 --- a/docs/capabilities/jwt-auth-gateway.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: jwt-auth-gateway -description: JWT 认证网关,集成 Spring Security 与 JJWT,支持 Token 创建、解析、Redis 有状态/无状态双模式 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter-security -symbols: - - JwtTokenGateway - - JWTTokenGatewayImpl - - JWTSecurityConfiguration - - SecurityJWTProperties - - Token - - TokenContext - - TokenGateway - - RedisTokenGateway - - RedisTokenGatewayImpl - - AuthenticationTokenFilter - - HttpSecurityConfigurer - - WebSecurityConfigurer - - LoginRequest - - LoginResponse -content_hash: 2dfa771d38597c44f2723f8f37c86531e625dc39fc7d352eae74844d43c949f3 ---- - -## 解决什么问题 - -Web 应用需要标准化的用户认证机制。本能力在 Spring Security 基础上封装了 JWT 认证体系: - -- **无状态认证(JWT 模式)**:Token 自包含用户信息,适合微服务架构 -- **有状态认证(Redis 模式)**:Token 存储在 Redis 中,支持主动注销和续期 -- **统一网关抽象**:`TokenGateway` 接口屏蔽 JWT/Redis 两种模式的差异 -- **Spring Security 集成**:自动配置 Filter 链,处理登录、登出、Token 校验 - -## 如何使用 - -### 配置 - -```properties -# 启用 JWT 无状态认证 -codingapi.security.jwt.enable=true -codingapi.security.jwt.secret-key=your-secret-key-at-least-32-chars -codingapi.security.jwt.valid-time=7200 - -# 或启用 Redis 有状态认证 -codingapi.security.redis.enable=true -codingapi.security.redis.valid-time=7200 -``` - -### 创建 Token - -```java -@Autowired -private TokenGateway tokenGateway; - -// 登录成功后创建 Token -Token token = tokenGateway.create(username, authorities); -// 或通过 JwtTokenGateway 创建 -Token token = jwtTokenGateway.create(username, iv, authorities, extra); -``` - -### 解析 Token - -```java -Token token = jwtTokenGateway.parser(jwtString); -String username = token.getUsername(); -List authorities = token.getAuthorities(); -``` - -### 配置免认证 URL - -```properties -codingapi.security.ignore-urls=/open/**,/#/**,/api/version -``` - -## 使用实例 - -```java -// 自定义登录处理 -@Service -public class LoginService { - @Autowired - private TokenGateway tokenGateway; - - public LoginResponse login(String username, String password) { - // 验证用户名密码... - UserDetails user = authenticate(username, password); - Token token = tokenGateway.create( - user.getUsername(), - user.getAuthorities().stream() - .map(GrantedAuthority::getAuthority).toList() - ); - return new LoginResponse(token.getToken(), token.getValidTime()); - } -} -``` diff --git a/docs/capabilities/kryo.md b/docs/capabilities/kryo.md deleted file mode 100644 index beebd2b66..000000000 --- a/docs/capabilities/kryo.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: kryo -description: Kryo 高性能序列化库 — 用于工作流数据快照的深拷贝与持久化 -status: 已实现 -scope: 后端 -source: 框架:Kryo -import: com.esotericsoftware:kryo -framework_version: 5.6.2 ---- - -## 解决什么问题 - -工作流引擎在审批过程中需要保存业务数据的快照(`BindDataSnapshot`),以便后续审批人能看到提交时的数据状态。Kryo 提供了高效的二进制序列化方案: - -- **深拷贝**:将绑定数据深拷贝为快照,避免引用污染 -- **高性能**:比 Java 原生序列化快 10 倍以上 -- **紧凑**:序列化后的二进制数据体积小,适合数据库存储 -- **类型注册**:通过预注册类型优化序列化效率 - -## 如何使用 - -### 基本序列化 - -```java -Kryo kryo = new Kryo(); -kryo.setRegistrationRequired(false); - -// 序列化 -Output output = new Output(4096, -1); -kryo.writeObject(output, myObject); -byte[] bytes = output.toBytes(); - -// 反序列化 -Input input = new Input(bytes); -MyObject restored = kryo.readObject(input, MyObject.class); -``` - -### 深拷贝 - -```java -Kryo kryo = new Kryo(); -MyObject copy = kryo.copy(original); -``` - -### 本框架中的使用 - -工作流引擎的 `BindDataSnapshot` 使用 Kryo 保存审批时的业务数据快照: - -```java -// 提交审批时自动保存快照 -FlowResult result = flowService.submit(processId, opinion, operator); -// 内部:kryo.copy(bindData) → 快照存储 -``` - -## 使用实例 - -```java -// 工作流快照恢复 -FlowDetail detail = flowService.detail(processId); -IBindData snapshot = detail.getSnapshot(); -// snapshot 保存的是提交审批时的数据状态,而非当前最新数据 -``` diff --git a/docs/capabilities/react/react.md b/docs/capabilities/react/react.md new file mode 100644 index 000000000..884ef7521 --- /dev/null +++ b/docs/capabilities/react/react.md @@ -0,0 +1,174 @@ +--- +name: react/react +module: react +description: React UI 框架配合 Ant Design 组件库,用于构建前端用户界面 +status: 已实现 +scope: 前端 +source: 框架:react +import: "react" +framework_version: 18.3.1 +--- + +## 解决什么问题 + +在企业级中后台与移动端业务系统中,前端开发面临以下核心痛点: + +- **UI 一致性差**:多页面、多团队并行开发时,缺乏统一的组件体系导致视觉风格与交互行为不一致,用户体验割裂。 +- **重复造轮子**:表单、表格、弹窗、导航等通用界面元素在每个项目中被反复实现,研发效率低下且质量参差不齐。 +- **状态管理复杂**:随着业务增长,组件间数据流变得难以追踪,传统命令式 DOM 操作模式维护成本急剧上升。 +- **前后端对接低效**:缺少与后端统一响应结构(`Response` / `SingleResponse` / `MultiResponse`)和分页查询(`PageRequest` + `Filter`)相匹配的前端消费范式。 + +React 18 + Ant Design 5 的组合提供了声明式 UI 编程模型与开箱即用的高质量企业级组件库,使开发者能够以组合式、可预测的方式快速构建一致的用户界面,并与 springboot-framework 后端的统一响应和数据查询能力无缝衔接。 + +## 如何使用 + +### 依赖安装 + +```bash +# 在 admin-ui 或 mobile-ui 目录下 +npm install react@^18.3.1 react-dom@^18.3.1 antd@^5.x +``` + +### 项目集成要点 + +1. **组件引入**:按需导入 Ant Design 组件,配合 Rsbuild 自动完成 Tree Shaking,无需额外配置 babel-plugin-import。 +2. **主题定制**:通过 Ant Design 5 的 ConfigProvider + Design Token 体系统一管理品牌色、圆角、字号等设计变量,确保多模块视觉一致。 +3. **与后端对接**:封装统一的 HTTP 请求层,将后端 `Response.errCode / errMessage` 映射为全局提示;将 `MultiResponse.data` + `PageRequest` 参数直接绑定到 ProTable / ProList 的 `request` 属性,实现分页筛选零胶水代码。 +4. **状态管理**:轻量场景使用 React Context + useReducer;跨模块共享状态推荐使用 Zustand 或 valtio,避免过度引入 Redux。 +5. **微前端集成**:admin-ui 与 mobile-ui 均通过 Module Federation 暴露/消费远程模块,React 作为共享依赖(shared singleton)确保运行时只存在一个实例。 + +### 关键 API + +| API / 概念 | 说明 | +|------------|------| +| `useState` / `useEffect` / `useMemo` | React Hooks,声明式状态与副作用管理 | +| `` | Ant Design 全局配置:主题 token、国际化、组件尺寸 | +| `` / `` | Ant Design Pro Components,内置搜索表单、分页、列定义,天然适配后端 PageRequest | +| `message` / `notification` | 全局反馈,对接后端 Response 错误码 | +| `useRequest` (ahooks) | 异步数据请求 Hook,支持缓存、重试、轮询 | + +## 使用实例 + +### 基础列表页(对接后端 MultiResponse + PageRequest) + +```tsx +import React from 'react'; +import { ProTable } from '@ant-design/pro-components'; +import type { ProColumns } from '@ant-design/pro-components'; +import { message } from 'antd'; +import { request } from '@/utils/request'; // 封装的统一请求 + +interface UserItem { + id: string; + name: string; + email: string; + status: number; +} + +const columns: ProColumns[] = [ + { title: '姓名', dataIndex: 'name' }, + { title: '邮箱', dataIndex: 'email', copyable: true }, + { + title: '状态', + dataIndex: 'status', + valueEnum: { 1: { text: '启用', status: 'Success' }, 0: { text: '禁用', status: 'Error' } }, + }, +]; + +const UserList: React.FC = () => { + return ( + + headerTitle="用户管理" + columns={columns} + rowKey="id" + request={async (params) => { + // params 自动携带 current, pageSize 及搜索条件 + const res = await request.get('/api/users', { params }); + if (!res.success) { + message.error(res.errMessage); + return { data: [], total: 0, success: false }; + } + return { data: res.data, total: res.totalElements, success: true }; + }} + search={{ labelWidth: 'auto' }} + pagination={{ defaultPageSize: 20 }} + /> + ); +}; + +export default UserList; +``` + +### 表单提交(对接后端 SingleResponse) + +```tsx +import React from 'react'; +import { ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components'; +import { message } from 'antd'; +import { request } from '@/utils/request'; + +interface CreateUserProps { + open: boolean; + onClose: () => void; + onSuccess: () => void; +} + +const CreateUserModal: React.FC = ({ open, onClose, onSuccess }) => { + return ( + { + const res = await request.post('/api/users', values); + if (res.success) { + message.success('创建成功'); + onSuccess(); + return true; + } + message.error(res.errMessage || '创建失败'); + return false; + }} + > + + + + + ); +}; + +export default CreateUserModal; +``` + +### 主题定制与国际化 + +```tsx +import React from 'react'; +import { ConfigProvider, zhCN } from 'antd'; + +const App: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default App; +``` + +以上示例展示了 React + Ant Design 在 springboot-framework 项目中的典型用法:列表页自动对接 `PageRequest` 分页与过滤、表单提交消费统一 `Response` 结构、以及通过 `ConfigProvider` 实现全局主题管控。结合 Module Federation 微前端架构,各业务模块可独立开发部署,同时共享统一的 UI 基础与设计规范。 diff --git a/docs/capabilities/redux/toolkit.md b/docs/capabilities/redux/toolkit.md new file mode 100644 index 000000000..718688160 --- /dev/null +++ b/docs/capabilities/redux/toolkit.md @@ -0,0 +1,308 @@ +--- +name: redux/toolkit +module: redux +description: Redux Toolkit 状态管理库,提供简化的 Redux 状态管理模式 +status: 已实现 +scope: 前端 +source: 框架:redux +import: "@reduxjs/toolkit" +framework_version: 2.2.7 +--- + +## 解决什么问题 + +原生 Redux 存在样板代码过多、配置繁琐、不可变数据更新复杂等痛点。Redux Toolkit (RTK) 作为官方推荐的状态管理方案,解决了以下核心问题: + +- **减少样板代码**:通过 `createSlice` 自动生成 action creators 和 reducer,无需手写 switch-case 和常量定义 +- **简化不可变更新**:内置 Immer.js,允许以"可变"风格编写 reducer,自动转换为安全的不可变更新 +- **标准化异步逻辑**:`createAsyncThunk` 统一处理请求生命周期(pending / fulfilled / rejected),避免手动管理 loading/error 状态 +- **开箱即用的 Store 配置**:`configureStore` 默认集成 thunk 中间件、序列化检查、开发工具,零配置即可使用 +- **TypeScript 友好**:完整类型推导,减少手写类型注解的负担 +- **性能优化内置**:提供 `createSelector`(Reselect)用于派生数据的记忆化计算,避免不必要的重渲染 + +适用于需要全局状态共享、复杂业务状态流转、多组件协同的前端应用,如 admin-ui 的管理后台、mobile-ui 的业务流程等场景。 + +## 如何使用 + +### 安装与引入 + +```bash +npm install @reduxjs/toolkit react-redux +``` + +### 核心 API + +#### configureStore — 创建 Store + +```typescript +import { configureStore } from '@reduxjs/toolkit' + +const store = configureStore({ + reducer: { + user: userReducer, + order: orderReducer, + }, + // devTools、thunk、serializableCheck 均默认启用 +}) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch +``` + +#### createSlice — 定义状态切片 + +```typescript +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +interface UserState { + name: string + token: string | null +} + +const initialState: UserState = { name: '', token: null } + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + setUser(state, action: PayloadAction<{ name: string; token: string }>) { + // 直接"修改"state,Immer 自动处理不可变更新 + state.name = action.payload.name + state.token = action.payload.token + }, + logout(state) { + state.name = '' + state.token = null + }, + }, +}) + +export const { setUser, logout } = userSlice.actions +export default userSlice.reducer +``` + +#### createAsyncThunk — 异步操作 + +```typescript +import { createAsyncThunk } from '@reduxjs/toolkit' + +export const fetchUser = createAsyncThunk( + 'user/fetchUser', + async (userId: string, thunkAPI) => { + const response = await fetch(`/api/users/${userId}`) + return response.json() + } +) + +// 在 slice 的 extraReducers 中处理三种状态 +extraReducers: (builder) => { + builder + .addCase(fetchUser.pending, (state) => { state.loading = true }) + .addCase(fetchUser.fulfilled, (state, action) => { + state.loading = false + state.data = action.payload + }) + .addCase(fetchUser.rejected, (state, action) => { + state.loading = false + state.error = action.error.message ?? 'Unknown error' + }) +} +``` + +#### createSelector — 派生数据记忆化 + +```typescript +import { createSelector } from '@reduxjs/toolkit' + +const selectOrders = (state: RootState) => state.order.list +const selectFilter = (state: RootState) => state.order.filter + +export const selectFilteredOrders = createSelector( + [selectOrders, selectFilter], + (orders, filter) => orders.filter(o => o.status === filter) +) +``` + +### React 绑定 + +```typescript +import { useSelector, useDispatch } from 'react-redux' +import type { RootState, AppDispatch } from './store' + +// 带类型的 hooks +const useAppSelector = useSelector.withTypes() +const useAppDispatch = useDispatch.withTypes() +``` + +## 使用实例 + +### 完整的用户模块示例 + +```typescript +// features/user/userSlice.ts +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import { userService } from '../../services/userService' + +export interface UserInfo { + id: string + username: string + role: string +} + +interface UserState { + current: UserInfo | null + list: UserInfo[] + loading: boolean + error: string | null +} + +const initialState: UserState = { + current: null, + list: [], + loading: false, + error: null, +} + +// 异步:获取当前用户 +export const loadCurrentUser = createAsyncThunk( + 'user/loadCurrent', + async () => { + return await userService.getCurrentUser() + } +) + +// 异步:获取用户列表 +export const loadUserList = createAsyncThunk( + 'user/loadList', + async ({ keyword } = {}) => { + return await userService.list({ keyword }) + } +) + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + clearError(state) { + state.error = null + }, + resetUser(state) { + Object.assign(state, initialState) + }, + }, + extraReducers: (builder) => { + builder + // loadCurrentUser + .addCase(loadCurrentUser.pending, (state) => { + state.loading = true + state.error = null + }) + .addCase(loadCurrentUser.fulfilled, (state, action) => { + state.loading = false + state.current = action.payload + }) + .addCase(loadCurrentUser.rejected, (state, action) => { + state.loading = false + state.error = action.error.message ?? '加载失败' + }) + // loadUserList + .addCase(loadUserList.pending, (state) => { + state.loading = true + }) + .addCase(loadUserList.fulfilled, (state, action) => { + state.loading = false + state.list = action.payload + }) + .addCase(loadUserList.rejected, (state, action) => { + state.loading = false + state.error = action.error.message ?? '列表加载失败' + }) + }, +}) + +export const { clearError, resetUser } = userSlice.actions +export default userSlice.reducer +``` + +```tsx +// features/user/UserProfile.tsx +import { useEffect } from 'react' +import { useAppSelector, useAppDispatch } from '../../app/hooks' +import { loadCurrentUser } from './userSlice' + +export function UserProfile() { + const dispatch = useAppDispatch() + const { current, loading, error } = useAppSelector((state) => state.user) + + useEffect(() => { + dispatch(loadCurrentUser()) + }, [dispatch]) + + if (loading) return
加载中...
+ if (error) return
错误: {error}
+ if (!current) return
未登录
+ + return ( +
+

{current.username}

+

角色: {current.role}

+
+ ) +} +``` + +### RTK Query — 声明式数据获取 + +```typescript +// app/api.ts +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +export const api = createApi({ + reducerPath: 'api', + baseQuery: fetchBaseQuery({ baseUrl: '/api', credentials: 'include' }), + tagTypes: ['User'], + endpoints: (builder) => ({ + getUsers: builder.query({ + query: ({ keyword }) => `/users?keyword=${keyword ?? ''}`, + providesTags: ['User'], + }), + updateUser: builder.mutation>({ + query: (body) => ({ url: `/users/${body.id}`, method: 'PUT', body }), + invalidatesTags: ['User'], + }), + }), +}) + +export const { useGetUsersQuery, useUpdateUserMutation } = api +``` + +```tsx +// 组件中使用 RTK Query hooks +function UserList() { + const { data: users, isLoading, error } = useGetUsersQuery({ keyword: 'admin' }) + const [updateUser] = useUpdateUserMutation() + + if (isLoading) return + if (error) return + + return ( + ( + + ), + }, + ]} + /> + ) +} +``` + +此模式在 admin-ui 和 mobile-ui 中广泛使用,配合 Module Federation 微前端架构,各子应用可独立管理自身状态切片,同时通过共享 Store 实现跨应用状态同步。 diff --git a/docs/capabilities/rest-client.md b/docs/capabilities/rest-client.md deleted file mode 100644 index 44a88b946..000000000 --- a/docs/capabilities/rest-client.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: rest-client -description: REST HTTP 客户端封装,支持自动重试、代理配置、请求/响应拦截器与信任所有证书模式 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter -symbols: - - RestClient - - HttpClient - - HttpRequest - - Request - - RestTemplateContext - - SessionClient - - IRestParam - - RestParam - - HttpProxyProperties - - TrustAnyHttpClientFactory -content_hash: 7c8d0b2743ca064f6b334c28b0fc2565f210b4898903f3cf030cf8613ff95854 ---- - -## 解决什么问题 - -微服务之间或调用第三方 API 时,需要封装 HTTP 请求。Spring RestTemplate 原生使用不够便捷。本能力提供了更友好的 REST 客户端: - -- **自动重试**:`RestClient` 内置重试机制,失败后自动重试(默认5次) -- **请求拦截器**:支持请求前/响应后的钩子处理(如自动添加 Header、日志记录) -- **代理支持**:通过 `HttpProxyProperties` 配置 HTTP 代理 -- **信任所有证书**:`TrustAnyHttpClientFactory` 支持 HTTPS 免证书验证(开发环境) -- **参数构建器**:`RestParam` 链式构建请求参数 - -## 如何使用 - -### RestClient(带重试) - -```java -// 简单使用 -RestClient client = new RestClient("https://api.example.com"); -String result = client.get("/users/1"); -String result = client.post("/users", jsonObject); - -// 带自定义 Header -HttpHeaders headers = new HttpHeaders(); -headers.set("Authorization", "Bearer " + token); -String result = client.get("/protected", headers); -``` - -### HttpClient(无重试) - -```java -HttpClient client = new HttpClient(); -String result = client.post("https://api.example.com/data", headers, jsonObject); -String result = client.get("https://api.example.com/data", headers, params); -``` - -### RestTemplateContext(单例) - -```java -// 全局共享的 RestTemplate(信任所有证书) -RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); -``` - -### 请求/响应拦截器 - -```java -RestClient client = new RestClient( - proxyProperties, - "https://api.example.com", - 5, // 重试次数 - "{}", // 失败时的默认响应 - request -> { /* 请求前处理 */ }, - response -> { /* 响应后处理 */ } -); -``` - -## 使用实例 - -```java -// 调用外部 API -RestClient client = new RestClient("https://api.weather.com"); -RestParam params = new RestParam() - .add("city", "北京") - .add("unit", "celsius"); -String weather = client.get("/forecast", params); -JSONObject data = JSON.parseObject(weather); -``` diff --git a/docs/capabilities/spring-data-jpa.md b/docs/capabilities/spring-data-jpa.md deleted file mode 100644 index c1c68ff88..000000000 --- a/docs/capabilities/spring-data-jpa.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -name: spring-data-jpa -description: Spring Data JPA — ORM 映射、Repository 抽象、分页查询、Specification 动态查询 -status: 已实现 -scope: 后端 -source: 框架:Spring Data JPA -import: org.springframework.boot:spring-boot-starter-data-jpa -framework_version: (由 Spring Boot 3.3.5 管理) ---- - -## 解决什么问题 - -Spring Data JPA 是项目持久层的标准基础: - -- **ORM 映射**:JPA 注解定义实体与表的映射关系 -- **Repository 抽象**:`JpaRepository` 提供标准 CRUD 操作 -- **分页查询**:`Pageable` / `Page` 标准化分页接口 -- **Specification**:`JpaSpecificationExecutor` 支持动态条件查询 -- **命名查询**:通过方法名自动推导 SQL(`findByUsername`) - -## 如何使用 - -### 基础 Repository - -```java -public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); - List findByStatus(String status); -} -``` - -### 与本框架集成 - -本框架的 `FastRepository` 扩展了 Spring Data JPA: - -```java -// FastRepository 继承 JpaRepository + JpaSpecificationExecutor -public interface UserRepository extends FastRepository { - // 继承 findAll(PageRequest) — 自动构建 Example/HQL -} -``` - -### 分页查询 - -```java -Page page = userRepository.findAll( - org.springframework.data.domain.PageRequest.of(0, 20, Sort.by("id")) -); -``` - -## 使用实例 - -```java -@Entity -@Table(name = "sys_user") -public class User { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String username; - private String email; - private String status; -} - -// 标准 CRUD -userRepository.save(user); -userRepository.findById(id); -userRepository.delete(user); -``` diff --git a/docs/capabilities/spring-framework.md b/docs/capabilities/spring-framework.md deleted file mode 100644 index 7388d1491..000000000 --- a/docs/capabilities/spring-framework.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: spring-framework -description: Spring Framework / Spring Boot 核心能力 — IoC 容器、自动配置、事件发布、AOP、Web MVC -status: 已实现 -scope: 后端 -source: 框架:Spring Boot -import: org.springframework.boot:spring-boot-starter -framework_version: 3.3.5 ---- - -## 解决什么问题 - -Spring Boot 作为项目的基础框架,提供以下核心能力: - -- **IoC 容器**:依赖注入(DI)管理所有 Bean 的生命周期 -- **自动配置**:`@SpringBootApplication` 自动装配 Starter 组件 -- **事件发布**:`ApplicationEventPublisher` 发布 Spring 原生事件 -- **AOP 切面**:声明式事务(`@Transactional`)、日志切面等 -- **Web MVC**:REST 控制器、参数绑定、拦截器、异常处理 -- **条件装配**:`@ConditionalOnClass` / `@ConditionalOnProperty` 按需加载 - -## 如何使用 - -### 自动配置注册 - -各 Starter 模块通过以下文件注册自动配置: -- `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`(Spring Boot 3.x) -- `META-INF/spring.factories`(兼容旧版) - -### Spring 事件集成 - -本框架的事件系统底层桥接了 Spring 的 `ApplicationEventPublisher`: -```java -// DomainEventContext 内部使用 Spring 事件发布 -context.publishEvent(new DomainEvent(event, sync, traceId)); -``` - -### 配置属性 - -```properties -# Spring Boot 基础配置 -server.port=8090 -spring.application.name=my-app -``` - -## 使用实例 - -```java -// 自定义自动配置 -@Configuration -@ConditionalOnClass(name = "org.springframework.web.servlet.HandlerExceptionResolver") -public class BasicHandlerExceptionResolverConfiguration { - @Bean - public HandlerExceptionResolver servletExceptionHandler() { - return new ServletExceptionHandler(); - } -} - -// 声明式事务 -@Transactional -public void createUser(User user) { - userRepository.save(user); - EventPusher.push(new UserCreatedEvent(user.getId())); -} -``` diff --git a/docs/capabilities/spring-security.md b/docs/capabilities/spring-security.md deleted file mode 100644 index a93d02a4e..000000000 --- a/docs/capabilities/spring-security.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: spring-security -description: Spring Security 认证与授权框架 — 提供 Filter 链、CSRF 防护、密码编码、权限控制 -status: 已实现 -scope: 后端 -source: 框架:Spring Security -import: org.springframework.boot:spring-boot-starter-security -framework_version: (由 Spring Boot 3.3.5 管理) ---- - -## 解决什么问题 - -Spring Security 为 Web 应用提供完整的安全基础设施: - -- **Filter 链**:请求级别的认证与授权过滤 -- **CSRF 防护**:跨站请求伪造保护(可通过配置关闭) -- **密码编码**:`PasswordEncoder` 安全存储密码 -- **会话管理**:有状态(Session)与无状态(JWT)两种模式 -- **权限注解**:`@PreAuthorize` / `@Secured` 方法级权限控制 - -## 如何使用 - -### 与本框架集成 - -本框架的 `springboot-starter-security` 模块已封装 Spring Security 配置: - -```java -// HttpSecurityConfigurer — 自定义 Security 配置 -public interface HttpSecurityCustomer { - void customer(HttpSecurity http) throws Exception; -} - -// 注入自定义配置 -@Bean -public HttpSecurityCustomer customSecurity() { - return http -> http.authorizeHttpRequests(auth -> auth - .requestMatchers("/admin/**").hasRole("ADMIN") - .anyRequest().authenticated() - ); -} -``` - -### 免认证 URL - -```properties -codingapi.security.ignore-urls=/open/**,/#/**,/api/version -``` - -### 权限注解 - -```java -@PreAuthorize("hasRole('ADMIN')") -public void deleteUser(Long id) { ... } -``` - -## 使用实例 - -```java -// 自定义 UserDetailsService -@Service -public class UserDetailsServiceImpl implements UserDetailsService { - @Override - public UserDetails loadUserByUsername(String username) { - User user = userService.findByUsername(username); - return new org.springframework.security.core.userdetails.User( - user.getUsername(), user.getPassword(), - user.getAuthorities() - ); - } -} -``` diff --git a/docs/capabilities/springboot-starter-data-authorization/sql-interception.md b/docs/capabilities/springboot-starter-data-authorization/sql-interception.md new file mode 100644 index 000000000..eab7667c5 --- /dev/null +++ b/docs/capabilities/springboot-starter-data-authorization/sql-interception.md @@ -0,0 +1,221 @@ +--- +name: springboot-starter-data-authorization/sql-interception +module: springboot-starter-data-authorization +description: SQL 拦截数据权限,通过 JDBC 代理透明注入权限条件实现行级数据过滤 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter-data-authorization" +symbols: + - SQLRunningContext + - ConnectionProxy + - PreparedStatementProxy + - StatementProxy + - DefaultSQLInterceptor +content_hash: d1ffbdc7fb8d45d6cc91248e02de5373e4ac8f0b417ecf34cd62ecf6709a2641 +--- + +## 解决什么问题 + +在企业级应用中,不同角色的用户只能看到自己有权限访问的数据(行级数据权限)。传统实现方式存在以下痛点: + +- **侵入性强**:需要在每个 Service/Repository 方法中手动拼接权限条件,代码散落各处 +- **容易遗漏**:新增查询接口时可能忘记添加权限过滤,导致数据泄露 +- **维护困难**:权限规则变更时需要修改大量业务代码 +- **与业务耦合**:权限逻辑与业务查询逻辑混杂,违反单一职责原则 + +SQL 拦截数据权限通过 JDBC 代理层实现了完全透明的权限注入: + +- **零侵入**:业务代码无需任何修改,所有 SELECT 查询自动注入权限条件 +- **全覆盖**:无论通过 JPA、MyBatis、原生 JDBC 还是其他 ORM 框架执行的 SQL,都会被统一拦截 +- **灵活配置**:通过 `RowHandler` 接口按表名自定义权限规则,支持 WHERE 条件和 JOIN 关联两种注入模式 +- **可跳过**:提供 `skipDataAuthorization()` API,允许特定场景下临时绕过权限检查 +- **SQL 解析增强**:使用 JSqlParser 解析 SQL AST,精确识别表名和别名,支持子查询、UNION、JOIN 等复杂 SQL 结构 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-data-authorization + +``` + +### 2. 架构概览 + +整个拦截链路由以下组件构成: + +``` +DataSource → ConnectionProxy → PreparedStatementProxy / StatementProxy + ↓ + SQLRunningContext.intercept(sql) + ↓ + SQLInterceptor (DefaultSQLInterceptor) + ↓ + DataPermissionSQLEnhancer (JSqlParser) + ↓ + RowHandler.handler(tableName, alias) + ↓ + Condition (WHERE / JOIN 条件注入) +``` + +### 3. 核心组件说明 + +#### ConnectionProxy + +JDBC `Connection` 的代理实现。在 `prepareStatement()`、`createStatement()`、`prepareCall()` 等方法中拦截 SQL,调用 `SQLRunningContext.intercept(sql)` 获取改写后的 SQL,并将 `SQLExecuteState` 传递给下游的 Statement 代理。 + +#### PreparedStatementProxy / StatementProxy + +JDBC `PreparedStatement` 和 `Statement` 的代理实现。在执行 `executeQuery()`、`execute()` 等方法时,确保使用经过权限改写的 SQL。对于直接传入 SQL 字符串的方法(如 `executeQuery(String sql)`),会再次调用 `SQLRunningContext.intercept()` 进行拦截。查询结果通过 `ResultSetProxy` 包装返回。 + +#### SQLRunningContext + +SQL 拦截的核心调度器(单例模式),负责: +- 从 `SQLInterceptorContext` 获取当前 `SQLInterceptor` 实例 +- 通过 `ThreadLocal skipInterceptor` 控制是否跳过拦截 +- 调用 `SQLInterceptor.beforeHandler()` 判断是否需要拦截(默认仅拦截 SELECT 语句) +- 调用 `SQLInterceptor.postHandler()` 执行 SQL 改写 +- 提供 `skipDataAuthorization(Supplier/Runnable)` API 临时跳过权限检查 + +#### DefaultSQLInterceptor + +默认的 SQL 拦截器实现,包含三个阶段的处理: +- `beforeHandler(sql)`:通过 `SQLUtils.isQuerySql()` 判断是否为查询语句,仅 SELECT 会被拦截 +- `postHandler(sql)`:创建 `DataPermissionSQLEnhancer`,使用 JSqlParser 解析 SQL 并通过 `RowHandler` 获取权限条件,返回增强后的 SQL +- `afterHandler(sql, newSql, exception)`:日志记录,当配置 `showSql=true` 时输出改写后的 SQL + +#### RowHandler + +行级权限处理器接口,由业务方实现: + +```java +public interface RowHandler { + Condition handler(String subSql, String tableName, String tableAlias); +} +``` + +返回值 `Condition` 支持两种注入模式: +- **WHERE 条件**:`Condition.customCondition("dept_id IN (1,2,3)")` — 在 WHERE 子句中追加 AND 条件 +- **JOIN 关联**:通过 `JoinConditionSQL` 添加 INNER/LEFT/RIGHT JOIN 关联表 + +### 4. 跳过数据权限 + +在某些管理操作或系统任务中需要绕过数据权限: + +```java +// 方式一:Lambda 表达式 +List allUsers = SQLRunningContext.getInstance() + .skipDataAuthorization(() -> userRepository.findAll()); + +// 方式二:Runnable +SQLRunningContext.getInstance() + .skipDataAuthorization(() -> { + reportService.generateMonthlyReport(); + }); +``` + +## 使用实例 + +### 示例一:按部门过滤数据 + +```java +@Component +public class DeptRowHandler implements RowHandler { + + @Override + public Condition handler(String subSql, String tableName, String tableAlias) { + // 仅对 employee 表注入权限条件 + if ("employee".equalsIgnoreCase(tableName)) { + List deptIds = SecurityContext.getCurrentDeptIds(); + if (deptIds == null || deptIds.isEmpty()) { + return Condition.emptyCondition(); // 无权限,返回 null 不注入 + } + String inClause = deptIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + return Condition.customCondition( + String.format("%s.dept_id IN (%s)", tableAlias, inClause) + ); + } + // 其他表不注入权限条件 + return Condition.emptyCondition(); + } +} +``` + +效果:原始 SQL `SELECT * FROM employee WHERE status = 'active'` 被改写为: +```sql +SELECT * FROM employee WHERE dept_id IN (1,2,3) AND status = 'active' +``` + +### 示例二:通过 JOIN 关联实现跨表权限 + +```java +@Component +public class ProjectRowHandler implements RowHandler { + + @Override + public Condition handler(String subSql, String tableName, String tableAlias) { + if ("project".equalsIgnoreCase(tableName)) { + Condition condition = new Condition(); + // 通过 JOIN 关联成员表,只查询当前用户参与的项目 + JoinConditionSQL joinSQL = new JoinConditionSQL( + "project_member pm", + JoinConditionSQL.Type.INNER, + String.format("pm.project_id = %s.id AND pm.user_id = %d", + tableAlias, SecurityContext.getCurrentUserId()) + ); + condition.addConditionSQL(joinSQL); + return condition; + } + return Condition.emptyCondition(); + } +} +``` + +效果:原始 SQL `SELECT * FROM project WHERE status = 'open'` 被改写为: +```sql +SELECT * FROM project +INNER JOIN project_member pm ON pm.project_id = project.id AND pm.user_id = 1001 +WHERE status = 'open' +``` + +### 示例三:自定义 SQLInterceptor + +如需替换默认的拦截逻辑(例如增加缓存或审计),可实现 `SQLInterceptor` 接口并注册为 Spring Bean: + +```java +@Component +public class AuditSQLInterceptor implements SQLInterceptor { + + @Override + public boolean beforeHandler(String sql) { + // 仅拦截 SELECT 且不包含系统表的查询 + return SQLUtils.isQuerySql(sql) && !sql.contains("sys_config"); + } + + @Override + public DataPermissionSQL postHandler(String sql) throws SQLException { + RowHandler rowHandler = RowHandlerContext.getInstance().getRowHandler(); + DataPermissionSQLEnhancer enhancer = new DataPermissionSQLEnhancer(sql, rowHandler); + return new DataPermissionSQL(sql, enhancer.getNewSQL(), enhancer.getTableAlias()); + } + + @Override + public void afterHandler(String sql, String newSql, SQLException exception) { + // 记录审计日志 + AuditLog.record(sql, newSql, exception); + } +} +``` + +### 内部工作原理 + +1. **连接代理**:DataSource 返回的 `Connection` 被包装为 `ConnectionProxy` +2. **SQL 拦截时机**:当调用 `connection.prepareStatement(sql)` 时,`ConnectionProxy` 立即调用 `SQLRunningContext.intercept(sql)` 对 SQL 进行改写 +3. **递归解析**:`DataPermissionSQLEnhancer` 使用 JSqlParser 解析 SQL AST,深度遍历 PlainSelect、SetOperationList(UNION)、子查询、JOIN 中的子 Select,对每个涉及的表调用 `RowHandler` +4. **条件注入**:`WhereConditionSQLHandler` 将 WHERE 条件通过 AND 拼接到原有 WHERE 子句;`JoinConditionSQLHandler` 向 FROM 子句追加 JOIN 关联 +5. **防重入**:`SQLRunningContext` 使用 ThreadLocal 标记,在拦截器内部执行的查询不会被二次拦截 diff --git a/docs/capabilities/springboot-starter-data-fast/fast-repository.md b/docs/capabilities/springboot-starter-data-fast/fast-repository.md new file mode 100644 index 000000000..6643f6ef0 --- /dev/null +++ b/docs/capabilities/springboot-starter-data-fast/fast-repository.md @@ -0,0 +1,170 @@ +--- +name: springboot-starter-data-fast/fast-repository +module: springboot-starter-data-fast +description: JPA 增强 Repository,支持 PageRequest 动态过滤查询和 HQL 构建 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter-data-fast" +symbols: + - FastRepository +content_hash: f840da63a739c7b4e23172d900dd2347480547290e1f947bc45f54f3e9916a53 +--- + +## 解决什么问题 + +在标准 Spring Data JPA 开发中,动态条件查询通常需要手动编写 `Specification`、`QueryDSL` 或拼接 HQL,代码冗长且难以维护。当业务列表页面需要支持多字段组合筛选(等于、模糊、范围、IN 等)时,开发者往往要为每种查询场景编写独立的 Repository 方法或复杂的 Specification 构建逻辑。 + +`FastRepository` 通过扩展 `JpaRepository` 和 `JpaSpecificationExecutor`,提供了基于 `PageRequest` 的声明式动态查询能力: + +- **自动 Example 查询**:当 Filter 条件均为简单等值匹配时,自动转换为 Spring Data `Example` 查询,零额外代码 +- **HQL 动态构建**:当包含模糊、范围、IN 等复杂条件时,自动构建参数化 HQL,避免 SQL 注入风险 +- **SearchRequest 集成**:支持从 HTTP 请求参数中自动解析 filter、sort 条件,适用于前端列表页的通用查询接口 +- **OR/AND 组合过滤**:支持嵌套的 OR/AND 条件组合,满足复杂业务筛选需求 + +## 如何使用 + +### 1. 定义 Repository 接口 + +继承 `FastRepository` 即可获得全部动态查询能力: + +```java +public interface UserEntityRepository extends FastRepository { + // 标准 JpaRepository 方法仍然可用 + UserEntity getUserEntityByUsername(String username); +} +``` + +### 2. 使用 PageRequest 进行动态过滤查询 + +```java +// 创建分页请求并添加过滤条件 +PageRequest request = PageRequest.of(0, 20); +request.addFilter("name", "张三"); // 等值匹配 +request.addFilter("age", Relation.GT, 18); // 大于 +request.addFilter("email", Relation.LIKE, "gmail"); // 模糊查询 + +// 方式一:自动选择 Example 或 HQL(推荐) +Page page = repository.findAll(request); + +// 方式二:强制使用 HQL 查询(适合复杂条件) +Page page2 = repository.pageRequest(request); +``` + +### 3. 支持的过滤关系(Relation) + +| Relation | 说明 | HQL 示例 | +|----------|------|----------| +| EQ(默认) | 等于 | `name = ?1` | +| NEQ | 不等于 | `name != ?1` | +| GT | 大于 | `age > ?1` | +| LT | 小于 | `age < ?1` | +| GTE | 大于等于 | `age >= ?1` | +| LTE | 小于等于 | `age <= ?1` | +| LIKE | 全模糊 | `name LIKE ?1`(自动加 `%value%`) | +| LEFT_LIKE | 左模糊 | `name LIKE ?1`(自动加 `%value`) | +| RIGHT_LIKE | 右模糊 | `name LIKE ?1`(自动加 `value%`) | +| IN | 包含 | `id IN (?1)` | +| NOT_IN | 不包含 | `id NOT IN (?1)` | +| BETWEEN | 区间 | `age BETWEEN ?1 AND ?2` | +| IS_NULL | 为空 | `name IS NULL` | +| IS_NOT_NULL | 非空 | `name IS NOT NULL` | + +### 4. 使用 SearchRequest 从 HTTP 请求自动解析 + +```java +// 在 Controller 中使用 SearchRequest,自动从 URL 参数解析 filter 和 sort +@GetMapping("/users") +public MultiResponse list() { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.addFilter("status", "active"); // 追加服务端固定条件 + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page.getContent()); +} +``` + +前端通过 URL 参数传递动态条件: +- `?filter=eyJuYW1lIjpbIuW8oCJdfQ==`(Base64 编码的 JSON:`{"name":["张"]}`) +- `?sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9`(Base64 编码的 JSON:`{"createTime":"descend"}`) + +### 5. OR / AND 组合条件 + +```java +PageRequest request = PageRequest.of(0, 20); + +// OR 条件:name = '张三' OR name = '李四' +request.orFilters( + new Filter("name", "张三"), + new Filter("name", "李四") +); + +// AND 条件组 +request.andFilter( + new Filter("age", Relation.GTE, 18), + new Filter("status", "active") +); +``` + +## 使用实例 + +### 完整示例:用户列表查询 + +```java +@Service +public class UserQueryService { + + @Resource + private UserEntityRepository userRepository; + + /** + * 基础动态查询 - 自动 Example/HQL + */ + public Page findUsers(String name, Integer minAge, String status) { + PageRequest request = PageRequest.of(0, 20); + if (name != null) { + request.addFilter("name", Relation.LIKE, name); + } + if (minAge != null) { + request.addFilter("age", Relation.GTE, minAge); + } + if (status != null) { + request.addFilter("status", status); + } + return userRepository.findAll(request); + } + + /** + * 复杂 HQL 查询 - 带排序 + */ + public Page findActiveUsersWithSort() { + PageRequest request = PageRequest.of(0, 20, Sort.by("createTime").descending()); + request.addFilter("status", "active"); + request.addFilter("age", Relation.BETWEEN, 18, 65); + return userRepository.pageRequest(request); + } + + /** + * 前端驱动的通用查询接口 + */ + public Page searchFromHttpRequest() { + SearchRequest searchRequest = new SearchRequest(); + // 追加服务端安全条件,防止越权查询 + searchRequest.addFilter("deleted", false); + return userRepository.searchRequest(searchRequest); + } +} +``` + +### 内部工作原理 + +`FastRepository.findAll(PageRequest)` 的执行流程: + +1. 检查 `request.hasFilter()` — 无过滤条件时直接委托给 Spring Data 的标准 `findAll(PageRequest)` +2. 有过滤条件时,通过 `ExampleBuilder` 尝试构建 `Example` 对象(仅处理等值匹配的属性) +3. 将 Example 与 PageRequest 一起传入 `findAll(Example, Pageable)` 执行查询 + +`FastRepository.pageRequest(PageRequest)` 的执行流程: + +1. 通过 `DynamicSQLBuilder` 根据 Filter 列表动态构建 HQL 语句和 COUNT 语句 +2. 所有值通过参数化绑定(`?1`, `?2`...),防止 SQL 注入 +3. 调用 `dynamicPageQuery(hql, countHql, request, params)` 执行分页查询 diff --git a/docs/capabilities/springboot-starter-flow/workflow-engine.md b/docs/capabilities/springboot-starter-flow/workflow-engine.md new file mode 100644 index 000000000..0350e651d --- /dev/null +++ b/docs/capabilities/springboot-starter-flow/workflow-engine.md @@ -0,0 +1,154 @@ +--- +name: springboot-starter-flow/workflow-engine +module: springboot-starter-flow +description: 工作流引擎,支持流程定义、节点流转、审批、委托、会签和数据快照 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter-flow" +symbols: + - FlowWork + - FlowNode + - FlowNodeService + - FlowWorkBuilder + - FlowStartService + - FlowApprovalEvent +content_hash: 0deb7127074232e52335663402b08ead2e42469e6813e555a3dc06e97362fd04 +--- + +## 解决什么问题 + +在企业级应用中,审批流程(如请假、报销、采购等)是高频且复杂的业务场景。传统硬编码方式存在以下痛点: + +- **流程变更成本高**:每次调整审批节点或流转规则都需要修改代码并重新部署 +- **审批模式单一**:难以同时支持会签、或签、传阅、退回、委托等多种审批形态 +- **状态追踪困难**:流程实例的当前节点、历史审批记录、数据快照缺乏统一管理 +- **事件通知分散**:审批通过、拒绝、转办等状态变更需要在多处手动触发通知逻辑 + +`workflow-engine` 提供了一套轻量级的嵌入式工作流引擎,将流程定义与业务代码解耦。通过 Builder 模式声明式构建流程,引擎自动处理节点流转、操作者匹配、数据快照绑定和事件推送,让开发者专注于业务逻辑本身。 + +## 如何使用 + +### 核心概念 + +| 概念 | 类 | 说明 | +|------|-----|------| +| 流程定义 | `FlowWork` | 描述一个完整的审批流程,包含节点集合、关系集合、启用状态等 | +| 流程节点 | `FlowNode` | 流程中的单个环节(开始/审批/传阅/结束),配置审批类型、操作者匹配器、超时时间等 | +| 节点关系 | `FlowRelation` | 定义节点之间的流转规则,支持条件触发(`OutTrigger`)和退回标记 | +| 流程构建器 | `FlowWorkBuilder` | 链式 API 构建 `FlowWork`,内部自动校验节点和关系的完整性 | +| 发起服务 | `FlowStartService` | 发起新流程实例,创建流程备份、数据快照和首条待办记录 | +| 节点服务 | `FlowNodeService` | 驱动节点流转:加载下一节点、匹配操作者、创建审批记录、处理传阅跳过 | +| 审批事件 | `FlowApprovalEvent` | 同步事件,覆盖创建/待办/通过/拒绝/转办/撤回/完成/催办/抄送/退回等 14 种状态 | +| 流程操作者 | `IFlowOperator` | 用户接口,支持委托 (`entrustOperator()`) 和管理员强制干预 | + +### 构建流程 + +使用 `FlowWorkBuilder` 声明式定义流程: + +```java +FlowWork work = FlowWorkBuilder.builder(operator) + .title("请假审批流程") + .description("员工请假审批") + .skipIfSameApprover(true) // 相同审批人自动跳过 + .postponedMax(3) // 最大延期次数 + .nodes() + .node("发起", "start", "startView", ApprovalType.UN_SIGN, startMatcher) + .node("部门审批", "dept_approve", "approveView", ApprovalType.SIGN, deptMatcher, true, false) + .node("HR备案", "hr_record", "hrView", ApprovalType.UN_SIGN, hrMatcher) + .node("结束", "over", "overView", ApprovalType.UN_SIGN, endMatcher) + .relations() + .relation("发起->部门审批", "start", "dept_approve") + .relation("部门审批->HR备案", "dept_approve", "hr_record") + .relation("HR备案->结束", "hr_record", "over") + .build(); +``` + +构建时 `build()` 会自动调用 `enable()` → `verify()`,校验以下内容: +- 必须存在 `start` 和 `over` 节点及其关联关系 +- 节点 code 不能重复 +- 每个节点的 `titleGenerator` 和 `operatorMatcher` 不能为空 + +### 发起流程 + +通过 `FlowStartService` 发起流程实例: + +```java +FlowStartService startService = new FlowStartService( + workCode, operator, bindData, advice, repositoryHolder); +FlowResult result = startService.startFlow(); +``` + +启动过程依次执行:加载并校验流程定义 → 创建版本快照(`FlowBackup`)→ 保存流程实例 → 序列化绑定数据 → 从 start 节点创建待办记录 → 推送 `FlowApprovalEvent`。 + +### 监听审批事件 + +实现 `IHandler` 即可接收所有审批状态变更: + +```java +@Component +public class LeaveApprovalHandler implements IHandler { + @Override + public void handle(FlowApprovalEvent event) { + if (event.isTodo()) { + // 发送待办通知 + } + if (event.isPass()) { + // 审批通过处理 + } + if (event.isFinish()) { + // 流程结束归档 + } + } +} +``` + +事件通过框架的 `EventPusher` 同步推送,在同一个事务内完成。 + +## 使用实例 + +以下示例展示一个完整的请假审批流程定义与发起: + +```java +// 1. 定义操作者匹配器 +OperatorMatcher startMatcher = OperatorMatcher.any(); // 发起人 +OperatorMatcher deptMatcher = OperatorMatcher.script( // 部门负责人 + "session.bindData.deptLeaderId"); +OperatorMatcher hrMatcher = OperatorMatcher.script( // HR + "session.bindData.hrUserId"); +OperatorMatcher endMatcher = OperatorMatcher.any(); + +// 2. 构建流程 +FlowWork leaveFlow = FlowWorkBuilder.builder(currentUser) + .title("请假审批") + .skipIfSameApprover(true) + .nodes() + .node("发起申请", "start", "leaveStart", ApprovalType.UN_SIGN, startMatcher) + .node("部门审批", "dept", "leaveApprove", ApprovalType.SIGN, deptMatcher, true, false) + .node("HR确认", "hr", "leaveHr", ApprovalType.UN_SIGN, hrMatcher) + .node("结束", "over", "leaveEnd", ApprovalType.UN_SIGN, endMatcher) + .relations() + .relation("提交", "start", "dept") + .relation("批准", "dept", "hr") + .relation("确认", "hr", "over") + .build(); + +// 3. 准备业务数据 +LeaveRequest leave = new LeaveRequest(); +leave.setUserId(currentUser.getUserId()); +leave.setDays(3); +leave.setDeptLeaderId(deptLeaderId); +leave.setHrUserId(hrUserId); + +// 4. 发起流程 +FlowStartService service = new FlowStartService( + leaveFlow.getCode(), currentUser, leave, "申请年假3天", repoHolder); +FlowResult result = service.startFlow(); + +// 5. 获取待办记录 +List records = result.getRecords(); +records.forEach(r -> System.out.println( + "待办: " + r.getTitle() + " -> " + r.getCurrentOperator().getName())); +``` + +当部门审批人点击通过后,引擎自动流转至 HR 确认节点;若设置了退回关系,审批人可选择退回至指定节点。全程数据快照通过 `BindDataSnapshot` 持久化,确保审批过程中业务数据不可变。 diff --git a/docs/capabilities/springboot-starter-script/groovy-runtime.md b/docs/capabilities/springboot-starter-script/groovy-runtime.md new file mode 100644 index 000000000..ac51d1aca --- /dev/null +++ b/docs/capabilities/springboot-starter-script/groovy-runtime.md @@ -0,0 +1,164 @@ +--- +name: springboot-starter-script/groovy-runtime +module: springboot-starter-script +description: Groovy 脚本运行时引擎,支持运行时编译、LRU 缓存、热更新和 REST API +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter-script" +symbols: + - GroovyScriptRuntime + - GroovyScript + - GroovyScriptCacheContext + - GroovyScriptRuntimeContext + - GroovyScriptController +content_hash: deddb103b5a58509ca7c08ace4195760e618f5b07782cfcb857fc1326d4ae526 +--- + +## 解决什么问题 + +在企业应用中,部分业务逻辑需要频繁调整但不希望每次都经历"改代码→编译→部署"的完整周期。典型场景包括: + +- **动态规则计算**:促销折扣、费率计算、风控阈值等业务规则经常变化 +- **自定义报表/导出**:不同租户或部门的报表格式差异大,用脚本比硬编码更灵活 +- **流程条件表达式**:工作流节点的条件判断、操作者匹配等需要运行时求值 +- **临时数据处理**:运维脚本、数据修复等一次性任务 + +`groovy-runtime` 提供了嵌入式的 Groovy 脚本执行引擎,具备以下特性: + +- **运行时编译执行**:无需重启应用即可加载和执行新脚本 +- **LRU 编译缓存**:基于 SHA256 指纹缓存已编译的 Script 对象,避免重复编译开销 +- **三级存储**:内存缓存 → 临时存储 → 持久化仓库,兼顾性能和可靠性 +- **事务集成**:支持只读(READONLY)和提交(COMMIT)两种事务模式 +- **REST API**:内置 Controller 提供脚本编译、查询、保存接口,方便管理端对接 + +## 如何使用 + +### 核心组件 + +| 组件 | 职责 | +|------|------| +| `GroovyScriptRuntime` | 底层执行引擎:GroovyShell 封装、LRU 编译缓存、invoke/run 方法、事务控制 | +| `GroovyScriptRuntimeContext` | Runtime 的单例上下文,从配置读取 `shellMaxCacheSize` 初始化 | +| `GroovyScript` | 脚本领域对象:封装 key/script/method/returnType/binds 等元信息,提供 compile/run/invoke 快捷方法 | +| `GroovyScriptCacheContext` | 脚本对象的 LRU 缓存(最大 10240 条),三级查找:缓存 → 临时存储 → Repository | +| `GroovyScriptController` | REST 端点 `/api/groovy-script/*`,提供 compile/getScript/getMetadata/save 接口 | + +### 直接执行脚本 + +通过 `GroovyScriptRuntimeContext` 单例直接运行脚本: + +```java +GroovyScriptRuntimeContext ctx = GroovyScriptRuntimeContext.getInstance(); + +// 简单执行 +String result = ctx.run("'hello ' + name", String.class, + TransactionMode.DEFAULT, Map.of("name", "world")); + +// 调用脚本中的函数 +int sum = ctx.invoke("add", "def add(a,b){ a + b }", int.class, + TransactionMode.DEFAULT, null, 3, 5); +``` + +### 使用 GroovyScript 对象 + +对于需要持久化和复用的脚本,使用 `GroovyScript` 封装: + +```java +GroovyScript script = GroovyScript.builder("discount_calc") + .script(""" + def calculate(price, rate) { + return price * rate + } + """) + .method("calculate") + .returnType(BigDecimal.class) + .description("折扣计算脚本") + .tag("pricing") + .build(); + +// 编译并缓存 +script.compile(true); + +// 执行 +BigDecimal result = script.invoke(Map.of(), new BigDecimal("100"), new BigDecimal("0.85")); + +// 持久化到仓库 +script.save(); +``` + +### 事务模式 + +`TransactionMode` 支持三种模式: + +| 模式 | 行为 | +|------|------| +| `DEFAULT` | 不参与事务,由调用方自行控制 | +| `READONLY` | 在只读事务中执行,适合查询类脚本 | +| `COMMIT` | 在可提交事务中执行,适合写入类脚本 | + +### REST API + +内置的 `GroovyScriptController` 提供以下端点: + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/groovy-script/compile` | 编译脚本(body: `{script, cache}`) | +| GET | `/api/groovy-script/getScript?key=` | 获取脚本内容 | +| GET | `/api/groovy-script/getMetadata?key=` | 获取脚本元数据 | +| POST | `/api/groovy-script/save` | 保存脚本(编译+持久化) | + +## 使用实例 + +以下示例展示一个完整的动态折扣计算场景: + +```java +// 1. 创建并保存折扣计算脚本 +GroovyScript discountScript = GroovyScript.builder("member_discount") + .script(""" + def calcDiscount(amount, memberLevel) { + switch(memberLevel) { + case 'GOLD': return amount * 0.8 + case 'SILVER': return amount * 0.9 + default: return amount + } + } + """) + .method("calcDiscount") + .returnType(BigDecimal.class) + .description("会员折扣计算") + .typeOne("pricing") + .typeTwo("discount") + .build(); + +// 编译并持久化 +discountScript.compile(true); +discountScript.save(); + +// 2. 后续使用时直接从缓存获取 +GroovyScript cached = GroovyScriptCacheContext.getInstance() + .getGroovyScript("member_discount"); + +// 3. 执行业务计算 +BigDecimal originalAmount = new BigDecimal("500"); +BigDecimal finalPrice = cached.invoke( + TransactionMode.READONLY, null, originalAmount, "GOLD"); +// finalPrice = 400.0 + +// 4. 热更新:修改脚本后重新保存即可生效 +cached.setScript(""" + def calcDiscount(amount, memberLevel) { + switch(memberLevel) { + case 'DIAMOND': return amount * 0.7 + case 'GOLD': return amount * 0.8 + case 'SILVER': return amount * 0.9 + default: return amount + } + } +"""); +cached.compile(true); +cached.save(); +// 下次 invoke 自动使用新版本 +``` + +缓存机制说明:`GroovyScriptRuntime` 内部以脚本内容的 SHA256 作为缓存 key,相同内容的脚本不会重复编译。当脚本内容变更后,SHA256 值改变,自动触发重新编译。LRU 策略确保内存占用可控,默认上限可通过配置项调整。 diff --git a/docs/capabilities/springboot-starter-security/auth-gateway.md b/docs/capabilities/springboot-starter-security/auth-gateway.md new file mode 100644 index 000000000..f3161fd1d --- /dev/null +++ b/docs/capabilities/springboot-starter-security/auth-gateway.md @@ -0,0 +1,201 @@ +--- +name: springboot-starter-security/auth-gateway +module: springboot-starter-security +description: 安全认证网关,支持 JWT 无状态认证和 Redis 有状态认证两种模式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter-security" +symbols: + - TokenGateway + - TokenContext + - Token + - MyLoginFilter + - MyAuthenticationFilter +content_hash: f387cbcf0fa1de5a7c51bf0d2afb16e342b53d42ad2031f6741cb6db4e4e2625 +--- + +## 解决什么问题 + +在企业级 Web 应用中,用户认证是安全体系的第一道防线。Spring Security 原生提供的表单登录和 Session 机制不适合前后端分离架构,开发者通常需要自行实现以下能力: + +- **Token 生命周期管理**:创建、解析、过期校验、自动续期 +- **多认证模式切换**:JWT 无状态模式适合微服务和移动端,Redis 有状态模式适合需要服务端主动踢人的管理后台 +- **登录流程定制**:在认证前后插入自定义逻辑(验证码校验、登录日志、额外业务数据注入等) +- **统一 JSON 响应**:认证成功/失败均返回标准 `Response` 格式,而非 Spring Security 默认的重定向或 HTML 页面 + +auth-gateway 将上述能力封装为开箱即用的安全网关层,通过 `TokenGateway` 策略接口抽象 Token 的创建与解析,配合 `MyLoginFilter`(登录拦截)和 `MyAuthenticationFilter`(请求鉴权拦截)两个 Servlet Filter,实现完整的认证闭环。开发者只需通过配置选择 JWT 或 Redis 模式,即可零代码获得生产级认证能力。 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-security + +``` + +### 2. 选择认证模式 + +通过 `application.properties` 启用其中一种模式(二选一): + +**JWT 无状态模式:** + +```properties +codingapi.security.jwt.enable=true +codingapi.security.jwt.secret-key=your-secret-key-must-be-at-least-32-chars +codingapi.security.jwt.valid-time=900000 # Token 有效期 15 分钟(毫秒) +codingapi.security.jwt.rest-time=600000 # 10 分钟后自动续期(毫秒) +``` + +**Redis 有状态模式:** + +```properties +codingapi.security.redis.enable=true +codingapi.security.redis.valid-time=900000 +codingapi.security.redis.rest-time=600000 +``` + +### 3. 配置安全策略 + +```properties +# 需要认证的 URL 模式(逗号分隔) +codingapi.security.authenticated-urls=/api/** + +# 免认证 URL 模式 +codingapi.security.ignore-urls=/open/**,/#/** + +# 登录接口地址 +codingapi.security.login-processing-url=/user/login + +# 登出接口地址 +codingapi.security.logout-url=/user/logout +``` + +### 4. 核心 API + +| 类 | 说明 | +|---|---| +| `TokenGateway` | Token 策略接口,提供 `create()` 和 `parser()` 方法。框架根据配置自动注入 `JWTTokenGatewayImpl` 或 `RedisTokenGatewayImpl` | +| `Token` | Token 数据对象,包含 username、authorities、extra、expireTime、remindTime 等字段,支持 `verify()` 过期校验和 `canRestToken()` 自动续期判断 | +| `TokenContext` | 线程安全的 Token 上下文工具类。`TokenContext.current()` 获取当前登录用户的 Token;`TokenContext.pushExtra()` / `getExtra()` 传递额外业务数据 | +| `SecurityLoginHandler` | 登录扩展点接口。实现 `preHandle()` 可在认证前做自定义校验;实现 `postHandle()` 可定制登录成功响应 | +| `AuthenticationTokenFilter` | 鉴权后扩展点接口。每次请求通过 Token 验证后回调,可用于刷新用户缓存等场景 | + +### 5. 自定义 UserDetailsService + +框架默认提供内存用户(admin/user),生产环境需自行实现: + +```java +@Service +public class CustomUserDetailsService implements UserDetailsService { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 从数据库加载用户信息 + } +} +``` + +## 使用实例 + +### 示例 1:自定义登录处理器 + +在登录前校验验证码,登录后返回额外的用户信息: + +```java +@Component +public class CustomSecurityLoginHandler implements SecurityLoginHandler { + + @Override + public void preHandle(HttpServletRequest request, HttpServletResponse response, + LoginRequest loginRequest) throws Exception { + // 校验验证码 + String captcha = loginRequest.getString("captcha"); + if (captcha == null || !captchaService.verify(captcha)) { + throw new AuthenticationServiceException("验证码错误"); + } + } + + @Override + public LoginResponse postHandle(HttpServletRequest request, HttpServletResponse response, + LoginRequest loginRequest, UserDetails user, Token token) { + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setToken(token.getToken()); + loginResponse.setUsername(token.getUsername()); + loginResponse.setAuthorities(token.getAuthorities()); + // 附加用户头像等信息 + Map data = new HashMap<>(); + data.put("avatar", "/static/avatar/default.png"); + loginResponse.setData(data); + return loginResponse; + } +} +``` + +### 示例 2:在业务代码中获取当前用户 + +```java +@RestController +@RequestMapping("/api/profile") +public class ProfileController { + + @GetMapping + public SingleResponse> getProfile() { + // 从 SecurityContext 获取当前 Token + Token token = TokenContext.current(); + String username = token.getUsername(); + List roles = token.getAuthorities(); + + // 解析 extra 中的自定义数据 + UserInfo extra = token.parseExtra(UserInfo.class); + + Map profile = new HashMap<>(); + profile.put("username", username); + profile.put("roles", roles); + profile.put("department", extra != null ? extra.getDepartment() : null); + return SingleResponse.of(profile); + } +} +``` + +### 示例 3:鉴权后刷新用户缓存 + +```java +@Component +public class CacheRefreshFilter implements AuthenticationTokenFilter { + + private final UserService userService; + + public CacheRefreshFilter(UserService userService) { + this.userService = userService; + } + + @Override + public void doFilter(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + Token token = TokenContext.current(); + // 每次认证通过后刷新用户权限缓存 + userService.refreshPermissionCache(token.getUsername()); + } +} +``` + +### 前端调用示例 + +```javascript +// 登录 +const res = await fetch('/user/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'admin', password: 'admin' }) +}); +const { data } = await res.json(); +// data.token → 保存 Token + +// 携带 Token 访问受保护接口 +const profile = await fetch('/api/profile', { + headers: { 'Authorization': data.token } +}); +``` diff --git a/docs/capabilities/springboot-starter/domain-proxy.md b/docs/capabilities/springboot-starter/domain-proxy.md new file mode 100644 index 000000000..93fc3cc2a --- /dev/null +++ b/docs/capabilities/springboot-starter/domain-proxy.md @@ -0,0 +1,134 @@ +--- +name: springboot-starter/domain-proxy +module: springboot-starter +description: 领域实体变更代理,通过 CGLIB 代理拦截实体字段变更并自动推送领域事件 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - DomainProxyFactory + - DomainChangeInterceptor + - IDomain + - DomainCreateEvent + - DomainChangeEvent + - DomainDeleteEvent + - DomainPersistEvent +content_hash: 94b780644f3676986d6041a009a7bf58e42543180ab2f6e1c0349244ea62032e +--- + +## 解决什么问题 + +在 DDD(领域驱动设计)实践中,领域实体的状态变更需要产生对应的领域事件(创建、变更、删除、持久化),以便下游处理器执行副作用操作(如发送通知、更新缓存、触发工作流等)。然而手动在每个 setter 方法中编写事件推送代码存在以下痛点: + +1. **侵入性强**:每个实体类的修改方法都需要显式调用 `EventPusher.push()`,业务逻辑与事件机制耦合。 +2. **容易遗漏**:开发者可能忘记在某些字段变更后推送事件,导致数据不一致。 +3. **变更追踪困难**:无法自动获取字段的旧值与新值,手动记录增加出错风险。 +4. **嵌套对象变更不可见**:当实体包含子对象时,子对象字段的变更更难被感知和追踪。 + +`domain-proxy` 能力通过 CGLIB 动态代理透明地拦截实体方法调用,自动比较字段值变化并推送 `DomainChangeEvent`,同时提供完整的实体生命周期事件(创建、持久化、删除),让领域事件的发布对业务代码零侵入。 + +## 如何使用 + +### 核心组件 + +| 组件 | 说明 | +|------|------| +| `IDomain` | 领域实体标记接口,提供 `persist()` 和 `delete()` 默认方法,分别推送 `DomainPersistEvent` 和 `DomainDeleteEvent` | +| `DomainProxyFactory` | 静态工厂类,通过 `create(Class, Object... args)` 创建代理实例,同时自动推送 `DomainCreateEvent` | +| `DomainChangeInterceptor` | CGLIB `MethodInterceptor` 实现,拦截带参数的方法调用,对比执行前后字段值差异,自动推送 `DomainChangeEvent` | +| `DomainEvent` | 领域事件基类,携带实体引用、实体类型和时间戳 | +| `DomainCreateEvent` | 实体创建事件,由 `DomainProxyFactory.create()` 自动推送 | +| `DomainChangeEvent` | 实体字段变更事件,包含 `fieldName`、`oldValue`、`newValue` | +| `DomainDeleteEvent` | 实体删除事件,通过 `IDomain.delete()` 推送 | +| `DomainPersistEvent` | 实体持久化事件,通过 `IDomain.persist()` 推送 | + +### 使用步骤 + +1. **定义领域实体**:创建实体类并实现 `IDomain` 接口。实体类需要有公开的构造函数供代理工厂反射调用。 + +2. **通过工厂创建实例**:使用 `DomainProxyFactory.create(EntityClass.class, constructorArgs...)` 代替 `new` 关键字创建实体。工厂会自动创建 CGLIB 代理并推送 `DomainCreateEvent`。 + +3. **正常调用业务方法**:对代理对象调用任何带参数的方法后,拦截器会自动比较所有字段(包括嵌套对象)的值变化,若有变更则推送 `DomainChangeEvent`。 + +4. **触发生命周期事件**:在适当时机调用 `entity.persist()` 推送持久化事件,或调用 `entity.delete()` 推送删除事件。 + +5. **注册事件处理器**:编写 `IHandler`、`IHandler` 等处理器 Bean,框架会自动扫描并注册。 + +### 注意事项 + +- 代理仅拦截**带参数**的方法调用,无参方法(如 getter)不会触发变更检测。 +- 支持基本类型(String、数值、布尔、枚举等)的直接比较,以及嵌套对象的递归字段比较。 +- 事件推送通过 `EventPusher` 进入框架的事件系统,遵循同步/异步分发和循环检测机制。 + +## 使用实例 + +### 定义领域实体 + +```java +public class User implements IDomain { + + @Getter + private final long id; + + @Getter + private String name; + + @Getter + private String email; + + public User(String name, String email) { + this.id = System.currentTimeMillis(); + this.name = name; + this.email = email; + } + + public void changeName(String name) { + this.name = name; + } + + public void changeEmail(String email) { + this.email = email; + } +} +``` + +### 创建代理并触发事件 + +```java +// 通过工厂创建代理实例,自动推送 DomainCreateEvent +User user = DomainProxyFactory.create(User.class, "张三", "zhangsan@example.com"); + +// 调用带参方法后,拦截器自动检测字段变更并推送 DomainChangeEvent +user.changeName("李四"); // → DomainChangeEvent(fieldName="name", oldValue="张三", newValue="李四") +user.changeEmail("li@example.com"); // → DomainChangeEvent(fieldName="email", ...) + +// 手动触发持久化事件 +user.persist(); // → DomainPersistEvent + +// 手动触发删除事件 +user.delete(); // → DomainDeleteEvent +``` + +### 编写事件处理器 + +```java +@Component +public class UserChangeHandler implements IHandler { + + @Override + public void handler(DomainChangeEvent event) { + log.info("用户字段变更: {} = {} → {}", + event.getFieldName(), event.getOldValue(), event.getNewValue()); + } +} + +@Component +public class UserCreateHandler implements IHandler { + + @Override + public void handler(DomainCreateEvent event) { + log.info("新用户创建: {}", event.getEntity().getClass().getSimpleName()); + } +} +``` diff --git a/docs/capabilities/springboot-starter/event-system.md b/docs/capabilities/springboot-starter/event-system.md new file mode 100644 index 000000000..1c5256f7f --- /dev/null +++ b/docs/capabilities/springboot-starter/event-system.md @@ -0,0 +1,283 @@ +--- +name: springboot-starter/event-system +module: springboot-starter +description: 事件发布-订阅系统,支持同步/异步事件、事件循环检测、事务事件 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - EventPusher + - IEvent + - IHandler + - ISyncEvent + - IAsyncEvent + - DomainEventContext + - ApplicationHandlerUtils + - SpringHandlerConfiguration + - EventTraceContext + - EventStackContext +content_hash: f911f56fe4d24daddc91a560d340759aa3c208f085ebc0aca84954d8b8d4ae9a +--- + +## 解决什么问题 + +在 DDD(领域驱动设计)架构中,领域事件是实现聚合间解耦、触发副作用的核心机制。然而在实际开发中,事件系统面临以下痛点: + +1. **事件与主业务事务的边界模糊**:开发者容易将事件处理与主业务强绑定,导致事务范围膨胀、性能下降。框架需要明确"事件对主业务可成功可失败"的设计原则。 +2. **同步与异步事件的区分困难**:部分场景要求事件在同一事务内同步执行(如数据校验),部分场景则需异步解耦(如发送通知)。手动管理线程池和执行模式增加复杂度。 +3. **事件循环调用风险**:当 Handler A 处理 EventX 时又推送了 EventY,而 EventY 的 Handler 又推送了 EventX,就会形成无限循环。缺乏自动检测机制会导致栈溢出或系统挂起。 +4. **多 Handler 执行的异常隔离**:同一事件可能被多个 Handler 订阅,某个 Handler 抛异常不应影响其他 Handler 的执行,同时需要收集所有异常统一上报。 +5. **Handler 注册与排序繁琐**:手动注册处理器、维护执行顺序的代码分散且易出错。 + +本模块提供了一套完整的发布-订阅事件基础设施,通过 `EventPusher` 统一入口、`IEvent` 类型体系区分同步/异步、`EventTraceContext` + `EventStackContext` 实现事件链路追踪与循环检测、`ApplicationHandlerUtils` 自动匹配并按序分发 Handler,以及可选的事务提交后触发模式(`SpringTransactionEventHandler`)。 + +## 如何使用 + +### 核心接口 + +| 接口/类 | 说明 | +|---------|------| +| `IEvent` | 事件标记接口,继承 `Serializable`。默认视为同步事件 | +| `ISyncEvent` | 同步事件标记接口,继承 `IEvent`。事件在当前线程同步执行 | +| `IAsyncEvent` | 异步事件标记接口,继承 `IEvent`。事件在线程池中异步执行 | +| `IHandler` | 事件处理器接口,泛型 `T` 指定订阅的事件类型 | +| `EventPusher` | 事件推送静态工具类,是发布事件的唯一入口 | +| `@Handler` | 注解,标记一个类为事件处理器 Bean(也可使用 `@Component`/`@Service`) | + +### IHandler 接口方法 + +```java +public interface IHandler { + // 执行顺序,数值越小越先执行,默认为 0 + default int order() { return 0; } + + // 事件处理逻辑 + void handler(T event); + + // 异常回调,默认重新抛出异常(阻止后续 Handler 执行) + // 可覆盖此方法实现异常吞没或自定义处理 + default void error(Exception exception) throws Exception { + throw exception; + } +} +``` + +### 事件推送 + +```java +// 推送事件(自动根据 IEvent 子接口判断同步/异步) +EventPusher.push(new MyEvent(data)); + +// 显式声明允许循环事件(跳过循环检测) +EventPusher.push(new MyEvent(data), true); +``` + +事件类型的判定规则: +- 实现 `IAsyncEvent` → 异步执行(线程池) +- 实现 `ISyncEvent` → 同步执行(当前线程) +- 仅实现 `IEvent` → 默认同步执行 + +### 配置项 + +```properties +# 启用事务事件模式(事件在事务提交后触发) +codingapi.framework.event.transaction.enable=true + +# 异步事件线程池大小(默认值见 PropertiesContext) +codingapi.framework.handler-thread-pool-size=20 +``` + +### 事件分发模式 + +框架通过 `SpringHandlerConfiguration` 自动装配事件分发器: + +- **默认模式**(`SpringDefaultEventHandler`):使用 Spring `@EventListener` 即时触发,同步事件在当前线程执行,异步事件提交到固定大小线程池。 +- **事务模式**(`SpringTransactionEventHandler`):当配置 `codingapi.framework.event.transaction.enable=true` 时激活,使用 `@TransactionalEventListener(phase = AFTER_COMMIT)` 确保事件在事务提交后才触发,避免读取到未提交的数据。设置 `fallbackExecution = true` 保证无事务上下文时也能正常执行。 + +两种模式互斥,事务模式优先(`@ConditionalOnMissingBean` 兜底默认模式)。 + +### 事件循环检测 + +框架通过 `EventTraceContext` 和 `EventStackContext` 实现同一条事件链路内的循环检测: + +1. 每次推送事件时生成/复用 traceId,将事件类压入该 traceId 对应的事件栈。 +2. 若同一 traceId 下出现相同事件类,立即抛出 `EventLoopException`,并附带完整的事件调用栈信息。 +3. 事件处理完成后自动清理 traceId 和事件栈。 +4. 若确实需要允许循环(如状态机重试),可调用 `EventPusher.push(event, true)` 跳过检测。 + +### Handler 异常处理策略 + +当某个 Handler 抛出异常时: +1. 若异常为 `EventLoopException`,直接向上抛出,终止后续所有 Handler。 +2. 否则调用该 Handler 的 `error(exception)` 回调。 +3. 若 `error()` 也抛出异常,标记为"有异常"并继续执行下一个 Handler。 +4. 所有 Handler 执行完毕后,若存在异常,统一包装为 `EventException` 抛出。 + +## 使用实例 + +### 1. 定义事件 + +```java +package com.example.domain.order.event; + +import com.codingapi.springboot.framework.event.ISyncEvent; +import lombok.Getter; +import lombok.AllArgsConstructor; + +/** + * 订单创建事件(同步) + */ +@Getter +@AllArgsConstructor +public class OrderCreatedEvent implements ISyncEvent { + private final String orderId; + private final String userId; + private final long amount; +} +``` + +```java +package com.example.domain.order.event; + +import com.codingapi.springboot.framework.event.IAsyncEvent; +import lombok.Getter; +import lombok.AllArgsConstructor; + +/** + * 订单通知事件(异步) + */ +@Getter +@AllArgsConstructor +public class OrderNotifyEvent implements IAsyncEvent { + private final String orderId; + private final String message; +} +``` + +### 2. 编写事件处理器 + +```java +package com.example.handler; + +import com.codingapi.springboot.framework.event.IHandler; +import com.example.domain.order.event.OrderCreatedEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 订单创建后扣减库存(优先执行) + */ +@Slf4j +@Service +public class InventoryDeductHandler implements IHandler { + + @Override + public int order() { + return 1; // 最先执行 + } + + @Override + public void handler(OrderCreatedEvent event) { + log.info("扣减库存, orderId={}, amount={}", event.getOrderId(), event.getAmount()); + // inventoryService.deduct(event.getOrderId(), event.getAmount()); + } +} +``` + +```java +package com.example.handler; + +import com.codingapi.springboot.framework.event.IHandler; +import com.example.domain.order.event.OrderCreatedEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 订单创建后记录积分(其次执行) + */ +@Slf4j +@Service +public class PointsRecordHandler implements IHandler { + + @Override + public int order() { + return 2; + } + + @Override + public void handler(OrderCreatedEvent event) { + log.info("记录积分, userId={}", event.getUserId()); + // pointsService.record(event.getUserId(), event.getAmount()); + } + + @Override + public void error(Exception exception) { + // 积分记录失败不影响其他 Handler,仅记录日志 + log.error("积分记录失败, 但不阻断流程", exception); + } +} +``` + +```java +package com.example.handler; + +import com.codingapi.springboot.framework.event.IHandler; +import com.example.domain.order.event.OrderNotifyEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 异步发送订单通知 + */ +@Slf4j +@Service +public class OrderNotifyHandler implements IHandler { + + @Override + public void handler(OrderNotifyEvent event) { + log.info("发送通知, orderId={}, message={}", event.getOrderId(), event.getMessage()); + // notificationService.send(event.getOrderId(), event.getMessage()); + } +} +``` + +### 3. 在领域服务中推送事件 + +```java +package com.example.domain.order.service; + +import com.codingapi.springboot.framework.event.EventPusher; +import com.example.domain.order.event.OrderCreatedEvent; +import com.example.domain.order.event.OrderNotifyEvent; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class OrderService { + + @Transactional + public String createOrder(String userId, long amount) { + String orderId = generateOrderId(); + // ... 持久化订单实体 ... + + // 同步事件:在当前事务内执行库存扣减、积分记录 + EventPusher.push(new OrderCreatedEvent(orderId, userId, amount)); + + // 异步事件:事务外异步发送通知 + EventPusher.push(new OrderNotifyEvent(orderId, "您的订单已创建")); + + return orderId; + } +} +``` + +### 4. 启用事务事件模式(可选) + +若希望所有事件都在事务提交后再触发(避免 Handler 读到未提交数据),在 `application.properties` 中添加: + +```properties +codingapi.framework.event.transaction.enable=true +``` + +此时 `SpringTransactionEventHandler` 替代默认的 `SpringDefaultEventHandler`,所有事件将在 `AFTER_COMMIT` 阶段触发。 diff --git a/docs/capabilities/springboot-starter/exception-handling.md b/docs/capabilities/springboot-starter/exception-handling.md new file mode 100644 index 000000000..fa5dd32e4 --- /dev/null +++ b/docs/capabilities/springboot-starter/exception-handling.md @@ -0,0 +1,142 @@ +--- +name: springboot-starter/exception-handling +module: springboot-starter +description: 全局异常处理,统一拦截 Controller 层异常并返回标准 Response +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - ExceptionConfiguration + - BasicHandlerExceptionResolverConfiguration +content_hash: e2cf2bcf396be0bf0a00caf77f61e89c1f00b90213d98196d70971b61a7e0f54 +--- + +## 解决什么问题 + +在 Spring MVC 项目中,Controller 层抛出的异常如果不做统一处理,会导致以下问题: + +- **响应格式不一致**:不同接口返回的错误结构各异(有的返回 HTML 错误页、有的返回纯文本、有的返回自定义 JSON),前端难以统一解析。 +- **错误码缺失**:原生异常只有 message,没有业务错误码(errCode),前端无法根据错误码做多语言提示或差异化逻辑。 +- **国际化困难**:错误信息硬编码在代码中,无法根据用户 Locale 动态切换语言。 +- **敏感信息泄露**:未捕获的异常可能将堆栈信息直接暴露给客户端。 + +`springboot-starter` 的异常处理能力通过 `HandlerExceptionResolver` 机制,在 Servlet 层面统一拦截所有 Controller 异常,将其转换为标准的 `{success, errCode, errMessage}` JSON 响应,同时结合 `LocaleMessageException` 和 `MessageSource` 实现错误信息的国际化管理。 + +## 如何使用 + +### 自动生效 + +引入 `springboot-starter` 依赖后,异常处理自动配置即可生效,无需额外注解或配置: + +```xml + + com.codingapi.springboot + springboot-starter + +``` + +框架通过以下两个配置类自动注册: + +- **`ExceptionConfiguration`**:注册 `LocaleMessage` Bean,绑定 Spring `MessageSource`,在初始化时将自身注入 `MessageContext` 单例,为 `LocaleMessageException` 提供国际化消息解析能力。 +- **`BasicHandlerExceptionResolverConfiguration`**:当 classpath 中存在 `HandlerExceptionResolver`(即 Spring MVC 环境)时,注册 `ServletExceptionHandler` 作为全局异常解析器。 + +### 异常类型与响应映射 + +| 异常类型 | errCode | errMessage | 说明 | +|---------|---------|------------|------| +| `LocaleMessageException` | 异常中指定的 errCode | 国际化解析后的消息 | 业务异常,推荐使用 | +| 其他 `Exception` | `system.err` | `ex.getMessage()` | 未预期的系统异常 | + +### 抛出业务异常 + +在 Service 或 Controller 中抛出 `LocaleMessageException`,支持三种构造方式: + +```java +// 1. 直接使用错误码(从 messages.properties 中解析消息) +throw new LocaleMessageException("user.not.found"); + +// 2. 错误码 + 占位符参数 +throw LocaleMessageException.of("order.amount.exceed", maxAmount); + +// 3. 错误码 + 自定义消息(不走国际化) +throw new LocaleMessageException("custom.error", "自定义错误描述"); +``` + +### 配置国际化消息 + +在 `src/main/resources/messages.properties`(及对应的多语言文件)中定义错误消息: + +```properties +# messages.properties +user.not.found=用户不存在 +order.amount.exceed=订单金额超出限制,最大金额为 {0} + +# messages_en.properties +user.not.found=User not found +order.amount.exceed=Order amount exceeds limit, maximum is {0} +``` + +## 使用实例 + +### 完整示例:Controller 中抛出业务异常 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + User user = userService.findById(id); + if (user == null) { + // 抛出国际化业务异常 + throw new LocaleMessageException("user.not.found"); + } + return SingleResponse.of(user); + } + + @PostMapping + public Response createUser(@RequestBody CreateUserRequest request) { + if (request.getAmount().compareTo(MAX_AMOUNT) > 0) { + // 带占位符参数的异常 + throw LocaleMessageException.of("order.amount.exceed", MAX_AMOUNT); + } + userService.create(request); + return Response.buildSuccess(); + } +} +``` + +当请求触发异常时,框架自动返回统一格式的 JSON 响应: + +```json +{ + "success": false, + "errCode": "user.not.found", + "errMessage": "用户不存在" +} +``` + +若当前请求的 Locale 为英文,则自动返回: + +```json +{ + "success": false, + "errCode": "user.not.found", + "errMessage": "User not found" +} +``` + +对于未预期的系统异常(如 NullPointerException),返回: + +```json +{ + "success": false, + "errCode": "system.err", + "errMessage": "Cannot invoke method on null object" +} +``` diff --git a/docs/capabilities/springboot-starter/locale-message.md b/docs/capabilities/springboot-starter/locale-message.md new file mode 100644 index 000000000..d4fca5f3c --- /dev/null +++ b/docs/capabilities/springboot-starter/locale-message.md @@ -0,0 +1,121 @@ +--- +name: springboot-starter/locale-message +module: springboot-starter +description: 国际化异常消息,支持多语言异常信息和本地化消息解析 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - LocaleMessageException + - LocaleMessage + - MessageContext +content_hash: 1480d3ed2cf99ad0eb3cd955540f5bc263adcf0196bc8f0b3f79e713eb16b6ad +--- + +## 解决什么问题 + +在企业级应用中,异常消息往往需要面向不同语言和地区的用户展示本地化内容。直接在代码中硬编码中文或英文错误提示存在以下痛点: + +- **多语言维护困难**:错误消息散落在业务代码各处,新增语言时需要逐一修改源码。 +- **异常与消息耦合**:`RuntimeException` 只接受字符串消息,无法携带错误码进行统一拦截和翻译。 +- **占位符参数化缺失**:部分错误消息需要动态插入变量(如用户名、金额),拼接字符串容易出错且不利于翻译。 + +`locale-message` 能力通过 `LocaleMessageException` + Spring `MessageSource` 的组合,将错误码作为异常的语义标识,在抛出时自动根据当前请求的 `Locale` 从 `messages.properties` 资源文件中解析出对应的本地化消息,从而实现异常消息与业务代码的解耦和多语言支持。 + +## 如何使用 + +### 核心组件 + +| 类 | 职责 | +|----|------| +| `LocaleMessageException` | 国际化异常,支持仅传错误码、错误码+占位符参数、错误码+自定义消息等多种构造方式 | +| `LocaleMessage` | 封装 Spring `MessageSource`,提供按 code + args + locale 解析消息的能力 | +| `MessageContext` | 单例上下文,持有 `LocaleMessage` 实例,供异常类在静态上下文中获取本地化消息 | + +### 配置消息资源文件 + +在 `src/main/resources/` 下创建标准 Spring 消息资源文件: + +```properties +# messages.properties(默认/回退) +error.user.not.found=User not found: {0} +error.param.invalid=Invalid parameter: {0}, expected {1} + +# messages_zh_CN.properties +error.user.not.found=用户不存在: {0} +error.param.invalid=参数无效: {0},期望值 {1} +``` + +Spring Boot 会自动加载这些文件作为 `MessageSource`。 + +### 抛出国际化异常 + +`LocaleMessageException` 提供多种构造方式和静态工厂方法: + +```java +// 1. 仅传错误码 —— 自动从 messages.properties 解析消息 +throw new LocaleMessageException("error.user.not.found"); + +// 2. 错误码 + 占位符参数 +throw LocaleMessageException.of("error.user.not.found", "zhangsan"); + +// 3. 错误码 + 多个占位符参数 +throw LocaleMessageException.of("error.param.invalid", "age", "Integer"); + +// 4. 错误码 + 自定义消息(不走 MessageSource) +throw new LocaleMessageException("ERR_CUSTOM", "自定义错误消息"); + +// 5. 携带原始异常 +throw new LocaleMessageException("error.user.not.found", cause); +``` + +### 自动装配 + +框架启动时,`LocaleMessage` Bean 会被创建并调用 `init()` 方法,将自身注册到 `MessageContext` 单例中。`LocaleMessageException` 在构造时通过 `MessageContext.getInstance().getErrorMsg(errCode, args)` 获取本地化消息,整个过程对业务代码透明,无需手动注入任何 Bean。 + +当前请求的 `Locale` 由 Spring 的 `LocaleContextHolder` 提供(通常通过 `Accept-Language` 请求头或 `LocaleChangeInterceptor` 设置)。 + +## 使用实例 + +### 完整示例:用户查询服务 + +```java +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + public User getUser(String username) { + return userRepository.findByUsername(username) + .orElseThrow(() -> + LocaleMessageException.of("error.user.not.found", username) + ); + } + + public void updateUserAge(String username, int age) { + if (age < 0 || age > 150) { + throw LocaleMessageException.of("error.param.invalid", "age", "0~150"); + } + // ... + } +} +``` + +当客户端发送 `Accept-Language: zh-CN` 请求时,若用户不存在,返回的异常消息为 `"用户不存在: zhangsan"`;若未指定语言或使用默认 locale,则返回 `"User not found: zhangsan"`。 + +### 在全局异常处理器中统一捕获 + +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(LocaleMessageException.class) + public Response handleLocaleException(LocaleMessageException e) { + return Response.buildFailure(e.getErrCode(), e.getErrMessage()); + } +} +``` + +由于异常对象已经携带了本地化后的 `errMessage`,全局处理器只需直接提取即可,无需再做二次翻译。 diff --git a/docs/capabilities/springboot-starter/page-request.md b/docs/capabilities/springboot-starter/page-request.md new file mode 100644 index 000000000..bf94c95a5 --- /dev/null +++ b/docs/capabilities/springboot-starter/page-request.md @@ -0,0 +1,182 @@ +--- +name: springboot-starter/page-request +module: springboot-starter +description: 动态分页查询请求,扩展 Spring PageRequest 支持 RequestFilter 动态过滤条件 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - PageRequest + - RequestFilter + - SearchRequest + - Filter +content_hash: c4302ccd858320110405285846ca4d4087b4844a4334452fa63ee34b68fffa5f +--- + +## 解决什么问题 + +在企业级后台管理系统中,列表查询是最常见的业务场景之一。Spring Data 原生的 `PageRequest` 仅支持分页参数(页码、大小、排序),无法携带动态过滤条件。开发者通常需要为每个查询接口手动编写 Specification、QueryDSL 或自定义 HQL,导致大量重复的查询构建代码。 + +`PageRequest` 能力解决了以下痛点: + +- **动态过滤**:在分页请求对象上直接附加任意字段、任意比较关系的过滤条件,无需为每种查询组合编写独立方法。 +- **前端驱动查询**:通过 `SearchRequest` 自动解析 HTTP 请求参数(包括 Base64 编码的 `filter`、`sort`、`params` JSON),将前端传入的筛选/排序规则转换为类型安全的过滤条件,减少 Controller 层样板代码。 +- **复杂条件组合**:支持 AND/OR 嵌套组合过滤,满足多条件联合查询需求。 +- **与 FastRepository 无缝集成**:`FastRepository.findAll(PageRequest)` 和 `pageRequest(PageRequest)` 自动根据过滤条件构建 Example 或 HQL 查询,开发者只需关注业务逻辑。 + +## 如何使用 + +### 核心类说明 + +| 类 | 职责 | +|---|------| +| `PageRequest` | 继承 Spring `PageRequest`,增加 `RequestFilter` 和链式 `addFilter` API | +| `RequestFilter` | 过滤条件容器,管理 `Filter` 列表并提供按 key 读取过滤值的快捷方法 | +| `Filter` | 单个过滤条件,包含字段名(key)、比较关系(Relation)和值 | +| `Relation` | 枚举,定义了 14 种比较关系:EQUAL、NOT_EQUAL、LIKE、LEFT_LIKE、RIGHT_LIKE、BETWEEN、IN、NOT_IN、IS_NULL、IS_NOT_NULL、GREATER_THAN、LESS_THAN、GREATER_THAN_EQUAL、LESS_THAN_EQUAL | +| `SearchRequest` | 从当前 `HttpServletRequest` 自动解析分页、排序、过滤参数并生成 `PageRequest` | + +### 创建 PageRequest + +```java +// 基本分页(第 0 页,每页 20 条) +PageRequest request = PageRequest.of(0, 20); + +// 带排序 +PageRequest request = PageRequest.of(0, 20, Sort.by("createTime").descending()); +``` + +### 添加过滤条件 + +```java +// 等值过滤(默认 EQUAL) +request.addFilter("name", "张三"); + +// 指定比较关系 +request.addFilter("age", Relation.GREATER_THAN, 18); +request.addFilter("createTime", Relation.BETWEEN, startDate, endDate); +request.addFilter("status", Relation.IN, "ACTIVE", "PENDING"); + +// AND / OR 组合 +request.andFilter( + Filter.as("deptId", 1), + Filter.as("roleId", 2) +); + +request.orFilters( + Filter.as("name", Relation.LIKE, "张"), + Filter.as("email", Relation.LIKE, "zhang") +); +``` + +### 读取过滤值 + +```java +String name = request.getStringFilter("name"); +int age = request.getIntFilter("age", 0); +boolean hasConditions = request.hasFilter(); +``` + +### 配合 FastRepository 使用 + +```java +// findAll — 简单过滤走 Example 查询 +Page page = userRepository.findAll(request); + +// pageRequest — 复杂过滤走 HQL 动态查询 +Page page = userRepository.pageRequest(request); + +// searchRequest — 从 HTTP 请求自动解析 +Page page = userRepository.searchRequest(searchRequest); +``` + +### SearchRequest 自动解析 + +`SearchRequest` 从当前 HTTP 请求中提取以下参数: + +| 参数 | 格式 | 说明 | +|------|------|------| +| `current` | int | 页码(从 0 开始) | +| `pageSize` | int | 每页大小 | +| `sort` | Base64(JSON) | 排序规则,如 `{"createTime":"descend"}` | +| `filter` | Base64(JSON) | 过滤条件,如 `{"status":["ACTIVE"]}` | +| `params` | Base64(JSON Array) | 指定字段的比较关系,如 `[{"key":"age","type":"GREATER_THAN"}]` | +| 其他参数 | string | 作为 EQUAL 过滤条件自动添加 | + +## 使用实例 + +### 示例 1:Service 层手动构建动态查询 + +```java +@Service +public class UserQueryService { + + @Autowired + private UserRepository userRepository; + + public Page searchUsers(String keyword, Integer minAge, String status) { + PageRequest request = PageRequest.of(0, 20); + request.addSort(Sort.by("createTime").descending()); + + if (keyword != null) { + request.addFilter("name", Relation.LIKE, keyword); + } + if (minAge != null) { + request.addFilter("age", Relation.GREATER_THAN_EQUAL, minAge); + } + if (status != null) { + request.addFilter("status", status); + } + + return userRepository.findAll(request); + } +} +``` + +### 示例 2:Controller 层使用 SearchRequest 自动绑定 + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + @GetMapping + public MultiResponse list(SearchRequest searchRequest) { + // 自动从 HTTP 参数解析 current、pageSize、sort、filter + Page page = userRepository.searchRequest(searchRequest); + return MultiResponse.of(page.getContent(), (int) page.getTotalElements()); + } +} +``` + +前端请求示例: +``` +GET /api/users?current=0&pageSize=20 + &sort=eyJjcmVhdGVUaW1lIjoiZGVzY2VuZCJ9 + &filter=eyJzdGF0dXMiOlsiQUNUSVZFIiwiUEVORElORyJdfQ== + ¶ms=W3sia2V5IjoiYWdlIiwidHlwZSI6IkdSRUFURVJfVEhBTiJ9XQ== + &deptId=1 +``` + +其中 `filter` 解码后为 `{"status":["ACTIVE","PENDING"]}`,`params` 解码后为 `[{"key":"age","type":"GREATER_THAN"}]`,`deptId` 作为额外 EQUAL 条件自动加入。 + +### 示例 3:AND/OR 组合过滤 + +```java +PageRequest request = PageRequest.of(0, 20); + +// (deptId=1 AND roleId=2) OR (name LIKE '%admin%') +request.orFilters( + Filter.and( + Filter.as("deptId", 1), + Filter.as("roleId", 2) + ), + Filter.as("name", Relation.LIKE, "admin") +); + +Page page = userRepository.pageRequest(request); +``` diff --git a/docs/capabilities/springboot-starter/response-dto.md b/docs/capabilities/springboot-starter/response-dto.md new file mode 100644 index 000000000..3794043fd --- /dev/null +++ b/docs/capabilities/springboot-starter/response-dto.md @@ -0,0 +1,177 @@ +--- +name: springboot-starter/response-dto +module: springboot-starter +description: 统一响应封装,提供 Response/SingleResponse/MultiResponse/MapResponse 四种响应类型 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - Response + - SingleResponse + - MultiResponse + - MapResponse +content_hash: 1dfe2559a6b514effb51735ca8ca49aba308959e02814c5862b842eed3b04fb1 +--- + +## 解决什么问题 + +在企业级 REST API 开发中,前后端交互需要统一的响应契约。若每个接口各自定义返回结构,会导致以下问题: + +- **前端解析复杂**:不同接口的成功/失败字段命名不一致,前端需逐接口适配 +- **错误处理分散**:缺乏统一的 `errCode` / `errMessage` 规范,全局异常拦截难以标准化 +- **分页结构不统一**:列表接口的总数、数据字段名各异,通用分页组件无法复用 +- **序列化行为不可控**:响应对象在缓存、消息队列等场景下需要可靠的 JSON 序列化能力 + +`response-dto` 提供了四种标准响应类型,覆盖无数据、单对象、列表/分页、键值对四类返回场景,使所有 API 输出遵循同一契约。基类 `Response` 实现了 `JsonSerializable` 接口,支持通过 Fastjson 进行一致的 JSON 序列化。 + +## 如何使用 + +### 类层次结构 + +``` +JsonSerializable (interface) + └── Response — 基础响应(success / errCode / errMessage) + ├── SingleResponse — 单对象响应,data 字段为泛型 T + ├── MultiResponse — 列表响应,data 字段包含 total + list + └── MapResponse — 键值对响应,data 字段为 Map +``` + +### 核心 API + +| 类 | 静态工厂方法 | 说明 | +|---|---|---| +| `Response` | `buildSuccess()` | 构建成功响应(无业务数据) | +| `Response` | `buildFailure(errCode, errMessage)` | 构建失败响应 | +| `SingleResponse` | `of(T data)` | 包装单个业务对象 | +| `SingleResponse` | `empty()` | 返回 data=null 的成功响应 | +| `MultiResponse` | `of(Collection data, long total)` | 包装集合 + 总数 | +| `MultiResponse` | `of(Page page)` | 直接包装 Spring Data Page 对象 | +| `MultiResponse` | `of(Collection data)` | 包装集合,total 自动取 size | +| `MultiResponse` | `empty()` | 返回空列表的成功响应 | +| `MapResponse` | `create()` | 创建空的键值对响应 | +| `MapResponse` | `empty()` | 返回 data=null 的键值对响应 | +| `MapResponse` | `add(key, value)` | 链式添加键值对 | + +### JSON 序列化 + +所有响应类均继承自 `Response`,而 `Response` 实现了 `JsonSerializable` 接口,可直接调用 `toJson()` 方法获取 JSON 字符串: + +```java +String json = response.toJson(); // 使用 Fastjson 序列化 +``` + +### Maven 依赖 + +```xml + + com.codingapi.springboot + springboot-starter + +``` + +## 使用实例 + +### 1. 无数据的操作响应 + +```java +@PostMapping("/save") +public Response save(@RequestBody UserRequest request) { + userService.save(request); + return Response.buildSuccess(); +} +``` + +返回 JSON: +```json +{ "success": true, "errCode": null, "errMessage": null } +``` + +### 2. 返回单个对象 + +```java +@GetMapping("/detail") +public SingleResponse detail(@RequestParam Long id) { + UserVO user = userQueryService.getById(id); + return SingleResponse.of(user); +} +``` + +返回 JSON: +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { "id": 1, "name": "张三", "email": "zhangsan@example.com" } +} +``` + +### 3. 返回列表(含分页) + +```java +@GetMapping("/list") +public MultiResponse list(SearchRequest searchRequest) { + Page page = userQueryService.page(searchRequest); + return MultiResponse.of(page); +} +``` + +返回 JSON: +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "total": 100, + "list": [ + { "id": 1, "name": "张三" }, + { "id": 2, "name": "李四" } + ] + } +} +``` + +### 4. 返回键值对数据 + +```java +@GetMapping("/dashboard/stats") +public MapResponse dashboardStats() { + return MapResponse.create() + .add("userCount", 1024) + .add("orderCount", 5678) + .add("todayRevenue", 99800.50); +} +``` + +返回 JSON: +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "userCount": 1024, + "orderCount": 5678, + "todayRevenue": 99800.50 + } +} +``` + +### 5. 返回错误响应 + +```java +@PostMapping("/login") +public Response login(@RequestBody LoginRequest request) { + if (!userService.authenticate(request)) { + return Response.buildFailure("AUTH_FAILED", "用户名或密码错误"); + } + return Response.buildSuccess(); +} +``` + +返回 JSON: +```json +{ "success": false, "errCode": "AUTH_FAILED", "errMessage": "用户名或密码错误" } +``` diff --git a/docs/capabilities/springboot-starter/rest-client.md b/docs/capabilities/springboot-starter/rest-client.md new file mode 100644 index 000000000..579c94726 --- /dev/null +++ b/docs/capabilities/springboot-starter/rest-client.md @@ -0,0 +1,111 @@ +--- +name: springboot-starter/rest-client +module: springboot-starter +description: REST 客户端封装,提供 RestTemplate 上下文管理和 HTTPS 信任所有证书支持 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +symbols: + - RestTemplateContext + - TrustAnyHttpClientFactory +content_hash: 70d327e5685d61447513a8df011bccc1c0ab9153b4b1c1d3763096d86fd90e9c +--- + +## 解决什么问题 + +在企业内部系统集成和第三方 API 对接场景中,经常需要调用外部 HTTPS 服务。这些服务的证书可能由私有 CA 签发、已过期或主机名不匹配,导致标准 HTTP 客户端抛出 SSL 握手异常。同时,项目中多处使用 `RestTemplate` 时,如果每次都手动创建实例并配置底层 HttpClient,会导致代码重复且难以统一维护。 + +本能力通过两个核心类解决上述问题: + +- **TrustAnyHttpClientFactory** — 创建信任所有证书的 Apache HttpClient 5 实例,跳过服务端证书校验和主机名验证,适用于开发调试及内网可信环境下的 HTTPS 调用。 +- **RestTemplateContext** — 以单例模式持有预配置好的 `RestTemplate`(底层使用 TrustAnyHttpClientFactory),为全应用提供统一的 REST 客户端访问入口,避免重复创建和配置。 + +## 如何使用 + +### 依赖引入 + +在 Maven 项目中添加 springboot-starter 依赖即可: + +```xml + + com.codingapi.springboot + springboot-starter + +``` + +### 获取 RestTemplate 实例 + +通过 `RestTemplateContext` 的单例方法获取已配置好 HTTPS 信任策略的 `RestTemplate`: + +```java +RestTemplate restTemplate = RestTemplateContext.getInstance().getRestTemplate(); +``` + +该实例底层使用 Apache HttpComponents 5 作为传输层,并已启用以下特性: + +- TLS 协议下信任所有服务端证书(`TrustAnyTrustManager`) +- 禁用主机名验证(`NoopHostnameVerifier`) +- 允许循环重定向 + +### 自定义 ClientHttpRequestFactory + +如果需要替换默认的请求工厂,可以使用 `restTemplate(ClientHttpRequestFactory)` 方法创建新的 `RestTemplate` 实例: + +```java +ClientHttpRequestFactory customFactory = new HttpComponentsClientHttpRequestFactory(customHttpClient); +RestTemplate customTemplate = RestTemplateContext.getInstance().restTemplate(customFactory); +``` + +### 直接使用 TrustAnyHttpClientFactory + +若仅需创建信任所有证书的 HttpClient 而不使用 RestTemplateContext 封装,可直接调用静态工厂方法: + +```java +HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); +``` + +返回的 `HttpClient` 基于 Apache HttpClient 5,可用于任何需要绕过 SSL 校验的场景。 + +## 使用实例 + +### 示例 1:调用外部 HTTPS 接口 + +```java +@Service +public class ExternalApiService { + + private final RestTemplate restTemplate; + + public ExternalApiService() { + this.restTemplate = RestTemplateContext.getInstance().getRestTemplate(); + } + + public String fetchRemoteData(String url) { + ResponseEntity response = restTemplate.getForEntity(url, String.class); + return response.getBody(); + } + + public T postJson(String url, Object request, Class responseType) { + return restTemplate.postForObject(url, request, responseType); + } +} +``` + +### 示例 2:独立使用 TrustAnyHttpClientFactory + +```java +// 在不依赖 RestTemplateContext 的场景下,单独创建信任所有证书的 HttpClient +HttpClient httpClient = TrustAnyHttpClientFactory.createTrustAnyHttpClient(); + +// 配合 Spring 的 HttpComponentsClientHttpRequestFactory 构建自定义 RestTemplate +HttpComponentsClientHttpRequestFactory factory = + new HttpComponentsClientHttpRequestFactory(httpClient); +factory.setConnectTimeout(Duration.ofSeconds(5)); +factory.setReadTimeout(Duration.ofSeconds(30)); + +RestTemplate restTemplate = new RestTemplate(factory); +String result = restTemplate.getForObject("https://internal-api.example.com/data", String.class); +``` + +> ⚠️ **安全提示**:信任所有证书仅适用于开发调试和内网可信环境。在生产环境中应正确配置受信任的 CA 证书链,避免中间人攻击风险。 diff --git a/docs/capabilities/springboot/data-jpa.md b/docs/capabilities/springboot/data-jpa.md new file mode 100644 index 000000000..989b5f60b --- /dev/null +++ b/docs/capabilities/springboot/data-jpa.md @@ -0,0 +1,211 @@ +--- +name: springboot/data-jpa +module: springboot +description: Spring Data JPA,提供 ORM 映射、Repository 抽象和分页支持 +status: 已实现 +scope: 后端 +source: 框架:springboot +import: "org.springframework.boot:spring-boot-starter-data-jpa" +framework_version: 3.3.5 +--- + +## 解决什么问题 + +在企业级 Java 应用中,数据持久化是核心基础设施需求。直接使用 JDBC 或原生 JPA API 存在以下痛点: + +- **样板代码冗余**:每个实体都需要手动编写 CRUD 操作、SQL 拼接、结果集映射,重复劳动量大 +- **ORM 配置复杂**:原生 Hibernate/JPA 的 XML 或注解配置繁琐,事务管理和 EntityManager 生命周期需要手工控制 +- **分页与排序缺乏统一抽象**:不同数据库的分页语法差异大,业务层需要自行处理 offset/limit 逻辑 +- **查询方法命名约定缺失**:简单的条件查询也需要手写 JPQL/SQL,无法通过方法名自动推导 + +Spring Data JPA 通过 Repository 抽象层和自动实现机制,将上述问题封装为声明式接口,开发者只需定义接口和方法签名即可获得完整的持久化能力,大幅降低数据访问层的开发和维护成本。 + +## 如何使用 + +### 1. 添加依赖 + +在 `pom.xml` 中引入 Spring Boot Data JPA Starter: + +```xml + + org.springframework.boot + spring-boot-starter-data-jpa + +``` + +### 2. 配置数据源 + +在 `application.properties` 中配置数据库连接和 JPA 行为: + +```properties +spring.datasource.url=jdbc:h2:file:./data/demo +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +``` + +### 3. 定义实体类 + +使用 JPA 注解将 POJO 映射到数据库表: + +```java +@Entity +@Table(name = "t_user") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64) + private String username; + + private String email; + + // getters & setters +} +``` + +### 4. 定义 Repository 接口 + +继承 `JpaRepository` 即可获得标准 CRUD、分页、排序等能力;通过方法命名约定可自动生成查询: + +```java +public interface UserRepository extends JpaRepository { + + List findByUsername(String username); + + Page findByEmailContaining(String keyword, Pageable pageable); + + Optional findByUsernameAndEmail(String username, String email); +} +``` + +### 5. 核心接口说明 + +| 接口 | 说明 | +|------|------| +| `CrudRepository` | 基础 CRUD 操作(save、findById、delete 等) | +| `PagingAndSortingRepository` | 继承 CrudRepository,增加分页和排序支持 | +| `JpaRepository` | 继承上述两者,增加批量操作、flush、Example 查询等 JPA 特有能力 | +| `Specification` | 用于构建动态查询条件的函数式接口 | +| `Pageable` / `PageRequest` | 分页参数抽象(页码、每页大小、排序) | +| `Page` | 分页结果封装(内容列表、总记录数、总页数) | + +### 6. 事务管理 + +Spring Data JPA 默认集成 Spring 声明式事务。Service 层方法添加 `@Transactional` 即可自动管理事务边界: + +```java +@Service +public class UserService { + + @Transactional + public User createUser(User user) { + return userRepository.save(user); + } +} +``` + +## 使用实例 + +### 基本 CRUD 操作 + +```java +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public User create(String username, String email) { + User user = new User(); + user.setUsername(username); + user.setEmail(email); + return userRepository.save(user); + } + + public Optional getById(Long id) { + return userRepository.findById(id); + } + + public void delete(Long id) { + userRepository.deleteById(id); + } +} +``` + +### 分页与排序查询 + +```java +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + + private final UserRepository userRepository; + + @GetMapping + public Page list( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String keyword) { + + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + + if (keyword != null && !keyword.isBlank()) { + return userRepository.findByEmailContaining(keyword, pageable); + } + return userRepository.findAll(pageable); + } +} +``` + +### Specification 动态查询 + +适用于多条件组合筛选场景,避免为每种组合定义单独的 Repository 方法: + +```java +public class UserSpecifications { + + public static Specification hasUsername(String username) { + return (root, query, cb) -> + username == null ? null : cb.equal(root.get("username"), username); + } + + public static Specification emailContains(String email) { + return (root, query, cb) -> + email == null ? null : cb.like(root.get("email"), "%" + email + "%"); + } +} + +// 在 Service 中组合使用 +@Service +@RequiredArgsConstructor +public class UserSearchService { + + private final UserRepository userRepository; + + public Page search(String username, String email, Pageable pageable) { + Specification spec = Specification + .where(UserSpecifications.hasUsername(username)) + .and(UserSpecifications.emailContains(email)); + return userRepository.findAll(spec, pageable); + } +} +``` + +### Example 查询 + +基于实体对象字段值自动匹配,适合简单等值条件查询: + +```java +User probe = new User(); +probe.setUsername("zhangsan"); + +ExampleMatcher matcher = ExampleMatcher.matching() + .withIgnoreCase() + .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING); + +List results = userRepository.findAll(Example.of(probe, matcher)); +``` diff --git a/docs/capabilities/springboot/ioc-container.md b/docs/capabilities/springboot/ioc-container.md new file mode 100644 index 000000000..9b6ba0257 --- /dev/null +++ b/docs/capabilities/springboot/ioc-container.md @@ -0,0 +1,218 @@ +--- +name: springboot/ioc-container +module: springboot +description: Spring IoC 容器,提供依赖注入、AOP、事件发布等核心功能 +status: 已实现 +scope: 后端 +source: 框架:springboot +import: "org.springframework.boot:spring-boot-starter" +framework_version: 3.3.5 +--- + +## 解决什么问题 + +在企业级 Java 应用开发中,对象之间的依赖关系往往错综复杂。如果由开发者手动创建和管理所有对象及其依赖,会导致代码高度耦合、难以测试、难以维护。Spring IoC(Inversion of Control)容器通过以下机制解决这些核心痛点: + +- **依赖注入(DI)**:将对象的创建和组装交由容器统一管理,业务代码无需关心依赖的来源与生命周期,从而实现组件间的解耦。 +- **声明式配置**:通过注解或 Java Config 描述 Bean 的定义与装配规则,替代冗长的工厂模式或手动 new 对象的方式。 +- **AOP 支持**:在不修改业务代码的前提下,以切面方式织入事务管理、日志记录、权限校验等横切关注点。 +- **事件驱动**:提供 `ApplicationEventPublisher` 发布-订阅机制,使模块间可以通过事件进行松耦合通信,这也是本项目 DDD 领域事件基础设施的底层支撑。 +- **生命周期管理**:容器负责 Bean 的初始化、回调(如 `@PostConstruct`)和销毁,简化资源管理逻辑。 + +典型复用场景包括:Service 层注入 Repository、Controller 注入 Service、通过 AOP 统一处理异常与日志、使用 `ApplicationListener` 监听启动完成事件等。 + +## 如何使用 + +### 引入依赖 + +在 Maven 项目的 `pom.xml` 中添加: + +```xml + + org.springframework.boot + spring-boot-starter + +``` + +该 starter 已包含 `spring-context`、`spring-aop`、`spring-beans` 等 IoC 核心模块。在本项目中,`springboot-starter` 及其他所有 starter 均已传递依赖此包,无需额外声明。 + +### Bean 定义与注入 + +| 方式 | 说明 | +|------|------| +| `@Component` / `@Service` / `@Repository` / `@Controller` | 类级别注解,标记为 Spring 管理的 Bean | +| `@Bean` | 在 `@Configuration` 类的方法上声明第三方或非注解类的 Bean | +| `@Autowired` | 按类型自动注入(推荐用于构造器注入) | +| `@Qualifier("beanName")` | 当同类型存在多个 Bean 时,指定具体名称 | +| `@Value("${key}")` | 注入配置文件中的属性值 | +| `@Scope("prototype")` | 改变 Bean 作用域,默认为 `singleton` | + +### AOP 使用 + +1. 添加 `spring-boot-starter-aop` 依赖(或使用 AspectJ)。 +2. 编写切面类,使用 `@Aspect` + `@Component` 标注。 +3. 通过 `@Around`、`@Before`、`@After` 等通知注解定义切入点与增强逻辑。 + +### 事件发布与监听 + +- 注入 `ApplicationEventPublisher`,调用 `publishEvent()` 发布自定义事件。 +- 使用 `@EventListener` 注解或实现 `ApplicationListener` 接口监听事件。 +- 配合 `@TransactionalEventListener` 可在事务提交后再触发处理逻辑。 + +### 配置方式 + +Spring Boot 优先使用基于注解的配置风格: + +- `@SpringBootApplication` 组合了 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan`。 +- `@Import` 可导入额外的配置类。 +- `application.properties` / `application.yml` 提供外部化配置,通过 `@ConfigurationProperties` 绑定到 Bean。 + +## 使用实例 + +### 基本依赖注入 + +```java +@Service +public class UserService { + + private final UserRepository userRepository; + + // 构造器注入(推荐方式,Spring 4.3+ 单构造器可省略 @Autowired) + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User findById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("User not found: " + id)); + } +} + +@RestController +@RequestMapping("/api/users") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + return Response.ok(userService.findById(id)); + } +} +``` + +### 通过 @Bean 注册第三方组件 + +```java +@Configuration +public class AppConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(10)) + .build(); + } +} +``` + +### AOP 切面示例 + +```java +@Aspect +@Component +@Slf4j +public class MethodExecutionLogAspect { + + @Around("@annotation(org.springframework.stereotype.Service)") + public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable { + String methodName = joinPoint.getSignature().toShortString(); + long start = System.currentTimeMillis(); + + try { + Object result = joinPoint.proceed(); + log.info("{} executed in {}ms", methodName, System.currentTimeMillis() - start); + return result; + } catch (Throwable ex) { + log.error("{} failed after {}ms", methodName, System.currentTimeMillis() - start, ex); + throw ex; + } + } +} +``` + +### 事件发布与监听 + +```java +// 定义事件 +public class UserCreatedEvent extends ApplicationEvent { + private final User user; + + public UserCreatedEvent(Object source, User user) { + super(source); + this.user = user; + } + + public User getUser() { + return user; + } +} + +// 发布事件 +@Service +public class UserService { + + private final ApplicationEventPublisher eventPublisher; + + public UserService(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Transactional + public User createUser(CreateUserCommand command) { + User user = new User(command.getName(), command.getEmail()); + userRepository.save(user); + eventPublisher.publishEvent(new UserCreatedEvent(this, user)); + return user; + } +} + +// 监听事件 +@Component +@Slf4j +public class UserCreatedNotificationHandler { + + @EventListener + public void onUserCreated(UserCreatedEvent event) { + log.info("New user created: {}, sending welcome notification", event.getUser().getName()); + // 发送欢迎邮件或消息... + } +} +``` + +### 生命周期回调 + +```java +@Component +public class CacheWarmupService { + + @PostConstruct + public void warmupCache() { + // 容器初始化完成后预热缓存 + log.info("Warming up application cache..."); + } + + @PreDestroy + public void cleanup() { + // 容器关闭前释放资源 + log.info("Cleaning up resources before shutdown..."); + } +} +``` + +以上示例展示了 Spring IoC 容器在日常开发中最常用的能力。在本项目中,框架的事件系统(`IEvent`、`EventPusher`)、数据权限 SQL 拦截、工作流引擎等高级特性均构建于 IoC 容器之上,理解 IoC 是掌握整个框架的基础。 diff --git a/docs/capabilities/springboot/security.md b/docs/capabilities/springboot/security.md new file mode 100644 index 000000000..d30523bac --- /dev/null +++ b/docs/capabilities/springboot/security.md @@ -0,0 +1,231 @@ +--- +name: springboot/security +module: springboot +description: Spring Security,提供认证、授权和 CSRF 防护 +status: 已实现 +scope: 后端 +source: 框架:springboot +import: "org.springframework.boot:spring-boot-starter-security" +framework_version: 3.3.5 +--- + +## 解决什么问题 + +在企业级 Web 应用中,安全是最基础且最复杂的横切关注点。开发者通常需要面对以下痛点: + +- **认证机制繁琐**:手动实现 JWT / Redis Token 的签发、解析、续期和过期校验逻辑,代码重复且容易出错。 +- **授权规则分散**:URL 级别的访问控制散落在各个 Controller 或拦截器中,缺乏统一的安全策略配置入口。 +- **登录/登出流程耦合**:JSON API 场景下的登录成功/失败响应、Token 返回格式、登出清理等逻辑与业务代码高度耦合。 +- **安全防护遗漏**:CSRF、CORS、Frame Options 等浏览器安全头需要逐一配置,默认值往往不适合前后端分离架构。 +- **扩展困难**:当需要在认证前后插入自定义逻辑(如验证码校验、审计日志、多因子认证)时,缺少标准化的扩展点。 + +`springboot-starter-security` 模块在 Spring Security 6.x 基础上进行了二次封装,通过 `TokenGateway` 抽象令牌存储(JWT / Redis 双模式)、`SecurityLoginHandler` 开放登录生命周期钩子、`HttpSecurityCustomer` 允许自定义安全链配置,将上述问题收敛到统一的自动配置体系中,使业务项目只需声明少量配置即可获得生产级的安全基础设施。 + +## 如何使用 + +### 1. 引入依赖 + +```xml + + com.codingapi.springboot + springboot-starter-security + +``` + +该 starter 会自动引入 `spring-boot-starter-security` 并注册 `AutoConfiguration`。 + +### 2. 核心配置项 + +在 `application.properties` 中通过 `codingapi.security.*` 前缀进行配置: + +```properties +# 需要认证的 URL 模式(逗号分隔),匹配后必须携带有效 Token +codingapi.security.authenticated-urls=/api/** + +# 免认证的 URL 模式(逗号分隔) +codingapi.security.ignore-urls=/open/**,/public/** + +# 登录接口地址(POST) +codingapi.security.login-processing-url=/user/login + +# 登出接口地址 +codingapi.security.logout-url=/user/logout + +# 禁用 CSRF(前后端分离场景通常为 true) +codingapi.security.disable-csrf=true + +# 禁用 CORS 内置处理(由框架自动配置 CORS 映射) +codingapi.security.disable-cors=true + +# 禁用 Basic Auth +codingapi.security.disable-basic-auth=true + +# 禁用 Frame Options +codingapi.security.disable-frame-options=true +``` + +### 3. 核心接口 + +| 接口 | 职责 | 扩展方式 | +|------|------|----------| +| `TokenGateway` | 令牌的创建(`create`)与解析(`parser`) | 实现该接口或使用内置的 `JWTTokenGatewayImpl` / `RedisTokenGatewayImpl` | +| `SecurityLoginHandler` | 登录前置校验(`preHandle`)与后置响应构建(`postHandle`) | 实现该接口并注册为 Bean,替换默认行为 | +| `AuthenticationTokenFilter` | Token 验证通过后的附加过滤逻辑 | 实现该接口并注册为 Bean | +| `HttpSecurityCustomer` | 自定义 `HttpSecurity` 配置链 | 实现 `customize(HttpSecurity)` 方法 | +| `UserDetailsService` | 用户加载 | 注册自定义 Bean 替换默认的内存用户 | +| `PasswordEncoder` | 密码编码 | 注册自定义 Bean 替换默认的 DelegatingPasswordEncoder | + +所有接口均标注 `@ConditionalOnMissingBean`,业务项目注册同名 Bean 即可无缝替换默认实现。 + +### 4. Token 模型 + +`Token` 对象封装了完整的令牌信息: + +- `username` — 用户名 +- `authorities` — 权限列表 +- `extra` — 业务扩展字段(JSON 字符串,可通过 `parseExtra(Class)` 反序列化) +- `expireTime` / `remindTime` — 过期时间与续期提醒时间 +- `canRestToken()` — 判断是否需要自动续期 +- `getAuthenticationToken()` — 转换为 Spring Security 的 `UsernamePasswordAuthenticationToken` + +### 5. 自动装配链路 + +`AutoConfiguration` 自动完成以下装配: + +1. 注册 `SecurityFilterChain`,配置 URL 授权规则、异常处理、登录/登出端点 +2. 通过 `HttpSecurityConfigurer` 注入 `MyLoginFilter`(登录)和 `MyAuthenticationFilter`(Token 鉴权) +3. 注册 `DaoAuthenticationProvider` 绑定 `UserDetailsService` 和 `PasswordEncoder` +4. 根据 `disableCors` 配置自动注册 CORS 映射 + +## 使用实例 + +### 示例 1:自定义 UserDetailsService 对接数据库 + +```java +@Service +public class DatabaseUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public DatabaseUserDetailsService(UserRepository userRepository, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserEntity entity = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); + + return User.withUsername(entity.getUsername()) + .password(entity.getPassword()) + .roles(entity.getRole()) + .build(); + } +} +``` + +注册此 Bean 后,框架自动替换默认的内存用户管理器。 + +### 示例 2:自定义登录处理器(增加验证码校验) + +```java +@Component +public class CaptchaSecurityLoginHandler implements SecurityLoginHandler { + + private final CaptchaService captchaService; + + public CaptchaSecurityLoginHandler(CaptchaService captchaService) { + this.captchaService = captchaService; + } + + @Override + public void preHandle(HttpServletRequest request, HttpServletResponse response, + LoginRequest loginRequest) throws Exception { + String captchaCode = loginRequest.getString("captcha"); + String captchaKey = loginRequest.getString("captchaKey"); + if (!captchaService.verify(captchaKey, captchaCode)) { + throw new AuthenticationServiceException("验证码错误"); + } + } + + @Override + public LoginResponse postHandle(HttpServletRequest request, HttpServletResponse response, + LoginRequest loginRequest, UserDetails user, Token token) { + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setToken(token.getToken()); + loginResponse.setUsername(token.getUsername()); + loginResponse.setAuthorities(token.getAuthorities()); + // 可在此处追加额外的响应字段 + return loginResponse; + } +} +``` + +### 示例 3:Token 验证后注入上下文 + +```java +@Component +public class TenantAuthenticationTokenFilter implements AuthenticationTokenFilter { + + @Override + public void doFilter(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getPrincipal() instanceof Token token) { + // 从 Token extra 中提取租户 ID 并放入线程上下文 + TenantContext context = token.parseExtra(TenantContext.class); + if (context != null) { + TenantHolder.set(context.getTenantId()); + } + } + } +} +``` + +### 示例 4:自定义 HttpSecurity 配置 + +```java +@Component +public class CustomHttpSecurityCustomer implements HttpSecurityCustomer { + + @Override + public void customize(HttpSecurity security) throws Exception { + // 启用 OAuth2 Resource Server + security.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); + // 添加自定义 Header + security.headers(headers -> headers + .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))); + } +} +``` + +### 示例 5:前端调用登录接口 + +```bash +curl -X POST http://localhost:8090/user/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' +``` + +成功响应: + +```json +{ + "success": true, + "data": { + "token": "eyJhbGciOiJIUzI1NiJ9...", + "username": "admin", + "authorities": ["ROLE_ADMIN"] + } +} +``` + +后续请求携带 Token: + +```bash +curl http://localhost:8090/api/users \ + -H "Authorization: eyJhbGciOiJIUzI1NiJ9..." +``` diff --git a/docs/capabilities/unified-response.md b/docs/capabilities/unified-response.md deleted file mode 100644 index 7c7ca82e1..000000000 --- a/docs/capabilities/unified-response.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: unified-response -description: 统一响应封装体系(Response / SingleResponse / MultiResponse / MapResponse),标准化 API 返回格式 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter -symbols: - - Response - - SingleResponse - - MultiResponse - - MapResponse -content_hash: 1dfe2559a6b514effb51735ca8ca49aba308959e02814c5862b842eed3b04fb1 ---- - -## 解决什么问题 - -前后端分离项目中,API 返回格式需要统一规范,便于前端统一处理。本能力提供了标准化的响应封装: - -- **统一结构**:所有 API 返回 `{success, errCode, errMessage, data}` 标准格式 -- **泛型支持**:`SingleResponse` 和 `MultiResponse` 支持任意数据类型 -- **分页集成**:`MultiResponse.of(Page)` 直接从 Spring Data Page 构建响应 -- **失败标准化**:`Response.buildFailure(code, message)` 统一错误响应 - -## 如何使用 - -### 成功响应 - -```java -// 无数据 -return Response.buildSuccess(); - -// 单对象 -return SingleResponse.of(user); - -// 列表 + 总数 -return MultiResponse.of(userList, totalCount); - -// 从 Spring Data Page 构建 -Page page = userRepository.findAll(PageRequest.of(0, 20)); -return MultiResponse.of(page); -``` - -### 失败响应 - -```java -return Response.buildFailure("user.not.found", "用户不存在"); -``` - -### 响应结构 - -```json -// SingleResponse -{ "success": true, "errCode": null, "errMessage": null, "data": { "id": 1, "name": "张三" } } - -// MultiResponse -{ "success": true, "data": { "total": 100, "list": [...] } } - -// 失败响应 -{ "success": false, "errCode": "user.not.found", "errMessage": "用户不存在" } -``` - -## 使用实例 - -```java -@RestController -@RequestMapping("/api/users") -public class UserController { - - @GetMapping("/{id}") - public SingleResponse get(@PathVariable Long id) { - User user = userService.findById(id); - if (user == null) { - throw new LocaleMessageException("user.not.found", "用户不存在"); - } - return SingleResponse.of(user); - } - - @GetMapping - public MultiResponse list() { - Page page = userRepository.findAll( - PageRequest.of(0, 20).addFilter("status", "ACTIVE") - ); - return MultiResponse.of(page); - } -} -``` diff --git a/docs/capabilities/workflow-engine.md b/docs/capabilities/workflow-engine.md deleted file mode 100644 index c5cda07c1..000000000 --- a/docs/capabilities/workflow-engine.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: workflow-engine -description: 轻量级工作流引擎,支持流程定义、节点流转、审批、退回、委托、会签、抄送、数据快照与事件通知 -status: 已实现 -scope: 后端 -source: 项目自有 -import: com.codingapi.springboot:springboot-starter-flow -symbols: - - FlowWork - - FlowNode - - FlowRelation - - FlowRecord - - FlowBackup - - FlowProcess - - FlowWorkBuilder - - SchemaReader - - FlowNodeService - - FlowStartService - - FlowStepService - - FlowSubmitService - - FlowBackService - - FlowRecallService - - FlowTransferService - - FlowSession - - FlowApprovalEvent - - TitleGenerator - - OperatorMatcher - - OutTrigger - - IFlowOperator - - FlowRecordRepository - - FlowWorkRepository - - FlowOperatorRepository - - BindDataSnapshot - - IBindData -content_hash: 42954fc1cb16f19b5ed4693dfa2ba2887cc9a90673dbb5bafd1dd7c291631712 ---- - -## 解决什么问题 - -企业应用中审批流程(请假、报销、合同审批等)是高频需求。本工作流引擎解决了以下问题: - -- **流程定义与构建**:通过 `FlowWorkBuilder` 或 JSON Schema 定义流程节点和流转关系 -- **灵活的节点流转**:支持条件分支、退回、委托、抄送、会签等多种流转模式 -- **操作者匹配**:通过 Groovy 脚本动态匹配节点审批人 -- **数据快照**:使用 Kryo 深拷贝保存审批时的业务数据快照 -- **事件驱动**:每个状态变更推送 `FlowApprovalEvent`,与事件系统集成 - -## 如何使用 - -### 核心领域模型 - -- `FlowWork` — 流程定义(包含节点和关系) -- `FlowNode` — 流程节点(开始/审批/传阅/结束) -- `FlowRelation` — 节点间关系(含条件触发器) -- `FlowRecord` — 审批记录 -- `FlowBackup` — 流程版本快照 - -### 流程发起 - -```java -@Autowired -private FlowService flowService; - -// 发起流程 -FlowResult result = flowService.start("leave-approval", bindData, createOperator); -``` - -### 流程审批 - -```java -// 提交审批意见 -Opinion opinion = Opinion.builder() - .pass(true) - .comment("同意") - .build(); -flowService.submit(processId, opinion, currentOperator); -``` - -### 流程退回 - -```java -flowService.back(processId, opinion, currentOperator); -``` - -### 监听审批事件 - -```java -@Service -public class LeaveHandler implements IHandler { - @Override - public void handler(FlowApprovalEvent event) { - if (event.isFinish() && event.match(LeaveForm.class)) { - // 审批通过,执行业务逻辑 - } - } -} -``` - -## 使用实例 - -参考 `example` 模块中的请假流程示例: -- 流程定义:`FlowWorkCmd` 通过 `FlowWorkBuilder` 构建 -- 审批处理:`LeaveHandler` 监听 `FlowApprovalEvent` -- 数据绑定:`LeaveForm implements IBindData` diff --git a/docs/conventions/ddd-layered-architecture.md b/docs/conventions/ddd-layered-architecture.md deleted file mode 100644 index b49a1024d..000000000 --- a/docs/conventions/ddd-layered-architecture.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -name: ddd-layered-architecture -description: DDD 分层架构规范 — 遵循 interface → app → domain ← infra 四层依赖规则,domain 层定义接口,infra 层提供实现 -status: 已实现 -scope: 后端 -source: 项目自有 -symbols: - - IDomain -content_hash: dc7a9d76224be09498f0278ec05c56ea34b0c5e2dbb80850ccb826fabe721da6 ---- - -## 解决什么问题 - -不遵守此规范会导致: -- 业务逻辑散落在 Controller 或基础设施层,难以测试和复用 -- 层间循环依赖,修改一处牵动全局 -- 领域模型贫血(只有 getter/setter,没有业务行为) -- 基础设施变更(如换数据库、换消息队列)影响业务代码 - -## 如何使用 - -### 四层架构 - -``` -example/ - example-interface/ ← 接口层:接收外部请求 - example-app/ ← 应用层:编排业务流程 - example-domain/ ← 领域层:核心业务逻辑 - example-infra/ ← 基础设施层:技术实现 -``` - -### 依赖规则 - -``` -interface → app → domain ← infra -``` - -- **interface → app**:接口层调用应用层服务 -- **app → domain**:应用层调用领域层服务和实体 -- **infra → domain**:基础设施层实现领域层定义的接口 -- **domain 不依赖任何层**:领域层是核心,独立存在 - -### 各层职责 - -| 层 | 职责 | 包含 | -|----|------|------| -| **interface** | 接收 HTTP 请求,参数转换,响应封装 | Controller、Handler(事件处理)、Runner | -| **app** | 编排业务流程,调用领域服务,管理事务 | 命令服务(cmd)、查询服务(query) | -| **domain** | 核心业务逻辑,定义 Repository 接口和 Gateway 接口 | Entity、Repository(接口)、Service、Event | -| **infra** | 技术实现:数据库、缓存、消息队列、外部 API | Repository(实现)、Entity(JPA)、Gateway(实现) | - -### CQRS 模式 - -应用层按读写分离: -- **cmd(命令侧)**:写操作,调用领域服务 -- **query(查询侧)**:读操作,直接查询数据库 - -## 使用实例 - -✅ **正确示例**: - -```java -// domain 层 — 定义接口 -public interface UserRepository { - Optional findById(Long id); - User save(User user); -} - -// infra 层 — 提供实现 -@Repository -public class UserRepositoryImpl implements UserRepository { - @Override - public Optional findById(Long id) { - return jpaRepository.findById(id).map(UserConvertor::toDomain); - } -} - -// app 层 — 编排 -@Service -public class UserCommandService { - @Autowired - private UserRepository userRepository; // 依赖 domain 层接口 - - @Transactional - public void registerUser(RegisterCmd cmd) { - User user = new User(cmd.getUsername(), cmd.getEmail()); - userRepository.save(user); - EventPusher.push(new UserRegisteredEvent(user.getId())); - } -} - -// interface 层 — 接收请求 -@RestController -@RequestMapping("/api/users") -public class UserController { - @Autowired - private UserCommandService commandService; // 依赖 app 层 - - @PostMapping("/register") - public Response register(@RequestBody RegisterCmd cmd) { - commandService.registerUser(cmd); - return Response.buildSuccess(); - } -} -``` - -❌ **错误示例**: - -```java -// domain 层直接依赖 infra 实现(违反依赖方向) -public class User { - @Autowired - private UserRepositoryImpl repository; // ❌ 不应依赖实现类 -} - -// Controller 直接调用 Repository(跳过 app 层) -@RestController -public class UserController { - @Autowired - private UserRepository userRepository; // ❌ 应通过 app 层 -} - -// 业务逻辑在 Controller 中(应在 domain 层) -@PostMapping -public Response create(@RequestBody UserDTO dto) { - if (dto.getAge() < 18) { /* ❌ 业务逻辑不应在 Controller */ } - // ... -} -``` diff --git a/docs/conventions/event-handler-standard.md b/docs/conventions/event-handler-standard.md deleted file mode 100644 index 161b2b88b..000000000 --- a/docs/conventions/event-handler-standard.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -name: event-handler-standard -description: 事件处理规范 — Handler 必须实现 IHandler 接口并注册为 Spring Bean,事件必须实现 IEvent 接口 -status: 已实现 -scope: 后端 -source: 项目自有 -symbols: - - IHandler - - IEvent - - ISyncEvent - - IAsyncEvent - - EventPusher -content_hash: 0703329f337a48ff76c084efb93b0c0fcb3ef426ba4b201f81e0c28b646c7405 ---- - -## 解决什么问题 - -不遵守此规范会导致: -- Handler 无法被框架自动发现和注册 -- 事件类型混乱,同步/异步边界不清 -- 多个 Handler 执行顺序不可控 -- 事件循环引用导致系统崩溃 - -## 如何使用 - -### 事件定义规范 - -```java -// 事件必须实现 IEvent(或其子接口) -// 事件类应是不可变的数据载体 -public class OrderCreatedEvent implements ISyncEvent { - private final Long orderId; - private final Long userId; - - public OrderCreatedEvent(Long orderId, Long userId) { - this.orderId = orderId; - this.userId = userId; - } - - public Long getOrderId() { return orderId; } - public Long getUserId() { return userId; } -} -``` - -### 事件类型选择 - -| 接口 | 执行方式 | 适用场景 | -|------|----------|----------| -| `IEvent` | 同步(默认) | 一般业务事件 | -| `ISyncEvent` | 同步 | 需要保证执行顺序的事件 | -| `IAsyncEvent` | 异步(线程池) | 通知类、日志类非关键事件 | - -### Handler 规范 - -```java -// 1. 必须注册为 Spring Bean(@Service / @Component) -// 2. 泛型参数指定订阅的事件类型 -// 3. 不应在 Handler 中抛出未处理异常 -@Service -public class OrderNotifyHandler implements IHandler { - - @Override - public int order() { - return 10; // 排序值,数值小的先执行 - } - - @Override - public void handler(OrderCreatedEvent event) { - // 处理事件逻辑 - notifyService.sendOrderNotification(event.getOrderId()); - } - - @Override - public void error(Exception exception) throws Exception { - // 异常处理:记录日志但不影响其他 Handler - log.error("订单通知发送失败: {}", exception.getMessage()); - } -} -``` - -### 推送规范 - -```java -// ✅ 使用 EventPusher.push() 推送 -EventPusher.push(new OrderCreatedEvent(orderId, userId)); - -// ✅ 允许循环事件的场景(需谨慎) -EventPusher.push(event, true); - -// ❌ 不要直接使用 Spring ApplicationEventPublisher -applicationEventPublisher.publishEvent(event); // 绕过框架的事件管理 -``` - -## 使用实例 - -✅ **正确示例**: -```java -// 事件定义 — 不可变数据载体 -public class UserRegisteredEvent implements ISyncEvent { - private final Long userId; - public UserRegisteredEvent(Long userId) { this.userId = userId; } - public Long getUserId() { return userId; } -} - -// 推送 -EventPusher.push(new UserRegisteredEvent(user.getId())); - -// 订阅 — Spring Bean + IHandler 泛型 -@Service -public class WelcomeEmailHandler implements IHandler { - @Override - public void handler(UserRegisteredEvent event) { - emailService.sendWelcome(event.getUserId()); - } -} -``` - -❌ **错误示例**: -```java -// 事件包含可变状态 -public class BadEvent implements IEvent { - public List mutableList = new ArrayList<>(); // 不应暴露可变集合 -} - -// Handler 不是 Spring Bean — 不会被自动注册 -public class OrphanHandler implements IHandler { - @Override - public void handler(MyEvent event) { /* 永远不会被调用 */ } -} -``` diff --git a/docs/conventions/global-exception-handling.md b/docs/conventions/global-exception-handling.md deleted file mode 100644 index baf628f78..000000000 --- a/docs/conventions/global-exception-handling.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: global-exception-handling -description: 全局异常处理规范 — 业务异常必须使用 LocaleMessageException,禁止在 Controller 中 try-catch 吞没异常 -status: 已实现 -scope: 后端 -source: 项目自有 -symbols: - - LocaleMessageException - - BasicHandlerExceptionResolverConfiguration - - ServletExceptionHandler -content_hash: e3029dc411b34af5ea488d45c2ac414e2ea7578b571e681e0d7dc938ae75f052 ---- - -## 解决什么问题 - -不遵守此规范会导致: -- 异常堆栈信息直接暴露给前端,存在安全风险 -- 不同模块的错误响应格式不一致 -- 异常被 try-catch 吞没后,问题难以排查 -- 无法支持国际化错误消息 - -## 如何使用 - -### 异常类型规范 - -| 异常类型 | 使用场景 | -|----------|----------| -| `LocaleMessageException` | 所有业务异常(参数校验失败、数据不存在、状态错误等) | -| `IllegalArgumentException` | 框架级参数校验(不推荐在业务层使用) | -| `RuntimeException` | 不可预期的系统异常 | - -### 错误码命名规范 - -错误码采用 **英文点分隔** 的格式:`模块.实体.错误类型` - -```java -// ✅ 正确的错误码 -throw new LocaleMessageException("user.not.found", "用户不存在"); -throw new LocaleMessageException("order.status.invalid", "订单状态无效"); -throw new LocaleMessageException("auth.permission.denied", "权限不足"); - -// ❌ 错误的错误码 -throw new LocaleMessageException("USER_NOT_FOUND", "用户不存在"); // 不应大写 -throw new LocaleMessageException("userNotFound", "用户不存在"); // 不应驼峰 -``` - -### 异常处理规则 - -1. **Controller 层**:不 try-catch,直接抛出 `LocaleMessageException`,由全局处理器捕获 -2. **Service 层**:业务校验失败抛 `LocaleMessageException`,系统异常可包装后向上抛 -3. **基础设施层**:捕获底层异常,转换为 `LocaleMessageException` 向上传递 - -## 使用实例 - -✅ **正确示例**: -```java -@Service -public class UserService { - public User findById(Long id) { - return userRepository.findById(id) - .orElseThrow(() -> new LocaleMessageException("user.not.found", "用户不存在")); - } - - public void delete(Long id) { - User user = findById(id); - if ("ADMIN".equals(user.getRole())) { - throw new LocaleMessageException("user.admin.cannot.delete", "管理员账户不可删除"); - } - userRepository.delete(user); - } -} - -@RestController -public class UserController { - @DeleteMapping("/{id}") - public Response delete(@PathVariable Long id) { - userService.delete(id); // 不 try-catch,异常由全局处理器捕获 - return Response.buildSuccess(); - } -} -``` - -❌ **错误示例**: -```java -// 在 Controller 中 try-catch 吞没异常 -@GetMapping("/{id}") -public SingleResponse get(@PathVariable Long id) { - try { - User user = userService.findById(id); - return SingleResponse.of(user); - } catch (Exception e) { - log.error("error", e); - return null; // 返回 null,前端无法识别错误 - } -} - -// 直接抛出原始异常 -throw new RuntimeException("数据库连接失败"); // 应包装为 LocaleMessageException -``` diff --git a/docs/conventions/index.md b/docs/conventions/index.md index 0b6ad7086..df2f11ffb 100644 --- a/docs/conventions/index.md +++ b/docs/conventions/index.md @@ -4,13 +4,14 @@ ## ✅ 已实现 -| 名称 | 描述 | 范围 | 来源 | -|------|------|------|------| -| [ddd-layered-architecture](./ddd-layered-architecture.md) | DDD 分层架构规范 — 遵循 interface → app → domain ← infra 四层依赖规则,domain 层定义接口,infra 层提供实现 | 后端 | 项目自有 | -| [event-handler-standard](./event-handler-standard.md) | 事件处理规范 — Handler 必须实现 IHandler 接口并注册为 Spring Bean,事件必须实现 IEvent 接口 | 后端 | 项目自有 | -| [global-exception-handling](./global-exception-handling.md) | 全局异常处理规范 — 业务异常必须使用 LocaleMessageException,禁止在 Controller 中 try-catch 吞没异常 | 后端 | 项目自有 | -| [unified-response-format](./unified-response-format.md) | 统一响应格式规范 — 所有 API 必须使用 Response/SingleResponse/MultiResponse 标准封装返回 | 后端 | 项目自有 | +| 名称 | 模块 | 描述 | 范围 | 来源 | +|------|------|------|------|------| +| [springboot-starter/ddd-layered-architecture](./springboot-starter/ddd-layered-architecture.md) | springboot-starter | DDD 分层架构规范,定义 interface/app/domain/infra 四层的依赖规则和职责划分 | 后端 | 项目自有 | +| [springboot-starter/event-driven-convention](./springboot-starter/event-driven-convention.md) | springboot-starter | 事件驱动开发规范,定义 IEvent/IHandler 的使用约定和事件处理模式 | 后端 | 项目自有 | +| [springboot-starter/exception-handling-convention](./springboot-starter/exception-handling-convention.md) | springboot-starter | 全局异常处理规范,定义异常抛出和捕获的标准模式 | 后端 | 项目自有 | +| [springboot-starter/response-convention](./springboot-starter/response-convention.md) | springboot-starter | 统一响应格式规范,定义 Controller 层返回值的标准格式 | 后端 | 项目自有 | +| [springboot-starter-data-fast/dynamic-query-convention](./springboot-starter-data-fast/dynamic-query-convention.md) | springboot-starter-data-fast | 动态查询规范,定义 PageRequest + RequestFilter + FastRepository 的使用模式 | 后端 | 项目自有 | --- -**统计**: 共 4 篇 — 已实现 4 / 计划中 0 / 已废弃 0 +**统计**: 共 5 篇 — 已实现 5 / 计划中 0 / 已废弃 0 diff --git a/docs/conventions/springboot-starter-data-fast/dynamic-query-convention.md b/docs/conventions/springboot-starter-data-fast/dynamic-query-convention.md new file mode 100644 index 000000000..f4617ca49 --- /dev/null +++ b/docs/conventions/springboot-starter-data-fast/dynamic-query-convention.md @@ -0,0 +1,148 @@ +--- +name: springboot-starter-data-fast/dynamic-query-convention +module: springboot-starter-data-fast +description: 动态查询规范,定义 PageRequest + RequestFilter + FastRepository 的使用模式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter-data-fast" +content_hash: 0369b4e3ba19d679dea69480cf4c5f6e4687c186c515e56883c004884711ab83 +--- + +## 解决什么问题 + +在企业级应用中,列表查询往往需要根据前端传入的动态条件进行过滤。如果不遵守本规范,会导致以下问题: + +1. **SQL 注入风险**:在 Service 层手动拼接 SQL 字符串实现动态查询,极易引入 SQL 注入漏洞。 +2. **代码重复与维护困难**:每个实体的动态查询都手写 `if-else` 判断和原生 SQL,导致大量重复代码,新增过滤字段需修改多处。 +3. **分页逻辑不一致**:各模块自行处理分页参数(偏移量计算、排序),缺乏统一标准,容易出现边界错误。 +4. **CQRS 职责混乱**:查询逻辑与命令逻辑耦合在同一 Service 中,违反读写分离原则,增加系统复杂度。 +5. **类型安全缺失**:使用 Map 或 JSON 传递查询条件时,缺乏编译期检查,运行时才发现字段名拼写错误或类型不匹配。 +6. **框架能力浪费**:不使用 `FastRepository` 提供的自动 Example/HQL 构建能力,绕过框架自建查询机制,失去统一的 SQL 拦截(如数据权限)支持。 + +## 如何使用 + +### 核心规则 + +1. **列表查询统一使用 `PageRequest` 构建分页参数** + - 通过 `PageRequest.of(page, size)` 或 `new PageRequest()` 创建请求对象。 + - 禁止直接使用 Spring Data 原生 `org.springframework.data.domain.PageRequest` 来承载业务过滤条件。 + +2. **动态过滤条件通过 `PageRequest.addFilter()` 方法添加** + - 简单等值过滤:`pageRequest.addFilter("name", "张三")`,默认使用 `Relation.EQ`。 + - 指定关系过滤:`pageRequest.addFilter("age", Relation.GT, 18)`。 + - 组合过滤:使用 `andFilter(Filter...)` 和 `orFilters(Filter...)` 构建复杂条件。 + +3. **过滤关系使用 `Relation` 枚举** + - 可用关系包括:`EQ`、`GT`、`LT`、`GTE`、`LTE`、`LIKE`、`IN` 等。 + - 所有过滤关系必须通过枚举表达,禁止硬编码字符串比较运算符。 + +4. **Repository 接口需继承 `FastRepository`** + - 实体 Repository 必须扩展 `FastRepository` 以获得动态查询能力。 + - `FastRepository` 同时继承了 `JpaRepository`、`JpaSpecificationExecutor`、`DynamicRepository` 和 `DynamicNativeRepository`。 + +5. **调用 `FastRepository.findAll(PageRequest)` 执行动态查询** + - 当 `PageRequest` 包含 Filter 时,自动通过 `ExampleBuilder` 构建 Example 查询。 + - 需要 HQL 级别查询时,使用 `pageRequest(PageRequest)` 方法,自动通过 `DynamicSQLBuilder` 生成 HQL。 + - 禁止绕过 `FastRepository` 提供的默认方法自行编写动态查询逻辑。 + +6. **禁止在 Service 层手写原生 SQL 实现动态查询** + - 不允许使用 `@Query(nativeQuery = true)` 或 `EntityManager.createNativeQuery()` 拼接动态条件。 + - 所有动态过滤必须委托给 `FastRepository` 的 Filter 机制完成。 + +7. **查询服务(CQRS Query 侧)应独立于命令服务** + - 查询服务放在 `*-app-query` 模块中,仅负责读取操作。 + - 命令服务放在 `*-app-cmd-*` 模块中,负责写入与领域编排。 + - 两者不得互相依赖或合并为同一个类。 + +### 命名约定 + +- 查询服务类命名:`{Entity}QueryService`,位于 `*.query` 包下。 +- 命令服务类命名:`{Entity}CmdService`,位于 `*.cmd` 包下。 +- Controller 中的列表查询方法参数类型统一为 `PageRequest`。 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. Repository 继承 FastRepository +public interface UserRepository extends FastRepository { +} + +// 2. 查询服务独立于命令服务 +@Service +public class UserQueryService { + + @Autowired + private UserRepository userRepository; + + public Page listUsers(String name, Integer minAge, int page, int size) { + PageRequest request = PageRequest.of(page, size); + + // 动态添加过滤条件 + if (StringUtils.hasText(name)) { + request.addFilter("name", Relation.LIKE, name); + } + if (minAge != null) { + request.addFilter("age", Relation.GTE, minAge); + } + + // 委托 FastRepository 自动构建查询 + return userRepository.findAll(request); + } +} + +// 3. Controller 接收 PageRequest +@GetMapping("/users") +public MultiResponse list( + @RequestParam(required = false) String name, + @RequestParam(required = false) Integer minAge, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + Page result = userQueryService.listUsers(name, minAge, page, size); + return ResponseUtils.toMultiResponse(result); +} +``` + +### ❌ 错误示例 + +```java +// 错误 1: Repository 未继承 FastRepository,丧失动态查询能力 +public interface UserRepository extends JpaRepository { +} + +// 错误 2: Service 中手写原生 SQL 拼接动态条件 +@Service +public class UserService { + + @PersistenceContext + private EntityManager entityManager; + + public List listUsers(String name, Integer minAge) { + StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1"); + if (name != null) { + sql.append(" AND name LIKE '%").append(name).append("%'"); // SQL 注入风险! + } + if (minAge != null) { + sql.append(" AND age >= ").append(minAge); + } + return entityManager.createNativeQuery(sql.toString(), User.class).getResultList(); + } +} + +// 错误 3: 查询与命令逻辑混合在同一个 Service 中 +@Service +public class UserService { + public Page list(...) { /* 查询逻辑 */ } + public void createUser(...) { /* 命令逻辑 */ } + public void deleteUser(...) { /* 命令逻辑 */ } +} + +// 错误 4: 使用 Spring Data 原生 PageRequest,无法承载 Filter +@GetMapping("/users") +public Page list(org.springframework.data.domain.PageRequest pageable) { + // 丢失了框架的动态过滤能力 + return userRepository.findAll(pageable); +} +``` diff --git a/docs/conventions/springboot-starter/ddd-layered-architecture.md b/docs/conventions/springboot-starter/ddd-layered-architecture.md new file mode 100644 index 000000000..c1a97a8cd --- /dev/null +++ b/docs/conventions/springboot-starter/ddd-layered-architecture.md @@ -0,0 +1,224 @@ +--- +name: springboot-starter/ddd-layered-architecture +module: springboot-starter +description: DDD 分层架构规范,定义 interface/app/domain/infra 四层的依赖规则和职责划分 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +--- + +## 解决什么问题 + +不遵守 DDD 分层架构规范会导致以下问题: + +1. **领域逻辑污染**:domain 层直接引用 infra 层实现类(如 JPA Repository、Redis Client),导致业务规则与基础设施耦合,无法独立演进和测试。 +2. **依赖方向混乱**:若 domain 反向依赖 app 或 interface,修改一个 API 接口可能牵连整个领域模型重构,变更成本指数级增长。 +3. **CQRS 读写混淆**:查询服务与命令服务混杂在同一模块中,复杂查询被迫走聚合根加载全量数据,性能瓶颈难以定位和优化。 +4. **接口层职责膨胀**:Controller 中直接编排业务流程、拼装 DTO、处理事件,导致接口层成为"上帝类",复用性和可维护性急剧下降。 +5. **Repository 契约缺失**:domain 层直接使用具体持久化实现而非接口,切换存储方案(如从 MySQL 迁移到 MongoDB)需要改动所有调用方代码。 + +## 如何使用 + +### 四层架构与依赖方向 + +``` +interface → app → domain ← infra +``` + +| 层级 | 模块命名约定 | 核心职责 | 允许依赖 | +|------|-------------|---------|---------| +| **interface**(接口层) | `*-interface` | Controller(REST API)、Handler(事件处理器)、Runner(启动任务) | app | +| **app**(应用层) | `*-app-query` / `*-app-cmd-*` | query:CQRS 查询服务;cmd:命令服务与领域编排 | domain | +| **domain**(领域层) | `*-domain-*` | Entity、ValueObject、DomainService、Repository 接口、DomainEvent | 无外部框架 | +| **infra**(基础设施层) | `*-infra-*` | Repository 实现、Gateway 实现、外部服务适配、持久化配置 | domain | + +### 核心规则 + +1. **依赖方向严格单向**:`interface → app → domain ← infra`。禁止反向依赖和跨层依赖。 +2. **domain 层纯业务**:domain 层只包含领域模型和业务逻辑,不依赖 Spring、JPA、Redis 等任何外部框架。 +3. **Repository 接口在 domain,实现在 infra**:domain 层定义 Repository 接口,infra 层提供具体实现并通过 Spring Bean 注入。 +4. **app 层按 CQRS 拆分**:查询服务(query)与命令服务(cmd)分模块,query 侧可直接读取视图/DTO,cmd 侧通过聚合根执行业务操作。 +5. **interface 层仅做适配**:Controller 只做参数校验、权限检查和响应封装,业务编排委托给 app 层;Handler 只负责事件监听与转发。 +6. **禁止 domain 引用 infra 实现类**:domain 层不得 import infra 包下的任何类,包括具体的 Repository 实现、DAO、Client 等。 + +### 包结构约定 + +``` +{bounded-context}/ + {context}-interface/ # 接口层 + controller/ + handler/ + runner/ + {context}-app/ # 应用层 + {context}-app-query/ # CQRS Query 侧 + {context}-app-cmd-domain/ # CQRS Command 侧(领域编排) + {context}-domain/ # 领域层 + model/ # Entity, ValueObject + repository/ # Repository 接口 + service/ # DomainService + event/ # DomainEvent + {context}-infra/ # 基础设施层 + jpa/ # Repository 实现 + gateway/ # 外部服务适配 +``` + +## 使用实例 + +### ✅ 正确示例 + +**domain 层定义 Repository 接口:** + +```java +// example-domain-user/src/.../repository/UserRepository.java +package com.example.domain.user.repository; + +public interface UserRepository { + User findById(Long id); + void save(User user); +} +``` + +**infra 层提供 Repository 实现:** + +```java +// example-infra-jpa/src/.../jpa/UserRepositoryImpl.java +package com.example.infra.jpa; + +import com.example.domain.user.repository.UserRepository; +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepositoryImpl implements UserRepository { + private final UserJpaDao userJpaDao; + + @Override + public User findById(Long id) { + return userJpaDao.findById(id).map(UserConverter::toDomain).orElse(null); + } + + @Override + public void save(User user) { + userJpaDao.save(UserConverter.toEntity(user)); + } +} +``` + +**app 层按 CQRS 拆分:** + +```java +// example-app-query: 查询服务,直接返回 DTO +@Service +public class UserQueryService { + private final UserReadDao userReadDao; // 可直接读视图 + + public SingleResponse getUser(Long id) { + return SingleResponse.of(userReadDao.findUserDTO(id)); + } +} + +// example-app-cmd-domain: 命令服务,通过聚合根操作 +@Service +public class UserCommandService { + private final UserRepository userRepository; // domain 层接口 + + @Transactional + public Response createUser(CreateUserCmd cmd) { + User user = User.create(cmd.getName(), cmd.getEmail()); + userRepository.save(user); + return Response.success(); + } +} +``` + +**interface 层仅做适配:** + +```java +@RestController +@RequestMapping("/api/users") +public class UserController { + private final UserQueryService queryService; + private final UserCommandService commandService; + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + return queryService.getUser(id); // 委托 app 层 + } + + @PostMapping + public Response createUser(@Valid @RequestBody CreateUserCmd cmd) { + return commandService.createUser(cmd); // 委托 app 层 + } +} +``` + +### ❌ 错误示例 + +**domain 层直接引用 infra 实现类:** + +```java +// ❌ domain 层 import 了 infra 包的类 +package com.example.domain.user.service; + +import com.example.infra.jpa.UserJpaDao; // 违规!domain 不应依赖 infra + +@Service +public class UserService { + private final UserJpaDao userJpaDao; // 应使用 UserRepository 接口 + + public User findUser(Long id) { + return userJpaDao.findById(id).orElse(null); + } +} +``` + +**依赖方向反转:** + +```java +// ❌ domain 层反向依赖 app 层 +package com.example.domain.user.model; + +import com.example.app.cmd.domain.UserService; // 违规!domain 不应依赖 app + +public class User { + public void activate() { + UserService userService = ...; // 领域对象不应调用应用服务 + userService.sendActivationEmail(this); + } +} +``` + +**app 层未拆分 CQRS,查询走聚合根:** + +```java +// ❌ 查询和命令混在一起,列表查询加载完整聚合根 +@Service +public class UserService { + private final UserRepository userRepository; + + // 列表查询不应加载完整 User 聚合根 + public List listUsers(String keyword) { + return userRepository.findAll().stream() + .filter(u -> u.getName().contains(keyword)) + .collect(Collectors.toList()); // 内存过滤,性能灾难 + } +} +``` + +**Controller 中直接编排业务逻辑:** + +```java +// ❌ Controller 承担了本应属于 app 层的职责 +@PostMapping("/orders") +public Response createOrder(@RequestBody OrderCmd cmd) { + User user = userRepository.findById(cmd.getUserId()); // 应在 app 层 + Product product = productRepository.findById(cmd.getProductId()); // 应在 app 层 + if (product.getStock() < cmd.getQuantity()) { // 业务校验应在 domain + return Response.error("库存不足"); + } + Order order = new Order(user, product, cmd.getQuantity()); // 领域创建应在 app/cmd + orderRepository.save(order); // 应在 app 层 + eventPusher.push(new OrderCreatedEvent(order)); // 事件推送应在 app 层 + return Response.success(order.getId()); +} +``` diff --git a/docs/conventions/springboot-starter/event-driven-convention.md b/docs/conventions/springboot-starter/event-driven-convention.md new file mode 100644 index 000000000..3cfb076bc --- /dev/null +++ b/docs/conventions/springboot-starter/event-driven-convention.md @@ -0,0 +1,217 @@ +--- +name: springboot-starter/event-driven-convention +module: springboot-starter +description: 事件驱动开发规范,定义 IEvent/IHandler 的使用约定和事件处理模式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +content_hash: 02975087ebd599ed5978c834297c16c37ae4e31ec37d964d753353b1de9fb07d +--- + +## 解决什么问题 + +不遵守事件驱动开发规范会导致以下问题: + +1. **事务耦合风险**:将事件处理与主业务事务强绑定,导致分支业务的失败回滚整个主事务,破坏领域事件的独立性原则。当分支逻辑(如通知、日志、缓存更新)出错时,不应影响核心业务流程的提交。 + +2. **事件类型混乱**:不区分同步事件与异步事件,导致关键业务事件被异步执行产生时序问题,或非关键事件被同步阻塞影响主流程性能。 + +3. **Handler 注册失效**:未通过 `@Service` 注解将 Handler 注册为 Spring Bean,导致框架无法自动发现处理器,事件推送后无响应且难以排查。 + +4. **执行顺序不可控**:多个 Handler 订阅同一事件时未指定 `order()`,导致处理顺序依赖 Bean 加载顺序,在不同环境或重启后行为不一致。 + +5. **异常处理缺失**:未实现 `error()` 回调或直接在 `handler()` 中抛出未捕获异常,导致后续 Handler 被跳过且错误信息丢失,事件链中断而无法感知。 + +6. **绕过 EventPusher 直接调用**:手动实例化 Handler 或直接调用处理方法,绕过框架的循环检测、线程池调度和上下文传递机制,引发循环事件死锁或丢失链路追踪信息。 + +## 如何使用 + +### 规则 1:事件类必须实现 IEvent 接口 + +所有事件类必须实现 `IEvent` 接口。根据执行方式选择子接口: + +- **同步事件**:实现 `ISyncEvent`,在当前线程内按顺序执行所有 Handler +- **异步事件**:实现 `IAsyncEvent`,由框架线程池异步调度执行 + +事件类应为纯数据载体,不包含业务逻辑,并实现 `Serializable` 以支持序列化传输。 + +### 规则 2:事件处理器必须实现 IHandler 接口 + +Handler 通过泛型参数声明订阅的事件类型。框架在启动时通过 `HandlerBeanDefinitionRegistrar` 自动扫描所有 `IHandler` 实现并注册到 `ApplicationHandlerUtils`。 + +```java +public interface IHandler { + default int order() { return 0; } + void handler(T event); + default void error(Exception exception) throws Exception { throw exception; } +} +``` + +### 规则 3:事件处理器通过 @Service 注解注册为 Spring Bean + +Handler 必须标注 `@Service`(或 `@Component`)注解,确保被 Spring 容器管理。未注册为 Bean 的 Handler 不会被框架发现和调用。 + +### 规则 4:事件推送统一使用 EventPusher.push() 静态方法 + +所有事件推送必须通过 `EventPusher.push(event)` 发起,禁止手动调用 Handler。框架内置循环事件检测机制,当检测到事件循环推送时自动抛出异常。若确认需要允许循环事件,可使用 `EventPusher.push(event, true)` 关闭检测。 + +### 规则 5:事件不应与主业务强耦合事务绑定 + +事件处理的核心理念是**解耦**。事件对于主业务来说可成功可失败,成功与失败都不应强关联主体业务。若分支逻辑必须与主事务保持一致,应直接使用服务调用而非事件机制。 + +### 规则 6:多个 Handler 通过 order() 方法控制执行顺序 + +当多个 Handler 订阅同一事件时,通过重写 `order()` 方法指定执行优先级。数值越小越先执行,默认值为 0。相同 order 值的 Handler 执行顺序不确定。 + +### 规则 7:Handler 的 error() 回调处理异常 + +`error()` 方法接收 Handler 执行过程中抛出的异常。默认实现会重新抛出异常,这将**阻止后续 Handler 的执行**。若希望某个 Handler 的失败不影响其他 Handler,应在 `error()` 中记录日志而不重新抛出。 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. 定义同步事件 +public class UserCreatedEvent implements ISyncEvent { + private final String userId; + private final String username; + + public UserCreatedEvent(String userId, String username) { + this.userId = userId; + this.username = username; + } + + public String getUserId() { return userId; } + public String getUsername() { return username; } +} + +// 2. 定义异步事件 +public class UserNotificationEvent implements IAsyncEvent { + private final String userId; + private final String message; + + public UserNotificationEvent(String userId, String message) { + this.userId = userId; + this.message = message; + } + + public String getUserId() { return userId; } + public String getMessage() { return message; } +} + +// 3. 注册 Handler 并指定执行顺序 +@Service +public class UserCacheRefreshHandler implements IHandler { + + @Override + public int order() { + return 1; // 优先刷新缓存 + } + + @Override + public void handler(UserCreatedEvent event) { + cacheService.refreshUserCache(event.getUserId()); + } + + @Override + public void error(Exception exception) { + // 缓存刷新失败不影响后续 Handler,仅记录日志 + log.warn("缓存刷新失败: {}", exception.getMessage()); + } +} + +@Service +public class UserAuditLogHandler implements IHandler { + + @Override + public int order() { + return 2; // 缓存之后写审计日志 + } + + @Override + public void handler(UserCreatedEvent event) { + auditService.logUserCreation(event.getUserId(), event.getUsername()); + } +} + +// 4. 在业务中推送事件 +@Service +public class UserService { + + public User createUser(CreateUserCommand command) { + User user = userRepository.save(command.toEntity()); + // 通过 EventPusher 推送,不直接调用 Handler + EventPusher.push(new UserCreatedEvent(user.getId(), user.getUsername())); + return user; + } +} +``` + +### ❌ 错误示例 + +```java +// 错误 1:事件未实现 IEvent 接口 +public class UserCreatedEvent { // ❌ 缺少 ISyncEvent / IAsyncEvent + private String userId; +} + +// 错误 2:Handler 未注册为 Spring Bean +public class UserCacheHandler implements IHandler { // ❌ 缺少 @Service + @Override + public void handler(UserCreatedEvent event) { + cacheService.refreshUserCache(event.getUserId()); + } +} + +// 错误 3:在事务中强绑定事件结果 +@Transactional +public User createUser(CreateUserCommand command) { + User user = userRepository.save(command.toEntity()); + try { + EventPusher.push(new UserCreatedEvent(user.getId(), user.getUsername())); + } catch (Exception e) { + throw new RuntimeException("事件处理失败,回滚事务", e); // ❌ 事件失败不应回滚主事务 + } + return user; +} + +// 错误 4:手动调用 Handler 绕过框架 +@Service +public class UserService { + @Autowired + private UserCacheRefreshHandler cacheHandler; + + public User createUser(CreateUserCommand command) { + User user = userRepository.save(command.toEntity()); + cacheHandler.handler(new UserCreatedEvent(user.getId(), user.getUsername())); // ❌ 绕过 EventPusher + return user; + } +} + +// 错误 5:未处理异常导致事件链中断 +@Service +public class FragileHandler implements IHandler { + @Override + public void handler(UserCreatedEvent event) { + externalService.notify(event.getUserId()); // 可能抛出异常 + } + // ❌ 未重写 error(),默认重新抛出异常,阻止后续 Handler 执行 +} + +// 错误 6:多个 Handler 未指定 order,执行顺序不可预测 +@Service +public class HandlerA implements IHandler { + @Override + public void handler(UserCreatedEvent event) { /* ... */ } + // ❌ 未重写 order(),默认 0,与 HandlerB 顺序不确定 +} + +@Service +public class HandlerB implements IHandler { + @Override + public void handler(UserCreatedEvent event) { /* ... */ } + // ❌ 未重写 order(),默认 0,与 HandlerA 顺序不确定 +} +``` diff --git a/docs/conventions/springboot-starter/exception-handling-convention.md b/docs/conventions/springboot-starter/exception-handling-convention.md new file mode 100644 index 000000000..6f6146175 --- /dev/null +++ b/docs/conventions/springboot-starter/exception-handling-convention.md @@ -0,0 +1,152 @@ +--- +name: springboot-starter/exception-handling-convention +module: springboot-starter +description: 全局异常处理规范,定义异常抛出和捕获的标准模式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +content_hash: 669e54861ccb844ae926652bdd6b30a9a1b8bbc9f9a55ce145d19c283d499596 +--- + +## 解决什么问题 + +不遵守此规范会导致以下问题: + +1. **错误响应格式不一致**:不同开发者在 Controller 中自行 try-catch 并返回自定义格式的错误信息,导致前端无法统一解析 `errCode` / `errMessage` 字段,增加联调和维护成本。 +2. **缺失国际化支持**:直接抛出 `RuntimeException` 或硬编码中文消息,无法根据请求 Locale 切换语言,多语言场景下用户体验差且需要大量改造。 +3. **异常被吞没**:Controller 层使用 try-catch 捕获异常后仅打印日志或返回 null,调用方无法感知业务失败原因,排查线上问题时缺少关键上下文。 +4. **错误码体系混乱**:各模块自行定义错误码常量或直接拼接字符串,缺乏统一的 key → message 映射机制,同一个业务含义可能出现多个不同的 errCode。 +5. **全局拦截失效**:绕过框架的 `HandlerExceptionResolver` 机制手动处理异常,导致日志记录、监控埋点等横切关注点被跳过。 + +## 如何使用 + +### 规则 1:业务异常统一使用 LocaleMessageException + +所有业务校验失败、参数非法、权限不足等场景,必须抛出 `LocaleMessageException`,禁止直接使用 `RuntimeException`、`IllegalArgumentException` 等原生异常。 + +```java +// 构造方式一:errCode + 默认消息(推荐用于简单场景) +throw new LocaleMessageException("user.not.found", "用户不存在"); + +// 构造方式二:仅 errCode,消息从 message.properties 自动解析 +throw new LocaleMessageException("user.not.found"); + +// 构造方式三:带占位符参数,对应 properties 中 user.duplicate=用户名 {0} 已存在 +throw LocaleMessageException.of("user.duplicate", username); +``` + +### 规则 2:异常消息采用 message key + 默认消息格式 + +- 第一个参数为 **errCode**(即 i18n message key),用于前端匹配和国际化查找。 +- 第二个参数为 **默认消息**(defaultMessage),当 message.properties 中未配置该 key 时作为兜底展示。 +- errCode 命名采用 **点分隔小写** 格式,如 `order.status.invalid`、`auth.token.expired`。 + +### 规则 3:依赖 ExceptionConfiguration 全局拦截 + +框架通过 `ExceptionConfiguration` 注册 `LocaleMessage` Bean,并由 `BasicHandlerExceptionResolverConfiguration` 中的 `ServletExceptionHandler` 统一拦截所有 Controller 层异常: + +- `LocaleMessageException` → 提取 `errCode` 和 `errMessage`,返回标准 Response 格式。 +- 其他未知异常 → 使用默认 errCode `system.err`,返回异常原始消息。 + +**无需在任何 Controller 中添加额外的 @ExceptionHandler。** + +### 规则 4:异常响应统一返回 Response 格式 + +全局拦截器返回的 JSON 结构如下: + +```json +{ + "success": false, + "errCode": "user.not.found", + "errMessage": "用户不存在" +} +``` + +前端只需判断 `success === false` 即可进入统一错误处理流程。 + +### 规则 5:禁止在 Controller 中使用 try-catch 吞掉异常 + +Controller 方法应保持简洁,让异常自然向上抛出由全局处理器接管。如需对特定异常做差异化处理(如参数校验),应在 Service 层完成转换后再抛出 `LocaleMessageException`。 + +### 规则 6:禁止直接抛出 RuntimeException + +`RuntimeException` 没有 `errCode` 字段,全局拦截器只能将其归入 `system.err`,丢失了业务语义。所有可预见的业务异常都必须使用 `LocaleMessageException`。 + +## 使用实例 + +### ✅ 正确示例 + +```java +@Service +public class UserService { + + public User getUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new LocaleMessageException("user.not.found", "用户不存在")); + } + + public void createUser(String username) { + if (userRepository.existsByUsername(username)) { + // 使用静态工厂方法 + 占位符参数 + throw LocaleMessageException.of("user.duplicate", username); + } + // ... + } +} + +@RestController +@RequestMapping("/users") +public class UserController { + + @GetMapping("/{id}") + public SingleResponse getUser(@PathVariable Long id) { + // 不需要 try-catch,异常由全局处理器接管 + return SingleResponse.of(userService.getUser(id)); + } +} +``` + +对应的 `messages_zh_CN.properties`: + +```properties +user.not.found=用户不存在 +user.duplicate=用户名 {0} 已存在 +``` + +### ❌ 错误示例 + +```java +// 错误 1:直接抛出 RuntimeException,缺少 errCode +public User getUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("用户不存在")); +} + +// 错误 2:Controller 中 try-catch 吞掉异常 +@GetMapping("/{id}") +public SingleResponse getUser(@PathVariable Long id) { + try { + return SingleResponse.of(userService.getUser(id)); + } catch (Exception e) { + log.error("查询失败", e); + return null; // 前端收到 null,无法区分成功与失败 + } +} + +// 错误 3:Controller 中自行构建错误响应,绕过全局拦截 +@GetMapping("/{id}") +public ResponseEntity getUser(@PathVariable Long id) { + try { + return ResponseEntity.ok(SingleResponse.of(userService.getUser(id))); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("code", 500); + error.put("msg", e.getMessage()); + return ResponseEntity.status(500).body(error); + } +} + +// 错误 4:硬编码中文消息,不支持国际化 +throw new LocaleMessageException("err001", "这个用户找不到啊"); +``` diff --git a/docs/conventions/springboot-starter/response-convention.md b/docs/conventions/springboot-starter/response-convention.md new file mode 100644 index 000000000..1c5c30712 --- /dev/null +++ b/docs/conventions/springboot-starter/response-convention.md @@ -0,0 +1,156 @@ +--- +name: springboot-starter/response-convention +module: springboot-starter +description: 统一响应格式规范,定义 Controller 层返回值的标准格式 +status: 已实现 +scope: 后端 +source: 项目自有 +import: "com.codingapi.springboot:springboot-starter" +content_hash: f34034d99b0e32ca0ef9f72cf98793135fd15e335facfaca548ab3caa9e14cc1 +--- + +## 解决什么问题 + +在 REST API 开发中,如果不对响应格式做统一约束,会导致以下问题: + +- **前端解析困难**:不同接口返回结构不一致(有的直接返回对象、有的返回 Map、有的包裹在自定义结构中),前端需要为每个接口单独适配,增加维护成本。 +- **错误处理碎片化**:没有统一的 `success` / `errCode` / `errMessage` 字段,前端无法用一套逻辑判断请求是否成功、展示错误提示。 +- **分页结构不统一**:列表接口各自定义分页字段名(`total` / `count` / `records` / `items`),前端分页组件难以复用。 +- **API 契约不稳定**:随意返回裸 Map 或临时 DTO,字段增减无感知,容易引发线上联调故障。 +- **国际化与监控缺失基础**:缺少标准化的错误码字段,后续接入国际化、链路追踪、告警分类时改造成本极高。 + +本规范通过强制使用框架提供的 Response 体系,确保所有 API 输出结构一致、可预测、可机器解析。 + +## 如何使用 + +### 核心规则 + +1. **Controller 方法返回值必须使用 Response 体系**:只能返回 `Response`、`SingleResponse`、`MultiResponse` 或 `MapResponse` 之一,禁止返回其他类型。 +2. **单个对象返回**:使用 `SingleResponse.of(data)`。 +3. **列表返回**:使用 `MultiResponse.of(list)` 或 `MultiResponse.of(page)`(接受 Spring Data `Page` 对象)。 +4. **无数据操作成功返回**:使用 `Response.buildSuccess()`。 +5. **失败返回**:使用 `Response.buildFailure(errCode, errMessage)`。 +6. **禁止直接返回 Map 或自定义 DTO 作为 API 响应**。 +7. **响应 JSON 结构固定包含**:`success`(boolean)、`errCode`(string)、`errMessage`(string)、`data`(业务数据,仅 SingleResponse/MultiResponse 携带)。 + +### 补充说明 + +- `SingleResponse.empty()` 用于查询可能为空但语义上成功的场景,返回 `{ success: true, data: null }`。 +- `MultiResponse.of(collection, total)` 用于手动分页场景;`MultiResponse.of(page)` 自动从 Spring Data Page 提取 total。 +- `MultiResponse.empty()` 返回空列表 `{ success: true, data: { total: 0, list: [] } }`。 +- 异常处理应通过全局异常处理器统一转换为 `Response.buildFailure(...)`,Controller 内不要 try-catch 后自行拼装错误响应。 + +## 使用实例 + +### ✅ 正确示例 + +```java +// 1. 单对象查询 +@GetMapping("/users/{id}") +public SingleResponse getUser(@PathVariable Long id) { + UserDTO user = userService.findById(id); + return SingleResponse.of(user); +} + +// 2. 分页列表查询 +@GetMapping("/users") +public MultiResponse listUsers(PageRequest pageRequest) { + Page page = userService.findAll(pageRequest); + return MultiResponse.of(page); +} + +// 3. 非分页列表查询 +@GetMapping("/roles") +public MultiResponse listRoles() { + List roles = roleService.findAll(); + return MultiResponse.of(roles); +} + +// 4. 写操作成功(无返回数据) +@PostMapping("/users") +public Response createUser(@RequestBody CreateUserCmd cmd) { + userService.create(cmd); + return Response.buildSuccess(); +} + +// 5. 业务校验失败 +@PostMapping("/orders") +public Response createOrder(@RequestBody CreateOrderCmd cmd) { + if (cmd.getQuantity() <= 0) { + return Response.buildFailure("INVALID_QUANTITY", "订单数量必须大于0"); + } + orderService.create(cmd); + return Response.buildSuccess(); +} +``` + +正确返回的 JSON 结构: + +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { "id": 1, "name": "张三" } +} +``` + +```json +{ + "success": true, + "errCode": null, + "errMessage": null, + "data": { + "total": 100, + "list": [{ "id": 1, "name": "张三" }] + } +} +``` + +### ❌ 错误示例 + +```java +// 错误1:直接返回实体对象 +@GetMapping("/users/{id}") +public UserDTO getUser(@PathVariable Long id) { + return userService.findById(id); +} + +// 错误2:返回裸 Map +@GetMapping("/users/{id}") +public Map getUser(@PathVariable Long id) { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("data", userService.findById(id)); + return result; +} + +// 错误3:自定义响应结构 +@GetMapping("/users/{id}") +public Result getUser(@PathVariable Long id) { + return new Result<>(userService.findById(id)); +} + +// 错误4:Controller 内 try-catch 自行拼装错误 +@PostMapping("/users") +public Response createUser(@RequestBody CreateUserCmd cmd) { + try { + userService.create(cmd); + return Response.buildSuccess(); + } catch (Exception e) { + // 不应在 Controller 中捕获并手动构建错误响应 + Response resp = new Response(); + resp.setSuccess(false); + resp.setErrMessage(e.getMessage()); + return resp; + } +} + +// 错误5:列表接口不使用 MultiResponse +@GetMapping("/users") +public List listUsers() { + return userService.findAll(); +} +``` + +以上错误写法会导致前端无法统一解析响应、错误处理逻辑分散、API 契约不可靠等问题。 diff --git a/docs/conventions/unified-response-format.md b/docs/conventions/unified-response-format.md deleted file mode 100644 index 3b5dcbce6..000000000 --- a/docs/conventions/unified-response-format.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: unified-response-format -description: 统一响应格式规范 — 所有 API 必须使用 Response/SingleResponse/MultiResponse 标准封装返回 -status: 已实现 -scope: 后端 -source: 项目自有 -symbols: - - Response - - SingleResponse - - MultiResponse - - MapResponse -content_hash: 1dfe2559a6b514effb51735ca8ca49aba308959e02814c5862b842eed3b04fb1 ---- - -## 解决什么问题 - -不遵守此规范会导致: -- 前端需要针对不同的 API 返回格式做多种适配 -- 错误响应格式不一致,前端无法统一处理错误提示 -- 分页数据结构不统一,增加前端列表组件的复杂度 - -## 如何使用 - -### 响应类型选择规则 - -| 场景 | 使用类型 | 说明 | -|------|----------|------| -| 无返回数据的操作 | `Response` | 创建/更新/删除操作 | -| 返回单个对象 | `SingleResponse` | 详情查询、单条记录 | -| 返回列表/分页 | `MultiResponse` | 列表查询、分页查询 | -| 返回 Map 结构 | `MapResponse` | 键值对数据 | - -### 分页数据格式 - -分页数据必须使用 `MultiResponse.of(Page)` 构建,结构为: -```json -{ - "success": true, - "data": { - "total": 100, - "list": [...] - } -} -``` - -### 错误响应格式 - -```json -{ - "success": false, - "errCode": "业务错误码(英文点分隔)", - "errMessage": "用户可读的错误描述" -} -``` - -## 使用实例 - -✅ **正确示例**: -```java -@GetMapping("/{id}") -public SingleResponse get(@PathVariable Long id) { - return SingleResponse.of(userService.findById(id)); -} - -@GetMapping -public MultiResponse list() { - Page page = userRepository.findAll(PageRequest.of(0, 20)); - return MultiResponse.of(page); -} - -@PostMapping -public Response create(@RequestBody UserDTO dto) { - userService.create(dto); - return Response.buildSuccess(); -} -``` - -❌ **错误示例**: -```java -// 直接返回实体 -@GetMapping("/{id}") -public User get(@PathVariable Long id) { - return userService.findById(id); -} - -// 自定义 Map 返回 -@GetMapping -public Map list() { - Map result = new HashMap<>(); - result.put("code", 200); - result.put("data", users); - return result; -} -``` From 1d97442505f123930814c30714a062fc1bfc1279 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Thu, 6 Aug 2026 21:23:06 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=B4?= =?UTF-8?q?=E6=97=B6=E8=84=9A=E6=9C=AC=E7=BA=BF=E7=A8=8B=E6=B3=84=E6=BC=8F?= =?UTF-8?q?=EF=BC=88OOM=20=E6=A0=B9=E5=9B=A0=EF=BC=89=E5=B9=B6=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E7=89=88=E6=9C=AC=E8=87=B3=203.4.55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TempGroovyScriptContext 改用共享 daemon 调度线程,根治每脚本 new Timer() 的原生线程泄漏 - 覆盖/删除/清空/cache-miss 时 cancel 定时任务,ConcurrentHashMap 防并发 - 修正绝对到期时间戳误传为相对延迟的清理 bug(原实现约 55 年后才触发,自动清理从未生效) - 原子化 remove(key, value) 防止过期任务误删被刷新脚本 - 新增回归测试 TempGroovyScriptContextTest(线程数不增长 + 到期自动清理) - 版本号 3.4.54 → 3.4.55(全部 pom + CLAUDE.md) Co-Authored-By: Claude --- CLAUDE.md | 2 +- .../example-app-cmd-domain/pom.xml | 2 +- .../example-app/example-app-cmd-meta/pom.xml | 2 +- example/example-app/example-app-query/pom.xml | 2 +- example/example-app/pom.xml | 2 +- .../example-domain-leave/pom.xml | 2 +- .../example-domain-user/pom.xml | 2 +- example/example-domain/pom.xml | 2 +- .../example-infra/example-infra-flow/pom.xml | 2 +- .../example-infra/example-infra-jpa/pom.xml | 2 +- .../example-infra-security/pom.xml | 2 +- example/example-infra/pom.xml | 2 +- example/example-interface/pom.xml | 2 +- example/example-server/pom.xml | 2 +- example/pom.xml | 4 +- pom.xml | 2 +- springboot-starter-data-authorization/pom.xml | 2 +- springboot-starter-data-fast/pom.xml | 2 +- springboot-starter-flow/pom.xml | 2 +- springboot-starter-script/pom.xml | 2 +- .../script/temp/TempGroovyScriptContext.java | 89 +++++++++++++++---- .../temp/TempGroovyScriptContextTest.java | 84 +++++++++++++++++ springboot-starter-security/pom.xml | 2 +- springboot-starter/pom.xml | 2 +- 24 files changed, 178 insertions(+), 41 deletions(-) create mode 100644 springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java diff --git a/CLAUDE.md b/CLAUDE.md index 551e19509..55d2c51b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目概述 -springboot-framework 是一个基于 Spring Boot 3.3.5(Java 17)的多模块企业级开发框架,由 CodingApi 团队维护。核心定位是为领域驱动设计(DDD)与事件风暴提供开箱即用的基础设施,当前版本为 `3.4.54`。 +springboot-framework 是一个基于 Spring Boot 3.3.5(Java 17)的多模块企业级开发框架,由 CodingApi 团队维护。核心定位是为领域驱动设计(DDD)与事件风暴提供开箱即用的基础设施,当前版本为 `3.4.55`。 **回复语言**:请使用中文进行回答。 diff --git a/example/example-app/example-app-cmd-domain/pom.xml b/example/example-app/example-app-cmd-domain/pom.xml index fb2339898..6fd7afd26 100644 --- a/example/example-app/example-app-cmd-domain/pom.xml +++ b/example/example-app/example-app-cmd-domain/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot example-app - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-app/example-app-cmd-meta/pom.xml b/example/example-app/example-app-cmd-meta/pom.xml index 9ab44d9a3..dc6b15fc1 100644 --- a/example/example-app/example-app-cmd-meta/pom.xml +++ b/example/example-app/example-app-cmd-meta/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot example-app - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-app/example-app-query/pom.xml b/example/example-app/example-app-query/pom.xml index f8f02e4db..982facb8b 100644 --- a/example/example-app/example-app-query/pom.xml +++ b/example/example-app/example-app-query/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot example-app - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-app/pom.xml b/example/example-app/pom.xml index 95f11dcee..111b8a503 100644 --- a/example/example-app/pom.xml +++ b/example/example-app/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot springboot-example - 3.4.54 + 3.4.55 ../pom.xml pom diff --git a/example/example-domain/example-domain-leave/pom.xml b/example/example-domain/example-domain-leave/pom.xml index f5c1734e9..6717974d2 100644 --- a/example/example-domain/example-domain-leave/pom.xml +++ b/example/example-domain/example-domain-leave/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot example-domain - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-domain/example-domain-user/pom.xml b/example/example-domain/example-domain-user/pom.xml index 2477be54c..cd68cdab2 100644 --- a/example/example-domain/example-domain-user/pom.xml +++ b/example/example-domain/example-domain-user/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot example-domain - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-domain/pom.xml b/example/example-domain/pom.xml index c47712d36..085db923f 100644 --- a/example/example-domain/pom.xml +++ b/example/example-domain/pom.xml @@ -5,7 +5,7 @@ com.codingapi.springboot springboot-example - 3.4.54 + 3.4.55 ../pom.xml 4.0.0 diff --git a/example/example-infra/example-infra-flow/pom.xml b/example/example-infra/example-infra-flow/pom.xml index da43affa7..6ca6db430 100644 --- a/example/example-infra/example-infra-flow/pom.xml +++ b/example/example-infra/example-infra-flow/pom.xml @@ -5,7 +5,7 @@ com.codingapi.springboot example-infra - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-infra/example-infra-jpa/pom.xml b/example/example-infra/example-infra-jpa/pom.xml index f5f1c4e2e..3b0fe0c48 100644 --- a/example/example-infra/example-infra-jpa/pom.xml +++ b/example/example-infra/example-infra-jpa/pom.xml @@ -5,7 +5,7 @@ com.codingapi.springboot example-infra - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-infra/example-infra-security/pom.xml b/example/example-infra/example-infra-security/pom.xml index d0fdab139..6d13113e1 100644 --- a/example/example-infra/example-infra-security/pom.xml +++ b/example/example-infra/example-infra-security/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot example-infra - 3.4.54 + 3.4.55 ../pom.xml diff --git a/example/example-infra/pom.xml b/example/example-infra/pom.xml index 637e53caf..138f14a8f 100644 --- a/example/example-infra/pom.xml +++ b/example/example-infra/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot springboot-example - 3.4.54 + 3.4.55 ../pom.xml pom diff --git a/example/example-interface/pom.xml b/example/example-interface/pom.xml index 5eafea994..9c99fbb0e 100644 --- a/example/example-interface/pom.xml +++ b/example/example-interface/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot springboot-example - 3.4.54 + 3.4.55 example-interface diff --git a/example/example-server/pom.xml b/example/example-server/pom.xml index b5c8954cd..73e58ece6 100644 --- a/example/example-server/pom.xml +++ b/example/example-server/pom.xml @@ -5,7 +5,7 @@ springboot-example com.codingapi.springboot - 3.4.54 + 3.4.55 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index f12815507..ea1257c2f 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -14,12 +14,12 @@ com.codingapi.springboot springboot-parent - 3.3.5 + 3.4.55 ../pom.xml springboot-example - 3.4.54 + 3.4.55 springboot-example springboot-example project for Spring Boot diff --git a/pom.xml b/pom.xml index 82a3b0153..a2d03bc92 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.codingapi.springboot springboot-parent - 3.4.54 + 3.4.55 https://github.com/codingapi/springboot-framewrok springboot-parent diff --git a/springboot-starter-data-authorization/pom.xml b/springboot-starter-data-authorization/pom.xml index d673b1063..97370691f 100644 --- a/springboot-starter-data-authorization/pom.xml +++ b/springboot-starter-data-authorization/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot springboot-parent - 3.4.54 + 3.4.55 springboot-starter-data-authorization diff --git a/springboot-starter-data-fast/pom.xml b/springboot-starter-data-fast/pom.xml index e1e9666d6..e94c4e69d 100644 --- a/springboot-starter-data-fast/pom.xml +++ b/springboot-starter-data-fast/pom.xml @@ -5,7 +5,7 @@ springboot-parent com.codingapi.springboot - 3.4.54 + 3.4.55 4.0.0 diff --git a/springboot-starter-flow/pom.xml b/springboot-starter-flow/pom.xml index ba4a583f9..0b58df480 100644 --- a/springboot-starter-flow/pom.xml +++ b/springboot-starter-flow/pom.xml @@ -6,7 +6,7 @@ springboot-parent com.codingapi.springboot - 3.4.54 + 3.4.55 springboot-starter-flow diff --git a/springboot-starter-script/pom.xml b/springboot-starter-script/pom.xml index d9c4853ba..07954d4e6 100644 --- a/springboot-starter-script/pom.xml +++ b/springboot-starter-script/pom.xml @@ -6,7 +6,7 @@ com.codingapi.springboot springboot-parent - 3.4.54 + 3.4.55 springboot-starter-script diff --git a/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java b/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java index 4cad69aa5..e12ca04ca 100644 --- a/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java +++ b/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java @@ -5,7 +5,13 @@ import com.codingapi.springboot.script.repository.TempGroovyScriptRepositoryContext; import lombok.Getter; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; /** * 临时脚本数据上下文管理对象 @@ -15,10 +21,20 @@ public class TempGroovyScriptContext { @Getter private final static TempGroovyScriptContext instance = new TempGroovyScriptContext(); + /** + * 共享的清理调度线程(daemon) + * 所有临时脚本共用一个调度线程,避免每个脚本创建原生线程导致线程泄漏 + */ + private final static ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r, "temp-groovy-script-clear"); + thread.setDaemon(true); + return thread; + }); + private final Map cache; private TempGroovyScriptContext() { - this.cache = new HashMap<>(); + this.cache = new ConcurrentHashMap<>(); } private static class ClearJob { @@ -26,30 +42,37 @@ private static class ClearJob { @Getter private final TempGroovyScript tempGroovyScript; - private final Timer timer; + private final ScheduledFuture future; public ClearJob(TempGroovyScript groovyScript) { this.tempGroovyScript = groovyScript; - this.timer = new Timer(); - this.initTimer(); + // clearTime 为绝对到期时间戳,需换算为相对延迟 + long delay = groovyScript.getClearTime() - System.currentTimeMillis(); + this.future = SCHEDULER.schedule(this::expire, delay, TimeUnit.MILLISECONDS); } public GroovyScript getGroovyScript() { return this.tempGroovyScript.getGroovyScript(); } - private void initTimer() { - this.timer.schedule(new TimerTask() { - @Override - public void run() { - TempGroovyScriptContext.getInstance().remove(getKey()); - } - }, tempGroovyScript.getClearTime()); - } - public String getKey() { return tempGroovyScript.getKey(); } + + /** + * 到期清理 + * 仅当缓存中仍是当前任务时才删除,避免过期任务误删已被刷新的脚本 + */ + private void expire() { + TempGroovyScriptContext.getInstance().removeIfCurrent(getKey(), this); + } + + /** + * 取消定时清理任务 + */ + private void cancel() { + this.future.cancel(false); + } } /** @@ -60,7 +83,17 @@ public String getKey() { public void save(GroovyScript script) { if (script != null) { long tempValidTime = PropertiesContext.getInstance().getTempValidTime(); - this.cache.put(script.getKey(), new ClearJob(new TempGroovyScript(script, tempValidTime + System.currentTimeMillis()))); + this.put(script.getKey(), new ClearJob(new TempGroovyScript(script, tempValidTime + System.currentTimeMillis()))); + } + } + + /** + * 写入缓存,覆盖时取消旧任务的定时清理 + */ + private void put(String key, ClearJob job) { + ClearJob previous = this.cache.put(key, job); + if (previous != null) { + previous.cancel(); } } @@ -73,7 +106,7 @@ public void loadAll(List groovyScripts) { if (groovyScript.isExpired()) { this.remove(groovyScript.getKey()); } else { - this.cache.put(groovyScript.getKey(), new ClearJob(groovyScript)); + this.put(groovyScript.getKey(), new ClearJob(groovyScript)); } } } @@ -93,10 +126,23 @@ public List findAll() { * @param key 脚本key */ public void remove(String key) { - this.cache.remove(key); + ClearJob job = this.cache.remove(key); + if (job != null) { + job.cancel(); + } TempGroovyScriptRepositoryContext.getInstance().delete(key); } + /** + * 仅当缓存中的任务仍为当前任务时删除,防止过期任务误删已被刷新的脚本。 + * 使用 ConcurrentHashMap.remove(key, value) 原子判断 + 删除,避免 check-then-act 竞态 + */ + private void removeIfCurrent(String key, ClearJob job) { + if (this.cache.remove(key, job)) { + job.cancel(); + TempGroovyScriptRepositoryContext.getInstance().delete(key); + } + } /** * 脚本总数量 @@ -118,7 +164,11 @@ public GroovyScript getGroovyScript(String key) { if (job == null) { TempGroovyScript groovyScript = TempGroovyScriptRepositoryContext.getInstance().get(key); if (groovyScript != null) { - this.cache.put(key, new ClearJob(groovyScript)); + if (groovyScript.isExpired()) { + this.remove(key); + return null; + } + this.put(key, new ClearJob(groovyScript)); return groovyScript.getGroovyScript(); } return null; @@ -131,6 +181,9 @@ public GroovyScript getGroovyScript(String key) { * 清空脚本数据 */ public void clear() { + for (ClearJob job : this.cache.values()) { + job.cancel(); + } this.cache.clear(); } } diff --git a/springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java b/springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java new file mode 100644 index 000000000..d7fbd7c92 --- /dev/null +++ b/springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java @@ -0,0 +1,84 @@ +package com.codingapi.springboot.script.temp; + +import com.codingapi.springboot.script.GroovyScript; +import com.codingapi.springboot.script.properties.GroovyScriptProperties; +import com.codingapi.springboot.script.properties.PropertiesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * 回归测试:临时脚本不得为每个脚本创建原生线程(OOM 根因), + * 且脚本到期后应被自动清理。 + */ +class TempGroovyScriptContextTest { + + private TempGroovyScriptContext context; + + @BeforeEach + void setUp() { + context = TempGroovyScriptContext.getInstance(); + // 清理上一用例残留的定时任务 + context.clear(); + // 预热共享调度线程,避免首个任务创建线程影响基线统计 + context.save(script("__warmup__")); + context.remove("__warmup__"); + } + + @AfterEach + void tearDown() { + // 清理定时任务并恢复默认配置,避免泄漏到同 JVM 的其他测试 + context.clear(); + PropertiesContext.getInstance().setProperties(new GroovyScriptProperties()); + } + + private GroovyScript script(String key) { + return GroovyScript.builder(key).script("return 1;").build(); + } + + @Test + void saveManyScriptsShouldNotCreateThreads() { + int threadsBefore = Thread.getAllStackTraces().size(); + + for (int i = 0; i < 200; i++) { + context.save(script("key-" + i)); + } + + assertEquals(200, context.count()); + assertEquals(threadsBefore, Thread.getAllStackTraces().size(), + "注册 200 个临时脚本不应创建任何新线程"); + } + + @Test + void overwriteRemoveClearShouldNotCreateThreads() { + int threadsBefore = Thread.getAllStackTraces().size(); + + for (int i = 0; i < 100; i++) { + context.save(script("key-" + i)); + context.save(script("key-" + i)); // 覆盖刷新 + } + for (int i = 0; i < 100; i++) { + context.remove("key-" + i); + } + context.clear(); + + assertEquals(0, context.count()); + assertEquals(threadsBefore, Thread.getAllStackTraces().size(), + "覆盖/删除/清空不应创建新线程"); + } + + @Test + void expiredScriptShouldBeRemovedAutomatically() throws InterruptedException { + GroovyScriptProperties properties = new GroovyScriptProperties(); + properties.setTempValidTime(300); + PropertiesContext.getInstance().setProperties(properties); + + context.save(script("expire-key")); + assertEquals(1, context.count()); + + Thread.sleep(1500); + assertEquals(0, context.count(), "脚本到期后应被自动清理"); + } +} \ No newline at end of file diff --git a/springboot-starter-security/pom.xml b/springboot-starter-security/pom.xml index ba60d6d0c..596f4df7d 100644 --- a/springboot-starter-security/pom.xml +++ b/springboot-starter-security/pom.xml @@ -6,7 +6,7 @@ springboot-parent com.codingapi.springboot - 3.4.54 + 3.4.55 springboot-starter-security diff --git a/springboot-starter/pom.xml b/springboot-starter/pom.xml index 8e3937d68..4aed573ed 100644 --- a/springboot-starter/pom.xml +++ b/springboot-starter/pom.xml @@ -5,7 +5,7 @@ com.codingapi.springboot springboot-parent - 3.4.54 + 3.4.55 springboot-starter