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 微服务实战是另一条路线的参照。

延伸阅读