# OA 审批实例评论(Web 先)设计文档 **日期**: 2026-09-20 **状态**: 已批准 **关联**: `docs/superpowers/specs/2026-07-14-oa-approval-dingtalk-design.md`(审批意见 ≠ 本能力) **交付策略**: Web 管理端先实现 → 同一套 API 再推广到微信小程序 / 移动端 --- ## 1. 目标与范围 ### 1.1 目标 在 OA 审批实例详情中提供独立「讨论评论」能力:参与人可发表文字评论、引用流程表单中已有附件,并可撤回自己的评论。评论**不改变**审批结果、不推进流程。 ### 1.2 V1 包含(Web) | 能力 | 说明 | |------|------| | 发表评论 | 纯文字,必填,最长 1024 字 | | 引用表单附件 | 从当前实例 `form_data_json` 中 upload 类字段勾选已有文件;存引用快照,不复制对象存储文件 | | 撤回自己的评论 | 仅作者;撤回后时间线保留「已撤回」痕迹 | | 评论列表 | 详情页独立评论区展示;含正文、引用附件、人、时间 | | 状态门禁 | 仅实例 `status=running` 可发表 / 撤回;结束后只读 | | 权限 | 与「能打开该实例详情」同一套可见性;租户隔离 | ### 1.3 V1 明确不做 - 一键通知审批人 / 抄送人 / 参与人 - @ 某人 - 快捷回复词、表情面板 - 评论区**新上传**图片 / 附件(仅引用表单已有附件) - 楼中楼回复、编辑已发评论 - 将讨论评论混入审批进度链(`OaProgressChain`)中的办理意见 - 本期不交付小程序 UI(接口按双端预留) ### 1.4 与「审批意见」的边界(SHALL) | | 审批评论(本能力) | 审批意见(现有) | |---|---|---| | 存储 | `oa_instance_comment` | `oa_task.comment_text` / `oa_instance_event.comment_text` | | 时机 | 详情评论区随时(running) | 同意 / 拒绝 / 打回 / 加签 / 转交 | | 是否推进流程 | 否 | 是 | | UI | 独立「评论」区块 | 办理弹窗 / 进度链「意见」 | **禁止**把讨论评论写入 `OaTask.comment_text`。 --- ## 2. 方案决策 曾对比:独立表 / 复用 `oa_instance_event` / 塞入实例 JSON。 **选定:独立表 `oa_instance_comment`。** 理由:发表 / 撤回 / 列表语义清晰;Web 与小程序共用同一 API;不污染办理事件模型。 --- ## 3. 数据模型 ### 3.1 表 `oa_instance_comment` 字段顺序:`id` → 业务字段 → 公共字段(`tenant_no` … `del_flag`),与工程 database-conventions 一致。 | 字段 | 类型 | 说明 | |------|------|------| | `id` | VARCHAR(64) PK | UUID | | `instance_id` | VARCHAR(64) NOT NULL | 所属 `oa_instance.id` | | `content` | VARCHAR(1024) NOT NULL | 评论文本(撤回后仍保留原文,便于审计;前端对 revoked 不展示正文或仅展示占位) | | `attachment_refs_json` | TEXT NULL | 引用快照 JSON 数组,见 §3.2 | | `status` | VARCHAR(16) NOT NULL | `normal` \| `revoked`,默认 `normal` | | `revoked_time` | DATETIME NULL | 撤回时间 | | `tenant_no` | … | 公共 | | `order_num` | … | 公共 | | `remark` | … | 公共 | | `create_by` | … | **评论作者 user_id**(与现网一致用 create_by) | | `create_dept_id` | … | 公共 | | `create_time` | … | 发表时间 | | `update_by` / `update_time` | … | 撤回时更新 | | `del_flag` | … | 默认 0;列表默认排除硬删;撤回用 `status=revoked`,**不**靠 del_flag 表示撤回 | 索引建议:`(tenant_no, instance_id, create_time)`。 SQL:新增迁移脚本 `docs/sql/migrations/2026-09-20-oa-instance-comment.sql`,并按仓库惯例评估是否同步全量 `docs/sql/zkxw.sql`。 ### 3.2 `attachment_refs_json` 结构 数组元素: ```json { "fieldKey": "protocolFiles", "fieldLabel": "协议附件", "url": "https://minio.example/.../xx.pdf", "fileName": "协议.pdf" } ``` - 发表时服务端根据实例 `form_schema_json` + `form_data_json` 校验:每个 `url` **必须**出现在该实例某一 upload 类字段的当前值中(规范化逻辑对齐 `oa_related_approval._extract_upload_urls`)。 - 校验失败 → 400,中文错误(如「引用的附件不存在或不属于本表单」)。 - 快照在发表时刻固化;之后表单数据变更不影响已发表评论中的展示名 / url(若对象存储文件被删导致 404,属存储层问题,本期不处理)。 Upload 类字段类型(与现网一致):`upload` / `fcUpload` / `image` / `oaAttachment`。 --- ## 4. API 设计 前缀:现有 `/web/admin/oa-approval`(与 `backend/app/api/oa_approval.py` 一致)。 权限:调用方须通过与 `GET /instances/{instance_id}` **相同的实例可见性校验**;所有查询 / 写入强制 `tenant_no` = 当前用户租户。 ### 4.1 可引用附件列表 `GET /instances/{instance_id}/form-attachments` 响应概念: ```json { "items": [ { "fieldKey": "protocolFiles", "fieldLabel": "协议附件", "files": [ { "url": "...", "fileName": "协议.pdf" } ] } ] } ``` 无附件时 `items: []`。 ### 4.2 评论列表 `GET /instances/{instance_id}/comments` - 返回该实例全部评论(含 `revoked`),按 `create_time` **升序**。 - 每条含:`id`、`content`、`attachmentRefs`、`status`、`revokedTime`、`createBy`、`createByName`(若详情已有用户名投影则对齐)、`createTime`、`canRevoke`(当前用户且 status=normal 且实例 running)。 另:`GET /instances/{instance_id}` 详情响应 **SHALL** 增加字段 `comments`(结构同列表项数组),Web 首屏少一次请求;独立列表接口供刷新 / 小程序复用。 ### 4.3 发表评论 `POST /instances/{instance_id}/comments` Body: ```json { "content": "请补充第二页签字页", "attachmentRefs": [ { "fieldKey": "protocolFiles", "url": "...", "fileName": "协议.pdf" } ] } ``` 规则: - `content` trim 后非空,长度 ≤ 1024。 - `attachmentRefs` 可选;有则服务端补全 / 校验 `fieldLabel`,拒绝非法 url。 - 仅 `oa_instance.status == running`;否则 400「仅审批进行中可评论」。 - 成功返回新建评论对象(同列表项)。 ### 4.4 撤回评论 `POST /instances/{instance_id}/comments/{comment_id}/revoke` 规则: - 评论存在、未删、属于该实例与租户。 - 仅 `create_by == 当前用户`。 - 仅 `status == normal`;已撤回 → 幂等成功或 400(实现选一种,建议 **幂等成功**)。 - 仅实例 `running`;结束后不可撤回。 - 置 `status=revoked`、`revoked_time=now`,更新 `update_by` / `update_time`。 - **不**物理删除,**不**清空 `content` / `attachment_refs_json`(审计保留;UI 对 revoked 不展示正文与附件)。 --- ## 5. 后端实现要点 | 位置 | 说明 | |------|------| | `backend/app/models/models.py` | 新增 `OaInstanceComment` | | `backend/app/schemas/` | CreateBody / 列表项 Pydantic(camelCase 对外) | | `backend/app/crud/` 或 `services/` | 列表、发表校验、撤回;附件枚举可放 `oa_related_approval` 旁或 `oa_instance_comment` 服务模块 | | `backend/app/api/oa_approval.py` | 注册上述 4 个路由;详情组装注入 `comments` | | 字段一致性 | `_row_to_dict` 为可读字段真相源;CreateBody 与之对齐可写字段 | --- ## 6. Web 管理端 UI ### 6.1 页面 - 主入口:`web-admin/src/views/oa-approval/instances/detail.vue` - 若存在详情弹窗复用同一详情数据源:`OaInstanceDetailDialog.vue` 等亦需挂载同一面板(与现网详情入口对齐,避免漏入口)。 ### 6.2 组件 新建 `web-admin/src/components/oa-approval/OaCommentPanel.vue`(建议): 1. **列表区**:升序;normal 展示头像/姓名、时间、正文、引用附件芯片(预览/下载对齐现有附件交互);revoked 展示灰条「{姓名} 撤回了一条评论」+ 撤回时间。 2. **操作**:本人 + normal + 实例 running → 「撤回」按钮(需确认框)。 3. **输入区**(仅 running):`el-input` textarea;「引用附件」打开多选(数据来自 form-attachments);「发送」。 4. 按钮带图标(ui-conventions);遮罩无 blur;文案中文。 视觉:Fresh Mint;独立 `section`,**不要**并入 `OaProgressChain`。 > **2026-09-21 更新(钉钉对齐)**:展示已改为并入审批进度时间线,见 `docs/superpowers/specs/2026-09-21-oa-comment-in-progress-timeline-design.md`。`OaCommentPanel` 仅保留底部输入;列表/撤回在 `OaProgressChain`(`kind=comment`)。 ### 6.3 API 模块 `web-admin/src/api/oaApproval.ts` 增加对应 fetch 方法。 --- ## 7. 移动端 / 小程序(二期,本期仅约束) - V1 不实现小程序页面。 - API 不得依赖仅 Web 才有的请求头或 Element 专用结构;响应字段稳定、camelCase。 - 二期在微信小程序审批详情复用:`form-attachments` / `comments` CRUD;UI 遵循 `docs/miniprogram-ui-spec-sanqiang.md`。 - 现有 `mobile-oa-h5` 若与小程序并行,二期可择一或双端同接,不在本设计展开。 --- ## 8. 验收标准 1. 有详情可见权限的用户,在 running 实例详情可发表非空文字评论。 2. 可多选引用表单附件;非法 url 被拒绝;评论区可预览/下载引用文件。 3. 作者可撤回自己的 normal 评论;他人不可撤;撤回后见「已撤回」占位,正文与附件对用户隐藏。 4. 非 running(completed / rejected / cancelled / returned 等)不可发、不可撤,仍可读历史评论。 5. 同意/拒绝意见与进度链行为与改前一致。 6. 租户隔离:不可读写他租户实例评论。 --- ## 9. 非目标与后续 - 评论通知、@、新上传附件、楼中楼 → 另开变更。 - 小程序 UI → V2。 - 是否把评论摘要打进打印 PDF → 不在 V1。 --- ## 10. 实现顺序建议 1. SQL + Model + Schema 2. 服务:附件枚举、发表校验、撤回、列表 3. API 路由 + 详情注入 `comments` 4. 前端 API + `OaCommentPanel` + 挂到详情页 5. 手工验收(Web) 6. (可选)`./scripts/update-graph.sh backend` / `frontend`