Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M10.prompt.md). Fill in{{medicine_scheduleTime}}before use. Copyright stays with the author.
M10 · 统一数据层重构
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand + 微信云开发专家。请按照以下规范,逐步完成 M10 统一数据层重构。完成每个子任务后自行验证,确认无误再进入下一个。
本模块是 V1.1「药箱」能力的阻塞模块:将当前 Reminder 提醒实体升级为统一 Medicine 药品实体,补充服药信息与 reminderEnabled 开关,同时保持 M1-M9 已完成页面和已开药闭环不回归。
参考资料
- 开发计划:
docs/3.开发计划.md的「V1.1 药箱模块开发计划」与「M10 · 统一数据层重构」 - UI 参考:
ui7.0/index.htmlallMedicines():药箱视角,展示全部未删除药品reminderList():提醒视角,只展示reminderEnabled=true的药品inventoryCard():药箱卡片字段参考composerContent():后续 M11 表单字段参考,本模块只完成数据层准备
- 前置模块:M1-M9 已完成,当前代码仍以
Reminder/DerivedReminder/medicineName/medicineSpec为主
当前项目状态
src/types/index.ts当前定义Reminder和DerivedRemindersrc/store/reminderStore.ts当前使用reminders: Reminder[],方法名仍为addReminder/updateReminder/deleteRemindersrc/utils/dateUtils.ts当前derive()只计算提醒字段src/services/reminder.ts当前有migrateReminder(),但只迁移旧提醒字段,尚未补齐药品字段src/hooks/useReminders.ts当前useDerivedList()返回所有未删除提醒src/pages/medicines/index.tsx当前仍是占位页,M10 不实现药箱页面src/app.config.ts已包含pages/medicines/indexTab- 云函数路径为
cloudfunctions/cloud1-d3gqjwfefe40e4dba/functions/reminder/index.js,当前扫描条件仍为{ status: 'active' }
技术约束
- 保持函数式组件 + Hooks + TypeScript 类型安全
- 不新增表或集合,继续使用云数据库
medicines集合 - 不实现 M11
MedicineComposer、M12 药箱页、M13 首页/列表 UI 适配;本模块只做数据层和兼容迁移 - 保留现有 store 方法名,避免旧 UI 立刻大面积改动
- 可以新增 Medicine 命名的方法和选择器,但旧方法必须继续可用
- 旧数据必须平滑迁移:旧字段、当前字段、新字段混合存在时,首页 / 列表 / 详情不能出现空字段或崩溃
- 药箱页不做库存估算,本模块也不引入
stockPerPrescription、estimatedStock、stockDays等库存派生 - 云端同步继续沿用当前
src/services/reminder.ts的实现风格,不重写认证、订阅消息或云初始化逻辑
本模块新增 / 修改文件
src/
├── types/
│ └── index.ts ← 修改:新增 Medicine 类型,Reminder 作为兼容别名
├── constants/
│ └── index.ts ← 修改:新增剂型、单位、服用时机、每日次数选项
├── utils/
│ └── dateUtils.ts ← 修改:derive 支持 Medicine,补充 scheduleLabel
├── services/
│ └── reminder.ts ← 修改:migrateMedicine,云端读写兼容新字段
├── store/
│ └── reminderStore.ts ← 修改:内部数据升级为 Medicine,保留旧 API
└── hooks/
└── useReminders.ts ← 修改:新增药箱全量选择器,提醒视角过滤 reminderEnabled
cloudfunctions/
└── cloud1-d3gqjwfefe40e4dba/
└── functions/
└── reminder/
└── index.js ← 修改:扫描条件新增 reminderEnabled: true,兼容 name 字段
子任务 1:类型重构 src/types/index.ts
1.1 新增药品相关枚举类型
在现有类型基础上新增:
export type MedicineForm =
| 'tablet'
| 'capsule'
| 'liquid'
| 'injection'
| 'external'
| 'patch'
| 'drops'
| 'other';
export type DosageUnit = '片' | '粒' | 'ml' | '支' | '贴' | '滴';
export type MedicineSchedule =
| '饭前'
| '饭后'
| '随餐'
| '空腹'
| '睡前'
| '固定时间'
| '按医嘱';
/** 药品状态(兼容原 ReminderStatus) */
export type MedicineStatus = ReminderStatus;
1.2 新增 Medicine 主体类型
将统一实体定义为:
export interface Medicine {
id: string;
// 基础信息
name: string;
spec: string;
form: MedicineForm;
expiryDate: string;
note: string;
// 服药信息
dosagePerUse: number;
dosageUnit: DosageUnit;
timesPerDay: number;
scheduleTiming: MedicineSchedule;
scheduleTime: string;
// 开药提醒
reminderEnabled: boolean;
currentPrescriptionDate: string;
intervalDays: number;
remindAdvanceDays: number;
remindTime: string;
status: MedicineStatus;
prescriptionHistory: string[];
lastWechatReminderDate: string;
lastWechatReminderAt: string;
createdAt: string;
updatedAt: string;
}
1.3 新增 DerivedMedicine
export interface DerivedMedicine extends Medicine {
nextPrescriptionDate: string;
nextRemindDate: string;
daysLeft: number;
level: ReminderLevel;
levelLabel: string;
progress: number;
scheduleLabel: string;
}
1.4 保留兼容别名
为了让旧 UI 在 M10 后继续编译:
export type Reminder = Medicine;
export type DerivedReminder = DerivedMedicine;
注意:别名只能兼容类型命名,不能自动兼容字段名。必须通过
migrateMedicine()补齐name/spec,并在过渡期对云端旧字段进行双写。
子任务 2:扩展常量 src/constants/index.ts
新增:
export const MEDICINE_FORM_OPTIONS = [
{ value: 'tablet', label: '片剂' },
{ value: 'capsule', label: '胶囊' },
{ value: 'liquid', label: '液体/口服液' },
{ value: 'injection', label: '注射液' },
{ value: 'external', label: '外用' },
{ value: 'patch', label: '贴剂' },
{ value: 'drops', label: '滴剂' },
{ value: 'other', label: '其他' }
] as const;
export const DOSAGE_UNIT_OPTIONS: DosageUnit[] = [
'片',
'粒',
'ml',
'支',
'贴',
'滴'
];
export const SCHEDULE_OPTIONS: MedicineSchedule[] = [
'饭前',
'饭后',
'随餐',
'空腹',
'睡前',
'固定时间',
'按医嘱'
];
export const TIMES_PER_DAY_OPTIONS = [1, 2, 3, 4] as const;
如需引用 DosageUnit / MedicineSchedule 类型,使用 type-only import,避免运行时循环依赖。
子任务 3:扩展派生函数 src/utils/dateUtils.ts
3.1 derive() 支持 Medicine
- 入参和出参升级为
Medicine/DerivedMedicine - 继续计算:
nextPrescriptionDatenextRemindDatedaysLeftlevellevelLabelprogress
- 新增
scheduleLabel
3.2 reminderEnabled=false 的稳定派生
当 medicine.reminderEnabled === false 时:
level返回REMINDER_LEVEL.PAUSEDlevelLabel返回未开启提醒progress返回0daysLeft可以继续按字段计算,也可以返回0,但不得进入提醒视角排序和首页风险统计nextPrescriptionDate/nextRemindDate仍需返回稳定字符串,不允许页面读取时报错
3.3 scheduleLabel 规则
function buildScheduleLabel(medicine: Medicine): string {
if (medicine.scheduleTiming === '固定时间' && medicine.scheduleTime) {
return `固定时间 {{medicine_scheduleTime}}`;
}
return medicine.scheduleTiming || '服用时机未填';
}
3.4 deriveAll() 保持提醒视角
deriveAll(medicines) 默认作为首页 / 提醒列表视角:
- 过滤
status !== deleted - 过滤
reminderEnabled === true - 按原规则排序:
danger < warning < good < paused,同级按nextPrescriptionDate升序
3.5 新增药箱视角派生函数
新增:
export function deriveAllMedicines(medicines: Medicine[]): DerivedMedicine[];
规则:
- 过滤
status !== deleted - 不过滤
reminderEnabled - 排序:
reminderEnabled=true在前,同级按name中文升序
子任务 4:服务层迁移 src/services/reminder.ts
4.1 将 migrateReminder() 升级为 migrateMedicine()
要求兼容三类字段:
- 旧 UI7 原型字段:
name/spec/dosageForm/dosePerTime/doseUnit/timing/timingTime - 当前代码字段:
medicineName/medicineSpec/currentPrescriptionDate/intervalDays/remindAdvanceDays/remindTime - V1.1 新字段:
form/dosagePerUse/dosageUnit/scheduleTiming/scheduleTime/reminderEnabled
参考实现要点:
function migrateMedicine(raw: any): Medicine {
const id = raw.id ?? raw._id ?? '';
const name = raw.name ?? raw.medicineName ?? '';
const spec = raw.spec ?? raw.medicineSpec ?? '';
return {
id,
name,
spec,
form: raw.form ?? normalizeMedicineForm(raw.dosageForm) ?? 'tablet',
expiryDate: raw.expiryDate ?? '',
note: raw.note ?? raw.notes ?? '',
dosagePerUse: Number(raw.dosagePerUse ?? raw.dosePerTime ?? 1),
dosageUnit: raw.dosageUnit ?? raw.doseUnit ?? '片',
timesPerDay: Number(raw.timesPerDay ?? 1),
scheduleTiming: raw.scheduleTiming ?? raw.timing ?? '饭后',
scheduleTime: raw.scheduleTime ?? raw.timingTime ?? '',
reminderEnabled: raw.reminderEnabled ?? true,
currentPrescriptionDate: raw.currentPrescriptionDate ?? raw.lastDate ?? '',
intervalDays: Number(raw.intervalDays ?? raw.interval ?? 30),
remindAdvanceDays: Number(raw.remindAdvanceDays ?? raw.before ?? 7),
remindTime: raw.remindTime ?? raw.time ?? '09:00',
status: raw.status ?? REMINDER_STATUS.ACTIVE,
prescriptionHistory: raw.prescriptionHistory ?? raw.history ?? [],
lastWechatReminderDate: raw.lastWechatReminderDate ?? '',
lastWechatReminderAt: raw.lastWechatReminderAt ?? '',
createdAt: raw.createdAt ?? '',
updatedAt: raw.updatedAt ?? ''
};
}
normalizeMedicineForm() 将中文剂型映射到枚举值:
片剂→tablet胶囊→capsule注射剂/注射液→injection滴剂→drops液体/口服液→liquid外用→external贴剂→patch- 其他 →
other
4.2 导出迁移函数
将 migrateMedicine 导出,方便 store 初始化、本地旧缓存迁移或后续测试复用。
export function migrateMedicine(raw: any): Medicine;
4.3 fetch 返回 Medicine
fetchReminders() 可以保留旧函数名,但返回值类型改为 Promise<Medicine[]>,内部 data.map(migrateMedicine)。
4.4 写入云端时双写兼容字段
新增 helper:
function toCloudMedicinePayload(medicine: Medicine): Record<string, unknown>;
写入或更新云端时,除新字段外,过渡期同步写入:
medicineName: medicine.namemedicineSpec: medicine.spec
这样 M10 之后旧云函数、旧页面或调试数据仍能读取。
子任务 5:Store 过渡 src/store/reminderStore.ts
5.1 内部数据升级为 Medicine
- import 类型升级为
Medicine/DerivedMedicine reminders字段可暂时保留命名,但类型改为Medicine[]AddPayload改为Omit<Medicine, 'id' | 'createdAt' | 'updatedAt'>
5.2 保留旧 API 名
以下方法名继续保留,旧 UI 不需要改调用:
addReminderupdateReminderdeleteRemindermarkDonetogglePausegetDerivedListgetByIdloadFromCloud
5.3 新增药箱选择器
在 store interface 中新增:
getAllDerivedMedicines: () => DerivedMedicine[];
实现调用 deriveAllMedicines(get().reminders)。
5.4 新增默认值补齐
addReminder(payload) 创建新项时必须补齐所有 Medicine 字段:
const newItem: Medicine = {
...payload,
id: genId(),
createdAt: now,
updatedAt: now,
name: payload.name || '',
spec: payload.spec || '',
form: payload.form || 'tablet',
expiryDate: payload.expiryDate || '',
note: payload.note || '',
dosagePerUse: payload.dosagePerUse ?? 1,
dosageUnit: payload.dosageUnit || '片',
timesPerDay: payload.timesPerDay ?? 1,
scheduleTiming: payload.scheduleTiming || '饭后',
scheduleTime: payload.scheduleTime || '',
reminderEnabled: payload.reminderEnabled ?? true,
currentPrescriptionDate: payload.currentPrescriptionDate || today(),
intervalDays: payload.intervalDays || DEFAULT_INTERVAL,
remindAdvanceDays: payload.remindAdvanceDays || DEFAULT_BEFORE,
remindTime: payload.remindTime || DEFAULT_REMIND_TIME,
status: payload.status || REMINDER_STATUS.ACTIVE,
prescriptionHistory: payload.prescriptionHistory || [],
lastWechatReminderDate: payload.lastWechatReminderDate || '',
lastWechatReminderAt: payload.lastWechatReminderAt || ''
};
5.5 loadFromCloud() 使用迁移结果
fetchReminders() 已返回迁移后的 Medicine,store 只需 set。
如代码中仍从本地缓存读取旧数据,也必须在进入 store 前调用 migrateMedicine()。
5.6 查询过滤规则
getDerivedList():继续返回提醒视角,即reminderEnabled=true && status!==deletedgetAllDerivedMedicines():返回药箱视角,即所有status!==deletedgetById(id):允许详情读取任意未删除药品,但旧提醒详情只会从提醒视角进入
子任务 6:Hooks 适配 src/hooks/useReminders.ts
6.1 旧 Hook 保持提醒视角
useDerivedList()继续返回deriveAll(reminders),因此只含开启提醒的药品useHomeSummary()继续基于deriveAll(reminders)统计,不能把reminderEnabled=false的药品算进风险中心
6.2 新增药箱 Hook
新增:
export function useAllDerivedMedicines();
返回 deriveAllMedicines(reminders)。
6.3 统计口径修正
useProfileStats():
total表示所有未删除药品数量activeCount如仍展示“启用中”,只统计reminderEnabled=true && status=activehistoryTotal统计所有未删除药品的prescriptionHistory.length
子任务 7:云函数适配
修改:
cloudfunctions/cloud1-d3gqjwfefe40e4dba/functions/reminder/index.js
7.1 扫描条件追加提醒开关
将数据库查询条件从:
.where({
status: 'active'
})
改为:
.where({
reminderEnabled: true,
status: 'active'
})
7.2 字段读取兼容
云函数当前主要读取 medicine.medicineName。改为兼容:
const medicineName = medicine.name || medicine.medicineName || '开药提醒';
const note = medicine.note || medicine.notes || '复诊开药';
日志、订阅消息文案、安全裁剪都使用兼容后的变量。
日期字段仍使用:
currentPrescriptionDateintervalDaysremindAdvanceDaysremindTime
子任务 8:兼容旧组件
M10 不要求大面积改 UI,但需要确保当前已实现页面不因类型字段重构报错。
如果现有组件仍读取 medicineName / medicineSpec,请选择以下其一:
方案 A(推荐):小范围替换字段名
将当前组件中的:
medicineName→namemedicineSpec→spec
重点检查:
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
方案 B:过渡类型兼容字段
如果短期替换字段名会导致范围过大,可在 Medicine 类型中临时保留可选兼容字段:
medicineName?: string;
medicineSpec?: string;
并在 migrateMedicine() / toCloudMedicinePayload() 中双写,确保旧组件仍可显示。
无论采用哪种方案,最终页面显示的药品名称和规格不能为空。
验收标准
类型与编译
-
Medicine/DerivedMedicine类型存在,Reminder/DerivedReminder兼容别名存在 -
pnpm exec tsc --noEmit或项目等效 TypeScript 检查无新增类型错误 -
pnpm dev:weapp可启动,无 M10 引入的编译错误
迁移兼容
-
migrateMedicine()能兼容name/spec、medicineName/medicineSpec、旧lastDate/interval/before/time/history字段 - 存量提醒默认补齐
reminderEnabled=true - 新增药品字段默认值完整:
form、expiryDate、dosagePerUse、dosageUnit、timesPerDay、scheduleTiming、scheduleTime -
expiryDate、scheduleTime为空字符串时页面不报错
业务行为
- 首页风险中心和提醒列表只展示
reminderEnabled=true且未删除的药品 -
reminderEnabled=false的药品不会进入首页统计、风险排序和提醒列表 -
getAllDerivedMedicines()/useAllDerivedMedicines()返回所有未删除药品,供后续药箱页使用 -
derive()保持开药提醒派生正确,不引入库存估算 -
scheduleLabel对固定时间展示为固定时间 HH:mm,其他情况展示服用时机或兜底文案 - 已开药、暂停/启用、删除、编辑等旧流程不回归
云开发
-
fetchReminders()返回迁移后的Medicine[] - 新增 / 更新云端时过渡期双写
name/spec与medicineName/medicineSpec - 云函数
reminder扫描条件包含reminderEnabled: true - 云函数订阅消息文案兼容
name和medicineName
开发提示
- 先做类型和迁移函数,再改
derive(),最后接 store / hooks / 云函数 - 不要在 M10 实现
MedicineComposer或药箱页 UI;只为 M11/M12 提供数据基础 - 如果 TypeScript 报错范围很大,优先通过兼容字段让旧 UI 回归,再小步替换字段名
- 不要删除现有云开发、微信订阅、分享、TabBar 或页面路由逻辑