mopybird 2 gadi atpakaļ
vecāks
revīzija
1c142b474f

+ 1 - 3
apps/web/core/bridge/__init__.py

@@ -1,4 +1,2 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-#!/usr/bin/env python
-
-from apps.web.core.bridge.wechat.WechatClientProxy import WechatClientProxy
+# !/usr/bin/env python

+ 7 - 1
apps/web/core/bridge/wechat/__init__.py

@@ -1,2 +1,8 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-#!/usr/bin/env python
+# !/usr/bin/env python
+
+from .WechatClientProxy import WechatClientProxy
+from .WechatClientProxy import MyWeChatComponent
+from .WechatClientProxy import MyWeChatComponentClient
+from .WechatClientProxy import MyWeChatClient
+from .WechatClientV3 import MyWechatClientV3

+ 0 - 694
apps/web/core/bridge/wechat/v3api.py

@@ -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
-

+ 121 - 99
apps/web/core/models.py

@@ -158,7 +158,7 @@ class WechatAuthApp(EmbeddedApp):
 
 
 
 
 class WechatManagerApp(EmbeddedApp):
 class WechatManagerApp(EmbeddedApp):
-    templateIdMap = DictField(verbose_name = "微信推送消息模版ID字典", default = {})
+    templateIdMap = DictField(verbose_name = u"微信推送消息模版ID字典", default = {})
 
 
     MESSAGE_TEMPLATE = {
     MESSAGE_TEMPLATE = {
         'feedback': {
         'feedback': {
@@ -482,6 +482,56 @@ class WechatUserManagerApp(EmbeddedApp):
                 }
                 }
             })
             })
         },
         },
