# T5 · 诊单待办事项 + 企微定时推送 > 来源:~/.claude/plans/6-7-fluffy-sunset.md(T5 节) > 范围:单个 PR、全栈,独立于 T1–T4,可单独并行上线。 ## 用户故事 二中心医助希望在诊单上挂一些「将来要做的事」(例:3 天后回访、2 小时后提醒患者复测血糖),到点由企业微信推送给**创建人本人**(不是患者、不是当前指派医助)。失败一次即终态,由人工取消重建,不做自动重试。 ## 范围 - 「待办事项」tab 出现在诊单编辑页 (`tcm/diagnosis/edit.vue`) — 在「运动打卡记录」之后。 - 待办列表(历史 + 当前一锅端,按 `remind_time desc`,分页)。 - 新建:选时间 + 输入内容;只允许 `remind_time > now`。 - 取消:仅 `status=0` 可取消;只允许创建人本人或超管,按已有 role 边界判定。 - 不实现编辑(要改 = 取消重建)。 - 不实现提前 N 分钟提醒、不实现自动重试。 - 列表本身不做轮询,进 tab 拉一次。 ## 数据模型 新建表 `zyt_tcm_diagnosis_todo`: | 字段 | 类型 | 备注 | |---|---|---| | `id` | int unsigned PK auto_increment | | | `diagnosis_id` | int unsigned | 所属诊单 | | `patient_id` | int unsigned | 冗余便于反查 | | `content` | varchar(500) | 通知内容 | | `remind_time` | int(10) unsigned | 提醒时间戳(秒) | | `status` | tinyint | 0=待执行 / 1=已发送 / 2=已取消 / 3=发送失败 | | `creator_id` | int unsigned | admin.id | | `creator_name` | varchar(64) | 冗余,免 join | | `notified_at` | int(10) unsigned NULL | 实际推送时间(成功时写) | | `error` | varchar(500) NULL | 失败原因 | | `cancelled_at` | int(10) unsigned NULL | | | `cancelled_by` | int unsigned NULL | | | `create_time` / `update_time` / `delete_time` | int(10) unsigned | 软删 + 整型时间戳 | 索引: - `idx_diagnosis_id (diagnosis_id)` - `idx_creator_id (creator_id)` - `idx_status_remind_time (status, remind_time)` —— cron 扫表核心索引 迁移文件:`server/sql/1.9.20260420/add_tcm_diagnosis_todo.sql` ## API 路径 ``` GET /tcm.diagnosisTodo/lists?diagnosis_id=:id 分页列表 POST /tcm.diagnosisTodo/add { diagnosis_id, content, remind_time } POST /tcm.diagnosisTodo/cancel { id } GET /tcm.diagnosisTodo/detail?id=:id ``` 权限节点(`add_diagnosis_todo_menu.sql`,按钮型菜单挂在「诊单管理」下): - `tcm.diagnosisTodo/lists` - `tcm.diagnosisTodo/add` - `tcm.diagnosisTodo/cancel` ## 后端文件 - `server/app/common/model/tcm/DiagnosisTodo.php` - `server/app/adminapi/controller/tcm/DiagnosisTodoController.php` - `server/app/adminapi/logic/tcm/DiagnosisTodoLogic.php` - `server/app/adminapi/lists/tcm/DiagnosisTodoLists.php` - `server/app/adminapi/validate/tcm/DiagnosisTodoValidate.php` - `server/app/command/DiagnosisTodoNotify.php` ## Command 行为(`tcm:diagnosis-todo-notify`) 每次执行: 1. `select * from zyt_tcm_diagnosis_todo where status=0 AND remind_time<=now AND delete_time IS NULL order by remind_time asc limit 200` 2. 对每条: - 乐观锁更新:`update set status=1, notified_at=now where id=? and status=0` —— 如果影响行 0,说明被其它进程抢走,跳过。 - 取 `creator_id` → `Admin::find($creator_id)->work_wechat_userid`,没绑定时记录 `error='创建人未绑定企业微信'` 并置 `status=3`。 - 推文本:`【患者跟踪提醒】\n患者:{patient_name}\n时间:{remind_time_text}\n内容:{content}\n— 二中心跟踪系统` - 调 `WechatWorkAppMessageService::sendTextToUser($wxId, $text)`;失败 → `status=3, error=`。 3. 每条 try-catch,整批不中断;输出 `处理 N / 成功 X / 失败 Y / 跳过未绑定 Z`。 4. 注册到 `server/config/console.php`:`'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify'` 5. 用户在服务器 crontab 自挂:`* * * * * cd /path && php think tcm:diagnosis-todo-notify >> /var/log/zyt-todo.log 2>&1` ## 前端文件 - 新建:`admin/src/views/tcm/diagnosis/components/DiagnosisTodoList.vue` - 修改:`admin/src/views/tcm/diagnosis/edit.vue`(在 `name="exercise"` 之后追加 tab) - 修改:`admin/src/api/tcm.ts`(加 4 个函数) UI: - 顶部「+ 新增待办」按钮 → `el-dialog` (`el-date-picker type=datetime` + `el-input type=textarea`)。 - 表格列:提醒时间 / 内容 / 状态 chip / 创建人 / 推送时间 / 失败原因 / 操作。 - 状态 chip:`info` (待执行) / `success` (已发送) / `warning` (已取消) / `danger` (失败)。 - 操作列:`status===0` 显「取消」二次确认;其它隐藏。 - 排序:`remind_time desc`;分页。 ## 验收清单(对应计划文件 T5 验收) 1. [x] 在某诊单编辑页 → 待办事项 tab → 新建一条「2 分钟后,内容=测试」→ 表格出现「待执行」。 2. [x] 等 2 分钟,手动跑 `php think tcm:diagnosis-todo-notify`,企微收到文本;状态变「已发送」、推送时间已写。 3. [x] 把 admin `work_wechat_userid` 清空,再创建一条,跑命令 → 状态「失败」+ `error` 列显示原因;不会再次重试。 4. [x] 创建一条 1 小时后的 → 立即点「取消」→ 状态变「已取消」、操作列消失;再跑命令也不会推。 5. [x] 切换到该诊单的另一名 admin(非创建人)→ 看见这条但**没有取消按钮**。 6. [x] 历史 / 当前同表,按时间倒序,分页正常。 7. [x] `tcm:diagnosis-todo-notify` 二次执行不重复推送同一条(乐观锁验证)。 ## 不做(边界) - 不实现「提前 N 分钟提醒」逻辑("时间到了"做精准触发即可)。 - 不实现重试退避 —— 失败即终态。 - 不引入 SSE / WebSocket(待办列表不实时刷新,分钟级 cron 已够用)。 - 不修改已有的 `BloodRecordList.vue / DietRecordList.vue / ExerciseRecordList.vue`。 - 不与 T1–T4 共用任何文件之外的逻辑(这些子任务是独立 PR)。