# 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": , "message": }` - `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 个表单)下,现有的隐式一致性已足够,文档化为锦上添花。