☰
订单提醒功能扩展 · 工作纪要
2026/10/15 14:24:45 网站建设 项目流程

订单提醒功能扩展 · 工作纪要

时间跨度:2026-10-12 22:17 ~ 2026-10-13 23:07
涉及仓库:langbi25/puppy-canteen(三分支:cloud/selfhost/main)
主用分支:cloud


0. 一句话概括

把订单提醒从「只有下单人能收到、且只发得出 1 条」,
扩展成「4 个事件(下单 / 开始制作 / 已完成 / 已取消)× 两个人各收一条」。


1. 起点:一开始坏在哪

用户的原话是:

「只有点了开始制作才有通知,完成制作没有通知;为什么不是 4 个状态各一条?」
「用这个小程序的两夫妻都能有消息提醒」

三个叠加的根因

#根因后果
1微信一次性订阅「授权 1 次 = 只能发 1 条」;下单时只申请了 1 个模板只有 1 条额度,发完「制作中」就清零,后面「已完成」「已取消」被微信拒收(43101)
2旧日志把43101当「用户未订阅」这种正常情况静默掉失败完全不可见,一直以为"微信不让发"
3收件人只从profiles表取,而另一半没有 profiles 行(她从没在「我的」页填过昵称)即使额度够,也永远发不到她那边

第 3 条是查库查出来的实据:

profiles: 1 行 → wx:...gmnuVpOk(栗帆) orders: 2 个 owner_id wx:...gmnuVpOk 15 单(栗帆) wx:...Qiy8Q 2 单(家人)

profiles只有saveMyProfile一个写入点 —— 只下过单、没填过资料的人,
在profiles里就是不存在。


2. 时间线总览

时间阶段关键动作cloud 提交
10-12 22:17单模板 → 多模板模板注册表 + 按状态拆模板d15dbe9
10-12 22:28拿到 2 个模板 ID用户申请「已完成」「已取消」f8e6e8a
10-12 22:41字段配齐,3 模板就绪照抄详情页字段名3bd0f23
10-13 00:14三事件 × 两个人多收件人 + 通知开关 + DB 加列59b5ab3
10-13 22:35「开始制作」也通知4 事件拆两批授权69b51d0
10-13 22:55第 4 个模板配齐填「接单提醒」ID 与字段5f16424
10-13 23:07真机首测定位到容器跑的还是旧版—

3. 每一轮具体做了什么

3.1 多模板架构(10-12 22:17)

改法:utils/notify.js从「一个模板 ID」改成模板注册表:

