- 探讨一下商圈会员待积分状态查询接口存在的意义
这个接口的文档链接在这里:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_6_7.shtml,是八月新增的几个接口之一。在智慧商圈上线的时候就咨询过一个问题,能不能实现类似某宝的纯自动积分,不需要用户干预,当时提到的说是可能存在非商圈交易无法确认,所以需要用户自己到商圈插件中自己点击一下。 现在官方推出了『商圈会员待积分状态查询』接口,查询到有待提交积分交易后前端可以引导用户进入插件去点击提交。我的疑惑在于,既然可以引导用户去做一个『打开插件->点击』这样的动作,为什么平台方不能自己自动完成这个动作,必须要用户自己去多此一举呢?如果是为了避免非商圈交易积分,那如果用户在商圈内积分后未提交,等到离开商圈1小时后再提交,平台是怎么判断的呢?
2022-08-31 - 消费者投诉2.0推送消息接入企业微信群
微信支付用户如果对订单有疑问或异议,可以通过支付凭证发起投诉,商户需在限定时限内进行回复处理。这个机制设计初衷应该是为了保护消费者权益,促进商户提升服务质量,官方也推出了投诉处理指南。 但是…重点来了,看一下这个苦主记录一次令人糟心的微信支付投诉处理,实际上我们自己也遇到过类似的场景,在商户本身没有任何过错的情况下,用户涉嫌装无辜连续甚至恶意投诉的时候,商户本身除了退款之外几乎没有其他任何维护自身利益的手段。 微信支付在接到用户投诉的时候可以通过回调消息通知商户进行跟进处理,前面描述的这个场景其实跟本文没有直接关系,只是说明接入投诉消息回调的必要性,避免非商户自身意愿导致商家服务质量指标下降。 首先,调用创建投诉通知回调地址接口,向微信支付登记用于处理投诉通知回调的接口url。代码如下(以微信支付API Python SDK为例) [代码]from wechatpayv3 import WeChatPay, WeChatPayType APPID = 'wx1234567890' # 应用appid MCHID = '1234567890' # 微信支付商户号 APIV3_KEY = '1234567890ABCDEFG' # 微信支付APIV3 KEY CERT_SERIAL_NO = '1234567890ABCDEFG' # 商户证书序列号 with open('path_to_private_key/apiclient_key.pem') as f: PRIVATE_KEY = f.read() CERT_DIR = './cert' # 平台证书缓存目录 wxpay = WeChatPay( wechatpay_type=WeChatPayType.MINIPROG, mchid=MCHID, private_key=PRIVATE_KEY, cert_serial_no=CERT_SERIAL_NO, apiv3_key=APIV3_KEY, appid=APPID, notify_url='', cert_dir=CERT_DIR) code, message = wxpay.complaint_notification_create('https://xxx.com/complaint/notify') print('{}, {}'.format(code, message)) [代码] 调用成功的话,微信支付服务器会返回 [代码]{ "mchid": "1234567890", "url": "https://xxx.com/complaint/notify" } [代码] 当发生用户投诉的时候,微信支付会将相关投诉信息推送到前面设置的https://xxx.com/complaint/notify这个地址。 下面将回调消息解密,并将解密后的内容推送到企业微信群里,以便相关人员及时更进处理。 [代码]import json import requests from flask import jsonify, request from wechatpayv3 import WeChatPay, WeChatPayType QIYE_WX_HOOK_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=12345678-abcd-abcd-abcd-12345678' # 企业微信群机器人HOOK URL @app.route("/complaint/notify", methods=['POST']) def wxnotify(): try: wxpay = WeChatPay( wechatpay_type=WeChatPayType.MINIPROG, mchid=MCHID, private_key=PRIVATE_KEY, cert_serial_no=CERT_SERIAL_NO, apiv3_key=APIV3_KEY, appid=APPID, notify_url='', cert_dir=CERT_DIR) result = wxpay.callback(request.headers, request.data.decode()) if result and result.get('resource'): data = result.get('resource') complaint_id = data.get('complaint_id') action_type = data.get('action_type') message = '微信支付用户投诉,投诉单号:{},事件类型:{}。'.format(complaint_id, action_type) data = { "msgtype": "markdown", "markdown": { "content": message } } requests.post(QIYE_WX_HOOK_URL, json=data) return jsonify({"code": "SUCCESS","message": "成功"}) except Exception as e: sslogger.exception('exception in complaint wxnotify: %s' % e) return '', 500 [代码] 至此,调试通过后,只需将涉及到微信支付用户投诉的人员,比如:客服、售后、财务、物流等拉个企业微信群,在微信支付端收到用户投诉或者投诉状态变更时,企业微信群机器人将会自动将提示信息推送到群里,方便了日常更进,也提升了用户体验。 这里只是展示了一个相对简单的业务场景,如果商户的业务量较大,业务系统体系和架构复杂,完全可以利用微信支付提供的消费者投诉2.0接口将用户投诉全流程和自己的业务系统对接,不用跳转到第三方系统即可完成用户投诉的处理回复和结单。
2021-11-24 - 三行代码搞定V3版微信支付接口
wechatpayv3 [图片] [图片] 介绍 微信支付接口V3版python库。 欢迎微信支付开发者扫码进QQ群讨论: QQ群号:973102221 适用对象 wechatpayv3支持微信支付直连商户,接口说明详见 官网。 特性 平台证书自动更新,无需开发者关注平台证书有效性,无需手动下载更新; 支持本地缓存平台证书,初始化时指定平台证书保存目录即可; 敏感信息直接传入明文参数,SDK内部自动加密,无需手动处理。 适配进度 微信支付V3版API接口 其中: 基础支付 [代码]JSAPI支付 已适配 APP支付 已适配 H5支付 已适配 Native支付 已适配 小程序支付 已适配 合单支付 已适配 付款码支付 待官网更新 刷脸支付 无需适配 [代码] 行业方案 [代码]智慧商圈 已适配 微信支付分停车服务 已适配 [代码] 营销工具 [代码]图片上传(营销专用) 已适配 [代码] 经营能力 [代码]支付即服务 已适配 [代码] 其他能力 [代码]图片上传 已适配 视频上传 已适配 [代码] 需要的接口还没有适配怎么办? 由于wechatpayv3包内核心的core.py已经封装了请求签名和消息验证过程,开发者无需关心web请求细节,直接根据官方文档参考以下基础支付的申请退款接口代码自行适配,测试OK的话,欢迎提交代码。 必填的参数建议加上空值检查,可选的参数默认传入None。参数类型对照参考下表: 文档声明 wechatpayv3 string string int int object dict: {} array list: [] boolean bool: True, False [代码]def refund(self, out_refund_no, amount, transaction_id=None, out_trade_no=None, reason=None, funds_account=None, goods_detail=None, notify_url=None): """申请退款 :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' :param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'} :param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018' :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' :param reason: 退款原因,示例值:'商品已售完' :param funds_account: 退款资金来源,示例值:'AVAILABLE' :param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1} :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' """ params = {} params['notify_url'] = notify_url or self._notify_url # if out_refund_no: params.update({'out_refund_no': out_refund_no}) else: raise Exception('out_refund_no is not assigned.') if amount: params.update({'amount': amount}) else: raise Exception('amount is not assigned.') if transaction_id: params.update({'transaction_id': transaction_id}) if out_trade_no: params.update({'out_trade_no': out_trade_no}) if reason: params.update({'reason': reason}) if funds_account: params.update({'funds_account': funds_account}) if goods_detail: params.update({'goods_detail': goods_detail}) path = '/v3/refund/domestic/refunds' return self._core.request(path, method=RequestType.POST, data=params) [代码] 源码 github gitee 安装 [代码]$ pip install wechatpayv3 [代码] 使用方法 准备 参考微信官方文档准备好密钥, 证书文件和配置(证书/密钥/签名介绍) 初始化 [代码]from wechatpayv3 import WeChatPay, WeChatPayType # 微信支付商户号 MCHID = '1230000109' # 商户证书私钥 PRIVATE_KEY = open('path_to_key/apiclient_key.pem').read() # 商户证书序列号 CERT_SERIAL_NO = '444F4864EA9B34415...' # API v3密钥, https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_2.shtml APIV3_KEY = 'MIIEvwIBADANBgkqhkiG9w0BAQE...' # APPID APPID = 'wxd678efh567hg6787' # 回调地址,也可以在调用接口的时候覆盖 NOTIFY_URL = 'https://www.weixin.qq.com/wxpay/pay.php' # 微信支付平台证书缓存目录 CERT_DIR = './cert' wxpay = WeChatPay( wechatpay_type=WeChatPayType.MINIPROG, mchid=MCHID, private_key=PRIVATE_KEY, cert_serial_no=CERT_SERIAL_NO, apiv3_key=APIV3_KEY, appid=APPID, notify_url=NOTIFY_URL, cert_dir=CERT_DIR) [代码] 接口 [代码] # 统一下单 def pay(): code, message = wxpay.pay( description='demo-description', out_trade_no='demo-trade-no', amount={'total': 100}, payer={'openid': 'demo-openid'} ) print('code: %s, message: %s' % (code, message)) # 订单查询 def query(): code, message = wxpay.query( transaction_id='demo-transation-id' ) print('code: %s, message: %s' % (code, message)) # 关闭订单 def close(): code, message = wxpay.close( out_trade_no='demo-out-trade-no' ) print('code: %s, message: %s' % (code, message)) # 申请退款 def refund(): code, message=wxpay.refund( out_refund_no='demo-out-refund-no', amount={'refund': 100, 'total': 100, 'currency': 'CNY'}, transaction_id='1217752501201407033233368018' ) print('code: %s, message: %s' % (code, message)) # 退款查询 def query_refund(): code, message = wxpay.query_refund( out_refund_no='demo-out-refund-no' ) print('code: %s, message: %s' % (code, message)) # 申请交易账单 def trade_bill(): code, message = wxpay.trade_bill( bill_date='2021-04-01' ) print('code: %s, message: %s' % (code, message)) # 申请资金流水账单 def fundflow_bill(): code, message = wxpay.fundflow_bill( bill_date='2021-04-01' ) print('code: %s, message: %s' % (code, message)) # 下载账单 def download_bill(): code, message = wxpay.download_bill( url='https://api.mch.weixin.qq.com/v3/billdownload/file?token=demo-token' ) print('code: %s, message: %s' % (code, message)) # 合单支付下单 def combine_pay(): code, message = wxpay.combine_pay( combine_out_trade_no='demo_out_trade_no', sub_orders=[{'mchid':'1900000109', 'attach':'深圳分店', 'amount':{'total_amount':100,'currency':'CNY'}, 'out_trade_no':'20150806125346', 'description':'腾讯充值中心-QQ会员充值', 'settle_info':{'profit_sharing':False, 'subsidy_amount':10}}] ) print('code: %s, message: %s' % (code, message)) # 合单订单查询 def combine_query(): code, message = wxpay.combine_query( combine_out_trade_no='demo_out_trade_no' ) print('code: %s, message: %s' % (code, message)) # 合单订单关闭 def combine_close(): code, message = wxpay.combine_close( combine_out_trade_no='demo_out_trade_no', sub_orders=[{'mchid': '1900000109', 'out_trade_no': '20150806125346'}] ) print('code: %s, message: %s' % (code, message)) # 计算签名供调起支付时拼凑参数使用 # 注意事项:注意参数顺序,某个参数为空时不能省略,以空字符串''占位 def sign(): print(wxpay.sign(['wx888','1414561699','5K8264ILTKCH16CQ2502S....','prepay_id=wx201410272009395522657....'])) # 验证并解密回调消息,把回调接口收到的headers和body传入 def decrypt_callback(headers, body): print(wxpay.decrypt_callback(headers, body)) # 智慧商圈积分通知 def points_notify(): code, message = wxpay.points_notify( transaction_id='4200000533202000000000000000', openid='otPAN5xxxxxxxxrOEG6lUv_pzacc', earn_points=True, increased_points=100, points_update_time='2020-05-20T13:29:35.120+08:00' ) print('code: %s, message: %s' % (code, message)) # 智慧商圈积分授权查询 def user_authorization(): code, message = wxpay.user_authorizations( openid='otPAN5xxxxxxxxrOEG6lUv_pzacc' ) print('code: %s, message: %s' % (code, message)) # 支付即服务人员注册 def guides_register(): code, message = wxpay.guides_register( corpid='1234567890', store_id=1234, userid='rebert', name='robert', mobile='13900000000', qr_code='https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx', avatar='http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0', group_qrcode='http://p.qpic.cn/wwhead/nMl9ssowtibVGyrmvBiaibzDtp/0' ) print('code: %s, message: %s' % (code, message)) # 支付即服务人员分配 def guides_assign(): code, message = wxpay.guides_assign( guide_id='LLA3WJ6DSZUfiaZDS79FH5Wm5m4X69TBic', out_trade_no='20150806125346' ) print('code: %s, message: %s' % (code, message)) # 支付即服务人员查询 def guides_query(): code, message = wxpay.guides_query( store_id=1234, userid='robert', mobile='13900000000', work_id='robert', limit=5, offset=0 ) print('code: %s, message: %s' % (code, message)) # 支付即服务人员信息更新 def guides_update(): code, message = wxpay.guides_update( guide_id='LLA3WJ6DSZUfiaZDS79FH5Wm5m4X69TBic', name='robert', mobile='13900000000', qr_code='https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx', avatar='http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0', group_qrcode='http://p.qpic.cn/wwhead/nMl9ssowtibVGyrmvBiaibzDtp/0' ) print('code: %s, message: %s' % (code, message)) # 图片上传 def image_upload(): code, message = wxpay.image_upload( filepath='./media/demo.png' ) print('code: %s, message: %s' % (code, message)) # 视频上传 def video_upload(): code, message = wxpay.video_upload( filepath='./media/demo.mp4' ) print('code: %s, message: %s' % (code, message)) # 查询车牌服务开通信息 def parking_service_find(): code, message = wxpay.parking_service_find( plate_number='粤B888888', plate_color='BLUE', openid='oUpF8uMuAJOM2pxb1Q' ) print('code: %s, message: %s' % (code, message)) # 创建停车入场 def parking_enter(): code, message = wxpay.parking_enter( out_parking_no='1231243', plate_number='粤B888888', plate_color='BLUE', notify_url='https://yoursite.com/wxpay.html', start_time='2017-08-26T10:43:39+08:00', parking_name='欢乐海岸停车场', free_duration=3600 ) print('code: %s, message: %s' % (code, message)) # 停车扣费受理 def parking_order(): code, message = wxpay.parking_order( description='停车场扣费', out_trade_no='20150806125346', notify_url='https://yoursite.com/wxpay.html', total=888, parking_id='5K8264ILTKCH16CQ250', plate_number='粤B888888', plate_color='BLUE', start_time='2017-08-26T10:43:39+08:00', end_time='2017-08-26T10:43:39+08:00', parking_name='欢乐海岸停车场', charging_duration=3600, device_id='12313' ) print('code: %s, message: %s' % (code, message)) # 查询停车扣费订单 def parking_order_query(): code, message = wxpay.parking_order_query( out_trade_no='20150806125346' ) print('code: %s, message: %s' % (code, message)) # 图片上传(营销专用) def marking_image_upload(): code, message = wxpay.marketing_image_upload( filepath='./media/demo.png' ) print('code: %s, message: %s' % (code, message)) [代码]
2021-08-14 - 微信支付v3版几个重难点(签名、验签、加密、解密)的python实现
背景介绍 v3版微信支付通过商户证书和平台证书加强了安全性,也大幅提高了开发难度,python版sdk包wechatpayv3内部封装了安全性相关的签名、验签、加密和解密工作,降低了开发难度。下面几个特性的实现,更方便了开发者。 平台证书自动更新,无需开发者关注平台证书有效性,无需手动下载更新; 支持本地缓存平台证书,初始化时指定平台证书保存目录即可。 敏感信息直接传入明文参数,SDK内部自动加密,无需手动处理。 在这里将sdk内部几个涉及到安全性的方法单独抽出来结合官方文档对照一下,方便大家更直观的理解,有兴趣探究sdk内部实现细节的同学可以了解一下,一般应用的无需关注,直接调用sdk包即可。 构造签名信息 对应v3版微信支付api文档的签名生成部分。 [代码]def build_authorization(path, method, mchid, serial_no, mch_private_key, data=None, nonce_str=None): timeStamp = str(int(time.time())) nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper() body = json.dumps(data) if data else '' sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body) signature = rsa_sign(private_key=mch_private_key, sign_str=sign_str) authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (mchid, nonce_str, signature, timeStamp, serial_no) return authorization [代码] 验证签名 对应v3版微信支付api文档的签名验证部分。 [代码]def rsa_verify(timestamp, nonce, body, signature, certificate): sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body) public_key = certificate.public_key() message = sign_str.encode('UTF-8') signature = b64decode(signature) try: public_key.verify(signature, sign_str.encode('UTF-8'), PKCS1v15(), SHA256()) except InvalidSignature: return False return True [代码] 回调信息解密 对应v3版微信支付api文档的证书和回调报文解密部分。 [代码]def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key): key_bytes = apiv3_key.encode('UTF-8') nonce_bytes = nonce.encode('UTF-8') associated_data_bytes = associated_data.encode('UTF-8') data = b64decode(ciphertext) aesgcm = AESGCM(key=key_bytes) try: result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8') except InvalidTag: result = None return result [代码] 敏感信息加密 对应v3版微信支付api文档的敏感信息加解密的加密部分。 [代码]def rsa_encrypt(text, certificate): data = text.encode('UTF-8') public_key = certificate.public_key() cipherbyte = public_key.encrypt( plaintext=data, padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None) ) return b64encode(cipherbyte).decode('UTF-8') [代码] 敏感信息解密 对应v3版微信支付api文档的敏感信息加解密的解密部分。 [代码]def rsa_decrypt(ciphertext, private_key): data = private_key.decrypt(ciphertext=b64decode(ciphertext), padding=OAEP(mgf=MGF1(algorithm=SHA1), algorithm=SHA1)) result = data.decode('UTF-8') return result [代码] 注意事项 以上涉及到的几项签名如果计划抽出来单独使用,需要引入cryptography包。
2021-07-30