# P10-NS4-07：表单验证的统一规范

## 简要结论
- 状态：⚠️ 部分解决
- 各表单已实现基本验证逻辑且风格较一致，但缺少文档化的统一规范，部分表单必填标识缺失，后端 Pydantic 422 错误未映射到前端字段级提示。

## 详细审查

### 前端代码

#### 1. 必填标识（`required: true`）

| 表单 | 文件 | 必填字段 | 是否配置 `required` | 备注 |
|------|------|----------|---------------------|------|
| 登录 | `Login/index.tsx` | username, password | ✅ 均有 | — |
| 审核通过 | `UserApproval/index.tsx` (ReviewModal) | role | ✅ 有 | `suggestionIndex` 为可选，正确 |
| 审核拒绝 | `UserApproval/index.tsx` (ReviewModal) | reason | ✅ 有 | — |
| 用户编辑 | `UserManagement/index.tsx` (EditModal) | role, siteId | ❌ 均无 `required` | 角色和门店字段未标记必填，用户可提交空值 |
| 用户绑定 | `UserManagement/index.tsx` (BindModal) | assistantId, staffId | ✅ 正确无 `required` | 两个字段均为可选，符合业务逻辑 |
| 线索编辑 | `ClueEditor/index.tsx` | category, summary | ✅ 均有 | `detail` 为可选，正确 |

问题：`EditModal` 中 `role` 和 `siteId` 字段没有 `required` 规则。虽然后端 `UserEditRequest` 允许 `None`（部分更新语义），但前端 UI 上没有明确告知用户哪些字段是建议填写的。

#### 2. 错误提示文案

所有已配置 `rules` 的表单字段，`message` 均为中文：
- `"请输入用户名"` / `"请输入密码"` — Login
- `"请选择角色"` — UserApproval (approve)
- `"请填写拒绝原因"` — UserApproval (reject)
- `"请选择大类标签"` / `"请输入摘要"` / `"摘要不能超过 200 字符"` — ClueEditor

✅ 文案风格统一，均为 `"请 + 动词 + 名词"` 格式。

#### 3. 验证触发时机

| 表单 | 触发方式 | 说明 |
|------|----------|------|
| Login | `onFinish`（提交验证） | Form 的 `onFinish` 回调，Ant Design 默认在提交时触发全量校验 |
| ReviewModal (approve/reject) | `form.validateFields()`（手动提交验证） | Modal `onOk` 中手动调用 |
| EditModal | `form.validateFields()`（手动提交验证） | Modal `onOk` 中手动调用 |
| BindModal | `form.validateFields()`（手动提交验证） | Modal `onOk` 中手动调用 |
| ClueEditor | `form.validateFields()`（手动提交验证） | Modal `onOk` 中手动调用 |

✅ 统一采用提交时验证（`onFinish` 或 `validateFields`），未使用实时验证（`onChange`）。风格一致。

注意：所有 Modal 表单均未显式设置 `validateTrigger`，Ant Design 默认值为 `onChange`，即用户修改字段后会实时显示/清除错误。这实际上是"提交触发首次校验 + 后续实时反馈"的混合模式，属于 Ant Design 最佳实践。

#### 4. 自定义验证器

仅 `ClueEditor` 使用了 `max: 200` 长度限制规则，其余表单无自定义 `validator`。

ExcelUpload 页面不使用 Ant Design Form 进行表单验证，而是：
- 文件类型限制通过 `Upload` 组件的 `accept=".xlsx,.xls"` 属性
- 空文件检查通过 `fileList.length === 0` 手动判断
- 数据校验完全由后端完成，前端展示后端返回的校验结果

这种设计合理——Excel 上传的校验逻辑复杂（表头格式、金额精度、人员匹配等），适合后端统一处理。

#### 5. 表单布局

所有 Modal 内表单统一使用 `layout="vertical"`，Login 页面使用默认水平布局。风格基本一致。

### 后端代码

#### 1. Pydantic Schema 验证

| Schema | 文件 | 验证规则 | 备注 |
|--------|------|----------|------|
| `ApproveRequest` | `tenant_users.py` | `role: str = Field(..., min_length=1)` | 必填 + 非空 |
| `RejectRequest` | `tenant_users.py` | `reason: str = Field(..., min_length=1)` | 必填 + 非空 |
| `UserEditRequest` | `tenant_users.py` | 所有字段 `Optional` | 部分更新语义，合理 |
| `UserBindingRequest` | `tenant_users.py` | 所有字段 `Optional` | 合理 |
| `ClueEditRequest` | `tenant_clues.py` | `category: ClueCategory`（枚举）, `summary: str = Field(..., min_length=1, max_length=200)` | 枚举校验 + 长度限制 |
| `ClueVisibilityRequest` | `tenant_clues.py` | `is_hidden: bool = Field(...)` | 必填 |
| `ConfirmRequest` | `tenant_excel.py` | `upload_id: int`, `resolutions: list[Resolution]` | 结构化验证 |

