# Ineed Core 文档快照 适用 3.0.0-SNAPSHOT。规范的权威来源为 Core 仓库 docs/;源码引用目录记录相对工作区路径,网站不托管 Java 源码。 # 环境与依赖准备 来源:docs/getting-started/prerequisites.md SHA-256:c9da8ce668a60d86f5b7d9ba05e0ca4fa4d15bfa4260d3c2faffd29ada477433 网站:/docs/prerequisites/ Core 是 Maven 库聚合,不能直接 `java -jar` 启动。先运行不依赖业务数据库的框架示例,再进入应用装配。 ## 工具链 本轮文档验证使用 JDK 25、Maven 3.9.15;当前 effective POM 的 `java.version` 和 `maven.compiler.release` 均为 17。编译目标与执行 Maven 的 JVM 是两件事,本轮未宣称已经完成 JDK 17 运行兼容认证。Spring Boot parent 的默认 Java 属性与本地实际 effective POM 可能不同,应分别检查编译目标和 Maven 使用的 JVM。 在 Core 根目录执行: ```bash java -version mvn -version mvn help:effective-pom -Doutput=target/effective-pom.xml ``` 在 effective POM 中核对 Spring Boot 版本、`maven.compiler.release` 和 compiler plugin。不要只根据当前 shell 的 `java.version` 推导编译目标,也不要把另一台机器的 JDK 路径复制进项目命令。 当前没有 Maven wrapper。选择正确 JDK 后,让 `mvn` 指向安装的 Maven;本教程命令不固定操作系统安装路径。 文档结构检查需要 Python 3.9 或更新版本(仅使用标准库)。HTTP 联调示例使用支持 `--fail-with-body` 的 curl 7.76.0 或更新版本,登录示例还需要 jq。 ## 仓库布局 ```text 工作区/ ├── design/ # 跨域标准、业务需求、SQL └── java/ ├── ineed-parent/ ├── ineed-core/ ├── ineed-modules-parent/ ├── ineed-modules/ineed-config/ ├── ineed-project-parent/ └── ineed-projects/ineed-dev/ ``` 从 Core 根目录建立本地变量,后续命令沿用: ```bash INEED_CORE_ROOT="$PWD" INEED_JAVA_ROOT="$(cd .. && pwd)" export INEED_CORE_ROOT INEED_JAVA_ROOT ``` 独立拿到 Core 也可以阅读和运行框架测试;真实业务教程需要同版本的 config、design、dev 及其依赖仓库。不要在 Core 中创建这些仓库的空替代目录。 ## Parent、BOM 与构建顺序 1. `ineed-parent` 提供公共构建配置。Core 根 POM 的 parent 使用空 `relativePath`,从 Maven 仓库解析;编辑旁边的 parent 文件不会自动生效。 2. `ineed-core` 构建框架制品和 BOM。 3. `ineed-modules-parent` 以及所需业务域构建业务模块。 4. `ineed-project-parent` 与具体启动器装配应用。 初次构建且对应制品尚未安装时,先安装 parent,再构建所需 Core 路径: ```bash mvn -f "$INEED_JAVA_ROOT/ineed-parent/pom.xml" -N install mvn -f "$INEED_CORE_ROOT/pom.xml" \ -pl ineed-core-tests/ineed-core-test-webmvc,ineed-core-tests/ineed-core-test-webflux \ -am test ``` `-am` 只补当前 reactor 内依赖;它不会遍历旁边的 Git 仓库。业务应用缺少制品时按其 POM 安装对应模块;只安装 BOM 不会安装所有被它管理版本的 jar。 ## 下一步 [第一个业务模块](/docs/first-module/)从可执行框架链路开始,再对应真实分类模块。数据库、Redis、权限和 HTTP 运行步骤见[运行与验证](/docs/run-and-verify/)。 --- # 第一个业务模块:从分类定义走通 Core 来源:docs/getting-started/first-module.md SHA-256:7599c9a3acdfb282749e6c0ad283a0cae10a28b1a03b21e92095f29f67e6ea84 网站:/docs/first-module/ 本教程以现有 `ineed-config-category` 为可编译参考,重点学习分类定义及其简单/树形分类值。每层都链接到实际源码,避免维护一套与产品 API 脱节的教程代码。完成阅读后,可按第 8 步创建自己的模块。 前提:[环境准备](/docs/prerequisites/)。先理解[架构](/docs/architecture/)和[对象模型](/docs/object-model/)。 ## 1. 先跑通框架链路 在 Core 根目录运行: ```bash mvn -pl ineed-core-tests/ineed-core-test-webmvc -am \ -Dtest=WebMvcJdbcBusinessChainTest,WebMvcJdbcSearchBusinessChainTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` 观察 Surefire 报告,确认匹配用例确实执行。打开 [WebMvcJdbcBusinessChainTest](/references/#source-158c3b86f5bb8021),沿 `newHarness()` 找到 Controller → Service → Manager → TestRepository。 这个示例使用内存仓储,证明能力组合与生命周期;尚未启动 HTTP,也不访问真实 MySQL。先能解释这条链,再进入真实持久化实现。 ## 2. 读需求与 SQL 真实输入:[分类需求](/references/#source-5fbdc78043e960af)、[分类 SQL](/references/#source-35bf806c84c82b11)。这里是分类定义、简单值、树形值和使用记录等对象,不能将所有表机械生成为一套同能力 CRUD。 先列出对象、业务键、父子关系、租户模式、状态开关、删除约束和公开动作。SQL 定义存储约束,需求定义用户可执行的行为,两者需要一致。 ## 3. 理解 Maven 骨架 [分类聚合 POM](/references/#source-396d04b2efa88169)包含 `core/engine/jpa/jdbc/mybatis-flex/webmvc/engine-reactive/r2dbc/webflux` 对应子模块。 `core` 共享契约;同步 Service 和 Manager 接口在 `engine`;JDBC 实体与持久化实现在 `jdbc`;Web 入口在 `webmvc`。响应式独立装配,复用业务 `core`。 新增能力时首先决定需要哪些接口,不从九个目录复制九套相同业务代码。目录规则见[业务骨架](/docs/business-module-layout-guide/)。 ## 4. 从入参到持久化 | 层 | 打开实际文件 | 重点看什么 | | --- | --- | --- | | AO | [CategoryDefinitionAO](/references/#source-c0a2f2c5354172d1) | applicationKey/categoryType/categoryKey/categoryName 必填;类型限制 SIMPLE/TREE | | Entity | [JDBC CategoryDefinition](/references/#source-c45eeb0475d5bdab) | 基类、表列、GLOBAL_ONLY 全局定义与状态能力 | | Repository | [CategoryDefinitionRepository](/references/#source-b99c593064779200) | fragment 组合、getEntityClass、业务键查询 | | Converter | [CategoryDefinitionConverter](/references/#source-362c8352e6bc1388) | AO/Entity/VO 的转换与定制 | | Manager | [CategoryDefinitionManager](/references/#source-10e4c0b42961473b) | operation、converter、Q 表、查询过滤、业务生命周期 | | Service | [CategoryDefinitionService](/references/#source-7e7798b2b218f580) | 能力组合、Manager 桥接、读写事务 | | Controller | [CategoryDefinitionController](/references/#source-5cfcd69150a9f319) | 请求路径、标准能力、权限和自定义接口 | 学习样例中的能力组合与已核实行为;样例仓库也会持续演进,历史格式或个别旧写法不覆盖[现行规范](/docs/standards/)。 ## 5. 增加查询条件的练习 以现有 `categoryName` 条件为参照:定位 Query 字段 → JDBC Manager 的 `appendWhereOfQuery` → Q 类型字段 → 查询结果 VO。复制到自己的模块时,用自己的字段和规则。 至少验证空条件、部分匹配、分页、逻辑删除过滤和租户可见范围。分类定义是全局数据,不能套用租户私有数据的不可见断言;自己的模块应按实体模式补齐越界读写测试。只新增 Query 属性而不添加持久化条件,会出现接口接收参数但结果不变的问题。 ## 6. 业务动作的练习 先阅读 Service 中已有的自定义方法,标记它调用了哪些 Manager、何处开启事务、失败如何传播。为自己的模块增加需要同时修改主表和关系表的动作时: 1. 需求定义允许状态、权限、重复执行和失败结果。 2. AO 表达动作输入,Service 组织用例,Manager 实现对象规则。 3. 仓储封装关系写入/查询,不能在 Controller 或 Service 拼 SQL。 4. 制造第二步失败,验证第一步回滚;同时验证成功结果和越权拒绝。 详见[事务](/docs/transaction-and-concurrency/)与[分层](/docs/custom-business-layering-guide/)。 ## 7. 验证真实业务代码与其他技术栈 在 config 仓库运行选定业务测试,需已解析到匹配版本的 Core 依赖: ```bash mvn -f "$INEED_JAVA_ROOT/ineed-modules/ineed-config/pom.xml" \ -pl ineed-config-category/ineed-config-category-engine,ineed-config-category/ineed-config-category-engine-reactive,ineed-config-category/ineed-config-category-webflux \ -am '-Dtest=Phase61*Category*,PersistenceConverterBoundaryTest' \ -Dsurefire.failIfNoSpecifiedTests=false test ``` 必须选择具体代码子模块;仅 `-pl ineed-config-category` 选中聚合 POM,不能据此认为所有子模块测试已运行。 然后对照 JPA、MyBatis-Flex、R2DBC 中同名业务对象,比较持久化实现与响应式链;业务目标和断言保持一致。可从[例子索引](/docs/examples/)跳转各栈框架测试。 最后进入[应用运行与验证](/docs/run-and-verify/),核对真实数据库、Spring 装配、HTTP 和身份权限。这与前面的框架/业务测试分别报告。 ## 8. 创建自己的模块 按[需求模板](/docs/module-requirements-template/)完成对象表与行为表,按[模块骨架](/docs/business-module-layout-guide/)建立 POM/目录,先实现一个对象的端到端路径,再添加关系和业务动作。 逐项替换域名、包名、资源 key、表名、错误码和语言资源。错误码从[全局登记](/references/#source-6c67578a7e735914)取得。聚合/BOM、启动器依赖、组件/仓储扫描都要纳入接入检查。 完成标准:能够解释每层职责,并独立完成字段、查询、业务动作的修改和相应验证。不是只把样例类名替换后编译成功。 --- # 应用运行、验证与排错 来源:docs/getting-started/run-and-verify.md SHA-256:211a07a21b6019ade21b060c86477fcebd4720a9f783584828936795c4d9ad36 网站:/docs/run-and-verify/ 本页接续[首个模块教程](/docs/first-module/)。四个应用启动器属于 `ineed-projects/ineed-dev`,Core 本身是库。 ## 启动前检查 - 已安装与源码匹配的 parent/BOM/Core/业务制品;Maven 不会自动跨旁边的 Git 仓库构建。 - 选择一种 Web 与持久化组合;JDBC 示例使用 `ineed-dev-webmvc-jdbc-bootstrap`。 - 准备专用本地 MySQL、Quartz 库、Redis 和业务表。数据库初始化采用团队的隔离环境流程;不要把重建已有业务库作为教程默认步骤。 - 按启动器配置提供连接参数。不要复制仓库中历史本地默认密码;具体变量见[配置参考](/docs/configuration/)。 - 检查扫描入口:[WebMvcJdbcApplication](/references/#source-b68c2d2348e3fac6)使用 `com.ineed` 组件和 JDBC 仓储扫描,并使用全限定 Bean 名称生成器。 ## 启动与连通性 使用[环境准备](/docs/prerequisites/)中的变量,运行: ```bash mvn -f "$INEED_JAVA_ROOT/ineed-projects/ineed-dev/ineed-dev-webmvc-jdbc-bootstrap/pom.xml" spring-boot:run ``` 当前 JDBC 启动器默认端口 8082,实际以应用日志和环境覆盖为准。另一个终端设置地址并读取健康接口: ```bash INEED_DEV_BASE_URL=http://127.0.0.1:8082 curl --fail-with-body --silent --show-error "$INEED_DEV_BASE_URL/api/health" ``` 健康接口只证明连通性和该端点行为,不证明业务权限与数据操作完整。 ## 登录、创建与读取 按[登录指南](/docs/development-login-guide/)取得当前身份的短期 token,确认目标租户与权限。验证方法权限拒绝场景时必须恢复正常模式,不能在关闭方法安全的开发模式下作结论。 分类定义的默认地址来自 [CategoryConstant](/references/#source-ba1b2e4ca1c523d9) 与 Controller;如果修改 `application.api-prefix`,同步调整请求。 分类定义采用 `GLOBAL_ONLY`,写入的是全局数据。在隔离的本地练习环境中,使用具备全局维护资格和分类创建权限的身份;多租户模式要求操作身份属于全局租户,单租户模式也允许配置租户的身份维护。会话还必须提供有效目标租户,Service 的缓存失效逻辑会读取它;仅在请求中伪造 tenantKey 不能获得这些资格。 确认 `training` 应用下的分类 key 和名称均未占用,再创建示例: ```bash : "${INEED_DEV_TOKEN:?obtain a local session token first}" curl --fail-with-body --silent --show-error \ --header "Authorization: Bearer ${INEED_DEV_TOKEN}" \ --header 'Content-Type: application/json' \ --data '{"applicationKey":"training","categoryType":"SIMPLE","categoryKey":"first-category","categoryName":"入门分类"}' \ "$INEED_DEV_BASE_URL/api/config/category/definition" ``` 预期:统一结果返回新建对象及其标识;使用返回标识调用同一地址的 `/{id}` GET,确认字段一致。该样例 Manager 显式使用 `CreateOrRefreshManager`,按应用标识与分类 key(或名称)命中已有记录时会进入刷新流程,重复 POST 不能当作“必然报重复错误”的验证。返回外壳见[前端契约](/docs/frontend-integration-guide/)。 再验证缺少 `categoryName` 的非法请求、无权限身份,以及更新后读取。多租户下,普通租户身份修改全局分类应被拒绝;全局定义可读性应按其模式与接口权限断言,不能要求它像 `TENANT_ONLY` 数据一样在另一租户下必然不可见。响应成功还要确认真实持久化结果。 ## 测试层级 真实数据库测试随 dev 项目维护,例如 [Phase61ConfigJdbcIntegrationTest](/references/#source-f1605424e50aa97c)。它有自己的 `config.integration.*` 属性和数据清理逻辑,不能假定仅设置通用 MYSQL 环境变量就改变了测试数据库。 运行前检查专用测试库、DDL 与清理范围,再选择测试。该测试验证原生数据库及配置场景;真实 Controller、安全代理与 HTTP 仍需分别验证。各层证据见[测试规范](/docs/testing/)。 ## 常见故障 | 症状 | 优先排查 | | --- | --- | | 找不到父 POM/artifact | parent 是否安装、版本是否一致、依赖仓库;`-am` 是否只覆盖了当前 reactor | | 类存在却没有 Bean | 运行时选错、扫描范围、条件配置、依赖是否实际在 classpath | | 同名类型或 Bean 冲突 | 是否混装同步/响应式实现,是否采用应用约定的命名生成器 | | 租户无效/拒绝访问 | 操作身份、选定目标租户、实体模式与行归属 | | 请求带参数却无过滤 | Query 到数据栈条件是否实现,字段名/列名/转换是否对应 | | 方法有事务注解却未回滚 | 代理入口、事务管理器、自调用、异常是否被吞、响应式是否独立订阅 | | 修改数据后读取旧值 | 写入口是否触发缓存失效,提交/回滚、cache name 和租户 key 是否匹配 | 不要把关闭权限、关闭租户或绕过标准生命周期作为排错后的永久修复。 --- # 可执行例子索引 来源:docs/examples/README.md SHA-256:a82e355a31a8b2d40759e8b15312be1c7f7f2ad452a4bb3491bbedc5ab5aa4d9 网站:/docs/examples/ 例子直接引用受编译/测试维护的源码。示例的测试层级写在表中,不将内存仓储称为真实数据库。 | 学习目标 | 源码 | 范围 | | --- | --- | --- | | MVC + JDBC 能力链 | [WebMvcJdbcBusinessChainTest](/references/#source-158c3b86f5bb8021) | 内存测试仓储、Controller 到 Manager/operation | | MVC + JPA 能力链 | [WebMvcJpaBusinessChainTest](/references/#source-e08455339ac17201) | 框架契约测试 | | MVC + MyBatis-Flex 能力链 | [WebMvcMybatisFlexBusinessChainTest](/references/#source-7f5d999ae2ddbaa7) | 框架契约测试 | | WebFlux + R2DBC 能力链 | [WebFluxR2dbcBusinessChainTest](/references/#source-26ed219e94ff264c) | 订阅、上下文、内存测试仓储 | | 租户策略 | [EntityTenantSupportTest](/references/#source-f7f82053d239b656) | 初始化与读写策略相关契约 | | 缓存提交/回滚 | [ServiceCacheAfterCommitCoordinatorTest](/references/#source-62b9f09a589dc9b6) | 测试事务管理器与协调器 | | 完整业务分层 | [分类模块](/references/#source-396d04b2efa88169) | 真实业务源码,逐层阅读见入门教程 | | 真实数据库 | [Config JDBC 集成测试](/references/#source-f1605424e50aa97c) | 需要专用 MySQL,独立配置与清理 | 运行顺序与命令见[第一个模块](/docs/first-module/)、[测试规范](/docs/testing/)。生产业务应遵守当前规范,不照抄测试夹具的内存存储、手工实例化或测试用等待方式。 --- # Ineed-Core 文档 来源:docs/README.md SHA-256:dc60c4e0f302c293cd57cefa7be948223ad3ce4780ecb4c8a0e621cdb3194b6d 网站:/docs/documentation-map/ 两条使用路径共用一套规范。适用 Core `3.0.0-SNAPSHOT`,系统核验日期 2026-09-19;行为与环境范围见[核验记录](/docs/verification-2026-09-19/)。 - **AI 编码**:[短契约](/docs/ai-entry/) → [任务路由](/docs/task-routing/) → [规则索引](/docs/standards/) → 对应示例与验证。 - **开发人员**:[环境准备](/docs/prerequisites/) → [架构](/docs/architecture/) → [第一个模块](/docs/first-module/) → [运行与排错](/docs/run-and-verify/)。 - **维护框架**:[能力矩阵](/docs/runtime-matrix/) → [模块清单](/docs/module-inventory/) → [文档维护](/docs/documentation-policy/)。 应用错误码与数据库设计归工作区 `design/standards`,业务需求归所属域,Core 维护框架规则。不要把历史计划作为常规编码必读,也不要复制第二份规范正文。 ## 按问题查找 | 我需要解决的问题 | 入口 | | --- | --- | | 新模块如何命名、拆分、放目录 | [业务骨架](/docs/business-module-layout-guide/) | | 一次业务各层怎么写 | [分层](/docs/custom-business-layering-guide/)、[对象模型](/docs/object-model/) | | 查询或字段如何扩展 | [CRUD](/docs/crud-query-guide/)、[JDBC](/docs/jdbc/)及其他 ORM 专题 | | 为什么能读却不能修改 | [租户与上下文](/docs/tenant-and-context/)、[权限](/docs/authorization-integration/) | | 多表写入、缓存和事件何时生效 | [事务](/docs/transaction-and-concurrency/)、[缓存](/docs/cache/)、[事件](/docs/integration-events/) | | 删除、恢复和状态如何接入 | [删除](/docs/delete-hook-and-logging-guide/)、[状态](/docs/data-status/) | | 什么才算验证通过 | [测试与证据](/docs/testing/) | ## 全部文档 以下索引覆盖当前 `docs/` 页面,检查器会提示遗漏。 ### AI 执行路径 | 文档 | 路径 | | --- | --- | | AI 编码入口 | [ai/README.md](/docs/ai-entry/) | | 交付检查 | [ai/delivery-checklist.md](/docs/delivery-checklist/) | | 编码任务输入模板 | [ai/task-input-template.md](/docs/task-input-template/) | | 按任务读取与验证 | [ai/task-routing.md](/docs/task-routing/) | ### 开发人员入门 | 文档 | 路径 | | --- | --- | | 第一个业务模块:从分类定义走通 Core | [getting-started/first-module.md](/docs/first-module/) | | 环境与依赖准备 | [getting-started/prerequisites.md](/docs/prerequisites/) | | 应用运行、验证与排错 | [getting-started/run-and-verify.md](/docs/run-and-verify/) | ### 设计与能力边界 | 文档 | 路径 | | --- | --- | | 架构与开发边界 | [concepts/architecture.md](/docs/architecture/) | | 业务对象与能力组合 | [concepts/object-model.md](/docs/object-model/) | | 请求、事务与生命周期 | [concepts/request-lifecycle.md](/docs/request-lifecycle/) | | 运行时与能力验证矩阵 | [concepts/runtime-matrix.md](/docs/runtime-matrix/) | ### 规范入口 | 文档 | 路径 | | --- | --- | | 规范与权威来源 | [standards/README.md](/docs/standards/) | | 契约、错误与国际化 | [standards/contract-and-error.md](/docs/contract-and-error/) | | 测试与交付证据 | [standards/testing.md](/docs/testing/) | ### 任务专题 | 文档 | 路径 | | --- | --- | | 身份与权限接入 | [guides/authorization-integration.md](/docs/authorization-integration/) | | 服务缓存接入 | [guides/cache.md](/docs/cache/) | | HTTP Client、Socket 与数据库元数据 | [guides/clients-and-metadata.md](/docs/clients-and-metadata/) | | 数据状态能力与业务状态 | [guides/data-status.md](/docs/data-status/) | | 业务事件接入路线 | [guides/integration-events.md](/docs/integration-events/) | | JDBC 业务代码指南 | [guides/jdbc.md](/docs/jdbc/) | | 租户、操作身份与上下文 | [guides/tenant-and-context.md](/docs/tenant-and-context/) | | 事务、并发与副作用 | [guides/transaction-and-concurrency.md](/docs/transaction-and-concurrency/) | ### 配置与扩展点 | 文档 | 路径 | | --- | --- | | 配置参考 | [reference/configuration.md](/docs/configuration/) | | 扩展点速查 | [reference/extension-points.md](/docs/extension-points/) | ### 可执行例子 | 文档 | 路径 | | --- | --- | | 可执行例子索引 | [examples/README.md](/docs/examples/) | ### 既有框架参考(权威正文保留) | 文档 | 路径 | | --- | --- | | Ineed-Core 内部文档 | [core/README.md](/docs/framework-map/) | | API 兼容性治理策略 | [core/api-compatibility-policy.md](/docs/api-compatibility-policy/) | | API 文档使用指南 | [core/api-documentation-guide.md](/docs/api-documentation-guide/) | | Constant、ErrorCode 与 I18n 约定 | [core/constant-errorcode-i18n-guide.md](/docs/constant-errorcode-i18n-guide/) | | Ineed-Core 当前实现状态 | [core/current-implementation-status.md](/docs/current-implementation-status/) | | Ineed-Core 集成事件接入指南 | [core/integration-event-guide.md](/docs/integration-event-guide/) | | Java 模块目录结构指南 | [core/java-module-directory-guide.md](/docs/java-module-directory-guide/) | | Ineed-Core 模块能力地图 | [core/module-capability-map.md](/docs/module-capability-map/) | | Ineed-Core 真实模块清单 | [core/module-inventory.md](/docs/module-inventory/) | ### 既有开发专题(权威正文保留) | 文档 | 路径 | | --- | --- | | 基于 Ineed-Core 的项目开发文档 | [project-development/README.md](/docs/development-map/) | | 业务集成测试编写指南 | [project-development/business-integration-test-guide.md](/docs/business-integration-test-guide/) | | 业务模块骨架与目录结构约定 | [project-development/business-module-layout-guide.md](/docs/business-module-layout-guide/) | | 代码格式与源码风格指南 | [project-development/code-style-guide.md](/docs/code-style-guide/) | | 开发约定反模式清单 | [project-development/convention-anti-patterns.md](/docs/convention-anti-patterns/) | | CRUD / Query 开发主路径 | [project-development/crud-query-guide.md](/docs/crud-query-guide/) | | 自定义业务与分层扩展指南 | [project-development/custom-business-layering-guide.md](/docs/custom-business-layering-guide/) | | 删除钩子、分层日志与异常追踪规范 | [project-development/delete-hook-and-logging-guide.md](/docs/delete-hook-and-logging-guide/) | | 项目开发契约 | [project-development/development-contract.md](/docs/development-contract/) | | 开发模式登录与权限诊断 | [project-development/development-login-guide.md](/docs/development-login-guide/) | | 前端接入与联调指南 | [project-development/frontend-integration-guide.md](/docs/frontend-integration-guide/) | | JPA QueryDSL 代码编写指南 | [project-development/jpa-querydsl-code-guide.md](/docs/jpa-querydsl-code-guide/) | | 模块 Constant 与 I18n 样板 | [project-development/module-constant-i18n-template.md](/docs/module-constant-i18n-template/) | | 业务需求文档模板 | [project-development/module-requirements-template.md](/docs/module-requirements-template/) | | MyBatis-Flex 代码编写指南 | [project-development/mybatis-flex-code-guide.md](/docs/mybatis-flex-code-guide/) | | R2DBC 代码编写指南 | [project-development/r2dbc-code-guide.md](/docs/r2dbc-code-guide/) | ### 维护与核验 | 文档 | 路径 | | --- | --- | | 文档维护与验证约定 | [maintenance/documentation-policy.md](/docs/documentation-policy/) | | 文档入口迁移说明 | [maintenance/migration-notes.md](/docs/migration-notes/) | | 文档核验记录:2026-09-19 | [maintenance/verification-2026-09-19.md](/docs/verification-2026-09-19/) | --- # 架构与开发边界 来源:docs/concepts/architecture.md SHA-256:e11515aaf5c872aa164b10667e05da20c80b12670fef3e33e50bd0a96f474817 网站:/docs/architecture/ 适用:Core `3.0.0-SNAPSHOT`;源码核对:2026-09-19。本文说明设计,强制规则见[规则索引](/docs/standards/)。 Ineed-Core 将 Web、业务能力与持久化实现分开。业务模块通过组合能力接口获得 CRUD、查询、状态、排序等行为,再在明确的扩展点加入领域规则。 ## 一次业务调用 ![调用链:HTTP 与身份上下文经过 Controller、Service,组合多个 Manager,经 Repository 或 Mapper 访问数据库。](/images/core-architecture.svg) [打开架构图原图](/images/core-architecture.svg) - Controller 组合 `ICreateController` 等 Web trait,调用 Service;不直接组织跨表写入。 - Service 组合 `ICreateService` 等业务 trait,承担用例和事务边界。自定义事务需要真实 Spring 代理和正确事务管理器。 - Manager 组合 `CreateManager`、`SearchManager` 等能力,处理实体规则和生命周期;数据栈实现提供 operation、converter 和查询表达式。 - Repository 或 MyBatis-Flex Mapper 执行数据访问。外键 `count/exists/delete/findBy` 由具体仓储封装。 标准 CRUD 的不少代码在接口默认方法中。阅读业务类时,应继续跳转其能力接口;空方法体或很短的业务类并不表示行为缺失。 ## 两种运行时、四条关系型数据路径 | 运行时 | Web | 业务引擎 | 数据适配 | | --- | --- | --- | --- | | 阻塞式 | `ineed-core-webmvc` | `ineed-core-engine` | JPA / JDBC / MyBatis-Flex 三选一 | | 响应式 | `ineed-core-webflux` | `ineed-core-engine-reactive` | R2DBC | 两条运行时共享 `ineed-core-base`、`ineed-core-data-base` 和业务模块的 `*-core` 契约。运行时包含同包同名的 Service/缓存类型,不能为了同时具备四栈而把两套实现都装进同一个应用 classpath。四栈对齐是在四个独立装配中验证相同业务语义。 聚合 POM 决定哪些模块参加构建;BOM 只管理版本;starter 是否具备自动配置取决于代码。`ineed-core-starters` 当前主要聚合 Druid,不能假设添加一个通用 starter 就能装配完整业务应用。 ## Core 与业务项目 Core 管理技术契约、上下文、通用生命周期和适配器。业务模块管理字段、关系、业务状态机、业务权限及错误语义。应用项目选择实现、配置环境并启动。 通用 `PUBLISHED` 状态位不包含审核流、内容版本或发布历史;这些属于具体业务模型。跨进程可靠事件投递、业务幂等和并发控制也不能从 CRUD 默认方法推导出来。 跨模块共享接口放在提供该契约的 `*-core` 中;阻塞式 `service/` 和响应式 `service/reactive/` 分开。模块内业务编排保持 Service → Manager,不以跨模块需求为由随意新增平行编排层。具体跨域桥接先明确调用方、契约方、实现方及事务边界。 ## 继续阅读 - [对象模型](/docs/object-model/)、[请求生命周期](/docs/request-lifecycle/) - [能力与运行时矩阵](/docs/runtime-matrix/) - [真实 Maven 模块清单](/docs/module-inventory/) - [自定义业务分层](/docs/custom-business-layering-guide/) - [从第一个模块开始](/docs/first-module/) 源码入口:[根 POM](/references/#source-71ad9b7b2d21fadd)、[同步模块](/references/#source-a4d47139090273aa)、[响应式模块](/references/#source-68bd4a1889c0d8a5)。 --- # 业务对象与能力组合 来源:docs/concepts/object-model.md SHA-256:a05726d5e43f7d3141c39faef5f472f3c6617f093477bd0d1099337369bfefef 网站:/docs/object-model/ ## 对象如何分工 | 对象 | 作用 | 开发时的决定 | | --- | --- | --- | | AO | 创建、编辑或动作入参 | 可写字段、必填校验、禁止客户端控制的字段 | | Query | 业务查询条件 | 哪些条件可查询、如何映射到数据栈查询表达式 | | VO | 面向调用方的返回数据 | 展示字段、关联结果、敏感字段隐藏 | | DTO | 跨模块传递的共享数据 | 稳定语义和依赖边界,不绑定某 ORM | | Entity | 某数据栈的持久化实体 | 表映射、租户模式、状态能力和持久化类型 | | Converter | AO/Entity/VO 转换 | 复用父接口,特殊映射才覆盖方法 | 新业务 contract 放在 `contract/ao`、`contract/query`、`contract/vo`、`contract/dto`;转换实现放 `converter/`。MyBatis-Flex 的 `mapper/` 专用于持久化接口。 字段从 SQL → Entity → AO/VO/Query 不一定一对一暴露。例如审计字段由框架维护,秘密字段不能原样返回,关联名称可以是查询投影。为每个字段明确读写权限和转换来源。 ## 选择基类与能力 先判断实体需要租户、树、逻辑删除、排序、数据状态或所有者能力,再选择对应数据栈基类。不要为得到一个字段顺带开放所有动作。 `@EnableDataStatus` 声明数据状态能力;基础实体默认提供的能力与最终实体打开的能力见[数据状态指南](/docs/data-status/)。能力接口、Controller 暴露动作、权限清单与前端按钮必须保持一致。 一个对象是否“可以删除”和是否“有物理删除接口”是两回事。生命周期拦截仍会检查实体状态、租户和业务引用。 ## 以一个新增字段为例 新增条目说明字段时,按以下顺序检查:SQL 列与默认值 → 各目标数据栈 Entity → 可写 AO 与校验 → Converter → VO → OpenAPI/i18n → 测试。如果还需要按说明搜索,再改 Query、各栈查询实现和查询测试。 不要只看编译通过:补一个创建后查询的断言,才能发现转换遗漏;补旧记录读取场景,才能发现默认值或兼容问题。 源码参考:[AbstractAO](/references/#source-cc419c7322ea6552)、[AbstractBaseQuery](/references/#source-ac253353004b5007)、[IEntity](/references/#source-606e684472d0bf15)。完整生成输入见[需求模板](/docs/module-requirements-template/)。 --- # 请求、事务与生命周期 来源:docs/concepts/request-lifecycle.md SHA-256:a448d6ee1c51a544f8c741a585f313f42b42c3786054c771be2f69847d3931d5 网站:/docs/request-lifecycle/ ## 从请求到结果 1. 应用的 Web 与安全组件解析请求、会话及操作上下文。身份和授权实现属于安全模块,Core 提供通用上下文和 Web 支撑。 2. Controller 校验参数并委托 Service;统一异常处理与 `ResultBuild` 形成响应。 3. Service 在应用配置的事务边界内组织 Manager。 4. Manager 执行校验、转换、生命周期钩子和持久化操作。 5. 返回结果后,事务管理器完成提交或回滚;需要提交后执行的工作通过对应机制处理。 框架具体钩子的先后顺序以对应操作实现为准。不能把上面的概念顺序作为所有动作的逐行实现。 ## 三个容易混淆的时点 | 时点 | 含义 | 不能据此保证 | | --- | --- | --- | | 创建 Publisher | 构建响应式流程 | 已读库、已写库、已取得当前请求身份 | | `afterSave/afterUpdate/afterPhysicsDelete` | 对应 Manager 生命周期进行到该钩子 | 外层事务已提交 | | 事务提交后 | 当前事务成功结束 | 跨进程消息可靠送达或外部调用必定成功 | 响应式的依赖解析、上下文读取、查询和副作用应发生在订阅期;通过 `Mono.defer`、`Flux.defer` 或已有默认能力组合,不在业务链中调用 `subscribe()` 或 `block()` 来启动另一条流程。测试中的阻塞等待和业务代码中的阻塞调用需区别对待。 异常必须沿调用链传播,才能维持事务回滚和统一错误语义。把错误转换成“成功空结果”可能同时破坏事务、缓存和调用方判断。 ## 排查顺序 先确认请求是否进入目标 Controller,再检查操作身份/目标租户、参数、Manager 能力、仓储条件、事务代理,最后检查提交后工作。使用操作追踪信息关联日志,不记录完整 token 或敏感字段。 详见[事务与并发](/docs/transaction-and-concurrency/)、[租户与上下文](/docs/tenant-and-context/)、[事件](/docs/integration-events/)、[删除生命周期](/docs/delete-hook-and-logging-guide/)。 --- # 运行时与能力验证矩阵 来源:docs/concepts/runtime-matrix.md SHA-256:910ce99ae14ee92dff6718655c1cce434ee7dfd53054c6167e226c044515afe4 网站:/docs/runtime-matrix/ 核对日期:2026-09-19;版本 `3.0.0-SNAPSHOT`。源码声明、测试存在和本轮执行结果分别描述,不用“全部稳定”替代具体范围。 | 能力 | 源码/构建状态 | 本轮证据与边界 | | --- | --- | --- | | MVC + JPA | 已进入 reactor,有 CRUD/query 链路测试 | 本轮框架定向测试包含对应业务链;不是完整真实数据库验收 | | MVC + JDBC | 已进入 reactor,有 CRUD/query 链路测试 | 同上;入门教程采用该线展示实际业务分层 | | MVC + MyBatis-Flex | 已进入 reactor,有 CRUD/query 链路测试 | 同上;具体业务 Mapper 与数据库需独立验证 | | WebFlux + R2DBC | 已进入 reactor,有订阅、上下文及链路测试 | 本轮包含对应业务链;不能省略真实响应式数据库事务验证 | | 租户、缓存、事件 | 共享及两种运行时有对应实现/测试 | 本轮定向集合覆盖相关类;范围见验证记录 | | HTTP Client / Socket | 契约与各实现已进入对应聚合 | 本轮核对入口源码,未重新执行网络适配全矩阵 | | MongoDB | 在同步聚合 POM 中 | 本轮未做业务接入验证,不推导与四关系型栈等价 | | Elasticsearch | 目录存在,聚合声明处于注释中 | 不属于当前有效 reactor | | 动态数据源 | 模块已聚合 | `determineCurrentLookupKey()` 当前返回 null,不能声称已具备动态路由 | | Druid starter | 依赖聚合 | 不等于完整 Core 应用自动配置 | | RDB 元数据 | 存在读取工具与异常类型 | 本轮核对源码,未重跑各数据库厂商矩阵 | 框架定向测试共 366 个,分类模块定向业务测试 9 个,均通过;精确命令、报告和局限见[本轮验证](/docs/verification-2026-09-19/)。 选栈依据业务运行模式与部署条件。入门先讲 JDBC 是教学选择;正式任务应按[业务骨架](/docs/business-module-layout-guide/)确定交付范围,而不是把响应式当成可随意遗漏的附录。 更新此表时,先核实 [Maven 模块清单](/docs/module-inventory/),再记录实际证据。没有执行的环境标明“未验证”,不用历史日期或测试类数量证明当前生产能力。 --- # 规范与权威来源 来源:docs/standards/README.md SHA-256:21e0f7d4d4590110e94a5bf6c3553f93b012bf4cde711fc248839750b5a7cb21 网站:/docs/standards/ 本页是规则索引。详细规范保留在已有专题中,避免迁移路径时复制两份正文。教程解释做法,规则正文定义约束;业务特有规则归所属模块需求。 | 编号 | 规则主题 | 唯一正文 | 适用范围与验证 | | --- | --- | --- | --- | | CORE-LAYOUT-001 | 聚合、BOM、业务子模块及包结构 | [模块目录](/docs/java-module-directory-guide/)、[业务骨架](/docs/business-module-layout-guide/)分别负责通用目录与业务语义 | 新建或调整模块;核对 POM 与源码目录 | | CORE-LAYER-001 | Controller → Service → Manager → Repository/Mapper | [分层规范](/docs/custom-business-layering-guide/) | 业务代码;依赖检查和调用链审查 | | CORE-CONTRACT-001 | contract、converter、返回体与异常 | [契约与错误](/docs/contract-and-error/) | 新增接口或字段;编译、校验与响应断言 | | CORE-STYLE-001 | 源码格式、Javadoc、字段与日志门面 | [代码风格](/docs/code-style-guide/) | 新增和修改 Java 源码;风格检查 | | CORE-TENANT-001 | 租户读取、写入和上下文边界 | [租户指南](/docs/tenant-and-context/) | 租户实体及所有自定义访问;跨租户负向测试 | | CORE-AUTH-001 | API 权限、数据范围与模块接入 | [授权指南](/docs/authorization-integration/) | Web 和受保护业务动作;真实身份正反向验证 | | CORE-TX-001 | 事务、订阅期与副作用 | [事务指南](/docs/transaction-and-concurrency/) | 写用例、缓存、事件;提交/回滚测试 | | CORE-STATE-001 | 显式数据状态能力 | [状态指南](/docs/data-status/) | 支持状态动作的实体;能力和接口对齐 | | CORE-DELETE-001 | 删除、恢复及级联时序 | [删除规范](/docs/delete-hook-and-logging-guide/)的删除章节 | 删除和恢复;引用、级联、回收站断言 | | CORE-CACHE-001 | 缓存运行时、租户键和失效 | [缓存指南](/docs/cache/) | 接入缓存的 Service;隔离、提交、回滚测试 | | CORE-EVENT-001 | 内部/蒸馏事件与事务时序 | [事件规范](/docs/integration-event-guide/) | 事件发布与监听;开关和提交时序测试 | | CORE-STACK-001 | 同步/响应式与四数据栈对齐 | [业务骨架的跨模块对齐规则](/docs/business-module-layout-guide/) | 公共能力和未限定单栈的新功能;同一业务场景验证 | | CORE-TEST-001 | 分层验证与证据 | [测试规范](/docs/testing/) | 所有交付;记录命令、数量、结果与边界 | 必须/禁止表示约束,建议表示默认做法。任务或业务需求明确限定技术栈时按其范围交付,并记录理由;不能用“以后补齐”隐去本次承诺的范围。 ## 跨仓规范 - 数据库:[数据库设计标准](/references/#source-166a93f5ebf7feef)。 - 应用私有错误码:[全局登记表](/references/#source-6c67578a7e735914);Core 不维护另一份号段表。 - 业务需求:所属 `design/ineed-*/requirements/`。 - 应用装配:[ineed-dev](/references/#source-0dd5f2662bc35434)。 以上外部链接适用于标准工作区布局。独立获取 Core 时需同时取得匹配版本的相关仓库;可以用 [文档检查器](/references/#source-9c67036f4bd4256f) 的 `--workspace` 模式核对外部链接。 ## 冲突处理 规范与源码不一致时,区分“规范过时”和“实现缺陷”,在变更中记录证据并修正对应来源。旧业务类只能作为实现参考,不能覆盖本页链接的现行规则。不得为通过检查而删除规则或降低测试范围。 --- # 基于 Ineed-Core 的项目开发文档 来源:docs/project-development/README.md SHA-256:ee89ac03f58dfe0ddd3bbbe984ef1c22fac526b4bdfe3e1c16eed51e3ff02882 网站:/docs/development-map/ 面向所有基于 `ineed-core` 开发业务项目的开发者、代码生成器和自动化工具。 ## 选择你的路径 - 第一次接入:读[环境准备](/docs/prerequisites/)、[架构](/docs/architecture/)和[首个模块教程](/docs/first-module/),再按任务查本目录专题。 - AI 编码:读[短契约](/docs/ai-entry/)及[任务路由](/docs/task-routing/),按影响范围加载规则。 - 查强制约束:从[规则索引](/docs/standards/)定位唯一正文。 - 查租户、事务、缓存、事件、JDBC:从[总索引](/docs/documentation-map/)进入新增指南。 ## 按任务进入 | 任务 | 推荐文档 | |------|------| | 创建新业务模块骨架 | [business-module-layout-guide.md](/docs/business-module-layout-guide/) | | 设计分层(Controller/Service/Manager/Repository) | [custom-business-layering-guide.md](/docs/custom-business-layering-guide/) | | 接入 CRUD / Query 主路径 | [crud-query-guide.md](/docs/crud-query-guide/) | | 编写 JPA QueryDSL manager 查询 | [jpa-querydsl-code-guide.md](/docs/jpa-querydsl-code-guide/) | | 编写 MyBatis-Flex mapper / manager 查询 | [mybatis-flex-code-guide.md](/docs/mybatis-flex-code-guide/) | | 编写 R2DBC repository / manager 查询 | [r2dbc-code-guide.md](/docs/r2dbc-code-guide/) | | 处理删除钩子、级联清理、日志与异常追踪 | [delete-hook-and-logging-guide.md](/docs/delete-hook-and-logging-guide/) | | 编写模块级业务集成测试 | [business-integration-test-guide.md](/docs/business-integration-test-guide/) | | 统一代码格式、Javadoc、字段声明、formatter 保护 | [code-style-guide.md](/docs/code-style-guide/) | | 识别禁止写法 | [convention-anti-patterns.md](/docs/convention-anti-patterns/) | | 编写需求输入 | [module-requirements-template.md](/docs/module-requirements-template/) | | 使用代码生成器辅助生成 | [development-contract.md](/docs/development-contract/) | | 前端联调和接口消费 | [frontend-integration-guide.md](/docs/frontend-integration-guide/) | | 在开发模式登录并诊断身份、权限 | [development-login-guide.md](/docs/development-login-guide/) | | 生成 Constant / ErrorCode / i18n 样板 | [module-constant-i18n-template.md](/docs/module-constant-i18n-template/) | ## 核心原则 - 业务模块默认走 `Controller -> Service -> Manager -> Repository/EntityManager` - 标准 CRUD、查询、数据状态、删除优先复用 `ineed-core` 接口和默认实现 - `Service` 是事务和业务编排边界,不与其他 `Service` 横向互调 - `Manager` 承载单类实体规则,不成为跨表总控层 - 同步和响应式模块必须保持业务语义对齐 ## 关联框架文档 - [../core/module-inventory.md](/docs/module-inventory/) — 真实模块清单 - [../core/module-capability-map.md](/docs/module-capability-map/) — 框架能力地图 - [../core/current-implementation-status.md](/docs/current-implementation-status/) — 当前能力验证范围和缺口 - [../core/constant-errorcode-i18n-guide.md](/docs/constant-errorcode-i18n-guide/) — 常量、错误码、i18n 规则 > 最后更新:2026-07-30 --- # 业务模块骨架与目录结构约定 来源:docs/project-development/business-module-layout-guide.md SHA-256:1db18aeb068d956d074661bc6b5f4fd063d67429028fa62c5bf02dd9e85068ad 网站:/docs/business-module-layout-guide/ ## 文档定位 本文档定义基于 Ineed-Core 开发业务模块时,推荐采用的: - 聚合模块骨架 - 子模块命名规则 - 子模块物理目录模板 - `main/java` 下的语义包结构 - 创建模块前必须执行的一致性检查 本文档面向两类对象: - 人工创建新业务模块的开发者 - 基于 SQL / 需求说明自动生成代码的代码生成器或自动化工具 如果你要判断的是“普通 Java 模块、聚合模块、BOM 模块该不该有 `src`”,先读 [java-module-directory-guide.md](/docs/java-module-directory-guide/)。 本文档只负责业务聚合模块及其子模块骨架,不替代通用 Java 模块物理目录规范。 ## 适用范围 本文档描述的是“业务模块约定”,不是 `ineed-core` 自身的真实 Maven 聚合清单。 真实稳定模块清单仍以 [module-inventory.md](/docs/module-inventory/) 为准。 这里定义的是业务代码在接入 Ineed-Core 时应遵守的标准骨架,适用于类似以下目录: - `java/ineed-modules/ineed-security/ineed-security-authorization/ineed-security-authorization-access-credential` - `java/ineed-modules/ineed-security/ineed-security-authorization/ineed-security-authorization-system-identity` ## 命名规则 ### 聚合模块命名 聚合模块目录名统一使用: `ineed-{domain}-{capability}`;存在实际能力分组时可继续使用 `ineed-{domain}-{group}-{capability}` 要求: - 全部小写 - 使用短横线连接 - 必须体现业务域和能力;只有领域确有中间分组时才增加该层,不能为满足固定段数虚构分组 - 不允许使用含糊目录名,如 `common`、`core2`、`temp`、`demo` 示例: - `ineed-config-category`(域 + 能力) - `ineed-marketing-campaign`(域 + 能力) - `ineed-security-authorization-access-credential` - `ineed-security-authorization-system-identity` ### 子模块命名 子模块目录名统一使用: `{aggregate-module}-{layer}` 示例: - `ineed-security-authorization-access-credential-core` - `ineed-security-authorization-access-credential-engine` - `ineed-security-authorization-access-credential-jpa` - `ineed-security-authorization-access-credential-webmvc` ## 标准业务模块骨架 正式业务聚合模块默认按完整骨架创建: ```text {module}/ pom.xml {module}-core/ {module}-engine/ {module}-jpa/ {module}-jdbc/ {module}-mybatis-flex/ {module}-webmvc/ {module}-engine-reactive/ {module}-r2dbc/ {module}-webflux/ ``` 其中各子模块职责如下: - `core` 承载常量定义、枚举定义、工具类、共享 contract,以及跨模块复用的共享 service 契约,如 `contract/ao`、`contract/vo`、`contract/query`、`contract/dto` - `engine` 承载阻塞式业务编排、`service`、`manager` 接口与业务规则 - `jpa` 承载基于 JPA 的实体、仓储、manager 实现、映射 - `jdbc` 承载基于 JDBC 的实体、仓储、manager 实现、映射 - `mybatis-flex` 承载基于 MyBatis-Flex 的实体、mapper、manager 实现、映射 - `webmvc` 承载阻塞式 Web 入口层,如 controller - `engine-reactive` 承载响应式业务编排、响应式 `service`、响应式 `manager` 接口与业务规则 - `r2dbc` 承载基于 R2DBC 的实体、仓储、manager 实现、映射 - `webflux` 承载响应式 Web 入口层,如 controller ## 最小骨架规则 ### 阻塞式最小骨架 如果当前模块只接入阻塞式主链,可暂时最少创建: ```text {module}/ pom.xml {module}-core/ {module}-engine/ {module}-jpa/ {module}-jdbc/ {module}-mybatis-flex/ {module}-webmvc/ ``` ### 响应式最小骨架 如果当前模块只接入响应式主链,最少创建: ```text {module}/ pom.xml {module}-core/ {module}-engine-reactive/ {module}-r2dbc/ {module}-webflux/ ``` ### 约束 - `core` 是阻塞式与响应式共用的共享契约层 - 不再单独创建 `{module}-reactive-core` - 正式业务模块默认优先按完整骨架预留目录和 `pom.xml` - 即使某些子模块暂未实现,也建议先保留空骨架,避免后续模块结构失衡 - 只有在当前里程碑已明确限定为“阻塞式最小骨架”或“响应式最小骨架”时,才允许暂时省略其他子模块 - 后续补齐子模块时,必须继续沿用本文档的命名与分层,不得临时发明新的层名 ## 标准子模块物理目录模板 每个业务子模块都必须先生成统一的 `src` 骨架: ```text src/ ├── main │ ├── java │ │ └── .gitkeep │ └── resources │ └── .gitkeep └── test ├── java │ └── .gitkeep └── resources └── .gitkeep ``` 规则: - 即使当前还没有实现代码,也必须保留 `main/java`、`main/resources`、`test/java`、`test/resources` - 空目录通过 `.gitkeep` 入库 - 不允许只创建 `main/java` 而忽略测试目录 如果模块包含模块私有 `ErrorCode`、字段国际化或对象国际化,推荐继续补齐: ```text src/main/resources/ i18n/ineed/{module-segments}/ message.multiproperties messages.properties messages_en.properties messages_zh_TW.properties ``` 相关常量和 i18n 规则继续参考 [constant-errorcode-i18n-guide.md](/docs/constant-errorcode-i18n-guide/)。 模块自描述资源放在 `META-INF/ineed` 下,由框架通过 `classpath*:` 自动扫描聚合。不要把这类资源散落在 `resources` 根目录或业务目录下。 ```text src/main/resources/ META-INF/ineed/permissions/ {module-key}.json META-INF/ineed/security/whitelist/ {module-key}.properties ``` 规则: - 权限清单使用 `META-INF/ineed/permissions/*.json`。 - 安全访问白名单使用 `META-INF/ineed/security/whitelist/*.properties`。 - 不要再新增根目录 `whitelist.properties`,也不要使用 `security/{module}/whitelist.properties` 这类旧路径。 - i18n 继续放在 `i18n/ineed/...`,不要迁入 `META-INF`。 权限清单 JSON 的节点类型统一使用 `catalog / menu / page / api / button`: - `catalog` 表示目录或分组节点,不对应具体前端路由、组件或后端接口,不配置 `permissionUrl` / `component`。一级大类如平台、业务、运营、安全中心使用 `catalog`;多个独立菜单需要归组时,中间层也使用 `catalog`。 - `menu` 表示独立出现在菜单中的前端入口,必须配置前端 `permissionUrl` 和真实 `component`。只要资源是独立入口,就使用 `menu`,不要降级成 `page`。 - `page` 表示挂在某个菜单或目录下的隐藏/从属页面,不独立出现在菜单中,例如详情页、编辑页、配置页、主从资源的子列表页。典型识别信号是该页面依赖主资源 ID 进入,后端通常有 `ISearchOfMasterIdController` / `search-of-master-id` 能力。 - `api` 表达后端接口能力,`permissionUrl` 填后端接口路径;详情、批量详情、列表查询、字典查询、回收站查询、`search-of-master-id` 都应生成 `api`。 - `button` 表达页面可见操作按钮,不填 `permissionUrl`、`component`、`icon`。 - 独立入口不要套成 `menu -> page`。例如“系统身份”和“系统身份分组”都是独立入口时,结构应为 `安全中心 catalog -> 系统身份管理 catalog -> 系统身份 menu / 系统身份分组 menu`,而不是 `系统身份管理 menu -> 系统身份 page / 系统身份分组 page`。 - 权限清单必须补齐 `orderWeight`,按权限树 `DESC` 规则设计;同一资源推荐顺序为辅助查询 API、列表查询、CRUD、状态操作、回收站操作。 - `permissionDescription` 面向授权人员阅读,必须使用业务语言,不写 `idSet`、`query`、`commonParameters` 等后端参数名。 ## 子模块语义包结构 以下结构描述的是 `src/main/java` 下推荐的语义包布局。 ## 源码注释约定 开发者、代码生成器或自动化工具在创建业务模块源码时,Javadoc、`@author`、字段声明、注解分行、类声明换行和 import 整理统一遵守 [code-style-guide.md](/docs/code-style-guide/)。 本文档只补充业务模块骨架要求:生成模板中的默认职责边界应保持一致,`AO` 承载入参和输入校验,`Query` 只表达查询条件,`VO / Detail` 面向展示返回,`Entity` 只表达表结构和持久化映射,`Repository` 只提供数据访问能力,`Manager` 承载单实体规则、查询拼装、状态操作和持久化编排,`Service` 只组织一次业务用例流程,`Controller / API` 只暴露访问契约。 如果生成后业务规则已经明确,开发人员应继续把模板生成的通用说明补充为更具体的业务规则、状态约束、生命周期说明或异常边界,而不是保留空泛注释。 ### JPA QueryDSL Javadoc 构建约定 JPA 模块如果通过 `apt-maven-plugin` 生成 QueryDSL `Q*` 类,生成目录通常是 `target/generated-sources/java`。业务源码可以直接引用这些生成类,例如 `QLoginLog`、`QOperationLog`。编译阶段能够通过,是因为 Maven 编译会纳入生成源码;但 `maven-javadoc-plugin` 默认只扫描 `${project.build.sourceDirectory}`,即 `src/main/java`。 因此,JPA 模块只要源码 Javadoc 阶段需要解析 `Q*` 类型,就必须在该模块的 `maven-javadoc-plugin` 中补齐 `sourcepath`: ```xml org.apache.maven.plugins maven-javadoc-plugin ${project.build.sourceDirectory}${path.separator}${project.build.directory}/generated-sources/java ``` 这个配置的目的不是屏蔽 Javadoc 检查,也不是跳过 `Q*` 类,而是让 Javadoc 的类型解析范围与编译阶段一致,避免 `cannot find symbol: class Q...`。 父 POM 中的 `public` 只控制 Javadoc 对已纳入生成范围的 public API 生成文档;它不决定哪些源码目录被纳入类型解析,也不会专门屏蔽 QueryDSL `Q*` 类。`Q*` 生成类如果被 Javadoc 实际纳入文档生成集合,可能仍会出现 `no comment` 警告;这类 generated-sources 下的 `Q*` 注释警告可接受,不应为了它们给生成源码补手写注释或关闭真实源码的 Javadoc 质量检查。 需要注意的是,`sourcepath` 中加入 `target/generated-sources/java` 后,Javadoc 可以把 QueryDSL `Q*` 类当作依赖类型解析;但这不等同于把所有 `Q*` 类都作为本模块的主文档对象输出。也就是说,`Q*` 类虽然是 public,但只要它们只是被业务源码引用的解析依赖,而不是 Javadoc 插件本次要生成文档的源码入口,就不会因为 `public` 自动触发 `no comment` 警告。真正需要处理的是 `src/main/java` 下的业务源码 Javadoc 警告;`target/generated-sources/java` 下生成类的注释警告可以忽略。 ### MyBatis-Flex Annotation Processor 构建约定 MyBatis-Flex 模块如果使用 `@Table` 等注解生成辅助元数据,必须确保 `mybatis-flex-processor` 作为 annotation processor 参与编译。MyBatis-Flex 官方 Maven 文档建议把 `mybatis-flex-processor` 配置到 `maven-compiler-plugin` 的 `annotationProcessorPaths`;这属于当前编译模块自己的编译期处理器列表,不要假设依赖模块中的 processor 配置会自动作用到本业务模块。 Ineed 项目内可以在上层父 POM 统一维护 `mybatis-flex.version`,但 `mybatis-flex-processor` 本身应像 JPA QueryDSL APT 一样独立启用:只有需要生成 `entity.table.*TableDef` 的 `{module}-mybatis-flex` 子模块才配置该 processor。不要把 `mybatis-flex-processor` 放进全局父 POM 的通用 annotation processor path,避免所有 Java 模块都被无差别加载。 `mybatis-flex-processor` 只需要位于 annotation processor path 中,不应作为业务模块运行期依赖;如果为了 IDE 识别临时放入 ``,应使用 `provided`,不要把 processor 扩散为运行期依赖。 模块级核心配置示例: ```xml org.apache.maven.plugins maven-compiler-plugin com.mybatis-flex mybatis-flex-processor ${mybatis-flex.version} ``` 这个配置属于编译期 annotation processor 配置,不是 Javadoc `sourcepath` 配置;不要用 JPA QueryDSL 的 `apt-maven-plugin` 写法替代 MyBatis-Flex processor,也不要因为业务模块依赖了 `ineed-core-data-mybatis-flex` 就额外添加 `junit-jupiter` 等测试依赖。 ### `core` 新建业务模块推荐结构: ```text com/ineed/{domain}/{object}/ {Object}Constant.java enums/ contract/dto/ contract/ao/ contract/query/ contract/vo/ service/ service/reactive/ util/ ``` 约束: - `core` 只承载共享 contract、常量、枚举、工具类 - `core` 可以放跨模块复用的共享 `service` 契约,但不放业务 `service` 实现 - `core` 不放 ORM `entity` - `core` 不放 `repository`、`mapper`、`controller` - 同一共享能力如果同时存在阻塞式与响应式定义,阻塞式放 `service/`,响应式放 `service/reactive/` - 如果接口围绕当前登录上下文工作,优先直接传 `SecuritySession` 等聚合上下文对象,不把 `identityType`、`principalId`、`tenantKey` 拆成多个零散参数 - 如果某个动作型共享契约在阻塞式侧返回 `void`,响应式侧统一返回 `Mono`;只有查询、统计等读取型方法才返回具体结果值 - 新建模块和新增代码统一使用 `contract/*` 包名,而不是直接散落在根包下,也不要生成新的 `pojo/*` 兼容说明: - 遗留业务模块中可能仍然存在 `pojo/ao`、`pojo/query`、`pojo/vo` 一类旧命名 - `pojo/*` 只作为遗留代码识别项,不作为新模块或新增代码的生成目标 - 旧模块如果已经大面积使用 `pojo/*`,新增对象必须先迁移到 `contract/*` 或明确拆出迁移任务,不要继续扩散旧结构 - 创建新模块时,不应因为参考了历史示例目录,就回退生成 `pojo/*` 结构 ### `engine` 推荐结构: ```text com/ineed/{domain}/{object}/ manager/ service/ support/ ``` 约束: - `service` 是业务事务与编排边界 - `manager` 表达局部业务能力或规则 - `support` 只放少量辅助编排类 - `engine` 不放具体 ORM 实现 - 阻塞式 `service` 默认方法统一使用 `try/catch + ServiceSupport.logError(log, "...", e)` 记录异常后原样抛出,不再重复写 `if (log.isErrorEnabled())` 模板 - 阻塞式 `manager` 如果要访问可选下游能力,统一优先使用 `OperationSupport.getOperation(Class)` / `requireOperation(Class)` 获取 typed operation ### `jpa` 推荐结构: ```text com/ineed/{domain}/{object}/jpa/ entity/ repository/ manager/ converter/ ``` ### `jdbc` 推荐结构: ```text com/ineed/{domain}/{object}/jdbc/ entity/ querydsl/ repository/ manager/ converter/ ``` 补充约定: - `Repository` 默认生成 `interface`,不要回退生成 `class` - `Repository` 接口直接组合 `ineed-core-data-jdbc` 提供的 fragment,例如 `CreateRepository`、`UpdateRepository`、`FindByIdRepository`、`LastModifiedRepository`、`DataStatusRepository`、`LogicDeleteRepository`、`OrderRepository`、`TreeRepository`、`AccountRepository`、`PhysicsDeleteRepository` 等;按实体能力择需组合,不机械全量继承 - `Repository` 接口统一继续继承 `GetEntityClassOperation`,并在接口内保留 `default Class getEntityClass()` - 关系表、明细表或只承载少量动作的仓储,只组合真正需要的 fragment,并直接在接口中声明模块私有查询 / 删除方法 - 不再生成模块内的 `repository/support/*`、`Abstract*JdbcRepository` 或只做转发的 `RepositoryImpl` - 只有确实存在 fragment 无法覆盖的自定义 SQL 或 Spring Data JDBC 限制时,才允许新增具体 `Impl`;此时实现只承载那部分真实差异,不重复包一层通用 CRUD - JDBC manager 需要组装 Querydsl SQL 查询、排序、搜索字段或 join 时,必须在模块内提供手写 `querydsl/QXxx extends QuerydslSqlTable` 表路径类 - `QXxx` 类只承载 JDBC SQL 表达式元数据:表名、静态单例、业务字段 `StringPath` / `NumberPath` / `DateTimePath` 等;它是源码维护的 JDBC Querydsl SQL table path,不是 JPA APT 生成的 `Q*` 类 - manager 统一覆盖 `getSqlTablePath()` 返回对应 `QXxx` 静态单例,并在 `getKeySearchPath()`、`appendWhereOfQuery()`、`buildDefaultOrderBy()`、join 查询中直接引用 `QXxx.xxx.field` - 已存在 `QXxx` 可表达的列,不允许在 manager 查询组装中继续散落 `"column_name"` 字符串、`JdbcQuerydslUtil.stringColumn(getSqlTablePath(), "...")` 或等价临时列对象 - 当前 JDBC 代码生成的对标样板以 `ineed-security-authorization-system-identity-jdbc` 为准,`ineed-config-*` 与 `ineed-platform-tenant-jdbc` 已按这一路线收敛 ### `mybatis-flex` 推荐结构: ```text com/ineed/{domain}/{object}/mybatisflex/ entity/ mapper/ manager/ converter/ ``` 约束: - MyBatis-Flex 实现层使用 `mapper` - `mapper/` 只保留持久化接口,不承载对象转换实现 - 对象转换实现统一放 `converter/`,类名统一使用 `XxxConverter` - 业务 `*-mybatis-flex` 模块如果需要使用 `QueryWrapper` 拼装跨表查询,必须在本模块独立接入 `mybatis-flex-processor`;如果父级已配置 Lombok / MapStruct processor,本模块使用 `annotationProcessorPaths combine.children="append"` 追加 MyBatis-Flex processor,不要重复复制整套父级 processor 配置 - 跨表查询优先引用 processor 生成的 `entity.table.*TableDef`,例如 `XxxTableDef.XXX.as("x")` 后再使用生成列对象拼装 `select / join / where / orderBy`;不要在 `Manager` 或 `Mapper` 中手写一套散落的 `QueryTable / QueryColumn` 常量 - JPA / JDBC 实现层使用 `repository` - 新增或重构查询时,JPA 与 MyBatis-Flex 是优先优化路径:JPA 优先使用框架约定查询 / QueryDSL,MyBatis-Flex 优先使用 `QueryWrapper` + 生成的 `TableDef`。JDBC / R2DBC 保持当前 core 支撑层和集中 SQL 实现,只做必要维护,不为了风格统一主动引入 Criteria、jOOQ 或 MyBatis 集成。 - 字典查询默认标签列统一按 `label_value` / `labelValue` 对齐;默认标签读取失败时再 fallback 到 `id` ### `webmvc` 推荐结构: ```text com/ineed/{domain}/{object}/ controller/ service/ ``` 如确实存在明显分工差异,可按需要继续分为: - `controller/admin/` - `controller/internal/` - `controller/runtime/` 但如果尚未形成稳定分层,不要提前过度拆分。 补充约束: - `service/` 只放 `webmvc` 这一线的适配器或桥接实现,例如实现 `core` 中共享 service 契约的 `WebMvc*Service` - `webmvc` 模块中的 `service` 不承担业务主编排,不替代 `engine` 层 `service` ### `engine-reactive` 推荐结构: ```text com/ineed/{domain}/{object}/ manager/ service/ support/ ``` 补充约定: - 响应式 `service` 与 `manager` 的默认方法统一按订阅期装配,`requireManager(...)`、`requireOperation(...)`、上下文读取和能力判断都放进 reactive 链内部 - reactive `manager` 缺少下游 `operation` / `support` 时,应在订阅时发出错误信号,不保留链外同步 `throw` - 查询型 reactive `manager` 如果要给 `QueryParameters` 补默认过滤条件,也必须放进 `Mono.defer(...)` 或链内准备步骤;不要在返回 Publisher 之前同步修改入参 - create / update / delete / status / account 这类副作用型响应式 `manager` / `service`,阻塞式语义为 `void` 时,响应式统一返回 `Mono` ### `r2dbc` 推荐结构: ```text com/ineed/{domain}/{object}/r2dbc/ entity/ repository/ manager/ converter/ ``` 补充约定: - `Repository` 默认同样生成 `interface` - 响应式仓储与 `jdbc` 侧保持同一套路:直接组合 `ineed-core-data-r2dbc` fragment,并保留 `GetEntityClassOperation + default getEntityClass()` - 业务 `Manager` 不再实现 `getEntityClass()`;实体类型从 `getOperation().getEntityClass()` 读取,避免 `Repository` 和 `Manager` 两边返回不一致 - 没有真实自定义持久化逻辑时,不生成 `repository/support/*`、`Abstract*R2dbcRepository` 或空 `RepositoryImpl` - 只有遇到 fragment 无法覆盖的自定义 SQL 或框架限制时,才新增具体 `Impl`,并保持实现范围最小化 - `getKeySearchColumns()` 和 `appendWhereOfQuery(...)` 中的主表列必须使用 `getQualifiedColumn("column_name")`;不要直接写裸列名。只有确实需要 join 或 VO 扩展字段时才覆盖 `buildSelect()` / `buildJoin()` / `getDefaultTableAlias()` - 自定义 `RepositoryImpl` 一旦进入响应式链路,`entityClass`、`tableName`、`rowMapper` 等解析必须延迟到订阅期;优先复用 `getEntityClassMono()`、`getTableNameMono()` 或 `Mono.defer(...)` - 不允许在 Publisher 装配期提前求值 `getEntityClass()`、`getTableName()`,也不要生成 `Mono.just(getEntityClass())`、`Mono.just(getTableName())`、`final Class entityClass = getEntityClass()` 这类代码 - R2DBC manager 基类命名统一使用 `AbstractR2dbcManager`;旧的 `AbstractJdbcManager` 仅作为兼容别名保留,不要在新代码中继续引用 ### `webflux` 推荐结构: ```text com/ineed/{domain}/{object}/ controller/ service/reactive/ ``` 补充约束: - `service/reactive/` 只放 `webflux` 这一线的适配器或桥接实现,例如实现 `core` 中响应式共享契约的 `WebFlux*Service` - `webflux` 模块中的响应式适配器负责对外桥接和上下文转换,不替代 `engine-reactive` 层业务编排 补充约束: - 转换层统一使用 `converter/` 包,不再生成 `mapstruct/` - 转换实现类统一命名为 `XxxConverter` - MyBatis-Flex 的 `mapper` 与对象转换层 `converter` 必须拆开,避免同一聚合中两套语义不同的 `XxxMapper` - `XxxConverter` 继承 `AoEntityConverter` / `EntityVoConverter` 后,默认只保留继承关系和必要的 `INSTANCE`;不要重复生成 `ao2Entity`、`entityCopy`、`appleAo`、`entity2VO` - 需要字段表达式、字典标签、ignore 规则或其他方法级 MapStruct 配置时,才显式声明对应转换方法;普通一比一转换交给父接口继承方法和 MapStruct 生成实现 - 如果业务代码需要接入 `CreateManager` / `UpdateManager` / `FindByIdManager` / `OrderManager` 一类抽象,统一实现 `getCreateEntityConverter()`、`getUpdateEntityConverter()`、`getEntityVoConverter()` - 如果业务对象需要按稳定业务键重复创建或刷新,例如注册、同步、上报场景,显式实现 `CreateOrRefreshManager` 并实现 `findEntityByBusinessKey(ao)`;业务入口继续使用 `register` / `sync` / `importOne` 等命名并委托 `save(ao)`,不要生成 `saveOrUpdate` / `upsert` - 业务键查询需要在各数据栈同步落地:JPA / JDBC 返回 `Optional`,R2DBC 返回 `Mono`,MyBatis-Flex 可由 mapper 查询实体后在 Manager 包装 `Optional`;特殊 JDBC 更新只覆盖 `persistUpdatedEntity(...)` ## Java 类声明格式约定 业务模块生成 `Manager`、`Service`、`Controller`、`Repository`、`Mapper`、`Converter` 等类时,长 `extends` / `implements` 声明统一遵守 [code-style-guide.md](/docs/code-style-guide/)。 `ineed-develop-generator-template` 中的 Java 模板如果新增或调整 `AO / Query / VO / Detail / Entity / Repository / Manager / Controller / API / Constant` 等类模板,必须同步维护类级 Javadoc 的职责说明,避免重新生成后把业务模块注释质量打回空泛模板。 ## 共享扩展契约与 Web 适配约定 当某个能力由 A 模块定义、由 B 模块实现,并且需要同时支持 `webmvc` 与 `webflux` 两条线时,统一采用以下约定: - 共享接口定义优先放在发起方的 `core` 共享层,而不是散落在某个 `webmvc` / `webflux` 实现模块 - 阻塞式共享契约放 `service/` - 响应式共享契约放 `service/reactive/` - `webmvc` / `webflux` 模块只保留各自的适配实现,命名可使用 `WebMvc*Service` / `WebFlux*Service` - 调用方如果已经拿到当前会话对象,接口入参优先直接使用 `SecuritySession` 一类聚合上下文对象,而不是继续拆分成多个基础字段 - create / update / delete / status / account 这类副作用型响应式共享契约不要向上层暴露影响行数;阻塞式为 `void` 时,响应式统一定义为 `Mono` ## 新功能跨模块对齐规则 无论人工实现、代码生成器生成还是自动化工具修改,只要需求没有明确限制范围,新增功能都必须默认同步评估并对齐: - 数据层:`core`、`engine`、`engine-reactive`、`jpa`、`jdbc`、`mybatis-flex`、`r2dbc` - Web 层:`webmvc`、`webflux` - 对齐内容不只是类文件存在,还包括 `entity`、`repository / mapper`、`manager`、`service`、`controller`、映射、回调、统一返回、异常语义与测试入口 - 同一业务对象在不同模块中的目录层级、类命名、方法职责和代码风格必须保持一致,不能出现一边有 `manager / repository`、另一边缺层或风格漂移 - 阻塞式 `Service` 的异常日志收口也属于对齐范围,统一走 `ServiceSupport.logError(...)` - 阻塞式 `Manager` 的可选能力解析也属于对齐范围,统一优先走 typed `OperationSupport` - JDBC / JPA / MyBatis-Flex 的字典查询默认过滤语义必须一致:统一先 `handleQueryParameters(...)`,再强制 `dataStatus.enabled = true` - 只有在需求文档已经明确限定“本次只做阻塞式”或“本次只做指定数据栈”时,才允许暂时缩小范围;否则不要只改 `jpa` 或只改 `webmvc` ## 字段生成补充约定 无论人工实现、代码生成器生成还是自动化工具修改,新增 `entity`、`contract/*` 下对象时统一遵守 [code-style-guide.md](/docs/code-style-guide/),并额外满足: - `AO` 字段如果标记 `@Schema(requiredMode = RequiredMode.REQUIRED)`,必须同步增加 `jakarta.validation.constraints` 校验注解;`String` / `CharSequence` 使用 `@NotBlank`,`List` / `Set` / `Collection` / `Map` / 数组使用 `@NotEmpty`,其它对象、数字、布尔、时间、枚举类型使用 `@NotNull` - `AO` 字段如果标记 `RequiredMode.NOT_REQUIRED` 或未明确必填,不要为了“看起来更严格”生成 `@NotBlank`、`@NotEmpty`、`@NotNull`;业务条件必填应放在 Service / Manager 规则或专门的校验逻辑中表达 - 非必填字段的格式、范围、长度等输入形态校验,优先直接放在 `AO` 字段上使用 Jakarta Validation,例如 `@Email`、`@Pattern`、`@Size`、`@DecimalMin`、`@DecimalMax`;这类校验不需要额外编写 `*ValidationSupport` 或在 `Manager.beforeSave(...)` / `Manager.beforeUpdate(...)` 中重复调用 - 不要在 Controller 方法参数上新增 `@Valid`;Ineed-Core 的 Web 入口会按框架节奏手动调度 Jakarta Validation,便于统一封装报错消息,并精确控制什么时候验证、什么时候不验证 - 自定义 create / update / assign 等方法如果需要手动调度 AO 校验,应先记录入口日志,再执行 Jakarta Validation;不要在日志前展开 AO 字段做空值兜底,也不要为 AO 上已经表达的必填规则再写 `validateRequiredFields(...)` - `*ValidationSupport` 不承担 AO 字段级必填、格式、长度等输入形态校验;它只用于跨字段业务规则、依赖持久化状态的规则,或独立方法参数(例如 masterId、organizationId)这类不属于 AO 的入口参数 - 只有需要访问业务状态、持久化数据或跨字段业务规则时,才在 `Manager` 中覆盖 `validate(ao)`;不要为了业务校验覆盖 `save(ao)`、`update(ao)` 或仅转调 `super` 的 `beforeSave(...)`、`beforeUpdate(...)` - `AO / VO` 默认按未来 Excel 导入导出对象考虑;凡是明确参与导入导出的字段,应直接补齐 `@ExcelProperty` - 即使当前阶段暂未接入 Excel,也要保持 `AO / VO` 的字段命名、中文语义和注解落点可平滑接入,避免后续为导入导出返工拆类或改字段 ## 跨层禁行规则 - `core` 不放 ORM `entity` - `core` 不把共享扩展契约散落到某一条 Web 线实现模块 - `engine` / `engine-reactive` 不放 `repository` / `mapper` 实现 - `jpa` / `jdbc` / `mybatis-flex` / `r2dbc` 不放 `controller` - `webmvc` / `webflux` 不放持久化实现 - 同一扩展点的响应式定义不与阻塞式定义混放在同一个 `service` 根包下 - `Service` 不与 `Service` 互调 - 跨表业务通过组合多个 `Manager` 完成 - 删除顺序和依赖清理按业务规则落在正确的删除钩子或业务编排中 ## 创建模块前的一致性检查 创建业务模块前,至少必须检查以下内容: 1. SQL 文件、需求说明、目标模块目录三者是否一致 2. 聚合模块名称是否符合 `{domain}-{capability}-{object}` 3. 子模块名称是否符合 `{aggregate-module}-{layer}` 4. 是否已创建统一 `src` 骨架与 `.gitkeep` 5. 当前模块应走完整骨架、阻塞式最小骨架,还是响应式最小骨架 6. `core` 是否作为共享 contract 层被复用,而不是重复创建 `{module}-reactive-core` 7. `core` 下 contract 包命名是否已确定并在整个模块内保持一致 8. 如果存在共享扩展契约,是否已确定接口定义落在 `core`,并按 `service/` 与 `service/reactive/` 分包 9. 围绕当前会话上下文的接口,是否优先采用 `SecuritySession` 等聚合上下文对象作为入参 10. 是否已按 Ineed-Core 文档约定确定: - `Controller -> Service -> Manager -> Repository/EntityManager` - `Service` 不互调 - 删除扩展与日志/异常落点 11. 如果业务涉及多对多关系、级联删除、认证/授权或多数据栈实现,是否已按 [business-integration-test-guide.md](/docs/business-integration-test-guide/) 规划模块级主回归测试入口 12. 是否已明确本次功能要同步覆盖哪些数据栈与 Web 入口;如果没有明确限制,是否已按默认规则对齐 `jpa / jdbc / mybatis-flex / r2dbc` 以及 `webmvc / webflux` 13. 字段风格是否已统一遵守 [code-style-guide.md](/docs/code-style-guide/);是否没有生成新的 `pojo/*`;`AO` 中 `RequiredMode.REQUIRED` 字段是否已同步补齐 `@NotBlank` / `@NotEmpty` / `@NotNull`;需要导入导出的 `AO / VO` 是否已考虑 `@ExcelProperty` 如果以上检查未完成,不应直接开始生成代码。 ## SQL / 需求 / 目标模块一致性约定 创建或补齐业务聚合模块前,至少保持三类输入一致: - 一份 SQL 文档 - 一份需求说明 - 一个目标业务模块目录 命名应尽量保持一致,例如: - SQL:`ineed_security_access_credential.sql` - 需求:`ineed_security_access_credential.md` - 目标模块:`ineed-security-authorization-access-credential` 使用代码生成器或自动化工具生成前,必须先校验这三者和目标模块是否对齐;如不一致,应先停止并指出冲突。 ## 参考说明 [分类模块](/references/#source-396d04b2efa88169)目前已包含九层业务骨架和 `contract/*` 目录,可用于理解各层接入。逐层源码与运行步骤见[首个模块教程](/docs/first-module/)。 参考实际业务时,应核对当前 POM 和源码,不能沿用早期“缺少 JDBC/响应式、仍使用 pojo”的历史描述。新模块仍需按本规范决定实体能力、权限、租户和依赖边界,不逐字照抄样例中的所有业务规则。 ## 阅读建议 - 想看当前 `ineed-core` 框架真实稳定模块清单,先读 [module-inventory.md](/docs/module-inventory/) - 想理解框架能力分层与进入路径,读 [module-capability-map.md](/docs/module-capability-map/) - 想让代码生成器或自动化工具按固定规则建模块和写代码,结合 [development-contract.md](/docs/development-contract/) 一起使用本文档 > 最后更新:2026-05-15 --- # 代码格式与源码风格指南 来源:docs/project-development/code-style-guide.md SHA-256:06ead9a6a772d7735490d05c7b22f143bf7385069be11fad391268eb8147ad0b 网站:/docs/code-style-guide/ ## 文档定位 本文档集中约定基于 `ineed-core` 新增或修改后端业务源码时的代码格式、源码注释和基础风格规则。 这些规则不区分人工开发、代码生成器生成或自动化工具修改;所有业务源码都按同一套标准交付。 ## 适用范围 - Java 源码与测试源码 - `package-info.java` - `AO / Query / VO / DTO / Entity / Service / Manager / Repository / Mapper / Converter / Controller` - 业务模块中需要与 `ineed-core` 约定保持一致的源码模板 ## 源码版权头 新增 Java 源码时,文件头必须统一写入: ```java /* * Copyright (c) 2026 Ineedfull.Co.Ltd. All rights reserved. */ ``` 额外约束: - 适用范围包括 `src/main/java`、`src/test/java` 下的 `.java` 文件以及 `package-info.java` - 已有同类版权头的文件继续保持 `2026`,不要回退成旧年份 - 第三方许可证文件、上游源码镜像、非 Java 资源文件不要机械套用这条规则 ## 类级 Javadoc 类级 Javadoc 是正式交付内容,不能删除、压缩或空置。 统一要求: - `AO / Query / VO / DTO / Entity / Service / Manager / Repository / Mapper / Converter / Controller` 都要补齐类级 Javadoc - 类级 Javadoc 必须先说明职责边界,再按业务需要补充关键业务规则 - 类级 Javadoc 必须包含 `@author` 标签 - `@author` 的值按项目模板或既有模块约定填写 - 不要额外机械添加生成时间等易过期的生成信息 - 类级 Javadoc 除 `@author` 外,正文只描述职责边界和关键业务规则 - 不要只写“某某管理器”“某某服务类”“某某对象”这类空泛说明 默认职责口径: | 类型 | 类级 Javadoc 应表达的职责 | |------|------| | `AO` | 承载新增、修改等入参数据和接口层输入校验,不承载持久化逻辑或跨对象业务规则 | | `Query` | 只表达查询条件和过滤参数,查询组合、默认条件、权限范围由 `Manager` 统一拼装 | | `VO / Detail` | 面向接口返回和前端展示,写入操作仍使用对应 `AO` | | `Entity` | 只描述表结构和持久化映射,业务校验、状态流转和跨聚合规则落到 `Manager` 或领域支持类 | | `Repository / EntityManager` | 只提供底层数据访问能力,不承接业务流程判断或事务编排 | | `Mapper` | 只提供当前数据栈的持久化访问能力;对象转换实现不放在 `mapper` 包 | | `Converter` | 只负责实体、入参对象和返回对象之间的结构转换 | | `Manager` | 承载单实体规则、持久化编排、查询拼装、数据状态操作和必要领域校验 | | `Service` | 作为事务与业务用例编排边界,只组织流程,不直接访问 `Repository`,也不与其它 `Service` 互调 | | `Controller / API` | 只暴露访问契约、路由入口、参数校验和统一返回,不直接编排多表业务 | 如果需求中已经明确唯一性、状态流转、删除保护、级联清理、外部同步、审计或权限边界,必须把这些具体规则补充到对应源码的 Javadoc 或必要注释中,不能保留空泛模板原文作为最终交付。 类级 Javadoc 示例: ```java /** * 个人会员管理器 * *

负责个人会员单实体的创建、编辑、查询、数据状态切换和删除规则,不承接企业与会员关系的跨表编排。 * * @author "PangZhiGang, xiaopanghhh@163.com" */ public class MemberManager { } ``` ## 字段级 Javadoc 与字段声明 字段级 Javadoc 不是可选装饰。 统一要求: - `Entity` 所有成员变量都必须保留字段级 Javadoc,字段语义要和 SQL 及需求文档一致 - `core` 下 `contract/*` 中的 `AO / VO / Query / DTO` 以及其它普通契约对象,所有成员变量都必须保留字段级 Javadoc - `Entity` 与 `contract/*` 对象的成员变量统一使用 `protected` - 不要在同一模块内混用 `protected` 字段和 `private` 裸字段风格 - `@Schema(title = ...)`、Javadoc、`message.multiproperties` 中的对象名和字段名应保持一致 - 不要写空洞注释,例如“客户名称字段”“设置名称” - 字段注释应直接表达业务语义,例如“客户名称国际化 JSON 内容”“互动下次跟进时间” - 注解与字段声明分行书写,`@Schema(...)` 独占一行,下一行再写字段声明 正确格式示例: ```java /** * 经营场所 */ @Schema(title = "经营场所", requiredMode = RequiredMode.NOT_REQUIRED) protected String branchKey; ``` 错误格式示例: ```java /** 经营场所。 */ @Schema(title = "经营场所", requiredMode = RequiredMode.NOT_REQUIRED) protected String branchKey; ``` ## 实体数据状态与租户范围方法 继承 `AbstractTenantBase*Entity` 且需要声明数据状态能力或覆盖租户范围的实体,统一保持固定的类体布局和注解格式。 统一要求: - `@EnableDataStatus` 必须使用多行格式,每个 `DataStatusFeature` 独占一行 - `@EnableDataStatus` 必须使用 `//@formatter:off` / `//@formatter:on` 做局部保护 - `@EnableDataStatus` 的 formatter 标记只包住该注解,不要把 `@Table`、`@Entity`、Lombok 注解或类声明包进去 - `//@formatter:on` 后必须换行再写下一个注解,不能出现 `//@formatter:on@Table(...)` 这类粘连 - 类体内先声明 `serialVersionUID`,再放 `tenantScopeMode()`,再放业务成员变量 - `tenantScopeMode()` 只表达租户范围策略,不要夹在字段声明之间,也不要放到类尾 - `tenantScopeMode()` 内部保持标准 2 空格缩进,方法体内语句再缩进 2 空格 正确格式示例: ```java //@formatter:off @EnableDataStatus({ DataStatusFeature.FIXED, DataStatusFeature.ENABLED, DataStatusFeature.LOCKED }) //@formatter:on @Table("icfg_category_definition") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CategoryDefinition extends AbstractTenantBaseEntity { @Serial private static final long serialVersionUID = 1L; @Override public TenantScopeMode tenantScopeMode() { return TenantScopeMode.GLOBAL_ONLY; } /** * 应用标识 */ @Column("application_key") protected String applicationKey; } ``` 错误格式示例: ```java @EnableDataStatus({ DataStatusFeature.FIXED, DataStatusFeature.ENABLED, DataStatusFeature.LOCKED }) @Table("icfg_category_definition") public class CategoryDefinition extends AbstractTenantBaseEntity { /** * 应用标识 */ @Column("application_key") protected String applicationKey; @Override public TenantScopeMode tenantScopeMode() { return TenantScopeMode.GLOBAL_ONLY; } } ``` ## 方法 Javadoc 统一要求: - 方法 Javadoc 必须使用标准多行格式 - 摘要与 `@param`、`@return`、`@throws` 等标签之间必须保留一个空 `*` 行 - Javadoc 文本不要机械追加中文句号 `。` - 如果需求原文已经带标点,避免重复标点 正确格式示例: ```java /** * 追加应用指标 * * @param ao 应用指标提交对象 * @return 应用指标视图对象 */ ``` ## Javadoc 格式 统一要求: - 类级、字段级和方法级 Javadoc 必须保留 - `/**` 必须独占一行,摘要从下一行 `* ` 开始 - 不要生成 `/** 摘要 */`、`/** 摘要。 */` 或 `/** 摘要` 这类压缩格式 - `package-info.java` 也必须保留包级 Javadoc 和 `@author` ## Java 类声明格式 编写 `Manager`、`Service`、`Controller`、`Repository`、`Mapper`、`Converter` 以及其他长继承或长接口组合类声明时,统一遵守: - 当 `extends` / `implements` 后存在两个及以上父类型、fragment、能力接口,或单行声明明显过长时,继承 / 接口列表统一换行 - 使用 `//@formatter:off` / `//@formatter:on` 只包住继承 / 接口列表 - `extends` / `implements` 关键字放在类声明首行末尾,行尾不要留空格 - 每个父类型独占一行,使用 2 空格缩进,除最后一个父类型外保留逗号 - `//@formatter:on` 放在类体第一行之后,不能包住字段、构造器、方法或整个类 - 类体开始后,首个字段或成员声明前保留一个空行 - 单一父类型且声明较短时,可以保持单行,不要为了形式化强行加 formatter 标记 示例: ```java public interface IXxxManager extends //@formatter:off ICreateManager, IFindByIdManager, ISearchManager { //@formatter:on } public class XxxManager extends AbstractJdbcManager implements //@formatter:off IXxxManager, CreateManager, SearchManager { //@formatter:on private final XxxRepository xxxRepository; } ``` ## 局部 formatter 保护 自动格式化是默认规则,但有些代码块的换行、缩进和对齐本身承载了阅读结构。遇到这类块时,应使用 `//@formatter:off` / `//@formatter:on` 进行局部保护,避免 IDE 或批量格式化把结构压扁。 必须保护的典型场景: - 长 `extends` / `implements` 接口列表 - 多行 `@EnableDataStatus` 数据状态能力声明 - 多行 fluent chain,例如 Reactor 链、Stream 链、QueryDSL / MyBatis-Flex `QueryWrapper` 链 - 多行 builder,尤其是安全用户、权限树、数据范围快照、字典项等含多个业务字段的构造 - 多行 SQL / R2DBC / JDBC 查询绑定链 - 多行 `List.of(...)`、字典构造、Comparator 链 - Manager 中 `getKeySearchPath()` / `getKeySearchColumns()` 返回的查询键数组,包括空数组 - 人工排版过、格式变化会明显降低可读性的业务规则块 统一要求: - formatter 标记只包住需要保护的最小代码块,不包整个方法、整个类或无关字段 - `//@formatter:off` 与 `//@formatter:on` 必须成对出现 - 使用项目既有写法 `//@formatter:off`,不要写成 `// @formatter:off` - 普通短链、单行 builder、简单 getter / setter、短 Repository 调用不需要机械保护 - 测试代码中只有确实依赖多行结构表达场景意图时才加保护,不要把所有测试 builder 都包起来 - 新增或批量修改后,至少检查 formatter 标记数量是否平衡 批量调整流程: 1. 改动前先确认当前工作区是否已有无关修改;有无关修改时不要运行会覆盖大范围文件的格式化命令。 2. 对本次修改范围内的文件执行项目默认 formatter,优先使用 IDE 或项目既有格式化配置,不手写一套新的缩进规则。 3. 对需要人工排版的结构化代码块补 `//@formatter:off` / `//@formatter:on`,再重新格式化确认保护有效。 4. 格式化后用 diff 复核,只保留本次任务需要的排版变化。无关文件被 formatter 改动时,应从本次提交中排除。 5. 提交前运行 formatter 标记平衡检查,并用 `git diff --check` 检查尾随空白、冲突标记和空白错误。 示例: ```java //@formatter:off return roleManager.findById(roleId) .switchIfEmpty(Mono.error(StatefulException.of( AccessControlConstant.ErrorCode.ROLE_NOT_FOUND, "角色不存在"))) .flatMap(role -> Boolean.TRUE.equals(role.getDeleted()) ? Mono.error(StatefulException.of( AccessControlConstant.ErrorCode.ROLE_NOT_FOUND, "角色不存在")) : Mono.just(role)); //@formatter:on ``` ```java //@formatter:off return Comparator .comparing(Permission::getOrderWeight, Comparator.nullsLast(Comparator.reverseOrder())) .thenComparing(Permission::getCreatedTimestamp, Comparator.nullsLast(Comparator.naturalOrder())) .thenComparing(Permission::getId, Comparator.nullsLast(Comparator.naturalOrder())); //@formatter:on ``` ```java @Override public StringPath[] getKeySearchPath() { //@formatter:off return new StringPath[] { QMember.member.memberNo, QMember.member.memberName, QMember.member.mobile }; //@formatter:on } @Override public String[] getKeySearchColumns() { //@formatter:off return new String[] { }; //@formatter:on } ``` 批量修改后可用以下命令做平衡检查: ```bash awk '/formatter:off/{off++} /formatter:on/{on++} END{print "off=" off " on=" on}' $(find -L java -path '*/target/*' -prune -o -name '*.java' -print) ``` ## import 与注解格式 统一要求: - `import` 按 IDE / formatter 默认规则整理 - 同一组 import 之间不要每个 import 都插入空行 - 注解独占一行,不要和类声明、字段声明挤在同一行 - `@Schema(...)`、验证注解、Excel 注解和字段声明分行书写 ## 第三方 API 使用 新增或修改代码时,不允许继续使用 JDK 或第三方依赖中已经标记为 `@Deprecated` 的 API。 统一要求: - 编写或修改代码前,应处理 IDE / 编译器提示的 deprecation warning,不要用 `@SuppressWarnings("deprecation")` 掩盖问题 - 修改遗留代码时,如果当前方法或当前改动路径中存在过期 API,应顺手迁移到推荐替代 API - Apache Commons Lang 3 中已过期的字符串比较和查找方法不要继续使用,例如 `StringUtils.equals(...)`、`StringUtils.equalsIgnoreCase(...)`、`StringUtils.contains(...)`、`StringUtils.containsIgnoreCase(...)` - 大小写敏感比较和查找统一使用 `org.apache.commons.lang3.Strings.CS.equals(...)`、`Strings.CS.contains(...)` - 大小写不敏感比较和查找统一使用 `org.apache.commons.lang3.Strings.CI.equals(...)`、`Strings.CI.contains(...)` - `StringUtils.isBlank(...)`、`StringUtils.isNotBlank(...)`、`StringUtils.defaultIfBlank(...)` 等未过期的空白判断和默认值方法可以继续使用 示例: ```java // 错误:StringUtils.equals(...) 已过期 StringUtils.equals(user.getTenantKey(), tenantKey); // 正确:大小写敏感比较 Strings.CS.equals(user.getTenantKey(), tenantKey); // 正确:大小写不敏感查找 Strings.CI.contains(message, keyword); ``` ## 通用实体可用性判断 涉及关系绑定、授权对象筛选、候选数据过滤等场景时,不要在各业务模块重复手写“未删除、租户匹配、数据状态可用”判断。 统一要求: - 基础必要检查是逻辑删除和租户归属:实体必须未删除,且 `tenantKey` 必须匹配 - 数据状态检查是可选条件:按业务场景传入 `DataStatusFilter`,为空或不包含任何字段时不检查数据状态 - 关系绑定这类只做通用可用性过滤的场景,优先直接调用 `EntityAvailabilitySupport`,不要额外抽取无业务增量的私有判断函数 - 数据状态过滤对象统一使用 `DataStatusFilter` 示例: ```java if (!EntityAvailabilitySupport.isAvailableInTenant(user, tenantKey)) { continue; } ``` ## 日志注解 新增具体业务类统一使用 Lombok 的 `@Slf4j`,不要继续手写 `LoggerFactory.getLogger(...)`。 标准支持接口已有默认日志时,空壳实现不要再包一层重复日志;自定义方法按 [delete-hook-and-logging-guide.md](/docs/delete-hook-and-logging-guide/) 补入口日志和异常日志。 ## 生成与修改检查清单 提交或交付前至少检查: 1. Java 源码和 `package-info.java` 是否具备统一版权头 2. 类级 Javadoc 是否描述职责边界和关键业务规则 3. 类级 Javadoc 是否包含 `@author` 4. 是否没有机械添加生成时间等易过期信息 5. 字段级 Javadoc 是否完整,且语义与 SQL、需求和 `@Schema` 一致 6. `Entity` 与 `contract/*` 对象的成员变量是否统一使用 `protected` 7. 实体 `tenantScopeMode()` 是否位于 `serialVersionUID` 后、业务成员变量前 8. `@EnableDataStatus` 是否使用 formatter 保护,且每个 `DataStatusFeature` 独占一行 9. 是否没有单行压缩 Javadoc 10. 方法 Javadoc 的摘要与标签之间是否保留空行 11. 注解、字段声明和类声明是否分行书写 12. 长 `extends` / `implements` 是否只用局部 formatter 标记包住接口列表,且父类型列表使用 2 空格缩进 13. `import` 是否按 formatter 默认规则整理 14. 新写具体类是否统一使用 `@Slf4j` 15. 是否没有继续使用 JDK 或第三方依赖中已标记为过期的 API 16. 手写排版的链式调用、builder、SQL、字典、Comparator 等结构化代码块是否已用局部 formatter 标记保护,且 `off/on` 数量平衡 17. 批量格式化是否只影响本次任务相关文件,且无关 formatter diff 没有进入提交 18. 是否已执行 `git diff --check`,确认没有空白错误 > 最后更新:2026-06-01 --- # CRUD / Query 开发主路径 来源:docs/project-development/crud-query-guide.md SHA-256:b7758e48b216767baeb89f5ae31d67142b608b171512b2e83a119ef153fd95db 网站:/docs/crud-query-guide/ ## 适用范围 本文档覆盖 Ineed-Core 当前两条开发主链路: - **阻塞式**:`WebMvc + JPA/JDBC/MyBatis-Flex` - **响应式**:`WebFlux + R2DBC` 阻塞式链路是当前优先推荐的验收基线;响应式链路作为对等能力线同步维护。新业务模块、公共能力或代码生成任务,如果需求没有明确限定单线实现,应同时评估并对齐两条线。 ## 标准主路径 两条链路共享同一分层架构: `Controller -> Service -> Manager -> Repository/EntityManager` | 层次 | 阻塞式模块 | 响应式模块 | 职责 | |------|-----------|-----------|------| | Web 入口 | `ineed-core-webmvc` | `ineed-core-webflux` | 对外暴露 API、参数校验、统一返回 | | 业务编排 | `ineed-core-engine` | `ineed-core-engine-reactive` | 事务边界、业务编排、桥接 Manager | | 业务能力 | `ineed-core-engine` | `ineed-core-engine-reactive` | 单类实体规则、CRUD、查询、状态切换 | | 数据访问 | `ineed-core-data-jpa/jdbc/mybatis-flex` | `ineed-core-data-r2dbc` | 持久化与查询执行 | ## 标准能力入口总览 以下能力按需组合,只有业务对象具备对应能力时才实现对应接口。 | 能力 | WebMvc Controller | WebFlux Controller | Service | Manager | 说明 | |------|------|------|------|------|------| | 创建 | `ICreateController` | `ICreateController` | `ICreateService` | `ICreateManager` | 单条与批量创建 | | 更新 | `IUpdateController` | `IUpdateController` | `IUpdateService` | `IUpdateManager` | 按 `AO.id` 更新 | | 按 ID 查询 | `IFindByIdController` | `IFindByIdController` | `IFindByIdService` | `IFindByIdManager` | 单 ID 与批量 ID 查询 | | 分页查询 | `ISearchController` | `ISearchController` | `ISearchService` | `ISearchManager` | 标准 `CommonParameters + Query` | | 回收站查询 | `ISearchRecycleController` | `ISearchRecycleController` | `ISearchRecycleService` | `ISearchRecycleManager` | 查询已逻辑删除数据 | | 逻辑删除与恢复 | `ILogicDeleteController` | `ILogicDeleteController` | `ILogicDeleteService` | `ILogicDeleteManager` | 单/批量逻辑删除、恢复 | | 物理删除 | `IPhysicsDeleteController` | `IPhysicsDeleteController` | `IPhysicsDeleteService` | `IPhysicsDeleteManager` | 单/批量物理删除;`forceDelete` 无标准 Web 入口 | | 字典查询 | `ISearchDictionaryController` | `ISearchDictionaryController` | `ISearchDictionaryService` | `ISearchDictionaryManager` | 返回 `DictionaryValue` | | 树查询 | `ISearchTreeController` | 同左 | `ISearchTreeService` | `ISearchManager` | 返回树节点,`query.lazyLoad=true` 时按父节点懒加载 | | 树字典查询 | `ISearchDictionaryTreeController` | 同左 | `ISearchDictionaryTreeService` | `ISearchManager` | 返回字典树 | | 按主对象查询 | `ISearchOfMasterIdController` | 同左 | `ISearchOfMasterIdService` | `IFindByIdManager` + `ISearchManager` | 先查主对象再查关联列表 | | 按主对象字典查询 | `ISearchDictionaryOfMasterIdController` | 同左 | `ISearchDictionaryOfMasterIdService` | `IFindByIdManager` + `ISearchDictionaryManager` | 主对象关联字典查询 | | 按主对象字典树查询 | `ISearchDictionaryTreeOfMasterIdController` | 同左 | `ISearchDictionaryTreeOfMasterIdService` | `IFindByIdManager` + `ISearchManager` | 主对象关联字典树查询 | | 数据状态 | `IEnableController` / `ILockController` / `IPublishController` / `IDefaultController` / `IPrimaryController` | 同左 | 对应 `*Service` | 对应 `*Manager` | 单/批量状态切换 | | 排序 | `IOrderController` | `IOrderController` | `IOrderService` | `IOrderManager` | moveBefore/After/Top/Bottom | | 重复校验 | `IDuplicateController` | `IDuplicateController` | `IDuplicateService` | `IDuplicateManager` | 创建/更新场景重复检查 | | 账户动作 | `IAccountController` | `IAccountController` | `IAccountService` | `IAccountManager` | 重置密码、启用、禁用、锁定、解锁 | ## CRUD 入口矩阵 所有路径相对于业务 Controller 根路径。权限后缀两条线一致。 | 能力 | HTTP | 路径 | 权限后缀 | WebMvc 返回 | WebFlux 返回 | |------|------|------|------|------|------| | 单条创建 | `POST` | 根路径 | `create` | `ResponseResults` | `Mono>` | | 批量创建 | `POST` | `/batch` | `batch-create` | `ResponseResults>` | `Mono>>` | | 更新 | `PUT` | 根路径 | `update` | `ResponseResults` | `Mono>` | | 逻辑删除 | `DELETE` | `/{id}` | `logic-delete` | `ResponseResults` | `Mono>` | | 批量逻辑删除 | `DELETE` | `/batch/logic-delete` | `batch-logic-delete` | `ResponseResults` | `Mono>` | | 恢复 | `PUT` | `/recovery/{id}` | `recovery` | `ResponseResults` | `Mono>` | | 批量恢复 | `PUT` | `/recovery` | `batch-recovery` | `ResponseResults` | `Mono>` | | 物理删除 | `DELETE` | `/physics/{id}` | `physics-delete` | `ResponseResults` | `Mono>` | | 批量物理删除 | `DELETE` | `/batch/physics-delete` | `batch-physics-delete` | `ResponseResults` | `Mono>` | `forceDelete(id / idSet)` 只存在于 `IPhysicsDeleteService / IPhysicsDeleteManager`,标准 Controller 未暴露入口。业务如需开放强制删除,必须显式定义自定义接口和权限点。 ## 查询入口矩阵 | 能力 | HTTP | 路径 | WebMvc 返回 | WebFlux 返回 | |------|------|------|------|------| | 分页查询 | `GET` | 根路径 | `ResponseResults>` | `Mono>>` | | 按 ID 查询 | `GET` | `/{id}` | `ResponseResults` | `Mono>` | | 批量按 ID 查询 | `GET` | `/batch?idSet=...` | `ResponseResults>` | `Mono>>` | | 回收站查询 | `GET` | `/recycle` | `ResponseResults>` | `Mono>>` | | 字典查询 | `GET` | `/dictionary` | `ResponseResults>` | `Mono>>` | | 树查询 | `GET` | `/tree` | `ResponseResults>` | `Mono>>` | | 树字典查询 | `GET` | `/tree/dictionary` | `ResponseResults>` | `Mono>>` | | 按主对象查询 | `GET` | `/of/{masterId}` | `ResponseResults>` | `Mono>>` | | 按主对象字典查询 | `GET` | `/dictionary/of/{masterId}` | `ResponseResults>` | `Mono>>` | | 按主对象字典树查询 | `GET` | `/tree/dictionary/of/{masterId}` | `ResponseResults>` | `Mono>>` | 查询类标准 Controller 默认方法没有接口级 `@PreAuthorize`。如果业务模块要在权限清单中登记查询 API 权限,应按 [frontend-integration-guide.md](/docs/frontend-integration-guide/) 生成 `api` 节点。 ## 数据状态、排序、重复和账户入口矩阵 | 能力 | HTTP | 路径 | 权限后缀 | WebMvc 返回 | WebFlux 返回 | |------|------|------|------|------|------| | 启用 | `PUT` | `/enable/{id}` | `enable` | `ResponseResults` | `Mono>` | | 批量启用 | `PUT` | `/enable` | `batch-enable` | `ResponseResults` | `Mono>` | | 禁用 | `PUT` | `/disable/{id}` | `disable` | `ResponseResults` | `Mono>` | | 批量禁用 | `PUT` | `/disable` | `batch-disable` | `ResponseResults` | `Mono>` | | 锁定 | `PUT` | `/lock/{id}` | `lock` | `ResponseResults` | `Mono>` | | 批量锁定 | `PUT` | `/lock` | `batch-lock` | `ResponseResults` | `Mono>` | | 解锁 | `PUT` | `/unlock/{id}` | `unlock` | `ResponseResults` | `Mono>` | | 批量解锁 | `PUT` | `/unlock` | `batch-unlock` | `ResponseResults` | `Mono>` | | 发布 | `PUT` | `/publish/{id}` | `publish` | `ResponseResults` | `Mono>` | | 批量发布 | `PUT` | `/publish` | `batch-publish` | `ResponseResults` | `Mono>` | | 撤销发布 | `PUT` | `/withdraw/{id}` | `withdraw` | `ResponseResults` | `Mono>` | | 批量撤销发布 | `PUT` | `/withdraw` | `batch-withdraw` | `ResponseResults` | `Mono>` | | 设为默认 | `PUT` | `/default/{id}` | `default` | `ResponseResults` | `Mono>` | | 批量设为默认 | `PUT` | `/default` | `batch-default` | `ResponseResults` | `Mono>` | | 取消默认 | `PUT` | `/non-default/{id}` | `non-default` | `ResponseResults` | `Mono>` | | 批量取消默认 | `PUT` | `/non-default` | `batch-non-default` | `ResponseResults` | `Mono>` | | 设为主要 | `PUT` | `/primary/{id}` | `primary` | `ResponseResults` | `Mono>` | | 批量设为主要 | `PUT` | `/primary` | `batch-primary` | `ResponseResults` | `Mono>` | | 取消主要 | `PUT` | `/non-primary/{id}` | `non-primary` | `ResponseResults` | `Mono>` | | 批量取消主要 | `PUT` | `/non-primary` | `batch-non-primary` | `ResponseResults` | `Mono>` | | 移到目标前 | `PUT` | `/order/before` | `order` | `ResponseResults` | `Mono>` | | 移到目标后 | `PUT` | `/order/after` | `order` | `ResponseResults` | `Mono>` | | 移到顶部 | `PUT` | `/order/top` | `order` | `ResponseResults` | `Mono>` | | 移到底部 | `PUT` | `/order/bottom` | `order` | `ResponseResults` | `Mono>` | | 创建重复校验 | `POST` | `/duplicate` | `duplicate` | `ResponseResults` | `Mono>` | | 更新重复校验 | `PUT` | `/duplicate` | `duplicate` | `ResponseResults` | `Mono>` | | 重置账户密码 | `PUT` | `/account/passwd/reset/{id}` | `account-passwd-reset` | `ResponseResults` | `Mono>` | | 禁用账户 | `PUT` | `/account/disable/{id}` | `account-disable` | `ResponseResults` | `Mono>` | | 启用账户 | `PUT` | `/account/enable/{id}` | `account-enable` | `ResponseResults` | `Mono>` | | 锁定账户 | `PUT` | `/account/lock/{id}` | `account-lock` | `ResponseResults` | `Mono>` | | 解锁账户 | `PUT` | `/account/unlock/{id}` | `account-unlock` | `ResponseResults` | `Mono>` | 数据状态能力由实体 `DataStatusFeature` 决定,不应默认全开。实体没有对应能力时不生成对应 Controller / Service / Manager。 ## CRUD 接入步骤 ### 1. 定义 Web 层入口 阻塞式入口位于 `ineed-core-webmvc`,响应式入口位于 `ineed-core-webflux`,围绕以下接口展开: - `ICreateController` / `IUpdateController` / `ILogicDeleteController` / `IPhysicsDeleteController` ### 2. 定义 Service 层桥接 - `ICreateService` / `IUpdateService` / `ILogicDeleteService` / `IPhysicsDeleteService` 通过 `getManager()` 暴露 Manager,通过 `ServiceSupport.requireManager(...)` 调用具体能力。 ### 3. 定义 Manager 层能力 - `ICreateManager` / `IUpdateManager` / `ILogicDeleteManager` / `IPhysicsDeleteManager` 具体数据栈实现在对应 JPA/JDBC/MyBatis-Flex/R2DBC 模块中完成。 ### 4. 删除能力语义 - 逻辑删除使用 `ILogicDelete*` 系列接口 - 恢复已删除数据由 `recovery(...)` 暴露 - 物理删除使用 `IPhysicsDelete*` 系列接口 - 删除前保护、删除后清理和恢复补偿规则见 [delete-hook-and-logging-guide.md](/docs/delete-hook-and-logging-guide/) ## 查询接入步骤 ### 1. 定义查询 Web 入口 列表查询入口是 `ISearchController`,接收 `CommonParameters` 与业务 `Query` 组装为 `QueryParameters`。 按 ID 查询入口是 `IFindByIdController`,支持 `GET /{id}`(单条)和 `GET /batch?idSet=...`(批量)。 ### 2. 定义查询 Service / Manager 列表查询:`ISearchService` → `ISearchManager`,返回 `QueryResults`。 按 ID 查询:`IFindByIdService` → `IFindByIdManager`。 ### 3. 统一参数与返回模型 - `QueryParameters` 统一承载 `searchKey`、`current`、`pageSize`、`sorterList`、`queryObject` - `AbstractVO` 作为标准返回对象基类 - `ResponseResults` 作为统一响应外壳 ### 4. 扩展查询入口 | 查询能力 | 路径常量 | 返回模型 | |------|------|------| | 回收站查询 | `Urls.SEARCH_RECYCLE` | `QueryResults` | | 字典查询 | `Urls.SEARCH_DICTIONARY` | `QueryResults` | | 树查询 | `Urls.SEARCH_TREE` | `QueryResults` | | 树字典查询 | `Urls.SEARCH_TREE_DICTIONARY` | `QueryResults` | | 按主对象查询 | `Urls.SEARCH_OF_MASTER_ID` | `MasterQueryResults` | | 按主对象字典查询 | `Urls.SEARCH_DICTIONARY_OF_MASTER_ID` | `QueryResults` | | 按主对象字典树查询 | `Urls.SEARCH_DICTIONARY_TREE_OF_MASTER_ID` | `MasterQueryResults` | `SearchOfMasterId` 是主从/关联场景:先用 `IFindByIdManager` 查主对象,再用 `ISearchManager` 查询关联列表。 ## 动态注册约定 `ineed-core-webmvc` 扫描 `IWebMvcApi` Bean,`ineed-core-webflux` 扫描 `IWebFluxApi` Bean,把标准接口默认方法注册到路由中。 业务控制器通常只需要: - 实现对应 `I*Controller` 接口 - 提供 `getResourceKey()` 和 `getResourceName()` - 不需要为每个默认方法重复手写转发 `getResourceKey()` 和 `getResourceName()` 优先返回模块常量中的 `XxxConstant.Resource.KEY_*` / `XxxConstant.Resource.NAME_*`。 `getResourceName()` 返回业务展示名,OpenAPI 公共接口摘要会用它将 `资源` 占位替换为具体业务名称。 ## 响应式实现约束 WebFlux 实现必须避免将响应式链路退化成装配期同步执行: - 自定义 WebFlux Controller 方法统一返回 `Mono.defer(() -> { ... })`,把入口日志、上下文读取、参数准备和结果封装都放进订阅期 - Controller 中包装 Service 返回值时优先使用 `ResultBuild.success(serviceMono)`;不要在链外先执行日志或同步逻辑后再用 `serviceMono.flatMap(ResultBuild::success)` 拼接 - Controller 参数校验通过 `Mono.defer(...)` 和 `ValidationExceptionSupport.validate(...)` 延迟到订阅期 - 无返回体动作型接口统一返回 `Mono>`,使用 `.then(ResultBuild.success())` - 集合结果返回 `Mono>>`,不生成 `Mono>>` - `Flux` 只保留在 Service / Manager 内部或真正的流式接口中 - R2DBC manager / repository 中不使用 `Mono.just(getEntityClass())` 等装配期求值写法,使用 `Mono.defer(...)` 或 `getEntityClassMono()` ## WebMvc 与 WebFlux 对齐要求 同一业务对象如果同时暴露两条线,必须保持一致: - URL 路径一致 - 权限资源后缀一致 - 参数名一致(`common`、`query`、`idSet`) - `getResourceKey()` / `getResourceName()` 语义一致 - 返回 `ResponseResults` / `QueryResults` / `MasterQueryResults` 结构一致 - 标准能力组合一致,除非需求明确排除 ## 非业务对象级内置入口 `ineed-core-webmvc` 和 `ineed-core-webflux` 还包含健康检查、i18n 语言列表和应用模块元数据等内置 Controller。这些属于框架运行支撑,生成业务模块时不要为每个业务对象复制。 ## 运行与验证 阻塞式主链路验证: ```bash mvn -pl ineed-core-tests/ineed-core-test-webmvc -am \ -Dtest=WebMvcJpaBusinessChainTest,WebMvcJdbcBusinessChainTest,WebMvcMybatisFlexBusinessChainTest,\ WebMvcJpaSearchBusinessChainTest,WebMvcJdbcSearchBusinessChainTest,WebMvcMybatisFlexSearchBusinessChainTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` 响应式主链路验证: ```bash mvn -pl ineed-core-reactive-modules/ineed-core-webflux,ineed-core-tests/ineed-core-test-webflux -am test ``` ## 当前不覆盖 - 项目开发契约 → [development-contract.md](/docs/development-contract/) - 自定义业务约定细则 → [custom-business-layering-guide.md](/docs/custom-business-layering-guide/) - 删除钩子与日志追踪 → [delete-hook-and-logging-guide.md](/docs/delete-hook-and-logging-guide/) - 前端联调 → [frontend-integration-guide.md](/docs/frontend-integration-guide/) > 最后更新:2026-05-15 --- # 自定义业务与分层扩展指南 来源:docs/project-development/custom-business-layering-guide.md SHA-256:04c28d38ffe54dc458ed12c6e60eb2977a86ff146e3704128eebed28e5c6ef50 网站:/docs/custom-business-layering-guide/ ## 适用范围 本指南面向已经沿 Ineed-Core 标准主路径接入业务模块的开发人员,适用于阻塞式 `WebMvc + JPA/JDBC/MyBatis-Flex`,也适用于响应式 `WebFlux + R2DBC` 的对等扩展。 当标准 CRUD / query 之外还需要增加自定义业务、跨表协作、状态切换后的补充动作、排序相关规则时,应该继续沿现有分层扩展,而不是临时发明平行层。 ## 标准扩展主路径 Ineed-Core 当前推荐的自定义业务扩展主路径保持不变。 `Controller -> Service -> Manager -> Repository/EntityManager` 阻塞式与响应式链路均按 [crud-query-guide.md](/docs/crud-query-guide/) 中各自运行时的接口矩阵执行。本文只补充“标准能力之外的自定义业务应该如何继续往下写”的通用分层规则。 ## 共享扩展契约补充约定 当某个自定义能力由一个模块定义、另一个模块实现,并且需要同时支持 `webmvc` 与 `webflux` 两条线时,额外遵守: - 共享接口定义优先放在发起方的 `core` 共享层,而不是落在具体实现模块 - 阻塞式接口放 `service/` - 响应式接口放 `service/reactive/` - `webmvc` / `webflux` 模块只保留各自的适配实现,不重复定义另一套共享契约 - 调用方如果已经持有当前会话对象,接口入参优先直接传 `SecuritySession` 一类聚合上下文对象,而不是继续拆 `identityType`、`principalId`、`tenantKey` - create / update / delete / status / account 一类副作用型 reactive 契约,和阻塞式语义保持一致;阻塞式为 `void` 时,响应式统一定义为 `Mono`,不要把影响行数暴露到上层扩展接口 ## 账户认证型扩展补充约定 当某个业务模块需要接入统一认证 / 授权主链时,额外遵守: - 共享认证桥接接口优先复用 `IAccountAuthenticationService` / `IReactiveAccountAuthenticationService`;权限快照统一复用 `ISecurityAuthoritiesService` / `IReactiveSecurityAuthoritiesService`。 - 不要再生成已经废弃的 `IAccountAuthenticationManager`,也不要把共享认证能力重新收口到通用 `AccountManager`。 - 主体模块自己的 `Manager` 只补 `loadByUsername(...)`、`accountToSecurityUser(...)`、模块私有异常映射和主体特有查询。 - 模块共享的 `*SecurityUser` 统一放在该业务聚合的 `core` 子模块,供 `engine`、`engine-reactive`、`webmvc`、`webflux` 共用;不要在实现层重复定义多份。 - JDBC / R2DBC 仓储默认优先走接口组合;`Repository` 保持 `interface`,直接组合 core fragment,并在接口内保留 `GetEntityClassOperation + default getEntityClass()` - 关系表或只承载少量动作的仓储,只组合必要 fragment,并直接在接口上声明模块私有查询 / 删除方法 - 不再生成模块内的 `repository/support/*`、`Abstract*JdbcRepository`、`Abstract*R2dbcRepository` 或空转发 `RepositoryImpl` - 只有存在真实自定义 SQL 或框架限制时,才额外创建具体 `Impl` 类;对标样板优先参考 `ineed-security-authorization-system-identity-jdbc` ## 转换层补充约定 当业务模块需要生成实体转换、AO/VO 转换或对象映射实现时,额外遵守: - 转换层统一放在各数据栈实现模块下的 `converter/` 包,不再生成 `mapstruct/` - 转换实现类名统一使用 `XxxConverter` - MyBatis-Flex 的 `mapper/` 只保留持久化接口;对象转换实现必须放到 `converter/` - 普通 `XxxConverter` 继承 `AoEntityConverter` 与 `EntityVoConverter` 后,不要重复声明父接口已有的 `ao2Entity(...)`、`entityCopy(...)`、`appleAo(...)`、`entity2VO(...)` - 只有需要方法级 MapStruct 配置时才显式声明对应转换方法,例如 `@Mapping` 字典标签、派生字段、ignore 规则;无注解、无自定义逻辑的方法声明一律视为冗余 - 接入 `CreateManager` / `UpdateManager` / `FindByIdManager` / `OrderManager` 一类抽象时,统一实现 `getCreateEntityConverter()`、`getUpdateEntityConverter()`、`getEntityVoConverter()`,不要再沿用旧的 `get*Mapper()` 命名 ## 各层职责边界 | 层次 | 负责什么 | 不负责什么 | |------|----------|------------| | `Controller` | 暴露 API、接收参数、执行参数校验、记录请求入口日志、调用 `Service`、通过 `ResultBuild` 返回统一结果 | 不负责任务编排,不负责跨表事务,不直接操作 `Manager` / `Repository` | | `Service` | 作为事务边界和业务编排边界,组织一次业务用例,顺序调用多个 `Manager`,汇总业务结果;响应式侧在订阅期内完成依赖解析和编排 | `Service` 不与 `Service` 互调,不直接承载底层持久化细节,不把所有实体规则都堆在本层 | | `Manager` | 承载单类业务能力和实体相关规则,例如 CRUD、查询、状态切换、排序、删除扩展 | 不作为默认的“总编排中心”,不负责跨接口的 Web 参数解析 | | `Repository/EntityManager` | 执行最终持久化、查询、排序、批量更新等数据访问动作 | 不承接业务流程判断,不承担事务编排,不直接处理 Web 返回结构 | 代码生成模板中的类级 Javadoc 必须与这张职责表保持一致。模板可以生成通用职责说明,但生成后的业务代码如果已经有明确业务规则,应继续补充具体的校验条件、生命周期约束和异常边界,不能只停留在“某某管理器”“某某服务类”这类空泛描述。 ## 子表持久化动作落层规则 子表、关系表、从表围绕主对象外键做计数、存在性判断、查询或物理删除时,持久化动作必须落在对应 `Repository` / MyBatis-Flex `Mapper`,不能把表名、列名、通用 SQL 或通用 `BaseMapper` 条件留在 `Manager`。 `Manager` 可以决定“什么时候检查”“什么时候级联删除”“检查失败抛什么业务异常”,但不能承接“如何按外键 count/delete/find”的底层访问细节。 强制写法: - JDBC / JPA:在子表 `Repository` 上声明 Spring Data 派生方法,例如 `long countByCategoryIdAndDeleted(String categoryId, Byte deleted)`、`void deleteByCategoryId(String categoryId)` - R2DBC:在子表 `Repository` 上声明响应式派生方法,例如 `Mono countByCategoryIdAndDeleted(String categoryId, Byte deleted)`、`Mono deleteByCategoryId(String categoryId)` - MyBatis-Flex:在具体子表 `Mapper` 上声明明确的默认方法,例如 `countByCategoryIdAndDeleted(...)`、`deleteByCategoryId(...)`,方法内部使用 `QueryWrapper` 或 processor 生成的 `TableDef` 禁行写法: - JDBC / R2DBC `Manager` 中用 `QuerySqlSupport` 或字符串拼 SQL 来做子表 count/delete - JPA `Manager` 中为了简单外键计数或级联物理删除手写 QueryDSL `select count` / `delete` - MyBatis-Flex `Manager` 中写 `countActiveValue(BaseMapper mapper, String columnName, String columnValue)`、`deleteValue(BaseMapper mapper, String columnName, String columnValue)` 这类通用 helper 示例: ```java // Manager:只表达业务生命周期和业务异常 final long valueCount = valueRepository.countByDefinitionIdAndDeleted(entity.getId(), CoreYesNo.NO.getByte()); if (valueCount > 0) { throw StatefulException.of(ErrorCode.DATA_IN_USE, "配置项"); } // Repository / Mapper:表达具体外键访问 long countByDefinitionIdAndDeleted(String definitionId, Byte deleted); void deleteByDefinitionId(String definitionId); ``` 物理删除级联时同样遵守该规则:`beforePhysicsDelete` 中可以调用多个子表 `Repository` / `Mapper` 的 `deleteBy...` 方法,但每个 `deleteBy...` 的持久化条件必须由对应数据访问层封装。 ## Controller 生成补充约定 - 标准支持接口生成时,`webmvc` 与 `webflux` 的方法顺序、URL、权限表达式、参数名、日志语义统一对齐,不要一边叫 `common`、另一边叫 `commonParameters` - `getService()` 一类桥接方法统一放在接口最上面,方便实现类与代码生成器稳定复用 - `ValidationUtils` 只负责生成 `ValidationResult`;Web 层异常抛出分别收口到 `webmvc` / `webflux` 的 `ValidationExceptionSupport` - `webflux` 走统一返回体时,集合结果统一包装为 `Mono>>`;`Flux` 只保留在内部链路或真正的流式接口中 - 无返回体动作型 API 统一使用 `.then(ResultBuild.success())`;不要生成 `ResultBuild.success()` ## Service 生成补充约定 - 标准支持接口生成时,阻塞式与响应式 `Service` 的方法顺序、命名、Javadoc、日志语义统一对齐;响应式只保留必要的返回类型差异 - 不再额外生成只做能力聚合/转发的顶层别名接口,例如 `ICrudService`、`IDataStatusService`、`ICrudManager`、`IDataStatusManager` - 业务模块如果需要同时具备多种标准能力,直接 `implements` / `extends` 真实能力接口组合,例如 `ICreateService + IUpdateService + IEnableService`,不要再包一层无约束价值的壳接口 - `getManager()` 一类桥接方法统一放在接口最上面;响应式默认方法继续通过 `ServiceSupport.requireManager(...)` 在链内获取下游 `Manager` - 阻塞式 `Service` 默认方法统一使用 `try/catch + ServiceSupport.logError(log, "...", e)` 记录异常后原样抛出;不要再手写重复的 `if (log.isErrorEnabled()) { log.error(...) }` 模板 - reactive `Service` 默认方法统一使用 `Mono.defer(...)` / `Flux.defer(...)` 装配调用链,把日志、`requireManager(...)`、组合查询、上下文读取都收进订阅期执行 - reactive `Service` 异常日志统一挂在链尾 `doOnError(...)`,不要再生成链外 `try/catch`、同步 `throw`,也不要额外包一层等价的 `onErrorMap(...)` / `Mono.error(...)` - create / update / delete / status / account 一类副作用型 reactive 契约,若阻塞式语义为 `void`,则统一定义为 `Mono`;不要在 `Service` 扩展契约层暴露影响行数 - 查询型 reactive `Service` 只做必要的响应式封装,不额外引入与阻塞式不一致的空值分支、泛型边界或日志命名 - 遗留阻塞式别名接口完成下游切换后删除;新增或改造模块统一直接依赖真实能力接口,不再引用过期别名 ## 阻塞式 Manager / ORM 补充约定 - 阻塞式 `Manager` 如果需要访问可选下游能力,例如 `OrderOperation`、`TreeOperation`、额外的 `FindByIdOperation`,统一优先通过 `OperationSupport.getOperation(Class)` / `requireOperation(Class)` 获取 typed operation;不要在默认方法里反复散落 `instanceof + 强转` - `CreateManager` / `UpdateManager` 这一类抽象在处理排序、树路径、父节点查询等扩展能力时,统一沿上述 typed helper 收口,保持与 reactive 侧一致的能力解析方式 - 具体持久化 `Manager` 的方法布局固定按能力分组:先放实体元信息方法,例如 JPA `getEntityPath()`、JDBC `getEntityClass()`;再放 `getOperation()` / `getSqlTablePath()` 等持久化能力入口;随后放 `getCreateEntityConverter()`、`getUpdateEntityConverter()`、`getEntityVoConverter()`;再放 save / update 生命周期扩展、delete / recover 生命周期扩展,最后放 search 相关方法,例如 `getKeySearchPath()`、`getKeySearchColumns()`、`buildDefaultOrderBy()`、`appendWhereOfQuery()`、自定义 search / find 方法。R2DBC / MyBatis-Flex `Manager` 只保留 `getOperation()` 这一个持久化入口,不再实现 `getEntityClass()`;MyBatis-Flex 也不再实现 `getMapper()`。 - JDBC / JPA / MyBatis-Flex 的 `searchDictionary(...)` 默认实现统一先执行 `handleQueryParameters(queryParameters)`,再强制 `dataStatus.enabled = true`;不要先写死默认 `DataStatusFilter`,否则基础查询参数可能把 `enabled=true` 覆盖掉 - MyBatis-Flex 字典默认标签列统一对齐为 `label_value`,读取实体属性时按 `labelValue` 解析;取不到标签时再 fallback 到 `id` - JPA 与 MyBatis-Flex 是自定义查询的优先优化路径:JPA 优先使用框架约定查询 / QueryDSL,MyBatis-Flex 优先使用 `QueryWrapper` + `mybatis-flex-processor` 生成的 `entity.table.*TableDef`。需要生成 TableDef 的 `*-mybatis-flex` 模块必须像 JPA 模块配置 QueryDSL APT 一样独立接入 `mybatis-flex-processor`;不要因为缺少生成类而退回手写 `@Select` 或在业务代码里散落表名、列名字符串。 - JDBC / R2DBC 暂不作为查询 DSL 改造主路径;当前继续保留 core 支撑层和集中 SQL 实现,只做必要维护。不要为了追求和 JPA / MyBatis-Flex 的写法一致,主动引入 Spring Data `Criteria`、jOOQ 或 Spring Data JDBC 的 MyBatis 集成。 - R2DBC 查询条件和关键字查询列统一通过 `getQualifiedColumn("column_name")` 构建;不需要 join 时不要覆盖 `buildFrom()`、`buildSelect()`、`getDefaultTableAlias()`,需要 join 时主表列仍通过 `getQualifiedColumn(...)` 引用。 - JDBC manager 只要需要组装 Querydsl SQL 搜索、排序、过滤或 join,就必须使用模块内手写 `querydsl/QXxx extends QuerydslSqlTable` 表路径,并覆盖 `getSqlTablePath()` 返回该静态单例;不要在 manager 中继续用列名字符串或临时 `stringColumn(...)` 拼字段。 ### 多对多关系处理 多对多关系默认按“写关系、查已分配对象、认证/鉴权轻量查询”三类场景拆开处理,不再把所有逻辑堆在 `Service` 或关系 `Manager` 里。 - 分配关系的 `Service` 只负责业务入口、事务、主对象状态校验和入参归一化,例如使用 `IdSetUtil.normalizeIdSet(...)`、授权模式 normalize;不要为了保存关系先批量加载被分配对象再逐个过滤。 - 获取对象和校验对象有效性的职责下沉到对应 `Manager` 或数据库查询中;前端需要展示“已分配对象列表”时,由主对象 `Manager` 直接通过关系表 join 目标对象表,并直接返回 VO。 - 关系 `Manager` 只保留确实被业务使用的关系写入、删除和轻量 ID 查询;不要保留已经被 join 查询替代的“先查 ID 集合再让 Service 回查对象”的旧方法。 - 认证、鉴权、权限快照这类高频链路可以保留轻量 ID 查询,例如“主体 -> 角色 ID”“角色 -> 权限 ID”;这类方法服务于权限计算,不等同于前端分配列表查询。 - JPA 已经进入 ORM 实现时,自定义关系查询优先使用框架约定查询或 QueryDSL join,不写 native SQL;查询已分配对象时同时剔除逻辑删除数据,并按业务需要过滤启用状态。 - MyBatis-Flex 优先使用 `QueryWrapper` 和生成的 `TableDef` 表达 join / where / orderBy;不要因为缺少 processor 生成类就退回散落的字符串 SQL。 - JDBC / R2DBC 当前保持集中 SQL 实现,只做必要维护;字段名、表名和过滤条件应集中在对应 manager / repository 方法内,不向 `Service` 泄漏。 - 阻塞式与响应式两条线的方法命名、职责和过滤语义必须对齐;差异只体现在返回类型,例如 `Collection` 与 `Flux`。 ### 按业务键创建或刷新 当业务对象天然具备稳定业务键,并且重复提交应该刷新旧数据而不是报重复错误时,例如监控节点注册、应用实例上报、外部系统同步,可在具体 Manager 上显式实现 `CreateOrRefreshManager`: - 对外方法继续保留业务命名,例如 `register(ao)`、`sync(ao)`、`importOne(ao)`;方法体直接调用 `save(ao)` - 具体 Manager 只实现 `findEntityByBusinessKey(ao)` 表达业务键查找,不手写重复的查旧值、拷贝 AO、更新实体、转 VO 流程 - `CreateOrRefreshManager` 会复用 core 创建与更新生命周期,统一处理租户校验、数据状态校验、重复校验、before/after update hook 和更新事件发布 - 不新增 `saveOrUpdate(...)` 或 `upsert(...)` 通用入口;这不是数据库 upsert,而是业务键 create-or-refresh - 如果 JDBC 或特殊数据栈需要显式按业务键更新,只覆盖 `persistUpdatedEntity(beSave, oldEntity, ao)`,不要复制整套更新生命周期 - 仓储层按数据栈补齐业务键查询:JPA / JDBC 返回 `Optional`,R2DBC 返回 `Mono`,MyBatis-Flex 可由 mapper 查询实体后在 Manager 包装 `Optional` ## Reactive Manager / Repository 补充约定 - reactive `Manager` 默认方法与 `Service` 保持同一口径:依赖解析、能力判断、上下文读取都收进订阅期执行,不保留链外同步 `throw` - `OperationSupport.requireOperation(...)` 一类桥接入口统一返回 `Mono`;下游默认方法通过 `flatMap(...)` / `flatMapMany(...)` 在链内取 `operation` - reactive `Manager` 不要再先同步取 `operation/support` 再拼装 `Mono` / `Flux`,缺失依赖时应在订阅时发出错误信号 - reactive 查询默认方法如果需要补 `QueryParameters` 默认值,例如回收站查询补 `deleted=true`、字典查询补 `enabled=true`,也必须放进 `Mono.defer(...)` 或链内准备步骤;不要在返回 Publisher 前同步改写入参 - R2DBC 自定义 `RepositoryImpl` 中,`entityClass`、`tableName`、`rowMapper` 等解析统一延迟到订阅期;优先复用 `getEntityClassMono()`、`getTableNameMono()` 或 `Mono.defer(...)` - R2DBC 实现中不要生成 `Mono.just(getEntityClass())`、`Mono.just(getTableName())`、`final Class entityClass = getEntityClass()` 这类装配期求值代码;否则会把“不支持该操作”提前成同步异常 - R2DBC `Manager` 不手写 `getEntityClass()`;实体类型只在 `Repository` / operation 层声明,`Manager` 通过 `getOperation().getEntityClass()` 走 core 默认实现 - reactive 链路对应测试统一按“创建 Publisher 不抛错,订阅时收到异常”编写;不要再用旧的同步 `assertThrows(...)` 去断言装配期异常 ## Manager 方法顺序 具体 `Manager` 类的方法顺序必须稳定,便于对照阻塞式 / 响应式实现,也便于审查生命周期钩子是否落在正确位置。不要按写代码时的临时先后顺序随意插入方法。 接入标准 `CreateManager` / `UpdateManager` / `FindByIdManager` / `SearchManager` / `LogicDeleteManager` / `PhysicsDeleteManager` 等能力的实体 `Manager`,类体推荐顺序如下: 1. `static final` 常量。 2. 构造注入的 `final` 依赖字段,先 converter,再 repository / mapper / operation,再其它 manager。 3. 框架依赖入口:`getOperation()`、`getEntityClass()`、`getSqlTablePath()`、`getTableDef()` 等。 4. Converter 入口:`getCreateEntityConverter()`、`getUpdateEntityConverter()`、`getEntityVoConverter()`。 5. 业务键与查询辅助:`findEntityByBusinessKey(...)`、`getKeySearchPath()`、`appendWhereOfQuery(...)`、`appendWhereOfSearch(...)` 等。 6. 创建 / 更新生命周期:`validate(...)`、`beforeSave(...)`、`afterSave(...)`、`beforeUpdate(...)`、`persistUpdatedEntity(...)`、`afterUpdate(...)`。 7. 状态、排序、发布、默认、主数据等标准能力覆盖方法。 8. 删除与恢复生命周期:`checkCustomCanDelete(...)`、`beforeLogicDelete(...)`、`afterLogicDelete(...)`、`afterRecovery(...)`、`beforePhysicsDelete(...)`、`afterPhysicsDelete(...)`。 9. 当前业务接口额外暴露的 public / protected 领域方法。 10. 当前类私有 helper 方法,按调用链路或业务主题分组;不要把 private helper 插到标准生命周期方法中间。 纯领域编排型 `Manager` 如果不接入标准数据能力,例如运行时生成、分配、同步器一类,方法顺序按“公开业务入口在前,私有 helper 在后”组织: 1. 常量。 2. 依赖字段。 3. `@Override` 的业务接口方法,按接口声明和业务流程顺序排列。 4. 支撑这些入口的 private / protected helper,优先按首次调用顺序排列。 阻塞式和响应式两个 `Manager` 实现同名能力时,方法顺序、命名、Javadoc 摘要和日志语义应尽量一一对应;响应式只保留返回类型、链式写法和订阅期语义差异。 ## 新增自定义业务时的推荐步骤 1. 先确认这个需求是否仍然属于现有 `Controller -> Service -> Manager -> Repository/EntityManager` 主路径中的某一层扩展,而不是额外增加新层。 2. 在 `Controller` 层定义对外入口,完成参数接收、参数校验和统一返回。 3. 如果入口已经拿到当前会话对象,优先把 `SecuritySession` 这类聚合上下文继续往后传,不再拆成多个基础字段。 4. 在 `Service` 层定义业务入口,作为事务边界承接一次完整业务用例。 5. 在 `Service` 内通过 `ServiceSupport.requireManager(...)` 获取所需 `Manager`,按业务顺序组织调用。 6. 将单类业务能力下沉到对应 `Manager`,例如状态切换规则、排序规则、删除前后扩展、单表业务校验。 7. 将最终的数据访问落到 `Repository/EntityManager`,并沿当前数据栈选择 JPA、JDBC 或 MyBatis-Flex 的实现。 8. 如果一个业务需要多张表协作,保持“一个业务 `Service` + 多个职责清晰的 `Manager`”的结构,不拆成多个 `Service` 横向互调。 ## 分层职责判定 | 事项 | 推荐落层 | 说明 | |------|----------|------| | 参数校验 | `Controller` | 入口参数、ID、请求体合法性校验优先放在 Web 入口;AO 字段校验由框架手动调度 Jakarta Validation,不在方法参数上加 `@Valid` | | 事务编排 | `Service` | 一次业务用例的开始、提交、回滚边界由 `Service` 控制 | | 跨表协作 | `Service` | 多表流程由一个 `Service` 组合多个 `Manager` 完成 | | 实体规则 | `Manager` | 单类实体的创建、删除、排序、状态流转和局部约束落在 `Manager`;业务校验优先覆盖 `validate(ao)`,不要为校验覆盖 `save(ao)` / `update(ao)`;AO 已表达的字段级校验不要在 Manager 再写一遍 | | 数据访问 | `Repository/EntityManager` | 查询、持久化、排序字段落库、批量更新等最终执行动作落在数据层 | ## 多表业务协作 一个业务如果同时操作多张表,默认结构是“一个业务 `Service` 负责事务与编排,再组合多个 `Manager` 完成落地”。 推荐顺序如下: 1. `Controller` 只暴露业务入口。 2. `Service` 作为业务入口方法,开启事务并组织完整业务流程。 3. `Service` 通过 `ServiceSupport.requireManager(...)` 依次获取并调用多个 `Manager`。 4. 每个 `Manager` 只处理自己负责的一类业务能力或实体规则。 5. `Repository/EntityManager` 只处理本实体或本查询动作的最终持久化。 这意味着跨表业务的主编排层是 `Service`,不是 `Controller`,也不是默认的大而全组合型 `Manager`。 多对多关系和删除级联虽然会触碰多张表,但不应该因此把职责混成一个“大而全 Manager”: - 关系分配属于一个业务入口,由 `Service` 控制事务并调用对应关系 `Manager` 覆盖保存。 - 已分配对象列表属于主对象的查询能力,由主对象 `Manager` 通过 join 返回 VO。 - 删除主对象时必须清理的关系表、从表或强依赖数据,属于该实体的物理删除生命周期,由该实体 `Manager` 在删除钩子中处理。 - 关系表、从表按主对象外键做 `count/delete/findBy/exists` 时,具体查询和删除方法必须定义在关系表、从表自己的 `Repository` / `Mapper` 中,`Manager` 只负责调用和业务判断。 ## 推荐模式 推荐把一次业务写成“一个入口 `Service` + 多个职责稳定的 `Manager`”: - `Controller` 负责收口 API 和参数。 - `Service` 负责事务、步骤顺序和失败回滚。 - `Manager A` 负责主实体处理。 - `Manager B` 负责关联实体处理。 - `Manager C` 负责状态切换、排序或删除后清理等扩展。 当 `Service` 需要调用 `Manager` 时,统一使用 `ServiceSupport.requireManager(...)` 作为桥接入口。这样可以保持与现有 `ICreateService`、`ILogicDeleteService` 等默认实现一致。 ## 反模式与禁行规则 ### 1. Service 互调 禁行规则:Service 不与 Service 互调。 原因是 `Service` 已经是事务与业务编排边界。如果一个 `Service` 再去调用另一个 `Service`,事务边界、日志入口和职责归属都会变得不清晰。 正确做法是由当前业务 `Service` 直接组合多个 `Manager`。 ### 2. 在 Controller 中编排多表业务 禁行规则:不要在 `Controller` 中编排多表业务。 `Controller` 只负责参数校验、请求入口日志和统一返回,不应该把跨表协作、状态切换顺序、删除后清理这些流程塞进 Web 层。 ### 3. 组合型 Manager 取代 Service 编排 不推荐把“大而全的组合型 Manager”作为默认主编排层。 如果某个 `Manager` 同时负责多个实体、多类流程和事务顺序,它实际上已经在扮演 `Service` 的角色,会破坏 `Service -> Manager` 的职责边界。 ## 落地检查清单 - 自定义业务是否仍然走 `Controller -> Service -> Manager -> Repository/EntityManager` - `Controller` 是否只处理入口、校验和统一返回 - `Service` 是否承担事务与编排,而不是横向调用其他 `Service` - 多表业务是否通过组合多个 Manager 落地 - `Manager` 是否聚焦单类业务能力,而不是成为新的总控层 - 多对多关系是否避免了 Service 先查 ID 再批量回查对象的旧模式 - 物理删除级联是否落在对应实体 Manager 的删除生命周期中,而不是散落在 Controller 或无关 Service 中 > 最后更新:2026-05-18 --- # 删除钩子、分层日志与异常追踪规范 来源:docs/project-development/delete-hook-and-logging-guide.md SHA-256:83dc29413c64035b539fae0f379819489e9fb303352eb2dc1fbc6666e7126b83 网站:/docs/delete-hook-and-logging-guide/ ## 删除生命周期钩子映射 Ineed-Core 当前删除与恢复扩展必须对齐 `LogicDeleteManager` / `PhysicsDeleteManager` 中已经存在的真实方法名: | 业务语义 | 真实钩子 | 适用时机 | |----------|----------|----------| | 逻辑删除前校验 / 逻辑删除前拦截 | `beforeLogicDelete` | 逻辑删除标记真正执行之前 | | 逻辑删除成功后的扩展 | `afterLogicDelete` | 逻辑删除标记已经成功落库之后 | | 物理删除前校验 / 物理删除前拦截 | `beforePhysicsDelete` | 物理删除真正执行之前 | | 物理删除成功后的扩展 | `afterPhysicsDelete` | 物理删除已经成功落库之后 | | 恢复前准备 | `beforeRecovery` | 恢复逻辑删除数据之前 | | 恢复后的补偿处理 | `afterRecovery` | 恢复已经成功之后 | 业务口语里常说的 `afterDelete` 必须先确认删除语义: - 如果是逻辑删除,真实实现点是 `LogicDeleteManager.afterLogicDelete(...)`。 - 如果是物理删除,真实实现点是 `PhysicsDeleteManager.afterPhysicsDelete(...)`。 ## 级联删除落位规则 级联删除属于物理删除语义,不属于逻辑删除语义。 逻辑删除只负责把当前对象标记为已删除,并保留后续恢复的可能性。逻辑删除的 `afterLogicDelete` 可以记录审计、刷新索引、失效缓存或发布逻辑删除事件,但不应该物理清理从表、关系表、附件对象或授权关系。 物理删除级联按依赖方向分两类落位: - 会影响当前实体物理删除能否成功的强依赖清理,放在 `beforePhysicsDelete`。例如关系表、从表、授权关系、明细表等存在外键或业务强依赖时,应先清理依赖数据,再删除当前实体。 - 依赖当前实体已经物理删除成功的外部副作用,放在 `afterPhysicsDelete`。例如回收不可恢复资源、失效外部索引、发布明确的删除完成事件、记录完成日志。 推荐放在 `beforePhysicsDelete` 的动作包括: - 删除当前实体作为主对象时必须同步清理的多对多关系表 - 删除当前实体前必须删除的从表、明细表或子节点 - 删除树形节点前按 `idPath` / parent 关系定位并物理删除后代 - 删除当前实体前必须解除的授权关系、绑定关系或业务强依赖 核心原则:**物理删除时,必须在 `beforePhysicsDelete` 中级联删除所有关联数据的从表/引用行**(多对多关系表同样适用),确保不因 FK 约束阻断物理删除主流程。每个引用方的级联删除顺序应遵循「先删子、后删父;先删引用方、后删被引用方」。 推荐放在 `afterPhysicsDelete` 的动作包括: - 物理删除成功后再回收不可恢复资源 - 物理删除成功后再刷新外部索引或缓存 - 物理删除成功后再发布删除完成事件 - 物理删除成功后再记录明确的业务完成日志 判断标准是:如果不先执行会导致当前实体删不掉,放 `beforePhysicsDelete`;如果必须确认当前实体已经删掉才有意义,放 `afterPhysicsDelete`。 `forceDelete` 与 `physicsDelete` 的差异只应体现在是否跳过状态判断并直接进入物理删除流程;不要为 `forceDelete` 额外发明 `beforeForceDelete` / `afterForceDelete` 钩子。需要级联清理时仍复用物理删除生命周期。 ## 删除前置保护 删除前校验与拦截逻辑统一落在 `beforeLogicDelete`。 适合放在 `beforeLogicDelete` 的动作包括: - 检查当前实体是否允许删除 - 检查数据状态是否固定、锁定,或被其他业务状态规则禁止删除 - 检查是否存在必须先解除的前置依赖 - 对不满足删除条件的场景直接抛出业务异常 ### checkCustomCanDelete:直接外键引用保护 如果表有直接的一对多/一对一外键关联(多对多关系表除外),必须覆写 `LogicDeleteManager.checkCustomCanDelete(...)` 查询所有引用点。只要存在未删除的引用数据,就抛 `DataInUseException` 阻止逻辑删除。 抛异常时必须使用 FK 约束的 i18n key,而非硬编码中文文案。直接引用保护属于“引用目标正在被使用”语义,i18n key 格式为 `{referencingTableName}.fk.in-use.{constraintName}`,其中 `referencingTableName` 是引用方表,也就是 FK 所在表;不要写成被引用目标表。`constraintName` 是数据库真实 FK 约束名。 ```java @Override public void checkCustomCanDelete(final Demo entity) { final Long refCount = getQueryFactory() .select(QRefTable.refTable.id.count()) .from(QRefTable.refTable) .where( QRefTable.refTable.deleted.eq(CoreConstant.YESNO.NO.getByte()), QRefTable.refTable.demoId.eq(entity.getId()) ) .fetchOne(); if (refCount != null && refCount > 0) { throw new DataInUseException("demo_ref_table.fk.in-use.fk_constraint_name"); } } ``` 对应的 i18n 资源需要在模块的 `messages.properties` / `messages_en.properties` / `messages_zh_TW.properties` 中同步补齐: ```properties # 以 config.category 为例: icfg_category_simple_value.fk.in-use.fk_icfg_cat_simple_cat_id=分类类型定义数据被分类项(简单)使用,不能删除或更新 icfg_category_tree_value.fk.in-use.fk_icfg_cat_tree_cat_id=分类类型定义数据被分类项(树形)使用,不能删除或更新 ``` `DataInUseException` 的 `userObjectName` 最终通过 `I18nExceptionHandler` 走 `I18nUtil.getMessage()` 查 i18n 资源,因此同一 FK key 可以覆盖简体中文、英文、繁体中文三语,并且与数据库 FK 违规时的自动解析消息共享同一套 key。数据库异常解析会优先使用 SQL 错误中解析出的表名精确查找 `{tableName}.fk.*.{constraintName}`,解析不到表名时才按约束名后缀兼容查找。 同一个数据库 FK 约束必须准备两类文案: - `{referencingTableName}.fk.not-found.{constraintName}`:子表 `INSERT` / `UPDATE` 时引用的目标数据不存在 - `{referencingTableName}.fk.in-use.{constraintName}`:父表 `DELETE` / `UPDATE` 时已有引用数据正在使用目标数据 历史 `{basePackage}.{ReferencingEntity}.fk.{constraintName}` 仅作为兼容回退,新模块和新文案不要再使用单一 FK key。 直接 FK 引用保护与通用 `beforeLogicDelete` 的区别:`checkCustomCanDelete` 是 `LogicDeleteManager` 定义的独立扩展点,在 `checkCanDelete`(null、租户、固定/锁定状态)之后、`beforeLogicDelete` 之前执行。它只负责「FK 引用计数查询」,不混合其他业务拦截。 如果某个动作的目标是”阻止删除发生”,就应该落在 `beforeLogicDelete`(通用拦截)或 `checkCustomCanDelete`(FK 引用拦截),而不是等删除完成后再补救。 ## 恢复后补偿 恢复后的关联处理统一落在 `afterRecovery`。 适合放在 `afterRecovery` 的动作包括: - 恢复后重建关联关系 - 恢复后补齐状态同步 - 恢复后执行重新开放或重新挂接动作 如果某个动作依赖“数据已经恢复成功”,就不应放在 `beforeRecovery`,而应放在 `afterRecovery`。 ## 日志总体原则 ### 0. SLF4J 唯一允许的日志门面 项目中所有日志输出必须通过 SLF4J。禁止使用以下方式: - `System.out.println` / `System.err.println` - `java.util.logging`(JUL) - `Log4j` / `Log4j2` 原生 API - `commons-logging`(JCL) 简单判断:只要能拿到 `org.slf4j.Logger` 实例,就必须用它;不要绕开 SLF4J 直接调用底层实现。 唯一的例外:**interface 的默认方法**无法使用 `@Slf4j`(Lombok 不支持接口字段),此时允许通过 `LoggerFactory.getLogger(...)` 声明静态 Logger,这也是 ineed-core 框架接口当前的做法。 ### 1. 新写具体类统一使用 `@Slf4j` 开发者、代码生成器或自动化工具在新增具体业务类时,统一直接使用 Lombok 的 `@Slf4j`: - `Controller` - `Service` - `Manager` - `Repository / EntityManager / Operation` 的自定义实现类 - 删除钩子实现类 - `Filter` - `Handler` - `Listener` - `Task / Initializer / Engine` 不要在新生成的具体类里继续手写 `LoggerFactory.getLogger(...)`。 例外只有一类:`Ineed-Core` 现有的接口默认实现,例如 `ICreateController`、`ICreateService`、`ILogicDeleteController`、`IAccountService` 等,由于它们本身是接口默认方法,已经使用接口内静态 `Logger`,这是框架底座实现,不是新增业务代码应该模仿的写法。 ### 2. 标准支持接口已有默认日志 当前框架已经在标准支持接口里提供了一部分默认日志: | 层次 | 已有默认日志入口 | |------|------------------| | `Controller` | `ICreateController`、`IUpdateController`、`ILogicDeleteController`、`IAccountController`、`IEnableController`、`ISearchController` 等 | | `Service` | `ICreateService`、`IUpdateService`、`ILogicDeleteService`、`IAccountService`、`ISearchService`、`IOrderService` 等 | | 数据回调 / 基础设施 | `JpaEntityCallback`、`JdbcEntityCallback`、`MybatisFlexEntityCallback`、`R2dbcEntityCallback`、`WebMvcOperateInfoFilter`、`WebFluxOperateInfoFilter`、`GlobalExceptionHandler` 等 | 这意味着: - 如果某个 `Controller` / `Service` 只是纯粹继承标准接口、没有任何手写业务方法,默认日志已经覆盖了标准 CRUD / Query / DataStatus / Account 主链。 - 即便如此,具体类仍然建议保留 `@Slf4j`,便于后续补充自定义接口时直接沿统一写法扩展。 - 不要为了“每层都打日志”而在只调用接口默认方法的空壳类里再人为包一层重复日志。 ### 3. 自定义方法必须显式补日志 凡是自己手写的方法,特别是以下几类,必须显式补日志: - 自定义 `Controller` 接口 - 自定义 `Service` 编排方法 - `Manager` 中的业务规则方法 - 删除 / 恢复钩子方法 - 自定义 `Repository` / `EntityManager` / `Operation` - 定时任务、初始化器、监听器、过滤器、鉴权处理器 ### 4. 日志级别统一建议 | 级别 | 使用场景 | |------|----------| | `info` | 写操作入口、登录、安全敏感动作、批量任务开始/完成、初始化开始/完成、跨表业务主编排开始/完成 | | `debug` | 读操作入口、关键分支判断、关联数量、命中条件、动态查询参数摘要、批处理中间节点 | | `warn` | 可预期但值得关注的异常分支,例如重复跳过、可恢复失败、缺少可选依赖、补偿动作未执行 | | `error` | 已捕获异常且准备继续抛出,或者进入明确失败分支 | ### 5. 敏感信息禁止直接打印 日志里禁止直接输出以下内容: - 明文密码 - 新密码 / 旧密码 - Token 全串 - 密钥 / 私钥 / 密文原文 - 完整凭证对象 - 大体量二进制内容 可以输出的替代信息包括: - `principalId` - `identityType` - `tenantKey` - `resourceKey` - `id` - `idSet.size()` - 经过裁剪的查询条件摘要 ## LogHelper 统一用户注入 API Ineed-Core 提供了 `LogHelper`(阻塞式)和 `ReactiveLogHelper`(响应式)两个统一的日志辅助工具。它们与 `@Slf4j` 不冲突——`@Slf4j` 负责生成 `log` 实例,`LogHelper`/`ReactiveLogHelper` 负责在每次日志输出时自动注入当前操作用户信息。 ### API 两个类提供完全一致的静态方法: ```java // 导入 import com.ineed.base.context.LogHelper; // 阻塞式(WebMvc) import com.ineed.base.context.ReactiveLogHelper; // 响应式(WebFlux) // 用法:与 log.xxx(format, args) 相同,只是第一个参数传入 log 实例 LogHelper.debug(log, "message, id = {}", id); LogHelper.info(log, "message, id = {}", id); LogHelper.warn(log, "message, id = {}", id); LogHelper.error(log, "message, id = {}", id); ``` ### 选择规则 | 场景 | 使用 | |------|------| | WebMvc Controller / Web 适配层 | `@Slf4j + LogHelper` | | WebFlux Controller / Web 适配层 | `@Slf4j + ReactiveLogHelper` | | Engine Service / Manager / Repository(阻塞式业务链路) | `LogHelper` | | Engine Service / Manager / Repository(响应式业务链路) | `ReactiveLogHelper` | | ineed-core 框架接口默认方法(engine + webmvc) | `LogHelper`(接口内使用静态 Logger) | | ineed-core 框架接口默认方法(engine-reactive + webflux) | `ReactiveLogHelper`(接口内使用静态 Logger) | 边界约定: - `LogHelper` 位于 `ineed-core-context`,`ReactiveLogHelper` 位于 `ineed-core-context-reactive`。只有模块已经直接依赖对应 context 模块,或当前模块本来就是 Web / Engine / Manager / Repository 等业务链路模块时,才迁移到 helper。 - 不要为了单条日志给纯 core、纯 runtime、独立工具、agent、测试辅助模块新增 `ineed-core-context` / `ineed-core-context-reactive` 依赖。此类模块继续保留 `@Slf4j + log.xxx(...)`,但仍必须遵守 SLF4J 门面、敏感信息保护和异常对象传递规则。 - Web 侧具体类(Controller、Filter、Handler、Listener、Web 适配 Service)继续使用 Lombok `@Slf4j` 生成 `log` 实例;需要输出业务日志时,把已有 `log` 传给 `LogHelper` / `ReactiveLogHelper`。不要自己构建 Logger、日志对象、请求描述对象或另一套日志上下文。 - Reactive 链路必须使用 `ReactiveLogHelper`;不要在 WebFlux / R2DBC 代码中混用阻塞式 `LogHelper`。 ### 用户信息来源 - `LogHelper` 从 `ContextHandler.getOperationUserJson()` 获取,底层基于 ThreadLocal - `ReactiveLogHelper` 从 `ReactiveContextHandler.getThreadLocalUser()` 获取,通过 Reactor Context → ThreadLocal 桥接(与 Spring Security 的模式一致) - 未登录时自动回退为匿名用户 JSON ### 日志输出效果 ``` // 使用 LogHelper 前 // CreateService.save(), ao = CreateAO(name=test) // 使用 LogHelper 后 // user={"principalId":"u123","principalAccount":"zhangsan","principalName":"张三","tenantKey":"t1"}, CreateService.save(), ao = CreateAO(name=test) ``` ### 框架接口默认方法已统一使用 以下框架标准接口的默认方法已全部迁移到 `LogHelper` / `ReactiveLogHelper`: | 层次 | 已使用 LogHelper/ReactiveLogHelper 的接口 | |------|------------------------------------------| | Engine Service | `ICreateService`、`IUpdateService`、`ILogicDeleteService`、`IPhysicsDeleteService`、`IDefaultService`、`IEnableService`、`ILockService`、`IPrimaryService`、`IPublishService`、`IAccountService`、`IOrderService`、`IDuplicateService`、`IFindByIdService`、`ISearchService` 等 | | Engine Service (Reactive) | 同上,对应 reactive 版本 | | WebMvc Controller | `ICreateController`、`IUpdateController`、`ILogicDeleteController`、`IPhysicsDeleteController`、`IDefaultController`、`IEnableController`、`ILockController`、`IPrimaryController`、`IPublishController`、`IAccountController`、`IOrderController`、`IImportController`、`IExportController`、`IDuplicateController` 等 | | WebFlux Controller | 同上,对应 webflux 版本 | ### 业务代码建议 - 业务自定义 `Controller` 方法在 WebMvc 中使用 `LogHelper`,在 WebFlux 中使用 `ReactiveLogHelper`;入口日志仍然放在手动校验之前,保证校验失败也能留下调用痕迹 - WebFlux 自定义 `Controller` 方法的入口日志必须放在 `Mono.defer(() -> { ... })` 内,与 `ResultBuild.success(serviceMono)` 一起延迟到订阅期执行,避免链外提前读取响应式上下文或提前产生副作用 - 业务自定义 `Service`、`Manager`、`Repository` 方法在业务链路模块中使用 `LogHelper`(或 `ReactiveLogHelper`)替代 `log.xxx()`,确保排查问题时能追溯到操作人 - 具体类仍然使用 `@Slf4j` 生成 `log` 实例;调用 helper 时只把这个已有 `log` 作为第一个参数传入 - `LogHelper` 内部已经做了级别判断(如 `log.isInfoEnabled()`),业务代码不需要再在外面包一层 `if` ## 分层日志方案 ### Controller 层 `Controller` 的职责是 API 入口,因此日志重点是“谁调用了什么动作,入口参数是什么”。 规则如下: - 标准支持接口方法继续复用框架默认日志。 - 自定义 `Controller` 方法必须在真正执行业务前记录入口日志。 - 手动触发 Jakarta Validation 或其它参数校验的自定义方法,入口日志放在校验之前;这样即使校验失败,也能在日志中看到调用痕迹。 - 纯查询、辅助查询、字典类接口优先 `debug`。 - 创建、编辑、删除、导入、导出、登录、切换租户、权限刷新等动作优先 `info`。 - 一般不要在 `Controller` 中捕获业务异常只为打印一遍日志;异常统一交给下层抛出和 `GlobalExceptionHandler` 兜底。 推荐记录内容: - 类名 + 方法名 - `id` / `idSet` - 关键查询对象 - 关键业务参数摘要 示例: ```java @Slf4j public class DemoController { public ResponseResults bindRole(final String id, final BindRoleAO ao) { LogHelper.info(log, "DemoController.bindRole(), id = {}, roleIdSet = {}", id, ao.getRoleIdSet()); demoService.bindRole(id, ao); return ResultBuild.success(); } public ResponseResults> searchAudit(final DemoAuditQuery query) { LogHelper.debug(log, "DemoController.searchAudit(), query = {}", query); return ResultBuild.success(demoService.searchAudit(query)); } } ``` ### Service 层 `Service` 是事务边界和业务编排边界,因此日志重点是“一次业务用例开始了、关键编排节点走到了哪、失败原因是什么”。 规则如下: - 标准 CRUD / Query / DataStatus / Account 默认方法继续复用接口日志。 - 自定义 `Service` 方法必须至少记录一条入口日志。 - 跨 `Manager` 编排、批量处理、初始化、补偿等方法,建议再补关键节点 `debug` 日志。 - 明显的长链路操作建议补一条完成日志。 - 如果方法里 `catch` 了异常,就必须 `log.error(..., e)` 后继续抛出,不吞异常。 推荐记录内容: - 类名 + 方法名 - `tenantKey` - 主对象 `id` - 关键步骤名 - 批量数量 示例: ```java @Slf4j @Service public class DemoService { @Transactional(rollbackFor = Exception.class) public void bindRole(final String id, final BindRoleAO ao) { LogHelper.info(log, "DemoService.bindRole(), id = {}, roleIdSet = {}", id, ao.getRoleIdSet()); try { LogHelper.debug(log, "DemoService.bindRole(), step = validate-demo"); demoManager.checkCanBindRole(id); LogHelper.debug(log, "DemoService.bindRole(), step = replace-relation"); demoRoleRelationManager.replaceRelation(id, ao.getRoleIdSet()); LogHelper.info(log, "DemoService.bindRole() completed, id = {}, relationCount = {}", id, ao.getRoleIdSet().size()); } catch (final Exception e) { LogHelper.error(log, "DemoService.bindRole() is error : {}.", e.getMessage(), e); throw e; } } } ``` ### Manager 层 `Manager` 不负责整条请求链路的入口审计,它负责“单类业务规则”和“局部实体规则”,因此日志重点是“规则判断、状态变化、钩子清理、补偿结果”。 规则如下: - 如果 `Manager` 只是纯继承标准 CRUD / Query 能力且没有手写方法,不需要为了形式强行打印空日志。 - 自定义 `Manager` 方法、规则判断方法、删除/恢复钩子必须使用 `@Slf4j` 并记录关键业务动作。 - `beforeLogicDelete` 重点记录逻辑删除拦截条件和对象标识。 - `afterLogicDelete` 重点记录逻辑删除后的审计、缓存、索引、事件等可恢复扩展,不做级联物理清理。 - `beforePhysicsDelete` 重点记录物理删除拦截条件和对象标识。 - `afterPhysicsDelete` 重点记录级联物理清理动作和关联数量。 - `afterRecovery` 重点记录恢复后的补偿动作和关联数量。 - 规则拦截前如果需要补充定位信息,可以使用 `warn`;如果只是普通判断过程,使用 `debug` 即可。 推荐记录内容: - `tenantKey` - 主对象 `id` - 关联对象数量 - 当前规则判断结果 示例: ```java @Slf4j @Component public class DemoManager { public void beforeLogicDelete(final Demo beDelete) { LogHelper.debug(log, "DemoManager.beforeLogicDelete(), tenantKey = {}, id = {}", beDelete.getTenantKey(), beDelete.getId()); if (beDelete.isFixed()) { LogHelper.warn(log, "DemoManager.beforeLogicDelete() blocked, tenantKey = {}, id = {}, reason = fixed-data", beDelete.getTenantKey(), beDelete.getId()); throw StatefulException.of(ErrorCode.DATA_FIXED_CANNOT_UPDATE); } } public void afterPhysicsDelete(final Demo beDelete) { final int relationCount = demoRelationRepository.deleteByDemoId(beDelete.getId()); LogHelper.info(log, "DemoManager.afterPhysicsDelete(), tenantKey = {}, id = {}, relationCount = {}", beDelete.getTenantKey(), beDelete.getId(), relationCount); } } ``` ### Repository / EntityManager / Operation 层 这一层不应该把普通单表 CRUD 全部重复打一遍日志,否则会制造噪音。 规则如下: - 对于框架已有的标准持久化方法,通常不额外加日志。 - 只有在存在自定义 SQL、动态 SQL 组装、批量更新、元数据扫描、跨库连接、复杂查询降级等情况时,才补必要日志。 - 推荐记录“参数摘要、命中数量、耗时、失败原因”,不要打印整包大对象。 - 如果只是单纯 `save/update/findById`,不要再打一遍低价值日志。 推荐记录内容: - `id` - `idSet.size()` - 关键查询条件摘要 - 结果数量 - 耗时 示例: ```java @Slf4j @Repository public class DemoJdbcRepository { public List searchCustom(final DemoQuery query) { LogHelper.debug(log, "DemoJdbcRepository.searchCustom(), tenantKey = {}, keyword = {}, pageSize = {}", query.getTenantKey(), query.getSearchKey(), query.getPageSize()); try { return jdbcTemplate.query(SQL, params, rowMapper); } catch (final Exception e) { LogHelper.error(log, "DemoJdbcRepository.searchCustom() is error : {}.", e.getMessage(), e); throw e; } } } ``` ### Hook / Task / Listener / Filter / Handler 层 这类类虽然不在标准业务主链的四层内,但往往承担运行时关键动作,因此也必须统一使用 `@Slf4j`。 规则如下: - 定时任务、初始化器:开始/完成用 `info`,中间步骤用 `debug`。 - 鉴权过滤器、会话过滤器:轻量入口日志用 `debug`,认证失败或拒绝访问由专门处理器记录。 - 监听器:成功事件、失败事件优先 `debug`;真正异常再用 `error`。 - Handler:不重复打印业务异常栈;聚焦处理器自身的分支与兜底异常。 ## 一次方法的最小日志结构 对自定义方法,推荐最少遵守下面这个结构: 1. 入口日志 2. 关键节点日志 3. 成功完成日志 4. 异常日志 最小模板: ```java LogHelper.info(log, "DemoService.doSomething(), id = {}", id); try { LogHelper.debug(log, "DemoService.doSomething(), step = check"); // business } catch (final Exception e) { LogHelper.error(log, "DemoService.doSomething() is error : {}.", e.getMessage(), e); throw e; } ``` ## 与现有 Ineed-Core 代码的对齐关系 当前方案不是重新发明一套日志风格,而是基于现有 Ineed-Core 代码收敛出来的统一规则: - `ICreateController`、`ILogicDeleteController` 等已经负责标准 Web 入口日志 - `ICreateService`、`IAccountService`、`ILogicDeleteService` 等已经负责标准服务入口日志与异常日志 - `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter` 已经负责请求上下文采集 - `GlobalExceptionHandler` 已经负责统一异常输出 - `JpaEntityCallback`、`JdbcEntityCallback`、`MybatisFlexEntityCallback`、`R2dbcEntityCallback` 已经负责底层实体回调日志 本规范要求新增代码与这些真实锚点保持一致,而不是平行造轮子。 ## 追踪信息来源 请求级追踪信息来自 `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter`,不需要每个业务重复采集。 当前至少可以稳定拿到以下上下文字段: - `requestUrl` - `requestMethod` - `signature` - `applicationKey` - `ip` - `userAgent` 业务日志在需要补充追踪时,应优先复用这些已有上下文,而不是重新拼装另一套请求描述模型。 ## 业务代码日志硬约束 新增或修改 Ineed-Core 业务代码时,必须同时遵守: - 日志门面必须走 SLF4J:具体类用 `@Slf4j`,interface 默认方法用 `LoggerFactory.getLogger(...)` - Web 侧具体类统一使用 `@Slf4j` 生成的 `log`,调用 `LogHelper` / `ReactiveLogHelper` 时只传入这个已有 `log`,不自定义 Logger 或日志对象,不重复构建请求上下文 - 已经具备 context 依赖的 Engine Service / Manager / Repository 业务链路日志统一使用 `LogHelper`(阻塞式)或 `ReactiveLogHelper`(响应式),不再使用 `log.xxx()`,确保每条日志自动携带操作用户信息 - 无 context 依赖的纯 core、runtime、agent、工具、测试辅助模块不要仅为日志新增依赖;继续保留 `@Slf4j + log.xxx(...)` - 标准支持接口已有的默认日志不要重复包一层 - 自定义 `Controller` / `Service` / `Manager` / `Hook` / `Repository` 方法必须显式补日志 - 只打印必要的关键参数,不打印密码、Token、密钥、完整凭证 - 发生异常时记录 `error` 后继续抛出 ## 落地检查清单 - 是否先区分逻辑删除与物理删除,再映射删除后钩子 - 逻辑删除前保护是否落在 `beforeLogicDelete` - 直接 FK 引用(非 M2M)是否在 `checkCustomCanDelete` 中查询引用点,抛 `DataInUseException` 并使用 FK i18n key - FK i18n key 是否符合 `{referencingTableName}.fk.in-use.{constraintName}` / `{referencingTableName}.fk.not-found.{constraintName}` 格式,且三语同步 - 物理删除前保护是否落在 `beforePhysicsDelete` - 物理删除时是否在 `beforePhysicsDelete` 中级联删除所有关联数据的从表/引用行 - 逻辑删除后的可恢复扩展是否落在 `afterLogicDelete` - 级联物理清理是否落在 `afterPhysicsDelete` - 恢复后补偿是否落在 `afterRecovery` - 新写具体类是否统一使用 `@Slf4j` - 是否正确复用了框架默认 `Controller` / `Service` 日志而没有重复套娃 - 自定义方法是否包含动作名、关键入参、异常对象 - Web 侧具体类是否使用 `@Slf4j` 生成的 `log` 调用 `LogHelper` / `ReactiveLogHelper`,没有引入自定义 Logger、日志对象或重复请求上下文 - 具备 context 依赖的 Engine Service / Manager / Repository 业务链路是否使用 `LogHelper`(阻塞式)或 `ReactiveLogHelper`(响应式)输出日志,确保用户信息已注入 - 无 context 依赖的底层模块是否没有为了日志新增依赖,且仍遵守 SLF4J 与敏感信息规则 - 是否避免打印密码、Token、密钥、完整凭证 - 是否继续复用 `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter`、`GlobalExceptionHandler`、`ResultBuild` > 最后更新:2026-05-16 --- # 契约、错误与国际化 来源:docs/standards/contract-and-error.md SHA-256:8c64e26726ba9741e6a6fbf91ab73868735c29ec9392831f7bd2377fb538ee40 网站:/docs/contract-and-error/ 规则:`CORE-CONTRACT-001`。适用于 Web 入参/返回、业务共享契约和错误处理。 ## 必须遵守 - 共享 AO/Query/VO/DTO 放业务 `*-core` 的 `contract/*`;新代码不生成 `pojo/*`。 - 对象转换使用 `converter/XxxConverter`。普通 Converter 直接复用父接口方法,有定制映射才覆盖。 - `@Schema(requiredMode = REQUIRED)` 不是运行时校验;AO 同步声明适当 Jakarta Validation 注解。 - Web 返回复用 `ResultBuild`、`ResponseResults` 与统一异常处理,不另造外壳;不能把裸 `Flux` 塞入普通 JSON `data`。 - 响应式普通集合接口收敛为 `Mono>>`;动作按真实语义使用 `Mono`,Web 层用 `.then(ResultBuild.success())` 构建结果。 - 框架通用错误复用根级 `ErrorCode`;业务专有错误先到全局登记表确认范围,再维护模块常量和所有语言资源。 完整接口组合和返回类型见 [CRUD/Query 指南](/docs/crud-query-guide/)。错误机制、约束 key 和 i18n API 的权威正文见 [Constant/ErrorCode/I18n](/docs/constant-errorcode-i18n-guide/),可复制样板见[模块样板](/docs/module-constant-i18n-template/)。 ## 错误码来源 [应用错误码登记表](/references/#source-6c67578a7e735914)是应用私有错误码唯一分配源。不要根据旧模块名、数字长度或旧 Core 表自行推导可用号段。已发布错误码的变更还需考虑调用方和 i18n 兼容。 UK/FK 约束使用通用错误与对应约束 i18n key,避免每个数据库约束再造一套私有码。约束 key 定义在模块 Constant 中。 ## 验证 新增字段至少验证非法输入、创建/编辑后的读取、转换与敏感信息隐藏。新增错误至少验证错误语义、资源 key/语言文件与实际响应。数据库错误和业务校验错误可能来自不同入口,不能只测其中一条。 反例:只补 OpenAPI 必填注解;把 Entity 直接作为公共契约;捕获异常后返回成功;复制另一个模块的错误码数字。 --- # 业务需求文档模板 来源:docs/project-development/module-requirements-template.md SHA-256:8cfc7bdfefa0391564fc976424b8df7f42a966ffa05284f122882fbc4cb6fddd 网站:/docs/module-requirements-template/ ## 文档定位 本文档提供一份面向业务模块的需求说明模板,用于: - 在写模块需求时提前补齐开发和代码生成所需的关键信息 - 把实体层约定、数据状态能力、删除顺序、前后端对齐约束写清楚 - 降低开发者、代码生成器或自动化工具因为需求输入不完整而自行猜测的概率 它不是强制的一字不差模板,但以下关键段落建议保留。 ## 建议结构 ### 1. 模块目标 - 模块名称 - 业务对象名称 - 目标用户 - 核心场景 ### 2. 输入资料 - SQL 文件路径 - 设计总览路径 - 关联基础需求路径 - 目标模块目录 ### 3. 业务对象与字段 按实体分别列出: - 主表 / 从表 - 关键字段 - `AO` 创建 / 编辑入参字段及必填性 - 唯一约束 - 排序字段 - 逻辑删除字段 - 敏感字段 `AO` 必填性需要明确到字段级,供开发者、代码生成器或自动化工具同步生成 OpenAPI 与 Jakarta Validation 注解: | AO 类 | 字段 | 类型 | 是否必填 | 校验注解 | 说明 | | --- | --- | --- | --- | --- | --- | | `XxxAO` | `xxxName` | `String` | 是 | `@NotBlank` | 名称不能为空白 | | `XxxAO` | `sortWeight` | `Integer` | 是 | `@NotNull` | 排序值必须提交 | | `XxxBatchAO` | `targetIdSet` | `Set` | 是 | `@NotEmpty` | 批量目标集合不能为空 | 如果字段在 `@Schema(requiredMode = RequiredMode.REQUIRED)` 中标记必填,必须同步写出对应的 `jakarta.validation.constraints` 注解;不要只维护 OpenAPI 必填标记。 ### 4. 数据状态能力开关 这一段必须写清楚,供生成代码时直接判断。 固定规则: - `AbstractTenantBaseEntity`、`AbstractTenantBaseTreeEntity`、`AbstractTenantBaseAccountEntity`、`AbstractTenantBaseOwnerEntity` 基类只默认提供 `DataStatusFeature.FIXED` - 除 `FIXED` 外,当前可用开关为 `ENABLED`、`DEFAULT`、`PRIMARY`、`LOCKED`、`PUBLISHED`,必须由最终实体类显式打开 - 需求文档未声明的开关,生成代码时不要默认开启 - 如果最终实体不需要额外开关,可以不声明 `@EnableDataStatus`,此时只保留基类的 `FIXED` 推荐按下面的表填写: | 实体类 | 是否继承 `AbstractTenantBase*Entity` | 需要开启的开关 | 说明 | | --- | --- | --- | --- | | `Xxx` | 是 | `ENABLED, DEFAULT` | 支持启停,新增默认启用 | | `XxxCategory` | 是 | `ENABLED, PUBLISHED, DEFAULT` | 支持启停、发布、默认项 | | `XxxLog` | 否 | 无 | 日志实体不走数据状态能力 | 可选开关值: - `ENABLED` - `DEFAULT` - `PRIMARY` - `LOCKED` - `PUBLISHED` ### 5. 标准能力范围 按实体或模块说明是否需要: - CRUD - 分页查询 - 详情查询 - 回收站查询 - 状态切换 - 排序 - 账号状态 - 自定义动作 ### 6. 共享扩展与转换层 这一段建议显式写清楚,避免生成代码时把共享认证能力和对象转换层生成错位。 至少说明: - 模块是否接入统一认证链 - 如果接入统一认证链,是否复用 `IAccountAuthenticationService`、`IReactiveAccountAuthenticationService`、`ISecurityAuthoritiesService`、`IReactiveSecurityAuthoritiesService` - 主体模块自己的 `Manager` 只需要保留哪些能力,例如 `loadByUsername(...)`、`accountToSecurityUser(...)`、主体特有查询、模块私有异常映射 - 哪些数据栈需要生成对象转换实现,例如 `jpa / jdbc / mybatis-flex / r2dbc` - 转换实现是否统一放在实现模块的 `converter/` 包 - 是否同时存在 MyBatis-Flex 持久化 `mapper/` 与对象转换 `converter/` - 是否需要接入 `CreateManager` / `UpdateManager` / `FindByIdManager` / `OrderManager` 一类抽象,以及对应的 `getCreateEntityConverter()`、`getUpdateEntityConverter()`、`getEntityVoConverter()` ### 7. 删除顺序与清理规则 必须明确: - 主表与依赖表 - 删除顺序 - 哪些关系清理落在 `afterLogicDelete` - 哪些恢复补偿落在 `afterRecovery` ### 8. 分层落点 至少说明: - 哪些规则落在 `Controller` - 哪些规则落在 `Service` - 哪些规则落在 `Manager` - 哪些规则落在 `Repository / EntityManager` - 生成代码的类级 Javadoc 需要说明哪些职责边界和业务规则,尤其是唯一性、状态流转、删除保护、级联清理、外部同步、审计或权限边界 - 代码格式、Javadoc、`@author`、字段注释、`@Schema` 与字段声明统一遵守 [code-style-guide.md](/docs/code-style-guide/) ### 9. 前后端对齐 至少说明: - 控制器根路径 - `resourceKey` - 页面入口 - 页面路由 - 按钮权限点 - 权限清单节点类型:`catalog / menu / page / api / button` - 如果存在一级大目录,权限清单节点类型必须包含 `catalog`,并说明该模块挂在哪个大目录下 - `menu / page` 需要写清前端访问路径和真实组件路径,供后续动态路由使用;`api` 写后端接口路径;`button` 不写 `permissionUrl / component / icon` - 权限排序:按 `orderWeight DESC` 设计,建议顺序为辅助查询 API、列表查询、CRUD、状态操作、回收站操作 - 权限说明口径:写给授权人员看,使用业务语言,不使用 `idSet`、`query`、`commonParameters` 等后端参数名 - 前端需要的列表字段、查询字段、详情字段 - 创建字段、编辑字段 - 默认排序 - 是否需要回收站 - 是否需要状态切换 - 是否需要账号能力 - 是否需要导入与导出 - 如果需要导入与导出,`AO / VO` 对应哪些字段参与,Excel 列头中文名是什么 - 是否要求 `webmvc` 与 `webflux` 两条线都暴露同等能力 ### 10. 租户、权限、事务与集成边界 按实体写明租户模式、读写范围和操作身份;区分 API 权限与数据范围。按业务动作写明事务边界、失败回滚、重复请求及并发竞争语义。 需要缓存或事件时,明确 key 的上下文维度、失效范围、提交时序和失败处理;不需要时写“不适用”。这些字段描述本模块的业务选择,通用规范见[规则索引](/docs/standards/)。 ### 11. 验收与测试 至少说明: - Happy path - Negative path - 排序与状态切换验证点 - 删除与恢复验证点 ## 可复制片段 ```md ## 共享扩展与转换层 - 是否接入统一认证链:是 - 共享认证契约:复用 `IAccountAuthenticationService`、`IReactiveAccountAuthenticationService` - 权限快照契约:复用 `ISecurityAuthoritiesService`、`IReactiveSecurityAuthoritiesService` - 主体模块 Manager 保留能力:`loadByUsername(...)`、`accountToSecurityUser(...)`、主体特有查询 - 需要生成对象转换实现的数据栈:`jdbc`、`mybatis-flex` - 转换实现落点:统一放在 `converter/` 包,类名使用 `XxxConverter` - MyBatis-Flex 是否同时存在持久化 `mapper/`:是;`mapper/` 仅承载持久化接口,对象转换单独放 `converter/` - 是否需要接入统一转换方法:是,需要实现 `getCreateEntityConverter()`、`getUpdateEntityConverter()`、`getEntityVoConverter()` ## 数据状态能力开关 固定规则: - `AbstractTenantBase*Entity` 基类只默认提供 `DataStatusFeature.FIXED` - 其他开关必须由最终实体类显式打开 - 需求文档未声明的开关,生成代码时不要默认开启 | 实体类 | 是否继承 `AbstractTenantBase*Entity` | 需要开启的开关 | 说明 | | --- | --- | --- | --- | | `AccessCredential` | 是 | `ENABLED, DEFAULT` | 支持启停,新增默认启用 | | `AccessCredentialGroup` | 是 | `ENABLED` | 只支持启停 | ``` > 最后更新:2026-05-15 --- # 开发约定反模式清单 来源:docs/project-development/convention-anti-patterns.md SHA-256:de6208cd8d21bb645053562434e8181b6ba61167c15f517ae1b82f7ae107e6b6 网站:/docs/convention-anti-patterns/ 本文档列出基于 Ineed-Core 开发时经常出现的错误写法。关于分层职责边界、多表协作、Service/Manager 编排的正确做法,详见 [custom-business-layering-guide.md](/docs/custom-business-layering-guide/)。 ## 会话上下文拆散传参 错误方式:调用方已经拿到 `SecuritySession`,却继续把 `identityType`、`principalId`、`tenantKey` 等字段拆散后逐层传递。 问题: - 接口参数越来越多,语义反而更不完整 - 会话字段一旦扩展,调用链会发生连锁改签名 - 很容易出现某些字段传了、某些字段忘了传的半残调用 正确方式:围绕当前登录上下文工作的扩展接口,优先直接接收 `SecuritySession` 或等价聚合上下文 DTO。 ## 把登录状态重新塞回 JWT 错误方式:为了让前端少查接口,把完整用户、租户、权限快照、完整 Session 对象或服务端过期策略重新写进 JWT payload。 问题: - JWT 一旦泄漏,暴露的信息面会变大 - 用户权限、租户或会话状态变化后,旧 JWT 很容易与服务端真实状态不一致 - 强制下线和会话续期会被 token 自身内容干扰,最终形成两套登录状态来源 正确方式:JWT 只作为传输凭证,payload 只保留 `sid` 等最小会话标识。服务端通过 `sid -> SecuritySession` 从 Redis 读取权威登录状态;前端不要解析 token 驱动页面身份、租户或权限。 ## 在线会话继续使用用户维度命名 错误方式:把在线会话管理继续命名为用户维度的类名、路径或资源标识,或者用 `principalId`、用户名、token 全串作为强制下线主键。 问题: - 一个用户可能存在多个设备、浏览器或登录实例,在线对象本质是会话而不是用户 - 用用户维度强制下线会误伤同账号的其他会话 - 旧命名会让前端权限点、接口路径和 Redis 索引语义继续漂移 正确方式:在线会话统一使用 `OnlineSessionVO`、`online-session`、`security.online-session`,查询和强制下线以 `sid` 为会话标识。 ## 同步与响应式扩展契约不分线 错误方式:为同一扩展点继续保留未分线的旧接口,或者把阻塞式与响应式方法混在同一个契约里。 问题: - 同步与响应式语义会互相污染 - `webmvc` 与 `webflux` 的适配关系不清楚 - 后续很容易出现一侧改了签名、另一侧还挂在旧接口上的漂移 正确方式:同一扩展点按阻塞式与响应式分开定义;阻塞式放 `service/`,响应式放 `service/reactive/`。 ## 共享扩展契约落在实现模块 错误方式:只在 `webmvc` 或 `webflux` 实现模块中定义接口,再让其他模块反向依赖这些实现层包名。 问题: - 共享契约无法被两条 Web 线稳定复用 - 发起方与实现方的依赖方向会变得混乱 - 实现模块一旦调整包名,跨模块调用会直接断裂 正确方式:跨模块复用的共享扩展契约优先放在发起方 `core` 共享层;`webmvc` / `webflux` 模块只保留各自适配实现。 ## 响应式契约乱放包 错误方式:响应式接口或响应式适配器继续散落在 `service` 根包或任意自定义包名下。 问题: - 同一模块中同步与响应式目录结构不稳定 - 搜索和维护响应式扩展点时难以快速定位 - 后续修改很容易沿着旧样式继续生成错误包结构 正确方式:当同一代码模块中同时存在同步与响应式 `service` 契约时,响应式统一放在 `service/reactive/`。 ## 响应式动作型方法暴露影响行数 错误方式:把创建、编辑、删除、状态切换、账户密码更新、登录成功回写这类副作用型 reactive 方法定义成 `Mono`,并把影响行数继续向 `service` / `manager` / Web 扩展契约上传递。 问题: - 会把底层持久化细节泄漏到上层业务契约 - 阻塞式与响应式同一能力的签名语义会发生漂移 - 上层调用者会错误依赖“影响行数”而不是动作是否完成 正确方式:与阻塞式语义保持一致。阻塞式为 `void` 的动作型契约,在响应式侧统一定义为 `Mono`;只有查询、统计一类需要返回结果的方法才返回具体值,例如 `Mono`。 ## 对象转换层混用 Mapper 命名 错误方式:继续把对象转换实现生成为 `mapstruct/` 包、`XxxMapper` 类,或者把 MyBatis-Flex 持久化 `mapper/` 与对象转换实现混在同一个包里。 问题: - `mapper` 在当前约定中要留给 MyBatis 持久化层 - 同一聚合里会同时出现两个语义完全不同的 `XxxMapper` - 后续修改很容易继续沿着旧命名扩散错误骨架 正确方式:对象转换实现统一放在各实现模块的 `converter/` 包下,类名统一使用 `XxxConverter`;MyBatis-Flex 的 `mapper/` 只保留持久化接口。 ## 新代码继续生成 pojo 包 错误方式:新建模块或新增对象时,继续参考历史模块生成 `pojo/ao`、`pojo/query`、`pojo/vo`。 问题: - 新模块会和当前 `core/contract/*` 约定分裂 - 前端、自动化输入和需求模板会继续出现两套对象命名 - 后续迁移时容易在同一模块内混用 `contract/*` 与 `pojo/*` 正确方式:新模块和新增代码统一使用 `contract/ao`、`contract/query`、`contract/vo`、`contract/dto`。如果目标历史模块已经大量使用 `pojo/*`,先明确迁移策略,不默认继续生成。 ## 压缩 Javadoc 和字段声明 错误方式:生成 `/** 订单编号。 */` 这类单行 Javadoc,或者把 `@Schema(...) protected String orderNo;` 挤在同一行。 问题: - 与当前源码格式约定不一致 - IDE 格式化后仍难以稳定恢复成统一风格 - 后续修改会沿着压缩格式继续扩散 正确方式:统一遵守 [code-style-guide.md](/docs/code-style-guide/),类级、字段级、方法级 Javadoc 都使用标准多行格式;`@Schema(...)` 独占一行,字段声明另起一行。 ## 普通 Converter 重复声明父接口方法 错误方式:每个 `XxxConverter` 都机械重写 `ao2Entity(...)`、`entityCopy(...)`、`appleAo(...)`、`entity2VO(...)`,但方法上没有 `@Mapping`、`@BeanMapping` 等配置,也没有自定义逻辑。 问题: - `AoEntityConverter` 与 `EntityVoConverter` 已经声明了这些抽象方法,MapStruct 会为继承方法生成实现 - 重复声明会扩大代码量,后续批量修改更容易漏改或产生无用 import - 后续生成或修改会误以为这些方法是每个 Converter 的必备模板,从而继续扩散冗余代码 正确方式:普通 Converter 只保留 `@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)`、继承关系和必要的 `INSTANCE`。只有需要方法级 `@Mapping`、`@Mappings`、`@BeanMapping`、`@InheritConfiguration`、`@InheritInverseConfiguration` 等配置时,才显式声明对应方法;带派生字段或字典标签的 `entity2VO(...)` 应保留。 ## 把共享认证能力重新塞回 AccountManager 错误方式:为业务主体重新生成 `IAccountAuthenticationManager`,或者把共享认证逻辑重新收口到通用 `AccountManager`。 问题: - 共享认证能力会与主体模块自身职责重新耦合 - 阻塞式与响应式两条线容易再次出现不一致 - 新模块会继续复制密码治理、锁定规则和权限快照处理 正确方式:统一复用 `IAccountAuthenticationService` / `IReactiveAccountAuthenticationService` 与 `ISecurityAuthoritiesService` / `IReactiveSecurityAuthoritiesService`;主体模块自己的 `Manager` 只保留 `loadByUsername(...)`、`accountToSecurityUser(...)`、主体特有查询和模块私有异常映射。 ## 错误钩子命名 错误方式:直接在代码中凭空实现 `afterDelete`,或者让代码生成器生成不存在的删除后钩子名。 问题: - 当前框架真实钩子名不是 `afterDelete` - 生成的代码会偏离 `LogicDeleteManager` 现有接口 正确方式:把业务上的 `afterDelete` 语义明确映射到 `afterLogicDelete`;恢复后的补偿逻辑映射到 `afterRecovery`。 ## 自造响应包装 错误方式:在业务层手工创建另一套响应对象,或者绕开统一返回体系直接拼装 `ResponseResults`。 问题: - 会破坏统一异常映射 - 会让返回结构和错误码风格不一致 正确方式:成功结果在 `Controller` 侧继续通过 `ResultBuild` 返回,异常交给 `GlobalExceptionHandler` 处理。 ## 权限清单字段语义混乱 错误方式:把 `api` 的 `permissionUrl` 写成前端路由,把 `menu/page` 的 `component` 写成后端路径,给 `button` 填 `permissionUrl/component/icon`,或者为了分组虚构一个 `*.menu` 节点再把多个独立入口生成为 `page`。 问题: - 前端动态路由会找不到真实组件 - 后端接口授权和前端按钮显隐无法稳定对应 - 授权页面读取权限树时会出现目录、页面、接口和按钮混杂 正确方式:权限类型统一使用 `catalog / menu / page / api / button`。`catalog` 是目录或分组,不写 `permissionUrl/component`;`menu` 是独立菜单入口,写前端访问路径和真实组件路径;`page` 是不独立出现在菜单的隐藏/从属页面,常见于详情、编辑、配置、主从资源子列表页,通常伴随 `ISearchOfMasterIdController` / `search-of-master-id`;`api` 写后端接口路径;`button` 只表达页面可见操作,不写 `permissionUrl/component/icon`。 ## 日志缺少关键追踪信息 错误方式:只打印“操作失败”之类的空日志,不带动作名、关键入参和异常对象,也不复用请求上下文。 问题: - 无法关联到具体接口和具体请求 - 排查异常时缺少 `requestUrl`、`requestMethod`、`signature`、`applicationKey` 等线索 正确方式:日志至少带上动作名、关键入参、异常对象,并优先复用 `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter` 提供的追踪上下文。 ## 继续使用过期 API 错误方式:新增或修改代码时继续使用 JDK 或第三方依赖中已经标记为 `@Deprecated` 的 API,例如在 `commons-lang3` 中继续使用 `StringUtils.equals(...)`、`StringUtils.equalsIgnoreCase(...)`、`StringUtils.contains(...)` 或 `StringUtils.containsIgnoreCase(...)` 做字符串比较和查找。 问题: - 过期 API 可能在后续依赖升级中被移除,导致模块迭代时集中编译失败 - 编译器和 IDE 的 deprecation warning 会被噪声淹没,真正需要处理的升级风险不容易被发现 - 同一代码库中混用旧 API 和替代 API,会让生成器、人工修改和代码审查继续沿着旧写法扩散 正确方式:不使用过期 API,也不要用 `@SuppressWarnings("deprecation")` 掩盖问题。`commons-lang3` 字符串大小写敏感比较和查找使用 `Strings.CS.equals(...)`、`Strings.CS.contains(...)`,大小写不敏感比较和查找使用 `Strings.CI.equals(...)`、`Strings.CI.contains(...)`;空白判断和默认值处理继续按未过期的 `StringUtils.isBlank(...)`、`StringUtils.defaultIfBlank(...)` 等方法使用。 ## Service 回查多对多对象 错误方式:查询已分配对象列表时,`Service` 先调用关系 `Manager` 查 ID 集合,再调用另一个 `Manager` / `Service` 批量回查对象;保存关系时,`Service` 又先加载全部目标对象,再逐个过滤后保存关系。 问题: - `Service` 会混入持久化细节和对象过滤规则 - 关系读取链路变成多次查询,代码绕、性能差,也容易漏掉逻辑删除或启用状态过滤 - 后续前端分配列表、认证权限计算、关系写入三类场景会挤在同一批方法里,导致关系 `Manager` 越来越臃肿 - `Service` 容易为了拿对象而横向调用其它 `Service`,破坏事务和职责边界 正确方式:保存关系时,`Service` 只做主对象校验、状态判断和 ID / 授权模式归一化,然后调用关系 `Manager` 覆盖保存;查询已分配对象列表时,由主对象 `Manager` 通过关系表 join 目标对象表并直接返回 VO。认证/鉴权链路如需高频计算,可以保留轻量 ID 查询,但不要把它复用成前端列表查询。 ## JPA 关系查询手写 SQL 错误方式:JPA 实现中为了多对多列表或级联查询直接写 native SQL / 拼接 SQL。 问题: - 字段改名、实体映射调整后,SQL 不会随 ORM 模型一起演进 - 逻辑删除、启用状态、租户等通用过滤容易在手写 SQL 中漏掉 - JPA、MyBatis-Flex、JDBC、R2DBC 的实现边界会混乱 正确方式:JPA 自定义关系查询优先使用框架约定查询或 QueryDSL join;MyBatis-Flex 优先使用 `QueryWrapper` 和 processor 生成的 `TableDef`;JDBC / R2DBC 暂时保持集中 SQL 实现,不向上层泄漏 SQL 细节。 ## 级联删除散落在业务入口 错误方式:在 `Controller` 或任意业务 `Service` 中手工清理关系表、从表、后代节点,然后再调用删除方法。 问题: - 删除规则会随着入口数量增加而重复 - 直接调用 `physicsDelete` / `forceDelete` 的其它入口可能绕过清理逻辑 - 级联清理和实体生命周期脱节,后续很难判断哪些关系必须随实体物理删除而删除 正确方式:当前实体被物理删除时必须同步清理的关系表、从表和后代数据,落在该实体 `Manager` 的 `beforePhysicsDelete`;物理删除成功后才有意义的外部副作用,落在 `afterPhysicsDelete`。`forceDelete` 只表示不做状态判断直接物理删除,不额外生成 `beforeForceDelete` / `afterForceDelete`。 ## 子表 count/delete 写在 Manager 错误方式:在 `Manager` 中为了判断是否存在引用数据或做物理级联删除,手写通用 helper,例如: - JDBC / R2DBC:`countActiveValue(tableName, columnName, columnValue)`、`deleteValue(tableName, columnName, columnValue)`,内部拼 SQL - JPA:在 `Manager` 中为简单外键计数或删除手写 QueryDSL `select count` / `delete` - MyBatis-Flex:在 `Manager` 中写 `countActiveValue(BaseMapper mapper, String columnName, String columnValue)`,通过 `QueryMethods.column(columnName)` 拼通用条件 问题: - `Manager` 会泄漏表名、列名、SQL/DSL 和 Mapper 细节,职责从业务生命周期滑到持久化实现 - 相同规则在 JDBC、R2DBC、JPA、MyBatis-Flex 中容易写出四套不同风格,后续维护难以对齐 - 子表字段改名、外键字段调整或持久化框架升级时,需要到业务层搜 SQL/列名,容易漏改 - MyBatis-Flex 使用 `BaseMapper + columnName` 会丢掉具体 Mapper 的语义,代码审查时看不出到底在操作哪张表 正确方式:把按外键 `count/delete/findBy/exists` 的动作放到对应子表、关系表、从表自己的 `Repository` / `Mapper`。 - JDBC / JPA 使用 Spring Data 派生方法,例如 `countByCategoryIdAndDeleted(...)`、`deleteByCategoryId(...)` - R2DBC 使用响应式派生方法,例如 `Mono countByCategoryIdAndDeleted(...)`、`Mono deleteByCategoryId(...)` - MyBatis-Flex 在具体 `Mapper` 中定义明确默认方法,内部用 `QueryWrapper` 或生成的 `TableDef` 表达条件 - `Manager` 只保留业务判断、生命周期落点和异常语义,例如在 `checkCanDelete` / `beforePhysicsDelete` 中调用这些方法 > 最后更新:2026-05-18 --- # 测试与交付证据 来源:docs/standards/testing.md SHA-256:2b57599d8f03e6b85ee49bccbf2e2cc501ed5b08a78cc251e50983c4c34590ea 网站:/docs/testing/ 规则 `CORE-TEST-001`。当前测试使用 JUnit Jupiter、Spring Test 与响应式测试工具,依赖版本由 parent/POM 解析。业务场景详细要求见[业务集成测试指南](/docs/business-integration-test-guide/)。 ## 分层报告 | 层级 | 证明什么 | 不单独证明什么 | | --- | --- | --- | | 编译 | 类型、依赖、生成源码可用 | 业务行为正确 | | 契约/单元 | 能力组合、生命周期、上下文、算法 | 真实 SQL、Spring 装配、权限代理 | | 持久化集成 | 真数据库映射、约束、过滤、事务 | HTTP 安全与前端流程 | | 应用集成 | Spring 装配、实际 HTTP、身份与权限 | 浏览器操作完成 | | 前端 E2E | 实际用户流程 | 所有后端边界组合 | 测试中 `new Service(...)` 不会自动具备事务或安全代理;Map/mock 仓储不证明真实数据库行为。不要根据类名含 `Integration` 就推断覆盖范围。 ## Core 验证命令 工作目录为 Core 根目录;工具链见[环境准备](/docs/prerequisites/)。运行所有 Core 测试: ```bash mvn test ``` 与文档关键行为相关的定向入口: ```bash mvn -pl ineed-core-tests/ineed-core-test-webmvc,ineed-core-tests/ineed-core-test-webflux \ -am '-Dtest=*BusinessChainTest,*Tenant*Test,*ServiceCache*Test,*IntegrationEvent*Test' \ -Dsurefire.failIfNoSpecifiedTests=false test ``` 运行基础契约模块: ```bash mvn -pl ineed-core-base -am test ``` `-Dsurefire.failIfNoSpecifiedTests=false` 允许上游模块没有匹配类;执行后仍须汇总目标 `target/surefire-reports/TEST-*.xml`,确认目标测试实际运行,不能接受零测试的假通过。避免把历史报告混入本次统计。 ## 新测试应该断言什么 按业务结果命名用例,正向场景与错误路径配对。多表动作验证回滚与最终持久化;关系操作验证读取、替换、清空和删除过滤;租户与权限验证拒绝后无数据变化。 响应式使用 StepVerifier 或明确的订阅断言:创建 Publisher 不提前执行,订阅后完成或错误。同步与响应式场景表达同一业务语义。 修改小范围文档或风格不要求全量业务回归;修改公共生命周期、租户、事务或接口时扩大到受影响的框架与业务验证。 ## CI 与覆盖率 本仓当前未发现已纳入版本控制的 `.github/workflows` CI 工作流,也未在本次核查范围发现统一覆盖率阈值;不虚构百分比门槛。团队接入 CI 时先执行文档检查、样例所在模块编译和必要行为测试,再按环境提供数据库层验证。 本轮执行证据见[验证记录](/docs/verification-2026-09-19/)。部署环境和未执行的数据库/HTTP 场景必须单独标注。 --- # 业务集成测试编写指南 来源:docs/project-development/business-integration-test-guide.md SHA-256:00c0133162db9d0240d731c93894bba919327157c3ad57ed8278c8ff43639e71 网站:/docs/business-integration-test-guide/ ## 文档定位 本指南用于约束基于 Ineed-Core 开发业务模块时的 Java 业务集成测试写法。 当一个业务聚合已经包含 CRUD、状态流转、多对多关系、认证/授权接入、级联删除或多数据栈实现时,不能只依赖零散单元测试。模块应提供一个稳定的全业务集成测试入口,用于每次改完模块后的主回归验证。 ## 验证层级与证据 先按[测试规范](/docs/testing/)区分内存/Mock 契约测试、真实数据库、Spring/HTTP 和前端 E2E。类名含 Integration 或调用 Controller 方法不等于已经验证事务代理、授权或真实数据库。报告应包含命令、实际数量、结果和未验证项。 ## 主回归入口 每个复杂业务聚合推荐保留一个语义清晰的全业务集成测试类,例如: - `XxxBusinessIntegrationTest` - `XxxBusinessScenarioTest` - `XxxFullBusinessIntegrationTest` 该测试类不替代低层单元测试,而是作为模块级主回归入口,覆盖从业务入口到持久化结果的完整链路。 命名和职责要求: - 测试类名表达业务对象或业务聚合,不按技术栈命名。 - 测试方法按业务场景命名,不按内部实现方法命名。 - 测试覆盖真实业务结果,不只验证 mock 调用次数。 - 每次修复多对多、级联删除、权限分配、认证加载等核心链路后,应优先运行该集成测试。 ## 覆盖范围 全业务集成测试至少覆盖以下维度: - 主对象创建、查询、编辑。 - 启用、停用、锁定、解锁等已声明的数据状态能力。 - 逻辑删除、恢复、物理删除。 - 多对多关系写入、替换、清空和读取。 - 前端分配类接口对应的读列表与保存关系两类动作。 - 认证/授权类查询,例如加载当前主体的分组、角色、权限或数据范围。 - 逻辑删除数据、未启用数据、跨租户数据等不应出现在业务结果中的过滤规则。 - 物理删除后的关系表、从表、后代节点或绑定关系级联清理。 如果业务对象本身没有某类能力,可以不测该能力;但测试代码或测试说明应体现这是业务能力缺失,而不是遗漏。 ## 多对多场景测试 多对多关系测试必须覆盖业务结果,而不是只覆盖保存方法返回成功。 推荐场景: 1. 创建主对象和多个目标对象。 2. 调用分配接口保存目标对象 ID 集合或授权集合。 3. 查询已分配对象列表,断言返回 VO 包含目标对象,并且字段满足前端展示需要。 4. 再次调用分配接口替换为另一批目标对象,断言旧关系消失、新关系存在。 5. 调用空集合分配,断言关系被清空。 6. 将目标对象逻辑删除或停用后,断言已分配对象列表或认证结果不再返回该对象。 落层期望: - 写关系时,`Service` 只做 ID / 授权模式归一化和主对象状态校验。 - 查询已分配对象列表时,由主对象 `Manager` 通过 join 返回 VO。 - 测试应能暴露“Service 先查 ID 再回查对象”的旧模式遗漏过滤条件或绕路查询的问题。 ## 级联删除场景测试 级联删除测试必须证明删除后的数据状态符合业务约束。 推荐场景: - 删除主对象后,关系表中该主对象的关系被清理。 - 删除目标对象后,关系表中指向该目标对象的关系被清理。 - 删除树形节点后,后代节点按业务规则被物理删除或被禁止删除。 - 删除前存在外键或强依赖时,`beforePhysicsDelete` 中的清理足以保证主对象物理删除成功。 - 逻辑删除不应物理清理可恢复关系,除非业务需求明确要求不可恢复。 断言要求: - 不只断言删除方法没有抛异常。 - 必须查询关系表、从表或业务读取结果,确认级联结果。 - `forceDelete` 测试只验证跳过状态判断后进入物理删除流程;不要假设存在独立的 `beforeForceDelete` / `afterForceDelete` 钩子。 ## 认证与授权场景测试 如果模块参与认证或授权链路,业务集成测试应覆盖认证入口用到的关键查询。 推荐场景: - 创建可认证主体和相关分组、角色、权限或数据范围。 - 建立必要关系后,通过认证/授权服务加载业务快照。 - 断言快照中包含有效关系。 - 停用或逻辑删除相关对象后,再次加载并断言结果已剔除无效对象。 - 替换关系后,断言旧关系不再影响认证/授权结果。 该类测试应验证业务快照内容,不应只 mock 底层 manager。 ## 多数据栈对齐 同一业务聚合如果同时实现 `jpa / jdbc / mybatis-flex / r2dbc`,测试策略应同步评估四条线。 推荐做法: - 先跑与业务规则无关的单元测试。 - 再跑阻塞式主链路集成测试。 - 再跑响应式主链路集成测试。 - 对 JPA、JDBC、MyBatis-Flex、R2DBC 的核心行为保持同一组业务场景和断言语义。 数据栈差异只允许体现在测试启动配置、事务方式、返回类型和等待方式上;业务断言不能漂移。 JPA 与 MyBatis-Flex 是优先优化路径: - JPA 测试应能覆盖 QueryDSL / ORM 查询是否剔除逻辑删除和未启用数据。 - MyBatis-Flex 测试应能覆盖 `QueryWrapper` / `TableDef` 查询是否与 JPA 语义一致。 - JDBC / R2DBC 当前允许保持集中 SQL 实现,但测试仍应约束返回结果一致。 ## 响应式测试要求 响应式测试必须验证订阅期行为。 - 使用 `StepVerifier` 或等价方式订阅 `Mono` / `Flux`。 - 不用同步 `assertThrows(...)` 断言 Publisher 创建期异常。 - 对动作型方法,阻塞式为 `void` 时,响应式统一断言 `Mono` 完成或错误。 - 对集合查询,断言元素内容和数量,不只断言 `Flux` 非空。 如果阻塞式和响应式同时存在,测试场景名称、前置数据和业务断言应保持一致。 ## 权限与前端分配接口 对前端分配类接口,应同时测试“读已分配列表”和“提交分配结果”。 这两个动作通常服务于同一个页面操作,权限点也应保持一致: - 读已分配列表用于打开分配弹窗或分配页面。 - 提交分配结果用于保存关系。 - 测试应覆盖用户具备该分配权限时,两类接口都可完成业务动作。 - 如果读列表和提交关系拆成两个 resource key,应先确认是否确实存在两个独立授权动作。 权限清单测试或权限清单扫描应避免为同一个分配动作生成多余权限点。 ## 推荐执行命令 模块级回归命令应写在任务说明、PR 描述或模块文档中。 示例: ```bash mvn -pl ,,,,,,,, -am test -DskipTests=false -Dmaven.javadoc.skip=true ``` 如果只改了某一条线,也可以先跑局部测试,但最终合并前仍应运行覆盖业务聚合主链路的回归命令。 ## 禁止写法 - 只测 converter、mapper 或 repository,不测业务入口到结果的完整链路。 - 只断言方法被调用,不断言数据库或业务返回结果。 - 多对多只测保存成功,不测替换、清空、过滤和读取。 - 级联删除只测删除不报错,不查关系表或从表清理结果。 - 响应式测试用同步 `assertThrows(...)` 断言订阅期错误。 - JPA 测试通过后,在实现中保留 native SQL 多对多查询。 - 只维护 WebMvc 测试,完全忽略 WebFlux 对等能力,除非需求明确排除响应式链路。 > 最后更新:2026-05-15 --- # 前端接入与联调指南 来源:docs/project-development/frontend-integration-guide.md SHA-256:37444b62f21f82a60f5abb6b934ce057b088d79da0dc7c968d9e2674cfd3184a 网站:/docs/frontend-integration-guide/ ## 文档定位 本文档面向: - 前端开发者 - 负责前后端联调的后端开发者 - 编写业务需求说明的人 重点回答三类问题: - 前端在 `ineed-core` 里真正应该依赖哪些稳定契约 - `webmvc` 与 `webflux` 两条线对前端来说哪些语义必须保持一致 - 新业务模块在需求阶段,哪些前端信息必须先写清楚,避免后续返工 如果你要先理解当前框架模块边界,先读 [module-capability-map.md](/docs/module-capability-map/)。 如果你要新建业务模块,继续读 [business-module-layout-guide.md](/docs/business-module-layout-guide/)。 如果你要写业务需求说明,继续读 [module-requirements-template.md](/docs/module-requirements-template/)。 如果你要用代码生成器或自动化工具同时生成后端和前端,继续读 [development-contract.md](/docs/development-contract/) 和 [code-style-guide.md](/docs/code-style-guide/)。 ## 前端只依赖这些稳定契约 前端不应直接围绕 `entity`、`repository`、数据栈实现做联调。 前端真正应该依赖的是: - `AO` 作为创建、编辑、导入等提交对象 - `Query` 作为业务筛选条件对象 - `CommonParameters` 作为分页、搜索、排序等通用查询参数 - `VO` 作为列表、详情、树、字典等展示对象 - `ResponseResults` 作为统一响应外壳 - `QueryResults` 作为分页结果对象 这套契约主要位于: - `ineed-core-base` - `ineed-core-webmvc` - `ineed-core-webflux` 新业务模块面向前端的对象应位于后端 `core` 子模块的 `contract/ao`、`contract/query`、`contract/vo`、`contract/dto` 包下。需求、接口说明和前端联调文档统一称为 `AO / Query / VO / DTO` 契约对象,不再把这些对象写成 `pojo`。 当前前端实现主要通过以下入口消费这些契约: - `ineed-frontend/src/shared/api/request.js` 统一注入 token,处理 `ResponseResults`、HTTP 错误和登录过期。 - `ineed-frontend/src/shared/api/serviceFactory.js` 通过 `createConfigCrudService(url, options)` 组合标准 CRUD / query / data status / recycle API。 - `ineed-frontend/src/shared/store/storeFactory.js` 通过 `createConfigCrudStore(storeId, service, options)` 组合列表、编辑、详情、回收站和状态动作。 - `ineed-frontend/src/shared/composables/support/dataStatusActionConfigs.js` 定义状态按钮、权限后缀、显示条件和确认文案。 - `ineed-frontend/src/app/router/index.js` 使用路由 `meta.resourceKey` 或 `meta.resourceKeyBuilder` 做页面级权限判断。 新模块前端代码应优先复用这些工厂和约定,只有领域特有接口才在模块自己的 `service/*.js` 里额外补方法。 ## 登录 Token 与在线会话 当前安全框架把 JWT 当作前后端传输凭证,服务端 Session 才是登录状态的权威来源。 前端接入时遵守: - 登录接口返回的 token 只需要按统一 request 层注入到后续请求中,前端不要解析 token 内容来判断登录身份、租户或权限。 - JWT 只承载最小会话标识,例如 `sid`;不要依赖 JWT 里的过期时间、用户字段或权限字段驱动页面状态。 - token 是否仍有效由后端通过 Redis Session 校验兜底;前端收到统一未登录 / 会话失效错误后,按错误码进入重新登录或刷新当前登录态流程。 - 在线会话管理接口使用 `/security/online-session` 语义,资源标识使用 `security.online-session`;不要继续生成或消费旧的用户维度在线管理路径与资源标识。 - 在线会话列表面向 `OnlineSessionVO`,展示字段来自 VO;强制下线等动作以 `sid` 为目标会话标识,不要用用户名、用户 ID 或 token 全串作为操作主键。 - Redis 在线会话索引语义是 `ONLINE_SESSION`,前端、文档、测试样例不要继续引用旧的 `ONLINE` 命名。 ## AO / Query / VO 的前端语义 ### AO `AO` 是前端提交给后端的应用对象,主要用于: - 新增表单 - 编辑表单 - 批量导入 - 自定义业务动作入参 基础约束来自 `AbstractAO`: - 统一带 `id` - 保存时 `id` 必须为空 - 更新时 `id` 不能为空 这意味着前端在新增和编辑时,不能把同一份未区分状态的表单对象直接复用成两套语义。 ### Query `Query` 是业务筛选条件对象,只承载业务字段,不承担分页与排序。 例如: - 名称 - 编码 - 类型 - 状态 - 时间区间 分页、搜索关键字、排序这些通用参数不应塞进业务 `Query` 里,而是继续通过 `CommonParameters` 传递。 ### VO `VO` 是前端看到的展示对象,主要用于: - 列表行 - 详情页 - 树节点 - 字典项 - 导出对象 基础约束来自 `AbstractVO`: - 统一带 `id` 同一个业务对象在 `webmvc` 与 `webflux` 两条线中的 `VO` 字段语义应保持一致,不允许一边有字段、另一边缺字段。 ## 通用查询参数怎么传 标准查询入口会把前端传入的 `CommonParameters + Query` 组装成 `QueryParameters`。 前端真正需要关注的是 `CommonParameters`: - `dc` 防缓存动态参数 - `searchKey` 通用关键字搜索 - `current` 当前页码 - `pageSize` 每页条数 - `sortJson` 排序 JSON 字符串 `sortJson` 对应的排序对象字段为: - `property` 排序属性。这里必须传后端实体属性名,例如 `createdTimestamp`,不要传 SQL 片段、列别名表达式或 `id desc` 这类拼接内容。 - `direction` 排序方向,支持 `asc`、`ASC`、`ascend`、`desc`、`DESC`、`descend` 当前 JDBC / R2DBC / MyBatis-Flex 标准查询实现会按实体字段做排序白名单校验。非法或未知的 `property` 不应作为有效排序条件进入 SQL。 标准查询接口对前端更接近下面这种调用方式: ```text GET {controllerRoot} ?current=1 &pageSize=20 &searchKey=zhang &sortJson=[{"property":"createdTimestamp","direction":"descend"}] &name=张三 &enabled=true ``` 其中: - `current / pageSize / searchKey / sortJson` 来自 `CommonParameters` - `name / enabled` 这类业务字段来自具体 `Query` 除非某个自定义接口明确要求,否则前端不需要自己手工构造 `QueryParameters` JSON。 ## 当前前端服务与 Store 落地方式 标准模块的前端服务通常长这样: ```javascript import request from '@/shared/api/request'; import { API_PREFIX } from '@/shared/config/constants'; import { createConfigCrudService } from '@/shared/api/serviceFactory'; const URL = 'business/customer'; export const customerDataStatusCapabilities = { enable: true, lock: false, default: false, primary: false, publish: false, reserve: false }; export default { ...createConfigCrudService(URL, { dataStatusCapabilities: customerDataStatusCapabilities }), findProfile(id) { return request.get(`${API_PREFIX}/${URL}/${id}/profile`); } }; ``` 约定如下: - `URL` 不带 `/api` 前缀,由 `createConfigCrudService` 内部拼接 `API_PREFIX`。 - 只有自定义接口需要直接引用 `API_PREFIX`。 - `dataStatusCapabilities` 必须来自需求或后端实体的 `DataStatusFeature` 开关,不能默认全开。 - Store 优先使用 `createConfigCrudStore(storeId, service, { tree, dataStatusCapabilities })`。 - 如果模块是树结构,前端 service/store 要同时打开 `tree: true` 并确认后端提供 `/tree` 查询能力。 字段和页面代码应围绕 `AO / Query / VO` 分开设计: | 前端文件类型 | 对应后端契约 | 说明 | |--------------|--------------|------| | `service/*.js` | Controller 路径与标准后缀 | 只封装 API,不承载页面状态 | | `store/*.js` | `ResponseResults` / `QueryResults` | 只处理加载、列表、编辑、详情、动作状态 | | `routes.js` | `resourceKey` / 页面入口 | 路由 `meta.resourceKey` 必须能映射到后端权限点 | | `pages/*.vue` | VO / Query / action 能力 | 组合表格、表单、状态动作和自定义页面行为 | | schema / columns 文件 | AO / Query / VO 字段 | 维护表单字段、查询字段、列表列、详情字段、校验规则 | 如果一个模块没有 schema/columns 独立文件而是内联在页面中,新增字段时也必须按上表逐项核对,不要只改表单或只改列表。 ## 统一返回外壳 无论是 `webmvc` 还是 `webflux`,对前端可见的统一响应模型都应保持同一语义: - `traceId` 请求追踪号 - `success` 是否成功 - `errorCode` 业务错误码 - `errorMessage` 错误消息 - `alertMessage` 友好提示 - `data` 业务数据 成功响应示例: ```json { "traceId": "7a1c6f3a0f0d4e84", "success": true, "errorCode": null, "errorMessage": null, "alertMessage": "保存成功", "data": { "id": "1001", "name": "示例对象" } } ``` 分页响应示例: ```json { "traceId": "7a1c6f3a0f0d4e84", "success": true, "data": { "total": 52, "first": 1, "pageCount": 3, "pageSize": 20, "current": 1, "list": [ { "id": "1001", "name": "示例对象" } ] } } ``` 前端联调时建议遵守: - 先按 HTTP 状态码区分是否成功返回 - 再按 `success` 做业务成功判断 - 任何异常反馈都保留 `traceId`,便于后端排查 - `alertMessage` 作为提示文案使用,不要把它当成错误码 - 需要分支处理时优先依赖 `errorCode`,不要只匹配中文报错文案 ## 分页对象语义 标准分页对象 `QueryResults` 至少包含: - `total` 总记录数 - `first` 第一条序号 - `pageCount` 总页数 - `pageSize` 每页条数 - `current` 当前页码 - `list` 当前页数据 前端表格、分页器、导出预览应统一围绕这组字段消费,不要为不同数据栈分别写三套分页适配。 ## 标准接口能力一览 控制器根路径由业务控制器自己的类级 `@RequestMapping` 决定。 下面列的是标准后缀语义,来源于 `CoreConstant.Urls`: | 能力 | 方法 | 标准路径后缀 | 典型入参 | 典型返回 | |------|------|--------------|----------|----------| | 创建 | `POST` | `{root}` | `AO` | `ResponseResults` | | 批量创建 | `POST` | `{root}/batch` | `Collection` | `ResponseResults>` | | 更新 | `PUT` | `{root}` | `AO` | `ResponseResults` | | 详情 | `GET` | `{root}/{id}` | `id` | `ResponseResults` | | 批量详情 | `GET` | `{root}/batch?idSet=...` | `idSet` | `ResponseResults>` | | 条件查询 | `GET` | `{root}` | `CommonParameters + Query` | `ResponseResults>` | | 逻辑删除 | `DELETE` | `{root}/{id}` | `id` | `ResponseResults` | | 批量逻辑删除 | `DELETE` | `{root}?idSet=...` | `idSet` | `ResponseResults` | | 恢复 | `PUT` | `{root}/recovery/{id}` | `id` | `ResponseResults` | | 批量恢复 | `PUT` | `{root}/recovery` | `Set` | `ResponseResults` | | 启用/禁用 | `PUT` | `{root}/enable/{id}`、`{root}/disable/{id}` | `id` | `ResponseResults` | | 锁定/解锁 | `PUT` | `{root}/lock/{id}`、`{root}/unlock/{id}` | `id` | `ResponseResults` | | 发布/撤销发布 | `PUT` | `{root}/publish/{id}`、`{root}/withdraw/{id}` | `id` | `ResponseResults` | | 默认/取消默认 | `PUT` | `{root}/default/{id}`、`{root}/non-default/{id}` | `id` | `ResponseResults` | | 主要/取消主要 | `PUT` | `{root}/primary/{id}`、`{root}/non-primary/{id}` | `id` | `ResponseResults` | | 账号能力 | `PUT` | `{root}/account/passwd/reset/{id}` 等 | `id` | `ResponseResults` | 如果某个实体没有开启对应数据状态能力,前端不要默认假定这些按钮和接口一定存在。 注意:当前前端通用 data status helper 中,发布的反向动作使用了 `non-publish` 命名;而 `CoreConstant.Urls` 和 WebMvc/WebFlux `IPublishController` 使用的是 `withdraw`。在代码统一前,启用 `publish` 能力的模块必须显式确认前端调用路径与后端控制器一致,不能盲目依赖默认 helper。 ## URL 命名与迁移调整 业务 URL 应优先表达稳定资源层级,不要把多个资源语义压成一个横线单词。遇到历史 URL 调整时,先做全量扫描和影响面确认,再一次性对齐后端、前端、权限清单和测试。 统一命名规则: - 前端页面路径使用业务层级,例如 `/resource/number/serial/definition`、`/resource/number/serial/pool`、`/resource/number/serial/data`。 - 后端管理 API 与页面层级保持同一资源语义,例如 `/api/resource/number/serial/definition`。 - 后端运行时 API 仍按运行时入口分组,例如 `/api/runtime/resource/number/serial/{applicationKey}/{serialNumberKey}/preview`。 - `createConfigCrudService(url, options)` 里的 `url` 不带 `/api`,但必须和后端 Controller 根路径去掉 `API_PREFIX` 后一致。 - `permissionUrl` 中,`menu/page` 写前端访问路径,`api` 写后端接口路径;两者都要随 URL 调整同步更新。 - `resourceKey`、模块目录、组件路径不因为 URL 层级调整而机械改名,除非需求明确要求一起重命名。 调整步骤: 1. 先用全文搜索列出旧路径的所有出现位置,至少覆盖后端常量、Controller、权限清单、前端 routes/service/store/page、mock、E2E。 2. 确定目标路径后,后端优先修改模块 `Constant.Urls`,避免 Controller 中散落字面量路径。 3. 同步更新权限清单中的前端菜单 URL 和后端 API URL,确保授权后台看到的路径与真实入口一致。 4. 同步更新前端 `routes.js`、模块 `service/*.js`、运行时自定义接口和 Playwright 用例。 5. 如果运行时接口的路径参数语义变化,例如从 `ruleKey` 变为 `applicationKey + serialNumberKey`,前端页面和 service 方法签名必须一起调整,不能只替换 URL 字符串。 6. 对兼容 redirect 明确做产品决策:需要兼容时只保留列出的旧入口;要求“一刀切”时删除旧路由和旧 API,不留下临时 redirect。 7. 修改后再次搜索旧 HTTP 路径,确认没有新增残留;保留的模块目录名、import 路径或 `resourceKey` 需要能解释清楚。 8. 在脏工作区中提交时,只暂存本次 URL 调整相关 diff。混有无关改动的文件必须用 selective staging 或 `git apply --cached` 精确暂存,再用 `git diff --cached` 复核。 调整示例: ```text 旧页面:/resource/serial-number-definition/manage 新页面:/resource/number/serial/definition 旧 API:/api/resource/serial-number/definition 新 API:/api/resource/number/serial/definition 旧运行时 API:/api/runtime/resource/serial-number/{ruleKey}/preview 新运行时 API:/api/runtime/resource/number/serial/{applicationKey}/{serialNumberKey}/preview ``` ## `webmvc` 与 `webflux` 对前端必须保持一致的点 新增功能默认应同时评估 `webmvc` 与 `webflux` 两条线。 对前端来说,必须保持一致的不是“是不是 `Mono`”,而是下面这些外部语义: - 控制器根路径语义 - URL 后缀语义 - `resourceKey` 与按钮权限点 - `AO / Query / VO` 字段集合 - `ResponseResults` 外壳 - `QueryResults` 分页字段 - HTTP 状态码语义 - `errorCode` 与错误消息语义 前端不应因为后端采用 `webflux` 就另写一套接口字段解析逻辑。 对前端而言,`Mono>` 和 `ResponseResults` 的区别属于服务端实现细节,不应演变成两套 JSON 契约。 ## 按钮权限点与 `resourceKey` `IWebMvcApi` 与 `IWebFluxApi` 都要求业务控制器提供 `getResourceKey()` 和 `getResourceName()`。 `getResourceKey()` 是稳定权限资源标识,`getResourceName()` 是业务展示名。OpenAPI 输出公共接口摘要时会用 `getResourceName()` 替换 `资源` 占位,例如 `创建资源` 输出为 `创建系统身份`。 业务控制器应优先从模块常量读取这两个值:`XxxConstant.Resource.KEY_*` / `XxxConstant.Resource.NAME_*`。 最终权限点通过: `resourceKey + "." + action` 拼出来。 `permissionKey` 统一使用点号分隔,禁止使用 `:`。推荐格式为 `{moduleKey}.{resource}.{action}`,例如 `platform.area.create`。 权限清单中需要同时区分目录、前端导航、后端接口和页面按钮: - `catalog` 表达目录或分组,不对应具体前端组件或后端接口,也不配置 `permissionUrl` / `component`。 - `menu` 表达独立前端菜单入口,`permissionUrl` 填前端访问路径,`component` 填真实前端组件路径,`icon` 只给可见菜单节点使用。 - `page` 表达隐藏/从属页面,不独立出现在菜单中,例如详情、编辑、配置、主从资源子列表页;这类页面通常依赖主资源 ID,后端会出现 `ISearchOfMasterIdController` / `search-of-master-id` 能力。 - `api` 的 `permissionUrl` 填后端接口路径,用于表达查询、详情、字典、回收站查询、`search-of-master-id` 等接口能力。 - `button` 只表达页面上的可见操作按钮,不填 `permissionUrl/component/icon`。 - 不要把多个独立菜单入口错误包在一个 `menu` 下再生成为 `page`。需要中间分组时使用 `catalog`,例如 `安全中心 catalog -> 系统身份管理 catalog -> 系统身份 menu / 系统身份分组 menu`。 - `orderWeight` 按权限树 `DESC` 规则设计,权重越大越靠前;推荐顺序是辅助查询 API、列表查询、CRUD、状态操作、回收站操作。 - `permissionDescription` 面向后台授权人员阅读,必须写业务语义,不要把 `idSet`、`ID 集合`、`query`、`commonParameters` 等后端参数名写进说明。 常见 `action` 取值来自 `CoreConstant.ResourceKey`,例如: - `create` - `batch-create` - `update` - `logic-delete` - `batch-logic-delete` - `find-by-id` - `batch-find-by-id` - `recovery` - `enable` - `disable` - `lock` - `unlock` - `publish` - `default` - `primary` - `account-passwd-reset` 因此需求说明里必须提前写清: - 控制器根路径 - `resourceKey` - 页面按钮和权限点映射 - 路由 `meta.resourceKey` 或 `meta.resourceKeyBuilder` 否则前端菜单、按钮显隐和后端授权很容易对不上。 当前前端权限判断还会做少量历史动作别名兼容: - `save` 可以映射到 `create` - `details` 可以映射到 `details` 或 `find-by-id` - `recycle` 可以映射到 `recovery` - `physics-delete` 可以映射到 `physics-delete` 或 `logic-delete` 这些别名只用于兼容前端权限判断,不应作为新后端权限命名的依据。新接口仍应优先使用 `CoreConstant.ResourceKey` 中的标准 action。 ## 导入导出与 Excel 约定 `AO / VO` 不只是表单和展示对象,也要预留后续导入导出能力。 当前约定是: - 需要导入的对象优先落在 `AO` - 需要导出的对象优先落在 `VO` - 只要业务已经明确存在导入导出场景,就应在 `AO / VO` 上直接考虑 `@ExcelProperty` - 即使当前阶段暂未接入 Excel,也要先把字段中文名、列含义、顺序、是否必填写进需求,避免后续返工 需求里至少要先写清: - 哪些字段参与导入 - 哪些字段参与导出 - 列头中文名 - 是否需要模板下载 - 是否允许部分字段只导出不导入 ## 写需求时前端信息至少补齐这些 参考 [module-requirements-template.md](/docs/module-requirements-template/),前端相关内容至少要补齐: - 控制器根路径 - `resourceKey` - 页面路由或页面入口 - 列表字段 - 查询字段 - 详情字段 - 创建字段 - 编辑字段 - 默认排序 - 按钮权限点 - 是否需要回收站 - 是否需要状态切换 - 是否需要账号能力 - 是否需要导入导出 - 导入导出列头与字段映射 - 是否要求 `webmvc` 与 `webflux` 两条线都开放同等能力 - 前端模块路径,例如 `ineed-frontend/src/modules/business/customer` - service/store/page/route/schema 是否都需要新增或修改 - 是否复用 `createConfigCrudService` / `createConfigCrudStore` - `dataStatusCapabilities` 的明确取值 - 路由名称、路由 path、菜单标题和页面级 `resourceKey` - 表格列、查询项、创建表单、编辑表单、详情面板是否字段一致 - 自定义接口的 URL、方法、入参、返回 `data` 结构和错误码 - Playwright E2E 是否需要新增 fixture、登录凭据依赖和跳过条件 ## 前端代码生成的最小输入 如果用代码生成器或自动化工具处理前端联调、页面字段整理任务,输入至少要提供: ```text 后端 Controller 根路径: 后端 resourceKey: 前端模块目录: 路由 path/name/meta.resourceKey: 列表列: 查询字段: 创建 AO 字段: 编辑 AO 字段: 详情 VO 字段: 默认排序: dataStatusCapabilities: 自定义接口: 需要新增或更新的 service/store/page/schema/routes: 需要新增或更新的 Playwright 用例: ``` 实现前必须先核对后端 `AO / Query / VO / Controller` 与前端字段表是否一致;字段不一致时先报告缺口,不要自行猜字段。 ## 联调检查清单 - 前端是否只依赖 `AO / Query / VO / ResponseResults / QueryResults` - 列表分页是否统一消费 `current / pageSize / total / list` - 查询接口是否通过 `CommonParameters + Query` 传参,而不是自造分页对象 - 是否保留并展示 `traceId` - 是否按 `errorCode` 做业务分支,而不是硬编码中文文案 - 页面按钮权限点是否与 `resourceKey.action` 一致 - 页面路由、后端 API、权限清单、service URL 和 E2E 是否使用同一套资源路径 - URL 迁移后是否已搜索旧路径,且兼容 redirect 是否符合本次决策 - 如果有导入导出,`AO / VO` 字段和列头是否已经在需求里写清 - 如果本次功能是公共能力,`webmvc` 与 `webflux` 是否都已对齐 > 最后更新:2026-06-01 --- # 开发模式登录与权限诊断 来源:docs/project-development/development-login-guide.md SHA-256:9a6db2ffbd195f280627c9f8b926a7b3cc133208819615edf23568206a88b32e 网站:/docs/development-login-guide/ 本文用于本地开发联调。开发模式仍然必须完成登录认证,它只改变方法级权限配置,不是免认证模式。 ## 1. 开启开发模式 `application.is-development` 默认值为 `false`。仅在受控的本地配置中显式设置: ```yaml application: is-development: true ``` 重启应用后再执行后续步骤。开发认证服务只在 `application.is-development=true` 时注册。 ## 2. 准备本地输入 从团队批准的本地配置或 secret store 取得联调值,在当前 shell 设置以下变量。不要把值写进源码、文档、命令历史或 evidence: ```bash : "${INEED_DEV_BASE_URL:?set from local environment}" : "${INEED_DEV_USER_TYPE:?set from approved local configuration}" : "${INEED_DEV_USER_ACCOUNT:?set from secret store}" : "${INEED_DEV_PASSWORD:?set from secret store}" ``` `INEED_DEV_BASE_URL` 不包含末尾 `/`。`INEED_DEV_TOKEN` 将从登录响应在本地生成,不应预先设置或持久化。 ## 3. 登录 请求体字段由 `LoginAO` 定义为 `identityType`、`account` 和 `password`: ```bash LOGIN_RESPONSE_FILE="$(mktemp)" chmod 600 "${LOGIN_RESPONSE_FILE}" curl --fail-with-body --silent --show-error \ --request POST \ --header 'Content-Type: application/json' \ --data "$(jq -n \ --arg identityType "${INEED_DEV_USER_TYPE}" \ --arg account "${INEED_DEV_USER_ACCOUNT}" \ --arg password "${INEED_DEV_PASSWORD}" \ '{"identityType": $identityType, "account": $account, "password": $password}')" \ "${INEED_DEV_BASE_URL}/api/security/authentication/login" \ >"${LOGIN_RESPONSE_FILE}" INEED_DEV_TOKEN="$(jq -er '.data.authenticationToken.token' "${LOGIN_RESPONSE_FILE}")" rm -f "${LOGIN_RESPONSE_FILE}" export INEED_DEV_TOKEN ``` 不要打印、复制到工单或提交 `INEED_DEV_TOKEN`。命令失败时先删除临时响应文件,再检查本地配置。 ## 4. 诊断身份与权限 以下请求仍经过正常的 URL 安全过滤器、令牌和会话校验: ```bash curl --fail-with-body --silent --show-error \ --header "Authorization: Bearer ${INEED_DEV_TOKEN}" \ "${INEED_DEV_BASE_URL}/api/security/authentication/who" curl --fail-with-body --silent --show-error \ --header "Authorization: Bearer ${INEED_DEV_TOKEN}" \ "${INEED_DEV_BASE_URL}/api/security/authentication/permission" curl --fail-with-body --silent --show-error \ --request PUT \ --header "Authorization: Bearer ${INEED_DEV_TOKEN}" \ "${INEED_DEV_BASE_URL}/api/security/authentication/permission/refresh" ``` ## 5. 安全边界 - 开发模式仍然必须完成登录认证。 - `application.is-development=true` 仅关闭 Method Security,即方法上的权限注解不参与拦截。 - URL 安全过滤器仍然生效,不能把未授权路径视为公开路径。 - 会话校验仍然生效,令牌失效或会话被移除后请求应失败。 - 租户边界仍然生效,不能跨租户读取或修改数据。 - 开发模式不会关闭其他 WebMvc/WebFlux 安全过滤、密码处理或审计边界。 ## 6. 恢复正常模式 1. 清理当前 shell 中的凭据和令牌: ```bash unset INEED_DEV_USER_TYPE INEED_DEV_USER_ACCOUNT INEED_DEV_PASSWORD INEED_DEV_TOKEN ``` 2. 将本地配置恢复为 `application.is-development=false`,或删除该配置以使用默认值。 3. 重启应用。 4. 确认 development service 不再注册。 5. 使用正常身份确认方法级权限检查恢复,并确认未授权请求被拒绝。 --- # JDBC 业务代码指南 来源:docs/guides/jdbc.md SHA-256:78fd061d7d7d8ca8695bcbe6eaa9623b2e8b6cf4367b73bf676ea7aa69956d23 网站:/docs/jdbc/ 与 [JPA](/docs/jpa-querydsl-code-guide/)、[MyBatis-Flex](/docs/mybatis-flex-code-guide/)、[R2DBC](/docs/r2dbc-code-guide/)共同组成持久化开发入口。 ## Repository 业务 Repository 使用接口组合 Core 的 CRUD/状态等 fragment,声明 `GetEntityClassOperation` 并提供 `getEntityClass()`。只组合对象实际需要的能力;不要生成空 `RepositoryImpl` 或转发壳。 简单外键 `count/exists/delete/findBy` 定义在具体仓储。派生方法表达字段条件并不自动代表所有租户、删除、权限条件已具备;按实际调用核对隔离。 完整参考:[CategoryDefinitionRepository](/references/#source-b99c593064779200)。 ## Entity、Converter、Manager Entity 使用当前 JDBC 基类和 Spring Data 映射,声明业务所需租户/状态能力。Converter 在 `converter/`,仅覆盖有特殊映射的方法。 Manager 组合具体能力,提供仓储 operation、converter 和查询表路径。业务规则放生命周期或领域方法,事务编排仍由 Service 承担。 查询使用本模块手写 `QXxx extends QuerydslSqlTable`;Manager 覆盖 `getSqlTablePath()` 并引用 Q 类型字段。不要将 JPA 自动生成 Q 类或 MyBatis-Flex TableDef 当作 JDBC 的实现。 源码参考:[QCategoryDefinition](/references/#source-2af0409f5f3fc09a)、[CategoryDefinitionManager](/references/#source-10e4c0b42961473b)。 ## 查询和关系 在 `appendWhereOfQuery` 等真实扩展点补业务过滤;关键字搜索通过既有入口声明字段。查询投影与 VO 对齐,检查排序、分页、空条件和 join 的租户/删除过滤。 关系查询由 Manager 组织查询并返回需要的 VO;Service 不先查关系 ID 再逐个回查对象。物理删除子表数据通过对应 Repository 方法封装,钩子时序见[删除规范](/docs/delete-hook-and-logging-guide/)。 ## 验证 先运行 JDBC 框架契约入口,再验证真实目标数据库中的列映射、唯一约束、派生查询、join 和事务回滚。Core 的记录型或内存型仓储测试不能代替真实数据库。 在 Core 根目录运行框架链路: ```bash mvn -pl ineed-core-tests/ineed-core-test-webmvc -am \ -Dtest=WebMvcJdbcBusinessChainTest,WebMvcJdbcSearchBusinessChainTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` 命令的环境前提与证据分层见[测试规范](/docs/testing/)。业务数据库测试入口随具体模块维护,不使用教程命令重建已有业务库。 --- # JPA QueryDSL 代码编写指南 来源:docs/project-development/jpa-querydsl-code-guide.md SHA-256:9e8ee6fe6a28c7cc92c7140e3ed22a9424f23254d5db3d39465ddeb77f9df0af 网站:/docs/jpa-querydsl-code-guide/ 本文约定业务模块中 `*-jpa` 子模块的标准 QueryDSL 写法。目标是让 JPA 自定义查询继续使用 QueryDSL 生成的 `Q*` 元数据,同时把实体字段投影、关联字段扩展和查询条件边界收口到稳定位置,避免业务 `Manager` 中散落重复字段清单。 ## 核心原则 - JPA 自定义查询优先使用 Spring Data 派生查询或 QueryDSL,不写 native SQL。 - `buildVOQBean()` 中实体自身字段必须通过 `QuerydslUtil.projectionFields(entityClass, entityPath)` 或 `QuerydslUtil.projectionBean(entityClass, entityPath)` 提取。 - 只有 join 出来的扩展字段才在业务 `Manager` 中显式追加,例如 `group.groupKey.as("groupKey")`。 - 不要在 `buildVOQBean()` 中手写完整的 `table.id`、`table.tenantKey`、`table.createdTimestamp`、业务字段等长列表;这类列表应该由 core 按实体字段统一提取。 - `IBaseSearch#buildVOQBean()` 默认仍返回 `null`,不强制所有 JPA 查询走 QBean 投影;只有需要 join 填充额外 VO 字段时才覆盖。 ## 实体字段投影 当查询只需要实体自身字段时,优先直接使用 core 工具: ```java @Override public QBean buildVOQBean() { return QuerydslUtil.projectionBean(SampleEntity.class, QSampleEntity.sampleEntity); } ``` 当查询还需要 join 字段时,实体字段仍由 core 提取,业务代码只追加额外字段: ```java @Override public QBean buildVOQBean() { final QParameter table = QParameter.parameter; final QParameterGroup group = QParameterGroup.parameterGroup; final List> fields = new ArrayList<>(List.of(QuerydslUtil.projectionFields(Parameter.class, table))); fields.add(group.applicationKey.as("applicationKey")); fields.add(group.groupKey.as("groupKey")); fields.add(group.groupName.as("groupName")); //@formatter:off return Projections.fields( Parameter.class, fields.toArray(new Expression[0])); //@formatter:on } ``` 这种写法有两个好处: - 实体新增、删除字段时,投影字段自动跟随实体持久化字段变化。 - join 字段一眼可见,代码审查时能快速识别哪些字段来自关联表。 ## Join 查询 `buildQuery(...)` 只负责表达关联关系,不再顺带维护实体字段投影清单: ```java @Override public JPAQuery buildQuery(final JPAQuery query) { final QParameter table = QParameter.parameter; final QParameterGroup group = QParameterGroup.parameterGroup; return query.leftJoin(group).on(group.id.eq(table.groupId)); } ``` 如果 join 字段要映射到 VO / 查询返回对象,必须在 `buildVOQBean()` 中使用 `as("propertyName")` 明确别名,别名应对应目标对象属性名。 ## 禁止写法 不要在 `buildVOQBean()` 中维护实体字段全量列表: ```java return Projections.fields( Parameter.class, table.id, table.tenantKey, table.orderWeight, table.dataStatus, table.createdTimestamp, table.groupId, table.parameterKey, table.parameterName, table.parameterDescription, group.groupKey.as("groupKey")); ``` 这个写法的问题是实体字段来源和 join 字段混在一起。实体字段变化时容易漏改,多个 Manager 也会产生重复字段列表。 ## 验证 改造或新增 JPA QueryDSL 自定义查询后,至少执行对应模块编译: ```bash mvn -pl /-jpa -am install -DskipTests ``` 同时建议扫描业务 JPA Manager 是否仍残留全量实体字段手写投影: ```bash rg -n "buildVOQBean|Projections\\.fields|table\\.id|table\\.tenantKey|table\\.createdTimestamp" -g '*Manager.java' ``` 允许保留少量业务 join 字段追加,但实体自身字段应优先来自 `QuerydslUtil.projectionFields(...)`。 --- # MyBatis-Flex 代码编写指南 来源:docs/project-development/mybatis-flex-code-guide.md SHA-256:9dcab4122fe4a1dec3b57f01211366a8d19487beaeb7d2db386cd4fc3b55f783 网站:/docs/mybatis-flex-code-guide/ 本文约定业务模块中 `*-mybatis-flex` 子模块的标准写法。目标是让 MyBatis-Flex 和 JPA / QueryDSL 一样,使用编译期生成的结构化元数据表达表、列、关联和排序,避免业务代码散落表名、列名字符串。 ## 基本原则 - `mybatis-flex-processor` 生成的 `entity.table.*TableDef` 是 MyBatis-Flex 查询元数据的唯一业务来源。 - 业务 `Mapper` 优先组合 core 标准能力接口,例如 `CreateMapper`、`UpdateMapper`、`FindByIdMapper`;这些能力接口已经统一继承 `MybatisFlexOperation`。 - 业务 `Mapper` 必须实现 `getTableDef()`;没有任何标准能力接口可继承的特殊 mapper,才直接继承 `MybatisFlexOperation`。 - 业务 `Manager` 只提供 `getOperation()` 作为 MyBatis-Flex 持久化入口,不再提供 `getMapper()`。 - 业务 `Manager` 不实现 `getEntityClass()`;core 默认通过 `getOperation().getEntityClass()` 获取实体类型,`Mapper` 再由 `getTableDef()` 解析实体类型。 - 查询、删除、计数、join、orderBy 优先使用 `QueryWrapper` + generated `*TableDef`。 - 关系表、子表、从表的 `findBy...` / `countBy...` / `deleteBy...` 放在具体 `Mapper`,`Manager` 只做业务编排。 - 业务代码不要使用 `QueryMethods.column("...")`、表名字符串或列名字符串拼查询。 - 如果生成类缺失,先修 `pom.xml` 的 processor 配置,不退回手写字符串 SQL。 ## Processor 配置 每个 `*-mybatis-flex` 模块必须独立配置 `mybatis-flex-processor`,类似 JPA 模块接入 QueryDSL APT。 ```xml org.apache.maven.plugins maven-compiler-plugin com.mybatis-flex mybatis-flex-processor ${mybatis-flex.version} ``` 生成类路径应为: ```text {basePackage}.mybatisflex.entity.table.{Entity}TableDef ``` 业务代码直接 import 生成类,例如: ```java import com.ineed.config.parameter.mybatisflex.entity.table.ParameterTableDef; ``` ## Mapper 写法 业务 `Mapper` 负责持久化操作入口和明确的表级查询方法。标准能力接口已经继承 `MybatisFlexOperation`,所以业务 mapper 通常只组合需要的能力接口并实现 `getTableDef()`: ```java @Mapper public interface ParameterMapper extends //@formatter:off CreateMapper, UpdateMapper, PhysicsDeleteMapper, LastModifiedMapper, DataStatusMapper, FindByIdMapper, LogicDeleteMapper { //@formatter:on @Override default ParameterTableDef getTableDef() { return ParameterTableDef.PARAMETER; } } ``` 如果某个 mapper 不需要任何标准能力接口,但仍需要作为 MyBatis-Flex 持久化入口使用,可以直接继承 `MybatisFlexOperation`: ```java @Mapper public interface ParameterSnapshotMapper extends MybatisFlexOperation { @Override default ParameterSnapshotTableDef getTableDef() { return ParameterSnapshotTableDef.PARAMETER_SNAPSHOT; } } ``` 不要在业务 `Mapper` 中再写: ```java default Class getEntityClass() { return Parameter.class; } ``` `MybatisFlexOperation` 通过 `GetTableDefOperation` 承接实体类型解析,`getTableDef()` 默认解析实体类型。如果实体包名和生成类包名不符合 `entity` / `entity.table` 约定,应先修包结构或生成配置,不在业务代码里补第二套实体类型声明。 ## Mapper 查询方法 子表、关系表、从表的直接外键访问放在具体 `Mapper`: ```java default long countByGroupIdAndDeleted(final String groupId, final Byte deleted) { final QueryWrapper queryWrapper = QueryWrapper.create(); queryWrapper.where(getTableDef().GROUP_ID.eq(groupId)); queryWrapper.and(getTableDef().DELETED.eq(deleted)); final Long count = selectCountByQuery(queryWrapper); return count == null ? 0L : count; } default void deleteByGroupId(final String groupId) { final QueryWrapper queryWrapper = QueryWrapper.create(); queryWrapper.where(getTableDef().GROUP_ID.eq(groupId)); deleteByQuery(queryWrapper); } ``` 不要把这类逻辑写成 `Manager` 中的通用 helper: ```java // 禁止 countActiveValue(BaseMapper mapper, String columnName, String columnValue) deleteValue(BaseMapper mapper, String columnName, String columnValue) ``` 也不要写: ```java // 禁止 QueryMethods.column("group_id").eq(groupId) ``` 正确写法是: ```java getTableDef().GROUP_ID.eq(groupId) ``` ## Manager 写法 业务 `Manager` 组合 core 默认能力,只提供 `getOperation()` / converter 入口,不再提供 `getMapper()`,也不提供 `getEntityClass()`。需要实体类型时走 core 默认实现:`getOperation().getEntityClass()`。 ```java @Component @RequiredArgsConstructor @Slf4j public class ParameterManager extends AbstractMybatisFlexManager implements //@formatter:off IParameterManager, CreateOrRefreshManager, EnableManager, LockManager, PublishManager, FindByIdManager, LogicDeleteManager, PhysicsDeleteManager, SearchManager, SearchRecycleManager { //@formatter:on private static final ParameterTableDef PARAMETER = ParameterTableDef.PARAMETER; private final ParameterConverter parameterConverter; private final ParameterMapper parameterMapper; @Override public ParameterMapper getOperation() { return parameterMapper; } @Override public AoEntityConverter getCreateEntityConverter() { return parameterConverter; } } ``` `getOperation()` 返回当前 mapper,是 Manager 到 MyBatis-Flex 的唯一持久化入口。core 的 search、find、delete、data status 等默认能力都通过这个入口调用 mapper 方法、读取 `getTableDef()` 和 `getEntityClass()`。不要为了“语义清晰”再在业务 Manager 中补一个 `getMapper()` 委托方法,否则同一对象会出现两套入口,后续很容易出现返回对象不一致的问题。 `Manager` 中可以声明当前查询需要的 `TableDef` 常量。需要 join 时使用 alias: ```java private static final ParameterTableDef PARAMETER_TABLE = ParameterTableDef.PARAMETER.as("p"); private static final ParameterGroupTableDef PARAMETER_GROUP_TABLE = ParameterGroupTableDef.PARAMETER_GROUP.as("g"); ``` ## Search 写法 `getKeySearchColumns()` 返回 `QueryColumn[]`,必须直接返回 generated 列: ```java @Override public QueryColumn[] getKeySearchColumns() { //@formatter:off return new QueryColumn[] { PARAMETER.PARAMETER_KEY, PARAMETER.PARAMETER_NAME }; //@formatter:on } ``` 不要返回字符串: ```java // 禁止 return new QueryColumn[] { "parameter_key", "parameter_name" }; ``` `appendWhereOfQuery(...)` 使用 generated 列: ```java @Override public void appendWhereOfQuery(final QueryWrapper wb, final ParameterQuery query) { if (query == null) { return; } if (StringUtils.isNotBlank(query.getGroupId())) { wb.and(PARAMETER.GROUP_ID.eq(query.getGroupId())); } if (StringUtils.isNotBlank(query.getParameterName())) { wb.and(PARAMETER.PARAMETER_NAME.like("%" + query.getParameterName() + "%")); } } ``` 如果 `query` 中有字段,但当前实体表没有对应列,不能为了编译通过回退到 `QueryMethods.column("...")`。应先判断: - 字段是否是契约遗留,应从当前 MyBatis-Flex 查询中移除。 - 字段是否本应存在于实体,应先补实体 `@Column` 和数据库字段,再由 processor 生成列。 - 字段是否来自关联表,应通过 join 引入对应表的 generated `TableDef`。 ## Join 写法 join 必须使用 generated `TableDef` 和 alias,不写表名字符串: ```java //@formatter:off final QueryWrapper queryWrapper = QueryWrapper.create() .select(PARAMETER_TABLE.DEFAULT_COLUMNS) .select( PARAMETER_GROUP_TABLE.GROUP_KEY.as("groupKey"), PARAMETER_GROUP_TABLE.GROUP_NAME.as("groupName")) .from(PARAMETER_TABLE) .leftJoin(PARAMETER_GROUP_TABLE) .on(PARAMETER_GROUP_TABLE.ID.eq(PARAMETER_TABLE.GROUP_ID)) .where(PARAMETER_TABLE.DELETED.eq(CoreYesNo.NO.getByte())); //@formatter:on ``` 跨表删除前检查也要指向被检查表的 `TableDef`: ```java //@formatter:off final long count = dataScopeRuleMapper.selectCountByQuery(QueryWrapper.create() .from(DATA_SCOPE_RULE_VALUE) .where(DATA_SCOPE_RULE_VALUE.RULE_ID.eq(entity.getId())) .and(DATA_SCOPE_RULE_VALUE.DELETED.eq(CoreYesNo.NO.getByte()))); //@formatter:on ``` 不要写: ```java // 禁止 .from("isec_data_scope_rule_value") .where(QueryMethods.column("rule_id").eq(entity.getId())) ``` ## 字段命名 generated `TableDef` 字段名来自实体属性,而不一定等同于数据库列名的直译。 例如实体字段: ```java @Column("group_id") protected String workGroupId; ``` 生成列可能是: ```java WORK_GROUP_ID ``` 业务代码必须使用 generated 字段: ```java WORK_GROUP_USER.WORK_GROUP_ID.eq(workGroupId) ``` 不要凭数据库列名猜成: ```java // 禁止 WORK_GROUP_USER.GROUP_ID.eq(workGroupId) ``` 遇到编译错误时,以 `target/generated-sources/annotations/.../*TableDef.java` 为准。 ## QueryMethods 使用边界 业务模块禁止使用 `QueryMethods.column("...")` 生成列。以下场景例外: - core 内部工具类为了兼容通用字段名、函数表达式或默认排序做集中转换。 - MyBatis-Flex 没有 `QueryWrapper` 等价 API 的函数表达式,例如 `QueryMethods.distinct(tableDef.FIELD)`,前提是参数列来自 generated `TableDef`。 业务代码中如果只是 where / join / orderBy / select 普通列,必须使用 generated `TableDef`。 ## 删除和关系维护 物理删除前后的关系处理保持分层: - `Manager` 的 `beforePhysicsDelete` / `afterPhysicsDelete` 只编排业务动作。 - 关系表删除、子表计数、外键查询落到具体 `Mapper`。 - 查询条件使用关系表自己的 `TableDef`,不要拿主表 `TableDef` 查子表字段。 示例: ```java @Override public void beforePhysicsDelete(final Parameter beDelete) { parameterValueMapper.deleteByParameterId(beDelete.getId()); } ``` 对应 mapper: ```java default void deleteByParameterId(final String parameterId) { final QueryWrapper queryWrapper = QueryWrapper.create(); queryWrapper.where(getTableDef().PARAMETER_ID.eq(parameterId)); deleteByQuery(queryWrapper); } ``` ## 验证命令 改造或新增 MyBatis-Flex 模块后,至少执行对应模块编译: ```bash mvn -pl :{module}-mybatis-flex -am -DskipTests compile ``` 批量改造后应扫描业务 MyBatis-Flex 源码: ```bash find . -path '*/target/*' -prune -o \ -path '*mybatis*flex*/src/main/java/*' -type f -name '*.java' -print0 \ | xargs -0 rg "QueryMethods\\.column\\(|GetEntityClassOperation|default Class<[^>]+> getEntityClass\\(|public Class<[^>]+> getEntityClass\\(" ``` 同时扫描 MyBatis-Flex Manager 是否残留 `getMapper()`: ```bash find . -path '*/target/*' -prune -o \ -path '*mybatis*flex*/src/main/java/*/manager/*.java' -type f -name '*.java' -print0 \ | xargs -0 rg "public .* getMapper\\(" ``` 预期结果: - 业务 MyBatis-Flex 模块没有 `QueryMethods.column(...)`。 - 业务 MyBatis-Flex 模块没有直接继承 `GetEntityClassOperation`。 - 业务 mapper / manager 没有手写 `getEntityClass()`。 - 业务 Manager 没有 `getMapper()`,MyBatis-Flex 持久化入口统一为 `getOperation()`。 - `GetEntityClassOperation` 只由 core 的 `GetTableDefOperation` / `MybatisFlexOperation` 间接承接。 ## 常见错误 | 错误 | 正确处理 | |------|----------| | 找不到 `*TableDef` | 检查 `mybatis-flex-processor` 是否配置,先编译生成 | | `TableDef` 没有某列 | 以实体字段和生成类为准;不要手写字符串列绕过 | | `Mapper` 不知道继承什么 | 优先继承 core 标准 mapper 能力接口;没有标准能力时再直接继承 `MybatisFlexOperation` | | `Manager` 需要实体类型 | 不实现 `getEntityClass()`;通过 `getOperation().getEntityClass()` 走 core 默认实现 | | `Manager` 想拿 mapper | 直接使用 `getOperation()`;不要新增 `getMapper()` 委托 | | 子表计数写在 `Manager` | 下沉到子表 `Mapper` 的明确方法 | | join 中写 `.from("table")` | 改为 `.from(TABLE_DEF)` 或 `.from(TABLE_DEF.as("alias"))` | | key search 返回字符串 | 改为 `QueryColumn[]` generated 列 | | generated 字段名与列名不一致 | 使用 generated 字段名,不按数据库列名猜测 | > 最后更新:2026-06-02 --- # R2DBC 代码编写指南 来源:docs/project-development/r2dbc-code-guide.md SHA-256:6297136f18291a99865e1234437a06dc59129b67946d62a41570195150c3a973 网站:/docs/r2dbc-code-guide/ 本文约定业务模块中 `*-r2dbc` 子模块的标准写法。R2DBC 当前不引入额外查询 DSL,继续使用 core 的集中 SQL 支撑,但实体类型、表名、默认别名、基础过滤和排序必须由 core 统一收口,避免业务 `Manager` 中散落第二套规则。 ## 基本原则 - 业务 `Repository` 是 R2DBC operation,必须组合 core repository fragment,并继承 `GetEntityClassOperation`。 - 业务 `Repository` 保留 `default getEntityClass()`;业务 `Manager` 不再实现 `getEntityClass()`。 - 业务 `Manager` 只通过 `getOperation()` 返回 repository;core 默认通过 `getOperation().getEntityClass()` 获取实体类型。 - 查询列、过滤列、排序列必须通过 `getQualifiedColumn("column_name")` 生成;不要在 `appendWhereOfQuery(...)` 或 `getKeySearchColumns()` 中直接写裸列名。 - 不需要 join 或 VO 扩展字段时,不覆盖 `buildFrom()`、`buildSelect()`、`getDefaultTableAlias()`。 - 需要 join 时,只覆盖必要方法:`getDefaultTableAlias()`、`buildSelect()`、`buildJoin()`;主表列继续通过 `getQualifiedColumn(...)` 引用。 - 子表、关系表、从表的 `findBy...` / `countBy...` / `deleteBy...` 放在具体 `Repository`,优先使用 Spring Data R2DBC 派生方法。 - 自定义 `RepositoryImpl` 只在 fragment 或派生方法覆盖不了时新增,并且实体类型、表名、row mapper 解析必须延迟到订阅期。 ## Repository 写法 业务 `Repository` 负责暴露持久化能力和实体类型: ```java public interface ParameterValueRepository extends //@formatter:off ReactiveCrudRepository, CreateRepository, UpdateRepository, FindByIdRepository, LogicDeleteRepository, DataStatusRepository, GetEntityClassOperation { //@formatter:on @Override default Class getEntityClass() { return ParameterValue.class; } Mono countByParameterIdAndDeleted(String parameterId, Byte deleted); Mono deleteByParameterId(String parameterId); } ``` 不要为普通派生方法生成 `RepositoryImpl`。只有需要手写复杂 SQL、框架派生方法无法表达,或者 core fragment 无法覆盖时,才新增具体实现类。 ## Manager 写法 业务 `Manager` 组合 core 默认能力,只提供 `getOperation()` / converter / 生命周期钩子 / 查询条件: ```java @Component @RequiredArgsConstructor @Slf4j public class ParameterValueManager extends AbstractR2dbcManager implements //@formatter:off CreateManager, UpdateManager, FindByIdManager, SearchManager { //@formatter:on private final ParameterValueConverter parameterValueConverter; private final ParameterValueRepository parameterValueRepository; @Override public ParameterValueRepository getOperation() { return parameterValueRepository; } @Override public EntityVoConverter getEntityVoConverter() { return parameterValueConverter; } } ``` 不要在业务 `Manager` 中再写: ```java @Override public Class getEntityClass() { return ParameterValue.class; } ``` R2DBC search、find、data status 等 core 默认能力都从 `getOperation().getEntityClass()` 读取实体类型。实体类型在 `Repository` 和 `Manager` 同时声明,会形成两套来源,后续很容易出现不一致。 ## Search 写法 `getKeySearchColumns()` 返回 SQL 列表达式,必须使用 `getQualifiedColumn(...)`: ```java @Override public String[] getKeySearchColumns() { //@formatter:off return new String[] { getQualifiedColumn("parameter_value") }; //@formatter:on } ``` 不要直接返回裸列名: ```java // 禁止 return new String[] { "parameter_value" }; ``` `appendWhereOfQuery(...)` 同样使用 `getQualifiedColumn(...)`: ```java @Override public void appendWhereOfQuery(final List conditions, final Map params, final ParameterValueQuery query) { if (query == null) { return; } if (StringUtils.isNotBlank(query.getParameterId())) { conditions.add(getQualifiedColumn("parameter_id") + " = :parameterId"); params.put("parameterId", query.getParameterId()); } } ``` 不要写: ```java // 禁止 conditions.add("parameter_id = :parameterId"); ``` core 默认的租户、经营场所、归属、逻辑删除、数据状态、排序条件都会通过 `getQualifiedColumn(...)` 走同一套列限定逻辑。业务查询条件如果直接写裸列名,在主表设置别名或引入 join 后很容易出现歧义。 ## Join 写法 只有查询结果确实需要关联表字段,或者过滤条件必须跨表时,才覆盖 join 相关方法: ```java @Override public String buildSelect() { return getQualifiedColumn("*") + ", g.application_key AS application_key" + ", g.group_key AS group_key" + ", g.group_name AS group_name"; } @Override public String buildJoin() { return "LEFT JOIN icfg_parameter_group g ON g.id = " + getQualifiedColumn("group_id"); } @Override public String getDefaultTableAlias() { return "p"; } ``` 不需要 join 时不要为了统一格式强行覆盖: ```java // 禁止 @Override public String buildFrom() { return "table_name p"; } ``` 默认 `buildFrom()` 已经会根据 `getDefaultTableAlias()` 生成 `table alias`。如果没有默认别名,默认查询就是实体表名。 ## RepositoryImpl 写法 自定义 R2DBC repository 实现必须保持响应式订阅期语义: ```java return getTableNameMono().flatMap(tableName -> { final String sql = "SELECT * FROM " + tableName + " WHERE id = :id"; return databaseClient.sql(sql) .bind("id", id) .map(getEntityRowMapper()) .one(); }); ``` 不要在 Publisher 装配期提前求值: ```java // 禁止 final Class entityClass = getEntityClass(); return Mono.just(getTableName()); ``` 这类写法会把“不支持该操作”从订阅期提前成同步异常,破坏 reactive 调用契约。 ## 验证命令 改造或新增 R2DBC 模块后,至少执行对应模块编译: ```bash mvn -pl :{module}-r2dbc -am -DskipTests compile ``` 批量改造后扫描业务 R2DBC Manager: ```bash find . -path '*/target/*' -prune -o \ -path '*-r2dbc/src/main/java/*/manager/*.java' -type f -name '*.java' -print0 \ | xargs -0 rg "public Class<[^>]+> getEntityClass\\(|conditions\\.add\\(\"[a-zA-Z_][a-zA-Z0-9_]* (?:=|LIKE|IN|BETWEEN|IS)|\"[a-zA-Z_][a-zA-Z0-9_]*\"" ``` 预期结果: - 业务 R2DBC `Manager` 没有手写 `getEntityClass()`。 - `appendWhereOfQuery(...)` 中普通主表列条件通过 `getQualifiedColumn(...)` 构建。 - `getKeySearchColumns()` 返回 `getQualifiedColumn(...)` 结果。 - 没有不必要的 `buildFrom()` / `buildSelect()` / `getDefaultTableAlias()` 覆盖。 - 业务 R2DBC `Repository` 保留 `GetEntityClassOperation` 和 `getEntityClass()`。 ## 常见错误 | 错误 | 正确处理 | |------|----------| | `Manager` 需要实体类型 | 不实现 `getEntityClass()`;通过 `getOperation().getEntityClass()` 走 core 默认实现 | | `getKeySearchColumns()` 返回裸列名 | 改为 `getQualifiedColumn("column_name")` | | `appendWhereOfQuery(...)` 直接写 `"column = :param"` | 改为 `getQualifiedColumn("column") + " = :param"` | | 为普通查询覆盖 `buildFrom()` | 删除覆盖,使用 core 默认 `buildFrom()` | | 为普通查询设置别名 | 不需要别名时删除 `getDefaultTableAlias()` | | 需要 join 后主表列歧义 | 保留 `getDefaultTableAlias()`,主表列统一走 `getQualifiedColumn(...)` | | 简单子表 count/delete 写在 `Manager` | 下沉到对应 `Repository` 派生方法 | | 自定义 `RepositoryImpl` 装配期求值 | 改为 `Mono.defer(...)`、`getEntityClassMono()` 或 `getTableNameMono()` | > 最后更新:2026-06-02 --- # 租户、操作身份与上下文 来源:docs/guides/tenant-and-context.md SHA-256:2bf594e8a36f37fff90cf092ac8ff6053c60131721fef09fbafea89eef5f4292 网站:/docs/tenant-and-context/ 规则 `CORE-TENANT-001`。新增实体、自定义查询、关系写入、后台任务、缓存均需检查本规则。 ## 先区分四个概念 - 操作身份:谁执行操作,由受信任会话/任务身份建立。 - 目标租户:本次选择在哪个租户范围工作。 - 实体租户:数据实际归属。 - 数据范围:租户内部还允许访问哪些数据,由业务与授权模块进一步约束。 租户过滤不能代替 API 授权,角色授权也不能替代数据行的租户校验。客户端传入一个 tenantKey 不会自动成为可信操作上下文。 ## 实体模式 来源:[TenantScopeMode](/references/#source-a301048e69f1ec0d)。 | 模式 | 数据语义 | 写入时重点 | | --- | --- | --- | | `TENANT_ONLY` | 当前租户数据 | 不能保存成全局租户行 | | `GLOBAL_ONLY` | 全局数据 | 行必须属于全局租户,要求具备全局维护身份 | | `GLOBAL_WITH_TENANT` | 全局数据与当前租户数据可一起读取 | 普通租户能读取全局行不代表能修改它 | | `TENANT_SELF` | 租户主体自身 | 创建/修改规则不同于普通租户业务实体,查看具体实现 | 标准 ID 读取使用 `findEntityById / findAllEntityById / existsEntityById`。JDBC、JPA、MyBatis-Flex、MongoDB 的这些 Core 能力在返回实体前通过 `EntityTenantSupport.isReadable` 过滤租户范围;同步 `FindByIdManager` 转换 VO 前再做相同检查。单条不可见时 Manager 返回 `DATA_NOT_FOUND`,批量结果排除不可见行,存在性检查返回 `false`。非租户实体不受此过滤;`TENANT_SELF` 沿用租户主体可跨目标读取的语义,API 授权仍由业务约束。 这是 Core 能力入口的返回值隔离,不是数据库级行安全。底层原生 `findById / findAllById / existsById / selectOneById`、自定义 SQL、派生方法及 join 不自动获得这些检查;业务应使用标准能力或显式加入读取范围。R2DBC 的标准仓储基类在查询条件中限定范围。任何数据栈都不能把这些租户规则当作逻辑删除、字段脱敏或租户内数据范围授权。 ## 写入链路 [EntityTenantSupport](/references/#source-9e2a8a290103fb58)分别处理初始化、创建和修改校验。`init` 在缺少实体租户时按模式和目标租户补值;多租户下缺少必要目标会报错。 `checkModifyAccess(Object)` 先检查操作身份与实体模式,再检查当前目标租户。其带显式操作租户参数的重载只做对应写权限判断,不能当作包含全部目标租户检查的通用替代。 对于非全局行,修改时目标租户必须与实体租户相符;支持全局行的模式另有分支。即使操作身份可维护多个租户,也应在选定目标范围内操作。单租户模式下还会考虑配置租户的维护资格,具体见上述源码。 标准账户管理动作(重置密码、启停、密码过期标记、解锁时间)会先读取账户,再检查操作身份与目标租户,缺失或越界时不发出更新。响应式检查在订阅链内获取上下文。底层认证计数操作不属于该管理入口,认证模块需维护自己的可信调用边界。 新建和修改应沿 Core 标准生命周期执行;业务直接调用仓储时必须自行确认绕过了哪些校验。优先通过已有 Manager 能力组合,而不是在每个入口复制租户判断。 ## 同步与响应式上下文 同步入口使用 [ContextHandler](/references/#source-3cc51ad22db4f563)。响应式入口使用 [ReactiveContextHandler](/references/#source-e8119aab84517c7c),上下文必须随订阅链传播。 不要在 Publisher 装配时提前读取当前用户,也不要把同步 ThreadLocal 当作任意异步线程都能读取的事实。后台任务需要显式建立并清理受信任上下文,不能沿用上次请求残留。WebFlux 请求语言保存在 `OperateInfo` 的 Reactor Context 中,使用 `ReactiveContextHandler.getLocale()` / `ReactiveI18nHandler` 读取;过滤器不向共享线程写入 `LocaleContextHolder`。 ## 验证清单 准备租户 A/B 和全局数据,验证创建、查询、按 ID 读取、修改、状态、删除、关系绑定及自定义查询。至少包含:A 修改 B 失败、全局身份选 A 不能修改 B、普通身份不能修改全局行、支持全局读取时返回集合正确、缺失上下文不越界。 源码级例子:[EntityTenantSupportTest](/references/#source-f7f82053d239b656)。它证明租户策略,不替代各数据栈真实 SQL 和 HTTP 授权测试。 ID 读取与账户写入的实际 JDBC 验证见 [ID 仓储集成测试](/references/#source-8b375106a8746793)和[账户隔离测试](/references/#source-90c3a3d17e6c05f3),使用 H2;其他阻塞适配器另有默认方法契约测试,不代表已连接其生产数据库。 --- # 身份与权限接入 来源:docs/guides/authorization-integration.md SHA-256:1cb10f828a183543c5880c85d6a79f519df6acbee47122ce96e40832fae3537b 网站:/docs/authorization-integration/ 规则 `CORE-AUTH-001`。Core 提供 Web、上下文和通用契约;身份认证、会话存储、授权与数据范围由安全模块和应用装配实现。 ## 接入顺序 1. 在需求中区分后台维护、用户本人操作、公共读取等 API 面,定义主体和数据范围。 2. 选择应用的认证、会话和授权模块,确认 Controller 所依赖的授权 Bean 与安全链已装配。 3. 为标准能力和自定义动作维护稳定 resource key、权限清单和 Controller 授权表达式。 4. 将可信操作用户和目标租户传入业务链路,按[租户规则](/docs/tenant-and-context/)保护数据访问。 5. 使用真实身份分别验证允许、拒绝、跨租户和范围外数据。 方法上出现 `@PreAuthorize` 仍需方法安全配置才能生效;一个普通 `new Controller(...)` 的单元测试不会自动执行 Spring 安全拦截。权限清单存在也不能证明实际 HTTP 已受保护。 ## 正式契约与样例 当前会话采用服务端 `SecuritySession`;JWT 是传输凭据,不能由前端解析 token 来决定身份/权限。共享认证/授权桥接及契约落点见[分层指南](/docs/custom-business-layering-guide/);登录与诊断见[开发登录指南](/docs/development-login-guide/)。 真实 Controller 样例:[CategoryDefinitionController](/references/#source-5cfcd69150a9f319)。可以核对资源授权表达式、自定义接口与标准 trait 的组合,但新模块应使用自己的资源 key。 前端按钮权限只是展示控制;后端 API 必须独立校验。读取分配列表与保存分配结果是否属于一个授权动作,由业务语义确定,避免机械生成多余权限点。 ## 验证 至少检查:未登录、无权限、有权限、同租户但数据范围外、其他租户。验证实际响应与数据未被修改,不只断言权限表达式字符串。 开发模式用于本地诊断,不作为生产认证方案。具体会话 Redis 和启动参数以[开发应用](/references/#source-0dd5f2662bc35434)及所属安全模块为准。 --- # 事务、并发与副作用 来源:docs/guides/transaction-and-concurrency.md SHA-256:592c1a4ef166b49648d6ce982a3dc0caac14cac50cd6841864163553954d7061 网站:/docs/transaction-and-concurrency/ 规则 `CORE-TX-001`。分层正文见[业务分层](/docs/custom-business-layering-guide/)。 ## 写用例的事务如何生效 Service 负责业务编排与事务边界,组合多个 Manager 完成一次用例。必须同时满足:Spring 管理的 Service、事务代理实际拦截该调用、选定数据栈的事务管理器、所有需要原子性的访问参加同一事务。 不要只添加 `@Transactional` 后便宣称支持回滚。对象自己调用自己的方法、直接 `new` Service、不同 DataSource/ConnectionFactory 或独立订阅都可能不符合预期边界。 同步真实样例:[CategoryDefinitionService](/references/#source-7e7798b2b218f580)。它使用类级只读事务与写动作覆盖;复制模式时同时核对传播、回滚异常和方法覆盖。 ## 响应式事务 事务随 Publisher 的订阅链工作。使用匹配的响应式事务管理器以及应用已经采用的事务声明/操作器,确保多个 Manager 返回的 Publisher 被组合在同一条链中。 - 延迟读取上下文与依赖,不在组装 Publisher 时执行写操作。 - 顺序依赖使用正确的链式组合,不能调用 `subscribe()` 后立刻返回成功。 - 业务方法不通过 `block()` 借用阻塞链。 - `onErrorResume` 把异常转换为正常完成可能改变回滚结果,使用前明确语义并测试。 ## 数据库写入与外部副作用 `afterSave` 等钩子处于 Manager 生命周期中,不代表外层事务已经提交。外部消息、HTTP 调用或不可撤销动作,应使用具有明确提交与失败语义的方案。 树节点移动会先更新子孙的标签路径,再更新 ID 路径,两步都按旧 ID 路径及 `/` 分隔边界筛选;当前节点由正常更新生命周期保存。调用这类 Manager 必须处于同一 Service 事务,避免中途失败留下部分路径。真实 SQL 与回滚验证见 [TreeMoveJdbcIntegrationTest](/references/#source-dd144f6854dbc0bb)。 Core 服务缓存有提交后失效协调,见[缓存指南](/docs/cache/);集成事件区分事务内内部事件和提交后蒸馏事件,见[事件指南](/docs/integration-events/)。蒸馏事件仍属于 JVM 内事件,不承诺跨进程可靠投递。 ## 幂等与并发由业务定义 先说明唯一业务键、重复请求应该报错还是返回已有结果,以及并发竞争允许的结果,再选择数据库唯一约束、锁、条件更新或其他方案。 `CreateOrRefreshManager` 复用创建/更新生命周期,但不是数据库原子 upsert,也不是所有并发场景的幂等保证。唯一性检查与最终写入之间仍要考虑竞争。 ## 验证 事务测试必须通过真实代理入口执行:第一张表写成功、第二步人为失败,最后在事务之外查询,确认第一步也回滚。再验证成功提交后的最终数据和副作用。 测试方法外层自动回滚可能掩盖被测 Service 没有事务的问题;要明确哪一层开启事务。缓存/事件测试分别覆盖提交、回滚和无事务路径;并发要求用可重现竞争场景验证。 --- # 数据状态能力与业务状态 来源:docs/guides/data-status.md SHA-256:d3e91f392cbb64cc9fa3222b05a4428f7f42f25eb87d7141939da3a3dc192122 网站:/docs/data-status/ 规则 `CORE-STATE-001`。权威类型:[DataStatusFeature](/references/#source-84d6f191c84aaa3c)、[EnableDataStatus](/references/#source-974fdf64ed1950db)。 ## 开发顺序 1. 逐个实体声明需要的能力:`FIXED`、`ENABLED`、`DEFAULT`、`PRIMARY`、`LOCKED`、`PUBLISHED`。 2. 选择基类并由最终实体显式开启额外能力。`AbstractTenantBase*Entity` 系列默认提供 `FIXED`;不要认为字段存在就已开放全部状态动作。 3. 对齐 Manager、Service、Controller、权限与前端 `dataStatusCapabilities`。 4. 确定默认值、互斥/唯一范围、批量行为和状态切换后的业务约束。 填写方式见[需求模板](/docs/module-requirements-template/),代码排布见[源码风格](/docs/code-style-guide/)。未声明的开关不能在生成代码时默认开启。 ## 通用状态与领域状态机 通用状态位表达独立能力,例如启停、锁定、是否发布。业务状态机表达流程与允许转移,例如草稿、待审核、已批准、已执行。 同一条数据可以同时有两种状态,但必须明确哪种状态控制哪些动作。`PUBLISHED` 不自动提供版本冻结、审核、发布时间或发布快照。 ## 验证 验证未开启能力的动作、重复状态请求、批量中存在非法对象、默认/主项的唯一范围、逻辑删除后的状态行为,以及两条运行时的同义结果。事件规则只为实际发生的状态变化发事件,详见[集成事件规范](/docs/integration-event-guide/)。 --- # 服务缓存接入 来源:docs/guides/cache.md SHA-256:9fa608556f62bece93edc336258e811b071855d305c0020415b6297f1484a3a9 网站:/docs/cache/ 规则 `CORE-CACHE-001`。2026-09-19 对照同步与响应式缓存源码。 ## 选择运行时 | 业务引擎 | 缓存模块 | 主要依赖 | | --- | --- | --- | | `ineed-core-engine` | `ineed-core-service-cache` | Spring Cache 的 `CacheManager` 与同步事务协调 | | `ineed-core-engine-reactive` | `ineed-core-service-cache-reactive` | Reactive store/Redis 与响应式事务协调 | 两套模块含同包同名契约,分别用于独立运行时。不能将阻塞缓存引入响应式应用作为复用手段。 ## 声明缓存 Service 的可选缓存能力由 `IServiceCache.getServiceCacheSpec()` 提供;默认返回 `null`,表示不启用。规格由 `ServiceCacheSpec` 描述缓存名称及各读取能力开关。 源码:[IServiceCache](/references/#source-2573c4e63e1a86f2)、[ServiceCacheSpec](/references/#source-1ba1ed9a5fb3847f)。 接入步骤:确认查询适合缓存 → 提供明确 cache name 和规格 → 确认运行时实际有 cache/store → 检查标准读取能力与自定义方法的注解 → 确认全部写入口的失效范围 → 验证隔离与回滚。 自定义只读方法使用 `@ServiceCacheable` 时仍要满足 Service 策略与 Spring 代理条件;`@ServiceCacheEvict` 用于成功写入后的失效。同步标准 CRUD、账户、状态及排序 Service 写方法也使用该注解,清理当前目标租户登记的缓存 key;业务覆盖标准方法或新增写动作时需保留对应失效声明,避免用 `@CacheEvict(allEntries=true)` 替换。自调用不会自动经过代理。 ## Key 与权限 Core 缓存包含租户隔离契约。业务仍需判断结果是否还依赖用户、角色、数据范围、语言等条件;有租户 key 不等于所有个性化查询都安全。无法稳定表达授权上下文的结果,优先不缓存。 不要用另一个模块的 cache name,也不要在绕过 Service 的仓储写入后假定缓存一定失效。自定义写动作必须被纳入失效测试。 ## 提交与回滚 同步 [ServiceCacheAfterCommitCoordinator](/references/#source-9dbaf9d560cc1c0c)在有事务时协调提交后失效;回滚不执行该提交动作,无事务时在方法成功返回后执行。`REQUIRES_NEW` 挂起外层失效批次,内层提交独立失效,再恢复外层批次;外层回滚不能吞掉已提交内层的失效。 响应式 [ReactiveServiceCacheAfterCommitCoordinator](/references/#source-38a0e2169bc28c9b)组合源 Publisher 与响应式事务同步。未订阅不得执行缓存工作。 事务成功后缓存失效仍可能失败;这不等于数据库回滚。业务应根据一致性要求设计重试、告警或补偿,不能把本地缓存接口当作可靠消息系统。 ## 验证 同租户命中、不同租户隔离、写后失效、事务内不提前失效、回滚保留原缓存、无事务行为、响应式未订阅无动作。可运行依据:[提交后协调测试](/references/#source-62b9f09a589dc9b6)、[缓存注解集成测试](/references/#source-bb471474084587a1)。这些测试不代表已连接生产 Redis。 标准默认方法的真实代理验证见 [StandardServiceCacheTransactionTest](/references/#source-bcc6cad693563333):覆盖外层事务、提交前旧值回填、回滚、其他租户保留、Service 自身事务和 `REQUIRES_NEW`。测试采用默认 Spring 事务 Advisor 顺序;应用若自定义 Advisor 顺序,应让事务拦截器包住缓存失效拦截器(事务 order 数值更小),并运行同类传播测试。独立事务的四种提交/回滚组合见 [RequiresNewCacheEvictionTest](/references/#source-bce96a013cfae3f5),使用真实 H2 事务。 --- # 业务事件接入路线 来源:docs/guides/integration-events.md SHA-256:86944f39f8ce503f38a89e8610429e121a70b91a2fedfebea8be859b85c35a81 网站:/docs/integration-events/ 规则 `CORE-EVENT-001`。事件模型、配置 key、payload、资源解析和监听代码的唯一正文是[集成事件指南](/docs/integration-event-guide/)。本页帮助决定如何接入。 ## 选择事件 需要参与当前事务的模块内协作,先检查内部事件及监听器的异常/事务行为;需要在提交后执行的 JVM 内协作,检查蒸馏事件。需要可靠跨进程投递时另行设计可靠性方案,不能把蒸馏事件直接当作持久消息。 ## 接入顺序 1. 明确业务动作、资源 key、消费方以及失败对主流程的影响。 2. 检查 Core 已有发送点,优先复用,避免在 Service 再补发一次。 3. 按正式指南配置全局与资源级开关,确认内部事件和蒸馏事件的依赖。 4. 监听器使用对应 payload,避免依赖某个 ORM 实体的未声明字段。 5. 验证事务提交、回滚、开关关闭、重复/无变化动作和响应式未订阅行为。 ## 业务约束 事件消费者需要幂等时,应依据稳定业务标识设计。提交后监听失败不能保证撤销已经提交的数据库数据;外部副作用需明确重试和补偿。 新增事件字段时同步更新契约、生产方、消费方、测试和兼容说明。验证入口见[测试规范](/docs/testing/)。 --- # HTTP Client、Socket 与数据库元数据 来源:docs/guides/clients-and-metadata.md SHA-256:38ed99cb3f5ed4f25b95c8758fd8847307f8ef18af5ca2737abb80fe8e5779ca 网站:/docs/clients-and-metadata/ 本页是外围能力接入入口。本轮核对接口和模块,不宣称已重新验证所有网络实现和数据库厂商。 ## HTTP Client 共享请求/响应来自 `ineed-core-httpclient`;阻塞实现有 JDK 与 OkHttp,响应式使用独立 reactive 契约及 WebClient 实现。按应用运行时选择实现,不为了复用阻塞客户端在响应式链中阻塞等待。 [BlockingHttpClientExecutor](/references/#source-639d404951b3c12b)以 `execute(HttpClientRequest)` 返回 `HttpClientResponse`,声明 IO 与中断异常。请求配置从[HttpClientRequest](/references/#source-8cecd590887ba5f9)核对。 接入时明确执行器、超时、响应体限制、错误处理、重试及幂等。重试外部写请求前先确认业务允许重复;不能把客户端超时直接解释为对端没有执行。 JDK 执行器的 `timeout` 覆盖等待响应与完整响应体接收;缺省或非正值使用 30 秒。接收时逐块检查 `maxResponseBytes`,超限抛出 `HttpResponseTooLargeException`;超时抛出 `HttpTimeoutException`,中断继续向调用方抛出 `InterruptedException`,同时取消交换与响应体订阅。回环 HTTP 验证见 [JdkBlockingHttpClientExecutorTest](/references/#source-c82cb55e002e9561),包含立即返回头但慢传正文、持续小块传输、大小边界与中断。 ## Socket 接入 [SocketAccessExecutor](/references/#source-cd9e7eac0b27d5a2)声明 `supports(SocketAccessMode)` 和 `start(SocketAccessEndpoint, SocketAccessMessageHandler)`,启动返回 session。实际支持模式以所选执行器为准,不能因为枚举含某模式就认为每个实现都支持。 应用负责明确连接生命周期、消息处理、取消/关闭、重连与资源释放。参考[SocketAccessSession](/references/#source-b11e5ecdc6628024);响应式实现位于 reactive 聚合。 ## 数据库元数据 [RdbMetadataUtil](/references/#source-3beb2916d12b37bb)提供数据库、表、列等读取入口。DataSource 重载内部获取并关闭连接;传入 Connection 时按调用方连接生命周期管理。 读取 SQLException 通过 `RdbMetadataException.readFailed` 转换,不应将失败伪装成空表列表。元数据为空和读取失败要分别处理,具体支持的 schema/catalog 行为需要针对目标数据库验证。 ## 验证与维护 客户端至少覆盖成功、超时、异常、取消/关闭和资源释放;元数据至少覆盖有效结构、无结果与连接/读取失败。新增适配器时更新[模块清单](/docs/module-inventory/)和[运行时矩阵](/docs/runtime-matrix/),不要把“已加入 POM”标记为“全部能力已验证”。 --- # AI 编码入口 来源:docs/ai/README.md SHA-256:4a5076479d04775976a1951dd7c1ba703042961f61f5846304716951c4eaed73 网站:/docs/ai-entry/ 适用 Core `3.0.0-SNAPSHOT`。先识别任务,再按[任务路由](/docs/task-routing/)读取规范和示例。当前任务明确约束优先;对于未说明的框架行为,以真实 API、现行规范和验证结果为依据,不猜测接口。 ## 最短执行路径 1. 确认需求、SQL、目标仓库/模块、Core 版本与技术栈一致。 2. 阅读本页及[规则索引](/docs/standards/),加载任务相关专题。 3. 打开一个匹配技术栈的实际实现和测试,核对接口签名与依赖。 4. 实现最小完整业务行为,并保持约定的同步/响应式与数据栈范围。 5. 按[交付清单](/docs/delivery-checklist/)运行验证,报告实际结果和限制。 ## 编码前必须掌握的约束 - `CORE-LAYOUT-001`:业务模块使用既有 Maven 分层;聚合/BOM 不生成业务源码;contract 共用,不另建 reactive-core。 - `CORE-LAYER-001`:Controller 调用 Service;Service 是用例与事务边界;多实体组合 Manager;仓储承担持久化。Service 不横向互调,不发明平行编排层。 - `CORE-CONTRACT-001`:新代码使用 `contract/*`、`converter/*`;复用已有能力接口、异常和返回体,不复制默认实现。 - `CORE-TENANT-001`:每个实体明确租户模式;读权限不能代替写校验;自定义查询、关系操作和缓存同样遵守隔离。 - `CORE-AUTH-001`:复用安全模块的身份、权限与数据范围,不因接口带有资源名就假定已完成授权。 - `CORE-TX-001`:写动作核对事务代理、事务管理器和回滚;响应式工作在订阅期发生,不在业务链中 `block/subscribe`。 - `CORE-STATE-001`:只开启需求声明的数据状态能力;通用状态位不替代业务状态机。 - `CORE-DELETE-001`:区分逻辑删除、物理删除、恢复与引用保护;钩子结束不等于事务提交。 - `CORE-CACHE-001`、`CORE-EVENT-001`:使用匹配运行时,核对租户键、失效/提交时序和事件投递边界。 - `CORE-STACK-001`:按任务确定的四栈/双 Web 范围对齐;多个运行时分别装配,不混装同包同名类型。 - `CORE-STYLE-001`:按源码风格写 Javadoc、字段、日志和局部格式保护,具体规则不在此复制。 - `CORE-TEST-001`:按行为和影响范围验证;区分内存契约测试、真实数据库、应用 HTTP 与前端端到端证据。 这些是导航摘要。完整约束、反例和验证方式以[规则索引](/docs/standards/)所指正文为准。 ## 避免凭历史样板推导规则 应用错误码必须查[全局登记](/references/#source-6c67578a7e735914),不能沿用旧 Core 号段。类名、泛型、配置项和 URL 先定位源码。模板中的占位名称不是已存在的 API。 发现冲突先记录具体文件和行为;不能通过新增另一层包装绕过缺失依赖或把未验证实现描述成稳定能力。 ## 多仓库工作 本仓 `AGENTS.md` 不会自动成为并列业务仓库的规则。开始业务任务时显式提供本入口与目标仓库入口;独立获取源码时使用匹配版本的文档,不复制会长期漂移的规则正文。 工具输入见[任务模板](/docs/task-input-template/)。只改文档时使用[维护规则](/docs/documentation-policy/),无需加载无关 ORM 细节。 --- # 按任务读取与验证 来源:docs/ai/task-routing.md SHA-256:806e15925b74138d24e6c07dff206fd5920a1108b76e90cc2b194e4a64b12471 网站:/docs/task-routing/ 每项任务先读 [AI 入口](/docs/ai-entry/)。下表补充加载内容;涉及数据的任务必须检查隐含租户/权限影响,不能只按文件名判断。 | 任务 | 读取 | 最小行为验证 | | --- | --- | --- | | 新建业务模块 | [入门](/docs/first-module/)、[骨架](/docs/business-module-layout-guide/)、[需求模板](/docs/module-requirements-template/)、目标 ORM 指南 | 编译、创建查询、非法输入、租户权限、应用装配 | | 新增字段 | [契约](/docs/contract-and-error/)、[对象模型](/docs/object-model/)、目标数据栈 | 写后读、旧数据默认值、converter、必填与敏感字段 | | 新增查询/join | [CRUD](/docs/crud-query-guide/)、目标 ORM、[租户](/docs/tenant-and-context/) | 过滤、分页、空结果、逻辑删除、跨租户 | | 新增业务动作 | [分层](/docs/custom-business-layering-guide/)、[事务](/docs/transaction-and-concurrency/)、[授权](/docs/authorization-integration/) | 正常结果、回滚、越权、重复请求和并发要求 | | 状态/删除/恢复 | [状态](/docs/data-status/)、[删除](/docs/delete-hook-and-logging-guide/) | 不支持能力、重复动作、引用保护、级联、恢复 | | 缓存/事件 | [缓存](/docs/cache/)、[事件](/docs/integration-events/)、事务与租户 | 未提交、提交、回滚、租户隔离、未订阅 | | 响应式能力 | [R2DBC](/docs/r2dbc-code-guide/)、[生命周期](/docs/request-lifecycle/) | Publisher 创建无副作用,订阅完成/失败及上下文 | | 前端接入 | [前端契约](/docs/frontend-integration-guide/)、所属前端规范 | API/权限/状态/分页一致,相关页面 E2E | | 文档修订 | [维护规则](/docs/documentation-policy/) | 链接、源码事实、命令适用性;变更示例才跑相关代码 | ORM 入口:[JPA](/docs/jpa-querydsl-code-guide/)、[JDBC](/docs/jdbc/)、[MyBatis-Flex](/docs/mybatis-flex-code-guide/)、[R2DBC](/docs/r2dbc-code-guide/)。 任务只有特定技术栈时记录范围;不能因阅读顺序从 JDBC 开始,就省略任务要求的其他实现。 --- # 编码任务输入模板 来源:docs/ai/task-input-template.md SHA-256:65fd48ea499915176c199da77598aa8fabd5707b60a50939300ad48b20da2e57 网站:/docs/task-input-template/ 复制以下结构,填写当前任务需要的内容。已有需求说明可链接引用,不重复抄写。 ```text 业务目标:用户完成什么操作,成功和失败结果是什么 需求/SQL:仓库与相对路径 目标:业务仓库、模块、分支或工作树 基线:Core/parent/业务模块版本 技术栈:JPA / JDBC / MyBatis-Flex / R2DBC;MVC / WebFlux 对象:主从关系、字段、AO/Query/VO、业务键与唯一性 边界:租户模式、操作身份、数据范围、权限资源 状态:通用能力开关、独立业务状态机、删除与恢复规则 事务:用例边界、失败回滚、并发/幂等要求 集成:需要时填写缓存、事件、外部契约与提交时序 错误:全局登记表中的模块范围;未知时查询,不能猜号 验收:真实业务场景、数据准备、测试环境、预期结果 前端:涉及时提供路由、字段、权限、状态能力及 E2E 范围 ``` 业务建模细节使用[需求模板](/docs/module-requirements-template/)。任务输入回答“这次做什么”;规范回答“如何正确做”。 输入不一致时先核实具体冲突,例如 SQL 属于另一业务域、目标模块选错或声明四栈却只指定一个实现目录。可通过仓库查清的事实直接核实,只有业务决策确实缺失时再向需求方澄清。 --- # 交付检查 来源:docs/ai/delivery-checklist.md SHA-256:b6e6188458c36d47a541d0af235ed2be3c85ed57161bcd68bc2832a917766006 网站:/docs/delivery-checklist/ ## 实现检查 - 需求、SQL、目标模块和技术栈一致;关联规则可从[索引](/docs/standards/)定位。 - 核心分层、contract、converter 与真实 Core API 一致,无复制旧包或虚构接口。 - 租户读取与写入、权限和数据范围有相应正反向场景。 - 状态、删除、恢复、事务、缓存、事件按任务涉及范围检查。 - 已承诺的数据栈/Web 实现语义一致;未涉及的能力标记“不适用”并说明依据。 - 错误码、语言资源、文档和实际接口同步。 ## 验证报告 | 项目 | 应记录内容 | | --- | --- | | 工作基线 | 仓库、版本或提交、在途修改情况 | | 命令 | 工作目录、工具链、目标模块与测试选择 | | 结果 | 实际测试数量、通过/失败/跳过,相关报告位置 | | 测试层级 | 编译、内存契约、真实持久化、真实 HTTP 或前端 E2E | | 未验证项 | 原因及影响,不以“有测试文件”代替执行证据 | 不要把 `-DskipTests` 构建报告为测试通过;不要把没有匹配用例的零退出码报告为行为验证。针对变更运行必要检查,已经通过且没有新问题时无需反复扩大全仓测试。 --- # 项目开发契约 来源:docs/project-development/development-contract.md SHA-256:055870895ce672f54f286fa1dc40e02f92bd8d49ae4fcd0b59d6ad78d7eba961 网站:/docs/development-contract/ 本路径保留为兼容入口。现行开发契约已按任务拆分,避免每次修改都要求通读所有专题。 - [AI 开发入口](/docs/ai-entry/):执行顺序与关键规则导航。 - [任务路由](/docs/task-routing/):新模块、字段、查询、业务动作、删除、缓存和事件分别读取哪些材料。 - [规则索引](/docs/standards/):每条规则的唯一正文、适用范围与验证方式。 - [任务输入模板](/docs/task-input-template/):需求、SQL、目标目录、技术栈和验收。 - [交付检查](/docs/delivery-checklist/):实际验证命令、结果与限制。 - [开发人员入门](/docs/first-module/):从能力链到真实模块。 原契约的分层、仓储、转换层、响应式、源码风格、数据状态、权限、错误与测试要求继续生效,详细正文由规则索引定位。只读取短入口不代表可以跳过任务涉及的完整规范。 应用私有错误码只到[全局登记表](/references/#source-6c67578a7e735914)查号和登记;不在 Core 文档分配业务号段。 > 更新:2026-09-19 --- # Ineed-Core 内部文档 来源:docs/core/README.md SHA-256:a0f05fd441d2864c2d656368ea57b4ce4dd4344d25d60b4803063463211f5beb 网站:/docs/framework-map/ 面向 `ineed-core` 维护者,回答框架本身的问题: - 当前有哪些真实 Maven 模块 - 各模块分别承载哪些框架能力 - 各运行时已验证范围和已知缺口是什么 - 集成事件、常量、错误码、i18n 等框架机制如何维护 业务开发的入口已分为[AI 路径](/docs/ai-entry/)和[开发人员教程](/docs/first-module/)。本目录保留框架参考与维护主题;完整索引见[文档首页](/docs/documentation-map/)。 ## 推荐阅读顺序 1. [module-inventory.md](/docs/module-inventory/) — 真实模块清单 2. [current-implementation-status.md](/docs/current-implementation-status/) — 当前实现状态与缺口 3. [module-capability-map.md](/docs/module-capability-map/) — 能力分层地图 4. [java-module-directory-guide.md](/docs/java-module-directory-guide/) — 模块目录结构规范 5. [integration-event-guide.md](/docs/integration-event-guide/) — 集成事件机制 6. [constant-errorcode-i18n-guide.md](/docs/constant-errorcode-i18n-guide/) — 常量、错误码与 i18n 7. [api-compatibility-policy.md](/docs/api-compatibility-policy/) — 废弃 API 的登记、复查与安全移除策略 8. [api-documentation-guide.md](/docs/api-documentation-guide/) — Scalar 与 springdoc 约定 ## 按任务进入 | 任务 | 推荐文档 | |------|------| | 确认真实模块声明 | [module-inventory.md](/docs/module-inventory/) | | 确认当前实现状态和已知缺口 | [current-implementation-status.md](/docs/current-implementation-status/) | | 理解框架能力地图和模块边界 | [module-capability-map.md](/docs/module-capability-map/) | | 判断模块目录形态 | [java-module-directory-guide.md](/docs/java-module-directory-guide/) | | 维护集成事件机制 | [integration-event-guide.md](/docs/integration-event-guide/) | | 维护常量/错误码/i18n | [constant-errorcode-i18n-guide.md](/docs/constant-errorcode-i18n-guide/) | | 登记、复查或移除废弃 API | [api-compatibility-policy.md](/docs/api-compatibility-policy/) | | 配置或验证 API 文档入口 | [api-documentation-guide.md](/docs/api-documentation-guide/) | 如果你要开发新的业务模块,请转到 [../project-development/README.md](/docs/development-map/)。 > 最后更新:2026-07-30 --- # Ineed-Core 真实模块清单 来源:docs/core/module-inventory.md SHA-256:07d2d57e652bbcce804424dd256709904eabab71a2e040c814b33f3ae3fec6c2 网站:/docs/module-inventory/ ## 文档定位 本文档按当前仓库里的聚合 `pom.xml` 说明“真实模块声明”。 这里的 source of truth 是: - 根 `pom.xml` - `ineed-core-modules/pom.xml` - `ineed-core-reactive-modules/pom.xml` - `ineed-core-tests/pom.xml` - `ineed-core-starters/pom.xml` - `ineed-core-bom/pom.xml` 如果某个目录只存在于工作区、但还没有进入这些 `pom.xml` 的 `` 声明,它不应被视为当前稳定模块清单的一部分。 如果要判断普通 Java 模块、聚合模块、BOM 模块到底该长什么样,应阅读 [java-module-directory-guide.md](/docs/java-module-directory-guide/)。 如果要创建新的业务聚合模块或判断业务模块标准骨架,应阅读 [business-module-layout-guide.md](/docs/business-module-layout-guide/);那是业务模块约定,不是当前 `ineed-core` 自身的稳定声明清单。 如果要确认这些模块当前是否推荐用于业务主链路,请阅读 [current-implementation-status.md](/docs/current-implementation-status/)。 ## 根聚合模块 根 `pom.xml` 当前声明的模块如下: | 模块 | 角色 | |------|------| | `ineed-rdb-metadata` | 数据库元数据分析与读取支撑 | | `ineed-core-base` | 基础契约、公共工具与共享上下文能力 | | `ineed-integration` | 内部事件、蒸馏事件与业务事件配置支撑 | | `ineed-core-data-base` | 数据层共享底座 | | `ineed-core-httpclient` | HTTP Client 同步/响应式共享契约 | | `ineed-core-socketaccess` | Socket 接入同步/响应式共享契约 | | `ineed-core-modules` | 阻塞式主链路聚合模块 | | `ineed-core-reactive-modules` | 响应式能力线聚合模块 | | `ineed-core-tests` | 测试聚合模块 | | `ineed-core-starters` | starter 聚合模块 | | `ineed-core-bom` | BOM 聚合模块 | ## `ineed-core-modules` 子模块 `ineed-core-modules/pom.xml` 当前声明: | 模块 | 角色 | |------|------| | `ineed-core-context` | 阻塞式上下文能力 | | `ineed-core-httpclient-jdk` | JDK 阻塞式 HTTP Client 实现 | | `ineed-core-httpclient-okhttp` | OkHttp 阻塞式 HTTP Client 实现 | | `ineed-core-socketaccess-jdk` | JDK Socket 接入实现 | | `ineed-core-socketaccess-netty` | Netty Socket 接入实现 | | `ineed-core-socketaccess-nio` | NIO Socket 接入实现 | | `ineed-core-service-cache` | 基于 Spring Cache 的阻塞式服务缓存、租户隔离键与事务提交后失效能力 | | `ineed-core-engine` | `Service` / `Manager` 业务编排能力 | | `ineed-core-webmvc` | 阻塞式 Web API 入口 | | `ineed-core-datasource` | 数据源相关支撑 | | `ineed-core-data-core` | 共享 CRUD / 查询 / 状态 / 排序 / 删除数据能力 | | `ineed-core-data-jpa` | JPA 数据实现 | | `ineed-core-data-jdbc` | JDBC 数据实现 | | `ineed-core-data-mybatis-flex` | MyBatis-Flex 数据实现 | | `ineed-core-data-mongodb` | MongoDB 数据实现 | 补充说明: - `ineed-core-data-elasticsearch` 目录可以存在,但当前在聚合 `pom.xml` 中仍是注释状态,不属于当前稳定声明模块。 ## `ineed-core-reactive-modules` 子模块 `ineed-core-reactive-modules/pom.xml` 当前声明: | 模块 | 角色 | |------|------| | `ineed-core-context-reactive` | 响应式上下文能力 | | `ineed-core-httpclient-reactive` | 响应式 HTTP Client 共享契约 | | `ineed-core-socketaccess-reactor-netty` | Reactor Netty Socket 接入实现 | | `ineed-core-service-cache-reactive` | 基于 Reactive Redis 的响应式服务缓存、租户隔离键与事务提交后失效能力 | | `ineed-core-engine-reactive` | 响应式 `Service` / `Manager` 编排能力 | | `ineed-core-httpclient-webclient` | Spring WebClient 响应式 HTTP Client 实现 | | `ineed-core-webflux` | 响应式 Web 入口 | | `ineed-core-data-core-reactive` | 响应式数据共享能力 | | `ineed-core-data-r2dbc` | R2DBC 数据实现 | ## `ineed-core-tests` 子模块 `ineed-core-tests/pom.xml` 当前声明: | 模块 | 角色 | |------|------| | `ineed-core-test-webmvc` | 阻塞式主链路测试聚合入口 | | `ineed-core-test-webflux` | 响应式测试聚合入口 | 补充说明: - 如果工作区里还出现 `ineed-core-test-webmvc-jpa`、`ineed-core-test-webmvc-jdbc`、`ineed-core-test-webmvc-mybatisflex` 等目录,但它们尚未进入当前 `pom.xml` 模块声明,就应视为在建目录,而不是当前稳定聚合模块。 ## `ineed-core-starters` 子模块 `ineed-core-starters/pom.xml` 当前声明: | 模块 | 角色 | |------|------| | `ineed-starter-druid` | Druid 官方 starter 与常见 JDBC 驱动的依赖聚合 | 补充说明: - 当前 starter 模块没有自定义源码级自动配置类。 - 当前未检测到 `spring.factories` 或 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 自动配置入口。 - 因此它应被理解为依赖聚合,不应被理解为完整的 ineed 开箱即用 starter。 ## `ineed-core-bom` 管理范围 `ineed-core-bom/pom.xml` 通过 `dependencyManagement` 管理 core、reactive、data 和 starter artifact 版本。 需要注意: - BOM 管理 artifact 不等于当前根聚合模块清单。 - `ineed-core-bom` 已同时管理 `ineed-core-service-cache` 与 `ineed-core-service-cache-reactive`,下游可以通过 BOM 对齐两套缓存运行时的版本。 - 新增或删除真实 Maven 模块时,应同时核对根聚合 `pom.xml`、对应分组聚合 `pom.xml` 和 BOM 管理范围,避免下游只通过 BOM 看到不存在或未发布的 artifact。 ## 当前推荐主链路对应模块 当前推荐主链路 `WebMvc + JPA/JDBC/MyBatis-Flex` 对应的真实模块主要是: - Web 入口:`ineed-core-webmvc` - 编排层:`ineed-core-engine` - 服务缓存:`ineed-core-service-cache`(由 `ineed-core-engine` 直接依赖) - 共享数据能力:`ineed-core-data-core` - 具体数据栈:`ineed-core-data-jpa`、`ineed-core-data-jdbc`、`ineed-core-data-mybatis-flex` - 回归测试入口:`ineed-core-test-webmvc` ## 阅读建议 - 想理解“应该怎么进入框架、各层怎么分工”,先看 [module-capability-map.md](/docs/module-capability-map/)。 - 想确认“当前仓库到底声明了哪些模块、哪些目录只是工作区存在”,看本文档。 - 想判断“这个 Java 模块到底该不该有 `src`、是不是聚合模块或 BOM 模块”,读 [java-module-directory-guide.md](/docs/java-module-directory-guide/)。 - 想创建新的业务聚合模块、补齐 `core/engine/jpa/jdbc/mybatis-flex/webmvc/engine-reactive/r2dbc/webflux` 骨架,读 [business-module-layout-guide.md](/docs/business-module-layout-guide/)。 - 想判断“模块已声明但是否推荐使用”,读 [current-implementation-status.md](/docs/current-implementation-status/)。 > 最后更新:2026-09-02 --- # Ineed-Core 模块能力地图 来源:docs/core/module-capability-map.md SHA-256:34a044568c5fa188e1bfa543266d408c730915069e766afabb87496060588302 网站:/docs/module-capability-map/ ## 文档定位 本文档说明的是“开发者视角的能力与分层地图”,重点回答: - 应该从哪条主链路进入框架 - `Controller -> Service -> Manager -> Repository/EntityManager` 各层分别落在哪些模块 - 当前哪些能力线是稳定推荐入口 它不是根 `pom.xml` 的完整 Maven 模块清单。 如果你要核对真实模块名、聚合层级和当前 `pom.xml` 声明,请同时阅读 [module-inventory.md](/docs/module-inventory/)。 如果你要确认当前代码状态、测试入口和已知缺口,请同时阅读 [current-implementation-status.md](/docs/current-implementation-status/)。 ## 能力分层地图 ### 共享契约与基础支撑 - `ineed-core-base` 提供通用基础设施与共享契约,包括异常、上下文、结果模型、查询参数、AO / VO 等基础类型。 - `ineed-core-data-base` 提供跨数据栈复用的数据基础抽象,作为数据能力的公共底座。 - `ineed-core-httpclient` 提供 HTTP Client 请求、响应与执行器共享契约,供阻塞式和响应式实现共同复用。 - `ineed-core-socketaccess` 提供 Socket 接入共享契约,供阻塞式和响应式实现共同复用。 - `ineed-core-context` 提供阻塞式主链路所需的上下文支撑能力。 - `ineed-integration` 提供内部事件、蒸馏事件、事件开关、默认事件 key 解析与 Spring 事件发布能力。 - `ineed-core-datasource` 提供动态数据源相关支撑。当前已进入聚合模块,但 lookup key 解析仍未完成真实路由实现,因此不能把它视为已经可用的多数据源方案。 ### 阻塞式业务主链路 - `ineed-core-service-cache` 提供基于 Spring Cache 的服务缓存契约、租户隔离键、缓存成员索引和事务提交后失效协调;`ineed-core-engine` 直接依赖该模块。 - `ineed-core-engine` 承载 `Service` / `Manager` 这一层的业务编排与标准能力接口,是当前阻塞式主路径的核心桥接层。 - `ineed-core-webmvc` 提供阻塞式 Web API 入口,对外暴露标准 Controller 接口和统一返回模型。 - `ineed-core-data-core` 提供 CRUD、查询、状态、排序、删除等共享数据能力支撑。 - `ineed-core-data-jpa` 提供基于 JPA / QueryDSL 的阻塞式数据实现。 - `ineed-core-data-jdbc` 提供基于 Spring Data JDBC 的阻塞式数据实现。 - `ineed-core-data-mybatis-flex` 提供基于 MyBatis-Flex 的阻塞式数据实现。 - `ineed-core-data-mongodb` 已进入当前聚合模块,提供 MongoDB 数据实现支撑;但它不是当前业务开发的优先推荐主链路。 - `ineed-core-httpclient-jdk` 提供 JDK 阻塞式 HTTP Client 实现。 - `ineed-core-httpclient-okhttp` 提供 OkHttp 阻塞式 HTTP Client 实现。 - `ineed-core-socketaccess-jdk`、`ineed-core-socketaccess-nio`、`ineed-core-socketaccess-netty` 提供阻塞式 Socket 接入实现。 ### 响应式扩展能力线 - `ineed-core-context-reactive` 提供响应式链路的上下文支撑。 - `ineed-core-service-cache-reactive` 提供基于 Reactive Redis 的响应式服务缓存、租户隔离键和响应式事务提交后失效协调;`ineed-core-engine-reactive` 直接依赖该模块。 - `ineed-core-engine-reactive` 提供响应式 `Service` / `Manager` 编排能力。 - `ineed-core-webflux` 提供响应式 Web API 入口。 - `ineed-core-data-core-reactive` 提供响应式数据能力的公共支撑。 - `ineed-core-data-r2dbc` 提供基于 R2DBC 的响应式数据实现。 - `ineed-core-httpclient-reactive` 提供响应式 HTTP Client 执行器共享契约。 - `ineed-core-httpclient-webclient` 提供基于 Spring WebClient 的响应式 HTTP Client 实现。 - `ineed-core-socketaccess-reactor-netty` 提供基于 Reactor Netty 的 Socket 接入实现。 ### 构建、测试与配套支撑 - `ineed-core-tests` 聚合框架验证模块,是当前主链路回归验证的测试入口。 - `ineed-core-starters` 当前只提供 starter 级别的依赖聚合,不包含 ineed 自定义自动配置入口。 - `ineed-core-bom` 提供 BOM 依赖对齐能力。BOM 管理的 artifact 不完全等同于根聚合 `pom.xml` 当前声明的模块。 - `ineed-rdb-metadata` 提供数据库元数据分析与读取支撑。 ## 分层关系 当前标准开发主路径仍然是: `Controller -> Service -> Manager -> Repository/EntityManager` 对应到当前代码树,可以理解为: - `Controller` 主要位于 `ineed-core-webmvc`,负责对外暴露 API、参数校验、权限注解和统一返回。 - `Service` 主要位于 `ineed-core-engine`,负责事务边界和业务编排。 - `Manager` 主要位于 `ineed-core-engine` 的接口层,并由 `ineed-core-data-core` 与各数据栈实现共同承接具体能力。 - `Repository/EntityManager` 主要位于 `ineed-core-data-jpa`、`ineed-core-data-jdbc`、`ineed-core-data-mybatis-flex` 等具体数据模块中。 响应式对等链路使用 `ineed-core-webflux`、`ineed-core-engine-reactive`、`ineed-core-data-core-reactive` 和 `ineed-core-data-r2dbc`。它们用于维护 reactive 能力线。业务开发验收应先保证阻塞式主链路成立;如果需求或公共能力要求同步响应式,则在阻塞式基线成立后继续补齐 `webflux / r2dbc` 对等能力。 服务缓存同样按运行时分开:阻塞式链路使用 `ineed-core-service-cache`,响应式链路使用 `ineed-core-service-cache-reactive`。两者保留相同的服务缓存概念与注解名称,但拥有独立实现和依赖图,不能把其中一套作为另一套的运行时依赖。 ## 对外开发常用入口 ### Web 层入口 - `ineed-core-webmvc` 当前对外开发最常用的 Web 入口,适合标准阻塞式业务 API。 - `ineed-core-webflux` 响应式入口,当前可以作为能力认知参考,但不是当前推荐主路径。 ### Service / Manager 入口 - `ineed-core-engine` 当前标准 `Service` / `Manager` 能力入口所在模块,是业务主路径的桥接中心。 ### 数据栈入口 - `ineed-core-data-jpa` 当前稳定的 JPA 数据实现。 - `ineed-core-data-jdbc` 当前稳定的 JDBC 数据实现。 - `ineed-core-data-mybatis-flex` 当前稳定的 MyBatis-Flex 数据实现。 ### 模块声明入口 如果要确认“这些模块在当前仓库里是否真实存在、由哪个聚合 `pom.xml` 声明”,不要只看本文档,应继续看 [module-inventory.md](/docs/module-inventory/)。 ## 当前稳定能力边界 四条关系型业务路径都有当前源码与框架测试入口;适用范围与执行证据见[运行时矩阵](/docs/runtime-matrix/)。不要把“声明模块”“通过契约测试”和“真实数据库/应用完成验收”混为同一状态。 新业务模块、公共能力和代码生成任务,如果需求没有明确限定范围,应按[业务骨架](/docs/business-module-layout-guide/)评估并对齐目标技术栈。MongoDB、未聚合的 Elasticsearch 和动态数据源能力另按已知边界处理。 特别说明: - `ineed-core-datasource` 已接入聚合,但动态 lookup key 仍是占位实现。 - `ineed-core-service-cache` 与 `ineed-core-service-cache-reactive` 已分别进入阻塞式、响应式聚合和 BOM;选择模块时必须与 `engine` / `engine-reactive` 运行时一致。 - `ineed-core-data-elasticsearch` 目录存在,但在 `ineed-core-modules/pom.xml` 中仍处于注释状态。 - `ineed-core-starters/ineed-starter-druid` 是依赖聚合 starter,不是框架自动配置 starter。 ## 关键接口锚点 - `ineed-core-modules/ineed-core-webmvc/src/main/java/com/ineed/core/webmvc/support/crud/ICreateController.java` 标准创建入口,对外暴露保存 API,并在 Web 层完成参数校验和统一返回。 - `ineed-core-modules/ineed-core-webmvc/src/main/java/com/ineed/core/webmvc/support/search/ISearchController.java` 标准查询入口,把 `CommonParameters` 和查询对象组装为 `QueryParameters` 后交给 Service。 - `ineed-core-modules/ineed-core-engine/src/main/java/com/ineed/core/service/crud/ICreateService.java` 创建服务入口,负责把 create 请求桥接到对应的 Manager。 - `ineed-core-modules/ineed-core-engine/src/main/java/com/ineed/core/service/search/ISearchService.java` 查询服务入口,负责承接查询请求并转发到搜索 Manager。 - `ineed-core-modules/ineed-core-engine/src/main/java/com/ineed/core/manager/crud/ICreateManager.java` 创建能力的 Manager 契约,是业务创建逻辑的主要落点之一。 - `ineed-core-modules/ineed-core-engine/src/main/java/com/ineed/core/manager/search/ISearchManager.java` 查询能力的 Manager 契约,是标准 query 实现的核心入口。 > 最后核验:2026-09-19 --- # Ineed-Core 当前实现状态 来源:docs/core/current-implementation-status.md SHA-256:3b0fcde45b1345fe5c90320c1fe3195082fe2e369f85d7bfef5aab02073a3671 网站:/docs/current-implementation-status/ ## 文档定位 本文档用于把 `ineed-core` 当前代码、聚合 `pom.xml`、测试入口和已知缺口放在同一处说明。 它回答的是“现在仓库里实际有什么、推荐怎么用、哪些能力还不能按稳定主路径理解”。如果要看完整模块声明,读 [module-inventory.md](/docs/module-inventory/);如果要看能力分层,读 [module-capability-map.md](/docs/module-capability-map/)。 ## 当前使用口径 四条关系型业务路径为 WebMVC + JPA/JDBC/MyBatis-Flex,以及 WebFlux + R2DBC,均有源码与框架测试入口。详细的源码状态、本轮执行证据和未验证范围统一维护在[运行时矩阵](/docs/runtime-matrix/)。 开发人员可先沿[首个模块教程](/docs/first-module/)学习 JDBC 路径,再对照其他实现;教学顺序不决定正式业务交付范围。公共能力、新模块和未限定单栈的任务,仍按[业务骨架](/docs/business-module-layout-guide/)评估并对齐目标栈。 服务缓存按阻塞/响应式运行时分别使用,不能混装。MongoDB 已进入聚合但不在本轮业务验证范围;Elasticsearch 聚合声明仍为注释;动态数据源 lookup key 仍返回 null;Druid starter 是依赖聚合。 ## 代码结构速览 各模块的角色说明详见 [module-inventory.md](/docs/module-inventory/)。 ## 测试现状 当前 `ineed-core` 下存在较多可执行测试,覆盖基础契约、数据能力、WebMvc/WebFlux support、JPA/JDBC/MyBatis-Flex/R2DBC adapter 默认行为和聚合链路。 常用验证入口: ```bash cd java/ineed-core mvn test cd java/ineed-core mvn -pl ineed-core-base test cd java/ineed-core mvn -pl ineed-core-tests/ineed-core-test-webmvc test cd java/ineed-core mvn -pl ineed-core-tests/ineed-core-test-webflux test ``` 当前仓库没有 Maven wrapper。本轮 effective POM 的编译 release 为 17,定向测试使用 JDK 25、Maven 3.9.15;这不等于已经完成 JDK 17 运行兼容性验证。准备步骤见[工具链说明](/docs/prerequisites/)。 ## 已知缺口 ### 动态数据源 `ineed-core-modules/ineed-core-datasource/src/main/java/com/ineed/core/datasource/DynamicDataSource.java` 当前没有从上下文读取真实 lookup key。后续如果要支持租户级或运行时选择数据源,应先补齐: - lookup key 来源与生命周期 - `DynamicDataSourceContextHolder` 的设置、清理和嵌套调用规则 - 对应单元测试和至少一个启动模块验证 ### 数据库元数据 `ineed-rdb-metadata` 提供连接、表、列等元数据读取。当前 `RdbMetadataUtil` 已通过 `RdbMetadataException.readFailed` 转换 SQLException;调用方应区分读取失败和真实空结果。目标数据库厂商的支持范围仍需对应验证,见[接入指南](/docs/clients-and-metadata/)。 ### 响应式链路 `WebFlux + R2DBC` 已有模块和测试入口,本轮已执行相关框架链路测试,实际覆盖与环境边界见[验证记录](/docs/verification-2026-09-19/)。响应式实现中要避免装配期求值,例如不要用 `Mono.just(getEntityClass())` 提前触发“不支持该操作”的同步异常。 ### Starter 体系 `ineed-starter-druid` 当前没有源码级自动配置类,也没有 `spring.factories` 或 `AutoConfiguration.imports`。它只负责依赖聚合。后续如果要演进为真正 starter,应补齐: - `@AutoConfiguration` 入口 - 条件装配规则 - 默认配置与用户覆盖策略 - starter 专用测试 ### 服务缓存运行时边界 服务缓存已从 engine 中拆分为两套独立运行时: - `ineed-core-service-cache` 由 `ineed-core-engine` 直接依赖,使用 Spring Cache、阻塞式 Redis/Redisson 依赖与阻塞式事务同步机制。 - `ineed-core-service-cache-reactive` 由 `ineed-core-engine-reactive` 直接依赖,使用 Reactive Redis 与响应式事务同步机制。 - 两套模块都通过 `AutoConfiguration.imports` 注册各自配置,但响应式 engine 不依赖阻塞式 engine,响应式缓存也不依赖阻塞式缓存。 新增缓存能力时,应分别验证阻塞式和响应式实现,不要通过引入另一套运行时来复用实现。 ## 文档维护要求 当代码结构变化时,按下面顺序更新文档: 1. 先更新 [module-inventory.md](/docs/module-inventory/),确保模块声明与聚合 `pom.xml` 一致。 2. 再更新本文档,说明能力状态、推荐口径和已知缺口是否变化。 3. 如果影响业务开发主路径,再更新 [module-capability-map.md](/docs/module-capability-map/)、[crud-query-guide.md](/docs/crud-query-guide/)、[r2dbc-code-guide.md](/docs/r2dbc-code-guide/) 和 [business-module-layout-guide.md](/docs/business-module-layout-guide/)。 4. 如果影响项目开发规则,再更新 [development-contract.md](/docs/development-contract/) 和 [code-style-guide.md](/docs/code-style-guide/)。 > 最后核验:2026-09-19 --- # Java 模块目录结构指南 来源:docs/core/java-module-directory-guide.md SHA-256:784eb27a3e652028dccd8fdf878b3f253f4cc6b42cd03dc11039af13f7a15ac2 网站:/docs/java-module-directory-guide/ ## 文档定位 本文档约定的是 `ineed-core` 及同类 Java Maven 仓库中的“模块物理目录结构”。 它回答的是: - 一个目标目录到底应该是聚合模块、BOM 模块、普通代码模块,还是业务聚合模块 - 什么类型的模块应该有 `src` - 什么类型的模块只能有 `pom.xml` 和子模块目录,不能生成 `src` 如果你要创建的是业务聚合模块骨架,继续读 [business-module-layout-guide.md](/docs/business-module-layout-guide/)。 如果你要确认当前仓库真实声明了哪些模块,继续读 [module-inventory.md](/docs/module-inventory/)。 ## 先判断模块类型,再决定目录结构 开发者、代码生成器或自动化工具在创建 Java 模块前,必须先判断目标属于哪一类。 当前至少区分四类: | 类型 | 典型示例 | `packaging` | 是否应有 `src` | |------|----------|-------------|----------------| | 根聚合模块 | `ineed-core/` | `pom` | 否 | | 分组聚合模块 | `ineed-core-modules/`、`ineed-core-reactive-modules/`、`ineed-core-tests/`、`ineed-core-starters/` | `pom` | 否 | | BOM 模块 | `ineed-core-bom/` | `pom` | 否 | | 普通代码模块 | `ineed-core-base/`、`ineed-core-data-base/`、`ineed-rdb-metadata/`、`ineed-core-webmvc/`、`ineed-core-data-jpa/`、`ineed-starter-druid/`、`ineed-core-test-webmvc/` | 通常为 `jar` 默认值 | 是 | 额外说明: - 业务聚合模块属于“特殊聚合模块”,目录结构继续按 [business-module-layout-guide.md](/docs/business-module-layout-guide/) 处理 - 不允许在还没判断模块类型前,就机械地创建 `src/main/java` ## 根聚合模块 根聚合模块只负责: - 声明顶级 `` - 统一版本、插件和依赖管理 - 作为整个仓库入口 标准形态: ```text {repo-root}/ pom.xml README.md docs/ {child-module-a}/ {child-module-b}/ ``` 规则: - 根聚合模块本身不放 `src/main/java` - 不在根聚合模块下直接放业务 Java 源码 - 新增顶层模块前,先确认是否真的需要进入根 `pom.xml` 的 `` ## 分组聚合模块 分组聚合模块本身是 `packaging=pom` 的父级目录,负责聚合一组同类型子模块。 例如: - `ineed-core-modules` - `ineed-core-reactive-modules` - `ineed-core-tests` - `ineed-core-starters` 标准形态: ```text {aggregate-module}/ pom.xml {child-module-a}/ {child-module-b}/ {child-module-c}/ ``` 规则: - 分组聚合模块自身不生成 `src/main/java` - 分组聚合模块自身不直接承载 Java 源码、资源文件、测试代码 - 真正的源码只能放在其子模块里 - 子模块目录必须和 `pom.xml` 中 `` 声明保持一致 ## BOM 模块 BOM 模块只负责 `dependencyManagement`,不负责承载运行时代码。 例如: - `ineed-core-bom` 标准形态: ```text {bom-module}/ pom.xml ``` 规则: - BOM 模块不生成 `src/main/java` - BOM 模块不生成 `src/test/java` - BOM 模块不承载业务实现、配置类、测试类、样板代码 - 不要把 BOM 模块误当成普通代码模块去补目录骨架 ## 普通代码模块 普通代码模块是实际承载 Java 源码、资源和测试的模块。 例如: - `ineed-core-base` - `ineed-core-data-base` - `ineed-rdb-metadata` - `ineed-core-context` - `ineed-core-engine` - `ineed-core-webmvc` - `ineed-core-data-core` - `ineed-core-data-jpa` - `ineed-core-data-jdbc` - `ineed-core-data-mybatis-flex` - `ineed-core-context-reactive` - `ineed-core-engine-reactive` - `ineed-core-webflux` - `ineed-core-data-core-reactive` - `ineed-core-data-r2dbc` - `ineed-starter-druid` - `ineed-core-test-webmvc` - `ineed-core-test-webflux` 标准物理目录模板: ```text {code-module}/ pom.xml src/ main/ java/ .gitkeep resources/ .gitkeep test/ java/ .gitkeep resources/ .gitkeep ``` 规则: - 所有普通代码模块都应统一具备 `src/main/java`、`src/main/resources`、`src/test/java`、`src/test/resources` - 即使暂时没有源码,也应先保留四类目录;空目录通过 `.gitkeep` 入库 - 不允许只创建 `src/main/java` 而忽略测试目录 - 不允许把 `entity`、`controller`、`service`、`repository` 直接散落在模块根目录 - 如果同一代码模块中同时暴露阻塞式与响应式共享 `service` 契约,统一使用 `service/` 与 `service/reactive/` 分包 - `.settings`、`target`、IDE 文件、构建输出不属于需要生成或提交的目录骨架 ## 业务聚合模块是特殊情况 业务聚合模块不是单个代码模块,而是“聚合目录 + 多个子模块”的组合。 标准形态继续按 [business-module-layout-guide.md](/docs/business-module-layout-guide/): ```text {business-aggregate}/ pom.xml {business-aggregate}-core/ {business-aggregate}-engine/ {business-aggregate}-jpa/ {business-aggregate}-jdbc/ {business-aggregate}-mybatis-flex/ {business-aggregate}-webmvc/ {business-aggregate}-engine-reactive/ {business-aggregate}-r2dbc/ {business-aggregate}-webflux/ ``` 关键点: - 业务聚合模块本身是聚合目录,不是直接写 Java 源码的地方 - 真正放源码的是各个业务子模块 - `core` 是共享契约层,不再单独创建 `{module}-reactive-core` ## 共享 service 契约的目录落点 如果业务聚合模块中的 `core` 子模块需要同时暴露阻塞式与响应式共享 `service` 契约,推荐物理落点如下: ```text {business-aggregate}-core/ src/main/java/com/ineed/{domain}/{object}/service/ src/main/java/com/ineed/{domain}/{object}/service/reactive/ {business-aggregate}-webmvc/ src/main/java/com/ineed/{domain}/{object}/service/ {business-aggregate}-webflux/ src/main/java/com/ineed/{domain}/{object}/service/reactive/ ``` 规则: - `core` 子模块中的 `service/` 与 `service/reactive/` 放共享接口定义 - `webmvc` / `webflux` 子模块中的对应目录只放本线适配实现,不重复定义另一套共享契约 - 不要把跨模块复用的共享接口只放进某个 `webmvc` 或 `webflux` 实现模块 ## 模块类型与目录结构的映射规则 创建新模块时,必须满足下面的映射: | 判断结果 | 应创建的目录 | |----------|--------------| | 根聚合模块 | `pom.xml` + 子模块目录,不创建 `src` | | 分组聚合模块 | `pom.xml` + 子模块目录,不创建 `src` | | BOM 模块 | 仅 `pom.xml`,不创建 `src` | | 普通代码模块 | `pom.xml` + 标准四类 `src` 目录 | | 业务聚合模块 | `pom.xml` + 业务子模块目录;各子模块内部再创建标准四类 `src` 目录 | ## 常见错误点 下面这些都是错误生成方式: - 把 `packaging=pom` 的聚合模块生成成带 `src/main/java` 的代码模块 - 在 `ineed-core-modules/`、`ineed-core-reactive-modules/`、`ineed-core-tests/`、`ineed-core-starters/` 下面直接写源码 - 给 `ineed-core-bom/` 生成 `src` 目录 - 把业务聚合目录当成单模块,在聚合目录根下直接放 `controller`、`service`、`entity` - 忽略 `src/test/java` 和 `src/test/resources` - 重复发明 `{module}-reactive-core` - 把阻塞式与响应式共享契约混放在同一个 `service` 根包,或者不使用 `service/reactive/` - 把跨模块共享接口只定义在 `webmvc` / `webflux` 实现模块中 - 没有先看 `pom.xml` 中 `` 和 ``,就凭名字猜目录结构 ## 创建前自检 创建任何 Java 模块前,至少先检查: 1. 目标目录是根聚合模块、分组聚合模块、BOM 模块、普通代码模块,还是业务聚合模块 2. 目标 `pom.xml` 是否是 `packaging=pom` 3. 如果是聚合/BOM 模块,是否错误地准备生成 `src` 4. 如果是普通代码模块,是否已补齐四类 `src` 目录 5. 如果是业务聚合模块,是否转去遵循 [business-module-layout-guide.md](/docs/business-module-layout-guide/) 6. 子模块目录名是否与父级 `` 声明一致 7. 是否错误把 IDE / 构建输出目录当成应入库骨架 如果以上检查未完成,不应直接创建目录结构。 ## 阅读建议 - 想确认当前仓库有哪些真实模块,读 [module-inventory.md](/docs/module-inventory/) - 想创建业务聚合模块,读 [business-module-layout-guide.md](/docs/business-module-layout-guide/) - 想约束业务代码生成,结合 [development-contract.md](/docs/development-contract/) 和 [code-style-guide.md](/docs/code-style-guide/) 一起使用本文档 > 最后更新:2026-05-15 --- # 配置参考 来源:docs/reference/configuration.md SHA-256:098fd124e19422116a2e00b2d49ee5ab2a17e03bf80cedcdd6280990f8344592 网站:/docs/configuration/ 适用 Core `3.0.0-SNAPSHOT`,2026-09-19 对照源码。表中“默认”指字段初始值,应用 YAML、环境变量或自定义配置可能覆盖它。Core 是库,没有独立的生产地址或必填数据库凭据。 ## 应用配置 来源:[ApplicationProperties](/references/#source-a9e800e5a6537a73),前缀 `application`。 | 配置 | 默认值 | 使用说明 | | --- | --- | --- | | `locale` | `zh_CN` | 默认语言 | | `time-zone` | `Asia/Shanghai` | 应用时区 | | `api-prefix` | `CoreApiUrls.API_PREFIX` | 默认 API 前缀;实际地址结合 Controller 与应用配置 | | `ws-prefix` | `CoreApiUrls.WS_PREFIX` | WebSocket 前缀 | | `show-api-doc` | `false` | API 文档展示开关,仍受应用安全策略约束 | | `application-key` | `null` | 应用标识,由装配项目提供 | | `multi-tenant` | `false` | 是否启用多租户模式 | | `branch-context-enabled` | `false` | 是否启用经营场所上下文 | | `tenant-key` | `null` | 单租户配置下的租户标识;不能用任意值代替真实租户语义 | | `tenant-name` | `IneedFull技术部` | 配置中的默认名称 | | `is-development` | `false` | 开发模式,具体身份接入依赖安全模块 | | `i18n-enabled` | `true` | i18n 开关 | | `cors` | `null` | 未配置;实际 CORS 行为还要查看 Web 配置 | | `temp-directory` | `null` | 临时目录覆盖项 | Spring Boot 使用 YAML/properties 与环境覆盖。不要把字段允许 `null` 理解成所有业务流程都能在缺失该配置时运行;租户等规则在实际使用时会校验上下文。 以下只是应用级示例,不能替代数据库、安全或缓存的装配: ```yaml application: application-key: local-training multi-tenant: true is-development: false show-api-doc: false ``` ## 外部设施与条件 | 能力 | 配置/依赖入口 | 接入前提 | | --- | --- | --- | | JDBC/JPA/MyBatis-Flex | 应用 `spring.datasource.*` | 驱动、DataSource、目标表和正确事务管理器 | | R2DBC | 应用 `spring.r2dbc.*` | 响应式驱动、ConnectionFactory、响应式事务配置 | | 阻塞服务缓存 | Spring `CacheManager` + Core 缓存配置 | 缓存管理器存在;Service 显式声明缓存规格 | | 响应式服务缓存 | Reactive 缓存配置与 store | 匹配的响应式 Redis/store 依赖,不能混入阻塞运行时 | | 业务集成事件 | `ineed.integration.*` | 开关、资源 key 与监听器,详见事件指南 | | 会话、身份与授权 | `ineed-security` 所属配置 | Core 本身不能替代安全模块装配 | `ineed-dev` 四个启动器使用 `MYSQL_HOST`、`MYSQL_PORT`、`MYSQL_USER`、`MYSQL_PASSWORD`、`MYSQL_DATABASE`、`MYSQL_QUARTZ_DATABASE`、`REDIS_HOST`、`REDIS_PORT`、`REDIS_DATABASE` 等环境变量。这些属于开发应用配置,查看 [ineed-dev README](/references/#source-0dd5f2662bc35434) 和所选启动器。集成测试可能使用另一组专用属性,运行前读测试类。 生产值由部署环境提供,不把密码、会话 token 或私钥放入教程。开发配置通过独立 profile 或显式环境变量覆盖,不能直接用于生产。 ## 配置不生效时 1. 核对启动器和 classpath 是否选择了正确运行时。 2. 核对生效 profile、属性名和环境覆盖,检查必需 Bean。 3. 区分 Core 属性与业务模块属性,避免把某模块配置误写成 `application.*`。 4. 参考[运行与排错](/docs/run-and-verify/),用最窄配置测试验证,不靠修改多个开关试错。 源码依据:[ApplicationConfiguration](/references/#source-f50b386d7972eda9)、[阻塞缓存配置](/references/#source-a04b57373ab0e9fe)、[响应式缓存配置](/references/#source-0578bfe88273d76c)。 ## 文档开关的路径匹配 MVC 与 WebFlux 的文档过滤器按 Spring 路径解析语义单次解码并移除矩阵参数,再匹配文档地址;关闭时 `/v3/%61pi-docs` 与 `/v3/api-docs` 同样返回 404。过滤同时考虑 `scalar.path`、`springdoc.api-docs.path` 及其 YAML 地址。应用安全链仍独立生效。验证见 [MVC 路由测试](/references/#source-2dfebbb3638bdbd4)与 [WebFlux 路由测试](/references/#source-0729681b0fd1962b)。 --- # 扩展点速查 来源:docs/reference/extension-points.md SHA-256:009ef804f91b9a84483193695fcb9ac41dd0c1c090d2c33cb0324937c5b99eca 网站:/docs/extension-points/ 先使用标准能力,再选择真实扩展点。下表是源码导航,泛型与完整签名以链接源码为准。 | 要做什么 | 入口与参考 | 注意事项 | | --- | --- | --- | | 接入创建/编辑 | [CRUD 指南](/docs/crud-query-guide/)中的 Controller/Service/Manager 矩阵 | 不复制整套生命周期;converter 复用父接口 | | 扩展查询 | [JDBC](/docs/jdbc/)、[JPA](/docs/jpa-querydsl-code-guide/)、[MyBatis-Flex](/docs/mybatis-flex-code-guide/)、[R2DBC](/docs/r2dbc-code-guide/) | 各栈表达式不同,租户/删除过滤需验证 | | 按业务键创建或刷新 | [分层指南](/docs/custom-business-layering-guide/)中的 CreateOrRefreshManager | 不是数据库原子 upsert,需考虑并发 | | 删除前保护/删除后清理/恢复 | [删除钩子表](/docs/delete-hook-and-logging-guide/) | `beforeLogicDelete`、`afterLogicDelete`、`beforePhysicsDelete`、`afterPhysicsDelete`、`afterRecovery` 职责不同 | | 共享跨模块契约 | [分层指南](/docs/custom-business-layering-guide/)共享扩展章节 | core 中定义、阻塞/响应式分包、明确实现方 | | 增加状态能力 | [状态指南](/docs/data-status/) | 最终实体声明,所有接口与权限一致 | | 当前操作身份与目标租户 | [上下文指南](/docs/tenant-and-context/) | 异步/响应式必须正确传播与清理 | | Service 缓存 | [缓存指南](/docs/cache/) | 明确 key 维度、代理和失效时序 | | 生命周期事件 | [集成事件](/docs/integration-event-guide/) | 先确认已有发送点,避免重复发布 | | 统一日志/异常 | [日志与异常规范](/docs/delete-hook-and-logging-guide/) | 异常继续传播、敏感字段不记录 | 动态注册、请求参数和统一返回详见[CRUD 主路径](/docs/crud-query-guide/)。若 Core 没有合适扩展点,先明确需要解决的业务行为和边界,不通过虚构一个同名 API 继续生成代码。 --- # Ineed-Core 集成事件接入指南 来源:docs/core/integration-event-guide.md SHA-256:db59ea904c0567b5d66946461941d64b5e16551e4eb4790ae1c1c573d633b75b 网站:/docs/integration-event-guide/ ## 文档定位 本文档说明 `ineed-integration` 当前已经落地的接入方式、配置规则、事件模型和业务模块的推荐用法。 当前范围覆盖标准 `Manager` 主链路中的 CRUD 和数据状态事件: - `create` - `update` - `logicDelete` - `physicsDelete` - `dataStatusChange` 已接入的执行线包括: - 阻塞式 `ineed-core-data-core` - 响应式 `ineed-core-data-core-reactive` 暂不覆盖: - `recovery` - WebHook adapter - Spring Cloud adapter - outbox / 可靠投递 ## 1. 当前模块关系 - `ineed-integration` 负责事件模型、开关、默认 key 解析、Spring 事件发布与蒸馏监听。 - `ineed-core-data-core` 已在标准同步 `Manager` 默认实现里接入事件发送。 - `ineed-core-data-core-reactive` 已在标准响应式 `Manager` 默认实现里接入同一套事件发送。 - 业务模块 当前不需要改 `Service` 主流程,只需要按需配置开关、监听事件,或者覆盖默认 key 解析。 ## 2. 当前接入前提 ### 2.1 标准启动方式 如果应用主类使用: ```java @SpringBootApplication(scanBasePackages = "com.ineed") ``` 当前可以直接扫描到: - `IntegrationConfiguration` - `BusinessEventPublisher` - `DistilledBusinessEventListener` 这是仓库里当前 demo / test project 的默认接法。 ### 2.2 非标准扫描方式 如果应用没有扫描 `com.ineed` 根包,需要显式接入: ```java import org.springframework.context.annotation.Import; import com.ineed.integration.configuration.IntegrationConfiguration; @Import(IntegrationConfiguration.class) public class YourApplication { } ``` 或者把 `com.ineed.integration` 加入你的扫描路径。 ## 3. 当前配置方式 ### 3.1 YAML 示例 下面示例是“开启发送”的示例。 真实默认值仍然是: - `ineed.integration.enabled = false` - `ineed.integration.internal.enabled = false` - `ineed.integration.distilled.enabled = false` ```yaml ineed: integration: enabled: true internal: enabled: true keys: security.member-identity: true distilled: enabled: true keys: security.member-identity: true ``` ### 3.2 Properties 示例 ```properties ineed.integration.enabled=true ineed.integration.internal.enabled=true ineed.integration.internal.keys[security.member-identity]=true ineed.integration.distilled.enabled=true ineed.integration.distilled.keys[security.member-identity]=true ``` ### 3.3 判断规则 事件是否会进入发布链,当前按下面顺序判断: 1. `ineed.integration.enabled` 必须为 `true` 2. 对应通道 `enabled` 必须为 `true` 3. 对应 `keys[eventKey]` 必须为 `true` 未配置的 key 默认视为关闭。 这里的 `eventKey` 是兼容命名。Manifest 配置了 `resourceKey` 时,发布链使用 `resourceKey` 作为 key;旧 Manifest 只配置 `eventKey` 时仍按旧 key 兼容。 ### 3.4 一个容易忽略的点 当前 `shouldPublishInternal(eventKey)` 的规则是: - 只要内部通道开启了该 key,就发布内部事件 - 只要蒸馏通道开启了该 key,也会先发布内部事件 这是当前的有意设计。 原因是蒸馏事件本身依赖 `InternalBusinessEvent`,并且蒸馏监听器要在事务提交后再处理。 ## 4. 默认资源键规则 当前默认解析器是 `DefaultBusinessEventDefinitionResolver`。 规则如下: - `sourceModule` 取实体包名中 `com.ineed` 后面的第一个段 - `aggregateType` 取实体简单类名并转换为 `snake_case` - `resourceKey` 注册后使用 Manifest 中的业务资源键;未注册时回退为 `类名.操作后缀` - `operationKey` 取 `BusinessOperationType` 的资源操作键,如 `create`、`update` - `eventKey` 兼容字段;有 `resourceKey` 时等同 `resourceKey`,旧 Manifest 仅配置 `eventKey` 时继续使用旧值 例如: - 实体类:`com.ineed.security.authorization.memberidentity.jdbc.entity.MemberIdentity` - 操作:`UPDATE` - 默认 resourceKey/eventKey:`com.ineed.security.authorization.memberidentity.jdbc.entity.MemberIdentity.updated` - 默认 operationKey:`update` 如果实体类名本身带后缀,当前也会保留下来。 例如: - 实体类:`MemberIdentityEntity` - 默认聚合名:`member_identity_entity` 如果这个默认规则不符合业务语义,可以覆盖 `BusinessEventDefinitionResolver`。 ## 5. 当前事件模型 ### 5.1 顶层结构 内部事件和蒸馏事件共用同一份 `BusinessEventMessage`,核心字段包括: - `eventId` - `eventKey`(兼容字段,推荐与 `resourceKey` 保持一致) - `resourceKey` - `operationKey` - `aggregateType` - `operationType` - `entityId` - `tenantKey` - `sourceModule` - `sourceType` - `occurredAt` - `payload` ### 5.2 当前 CRUD payload 当前 `payload` 类型是 `CrudBusinessEventPayload`,包含: - `entityId` - `before` - `after` - `deleteMode` ### 5.3 当前各操作的实际负载 `CREATE` - `before = null` - `after = 创建后的 VO` - `deleteMode = null` `UPDATE` - `before = 修改前 VO` - `after = 修改后 VO` - `deleteMode = null` `LOGIC_DELETE` - `before = null` - `after = null` - `deleteMode = LOGIC` `PHYSICS_DELETE` - `before = null` - `after = null` - `deleteMode = PHYSICS` 删除事件现在只保证: - `entityId` - `tenantKey` - `operationType` - `deleteMode` 如果后面要补删除前快照,再单独增强。 ### 5.4 DataStatus payload 数据状态变化使用 `DataStatusBusinessEventPayload`,字段固定为: - `id` - `action`:追加状态位为 `append`,移除状态位为 `subtract` - `statusFeature`:`DataStatusFeature` 枚举名,例如 `ENABLED` - `beforeStatus`:变化前的完整 byte 状态;实体原值为 `null` 时按 `0` 处理 - `afterStatus`:变化后的完整 byte 状态 `Manager` 先读取并校验全部目标,再比较完整状态。只有状态位实际变化的目标才会进入唯一状态处理、持久化和事件发布;空编号集合、全部幂等的命令以及未注册或关闭的事件 key 都不会发送事件。批量操作对每个实际变化的目标发送一条事件。 ## 6. 事件时序 阻塞式主链路当前时序如下: 1. `Manager` 主流程完成数据变更 2. 立即发布 `InternalBusinessEvent` 3. `DistilledBusinessEventListener` 用 `@TransactionalEventListener(phase = AFTER_COMMIT)` 监听 4. 主事务提交成功后,才发布 `DistilledBusinessEvent` 这意味着: - 内部事件是事务内的 Spring 事件 - 蒸馏事件是提交后的 JVM 内事件 - 当前不保证跨进程可靠送达 响应式主链路使用同一套 `IntegrationEventSupport` 发布事件: - 事件在 repository 保存 / 删除链路完成、`afterXxx` 钩子完成之后发布 - 数据状态事件在对应 append / subtract 持久化完成之后发布 - 发布动作发生在当前 reactive subscription 内,不要求业务 `Service` 手工补发 - 未订阅时不会查询目标、修改状态或发布事件 - 如果业务链路接入 reactive transaction,`DistilledBusinessEvent` 只在事务提交后发送,回滚时不发送 - 当前不承诺跨进程可靠投递,也不承诺并发写入下的可串行化行为 ## 7. 业务模块怎么监听 ### 7.1 模块内协作监听内部事件 如果你要做同 JVM 的模块内协作,并且需要拿到内部标准消息,可以监听 `InternalBusinessEvent`: ```java import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import com.ineed.integration.event.BusinessEventMessage; import com.ineed.integration.event.InternalBusinessEvent; @Component public class MemberInternalEventListener { @EventListener public void onInternalEvent(final InternalBusinessEvent event) { final BusinessEventMessage message = event.getMessage(); if (!"security.member-identity".equals(message.getResourceKey()) || !"create".equals(message.getOperationKey())) { return; } // 模块内协作逻辑 } } ``` ### 7.2 跨模块或外发前置监听蒸馏事件 如果你要做 WebHook、对外同步、跨模块桥接,优先监听 `DistilledBusinessEvent`: ```java import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import com.ineed.integration.event.BusinessEventMessage; import com.ineed.integration.event.DistilledBusinessEvent; @Component public class MemberPrivilegeBridgeListener { @EventListener public void onDistilledEvent(final DistilledBusinessEvent event) { final BusinessEventMessage message = event.getMessage(); if (!"security.member-identity".equals(message.getResourceKey()) || !"create".equals(message.getOperationKey())) { return; } // 这里再去调用权益中心 adapter / webhook / stream bridge } } ``` 推荐原则: - 模块内同步协作:优先内部事件 - 跨模块、外发、集成边界:优先蒸馏事件 ## 8. 业务模块怎么覆盖默认 key 如果默认 key 不够稳定,或者你不想把实体命名暴露给事件 key,可以自己提供 `BusinessEventDefinitionResolver` Bean: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.ineed.integration.event.BusinessEventDefinitionResolver; import com.ineed.integration.event.BusinessOperationType; import com.ineed.integration.event.DefaultBusinessEventDefinitionResolver; import com.ineed.integration.event.ResolvedBusinessEventDefinition; import com.ineed.security.authorization.memberidentity.jdbc.entity.MemberIdentity; @Configuration public class MemberEventDefinitionConfiguration { @Bean public BusinessEventDefinitionResolver memberBusinessEventDefinitionResolver() { final BusinessEventDefinitionResolver delegate = new DefaultBusinessEventDefinitionResolver(); return (entityType, operationType) -> { if (MemberIdentity.class.equals(entityType)) { return memberIdentityDefinition(operationType); } return delegate.resolve(entityType, operationType); }; } private static ResolvedBusinessEventDefinition memberIdentityDefinition(final BusinessOperationType operationType) { final ResolvedBusinessEventDefinition definition = new ResolvedBusinessEventDefinition(); definition.setSourceModule("security"); definition.setAggregateType("member_identity"); definition.setOperationType(operationType); definition.setResourceKey("security.member-identity"); definition.setOperationKey(operationType.getOperationKey()); definition.setEventKey(definition.getResourceKey()); return definition; } } ``` 更推荐的实际做法是: - 业务模块内部自己封装一层 resolver - 未命中的实体继续委托给默认 resolver 这样不会把所有规则都重写掉。 ## 9. 当前已经接入的发送点 当前标准链路的发送点在 `Manager` 主流程中,不在 `Service` 中。 阻塞式发送点: - `CreateManager.save(...)` - `UpdateManager.update(...)` - `LogicDeleteManager.logicDelete(...)` - `PhysicsDeleteManager.physicsDelete(...)` 响应式发送点: - `com.ineed.core.data.reactive.manager.crud.CreateManager.save(...)` - `com.ineed.core.data.reactive.manager.crud.UpdateManager.update(...)` - `com.ineed.core.data.reactive.manager.crud.LogicDeleteManager.logicDelete(...)` - `com.ineed.core.data.reactive.manager.crud.PhysicsDeleteManager.physicsDelete(...)` 并且发布动作都放在: - 业务 `afterXxx` 钩子执行完成之后 - 返回结果之前 这样可以避免业务覆写 `afterXxx` 时绕过底座事件。 ## 10. 当前不建议的用法 - 不要在业务 `Service` 里手工重复发同一类标准 CRUD 事件 - 不要把开关分散到业务模块各自配置里 - 不要直接依赖删除事件里存在完整快照,当前没有这个保证 - 不要把 `DistilledBusinessEvent` 当成可靠消息中间件 ## 11. 下一阶段候选 后续如果继续推进,下一批更自然的工作是: - `DataStatus` 事件 - `recovery` 事件 - 删除前快照增强 - WebHook adapter - Spring Cloud adapter - outbox / 可靠投递 > 最后更新:2026-05-15 --- # Constant、ErrorCode 与 I18n 约定 来源:docs/core/constant-errorcode-i18n-guide.md SHA-256:8816c08a537598f19194f321e95a1478d7728b115e77025b36c263d1eb1cadc2 网站:/docs/constant-errorcode-i18n-guide/ ## 文档定位 本文档定义业务模块中的常量、模块内错误码和国际化资源的统一规则,包含: - 框架通用错误码与业务模块私有错误码的编号边界 - 业务模块 `{Object}Constant`、`Urls`、`Resource`、`ErrorCode` 的落点 - 应用错误码权威登记入口(避免不同模块重复占用) - i18n 资源文件的格式、顺序和 key 约定 如果需要直接复制代码骨架,使用 [module-constant-i18n-template.md](/docs/module-constant-i18n-template/)。 --- ## 一、框架通用错误码(根级 ErrorCode) ### 总体规则 - 框架通用错误码使用**负数数字字符串**,定义在 `ineed-core-base/src/main/java/com/ineed/base/exception/ErrorCode.java` - 业务模块私有错误码使用 **6 位正整数数字字符串** - 新增业务错误码必须按模块分段分配,不允许随意占号 - 错误文案占位符统一使用 `MessageFormat` 风格:`{0}`、`{1}`、`{2}` ### 框架保留区间 | 区间 | 类型 | 说明 | |------|------|------| | `-1 ~ -99` | 系统通用错误 | 未知错误、不支持操作、非法操作、配置缺失、任务运行中 | | `-100 ~ -199` | 数据通用错误 | 数据重复、不存在、已删除、被占用、版本冲突、固定/锁定/保留/同步数据限制、树结构限制 | | `-200 ~ -299` | 请求/参数错误 | 参数为空、参数取值错误、参数超过限制等非 Bean Validation 场景 | | `-300 ~ -399` | 数据库/基础设施错误 | SQL 执行、文件处理、网关调用等底层或外部基础设施错误 | | `-400 ~ -499` | 租户/上下文错误 | 租户无效、上下文缺失、上下文不匹配等 | | `-500 ~ -599` | 验证/校验错误 | Bean Validation、领域校验、验证码错误 | | `-800 ~ -999` | 认证/会话/授权错误 | 登录、账号状态、会话、密码、凭证、访问授权等通用安全错误 | 子段划分: `-800 ~ -999` 内部: | 区间 | 类型 | |------|------| | `-800 ~ -849` | 登录 / 账号 / 会话 | | `-850 ~ -879` | 密码 / 凭证 | | `-880 ~ -899` | 授权 / 访问控制 | `-500 ~ -599` 内部: | 区间 | 类型 | |------|------| | `-500 ~ -549` | Bean Validation / 参数校验 | | `-550 ~ -559` | 验证码 | ### 框架通用错误边界 以下场景应继续复用根级 `ErrorCode`: - 数据不存在、重复、被使用、版本冲突 - 参数为空、取值错误、Bean Validation 失败 - 租户上下文无效、未授权访问 - 账号密码错误、账号锁定/禁用/过期 以下场景应放入模块私有错误码: - 某个业务对象的特定规则 - 某个业务流程的状态机限制 - 某个模块私有的授权/密码治理/第三方集成错误 --- ## 二、应用模块错误码的权威来源 应用私有错误码的格式、范围、状态和新增流程以工作区 [design/standards/error-code-registry.md](/references/#source-6c67578a7e735914) 为唯一来源。Core 只维护框架通用错误机制与使用约定。 原本文中的 `BBNNNN` 分段规则和业务块号表已退出维护,不能用于新代码。实际业务域的编号方式不同,不根据旧模块名或示例数字推导可用号段。 新增错误时:先判断是否能复用通用错误 → 查全局登记与真实模块常量 → 按登记流程分配 → 同步常量、语言资源与测试。已发布错误不随意改号或复用。 ## 三、业务登记与迁移边界 本页不保留应用模块号段副本。UK/FK 约束继续使用 Core 通用错误与模块约束 i18n key,不为每个数据库约束创建私有错误码。 模板中的 `` 是占位符,必须替换为登记表确认过的真实号码。遇到历史代码与登记表不一致时,核实兼容策略并在所属模块修正,不为适配旧代码恢复另一套全局登记。 --- ## 四、Constant 分层边界 ### `CoreConstant`(框架级) `ineed-core-base/src/main/java/com/ineed/base/CoreConstant.java` 只承载跨模块稳定契约: - 根级 URL / WS 前缀 - 通用状态位(`YESNO`、`DataStatus`、`AccountStatus`) - 通用动作路径(`CoreConstant.Urls`) - 通用动作资源标识(`CoreConstant.ResourceKey`) - HTTP Header Key、上下文 Key 约束:不放具体业务对象的模块路径、私有资源标识、私有错误码。 ### `CoreDataConstant`(数据层) `ineed-core-data-base/src/main/java/com/ineed/core/data/CoreDataConstant.java` 承载跨数据栈共享的数据层常量: - 通用列名 `Columns` - 通用属性名 `Fields` - 通用数据层 SpEL `Spel` 约束:只放 JPA / JDBC / MyBatis-Flex / R2DBC 共享契约,不塞入业务域私有字段。 ### 业务模块 `{Object}Constant` 放在 `core` 层根包下的 `{Object}Constant.java` 中: ```java public final class UserConstant { private UserConstant() { super(); } public static final class Urls { public static final String USER = AuthorizationCoreConstant.Urls.AUTHORIZATION + "/user"; } public static final class Resource { public static final String KEY_USER = "authorization.user"; public static final String NAME_USER = "用户"; } public static final class ErrorCode { public static final String USER_GROUP_ALREADY_ASSIGNED = ""; } } ``` 默认允许的嵌套类:`Urls`、`Resource`、`ErrorCode`。按需增加:`CacheKey`、`Topic`、`HeaderKey`、`PropertyKey`。 不放:临时变量、日志文案、SQL 片段、展示文案、magic value。 ### `Urls` 约定 只定义模块的稳定资源路径: - 优先复用上级常量组合路径 - 通用动作后缀复用 `CoreConstant.Urls` - 不重复声明框架已稳定的动作路径(`/enable`、`/disable`、`/order/bottom` 等) ### `Resource` 约定 集中表达资源标识和资源展示名: - `KEY_*` 使用小写英文和点分层,如 `authorization.user` - `KEY_*` 子资源沿用点分层,如 `platform.tenant.config` - `NAME_*` 放业务展示名,用于 `getResourceName()`、OpenAPI 摘要和权限资源展示 - 不把按钮文案放进 `Resource` - 不重复定义通用动作资源名(`create`、`update`、`delete` 等) --- ## 五、模块内 ErrorCode 落点 同一 `{Object}Constant.ErrorCode` 中的编号必须落在当前模块预留块号内,不混入根级负数编号。 抛出示例: ```java throw StatefulException.of(UserConstant.ErrorCode.USER_GROUP_ALREADY_ASSIGNED); throw StatefulException.of(UserConstant.ErrorCode.BUILTIN_USER_ROLE_CANNOT_REMOVE, roleName); ``` ### 与数据库约束消息的关系 唯一索引、外键等约束不建议为每个约束新增错误码: - 错误码使用框架通用的数据重复或数据库约束错误,不为每个数据库约束新增模块私有错误码 - 唯一约束文案通过 `{tableName}.uk.{dbConstraintName}` 映射 - 外键约束必须区分引用目标不存在、引用目标正在被使用两类语义 - 历史 `constraint.*` key 只作为兼容回退 示例: ```properties isec_permission.uk.uk_isec_permission_key=权限标识已经存在 isec_role_permission.fk.not-found.fk_role_permission_role=角色不存在,不能建立角色权限关系 isec_role_permission.fk.in-use.fk_role_permission_role=角色数据被角色权限关系使用,不能删除或更新 ``` ### 约束 i18n key 常量化 唯一约束和外键约束的 i18n key 必须沉淀到模块 `{Object}Constant` 中,业务代码只引用常量,不在 `Manager`、`Service`、`Repository` 或 `Mapper` 中散落字符串字面量。 固定使用一个内部类: ```java public static final class I18nKey { public static final String USER_ACCOUNT = "isec_user.uk.uk_isec_user_account"; public static final String USER_GROUP_USED_BY_USER = "isec_user_group.fk.in-use.fk_isec_user_group_user_id"; private I18nKey() { super(); } } ``` 使用示例。重复数据业务校验传实体类名和字段名,由 core 统一转换对象与字段文案;外键被占用类主动校验可以传 FK i18n key,直接输出对应约束文案: ```java throw StatefulException.of(ErrorCode.DATA_DUPLICATE, this.getEntityClass().getName(), "account"); throw StatefulException.of(ErrorCode.DATA_IN_USE, UserConstant.I18nKey.USER_GROUP_USED_BY_USER); ``` 约束: - `I18nKey` 只放代码需要主动引用的模块内 i18n key,当前主要包括唯一约束文案 key 和外键约束文案 key - UK 常量命名按业务含义表达唯一索引覆盖的字段,例如 `APP_VERSION`、`USER_ACCOUNT` - FK 常量命名按“引用关系 + 方向”表达,例如 `ROLE_USED_BY_ROLE_PERMISSION` - 常量值必须与 `messages*.properties` 和 `message.multiproperties` 中的 key 完全一致 - 常量值必须以数据库真实表名开头,末段必须使用数据库真实约束名,不得改成字段名、Java 属性名或业务别名 - 数据库约束错误继续使用框架通用错误码,例如 `ErrorCode.DATA_DUPLICATE`、`ErrorCode.DATA_IN_USE`;不要为了每个 UK/FK 新增模块私有错误码 --- ## 六、i18n 资源文件约定 ### 目录结构 ```text src/main/resources/ i18n/ineed/{module-segments}/ message.multiproperties messages.properties messages_en.properties messages_zh_TW.properties ``` - `message.multiproperties` 是编辑源,多语言并排维护 - 运行时实际加载的是 `messages*.properties` - `I18nConfiguration` 自动扫描 `classpath*:i18n/**/messages.properties` ### 生成顺序(固定) 1. Errors 2. Common 3. Entity name 4. Entity fields 5. Entity unique constraints 6. Entity foreign-key constraints `message.multiproperties` 中也必须使用同样顺序,通过 `Comment` 分组标注:`Errors`、`Common`、`Entity - Xxx`。 ### i18n Key 约定 **错误码文案**:key 直接使用错误码本身 ```properties =用户已经分配到该分组,不允许重复分配 ``` **对象与字段文案**:使用 `{basePackage}.{Entity}` 格式 ```properties com.ineed.config.dictionary.DictionaryDefinition=字典定义 com.ineed.config.dictionary.DictionaryDefinition.applicationKey=应用标识 ``` - `{basePackage}` 是业务基础包,不包含 `contract.ao`、`jpa.entity`、`jdbc.entity`、`mybatisflex.entity`、`r2dbc.entity` 等实现分层片段 - `{Entity}` 使用实体类简单名,保持 UpperCamelCase - `{fieldName}` 直接使用 Java 字段名 - 对象和字段 key 必须跨 `AO / Query / VO / Entity` 复用 - 新生成代码不得使用持久化实现类全名 key,例如 `com.ineed.security.authorization.accesscontrol.jdbc.entity.Role.rolePurpose` - `applicationKey` 仍用于 `META-INF/ineed/modules/*.json`、权限 `permissionKey`、菜单资源标识等业务标识,不作为对象/字段 i18n key 前缀 **数据库约束文案**: ```properties {tableName}.uk.{dbConstraintName}=唯一约束文案 {tableName}.fk.not-found.{dbConstraintName}=引用目标不存在时的外键文案 {tableName}.fk.in-use.{dbConstraintName}=引用目标正在被使用时的外键文案 ``` - `uk` 后缀、`fk.not-found` / `fk.in-use` 后缀末段都必须使用数据库返回的真实约束名 - `{tableName}` 必须使用数据库真实表名 - 外键 key 中的 `{tableName}` 必须是引用方表,也就是 FK 所在表;不要使用被引用目标表 - `not-found` 对应子表 `INSERT` / `UPDATE` 时引用的父表数据不存在,例如 MySQL `Cannot add or update a child row` - `in-use` 对应父表 `DELETE` / `UPDATE` 时仍被子表数据引用,例如 MySQL `Cannot delete or update a parent row` - `SQLException` 国际化解析会优先使用 SQL 错误中解析出的 `{tableName}` 精确查找;解析不到表名时才按约束名后缀兼容查找 - 历史 `{basePackage}.{Entity}.fk.{dbConstraintName}` 只作为兼容回退,新模块必须生成表名格式的两类语义化 FK key - 不再新增根级 `constraint.*` key ### 占位符格式 统一使用 `MessageFormat` 风格:`{0}`、`{1}`、`{2}`。不再新增 `%1`、`%s`、`{}`。 ### 生成要求 创建新业务模块时,如果存在以下任一情况,应同时生成 `{Object}Constant` 与 i18n 资源: - 新建了模块私有控制器资源根路径 - 新建了模块私有资源标识 - 新增了模块私有业务错误码 - 需要对象名、字段名、重复校验、校验失败的国际化文案 最少输出物: 1. `{Object}Constant.java` 2. `message.multiproperties` 3. `messages.properties` 4. `messages_en.properties` 5. `messages_zh_TW.properties` 生成顺序:先写 `{Object}Constant` → 接 Controller 注解 → 再补模块内 `ErrorCode` → 再补 `message.multiproperties` → 再补 `messages*.properties`。 --- ## 七、历史兼容区间 `i18n/ineed/error/v1/messages*.properties` 作为历史兼容资源保留,状态为 `legacy/frozen`: - 历史正数错误码可以继续保留 - 新模块不要参考历史编号无序追加 - 不要向 `error/v1` 继续追加新错误码 --- ## 八、自检清单 1. `Urls` 里没有重复定义框架通用动作后缀 2. `Resource` 同时承载 `KEY_*` 稳定资源标识和 `NAME_*` 业务展示名 3. 通用错误继续复用根级 `ErrorCode` 4. 新增业务错误码不是负数,且是 6 位正整数 5. 同一模块的新错误码落在同一个块号区间内 6. 模块号位已登记到 design 全局错误码登记表,代码中的错误码落在登记区间内 7. 模块私有业务错误码已配齐 `message.multiproperties` + 三份 `messages*.properties` 8. 新增文案占位符统一使用 `{0}`、`{1}` 风格 9. 对象名、字段名 i18n key 使用 `{basePackage}.{Entity}` 格式,字段追加 `.{fieldName}`;新代码不得使用持久化实现类全名 key 10. 控制器没有直接写资源路径/资源标识字面量 11. 所有出现在 `messages*.properties` 的模块私有 key 已同步到 `message.multiproperties` ## 九、I18n Java API 使用指南 ### 架构分层 I18n 按调用栈分为三层,核心逻辑共享,locale 解析按同步/响应式分别处理: ``` ineed-core-base I18nResolver 通用核心(需显式传入 Locale) I18nExceptionHandler 异常国际化(需显式传入 Locale) ineed-core-context I18nHandler 同步包装(从 ThreadLocal 获取 locale) └─ ContextHandler.getLocale() → OperateInfo.locale ineed-core-context- ReactiveI18nHandler 响应式包装(从 Reactor Context 获取 locale) reactive └─ ReactiveContextHandler.getLocale() → OperateInfo.locale ``` **Locale 来源链路**:请求头 `Content-Language` → `OperateInfoRequestResolver` 解析 → 写入 `OperateInfo.locale` → `WebMvcOperateInfoFilter`/`WebFluxOperateInfoFilter` 分别存入 ThreadLocal / Reactor Context。 ### 各层调用场景 | 调用方所在模块 | 使用类 | 返回类型 | |---|---|---| | Controller / Service / Manager(同步) | `I18nHandler` | 直接返回值 | | Controller / Service / Manager(响应式) | `ReactiveI18nHandler` | `Mono` | | 纯逻辑工具、不依赖上下文 | `I18nResolver`(显式传 Locale) | 直接返回值 | ### 同步代码(WebMVC / Service / Manager) ```java import com.ineed.base.context.I18nHandler; // 获取国际化消息 String message = I18nHandler.getMessage("error.code.key"); String messageWithArgs = I18nHandler.getMessage("parameter.incorrect", "昵称"); // 获取属性名称文案 String fieldLabel = I18nHandler.getPropertyMessage("com.ineed.demo.contract.ao.SampleAO", "nickname"); // 获取校验错误消息 String validationMsg = I18nHandler.getValidationMessage(validationResult); // 获取重复数据错误消息 String duplicateMsg = I18nHandler.getDuplicateMessage(entity, Set.of("account", "mobileNumber")); // 处理异常,返回国际化的 ErrorMessage ErrorMessage error = I18nHandler.handleException(exception); ``` ### 响应式代码(WebFlux) ```java import com.ineed.base.context.ReactiveI18nHandler; // 所有方法返回 Mono,在响应式链中安全使用 Mono message = ReactiveI18nHandler.getMessage("error.code.key"); Mono fieldLabel = ReactiveI18nHandler.getPropertyMessage( "com.ineed.demo.contract.ao.SampleAO", "nickname"); Mono validationMsg = ReactiveI18nHandler.getValidationMessage(validationResult); // 异常处理 ReactiveI18nHandler.handleException(exception) .flatMap(errorMessage -> ...); ``` ### 直接使用 I18nResolver(不依赖上下文) 仅当已有显式 `Locale` 且不需要从上下文获取时使用: ```java import com.ineed.base.i18n.I18nResolver; String message = I18nResolver.getMessage("key", locale); String property = I18nResolver.getPropertyMessage(className, property, locale); String duplicate = I18nResolver.getDuplicateMessage(object, fields, locale); String validation = I18nResolver.getValidationMessage(result, locale); ``` ### 异常处理 `I18nExceptionHandler` 也需要显式 `Locale`。优先使用 `I18nHandler.handleException()` / `ReactiveI18nHandler.handleException()`,它们会自动从上下文获取 locale: ```java // 同步 ErrorMessage error = I18nHandler.handleException(exception); // 响应式 Mono error = ReactiveI18nHandler.handleException(exception); ``` 仅在已有显式 Locale 时直接调用: ```java ErrorMessage error = I18nExceptionHandler.invokeException(exception, locale); ``` ### 服务层与 Manager 层使用 `I18nHandler` 和 `ReactiveI18nHandler` 都放在 `ineed-core-context` / `ineed-core-context-reactive` 模块中,不被 web 层独占。Service 和 Manager 层可以直接依赖这两个 context 模块获取国际化消息,无需反向依赖 web 模块。 ### 选择指引 1. **同步链**(WebMVC 请求线程、同步 Service/Manager):直接用 `I18nHandler` 2. **响应式链**(WebFlux 请求、Reactor 链内的 Service/Manager):用 `ReactiveI18nHandler`,返回 `Mono<>` 3. **没有请求上下文**(定时任务、消息消费者、工具类):直接传 `Locale` 给 `I18nResolver` 或 `I18nExceptionHandler.invokeException(exception, locale)` > 最后更新:2026-05-17 --- # 模块 Constant 与 I18n 样板 来源:docs/project-development/module-constant-i18n-template.md SHA-256:ad0b28bc0134172c97707a8f5cac364dc5f8daba94a0afea82f85a43cde48e81 网站:/docs/module-constant-i18n-template/ ## 文档定位 本文档提供一份可直接复制的模块级样板,用于新建业务模块时一次性补齐: - `{Object}Constant` - 控制器 `Urls` / `Resource` 接入 - 模块内 `ErrorCode` - `messages.properties` - `messages_en.properties` - `messages_zh_TW.properties` - `message.multiproperties` 如果你想先理解规则边界,先读 [constant-errorcode-i18n-guide.md](/docs/constant-errorcode-i18n-guide/)。 所有 `REGISTERED_ERROR_1/2` 及带尖括号的同名项都是模板占位符,必须从[应用错误码登记表](/references/#source-6c67578a7e735914)取得当前模块真实号码后统一替换。它们不能直接作为运行时错误码。XML `` 中使用不带尖括号的占位文本。样板用于表达结构;真实可编译例子见[示例索引](/docs/examples/)。 ## 适用场景 适合以下情况: - 新建一个业务对象模块 - 模块需要私有资源路径 - 模块需要私有资源标识 - 模块需要私有业务错误码 - 模块需要对象名、字段名、错误码的国际化文案 ## 样板一:`{Object}Constant.java` 路径建议: `{module}-core/src/main/java/com/ineed/{domain}/{object}/{Object}Constant.java` 示例: ```java package com.ineed.security.authorization.user; import com.ineed.security.authorization.core.AuthorizationCoreConstant; public final class UserConstant { private UserConstant() { super(); } public static final class Urls { public static final String USER = AuthorizationCoreConstant.Urls.AUTHORIZATION + "/user"; public static final String GROUP = AuthorizationCoreConstant.Urls.AUTHORIZATION + "/group"; private Urls() { super(); } } public static final class Resource { public static final String KEY_USER = "authorization.user"; public static final String NAME_USER = "用户"; public static final String KEY_GROUP = "authorization.group"; public static final String NAME_GROUP = "分组"; private Resource() { super(); } } public static final class I18nKey { public static final String USER_ACCOUNT = "isec_user.uk.uk_isec_user_account"; public static final String GROUP_NAME = "isec_group.uk.uk_isec_group_name"; public static final String USER_GROUP_USER_NOT_FOUND = "isec_user_group.fk.not-found.fk_isec_user_group_user_id"; public static final String USER_USED_BY_USER_GROUP = "isec_user_group.fk.in-use.fk_isec_user_group_user_id"; private I18nKey() { super(); } } public static final class ErrorCode { public static final String USER_GROUP_ALREADY_ASSIGNED = ""; public static final String BUILTIN_USER_ROLE_CANNOT_REMOVE = ""; private ErrorCode() { super(); } } } ``` 约束: - `Urls` 只放模块私有资源根路径 - `Resource` 同时放稳定资源标识和业务展示名,资源标识使用 `KEY_*`,资源名称使用 `NAME_*` - `I18nKey` 只放代码需要主动引用的模块内 i18n key,当前主要包括唯一约束和外键约束文案 key,常量值必须与三语 `messages*.properties` 保持一致 - `ErrorCode` 只放模块私有业务错误 - 通用动作路径继续复用 `CoreConstant.Urls` - 通用错误继续复用根级 `ErrorCode` - UK/FK 约束错误继续复用通用错误码,不为每个数据库约束新增模块私有错误码 ## 样板二:Controller 接入 示例: ```java @RestController @RequestMapping(UserConstant.Urls.USER) public class UserController implements ICrudController, IFindByIdController, ISearchController { private final UserService userService; @Override public UserService getService() { return userService; } @Override public String getResourceKey() { return UserConstant.Resource.KEY_USER; } @Override public String getResourceName() { return UserConstant.Resource.NAME_USER; } } ``` 约束: - `@RequestMapping` 优先使用 `UserConstant.Urls.*` - `getResourceKey()` 优先返回 `UserConstant.Resource.KEY_*` - `getResourceName()` 优先返回 `UserConstant.Resource.NAME_*`,用于 OpenAPI 公共摘要输出,例如把 `创建资源` 渲染为 `创建用户` - 新代码不要直接写 `"authorization.user"` 这类字面量 ## 样板三:Service / Manager 抛业务错误 示例: ```java if (alreadyAssigned) { throw StatefulException.of(UserConstant.ErrorCode.USER_GROUP_ALREADY_ASSIGNED); } ``` 带参数示例: ```java if (builtinRole) { throw StatefulException.of(UserConstant.ErrorCode.BUILTIN_USER_ROLE_CANNOT_REMOVE, roleName); } ``` 约束: - 模块私有业务规则使用 `{Object}Constant.ErrorCode` - 框架通用错误继续使用根级 `ErrorCode` - 不要在代码里直接硬编码中文错误文案 ## 样板四:`messages*.properties` 路径建议: ```text {module}-core/src/main/resources/ i18n/ineed/security/authorization/user/ message.multiproperties messages.properties messages_en.properties messages_zh_TW.properties ``` 默认语言: ```properties # Errors =用户已经分配到该分组,不允许重复分配 =系统内置用户的默认角色不允许移除:{0} # Common security.user.accountStatus.enabled=启用 # Entity - User com.ineed.security.authorization.user.User=用户 com.ineed.security.authorization.user.User.account=账号 com.ineed.security.authorization.user.User.nickname=昵称 isec_user.uk.uk_isec_user_account=用户已存在:账号不允许重复 # Entity - UserGroup com.ineed.security.authorization.user.UserGroup=用户分组关系 isec_user_group.fk.not-found.fk_isec_user_group_user_id=用户不存在,不能建立用户分组关系 isec_user_group.fk.in-use.fk_isec_user_group_user_id=用户数据被用户分组关系使用,不能删除或更新 # Entity - Group com.ineed.security.authorization.user.Group=分组 com.ineed.security.authorization.user.Group.name=分组名称 isec_group.uk.uk_isec_group_name=分组已存在:分组名称不允许重复 ``` 英文: ```properties # Errors =User has already been assigned to the group, duplicate assignment is not allowed =Default role of builtin user cannot be removed: {0} # Common security.user.accountStatus.enabled=Enabled # Entity - User com.ineed.security.authorization.user.User=User com.ineed.security.authorization.user.User.account=Account com.ineed.security.authorization.user.User.nickname=Nickname isec_user.uk.uk_isec_user_account=User already exists: account must be unique # Entity - UserGroup com.ineed.security.authorization.user.UserGroup=User Group Relation isec_user_group.fk.not-found.fk_isec_user_group_user_id=User does not exist and the user group relation cannot be created isec_user_group.fk.in-use.fk_isec_user_group_user_id=User data is referenced by user group relation and cannot be deleted or updated # Entity - Group com.ineed.security.authorization.user.Group=Group com.ineed.security.authorization.user.Group.name=Group Name isec_group.uk.uk_isec_group_name=Group already exists: group name must be unique ``` 繁体中文: ```properties # Errors =使用者已經分配到該分組,不允許重複分配 =系統內建使用者的預設角色不允許移除:{0} # Common security.user.accountStatus.enabled=啟用 # Entity - User com.ineed.security.authorization.user.User=使用者 com.ineed.security.authorization.user.User.account=帳號 com.ineed.security.authorization.user.User.nickname=暱稱 isec_user.uk.uk_isec_user_account=使用者已存在:帳號不允許重複 # Entity - UserGroup com.ineed.security.authorization.user.UserGroup=使用者分組關係 isec_user_group.fk.not-found.fk_isec_user_group_user_id=使用者不存在,不能建立使用者分組關係 isec_user_group.fk.in-use.fk_isec_user_group_user_id=使用者資料被使用者分組關係使用,不能刪除或更新 # Entity - Group com.ineed.security.authorization.user.Group=分組 com.ineed.security.authorization.user.Group.name=分組名稱 isec_group.uk.uk_isec_group_name=分組已存在:分組名稱不允許重複 ``` 顺序要求: 1. Errors 2. Common 3. Entity name 4. Entity fields 5. Entity unique constraints 6. Entity foreign-key constraints 对象、字段 key 使用业务基础包 key;数据库约束 key 使用表名 key: - `{basePackage}.{Entity}` - `{basePackage}.{Entity}.{fieldName}` - `{tableName}.uk.{dbConstraintName}` - `{tableName}.fk.not-found.{dbConstraintName}` - `{tableName}.fk.in-use.{dbConstraintName}` 其中 `{basePackage}` 是业务基础包,不包含 `contract.ao`、`contract.query`、`contract.vo`、`jpa.entity`、`jdbc.entity`、 `mybatisflex.entity`、`r2dbc.entity` 等实现分层片段;`{Entity}` 使用实体类简单名。`{tableName}` 必须使用数据库真实表名, 例如 `isec_user` 或 `isec_user_group`。`{dbConstraintName}` 必须使用数据库返回的真实约束名,例如 `uk_isec_user_account` 或 `fk_isec_user_group_user_id`。外键 key 中的 `{tableName}` 必须是引用方表,也就是 FK 所在表;同一个 FK 约束必须同时准备引用目标不存在、引用目标正在被使用两类文案。历史 `{basePackage}.{Entity}.fk.{dbConstraintName}` 和根级 `constraint.*` 仅作为兼容回退,不再新增。 如果业务代码需要主动抛出 FK 被占用类错误,必须先在 `{Object}Constant.I18nKey` 中定义常量,再引用常量;重复数据业务校验应传实体类名和字段名,由 core 统一转换对象与字段文案: ```java throw StatefulException.of(ErrorCode.DATA_DUPLICATE, this.getEntityClass().getName(), "account"); throw StatefulException.of(ErrorCode.DATA_IN_USE, UserConstant.I18nKey.USER_USED_BY_USER_GROUP); ``` 不要在 `Manager`、`Service`、`Repository` 或 `Mapper` 中直接写完整 i18n key 字符串。 ## 样板五:`message.multiproperties` 如果团队使用 MultiProperties 编辑器维护多语言资源,建议同时生成: ```xml 1.2 message Java Properties Handler 200 380 /{module}-core/src/main/resources/i18n/ineed/security/authorization/user/messages.properties|false|false|false|false|ISO-8859-1 en 538 /{module}-core/src/main/resources/i18n/ineed/security/authorization/user/messages_en.properties|false|false|false|false|ISO-8859-1 zh_TW 348 /{module}-core/src/main/resources/i18n/ineed/security/authorization/user/messages_zh_TW.properties|false|false|false|false|ISO-8859-1 Errors REGISTERED_ERROR_1 false false 用户已经分配到该分组,不允许重复分配 User has already been assigned to the group, duplicate assignment is not allowed 使用者已經分配到該分組,不允許重複分配 Common security.user.accountStatus.enabled false false 启用 Enabled 啟用 Entity - User com.ineed.security.authorization.user.User false false 用户 User 使用者 com.ineed.security.authorization.user.User.account false false 账号 Account 帳號 ``` 说明: - `message.multiproperties` 是编辑视图,不是运行时加载入口 - 它应与三份 `messages*.properties` 保持同步 - 它不能只维护错误码;对象名、字段名、AO / Query / VO 对外字段名和关键枚举标签也必须同步维护 - 新增模块错误码或字段文案时,建议优先改 `message.multiproperties`,再同步导出 ## 样板六:对象与字段文案 统一使用 `{basePackage}.{Entity}.{fieldName}` 格式: ```properties com.ineed.security.authorization.user.User=用户 com.ineed.security.authorization.user.User.account=账号 com.ineed.security.authorization.user.User.nickname=昵称 ``` 约束: - `{basePackage}` 使用业务基础包;例如 `com.ineed.security.authorization.user` - `{Entity}` 使用实体类简单名,保持 UpperCamelCase - 对象和字段 key 必须跨 `AO / Query / VO / Entity` 复用,同一个业务字段只维护一份文案 - 不要使用持久化实现类全名作为 i18n key(例如 `com.ineed.security.authorization.user.jdbc.entity.User.account`) - `applicationKey` 仍用于 `META-INF/ineed/modules/*.json`、权限 `permissionKey`、菜单资源标识等业务标识,不作为对象/字段 i18n key 前缀 如果 AO 和 Entity 的文案必须区分(例如 AO 中需要更面向用户的标签),可以显式补: ```properties com.ineed.security.authorization.user.UserAO.account=登录账号 ``` ## 推荐生成顺序 创建新模块时,建议按以下顺序生成: 1. 先写 `{Object}Constant` 2. 再接控制器 `@RequestMapping`、`getResourceKey()` 和 `getResourceName()` 3. 再补模块内 `ErrorCode` 4. 再补 `message.multiproperties` 5. 再补 `messages.properties` 6. 最后补 `messages_en.properties` 和 `messages_zh_TW.properties` 这样能避免代码先落地、常量和文案后补时发生漂移。 ## 自检清单 提交前至少确认: 1. 控制器没有直接写资源路径字面量 2. 控制器没有直接写资源标识字面量 3. 模块私有业务错误没有直接写中文消息 4. 模块私有 `ErrorCode` 已配齐 `message.multiproperties` 和三份 `messages*.properties` 5. 文案占位符统一使用 `{0}`、`{1}` 风格 6. 对象名和字段名 key 与运行时类名一致 7. 所有出现在 `messages*.properties` 的模块私有对象名、字段名和关键枚举标签 key 都已同步补齐到 `message.multiproperties` 8. UK/FK 约束文案 key 已在 `{Object}Constant.I18nKey` 中定义,业务代码没有直接硬编码完整 key 字符串 > 最后更新:2026-05-15 --- # API 文档使用指南 来源:docs/core/api-documentation-guide.md SHA-256:78e66b758d3e669ec3dcb90280414ee5a420d17e3d4d029f67fc9599be3745f3 网站:/docs/api-documentation-guide/ 本文约定 MVC 与 WebFlux 应用统一的 API 文档生成与展示策略。 版本说明(2026-09-19):下文 Scalar + springdoc API starter 是本地工作树正在采用的目标配置。此次文档提交不包含配套 Java/POM 修改;其基线代码 `95a72d03` 仍保留 Swagger UI starter 与兼容路径。在代码迁移独立完成并验证之前,不能仅凭本页认定 Swagger UI 已从制品移除,也不能提前移除已有兼容约束。 基线兼容约束:由 `Phase53.4` 在 v1.9 closeout 或 2026-10-31(取较早者)发起复查,到期不自动删除。移除前需确认仓库、脚本和已知消费者没有活跃 Swagger UI 引用,Scalar 替代入口可用,并通过 MVC/WebFlux 文档路径回归;owner 与证据记录在所属规划工作区的 `.planning/phases/53.4-api/53.4-swagger-compatibility.json`。 ## 入口与职责 | 能力 | 路径 | 状态 | 职责 | |------|------|------|------| | Scalar | `/scalar` | 主入口 | 读取本应用的 OpenAPI 文档并提供统一展示界面 | | springdoc | `/v3/api-docs` | 唯一生成底座 | 从当前应用的控制器和注解生成 OpenAPI JSON | Scalar 必须把 `url` 固定为本地 `/v3/api-docs`。不得配置远程规范地址,不得预填用户名、密码、令牌、API key 或 OAuth secret。开发启动器同时关闭 Scalar Agent 和 Test Request 按钮,避免浏览器侧代理或带凭据请求成为默认能力。 springdoc 是唯一 OpenAPI 生成底座,项目只引入其 API starter。Scalar 消费 springdoc 的输出,不引入 Swagger UI 或第二套规范生成与注解体系。 ## 分组约定 - 默认未分组文档使用 `/v3/api-docs`,Scalar 主入口也固定读取该地址。 - 只有应用确实需要隔离不同消费方或 API 面时才新增 springdoc group;group 名称必须稳定、体现业务边界,并在应用文档中记录对应的 `/v3/api-docs/{group}` 地址。 - 不要为 MVC 与 WebFlux 创建语义不同的默认 group。相同业务面应使用相同命名。 ## 鉴权约定 - 文档页面是否需要登录由应用安全策略决定,不能通过在 Scalar 配置中写入固定凭据来绕过鉴权。 - OpenAPI security scheme 只描述客户端应如何认证;真实 token、API key、密码和 OAuth secret 不得进入 source YAML、OpenAPI 示例或文档仓库。 - 需要调试受保护接口时,由开发者在受控客户端中显式提供短期凭据。Scalar 的 Test Request 功能在开发启动器中保持关闭。 ## 验证 本轮开发验证使用 JDK 25、Maven 3.9.15;编译目标和环境前提见[环境准备](/docs/prerequisites/)。先配置本地 `JAVA_HOME` 与 Maven PATH,在最窄的 ineed-dev reactor 中运行双栈兼容测试: ```bash mvn \ -f "${INEED_JAVA_ROOT:?set as described in prerequisites}/ineed-projects/ineed-dev/pom.xml" \ -pl ineed-dev-webmvc-jpa-bootstrap,ineed-dev-webflux-r2dbc-bootstrap \ -am \ -Dtest=WebMvcApiDocumentationCompatibilityTest,WebFluxApiDocumentationCompatibilityTest \ -Dsurefire.failIfNoSpecifiedTests=false \ test ``` 测试必须同时确认 `/scalar` 返回引用本地 `/v3/api-docs` 的 HTML、`/v3/api-docs` 返回 OpenAPI JSON,并扫描四个开发启动器的 Scalar 配置一致、显式开启 `application.show-api-doc` 且不含预填凭据。 --- # API 兼容性治理策略 来源:docs/core/api-compatibility-policy.md SHA-256:89d713796091f6c89dd810a829a5b67e54480bb7dc7fe65677d6d6a0adc42530 网站:/docs/api-compatibility-policy/ 本策略用于管理 Ineed Java 框架中的公开废弃 API。目标不是“看到 `@Deprecated` 就删除”,而是让每个兼容入口都有明确替代项、负责人、引用证据和可复查期限。 当前事实清单由 Phase 53.4 的 `53.4-deprecated-api-registry.json` 维护,并由 `53.4-compatibility-gate.mjs` 从生产 Java 源码重建和校验。 ## 扫描边界 Gate 扫描以下五个逻辑根: 1. `java/ineed-core` 2. `java/ineed-parent` 3. `java/ineed-modules-parent` 4. `java/ineed-project-parent` 5. `java/ineed-modules` 扫描排除 `.git`、`.worktrees`、`target` 和 `src/test` 中的 `@Deprecated` 声明。接口声明和实现类 override 按包名、声明类型、方法名及参数类型生成稳定 ID;override 只作为声明项的证据,不另计公开 API。 ## 状态机 公开废弃 API 只能处于以下状态之一: - `retained_external_compatibility`:外部源码或二进制兼容 fixture 仍要求保留。 - `retained_active_usage`:生产或测试仍存在活跃引用。 - `qualified_removal`:满足全部移除前置条件,可进入最窄删除变更。 - `removed`:源码声明已经删除,并保留历史治理记录。 新增 `@Deprecated` 时必须在同一变更中登记 replacement、迁移步骤、owner、证据命令、`review_on` 和 `removal_not_before`。未知声明、重复 ID、wildcard 路径、缺失日期或未绑定 source HEAD 都会使 gate 失败。 ## 保留与复查 `review_on` 是下一次强制复查日期,不是自动删除日期;到期后必须重新扫描引用和 compatibility fixture,并更新分类或下一次复查安排。`removal_not_before` 是允许移除的最早日期,必须晚于或等于 `review_on`。 保留项必须持续满足: - replacement 在当前生产源码中真实存在; - 迁移步骤能把调用方带到精确 replacement,而不是只给模糊说明; - owner 对兼容成本和消费方迁移负责; - evidence command 使用结构化 executable/argv 和精确路径,不使用 glob 或 shell command string; - source 路径、行号和所属仓库 HEAD 与当前 inventory 一致。 ## 删除前置条件 只有同时满足以下四项,registry 才能把条目标记为 `qualified_removal`: 1. **Replacement exists**:replacement 已在生产源码中存在并可被调用。 2. **Zero live references**:生产引用和普通测试引用均为零;不能只运行一次宽泛 grep 就宣称安全。 3. **Compatibility fixture cleared**:外部/legacy consumer compile fixture 已删除、替代或明确证明不再要求旧入口。 4. **Targeted regression passes**:最窄 consumer compile 与目标 Maven regression 全部通过。 满足四项后仍必须遵守 `removal_not_before`。实际删除应作为单独、可回滚的变更,重新运行 registry/source 双向校验,并检查源码兼容与二进制兼容影响。方法签名、默认方法、继承关系或异常类型的改变,即使仓库内引用为零,也可能破坏已编译的外部消费方。 ## Nginx 兼容记录的历史示例 以下是现有治理记录的历史条目,用于说明登记字段;不是随 Core 文档发布而更新的业务模块当前状态。执行 Operations 迁移前,重新读取所属仓库与登记表的当前证据。 `NginxCommandExecutor.test(NginxDiscoveryRequest)` 当前分类为 `retained_external_compatibility`: - replacement:`test(NginxConfigValidationRequest)`; - 迁移方式:调用方提供精确 revision/config artifact,再构造 exact validation request; - owner:Operations Nginx; - `review_on`:`2026-10-31`; - `removal_not_before`:`2026-12-31`; - 阻塞原因:`NginxConfigLifecycleTest` 仍编译 legacy-only implementor,证明旧方法继续承担外部源码兼容。 该记录当时未满足移除条件。实际是否允许删除必须重新验证引用、兼容 fixture 和时间约束,不能直接用这段历史摘要作决定。 ## 与 Scalar/OpenAPI 文档治理的边界 本 registry 只管理 Java 生产 `@Deprecated` API。Scalar/OpenAPI 文档路径、operation summary、schema 或消费者契约由独立的 API documentation contract 管理,不能把文档状态混入本清单,也不能用本清单替代文档契约验证。 --- # 文档维护与验证约定 来源:docs/maintenance/documentation-policy.md SHA-256:bc2f42b87c5112fa67eb39b32932e067109919194193e399efcdde3dbb02b8a5 网站:/docs/documentation-policy/ ## 归属与版本 Core 文档维护框架契约、接入方式和源码参考;数据库设计与业务错误码登记归 `design/standards`,业务规则归对应业务域,启动器环境归应用项目,前端工程规范归前端仓库。 规范只有一个正文,AI、教程和反模式引用它。新专题在总索引登记,明确适用版本、源码依据和验证范围。当前文档基线为 `3.0.0-SNAPSHOT`,最近系统核验为 2026-09-19;变化频繁的能力另记验证日期。 ## 何时更新 修改公共接口/泛型、生命周期、配置项、依赖矩阵、权限租户语义、返回/异常或样例命令时,同一变更复核对应专题与示例。纯内部重构且行为不变,不机械更新所有文档日期。 新文件先加入总索引;迁移旧文件时更新入链,可保留简短过渡页,不能保留两套互相冲突的规范正文。历史执行记录应注明历史版本,不作为新模块必读规范。 ## 本地检查 ```bash python3 scripts/check-docs.py python3 scripts/check-docs.py --workspace ``` 检查器需要 Python 3.9 或更新版本。默认检查本仓 Markdown 的内联文件链接、可解析的标题锚点、总索引覆盖、代码围栏、机器专用路径及常见凭据模式;对缺少外部仓库的链接计数提示跳过。`--workspace` 要求外部目标也存在。普通输出只报告问题位置和类型。 检查器不访问 HTTP(S) 链接,不解析图片或引用式链接,也不能识别“文件仍存在但规范已经废弃”的语义;这些情况需要人工核对。`--json` 包含链接目标供审查,分享报告前应检查目标是否包含不宜公开的信息。 它不能证明 Java 示例编译、API 真实返回、复杂权限或事务语义。代码示例优先链接到可编译源码;变更相关行为时执行[测试规范](/docs/testing/)的相应层级。 ## 事实核验流程 1. 对照 POM 确认依赖/模块;对照源码确认类型、签名、默认值与条件。 2. 区分规范要求、当前实现、设计建议和执行证据。 3. 对命令核对工作目录、工具链、目标模块和测试选择;运行结果记录用例数量。 4. 明确环境依赖与未执行项,不通过编造地址、配置或结果填补缺口。 5. 保留既有用户修改,只提交本次改动;代码与文档冲突时先查证原因。 ## 两类读者的持续验收 AI 试用任务:新建小模块、修改查询、增加事务动作。记录自造接口、遗漏目标栈、租户/权限遗漏和需要人工补充的规则。 开发人员试用任务:依教程启动,新增字段/查询/动作并说明各层职责和回滚语义。由未参与框架实现的人记录卡点。人工试用结果需要真实参与者,不能由作者自评冒充完成。 自动检查和作者核验已经执行的范围见[验证记录](/docs/verification-2026-09-19/)。 --- # 文档入口迁移说明 来源:docs/maintenance/migration-notes.md SHA-256:8bec2cc578f238a4c83e8308dd5c0b5544ae89b076b236c6d8c605d9a2d4e11f 网站:/docs/migration-notes/ 2026-09-19 建立 AI 与开发人员双入口。现有专题保留原路径,避免破坏既有任务链接;有效规则继续作为权威正文。 | 原入口/内容 | 当前处理 | | --- | --- | | `project-development/development-contract.md` | 过渡到 `ai/README.md`、任务路由和规则索引 | | Core 应用错误码旧号段表 | 移除旧分配流程,唯一来源为 `design/standards/error-code-registry.md` | | `webmvc-crud-query-guide.md` / `webflux-crud-query-guide.md` 旧引用 | 指向合并后的 `project-development/crud-query-guide.md` | | `constant-guidelines.md` 旧引用 | 指向 `core/constant-errorcode-i18n-guide.md` | | 工具链与绝对机器路径 | 转到 `getting-started/prerequisites.md`,使用环境变量和 PATH | | “当前稳定主链路”概括 | 使用 `concepts/runtime-matrix.md` 按能力说明已声明与已执行范围 | | 既有骨架、分层、风格、ORM、删除、事件规则 | 原路径保留,由 `standards/README.md`统一索引 | 这次变更不要求业务代码迁移错误码,也不变更 Core API。需要调整历史错误码时在所属业务模块执行兼容迁移;不要因为文档表已移除就批量替换生产常量。 Core API 的实际移除策略仍见[兼容性治理](/docs/api-compatibility-policy/)。 --- # 文档核验记录:2026-09-19 来源:docs/maintenance/verification-2026-09-19.md SHA-256:a6f9b36dd6ce8e9f5bf4bcef16325041d2edd8f1452b31a46cdff93e26ff5461 网站:/docs/verification-2026-09-19/ ## 基线与范围 Core HEAD:`95a72d03`。核验时工作树已有文档、API 文档配置及相关 Java/POM 的在途修改;本轮文档以当前工作树为依据,不将这些在途修改宣称为已发布版本。 范围:根 README、AGENTS 与 `docs/` 文档体系。更新导航与规范入口,修复旧链接和错误码双源,补充 AI 任务路径、开发教程、配置/租户/权限/事务/缓存/事件/JDBC 等指南。 ## 实际执行 | 检查 | 结果 | 证明范围 | | --- | --- | --- | | Core 定向 Maven 测试 | 29 个报告,366 个测试;失败/错误/跳过均为 0 | 选定业务链、租户、缓存、事件契约 | | 分类模块定向 Maven 测试 | 6 个报告,9 个测试;失败/错误/跳过均为 0 | 真实样例模块的闭合、事务/授权相关测试与边界检查 | | effective POM | `java.version=17`、`maven.compiler.release=17` | 本机当前解析到的编译配置 | | 文档结构检查 | 56 份文档、416 个链接;0 个问题、0 个外部链接跳过 | 内外部文件链接、标题锚点、总索引、围栏和可检测敏感模式 | | 检查器回归 | 6 个 Python 测试通过 | 中文/重复标题、外部仓库、代码块、缺失索引和敏感值报告边界 | | 核心事实核对 | 对照 POM、源码和现有测试 | 类型、配置默认值、注册条件、租户写入、缓存/事件时序与示例落点 | 工具链:JDK 25、Maven 3.9.15。测试命令分别见[测试规范](/docs/testing/)与[分类教程](/docs/first-module/)。测试报告统计仅包含对应执行时间和选择模式内的 Surefire XML,不能将历史报告一起累加。 结构检查与测试摘要的机器可读记录见[验证数据](/references/#source-7600f2284c6c74ea)。 ## 本次发现并修正 - 原 3 处失效文件链接改为现行路径。 - Core 中旧应用错误码块表退出维护,使用 design 全局登记;样板号码改为明确占位。 - 原统一长必读清单改为短入口与任务路由;规则正文由稳定索引定位。 - 工具链区分编译 release、验证 JVM 与尚未执行的运行兼容矩阵,移除通用指南里的本机安装路径。 - 模块支持口径改为按能力和证据描述,保留动态数据源等已确认边界。 - 数据库元数据现状按当前 `RdbMetadataException` 实现修正。 - 业务模块命名允许实际存在的“域+能力”和带分组结构,不要求虚构中间层。 - 更新分类模块的历史描述:当前已有九层骨架与 contract 目录,不再标记为缺少 JDBC/响应式。 - 分类教程明确选择具体代码子模块;仅选择聚合 POM 的零测试执行不计入上述结果。 ## 不包含的验证 本轮没有重建业务数据库、启动新的开发服务或执行带真实身份的浏览器流程;教程中 MySQL/Redis/HTTP 步骤已对照源码,但未宣称本轮运行通过。框架 Map/mock 测试及分类模块局部测试不替代这些环境验证。 未执行所有 JDK/数据库厂商兼容矩阵,未逐行编译旧模板中的占位代码。可执行例子链接到真实编译单元;占位模板明确要求替换。 尚无独立开发人员试读或独立 AI 实施试用结果。持续验收任务已写入[维护规则](/docs/documentation-policy/),后续真实使用中记录卡点;不能由文档作者自评替代这些结果。 ## 工作树保护 首次整理修改 15 份已有文档,新增 28 份专题/入口文档和根 AGENTS 入口,并提供检查脚本与核验记录。工作树还包含 3 份原有文档变更,经本次复核后一并纳入用户授权的文档提交。开始前已有的 11 个非文档变更文件,其 SHA-256 与复核时一致;本次提交不包含这些 Java/POM 修改。 ## 提交前第二轮复核 用户要求再次核验并提交文档后,重新执行上述两组 Maven 测试、文档检查、6 项检查器测试及 `git diff --check`,结果均通过。重新生成的 Surefire 报告仍为 Core 29 份 / 366 项、分类 6 份 / 9 项;只统计本次命令之后更新且符合选择模式的报告。 本轮额外修正: - 分类定义是 `GLOBAL_ONLY`;教程改为隔离环境中的全局维护身份,并说明有效目标租户的前提和普通租户写入拒绝的断言。 - 分类 Manager 使用 `CreateOrRefreshManager`;重复 POST 可能刷新已有对象,不再暗示必然触发重复创建错误。 - 补充 Python 3.9+、curl 7.76.0+ 与 jq 的要求,说明链接检查器的解析范围与人工复核边界。 - API 文档指南区分 Scalar 目标配置与 `95a72d03` 仍保留的 Swagger UI 兼容基线,保留迁移门槛。MongoDB 已在该基线聚合中;原有 POM 修改只调整了声明顺序。 提交范围为根 README、AGENTS、`docs/` 及文档检查器和其回归测试。测试证据来自当前工作树,不能推导为此次纯文档提交已交付 API 文档相关代码迁移;真实数据库和 HTTP 等未执行范围仍如上所述。