+        'charge_order_complete': {
+            'templateId': '',
+            'context': json.dumps({
+                'keyword1': {
+                    'value': u'${orderNo}',
+                    'color': '#173177'
+                },
+                'keyword2': {
+                    "value": u'${terminal}',
+                    "color": '#173177'
+                },
+                'keyword3': {
+                    'value': u'${reason}',
+                    'color': '#173177'
+                },
+                'keyword4': {
+                    'value': u'${service}',
+                    'color': '#173177'
+                },
+                'keyword5': {
+                    'value': u'${settle}',
+                    'color': '#173177'
+                }
+            })
+        },
+        'common_order_complete': {
+            'templateId': '',
+            'context': json.dumps({
+                'keyword1': {
+                    'value': u'${orderNo}',
+                    'color': '#173177'
+                },
+                'keyword2': {
+                    "value": u'${settle}',
+                    "color": '#173177'
+                },
+                'keyword3': {
+                    'value': u'${terminal}',
+                    'color': '#173177'
+                },
+                'keyword4': {
+                    'value': u'${startTime}',
+                    'color': '#173177'
+                },
+                'keyword5': {
+                    'value': u'${finishTime}',
+                    'color': '#173177'
+                }
+            })
+        },
         'refund_coins': {
         'refund_coins': {
             'templateId': '',
             'templateId': '',
             'context': json.dumps({
             'context': json.dumps({
@@ -732,7 +782,7 @@ class WechatUserManagerApp(EmbeddedApp):
 
 
 
 
 class WechatUserSubscribeManagerApp(EmbeddedApp):
 class WechatUserSubscribeManagerApp(EmbeddedApp):
-    templateIdMap = DictField(verbose_name = "微信推送消息模版ID字典", default = {})
+    templateIdMap = DictField(verbose_name = u"微信推送消息模版ID字典", default = {})
 
 
     MESSAGE_TEMPLATE = {
     MESSAGE_TEMPLATE = {
         'service_start': {
         'service_start': {
@@ -806,7 +856,7 @@ class WechatUserSubscribeManagerApp(EmbeddedApp):
 
 
 
 
 class WechatDealerSubscribeManagerApp(EmbeddedApp):
 class WechatDealerSubscribeManagerApp(EmbeddedApp):
-    templateIdMap = DictField(verbose_name = "微信推送消息模版ID字典", default = {})
+    templateIdMap = DictField(verbose_name = u"微信推送消息模版ID字典", default = {})
 
 
     MESSAGE_TEMPLATE = {
     MESSAGE_TEMPLATE = {
         'service_start': {
         'service_start': {
@@ -881,6 +931,38 @@ class WechatDealerSubscribeManagerApp(EmbeddedApp):
                 return []
                 return []
 
 
 
 
+
+
+class WechatChannelApp(DynamicDocument):
+    mchid = StringField(verbose_name = u'mchid', null = False)
+
+    sslKey = StringField(verbose_name = u'PKCS8 PERM私钥内容', default = '')
+
+    apikey_v3 = StringField(verbose_name = u'v3 api key. 加解密V3接口敏感信息', default = None)
+    app_serial_number = StringField(verbose_name = u'商户证书序列号', default = None)
+    platform_certificates = ListField(verbose_name = u'平台证书列表', default = None)
+
+    appid = StringField(verbose_name = u'通道应用appid', default = None)
+
+    search_fields = ('mchid', 'name', 'remarks')
+
+    meta = {
+        'indexes': [
+            {
+                'fields': ['mchid'], 'unique': True
+            },
+        ],
+        'collection': 'wechat_channel_app',
+        'db_alias': 'default'
+    }
+
+    def __repr__(self):
+        return '<WechatChannelApp mchid={}>'.format(self.mchid)
+
+    @property
+    def ssl_key(self):
+        return self.sslKey
+
 class PayAppBase(Searchable):
 class PayAppBase(Searchable):
     meta = {
     meta = {
         'abstract': True
         'abstract': True
@@ -901,7 +983,10 @@ class PayAppBase(Searchable):
 
 
     debug = BooleanField(verbose_name = u'是否测试账号', default = False)
     debug = BooleanField(verbose_name = u'是否测试账号', default = False)
 
 
-    functions = ListField(verbose_name = u'支持功能列表', default = [Function.PAY])
+    agentNo = StringField(verbose_name = u'商户代理商编号', default = None)
+
+    supportWithdraw = BooleanField(verbose_name = u'是否支持提现零钱', default = False)
+    supportWithdrawBank = BooleanField(verbose_name = u'是否支持提现到银行卡', default = False)
 
 
     @property
     @property
     def pay_app_type(self):
     def pay_app_type(self):
@@ -927,6 +1012,7 @@ class PayAppBase(Searchable):
     @occupant.setter
     @occupant.setter
     def occupant(self, occupant):
     def occupant(self, occupant):
         self.__occupant__ = occupant
         self.__occupant__ = occupant
+        self.__occupant_id__ = str(occupant.id)
         self.role = occupant.role
         self.role = occupant.role
 
 
     @property
     @property
@@ -992,7 +1078,7 @@ class PayAppBase(Searchable):
     def new_gateway(self, app_platform_type):
     def new_gateway(self, app_platform_type):
         raise NotImplementedError('must implement new_gateway')
         raise NotImplementedError('must implement new_gateway')
 
 
-    def new_withdraw_gateway(self, is_ledger = True, gateway_version = None):
+    def new_withdraw_gateway(self, gateway_version = None):
         raise NotImplementedError('must implement new_withdraw_gateway')
         raise NotImplementedError('must implement new_withdraw_gateway')
 
 
 
 
@@ -1009,24 +1095,27 @@ class WechatPayApp(PayAppBase):
 
 
     sslKey = StringField(verbose_name = u'PKCS8 PERM私钥内容', default = '')
     sslKey = StringField(verbose_name = u'PKCS8 PERM私钥内容', default = '')
 
 
+    # 对于V1接口, 需要sslKey(证书私钥)和sslCert(证书字符串)来调用接口, 使用apikey来加解密信息
     apikey = StringField(verbose_name = u'apikey', null = False)
     apikey = StringField(verbose_name = u'apikey', null = False)
     sslCert = StringField(verbose_name = u'PERM证书内容', default = '')
     sslCert = StringField(verbose_name = u'PERM证书内容', default = '')
 
 
+    # 对于V3接口, 需要sslKey(证书私钥)和app_serial_number(证书序列号)来调用接口, 使用apikey_v3来加解密信息
     apikey_v3 = StringField(verbose_name = u'v3 api key. 加解密V3接口敏感信息', default = None)
     apikey_v3 = StringField(verbose_name = u'v3 api key. 加解密V3接口敏感信息', default = None)
     app_serial_number = StringField(verbose_name = u'商户证书序列号', default = None)
     app_serial_number = StringField(verbose_name = u'商户证书序列号', default = None)
     platform_certificates = ListField(verbose_name = u'平台证书列表', default = None)
     platform_certificates = ListField(verbose_name = u'平台证书列表', default = None)
 
 
-    # 暂时保留
+    withdrawV3 = BooleanField(verbose_name = u'是否使用V3接口对微信转账', default = False)
+    manual_withdraw = BooleanField(verbose_name = u'是否手动提现', default = False)
+
+    # 以下字段不在使用, 暂时兼容保留
     sslcert_path = StringField(verbose_name = '证书pem格式', default = None)
     sslcert_path = StringField(verbose_name = '证书pem格式', default = None)
     sslkey_path = StringField(verbose_name = '证书密钥pem格式', default = None)
     sslkey_path = StringField(verbose_name = '证书密钥pem格式', default = None)
     rawAppId = StringField(verbose_name = u'原始ID', default = '')
     rawAppId = StringField(verbose_name = u'原始ID', default = '')
     rootca_path = StringField(verbose_name = u'root证书路径', default = '')
     rootca_path = StringField(verbose_name = u'root证书路径', default = '')
     rootCA = StringField(verbose_name = u'root证书', default = '')
     rootCA = StringField(verbose_name = u'root证书', default = '')
 
 
-    withdrawV3 = BooleanField(verbose_name = u'是否使用V3接口对微信转账', default = False)
-    manual_withdraw = BooleanField(verbose_name = u'是否手动提现', default = False)
-
-    functions = ListField(verbose_name = u'支持功能列表', default = [PayAppBase.Function.PAY, PayAppBase.Function.WITHDRAW])
+    supportWithdraw = BooleanField(verbose_name = u'是否支持提现', default = True)
+    supportWithdrawBank = BooleanField(verbose_name = u'是否支持提现到银行卡', default = True)
 
 
     search_fields = ('appid', 'mchid', 'name', 'remarks')
     search_fields = ('appid', 'mchid', 'name', 'remarks')
 
 
@@ -1096,6 +1185,8 @@ class WechatPayApp(PayAppBase):
                   manual_withdraw = False)
                   manual_withdraw = False)
         app.enable = False
         app.enable = False
         app.valid = True
         app.valid = True
+        app.supportWithdraw = False
+        app.supportWithdrawBank = False
         return app
         return app
 
 
     @classmethod
     @classmethod
@@ -1131,17 +1222,14 @@ class WechatPayApp(PayAppBase):
         from apps.web.core.payment.wechat import WechatPaymentGateway
         from apps.web.core.payment.wechat import WechatPaymentGateway
         return WechatPaymentGateway(self)
         return WechatPaymentGateway(self)
 
 
-    def new_withdraw_gateway(self, is_ledger = True, gateway_version = 'v1'):
+    def new_withdraw_gateway(self, gateway_version = 'v1'):
         from apps.web.core.payment.wechat import WechatWithdrawGateway
         from apps.web.core.payment.wechat import WechatWithdrawGateway
 
 
-        if not self.enable:
-            is_ledger = False
-
         if gateway_version == 'v3':
         if gateway_version == 'v3':
             if not self.withdrawV3 or not self.apikey_v3:
             if not self.withdrawV3 or not self.apikey_v3:
                 return None
                 return None
 
 
-        return WechatWithdrawGateway(self, gateway_version = gateway_version, is_ledger = is_ledger)
+        return WechatWithdrawGateway(self, gateway_version = gateway_version)
 
 
     @property
     @property
     def ssl_cert(self):
     def ssl_cert(self):
@@ -1180,6 +1268,7 @@ class WechatPayApp(PayAppBase):
 
 
 @update_certificates.connect
 @update_certificates.connect
 def update_v3_certificates(sender, mchid, cert_str_list):
 def update_v3_certificates(sender, mchid, cert_str_list):
+    WechatChannelApp.objects(mchid = mchid).update(platform_certificates = cert_str_list)
     WechatPayApp.objects(mchid = mchid).update(platform_certificates = cert_str_list)
     WechatPayApp.objects(mchid = mchid).update(platform_certificates = cert_str_list)
 
 
 
 
@@ -1187,30 +1276,28 @@ class AliApp(PayAppBase):
     appid = StringField(verbose_name = u'应用ID', required = True, null = False)
     appid = StringField(verbose_name = u'应用ID', required = True, null = False)
 
 
     # 参数加密秘钥. 一般不加密
     # 参数加密秘钥. 一般不加密
-    aesEncryptKey = StringField(verbose_name = u'加密秘钥', default = '')
+    aesEncryptKey = StringField(verbose_name = u'加密秘钥', default = None)
+
+    signKeyType = StringField(verbose_name = u'签名方式(normal,cert)', default = 'normal')
 
 
     # 公钥模式
     # 公钥模式
-    app_private_key_path = StringField(verbose_name = u'支付宝应用私钥路径', default = '')
     appPrivateKey = StringField(verbose_name = u'支付宝应用私钥. 只有测试情况下才能使用字符串', default = '')
     appPrivateKey = StringField(verbose_name = u'支付宝应用私钥. 只有测试情况下才能使用字符串', default = '')
-
-    public_key_path = StringField(verbose_name = u'支付宝账户公钥路径', default = '')
     alipayPublicKey = StringField(verbose_name = u'文本形式支付宝账户公钥', default = '')
     alipayPublicKey = StringField(verbose_name = u'文本形式支付宝账户公钥', default = '')
 
 
     # 证书模式
     # 证书模式
-    app_publickey_cert_path = StringField(verbose_name = u'支付宝应用公共证书路径', default = '')
     appPublicKeyCert = StringField(verbose_name = u'支付宝应用公共证书', default = '')
     appPublicKeyCert = StringField(verbose_name = u'支付宝应用公共证书', default = '')
-
-    publickey_cert_path = StringField(verbose_name = u'支付宝公共证书路径', default = '')
     publicKeyCert = StringField(verbose_name = u'支付宝公共证书', default = '')
     publicKeyCert = StringField(verbose_name = u'支付宝公共证书', default = '')
-
-    root_cert_path = StringField(verbose_name = u'支付宝根证书路径', default = '')
     rootCert = StringField(verbose_name = u'支付宝根证书', default = '')
     rootCert = StringField(verbose_name = u'支付宝根证书', default = '')
 
 
-    signKeyType = StringField(verbose_name = u'签名方式(normal,cert)', default = 'normal')
-
     # 中间字段
     # 中间字段
-    alipayPublicKeyContent = StringField(verbose_name = u'支付宝公钥文本', default = '')
-    appPublicKeyContent = StringField(verbose_name = u'应用公钥文本', default = '')
+    alipayPublicKeyContent = StringField(verbose_name = u'支付宝公钥文本', default = None)
+    appPublicKeyContent = StringField(verbose_name = u'应用公钥文本', default = None)
+
+    app_private_key_path = StringField(verbose_name = u'支付宝应用私钥路径', default = None)
+    public_key_path = StringField(verbose_name = u'支付宝账户公钥路径', default = None)
+    app_publickey_cert_path = StringField(verbose_name = u'支付宝应用公共证书路径', default = None)
+    publickey_cert_path = StringField(verbose_name = u'支付宝公共证书路径', default = None)
+    root_cert_path = StringField(verbose_name = u'支付宝根证书路径', default = None)
 
 
     search_fields = ('appid', 'name', 'remarks')
     search_fields = ('appid', 'name', 'remarks')
 
 
@@ -1361,6 +1448,8 @@ class AliApp(PayAppBase):
         app = cls(appid = '', public_key_path = '', app_private_key_path = '', shadow = False)
         app = cls(appid = '', public_key_path = '', app_private_key_path = '', shadow = False)
         app.enable = False
         app.enable = False
         app.valid = True
         app.valid = True
+        app.supportWithdraw = False
+        app.supportWithdrawBank = False
         return app
         return app
 
 
     @classmethod
     @classmethod
@@ -1384,13 +1473,9 @@ class AliApp(PayAppBase):
         from apps.web.core.payment.ali import AliPayGateway
         from apps.web.core.payment.ali import AliPayGateway
         return AliPayGateway(self)
         return AliPayGateway(self)
 
 
-    def new_withdraw_gateway(self, is_ledger = True, gateway_version = None):
+    def new_withdraw_gateway(self, gateway_version = None):
         from apps.web.core.payment.ali import AliPayWithdrawGateway
         from apps.web.core.payment.ali import AliPayWithdrawGateway
-
-        if not self.enable:
-            return AliPayWithdrawGateway(self, False)
-        else:
-            return AliPayWithdrawGateway(self, is_ledger)
+        return AliPayWithdrawGateway(self)
 
 
     @property
     @property
     def client(self):
     def client(self):
@@ -1477,7 +1562,7 @@ class PlatformAppBase(PayAppBase):
 
 
 class PlatformPromotionApp(PlatformAppBase):
 class PlatformPromotionApp(PlatformAppBase):
     """
     """
-        用于平台记账后 分账给其他人, 例如红包受益  广告收益等 分给经销商
+    用于平台记账后 分账给其他人, 例如红包受益  广告收益等 分给经销商
     """
     """
     meta = {
     meta = {
         'abstract': True
         'abstract': True
@@ -1521,7 +1606,7 @@ class PlatformReconcileApp(PlatformAppBase):
 
 
 class PlatformWalletApp(PlatformPromotionApp):
 class PlatformWalletApp(PlatformPromotionApp):
     """
     """
-        用于使用经销商余额进行SIM卡支付
+        用于使用经销商余额(平台资金池)进行SIM卡支付
     """
     """
 
 
     meta = {
     meta = {
@@ -2173,69 +2258,6 @@ class DriverEventer(DynamicDocument):
         return eval('eventer_module.builder(device_adapter)')
         return eval('eventer_module.builder(device_adapter)')
 
 
 
 
-class WechatServiceProvider(Searchable):
-    mchid = StringField(verbose_name = u'商户号', null = False)
-
-    apikey = StringField(verbose_name = u'api秘钥', default = '')
-
-    apiclient_cert_path = StringField(verbose_name = u'api证书路径', default = '')
-    apiclient_key_path = StringField(verbose_name = u'api证书秘钥路径', default = '')
-    apiclient_serial_number = StringField(verbose_name = u'api证书序列号', default = '')
-
-    sslCert = StringField(verbose_name = u'PERM证书内容', default = '')
-    sslKey = StringField(verbose_name = u'PKCS8 PERM私钥内容', default = '')
-
-    # v3的
-    apiKeyV3 = StringField(verbose_name = u'APIv3秘钥', default = '', db_field = "apikey_v3")
-    sslCertV3 = StringField(verbose_name = u"平台证书 用来加密敏感信息使用")
-
-    search_fields = ('mchid')
-
-    meta = {
-        'indexes': [
-            {
-                'fields': ['mchid'], 'unique': True
-            },
-        ],
-        'collection': 'wechat_service_provider',
-        'db_alias': 'default'
-    }
-
-    @property
-    def ssl_cert(self):
-        if not hasattr(self, '__ssl_cert__'):
-            try:
-                if self.sslCert.startswith('-----BEGIN'):
-                    setattr(self, '__ssl_cert__', self.sslCert)
-                elif self.apiclient_cert_path.startswith('-----BEGIN'):
-                    setattr(self, '__ssl_cert__', self.apiclient_cert_path)
-                else:
-                    with open(self.apiclient_cert_path) as fp:
-                        setattr(self, '__ssl_cert__', fp.read())
-            except Exception as e:
-                logger.error('{} ssl_cert exception = {}'.format(repr(self), e.message))
-                setattr(self, '__ssl_cert__', '')
-
-        return getattr(self, '__ssl_cert__')
-
-    @property
-    def ssl_key(self):
-        if not hasattr(self, '__ssl_key__'):
-            try:
-                if self.sslKey.startswith('-----BEGIN'):
-                    setattr(self, '__ssl_key__', self.sslKey)
-                elif self.apiclient_key_path.startswith('-----BEGIN'):
-                    setattr(self, '__ssl_key__', self.apiclient_key_path)
-                else:
-                    with open(self.apiclient_key_path) as fp:
-                        setattr(self, '__ssl_key__', fp.read())
-            except Exception as e:
-                logger.error('{} ssl_key exception = {}'.format(repr(self), e.message))
-                setattr(self, '__ssl_key__', '')
-
-        return getattr(self, '__ssl_key__')
-
-
 class CustomerPayRelation(Searchable):
 class CustomerPayRelation(Searchable):
     """
     """
     app 和使用者的关联表
     app 和使用者的关联表

+ 225 - 1
apps/web/dealer/transaction_deprecated.py

@@ -1,10 +1,21 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 
 
+import datetime
 import logging
 import logging
 
 
+from django.conf import settings
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
+from apilib.monetary import RMB, Percent
+from apilib.utils_sys import memcache_lock
+from apps.web.common.transaction.refund import RefundCashMixin
+from apps.web.dealer.define import DealerConst
+from apps.web.dealer.models import RefundDealerRechargeRecord, DealerRechargeRecord, Dealer
+from apps.web.dealer.proxy import record_income_proxy
+from apps.web.exceptions import UserServerException
+from apps.web.user.models import RechargeRecord
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -12,6 +23,219 @@ if TYPE_CHECKING:
 
 
 
 
 def refund_post_pay(refundOrder, success):
 def refund_post_pay(refundOrder, success):
-    # TODO: 代理商收益调整
     pass
     pass
 
 
+def refund_cash_to_dealer(dealer_recharge_record, refundFee, isInsure = False):
+    if dealer_recharge_record.product == DealerRechargeRecord.ProductType.SimCard:
+        return RefundSimRecharge(dealer_recharge_record, refundFee, u'流量卡退款').execute(
+            frozen_callable = None, refund_callable = refund_post_pay)
+    elif dealer_recharge_record.product == DealerRechargeRecord.ProductType.AutoSimCard:
+        # return RefundSimWallet(dealer_recharge_record, refundFee).execute()
+        # else:
+        raise UserServerException(u'目前不支持该种类型订单退款')
+    else:
+        raise UserServerException(u'目前不支持该种类型订单退款')
+
+
+class RefundCash(RefundCashMixin):
+    MAX_LEDGER_CHECK_TIME = 15  # 最长的查询分账时间
+
+    def __init__(self, rechargeOrder, refundFee, reason):  # type:(DealerRechargeRecord, RMB, basestring) -> None
+        super(RefundCash, self).__init__(rechargeOrder, refundFee)
+        self.reason = reason
+
+    def pre_check(self):
+        """
+        退款的预检查
+        :return:
+        """
+        # 首先检查退款的金额 原则上退款金额不能小于0 不能大于交易定安的金额
+        if self.refundFee <= RMB(0) or self.refundFee > self.totalFee:
+            raise UserServerException(u"退费金额错误")
+
+        refundOrder = RefundDealerRechargeRecord.objects.filter(
+            rechargeObjId = self.paySubOrder.id).first()  # type: RefundDealerRechargeRecord
+        if refundOrder:
+            if refundOrder.is_successful:
+                raise UserServerException(u"该单已经退单")
+            else:
+                raise UserServerException(u"订单正在退款中")
+
+    def execute(self, frozen_callable, refund_callable, notify_url = None):
+        """
+        执行退款的动作
+        :return:
+        """
+
+        lockKey = "refund_dealer_recharge_cash_{}".format(self.paySubOrder.id)
+
+        with memcache_lock(key = lockKey, value = self.paySubOrder.id, expire = 360) as acquired:
+            if not acquired:
+                raise UserServerException(u"退款订单正在处理,等订单结束后,您才能再次重试哦")
+
+            self.pre_check()
+
+            refundOrder = RefundDealerRechargeRecord.issue(self.payOrder, self.refundFee)
+
+            logger.info('refund paras: {} {}'.format(refundOrder.orderNo, self.refund_paras))
+
+            refundOrder.processing()
+
+            try:
+                self.submit_refund(
+                    refundOrder, None, self.reason, notify_url or refundOrder.notify_url, refund_callable)
+
+            except Exception:
+                import traceback
+                logger.warning(
+                    'Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, traceback.format_exc()))
+
+            finally:
+                pass
+
+            return refundOrder
+
+
+class RefundSimRecharge(RefundCash):
+    def pre_check(self):
+        super(RefundSimRecharge, self).pre_check()
+
+        for partition in self.payOrder.settleInfo['partition']:
+            if partition['id'] != settings.MY_PRIMARY_AGENT_ID and partition['earned'] > 0:
+                raise UserServerException(u'目前仅支持不分账情况下退账。')
+
+
+class RefundInsureRecharge(RefundCash):
+    def pre_check(self):
+        if self.refundFee <= RMB(0) or self.refundFee > self.totalFee:  # DealerChargeRecord中的退费总额是负数
+            raise UserServerException(u"退费金额错误")
+
+        # 目前只支持一个单,退一次,不允许退多次
+        refundOrder = RefundDealerRechargeRecord.objects.filter(
+            rechargeObjId = self.paySubOrder.id).first()  # type: RefundDealerRechargeRecord
+        if refundOrder:
+            if refundOrder.is_successful:
+                raise UserServerException(u"该单已经退单")
+            else:
+                raise UserServerException(u"订单正在退款中")
+
+
+class RefundToWallet(object):
+    def __init__(self, rechargeOrder, refundFee, reason, subType, via, source):
+        self._payOrder = rechargeOrder
+        self.refundFee = refundFee
+        self.reason = reason
+        self.subType = subType
+        self.via = via
+        self.source = source
+
+    @property
+    def outTradeNo(self):
+        """
+        交易单号
+        :return:
+        """
+        return self._payOrder.orderNo
+
+    @property
+    def totalFee(self):
+        return RMB(round(float(self._payOrder.totalFee) / 100, 2))
+
+    @property
+    def payOrder(self):
+        """
+        交易订单 即支付订单 与第三方系统产生关联的订单
+        :return:
+        """
+        return self._payOrder
+
+    def pre_check(self):
+        """
+        退款的预检查
+        :return:
+        """
+        # 首先检查退款的金额 原则上退款金额不能小于0 不能大于交易定安的金额
+        if self.refundFee <= RMB(0) or self.refundFee > self.totalFee:
+            raise UserServerException(u"退费金额错误")
+
+        # 检查退款订单是否已经存在 是否已经退款成功
+        refundOrder = RefundDealerRechargeRecord.objects.filter(
+            rechargeObjId = self._payOrder.id).first()  # type: RefundDealerRechargeRecord
+        if refundOrder:
+            if refundOrder.is_successful:
+                raise UserServerException(u"该单已经退单")
+            else:
+                raise UserServerException(u"订单正在退款中")
+
+    def refund_dealer_balance(self, refundFee):
+        source_key = self._payOrder.withdraw_source_key
+        income_type = DealerConst.MAP_SOURCE_TO_TYPE[self.source]
+        dealer = Dealer.objects(id = self._payOrder.dealerId).first()
+        if not dealer:
+            return False
+
+        fundKey = dealer.fund_key(income_type = income_type, source_key = source_key)
+
+        queryFilter = {
+            '_id': dealer.id
+        }
+
+        update = {
+            '$inc': {'{fundKey}.balance'.format(fundKey = fundKey): (refundFee).mongo_amount}
+        }
+
+        result = Dealer.get_collection().update_one(queryFilter, update, upsert = False)
+        if result.matched_count == 1 and result.modified_count == 1:
+            return True
+        else:
+            return False
+
+    def execute(self, refund_callable = None):
+        """
+        执行退款的动作
+        :return:
+        """
+        lockKey = "refund_dealer_recharge_wallet_{}".format(self._payOrder.id)
+
+        with memcache_lock(key = lockKey, value = self._payOrder.id, expire = 360) as acquired:
+            if not acquired:
+                raise UserServerException(u"退款订单正在处理,等订单结束后,您才能再次重试哦")
+
+            self.pre_check()
+
+            refundOrder = RefundDealerRechargeRecord.issue(self.payOrder, self.refundFee)
+
+            logger.info(
+                'refund[wallet], order = %s out_refund_no=%s, out_trade_no=%s, refund_fee=%s, total_fee=%s' % (
+                    self._payOrder.orderNo, self.payOrder.orderNo, refundOrder.orderNo, self.refundFee,
+                    str(float(self.payOrder.totalFee) / 100)))
+
+            refundOrder.processing()
+
+            try:
+                self.refund_dealer_balance(self.refundFee)  # 先把钱包的余额返回
+                income_record = RechargeRecord.issue_refund_order(self.payOrder, self.refundFee, self.subType,
+                                                                  self.via)  # 补充一条充值记录
+            except Exception as e:
+                logger.exception(e)
+                refundOrder.fail(errorCode = 'EXCEPTION', errorDesc = u'退款过程发生异常')
+                raise e
+
+            record_income_proxy(self.source, income_record, {
+                "owner": [
+                    {
+                        "money": RMB(income_record.money).mongo_amount,
+                        "role": "owner",
+                        "share": Percent("100.0").mongo_amount,
+                        "id": str(income_record.ownerId)
+                    }
+                ],
+                'partner': []
+            })
+
+            refundOrder.succeed(finishedTime = datetime.datetime.now())
+
+            if refund_callable:  # 用于退款到钱包后的善后工作,比如扣除代理商的分成,补充退款记录
+                refund_callable(refundOrder, True)
+
+            return refundOrder

+ 59 - 47
apps/web/superadmin/tasks.py

@@ -17,13 +17,11 @@ from apps.thirdparties.dingding import DingDingRobot
 from apps.web.agent.define import AgentConst
 from apps.web.agent.define import AgentConst
 from apps.web.agent.models import Agent
 from apps.web.agent.models import Agent
 from apps.web.constant import Const, ComplaintResponseTemplate
 from apps.web.constant import Const, ComplaintResponseTemplate
-from apps.web.core.models import WechatServiceProvider
 from apps.web.core.payment import WithdrawGateway
 from apps.web.core.payment import WithdrawGateway
 from apps.web.core.utils import generate_excel_report
 from apps.web.core.utils import generate_excel_report
 from apps.web.dealer.define import DealerConst
 from apps.web.dealer.define import DealerConst
 from apps.web.dealer.models import Dealer, DealerRechargeRecord
 from apps.web.dealer.models import Dealer, DealerRechargeRecord
 from apps.web.device.models import SIMCard, Device
 from apps.web.device.models import SIMCard, Device
-from apps.web.core.bridge.wechat.v3api import WechatComplaint
 from apps.web.user.models import RechargeRecord, ConsumeRecord
 from apps.web.user.models import RechargeRecord, ConsumeRecord
 
 
 logger = get_task_logger(__name__)
 logger = get_task_logger(__name__)
@@ -422,45 +420,41 @@ def sum_customer(path=None):
 
 
 
 
 def handle_customer_complaints_yesterday():
 def handle_customer_complaints_yesterday():
-    def submitAndFeedback(mchid, origin_mchid, complaintId, responseContent):
-        trueMchid = origin_mchid if mchid == '' else mchid
-        submitResult = wechatComplaintUtil.submit_complaint_reply(mchid=trueMchid, complaintId=complaintId,
-                                                                  responseContent=responseContent)
-        if submitResult != 204:
-            logger.error('handle_customer_complaints, submit error, c_id=%s' % complaintId)
-        else:
-            wechatComplaintUtil.feedback_complaint_completed(trueMchid, complaintId)
-            logger.info('done! for handle_customer_complaints %s' % complaintId)
+    # def submitAndFeedback(complaintUtil, mchid, origin_mchid, complaintId, responseContent):
+    #     trueMchid = origin_mchid if mchid == '' else mchid
+    #     submitResult = complaintUtil.complaint_response(
+    #         complaint_id = complaintId, complainted_mchid = trueMchid, response_content = responseContent)
+    #     if submitResult != 204:
+    #         logger.error('handle_customer_complaints, submit error, c_id=%s' % complaintId)
+    #     else:
+    #         complaintUtil.complaint_complete(complaint_id = complaintId, complainted_mchid = trueMchid)
+    #         logger.info('done! for handle_customer_complaints %s' % complaintId)
 
 
     if settings.SKIP_HANDLE_WECHAT_COMPLAINT_TASK:
     if settings.SKIP_HANDLE_WECHAT_COMPLAINT_TASK:
         logger.debug('SKIP_HANDLE_WECHAT_COMPLAINT_TASK is True.')
         logger.debug('SKIP_HANDLE_WECHAT_COMPLAINT_TASK is True.')
         return
         return
 
 
-    # 处理投诉有单次最大处理限制或者单次最大处理时间限制,所以需要重复执行几次
-    for times in range(3):
-        crt = ComplaintResponseTemplate()
-        wsps = WechatServiceProvider.objects()
-        for wsp in wsps:
-            wechatComplaintUtil = WechatComplaint(
-                mchid=wsp.mchid,
-                asn=wsp.apiclient_serial_number,
-                slk=wsp.sslKey
-            )
-            yesterday = (datetime.date.today() + datetime.timedelta(days=-1)).strftime("%Y-%m-%d")
-            threeDaysAgo = (datetime.date.today() + datetime.timedelta(days=-20)).strftime("%Y-%m-%d")
-            wechatComplaints = wechatComplaintUtil.query_complaint_list(dateStart=threeDaysAgo, dateEnd=yesterday)
+    channels = [item for item in WechatChannelApp.objects().all()]
+    crt = ComplaintResponseTemplate()
+
+    for channel in channels:  # type: WechatChannelApp
+        loop = 0
+        pageSize = 30
+
+        while True:
+            wechatComplaintUtil = MyWechatClientV3(channel).complaint  # type: Complaint
 
 
-            loop = 1
-            while len(wechatComplaints) == loop * 30:
-                tempWechatComplaints = wechatComplaintUtil.query_complaint_list(dateStart=threeDaysAgo, dateEnd=yesterday, pageIndex=loop * 30, pageSize=30)
-                wechatComplaints.extend(tempWechatComplaints)
-                loop += 1
-                if loop == 100:
-                    break
+            yesterday = (datetime.date.today() + datetime.timedelta(days = -1)).strftime("%Y-%m-%d")
+            threeDaysAgo = (datetime.date.today() + datetime.timedelta(days = -20)).strftime("%Y-%m-%d")
 
 
-            trueWechatComplaints = [w for w in wechatComplaints if w['complaint_state'] == u'PENDING']
+            result = wechatComplaintUtil.complaint_list_query(
+                begin_date = threeDaysAgo, end_date = yesterday, offset = loop * pageSize, limit = pageSize)
 
 
-            for _ in trueWechatComplaints:
+            if 'data' not in result:
+                logger.warning('handle_customer_complaints, no data in query result.')
+                break
+
+            for _ in [w for w in result['data'] if w['complaint_state'] == u'PENDING']:
                 mchid = _.get('complainted_mchid', '')
                 mchid = _.get('complainted_mchid', '')
                 userComplaintTimes = _.get('user_complaint_times', 1)
                 userComplaintTimes = _.get('user_complaint_times', 1)
                 try:
                 try:
@@ -475,7 +469,7 @@ def handle_customer_complaints_yesterday():
                         logger.error('handle_customer_complaints, no transaction_id')
                         logger.error('handle_customer_complaints, no transaction_id')
                         continue
                         continue
 
 
-                    rechargeRecord = RechargeRecord.objects(__raw__={
+                    rechargeRecord = RechargeRecord.objects(__raw__ = {
                         '$or': [
                         '$or': [
                             {
                             {
                                 'wxOrderNo': transactionId
                                 'wxOrderNo': transactionId
@@ -488,11 +482,15 @@ def handle_customer_complaints_yesterday():
                         outTradeNo = complaintOrderInfo[0].get('out_trade_no', '')
                         outTradeNo = complaintOrderInfo[0].get('out_trade_no', '')
                         logger.error('handle_customer_complaints, no rechargeRecord, id=%s' % transactionId)
                         logger.error('handle_customer_complaints, no rechargeRecord, id=%s' % transactionId)
                         responseContent = crt.NO_RECHARGE_RECORD_CONTENT
                         responseContent = crt.NO_RECHARGE_RECORD_CONTENT
-                        submitAndFeedback(mchid, wsp.mchid, complaintId, responseContent)
-                        DingDingRobot().send_msg(msg=u'[客诉异常] 投诉单关联的商户订单号不存在, outTradeNo=%s' % outTradeNo)
+                        trueMchid = channel.mchid if mchid == '' else mchid
+                        wechatComplaintUtil.complaint_response(complaint_id=complaintId, complainted_mchid=trueMchid,
+                                                               response_content=responseContent)
+                        time.sleep(1)
+                        wechatComplaintUtil.complaint_complete(complaint_id=complaintId, complainted_mchid=trueMchid)
+                        DingDingRobot().send_msg(msg = u'[客诉异常] 投诉单关联的商户订单号不存在, outTradeNo=%s' % outTradeNo)
                         continue
                         continue
 
 
-                    dealer = Dealer.objects(id=rechargeRecord.ownerId).first()
+                    dealer = Dealer.objects(id = rechargeRecord.ownerId).first()
                     if dealer is None:
                     if dealer is None:
                         logger.error('handle_customer_complaints, no dealer, ref_id=%s' % str(rechargeRecord.id))
                         logger.error('handle_customer_complaints, no dealer, ref_id=%s' % str(rechargeRecord.id))
                         continue
                         continue
@@ -503,7 +501,7 @@ def handle_customer_complaints_yesterday():
                     refundedMoney = 0
                     refundedMoney = 0
                     refundedCash = 0
                     refundedCash = 0
                     if startKey != '':
                     if startKey != '':
-                        consumeRecord = ConsumeRecord.objects(startKey=startKey).first()
+                        consumeRecord = ConsumeRecord.objects(startKey = startKey).first()
                         if consumeRecord is not None:
                         if consumeRecord is not None:
                             if consumeRecord.isNormal is True:
                             if consumeRecord.isNormal is True:
                                 refundedMoney = consumeRecord.servicedInfo.get('refundedMoney', 0.0)  # 金币
                                 refundedMoney = consumeRecord.servicedInfo.get('refundedMoney', 0.0)  # 金币
@@ -515,7 +513,7 @@ def handle_customer_complaints_yesterday():
                             else:
                             else:
                                 refundedType = 3
                                 refundedType = 3
 
 
-                    device = Device.objects(logicalCode=rechargeRecord.logicalCode).first()
+                    device = Device.objects(logicalCode = rechargeRecord.logicalCode).first()
                     if device is None:
                     if device is None:
                         logger.error('handle_customer_complaints, no device, ref_id=%s' % str(rechargeRecord.id))
                         logger.error('handle_customer_complaints, no device, ref_id=%s' % str(rechargeRecord.id))
                         deviceTypeStr = u'设备'
                         deviceTypeStr = u'设备'
@@ -524,13 +522,24 @@ def handle_customer_complaints_yesterday():
 
 
                     if userComplaintTimes == 1:
                     if userComplaintTimes == 1:
                         if refundedType == 1:
                         if refundedType == 1:
-                            responseContent = crt.REFUND_RESPONSE_CONTENT_1.format(deviceTypeStr, rechargeRecord.logicalCode, consumeRecord.finishedTime.strftime('%Y-%m-%d %H:%M:%S'), str(refundedMoney))
+                            responseContent = crt.REFUND_RESPONSE_CONTENT_1.format(deviceTypeStr,
+                                                                                   rechargeRecord.logicalCode,
+                                                                                   consumeRecord.finishedTime.strftime(
+                                                                                       '%Y-%m-%d %H:%M:%S'),
+                                                                                   str(refundedMoney))
                         elif refundedType == 2:
                         elif refundedType == 2:
-                            responseContent = crt.REFUND_RESPONSE_CONTENT_2.format(deviceTypeStr, rechargeRecord.logicalCode, consumeRecord.finishedTime.strftime('%Y-%m-%d %H:%M:%S'), str(refundedCash))
+                            responseContent = crt.REFUND_RESPONSE_CONTENT_2.format(deviceTypeStr,
+                                                                                   rechargeRecord.logicalCode,
+                                                                                   consumeRecord.finishedTime.strftime(
+                                                                                       '%Y-%m-%d %H:%M:%S'),
+                                                                                   str(refundedCash))
                         elif refundedType == 3:
                         elif refundedType == 3:
-                            responseContent = crt.REFUND_RESPONSE_CONTENT_3.format(deviceTypeStr, rechargeRecord.logicalCode)
+                            responseContent = crt.REFUND_RESPONSE_CONTENT_3.format(deviceTypeStr,
+                                                                                   rechargeRecord.logicalCode)
                         else:
                         else:
-                            responseContent = crt.COMMON_RESPONSE_CONTENT.format(rechargeRecord.dateTimeAdded.strftime('%Y-%m-%d %H:%M:%S'), rechargeRecord.logicalCode, deviceTypeStr, str(rechargeRecord.money), rechargeRecord.orderNo, dealer.service_phone)
+                            responseContent = crt.COMMON_RESPONSE_CONTENT.format(
+                                rechargeRecord.dateTimeAdded.strftime('%Y-%m-%d %H:%M:%S'), rechargeRecord.logicalCode,
+                                deviceTypeStr, str(rechargeRecord.money), rechargeRecord.orderNo, dealer.service_phone)
                     elif userComplaintTimes == 2:
                     elif userComplaintTimes == 2:
                         responseContent = crt.COMMON_RESPONSE_CONTENT_2.format(dealer.service_phone)
                         responseContent = crt.COMMON_RESPONSE_CONTENT_2.format(dealer.service_phone)
                     elif userComplaintTimes == 3:
                     elif userComplaintTimes == 3:
@@ -538,16 +547,19 @@ def handle_customer_complaints_yesterday():
                     else:
                     else:
                         responseContent = crt.ERROR_RESPONSE_CONTENT
                         responseContent = crt.ERROR_RESPONSE_CONTENT
 
 
-                    submitAndFeedback(mchid, wsp.mchid, complaintId, responseContent)
+                    trueMchid = channel.mchid if mchid == '' else mchid
+                    wechatComplaintUtil.complaint_response(complaint_id=complaintId, complainted_mchid=trueMchid, response_content=responseContent)
+                    time.sleep(1)
+                    wechatComplaintUtil.complaint_complete(complaint_id=complaintId, complainted_mchid=trueMchid)
 
 
                 except Exception as e:
                 except Exception as e:
                     logger.error('error=%s, error_mchid=%s' % (e, mchid))
                     logger.error('error=%s, error_mchid=%s' % (e, mchid))
                     continue
                     continue
 
 
+            if len(result['data']) < pageSize:
+                break
 
 
-
-
-
+            loop = loop + 1
 
 
 
 
 
 

+ 0 - 6
apps/web/superadmin/urls.py

@@ -183,12 +183,6 @@ urlpatterns = patterns('',
 
 
                        url(r'^modifyDeviceCode$', modifyDeviceCode, name='modifyDeviceCode$'),
                        url(r'^modifyDeviceCode$', modifyDeviceCode, name='modifyDeviceCode$'),
 
 
-                       url(r'^getUserComplaintList$', getUserComplaintList, name='getUserComplaintList'),
-
-                       url(r'^replyComplaint', replyComplaint, name='replyComplaint'),
-
-                       url(r'^submitComplaint', submitComplaint, name='submitComplaint'),
-
                        url(r'^getInsuranceOrder', getInsuranceOrder, name='getInsuranceOrder'),
                        url(r'^getInsuranceOrder', getInsuranceOrder, name='getInsuranceOrder'),
                        url(r'^refundInsuranceOrder', refundInsuranceOrder, name='refundInsuranceOrder'),
                        url(r'^refundInsuranceOrder', refundInsuranceOrder, name='refundInsuranceOrder'),
                        url(r'^rentDevice', rentDevice, name='rentDevice'),
                        url(r'^rentDevice', rentDevice, name='rentDevice'),

+ 0 - 93
apps/web/superadmin/views.py

@@ -28,15 +28,12 @@ from apps.web.common.models import AddressType, FAQ, WithdrawBankCard, WithdrawB
 from apps.web.common.models import Feature
 from apps.web.common.models import Feature
 from apps.web.constant import Const, SimStatus
 from apps.web.constant import Const, SimStatus
 from apps.web.core import ROLE
 from apps.web.core import ROLE
-from apps.web.core.bridge import WechatClientProxy
-from apps.web.core.bridge.wechat.v3api import WechatComplaint
 from apps.web.core.exceptions import InvalidFileSize, InvalidFileName, \
 from apps.web.core.exceptions import InvalidFileSize, InvalidFileName, \
     ManagerServiceTimeOutException, ManagerServiceSerialException, ServiceException, RentDeviceError
     ManagerServiceTimeOutException, ManagerServiceSerialException, ServiceException, RentDeviceError
 from apps.web.core.file import AliOssFileUploader
 from apps.web.core.file import AliOssFileUploader
 from apps.web.core.models import AliApp, WechatPayApp
 from apps.web.core.models import AliApp, WechatPayApp
 from apps.web.core.models import OfflineTask
 from apps.web.core.models import OfflineTask
 from apps.web.core.models import WechatAuthApp
 from apps.web.core.models import WechatAuthApp
-from apps.web.core.models import WechatServiceProvider
 from apps.web.core.networking import MessageSender
 from apps.web.core.networking import MessageSender
 from apps.web.core.utils import DefaultJsonErrorResponse, JsonErrorResponse, JsonOkResponse
 from apps.web.core.utils import DefaultJsonErrorResponse, JsonErrorResponse, JsonOkResponse
 from apps.web.dealer.models import Dealer, ExchangeOrder, DealerRechargeRecord, ApiAppInfo
 from apps.web.dealer.models import Dealer, ExchangeOrder, DealerRechargeRecord, ApiAppInfo
@@ -2740,96 +2737,6 @@ def modifyDeviceCode(request):
         return JsonErrorResponse(description='修改失败!, 去手动添加操作')
         return JsonErrorResponse(description='修改失败!, 去手动添加操作')
 
 
 
 
-@error_tolerate(nil = DefaultJsonErrorResponse)
-@permission_required(ROLE.supermanager)
-def getUserComplaintList(request):
-    pageIndex = int(request.GET.get('pageIndex', 1)) - 1
-    pageSize = int(request.GET.get('pageSize', 10))
-    startTime = request.GET.get('startTime', None)
-    endTime = request.GET.get('endTime', None)
-    complaintId = request.GET.get('complaintId', None)
-    stateFilter = request.GET.get('stateFilter', None)
-
-    dataList = []
-    total = 0
-    if startTime is None and endTime is None and complaintId is None:
-        return JsonOkResponse(payload = {'total': total, 'dataList': dataList})
-
-    elif startTime is not None and endTime is not None and complaintId is None:
-        wxProvider = WechatServiceProvider.objects(mchid = '398254625').first()  # type: WechatServiceProvider
-        wechatComplaintUtil = WechatComplaint(
-            mchid = wxProvider.mchid,
-            asn = wxProvider.apiclient_serial_number,
-            slk = wxProvider.sslKey)
-
-        try:
-            dataList = wechatComplaintUtil.query_complaint_list(dateStart = startTime, dateEnd = endTime,
-                                                                pageIndex = pageIndex, pageSize = pageSize)
-        except Exception as e:
-            return JsonErrorResponse(description = u'查询条件错误, 不支持大于30天的间隔时间, 或是未知错误。')
-
-        if stateFilter is not None:
-            dataList1 = []
-            for _ in dataList:
-                if _['complaint_state'] == stateFilter:
-                    dataList1.append(_)
-
-            return JsonOkResponse(payload = {'total': len(dataList1), 'dataList': dataList1})
-
-        return JsonOkResponse(payload = {'total': len(dataList), 'dataList': dataList})
-
-    elif complaintId is not None:
-        wxProvider = WechatServiceProvider.objects(mchid = '398254625').first()  # type: WechatServiceProvider
-        wechatComplaintUtil = WechatComplaint(
-            mchid = wxProvider.mchid,
-            asn = wxProvider.apiclient_serial_number,
-            slk = wxProvider.sslKey)
-        dataList = [wechatComplaintUtil.query_complaint_details_from_id(complaintId)]
-        return JsonOkResponse(payload = {'total': 1, 'dataList': dataList})
-
-    else:
-        return JsonErrorResponse(description = u'查询条件不在业务范围内')
-
-
-@error_tolerate(nil = DefaultJsonErrorResponse)
-@permission_required(ROLE.supermanager)
-def replyComplaint(request):
-    payload = json.loads(request.body)
-    complaintId = payload.get('complaint_id', '')
-    remark = payload.get('remark', '')
-
-    wxProvider = WechatServiceProvider.objects(mchid = '398254625').first()  # type: WechatServiceProvider
-    wechatComplaintUtil = WechatComplaint(
-        mchid = wxProvider.mchid,
-        asn = wxProvider.apiclient_serial_number,
-        slk = wxProvider.sslKey)
-    result = wechatComplaintUtil.submit_complaint_reply(complaintId, remark)
-
-    if result == 204:
-        return JsonOkResponse(description = u'成功')
-    else:
-        return JsonErrorResponse(description = u'接口回复失败')
-
-
-@error_tolerate(nil = DefaultJsonErrorResponse)
-@permission_required(ROLE.supermanager)
-def submitComplaint(request):
-    payload = json.loads(request.body)
-    complaintId = payload.get('complaint_id', '')
-
-    wxProvider = WechatServiceProvider.objects(mchid = '398254625').first()  # type: WechatServiceProvider
-    wechatComplaintUtil = WechatComplaint(
-        mchid = wxProvider.mchid,
-        asn = wxProvider.apiclient_serial_number,
-        slk = wxProvider.sslKey)
-    result = wechatComplaintUtil.feedback_complaint_completed(complaintId)
-
-    if result == 204:
-        return JsonOkResponse(description = u'成功')
-    else:
-        return JsonErrorResponse(description = u'接口回复失败')
-
-
 @error_tolerate(nil = DefaultJsonErrorResponse)
 @error_tolerate(nil = DefaultJsonErrorResponse)
 @permission_required(ROLE.supermanager)
 @permission_required(ROLE.supermanager)
 def rentDevice(request):
 def rentDevice(request):

+ 77 - 0
apps/web/user/models.py

@@ -2088,6 +2088,83 @@ class RechargeRecord(OrderRecordBase):
 
 
     def to_json_dict(self):
     def to_json_dict(self):
         return self.to_dict(json_safe = True)
         return self.to_dict(json_safe = True)
+    
+    @classmethod
+    def issue_refund_order(cls, dealer_recharge_record, refundFee, sub_type, via): # type:(DealerRechargeRecord, RMB, str, via)->RechargeRecord
+        """
+        在经销商使用钱包余额支付情况下, 给经销商建立负收益记录
+        :param dealer_recharge_record:
+        :param refundFee:
+        :param sub_type:
+        :param via:
+        :return:
+        """
+
+        from apps.web.common.proxy import ClientRechargeModelProxy
+        record = ClientRechargeModelProxy.get_one(
+            shard_filter = {'ownerId': dealer_recharge_record.ownerId},
+            orderNo = dealer_recharge_record.orderNo)  # type: RechargeRecord
+
+        if not record:
+            owner = Dealer.objects(id = dealer_recharge_record.ownerId).first()
+
+            device = Device.get_dev(dealer_recharge_record.items[0]['devNo'])  # type: DeviceDict
+            if device.ownerId != dealer_recharge_record.ownerId:
+                raise UserServerException(u'该设备所属经销商和订单不一致,无法退款')
+
+            record = RechargeRecord(
+                openId = owner.username,
+                nickname = owner.nickname,
+                orderNo = dealer_recharge_record.orderNo,
+                ownerId = dealer_recharge_record.ownerId,
+                subject = dealer_recharge_record.subject,
+                finishedTime = dealer_recharge_record.finishedTime,
+                gateway = dealer_recharge_record.gateway,
+                payAppType = dealer_recharge_record.payAppType,
+                payGatewayKey = dealer_recharge_record.payGatewayKey,
+                devNo = device.devNo,
+                devType = device.devTypeName,
+                devTypeName = device.devTypeName,
+                devTypeCode = device.devTypeCode,
+                logicalCode = device.logicalCode,
+                groupId = device.groupId,
+                address = device.group.address,
+                groupNumber = device.groupNumber,
+                groupName = device.group.groupName)
+
+        payload = {
+            'openId': record.openId,
+            'nickname': record.nickname,
+
+            'orderNo': OrderNoMaker.make_order_no_32(
+                identifier = record.logicalCode,
+                main_type = OrderMainType.PAY,
+                sub_type = sub_type),
+
+            'ownerId': record.ownerId,
+            'money': refundFee,
+            'coins': VirtualCoin(0),
+            'subject': '{} 退费'.format(record.subject),
+            'result': cls.PayResult.SUCCESS,
+            'via': via,
+            'finishedTime': datetime.datetime.now(),
+            'gateway': record.my_gateway,
+            'payAppType': record.pay_app_type,
+            'payGatewayKey': record.pay_gateway_key,
+            'isAllocatedCardMoney': True,
+            'devNo': record.devNo,
+            'devType': record.devTypeName,
+            'devTypeName': record.devTypeName,
+            'devTypeCode': record.devTypeCode,
+            'logicalCode': record.logicalCode,
+            'groupId': record.groupId,
+            'address': record.address,
+            'groupNumber': record.groupNumber,
+            'groupName': record.groupName
+        }
+
+        record = cls(**payload).save()
+        return record
 
 
     def succeed(self, **kwargs):
     def succeed(self, **kwargs):
         payload = {
         payload = {

+ 5 - 22
script/swap/notify_order_info_to_north.py

@@ -1,40 +1,23 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 import datetime
 import datetime
-import os
-import requests
-import random
-import logging
-import hmac
 import hashlib
 import hashlib
+import hmac
 import json
 import json
-import itsdangerous
-from concurrent.futures import ThreadPoolExecutor
-
+import logging
+import os
+import random
 
 
+import requests
 from base import init_env
 from base import init_env
-from apps.web.core.helpers import ActionDeviceBuilder
 
 
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "configs.testing")
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "configs.testing")
 
 
 init_env(interactive = False)
 init_env(interactive = False)
-from apps.thirdparties.dingding import DingDingRobot
-from apps.web.device.models import Device
-from apps import serviceCache
-from django.conf import settings
-from apps.web.constant import RechargeRecordVia
-from apps.web.core.db import copy_document_classes
-from apps.web.dealer.proxy import DealerIncomeProxy
-from apilib.monetary import RMB
 from apps.web.user.models import RechargeRecord
 from apps.web.user.models import RechargeRecord
-from bson import ObjectId
-from apps.web.common.models import WithdrawRecord, WithdrawRefundRecord
-from apps.web.dealer.models import Dealer
-from apps.web.core.bridge.wechat.WechatClientProxy import MyWeChatComponent
 
 
 from apps.web.south_intf.swap_carcharger import SwapContract
 from apps.web.south_intf.swap_carcharger import SwapContract
 from apps.web.api.utils import AES_CBC_PKCS5padding_encrypt, AES_CBC_PKCS5padding_decrypt
 from apps.web.api.utils import AES_CBC_PKCS5padding_encrypt, AES_CBC_PKCS5padding_decrypt
-from apps.web.constant import DeviceOnlineStatus
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 6 - 23
script/test_swap_api.py

@@ -1,39 +1,22 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 import datetime
 import datetime
-import os
-import requests
-import random
-import logging
-import hmac
 import hashlib
 import hashlib
+import hmac
 import json
 import json
-import itsdangerous
-from concurrent.futures import ThreadPoolExecutor
+import logging
+import os
+import random
 
 
+import requests
 
 
 from base import init_env
 from base import init_env
 
 
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "configs.testing")
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "configs.testing")
 
 
 init_env(interactive = False)
 init_env(interactive = False)
-from apps.thirdparties.dingding import DingDingRobot
-from apps.web.device.models import Device
-from apps import serviceCache
-from django.conf import settings
-from apps.web.constant import RechargeRecordVia
-from apps.web.core.db import copy_document_classes
-from apps.web.dealer.proxy import DealerIncomeProxy
-from apilib.monetary import RMB
-from apps.web.user.models import RechargeRecord
-from bson import ObjectId
-from apps.web.common.models import WithdrawRecord, WithdrawRefundRecord
-from apps.web.dealer.models import Dealer
-from apps.web.core.bridge.wechat.WechatClientProxy import MyWeChatComponent
-
-from apps.web.south_intf.swap_carcharger import SwapContract
+
 from apps.web.api.utils import AES_CBC_PKCS5padding_encrypt, AES_CBC_PKCS5padding_decrypt
 from apps.web.api.utils import AES_CBC_PKCS5padding_encrypt, AES_CBC_PKCS5padding_decrypt
-from apps.web.constant import DeviceOnlineStatus
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 1 - 1
script/utils/upgrade_alipay_app.py

@@ -22,7 +22,7 @@ from apps.web.agent.models import Agent
 companyName = u'陕西满溢科技有限公司'
 companyName = u'陕西满溢科技有限公司'
 appPrefix = u'陕西满溢科技有限公司'
 appPrefix = u'陕西满溢科技有限公司'
 
 
-certPath = u'/var/backups/{}/支付宝'.format(companyName, appPrefix)
+certPath = u'/var/backups/{}/支付宝'.format(companyName)
 
 
 withdraw_app_id = '2021003183632080'
 withdraw_app_id = '2021003183632080'
 
 

+ 0 - 63
script/utils/upgrade_wechat_service_provider.py

@@ -1,63 +0,0 @@
-# -*- coding: utf-8 -*-
-# !/usr/bin/env python
-
-import datetime
-import os
-import sys
-
-import bson
-
-PROJECT_ROOT = os.path.join(os.path.abspath(os.path.split(os.path.realpath(__file__))[0] + "/.."), '..')
-sys.path.insert(0, PROJECT_ROOT)
-
-from script.base import init_env, get_logger
-
-logger = get_logger(__name__)
-
-init_env(interactive = True)
-
-from apps.web.core.models import WechatServiceProvider
-
-mchid = '398254625'
-sslcert_path = 'E:/temp/dayuan_apiclient_cert.pem'
-sslkey_path = 'E:/temp/dayuan_apiclient_key.pem'
-
-def is_valid_string(str):
-    try:
-        bson._make_c_string(str)
-        return True
-    except Exception as e:
-        return False
-
-
-app = WechatServiceProvider.objects(mchid = mchid).first()
-
-print('app id is: {}'.format(str(app.id)))
-
-if os.path.isfile(sslcert_path):
-    with open(sslcert_path) as f:
-        content = str(f.read())
-        if is_valid_string(content):
-            app.sslCert = content
-        else:
-            print('ssl cert is not valid.')
-
-else:
-    print('ssl cert path is not exist.')
-
-if os.path.isfile(sslkey_path):
-    with open(sslkey_path) as f:
-        content = str(f.read())
-        if is_valid_string(content):
-            app.sslKey = content
-        else:
-            print('ssl key is not valid.')
-else:
-    print('ssl key path is not exist.')
-
-app.dateTimeUpdated = datetime.datetime.now()
-
-print app.sslCert
-print app.sslKey
-
-app.save()

+ 0 - 17
static/administrator/js/config.router.js

@@ -333,23 +333,6 @@ angular.module('app')
                         }
                         }
                     })
                     })
                     // 用户投诉订单
                     // 用户投诉订单
-                    .state('app.order.complaint', {
-                        url: '/complaint',
-                        templateUrl: getTemplateUrl('tpl/complaintOrder.html'),
-                        resolve: {
-                            deps: ['$ocLazyLoad', 'uiLoad',
-                                function ($ocLazyLoad, uiLoad) {
-                                    return uiLoad.load([jsLib.moment, jsLib.momentZh]).then(function () {
-                                        return $ocLazyLoad.load(['ui.bootstrap.datetimepicker', 'ui.select']).then(
-                                            function () {
-                                                return $ocLazyLoad.load(['js/controllers/complaintOrder.js']);
-                                            }
-                                        );
-                                    });
-                                }]
-                        }
-                    })
-                    // 用户投诉订单
                     .state('app.order.insurance', {
                     .state('app.order.insurance', {
                         url: '/insurance',
                         url: '/insurance',
                         templateUrl: getTemplateUrl('tpl/insuranceOrder.html'),
                         templateUrl: getTemplateUrl('tpl/insuranceOrder.html'),

+ 0 - 301
static/administrator/js/controllers/complaintOrder.js

@@ -1,301 +0,0 @@
-app.controller('complaintOrderCtrl', ['$scope', '$filter', '$http', '$stateParams', '$timeout', 'uiGridConstants', 'i18nService', 'toaster', function ($scope, $filter, $http, $stateParams, $timeout, uiGridConstants, i18nService, toaster,) {
-    i18nService.setCurrentLang("zh-cn");
-    moment.locale('zh-cn');
-
-    $scope.startTimeOpen = false;
-    $scope.endTimeOpen = false;
-    $scope.timeChange = function (newDate, oldDate) {
-        $scope.startTimeOpen = false;
-        $scope.endTimeOpen = false;
-    };
-
-    $scope.gridOptions = {
-        data: 'myData',
-        showGridFooter: true, //是否显示grid footer
-
-        // rowHeight: 80,
-
-        //-------- 分页属性 ----------------
-        paginationPageSizes: [10, 20, 50, 100], //每页显示个数可选项
-        paginationCurrentPage: 1, //当前页码
-        paginationPageSize: 10, //每页显示个数
-        totalItems: 0,// 总数量
-        useExternalPagination: true,//是否使用分页按钮
-
-        //过滤
-        // enableFiltering: true,
-        columnDefs: [],
-
-        //---------------api---------------------
-        onRegisterApi: function (gridApi) {
-            $scope.gridApi = gridApi;
-            gridApi.pagination.on.paginationChanged($scope, function (newPage, pageSize) {
-                if ($scope.setPagingData) {
-                    $scope.getPagedDataAsync(newPage, pageSize, true);//翻页是强制刷新
-                }
-            });
-        }
-    };
-
-    let stateMap = {
-        '': '-请选择-',
-        PENDING: '未处理',
-        PROCESSING: '正在处理',
-        PROCESSED: '处理完成',
-    }
-    let stateList = []
-    for (let key in stateMap) {
-        stateList.push({
-            value: key,
-            label: stateMap[key]
-        })
-    }
-
-    //枚举常量
-    $scope.enum = {
-        stateMap: stateMap,
-        stateList: stateList
-    };
-
-    var searchType = $scope.enum.searchType = [
-        {value: 'complaintId', label: "按投诉订单查询", desc: '输入用户投诉订单'},
-    ];
-
-    //查询条件
-    var condition = $scope.condition = {
-        stateFilter: stateList[0],
-        searchType: searchType[0],
-        searchKey: '',
-    };
-
-    //查询条件
-    $scope.query = {
-        startTime: moment().format("YYYY-MM-DD"),
-        endTime: moment().format("YYYY-MM-DD"),
-    };
-
-    //事件
-    $scope.event = {
-        statusChange: function (key, item) {
-            condition[key] = {}
-            condition[key].value = item.value;
-            condition[key].label = item.label;
-            condition[key].desc = item.desc;
-        },
-
-        search: function () {
-            // 就在当前 厂商|代理商|经销商的域下进行搜索
-            if (condition.searchKey === "") {
-                $scope.getPagedDataAsync(1, $scope.gridOptions.paginationPageSize, true);
-
-            } else {
-                $scope.getPagedDataAsync(1, $scope.gridOptions.paginationPageSize);
-            }
-
-        },
-
-    };
-
-    $scope.getStateText = function (value) {
-        return stateMap[value]
-    }
-
-    function setColumnDefs() {
-        $scope.gridOptions.columnDefs = [
-            {
-                field: 'complaint_id',
-                displayName: '投诉订单号',
-            },
-            {
-                field: 'complaint_time',
-                displayName: '投诉时间',
-            },
-            {
-                field: 'complainted_mchid',
-                displayName: '用户投诉商户号',
-            },
-            {
-                field: 'complaint_state',
-                displayName: '投诉状态',
-                cellTemplate: `<div class="temp-row" ng-class="{
-'text-danger':row.entity.complaint_state=='PENDING',
-'text-warning':row.entity.complaint_state=='PROCESSING',
-'text-success':row.entity.complaint_state=='PROCESSED',
-}">{{ grid.appScope.getStateText(row.entity.complaint_state)}}</div>`
-            },
-            {
-                field: 'complaint_detail', displayName: '投诉详情',
-                cellTemplate: '<div class="temp-row" ng-click="grid.appScope.showInfoDetailInfo(\'投诉详情\',\'complaint_detail\',row.entity)" >{{row.entity.complaint_detail}}</div>'
-            },
-            {
-                field: 'payer_phone',
-                displayName: '用户联系方式',
-            },
-            {
-                field: 'complaint_order_info', displayName: '订单详情',
-                cellTemplate: '<div class="temp-row" ng-click="grid.appScope.showInfoDetailInfo(\'投诉详情\',\'complaint_order_info\',row.entity)" >{{row.entity.complaint_order_info}}</div>'
-            },
-            {field: 'complaint_full_refunded', displayName: '是否已退款',},
-            {field: 'incoming_user_response', displayName: '是否有新回复',},
-            {
-                field: 'user_complaint_times', displayName: '投诉次数',
-                cellTemplate: `<div class="temp-row" ng-class="{
-'text-success':row.entity.user_complaint_times<=1,
-'text-danger':row.entity.user_complaint_times>1,
-}">{{row.entity.user_complaint_times}}</div>`
-            },
-            {
-                field: 'operation',
-                displayName: '操作',
-                enableFiltering: false,
-                enableSorting: false,
-                enableHiding: false,//禁止在列选择器中隐藏
-                enableColumnMenu: false,// 是否显示列头部菜单按钮
-                minWidth: 100,
-                cellTemplate: '<div class="grid-button">' +
-                    '<button class="btn btn-sm btn-rounded btn-success" ng-click="grid.appScope.reply(row.entity)">回复用户</button>' +
-                    '<button class="btn btn-sm btn-rounded btn-danger" ng-click="grid.appScope.submitComplaint(row.entity)">提交投诉处理</button>' +
-                    '</div>'
-            },
-
-        ];
-
-        var fields = $scope.gridOptions.columnDefs;
-        for (var index in fields) {
-            var item = fields[index];
-            if (item && item['minWidth'] == null) {
-                item['minWidth'] = 100;
-            }
-        }
-    }
-
-    $scope.setPagingData = function (data) {
-        var pagedData = data.data.dataList;
-        $scope.myData = pagedData;
-        $scope.gridOptions.totalItems = data.data.total;
-    };
-
-    $scope.getPagedDataAsync = function (curPage, pageSize, force) {
-        if ($scope.gridOptionsLoading) {
-            return;
-        }
-        var params = {
-            pageSize: pageSize,
-            pageIndex: curPage,
-        };
-
-        var query = $scope.query;
-        params.startTime = query.startTime
-        params.endTime = query.endTime
-
-        console.log(condition.searchType)
-        if (condition.searchKey !== "") {
-            params[condition.searchType.value] = condition.searchKey
-        }
-        if (condition.stateFilter.value) {
-            params.stateFilter = condition.stateFilter.value
-        }
-
-        $scope.gridOptionsLoading = true;
-        $http.get('/superadmin/getUserComplaintList', {
-            params: params
-        }).then(function (data) {
-            data = data.data
-            $scope.gridOptionsLoading = false;
-            $scope.setPagingData(data, curPage, pageSize);
-        }).catch(function (data) {
-            toaster.pop("error", "提示", "获取数据列表失败");
-        });
-    };
-
-    function initDataGrid() {
-        //首次加载表格
-        $scope.getPagedDataAsync($scope.gridOptions.paginationCurrentPage, $scope.gridOptions.paginationPageSize);
-    }
-
-    setColumnDefs();
-    initDataGrid();
-
-    $scope.reply = function (entity) {
-        var url = "/superadmin/replyComplaint";
-
-        $.confirm({
-            content: '<textarea type="text" id="replyRemark" placeholder="输入回复内容" style="width: 320px;height: 100px"/>',
-            title: "回复用户",
-            buttons: {
-                ok: {
-                    btnClass: 'btn-red',
-                    action: function () {
-                        var remark = $('#replyRemark').val()
-                        if (!remark) {
-                            return false
-                        }
-
-                        $http({
-                            method: 'POST',
-                            url: url,
-                            data: {
-                                complainted_mchid: entity.complainted_mchid,
-                                complaint_id: entity.complaint_id,
-                                remark: remark,
-                            }
-                        }).then(function (response) {
-                            if (response.data.result) {
-                                toaster.pop("success", "提示", "回复成功!");
-                            } else {
-                                toaster.pop("error", "提示", "回复失败!");
-                            }
-                            $scope.getPagedDataAsync($scope.gridOptions.paginationCurrentPage, $scope.gridOptions.paginationPageSize);
-                        }, function (response) {
-                            toaster.pop("error", "提示", "回复失败!");
-                        });
-                    }
-                },
-
-            }
-        });
-    };
-    $scope.submitComplaint = function (entity) {
-        var url = "/superadmin/submitComplaint";
-
-        $.confirm({
-            content: '确定提交投诉处理?',
-            buttons: {
-                ok: {
-                    btnClass: 'btn-red',
-                    action: function () {
-                        $http({
-                            method: 'POST',
-                            url: url,
-                            data: {
-                                complainted_mchid: entity.complainted_mchid,
-                                complaint_id: entity.complaint_id,
-                            }
-                        }).then(function (response) {
-                            if (response.data.result) {
-                                toaster.pop("success", "提示", "提交成功!");
-                            } else {
-                                toaster.pop("error", "提示", "提交失败!");
-                            }
-                            $scope.getPagedDataAsync($scope.gridOptions.paginationCurrentPage, $scope.gridOptions.paginationPageSize);
-                        }, function (response) {
-                            toaster.pop("error", "提示", "提交失败!");
-                        });
-                    }
-                },
-
-            }
-        });
-    };
-
-    $scope.showInfoDetailInfo = function (title, key, content) {
-        var detail = content[key]
-        $scope.infoDetail = {title: title, content: detail};
-        $(".devManageMain #detailInfoPanel").modal();
-    };
-
-    $scope.closeDetailPanel = function () {
-        $(".devManageMain #detailInfoPanel").modal("hide");
-    };
-
-}]);