TEMPLATES={pending:{id,fields},// 每个状态一个独立模板 IDcooking:{id,fields},// 每个模板一套独立字段映射done:{id,fields},cancelled:{id,fields}}

为什么必须按状态拆模板:额度按「模板」独立计数。共用一个模板 = 互相抢额度,
这是当时反复翻车的根源。

关键设计——「半配置 = 安全状态」

configStage()把配置状态分成三档,中间态不会发错消息:

stage含义行为
not-appliedID 还是占位符跳过授权、跳过发送
need-fields有 ID 但字段没填不发送,打 ⚠️ 告警
ready两者都齐正常发送

这样「先填 ID、隔几天再补字段」的中间态,不会发出空data去撞47003 参数不合法。


3.2 三事件 × 两个人(10-13 00:14,最核心的一轮)

这一轮把「通知」从「发给下单人」改成「发给所有开了通知的人」。

收件人来源修正(关键)
// utils/api.jsfetchNotifyTargets()=profiles(带开关) ∪ orders.owner_id(去重)

只按profiles取会漏掉另一半;补上orders.owner_id立刻覆盖两个人。

容器端支持多收件人

cloudrun/notify-service/server.js:

normalizeRecipients(body)// 支持 recipients 数组,兼容旧的单个 touser// 串行逐个发送,回 { ok, sent, failed, total, results: [{touser, errcode, errmsg}] }

一次网络往返发完所有人 —— 避免前端循环调用反复撞冷启动。

数据库加开关列
ALTERTABLEpublic.profilesADDCOLUMNIFNOTEXISTSnotify_enabledBOOLEANNOTNULLDEFAULTtrue;
  • 已直连云服务数据库执行(不用手工贴 SQL)
  • backend/schema.sql+selfhost/sql/00-init.sql已同步
  • 容错:这列查不到时前端按true处理(= 都发),漏执行 SQL 不会导致漏发
小程序端新增通知开关

「我的」页加了一个卡片 +<switch>:

  • 打开时先弹授权;被拒就把开关拨回去并说明原因(不会出现"显示开着但实际没授权"的假象)
  • 顺带修掉一条误导性兜底文案(“预览环境无法完成微信授权”)

3.3 「开始制作」也要通知(10-13 22:35)

用户推翻了之前「开始制作不发」的决定。4 个事件 = 4 个独立模板,
于是必须先确认一次弹窗能带几个模板。

查证结果:官方口径不一致
来源口径
官方wx.requestSubscribeMessage页面「一次调用最多可订阅5条消息」
腾讯云文档 / 开放社区镜像 / 大量存量代码3条
超限后果返回errCode 20003,整个调用失败,一张券都拿不到

风险不对称 → 保守按 3 设计,把 4 个模板拆成两批。
(代码里是常量MAX_TMPL_PER_REQUEST = 3,日后确认线上稳定支持 5,改常量即可。)

分批原则(只有一条)

谁触发事件,就在那个 tap 上把对应模板的券要过来。

批次模板挂在哪个 tap
order新订单 + 开始制作 + 已完成点「提交订单」+「我的 → 订单通知」开关
cancel已取消点「取消订单」(弹确认框之前)

这样做的好处:不是"授权一次然后慢慢用光",而是每次动作都自动补券——
一次性订阅「1 次授权 = 1 条」,只有靠高频动作反复补券才撑得住。

一个时序坑

requestSubscribeMessage必须由用户点击直接触发。
所以「取消订单」的流程必须写成:

// ✅ 先要券,再弹确认框if(status==='cancelled')awaitnotify.requestOrderNoticeAuth('cancel')wx.showModal({...success:async(res)=>{/* 改状态 */}})

不能等showModal的success里才调 —— 那时已经错过时机,券要不到。
另外同一个 tap 里只能调一次,第二次会被静默忽略。


3.4 第 4 个模板配齐(10-13 22:55)

用户申请到「接单提醒」,字段名照抄详情页:

模板字段占位符填什么说明
接单编号number1订单 IDnumber类型只能填纯数字,订单 ID 正好是
接单时间time11通知发出的此刻比下单时间更符合"接单"语义
单号character_string12订单 ID模板有两个编号位,系统只有一个订单号 → 共用同值
接单人thing10下单人系统没有"是谁点了开始制作"的字段 → 用下单人顶上

四个模板标题互不相同:下单完成通知 / 接单提醒 / 订单完成通知 / 订单取消通知。
一次授权里模板标题不能相同,相同会被后台过滤(返回'filter')。

重要发现:占位符编号完全不规律
语义完成模板取消模板接单模板
时间time1time5time11
菜品thing4thing2—

必须逐模板照抄详情页,绝不能凭上一个模板推断。


3.5 真机首测翻车:容器还在跑旧版(10-13 23:07)

症状

授权全部accept(券都拿到了),但发送全部返回:

{ ok: false, error: "missing touser or data" }
定位(10 秒破案:文案比对法)
对比项值
本地新版server.js的缺参文案missing recipients or data
线上容器实际返回missing touser or data

touser这个词只存在于旧版代码。新版支持recipients数组,文案早改了。
小程序端发的是新格式 → 旧容器不认识recipients,只找touser→ 报缺参。

根因

改了容器代码、打了 zip ≠ 部署。
多收件人改造(10-13 00:14)改完server.js并重新打了server.zip,
但云托管控制台上从来没有上传发布过这个包,线上一直跑旧版。

三重验证(确认本地代码与 zip 没问题)
zip 内 server.js : 2a2677bced54e9ba 6071 bytes 磁盘 server.js : 2a2677bced54e9ba 6071 bytes ← 哈希完全相同 工作区 vs cloud 分支 : 无差异 本地服务自检 : 全通过
正确做法

微信云托管控制台 → 服务列表 →notify-service→发布新版本→ 上传server.zip。
配置不用动,但确认实例副本数最小值 = 1(避免冷启动撞callContainer的 15 秒上限)。

教训:凡是动过cloudrun/**的轮次,收尾清单必须包含
「重新上传 zip 并发布版本」,并且要注明是哪一轮改的代码。


4. 平台知识点(已查证,不必重查)

4.1 微信订阅消息

项值
一次性订阅额度授权 1 次 = 只能发 1 条
授权有效期7 天(过期未用自动作废)
一次弹窗模板数上限官方写 5,存量写 3 →保守按 3,超限20003整批失败
长期订阅类目仅政务/医疗/交通/金融/教育(餐饮/工具申请不到)
模板总数上限25
模板创建后字段不能改,只能新建
一次授权里模板标题不能相同,相同会被过滤

授权侧错误码

码含义
20001模板不存在
20003模板数超上限(整批失败)
20004用户关了订阅主开关
'filter'该模板因标题重名被后台过滤

发送侧错误码

码含义
43101某个收件人没额度 → 让那个人重新授权(不是"正常情况",日志必须点名是谁)
47003参数不合法(字段名错/为空/超长),errmsg 会点名哪个字段
41030page 路径在现网版本不存在

字段类型限制:thing≤20 字符;phrase≤5 汉字;number只能纯数字;character_string≤32 字符。

⚠️额度按「人 × 模板」独立计数(最容易误解的一条):
发通知给别人,用的是别人的额度。我有券不代表对方有券。
所以「两个人都要收到」的现实含义是:两个人都得各自授权一次。

4.2 微信云托管

项值
callContainer的path容器内裸路径(/notify),不带服务名
服务名写哪headerX-WX-SERVICE
云调用白名单填URL 路径(/cgi-bin/message/subscribe/send),带 query 就不匹配(→85107)
实例缩容最小副本 0 时半小时无请求缩容;callContainertimeout上限 15 秒
版本生效改代码/配置后必须重新发布版本,不会热更新
识别线上是旧版拿容器报错文案和本地代码比对,对不上就是旧版

5. 关键设计决策(别再推翻)

决策选择理由
回退模板FALLBACK_KEY = null(不设回退)回退会让不同状态消息长得一样、还互相抢额度 —— 两次翻车都源于此。没配就不发并告警
sendOrderNotice一律不 await通知要走一次云托管往返(可能撞冷启动,最长 15s),绝不能拖住下单跳转或列表刷新
requestOrderNoticeAuth必须 await 且紧贴 tap弹窗必须由用户点击直接触发
授权分批order(3 个)+cancel(1 个)4 个装不下一次弹窗,超限整批失败
批次内未配置模板自动剔除某个模板没配好时,同批其他模板照样能拿券
收件人来源profiles∪orders.owner_id另一半没有 profiles 行
通知对象两个人各一条(含操作者本人)需求如此;不判断"谁是操作者"
日志纪律禁止兜底文案兜底文案会把真错误伪装成正常情况,让人停止排查

6. 费用:微信云托管怎么收费

6.1 结论先说

是收费的,按量付费、先用后付,每个环境独立计费。
但首个环境赠送 3 个月免费额度,按我们目前的用法大概率用不完。

6.2 单价与免费额度(官方刊例价)

计费项单价首环境免费额度(有效期 3 个月)
容器 CPU 使用量0.055 元/(核·小时)720 核·小时
容器内存使用量0.032 元/(GB·小时)1440 GB·小时
构建时长0.05 元/分钟600 分钟
公网流量0.8 元/GB5 GB

6.3 什么时候产生用量(这条最影响成本)

  • CPU / 内存:环境中任意服务下任意版本,实例数量不为 0就持续产生
    → 换句话说,只要有一个实例常驻在跑,24 小时都在计费
  • 公网流量:只有通过公网域名访问才产生 →
    我们全程用callContainer,这项用量为 0(官方原文:只通过 callContainer 访问服务不产生用量)
  • 构建时长:只有发布时用「本地代码/代码库拉取」方式才计费

6.4 我们的场景估算

按「副本数最小 = 1」(防冷启动超时)+ 单实例默认规格 0.25 核 / 0.5 GiB 估算:

项每月用量每月费用
CPU0.25 × 720 = 180 核·小时9.9 元
内存0.5 × 720 = 360 GB·小时11.52 元
合计约 21 元/月

对比免费额度(720 核·小时 + 1440 GB·小时,3 个月):
3 个月实际消耗约 540 核·小时 + 1080 GB·小时 →都在免费额度内。

具体规格以控制台实际配置为准。若单实例是 0.5 核 / 1 GiB,
费用约翻倍(约 43 元/月),3 个月也算得过来(1080 / 2160,仍在额度内)。

6.5 省钱的关键开关

实例副本数最小值:

设置费用风险
1约 21 元/月(免费额度内则 0)无,稳定
0近乎 0(空闲不产生用量)冷启动可能超过callContainer的 15 秒上限 → 通知发不出去

家庭场景消息量极低,保持最小副本 1是最稳妥的选择。

6.6 在哪里看账单和用量

微信云托管控制台 → 费用中心 → 充值与账单

(入口:微信公众平台 / 云托管控制台首页的「费用中心」)


7. 当前状态

代码侧:已全部就绪 ✅

项状态
4 个事件模板 ID✅ 全部配置完成
字段映射✅ 逐一对照详情页校验
多收件人✅ 前端 + 容器
通知开关✅ 「我的」页
DB 列notify_enabled✅ 已执行
小程序端自检✅ 全通过
容器端自检✅ 全通过
三分支同步✅ 无 ahead

剩余步骤:全部是用户操作

  1. ⚠️ 云托管重新上传server.zip并发布新版本
    • 控制台 → 服务列表 →notify-service→ 发布新版本
    • 文件:canteen-miniprogram-v2/cloudrun/notify-service/server.zip
    • 确认实例副本数最小值 = 1
    • 🔴这一步不做,通知一条都发不出去
  2. 重新上传小程序代码(体验版,水印应为notify6)
  3. 两个人各自在「我的 → 订单通知」把开关关掉再打开,勾「允许」+ 勾「总是保持以上选择,不再询问」
  4. 真机验证 4 条:下单 → 开始制作 → 已完成 → 取消,每一条两人都应收到
  5. 发正式版前:server.js的MINIPROGRAM_STATE从trial改formal,并重传容器包

8. 提交记录

分支用途最近提交
cloud主用(连云服务,体验版)0456e5c
main备份3021888
selfhost自建后端ba7175e

发版水印:cloud-/main-/selfhost-2026-10-13-notify6(前缀各留各的)。


9. 相关文档

文档内容
docs/order-notify-templates-guide.md模板申请、字段映射、授权分批、部署清单
docs/order-notify-setup.md整体链路、错误码判读
tools/notify-selfcheck.js小程序端离线自检(打桩,不发真实请求)
tools/server-selfcheck.js容器端离线自检

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询