pay.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. from __future__ import unicode_literals
  4. import base64
  5. import datetime
  6. import hashlib
  7. import logging
  8. from binascii import hexlify, unhexlify
  9. from collections import OrderedDict
  10. import requests
  11. import simplejson as json
  12. import six
  13. from Crypto.Cipher import DES3
  14. from typing import Union, Dict, Optional
  15. from apilib.systypes import IterConstant
  16. from apilib.utils_url import add_query
  17. from library import to_binary, to_text
  18. from library.jd import JDErrorCode
  19. from library.jdbase.exceptions import JDNetworkException, JDException, JDSignError, JDValidationError, JDParameterError
  20. logger = logging.getLogger(__name__)
  21. __all__ = ('JDAggrePay')
  22. class PiType(IterConstant):
  23. WX = 'WX'
  24. ALIPAY = 'ALIPAY'
  25. JDPAY = 'JDPAY'
  26. UNIPAY = 'UNIPAY'
  27. JIOU = 'JIOU'
  28. class GatewayMethod(IterConstant):
  29. MINIPROGRAM = 'MINIPROGRAM'
  30. SUBSCRIPTION = 'SUBSCRIPTION'
  31. class TerminalType(IterConstant):
  32. PC = 'DT01'
  33. APP = 'DT02'
  34. BROWSER = 'DT03'
  35. POS = 'DT04'
  36. class JDAggrePay(object):
  37. PAY_HOST_DEV = 'http://testapipayx.jd.com'
  38. PAY_HOST = 'https://apipayx.jd.com'
  39. UN_SIGN_FIELDS = ['sign']
  40. UN_ENCRYPTE_FIELDS = ['systemId', 'sign']
  41. def __str__(self):
  42. _repr = '{kclass}(merchant_no: {merchant_no})'.format(
  43. kclass = self.__class__.__name__,
  44. merchant_no = self.merchant_no)
  45. if six.PY2:
  46. return to_binary(_repr)
  47. else:
  48. return to_text(_repr)
  49. def __repr__(self):
  50. return str(self)
  51. def __init__(self, merchant_no, desKey, saltMd5Key, systemId, debug = False):
  52. self.merchant_no = merchant_no
  53. self.desKey = desKey
  54. self.saltMd5Key = saltMd5Key
  55. self.systemId = systemId
  56. self.debug = debug
  57. if self.debug:
  58. self.host_url = JDAggrePay.PAY_HOST_DEV
  59. else:
  60. self.host_url = JDAggrePay.PAY_HOST
  61. def sign(self, raw, un_sign_fields):
  62. sign_raw = []
  63. for k in sorted(raw.keys()):
  64. if k in un_sign_fields or raw[k] is None or raw[k] == '':
  65. continue
  66. v = raw[k]
  67. if isinstance(v, basestring):
  68. sign_raw.append((k, v))
  69. elif isinstance(v, (dict, list)):
  70. sign_raw.append((k, json.dumps(v, sort_keys = True, separators = (',', ':'))))
  71. else:
  72. sign_raw.append((k, str(v)))
  73. s = u'&'.join('='.join(kv) for kv in sign_raw)
  74. s = u'{}{}'.format(s, self.saltMd5Key)
  75. m = hashlib.md5()
  76. m.update(s.encode('utf-8'))
  77. sign = m.hexdigest().lower()
  78. return sign
  79. def encrypt(self, raw, un_encrypt_fields):
  80. # type: (dict)->str
  81. params = {}
  82. for k, v in raw.iteritems():
  83. if k in un_encrypt_fields:
  84. continue
  85. if isinstance(v, (dict, list)):
  86. params[k] = json.dumps(v, sort_keys = True, separators = (',', ':'))
  87. else:
  88. params[k] = v
  89. plaintext = json.dumps(params, sort_keys = True, separators = (',', ':'))
  90. logger.debug('Request to JDAggre API decrypt payload: %s', str(plaintext))
  91. BS = DES3.block_size
  92. pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
  93. key = base64.b64decode(self.desKey)
  94. plaintext = pad(plaintext)
  95. cipher = DES3.new(key, DES3.MODE_ECB)
  96. encrypt_bytes = cipher.encrypt(plaintext.encode('utf-8'))
  97. return hexlify(encrypt_bytes)
  98. def decrypt(self, plaintext):
  99. # type: (str)->str
  100. unpad = lambda s: s[0:-ord(s[-1])]
  101. key = base64.b64decode(self.desKey)
  102. decrypted_text = unhexlify(plaintext)
  103. cipher = DES3.new(key, DES3.MODE_ECB)
  104. s = cipher.decrypt(decrypted_text)
  105. s = unpad(s)
  106. return s
  107. def check(self, data):
  108. sign = data.pop('sign')
  109. return sign == self.sign(data)
  110. def get(self, endpoint, **kwargs):
  111. return self._request(
  112. method = 'get',
  113. endpoint = endpoint,
  114. **kwargs
  115. )
  116. def post(self, endpoint, **kwargs):
  117. return self._request(
  118. method = 'post',
  119. endpoint = endpoint,
  120. **kwargs
  121. )
  122. def _decode_result(self, res):
  123. try:
  124. result = json.loads(res.content.decode('utf-8', 'ignore'), strict = False)
  125. except (TypeError, ValueError):
  126. return res
  127. return result
  128. def decrypt_response(self, payload):
  129. # 接口调用失败, SUCCESS/FAIL
  130. if not payload['success'] or ('errCode' in payload and payload['errCode'] != '000000'):
  131. raise JDException(
  132. errCode = payload['errCode'] if 'errCode' in payload else -1,
  133. errMsg = payload['errCodeDes'] if 'errCodeDes' in payload else u'未知错误',
  134. client = self,
  135. request = None,
  136. response = None)
  137. sign = payload['sign']
  138. decrypt_data = json.loads(self.decrypt(payload['cipherJson']))
  139. if sign != self.sign(decrypt_data, ['sign']):
  140. raise JDSignError(
  141. errCode = JDErrorCode.MY_ERROR_SIGNATURE,
  142. errMsg = u'签名不一致',
  143. client = self)
  144. if decrypt_data['merchantNo'] != self.merchant_no:
  145. raise JDValidationError(
  146. tips = u'商户号不一致',
  147. lvalue = self.merchant_no,
  148. rvalue = decrypt_data['merchantNo'],
  149. client = self)
  150. return decrypt_data
  151. def _handle_result(self, res, method = None, url = None,
  152. result_processor = None, **kwargs):
  153. if not isinstance(res, dict):
  154. result = self._decode_result(res)
  155. else:
  156. result = res
  157. logger.debug('Response from JDAggre API: %s', result)
  158. decrypt_data = self.decrypt_response(result)
  159. logger.debug('Response from JDAggre decrypt payload: %s', decrypt_data)
  160. if decrypt_data['resultCode'] != 'SUCCESS':
  161. raise JDException(
  162. errCode = decrypt_data['errCode'],
  163. errMsg = decrypt_data['errCodeDes'],
  164. client = self)
  165. return decrypt_data if not result_processor else result_processor(decrypt_data)
  166. def _request(self, method, endpoint, **kwargs):
  167. if endpoint.startswith('http://') or endpoint.startswith('https://'):
  168. url = endpoint
  169. else:
  170. url = '{base}{endpoint}'.format(
  171. base = self.host_url,
  172. endpoint = endpoint
  173. )
  174. headers = {'Content-Type': 'application/json;charset=utf-8'}
  175. if isinstance(kwargs.get('data', ''), dict):
  176. body = json.dumps(kwargs['data'], ensure_ascii = False)
  177. body = body.encode('utf-8')
  178. kwargs['data'] = body
  179. kwargs['timeout'] = kwargs.get('timeout', 15)
  180. result_processor = kwargs.pop('result_processor', None)
  181. logger.debug('Request to JDAggre API: %s %s\n%s', method, url, kwargs)
  182. with requests.sessions.Session() as session:
  183. res = session.request(
  184. url = url,
  185. method = method,
  186. headers = headers,
  187. **kwargs
  188. )
  189. try:
  190. res.raise_for_status()
  191. except requests.RequestException as reqe:
  192. raise JDNetworkException(
  193. errCode = 'HTTP{}'.format(res.status_code),
  194. errMsg = reqe.message,
  195. client = self,
  196. request = reqe.request,
  197. response = reqe.response)
  198. return self._handle_result(
  199. res, method, url, result_processor, **kwargs
  200. )
  201. def create_pay_url(self, out_trade_no, total_fee, notify_url, piType, tradeName = u'充值', expire = 300,
  202. returnParams = None, **kwargs):
  203. """
  204. 创建支付URL
  205. """
  206. url = 'https://payx.jd.com/getScanUrl'
  207. params = {
  208. 'version': 'V3.0',
  209. 'businessCode': 'AGGRE',
  210. 'merchantNo': str(self.merchant_no),
  211. 'outTradeNo': out_trade_no,
  212. 'amount': total_fee,
  213. 'successNotifyUrl': notify_url,
  214. 'expireTime': str(expire),
  215. 'businessType': '00',
  216. 'returnParams': returnParams,
  217. 'piType': piType,
  218. 'tradeName': tradeName
  219. }
  220. sign = self.sign(params, [])
  221. cipher_json = self.encrypt(params, [])
  222. data = {
  223. 'systemId': self.systemId,
  224. 'merchantNo': self.merchant_no,
  225. 'cipherJson': cipher_json,
  226. 'sign': sign
  227. }
  228. result = self.post(endpoint = url, data = data)
  229. return result['scanUrl']
  230. def create_pay_static_url(self, notify_url, tradeName = u'充值', expire = 300, returnParams = None, **kwargs):
  231. """
  232. 创建支付URL
  233. """
  234. url = 'https://payx.jd.com/getScanUrl'
  235. params = {
  236. 'version': 'V3.0',
  237. 'businessCode': 'AGGRE',
  238. 'merchantNo': str(self.merchant_no),
  239. 'expireTime': str(expire),
  240. 'businessType': '00',
  241. 'tradeName': tradeName
  242. }
  243. if notify_url:
  244. params.update({'successNotifyUrl': notify_url})
  245. if returnParams:
  246. params.update({'returnParams': returnParams})
  247. sign = self.sign(params, ['sign'])
  248. cipher_json = self.encrypt(params, ['systemId', 'sign'])
  249. data = {
  250. 'systemId': self.systemId,
  251. 'merchantNo': self.merchant_no,
  252. 'cipherJson': cipher_json,
  253. 'sign': sign
  254. }
  255. result = self.post(endpoint = url, data = data)
  256. return result['scanUrl']
  257. def unified_order(self, piType, out_trade_no, total_fee, notify_url, productName = u'充值',
  258. expire = 300, returnParams = None, client_ip = '127.0.0.1', openId = None,
  259. gatewayMethod = GatewayMethod.SUBSCRIPTION, **kwargs):
  260. # type: (str, str, str, str, basestring, int, Optional[Dict], str, Optional[str], str, Dict)->Union[str, Dict]
  261. """
  262. 统一下单
  263. """
  264. if piType in [PiType.ALIPAY, PiType.WX] and not openId:
  265. raise JDParameterError(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'缺少openid参数')
  266. params = {
  267. 'systemId': self.systemId,
  268. 'businessCode': 'AGGRE',
  269. 'deadlineTime': (datetime.datetime.now() + datetime.timedelta(seconds = expire)).strftime("%Y%m%d%H%M%S"),
  270. 'amount': total_fee,
  271. 'merchantNo': str(self.merchant_no),
  272. 'outTradeNo': out_trade_no,
  273. 'outTradeIp': client_ip,
  274. 'productName': productName,
  275. 'currency': 'RMB',
  276. 'version': 'V3.0',
  277. 'notifyUrl': notify_url,
  278. 'piType': piType,
  279. 'gatewayPayMethod': gatewayMethod,
  280. 'deviceInfo': {
  281. 'type': TerminalType.APP,
  282. 'ip': '192.168.0.1',
  283. 'imei': kwargs.get('imei')
  284. }
  285. }
  286. billSplitList = kwargs.get("billSplitList")
  287. if billSplitList:
  288. params["billSplitList"] = billSplitList
  289. if returnParams:
  290. params.update({
  291. 'returnParams': returnParams
  292. })
  293. if openId:
  294. params.update({
  295. 'openId': openId
  296. })
  297. sign = self.sign(params, ['systemId', 'sign'])
  298. cipher_json = self.encrypt(params, ['systemId', 'sign'])
  299. data = {
  300. 'systemId': self.systemId,
  301. 'merchantNo': self.merchant_no,
  302. 'cipherJson': cipher_json,
  303. 'sign': sign
  304. }
  305. result = self.post(endpoint = '/m/unifiedOrder', data = data)
  306. try:
  307. return json.loads(result['payInfo'])
  308. except Exception:
  309. return result['payInfo']
  310. def generate_query_openid_url(self, piType, callback_url, payload):
  311. assert callback_url is not None
  312. if piType not in [PiType.ALIPAY, PiType.WX]:
  313. raise JDParameterError(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'必须是支付宝或者微信')
  314. if payload:
  315. callback_url = add_query(callback_url, {'payload': payload})
  316. params = {
  317. 'version': 'V3.0',
  318. 'businessCode': 'AGGRE',
  319. 'merchantNo': self.merchant_no,
  320. 'successPageUrl': callback_url,
  321. 'piType': piType
  322. }
  323. sign = self.sign(params, ['systemId', 'sign'])
  324. cipher_json = self.encrypt(params, ['systemId', 'sign'])
  325. return '{host}/code/authorizeCode?merchantNo={merchantNo}&cipherJson={cipherJson}&sign={sign}' \
  326. '&systemId={systemId}'.format(host = 'http://payx.jd.com',
  327. merchantNo = self.merchant_no,
  328. cipherJson = cipher_json,
  329. sign = sign,
  330. systemId = self.systemId)
  331. def api_trade_query(self, out_trade_no = None, trade_no = None):
  332. assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time'
  333. params = {
  334. 'merchantNo': str(self.merchant_no),
  335. 'businessCode': 'AGGRE',
  336. 'version': 'V3.0',
  337. 'outTradeNo': str(out_trade_no)
  338. }
  339. if trade_no:
  340. params.update({'trandNo': trade_no})
  341. sign = self.sign(params, ['systemId', 'sign'])
  342. cipher_json = self.encrypt(params, ['systemId', 'sign'])
  343. data = {
  344. 'systemId': self.systemId,
  345. 'merchantNo': self.merchant_no,
  346. 'cipherJson': cipher_json,
  347. 'sign': sign
  348. }
  349. result = self.post(endpoint = '/m/querytrade', data = data)
  350. return result
  351. def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs):
  352. """
  353. :param outTradeNo:
  354. :param outRefundNo:
  355. :param amount:
  356. :return:
  357. """
  358. params = {
  359. "merchantNo": str(self.merchant_no),
  360. "businessCode": "AGGRE",
  361. "version": "V3.0",
  362. "outTradeNo": str(outTradeNo),
  363. "outRefundNo": str(outRefundNo),
  364. "amount": amount
  365. }
  366. billSplitList = kwargs.get("billSplitList")
  367. if billSplitList:
  368. params["billSplitList"] = billSplitList
  369. sign = self.sign(params, ['systemId', "sign"])
  370. cipher_json = self.encrypt(params, ["systemId", "sign"])
  371. data = {
  372. 'systemId': self.systemId,
  373. 'merchantNo': self.merchant_no,
  374. 'cipherJson': cipher_json,
  375. 'sign': sign
  376. }
  377. result = self.post(endpoint = '/m/refund', data = data)
  378. return result
  379. def api_refund_query(self, out_refund_no):
  380. params = {
  381. 'merchantNo': str(self.merchant_no),
  382. 'businessCode': 'AGGRE',
  383. 'version': 'V3.0',
  384. 'outRefundNo': str(out_refund_no)
  385. }
  386. sign = self.sign(params, ['systemId', 'sign'])
  387. cipher_json = self.encrypt(params, ['systemId', 'sign'])
  388. data = {
  389. 'systemId': self.systemId,
  390. 'merchantNo': self.merchant_no,
  391. 'cipherJson': cipher_json,
  392. 'sign': sign
  393. }
  394. result = self.post(endpoint = '/m/queryrefund', data = data)
  395. return result
  396. class JDJosPay(JDAggrePay):
  397. """
  398. JD JOS 支付的相关接口 加解密方式和JDAGGRE一直 但是部分字段实现不一致
  399. """
  400. def api_trade_query(self, out_trade_no=None, trade_no=None):
  401. shopInfo = getattr(self, "shopInfo", None)
  402. assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time'
  403. assert shopInfo, "shop info must be confided to josPayApp"
  404. businessData = OrderedDict([
  405. ("brandId", shopInfo.brandId),
  406. ("brandName", shopInfo.brandName),
  407. ("tradeName", shopInfo.tradeName),
  408. ("bizId", shopInfo.bizId)
  409. ])
  410. params = {
  411. 'merchantNo': str(self.merchant_no),
  412. 'businessCode': 'MEMBER',
  413. 'shopId': shopInfo.exStoreId,
  414. 'version': 'V3.0',
  415. 'businessData': json.dumps(businessData, separators=(",", ":")),
  416. 'outTradeNo': str(out_trade_no),
  417. }
  418. if trade_no:
  419. params.update({'trandNo': trade_no})
  420. sign = self.sign(params, ['systemId', 'sign'])
  421. cipher_json = self.encrypt(params, ['systemId', 'sign'])
  422. data = {
  423. 'systemId': self.systemId,
  424. 'merchantNo': self.merchant_no,
  425. 'cipherJson': cipher_json,
  426. 'sign': sign
  427. }
  428. result = self.post(endpoint='/m/querytrade', data=data)
  429. return result
  430. def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs):
  431. shopInfo = getattr(self, "shopInfo", None)
  432. assert outTradeNo and outRefundNo, 'outTradeNo and tradeNo must not be empty'
  433. assert shopInfo, "shop info must be confided to josPayApp"
  434. businessData = OrderedDict([
  435. ("brandId", shopInfo.brandId),
  436. ("brandName", shopInfo.brandName),
  437. ("tradeName", shopInfo.tradeName),
  438. ("bizId", shopInfo.bizId)
  439. ])
  440. params = {
  441. "merchantNo": str(self.merchant_no),
  442. "businessCode": "MEMBER",
  443. "version": "V3.0",
  444. "outTradeNo": str(outTradeNo),
  445. "outRefundNo": str(outRefundNo),
  446. "amount": amount,
  447. "operId": kwargs.get("operId") or "SYSTEM_AUTO",
  448. "shopId": shopInfo.exStoreId,
  449. 'businessData': json.dumps(businessData, separators=(",", ":")),
  450. }
  451. sign = self.sign(params, ['systemId', "sign"])
  452. cipher_json = self.encrypt(params, ["systemId", "sign"])
  453. data = {
  454. 'systemId': self.systemId,
  455. 'merchantNo': self.merchant_no,
  456. 'cipherJson': cipher_json,
  457. 'sign': sign
  458. }
  459. result = self.post(endpoint='/m/refund', data=data)
  460. return result