由于微信支付分开发没有python3版本的示例,整个网络找AEAD_AES_256_GCM验签python版本都找不到几个案例,所以有必要根据流程记录一下本次开发遇到的一些坑
1.首先是创建免押租借订单,因为对json相对熟悉一点,将所有参数写成json格式,然后封装成方法,供视图调用
def createrentbill(out_order_no, rent_unit_fee):
UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/wxv/createrentbill'
nonce_str = ''.join(random.sample(string.ascii_letters + string.digits, 32))
order_info = {
'version': '1.0', # 版本号
'appid': WEIXIN_APP_ID, # 公众账号ID
'mch_id': WEIXIN_MCHID, # 商户ID
'nonce_str': nonce_str, # 随机字符串
'sign_type': 'HMAC-SHA256',
'out_order_no': out_order_no, # 传入的商户平台订单号,也就是自己系统生成的订单号
'service_id': WEIXIN_SERVICE_ID, # 在接入支付分的时候,微信方会提供这个service_id,
'goods_name': '雨伞一个', # 商品名称
'deposit_amount': 5000, # 押金总额,单位/分
'rent_unit': 'FEN_1_HOUR', # 计费方式: 小时/分
'rent_unit_fee': rent_unit_fee #传入的单价
}
order_info['sign'] = hmac_sha256(order_info)
data = xmltodict.unparse({'xml': order_info}, pretty=True, full_document=False).encode('utf-8')
headers = {'Content-Type': 'application/xml'}
weixinapiclient_cert = WX_CERT_PATH # 需要传入证书和密钥
weixinapiclient_key = WX_KEY_PATH # 需要传入证书和密钥
response = requests.post(UNIFIED_ORDER_URL, data=data, headers=headers,
cert=(weixinapiclient_cert, weixinapiclient_key), verify=True)
msg = response.text
xmlmsg = xmltodict.parse(msg) # 将xml转成字典
if xmlmsg['xml']['return_code'] == 'SUCCESS':
if xmlmsg['xml']['result_code'] == 'SUCCESS':
order_id = xmlmsg['xml']['order_id'] # 微信支付服务订单号
package = xmlmsg['xml']['package'] # 跳转微信侧小程序订单数据
info = {
'mch_id': WEIXIN_MCHID,
'package': package,
'timestamp': str(int(time.time())), # 时间戳为string类型
'nonce_str': nonce_str,
'sign_type': 'HMAC-SHA256',
}
info['sign'] = hmac_sha256(info)
return info,order_id # 返回给微信小程序端的数据,根据自己的业务需要返回
return False, False
return False, False
上面需要使用到HMAC-SHA256做签名,参数名还要按ASCII码从小到大排序(字典序):
def hmac_sha256(param):
'''使用hmac-sha256生成签名'''
appkey = settings.WEIXIN_APP_KEY
stringA = ''
ks = sorted(param.keys())
# 参数排序
for k in ks:
stringA += (str(k) + '=' + str(param[k]) + '&')
# 拼接商户KEY
stringSignTemp = stringA + "key=" + appkey
signature = hmac.new(bytes(appkey, encoding='utf-8'), bytes(stringSignTemp, encoding='utf-8'),
digestmod=hashlib.sha256).digest()
# 二进制转为HEX
HEX = signature.hex()
# 转化为大写
sign = HEX.upper()
return sign
由于我在定义参数的时候使用的是字典,而发送给微信的时候需要转成xml,所以又写了个方法,后面还会用到:
def trans_dict_to_xml(data_dict):
'''定义字典转XML的函数'''
data_xml = []
for k in sorted(data_dict.keys()): # 遍历字典排序后的key
v = data_dict.get(k) # 取出字典中key对应的value
if k == 'detail' and not v.startswith(''.format(v)
data_xml.append('<{key}>{value}{key}>'.format(key=k, value=v))
return '{} '.format(''.join(data_xml)) # 返回XML
使用 requests.post给微信端发送消息,post请求:
response = requests.post(UNIFIED_ORDER_URL, data=data, headers=headers,
cert=(weixinapiclient_cert, weixinapiclient_key), verify=True)
第一步创建免押租借订单就算是完成了,有些固定值需要自己配置,比如appid,mchid之类的
这里碰到的第一个坑是,租金规则的示例值:
根据文档显示示例值为DAY_FEN,我的需求就应该是HOUR_FEN,然后微信那边一直给返回传参错误,后来根据右边的例如的例子,改为FEN_1_HOUR才对,然后当场给微信那边反应,第二天发现他们已经改回来了,用的还是我在代码里用的,他们这个文档写得确实是乱
2. 创建订单成功后,还要接收微信方发送过来的回调,回调地址必须是HTTPS的,在商户接入支付分的时候,微信的工作人员会让接入的商户填写一个表格,里面就包括要填的回调地址,注意:这个地址在这个时候开始就算是已经被使用了,其他的接口不能再使用这个路由地址
class AuthConfirmView(APIView):
'''确认订单回调通知'''
def post(self, request):
key = WEIXIN_APIV3_KEY # apiv3的key在商户后台里设置,一定要是32位的,作为验签的key使用
msg = request.body.decode('utf-8')
dic = trans_xml_to_dict(msg) # 微信方发过来的是xml格式的字符串,注意是字符串,所以又写了个方法给它转成字典
associated_data = dic['event_associated_data'] # 验签需要的数据
ciphertext = dic['event_ciphertext'] # 验签需要的数据
nonce = dic['event_nonce'] # 验签需要的数据
ciphert = decrypt(ciphertext, key, nonce, associated_data) # 解密验签 ,解出来的还是xml格式的
res = trans_xml_to_dict(ciphert) # 因为不会从xml中取值,所有又转成字典
finish_ticket = res['finish_ticket'] # 完结凭证,一定要存起来,后面创建完结订单的时候需要用到
out_order_no = res['out_order_no'] # 商户自己的订单号,微信给返回回来
try:
order = Order.objects.get(order_id=out_order_no) # 自己查库
except:
return Response({'message': '订单不存在'})
order.finish_ticket = finish_ticket # 将完结凭证村库
order.save()
return Response(trans_dict_to_xml({'return_code': '', 'return_msg': ''}))
将xml转成字典的方法:
def trans_xml_to_dict(xml_data):
#将xml字符串转换为字典
soup = BeautifulSoup(xml_data, features='xml')
xml = soup.find('xml') # 解析XML
if not xml:
return {}
data_dict = dict([(item.name, item.text) for item in xml.find_all()])
return data_dict
使用AEAD_AES_256_GCM解密验签,这个方法在网上好难找,后来是微信那边给提供的
def decrypt(ciphertext, key, nonce, associated_data):
'''使用AEAD_AES_256_GCM解密'''
key_bytes = str.encode(key)
aesgcm = AESGCM(key_bytes)
nonce_bytes = str.encode(nonce)
ad_bytes = str.encode(associated_data)
data = base64.b64decode(ciphertext)
return aesgcm.decrypt(nonce_bytes, data, ad_bytes)
如果整个过程都是成功的,那么返回给微信方的数据为:
return Response(trans_dict_to_xml({'return_code': '', 'return_msg': ''}))
这第二步的确认订单成功回调通知的处理就算完成了,这里又遇到的一个坑是:
也就是说,开发文档中写着,微信方那边返回来的数据时json格式的,后来怎么试都不对,然后鬼使神差就试了xml格式,居然对了。又跟微信那边的技术人员反映,后来他们又改回来了:
多加了两句话。。。
3.先总结完结免押租借订单吧,因为查询,修改,撤销之类的不用回调,而创建和完结需要用到回调
# 完结免压支付订单,还是一堆熟悉的数据
def finishrentbill(out_order_no, total_amount, rent_fee, finish_ticket):
UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/wxv/finishrentbill'
nonce_str = ''.join(random.sample(string.ascii_letters + string.digits, 32))
realtime = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
order_info = {
'version': '1.0',
'appid': WEIXIN_APP_ID,
'mch_id': WEIXIN_MCHID,
'nonce_str': nonce_str,
'sign_type': 'HMAC-SHA256',
'out_order_no': out_order_no,
'service_id': WEIXIN_SERVICE_ID,
'returned': 'TRUE', # 注意这里,这个是string类型,不是布尔类型,如果这里填了TRUE,下面的归还时间就一定要
'real_end_time': realtime, # 归还时间,选的是当前时间,也必须是string类型
'total_amount': int(total_amount), # 总金额,一定要是int类型的,否则还是会报错
'rent_fee': int(rent_fee), # 租金费用,也一定要是int类型的,如果没有赔偿金,其实总金额就是等于租金费用
'finish_ticket': finish_ticket, # 从数据库中取出来,然后传过来,一个订单一个完结凭证
}
order_info['sign'] = hmac_sha256(order_info) # 签名,并且加入到参数里面去,签名方法和前面的一样
data = xmltodict.unparse({'xml': order_info}, pretty=True, full_document=False).encode('utf-8')
headers = {'Content-Type': 'application/xml'}
weixinapiclient_cert = WX_CERT_PATH
weixinapiclient_key = WX_KEY_PATH
response = requests.post(UNIFIED_ORDER_URL, data=data, headers=headers,
cert=(weixinapiclient_cert, weixinapiclient_key), verify=True)
msg = response.text
xmlmsg = xmltodict.parse(msg)
if xmlmsg['xml']['return_code'] == 'SUCCESS':
if xmlmsg['xml']['result_code'] == 'SUCCESS':
order_id = xmlmsg['xml']['order_id'] # 微信支付服务订单号
data = {
'order_id': order_id, # 其实需要什么数据,自己可以看微信文档,然后按照自己的业务逻辑去取
}
return data
return False
return False
注意看右边的描述,如果单纯看是否必填就很容易忽略掉实际归还时间这个参数
这第三步完结免押租借订单就算是大功告成了,这里也遇到一个坑,手动捂脸
微信支付分文档里面,这个分账标识是必须传的,当时我们没有指定服务商分账,我就填了False,然后这个错就找了好久,后面鬼使神差的把这个分账标识去掉,它居然可以跑通了。。。4.既然完结订单也已经写好了,接下来就是处理微信发过来的回调了
class PaySuccessView(APIView):
'''支付成功回调通知'''
def post(self, request):
key = WEIXIN_APIV3_KEY
msg = request.body.decode('utf-8')
dic = trans_xml_to_dict(msg)
associated_data = dic['event_associated_data']
ciphertext = dic['event_ciphertext']
nonce = dic['event_nonce']
ciphert = decrypt(ciphertext, key, nonce, associated_data) # 解密验签
res = trans_xml_to_dict(ciphert)
out_order_no = res['out_order_no']
finish_transaction_id = res['finish_transaction_id']
try:
order = Order.objects.get(order_id=out_order_no)
except:
return Response({'message': '订单不存在'})
order.status = True # 修改订单状态为已支付
order.finish_transaction_id = finish_transaction_id # 退款时需要这个字段,所以也存起来
order.save()
return Response(trans_dict_to_xml({'return_code': '', 'return_msg': ''}))
写了上面的三个接口,这次就没有遇到什么坑啦啦啦啦啦
微信支付分明确提到,支付分这边是没有独立的退款接口的,如果实在是需要退款,用的是普通支付的退款接口
微信把这个东西写了问题集锦里面去了,当调用普通退款接口给支付分退款的时候,将finish_transaction_id 替换transaction_id 就可以了
5.查询,撤销,修改这三个接口就不做总结了,按照文档去写,不会遇到什么坑了
使用python3的Django框架开发微信支付分后端业务处理就算是大功告成了,也许是因为微信那边的支付分功能刚开通不久吧,文档上的漏洞还是很多,遇到问题就像微信那边的技术人员寻求帮助就可以了