一个帮你决定「这一顿吃什么」的微信小程序。
收录自己的菜品,按餐次、口味、预算圈定范围,摇一次骰子定下来;吃完记一餐、写一句当天心情。日历把每一餐和心情放进同一张日卡,往回翻能看见自己这大半年是怎么吃的。另有一个独立的日期提醒模块,支持公历 / 农历与循环规则,可选接入微信订阅消息做真正的到点推送。
技术形态是原生小程序 + Skyline 渲染 + 微信云开发,无 npm 依赖、无构建步骤,导入微信开发者工具即可编译运行。
README 只留「这是什么 / 怎么跑起来」。实现细节拆在 docs/ 下,改代码前按需要翻:
| 文档 | 讲什么 |
|---|---|
docs/sync.md |
本地优先的数据流、补传秩序、增量水位、纯函数引擎清单 |
docs/data-model.md |
目录结构、五个云集合、必建索引 |
docs/reminder-engine.md |
提醒的四类循环语义、「提前 N 天」、5 分钟窗口、订阅额度 |
docs/deploy.md |
云函数部署、定时触发器的唯一重建方式、推送链路排障 |
docs/privacy.md |
隐私保护指引的声明清单、可粘贴全文、错误码自检 |
- 四个维度可叠加筛选:餐次(早餐 / 午餐 / 晚餐 / 夜宵)、分类(13 类)、用餐方式(自己做 / 外卖 / 堂食)、预算档(经济 / 普通 / 丰盛 / 豪华,对应 10 元以下 / 10–30 / 30–120 / 120 元以上)
- 点「开始随机选」播放约 1.2 秒滚动动画(12 步 × 100ms)后在候选里落定;候选超过一道时保证不与上一次相同
- 不是均匀随机,是按权重抽:评分(5 分 ×1.6、1 分 ×0.4)、多久没吃(60 天以上 ×1.5)、吃过次数(20 次 ×0.5)三项相乘。打过 2 分又刚吃过的菜明显少出现,但不是永不出现
- 自动避开最近 N 天吃过的菜(1 / 3 / 7 / 14 / 30 天,默认 7 天);打开「允许近期重复」后不再硬过滤,改为把最近吃过的菜再压到 1/4 权重——允许重复不等于优先重复
- 单道菜可以单独标记为不参与随机
- 结果卡显示「3 天前吃过」与平均分,确认后点「就吃这个」直接进记录页,餐次已预填
- 「最近吃过」六个入口:按记录时间倒序取最近六道不同的菜(刚好铺两行),点一下直接进记录页(菜品 / 餐次 / 日期都预填),把「记一餐」从五步压到两步
- 候选为空时提示放宽条件,并给出一键清空
- 三类共 38 个:中国传统节日 14 个(腊八 / 小年 / 除夕 / 春节 / 元宵 / 龙抬头 / 上巳 / 端午 / 七夕 / 中元 / 中秋 / 重阳 + 清明 / 冬至两个节气节日)、国际节日与国际日 18 个、中国法定纪念日 6 个
- 农历节日每年现算(走
utils/lunar.js),不写死某一年的公历日期;闰月不重复过节;除夕按「腊月最后一天」算,腊月廿九那年也对 - 清明与冬至是节气,用寿星通用公式
int(Y×0.2422 + C) - int((Y-1)/4)算,误差最多一天 - 母亲节 / 父亲节 / 感恩节是「某月第 N 个星期几」,按当月第几个算
weekFestivals()返回本周剩余节日(最多 3 条),按日期升序
- 搜索(菜名 / 标签)、按分类过滤、四种排序:最近添加、名称、评分、吃过次数
- 新建、编辑、详情、删除。字段:名称(20 字)、Emoji 图标(内置图标库,默认平铺 24 个,可展开)、分类、标签、适用餐次、用餐方式、预算档、备注(200 字)
- 评分与吃过次数不由用户填写,统一从饮食记录聚合重算;改一餐会同时回算被换下和被换上的两道菜
- 日期倒序的日卡列表,一餐一条记录:餐次、菜品、日期、具体时间(时:分,默认取当前时刻、可改)、1–5 分(0.5 步进)、四档状态(吃得很好 / 还不错 / 一般般 / 不太行)、200 字随手记
- 搜索:日历只能按时间翻,翻不到「上次那家店叫什么」。输入关键词即切换成结果列表,菜名 / 备注 / 状态 / 餐次都能命中,最多返回最近 50 条;清空回到日历
- 日卡里的餐按时间先后排;老记录没有
time时用createdAt兜底,列表里不会留空 - 日历页一天一张卡:心情图标 + 随笔 + 当天各餐。有心情的日期在日历上显示心情图标,只有餐的显示当天第一道菜的图标
- 点日历格子只看那一天,再点月份标题右边的「月」回到整月;日历以当月为锚点,往前 12 个月、往后 6 个月,左右滑动或点两侧箭头翻页(到头会停住);翻到别的月份时标题右边还会多一个「今」,点一下直接跳回今天所在的月
- 月份标题右边这两个小胶囊按需出现:
今只在不是本月时出现,月只在选中了某一天时出现,都不是就都不出现 - 「+ 一餐」可补记历史日期,不能选未来;点某一餐是编辑(菜品 / 餐次 / 日期 / 评分 / 状态 / 备注都可改),长按是删除
- 顶部汇总当前范围的「N 餐 · M 天心情」
- 日历下面挂「本周节日」:本周还剩的节日才出现(周一算一周之始,已经过去的不提醒),一行一个,带倒计时、分类标签和「吃点:XX」;看当月且没挑某一天时才挂
- 节日卡那行小字二选一:有吃食的写「吃点:XX」,没有的写一句和这日子有关的话(15 字以内,比如教师节「给老师说声谢谢」)。两句不会同时出现
- 右侧的日期分两种:选中某一天时,节日 / 提醒 / 纪念日都显示农历(中秋 → 八月十五,教师节 → 七月廿九),因为那天在日历上已经写了公历,重复一遍没意义;没选中时(「本周节日」那块)只显示公历,那块要横着扫七个日子,公历更好认
- 在日历上点某一天,那一块改成显示那天的节日、当天会响的提醒与纪念日(标题「X 月 X 日这天」)。那天要是空白,会给一张「这天还是空白 · 写心情」的卡,点一下直接进那天的签到页(未来日子不给这张卡,只提示「这天还没到」)
- 整月视图且今天没记录时,同样挂一张「今天还是空白 · 写心情」的卡 —— 两种空态共用一个组件,只是文案和跳转目标跟着选中态走
- 空态卡左边那个天气图标每次卡片出现都换一个(12 种 emoji 的库里随机)—— 选中某天、取消选中、翻月重绘都会重新挑,不是写死的一朵云
- 未来的日子也能点开看(比如提前看下个月节日落在星期几),但只读:签到与记餐仍然只到当天,日历里点签到 / 补一餐会被拦下并提示,两个表单本身也会再拦一次
- 有节日的那天,日历数字是亮蓝色;自己加的纪念日那天是亮紫色。同一天两者都有时紫色赢;今天 / 选中态的颜色压过它们
- 有提醒的日子,数字右上角挂一个橙红小圆点(停用的提醒不算;纪念日不点,它已经有紫色数字了)。它是绝对定位的角标,不占格子高度;压在「今天」的橙框上时靠白描边分开
- 当周存在的自定义纪念日也会列进同一块,标题自动变成「本周节日 · 纪念日」
- 只记心情,不掺饮食(和「记一餐」是两条独立数据线);16 个心情,签到页 4×4 宫格选择(
images/moods/*.png:羞愧/厌恶/自豪/嘴馋/嫉妒/愤怒/高兴/孤独/焦虑/满足/迷茫/难受/疲惫/平静/期待/惬意);200 字随笔;一天一条,再保存即覆盖;可补历史,不能选未来 - 老心情(开心/兴奋/平淡/低落/忙乱/瞌睡)在
utils/checkin.js的LEGACY_KEYS里映射到新的一套(高兴/期待/平静/难受/焦虑/迷茫),历史签到不会变成同一个表情 - 页面顶部每进一次随机显示一句古诗词(诗经 / 楚辞 / 唐诗 / 宋词共 400 句,单句不超过 20 字),取自
utils/quotes.js,不会连着两次重复 - 连续签到天数:今天已填则从今天往前数,今天还没填则从昨天往前数,当天补填后自动接上
- 心情页汇总本月心情分布(按次数倒序)、最近 14 天心情条带(缺失那天显示为灰点)与历史心情列表
- 首页顶部有一条签到提示,按「今天签没签」分流:没签到点它直接进今天那张写心情的表单(少一步),已经签过就跳到签到记录页(那里点某天还能回头改);右侧的「去签到 / 看记录」跟着状态变
完整语义见下文「日期提醒:能力与含义」,这里只列形态。
- 入口在「我的 → 日期提醒」,列表按 纪念日 / 今天 / 未来 7 天 / 更远 / 已关闭或已结束 分组
- 支持公历与农历(含闰月,1900–2100)、不重复 / 每天 / 每月 / 每年四种循环、当天 / 提前 1 天 / 提前 3 天三种提前节点
- 「每天」可以改成每 N 天一次(表单里循环方式下面有步进器,默认 1 = 每天,范围 1–365);节奏以起始日期为准,10-01 建、每 3 天 → 10-01 / 10-04 / 10-07…,列表标签跟着写成「每 3 天」
- 同一张表单还能建「纪念日」:纪念日每年都有(公历按同月日、农历按农历同月日)
- 纪念日默认不推送,只在日历上把那天标成紫色;打开「到期提醒」开关后,可以像普通提醒一样选**当天 / 提前 1 天 / 提前 3 天(可多选同时提醒)**与提醒时间,会照常推送并计入当日角标;停用整条则不推
- 云函数扫描时按
kind+notify判断:没开提醒的纪念日一律跳过,开了的跟普通提醒走同一套触发计算 - 卡片开关可临时停用,轻点编辑、长按删除;「我的」页会显示条数与当日提醒角标
- 应用内提醒开箱可用;微信服务通知需要自己申请模板并部署云函数
- 收录菜品数、本月记录数、平均评分、「目前最喜欢」(评分最高者,并列时取吃过次数多的)
- 昵称与头像走
chooseAvatar/ 昵称填写能力,头像上传云存储,换设备自动恢复 - 三个偏好项:默认餐次、最近天数、是否允许重复(默认开着,最近吃过的只是降权到 1/4,不硬性排除)
入口在「我的 → 饮食报告」,可往前翻月份,不给看未来月份。
- 三个数字:本月餐数、有记录的天数、平均分;下面一行「比上月多 / 少 N 餐」
- 最常吃 Top5:按菜名合并(菜品被删了也还能统计,菜名是记下那一刻的快照),次数相同按总分排
- 评分走势:一天一根柱子,按 5 分制换算高度;没记录的天留一条浅色底线
- 餐次分布:早 / 午 / 晚 / 夜宵四条横条,带百分比
- 口味偏好:按菜品标签统计,只认「辣 / 清淡 / 减脂 / 高蛋白 / 快手」五类(「快餐」「聚餐」不算口味)
入口在「我的 → 数据」,导出和导入都走文件,不碰剪贴板。
- 导出:生成
今天吃什么-备份-20261030.json写到本地用户目录,再调wx.shareFileMessage打开微信转发面板——可以直接发给自己(文件传输助手)或任意联系人。用文件而不是剪贴板,是因为几千条记录约 800KB,剪贴板在部分机型会被截断或静默失败;低版本微信用不了转发时会退回复制文本 - 导入:
wx.chooseMessageFile从聊天记录里挑.json(文件得先发到某个聊天里),先预演一遍告诉用户「会导入 N 条、跳过 M 条」,确认后才真正写入 - 合并规则与云端同步完全一致:同 id 比
updatedAt,新的赢;本地独有的条目不删。同一份文件导两次,第二次全部跳过 - 文件带
schema版本号。比当前版本新的备份会被拒绝——更高版本可能带了这个版本不认识的字段,硬导进来会静默丢字段 - 导入的数据逐条排进待传清单,
syncFromCloud每轮最多补传 40 条(一条要 2 次网络往返),量大时需要多开几次小程序才能全部上云 - 导出时剥掉
cloudId(换机器没意义的本地指针)、avatarFileId(绑账号的云存储地址)、pushQuota(绑模板的授权次数) - 「我的」页的占用数字走
wx.getStorageInfoSync,一次同步调用拿总量,不解析任何数据
入口在「我的 → 危险操作」,本机与云端一起删,删完回到新装状态:8 道预置菜、空记录、默认设置、推送额度归 0。
昵称和头像会保留——它们标识「你是谁」,不属于「你吃了什么」。清一次库就要重新设昵称、重新传头像,那是找骂。云端 user_settings 虽然也被删,但下次同步会用本地这份重建,头像文件本来就躺在云存储里。
- 确认弹窗逐项列要删的数量(多少条记录 / 多少天心情 / 额度还剩几次),不是笼统的「确定吗」——看见具体数字才会真的犹豫
- 只清本机是不够的:下一次
syncFromCloud会按「本地没有、云端有」把数据全拉回来,用户会觉得根本没清掉。所以云端五个集合一起清,user_settings删掉后推送额度也随之消失 - 清空那一刻如果断网,会落一个
wte_wipe_pending_v1标记。下次同步开头先补删云端,并且这一轮跳过拉取——否则刚删的东西会被灌回来。补删成功才摘标记 - 订阅勾选状态不清(
wte_sub_state_v1):它只是「总是保持以上选择」在本地的镜像,微信侧本来就不受清空影响。跟着清掉只会让自动补额度整个瘫掉,用户还得再新建一次提醒才能恢复。所以clearAllData的删除列表里唯独排除了它 - 微信侧的订阅授权(模板那个开关)没有接口能撤销。要彻底停掉推送,得用户自己去「设置 → 订阅消息」里关
入口在「我的 → 反馈 → 意见反馈」。
- 类型三选一(功能建议 / 遇到问题 / 其他),内容上限 500 字,最多 3 张截图,邮箱选填
- 只收邮箱,不要改成「微信号 / QQ 号 / 手机号」:2026-11 提审按运营规范 5.12「非合理场景下收集微信帐号等敏感信息」被驳回,当时提示语就是「微信号 / 邮箱」。IM 账号一律不能出现在收集字段里
- 自动带上版本、机型、系统、微信版本与来源页——线上问题最缺的就是这些,指望用户手打不现实
- 提交走云函数
feedbackSubmit:服务端再校一遍长度和类型,图片只收本环境的cloud://fileID(外部链接会把通知变成钓鱼入口),超长内容按 500 字截断 - 落库后推一条群机器人消息:企业微信和飞书都行(地址放云函数环境变量
FEEDBACK_WEBHOOK,仓库是公开的,key 不进代码)- 渠道按 host 自动认:
qyapi.weixin.qq.com→ 企微 markdown,open.feishu.cn→ 飞书 post 富文本;认不出来用FEEDBACK_WEBHOOK_TYPE=wecom|feishu强制指定 - 飞书机器人若开了「签名校验」,密钥放
FEEDBACK_WEBHOOK_SECRET,云函数会按飞书的算法(HMAC-SHA256(key = timestamp\nsecret, data = ""))现算timestamp+sign - 成功与否不只看 HTTP 状态码,还看响应体里的
errcode/code——地址填错时也是 200,不回查就会记成「已推送」 - 截图发的是云存储临时链接,不是内嵌图片:飞书图片消息要
image_key,得用应用凭证调im/v1/images上传,群机器人没这个身份。链接默认 2 小时有效,过期点开 403;取链接失败会降级成「N 张(临时链接没取到)」 - 没配也能用,只是少一条推送;推送失败也照样算提交成功
- 渠道按 host 自动认:
- 同一台设备 60 秒内只放一条,防手滑连点
- 集合不存在时云函数会自己
createCollection,第一次提交就能通,不必先去控制台建表
- 同一微信账号在不同手机上读同一份云端数据;不同账号通过
_openid与集合权限隔离 - 写操作本地先落盘,云端失败只提示不阻断;某个集合缺失时只让对应功能退回本地缓存,补齐后下次启动自动补传
| 层 | 选型 | 说明 |
|---|---|---|
| 运行形态 | 原生小程序(JavaScript / WXML / WXSS) | 无 npm、无构建;Skyline 渲染 + glass-easel,全局自定义导航栏 |
| 本地存储 | wx.setStorageSync |
菜品 / 设置 / 提醒 / 签到各一个键;记录按年分片(wte_records_2026_v1),首屏只读本地 |
| 云端数据 | 云开发文档数据库 | 5 个集合,全部「仅创建者可读写」 |
| 云端文件 | 云存储 | 头像;同样仅创建者可读写 |
| 云端计算 | 云函数 reminderPush / feedbackSubmit / syncBatch |
Node + wx-server-sdk;定时推送、反馈通知、批量补传 |
| 消息 | 订阅消息 | 前端 wx.requestSubscribeMessage,云端 openapi.subscribeMessage.send |
| 测试 | Node 直跑 | 12 套回归测试脚本(仅本地 .workbuddy/tests/,不进仓库) |
- 安装并打开微信开发者工具
- 导入本项目根目录
- 确认登录微信号拥有该小程序的开发权限
- 确认云环境、五个必需集合(
dishes/meal_records/user_settings/reminders/daily_checkins)与云存储权限已就位;error_logs可选 - 点「编译」,再用「预览」做实机测试
项目不依赖 npm,无需安装依赖或构建。只有部署提醒推送云函数时才需要 wx-server-sdk,由开发者工具的「云端安装依赖」自动完成。