云托管解决了什么:把后端跑在云上

上一篇讲云开发时,我们用云函数和云数据库就搭起了一个内容类小程序的后端。但当业务走到「交易」这一步——要收单、要退款、要接第三方——云函数那种「一段函数做一件事」的模型就开始吃力了。你需要一个能跑完整 Web 服务、能自己控制路由和中间件、能直接连数据库的后端环境,这就是云托管登场的地方。

云托管本质上是把你写好的容器化服务直接跑在微信的基础设施上。你不再买服务器、配域名、扛证书,只需要把一份代码和一个 Dockerfile 交上去,平台帮你拉起实例、分配流量、按需扩缩容。它和云函数的差别在于:云函数是你按函数粒度被调度,云托管是你拥有一个完整的常驻服务。

云函数适合轻量逻辑,云托管适合完整后端。做交易,选云托管。
本文核心知识点
  • 云托管部署容器化服务,得到一个可对外访问的服务地址。
  • 统一下单由后端调用微信支付接口,换取一个前端可用的支付参数。
  • 支付回调微信在用户付完钱后异步通知你的服务,是更新订单的唯一可信时机。
  • 对账主动拉微信账单与自己订单核对,兜住回调丢失。
小程序云托管与支付示意:服务器容器、支付卡片与数据流箭头组成的蓝调扁平科技插画
云托管把后端变成一个常驻服务,支付链路围绕「下单—回调—对账」三步展开。

开通云托管与部署第一个服务

在云开发控制台里新建一个云托管服务,选好运行环境,把你的后端代码连同 Dockerfile 一起上传。以一个最简单的 Node 服务为例,它对外暴露一个健康检查接口:

// server/index.js
const express = require("express");
const app = express();

app.get("/health", (req, res) => {
  res.json({ ok: true });
});

const port = process.env.PORT || 80;
app.listen(port, () => console.log("service on", port));

部署完成后,平台会给服务分配一个默认访问路径。你的小程序前端通过 wx.request 请求这个路径,就像请求任何普通后端接口一样。几个要点先记住:

事项说明
监听端口必须监听平台注入的 PORT,通常是 80
对外路径平台分配,小程序里直接请求该路径
环境变量密钥、数据库连接串都放环境变量,不写进代码

把这个服务跑通之后,再接支付就只是往这个服务里加几个路由的事。

微信支付的整体流程

微信支付看上去接口很多,但主线其实很清晰。用户在小程序里点付款,背后依次发生这几步:前端把订单信息发给你的后端、后端调用微信的统一下单接口拿到一个支付凭证、前端用这个凭证唤起微信收银台、用户输入密码付款、微信异步通知你的服务结果。

// 时序:前端 → 你的云托管 → 微信支付平台
// 1. 前端请求你的后端下单
// 2. 后端调微信统一下单,返回支付参数
// 3. 前端用支付参数调起收银台
// 4. 用户付款,微信回调你的后端
// 5. 后端更新订单,前端轮询或跳转结果页

这条链上有一个关键认知:统一下单必须由后端做,绝不能放在前端。因为下单要用到商户密钥,一旦暴露在小程序代码里,任何人都能伪造你的支付请求。

  • 订单号在后端生成,前端不参与。
  • 金额以服务端为准,前端传的金额只能当提示,不能信。
  • 商户号、AppID、密钥三者要在微信商户平台先绑定好。

统一下单与拉起支付

后端在收到前端的下单请求后,要做三件事:生成订单、调用微信统一下单、把拿到的支付参数原样返回给前端。统一下单的关键是用商户密钥对请求做签名,签名错一个字符,接口就会直接拒绝。

// 后端:调用统一下单(示意,真实字段以官方文档为准)
async function createOrder(userId, amount) {
  const orderNo = genOrderNo();
  const params = {
    out_trade_no: orderNo,
    total: amount,
    openid: userId,
    description: "会员充值"
  };
  const payParams = await wxpay.unifiedorder(params);
  await db.order.add({ orderNo, amount, status: "pending" });
  return payParams; // 含 timeStamp / nonceStr / paySign
}

前端拿到支付参数后,调起收银台:

wx.requestPayment({
  timeStamp: res.timeStamp,
  nonceStr: res.nonceStr,
  package: res.package,
  signType: res.signType,
  paySign: res.paySign
}).then(() => {
  wx.showToast({ title: "支付成功" });
}).catch(() => {
  wx.showToast({ title: "已取消", icon: "none" });
});

注意:前端的成功回调不能当作真正的支付成功,它只表示收银台流程走完了。真正的到账要靠下一步的服务端回调确认。

支付回调与订单状态

用户付款成功后,微信会向你在商户平台配置的回调地址发一个 POST 请求。这个请求是更新订单状态的唯一可信来源,前端那个成功 toast 只是个友好提示。

// 后端:处理支付回调
app.post("/pay/callback", async (req, res) => {
  const notify = wxpay.verifyNotify(req.body); // 验签
  if (!notify.paid) {
    return res.json({ code: "fail" });
  }
  await db.order.update(
    { orderNo: notify.orderNo },
    { status: "paid", paidAt: Date.now() }
  );
  res.json({ code: "success" }); // 必须回执成功,否则微信会重推
});

三个新手最容易踩的坑:

  • 必须验签:回调地址是公网可达的,谁都能往这里发请求,不验签等于开后门。
  • 必须幂等:微信可能重推多次回调,同一订单重复处理要不会出错。
  • 必须回执成功:没返回成功,微信会按策略反复重推。