# 自定义业务与分层扩展指南

## 适用范围

本指南面向已经沿 Ineed-Core 标准主路径接入业务模块的开发人员，适用于阻塞式 `WebMvc + JPA/JDBC/MyBatis-Flex`，也适用于响应式 `WebFlux + R2DBC` 的对等扩展。

当标准 CRUD / query 之外还需要增加自定义业务、跨表协作、状态切换后的补充动作、排序相关规则时，应该继续沿现有分层扩展，而不是临时发明平行层。

## 标准扩展主路径

Ineed-Core 当前推荐的自定义业务扩展主路径保持不变。

`Controller -> Service -> Manager -> Repository/EntityManager`

阻塞式与响应式链路均按 [crud-query-guide.md](crud-query-guide.md) 中各自运行时的接口矩阵执行。本文只补充“标准能力之外的自定义业务应该如何继续往下写”的通用分层规则。

## 共享扩展契约补充约定

当某个自定义能力由一个模块定义、另一个模块实现，并且需要同时支持 `webmvc` 与 `webflux` 两条线时，额外遵守：

- 共享接口定义优先放在发起方的 `core` 共享层，而不是落在具体实现模块
- 阻塞式接口放 `service/`
- 响应式接口放 `service/reactive/`
- `webmvc` / `webflux` 模块只保留各自的适配实现，不重复定义另一套共享契约
- 调用方如果已经持有当前会话对象，接口入参优先直接传 `SecuritySession` 一类聚合上下文对象，而不是继续拆 `identityType`、`principalId`、`tenantKey`
- create / update / delete / status / account 一类副作用型 reactive 契约，和阻塞式语义保持一致；阻塞式为 `void` 时，响应式统一定义为 `Mono<Void>`，不要把影响行数暴露到上层扩展接口

## 账户认证型扩展补充约定

当某个业务模块需要接入统一认证 / 授权主链时，额外遵守：

- 共享认证桥接接口优先复用 `IAccountAuthenticationService` / `IReactiveAccountAuthenticationService`；权限快照统一复用 `ISecurityAuthoritiesService` / `IReactiveSecurityAuthoritiesService`。
- 不要再生成已经废弃的 `IAccountAuthenticationManager`，也不要把共享认证能力重新收口到通用 `AccountManager`。
- 主体模块自己的 `Manager` 只补 `loadByUsername(...)`、`accountToSecurityUser(...)`、模块私有异常映射和主体特有查询。
- 模块共享的 `*SecurityUser` 统一放在该业务聚合的 `core` 子模块，供 `engine`、`engine-reactive`、`webmvc`、`webflux` 共用；不要在实现层重复定义多份。
- JDBC / R2DBC 仓储默认优先走接口组合；`Repository` 保持 `interface`，直接组合 core fragment，并在接口内保留 `GetEntityClassOperation<T> + 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<E, AO>` 与 `EntityVoConverter<E, V>` 后，不要重复声明父接口已有的 `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<Long> countByCategoryIdAndDeleted(String categoryId, Byte deleted)`、`Mono<Void> 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<ResponseResults<Iterable<T>>>`；`Flux` 只保留在内部链路或真正的流式接口中
- 无返回体动作型 API 统一使用 `.then(ResultBuild.success())`；不要生成 `ResultBuild.<Void>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<Void>`；不要在 `Service` 扩展契约层暴露影响行数
- 查询型 reactive `Service` 只做必要的响应式封装，不额外引入与阻塞式不一致的空值分支、泛型边界或日志命名
- 遗留阻塞式别名接口完成下游切换后删除；新增或改造模块统一直接依赖真实能力接口，不再引用过期别名

## 阻塞式 Manager / ORM 补充约定

- 阻塞式 `Manager` 如果需要访问可选下游能力，例如 `OrderOperation`、`TreeOperation`、额外的 `FindByIdOperation`，统一优先通过 `OperationSupport.getOperation(Class<T>)` / `requireOperation(Class<T>)` 获取 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<VO>` 与 `Flux<VO>`。

### 按业务键创建或刷新

当业务对象天然具备稳定业务键，并且重复提交应该刷新旧数据而不是报重复错误时，例如监控节点注册、应用实例上报、外部系统同步，可在具体 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<Entity>`，R2DBC 返回 `Mono<Entity>`，MyBatis-Flex 可由 mapper 查询实体后在 Manager 包装 `Optional`

## Reactive Manager / Repository 补充约定

- reactive `Manager` 默认方法与 `Service` 保持同一口径：依赖解析、能力判断、上下文读取都收进订阅期执行，不保留链外同步 `throw`
- `OperationSupport.requireOperation(...)` 一类桥接入口统一返回 `Mono<T>`；下游默认方法通过 `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
