对接微信支付(基于 AWS 云函数)

对接微信支付(基于 AWS 云函数)

xiaolu
2022-02-18 / 0 评论 / 44 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2022年02月19日,已超过171天没有更新,若内容或图片失效,请留言反馈。

微信支付 JSON 接口文档

web 网站接入

具体需求是在某在线学习平台出售课程,选择微信支付平台来对接业务。整个后端基于 AWS,使用了 APIGateWay,Lambda 云函数,S3 存储,DynamoDB 数据库等云服务,可以说相当精彩了。关键是借助这个项目,不花钱体验了这么多云服务,哈哈。

获取 lambda 函数的动态出口 IP

因为微信支付业务的接口中有一个字段是请求方的 IP,如果整个后端部署在某个具体服务器上还好说,因为 IP 是固定的,但是 AWS 的 Lambda 服务是个集群,出口 IP 还不固定。机智的我!想到了自己请求自己的办法 😅,那这事不就好办多了嘛!

新建 lambda 函数,命名为 getLambdaExportIP,具体代码如下:

exports.handler = (event, context, callback) => {
  console.log(JSON.stringify(event));
  const response = {
      statusCode: 200,
      body: event.exportIP
  };
  callback(null, response);
};

打开 API Gateway, 选择已创建的 API,新建资源 lambda,同时创建类型为 GET 的方法,如图所示:

cid_145_1.png

cid_145_2.png

在集成请求处,选择 映射模板 ,请求正文传递选择 未定义模板时 ,新建模板并输入 application/json,在模板正文中填入以下信息:

{
  "exportIP" : "$input.params('x-forwarded-for')"
}

结果如下图所示:

cid_145_3.png

调用微信统一下单接口的准备

统一下单 接口参数(只考虑必要的参数),详细参数看 官方文档

变量名必填类型示例值
appidString(32)wxd678efh567hg6787
mch_idString(32)1230000109
device_infoString(32)013467007045764
nonce_strString(32)5K8264ILTKCH16CQ2502SI8ZNMTM67VS
signString(32)C380BEC2BFD727A4B6845133519F3AD6
sign_typeString(32)MD5
bodyString(128)腾讯充值中心-QQ会员充值
detailString(6000)
out_trade_noString(32)20150806125346
total_feeInt88
spbill_create_ipString(64)123.12.12.123
notify_urlString(256)http://www.weixin.qq.com/wxpay/pay.php
trade_typeString(16)JSAPI
product_idString(32)12235413214070356458058

参数说明

对于一些固定的参数就不再赘述,如 appid,mch_id

nonce_str,是类型为 string 的32位以下的随机字符串,关于其生成可以自己封装也可以采用别人开发好的包, 这里采用 string-random , 使用 npm 安装,下面的包也是用同样的方法。

out_trade_no ,订单 id, 可以采用 uuid.v4().substring(0, 32) 来生成,由于官方要求位数小于 32,uuid 生成的字符串大于32位,可以采用 substring()方法进行截取,同样 uuid 也需要安装。

spbill_create_ip ,就是一开始费尽波折得到的 lambda 函数的出口 IP,微信支付后台在得到我们的请求后会校验请求标头 x-forwarded-for 与 这里的 spbill_create_ip,防止请求被劫持修改。

notify_url ,用户支付成功后,微信支付后台会异步推送此结果到这个 url,后面会介绍具体的步骤。

trade_type , web 开发就是 NATIVE,小程序或其他就是 JSAPI文档详情

product_id ,这个是产品 id,此参数需要你的数据库拥有产品表,此参数一般由前端传过来。

sign ,签名验证,需要根据微信的要求进行 MD5 运算。我已经实现好了, 代码可以复制即用, 如果想自己实现, 请参考 官方说明

// 需要安装 md5
let MD5 = require("md5");
/**
 * 签名算法
 */
