# -*- coding: utf-8 -*- # !/usr/bin/env python import json import logging import time from urllib import urlencode import OpenSSL import requests import sys reload(sys) sys.setdefaultencoding("utf-8") logger = logging.getLogger(__name__) class HeNanRuralCreditUnion(object): DEFAULT = { 'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36' } def __init__(self, appCode, channelId, branchId, SericeCenterUrl, privateKey, publicKey): self.appCode = appCode self.channelId = channelId self.branchId = branchId self.SericeCenterUrl = SericeCenterUrl self._PRIVATE_KEY = privateKey self._PUBLIC = publicKey # def _load_pfx(self): # """ # 读取证书里面的私钥 # :return: # """ # pfx = open(self.PFX_PATH, 'rb').read() # p12 = OpenSSL.crypto.load_pkcs12(pfx, self.PFX_PASSWD) # return OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, p12.get_privatekey()) # # def _load_pub(self): # """ # 读取校验公钥 # :return: # """ # pub = open(self.PUB_PATH, 'rb').read() # return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pub) def _check_empty(self, kw): # type: (dict) -> dict """ 过滤值为空的数据 :param kw: :return: """ return dict(filter(lambda _: _[1] is not None, kw.items())) def _sign(self, string): # type: (str) -> str """ 用私钥签名 :param string: :return: """ pri_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, self._PRIVATE_KEY) sign = OpenSSL.crypto.sign(pri_key, string.encode(), 'SHA256') return sign.encode('hex') def _verify(self, signature, plain): """ 用公钥验签 :param string: :return: """ pub_key = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, self._PUBLIC) return OpenSSL.crypto.verify(pub_key, signature, plain, 'SHA256') def _connection_string(self, kw): # type: (dict) -> unicode """ 拼接字符串 :param kw: :return: """ return "&".join("{}={}".format(*_) for _ in kw.items()) def public_part(self): now = time.time() sequenceId = self.channelId sequenceId += time.strftime('%y%m%d', time.localtime(now)) sequenceId += '{}{:>3.0f}'.format(self.appCode, now * 1000)[:24] public_part = { # 公共部分 'appCode': self.appCode, 'channelId': self.channelId, 'branchId': self.branchId, 'sequenceId': sequenceId, 'timestamp': time.strftime('%Y%m%d%H%m%S', time.localtime(now)) + '{:0>3.0f}'.format( (now - int(now)) * 1000), 'charset': 'UTF-8', 'algorithm': 'SHA256withRSA', 'deviceId': '123456', 'version': '1.0', 'Content-Type': 'application/x-www-form-urlencoded', 'userId': None, 'userName': None, 'entoperId': None, 'entoperName': None, } return public_part def generate_signature(self, public, payload): # 公共部分 plain_dic = { # 公共部分 'appCode': public.get('appCode'), 'version': public.get('version'), 'sequenceId': public.get('sequenceId'), 'timestamp': public.get('timestamp'), 'charset': public.get('charset'), 'algorithm': public.get('algorithm'), 'deviceId': public.get('deviceId'), 'channelId': public.get('channelId'), 'branchId': public.get('branchId'), } # 业务部分 plain_dic.update(payload) plain_list = sorted(plain_dic.items(), key=lambda _: _[0]) plain = urlencode(plain_list) return self._sign(plain) def verify_signature(self, public, payload): signature = public.get('signature') signature = signature.decode('hex') # 公共部分 plain_dic = { 'code': public.get('code'), 'message': public.get('message'), 'sequenceId': public.get('sequenceId'), 'jnlId': public.get('jnlId'), 'timestamp': public.get('timestamp'), 'algorithm': public.get('algorithm'), 'charset': public.get('charset'), 'messagedigest': public.get('messagedigest'), 'serverIp': public.get('serverIp'), } # 业务部分 plain_dic.update(payload) result = sorted(plain_dic.items(), key=lambda _: _[0]) plain = urlencode(result) try: if self._verify(signature=signature, plain=plain) == None: return True except: import traceback logger.info(traceback.format_exc()) return False def pre_pay_order(self, payload): public = self.public_part() # 组织请求头 headers = public.copy() headers['transCode'] = 'hnnx.trade.sandbox.upp.qrCodePrePay' if not payload.get('userAgent'): headers['User-Agent'] = self.DEFAULT.get('userAgent') else: headers['User-Agent'] = payload.get('userAgent') payload = { 'MerNbr': payload.get('MerNbr'), # 原支付交易一级商户号 'EncryptSubMerchantId': payload.get('EncryptSubMerchantId'), # 加密二级商户号 'SubMerchantId': payload.get('SubMerchantId'), # 二级商户号 'SubMerchantName': payload.get('SubMerchantName'), # 二级商户名称 'MerTransDateTime': payload.get('MerTransDateTime'), # 交易时间 'TransAmt': payload.get('TransAmt'), # 交易金额 'AuthCode': payload.get('AuthCode'), # 授权code 'ChannelNbr': payload.get('ChannelNbr'), # 支付渠道 'PayTypCd': payload.get('PayTypCd'), # 支付类型 'OrderTitle': payload.get('OrderTitle'), # 订单标题 'CurrencyCd': payload.get('CurrencyCd'), # 币种 'MerUrl': payload.get('MerUrl'), # 异步通知Url 'OperatorId': payload.get('OperatorId'), # 操作员 'Remarks': payload.get('Remarks'), # 备注 'Remarks1': payload.get('Remarks1', ), # 备用字段 'Remarks2': payload.get('Remarks2'), # 备用字段 'MerSeqNbr': payload.get('MerSeqNbr'), # 支付订单号 'SubAppid': payload.get('SubAppid'), # 子商户公众账号ID } # logger.info('payload: {}'.format(payload)) public = self._check_empty(public) payload = self._check_empty(payload) signature = self.generate_signature(public, payload) send_data = public send_data['payload'] = json.dumps(payload, sort_keys = True, separators=(',', ':')) send_data['signature'] = signature send_data = self._connection_string(send_data) logger.info(json.dumps(headers, ensure_ascii=False)) logger.info(json.dumps(payload, ensure_ascii=False)) logger.info(send_data) try: response = requests.post(self.SericeCenterUrl, data=send_data, headers=headers) except: raise Exception('访问出错') if response.status_code != 200: raise Exception('返回数据有问题 code: {}'.format(response.status_code)) resp = response.json() logger.info(json.dumps(resp, ensure_ascii=False)) if resp.get('code') != '000000': raise Exception('code: {}, msg: {}'.format(resp.get('code'), json.dumps(resp, ensure_ascii=False))) payload = json.loads(response.json().get('payload')) result = self.verify_signature(resp, payload) if not result: raise Exception('签名校验错误') else: return resp def get_order_status(self, payload): public = self.public_part() headers = public.copy() headers['transCode'] = 'hnnx.trade.sandbox.upp.qryQrOrderStatus' if not payload.get('userAgent'): headers['User-Agent'] = self.DEFAULT.get('userAgent') else: headers['User-Agent'] = payload.get('userAgent') payload = { 'MerNbr': payload.get('MerNbr'), 'MerSeqNbr': payload.get('MerSeqNbr'), } public = self._check_empty(public) payload = self._check_empty(payload) signature = self.generate_signature(public, payload) send_data = public send_data['payload'] = json.dumps(payload, sort_keys=True, separators=(',', ':')) send_data['signature'] = signature send_data = self._connection_string(send_data) logger.info(json.dumps(headers, ensure_ascii=False)) logger.info(json.dumps(payload, ensure_ascii=False)) logger.info(send_data) try: response = requests.post(self.SericeCenterUrl, data=send_data, headers=headers) except: import traceback logger.info(traceback.format_exc()) raise Exception('访问出错') if response.status_code != 200: raise Exception('返回数据有问题') resp = response.json() logger.info(json.dumps(resp, ensure_ascii=False)) if resp.get('code') != '000000': raise Exception('code: {}, msg: {} data:{}'.format(resp.get('code'), resp.get('message'), resp)) payload = json.loads(response.json().get('payload')) result = self.verify_signature(resp, payload) if not result: raise Exception('签名校验错误') else: return resp def refund_order(self, payload): public = self.public_part() headers = public.copy() headers['transCode'] = 'hnnx.trade.sandbox.upp.returntrans' if not payload.get('userAgent'): headers['User-Agent'] = self.DEFAULT.get('userAgent') else: headers['User-Agent'] = payload.get('userAgent') payload = { 'MerNbr': payload.get('MerNbr'), 'MerSeqNbr': payload.get('MerSeqNbr'), 'MerTransDateTime': payload.get('MerTransDateTime'), 'OrigMerSeqNbr': payload.get('OrigMerSeqNbr'), 'OrigMerDate': payload.get('OrigMerDate'), 'OrigTransAmt': payload.get('OrigTransAmt'), 'SubTransAmt': payload.get('SubTransAmt'), 'OrigSubMerSeqNo': payload.get('OrigSubMerSeqNo'), 'OrigSubMerDate': payload.get('OrigSubMerDate'), 'SubMerchantId': payload.get('SubMerchantId'), 'SubMerSeqNo': payload.get('SubMerSeqNo'), 'SubMerDateTime': payload.get('SubMerDateTime'), 'OrderNbr': payload.get('OrderNbr'), } public = self._check_empty(public) payload = self._check_empty(payload) signature = self.generate_signature(public, payload) send_data = public send_data['payload'] = json.dumps(payload, sort_keys=True, separators=(',', ':')) send_data['signature'] = signature send_data = self._connection_string(send_data) logger.info(json.dumps(headers, ensure_ascii=False)) logger.info(json.dumps(payload, ensure_ascii=False)) logger.info(send_data) try: response = requests.post(self.SericeCenterUrl, data=send_data, headers=headers) except: raise Exception('访问出错') if response.status_code != 200: raise Exception('返回数据有问题') resp = response.json() logger.info(json.dumps(resp, ensure_ascii=False)) if resp.get('code') != '000000': raise Exception('code: {}, msg: {} data:{}'.format(resp.get('code'), resp.get('message'), resp)) payload = json.loads(response.json().get('payload')) result = self.verify_signature(resp, payload) if not result: raise Exception('签名校验错误') else: return resp def check(self, payload): signature = payload.pop('signature', None) signature = signature.decode('hex') result = sorted(payload.items(), key=lambda _: _[0]) plain = urlencode(result) logger.info(plain) try: if self._verify(signature=signature, plain=plain) == None: return True except: import traceback logger.info(traceback.format_exc()) return False # if __name__ == '__main__': # rcu = { # "companyName": "", # "appName": "测试2", # "remarks": "", # "appCode": "20200719001", # "MerNbr": "010020170801000001", # "SubMerchantId": "AA1810240024494", # "SubMerchantName": "test_pay", # "branchId": "1656199999", # "entoperId": "200000000000101342", # "channelId": "45", # "cert_path": "library/RuralCreditUnion/gaoan.pfx", # "cert_passwd": "1699999999", # "authCodeUrl": "https://wxbank.hnnx.com/mer/payOut/getAuthCode.do", # "SericeCenterUrl": "https://open.hnnx.com:8019/SericeCenter" # } # nxh = HeNanRuralCreditUnion(**rcu) # 公共部分 # public = { # 'appCode': '00000000003', # 'version': '1.0', # 'sequenceId': '45190904000000000031234567{}'.format(random.randint(100000, 999999)), # 前台流水号 # 'timestamp': '20190904220701873', # 'charset': 'UTF-8', # 'algorithm': 'SHA256withRSA', # 'deviceId': '123456', # 'channelId': '45', # 'Content-Type': 'application/x-www-form-urlencoded', # 'userId': None, # 'userName': None, # 'entoperId': None, # 'entoperName': None, # 'branchId': '1600299999', # 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36' # } # # TODO 预付费接口 # payload = { # 'MerNbr': '010020170801000001', # 'EncryptSubMerchantId': None, # 'SubMerchantId': 'AA1905050000819', # 'SubMerchantName': '测试商户', # 'MerTransDateTime': '20190904220701', # 'TransAmt': '0.01', # 'AuthCode': '0912bYZv3AMA8X229F3w3o78JL02bYZf', # 'ChannelNbr': '03', # 'PayTypCd': 'C', # 'OrderTitle': 'test_wechat_pay', # 'CurrencyCd': 'CNY', # 'MerUrl': 'https://develop.5tao5ai.com/user/authCode', # 'OperatorId': '200000000000101342', # 'Remarks': None, # 'Remarks1': None, # 'Remarks2': None, # 'MerSeqNbr': '20210929113354PQ00000005', # 'SubAppid': None # } # payload = { # "OrderTitle": "\\u81ea\\u52a9\\u670d\\u52a1-\\u5145\\u7535\\u6869/407614", # "EncryptSubMerchantId": None, # "Remarks1": None, # "OperatorId": "200000000000101342", # "Remarks2": None, # "PayTypCd": "B", # "MerNbr": "010020170801000001", # "SubMerchantId": "AA1905050000819", # "AuthCode": "0912bYZv3AMA8X229F3w3o78JL02bYZf", # "SubMerchantName": "\\u81ea\\u52a9\\u670d\\u52a1-\\u5145\\u7535\\u6869/407614", # "MerUrl": "https://develop.5tao5ai.com/user/wechat/finishedPay", # "TransAmt": "0.01", # "CurrencyCd": "CNY", # "MerTransDateTime": "20210929110955", # # "Remarks": "\\u81ea\\u52a9\\u670d\\u52a1-\\u5145\\u7535\\u6869/407614", # "SubAppid": None, # "MerSeqNbr": "20210929113354PQ0000000407614F2D", # "ChannelNbr": "03" # } # for _ in range(30): # res = nxh.pre_pay_order(payload) # print json.dumps(res, ensure_ascii=False) # print res.get('code') # # # result # result = { # "messagedigest": "", # "sequenceId": "45190904000000000031234567571034", # "code": "000000", # "algorithm": "SHA256withRSA", # "timestamp": "2021-09-24 17:47:09.019", # "charset": "UTF-8", # "serverIp": "", # "jnlId": "578400991112863744", # "message": "交易成功", # "payload": "{\"MerSeqNbr\":\"1234564545451212121512213\",\"AppId\":\"wx2421b1c4370ec43b\",\"InnerFundTransNbr\":\"000013802361\",\"TransSeqNbr\":\"202109240020305052\",\"Charset\":\"fb311c7377bb4ddfaf5b0f793c0bf1b9\",\"Sign\":\"dDXH8+ldwt61Z7Ly2VwSqMNRRnTO8i/1UcLzqw/3nYrC51Xgdy/SbpnacDo92U9pfBAO5lplHP+pAHQGLH6h61lYAqV8DNVLExO4gV5sGSgML5FL62pVx6HqTDxQVjvaMsRwaJb/SEGRGn4q4xqXFDydkEBXwaHpqaYPmuwtUQhWcvt/QKsUvz20u8nHMbuIcA3Vn78IkMEImrSUMPzD1lINCDWN7UTXYwXWs9JkslwNdzX7bI+EyUPPd2Cj5v91W9w3foiOQe354K6xMLIwjiQfDY7l9//JbJqmp4w1E0HopUOQEb6xy3vZUXZ4TCcj+6j/2jmYDey92Ueq4f869A==\",\"Subject\":\"prepay_id=wx2416474387379398553e0411ec39e20000\",\"TimeStamp\":\"1632473263\",\"SignType\":\"RSA\"}" # } # # # TODO 查询接口 # time.sleep(1) # payload = { # 'MerNbr': '010020170801000001', # 'MerSeqNbr': '20210930140930PQ87390576551893U9', # } # print json.dumps(nxh.get_order_status(payload), ensure_ascii=False) # # result = { # "messagedigest": "", # "sequenceId": "45190904000000000031234567584924", # "code": "000000", # "algorithm": "SHA256withRSA", # "timestamp": "2021-09-24 17:48:43.251", # "charset": "UTF-8", # "serverIp": "", # "jnlId": "578401388393144320", # "message": "交易成功", # "payload": "{\"InnerFundTransNbr\":\"000013802361\",\"TransSeqNbr\":\"202109240020305052\",\"TransStatus\":\"5\",\"PayTypCd\":\"C\"}" # } # # result = { # "messagedigest": "", # "sequenceId": "45190904000000000031234567540500", # "code": "000000", # "algorithm": "SHA256withRSA", # "timestamp": "2021-09-24 17:55:44.683", # "charset": "UTF-8", # "serverIp": "", # "jnlId": "578403156082565120", # "message": "交易成功", # "payload": "{\"InnerFundTransNbr\":\"000013802361\",\"TransSeqNbr\":\"202109240020305052\",\"TransStatus\":\"1\",\"PayTypCd\":\"C\"}" # } # # # TODO 退费接口 # payload = { # # "MerNbr": "010020170801000001", # 原支付交易一级商户号 # "SubMerchantId": "AA1905050000819", # 原支付交易二级商户号 # # "MerSeqNbr": "123456454545121212151221375", # 退货交易一级商户交易流水号(随机生成,不允许出现重复) # "SubMerSeqNo": "1234564545451212121512215", # 退货二级商户流水号(随机生成,不允许出现重复,可以和一级商户交易流水号保持一致) # # "OrigMerSeqNbr": "12345645454512121215122137", # 原支付交易一级商户号流水号 # "OrderNbr": "12345645454512121215122137", # 订单号 与二级商户流水号保持一致 # "OrigSubMerSeqNo": "12345645454512121215122137", # 原二级商户支付流水号(与原支付交易一级商户号流水号一致) # # "OrigTransAmt": "0.01", # 原支付交易总金额 # "SubTransAmt": "0.01", # 二级商户退货交易退货金额 # # "OrigMerDate": "20210928", # 原支付交易日期(例:20190810) # "OrigSubMerDate": "20210928", # 原二级商户支付交易日期(例:20190810) # # "MerTransDateTime": "20210928002030", # 商户时间戳 退货交易时间 # "SubMerDateTime": "20210928112217", # 二级商户退货交易时间 # # } # print json.dumps(nxh.refund_order(payload), ensure_ascii=False)