开发规范

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

在既有分层中组织业务动作,明确事务和多实体协作。

浏览全部手册
本页目录
文档来源与 Markdown 原文

权威正文:ineed-core/docs/project-development/custom-business-layering-guide.md。网站按工作区快照同步,原文中的历史日期和验证范围保留。

开始编码前核对同版本源码;跨仓文件引用可在源码定位目录查找。

下载 Markdown 原文 ↓

适用范围

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

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

标准扩展主路径

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

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

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

共享扩展契约补充约定

当某个自定义能力由一个模块定义、另一个模块实现,并且需要同时支持 webmvcwebflux 两条线时,额外遵守:

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

账户认证型扩展补充约定

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

  • 共享认证桥接接口优先复用 IAccountAuthenticationService / IReactiveAccountAuthenticationService;权限快照统一复用 ISecurityAuthoritiesService / IReactiveSecurityAuthoritiesService
  • 不要再生成已经废弃的 IAccountAuthenticationManager,也不要把共享认证能力重新收口到通用 AccountManager
  • 主体模块自己的 Manager 只补 loadByUsername(...)accountToSecurityUser(...)、模块私有异常映射和主体特有查询。
  • 模块共享的 *SecurityUser 统一放在该业务聚合的 core 子模块,供 engineengine-reactivewebmvcwebflux 共用;不要在实现层重复定义多份。
  • JDBC / R2DBC 仓储默认优先走接口组合;Repository 保持 interface,直接组合 core fragment,并在接口内保留 GetEntityClassOperation<T> + default getEntityClass()
  • 关系表或只承载少量动作的仓储,只组合必要 fragment,并直接在接口上声明模块私有查询 / 删除方法
  • 不再生成模块内的 repository/support/*Abstract*JdbcRepositoryAbstract*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

示例:

// 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 / MapperdeleteBy... 方法,但每个 deleteBy... 的持久化条件必须由对应数据访问层封装。

Controller 生成补充约定

  • 标准支持接口生成时,webmvcwebflux 的方法顺序、URL、权限表达式、参数名、日志语义统一对齐,不要一边叫 common、另一边叫 commonParameters
  • getService() 一类桥接方法统一放在接口最上面,方便实现类与代码生成器稳定复用
  • ValidationUtils 只负责生成 ValidationResult;Web 层异常抛出分别收口到 webmvc / webfluxValidationExceptionSupport
  • webflux 走统一返回体时,集合结果统一包装为 Mono<ResponseResults<Iterable<T>>>Flux 只保留在内部链路或真正的流式接口中
  • 无返回体动作型 API 统一使用 .then(ResultBuild.success());不要生成 ResultBuild.<Void>success()

Service 生成补充约定

  • 标准支持接口生成时,阻塞式与响应式 Service 的方法顺序、命名、Javadoc、日志语义统一对齐;响应式只保留必要的返回类型差异
  • 不再额外生成只做能力聚合/转发的顶层别名接口,例如 ICrudServiceIDataStatusServiceICrudManagerIDataStatusManager
  • 业务模块如果需要同时具备多种标准能力,直接 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 如果需要访问可选下游能力,例如 OrderOperationTreeOperation、额外的 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 中,entityClasstableNamerowMapper 等解析统一延迟到订阅期;优先复用 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(...) 作为桥接入口。这样可以保持与现有 ICreateServiceILogicDeleteService 等默认实现一致。

反模式与禁行规则

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

仍有疑问?按反馈清单整理复现信息 →