`
## 新功能跨模块对齐规则
无论人工实现、代码生成器生成还是自动化工具修改,只要需求没有明确限制范围,新增功能都必须默认同步评估并对齐:
- 数据层:`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