Home/Writing/从轮询抖音到微信提醒:我做了一个“抖抖先知”小程序
项目复盘August 15, 2026· 2 min read

从轮询抖音到微信提醒:我做了一个“抖抖先知”小程序

复盘一个抖音开播提醒工具的完整实现:状态采集、多用户订阅、微信一次性订阅消息、额度验证与低资源部署。

我最近做了一个叫“抖抖先知”的微信小程序。它解决的问题很直接:关注的抖音主播开播时,希望能及时收到微信提醒,而不是反复打开抖音查看。

这个需求看上去只是“定时查询 + 发消息”,真正做起来却横跨了抖音公开数据采集、状态机、多用户订阅、微信登录、一次性订阅消息、持久化和容器运维。更重要的是,抖音和微信都有各自的平台约束,系统不能只在理想路径上工作。

用户看到的流程

小程序把使用流程压缩成了几步:

  1. 粘贴从抖音复制的分享内容、短链接、作品链接或主播主页链接。
  2. 系统解析主播并添加提醒,开播提醒默认开启。
  3. 用户在小程序内主动授权微信订阅消息。
  4. 服务端持续检查主播状态。
  5. 检测到“未开播 → 开播”的状态变化后,向对应用户发送微信服务通知。

首页可以管理订阅、查看主播最近状态,也会醒目显示剩余通知次数。额度不足时,用户可以点击“续订 +1”;“测试通知”则会真实走完整发送链路,帮助确认模板、授权和服务端配置是否连通。

抖抖先知小程序首页:主播订阅与提醒开关

抖抖先知首页的真实运行界面:每个主播可以分别控制开播提醒和新作品提醒。点击截图可查看原图。

系统是怎样串起来的

微信小程序
  -> wx.login 换取 openid 和短期会话 Token
  -> 添加、修改、删除主播订阅
  -> 主动申请微信一次性订阅消息

Node.js 服务
  -> 解析抖音分享内容和 sec_uid
  -> 定时检查主播状态
  -> 识别开播、新作品等状态变化
  -> 查询需要接收该事件的用户
  -> 调用微信 subscribeMessage.send

SQLite
  -> 用户
  -> 主播库
  -> 用户与主播的订阅关系
  -> 主播状态基线
  -> 通知授权额度与投递记录

数据模型没有简单地给每个用户复制一套主播任务。主播以 sec_uid 全局去重,多个用户订阅同一主播时,后台只检查一次,再按照订阅关系分发通知。这样用户数量增长时,外部请求量不会跟着线性重复。

SQLite 使用事务、唯一约束和 WAL 模式保存业务状态。开播检测依赖上一轮状态,因此状态基线和事件去重必须持久化;否则服务重启后,很容易把已经开始的直播再次当成新事件发送。

从 Chromium 轮询到纯 HTTP 查询

最初版本使用无头 Chromium 打开抖音页面。这个方案对复杂链接解析和作品列表很有用,但如果每个主播、每轮检查都启动浏览器,CPU 和内存成本会很高。

我后来把开播状态查询单独拆了出来。实测发现,主播资料接口只需要一个匿名 ttwid Cookie。服务可以通过字节的注册接口获取 ttwid,随后使用普通 HTTP 并发读取昵称和 live_status

注册并复用 ttwid
  -> HTTP 并发查询主播资料与开播状态
  -> 空响应时刷新一次 ttwid
  -> 仍失败再降级到 Chromium

一次实验中,4 个主播的 HTTP 并发查询约 451 ms;部署镜像中的单个真实查询约 629 ms,而且完全不需要启动 Chromium。

最终采用混合策略:

  • 只开启开播提醒:优先走纯 HTTP。
  • 开启新作品提醒:继续使用 Chromium 读取作品列表。
  • 分享链接解析和需要页面能力的场景:保留有界 Chromium Worker。
  • HTTP 异常:自动降级到 Chromium,优先保证功能可用。

这次优化不是“把浏览器删掉”,而是让昂贵能力只出现在真正需要它的路径上。

最难处理的是微信订阅额度

微信普通订阅消息通常是一次授权对应一次发送机会,而且 wx.requestSubscribeMessage() 必须由小程序内真实的用户点击触发。服务端不能在额度归零后静默续订,打开小程序、点击通知卡片进入首页,也不能代替这次主动手势。

这意味着以下做法都不可靠:

  • 服务端发现额度为 0 后自动申请。
  • 一次点击后在程序里循环申请 10 次。
  • 只修改本地数据库,把一次授权伪装成 10 次或 100 次。

实际测试确认,每次真实手动点击可以增加 1 次,多次点击可以累计;但同一次点击后的自动循环会被微信以“只能由用户点击手势调用”拒绝。因此界面最终保留准确的“续订 +1”,并明确提示可以重复点击累计。

微信限制批量续订:一次点击仅首个请求有效

批量续订实验的真实结果:第一次授权有效,后续自动调用被微信以 can only be invoked by user TAP gesture 拒绝。点击截图可查看原图。

为了验证本地记录和微信真实权限一致,我做过一次边界测试:先通过 10 次独立授权累计额度,再连续发送 10 条真实测试通知。结果是 10/10 全部被微信接受,数据库也从 10 精确降到 0,没有继续发送第 11 条。

额度处理遵循一个原则:只有微信真正返回成功,系统才消费一次;微信永久拒绝或返回额度不可用时,本地记录立即失效。显示的数字必须是可解释的状态,而不是让界面看起来更好看的“积分”。

把失败路径也做成产品能力

提醒系统最容易出现的问题不是代码报错,而是用户不知道哪一环失效了。因此小程序提供了明确反馈:

  • 0 次时红色提示,低额度时橙色预警。
  • 续订结果区分同意、拒绝和权限关闭。
  • 测试通知会提前说明将消耗一次额度。
  • 测试失败会显示“未授权”“模板未配置”或微信返回的具体原因。
  • 通知模板 ID、字段名和小程序跳转版本都由服务端配置,敏感 AppSecret 不进入小程序包。

微信订阅消息点击后只能进入小程序内部页面,不能直接把 page 写成抖音外链。当前选择是进入小程序首页,让用户先看到提醒和订阅状态;如果以后增加详情页,也应该由小程序展示事件详情,再提供复制抖音链接的操作。

部署与资源边界

服务运行在一台资源有限的个人服务器上,因此部署配置刻意收紧:

  • Docker 容器内存上限为 1 GiB。
  • Chromium Worker、HTTP 查询和通知发送都有独立并发上限。
  • 应用以非特权、只读文件系统运行,删除 Linux capabilities。
  • 配置目录和密钥目录只读挂载,SQLite 数据单独持久化。
  • /tmp 使用有容量限制的 tmpfs。
  • 轮询看门狗和浏览器硬超时负责从卡死任务中恢复。

这套边界让服务既能处理抖音页面的不确定性,也不会因为一次浏览器峰值拖垮整台服务器。

这次实现带来的几个认识

第一,提醒产品的核心不是“能发一条消息”,而是状态变化、授权、去重和失败恢复组成的完整链路。

第二,平台限制应该进入产品设计。微信要求用户主动授权,就应该把剩余次数、续订入口和测试能力做清楚,而不是在服务端伪造一个无限额度。

第三,优化要根据工作负载拆分。纯 HTTP 很适合高频开播检查,Chromium 仍然适合复杂页面与作品解析,两者组合比押注单一路径更可靠。

最后,一个小工具真正变得可信,往往不是因为功能变多,而是因为每个数字、每次通知和每条失败提示都能对应到真实发生的事情。