3.4 定时触发器:每日自动摘要

评论和点赞接口上线后,每天早上我都要手动看一遍数据:昨天发了多少篇、哪篇最火、计数器有没有不一致。某天起晚了,中午才看,错过了上午该处理的一条违规评论。当天晚上我就给云函数挂上了定时触发器,让这些巡检和汇总每天凌晨自动跑。这一节记录定时触发器从配置到落地的全过程,以及调试过程中遇到的时区和间隔两个经典坑。

场景:哪些活适合交给定时器

不是所有巡检都值得自动化,先盘一下哪些是高频且无脑的:

  • 每日数据摘要,汇总发文量、互动量推送给管理员
  • 计数器对账,修正点赞数与明细数不一致的文章
  • 冷启动预热,每十分钟空跑一次列表函数
  • 过期内容下架,超过保留期的临时内容自动隐藏
  • 证书与额度巡检,快到期或快超额时提前告警

前两类每天跑一次,第三类一天跑一百多次。触发器和函数是多对多的:一个函数可以挂多个触发器,不同函数也可以共用同一时间规则。理解这一点很重要——不要为每个定时任务都新建一个函数,触发器只是函数的一个调用入口。

配置触发器

项目里统一用 CLI 配置,函数目录下的 cloudbaserc.json 是入口。触发器配置长这样:

{
  "functions": [
    {
      "name": "article",
      "triggers": [
        {
          "name": "dailyDigest",
          "type": "timer",
          "config": "0 0 8 * * * *"
        }
      ]
    }
  ]
}

config 是 Cron 表达式,七位格式:秒 分 时 日 月 星期 年。0 0 8 * * * * 表示每天早上八点执行。部署时一行命令:

# 部署函数的同时创建触发器
tcb fn deploy article --triggers

通信扩展相关的推送能力,可以看云开发文档里的扩展能力说明,摘要推送最终就是走的这个通道。

常用 Cron 规则对照表,直接抄就能用:

规则 含义 项目里的用途
0 0 8 * * * * 每天八点 每日摘要推送
0 30 2 * * * 每天凌晨两点半 计数器对账
0 */10 * * * * * 每十分钟 列表函数预热
0 0 1 * * * 每月一号 月度成本汇总
0 0 11 * * 1 每周一上午十一点 周报数据

我第一次写 Cron 就写错了:把 0 0 8 * * * * 少写了一位变成 0 0 8 * * *,部署直接失败。踩过之后养成习惯,写完在控制台的触发器面板里确认下次执行时间,那里会显示下一次触发的绝对时刻,一眼能看出对不对。

控制台也能配触发器:函数详情页 → 触发器 → 新建。在线编辑适合临时调整,但项目里不推荐,原因是它不进版本库,换台电脑部署就丢了。所有触发器一律跟代码走 cloudbaserc.json,这是血泪教训。

每日摘要的实现

摘要函数的核心是把散落的数据聚成一小段可读文本。代码结构分三步:取数、汇总、推送。

// 每日摘要函数
exports.main = async (event, context) => {
  const now = new Date()
  const start = new Date(now.getTime() - 24 * 60 * 60 * 1000)

  const [articles, likes, comments] = await Promise.all([
    db.collection('articles').where({
      createTime: cmd.gte(start.getTime())
    }).count(),
    db.collection('likes').where({
      createTime: cmd.gte(start.getTime())
    }).count(),
    db.collection('comments').where({
      createTime: cmd.gte(start.getTime())
    }).count()
  ])

  const hottest = await db.collection('articles')
    .orderBy('likeCount', 'desc')
    .field({ title: true, likeCount: true })
    .limit(3)
    .get()

  return buildReport({ articles, likes, comments, hottest })
}

buildReport 把数字拼成一段纯文本,通过推送通道发给管理员。推送的内容里会带上热度最高的三篇,这也是我们最早的数据驱动产品迭代来源——每天看哪篇最火,就知道下一版该往哪倾斜,这比每周开会拍脑袋有效得多,后来整个运营节奏都按着每日摘要走,起步方法在选型与开工里讲过的快速验证思路一脉相承。

摘要里还有一个重要的健康指标:计数器差异条数。就是把对账结果附在摘要末尾,正常是零,出现非零说明有并发更新漏掉了。有这个数字之后,计数器问题从被动发现变成主动监控。对账的实现完整版在评论与点赞里讲过幂等层,这里补上最后一层。

