Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M10a.prompt.md). Copyright stays with the author.
M10a · Medicine 兼容迁移与旧 UI 回归
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand + 微信云开发专家。请按照以下规范,逐步完成 M10a Medicine 兼容迁移与旧 UI 回归。完成每个子任务后自行验证,确认无误再进入下一个。
本模块承接 M10「统一数据层重构」:当前代码已从 Reminder 过渡到 Medicine,但还需要专门做一次兼容迁移补漏和旧 UI 回归,确保在 M11 MedicineComposer、M12 药箱页正式开发前,首页、提醒列表、提醒详情、旧 ReminderForm 和已开药闭环都不回归。
参考资料
- 开发计划:
docs/3.开发计划.md的「V1.1 药箱模块开发计划」与「M10a · Medicine 兼容迁移与旧 UI 回归」 - UI 参考:
ui7.0/index.htmlreminderList():提醒视角,只展示reminderEnabled=true的药品allMedicines():药箱视角,展示所有未删除药品,含reminderEnabled=falsedoneContent():已开药确认 Sheet 的文案和流程参考composerContent():后续 M11 统一表单参考,本模块不实现
- 前置模块:M1-M10 已完成,M10 已建立
Medicine/DerivedMedicine类型和提醒视角选择器
当前项目状态
src/types/index.ts已定义Medicine/DerivedMedicine,并保留Reminder/DerivedReminder类型别名src/services/reminder.ts已有migrateMedicine()和云端过渡双写 helper,但需要检查旧字段覆盖是否完整src/utils/dateUtils.ts已有derive()/deriveAll()/deriveAllMedicines(),需要确认reminderEnabled=false的稳定派生和过滤口径正确src/store/reminderStore.ts内部数据已是Medicine[],旧 API 名addReminder/updateReminder/deleteReminder/markDone/togglePause仍保留src/hooks/useReminders.ts已有useDerivedList()和useAllDerivedMedicines(),需要确认首页统计、我的页统计不把纯药箱药品计入提醒风险- 旧 UI 仍在使用:
src/components/ReminderForm/index.tsxsrc/components/MedicineCard/index.tsxsrc/components/ReminderDetail/index.tsxsrc/components/DoneDateSheet/index.tsxsrc/pages/home/index.tsxsrc/pages/list/index.tsx
- 云函数路径:
cloudfunctions/cloud1-d3gqjwfefe40e4dba/functions/reminder/index.js
技术约束
- 保持函数式组件 + Hooks + TypeScript 类型安全
- 优先使用
@taroify/core,小程序原生 Picker 继续使用@tarojs/components - 不新增集合,不新增路由,不实现 M11
MedicineComposer,不实现 M12 药箱页 UI - 不删除
ReminderForm,本模块目标是让旧新增 / 编辑提醒入口继续可用 - 不做库存估算,不引入
stockPerPrescription、estimatedStock、stockDays等字段或派生 - 不重写 M9 云开发认证、订阅消息授权和云初始化逻辑
- 允许小范围替换旧字段引用,但不要大面积重构页面结构或样式
本模块新增 / 修改文件
按检查结果最小修改,重点文件如下:
src/
├── services/
│ └── reminder.ts ← 检查并补齐 migrateMedicine 兼容、云端双写
├── utils/
│ └── dateUtils.ts ← 检查 reminderEnabled=false 派生和提醒视角过滤
├── store/
│ └── reminderStore.ts ← 检查旧 API、markDone、默认值补齐和本地/云端写入顺序
├── hooks/
│ └── useReminders.ts ← 检查首页/我的/药箱选择器统计口径
├── components/
│ ├── ReminderForm/
│ ├── ReminderDetail/
│ ├── MedicineCard/
│ └── DoneDateSheet/ ← 检查旧 UI 字段引用和已开药闭环
└── pages/
├── home/
└── list/ ← 检查提醒视角只展示 reminderEnabled=true
cloudfunctions/
└── cloud1-d3gqjwfefe40e4dba/
└── functions/
└── reminder/
└── index.js ← 检查 reminderEnabled=true 过滤和 name/medicineName 兼容
子任务 1:迁移函数兼容补漏
检查 src/services/reminder.ts 的 migrateMedicine(raw)。
1.1 必须兼容三类字段
旧 Reminder 字段:
medicineName/medicineSpeclastDate/interval/before/timehistory
UI7 原型字段:
name/specdosageFormdosePerTime/doseUnittiming/timingTime
V1.1 新字段:
name/specform/expiryDatedosagePerUse/dosageUnit/timesPerDayscheduleTiming/scheduleTimereminderEnabledcurrentPrescriptionDate/intervalDays/remindAdvanceDays/remindTimeprescriptionHistorylastWechatReminderDate/lastWechatReminderAt
1.2 字段兜底规则
migrateMedicine() 需满足:
name: raw.name ?? raw.medicineName ?? '';
spec: raw.spec ?? raw.medicineSpec ?? '';
reminderEnabled: raw.reminderEnabled ?? true;
currentPrescriptionDate: raw.currentPrescriptionDate ?? raw.lastDate ?? today();
intervalDays: Number(raw.intervalDays ?? raw.interval ?? DEFAULT_INTERVAL);
remindAdvanceDays: Number(
raw.remindAdvanceDays ?? raw.before ?? DEFAULT_BEFORE
);
remindTime: raw.remindTime ?? raw.time ?? DEFAULT_REMIND_TIME;
prescriptionHistory: raw.prescriptionHistory ?? raw.history ?? [];
注意:
- 旧数据没有
currentPrescriptionDate/lastDate时,必须回退到today()或稳定日期,不能返回空字符串导致parseDate('')出现异常日期 remindTime必须兼容旧字段timeprescriptionHistory不是数组时回退为空数组reminderEnabled只有在字段缺失时默认为true,显式false必须保留
1.3 剂型 / 单位 / 服用时机归一化
继续保留并检查归一化:
片剂→tablet胶囊→capsule注射剂/注射液→injection滴剂→drops液体/口服液→liquid外用→external贴剂→patch- 其他 →
other
非法 dosageUnit 回退为 片,非法 scheduleTiming 回退为 饭后。
子任务 2:云端读写兼容
检查 src/services/reminder.ts 的云端读写。
2.1 fetch 读取
fetchReminders() 返回值必须是 Promise<Medicine[]>,内部对所有云文档执行 migrateMedicine(item)。
查询条件应排除 deleted,但不能只查 reminderEnabled=true,因为后续药箱需要拉取纯药箱药品。
2.2 新增 / 更新双写
过渡期云端 payload 必须同时写:
name: medicine.name;
spec: medicine.spec;
medicineName: medicine.name;
medicineSpec: medicine.spec;
如果更新 payload 中包含以下新字段,也可按需兼容旧字段:
currentPrescriptionDate同步旧lastDateintervalDays同步旧intervalremindAdvanceDays同步旧beforeremindTime同步旧timeprescriptionHistory同步旧history
目标是旧调试数据、旧云函数日志或旧页面临时读取时不会丢主要字段。
子任务 3:提醒视角与药箱视角过滤
检查 src/utils/dateUtils.ts、src/store/reminderStore.ts、src/hooks/useReminders.ts。
3.1 derive() 稳定派生
当 medicine.reminderEnabled === false 时:
level返回REMINDER_LEVEL.PAUSEDlevelLabel返回未开启提醒progress返回0nextPrescriptionDate/nextRemindDate仍返回稳定字符串- 页面读取
daysLeft、scheduleLabel、日期字段时不能报错
3.2 deriveAll() 提醒视角
deriveAll(medicines) 只能返回:
status !== deletedreminderEnabled === true
排序维持旧提醒规则:
danger < warning < good < paused
同级按 nextPrescriptionDate 升序
3.3 deriveAllMedicines() 药箱视角
deriveAllMedicines(medicines) 返回所有未删除药品:
- 不过滤
reminderEnabled=false reminderEnabled=true排在前面- 同级按
name中文升序
3.4 Hooks 统计口径
useDerivedList():提醒视角,只给首页 / 提醒列表使用useHomeSummary():基于提醒视角,不统计纯药箱药品useProfileStats().activeCount:只统计status=active && reminderEnabled=trueuseProfileStats().total:统计所有未删除药品useProfileStats().historyTotal:使用prescriptionHistory.length
子任务 4:旧 UI 字段引用回归
检查以下文件,不允许继续读取已经不存在的旧字段导致空显示:
src/components/MedicineCard/index.tsxsrc/components/ReminderDetail/index.tsxsrc/components/ReminderForm/index.tsxsrc/components/DoneDateSheet/index.tsxsrc/pages/home/index.tsxsrc/pages/list/index.tsx
4.1 字段名
优先使用新字段:
namespeccurrentPrescriptionDateintervalDaysremindAdvanceDaysremindTimeprescriptionHistorynextPrescriptionDatenextRemindDate
如果发现组件仍读取:
medicineNamemedicineSpeclastDateintervalbeforetimehistorynextDateremindAt
请替换为新字段,或在迁移函数中明确补齐兼容字段。推荐小范围替换为新字段。
4.2 旧 ReminderForm 保持可用
ReminderForm 仍然是 M10a 阶段首页 / 列表的新增编辑入口,必须满足:
- 新增提醒时默认
reminderEnabled=true - 新增提醒时写入完整 Medicine 默认值
- 编辑提醒时回填
name/spec/currentPrescriptionDate/intervalDays/remindAdvanceDays/remindTime/note - 保存后首页和列表立即刷新
- 不实现服药信息、过期时间和提醒开关 UI,这些留给 M11
子任务 5:已开药闭环回归
重点检查:
src/store/reminderStore.ts的markDone()src/components/DoneDateSheet/index.tsxsrc/components/ReminderDetail/index.tsxsrc/pages/home/index.tsxsrc/pages/list/index.tsx
5.1 非逾期已开药
点击首页 / 列表 / 详情的「已开药」:
- 未暂停时可操作
- 非逾期时直接确认
currentPrescriptionDate更新为本轮记录日期prescriptionHistory头部插入本次记录日期,最多保留HISTORY_MAX- 重新派生
nextPrescriptionDate、nextRemindDate、daysLeft、progress - Toast 文案包含药品名且不为空
5.2 逾期已开药
逾期药品点击「已开药」时打开 DoneDateSheet:
- 默认日期为今天
- 可选择实际开药日期
- 确认后调用
markDone(id, selectedDate) - Sheet 关闭,页面刷新
注意:如果当前 DoneDateSheet 使用 start={item.nextPrescriptionDate} 且默认值是今天,当 today < item.nextPrescriptionDate 或边界条件不成立时需检查 Picker 是否可用。不要让日期选择范围和默认值互相矛盾。
5.3 暂停 / 删除
- 暂停药品不能执行已开药
- 删除为软删除,
status=deleted - 已删除药品不出现在首页、列表和药箱选择器中
子任务 6:云函数回归
检查:
cloudfunctions/cloud1-d3gqjwfefe40e4dba/functions/reminder/index.js
要求:
- 扫描条件包含
reminderEnabled: true - 扫描条件包含
status: active或等价 active 过滤 - 不扫描
reminderEnabled=false的药品 - 文案读取兼容:
const medicineName = medicine.name || medicine.medicineName || '开药提醒';
const note = medicine.note || medicine.notes || '复诊开药';
- 日期计算使用新字段:
currentPrescriptionDateintervalDaysremindAdvanceDaysremindTime
如需要兼容旧云数据,可在云函数侧补 fallback:
const currentPrescriptionDate =
medicine.currentPrescriptionDate || medicine.lastDate;
const intervalDays = medicine.intervalDays || medicine.interval;
const remindAdvanceDays = medicine.remindAdvanceDays || medicine.before;
const remindTime = medicine.remindTime || medicine.time || '09:00';
子任务 7:增加轻量验证
如果项目已有测试框架,优先补充单元测试。若暂无测试框架,不新增重型测试框架,至少做以下验证:
7.1 静态扫描
运行:
rg -n "medicineName|medicineSpec|lastDate|\\bhistory\\b|nextDate|remindAt|\\bbefore\\b|\\binterval\\b|\\btime\\b" src cloudfunctions/cloud1-d3gqjwfefe40e4dba/functions/reminder/index.js
对命中的旧字段逐条判断:
- 迁移函数 / 云端双写 / 兼容 fallback 中允许存在
- UI 组件和页面中原则上不应读取旧字段
- 注释中出现旧字段可以保留,但不要误导后续开发
7.2 TypeScript 检查
优先运行:
pnpm exec tsc --noEmit
如果项目没有独立 tsc 配置或依赖缺失,运行项目等效检查,并在最终说明中写清楚。
7.3 启动检查
运行:
pnpm dev:weapp
确认没有 M10a 引入的编译错误。若命令持续运行,看到编译成功后即可停止。
验收标准
迁移兼容
-
migrateMedicine()能兼容name/spec、medicineName/medicineSpec、lastDate/interval/before/time/history - 存量旧提醒默认
reminderEnabled=true - 显式
reminderEnabled=false不会被迁移成true - 空日期、非法数值、非法剂型 / 单位 / 服用时机不会导致页面崩溃
- 云端新增 / 更新过渡期双写
name/spec与medicineName/medicineSpec
旧 UI 回归
- 不接入
MedicineComposer时,旧ReminderForm仍可新增提醒 - 编辑旧提醒可正常回填和保存
- 首页 / 列表 / 详情中药品名称、规格、日期、提醒时间不为空或异常
- 旧数据和新数据混合存在时,首页 / 列表 / 详情无空字段、无崩溃
-
reminderEnabled=false的药品不会进入首页风险中心和提醒列表
已开药闭环
- 非逾期提醒点击「已开药」后,周期推进,列表刷新,Toast 出现
- 逾期提醒点击「已开药」后,打开实际日期确认 Sheet,确认后按所选日期推进
-
prescriptionHistory正确插入记录并限制长度 - 暂停提醒不能执行「已开药」
- 删除后不再出现在提醒视角
云函数
- 云函数只扫描
reminderEnabled=true && status=active - 云函数订阅消息文案兼容
name和medicineName - 云函数日期字段兼容旧数据,不因旧字段缺失而报错
验证
- 静态扫描确认旧字段只存在于迁移 / 双写 / fallback / 注释等合理位置
-
pnpm exec tsc --noEmit或等效 TypeScript 检查无 M10a 新增错误 -
pnpm dev:weapp可启动,无 M10a 引入的编译错误
开发提示
- 先修迁移函数,再修派生和选择器,最后回归旧 UI 和云函数
- 如果 TypeScript 报错范围较大,优先保障旧 UI 编译和业务闭环,再做字段命名清理
- 不要把 M11/M12 的需求提前塞进本模块;M10a 的目标是让旧 UI 稳稳站住
- 所有修改保持小步、可验证,避免顺手重构无关页面或样式