diff --git a/CLAUDE.md b/CLAUDE.md index c7d4461e..551e1950 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 00000000..93abe1e4 --- /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 00000000..22b7fbaf --- /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 00000000..ce900011 --- /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 00000000..b55477f5 --- /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 00000000..900b79df --- /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 00000000..db21dd95 --- /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 00000000..5e7ea7ea --- /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 00000000..347de11b --- /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 00000000..fb106df5 --- /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 00000000..ebb74218 --- /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 00000000..e6a12f58 --- /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 00000000..fa92c6fc --- /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 00000000..3ee7ca78 --- /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 00000000..8245f78c --- /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 00000000..c53b767c --- /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 00000000..3f2d53ff --- /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 00000000..4611f12f --- /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 00000000..14ff8297 --- /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 00000000..cb666ded --- /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 2ba9ed11..00000000 --- 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 d28883db..00000000 --- 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 c6ee76d6..00000000 --- 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 00000000..b417d4fd --- /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 9a533528..00000000 --- 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 3b5705d5..00000000 --- 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 dbdf9ddb..00000000 --- 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 e7dd9459..00000000 --- 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 1a0732dc..0ee1ed79 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 74cb4f2f..00000000 --- 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 00000000..78f63f6f --- /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 0bf67c15..00000000 --- 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 00000000..67392c83 --- /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 32b3e02e..00000000 --- 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 beebd2b6..00000000 --- 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 00000000..884ef752 --- /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 00000000..71868816 --- /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 44a88b94..00000000 --- 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 c1c68ff8..00000000 --- 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 7388d149..00000000 --- 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 a93d02a4..00000000 --- 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 00000000..eab7667c --- /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 00000000..6643f6ef --- /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 00000000..0350e651 --- /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 00000000..ac51d1ac --- /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 00000000..f3161fd1 --- /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 00000000..93fc3cc2 --- /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 00000000..1c5256f7 --- /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 00000000..fa5dd32e --- /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 00000000..d4fca5f3 --- /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 00000000..bf94c95a --- /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 00000000..3794043f --- /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 00000000..579c9472 --- /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 00000000..989b5f60 --- /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 00000000..9b6ba025 --- /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 00000000..d30523ba --- /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 7c7ca82e..00000000 --- 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 c5cda07c..00000000 --- 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 b49a1024..00000000 --- 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 161b2b88..00000000 --- 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 baf628f7..00000000 --- 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 0b6ad708..df2f11ff 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 00000000..f4617ca4 --- /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 00000000..c1a97a8c --- /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 00000000..3cfb076b --- /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 00000000..6f614617 --- /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 00000000..1c5c3071 --- /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 3b5dcbce..00000000 --- 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; -} -```