【原创】:转载请注明出处,谢谢!
相信大家在微信支付开发过程中,都多多少少遇到过不少的坑,笔者前不久从坑里跳出来,觉得有必要总结和分享一下走过的坑。这是一个系列文章一部分,也是基础概念和流程的介绍。
基本假设
首先,这里有一些基本的假设,
- 前后端分离的方式开发,你是后端,开发语言为Python;
- 需要开发微信支付;
- 你目前需要从小程序调起微信支付以便用户付款;
- 你需要以JSAPI的方式开发微信支付(其实其他方式开发也是大同小异)。
微信的坑
坑1:微信支付官方资料太过于混乱,以至于可能看了2-3天的资料,仍然没有头绪;
坑2:基本概念解释的既乏味,又没有营养;
坑3:原来各个支付方式中的概念是相通的!
支付流程
首先介绍一下支付的流程。其实,微信转账很容易完成,不用开发人员,你直接将自己微信的钱转给自己的女朋友,简单的描述如下:
- 从通讯录找到女朋友;
- 打开聊天窗口,点击"+"号;
- 选择转账;
- 输入金额,如有需要,添加备注;
- 确认转账;
- 完成验证,转账成功。
我们的支付开发也是类似的,但是会有如下的不同点:
- 收款方式:支付是由收款方发起,而非付款方发起,所以一定要在某个契机之下,发送消息给用户,要求收款;
- 时效性:我们的交易可能不是立刻完成的,所以我们需要将说好的交易先保存在某个地方;
- 避免干扰不想关用户:我们不能随随便便向所有人都发起收款申请,所以要向微信证明,我们能够找这个用户收款;
- 收款有据:我们作为企业方(公司),一定是因为提供了服务或者商品,才有资格收款的。通常公司都会是因为有订单,所以要收款;
- 信任问题:我们不是自己登陆在微信客户端,微信不一定信任我们。我们得证明自己是自己;
- 安全问题:信息都是在网络传输,被人篡改怎么办,所以得想个办法避免这个问题;
- 确认收款的方式:最后,作为公司,不可能专门派人去看着每笔款是否到账,只要微信告诉我们到账,我们就认为到账了。
前置知识:
- 微信为了管理所有的小程序,给所有的小程序都分配了一个唯一的号码(appid),同时,为了识别每个小程序下的用户,都会分配一个在该小程序下唯一的用户号(openid);
- 前端通常不存储微信发给商户的app key信息(这个注册的时候会有的)。
基于以上的不同点,目前,微信JSAPI支付的流程设计如下:
- 获取用户openid:前后端配合完成。这个openid是用来发起收款申请的,既然能够发起收款申请,也就意味着经过了用户同意了,也就意味着客户有一个授权过程。后面弹出支付要求时,我们也不算干扰不相关用户。这个授权的过程,由于我们目前前后端分离的开发(当然,也因为微信基于安全的设计),进一步分解如下:
- a. 获取code:在用户打开小程序时,小程序(前端wx.login来调用)会弹出授权请求,这个时候,用户一旦授权,前端就会收到临时的用户号,即code,此code仅有5分钟的有效时间;
- b. 获取openid:前端将临时的用户号(code)传给后端,后端调起相关接口,通过自己的app密码(app key)等获取openid等,具体参考如下:
官方openid获取指导
- 统一下单:后端完成。有了openid,意味着用户授权我们,我们可以"打扰"用户。分头叙述,假如这个时候,用户在小程序看到了好吃的,好产品,或者服务,想占为己有,就会下单,下单时,商户这边会生产一个商户自己的内部订单号,下单后,是否立刻支付,这个就涉及到订单时效性,为了记住订单,以便用户过一会儿付款也不丢失状态,所以,我们找微信下个订单。而这个下单,就是统一下单,为了一一对应,我们将自己的内部订单号给微信。同时,还有一些识别我们身份所需的东西,比如appid,mch_id,app key等等。下面解释一下所必需的东西以及理由:
-
appid,mch_id:与信任问题相关。这些不用说吧,你得告诉微信你是谁,当然光有这个不行,还要有app key,但是app key就像钥匙,不会直接传输的,这个要到最后sign的时候讲; -
nonce_str:这个是为了安全,提高安全系数; -
body:这个与收款有据相关。告诉微信,这个订单你提供了什么服务,相当于备忘,其实我们随便填什么微信都接受,但是为了方便后续查询等,避免麻烦等,最好参照官方规范; -
out_trade_no:这个与收款有据相关。内部订单号,用来和微信的支付订单一一对应的,在公司内部需要唯一,唯一性的要求很好理解,如果一旦用户支付,微信告诉你订单已经支付,你公司内部有两个一样的订单号指向同一个微信的支付订单的话,你怎么知道到底是哪个订单被支付?当然,具体内部订单号什么规则,这个由你来决定,别超过32位,也不要是奇奇怪怪的字符就行,比如ChiPuTaobutuputaopi,5chiputaoNichipi,这些都可以。 -
total_fee:订单金额,本来没什么说,收钱当然要告诉人家收多少,但是有个坑,记得单位为分,所以自然而然传给微信的是整数,否则后面会报错,“0参数错误”什么的。比如,收100元,传过去的数字要10000。 -
spbill_create_ip:这个是微信怕你抵赖,记录下订单的发生地址。这个相当于现实世界里奶茶店的地址,你外卖下单,一定会知道奶茶店的地址,实际是不是从那里发出的不要紧,但是一旦作假,只要微信想知道,查一下就可以了。毕竟,如果你从上海任何地方点一杯奶茶,外卖地址都是从上海人民广场,你查一下所有订单,肯定知道这个有问题。 -
notify_url:这个是用户支付这个订单后,微信通知我们支付结果的地址,即我们通过这里微信返回的信息确认是否收款。
- 首先,既然微信想通知我们,我们如果躲在局域网内,微信是找不到我们的,所以这个地址必须是公网地址;
- 其次,人家是过来送信的,如果我们在门口安排保安拦着,这样人家就送不过来了,所以,这个地址必须是无守卫的门一样,可以任意接受信件。
-
trade_type:这里是JSAPI。这个参数必传的原因是,这是一个通用的下单接口,为了方便微信收到后,给相应的下道处理程序,所以传这个参数。有点像去菜市场买肉,有几个卖肉(猪肉,牛肉,羊肉)的摊位,你需要指定买哪种肉几斤,不能只说要买几斤肉。 -
openid:注意看备注,当支付方式为JSAPI时,本项是必填项。意思是,在微信系统里,生成的这个支付订单只能找该用户收钱。这里其实是我们的收款方式决定的。 -
sign:这个特意放在最后写,因为这个与以上都相关,且是涉及安全问题的。
- 这里会用到我们手上的app key,用来证明确实是我们让微信创建待支付订单的,即信任问题;
- 需要这个sign,主要是担心以上这些内容传给微信时,万一传输途中被人篡改了呢?所以将以上所有字段按照一定规则,结合在手上的app key,一起非对称加密(这个概念很大,简单的说,就是一款由各种材料按照一定比例制成的香水,各种材料指的是以上的字段,做出来的香水就是sign)的结果,作为sign传给微信。万一有人截获信息,做任何的篡改,结果都会导致字段信息和sign对不上,微信就会告诉我们,sign不对。
参考如下: 微信统一下单要求 官方关于字段内容的规范要求
- 调起支付:这里由前端(WeixinJSBridge.invoke来调起支付)完成。从上一步,我们在微信支付系统中生成了订单,叫做prepay_id,从名字看得出,是待支付订单号。有了他,后端就可以将所有字段传给前端调起支付了。微信为了区分前后端的字段要求,不同于后端调起的带下划线的参数名(nonce_str等),特意将这些参数名字改成小驼峰了,比如timeStamp,nonceStr之类的。单个的单词sign直接改为了paySign,不好改的prepay_id干脆换了个单词package来替代。
官方调起支付的参数
- 弹出支付界面:如果以上都顺利,弹出微信支付界面,用户付款。付款OK后,微信会根据我们在上面给的notify_url访问该地址,并且将结果告诉我们。
辅助函数
好了,作为系列之一,我们先来看辅助函数,主要有:
- 生成随机数 nonce_str
- 生成xml格式:统一下单接口调用规定使用xml格式
- 转为字典:接受微信回传的信息,转换成字典
- 生成sign string:用于生成sign
- 生成sign或者paySign;
Talk is cheap,show me your code!
import hashlib
import hmac
import random
import string
from collections import OrderedDict
from lxml import etree
import wechat_setting
class WechatPay:
def __init__(self,
app_id: str = wechat_setting.APP_ID,
mch_id: str = wechat_setting.MCH_ID,
secret_key: str = wechat_setting.SECRET_KEY,
sign_algorithm_md5: str = wechat_setting.WECHAT_SIGN_ALGORITHM_MD5,
pay_notify_url: str = wechat_setting.PAY_NOTIFY_URL,
wechat_key: str = wechat_setting.WECHAT_KEY,
wechat_prepay_api: str = wechat_setting.WECHAT_PREPAY_API,
):
self.app_id = app_id
self.mch_id = mch_id
self.secret_key = secret_key
self.pay_notify_url = pay_notify_url
self.wechat_key = wechat_key
self.wechat_prepay_api = wechat_prepay_api
self.sign_algorithm_md5 = sign_algorithm_md5
def generate_nonce_str(self, length: int = 32) -> str:
nonce_symbols = string.digits + string.ascii_letters
return "".join(random.choice(nonce_symbols) for _ in range(length))
def generate_sign_str(self, request_data: dict, is_pay_sign: bool = False) -> str:
if is_pay_sign:
request_data["package"] = f"prepay_id={request_data.pop('prepay_id')}"
sign_string = ""
ordered_items = OrderedDict(sorted(request_data.items()))
for key, value in ordered_items.items():
if key != "sign" and key != "paySign" and value is not None:
sign_string = sign_string + f"{key}={value}&"
return sign_string + f"key={self.wechat_key}"
def create_sign(self, sign_string: str, sign_algorithm: str = "MD5") -> str:
if sign_algorithm.upper() == self.sign_algorithm_md5:
signed_string = hashlib.md5(sign_string.encode(encoding='UTF-8')).hexdigest()
else:
signed_string = hmac.new(str.encode(self.wechat_key), str.encode(sign_string),
digestmod=hashlib.sha256).hexdigest()
return signed_string.upper()
@staticmethod
def to_xml(raw: dict) -> bytes:
child_string = ""
for key, value in raw.items():
child_string += f"<{key}>{value}</{key}>"
root = f"<xml>{child_string}</xml>"
return root.encode("utf-8")
@staticmethod
def to_dict(xml_content: str) -> dict:
raw = {}
root = etree.fromstring(xml_content,
parser=etree.XMLParser(resolve_entities=False))
for child in root:
raw[child.tag] = child.text
return raw
好了,本系列第一期首先到这里,欢迎留言交流,后续有时间再整理其他相关概念,蹲的坑和跳出坑解决方法,证书验证,统一下单等具体支付细节。
喜欢可以点赞收藏哦…
|