做小程序有段时间了,回头看不难,但第一次接这三个功能的时候,每个都踩过坑。微信的文档写得挺全,就是太散了,东一块西一块,真到自己串起来用,总会卡在文档里一句话带过的地方。
这篇把登录、微信支付、订阅消息这三块最常见的功能串一遍,代码都是能直接跑的那种,顺手把坑标出来。
小程序登录的核心就一步:前端拿 code,后端换 openid。整个流程长这样:
前端代码:
// app.js 或者登录页
wx.login({
success: async (res) => {
// 注意:code 只有 5 分钟有效期,拿到马上发后端,别存着以后用
const result = await wx.request({
url: 'https://你的域名/api/wx/login',
method: 'POST',
data: { code: res.code }
})
// 后端返回自家签发的 token,后续请求都带它
wx.setStorageSync('token', result.data.token)
}
})
后端(Node 版):
// 用 code 换 openid,这一步必须在服务端做
// appsecret 放前端等于把家门钥匙贴门口
async function wxLogin(code) {
const url = 'https://api.weixin.qq.com/sns/jscode2session' +
`?appid=${APPID}&secret=${SECRET}&js_code=${code}` +
'&grant_type=authorization_code'
const res = await fetch(url).then(r => r.json())
// res: { openid, session_key, unionid? }
if (!res.openid) throw new Error('微信登录失败: ' + JSON.stringify(res))
// 这里查库/建用户,签发自家 token 返回给前端
return createSession(res.openid)
}
几个坑提前说:
wx.getUserInfo 早就废了(2021 年起),现在拿头像昵称得用 <button open-type="chooseAvatar"> 让用户主动选,别再去翻老教程了。<button open-type="getPhoneNumber">,用户点按钮授权后,后端拿 code 去换手机号,2023 年后还开始按次收费,一毛钱一次,预算里别忘了。支付这块前端反而是最简单的,真正的工作量全在后端。前端拿到的五个参数全是后端算好的:
wx.requestPayment({
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.package, // 形如 prepay_id=wx201410272009395522657a690399285100
signType: 'RSA', // 现在推荐 RSA,别再用 MD5 了
paySign: data.paySign,
success: () => wx.showToast({ title: '支付成功' }),
fail: (err) => {
// errMsg 是 'requestPayment:fail cancel' 就是用户自己取消的
// 不算错误,别弹报错吓用户
if (err.errMsg.includes('cancel')) return
wx.showToast({ title: '支付失败', icon: 'none' })
}
})
后端的核心流程,用伪代码捋一遍:
async function createOrder(userId, goodsId) {
// 1. 自己库里先落一单,状态 = 待支付
const order = await db.orders.insert({ userId, goodsId, status: 'UNPAID' })
// 2. 调微信 V3 统一下单(JSAPI 下单)
const prepay = await wxpay.transactions_jsapi({
appid: APPID,
mchid: MCH_ID,
description: '商品名称',
out_trade_no: order.id, // 自家订单号,回调靠它对上
notify_url: 'https://你的域名/api/wx/pay/notify',
amount: { total: 1, currency: 'CNY' }, // 单位:分
payer: { openid: user.openid }
})
// 3. 用商户私钥把五个参数签名,返回给前端
return buildPayParams(prepay.prepay_id)
}
支付这块的坑比登录多得多:
老的模板消息已经没了,现在是订阅消息。逻辑变了:用户每点一次授权,你才有发一条消息的额度。
前端引导订阅:
// 必须由用户点击触发,不能进页面就弹
wx.requestSubscribeMessage({
tmplIds: ['模板ID1', '模板ID2'], // 最多 3 个,提前在公众平台申请
success: (res) => {
// res 里是每个模板的订阅结果:accept / reject / ban
// 用户拒了也别追着弹,体验分和审核都过不了
if (res['模板ID1'] === 'accept') {
// 告诉后端:这个用户欠他一条通知
}
}
})
后端发送:
async function sendSubscribeMsg(openid) {
const token = await getAccessToken() // 记得缓存,7200 秒有效期
await fetch(
`https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}`,
{
method: 'POST',
body: JSON.stringify({
touser: openid,
template_id: '模板ID1',
page: 'pages/order/detail?id=123', // 点了消息跳哪
data: {
thing1: { value: '您的订单已发货' },
time2: { value: '2026-07-30 14:00' }
}
// 注意:data 里每个字段的类型和长度都受模板限制
// thing 类型最长 20 个字符,超了直接报错
})
}
)
}
最大的认知转变:订阅是一次性的。用户今天点了"允许",你只获得了一条消息的发送资格。想长期触达,就得在关键操作节点(下单、预约、报名)都埋订阅引导,让用户每次顺手点一下。这不是 bug,是微信故意这么设计的,防止你拿用户当营销靶子。
功能跑通只是第一步,送审之前过一遍这个清单,能省掉 80% 的驳回:
app.json 里配了 "__usePrivacyCheck__": true,首次进入弹了隐私协议security.msgSecCheck 内容安全检测三个功能串起来就是小程序的基本骨架了:登录解决"你是谁",支付解决"钱怎么收",订阅消息解决"怎么再联系到你"。剩下的都是业务细节,慢慢来。