# CRUD / Query 开发主路径

## 适用范围

本文档覆盖 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<V>` | `Mono<ResponseResults<V>>` |
| 批量创建 | `POST` | `/batch` | `batch-create` | `ResponseResults<Iterable<V>>` | `Mono<ResponseResults<Iterable<V>>>` |
| 更新 | `PUT` | 根路径 | `update` | `ResponseResults<V>` | `Mono<ResponseResults<V>>` |
| 逻辑删除 | `DELETE` | `/{id}` | `logic-delete` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量逻辑删除 | `DELETE` | `/batch/logic-delete` | `batch-logic-delete` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 恢复 | `PUT` | `/recovery/{id}` | `recovery` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量恢复 | `PUT` | `/recovery` | `batch-recovery` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 物理删除 | `DELETE` | `/physics/{id}` | `physics-delete` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量物理删除 | `DELETE` | `/batch/physics-delete` | `batch-physics-delete` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |

`forceDelete(id / idSet)` 只存在于 `IPhysicsDeleteService / IPhysicsDeleteManager`，标准 Controller 未暴露入口。业务如需开放强制删除，必须显式定义自定义接口和权限点。

## 查询入口矩阵

| 能力 | HTTP | 路径 | WebMvc 返回 | WebFlux 返回 |
|------|------|------|------|------|
| 分页查询 | `GET` | 根路径 | `ResponseResults<QueryResults<V>>` | `Mono<ResponseResults<QueryResults<V>>>` |
| 按 ID 查询 | `GET` | `/{id}` | `ResponseResults<V>` | `Mono<ResponseResults<V>>` |
| 批量按 ID 查询 | `GET` | `/batch?idSet=...` | `ResponseResults<Iterable<V>>` | `Mono<ResponseResults<Iterable<V>>>` |
| 回收站查询 | `GET` | `/recycle` | `ResponseResults<QueryResults<V>>` | `Mono<ResponseResults<QueryResults<V>>>` |
| 字典查询 | `GET` | `/dictionary` | `ResponseResults<QueryResults<DictionaryValue>>` | `Mono<ResponseResults<QueryResults<DictionaryValue>>>` |
| 树查询 | `GET` | `/tree` | `ResponseResults<QueryResults<TN>>` | `Mono<ResponseResults<QueryResults<TN>>>` |
| 树字典查询 | `GET` | `/tree/dictionary` | `ResponseResults<QueryResults<DictionaryValueTreeNode>>` | `Mono<ResponseResults<QueryResults<DictionaryValueTreeNode>>>` |
| 按主对象查询 | `GET` | `/of/{masterId}` | `ResponseResults<MasterQueryResults<M, V>>` | `Mono<ResponseResults<MasterQueryResults<M, V>>>` |
| 按主对象字典查询 | `GET` | `/dictionary/of/{masterId}` | `ResponseResults<QueryResults<DictionaryValue>>` | `Mono<ResponseResults<QueryResults<DictionaryValue>>>` |
| 按主对象字典树查询 | `GET` | `/tree/dictionary/of/{masterId}` | `ResponseResults<MasterQueryResults<M, DictionaryValueTreeNode>>` | `Mono<ResponseResults<MasterQueryResults<M, DictionaryValueTreeNode>>>` |

查询类标准 Controller 默认方法没有接口级 `@PreAuthorize`。如果业务模块要在权限清单中登记查询 API 权限，应按 [frontend-integration-guide.md](frontend-integration-guide.md) 生成 `api` 节点。

## 数据状态、排序、重复和账户入口矩阵

| 能力 | HTTP | 路径 | 权限后缀 | WebMvc 返回 | WebFlux 返回 |
|------|------|------|------|------|------|
| 启用 | `PUT` | `/enable/{id}` | `enable` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量启用 | `PUT` | `/enable` | `batch-enable` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 禁用 | `PUT` | `/disable/{id}` | `disable` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量禁用 | `PUT` | `/disable` | `batch-disable` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 锁定 | `PUT` | `/lock/{id}` | `lock` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量锁定 | `PUT` | `/lock` | `batch-lock` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 解锁 | `PUT` | `/unlock/{id}` | `unlock` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量解锁 | `PUT` | `/unlock` | `batch-unlock` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 发布 | `PUT` | `/publish/{id}` | `publish` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量发布 | `PUT` | `/publish` | `batch-publish` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 撤销发布 | `PUT` | `/withdraw/{id}` | `withdraw` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量撤销发布 | `PUT` | `/withdraw` | `batch-withdraw` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 设为默认 | `PUT` | `/default/{id}` | `default` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量设为默认 | `PUT` | `/default` | `batch-default` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 取消默认 | `PUT` | `/non-default/{id}` | `non-default` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量取消默认 | `PUT` | `/non-default` | `batch-non-default` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 设为主要 | `PUT` | `/primary/{id}` | `primary` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量设为主要 | `PUT` | `/primary` | `batch-primary` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 取消主要 | `PUT` | `/non-primary/{id}` | `non-primary` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 批量取消主要 | `PUT` | `/non-primary` | `batch-non-primary` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 移到目标前 | `PUT` | `/order/before` | `order` | `ResponseResults<V>` | `Mono<ResponseResults<V>>` |
| 移到目标后 | `PUT` | `/order/after` | `order` | `ResponseResults<V>` | `Mono<ResponseResults<V>>` |
| 移到顶部 | `PUT` | `/order/top` | `order` | `ResponseResults<V>` | `Mono<ResponseResults<V>>` |
| 移到底部 | `PUT` | `/order/bottom` | `order` | `ResponseResults<V>` | `Mono<ResponseResults<V>>` |
| 创建重复校验 | `POST` | `/duplicate` | `duplicate` | `ResponseResults<Boolean>` | `Mono<ResponseResults<Boolean>>` |
| 更新重复校验 | `PUT` | `/duplicate` | `duplicate` | `ResponseResults<Boolean>` | `Mono<ResponseResults<Boolean>>` |
| 重置账户密码 | `PUT` | `/account/passwd/reset/{id}` | `account-passwd-reset` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 禁用账户 | `PUT` | `/account/disable/{id}` | `account-disable` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 启用账户 | `PUT` | `/account/enable/{id}` | `account-enable` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 锁定账户 | `PUT` | `/account/lock/{id}` | `account-lock` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |
| 解锁账户 | `PUT` | `/account/unlock/{id}` | `account-unlock` | `ResponseResults<Void>` | `Mono<ResponseResults<Void>>` |

数据状态能力由实体 `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](delete-hook-and-logging-guide.md)