function sign(params, key) {
  let result = Object.keys(params).sort();
  let len = result.length;
  let stringA = result.reduce((temp, item, index) => {
    if (!params[item]) { 
      return temp;
    }
    return temp += `${item}=${params[item]}${index === len ? '' : '&'}`;
  }, "");
  return MD5(`${stringA}key=${key}`).toUpperCase();
}

notify_url 实现

🍉 打开 API Gateway,在之前创建的资源 lambda 下再次新建资源,名为 tenpay ,同时新建 POST 方法,绑定函数 acceptTencentPayResult (函数提前建一个即可), 函数的内容如下:

exports.handler = (event, context, callback) => {
   console.log(event);
   let backWords = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
   callback(null, backWords);
};

如果返回的内容不符合格式或者微信后台接收不到返回值,它会一直通知。

🍉 启用 CORS, 打开 集成请求,找到映射模板,新建 text/xml,内容如下:

{
   "xml" : "$util.escapeJavaScript($input.path('$'))"
}

cid_145_4.png

调用统一下单接口

axios 是第三方包,用于发送 http 或 https 请求,使用前需要自行安装
x2js 是第三方包,用于把对象转换为 xml,使用前需安装,使用方法为

'use strict';
let axios = require("axios");
let x2js = require("x2js");
let convert = new x2js(); // 这里需要 new 请注意
let random = require("string-random");

🍀 开始调用

/**
 * 获取统一支付订单
 *
 * @param {Object} event
 * @returns
 */
function getUnifiedOrder(event) {
  let params = {
    "appid": event.appid,
    "body": "**网页-课程购买",
    "detail": "该笔订单用于购买**课程",
    "device_info": "WEB",
    "mch_id": event.mchId,
    "nonce_str": random(32),
    "notify_url": event.notifyUrl,
    "out_trade_no": event.orderId,
    "product_id": event.productId,
    "sign_type": "MD5",
    "spbill_create_ip": event.ip,
    "total_fee": event.fee,
    "trade_type": "NATIVE",
  };
  params.sign = sign(params, event.key);
  return new Promise((resolve, reject) => {
    let data = convert.js2xml({ xml: params });
    axios
      .post("https://api.mch.weixin.qq.com/pay/unifiedorder", data)
      .then(res => { resolve(res.data) })
      .catch(err => { reject(err) });
  });
}

