v3api.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. # -*- coding: utf-8 -*-
  2. #!/usr/bin/env python
  3. import base64
  4. import datetime
  5. import hashlib
  6. import json
  7. import logging
  8. import os
  9. import random
  10. import string
  11. import time
  12. import uuid
  13. from base64 import b64encode
  14. import requests
  15. from Crypto.Hash import SHA256
  16. from Crypto.PublicKey import RSA
  17. from Crypto.Signature import PKCS1_v1_5
  18. from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
  19. from cryptography.hazmat.primitives.hashes import SHA1
  20. from typing import TYPE_CHECKING
  21. from apilib.utils_url import add_query
  22. from apps.web.core.exceptions import MerchantError
  23. from apps.web.core.file import AliOssFileUploader
  24. from library.wechatpy.utils import load_certificate, aes_decrypt
  25. logger = logging.getLogger(__name__)
  26. if TYPE_CHECKING:
  27. from apps.web.core.models import WechatServiceProvider
  28. from apps.web.merchant.models import WechatApplyMerchant
  29. from requests import Response
  30. class WechatApiProxy(object):
  31. URL = "https://api.mch.weixin.qq.com"
  32. TOKEN_SCHEME = "WECHATPAY2-SHA256-RSA2048"
  33. def __init__(self, provider, retry=False): # type:(WechatServiceProvider, bool) -> None
  34. self._mchid = provider.mchid
  35. self._serialNo = provider.apiclient_serial_number
  36. self._sslKey = provider.sslKey
  37. self._provider = provider
  38. self._certificate = self._init_certificate()
  39. self._retry = retry
  40. self._retry_times = 0
  41. def _init_certificate(self):
  42. """ 尝试初始化平台证书 """
  43. if not self._provider.sslCertV3:
  44. return
  45. # 加载一次
  46. return load_certificate(self._provider.sslCertV3)
  47. @property
  48. def certificate(self):
  49. if not self._certificate:
  50. result = self._get_certificates()
  51. for value in result.get("data"):
  52. serial_no = value.get('serial_no')
  53. effective_time = value.get('effective_time')
  54. expire_time = value.get('expire_time')
  55. encrypt_certificate = value.get('encrypt_certificate')
  56. algorithm = nonce = associated_data = ciphertext = None
  57. if encrypt_certificate:
  58. algorithm = encrypt_certificate.get('algorithm')
  59. nonce = encrypt_certificate.get('nonce')
  60. associated_data = encrypt_certificate.get('associated_data')
  61. ciphertext = encrypt_certificate.get('ciphertext')
  62. if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
  63. continue
  64. cert_str = aes_decrypt(
  65. nonce=nonce,
  66. ciphertext=ciphertext,
  67. associated_data=associated_data,
  68. apiv3_key=self._provider.apiKeyV3
  69. )
  70. certificate = load_certificate(cert_str)
  71. if not certificate:
  72. continue
  73. now = datetime.datetime.utcnow()
  74. if now < certificate.not_valid_before or now > certificate.not_valid_after:
  75. continue
  76. self._certificate = certificate
  77. self._provider.update(sslCertV3=cert_str)
  78. break
  79. return self._certificate
  80. def _get_certificates(self):
  81. path = '/v3/certificates'
  82. return self.send_request(path)
  83. def _get_token(self, timeStamp, nonceStr, signature):
  84. return '{scheme} mchid="{michid}",nonce_str="{nonce_str}",signature="{signature}",timestamp="{timestamp}",serial_no="{serial_no}"'.format(
  85. scheme=self.TOKEN_SCHEME,
  86. michid=self._mchid,
  87. nonce_str=nonceStr,
  88. signature=signature,
  89. timestamp=timeStamp,
  90. serial_no=self._serialNo
  91. )
  92. def send_request(self, path, method="GET", data=None, files=None, signData=None, cipherData=False):
  93. """
  94. url: 全路径的url 如果GET请求带有查询参数 请在外部进行拼接
  95. method:请求方法
  96. body: 请求参数
  97. """
  98. logger.info("send request to wechat v3 api, url= {}, data={}".format(path, data))
  99. timeStamp = str(int(time.time()))
  100. nonceStr = ''.join(str(uuid.uuid4()).split('-')).upper()
  101. # 签名的body数据
  102. signData = signData or data or ""
  103. body = json.dumps(signData) if not isinstance(signData, str) else signData
  104. # 生成签名的原数据
  105. signStr = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonceStr, body)
  106. signer = PKCS1_v1_5.new(RSA.importKey(self._sslKey))
  107. # 签名需要 字节码
  108. signature = signer.sign(SHA256.new(signStr.encode('utf-8')))
  109. # 最后的签名是 字符串
  110. signature = base64.b64encode(signature).decode('utf8').replace('\n', '')
  111. # 获取验证的token
  112. authorization = self._get_token(timeStamp, nonceStr, signature)
  113. # 根据请求内容的不同创建headers
  114. headers = {'Accept': 'application/json', 'Authorization': authorization}
  115. if files:
  116. headers.update({'Content-Type': 'multipart/form-data'})
  117. else:
  118. headers.update({'Content-Type': 'application/json'})
  119. # 是否有加密信息
  120. if cipherData:
  121. # python2 的long 尾后是带有L 的,转换成HEX的时候这个标记依然存在
  122. headers.update({'Wechatpay-Serial': hex(self.certificate.serial_number)[2:].upper().rstrip("L")})
  123. logger.info("cipherData add header, header = {}".format(headers))
  124. # 发送请求
  125. response = requests.request(
  126. method,
  127. url=self.URL+path,
  128. headers=headers,
  129. json=None if files else data,
  130. data=data if files else None,
  131. files=files
  132. )
  133. return self.handle_result(response, path=path, method=method, data=data, files=files, signData=signData, cipherData=cipherData)
  134. def handle_result(self, res, **kwargs): # type: (Response, dict) -> dict
  135. """
  136. 处理请求结果
  137. """
  138. logger.info(
  139. "[{} handle_result] send request to wechat v3 api, "
  140. "url= {}, data={}, content = {}, status code = {}".format(
  141. self.__class__.__name__, kwargs["path"], kwargs["data"], res.content, res.status_code
  142. )
  143. )
  144. try:
  145. if res.status_code == 500:
  146. raise requests.HTTPError(u"system error!")
  147. except requests.RequestException:
  148. if self._retry and self._retry_times < 1:
  149. self._retry_times += 1
  150. return self.send_request(
  151. path=kwargs["path"], method=kwargs["method"],
  152. data=kwargs["data"], files=kwargs["files"],
  153. signData=kwargs["signData"], cipherData=kwargs["cipherData"]
  154. )
  155. else:
  156. # 连续两次遇到申请错误的情况 这种情况应该比较少见 等待再一次发送即可
  157. raise MerchantError(u"system error")
  158. if res.status_code == 204:
  159. return {}
  160. try:
  161. result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
  162. except (TypeError, ValueError):
  163. logger.debug('Can not decode response as JSON', exc_info=True)
  164. result = res.content
  165. return result
  166. def get_complaints(self, dateStart, dateEnd, pageIndex=0, pageSize=30):
  167. """
  168. 商户可通过调用此接口,查询指定时间段的所有用户投诉信息,以分页输出查询结果。
  169. 对于服务商、渠道商,可通过调用此接口,查询指定子商户号对应子商户的投诉信息。
  170. 若不指定则查询所有子商户投诉信息
  171. """
  172. data = {
  173. 'limit': pageSize,
  174. 'offset': pageIndex,
  175. 'begin_date': dateStart,
  176. 'end_date': dateEnd,
  177. }
  178. path = "/v3/merchant-service/complaints-v2"
  179. path = add_query(path, data)
  180. return self.send_request(path)
  181. def get_complaint(self, complaintId):
  182. """
  183. 商户可通过调用此接口,查询指定投诉单的用户投诉详情。
  184. 包含投诉内容、投诉关联订单、投诉人联系方式等信息,方便商户处理投诉
  185. """
  186. path = "/v3/merchant-service/complaints-v2/{}".format(complaintId)
  187. return self.send_request(path)
  188. def get_complaint_history(self, complaintId, pageIndex=0, pageSize=10):
  189. """
  190. 商户可通过调用此接口,查询指定投诉的用户商户协商历史,以分页输出查询结果。
  191. 方便商户根据处理历史来制定后续处理方案
  192. """
  193. data = {
  194. "limit": pageSize,
  195. "offset": pageIndex
  196. }
  197. path = "/v3/merchant-service/complaints-v2/{}/negotiation-historys".format(complaintId)
  198. path = add_query(path, data)
  199. return self.send_request(path)
  200. def response_complaint(self, mchid, complaintId, responseContent, responseImg=None, jumpUrl=None, urlText=None):
  201. """
  202. 商户可通过调用此接口,提交回复内容。
  203. 其中上传图片凭证需首先调用商户上传反馈图片接口,得到图片id,再将id填入请求。
  204. 回复可配置文字链,传入跳转链接文案和跳转链接字段,用户点击即可跳转对应页面
  205. """
  206. path = "/v3/merchant-service/complaints-v2/{}/response".format(complaintId)
  207. data = {
  208. "complainted_mchid": mchid,
  209. "response_content": responseContent,
  210. }
  211. if responseImg:
  212. data.update({"response_images": responseImg})
  213. if jumpUrl:
  214. data.update({"jump_url": jumpUrl})
  215. if urlText:
  216. data.update({"jump_url_text": urlText})
  217. return self.send_request(path, "POST", data=data)
  218. def complaint_complete(self, mchid, complaintId):
  219. """
  220. 商户可通过调用此接口,反馈投诉单已处理完成
  221. """
  222. path = "/v3/merchant-service/complaints-v2/{}/complete".format(complaintId)
  223. data = {"complainted_mchid": mchid}
  224. return self.send_request(path, "POST", data=data)
  225. def get_merchant_apply_state(self, subMchId):
  226. """
  227. 当服务商需要确认微信支付子商户号是否完成确认时,如果调用此接口提到“已授权”状态,则说明该商户号已完成开户意愿确认。
  228. """
  229. path = "/v3/apply4subject/applyment/merchants/{}/state".format(subMchId)
  230. return self.send_request(path)
  231. def get_merchant_audit(self, applyId=None, busCode=None):
  232. """
  233. 当服务商提交申请单后,需要定期调用此接口查询申请单的审核状态
  234. """
  235. path = "/v3/apply4subject/applyment"
  236. data= {}
  237. if applyId:
  238. data.update({"applyment_id": int(applyId)})
  239. else:
  240. data.update({"business_code": busCode})
  241. path = add_query(path, data)
  242. return self.send_request(path)
  243. def cancel_merchant_apply(self, applyId=None, busCode=None):
  244. if applyId:
  245. path = "/v3/apply4subject/applyment/{}/cancel".format(applyId)
  246. elif busCode:
  247. path = "/v3/apply4subject/applyment/{}/cancel".format(busCode)
  248. else:
  249. logger.error("wechat provider {} cancel merchant apply error, no applyId and no busCode".format(self._mchid))
  250. return
  251. return self.send_request(path, method="POST")
  252. def submit_merchant(self, applier): # type:(WechatApplyMerchant) -> dict
  253. path = "/v3/apply4subject/applyment"
  254. data = applier.to_apply(self)
  255. return self.send_request(path, method="POST", data=data, cipherData=True)
  256. def upload_image(self, content, fileName):
  257. """
  258. 部分微信支付业务指定商户需要使用图片上传 API来上报图片信息,从而获得必传参数的值:图片MediaID
  259. :param content: 文件二进制数据
  260. :param fileName: 文件名称
  261. 此接口由于上报 流媒体文件 独立于其他 request 接口请求
  262. """
  263. # 进入之后强转一次file的类型
  264. fileName = fileName if isinstance(fileName, str) else str(fileName)
  265. path = "/v3/merchant/media/upload"
  266. data = {
  267. 'meta': '{"filename":"%s", "sha256":"%s"}' % (fileName, hashlib.sha256(content).hexdigest())
  268. }
  269. mimes = {
  270. '.bmp': 'image/bmp',
  271. '.jpg': 'image/jpeg',
  272. '.jpeg': 'image/jpeg',
  273. '.png': 'image/png'
  274. }
  275. media_type = os.path.splitext(fileName)[-1]
  276. files = [('file', (fileName, content, mimes.get(media_type, "image/jpg")))]
  277. return self.send_request(path, "POST", data=data, files=files, signData=data["meta"])
  278. def upload_image_from_oss(self, url):
  279. content = AliOssFileUploader.load(url)
  280. return self.upload_image(content, os.path.basename(url))
  281. def rsa_encrypt(self, text):
  282. data = text.encode('UTF-8')
  283. public_key = self.certificate.public_key()
  284. cipher = public_key.encrypt(
  285. plaintext=data,
  286. padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None)
  287. )
  288. return b64encode(cipher).decode('UTF-8')
  289. def change_goldplan_status(self, sub_mchid, operation_type='OPEN'):
  290. """
  291. 服务商为特约商户开通或关闭点金计划
  292. https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter8_5_1.shtml
  293. :param sub_mchid:
  294. :param operation_type:
  295. :return:
  296. """
  297. path = '/v3/goldplan/merchants/changegoldplanstatus'
  298. data = {
  299. "sub_mchid": sub_mchid,
  300. "operation_type": operation_type
  301. }
  302. return self.send_request(path, method="POST", data=data)
  303. def change_custompage_status(self, sub_mchid, operation_type='OPEN'):
  304. """
  305. 服务商为特约商户开通或关闭点金计划
  306. https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter8_5_1.shtml
  307. :param sub_mchid:
  308. :param operation_type:
  309. :return:
  310. """
  311. path = '/v3/goldplan/merchants/changecustompagestatus'
  312. data = {
  313. "sub_mchid": sub_mchid,
  314. "operation_type": operation_type
  315. }
  316. return self.send_request(path, method="POST", data=data)
  317. def open_advertising_show(self, sub_mchid, advertising_industry_filters=None):
  318. """
  319. 此接口为特约商户的点金计划页面开通广告展示功能,可同时配置同业过滤标签,防止特约商户支付后出现同行业的广告内容。
  320. https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter8_5_4.shtml
  321. :param sub_mchid:
  322. :param advertising_industry_filters:
  323. :return:
  324. """
  325. if advertising_industry_filters is None:
  326. advertising_industry_filters = []
  327. path = '/v3/goldplan/merchants/open-advertising-show'
  328. data = {
  329. "sub_mchid": sub_mchid
  330. }
  331. if advertising_industry_filters:
  332. data.update({
  333. 'advertising_industry_filters': advertising_industry_filters
  334. })
  335. return self.send_request(path, method="PATCH", data=data)
  336. def get_provinces(self):
  337. return self.send_request("/v3/capital/capitallhh/areas/provinces")
  338. def get_cities(self, province_code):
  339. return self.send_request("/v3/capital/capitallhh/areas/provinces/{province_code}/cities".format(province_code=province_code))
  340. def get_personal_bank(self, offset=None, limit=None):
  341. data = {
  342. "offset": offset or 0,
  343. "limit": limit or 200
  344. }
  345. path = "/v3/capital/capitallhh/banks/personal-banking"
  346. add_query(path, data)
  347. return self.send_request(add_query(path, data))
  348. def get_banks_by_bank_account(self, account):
  349. data = {
  350. "account_number": self.rsa_encrypt(account)
  351. }
  352. path = "/v3/capital/capitallhh/banks/search-banks-by-bank-account"
  353. return self.send_request(add_query(path, data), cipherData=True)
  354. class WechatComplaint(object):
  355. def __init__(self, mchid, asn, slk):
  356. self.nonceStr = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
  357. self.timestamp = str(int(time.time()))
  358. self.mchid = mchid
  359. self.serialNo = asn
  360. self.sslKey = slk
  361. super(WechatComplaint, self).__init__()
  362. def __generate_sign(self, method, signUrl, data=None):
  363. if method in ['GET', 'DELETE']:
  364. method = method
  365. url = signUrl
  366. timestamp = self.timestamp
  367. nonce_str = self.nonceStr
  368. # 当请求方法为GET时, 报文主体为空
  369. body = ''
  370. else:
  371. method = method
  372. url = signUrl
  373. timestamp = self.timestamp
  374. nonce_str = self.nonceStr
  375. body = json.dumps(data)
  376. signStr = method + '\n' + url + '\n' + timestamp + '\n' + nonce_str + '\n' + body + '\n'
  377. # signer = PKCS1_v1_5.new(RSA.importKey(open(self.apiclientKeyPath).read()))
  378. signer = PKCS1_v1_5.new(RSA.importKey(self.sslKey))
  379. signature = signer.sign(SHA256.new(signStr.encode('utf-8')))
  380. sign = base64.b64encode(signature).decode('utf8').replace('\n', '')
  381. return sign
  382. def _authorization(self, mchid, serial_no, nonceStr, timestamp, signature):
  383. return 'WECHATPAY2-SHA256-RSA2048 mchid="{michid}",nonce_str="{nonce_str}",signature="{signature}",timestamp="{timestamp}",serial_no="{serial_no}"'.format(
  384. michid=mchid, nonce_str=nonceStr, signature=signature, timestamp=timestamp, serial_no=serial_no)
  385. # 查询投诉单列表, date = 'yy-mm-dd', 4月28有例子
  386. def query_complaint_list(self, dateStart, dateEnd, pageIndex=0, pageSize=30):
  387. def get_sign_url(data):
  388. signUrl = '/v3/merchant-service/complaints-v2'
  389. params = '?'
  390. for k, v in data.items():
  391. params += (str(k) + '=' + str(v) + '&')
  392. params = params[0:-1]
  393. return signUrl + params
  394. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2'
  395. data = {
  396. 'limit': pageSize,
  397. 'offset': pageIndex,
  398. 'begin_date': dateStart,
  399. 'end_date': dateEnd,
  400. }
  401. signUrl = get_sign_url(data)
  402. headers = {
  403. 'Content-Type': 'application/json',
  404. 'Accept': 'application/json',
  405. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  406. timestamp=self.timestamp,
  407. signature=self.__generate_sign(method='GET', signUrl=signUrl))
  408. }
  409. result = requests.get(url, headers=headers, params=data)
  410. resultData = result.json()['data']
  411. # for _ in resultData:
  412. # phoneNumber = _.get('payer_phone', '')
  413. # if phoneNumber == '':
  414. # continue
  415. # _['payer_phone'] = self._decode_data(phoneNumber)
  416. return resultData
  417. # 通过投诉单id查询投诉单详情
  418. def query_complaint_details_from_id(self, complaintId):
  419. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2' + '/' + complaintId
  420. signUrl = '/v3/merchant-service/complaints-v2/' + complaintId
  421. headers = {
  422. 'Content-Type': 'application/json',
  423. 'Accept': 'application/json',
  424. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  425. timestamp=self.timestamp,
  426. signature=self.__generate_sign(method='GET', signUrl=signUrl))
  427. }
  428. result = requests.get(url, headers=headers)
  429. return result.json()
  430. # 通过投诉单id查询协商历史
  431. def query_history_of_complainants(self, complaintId):
  432. def get_sign_url(complaintId, data):
  433. signUrl = '/v3/merchant-service/complaints-v2/{}/negotiation-historys'.format(complaintId)
  434. params = '?'
  435. for k, v in data.items():
  436. params += (str(k) + '=' + str(v) + '&')
  437. params = params[0:-1]
  438. return signUrl + params
  439. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/{}/negotiation-historys'.format(
  440. complaintId)
  441. data = {
  442. 'limit': 50,
  443. 'offset': 0,
  444. }
  445. signUrl = get_sign_url(complaintId, data)
  446. headers = {
  447. 'Content-Type': 'application/json',
  448. 'Accept': 'application/json',
  449. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  450. timestamp=self.timestamp,
  451. signature=self.__generate_sign(method='GET', signUrl=signUrl))
  452. }
  453. result = requests.get(url, headers=headers, params=data)
  454. return result.json()['data']
  455. # 创建投诉通知回调地址api
  456. def create_complaint_notification_callback_address(self, callbackUrl):
  457. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
  458. data = {
  459. 'url': callbackUrl
  460. }
  461. signUrl = '/v3/merchant-service/complaint-notifications'
  462. headers = {
  463. 'Content-Type': 'application/json',
  464. 'Accept': 'application/json',
  465. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  466. timestamp=self.timestamp,
  467. signature=self.__generate_sign(method='POST', signUrl=signUrl,
  468. data=data))
  469. }
  470. result = requests.post(url, headers=headers, data=json.dumps(data))
  471. # 返回值示例 {u'url': u'https://develop.5tao5ai.com/superadmin/modifyDeviceCode', u'mchid': u'1510834731'}
  472. return result.json()
  473. # 查询投诉通知回调地址api
  474. def get_complaint_notification_callback_address(self):
  475. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
  476. signUrl = '/v3/merchant-service/complaint-notifications'
  477. headers = {
  478. 'Content-Type': 'application/json',
  479. 'Accept': 'application/json',
  480. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  481. timestamp=self.timestamp,
  482. signature=self.__generate_sign(method='GET', signUrl=signUrl))
  483. }
  484. result = requests.get(url, headers=headers)
  485. return result.json()
  486. # 更新投诉通知回调地址api
  487. def update_complaint_notification_callback_address(self, callbackUrl):
  488. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
  489. data = {
  490. 'url': callbackUrl
  491. }
  492. signUrl = '/v3/merchant-service/complaint-notifications'
  493. headers = {
  494. 'Content-Type': 'application/json',
  495. 'Accept': 'application/json',
  496. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  497. timestamp=self.timestamp,
  498. signature=self.__generate_sign(method='PUT', signUrl=signUrl,
  499. data=data))
  500. }
  501. result = requests.put(url, headers=headers, data=json.dumps(data))
  502. # 返回值示例 {u'url': u'https://develop.5tao5ai.com/superadmin/modifyDeviceCode', u'mchid': u'1510834731'}
  503. return result.json()
  504. # 删除投诉通知回调地址api
  505. def delete_complaint_notification_callback_address(self):
  506. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
  507. signUrl = '/v3/merchant-service/complaint-notifications'
  508. headers = {
  509. 'Content-Type': 'application/json',
  510. 'Accept': 'application/json',
  511. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  512. timestamp=self.timestamp,
  513. signature=self.__generate_sign(method='DELETE', signUrl=signUrl))
  514. }
  515. result = requests.delete(url, headers=headers)
  516. # 返回值为204代表成功, int类型
  517. return result.status_code
  518. # 提交回复api
  519. def submit_complaint_reply(self, mchid, complaintId, responseContent):
  520. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/{}/response'.format(complaintId)
  521. data = {
  522. 'complainted_mchid': mchid,
  523. 'response_content': responseContent
  524. }
  525. signUrl = '/v3/merchant-service/complaints-v2/' + complaintId + '/response'
  526. headers = {
  527. 'Content-Type': 'application/json',
  528. 'Accept': 'application/json',
  529. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  530. timestamp=self.timestamp,
  531. signature=self.__generate_sign(method='POST', signUrl=signUrl,
  532. data=data))
  533. }
  534. result = requests.post(url, headers=headers, data=json.dumps(data))
  535. # 返回值为204代表成功, int类型
  536. return result.status_code
  537. # 反馈处理完成api
  538. def feedback_complaint_completed(self, mchid, complaintId):
  539. url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/{}/complete'.format(complaintId)
  540. data = {
  541. 'complainted_mchid': mchid
  542. }
  543. signUrl = '/v3/merchant-service/complaints-v2/' + complaintId + '/complete'
  544. headers = {
  545. 'Content-Type': 'application/json',
  546. 'Accept': 'application/json',
  547. 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
  548. timestamp=self.timestamp,
  549. signature=self.__generate_sign(method='POST', signUrl=signUrl,
  550. data=data))
  551. }
  552. result = requests.post(url, headers=headers, data=json.dumps(data))
  553. # 返回值为204代表成功, int类型
  554. return result.status_code