|
@@ -1,694 +0,0 @@
|
|
|
-# -*- coding: utf-8 -*-
|
|
|
-#!/usr/bin/env python
|
|
|
-
|
|
|
-import base64
|
|
|
-import datetime
|
|
|
-import hashlib
|
|
|
-import json
|
|
|
-import logging
|
|
|
-import os
|
|
|
-import random
|
|
|
-import string
|
|
|
-import time
|
|
|
-import uuid
|
|
|
-from base64 import b64encode
|
|
|
-
|
|
|
-import requests
|
|
|
-from Crypto.Hash import SHA256
|
|
|
-from Crypto.PublicKey import RSA
|
|
|
-from Crypto.Signature import PKCS1_v1_5
|
|
|
-from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
|
|
|
-from cryptography.hazmat.primitives.hashes import SHA1
|
|
|
-from typing import TYPE_CHECKING
|
|
|
-from apilib.utils_url import add_query
|
|
|
-from apps.web.core.exceptions import MerchantError
|
|
|
-from apps.web.core.file import AliOssFileUploader
|
|
|
-from library.wechatpy.utils import load_certificate, aes_decrypt
|
|
|
-
|
|
|
-logger = logging.getLogger(__name__)
|
|
|
-
|
|
|
-if TYPE_CHECKING:
|
|
|
- from apps.web.core.models import WechatServiceProvider
|
|
|
- from requests import Response
|
|
|
-
|
|
|
-
|
|
|
-class WechatApiProxy(object):
|
|
|
- URL = "https://api.mch.weixin.qq.com"
|
|
|
- TOKEN_SCHEME = "WECHATPAY2-SHA256-RSA2048"
|
|
|
-
|
|
|
- def __init__(self, provider, retry=False): # type:(WechatServiceProvider, bool) -> None
|
|
|
- self._mchid = provider.mchid
|
|
|
- self._serialNo = provider.apiclient_serial_number
|
|
|
- self._sslKey = provider.sslKey
|
|
|
- self._provider = provider
|
|
|
- self._certificate = self._init_certificate()
|
|
|
-
|
|
|
- self._retry = retry
|
|
|
- self._retry_times = 0
|
|
|
-
|
|
|
- def _init_certificate(self):
|
|
|
- """ 尝试初始化平台证书 """
|
|
|
- if not self._provider.sslCertV3:
|
|
|
- return
|
|
|
- # 加载一次
|
|
|
- return load_certificate(self._provider.sslCertV3)
|
|
|
-
|
|
|
- @property
|
|
|
- def certificate(self):
|
|
|
- if not self._certificate:
|
|
|
-
|
|
|
- result = self._get_certificates()
|
|
|
- for value in result.get("data"):
|
|
|
- serial_no = value.get('serial_no')
|
|
|
- effective_time = value.get('effective_time')
|
|
|
- expire_time = value.get('expire_time')
|
|
|
- encrypt_certificate = value.get('encrypt_certificate')
|
|
|
- algorithm = nonce = associated_data = ciphertext = None
|
|
|
-
|
|
|
- if encrypt_certificate:
|
|
|
- algorithm = encrypt_certificate.get('algorithm')
|
|
|
- nonce = encrypt_certificate.get('nonce')
|
|
|
- associated_data = encrypt_certificate.get('associated_data')
|
|
|
- ciphertext = encrypt_certificate.get('ciphertext')
|
|
|
-
|
|
|
- if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
|
|
|
- continue
|
|
|
-
|
|
|
- cert_str = aes_decrypt(
|
|
|
- nonce=nonce,
|
|
|
- ciphertext=ciphertext,
|
|
|
- associated_data=associated_data,
|
|
|
- apiv3_key=self._provider.apiKeyV3
|
|
|
- )
|
|
|
- certificate = load_certificate(cert_str)
|
|
|
- if not certificate:
|
|
|
- continue
|
|
|
-
|
|
|
- now = datetime.datetime.utcnow()
|
|
|
- if now < certificate.not_valid_before or now > certificate.not_valid_after:
|
|
|
- continue
|
|
|
-
|
|
|
- self._certificate = certificate
|
|
|
- self._provider.update(sslCertV3=cert_str)
|
|
|
- break
|
|
|
-
|
|
|
- return self._certificate
|
|
|
-
|
|
|
- def _get_certificates(self):
|
|
|
- path = '/v3/certificates'
|
|
|
- return self.send_request(path)
|
|
|
-
|
|
|
- def _get_token(self, timeStamp, nonceStr, signature):
|
|
|
- return '{scheme} mchid="{michid}",nonce_str="{nonce_str}",signature="{signature}",timestamp="{timestamp}",serial_no="{serial_no}"'.format(
|
|
|
- scheme=self.TOKEN_SCHEME,
|
|
|
- michid=self._mchid,
|
|
|
- nonce_str=nonceStr,
|
|
|
- signature=signature,
|
|
|
- timestamp=timeStamp,
|
|
|
- serial_no=self._serialNo
|
|
|
- )
|
|
|
-
|
|
|
- def send_request(self, path, method="GET", data=None, files=None, signData=None, cipherData=False):
|
|
|
- """
|
|
|
- url: 全路径的url 如果GET请求带有查询参数 请在外部进行拼接
|
|
|
- method:请求方法
|
|
|
- body: 请求参数
|
|
|
- """
|
|
|
- logger.info("send request to wechat v3 api, url= {}, data={}".format(path, data))
|
|
|
-
|
|
|
- timeStamp = str(int(time.time()))
|
|
|
- nonceStr = ''.join(str(uuid.uuid4()).split('-')).upper()
|
|
|
- # 签名的body数据
|
|
|
- signData = signData or data or ""
|
|
|
- body = json.dumps(signData) if not isinstance(signData, str) else signData
|
|
|
-
|
|
|
- # 生成签名的原数据
|
|
|
- signStr = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonceStr, body)
|
|
|
-
|
|
|
- signer = PKCS1_v1_5.new(RSA.importKey(self._sslKey))
|
|
|
- # 签名需要 字节码
|
|
|
- signature = signer.sign(SHA256.new(signStr.encode('utf-8')))
|
|
|
- # 最后的签名是 字符串
|
|
|
- signature = base64.b64encode(signature).decode('utf8').replace('\n', '')
|
|
|
- # 获取验证的token
|
|
|
- authorization = self._get_token(timeStamp, nonceStr, signature)
|
|
|
-
|
|
|
- # 根据请求内容的不同创建headers
|
|
|
- headers = {'Accept': 'application/json', 'Authorization': authorization}
|
|
|
- if files:
|
|
|
- headers.update({'Content-Type': 'multipart/form-data'})
|
|
|
- else:
|
|
|
- headers.update({'Content-Type': 'application/json'})
|
|
|
-
|
|
|
- # 是否有加密信息
|
|
|
- if cipherData:
|
|
|
- # python2 的long 尾后是带有L 的,转换成HEX的时候这个标记依然存在
|
|
|
- headers.update({'Wechatpay-Serial': hex(self.certificate.serial_number)[2:].upper().rstrip("L")})
|
|
|
- logger.info("cipherData add header, header = {}".format(headers))
|
|
|
-
|
|
|
- # 发送请求
|
|
|
- response = requests.request(
|
|
|
- method,
|
|
|
- url=self.URL+path,
|
|
|
- headers=headers,
|
|
|
- json=None if files else data,
|
|
|
- data=data if files else None,
|
|
|
- files=files
|
|
|
- )
|
|
|
-
|
|
|
- return self.handle_result(response, path=path, method=method, data=data, files=files, signData=signData, cipherData=cipherData)
|
|
|
-
|
|
|
- def handle_result(self, res, **kwargs): # type: (Response, dict) -> dict
|
|
|
- """
|
|
|
- 处理请求结果
|
|
|
- """
|
|
|
- logger.info(
|
|
|
- "[{} handle_result] send request to wechat v3 api, "
|
|
|
- "url= {}, data={}, content = {}, status code = {}".format(
|
|
|
- self.__class__.__name__, kwargs["path"], kwargs["data"], res.content, res.status_code
|
|
|
- )
|
|
|
- )
|
|
|
-
|
|
|
- try:
|
|
|
- if res.status_code == 500:
|
|
|
- raise requests.HTTPError(u"system error!")
|
|
|
- except requests.RequestException:
|
|
|
- if self._retry and self._retry_times < 1:
|
|
|
- self._retry_times += 1
|
|
|
- return self.send_request(
|
|
|
- path=kwargs["path"], method=kwargs["method"],
|
|
|
- data=kwargs["data"], files=kwargs["files"],
|
|
|
- signData=kwargs["signData"], cipherData=kwargs["cipherData"]
|
|
|
- )
|
|
|
- else:
|
|
|
- # 连续两次遇到申请错误的情况 这种情况应该比较少见 等待再一次发送即可
|
|
|
- raise MerchantError(u"system error")
|
|
|
-
|
|
|
- if res.status_code == 204:
|
|
|
- return {}
|
|
|
-
|
|
|
- try:
|
|
|
- result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
|
|
|
- except (TypeError, ValueError):
|
|
|
- logger.debug('Can not decode response as JSON', exc_info=True)
|
|
|
- result = res.content
|
|
|
-
|
|
|
- return result
|
|
|
-
|
|
|
- def get_complaints(self, dateStart, dateEnd, pageIndex=0, pageSize=30):
|
|
|
- """
|
|
|
- 商户可通过调用此接口,查询指定时间段的所有用户投诉信息,以分页输出查询结果。
|
|
|
- 对于服务商、渠道商,可通过调用此接口,查询指定子商户号对应子商户的投诉信息。
|
|
|
- 若不指定则查询所有子商户投诉信息
|
|
|
- """
|
|
|
- data = {
|
|
|
- 'limit': pageSize,
|
|
|
- 'offset': pageIndex,
|
|
|
- 'begin_date': dateStart,
|
|
|
- 'end_date': dateEnd,
|
|
|
- }
|
|
|
- path = "/v3/merchant-service/complaints-v2"
|
|
|
- path = add_query(path, data)
|
|
|
-
|
|
|
- return self.send_request(path)
|
|
|
-
|
|
|
- def get_complaint(self, complaintId):
|
|
|
- """
|
|
|
- 商户可通过调用此接口,查询指定投诉单的用户投诉详情。
|
|
|
- 包含投诉内容、投诉关联订单、投诉人联系方式等信息,方便商户处理投诉
|
|
|
- """
|
|
|
- path = "/v3/merchant-service/complaints-v2/{}".format(complaintId)
|
|
|
- return self.send_request(path)
|
|
|
-
|
|
|
- def get_complaint_history(self, complaintId, pageIndex=0, pageSize=10):
|
|
|
- """
|
|
|
- 商户可通过调用此接口,查询指定投诉的用户商户协商历史,以分页输出查询结果。
|
|
|
- 方便商户根据处理历史来制定后续处理方案
|
|
|
- """
|
|
|
- data = {
|
|
|
- "limit": pageSize,
|
|
|
- "offset": pageIndex
|
|
|
- }
|
|
|
- path = "/v3/merchant-service/complaints-v2/{}/negotiation-historys".format(complaintId)
|
|
|
- path = add_query(path, data)
|
|
|
-
|
|
|
- return self.send_request(path)
|
|
|
-
|
|
|
- def response_complaint(self, mchid, complaintId, responseContent, responseImg=None, jumpUrl=None, urlText=None):
|
|
|
- """
|
|
|
- 商户可通过调用此接口,提交回复内容。
|
|
|
- 其中上传图片凭证需首先调用商户上传反馈图片接口,得到图片id,再将id填入请求。
|
|
|
- 回复可配置文字链,传入跳转链接文案和跳转链接字段,用户点击即可跳转对应页面
|
|
|
- """
|
|
|
- path = "/v3/merchant-service/complaints-v2/{}/response".format(complaintId)
|
|
|
- data = {
|
|
|
- "complainted_mchid": mchid,
|
|
|
- "response_content": responseContent,
|
|
|
- }
|
|
|
- if responseImg:
|
|
|
- data.update({"response_images": responseImg})
|
|
|
- if jumpUrl:
|
|
|
- data.update({"jump_url": jumpUrl})
|
|
|
- if urlText:
|
|
|
- data.update({"jump_url_text": urlText})
|
|
|
-
|
|
|
- return self.send_request(path, "POST", data=data)
|
|
|
-
|
|
|
- def complaint_complete(self, mchid, complaintId):
|
|
|
- """
|
|
|
- 商户可通过调用此接口,反馈投诉单已处理完成
|
|
|
- """
|
|
|
- path = "/v3/merchant-service/complaints-v2/{}/complete".format(complaintId)
|
|
|
- data = {"complainted_mchid": mchid}
|
|
|
- return self.send_request(path, "POST", data=data)
|
|
|
-
|
|
|
- def get_merchant_apply_state(self, subMchId):
|
|
|
- """
|
|
|
- 当服务商需要确认微信支付子商户号是否完成确认时,如果调用此接口提到“已授权”状态,则说明该商户号已完成开户意愿确认。
|
|
|
- """
|
|
|
- path = "/v3/apply4subject/applyment/merchants/{}/state".format(subMchId)
|
|
|
-
|
|
|
- return self.send_request(path)
|
|
|
-
|
|
|
- def get_merchant_audit(self, applyId=None, busCode=None):
|
|
|
- """
|
|
|
- 当服务商提交申请单后,需要定期调用此接口查询申请单的审核状态
|
|
|
- APPLYMENT_STATE_WAITTING_FOR_AUDIT:【审核中】,请耐心等待3~7个工作日,微信支付将会完成审核。
|
|
|
- APPLYMENT_STATE_EDITTING:【编辑中】,可能提交申请发生了错误导致,可用同一个业务申请编号重新提交。
|
|
|
- APPLYMENT_STATE_WAITTING_FOR_CONFIRM_CONTACT:【待确认联系信息】,请扫描微信支付返回的二维码确认联系信息(此过程可修改超级管理员手机号)。
|
|
|
- APPLYMENT_STATE_WAITTING_FOR_CONFIRM_LEGALPERSON:【待账户验证】,请扫描微信支付返回的二维码在小程序端完成账户验证。
|
|
|
- APPLYMENT_STATE_PASSED:【审核通过】,请扫描微信支付返回的二维码在小程序端完成授权流程。
|
|
|
- APPLYMENT_STATE_REJECTED:【审核驳回】,请按照驳回原因修改申请资料,并更换业务申请编码,重新提交申请。
|
|
|
- APPLYMENT_STATE_FREEZED:【已冻结】,可能是该主体已完成过入驻,请查看驳回原因,并通知驳回原因中指定的联系人扫描微信支付返回的二维码在小程序端完成授权流程。
|
|
|
- 8、APPLYMENT_STATE_CANCELED:【已作废】,表示申请单已被撤销,无需再对其进行操作。
|
|
|
- """
|
|
|
- path = "/v3/apply4subject/applyment"
|
|
|
- data= {}
|
|
|
- if applyId:
|
|
|
- data.update({"applyment_id": int(applyId)})
|
|
|
- else:
|
|
|
- data.update({"business_code": busCode})
|
|
|
- path = add_query(path, data)
|
|
|
-
|
|
|
- return self.send_request(path)
|
|
|
-
|
|
|
- def cancel_merchant_apply(self, applyId=None, busCode=None):
|
|
|
- if applyId:
|
|
|
- path = "/v3/apply4subject/applyment/{}/cancel".format(applyId)
|
|
|
- elif busCode:
|
|
|
- path = "/v3/apply4subject/applyment/{}/cancel".format(busCode)
|
|
|
- else:
|
|
|
- logger.error("wechat provider {} cancel merchant apply error, no applyId and no busCode".format(self._mchid))
|
|
|
- return
|
|
|
-
|
|
|
- return self.send_request(path, method="POST")
|
|
|
-
|
|
|
- def upload_image(self, content, fileName):
|
|
|
- """
|
|
|
- 部分微信支付业务指定商户需要使用图片上传 API来上报图片信息,从而获得必传参数的值:图片MediaID
|
|
|
- :param content: 文件二进制数据
|
|
|
- :param fileName: 文件名称
|
|
|
- 此接口由于上报 流媒体文件 独立于其他 request 接口请求
|
|
|
- """
|
|
|
- # 进入之后强转一次file的类型
|
|
|
- fileName = fileName if isinstance(fileName, str) else str(fileName)
|
|
|
-
|
|
|
- path = "/v3/merchant/media/upload"
|
|
|
- data = {
|
|
|
- 'meta': '{"filename":"%s", "sha256":"%s"}' % (fileName, hashlib.sha256(content).hexdigest())
|
|
|
- }
|
|
|
-
|
|
|
- mimes = {
|
|
|
- '.bmp': 'image/bmp',
|
|
|
- '.jpg': 'image/jpeg',
|
|
|
- '.jpeg': 'image/jpeg',
|
|
|
- '.png': 'image/png'
|
|
|
- }
|
|
|
- media_type = os.path.splitext(fileName)[-1]
|
|
|
- files = [('file', (fileName, content, mimes.get(media_type, "image/jpg")))]
|
|
|
- return self.send_request(path, "POST", data=data, files=files, signData=data["meta"])
|
|
|
-
|
|
|
- def upload_image_from_oss(self, url):
|
|
|
- content = AliOssFileUploader.load(url)
|
|
|
- return self.upload_image(content, os.path.basename(url))
|
|
|
-
|
|
|
- def rsa_encrypt(self, text):
|
|
|
- data = text.encode('UTF-8')
|
|
|
- public_key = self.certificate.public_key()
|
|
|
- cipher = public_key.encrypt(
|
|
|
- plaintext=data,
|
|
|
- padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None)
|
|
|
- )
|
|
|
- return b64encode(cipher).decode('UTF-8')
|
|
|
-
|
|
|
- def change_goldplan_status(self, sub_mchid, operation_type='OPEN'):
|
|
|
- """
|
|
|
- 服务商为特约商户开通或关闭点金计划
|
|
|
- https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter8_5_1.shtml
|
|
|
- :param sub_mchid:
|
|
|
- :param operation_type:
|
|
|
- :return:
|
|
|
- """
|
|
|
-
|
|
|
- path = '/v3/goldplan/merchants/changegoldplanstatus'
|
|
|
-
|
|
|
- data = {
|
|
|
- "sub_mchid": sub_mchid,
|
|
|
- "operation_type": operation_type
|
|
|
- }
|
|
|
-
|
|
|
- return self.send_request(path, method="POST", data=data)
|
|
|
-
|
|
|
- def change_custompage_status(self, sub_mchid, operation_type='OPEN'):
|
|
|
- """
|
|
|
- 服务商为特约商户开通或关闭点金计划
|
|
|
- https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter8_5_1.shtml
|
|
|
- :param sub_mchid:
|
|
|
- :param operation_type:
|
|
|
- :return:
|
|
|
- """
|
|
|
-
|
|
|
- path = '/v3/goldplan/merchants/changecustompagestatus'
|
|
|
-
|
|
|
- data = {
|
|
|
- "sub_mchid": sub_mchid,
|
|
|
- "operation_type": operation_type
|
|
|
- }
|
|
|
-
|
|
|
- return self.send_request(path, method="POST", data=data)
|
|
|
-
|
|
|
- def open_advertising_show(self, sub_mchid, advertising_industry_filters=None):
|
|
|
- """
|
|
|
- 此接口为特约商户的点金计划页面开通广告展示功能,可同时配置同业过滤标签,防止特约商户支付后出现同行业的广告内容。
|
|
|
- https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter8_5_4.shtml
|
|
|
- :param sub_mchid:
|
|
|
- :param advertising_industry_filters:
|
|
|
- :return:
|
|
|
- """
|
|
|
-
|
|
|
- if advertising_industry_filters is None:
|
|
|
- advertising_industry_filters = []
|
|
|
- path = '/v3/goldplan/merchants/open-advertising-show'
|
|
|
-
|
|
|
- data = {
|
|
|
- "sub_mchid": sub_mchid
|
|
|
- }
|
|
|
-
|
|
|
- if advertising_industry_filters:
|
|
|
- data.update({
|
|
|
- 'advertising_industry_filters': advertising_industry_filters
|
|
|
- })
|
|
|
-
|
|
|
- return self.send_request(path, method="PATCH", data=data)
|
|
|
-
|
|
|
- def get_provinces(self):
|
|
|
- return self.send_request("/v3/capital/capitallhh/areas/provinces")
|
|
|
-
|
|
|
- def get_cities(self, province_code):
|
|
|
- return self.send_request("/v3/capital/capitallhh/areas/provinces/{province_code}/cities".format(province_code=province_code))
|
|
|
-
|
|
|
- def get_personal_bank(self, offset=None, limit=None):
|
|
|
- data = {
|
|
|
- "offset": offset or 0,
|
|
|
- "limit": limit or 200
|
|
|
- }
|
|
|
- path = "/v3/capital/capitallhh/banks/personal-banking"
|
|
|
- add_query(path, data)
|
|
|
-
|
|
|
- return self.send_request(add_query(path, data))
|
|
|
-
|
|
|
- def get_banks_by_bank_account(self, account):
|
|
|
- data = {
|
|
|
- "account_number": self.rsa_encrypt(account)
|
|
|
- }
|
|
|
- path = "/v3/capital/capitallhh/banks/search-banks-by-bank-account"
|
|
|
- return self.send_request(add_query(path, data), cipherData=True)
|
|
|
-
|
|
|
-
|
|
|
-class WechatComplaint(object):
|
|
|
- def __init__(self, mchid, asn, slk):
|
|
|
- self.nonceStr = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
|
|
|
- self.timestamp = str(int(time.time()))
|
|
|
- self.mchid = mchid
|
|
|
- self.serialNo = asn
|
|
|
- self.sslKey = slk
|
|
|
-
|
|
|
- super(WechatComplaint, self).__init__()
|
|
|
-
|
|
|
- def __generate_sign(self, method, signUrl, data=None):
|
|
|
- if method in ['GET', 'DELETE']:
|
|
|
- method = method
|
|
|
- url = signUrl
|
|
|
- timestamp = self.timestamp
|
|
|
- nonce_str = self.nonceStr
|
|
|
- # 当请求方法为GET时, 报文主体为空
|
|
|
- body = ''
|
|
|
-
|
|
|
- else:
|
|
|
- method = method
|
|
|
- url = signUrl
|
|
|
- timestamp = self.timestamp
|
|
|
- nonce_str = self.nonceStr
|
|
|
- body = json.dumps(data)
|
|
|
-
|
|
|
- signStr = method + '\n' + url + '\n' + timestamp + '\n' + nonce_str + '\n' + body + '\n'
|
|
|
- # signer = PKCS1_v1_5.new(RSA.importKey(open(self.apiclientKeyPath).read()))
|
|
|
- signer = PKCS1_v1_5.new(RSA.importKey(self.sslKey))
|
|
|
- signature = signer.sign(SHA256.new(signStr.encode('utf-8')))
|
|
|
- sign = base64.b64encode(signature).decode('utf8').replace('\n', '')
|
|
|
-
|
|
|
- return sign
|
|
|
-
|
|
|
- def _authorization(self, mchid, serial_no, nonceStr, timestamp, signature):
|
|
|
- return 'WECHATPAY2-SHA256-RSA2048 mchid="{michid}",nonce_str="{nonce_str}",signature="{signature}",timestamp="{timestamp}",serial_no="{serial_no}"'.format(
|
|
|
- michid=mchid, nonce_str=nonceStr, signature=signature, timestamp=timestamp, serial_no=serial_no)
|
|
|
-
|
|
|
- # 查询投诉单列表, date = 'yy-mm-dd', 4月28有例子
|
|
|
- def query_complaint_list(self, dateStart, dateEnd, pageIndex=0, pageSize=30):
|
|
|
- def get_sign_url(data):
|
|
|
- signUrl = '/v3/merchant-service/complaints-v2'
|
|
|
- params = '?'
|
|
|
-
|
|
|
- for k, v in data.items():
|
|
|
- params += (str(k) + '=' + str(v) + '&')
|
|
|
- params = params[0:-1]
|
|
|
-
|
|
|
- return signUrl + params
|
|
|
-
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2'
|
|
|
-
|
|
|
- data = {
|
|
|
- 'limit': pageSize,
|
|
|
- 'offset': pageIndex,
|
|
|
- 'begin_date': dateStart,
|
|
|
- 'end_date': dateEnd,
|
|
|
- }
|
|
|
-
|
|
|
- signUrl = get_sign_url(data)
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='GET', signUrl=signUrl))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.get(url, headers=headers, params=data)
|
|
|
-
|
|
|
- resultData = result.json()['data']
|
|
|
-
|
|
|
- # for _ in resultData:
|
|
|
- # phoneNumber = _.get('payer_phone', '')
|
|
|
- # if phoneNumber == '':
|
|
|
- # continue
|
|
|
- # _['payer_phone'] = self._decode_data(phoneNumber)
|
|
|
-
|
|
|
- return resultData
|
|
|
-
|
|
|
- # 通过投诉单id查询投诉单详情
|
|
|
- def query_complaint_details_from_id(self, complaintId):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2' + '/' + complaintId
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaints-v2/' + complaintId
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='GET', signUrl=signUrl))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.get(url, headers=headers)
|
|
|
-
|
|
|
- return result.json()
|
|
|
-
|
|
|
- # 通过投诉单id查询协商历史
|
|
|
- def query_history_of_complainants(self, complaintId):
|
|
|
- def get_sign_url(complaintId, data):
|
|
|
- signUrl = '/v3/merchant-service/complaints-v2/{}/negotiation-historys'.format(complaintId)
|
|
|
- params = '?'
|
|
|
-
|
|
|
- for k, v in data.items():
|
|
|
- params += (str(k) + '=' + str(v) + '&')
|
|
|
- params = params[0:-1]
|
|
|
-
|
|
|
- return signUrl + params
|
|
|
-
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/{}/negotiation-historys'.format(
|
|
|
- complaintId)
|
|
|
-
|
|
|
- data = {
|
|
|
- 'limit': 50,
|
|
|
- 'offset': 0,
|
|
|
- }
|
|
|
-
|
|
|
- signUrl = get_sign_url(complaintId, data)
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='GET', signUrl=signUrl))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.get(url, headers=headers, params=data)
|
|
|
-
|
|
|
- return result.json()['data']
|
|
|
-
|
|
|
- # 创建投诉通知回调地址api
|
|
|
- def create_complaint_notification_callback_address(self, callbackUrl):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- data = {
|
|
|
- 'url': callbackUrl
|
|
|
- }
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='POST', signUrl=signUrl,
|
|
|
- data=data))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.post(url, headers=headers, data=json.dumps(data))
|
|
|
-
|
|
|
- # 返回值示例 {u'url': u'https://develop.5tao5ai.com/superadmin/modifyDeviceCode', u'mchid': u'1510834731'}
|
|
|
- return result.json()
|
|
|
-
|
|
|
- # 查询投诉通知回调地址api
|
|
|
- def get_complaint_notification_callback_address(self):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='GET', signUrl=signUrl))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.get(url, headers=headers)
|
|
|
-
|
|
|
- return result.json()
|
|
|
-
|
|
|
- # 更新投诉通知回调地址api
|
|
|
- def update_complaint_notification_callback_address(self, callbackUrl):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- data = {
|
|
|
- 'url': callbackUrl
|
|
|
- }
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='PUT', signUrl=signUrl,
|
|
|
- data=data))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.put(url, headers=headers, data=json.dumps(data))
|
|
|
-
|
|
|
- # 返回值示例 {u'url': u'https://develop.5tao5ai.com/superadmin/modifyDeviceCode', u'mchid': u'1510834731'}
|
|
|
- return result.json()
|
|
|
-
|
|
|
- # 删除投诉通知回调地址api
|
|
|
- def delete_complaint_notification_callback_address(self):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaint-notifications'
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='DELETE', signUrl=signUrl))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.delete(url, headers=headers)
|
|
|
-
|
|
|
- # 返回值为204代表成功, int类型
|
|
|
- return result.status_code
|
|
|
-
|
|
|
- # 提交回复api
|
|
|
- def submit_complaint_reply(self, mchid, complaintId, responseContent):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/{}/response'.format(complaintId)
|
|
|
-
|
|
|
- data = {
|
|
|
- 'complainted_mchid': mchid,
|
|
|
- 'response_content': responseContent
|
|
|
- }
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaints-v2/' + complaintId + '/response'
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='POST', signUrl=signUrl,
|
|
|
- data=data))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.post(url, headers=headers, data=json.dumps(data))
|
|
|
-
|
|
|
- # 返回值为204代表成功, int类型
|
|
|
- return result.status_code
|
|
|
-
|
|
|
- # 反馈处理完成api
|
|
|
- def feedback_complaint_completed(self, mchid, complaintId):
|
|
|
- url = 'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/{}/complete'.format(complaintId)
|
|
|
-
|
|
|
- data = {
|
|
|
- 'complainted_mchid': mchid
|
|
|
- }
|
|
|
-
|
|
|
- signUrl = '/v3/merchant-service/complaints-v2/' + complaintId + '/complete'
|
|
|
-
|
|
|
- headers = {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Authorization': self._authorization(mchid=self.mchid, serial_no=self.serialNo, nonceStr=self.nonceStr,
|
|
|
- timestamp=self.timestamp,
|
|
|
- signature=self.__generate_sign(method='POST', signUrl=signUrl,
|
|
|
- data=data))
|
|
|
- }
|
|
|
-
|
|
|
- result = requests.post(url, headers=headers, data=json.dumps(data))
|
|
|
-
|
|
|
- # 返回值为204代表成功, int类型
|
|
|
- return result.status_code
|
|
|
-
|