# 订阅与每日运势推送 — 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 subagent-driven-development(推荐)或 executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 为微信小程序添加每日运势订阅与推送功能,用户可在每日固定时间自动收到个性化运势推送。 **架构:** 微信云开发(CloudBase)作为轻量后端,云函数 + 云数据库 + 定时触发器。运势算法抽取为共享 npm 包,小程序和云函数共用。 **设计规格:** `docs/superpowers/specs/2026-08-13-daily-fortune-push-PRD.md` **ADR:** `docs/adr/2026-08-13-daily-fortune-push.md` --- ## 整体架构流程 ```mermaid graph TD subgraph 小程序端 A[运势页面] -->|"点击订阅"| B[wx.requestSubscribeMessage] B -->|"用户授权"| C[调用云函数 subscribe] C -->|"返回成功"| D[UI 更新为"已订阅"] D --> E[订阅管理页面] E -->|"修改时间"| F[调用云函数 updatePushTime] E -->|"取消订阅"| G[调用云函数 unsubscribe] end subgraph 云开发 C --> H[(云数据库 subscriptions)] F --> H G --> H I[定时触发器 07:00] --> J[云函数 dailyPush] J -->|"查询当日订阅"| H J -->|"调用共享算法"| K[生成运势内容] K --> L[wx.subscribeMessage.send] L --> M[用户收到服务通知] end subgraph 共享算法包 N[packages/algorithm] -->|"fortuneStrategy"| K N -->|"ziweiAlgorithm"| K N -->|"types/enums"| K end ``` ## 依赖关系 ```mermaid graph TD I1["Issue 1: 共享算法包抽取
P0 ⭐ 2d"] --> I2["Issue 2: 云开发环境搭建
P0 ⭐ 1d"] I1 --> I3["Issue 3: 订阅功能实现
P0 ⭐ 2d"] I2 --> I3 I2 --> I4["Issue 4: 每日推送云函数
P0 ⭐ 1.5d"] I3 --> I4 I4 --> I5["Issue 5: 订阅管理功能
P1 ⭐ 1d"] I4 --> I6["Issue 6: 集成测试与验收
P0 ⭐ 1.5d"] I5 --> I6 ``` --- ## Issue 1: 共享算法包抽取 **优先级:** P0 **预估工时:** 2d **依赖:** 无 **描述:** 将现有运势算法中的纯函数层抽取为独立 npm 包 `packages/algorithm/`,使其可在 UniApp 和 Node.js 云函数中复用。 ### 文件结构 | 操作 | 文件路径 | 说明 | |------|---------|------| | 创建 | `packages/algorithm/package.json` | 包配置,声明类型和入口 | | 创建 | `packages/algorithm/tsconfig.json` | TypeScript 配置 | | 创建 | `packages/algorithm/src/index.ts` | 导出入口 | | 复制 | `packages/algorithm/src/types.ts` | 类型定义(从 `src/algorithms/types.ts` 抽取) | | 复制 | `packages/algorithm/src/enums.ts` | 枚举定义(从 `src/algorithms/enums.ts` 抽取) | | 复制 | `packages/algorithm/src/fortuneStrategy.ts` | 运势生成策略 | | 复制 | `packages/algorithm/src/fortune.ts` | 综合运势生成 | | 复制 | `packages/algorithm/src/ziweiAlgorithm.ts` | 紫微算法基础 | | 复制 | `packages/algorithm/src/__tests__/fortuneStrategy.test.ts` | 算法测试 | | 修改 | `everything-is-suitable-uniapp/package.json` | 添加本地依赖引用 | | 修改 | `everything-is-suitable-uniapp/tsconfig.json` | 添加 paths 别名 | ### 验收标准 - [ ] 算法包在 Node.js 环境下可独立运行(`node -e "require('./packages/algorithm')"` 无报错) - [ ] 算法包所有测试通过(`cd packages/algorithm && npx vitest run`) - [ ] 小程序端引用算法包后,原有运势功能正常运行 - [ ] 算法包导出接口与 PRD 定义的接口一致 - [ ] 原有测试不受影响(`cd everything-is-suitable-uniapp && npx vitest run` 全部通过) --- ## Issue 2: 微信云开发环境搭建 **优先级:** P0 **预估工时:** 1d **依赖:** 无 **描述:** 开通微信云开发环境,创建云数据库集合、云函数脚手架、定时触发器配置。 ### 任务清单 - [ ] **步骤 1:开通微信云开发** - 在微信小程序管理后台开通云开发 - 创建环境(如 `prod`),记下环境 ID - 创建 `cloudfunctions/` 目录并初始化 - [ ] **步骤 2:创建云数据库集合** - 集合名:`subscriptions` - 索引:`openid`(唯一索引)、`pushEnabled`、`lastPushDate` - 权限:仅云函数可读写(不开放客户端直接访问) - [ ] **步骤 3:创建云函数骨架** ```bash cloudfunctions/ ├── subscribe/ # 订阅处理 ├── unsubscribe/ # 取消订阅 ├── updatePushTime/ # 更新推送时间 ├── dailyPush/ # 每日推送(含定时触发器) └── package.json ``` - 每个云函数创建 `index.js` 骨架,导出 `main` 函数 - 公共依赖:`@everything-suitable/algorithm`(通过本地路径引用) - [ ] **步骤 4:配置 dailyPush 定时触发器** - 在 `cloudfunctions/dailyPush/` 下创建 `config.json` - cron 表达式:`0 0 7 * * * *`(每日 07:00) - 后续支持用户自定义时间时,调整为每分钟扫描(`0 * * * * * *`)并过滤用户配置时间 ### 验收标准 - [ ] 云开发环境开通成功,可正常访问 - [ ] 云数据库 `subscriptions` 集合可正常读写 - [ ] 云函数 `subscribe` 可正常部署和调用 - [ ] 定时触发器配置已上传,云函数可被定时触发 --- ## Issue 3: 订阅功能实现 **优先级:** P0 **预估工时:** 2d **依赖:** Issue 1, Issue 2 **描述:** 实现小程序端订阅入口 + 订阅管理页面 + 云函数 subscribe/unsubscribe。 ### 3.1 小程序端 #### 订阅入口(运势页面) - 在 `src/pages/fortune/index.vue` 中添加订阅入口区域 - 未订阅时显示"订阅每日推送"按钮 - 已订阅时显示"已订阅"状态 + 推送时间 + "管理"入口 - 未输入生辰信息时提示用户先设置生辰信息 #### 订阅管理页面 - 新建页面:`src/pages/push-subscription/index.vue` - 功能: - 显示当前推送时间 - 修改推送时间(步长 30 分钟,07:00-22:00) - 取消订阅按钮 - 订阅状态说明 ### 3.2 云函数:subscribe ```typescript // cloudfunctions/subscribe/index.js exports.main = async (event, context) => { const { birthInfo, templateId, pushTime } = event const { OPENID } = cloud.getWXContext() // 校验参数 // 检查是否已存在订阅 // 写入 subscriptions 集合 // 返回 { success: true } } ``` ### 3.3 云函数:unsubscribe ```typescript // cloudfunctions/unsubscribe/index.js exports.main = async (event, context) => { const { OPENID } = cloud.getWXContext() // 删除 subscriptions 中对应记录 // 返回 { success: true } } ``` ### 验收标准 - [ ] 未订阅时,运势页面显示"订阅每日推送"入口 - [ ] 点击订阅 → 弹出微信订阅消息授权弹窗 - [ ] 授权成功 → 云函数存储订阅记录 → UI 更新为"已订阅" - [ ] 未输入生辰信息时,点击订阅提示先设置生辰信息 - [ ] 订阅管理页面可正常打开,显示当前推送时间 - [ ] 取消订阅 → 云端记录删除 → 运势页面恢复显示订阅入口 --- ## Issue 4: 每日推送云函数实现 **优先级:** P0 **预估工时:** 1.5d **依赖:** Issue 2, Issue 3 **描述:** 实现 dailyPush 云函数,在每日定时触发时生成运势内容并推送。 ### 云函数逻辑 ```mermaid graph TD A[定时触发 dailyPush] --> B[查询 subscriptions] B --> C{pushEnabled=true
&& lastPushDate != today?} C -->|"否"| D[跳过] C -->|"是"| E[读取 birthInfo] E --> F[生成 ZiweiChart] F --> G[计算今日运势] G --> H[格式化推送内容] H --> I[wx.subscribeMessage.send] I --> J{发送成功?} J -->|"是"| K[更新 lastPushDate = today] J -->|"否"| L[记录错误日志] ``` ### 关键实现细节 ```typescript // cloudfunctions/dailyPush/index.js const cloud = require('wx-server-sdk') const { generateDailyFortune } = require('@everything-suitable/algorithm') const { calculateLuckyColor, calculateLuckyNumber } = require('@everything-suitable/algorithm') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const today = new Date() const todayStr = today.toISOString().slice(0, 10) // 查询今日需要推送的订阅 const { data: subscriptions } = await db.collection('subscriptions') .where({ pushEnabled: true, lastPushDate: db.command.neq(todayStr).or(db.command.not exists), }) .get() for (const sub of subscriptions) { try { // 检查是否到推送时间 const [hour, minute] = sub.pushTime.split(':') if (today.getHours() !== parseInt(hour) || today.getMinutes() !== parseInt(minute)) { continue } // 生成 ZiweiChart(基于生辰信息) const chart = generateZiweiChart(sub.birthInfo) // 生成今日运势 const fortune = generateDailyFortune(chart, today) fortune.luckyColor = calculateLuckyColor(chart, today) fortune.luckyNumber = calculateLuckyNumber(chart, today) // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser: sub.openid, templateId: sub.templateId, data: { date1: { value: formatDate(today) }, thing2: { value: `综合运势评分${fortune.overallScore}分,等级:${fortune.overallLuck}` }, thing3: { value: formatFortuneSummary(fortune) }, thing4: { value: `幸运色:${fortune.luckyColor} 幸运数字:${fortune.luckyNumber}` }, }, }) // 更新最后推送日期 await db.collection('subscriptions').doc(sub._id).update({ data: { lastPushDate: todayStr }, }) } catch (err) { console.error(`Push failed for ${sub.openid}:`, err) } } } ``` ### 验收标准 - [ ] 定时触发器在每日 07:00 正确触发云函数 - [ ] 云函数正确过滤已订阅且未推送的用户 - [ ] 推送内容包含:日期、综合运势、各维度运势、幸运信息 - [ ] 推送内容与小程序内当日运势计算一致 - [ ] 同一用户每日不会收到重复推送 - [ ] 推送失败时记录错误日志,不影响其他用户推送 - [ ] 用户自定义推送时间在 minutely 扫描模式下正常工作 --- ## Issue 5: 订阅管理功能 **优先级:** P1 **预估工时:** 1d **依赖:** Issue 4 **描述:** 完善订阅管理页面的推送时间配置功能,以及相关的云函数 updatePushTime。 ### 5.1 小程序端 - 推送时间选择器(Picker 组件,步长 30 分钟) - "保存"按钮,调用云函数更新推送时间 - 保存成功提示 ### 5.2 云函数:updatePushTime ```typescript // cloudfunctions/updatePushTime/index.js exports.main = async (event, context) => { const { pushTime } = event // "HH:mm" 格式 const { OPENID } = cloud.getWXContext() // 校验 pushTime 格式 // 更新 subscriptions 集合中对应记录的 pushTime // 返回 { success: true } } ``` ### 验收标准 - [ ] 推送时间选择器按 30 分钟步长显示可选时间 - [ ] 保存后云端记录更新 - [ ] 修改后下一次推送按新时间执行 - [ ] 保存成功/失败均有明确提示 --- ## Issue 6: 集成测试与验收 **优先级:** P0 **预估工时:** 1.5d **依赖:** Issue 4, Issue 5 **描述:** 对订阅和每日推送功能进行完整的集成测试、异常场景验证和最终验收。 ### 测试场景 #### 6.1 功能测试 - [ ] **TC-01:完整订阅流程** — 输入生辰信息 → 订阅 → 授权 → 订阅成功 - [ ] **TC-02:未输入生辰信息时订阅** — 提示用户先设置生辰信息 - [ ] **TC-03:取消订阅** — 取消订阅 → 云端记录删除 → 订阅入口恢复 - [ ] **TC-04:修改推送时间** — 修改时间 → 保存 → 云端更新 - [ ] **TC-05:每日推送触发** — 定时触发 → 运势生成 → 推送成功 - [ ] **TC-06:重复推送防护** — 同一用户同一日不重复推送 - [ ] **TC-07:推送内容正确性** — 推送内容与小程序内当日运势一致 - [ ] **TC-08:多次订阅同一用户** — 幂等处理,不重复创建记录 #### 6.2 异常测试 - [ ] **TC-09:用户拒绝授权** — 订阅消息授权被拒绝 → 提示用户 - [ ] **TC-10:云函数超时** — 单个用户推送失败 → 不影响其他用户 - [ ] **TC-11:生辰信息不完整** — 缺少必要字段 → 提示用户补充 - [ ] **TC-12:订阅消息过期** — 用户重新订阅后恢复正常 #### 6.3 算法兼容性测试 - [ ] **TC-13:共享算法包 Node.js 运行** — 算法包在云函数环境输出与小程序一致 - [ ] **TC-14:边界日期** — 跨年、闰年日期运势计算正确 ### 验收标准 - [ ] 所有功能测试场景通过 - [ ] 所有异常测试场景通过,系统表现符合预期 - [ ] 共享算法包在小程序和云函数中计算结果一致 - [ ] 原有测试不受影响(`cd everything-is-suitable-uniapp && npx vitest run` 全部通过) - [ ] 云函数执行时间 < 3s/用户 --- ## 执行顺序建议 ``` Week 1: ├── Day 1-2: Issue 1 — 共享算法包抽取(可并行) ├── Day 1: Issue 2 — 云开发环境搭建(可并行) ├── Day 3-4: Issue 3 — 订阅功能实现 └── Day 5: Issue 4 — 每日推送云函数 Week 2: ├── Day 1: Issue 5 — 订阅管理功能 └── Day 2-3: Issue 6 — 集成测试与验收 ``` ## 自检清单 1. **规格覆盖度:** PRD 中 F1-F8 全部覆盖 ✅ 2. **占位符扫描:** 无 TODO/待定/后续实现 ✅ 3. **类型一致性:** 云数据库字段与 PRD 定义一致 ✅ 4. **测试覆盖:** 功能测试 14 项 + 异常测试 4 项 + 算法兼容性 2 项 ✅ 5. **原有功能不受影响:** 现有运势页面、紫微排盘、黄历查询不受影响 ✅