123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- # -*- 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
|