@@ -0,0 +1,112 @@
# 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=<msg>` 。
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)。