# Controller 出入参两类对象规范(Param / DTO) Status: ready-for-agent ADR: [ADR-0017](../../docs/adr/0017-controller-io-param-dto-conventions.md) > 入参查询用 `XxxParam`,入参写入和出参都用 `XxxDTO`,Entity 永不出现在 Controller 方法签名上。 ## 一句话规则 - 写新接口:查询/分页 → 建 `XxxParam extends BaseParam`;写入/保存 → 参数 ≥3 建一个 `XxxDTO`,≤2 保持 `@RequestParam` - 写新出参:建 `XxxDTO`,业务类继承 `BaseDTO` 白拿 `id` / `createTime` / `updateTime` - 想直接返回实体:不行 ## 对象职责 | 类型 | 包 | 用途 | 继承 | 硬约束 | |---|---|---|---|---| | `XxxParam` | `param/` | 查询/搜索/分页入参 | extends `BaseParam` | 必须经 `PageConverter.toMpPage` 转换 | | `XxxDTO` | `dto/` | 写入参 + 出参(双向) | 业务类 extends `BaseDTO`;统计/登录等无主键语义的不继承 | `fromEntity()` / `toEntity()` 写在 DTO 上,Service 层调用 | | Entity | `domain/entity/` | 表结构映射 | extends `BaseEntity` | **禁止出现在任何 Controller 方法签名中** | ## 硬约束 1. Entity 不出现在 Controller 方法签名上(入参出参都禁) 2. `deleted` 不出现在任何 HTTP 响应中 3. `creatorId` / `updaterId` 不出现在任何 HTTP 响应中 ## Mass assignment 防御 全局 `@ControllerAdvice` + `@InitBinder`,`dataBinder.setDisallowedFields(...)` strip 掉服务端裁定字段: `createTime`、`updateTime`、`creatorId`、`updaterId`、`deleted`、`builtin` 新增服务端裁定字段时必须同步更新此列表。 ## 封装阈值 ≥3 个参数封装为 `XxxDTO`;≤2 个保持 `@RequestParam`。纯参数个数,不看点参数形状。 ## 命名规则 `dto/` 包内所有 HTTP 出入参类一律 `XxxDTO` 后缀。内部传输对象(`ButtonSeed`、`PermissionModuleDescriptor`、`UploadSession`)不强制后缀。 ## 绑定方式 隐式表单绑定(不加 `@RequestBody`),契合项目 `application/x-www-form-urlencoded` 契约。 ## 存量改名清单 | 原名 | 新名 | 模块 | |---|---|---| | `RoleDetailVO` | `RoleDTO` | crm-auth | | `UserStatsVO` | `UserStatsDTO` | crm-auth | | `DictGroupVO` | `DictGroupDTO` | crm-dict | | `DictItemVO` | `DictItemDTO` | crm-dict | | `ResourceNode` | `ResourceNodeDTO` | crm-auth | ## 存量违规回填清单(9 处) | # | 违规 | 位置 | 改法 | |---|---|---|---| | 1 | 实体直收 | `DictGroupController.saveOrUpdate(DictGroup)` | 改为 `saveOrUpdate(DictGroupDTO)` | | 2 | 实体直收 | `DictItemController.saveOrUpdate(DictItem)` | 改为 `saveOrUpdate(DictItemDTO)` | | 3 | 实体直出 | `RoleController.page` → `Result>` | 改为 `Result>` | | 4 | 实体直出 | `SystemController.menuTree` → `List` | 改为 `List` | | 5 | 实体直出 | `SystemController.deptTree` → `List` | 改为 `List` | | 6 | 实体直出 | `DictGroupController.enabledList` → `List` | 改为 `List` | | 7 | 实体直出 | `DictItemController.enabledList` → `List` | 改为 `List` | | 8 | 分页绕过 | `RoleController.page` 自行 `new Page<>` | 改为 `RoleParam extends BaseParam`,经 `PageConverter` | | 9 | `@RequestBody` | `SystemController.userPage` | 去掉 `@RequestBody`,改表单绑定 | ## 前置条件 `BaseDTO` 必须先重构(当前仍是未改造版本): - 改为 `abstract` 类 - 只保留 `id`、`createTime`、`updateTime` - 三字段加 `@JsonInclude(NON_NULL)` - 移除 `creatorId`、`updaterId`、`deleted` 及默认值 - 移除泛型 `toEntity(Supplier)` 方法 ## 术语表 | 术语 | 定义 | |---|---| | **Param** (`XxxParam`) | 查询/搜索/分页入参对象,放 `param/` 包,继承 `BaseParam`,经 `PageConverter` 转换 | | **DTO** (`XxxDTO`) | 双向传输对象,既是写入参又是出参,放 `dto/` 包;业务类继承 `BaseDTO` | | **BaseParam** | 分页参数基类,提供 `current` / `size` / `asc` / `desc` + `PageConverter` 转换 | | **BaseDTO** | 出参基类(abstract),提供 `id` / `createTime` / `updateTime` + `@JsonInclude(NON_NULL)` | | **InitBinder 防护** | 全局 `@InitBinder` + `setDisallowedFields`,strip 服务端裁定字段防 mass assignment | | **PageConverter** | 分页转换器,页大小上限收敛 + 排序字段白名单校验 | | **封装阈值** | ≥3 个参数封 `XxxDTO`,≤2 保持 `@RequestParam` | | **Route A** | 同一个 DTO 类同时做写入参和出参(用户已接受双契约字段代价) |