1.4 技术栈对齐:后端框架与短信地图
场景:五个产品要在一周内接通
机器和网络都就位,接下来要把五个云产品串进同一套后端:轻量应用服务器跑服务、云数据库 MySQL 存数据、短信服务 SMS 发通知、对象存储 COS 放素材、日志服务 CLS 收日志。团队只有三个人,没人专精某一个产品,所以我们的做法是先画一张接入地图,把每个产品的鉴权方式、调用入口、失败回执方式全部列清楚,再动代码。地图没用什么画图工具,就是一张共享表格,五列产品五行关注点,谁负责谁填,填完一起 review。
前后端分工也一并定下:小程序端只渲染接口给的数据,一切业务规则都在 Go 服务里实现。这条线一旦画好,后面就算前端换人重写,后端一行代码都不用动,两端的迭代节奏彻底解耦。
画地图的那个下午,我们把五个产品的文档入口全部翻了一遍,重点只看三件事:请求怎么鉴权、SDK 还是裸 HTTP、配额和限频是多少。这三个问题的答案决定了后面代码里要写多少兜底逻辑。
林姐对这张地图毫无兴趣,她只关心一件事情:发短信要不要钱。我们回答完每条几毛钱之后,她立刻提了两条要求:验证码少发、提醒别漏发。于是触达策略的所有设计都围绕这个朴素诉求展开,最后算下来每个月短信费不到一杯咖啡钱。
方案:一张接入地图管五个产品
最终地图定稿如下,每个产品后面的数字是我们实际对接的耗时,供后来人参考:
| 产品 | 鉴权方式 | 我们的选择 | 对接耗时 | 主要坑点 |
|---|---|---|---|---|
| 轻量应用服务器 | SSH 密钥 | 自部署 Go | 半天 | 端口与防火墙 |
| 云数据库 MySQL | 账号密码加白名单 | database/sql 直连 | 1 天 | 时区与字符集 |
| 短信服务 SMS | SDKAppID 加密签名 | 裸 HTTP 调 API | 2 天 | 模板签名审核 |
| 对象存储 COS | SecretId 加 SecretKey | Go SDK | 1 天 | 权限与防盗链 |
| 日志服务 CLS | SecretId 加 SecretKey | 日志采集 Agent | 半天 | 主题与索引 |
短信 SMS 是唯一花了两天的产品,一天写代码一天等签名审核,详细过程见 模板与签名:审核实操。其余四个都在一天内搞定,秘诀是不要执着于高级特性,先把最短路径跑通。
各产品的默认限频和配额也必须提前摸清,不然高峰期被限流了还以为是代码的 bug:
| 产品 | 默认限制 | 我们的应对 |
|---|---|---|
| SMS | 单号码每分钟一条 | 触达策略合并同号码 |
| COS | 每秒请求数万级 | 前端直传减少中转 |
| CLS | 写入按主题限流 | 攒批 200 条刷一次 |
| MySQL | 连接数 200 到 800 | 连接池封顶 50 |
这些数字不查不知道,比如 SMS 对同一号码一分钟内只能发一条,我们的预约成功提醒和验证码要是撞在同一分钟,必然有一条发不出去。知道规则之后才好设计触达节奏,这块在 触达策略 里展开。
地图上还有一条隐线:轻量、MySQL、COS、CLS 全部买在同一地域,互相之间走内网,延迟低又不花公网流量。只有 SMS 是全局产品不受地域限制。这条隐线没写进任何文档,但它是整个架构里最省钱的设计之一。新项目每次开工,同地域采购已经成了我们的默认动作,比事后迁移桶和库省心太多。
后端框架最后定了 Go 加 Gin,中间件链路如下:
r := gin.Default()
r.Use(mwTimeout(3*time.Second)) // 全局超时
r.Use(mwRateLimit(50)) // 每秒 50 次限流
r.Use(mwReqLog()) // 请求日志推 CLS
r.Use(mwRecover()) // panic 兜底
四个中间件的顺序折腾了两回:超时必须放最外层,否则限流器里的等待时间会先耗光超时额度;日志放限流之后,这样打点能区分被拒请求和正常请求。这种顺序细节不写在文档里,全是疼过之后才记住的。
接口纪律我们也定了几条:所有返回包外面套一层固定结构,code 为 0 表示成功,msg 只给提示不给调试信息;版本前缀一律 /api/v1,省得以后升级时无处安放。小团队没有 API 网关,纪律就是网关。接口入口统一收口之后,后面接 CLS 日志和限流统计都只改一处代码,改动半径控制在一个文件里。
配置/代码:一个客户端管理全部外部依赖
所有外部产品的客户端在进程启动时初始化一次,存进一个全局结构体,避免每次请求都新建连接:
type Clients struct {
DB *sql.DB
SMS *SmsClient
COS *cos.Client
}
var C Clients
短信调用的最短路径,发送一条预约成功通知只要一个函数:
func (s *SmsClient) SendTpl(mobile string, tplID string, params []string) error {
body := map[string]interface{}{
"PhoneNumberSet": []string{mobile},
"SmsSdkAppId": s.appID,
"TemplateId": tplID,
"TemplateParamSet": params,
}
return s.post(host+"/sms/v3/sendisms", s.sign(body), body)
}
签名算法那一坨是整个接入里唯一有技术含量的部分,照着文档抄一遍即可,不要试图自己优化,验签失败给的信息极少。对象存储 COS 和日志服务 CLS 的 SDK 用官方 Go 库,省掉了自己造轮子的风险。
短信发送之前还要确认一下签名内容能不能过审:签名内容就是短信结尾那行小字,比如我们用的是林姐美甲。body 里的参数顺序必须和模板里的占位顺序一一对应,差一个位置发出去的内容就是错的。我们在自测环境里做了一张对照表,把每个模板 ID、参数名和示例值钉在一起:
| 模板用途 | 模板 ID | 参数示例 | 触发时机 |
|---|---|---|---|
| 预约成功 | 1234567 | 店名,时段 | 支付完成时 |
| 到店提醒 | 2234567 | 店名,时段 | 服务前两小时 |
| 排队超时 | 3234567 | 店名,电话 | 释放锁位时 |
| 验证码 | 4234567 | 四位数字 | 登录注册时 |
(模板 ID 为脱敏示例。)
踩坑:五件与签名有关的事
第一个坑是时区。MySQL 默认时区是 UTC,而门店的档期全是北京时间,第一周所有预约时间差了八小时,客人下午约的时段被存进了上午,前台看着排班表怀疑人生。修完之后我们在连接串里写死 loc=Local,并在所有跨进程传递的时间上一律用 Unix 时间戳,彻底杜绝时区翻译问题。
第二个坑是字符集。建库时没指定字符集,默认的 latin1 存美甲项目名里的生僻字直接乱码,客人看到菜单上出现问号还以为系统坏了。统一改成 utf8mb4,顺手把排序规则设为 utf8mb4_unicode_ci,问题消失。新库做好这件事,比老库整改容易一百倍。
第三个坑是密钥管理。SecretId 和 SecretKey 一开始写在代码里方便调试,后来一次提交差点推到公开仓库,惊出一身冷汗。从此密钥只待在 env 文件里,代码里一律从环境变量读。
第四个坑是 SDK 版本漂移。SMS 的 API 某次升级后我们的签名算法失效,排查半天才发现是指纹用的方式变了。因此建议裸调 HTTP 反而更可控,接口版本完全握在自己手里。另一个好处是依赖只剩标准库,编译出来的二进制丢到任何机器上都能跑。
第五个坑是 COS 的跨地域访问。门店素材最早存在另一个地域的桶里,每张作品图加载都要绕远路,店里的 iPad 相册轮播卡出幻灯片效果。把桶迁到同地域并开启 CDN 加速后,图片秒开。
这五个坑合起来花掉了三天,但每个坑都只付了一次学费。后来项目加 Stage 新功能时,地图上的经验直接复用,接入新组件再没超过半天。
小结
技术栈对齐本质上是把五个产品的接入路径化成一张地图,谁用 SDK、谁用裸 HTTP、超时和限流放哪一层,都想清楚再写代码。我们一周接完五个产品,中间没返工,靠的就是这张地图和最短路径原则。地图画完贴在团队 wiki,后来有新人加入,照地图半天就能跑通全链路,这比任何口口相传都高效。下一章正式进入最核心的部分:档期与占位的数据模型设计,那里才是预约系统真正的深水区。
延伸阅读
- 短信接入的完整过程,见 短信接入:腾讯云 SMS
- 日志与告警的落地,翻 监控告警:预约失败率
- 另一套腾讯云全栈实践,见 腾讯云实战合集
- 服务器初始化相关,见 机器准备:轻量应用服务器怎么选
