1.4 技术栈对齐:云函数与云数据库地图
场景
环境开通后的第一个晚上,我坐在店里把云开发的能力盘了一遍。我本人是前端出身,后端经验只限于给老系统改过几个 PHP 接口,对“云函数到底能干什么、不能干什么”心里完全没底。朋友在旁边问我:所以咱们的后端服务器在哪?我指着开发者工具里的云开发面板说,在这,看不见,但是都在。
与其边写边踩坑,不如先花一晚上把三件套(云函数、云数据库、云存储)的能力边界摸清楚:哪个活该前端直接干,哪个活必须走云函数,哪个数据放哪个集合。这晚我做的事很具体:把官方文档的限制页逐条抄到笔记上,按商城场景分类,再给每条配一个“我们的对策”和最坏情况的兜底方案。让朋友也扫了一遍这张表,她唯一的反应是“超时 3 秒是不是说明顾客付钱会失败”,我说会,所以咱们要把它调大并且提前预热,她立刻觉得这事很重要,并且记住了“超时”这个词,后来每次大促前都会问我超时调好了没。这张地图定下来之后,后面二十多篇的代码都照着它落位,几乎没在架构上返过工。这一节就是那张地图。
朋友看我画了一晚上图,问我这地图有啥用。我说就好比开店前先搞清楚哪是前台、哪是仓库、哪是收银台,不然之后每件货物进来你都要临时决定放哪,迟早乱套。
方案
云开发的三件套分工,我用一句话原则概括:前端直接读公开数据、私有数据和交易逻辑全部收进云函数、大文件全部进云存储再走 CDN。
| 能力 | 前端可直连 | 必须走云函数 | 限制要点 |
|---|---|---|---|
| 云数据库 | 公开集合只读 | 私有数据、跨表写入 | 单次查询最多 20 条 |
| 云存储 | 上传、下载 | 批量处理、权限校验 | 单文件 50MB 以内 |
| 云函数 | 调用入口 | 全部业务逻辑 | 默认超时 3 秒 |
| 微信支付 | 拉起收银台 | 下单、回调验签 | 回调需云函数域名 |
| 订阅消息 | 拉起授权 | 下发模板 | 模板需审核 |
商品列表这类公开数据,前端可以直接查 goods 集合,省一次云函数调用也省一次计费;但下单扣库存这种涉及私有数据和事务的操作,绝不能让前端直连数据库——前端代码是可以被篡改的,库存扣减放在客户端里等于把收银台钥匙挂门外,懂点技术的用户把 stock 改成负数也就是几分钟的事。
判断某个逻辑放哪,我固定问三个问题:这数据是公开的吗?这个操作涉及钱或者权益吗?这段代码被恶意调用最多损失什么?公开的数据让前端直读,涉及钱和权益的一律进云函数,云函数里再用输入校验和频率限制兜底。这套问法后来连前端同事都学会了,提需求时会主动说这个接口要走云函数,沟通成本降了一大截。第三个问题尤其有用,它把“写在云函数里”从一个技术偏好变成了一个风险账,老板也听得懂。
云函数本身也有边界,开工前我把官方限制摘成一张表贴在墙上:
| 限制项 | 数值 | 我们的对策 |
|---|---|---|
| 执行超时 | 最大 60 秒 | 下单链路设 10 秒 |
| 内存上限 | 默认 256MB | 大任务分批跑 |
| 并发实例 | 自动扩缩 | 大促前预热 |
| 单次返回 | 数据库 20 条 | 翻页用 skip 加 limit |
| 冷启动 | 闲置后回收 | 保留最小实例数 |
这张表后来被我从墙上搬到新人文档里,新人入职第一件事就是读它,能少走至少一周弯路。
配置/代码
云函数的目录结构我定成“一个功能一个文件夹”,入口统一叫 index.js,依赖 wx-server-sdk 只在入口加载一次:
// 云函数 createOrder/index.js 的骨架
const cloud = require("wx-server-sdk");
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
exports.main = async (event, context) => {
const { openid } = cloud.getWXContext(); // 拿到调用者身份
const db = cloud.database();
const _ = db.command;
// 原子扣减:where + stock 大于购买量 才更新
const res = await db.collection("skus")
.where({ _id: event.skuId, stock: _.gt(event.qty - 1) })
.update({ data: { stock: _.inc(-event.qty) } });
if (res.stats.updated === 0) {
return { code: "SOLD_OUT", msg: "库存不足" };
}
return { code: "OK" };
};
这段代码是全书交易链路的雏形,后面库存扣减那节会把它扩展成带快照和事务的完整版。注意 DYNAMIC_CURRENT_ENV 这个写法,它让云函数自动认准自己所在的环境,不用每个环境改一次配置,多环境部署时少很多麻烦。
云存储这边也先对齐了用法:前端上传后拿到临时链接 tempFileURL,正式访问需要在云函数或控制台换永久链接,再挂 CDN 域名加速。上传的调用也很短:
// 前端上传商品图并换永久链接
const { fileID } = await wx.cloud.uploadFile({
cloudPath: "goods/spu_2001/cover.jpg",
filePath: tempFilePath
});
上传时 cloudPath 的目录规范在这一步就定了,所有图按 goods/{spuId}/ 归档,详情页、封面、素材各占子目录。图片相关细节后面图床方案那节细说。整个项目的云函数目录最终长这样:
cloudfunctions/
getGoods/ // 商品列表
getDetail/ // 商品详情
createOrder/ // 下单
payCallback/ // 支付回调
notifyUser/ // 订阅消息下发
每个文件夹独立部署、独立日志,出问题能立刻定位到是哪个接口在叫,这一点在流量高峰时尤其值钱。
踩坑
第一个坑是云函数单次查询的 20 条上限。前端直接查数据库时 limit 默认 20,最多能放宽到 100,但我一开始没注意,列表翻页到第三页发现数据对不上,抓着 bug 找了半小时才反应过来是上限在作怪。后来列表场景统一走云函数,用 skip 加 limit 分页,并在集合上加了游标字段做深分页优化,翻到二十页以后也不会越来越慢。
第二个坑是时区。云函数里拿到的时间是 UTC,直接用 new Date() 存订单号里的日期段,凌晨八点前下的一单会算到前一天,和店里的手工账对不上,财务对账那天差点以为系统漏了一笔。所有时间统一转东八区再落库,订单统计才对得上财务口径,这个习惯后来贯穿了全书所有涉及时间的代码。
第三个坑是 wx-server-sdk 版本不匹配。本地能跑的函数部署到云端报 undefined,原因是云端依赖版本和本地不一致,本地从同事电脑拷来的 node_modules 坑了两回。解决办法:始终在云端安装依赖(部署时勾选),本地 package.json 只做记录,不同步 node_modules。
第四个坑是集合粒度。最早我把商品规格塞在 goods 文档的一个数组里,更新某个 SKU 价格要把整个数组读出改完再写回,并发改价时互相覆盖过两次,大促前一晚还在修数据,差点没法按时睡觉。拆出独立 skus 集合后,单条更新天然原子,问题消失。设计集合就是给未来的并发埋路——今天图省事塞数组,明天并发就教做人。
第五个坑是云函数日志全靠随手 print。初期没规范,排查线上问题要在日志海里捞针,有次找个回调失败的原因翻了两小时,当时如果有订单号前缀五分钟就够。后来约定每条日志带订单号或 openid 前缀,配合监控告警那节的日志告警,定位速度快了一个量级。
小结
技术栈对齐的本质是先画能力地图再写代码:公开数据前端直连省调用,私有数据和交易逻辑全收云函数,二进制文件全上云存储。云函数的 20 条、3 秒、UTC 这三个限制提前知道,后面就不必交学费。地图清晰之后,开发节奏明显变快——每个新功能来了,先在地图上归位,再落代码,遇到分歧也有据可依,不再凭感觉拍脑袋。 先花一晚对齐地图,胜过边写边返工。上一篇正式进入商品建模,把 SPU 和 SKU 拆明白。想系统补云开发底子的,可以翻云开发 Python 实战;对微服务后端分工感兴趣的,Go 微服务实战是另一条路线的参照。
延伸阅读
- 商品模型:SPU 与 SKU 集合设计:下一篇,把地图落到集合上
- 云开发 Python 实战:云函数工程化进阶
- 回到本书目录:全书五篇结构一览