踩坑一:时区

最经典的一个坑是时区。Cron 表达式默认是 UTC 时间,我配的八点实际是北京时间下午四点,摘要推了两天才反应过来为什么数据看着不对——它汇总的是下午四点前二十四小时的数据,把前一天的尾巴算进来了。

两种解法:一是配置时把北京时间换算成 UTC(北京时间是 UTC 加八,所以北京时间早上八点对应 UTC 零点),二是在云函数环境变量里设置时区(部分运行时支持)。项目里选了第一种,可配性差一点但确定性强,所有 Cron 都按 UTC 写再注释上换算:

{
  "name": "dailyDigest",
  "type": "timer",
  "config": "0 0 0 * * * *",
  "remark": "UTC 零点,即北京时间早八点"
}

第二个相关的坑是漏跑。定时触发器不保证精确,高峰期可能延迟几分钟甚至十几分钟。所以函数内部一定不要依赖它准点执行:取数的时间窗口用相对当前时刻计算(像上面代码里 now.getTime() - 24小时),而不是写死从昨天零点开始。这样无论函数延迟多久,二十四小时窗口永远是对的。

踩坑二:预热触发器的间隔

预热触发器踩的是另一个坑。前文列表获取那节提过冷启动问题,我配了每十分钟的预热,为了省事直接把预热函数和摘要函数通用化,让触发器调用同一个函数。结果某天早上一看日志,预热触发器也在发每日摘要推送——因为函数内部没区分调用来源。

定时触发器的 event 里带有触发器标识,函数可以根据它区分来源:

// 区分触发来源
exports.main = async (event, context) => {
  const isTimer = event.TriggerName || event.Message
  if (isTimer === 'keepWarm') {
    return warmUp()
  }
  if (isTimer === 'dailyDigest') {
    return dailyDigest()
  }
  // 普通调用
  return handleApi(event)
}

第二个预热的坑更隐蔽:预热频率过高直接把免费额度跑光。每十分钟一次等于一天 144 次调用,如果峰值时段之外也在预热就是纯浪费。优化方案是只在业务时段预热:

预热策略 每日调用数 效果 额度消耗
全天每十分钟 144 次 全天无冷启动 中等
早八到晚十二每十分钟 96 次 高峰无冷启动 较低
仅早晚各一次 2 次 仅首屏无冷启动 极低
按需由首个用户承担 0 次 首个用户略慢 为零

项目最后选了第二种。免费额度这件事在前面费用与免费额度里算过明细,这里只强调一句:定时器的调用次数本身也计费,配间隔时要把调用量一起算进去,别省了冷启动的体验、亏了额度的预算。

监控触发器本身

定时任务最怕的是静默失败:触发器不触发了,你却一无所知。防范手段是给摘要函数加一道自检——每天摘要发完顺手记录一条执行日志到集合里:

// 执行留痕,用于自检
await db.collection('digest_logs').add({
  date: new Date().toISOString(),
  status: 'ok',
  duration: Date.now() - startTime
})

再写一个每周一的巡检函数,检查过去七天 digest_logs 是不是正好七条,少了就立即告警。用定时器监控定时器,这个套娃是监控体系的常规操作。完整监控方案的搭建在后面监控与成本那一节,会连同云函数自身的告警配置一起讲。

还有一个排查小技巧:本地调试触发器要把 event 手动构造成定时器格式,传一个包含 TriggerName 的对象进去就行,这样可以在本地完整跑通摘要逻辑而不用等一晚上。云函数调试的整体方法见云函数创建那一节。

小结

定时触发器是让项目从手动走向自动的关键一步。核心记住四点:Cron 是七位格式且默认 UTC 时区、时间窗口用相对时刻计算、按触发来源分发逻辑、调用量计入成本预算。配置成本很低,但它把巡检、对账、摘要这些日常杂活彻底固化成了代码,我从此再没在早上手忙脚乱查过数据。

第三篇到这里就结束了。回顾一下:云函数创建打开了后端能力,列表分页处理了读多写少的主路径,原子计数器搞定了写操作的准确性,定时触发器补上了自动化运维的最后一块。下一章进入内容发布与存储,把发文流程、图片直传和内容审核串起来——那才是真正面向用户的部分。

延伸阅读