# -*- coding: utf-8 -*- # !/usr/bin/env python from __future__ import unicode_literals import base64 import datetime import hashlib import logging from binascii import hexlify, unhexlify from collections import OrderedDict import requests import simplejson as json import six from Crypto.Cipher import DES3 from typing import Union, Dict, Optional from apilib.systypes import IterConstant from apilib.utils_url import add_query from library import to_binary, to_text from library.jd import JDErrorCode from library.jdbase.exceptions import JDNetworkException, JDException, JDSignError, JDValidationError, JDParameterError logger = logging.getLogger(__name__) __all__ = ('JDAggrePay') class PiType(IterConstant): WX = 'WX' ALIPAY = 'ALIPAY' JDPAY = 'JDPAY' UNIPAY = 'UNIPAY' JIOU = 'JIOU' class GatewayMethod(IterConstant): MINIPROGRAM = 'MINIPROGRAM' SUBSCRIPTION = 'SUBSCRIPTION' class TerminalType(IterConstant): PC = 'DT01' APP = 'DT02' BROWSER = 'DT03' POS = 'DT04' class JDAggrePay(object): PAY_HOST_DEV = 'http://testapipayx.jd.com' PAY_HOST = 'https://apipayx.jd.com' UN_SIGN_FIELDS = ['sign'] UN_ENCRYPTE_FIELDS = ['systemId', 'sign'] def __str__(self): _repr = '{kclass}(merchant_no: {merchant_no})'.format( kclass = self.__class__.__name__, merchant_no = self.merchant_no) if six.PY2: return to_binary(_repr) else: return to_text(_repr) def __repr__(self): return str(self) def __init__(self, merchant_no, desKey, saltMd5Key, systemId, debug = False): self.merchant_no = merchant_no self.desKey = desKey self.saltMd5Key = saltMd5Key self.systemId = systemId self.debug = debug if self.debug: self.host_url = JDAggrePay.PAY_HOST_DEV else: self.host_url = JDAggrePay.PAY_HOST def sign(self, raw, un_sign_fields): sign_raw = [] for k in sorted(raw.keys()): if k in un_sign_fields or raw[k] is None or raw[k] == '': continue v = raw[k] if isinstance(v, basestring): sign_raw.append((k, v)) elif isinstance(v, (dict, list)): sign_raw.append((k, json.dumps(v, sort_keys = True, separators = (',', ':')))) else: sign_raw.append((k, str(v))) s = u'&'.join('='.join(kv) for kv in sign_raw) s = u'{}{}'.format(s, self.saltMd5Key) m = hashlib.md5() m.update(s.encode('utf-8')) sign = m.hexdigest().lower() return sign def encrypt(self, raw, un_encrypt_fields): # type: (dict)->str params = {} for k, v in raw.iteritems(): if k in un_encrypt_fields: continue if isinstance(v, (dict, list)): params[k] = json.dumps(v, sort_keys = True, separators = (',', ':')) else: params[k] = v plaintext = json.dumps(params, sort_keys = True, separators = (',', ':')) logger.debug('Request to JDAggre API decrypt payload: %s', str(plaintext)) BS = DES3.block_size pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) key = base64.b64decode(self.desKey) plaintext = pad(plaintext) cipher = DES3.new(key, DES3.MODE_ECB) encrypt_bytes = cipher.encrypt(plaintext.encode('utf-8')) return hexlify(encrypt_bytes) def decrypt(self, plaintext): # type: (str)->str unpad = lambda s: s[0:-ord(s[-1])] key = base64.b64decode(self.desKey) decrypted_text = unhexlify(plaintext) cipher = DES3.new(key, DES3.MODE_ECB) s = cipher.decrypt(decrypted_text) s = unpad(s) return s def check(self, data): sign = data.pop('sign') return sign == self.sign(data) def get(self, endpoint, **kwargs): return self._request( method = 'get', endpoint = endpoint, **kwargs ) def post(self, endpoint, **kwargs): return self._request( method = 'post', endpoint = endpoint, **kwargs ) def _decode_result(self, res): try: result = json.loads(res.content.decode('utf-8', 'ignore'), strict = False) except (TypeError, ValueError): return res return result def decrypt_response(self, payload): # 接口调用失败, SUCCESS/FAIL if not payload['success'] or ('errCode' in payload and payload['errCode'] != '000000'): raise JDException( errCode = payload['errCode'] if 'errCode' in payload else -1, errMsg = payload['errCodeDes'] if 'errCodeDes' in payload else u'未知错误', client = self, request = None, response = None) sign = payload['sign'] decrypt_data = json.loads(self.decrypt(payload['cipherJson'])) if sign != self.sign(decrypt_data, ['sign']): raise JDSignError( errCode = JDErrorCode.MY_ERROR_SIGNATURE, errMsg = u'签名不一致', client = self) if decrypt_data['merchantNo'] != self.merchant_no: raise JDValidationError( tips = u'商户号不一致', lvalue = self.merchant_no, rvalue = decrypt_data['merchantNo'], client = self) return decrypt_data def _handle_result(self, res, method = None, url = None, result_processor = None, **kwargs): if not isinstance(res, dict): result = self._decode_result(res) else: result = res logger.debug('Response from JDAggre API: %s', result) decrypt_data = self.decrypt_response(result) logger.debug('Response from JDAggre decrypt payload: %s', decrypt_data) if decrypt_data['resultCode'] != 'SUCCESS': raise JDException( errCode = decrypt_data['errCode'], errMsg = decrypt_data['errCodeDes'], client = self) return decrypt_data if not result_processor else result_processor(decrypt_data) def _request(self, method, endpoint, **kwargs): if endpoint.startswith('http://') or endpoint.startswith('https://'): url = endpoint else: url = '{base}{endpoint}'.format( base = self.host_url, endpoint = endpoint ) headers = {'Content-Type': 'application/json;charset=utf-8'} if isinstance(kwargs.get('data', ''), dict): body = json.dumps(kwargs['data'], ensure_ascii = False) body = body.encode('utf-8') kwargs['data'] = body kwargs['timeout'] = kwargs.get('timeout', 15) result_processor = kwargs.pop('result_processor', None) logger.debug('Request to JDAggre API: %s %s\n%s', method, url, kwargs) with requests.sessions.Session() as session: res = session.request( url = url, method = method, headers = headers, **kwargs ) try: res.raise_for_status() except requests.RequestException as reqe: raise JDNetworkException( errCode = 'HTTP{}'.format(res.status_code), errMsg = reqe.message, client = self, request = reqe.request, response = reqe.response) return self._handle_result( res, method, url, result_processor, **kwargs ) def create_pay_url(self, out_trade_no, total_fee, notify_url, piType, tradeName = u'充值', expire = 300, returnParams = None, **kwargs): """ 创建支付URL """ url = 'https://payx.jd.com/getScanUrl' params = { 'version': 'V3.0', 'businessCode': 'AGGRE', 'merchantNo': str(self.merchant_no), 'outTradeNo': out_trade_no, 'amount': total_fee, 'successNotifyUrl': notify_url, 'expireTime': str(expire), 'businessType': '00', 'returnParams': returnParams, 'piType': piType, 'tradeName': tradeName } sign = self.sign(params, []) cipher_json = self.encrypt(params, []) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint = url, data = data) return result['scanUrl'] def create_pay_static_url(self, notify_url, tradeName = u'充值', expire = 300, returnParams = None, **kwargs): """ 创建支付URL """ url = 'https://payx.jd.com/getScanUrl' params = { 'version': 'V3.0', 'businessCode': 'AGGRE', 'merchantNo': str(self.merchant_no), 'expireTime': str(expire), 'businessType': '00', 'tradeName': tradeName } if notify_url: params.update({'successNotifyUrl': notify_url}) if returnParams: params.update({'returnParams': returnParams}) sign = self.sign(params, ['sign']) cipher_json = self.encrypt(params, ['systemId', 'sign']) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint = url, data = data) return result['scanUrl'] def unified_order(self, piType, out_trade_no, total_fee, notify_url, productName = u'充值', expire = 300, returnParams = None, client_ip = '127.0.0.1', openId = None, gatewayMethod = GatewayMethod.SUBSCRIPTION, **kwargs): # type: (str, str, str, str, basestring, int, Optional[Dict], str, Optional[str], str, Dict)->Union[str, Dict] """ 统一下单 """ if piType in [PiType.ALIPAY, PiType.WX] and not openId: raise JDParameterError(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'缺少openid参数') params = { 'systemId': self.systemId, 'businessCode': 'AGGRE', 'deadlineTime': (datetime.datetime.now() + datetime.timedelta(seconds = expire)).strftime("%Y%m%d%H%M%S"), 'amount': total_fee, 'merchantNo': str(self.merchant_no), 'outTradeNo': out_trade_no, 'outTradeIp': client_ip, 'productName': productName, 'currency': 'RMB', 'version': 'V3.0', 'notifyUrl': notify_url, 'piType': piType, 'gatewayPayMethod': gatewayMethod, 'deviceInfo': { 'type': TerminalType.APP, 'ip': '192.168.0.1', 'imei': kwargs.get('imei') } } billSplitList = kwargs.get("billSplitList") if billSplitList: params["billSplitList"] = billSplitList if returnParams: params.update({ 'returnParams': returnParams }) if openId: params.update({ 'openId': openId }) sign = self.sign(params, ['systemId', 'sign']) cipher_json = self.encrypt(params, ['systemId', 'sign']) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint = '/m/unifiedOrder', data = data) try: return json.loads(result['payInfo']) except Exception: return result['payInfo'] def generate_query_openid_url(self, piType, callback_url, payload): assert callback_url is not None if piType not in [PiType.ALIPAY, PiType.WX]: raise JDParameterError(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'必须是支付宝或者微信') if payload: callback_url = add_query(callback_url, {'payload': payload}) params = { 'version': 'V3.0', 'businessCode': 'AGGRE', 'merchantNo': self.merchant_no, 'successPageUrl': callback_url, 'piType': piType } sign = self.sign(params, ['systemId', 'sign']) cipher_json = self.encrypt(params, ['systemId', 'sign']) return '{host}/code/authorizeCode?merchantNo={merchantNo}&cipherJson={cipherJson}&sign={sign}' \ '&systemId={systemId}'.format(host = 'http://payx.jd.com', merchantNo = self.merchant_no, cipherJson = cipher_json, sign = sign, systemId = self.systemId) def api_trade_query(self, out_trade_no = None, trade_no = None): assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time' params = { 'merchantNo': str(self.merchant_no), 'businessCode': 'AGGRE', 'version': 'V3.0', 'outTradeNo': str(out_trade_no) } if trade_no: params.update({'trandNo': trade_no}) sign = self.sign(params, ['systemId', 'sign']) cipher_json = self.encrypt(params, ['systemId', 'sign']) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint = '/m/querytrade', data = data) return result def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs): """ :param outTradeNo: :param outRefundNo: :param amount: :return: """ params = { "merchantNo": str(self.merchant_no), "businessCode": "AGGRE", "version": "V3.0", "outTradeNo": str(outTradeNo), "outRefundNo": str(outRefundNo), "amount": amount } billSplitList = kwargs.get("billSplitList") if billSplitList: params["billSplitList"] = billSplitList sign = self.sign(params, ['systemId', "sign"]) cipher_json = self.encrypt(params, ["systemId", "sign"]) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint = '/m/refund', data = data) return result def api_refund_query(self, out_refund_no): params = { 'merchantNo': str(self.merchant_no), 'businessCode': 'AGGRE', 'version': 'V3.0', 'outRefundNo': str(out_refund_no) } sign = self.sign(params, ['systemId', 'sign']) cipher_json = self.encrypt(params, ['systemId', 'sign']) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint = '/m/queryrefund', data = data) return result class JDJosPay(JDAggrePay): """ JD JOS 支付的相关接口 加解密方式和JDAGGRE一直 但是部分字段实现不一致 """ def api_trade_query(self, out_trade_no=None, trade_no=None): shopInfo = getattr(self, "shopInfo", None) assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time' assert shopInfo, "shop info must be confided to josPayApp" businessData = OrderedDict([ ("brandId", shopInfo.brandId), ("brandName", shopInfo.brandName), ("tradeName", shopInfo.tradeName), ("bizId", shopInfo.bizId) ]) params = { 'merchantNo': str(self.merchant_no), 'businessCode': 'MEMBER', 'shopId': shopInfo.exStoreId, 'version': 'V3.0', 'businessData': json.dumps(businessData, separators=(",", ":")), 'outTradeNo': str(out_trade_no), } if trade_no: params.update({'trandNo': trade_no}) sign = self.sign(params, ['systemId', 'sign']) cipher_json = self.encrypt(params, ['systemId', 'sign']) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint='/m/querytrade', data=data) return result def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs): shopInfo = getattr(self, "shopInfo", None) assert outTradeNo and outRefundNo, 'outTradeNo and tradeNo must not be empty' assert shopInfo, "shop info must be confided to josPayApp" businessData = OrderedDict([ ("brandId", shopInfo.brandId), ("brandName", shopInfo.brandName), ("tradeName", shopInfo.tradeName), ("bizId", shopInfo.bizId) ]) params = { "merchantNo": str(self.merchant_no), "businessCode": "MEMBER", "version": "V3.0", "outTradeNo": str(outTradeNo), "outRefundNo": str(outRefundNo), "amount": amount, "operId": kwargs.get("operId") or "SYSTEM_AUTO", "shopId": shopInfo.exStoreId, 'businessData': json.dumps(businessData, separators=(",", ":")), } sign = self.sign(params, ['systemId', "sign"]) cipher_json = self.encrypt(params, ["systemId", "sign"]) data = { 'systemId': self.systemId, 'merchantNo': self.merchant_no, 'cipherJson': cipher_json, 'sign': sign } result = self.post(endpoint='/m/refund', data=data) return result