Chapter 08

用户能力接入

完整掌握微信登录鉴权流程、微信支付集成与订阅消息推送

8.1 微信登录体系

微信小程序的登录体系基于 OpenID(用户在该小程序的唯一标识)和 UnionID(同一主体下所有微信产品的统一标识)。整个登录流程由前端、后端和微信服务器三方协同完成。

code
wx.login 返回的临时凭证,有效期 5 分钟,只能使用一次。不能直接在前端使用,必须发送到后端
openid
用户在某个小程序的唯一标识。同一用户在不同小程序的 openid 不同
unionid
同一微信开放平台账号下的多个应用共享同一 unionid,用于跨应用识别用户身份
session_key
会话密钥,服务端保存,用于解密用户敏感信息(手机号等)。绝对不能传给前端

完整登录流程

// ─── 前端(小程序)───

// utils/auth.js
const http = require('./request')

async function login() {
  // Step 1:调用 wx.login 获取 code
  const { code } = await wx.login()
  console.log('临时 code:', code)

  // Step 2:将 code 发送给自己的后端,换取 token
  const res = await http.post('/auth/wx-login', { code })
  // res 包含 { token, userInfo }

  // Step 3:存储 token
  wx.setStorageSync('token', res.token)
  const app = getApp()
  app.globalData.token = res.token
  app.globalData.isLoggedIn = true
  app.globalData.userInfo = res.userInfo

  return res
}
// ─── 后端(Node.js 示例)───

// POST /auth/wx-login
async function wxLogin(req, res) {
  const { code } = req.body

  // Step A:用 code 换取 openid + session_key
  const wxRes = await fetch(
    `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
  ).then(r => r.json())

  const { openid, session_key, unionid } = wxRes

  // Step B:查找或创建用户
  let user = await User.findOne({ openid })
  if (!user) {
    user = await User.create({ openid, unionid, createdAt: new Date() })
  }

  // Step C:生成 JWT,session_key 存 Redis(不传前端!)
  await redis.setEx(`sk:${openid}`, 7200, session_key)
  const token = jwt.sign({ openid, userId: user._id }, JWT_SECRET, { expiresIn: '7d' })

  res.json({ token, userInfo: user })
}

获取手机号(敏感能力)

<!-- WXML:使用 button 的 open-type="getPhoneNumber" -->
<button
  open-type="getPhoneNumber"
  bindgetphonenumber="onGetPhone"
  class="btn-primary"
>
  手机号一键绑定
</button>
// 获取手机号回调
async onGetPhone(e) {
  if (e.detail.errMsg !== 'getPhoneNumber:ok') {
    return  // 用户拒绝
  }
  // code 发给后端,后端用 session_key 解密手机号
  const { code } = e.detail
  await http.post('/user/bind-phone', { code })
  wx.showToast({ title: '绑定成功' })
}
🔴
安全红线:session_key 不能传给前端

session_key 是解密用户手机号、位置等敏感信息的密钥,绝对不能通过接口返回给小程序前端。只能在服务器端使用,并设置合理的过期时间(微信也会定期更新 session_key)。一旦泄露,用户隐私数据将暴露。

8.2 微信支付

小程序内支付使用 JSAPI 模式,流程涉及小程序前端、自有后端和微信支付服务器三方。

支付流程

  1. 用户点击支付,前端调用后端创建订单接口,传入商品信息
  2. 后端调用微信支付「统一下单 API」(v3 版本:POST /v3/pay/transactions/jsapi),获取 prepay_id
  3. 后端对支付参数进行签名,将签名后的参数返回给前端(timeStamp、nonceStr、package、signType、paySign)
  4. 前端调用 wx.requestPayment 拉起收银台
  5. 用户完成支付后,微信服务器异步通知后端(notify_url)
  6. 后端核实订单金额,更新订单状态
// 前端支付代码
async onPay() {
  wx.showLoading({ title: '创建订单...' })
  try {
    // 1. 创建订单,获取支付参数
    const payParams = await http.post('/orders', {
      productId: this.data.productId,
      quantity: this.data.quantity
    })
    // payParams: { timeStamp, nonceStr, package, signType, paySign }

    wx.hideLoading()

    // 2. 调起收银台
    await wx.requestPayment(payParams)

    // 3. 支付成功(注意:这里只代表 wx.requestPayment 成功,不代表后端已核实)
    wx.showToast({ title: '支付成功' })
    wx.navigateTo({ url: `/pages/order-result/order-result?orderId=${payParams.orderId}` })

  } catch (e) {
    wx.hideLoading()
    if (e.errMsg === 'requestPayment:fail cancel') {
      wx.showToast({ title: '已取消支付', icon: 'none' })
    } else {
      wx.showToast({ title: '支付失败,请重试', icon: 'error' })
    }
  }
}

8.3 订阅消息

订阅消息允许小程序在用户授权后,在特定场景(如订单发货、预约提醒)向用户发送服务通知。2019 年取代了旧版模板消息。

订阅流程

  1. 在微信公众平台「功能 → 订阅消息」中选择或申请消息模板,获取模板 ID
  2. 在用户发生相关操作时(如下单),调用 wx.requestSubscribeMessage 请求授权
  3. 用户同意后,在服务端触发事件时调用微信「发送订阅消息」API 推送通知
// 在下单成功后请求订阅"发货提醒"
async requestSubscribe() {
  try {
    const res = await wx.requestSubscribeMessage({
      tmplIds: [
        '模板ID_发货提醒',
        '模板ID_签收确认'
      ]
    })
    // res: { '模板ID_发货提醒': 'accept' | 'reject' | 'ban' }

    const accepted = Object.entries(res)
      .filter(([_, status]) => status === 'accept')
      .map(([tmplId]) => tmplId)

    // 将用户同意的模板 ID 发给后端保存
    if (accepted.length > 0) {
      await http.post('/user/subscribe', { tmplIds: accepted })
    }
  } catch (e) {
    // 用户未授权或已禁用,静默处理(不强制要求)
    console.log('订阅取消', e)
  }
}
// 后端发送订阅消息(Node.js)
async function sendShipNotice(openid, orderInfo) {
  // 获取 access_token(有效期 2 小时,建议缓存)
  const accessToken = await getAccessToken()

  await fetch(
    `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        touser: openid,
        template_id: '模板ID_发货提醒',
        page: `/pages/order-detail/index?id=${orderInfo.orderId}`,
        data: {
          thing1: { value: orderInfo.productName },     // 商品名称
          character_string2: { value: orderInfo.orderId }, // 订单号
          thing3: { value: '顺丰快递' },              // 物流公司
          character_string4: { value: orderInfo.trackingNo } // 快递单号
        }
      })
    }
  )
}
💡
订阅消息的使用规范

每次用户授权只能发一条订阅消息(一次性授权)。若用户在手机端设置了「长期订阅」,则可以多次发送。不要在与发送场景无关的时机请求订阅,否则用户体验差且容易被用户永久拒绝。请求订阅消息必须在用户主动操作后触发(不能在页面 onLoad 中调用)。