## 查询接入步骤

### 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<T>` 作为统一响应外壳

### 4. 扩展查询入口

| 查询能力 | 路径常量 | 返回模型 |
|------|------|------|
| 回收站查询 | `Urls.SEARCH_RECYCLE` | `QueryResults<V>` |
| 字典查询 | `Urls.SEARCH_DICTIONARY` | `QueryResults<DictionaryValue>` |
| 树查询 | `Urls.SEARCH_TREE` | `QueryResults<TN>` |
| 树字典查询 | `Urls.SEARCH_TREE_DICTIONARY` | `QueryResults<DictionaryValueTreeNode>` |
| 按主对象查询 | `Urls.SEARCH_OF_MASTER_ID` | `MasterQueryResults<M, V>` |
| 按主对象字典查询 | `Urls.SEARCH_DICTIONARY_OF_MASTER_ID` | `QueryResults<DictionaryValue>` |
| 按主对象字典树查询 | `Urls.SEARCH_DICTIONARY_TREE_OF_MASTER_ID` | `MasterQueryResults<M, DictionaryValueTreeNode>` |

`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<ResponseResults<Void>>`，使用 `.then(ResultBuild.success())`
- 集合结果返回 `Mono<ResponseResults<Iterable<T>>>`，不生成 `Mono<ResponseResults<Flux<T>>>`
- `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](development-contract.md)
- 自定义业务约定细则 → [custom-business-layering-guide.md](custom-business-layering-guide.md)
- 删除钩子与日志追踪 → [delete-hook-and-logging-guide.md](delete-hook-and-logging-guide.md)
- 前端联调 → [frontend-integration-guide.md](frontend-integration-guide.md)

> 最后更新：2026-05-15
