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