🍀 返回结果(Content-Type 类型为 text/xml

cid_145_5.png

🍀 使用 x2js 将返回的信息进行转换,获得对象,把 code_url 返回给前端渲染

🍀 然后该做的就是数据库插入预订单,等待微信支付后台推送支付结果,如下图所示:

cid_145_6.png

格式化后的内容

cid_145_7.png

推送的内容是 text/xml 格式, 由于我在 API Gateway的设置, 返回的结果是字符串,需要你使用 x2js 转换为对象格式

🍀 最后修改数据库订单状态

订单状态获取

由于微信不保证推送结果能推送到我们自己的服务器,所以不能依赖此消息来维持订单状态,比较好的一种办法如下:

用户支付完成之后点击 我已支付 检查数据库, 如果已支付, 打开相关资源的锁, 如果数据库没有支付, 则调用微信支付订单状态获取API, 获取订单支付结果,具体细节前面有类似步骤,读者可以自行实现,这里给出 微信官方文档




微信小程序接入

openid 获取

小程序的支付方式是 NATIVE 而不是 JSAPI,因此需要openid

🍒 安装第三方包 axios,请求代码如下:

/**
 * 获取用户的 openid (小程序专用)
 *
 * @param {Object} event
 * @returns
 */
function getUserOpenId(event) {
  let config = {
    params: {
      appid: event.appid,
      secret: event.secret,
      js_code: event.code,
      grant_type: "authorization_code"
    }
  };
  return new Promise((resolve, reject) => {
    axios
      .get("https://api.weixin.qq.com/sns/jscode2session", config)
      .then(res => { resolve(res.data) })
      .catch(err => { reject(err) });
  });
}

再次签名

在网页版微信支付接入时,微信返回一个 code-url 然后我们返回给前端用于二维码渲染。 在小程序接入时,情况不再如此,因为在调用统一下单接口时trade_type 字段需要由NATIVE 改为 JSAPI,微信返回的结果也不同了。

微信返回的结果如下:

<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <return_msg><![CDATA[OK]]></return_msg>
  <appid><![CDATA[wx2421b1c4370ec13b]]></appid>
  <mch_id><![CDATA[1000010010]]></mch_id>
  <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
  <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
  <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
  <result_code><![CDATA[SUCCESS]]></result_code>
  <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
  <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

我们需要把这个结果中的一些字段再次签名然后返回给小程序 (前面的签名算法仍可用)。

变量名必填类型示例值
appIdStringwxd678efh567hg6787
timeStampString1490840662
nonceStrString5K8264ILTKCH16CQ2502SI8ZNMTM67VS
packageStringprepay_id=wx2017033010242291fcfe0db70013231072
signTypeStringMD5

再次签名时的字段即为上面5个字段, timeStampnonceStr 可以再次生成,也可以用这个结果的。

建议返回给小程序的格式不要再是 xml 结构了,因为小程序还需要再次解析,直接返回 通用的 Json 格式更好。返回给小程序一共包括 6 个字段,上面表格 5 个以及生成的签名。

小程序调用 wx.requestPayment()

当小程序接收到返回的 6 个字段后,调用 sdk 中的 API ,把必要参数填写完毕后即可。

当小程序收到支付成功的结果之后,建议让后台检查支付结果 检查支付结果的API文档,并修改相应数据库。建议小程序发送一些必要字段给后台来完成 API 调用,如果不这样,后台需要查数据库才能得到刚刚的订单信息。

总结与说明

本次使用 AWS 服务作为后端, 如果开发时采用的其他框架如 Node Express, SpringBoot 也可以参考本文, 因为道理是相通的。如果在试验时接收不到微信的支付结果推送, 请不要怀疑微信没有推送, 这里列出可能的一些原因:

🎁 SNI 没有配置正确
如果同一个服务器拥有多个域名, 微信可能推送不到正确的地址上面, 可以查找相关的资料进行解决。

🎁 没有正确接收微信的推送
正如本文上面的 acceptTencentPayResult 函数使用的 API Gateway 的集成请求处设置为$input.eascapeJavaScript($input.path('$')) 作用就是把正文中的特殊字符进行转义, 如果只有 $input.path('$'), 我也接受不到 xml 正文。
这时可以只打印请求的头部 (headers),不管正文,如果能打印出请求的头部,说明微信后台已经推送到正确的地址上了,只是你没有正确接收而已。如果没有打印出来,微信可能没有推送到正确的地址上。 如果你在查阅资料时看到某些帖子上说微信根本就没有推送,不要相信这种言论,微信后台在尽力推送,只是没有推送到正确的地址上而已。

🎁 关于推送的地址是 https 与 http
微信官方文档上说不需要 SSL 证书的意思就是可以有也可以没有,对此不作要求。如果把https 改为http 时接收成功,一定是相关的配置错误了,或者您的SSL证书过期了,微信后台认为不安全所以没有推送。

如果仍然解决不了问题,可以到微信开放社区的微信支付版块那里提问,那里会有很多与你问题相同或类似的情况,你也可以主动提问请求他人帮助你解决问题。为了方便测试,我这里给出微信支付结果推送的头部(某次试验结果)。

{ 
    host: 'snnu.henrenx.cn',
    'user-agent': 'Mozilla/4.0',
    'content-length': '837',
    accept: '*',
    'cache-control': 'no-cache',
    'content-type': 'text/xml',
    pragma: 'no-cache',
    'x-forwarded-for': '140.207.54.75',
    'accept-encoding': 'gzip',
    connection: 'close' 
}
0

评论 (0)

取消