Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M9.prompt.md). Fill in{{medicine_before}},{{medicine_name}},{{results_sent}},{{results_failed}},{{results_skipped}}before use. Copyright stays with the author.
M9 · 微信云开发集成
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + 微信云开发 专家。请按照以下规范,逐步完成 M9 微信云开发集成的全部开发内容。完成每个子任务后自行验证,确认无误再进入下一个。
当前项目状态
- M1–M6、M8 已完成;M7(通知记录)未实现,
profile页 InboxEntry 保持占位文字,不做处理 - 已有组件:
AppBar、BottomSheet、MedicineCard、StatusTag、ProgressBar、ReminderForm、ReminderDetail - 已有 hooks(
src/hooks/useReminders.ts):useDerivedList、useDerivedById、useHomeSummary、useReminderActions、useProfileStats - 已有工具(
src/utils/storage.ts):loadReminders/saveReminders/loadSettings/saveSettings src/services/目录为空,本模块需全部填充src/store/reminderStore.ts:使用本地存储,本模块在其基础上追加云端同步,不重写现有逻辑src/pages/profile/index.tsx:handleToggleSubscribe当前为 Toast 占位,本模块接入真实requestSubscribeMessageAPIsrc/app.ts:仅有useLaunch打印日志,本模块追加云开发初始化和认证调用project.config.json:无cloudbaseRoot,本模块需追加
技术约束
- 云函数运行于 Node.js,使用 CommonJS(
require) - 小程序端通过
Taro.cloud访问云开发 SDK(已内置,不安装额外包) - 云数据库集合:
medicines(提醒)、users(用户) - 本地存储保留为离线缓存,云数据库为主存储;启动时从云端拉取覆盖本地
- 写操作:乐观更新——先更新内存 + 本地存储(保持现有逻辑),再异步调用云服务(不阻塞 UI);云端失败仅打印日志
- Reminder 的本地
id字段同步存储为云文档中的id字段,云_id不用于业务逻辑;更新/删除操作通过where({ id })定位文档 - 云函数仅依赖
wx-server-sdk,不引入其他 npm 包
本模块新增 / 修改文件
cloudfunctions/ ← 新建目录
├── auth/
│ ├── index.js ← 云函数:登录换取 openid
│ └── package.json
└── reminder/
├── index.js ← 云函数:定时扫描推送
└── package.json
src/
├── app.ts ← 修改:追加云开发初始化 + 认证
├── services/
│ ├── cloud.ts ← 新建:云开发初始化 & 工具函数
│ ├── auth.ts ← 新建:用户认证服务
│ └── reminder.ts ← 新建:提醒数据服务层(云数据库 CRUD)
├── store/
│ └── reminderStore.ts ← 修改:追加 loadFromCloud + 各写操作云端调用
└── pages/
└── profile/
└── index.tsx ← 修改:订阅消息授权 Toggle 接入真实 API
project.config.json ← 修改:追加 cloudbaseRoot
子任务 1:配置云开发环境
1.1 修改 project.config.json
在根层级追加字段(不修改已有字段):
"cloudbaseRoot": "cloudfunctions/"
⚠️ 云开发环境 ID 需在微信开发者工具中创建后获取,代码中统一以
"YOUR_ENV_ID"占位,部署前替换。
1.2 新建云函数目录与 package.json
新建 cloudfunctions/auth/package.json:
{
"name": "auth",
"version": "1.0.0",
"description": "愈历 · 登录认证云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
新建 cloudfunctions/reminder/package.json:
{
"name": "reminder",
"version": "1.0.0",
"description": "愈历 · 定时提醒推送云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
子任务 2:新建 src/services/cloud.ts
import Taro from '@tarojs/taro';
let initialized = false;
/**
* 初始化微信云开发。在 app.ts 的 useLaunch 中最先调用。
* 重复调用安全(由 initialized 标志防止)。
*/
export function initCloud(envId: string): void {
if (initialized) return;
Taro.cloud.init({ env: envId, traceUser: true });
initialized = true;
}
/** 获取指定集合的引用 */
export function getCollection(name: string) {
return Taro.cloud.database().collection(name);
}
/** 调用云函数的通用封装 */
export async function callCloudFn<T = unknown>(
name: string,
data?: Record<string, unknown>
): Promise<T> {
const res = await Taro.cloud.callFunction({ name, data: data ?? {} });
return res.result as T;
}
子任务 3:新建云函数 cloudfunctions/auth/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
const db = cloud.database();
/**
* 云函数:auth
* 微信登录换取 openid,新用户自动写入 users 集合。
*/
exports.main = async () => {
const { OPENID } = cloud.getWXContext();
if (!OPENID) {
return { success: false, error: '无法获取 OPENID' };
}
try {
const { data } = await db
.collection('users')
.where({ _openid: OPENID })
.limit(1)
.get();
if (data.length === 0) {
await db.collection('users').add({
data: {
_openid: OPENID,
createdAt: new Date().toISOString()
}
});
}
return { success: true, openid: OPENID };
} catch (err) {
console.error('[auth] 写入用户记录失败:', err);
// openid 已获取,写入失败不阻断流程
return { success: true, openid: OPENID };
}
};
子任务 4:新建 src/services/auth.ts
import { callCloudFn } from './cloud';
interface AuthResult {
success: boolean;
openid?: string;
error?: string;
}
let cachedUserId: string | null = null;
/**
* 调用 auth 云函数完成微信登录,获取并缓存 openid。
* 重复调用直接返回缓存值。
*/
export async function login(): Promise<string | null> {
if (cachedUserId) return cachedUserId;
try {
const result = await callCloudFn<AuthResult>('auth');
if (result.success && result.openid) {
cachedUserId = result.openid;
return cachedUserId;
}
console.warn('[auth] 登录失败:', result.error);
return null;
} catch (err) {
console.error('[auth] 云函数调用异常:', err);
return null;
}
}
/** 获取当前已缓存的 userId(openid),未登录时返回 null */
export function getUserId(): string | null {
return cachedUserId;
}
子任务 5:新建 src/services/reminder.ts
设计说明:本地
Reminder.id字段同时存储为云文档的id字段,云自动生成的_id不用于业务。更新/删除通过where({ id })定位,无需异步获取云_id,写操作可保持非阻塞。
import type { Reminder } from '@/types';
import { getCollection } from './cloud';
import { getUserId } from './auth';
const COL = 'medicines';
/** 拉取当前用户的全部提醒(排除 deleted) */
export async function fetchReminders(): Promise<Reminder[]> {
if (!getUserId()) return [];
try {
const { data } = await getCollection(COL)
.where({ status: Taro.cloud.database().command.neq('deleted') })
.limit(100)
.orderBy('createdAt', 'asc')
.get();
// 云文档中已存储 id 字段,直接忽略 _id 和 _openid
return data.map(({ _id, _openid, ...rest }: any) => rest as Reminder);
} catch (err) {
console.error('[reminderService] 拉取失败:', err);
return [];
}
}
/** 新增提醒(本地 id 作为 id 字段存入云文档) */
export async function addReminderToCloud(reminder: Reminder): Promise<void> {
if (!getUserId()) return;
try {
await getCollection(COL).add({ data: reminder });
} catch (err) {
console.error('[reminderService] 新增失败:', err);
}
}
/** 更新提醒(通过 where id 定位) */
export async function updateReminderInCloud(
id: string,
payload: Partial<Reminder>
): Promise<void> {
if (!getUserId()) return;
try {
await getCollection(COL).where({ id }).update({ data: payload });
} catch (err) {
console.error('[reminderService] 更新失败:', err);
}
}
/** 软删除(将 status 设为 deleted) */
export async function deleteReminderInCloud(id: string): Promise<void> {
if (!getUserId()) return;
try {
await getCollection(COL)
.where({ id })
.update({
data: { status: 'deleted', updatedAt: new Date().toISOString() }
});
} catch (err) {
console.error('[reminderService] 删除失败:', err);
}
}
⚠️
Taro.cloud.database().command.neq(...)在src/services/reminder.ts中需要 import Taro。在文件顶部添加import Taro from "@tarojs/taro";。
子任务 6:改造 src/store/reminderStore.ts
改造原则:现有同步逻辑一律不修改,只在以下位置追加代码:
6.1 文件顶部追加 import
在已有 import 块末尾追加:
import {
fetchReminders,
addReminderToCloud,
updateReminderInCloud,
deleteReminderInCloud
} from '@/services/reminder';
6.2 ReminderStore interface 末尾追加方法签名
/** 从云数据库拉取最新数据,覆盖本地缓存;云端为空时不覆盖 */
loadFromCloud: () => Promise<void>;
6.3 addReminder 实现末尾追加云端调用
在 set(...) 调用之后(}, 前面的括号闭合处之后)追加:
// 异步同步到云端,不阻塞
addReminderToCloud(newItem).catch((err) =>
console.error('[store] addReminder 云端同步失败:', err)
);
6.4 updateReminder 实现末尾追加云端调用
在 set(...) 调用之后追加:
updateReminderInCloud(id, {
...payload,
updatedAt: new Date().toISOString()
}).catch((err) => console.error('[store] updateReminder 云端同步失败:', err));
6.5 deleteReminder 实现末尾追加云端调用
在 set(...) 调用之后追加:
deleteReminderInCloud(id).catch((err) =>
console.error('[store] deleteReminder 云端同步失败:', err)
);
6.6 markDone 实现末尾追加云端调用
在 set(...) 调用之后追加(doneDate 已在该函数作用域内定义):
updateReminderInCloud(id, {
lastDate: doneDate,
history: [
doneDate,
...(get().reminders.find((r) => r.id === id)?.history ?? [])
].slice(0, HISTORY_MAX),
updatedAt: new Date().toISOString()
}).catch((err) => console.error('[store] markDone 云端同步失败:', err));
⚠️
markDone的云端 history 参数直接从 store 最新状态读取(set之后get()已是更新后状态)。
6.7 togglePause 实现末尾追加云端调用
在 set(...) 调用之后追加:
const updatedStatus =
get().reminders.find((r) => r.id === id)?.status ?? 'active';
updateReminderInCloud(id, {
status: updatedStatus,
updatedAt: new Date().toISOString()
}).catch((err) => console.error('[store] togglePause 云端同步失败:', err));
6.8 在 createStore 实现对象末尾追加 loadFromCloud
在最后一个方法(getById)之后追加(注意上一行末尾加逗号):
async loadFromCloud() {
const cloudList = await fetchReminders();
if (cloudList.length === 0) return; // 云端为空时不覆盖本地数据(兼容离线首次启动)
set({ reminders: cloudList });
saveReminders(cloudList);
},
子任务 7:更新 src/app.ts
完整替换 src/app.ts:
import { PropsWithChildren } from 'react';
import { useLaunch } from '@tarojs/taro';
import { initCloud } from '@/services/cloud';
import { login } from '@/services/auth';
import { reminderStore } from '@/store/reminderStore';
import './app.scss';
/**
* 云开发环境 ID。
* 在微信开发者工具 → 云开发控制台中创建环境后,将环境 ID 替换至此处。
* 格式示例:"yucheng-abc123"
*/
const CLOUD_ENV_ID = 'YOUR_ENV_ID';
function App({ children }: PropsWithChildren<any>) {
useLaunch(async () => {
console.log('App launched.');
// 1. 初始化云开发(重复调用安全)
initCloud(CLOUD_ENV_ID);
// 2. 微信登录,获取 openid
const userId = await login();
if (!userId) {
console.warn('[App] 登录失败,使用本地缓存数据继续运行');
return;
}
// 3. 从云端拉取最新数据(覆盖本地缓存)
await reminderStore.getState().loadFromCloud();
});
return children;
}
export default App;
子任务 8:新建云函数 cloudfunctions/reminder/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
const db = cloud.database();
/** 计算两日期(YYYY-MM-DD)的天数差 */
function diffDays(a, b) {
return Math.round((new Date(b) - new Date(a)) / 86400000);
}
/** 计算下次开药日期 */
function calcNextDate(lastDate, interval) {
const d = new Date(lastDate);
d.setDate(d.getDate() + interval);
return d.toISOString().slice(0, 10);
}
/** 计算提醒触发日期 */
function calcRemindDate(nextDate, before) {
const d = new Date(nextDate);
d.setDate(d.getDate() - before);
return d.toISOString().slice(0, 10);
}
/**
* 云函数:reminder
* 触发器:在云开发控制台配置 Cron "0 0 8,9,10 * * * *"(每天 8/9/10 点触发)
* 功能:扫描 medicines 集合,对今日应发送提醒的记录调用订阅消息 API
*/
exports.main = async () => {
const today = new Date().toISOString().slice(0, 10);
const nowHour = new Date().getHours();
const results = { sent: 0, failed: 0, skipped: 0 };
try {
const { data: medicines } = await db
.collection('medicines')
.where({ status: 'active' })
.limit(1000)
.get();
for (const medicine of medicines) {
const nextDate = calcNextDate(medicine.lastDate, medicine.interval);
const remindDate = calcRemindDate(nextDate, medicine.before);
// 不是今日提醒日期则跳过
if (remindDate !== today) {
results.skipped++;
continue;
}
// 检查提醒时间与当前执行时段是否匹配(±1小时内)
const [remindHour] = (medicine.time || '09:00').split(':').map(Number);
if (Math.abs(nowHour - remindHour) > 1) {
results.skipped++;
continue;
}
try {
await cloud.openapi.subscribeMessage.send({
touser: medicine._openid,
// 在微信公众平台 → 订阅消息 → 我的模板 中申请后填入真实模板 ID
templateId: 'YOUR_TEMPLATE_ID',
page: 'pages/home/index',
data: {
thing1: { value: medicine.name.slice(0, 20) },
time2: { value: nextDate },
thing3: {
value: `提前 {{medicine_before}} 天提醒,请及时复诊开药`.slice(
0,
20
)
}
}
});
results.sent++;
} catch (err) {
console.error(
`[reminder] 发送失败 name={{medicine_name}} err=${err.errMsg ?? err.message}`
);
results.failed++;
}
}
} catch (err) {
console.error('[reminder] 扫描数据库失败:', err);
}
console.log(
`[reminder] 完成:发送 {{results_sent}},失败 {{results_failed}},跳过 {{results_skipped}}`
);
return results;
};
⚠️
YOUR_TEMPLATE_ID需替换为在微信公众平台申请的订阅消息模板 ID。模板字段规范:thing1(提醒药品)、time2(建议开药时间)、thing3(提醒原因)。云函数需在控制台开启"调用微信开放接口"权限后方可调用subscribeMessageAPI。
子任务 9:更新 src/pages/profile/index.tsx 订阅消息授权
定位文件中的 handleToggleSubscribe 函数,将其完整替换:
替换前(M8 占位):
const handleToggleSubscribe = () => {
if (!settings.subscribeEnabled) {
// M9 接入后替换为真实的 requestSubscribeMessage 调用
Taro.showToast({
title: 'M9 接入后开启微信推送',
icon: 'none',
duration: 1500
});
}
updateSettings({ subscribeEnabled: !settings.subscribeEnabled });
};
替换后(M9 真实授权):
const handleToggleSubscribe = async () => {
// 关闭订阅:仅更新本地状态,微信侧已授权无法程序化撤销
if (settings.subscribeEnabled) {
updateSettings({ subscribeEnabled: false });
Taro.showToast({ title: '已关闭订阅消息', icon: 'none', duration: 1200 });
return;
}
// 开启订阅:调用微信授权弹层
// YOUR_TEMPLATE_ID 与云函数中一致,部署前替换为真实模板 ID
const TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
try {
const res = await Taro.requestSubscribeMessage({ tmplIds: [TEMPLATE_ID] });
const accepted = res[TEMPLATE_ID] === 'accept';
updateSettings({ subscribeEnabled: accepted });
Taro.showToast({
title: accepted ? '订阅消息已开启' : '授权被拒绝,可在系统设置中重新授权',
icon: accepted ? 'success' : 'none',
duration: 1800
});
} catch (err) {
console.error('[profile] 订阅消息授权失败:', err);
Taro.showToast({
title: '授权请求失败,请重试',
icon: 'none',
duration: 1500
});
}
};
handleToggleSubscribe由同步函数改为async函数,调用处(Toggle 的onClick)无需修改,Taro 的事件回调支持 async 函数。
验收标准
基础验收(无需真实云环境)
-
pnpm dev:weapp无报错启动(CLOUD_ENV_ID为占位符时,云初始化调用应优雅降级,不阻断页面渲染) -
src/services/cloud.ts、src/services/auth.ts、src/services/reminder.ts文件存在,TypeScript 编译无报错 -
cloudfunctions/auth/index.js和cloudfunctions/reminder/index.js文件存在,语法无 JS 错误 -
reminderStore.ts中存在loadFromCloud方法,各写操作在set(...)之后有异步云端调用 -
profile/index.tsx中handleToggleSubscribe已为async函数,调用Taro.requestSubscribeMessage
真实环境验收(替换占位符后)
- 替换
CLOUD_ENV_ID→ 微信登录可获取 openid,users集合出现新用户记录 - 添加提醒后数据写入云数据库
medicines集合,关闭重启后数据不丢失 - 编辑、删除、已开药、暂停/启用操作同步更新云数据库对应文档
- "我的"页 Toggle 点击弹出微信授权弹层;用户授权后开关视觉打开,持久化;拒绝后显示友好提示
- 替换
YOUR_TEMPLATE_ID→ 云函数reminder在开发者工具本地调试可触发,日志输出"发送/失败/跳过"统计
部署备忘
| 步骤 | 操作 |
|---|---|
| 1 | 微信开发者工具 → 云开发控制台 → 创建环境 → 复制环境 ID → 替换代码中所有 YOUR_ENV_ID |
| 2 | 云开发控制台 → 数据库 → 新建集合 medicines(权限:仅创建者可读写)和 users(权限:仅创建者可读写) |
| 3 | 微信公众平台 → 订阅消息 → 我的模板 → 申请模板(字段:thing1/time2/thing3)→ 复制模板 ID → 替换代码中所有 YOUR_TEMPLATE_ID(共 2 处:profile/index.tsx 和 reminder/index.js) |
| 4 | 云开发控制台 → 云函数 → 分别上传并部署 auth 和 reminder;reminder 需开启「调用微信开放接口」权限 |
| 5 | 云函数 reminder → 定时触发器 → 新建 Cron:0 0 8,9,10 * * * *(每天 8/9/10 点各触发一次) |
