云托管解决了什么:把后端跑在云上
上一篇讲云开发时,我们用云函数和云数据库就搭起了一个内容类小程序的后端。但当业务走到「交易」这一步——要收单、要退款、要接第三方——云函数那种「一段函数做一件事」的模型就开始吃力了。你需要一个能跑完整 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" }); // 必须回执成功,否则微信会重推
});
三个新手最容易踩的坑:
- 必须验签:回调地址是公网可达的,谁都能往这里发请求,不验签等于开后门。
- 必须幂等:微信可能重推多次回调,同一订单重复处理要不会出错。
- 必须回执成功:没返回成功,微信会按策略反复重推。
异常处理与对账
再完善的回调也有丢失的时候:网络抖动、服务重启、你忘了回执。所以正规做法是每天主动和微信对一次账,用微信的账单数据校正自己的订单表。
| 异常场景 | 现象 | 应对 |
|---|---|---|
| 回调丢了 | 用户付了钱,订单还 pending | 每日对账,按微信账单补齐 |
| 重复回调 | 同一单被通知多次 | 订单状态判断幂等,只处理一次 |
| 金额对不上 | 账单金额与本地不一致 | 人工介入,先标记再排查 |
把对账做成每日定时任务,交易类业务才睡得着觉。它是回调机制的兜底,不是可有可无的附加项。
上线前的检查清单
把整套支付链路收敛成一份上线前必须过一遍的清单。
- 密钥、商户号全部走环境变量,代码仓库里搜不到明文。
- 下单、回调都做了验签,回调接口幂等。
- 前端成功回调不直接发货,等后端确认。
- 每日对账任务跑通,异常订单有告警。
- 退款流程也接好,别只管收不管退。
支付是交易类小程序的命门,宁可慢一周上线,也别带着未验签的回调上。