每年一到员工生日这种日子行政和HR就开始手忙脚乱。翻Excel、找手机号、挨个发祝福语还要赶在当天发出稍微一忙就会漏掉一两个。之前我们团队就是这么干的直到有一次连续漏了三个同事的生日被吐槽“公司没有人情味”。后来我花了一个周末用微信小程序云开发搭了一个“员工生日短信自动提醒”的小工具结合阿里云短信服务每天定时跑一个云函数自动搜索当天过生日的员工名单然后批量发送祝福短信。整条链路不需要自己买服务器也不用维护定时任务基本上零成本运行。这篇文章就把这个项目的完整实现过程拆开讲一遍包括整体设计思路、短信服务申请、数据表结构、云函数代码、定时触发器配置以及我在实际调试中踩过的各种坑。适合小团队的技术负责人、前端开发或者想用低成本给团队做点自动化工具的开发者参考。只要你接触过小程序云开发和一点点Node.js跟着做就能跑起来。1. 整体设计为什么是“云开发短信API”这套组合1.1 业务链路拆解这个项目从源头上看本质上是“定时任务 条件查询 接口调用”三件事的组合。放在传统架构里你需要一台服务器或容器一个定时调度框架crontab或者任务平台一个数据库存员工信息再写一个脚本调阿里云短信SDK。小团队搞这一套光环境配置和权限管理就要折腾好几天。而微信小程序云开发把这套东西全部塞进了同一个生态里云数据库存员工信息天然支持小程序端读取和写入云函数代替后端服务可以跑Node.js逻辑也可以调外部API云函数定时触发器代替crontab在控制台配置一下就能每天定点执行小程序前端可以直接调云函数实现“搜索员工”和“手动补发”这类交互。整个数据流转是小程序端录入员工信息或者后台导入→ 云数据库employee集合 → 每天定时触发云函数 → 云函数查询当天过生日的人 → 调用阿里云短信接口发送 → 把发送记录写入sendLog集合。这一套闭环下来HR基本不用碰代码只要有人把员工生日维护好剩下的事全部自动化。1.2 为什么不用传统服务器定时任务短信发送本身就是一个请求频次不高的操作给员工发祝福短信一天最多也就发几十条。如果为了这个去买一台云服务器按月付费不说还要操心环境部署、安全组、数据库备份、HTTPS证书……这些运维成本对于一个小工具来说完全是浪费。云开发的免费额度对这类场景非常友好。云函数每月有调用次数和资源用量的免费额度云数据库也有一定免费存储和操作次数。员工几百人的团队每天一个云函数查询一次数据库总开销基本可以忽略不计。如果之后员工规模涨到几千云开发的付费价格也仍然比单独买服务器划算而且还不用自己处理扩容。另外还有一个很实际的原因微信生态的集成体验。云开发环境可以直接从小程序开发者工具里管理云函数上传部署、日志查看、数据库管理都在同一个后台不需要在多个平台之间跳来跳去。1.3 为什么短信通道选择阿里云短信服务商可选的不少国内主流的无非几家云厂商。我最终选了阿里云主要是看中了三点一是价格。阿里云短信套餐包很便宜不到一百块就能买几千条员工数量少的公司可以用很久。祝福短信属于行业短信验证码/通知类价格本身就很低。二是易用性。阿里云短信的API封装很成熟官方提供了Node.js的SDK云函数里直接引入就能用不需要自己拼HTTP请求。签名和模板的审核也比较快小程序类签名叫上就能过。三是稳定性。短信送达要求比推送高阿里云这条链路我测试了几个月的发送成功率基本接近100%。而且阿里云后台能看到每天的发送记录和返回状态出了问题排查非常方便。这里要特别说一下我一开始其实考虑过用小程序订阅消息做生日提醒毕竟不花钱。但订阅消息有一个硬门槛用户必须先授权订阅开发者才能在下发前给用户推消息。你想给员工发生日祝福但员工在入职的时候不一定愿意授权小程序订阅漏授权的人就收不到。而且订阅消息每次推送都会消耗一次授权没法保证长期稳定触达。短信则没有这个问题手机号在短信就能到。所以最终方案确定为主短信通道小程序内部的通知只作为辅助。2. 前置准备开通环境和短信服务申请2.1 云开发环境开通这个项目的小程序端其实很简单核心逻辑都在云函数里。所以第一步是在微信开发者工具里开通云开发。打开开发者工具新建一个项目选择小程序模板然后在工具栏点击“云开发”按钮按提示开通环境。环境名称可以取一个有意义的值比如“birthday-prod”后续云函数代码里会用到这个环境ID。如果你们公司有多个环境测试、生产建议至少在测试环境先跑通再部署到生产。开通之后开发者工具左侧会出现“云开发”面板可以看到数据库、云函数、存储等能力入口。后面我会在数据库里创建employee集合和sendLog集合云函数则直接通过右键新建。2.2 阿里云短信服务申请这一步是很多第一次接触短信服务的人最容易卡住的地方涉及实名认证、签名申请、模板申请三个环节。首先注册阿里云账号并完成实名认证然后在控制台搜索“短信服务”开通服务。新用户一般有免费测试套餐但要正式使用最好直接购买短信套餐包按量付费也行。接下来申请短信签名。签名就是短信开头显示的【某某公司】它必须代表实际使用短信的主体。咱们这个小程序场景一般选择“小程序”类型的签名提交时需要提供小程序的名称、小程序账号的认证信息。注意事项签名名称不能是测试、test、Demo这类词审核大概率会被驳回签名内容最好和公司/品牌强相关比如“某某科技”避免使用宽泛词汇审核一般需要半天到一天申请前把材料准备好可以省不少时间。签名通过后再申请短信模板。模板就是短信正文的骨架里面可以放变量比如亲爱的${name}今天是你的生日公司全体祝你生日快乐模板名称随便取但正文里的变量需要和实际发送时JSON里的参数一一对应。我一次性申请了两个模板一个是生日祝福模板一个是生日提醒模板给管理者发。前者给员工发后者用于异常监控。模板审核同样需要时间建议先申请签名签名通过后再申请模板因为模板审核文本里需要携带签名。2.3 AccessKey安全与云函数环境变量调用阿里云API需要AccessKey ID和AccessKey Secret。这一步千万不能直接用主账号的AccessKey安全风险太大。我建议创建一个RAM子用户仅授权短信服务的权限在阿里云控制台进入“访问控制RAM”创建用户选择“编程访问”系统会生成AccessKey ID和Secret给该用户只添加“短信服务SMS权限”的策略下载或记录密钥注意Secret只显示一次丢了就只能重置。AccessKey拿到之后不要写死在云函数代码里。云开发支持云函数配置环境变量在云函数所在的控制台里进入“配置”页面添加环境变量即可ALIBABA_CLOUD_ACCESS_KEY_ID LTAIxxxxx ALIBABA_CLOUD_ACCESS_KEY_SECRET xxxxx代码里通过process.env读取环境变量。这样做的原因是云函数代码会打包上传如果直接在代码里放明文密钥等于把密钥暴露给了所有能看到源码的人。环境变量相当于放在一个相对隔离的配置层安全等级高得多。3. 数据表设计与生日搜索逻辑3.1 员工信息表结构云数据库是一个文档型数据库集合相当于表文档相当于行。员工集合我命名为employee字段设计如下字段类型说明namestring员工姓名用于短信模板展示phonestring手机号短信接收号码birthdaystring出生日期格式YYYY-MM-DD用于展示和计算年龄monthDaystring月-日格式MM-DD用于快速匹配今天生日statusstringactive在职 / inactive离职只给在职员工发remarkstring备注比如“农历生日”这里有意识地冗余了一个monthDay字段而不是在查询时对birthday做字符串截取。云数据库的查询条件虽然能写正则但用正则去匹配日期字段既不高效也不直观。直接存储一个纯“月-日”字段查询条件就是精确匹配{ monthDay: 05-20, status: active }简单直接。缺点是批量导入数据时需要多算一个字段但这点计算成本完全值得。3.2 时区坑云函数默认是UTC时间这个坑几乎每个做定时任务的人都会踩一次。云函数运行环境默认用的时区是UTC而北京时间是UTC8。假设触发器定在每天早上8点执行云函数里直接调用new Date()拿到的日期在凌晨0点到8点之间还是“前一天”。举个实际例子员工生日是5月20日定时任务北京时间5月20日早上8点触发。如果代码里用new Date().toISOString().slice(5,10)去取日期得到的是UTC时间5月20日0点的结果吗不是。北京时间8点对应的UTC是0点这时候new Date()返回的还是5月20日0点刚好没出问题。但如果你的触发器定在早上7点北京7点对应UTC前一天的23点日期就变成了5月19日当天匹配就会漏发。解决方法是引入moment-timezone在所有日期计算里显式指定时区const moment require(moment-timezone); const today moment().tz(Asia/Shanghai).format(MM-DD);这样不管服务器运行在哪个时区拿到的都是北京时间。建议所有时间判断都统一走这个入口千万不要直接在代码里硬编码8小时因为夏令时等特殊情况会带来额外的坑虽然国内没有夏令时但项目如果以后部署到海外节点还是要小心。3.3 公历和农历生日的兼容处理大部分公司按公历过生日但也有一些地区习惯农历。如果数据表只存公历遇到农历生日的员工就会在错误的日子发祝福。支持农历有两种常见做法。第一种是存农历字段云函数在判断时把农历转换成当年的公历日期第二种是直接在员工信息里冗余存储一个“今年公历生日日期”定期更新。第一种比较灵活第二种做起来最简单。简单场景下我建议每个员工在文档里加一个可选字段{ lunar: true, lunarMonthDay: 四月初五 }云函数遍历当天符合条件的员工时如果发现lunar字段为true就调用农历库比如lunar-javascript把今年的农历生日转成公历再和今天比较。当然这个方案对数据处理量有要求。如果员工几百人云函数完全能扛住如果员工上万就得想别的办法了。对于小团队场景我觉得这个方案性价比最高。3.4 小程序端搜索员工标题里提到的“搜索员工生日”在小程序端体现为两个场景一是HR在小程序里按姓名或手机号查员工看到生日信息二是手动触发发送短信作为定时任务出错时的备份方案。搜索功能直接调云函数实现传一个关键词进来在employee集合里做正则匹配const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); exports.main async (event) { const { keyword } event; const reg new RegExp(keyword, i); const { data } await db.collection(employee) .where({ _id: db.command.exists(true), // 先限制范围 $or: [ { name: reg }, { phone: reg } ] }) .limit(20) .get(); return data.map(item ({ name: item.name, phone: item.phone, birthday: item.birthday, monthDay: item.monthDay, status: item.status })); };前端页面收集输入框的值调用wx.cloud.callFunction返回结果渲染到列表里。为了安全手机号在页面上要做脱敏处理比如138****8888避免敏感信息直接暴露给所有登录小程序的人。毕竟不是每个人都需要看到所有人的完整手机号。4. 核心实现定时发送短信4.1 创建云函数与安装依赖在开发者工具中右键cloudfunctions目录新建云函数命名为sendBirthdaySms。云函数创建后是本地的Node.js项目需要在目录里安装几个依赖npm install wx-server-sdk alicloud/dysmsapi20170525 alicloud/openapi-client moment-timezonewx-server-sdk是小程序云开发的官方SDK操作数据库全靠它后面两个是阿里云短信服务的新版SDK和OpenAPI客户端moment-timezone用来处理时区。安装完成后云函数目录结构大概是这样sendBirthdaySms/ ├── config.json ├── index.js ├── package.json └── node_modules/config.json后面用来配置触发器index.js写核心逻辑。4.2 发短信逻辑实现index.js的完整逻辑分三步查出生日匹配的员工、检查是否已发送过、调用短信接口并记录发送结果。const cloud require(wx-server-sdk); const moment require(moment-timezone); const Dysmsapi require(alicloud/dysmsapi20170525); const OpenApi require(alicloud/openapi-client); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; const config new OpenApi.Config({ accessKeyId: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID, accessKeySecret: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET, endpoint: dysmsapi.aliyuncs.com }); const client new Dysmsapi.default(config); exports.main async (event) { const today moment().tz(Asia/Shanghai).format(MM-DD); const todayFull moment().tz(Asia/Shanghai).format(YYYY-MM-DD); // 1. 查询今天过生日的在职员工 const res await db.collection(employee) .where({ monthDay: today, status: active }) .limit(1000) .get(); const employees res.data; if (employees.length 0) { return { sent: 0, skipped: 0, date: todayFull, message: 无今日生日员工 }; } // 2. 遍历发送短信 let sent 0; let skipped 0; const logColl db.collection(sendLog); for (const emp of employees) { const existed await logColl.where({ phone: emp.phone, date: todayFull, bizType: birthday }).count(); if (existed.total 0) { skipped; continue; } try { const req new Dysmsapi.SendSmsRequest({ phoneNumbers: emp.phone, signName: 你的签名名称, templateCode: SMS_123456789, templateParam: JSON.stringify({ name: emp.name, company: 你的公司名 }) }); const resp await client.sendSms(req); if (resp.body resp.body.Code OK) { sent; await logColl.add({ data: { phone: emp.phone, name: emp.name, date: todayFull, bizType: birthday, status: success, sendTime: db.serverDate(), requestId: resp.body.RequestId } }); } else { await logColl.add({ data: { phone: emp.phone, name: emp.name, date: todayFull, bizType: birthday, status: failed, errorCode: resp.body resp.body.Code, errorMsg: resp.body resp.body.Message, sendTime: db.serverDate() } }); } } catch (e) { console.error(发送失败, emp.phone, e); } } return { sent, skipped, total: employees.length, date: todayFull, message: 执行完成 }; };这里有一个关键设计在发送之前先查sendLog里今天是否已经给这个手机号发过。为什么这么做因为云函数定时触发器虽然理论上每天只触发一次但实际运行中可能出现手动触发、重复触发、平台重试等意外情况。万一当天执行了两次没有幂等保护就会给同事发两条祝福短信这比不发更尴尬。templateParam里的字段名必须和你在阿里云短信后台申请模板时填写的变量名完全一致。比如模板里写的变量是${name}那么JSON里就必须有name字段多一个少一个都会导致模板匹配失败。4.3 定时触发器配置定时触发器在云函数目录的config.json里配置以sendBirthdaySms为例{ permissions: { openapi: [] }, triggers: [ { name: dailyBirthdayTimer, type: timer, config: 0 0 8 * * * * } ] }这里最容易搞混的是cron表达式的位数。Linux系统的crontab通常是5位或6位分 时 日 月 周但微信云开发的定时触发器是7位秒 分 时 日 月 星期 年。0 0 8 * * * *表示每天8点0分0秒触发。配置好之后需要在开发者工具里右键云函数选择“上传触发器部署”或“云端安装依赖并上传”把config.json同步到云端。部署成功后云开发控制台里就能看到触发器列表显示下次运行时间。建议把发送时间定在早上8点到9点之间。太早会打扰员工休息太晚可能让祝福失去“当天早上收到”的时机感。8点是个不错的折中。4.4 执行时间与超时配置云函数默认超时时间是3秒但对于循环发送多条短信的场景3秒肯定不够。每条短信从云函数到阿里云接口再返回网络开销加外部调用整体差不多耗时200到500毫秒即使只发10个人也可能超过3秒。解决办法是给云函数设置更长的超时时间。在云开发控制台进入sendBirthdaySms函数的“配置”页面把超时时间改成30秒或60秒。如果员工规模很大超过100人可以在函数里做分批处理甚至把发送队列拆到多个云函数里但小团队用不上30秒足够。同时云函数的内存设置建议256MB或者512MB。设小了会导致Node.js进程内存不足设大了消耗的GBs资源更多影响免费额度。生日短信这种逻辑很简单256MB就够。5. 常见问题与排查5.1 短信发送失败Code不是OK这是最常见的问题。发送短信后阿里云会返回一个Code字段只有Code为OK才表示成功。我整理了几个高频错误和排查方向错误码含义排查方向isv.SMS_SIGNATURE_ILLEGAL签名不合法检查签名名称是否和申请表里完全一致是否已审核通过isv.SMS_TEMPLATE_ILLEGAL模板不合法检查模板Code是否填错模板是否已审核通过变量名是否匹配isv.MOBILE_NUMBER_ILLEGAL手机号不合法查看员工手机号是不是11位有没有带空格或86isv.BUSINESS_LIMIT_CONTROL触发流控发送频率过高降低发送速度或暂时停止重试isv.AMOUNT_NOT_ENOUGH余额不足购买短信套餐包或充值建议在云函数里把每次调用的返回体完整打到日志里方便出问题时定位。我第一次上线时就是因为模板里变量名写成了username而模板里是name结果返回isv.SMS_TEMPLATE_ILLEGAL排查了半天才发现是这个原因。5.2 定时任务不执行日志为空这种问题一般集中在触发器部署阶段。检查顺序如下云开发控制台里找到sendBirthdaySms函数点击“触发器”标签确认触发器列表里有记录。如果没有说明config.json没有被部署成功重新部署。触发器配置里的cron表达式必须严格7位少一位整个配置不会生效。确认云函数环境变量已经配置如果在本地测试需要临时手动调用时没有环境变量AccessKey会读到空值。手动在控制台点击“测试”按钮传一个空事件看能不能正常执行。如果手动能行但定时不触发多半是触发器没生效。5.3 漏发和多发漏发最常见的原因就是时区问题这个在前面专门讲了。另外如果你们公司有人工导入员工数据monthDay字段没有正确生成也会导致匹配不到。建议导入数据时做一个脚本从birthday里自动解析出monthDay不要手工填。多发的原因基本有两种一是同一个员工在employee表里存在重复记录姓名手机号都一样二是同一天触发器被手动触发定时触发各来了一次。第一种情况处理思路是给phone字段建唯一索引防止重复录入第二种情况就靠sendLog幂等逻辑兜底。5.4 手机号隐私合规员工的手机号和生日属于个人信息在小程序里展示时要注意脱敏云函数日志里也不要打印完整的手机号可以用前3后4的掩码。数据库权限设置成“仅云函数可读写”或“所有用户不可读写”避免前端用户直接遍历员工集合。小程序端如果需要展示员工生日列表建议通过云函数返回脱敏数据而不是直接在小程序里渲染数据库集合。否则任何能打开小程序的人都有可能在网络面板里看到原始数据。5.5 费用控制短信按条计费员工数量少的话每月费用几乎为零。我算过一笔账假设公司有200名员工平均每天过生日的概率大概是200/365约0.55人算上节假日一个月最多20条短信。按行业短信0.04元/条左右的价格一个月不到一块钱。真正的大头反而是云开发免费额度之外超出部分的几分钱基本可以忽略。但要注意如果员工表里有人离职了但状态没有更新生日短信还是会发出去。建议在员工管理页设置定时提醒每月底批量检查status字段把离职员工标记为inactive。5.6 联调测试技巧不想在测试阶段就真的发出短信可以在云函数里加一个debug开关if (event.debug) { console.log(模拟发送, emp.name, emp.phone); continue; }手动测试时传{ debug: true }函数只打印日志不调阿里云接口。这样联调数据库查询、触发器、模板参数都没问题真正上线前再把debug逻辑关掉或者保留但默认不开启。6. 一些后续可以扩展的方向这个项目做完之后我又陆续加了一些功能写出来供你参考。第一个是支持农历生日。员工填生日时多选一个“农历”选项云函数里对这类员工单独做农历到公历的转换再判断是否当天。这个功能某公司用得上特别是长辈比较多、过农历生日的场景。第二个是增加提前一天提醒。除了生日当天的祝福还可以在提前一天给HR发一条提醒短信。模板内容类似“明天是张三的生日记得关注”这样HR可以提前准备避免只靠自动短信显得敷衍。第三个是把短信模板做成可配置。目前模板Code硬编码在云函数里如果哪天你想改祝福语还得去改代码部署。更好的做法是把模板Code和签名名称放到settings集合里云函数每次执行前先从配置表拉一次。这样业务人员自己就能在管理后台更新祝福语。第四个是多通道触达。短信送达率高但成本也有一点点对于公司内部员工可以同时在小程序里做订阅消息提醒。员工如果授权了订阅生日当天小程序也会推一条通知。短信负责兜底订阅消息负责补充体验更好。最后分享一个我自己的使用习惯每天定时任务跑完之后去云开发控制台看一眼日志确认今天的发送结果。云开发日志支持按“执行时间”筛选如果发现某天日志里显示sent: 0大概率是时区问题或者员工数据没维护好。后来我在云函数里加了一个监控逻辑连续两天发送数量为0时通过另一个短信模板提醒我这样系统自己就能发现问题。整个项目下来我的体会是所谓“自动化”并不需要多复杂的架构堆一堆高深的技术名词反而把核心业务逻辑理清楚用最顺手的工具去实现才是关键。你要是也想给团队做个类似的生日关怀工具照着这套方案抄作业就行跑通了再根据自己的实际场景慢慢加功能。