✅ 后端 Pydantic 验证规则与前端 rules 基本对齐。

#### 2. 手动验证（路由层）

Excel 上传路由 (`tenant_excel.py`) 包含额外的手动验证：
- `upload_type` 枚举校验
- 文件扩展名校验（`.xlsx/.xls`）
- 文件内容非空校验
- Excel 解析成功校验
- 数据行非空校验

客户搜索路由 (`tenant_clues.py`) 使用 `Query(..., min_length=1)` 确保关键词非空。

#### 3. 错误响应格式

后端统一错误响应格式：`{ "code": <status_code>, "message": <detail> }`

- `http_exception_handler`：HTTPException → `{ code, message }`
- `unhandled_exception_handler`：未捕获异常 → `{ code: 500, message: "Internal Server Error" }`
- ❌ 未注册 `RequestValidationError` 处理器 — Pydantic 验证失败时，FastAPI 默认返回 422 + `{ "detail": [{ "loc": [...], "msg": "...", "type": "..." }] }` 格式，与统一的 `{ code, message }` 格式不一致

#### 4. 错误提示文案

后端 HTTPException 的 `detail` 均为中文：
- `"申请不存在"` / `"该申请已被处理"` / `"无效的球房编号"` / `"审核操作失败"`
- `"用户不存在"` / `"目标门店不在管辖范围内"` / `"编辑操作失败"`
- `"线索不存在"` / `"编辑操作失败"` / `"删除操作失败"`
- `"请上传有效的 Excel 文件（.xlsx/.xls）"` / `"文件内容为空"`

✅ 中文化程度高，风格统一。

### 差距分析

P10 标杆文件（`P10-tenant-admin-web.md`）中没有独立的"表单规范"章节，但在验收标准和设计要点中隐含了以下要求：
- AC4：Excel 上传校验（必填、金额精度、表头格式、类型合法）— ✅ 已实现
- 4 种模板的必填列定义 — ✅ 后端已实现校验
- 冲突处理流程 — ✅ 已实现

与通用表单规范最佳实践的差距：

| 维度 | 期望 | 现状 | 差距 |
|------|------|------|------|
| 必填标识 | 所有必填字段有 `required` 规则 + 红色星号 | 大部分有，EditModal 缺失 | 🟡 小 |
| 错误提示位置 | 统一在字段下方 | Ant Design Form.Item 默认行为，一致 | ✅ 无 |
| 提示文案 | 中文，格式统一 | `"请 + 动词 + 名词"` 格式统一 | ✅ 无 |
| 验证时机 | 明确约定实时/提交 | 统一提交验证 + Ant Design 默认 onChange 反馈 | ✅ 无 |
| 后端 422 映射 | Pydantic 验证错误映射到前端字段 | 未注册 RequestValidationError 处理器 | 🟡 小 |
| 文档化规范 | 有明确的表单验证规范文档 | 无 | 🟡 小 |
| 前后端验证对齐 | 前端 rules 与后端 schema 一一对应 | 基本对齐，个别字段前端缺 rules | 🟡 小 |

### 建议

1. **EditModal 补充必填规则**（低优先级）：`role` 字段建议加 `rules={[{ required: true, message: "请选择角色" }]}`，`siteId` 视业务需求决定是否必填。

2. **注册 RequestValidationError 处理器**（低优先级）：在 `apps/backend/app/main.py` 中添加：
   ```python
   from fastapi.exceptions import RequestValidationError
   
   @app.exception_handler(RequestValidationError)
   async def validation_exception_handler(request, exc):
       return JSONResponse(
           status_code=422,
           content={"code": 422, "message": "请求参数校验失败", "errors": exc.errors()},
       )
   ```
   使 Pydantic 验证错误也遵循统一的 `{ code, message }` 响应格式。

3. **文档化表单规范**（可选）：如需更高规范性，可在 NS4 或项目级文档中补充一节"表单验证约定"，明确：
   - 必填字段必须配置 `required: true`（Ant Design 自动显示红色星号）
   - 错误提示文案格式：`"请 + 动词 + 名词"`
   - 验证时机：提交时触发（`validateFields` / `onFinish`），依赖 Ant Design 默认 `onChange` 实时反馈
   - 后端验证错误通过 `message.error()` 全局提示，不映射到具体字段

   当前项目规模（5 个页面、6 个表单）下，现有的隐式一致性已足够，文档化为锦上添花。
