Hacking Limbo

Reading / Coding / Hacking

微信扫码支付

为了应付一个奇葩的审核需求,给公司官网集成了网页版的微信扫码支付,这里记录一下大致流程。

集成方式

根据官方文档的描述,我选了看起来最方便的对接流程模式二

  1. [服务端] 调用微信支付「统一下单 API」生成预付交易,获得 code_url.
  2. [客户端] 生成 code_url 对应的二维码。
  3. [微信] 用户扫码支付后完成交易,交易结果通过 HTTP POST 请求发送到调用方指定的 callback url.
  4. [服务端] 校验交易结果,更新订单状态。

注意:该模式的预付订单有效期为 2 小时,过期后无法支付。以及每个二维码只能被扫码支付一次。

API 调用

所谓「统一下单 API」,就是 POST https://api.mch.weixin.qq.com/pay/unifiedorder, 只不过跟所有微信 API 一样,调用时要给 request payload 签名,而且只能用 XML 编码 🙄️。

计算 request payload 签名:

def calc_signature(data: dict) -> str:
    api_key = '<微信商户 API Key>'
    pairs = sorted(order.items())
    pairs.append(('key', api_key))
    encoded = urlencode(pairs, quote_via=no_quote)
    return md5(encoded.encode('utf-8')).hexdigest().upper()

将 request payload 编码为 XML:

def dump_as_xml(data) -> str:
    root = Element('xml')

    for key, value in data.items():
        elem = Element(key)
        elem.text = str(value)
        root.append(elem)

    buffer = StringIO()
    ElementTree(root).write(buffer, encoding='unicode', xml_declaration=True)
    return buffer.getvalue()

一些小问题

  • urlencode() 拼接请求参数时,要指定 quote_via 为一个 stub 函数(原样返回字符串),因为在 XML 里传递的参数并没有被 quote, 需要保持两边的值一致才能通过签名校验。

  • 微信返回的 code_url 是类似 weixin://wxpay/bizpayurl?pr=<ID> 这样的 URL, 跟官方文档示例里的有一点不一样,URL 参数名并不是 sr 而是 pr.

  • 支付完成后,在「微信商户平台」里能看到订单信息,但是公众号管理员并没有收到通知。

  • 微信 API response header 里没有指定编码, 导致 HTTP client 认为 response body 编码是 ISO-8859-1, 解析出来的 XML 会有乱码,解决方法是强行指定 resp.encoding = 'utf-8.

参考