Ver Fonte

trivial

mopybird há 2 anos atrás
pai
commit
839624c1fa
100 ficheiros alterados com 3922 adições e 2157 exclusões
  1. 4 1
      apilib/monetary.py
  2. 21 0
      apilib/systypes.py
  3. 1 1
      apilib/utils.py
  4. 26 3
      apilib/utils_datetime.py
  5. 10 0
      apilib/utils_mongo.py
  6. 13 1
      apilib/utils_string.py
  7. 14 1
      apilib/utils_sys.py
  8. 78 17
      apps/common/utils.py
  9. 0 2
      apps/thirdparties/aliyun.py
  10. 11 6
      apps/thirdparties/dingding.py
  11. 12 16
      apps/web/ad/models.py
  12. 9 6
      apps/web/ad/views.py
  13. 7 4
      apps/web/agent/define.py
  14. 52 24
      apps/web/agent/models.py
  15. 32 25
      apps/web/agent/views.py
  16. 449 38
      apps/web/common/models.py
  17. 1 1
      apps/web/common/proxy.py
  18. 15 8
      apps/web/common/transaction/__init__.py
  19. 3 35
      apps/web/common/transaction/pay/__init__.py
  20. 2 2
      apps/web/common/transaction/pay/alipay.py
  21. 4 3
      apps/web/common/transaction/pay/wechat.py
  22. 192 10
      apps/web/common/transaction/refund/__init__.py
  23. 30 27
      apps/web/common/transaction/refund/alipay.py
  24. 106 56
      apps/web/common/transaction/refund/wechat.py
  25. 71 66
      apps/web/common/transaction/withdraw.py
  26. 2 2
      apps/web/common/views.py
  27. 23 78
      apps/web/constant.py
  28. 1 4
      apps/web/core/__init__.py
  29. 2 5
      apps/web/core/bridge/alipay/__init__.py
  30. 0 100
      apps/web/core/bridge/alipay/authorder.py
  31. 2 65
      apps/web/core/bridge/alipay/base.py
  32. 1 1
      apps/web/core/device_define/changyuan.py
  33. 0 12
      apps/web/core/payment/__init__.py
  34. 12 8
      apps/web/core/payment/ali.py
  35. 17 14
      apps/web/core/payment/base.py
  36. 12 9
      apps/web/core/payment/wechat.py
  37. 17 4
      apps/web/dealer/define.py
  38. 105 198
      apps/web/dealer/models.py
  39. 4 7
      apps/web/dealer/tasks.py
  40. 12 14
      apps/web/dealer/transaction.py
  41. 3 127
      apps/web/dealer/transaction_deprecated.py
  42. 50 43
      apps/web/dealer/views.py
  43. 1 1
      apps/web/dealer/withdraw.py
  44. 79 60
      apps/web/device/timescale.py
  45. 2 6
      apps/web/eventer/DcFastCharge.py
  46. 1 1
      apps/web/eventer/duibiji.py
  47. 4 8
      apps/web/eventer/gaoborui.py
  48. 5 1
      apps/web/exceptions.py
  49. 64 7
      apps/web/helpers.py
  50. 60 58
      apps/web/management/tasks.py
  51. 13 0
      apps/web/models.py
  52. 1 1
      apps/web/report/ledger.py
  53. 1 1
      apps/web/services/bluetooth/service.py
  54. 10 3
      apps/web/superadmin/views.py
  55. 338 292
      apps/web/user/models.py
  56. 5 5
      apps/web/user/tasks.py
  57. 415 253
      apps/web/user/transaction_deprecated.py
  58. 1 3
      apps/web/user/utils.py
  59. 6 6
      apps/web/user/views.py
  60. 0 5
      apps/web/utils.py
  61. 1 1
      apps/web/wechat3rd/views.py
  62. 5 1
      configs/base.py
  63. 1 1
      configs/servers.py
  64. 535 0
      library/RuralCreditUnion/pay.py
  65. 2 3
      library/alipay/__init__.py
  66. 0 1
      library/jd/__init__.py
  67. 1 44
      library/jd/exceptions.py
  68. 89 11
      library/jd/pay.py
  69. 1 2
      library/jdopen/__init__.py
  70. 18 2
      library/jdopen/base.py
  71. 2 1
      library/jdopen/client/__init__.py
  72. 1 1
      library/jdopen/client/api/__init__.py
  73. 1 1
      library/jdopen/client/api/attach.py
  74. 2 2
      library/jdopen/client/api/audit.py
  75. 3 3
      library/jdopen/client/api/complete.py
  76. 6 6
      library/jdopen/client/api/customer.py
  77. 0 72
      library/jdopen/client/api/settleAccount.py
  78. 7 7
      library/jdopen/client/api/shop.py
  79. 120 8
      library/jdopen/client/api/support.py
  80. 27 25
      library/jdopen/client/base.py
  81. 1 10
      library/jdopen/constants.py
  82. 9 33
      library/jdopen/exceptions.py
  83. 154 47
      library/jdopen/pay.py
  84. 377 0
      library/jdpsi/client.py
  85. 3 8
      library/jdpsi/constants.py
  86. 1 1
      library/qiben/simmanager.py
  87. 2 1
      library/sms/ucpaas.py
  88. 1 1
      library/sms/zthy.py
  89. 1 0
      library/wechatpayv3/client/__init__.py
  90. 1 0
      library/wechatpayv3/client/api/__init__.py
  91. 14 8
      library/wechatpayv3/client/api/complaint.py
  92. 22 14
      library/wechatpayv3/client/api/media.py
  93. 11 14
      library/wechatpayv3/core.py
  94. 20 25
      library/wechatpayv3/utils.py
  95. 2 2
      library/wechatpy/client/__init__.py
  96. 2 2
      library/wechatpy/client/base.py
  97. 18 17
      library/wechatpy/component.py
  98. 3 5
      library/wechatpy/pay/__init__.py
  99. 26 22
      middlewares/validPermission.py
  100. 0 0
      patch.py

+ 4 - 1
apilib/monetary.py

@@ -6,6 +6,7 @@ from bson.decimal128 import Decimal128
 
 from apilib.numerics import force_decimal, quantize, UnitBase
 
+
 class MoneyComparisonError(TypeError):
     # This exception was needed often enough to merit its own
     # Exception class.
@@ -60,7 +61,7 @@ class Money(UnitBase):
         if isinstance(other, Money):
             return self.__class__(self._amount - other.amount)
         else:
-            return self.__class__(other) - self
+            return self - self.__class__(other)
 
     def __mul__(self, other):
         if isinstance(other, Money):
@@ -131,6 +132,7 @@ class RMB(Money):
     def yuan_to_fen(cls, yuan):
         return int((yuan * 100))
 
+
 class VirtualCoin(Money):
     __currency__ = 'VirtualCoin'
 
@@ -153,6 +155,7 @@ def sum_virtual_coin(list_):
 def sum_accuracy_rmb(list_):
     return sum(list_, AccuracyRMB(0))
 
+
 class Ratio(object):
     def __init__(self, amount):
         if isinstance(amount, Ratio):

+ 21 - 0
apilib/systypes.py

@@ -91,3 +91,24 @@ class NoEmptyValueDict(dict):
             return super(NoEmptyValueDict, self).setdefault(k, d)
 
 
+class NoInstantiateMeta(type):
+    def __call__(cls, *args, **kwargs):
+        raise TypeError("BaseIterConstant class cannot be instantiated.")
+
+
+class BaseIterConstant(object):
+    """
+    代替IterConstant基类
+    """
+    __metaclass__ = NoInstantiateMeta
+
+    @classmethod
+    def choice(cls, call=None):
+        """
+        call 表示需要对获取的值执行的方法 比如int、str、float、lambda x: x+1
+        """
+        if call and callable(call):
+            return [call(getattr(cls, attr)) for attr in dir(cls) if not attr.startswith("_") and attr.isupper()]
+        else:
+            return [getattr(cls, attr) for attr in dir(cls) if not attr.startswith("_") and attr.isupper()]
+

+ 1 - 1
apilib/utils.py

@@ -460,7 +460,7 @@ def convert_encoding(source_file, target_file = None, source_encoding = None, ta
 def rec_update_dict(d, update_dict, firstLevelOverwrite = False):
     """
     递归地更新字典
-    overwrite特指是否覆盖第一层dict
+    :param firstLevelOverwrite: 特指是否覆盖第一层dict
     :param d:
     :param update_dict:
     :return:

+ 26 - 3
apilib/utils_datetime.py

@@ -102,7 +102,7 @@ datetime_to_timestamp = dt_to_timestamp
 def today_format_str(): return datetime.datetime.now().strftime("%Y-%m-%d")
 
 
-def yesterday_format_str(): return (datetime.datetime.now() - datetime.timedelta(days = 1)).strftime('%Y-%m-%d')
+def yesterday_format_str():  return (datetime.datetime.now() - datetime.timedelta(days = 1)).strftime('%Y-%m-%d')
 
 
 def the_day_before_yesterday_format_str(): return (datetime.datetime.now() - datetime.timedelta(days = 2)).strftime(
@@ -125,7 +125,7 @@ def first_day_datetime_of_month(date = None, time_format = '%Y-%m-%d %H:%M:%S'):
     return datetime.datetime.strptime('%s-%s-01 00:00:00' % (date.year, date.month), time_format)
 
 
-def to_datetime(ts_or_str, time_format="%Y-%m-%d %H:%M:%S"):
+def to_datetime(ts_or_str, time_format = "%Y-%m-%d %H:%M:%S"):
     if isinstance(ts_or_str, basestring):
         return datetime.datetime.strptime(ts_or_str, time_format)
     elif isinstance(ts_or_str, int) or isinstance(ts_or_str, long) or isinstance(ts_or_str, float):
@@ -136,7 +136,7 @@ def to_datetime(ts_or_str, time_format="%Y-%m-%d %H:%M:%S"):
         assert False, u'参数错误'
 
 
-def to_timestamp(ts_or_str, time_format="%Y-%m-%d %H:%M:%S"):
+def to_timestamp(ts_or_str, time_format = "%Y-%m-%d %H:%M:%S"):
     if isinstance(ts_or_str, basestring):
         return dt_to_timestamp(to_datetime(ts_or_str, time_format))
     elif isinstance(ts_or_str, int) or isinstance(ts_or_str, long) or isinstance(ts_or_str, float):
@@ -154,3 +154,26 @@ def datetime_between_months(months = 6):
     end = datetime.datetime.now() + datetime.timedelta(hours = 1)
 
     return begin, end
+
+
+def last_day_datetime_of_this_month(inputTime = None):
+    if not inputTime:
+        inputTime = datetime.datetime.now()
+    nextMonthDay = inputTime + relativedelta(months = 1)
+    return first_day_datetime_of_month(nextMonthDay) - datetime.timedelta(days=1)
+
+
+def get_month_days(year, month):
+    """
+    获取一个月的所有天的列表
+    :param year: 年份
+    :param month: 月份
+    :return: 该月所有天的列表
+    """
+    # 获取该月的第一天
+    first_day = datetime.date(year, month, 1)
+    # 获取该月的下一个月的第一天
+    next_month = datetime.date(year, month + 1, 1) if month != 12 else datetime.date(year + 1, 1, 1)
+    # 生成该月所有天的列表
+    days = [(first_day + datetime.timedelta(days=i)).day for i in range((next_month - first_day).days)]
+    return days

+ 10 - 0
apilib/utils_mongo.py

@@ -87,3 +87,13 @@ def format_dot_key(rule_dict, to_dot = False):
             rv[k.replace('.', '-')] = v
 
     return rv
+
+
+def dict_field_with_money(mydict):
+    for key, value in mydict.iteritems():
+        if hasattr(value, 'mongo_amount'):
+            mydict[key] = value.mongo_amount
+        else:
+            mydict[key] = value
+
+    return mydict

+ 13 - 1
apilib/utils_string.py

@@ -14,6 +14,7 @@ from typing import Iterable
 
 from apilib.utils_sys import PY3
 
+
 def cn(_): return unicode(_).encode('utf-8')
 
 
@@ -123,4 +124,15 @@ def make_title_from_dict(valueDictList):
 
                 result += '\\n\\n%s:%s%s' % (k,tabs,v)
     result += '\\n'
-    return result
+    return result
+
+
+def make_qr_code(url, logoUrl = None):
+    from io import BytesIO
+    import qrcode
+    img = qrcode.make(url)
+    bytesIo = BytesIO()
+    img.save(bytesIo, format = 'PNG')
+
+    import base64
+    return base64.b64encode(bytesIo.getvalue())

+ 14 - 1
apilib/utils_sys.py

@@ -2,7 +2,9 @@
 # !/usr/bin/env python
 
 import logging
+import shutil
 import sys
+import tempfile
 import threading
 import uuid
 
@@ -130,7 +132,8 @@ class MemcachedLock(object):
             result = self.mc.delete(self.key)
             logger.debug('=== MemcachedLock === delete result is: {}'.format(str(result)))
         else:
-            logger.warn("=== MemcachedLock === no lock to release {}. Increase TIMEOUT of lock operations".format(repr(self)))
+            logger.warn(
+                "=== MemcachedLock === no lock to release {}. Increase TIMEOUT of lock operations".format(repr(self)))
 
     @property
     def locked(self):
@@ -139,6 +142,7 @@ class MemcachedLock(object):
 
 PY3 = sys.version_info[0] == 3
 
+
 @contextmanager
 def MyStringIO():
     from six import StringIO
@@ -147,3 +151,12 @@ def MyStringIO():
         yield fi
     finally:
         fi.close()
+
+
+class TemporaryDirectory(object):
+    def __enter__(self):
+        self.name = tempfile.mkdtemp()
+        return self.name
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        shutil.rmtree(self.name)

+ 78 - 17
apps/common/utils.py

@@ -4,10 +4,17 @@ import calendar
 import datetime
 import math
 import numbers
+from typing import TYPE_CHECKING, Optional
 
 from django.conf import settings
 from pandas import date_range
 
+from apps.web.constant import Const
+
+if TYPE_CHECKING:
+    from apps.web.device.models import DeviceDict
+    from apps.web.dealer.models import Dealer
+
 
 class CoordinateTrans(object):
     """
@@ -70,9 +77,6 @@ class CoordinateTrans(object):
             pass
 
 
-coordinateHandler = CoordinateTrans()
-
-
 class IntToHex(object):
     """
     数字转换16进制字符串
@@ -94,18 +98,7 @@ class IntToHex(object):
             return ""
         return h[-2:] + IntToHex._reverse_hex(h[:-2])
 
-    def __call__(self, number, lens=4, reverse=False):
-        """
-
-        :param number:  带转换的数字
-        :type number: int
-        :param lens: 转换后的长度
-        :type lens: int
-        :param reverse: 是否高低位互换
-        :type reverse: bool
-        :return:
-        :rtype: str
-        """
+    def __call__(self, number, lens=4, reverse=False):  # type:(Optional[numbers.Integral, str], int, bool) -> str
         if not isinstance(lens, numbers.Integral):
             raise TypeError("type of lens must be Integral")
         self._add_trans_number(number)
@@ -118,6 +111,73 @@ class IntToHex(object):
         return numberHex
 
 
+class DealerGetPackages(object):
+
+    def __init__(self, device, visitor, isTemp=False):   # type: (DeviceDict, Dealer, bool) -> None
+        self._device = device
+        self._visitor = visitor
+        self._isTemp = isTemp
+
+    @property
+    def unitPrice(self):
+        return self._device.otherConf.get("unit_price", "")
+
+    @property
+    def devData(self):  # type: ()-> dict
+        """
+        设备数据
+        """
+        if not self._device.is_registered:
+            return {
+                "id": self._device.devNo,
+                "maxCoins": self._device.otherConf.get("maxCoins", 4),
+                "devNo": self._device.devNo,
+                "type": u"脉冲",
+                "typeCode": u"201",
+                "groupName": u"测试",
+                "groupNumber": 666,
+            }
+
+        devData = {
+            "id": self._device.devNo,
+            "maxCoins": self._device.otherConf.get("maxCoins", 4),
+            "devNo": self._device.devNo,
+            "type": self._device.devTypeName,
+            "typeCode": self._device.devTypeCode,
+            "groupName": self._device.group.groupName,
+            "groupNumber": self._device.groupNumber
+        }
+
+        if self._device.devTypeCode in [Const.DEVICE_TYPE_CODE_HP_GATE]:
+            chargeIndex = {}
+            if self._device.otherConf.get("controlEnter") in ("1", "2"):
+                chargeIndex["enter"] = "idle"
+            if self._device.otherConf.get("controlExit") in ("1", "2"):
+                chargeIndex["exit"] = "idle"
+
+            devData.update({"chargeIndex": chargeIndex})
+
+        return devData
+
+    @property
+    def ruleList(self):  # type: () -> list
+        """
+        套餐列表
+        """
+        # 此处可以将套餐抽象化表达
+        if not self._device.is_registered:
+            rules = [
+                {"id": "".format(i), "name": u"测试", "coins": i, "price": i}
+                for i in range(1, 6)
+            ]
+        else:
+            rules = []
+
+        return rules
+
+
+coordinateHandler = CoordinateTrans()
+
 int_to_hex = IntToHex()
 
 
@@ -146,7 +206,7 @@ def get_date_range(st, et, freq='D', reverse = True):
     if reverse:
         return list(reversed(date_range(st, et, freq=freq).tolist()))
     else:
-        return list(date_range(st, et, freq = freq).tolist())
+        return list(date_range(st, et, freq=freq).tolist())
 
 
 def get_test_point(domain, key):
@@ -164,4 +224,5 @@ def support_test_point(point):
     if test_point == 'yes':
         return True
     else:
-        return False
+        return False
+

+ 0 - 2
apps/thirdparties/aliyun.py

@@ -17,8 +17,6 @@ from aliyunsdkcore.client import AcsClient
 from aliyunsdkcore.request import CommonRequest
 from aliyunsdkunimkt.request.v20181207 import PopUpQueryRequest, QueryPromotionRequest
 from aliyunsdkunimkt.request.v20181212 import QueryUnionPromotionRequest, GetUnionTaskStatusRequest, \
-    CreateProxyBrandUserRequest, QueryUnionSumChannelDataRequest, QueryContentListRequest, QueryTaskRuleLimitRequest, \
-    CreateUnionTaskRequest, EndUnionTaskRequest, QueryUnionTaskInfoRequest, QueryUnionTaskListRequest, \
     RegistDeviceRequest
 from django.conf import settings
 from requests import ConnectionError

+ 11 - 6
apps/thirdparties/dingding.py

@@ -11,14 +11,19 @@ logger = logging.getLogger(__name__)
 
 _DINGDING_TALK_URL = 'https://oapi.dingtalk.com/robot/send?access_token=acb44458217bd584bd3c29129c7ac6e59b6484314b441374b8b79dab9181f7ee'
 
+SIM_CARD_EXPIRE_URL = "https://oapi.dingtalk.com/robot/send?access_token=6e0a9a36924477a9bbb60dd4abd2a0c957ff489d4d19621ccf3b533656258a67"
+
 
 class DingDingRobot(object):
-    def __init__(self, url=_DINGDING_TALK_URL):
+    def __init__(self, url = _DINGDING_TALK_URL):
         self.url = url
 
     def send_msg(self, msg):
-        data = {"msgtype": "text", "text": {"content": force_text(msg)}}
-        headers = {'Content-Type': 'application/json;charset=UTF-8'}
-        send_data = json.dumps(data).encode('utf-8')
-        response = requests.post(url=self.url, data=send_data, headers=headers, timeout=15)
-        logger.debug(response.text)
+        try:
+            data = {"msgtype": "text", "text": {"content": force_text(msg)}}
+            headers = {'Content-Type': 'application/json;charset=UTF-8'}
+            send_data = json.dumps(data).encode('utf-8')
+            response = requests.post(url = self.url, data = send_data, headers = headers, timeout = 15)
+            logger.debug(response.text)
+        except Exception as e:
+            logger.exception(e)

+ 12 - 16
apps/web/ad/models.py

@@ -5,23 +5,19 @@
     ~~~~~~~~~ad
 
 """
-import logging
-import random
-from collections import OrderedDict
 
 import datetime
+import logging
+import sys
+from collections import OrderedDict
 
 import simplejson as json
-import sys
 import user_agents
-from aliyunsdkcore.client import AcsClient
-from aliyunsdkunimkt.request.v20181212 import RegistDeviceRequest, ChargeLaunchRequest
 from bson.objectid import ObjectId
-from django.conf import settings
 from django.contrib.auth.hashers import make_password
 from django.utils.module_loading import import_string
-from mongoengine import StringField, DateTimeField, BooleanField, ListField, DoesNotExist, \
-    IntField, FloatField, DictField, EmbeddedDocument, DynamicDocument
+from mongoengine import StringField, DateTimeField, BooleanField, ListField, IntField, FloatField, DictField, \
+    EmbeddedDocument, DynamicDocument
 from typing import Union, List, AnyStr, Dict, Any, Optional, TYPE_CHECKING
 
 from apilib.utils_datetime import today_format_str, yesterday_format_str
@@ -32,17 +28,17 @@ from apps.web.common.models import UserSearchable, District
 from apps.web.constant import Const, AdType, AdSpace
 from apps.web.core.db import CustomizedSequenceField
 from apps.web.core.db import Searchable
-
 from apps.web.device.models import Device
-
 from apps.web.utils import CustomizedValidationError, detect_app_from_ua, is_user
 
+
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
     from apps.web.dealer.models import Dealer
     from apps.web.user.models import MyUser
     from apps.web.device.models import DeviceDict
+    from library.mongo_django_auth_backport.auth import User
 
 
 def nullable(obj, nil=''):
@@ -137,7 +133,7 @@ class Advertisement(UserSearchable):
     startTime = DateTimeField(verbose_name="开始时间", default=datetime.datetime.now)
     endTime = DateTimeField(verbose_name="结束时间", default=datetime.datetime.now() + datetime.timedelta(days=365))
 
-    groupName = StringField(verbose_name="商户名称", default="")
+    groupName = StringField(verbose_name="地址组名称", default="")
     provinceId = StringField(verbose_name="省份ID", default="")
     cityId = StringField(verbose_name="城市ID", default="")
     areaId = StringField(verbose_name="区域ID", default="")
@@ -529,7 +525,7 @@ class Advertisement(UserSearchable):
                 'adShow': 'show',
                 'url': ruhui['url'],
                 'showType': 'floatRedpack',
-                'img': 'https://resource.washpayer.com/ad/redpack2.png',
+                'img': 'https://cdn.washpayer.com/promotion/redpack2.png',
                 'title': '点击入会天猫领取红包',
             }
             logger.debug(
@@ -549,10 +545,10 @@ class Advertisement(UserSearchable):
             return None
 
         elif showType in ['floatJump', 'float']:
-            return 'https://resource.washpayer.com/ad/payafter_float_jump_1.png'
+            return 'https://cdn.washpayer.com/promotion/payafter_float_jump_1.png'
 
         else:
-            return 'https://resource.washpayer.com/ad/payafter_banner_2.png'
+            return 'https://cdn.washpayer.com/promotion/payafter_banner_2.png'
 
     @classmethod
     def _fetch_payafter_ad(cls, ua, user, dealer, device=None):
@@ -613,7 +609,7 @@ class Advertisement(UserSearchable):
 
     @classmethod
     def fetch_payafter_ad(cls, ua, user, dealer, device=None):
-        # type: (str, MyUser, Dealer, DeviceDict)->Optional[dict]
+        # type: (str, User, Dealer, DeviceDict)->Optional[dict]
 
         ad_dict = None
 

+ 9 - 6
apps/web/ad/views.py

@@ -290,6 +290,8 @@ def getDealerList(request):
     return JsonResponse({'result': 1, 'description': '', 'payload': dealers})
 
 
+
+
 @error_tolerate(nil = JsonErrorResponse(u'获取地址列表失败'))
 @permission_required(ROLE.manager)
 def getAddressList(request):
@@ -636,17 +638,18 @@ def exportExcel(request):
                                               user = manager,
                                               **(query.raw))
 
-    file_path, offline_task = OfflineTask.issue_export_report(offline_task_name = offline_task_name,
-                                                              process_func_name = 'generate_ad_excel_report',
-                                                              task_type = OfflineTaskType.AD_REPORT,
-                                                              userid = str(manager.id),
-                                                              role = ROLE.manager)
-
     #: make queryAttrs serializable
     queryAttrs = query.attrs
     queryAttrs['dateTimeAdded__lte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__lte'])
     queryAttrs['dateTimeAdded__gte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__gte'])
 
+    file_path, offline_task = OfflineTask.issue_export_report(offline_task_name = offline_task_name,
+                                                              process_func_name = 'generate_ad_excel_report',
+                                                              task_type = OfflineTaskType.AD_REPORT,
+                                                              userid = str(manager.id),
+                                                              role = ROLE.manager,
+                                                              paras = queryAttrs)
+
     task_caller(func_name = offline_task.process_func_name,
                 offline_task_id = str(offline_task.id),
                 filepath = file_path,

+ 7 - 4
apps/web/agent/define.py

@@ -21,8 +21,9 @@ class AGENT_INCOME_SOURCE(IterConstant):
     DEALER_DISABLE_AD = 'dealer_disable_ad'
     DEALER_DEVICE_FEE = 'dealer_device_fee'
     INSURANCE = 'insurance'
-    REDPACK = 'redpack'
     REFUND_CASH = 'refundCash'
+    REVOKE_REFUND_CASH = 'revokeRefundCash'
+    REDPACK = 'redpack'
 
 
 # 代理商的收益来源的翻译(前台展示使用字段的名称)
@@ -64,23 +65,25 @@ AgentConst.MAP_USER_SOURCE_TO_DEALER_SOURCE = {
     USER_RECHARGE_TYPE.RECHARGE_REDPACK: AGENT_INCOME_SOURCE.REDPACK,
 
     USER_RECHARGE_TYPE.RECHARGE_INSURANCE: AGENT_INCOME_SOURCE.INSURANCE,
-    USER_RECHARGE_TYPE.REFUND_CASH: AGENT_INCOME_SOURCE.REFUND_CASH
+    USER_RECHARGE_TYPE.REFUND_CASH: AGENT_INCOME_SOURCE.REFUND_CASH,
+    USER_RECHARGE_TYPE.REVOKE_REFUND_CASH: AGENT_INCOME_SOURCE.REVOKE_REFUND_CASH
 }
 
 
 AgentConst.MAP_SOURCE_TO_TYPE = {
     AGENT_INCOME_SOURCE.AD: AGENT_INCOME_TYPE.AD,
+    AGENT_INCOME_SOURCE.REDPACK: AGENT_INCOME_TYPE.AD,
     AGENT_INCOME_SOURCE.DEALER_WITHDRAW_FEE: AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE,
     AGENT_INCOME_SOURCE.DEALER_CARD_FEE: AGENT_INCOME_TYPE.DEALER_CARD_FEE,
     AGENT_INCOME_SOURCE.DEALER_API_QUOTA: AGENT_INCOME_TYPE.DEALER_API_QUOTA,
     AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
 
     AGENT_INCOME_SOURCE.INSURANCE: AGENT_INCOME_TYPE.INSURANCE,
+    AGENT_INCOME_SOURCE.REFUND_CASH: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
+    AGENT_INCOME_SOURCE.REVOKE_REFUND_CASH: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
 
     AGENT_INCOME_SOURCE.DEALER_DISABLE_AD: AGENT_INCOME_TYPE.DEALER_DISABLE_AD,
 
-    AGENT_INCOME_SOURCE.REDPACK: AGENT_INCOME_TYPE.AD,
-    AGENT_INCOME_SOURCE.REFUND_CASH: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE
 }
 
 AgentConst.MAP_TYPE_TO_FIELD = {

+ 52 - 24
apps/web/agent/models.py

@@ -363,7 +363,8 @@ class Agent(CapitalUser):
                     "northIp": ip,
                     "northPort": port
                 }
-            except Exception:
+            except Exception as e:
+                logger.error("agent filter got an error = {}".format(e))
                 pass
 
             item = {
@@ -633,16 +634,19 @@ class Agent(CapitalUser):
         为了方便识别,如果获取默认代理商失败,需要报错为默认代理商找不到
         """
         try:
-            return Agent.objects(id = str(settings.MY_PRIMARY_AGENT_ID)).get()
+            return Agent.objects(id = str(Agent.inhouse_prime_agent_id())).get()
         except DoesNotExist:
             raise PrimaryAgentDoesNotExist('failed to get default primary agent')
 
     @property
     def inhouse_prime_agent(self):
-        if str(self.id) == str(settings.MY_PRIMARY_AGENT_ID):
+        if str(self.id) == str(self.inhouse_prime_agent_id()):
             return self
         else:
             return Agent.get_inhouse_prime_agent()
+    @staticmethod
+    def inhouse_prime_agent_id():
+        return str(settings.MY_PRIMARY_AGENT_ID)
 
     @property
     def customizedCashflowAllowable(self):
@@ -880,25 +884,26 @@ class Agent(CapitalUser):
     @classmethod
     def withdraw_gateway_list(cls, source_key):
         is_ledger, pay_app_type, occupant_id, tokens = cls._parse_source_key(source_key)
+
         if not is_ledger:
-            return is_ledger, {
-                'alipay': WithdrawGateway(AliApp.get_null_app(), False),
-                'wechat': WithdrawGateway(WechatPayApp.get_null_app(), False),
-                'wechatV3': WithdrawGateway(WechatPayApp.get_null_app(), False)
+            return is_ledger, None, {
+                'alipay': WithdrawGateway(AliApp.get_null_app()),
+                'wechat': WithdrawGateway(WechatPayApp.get_null_app()),
+                'wechatV3': WithdrawGateway(WechatPayApp.get_null_app())
             }
 
-        if pay_app_type != PayAppType.WECHAT:
-            raise UserServerException(u'系统配置错误(第三方支付),请联系平台客服(1001)')
-
         agent = cls.objects(id = occupant_id).first()
         if not agent:
             raise UserServerException(u'系统配置错误(第三方支付),请联系平台客服(1002)')
 
+        if pay_app_type != PayAppType.WECHAT:
+            raise UserServerException(u'系统配置错误(第三方支付),请联系平台客服(1001)')
+
         if not agent.withdrawApps:
-            return is_ledger, {
-                'alipay': None,
-                'wechat': agent.my_wechat_pay_app.new_withdraw_gateway(is_ledger = is_ledger, gateway_version = 'v1'),
-                'wechatV3': agent.my_wechat_pay_app.new_withdraw_gateway(is_ledger = is_ledger, gateway_version = 'v3')
+            return is_ledger, agent, {
+                'alipay': WithdrawGateway(AliApp.get_null_app()),
+                'wechat': agent.my_wechat_pay_app.new_withdraw_gateway(gateway_version = 'v1'),
+                'wechatV3': agent.my_wechat_pay_app.new_withdraw_gateway(gateway_version = 'v3')
             }
         else:
             if source_key not in agent.withdrawApps:
@@ -906,19 +911,25 @@ class Agent(CapitalUser):
 
             withdraw_entity = agent.withdrawApps[source_key]  # type: WithdrawEntity
 
-            withdraw_entity.alipay_app.occupantId = str(agent.id)
-            withdraw_entity.alipay_app.occupant = agent
+            pay_rv = {
+                'alipay': WithdrawGateway(AliApp.get_null_app())
+            }
 
+            if withdraw_entity.alipay_app:
+                withdraw_entity.alipay_app.occupantId = str(agent.id)
+                withdraw_entity.alipay_app.occupant = agent
+
+                pay_rv['alipay'] = withdraw_entity.alipay_app.new_withdraw_gateway()
+
+            # 微信提现必须配置, 接口可能不支持提现, 但是手工提现以微信为准
             withdraw_entity.wechat_app.occupantId = str(agent.id)
             withdraw_entity.wechat_app.occupant = agent
 
-            return is_ledger, {
-                'alipay': withdraw_entity.alipay_app.new_withdraw_gateway(is_ledger = is_ledger),
-                'wechat': withdraw_entity.wechat_app.new_withdraw_gateway(
-                    is_ledger = is_ledger, gateway_version = 'v1'),
-                'wechatV3': withdraw_entity.wechat_app.new_withdraw_gateway(
-                    is_ledger = is_ledger, gateway_version = 'v3')
-            }
+            pay_rv.update({
+                'wechat': withdraw_entity.wechat_app.new_withdraw_gateway(gateway_version = 'v1'),
+                'wechatV3': withdraw_entity.wechat_app.new_withdraw_gateway(gateway_version = 'v3')
+            })
+            return is_ledger, agent, pay_rv
 
     @staticmethod
     def get_platform_wechat_manager_app():
@@ -1280,11 +1291,26 @@ class Agent(CapitalUser):
         """
         return self.incr_income(source, source_key, -money)
 
+    def check_withdraw_min_fee(self, income_type, pay_type, amount):
+        if pay_type == WITHDRAW_PAY_TYPE.BANK:
+            minimum = RMB(settings.WITHDRAW_MINIMUM)
+        else:
+            if income_type == AGENT_INCOME_TYPE.AD:
+                minimum = RMB(settings.AD_WITHDRAW_MINIMUM)
+            elif income_type == AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE:
+                minimum = RMB(settings.SIM_INCOME_WITHDRAW_MINIMUM)
+            else:
+                minimum = RMB(settings.WITHDRAW_MINIMUM)
+
+        if amount < minimum:
+            raise ServiceException(
+                {'result': 0, 'description': u"提现实际到账金额不能少于%s元" % (minimum,), 'payload': {}})
+
     def new_withdraw_record(self, withdraw_gateway, pay_entity, source_key, income_type, amount, pay_type, manual,
                             recurrent):
         # type: (WithdrawGateway, WithdrawBankCard, str, str, RMB, str, bool, bool) -> WithdrawRecord
 
-        if income_type == AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE:
+        if income_type in [AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE, AGENT_INCOME_TYPE.AD]:
             withdraw_fee_ratio = Permillage('0.00')  # type: Permillage
         else:
             withdraw_fee_ratio = Const.PLATFORM_DEFAULT_WITHDRAW_FEE_RATIO  # type: Permillage
@@ -1303,6 +1329,8 @@ class Agent(CapitalUser):
 
         actual_pay = amount - service_fee - bank_trans_fee  # type: RMB
 
+        self.check_withdraw_min_fee(income_type, pay_type, actual_pay)
+
         return WithdrawRecord.create(self,
                                      withdraw_gateway = withdraw_gateway,
                                      pay_entity = pay_entity,

+ 32 - 25
apps/web/agent/views.py

@@ -1693,36 +1693,43 @@ def withdrawEntry(request):
     if source_key not in user.balance_dict(source_type):
         return ErrorResponseRedirect(error = u'提现参数错误,请刷新后重试')
 
-    is_ledger, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
+    is_ledger, agent, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
     if not is_ledger:
         return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10005)')
 
     wechat_withdraw_gateway = withdraw_gateway_list['wechat']
-    if not wechat_withdraw_gateway:
-        return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10006)')
-
-    code = request.GET.get('code', None)
-    if not code:
-        redirect = request.GET.get('redirect')
-        return ExternalResponseRedirect(
-            WechatAuthBridge(wechat_withdraw_gateway.app).generate_auth_url_base_scope(concat_server_end_url(
-                uri = '/agent/withdraw/entry?sourceType={source_type}&sourceId={source_key}'.format(
-                    source_type = source_type,
-                    source_key = source_key
-                )), payload = base64.b64encode(redirect)))
-    else:
-        auth_bridge = WechatAuthBridge(wechat_withdraw_gateway.app)
-        openId = auth_bridge.authorize(code)
-        if openId is not None:
-            redirect = base64.b64decode(request.GET.get('payload'))
-            redirect = add_query(redirect, {
-                'sourceType': source_type,
-                'sourceId': source_key,
-                'openId': openId
-            })
-            return FrontEndResponseRedirect(redirect)
+    if wechat_withdraw_gateway.support_withdraw and not wechat_withdraw_gateway.manual_withdraw:
+        code = request.GET.get('code', None)
+        if not code:
+            redirect = request.GET.get('redirect')
+            return ExternalResponseRedirect(
+                WechatAuthBridge(wechat_withdraw_gateway.app).generate_auth_url_base_scope(concat_server_end_url(
+                    uri = '/agent/withdraw/entry?sourceType={source_type}&sourceId={source_key}'.format(
+                        source_type = source_type,
+                        source_key = source_key
+                    )), payload = base64.b64encode(redirect)))
         else:
-            return ErrorResponseRedirect(error = u'微信授权失败,请刷新后重试')
+            auth_bridge = WechatAuthBridge(wechat_withdraw_gateway.app)
+            openId = auth_bridge.authorize(code)
+            if openId is not None:
+                redirect = base64.b64decode(request.GET.get('payload'))
+                redirect = add_query(redirect, {
+                    'sourceType': source_type,
+                    'sourceId': source_key,
+                    'openId': openId
+                })
+                return FrontEndResponseRedirect(redirect)
+            else:
+                return ErrorResponseRedirect(error = u'微信授权失败,请刷新后重试')
+    else:
+        redirect = request.GET.get('redirect')
+
+        redirect = add_query(redirect, {
+            'sourceType': source_type,
+            'sourceId': source_key,
+            'openId': ''
+        })
+        return FrontEndResponseRedirect(redirect)
 
 
 @error_tolerate(logger = logger, nil = JsonErrorResponse(u'系统错误'))

+ 449 - 38
apps/web/common/models.py

@@ -19,14 +19,14 @@ from apilib.utils_json import json_dumps, json_loads
 from apilib.utils_sys import memcache_lock, ThreadLock
 from apps.web import district
 from apps.web.agent.define import AGENT_INCOME_TYPE
-from apps.web.common.transaction import WithdrawStatus, WITHDRAW_PAY_TYPE, WithdrawHandler, OrderNoMaker, OrderMainType
+from apps.web.common.transaction import WithdrawStatus, WITHDRAW_PAY_TYPE, WithdrawHandler, OrderNoMaker, OrderMainType, \
+    RefundSubType
 from apps.web.constant import Const
-from apps.web.core import APP_KEY_DELIMITER, PayAppType, ROLE
+from apps.web.core import ROLE
 from apps.web.core.db import Searchable, StrictDictField, MonetaryField, RoleBaseDocument, PermillageField, \
     AccuracyMoneyField, BooleanIntField, BaseDocument
 from apps.web.core.exceptions import ImproperlyConfigured
-from apps.web.core.models import WechatPayApp
-from apps.web.core.payment import WithdrawGateway
+from apps.web.core.payment import WithdrawGateway, PaymentGateway
 from apps.web.dealer.define import DEALER_INCOME_TYPE
 from apps.web.utils import LimitAttemptsManager
 from library.misc import BankAPI
@@ -36,6 +36,8 @@ if TYPE_CHECKING:
     from pymongo.results import UpdateResult
     from apps.web.device.models import DeviceDict, GroupDict
     from apps.web.core import PayAppBase
+    from apps.web.user.models import RechargeRecord
+    from apps.web.dealer.models import DealerRechargeRecord
 
 
 logger = logging.getLogger(__name__)
@@ -691,6 +693,14 @@ class CapitalUser(UserSearchable):
 
     bankWithdrawFee = BooleanField(verbose_name = u"银行卡提现手续开关(资金池代理商开关和这个开关为双开关)", default = True)
 
+    @property
+    def is_new_join(self):
+        one_month_before = datetime.datetime.now() - datetime.timedelta(days = 90)
+        if self.dateTimeAdded > one_month_before:
+            return True
+        else:
+            return False
+
     @classmethod
     def income_field_name(cls, income_type):
         # type: (str)->str
@@ -895,6 +905,35 @@ class CapitalUser(UserSearchable):
             total += self.sub_frozen_balance(income_type)
         return total
 
+    def re_freeze_balance(self, income_type, money, source_key, transaction_id):  # type:(str, RMB, str, str)->bool
+        """
+        某种情况下, 即使反馈成功, 后续也会退单, 这个时候手工处理, 重新设置经销商inhand列表, 但是不在扣除金额(已经在成功的时候扣除了).
+        :param source_key:
+        :param income_type:
+        :param money:
+        :param transaction_id:
+        """
+
+        assert source_key, 'gateway is null'
+        assert transaction_id, 'transaction id is null'
+
+        field = self.income_field_name(income_type = income_type)
+
+        query = {'_id': self.id, 'inhandWithdrawList.transaction_id': {'$ne': transaction_id}}
+        update = {
+            '$addToSet': {
+                'inhandWithdrawList': {
+                    'transaction_id': transaction_id,
+                    'field': field,
+                    'key': source_key,
+                    'value': money.mongo_amount
+                }
+            }
+        }
+
+        result = self.get_collection().update_one(query, update, upsert = False)  # type: UpdateResult
+        return bool(result.modified_count == 1)
+
     def _freeze_balance(self, income_type, money, source_key, transaction_id, freeze_type):
         assert source_key, 'gateway is null'
         assert transaction_id, 'transaction id is null'
@@ -963,7 +1002,7 @@ class CapitalUser(UserSearchable):
         """
         return self._freeze_balance(income_type, money, source_key, transaction_id, "inhandWithdrawList")
 
-    def clear_frozen_balance(self, transaction_id): # type:(str)->bool
+    def clear_frozen_balance(self, transaction_id):  # type:(str)->bool
         """
         提现完成 清理冻结
         :param transaction_id:
@@ -1006,6 +1045,9 @@ class CapitalUser(UserSearchable):
         # type: () -> basestring
         raise NotImplementedError()
 
+    def check_withdraw_min_fee(self, income_type, pay_type, amount):
+        raise NotImplementedError()
+
     def new_withdraw_record(self, withdraw_gateway, pay_entity, source_key, income_type, amount, pay_type, manual,
                             recurrent):
         # type: (WithdrawGateway, WithdrawBankCard, str, str, RMB, str, bool, bool) -> WithdrawRecord
@@ -1082,8 +1124,33 @@ class CapitalUser(UserSearchable):
     def auto_withdraw_bound_open_id(self):
         raise NotImplementedError()
 
-    def can_withdraw_today(self):
-        return True
+    def can_withdraw_today(self, amount):
+        from apps.web.core.exceptions import ServiceException
+
+        count, total = WithdrawRecord.count_today(ownerId = str(self.id), role = self.role)
+
+        if count >= 10:
+            raise ServiceException(
+                {
+                    'result': 0, 'description': u"今日提现次数超限",
+                    'payload': {}
+                })
+
+        if not self.supports('in_withdraw_whitelist') and self.is_new_join:
+            if amount + total > RMB(500):
+                raise ServiceException(
+                    {
+                        'result': 0, 'description': u"今日提现金额超限",
+                        'payload': {}
+                    })
+        else:
+            if total + amount > RMB(settings.WITHDRAW_MAXIMUM):
+                raise ServiceException(
+                    {
+                        'result': 0,
+                        'description': u"今日提现金额超限",
+                        'payload': {}
+                    })
 
     @property
     def current_wallet_withdraw_source_key(self):
@@ -1095,7 +1162,7 @@ class CapitalUser(UserSearchable):
     def withdraw_support(self, source_key):
         from apps.web.agent.models import Agent
 
-        is_ledger, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
+        is_ledger, agent, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
 
         if not is_ledger:
             return {
@@ -1112,22 +1179,24 @@ class CapitalUser(UserSearchable):
                 }
             }
 
-        dealerBankWithdrawFee = withdraw_gateway_list['wechat'].occupant.dealerBankWithdrawFee
-
         rv = {
             'wechat': {
-                'support': True if withdraw_gateway_list['wechat'] else False,
+                'support':
+                    withdraw_gateway_list['wechat'].support_withdraw and not withdraw_gateway_list[
+                        'wechat'].manual_withdraw,
                 'realName': self.withdraw_wechat_real_name
             },
             'alipay': {
-                'support': True if withdraw_gateway_list['alipay'] else False,
+                'support': withdraw_gateway_list['alipay'].support_withdraw and not withdraw_gateway_list[
+                    'wechat'].manual_withdraw,
                 'realName': self.withdraw_alipay_real_name,
                 'loginId': self.withdraw_alipay_login_id
             },
             'bank': {
-                'support': True,
+                'support': withdraw_gateway_list['wechat'].support_withdraw_bank or withdraw_gateway_list[
+                    'alipay'].support_withdraw_bank,
                 'cards': self.withdraw_bank_cards,
-                'transFee': dealerBankWithdrawFee and self.bankWithdrawFee
+                'transFee': agent.dealerBankWithdrawFee and self.bankWithdrawFee
             }
         }
 
@@ -1237,6 +1306,10 @@ class WithdrawRecord(Searchable):
         return cls.objects(status__in = [WithdrawStatus.PROCESSING, WithdrawStatus.BANK_PROCESSION],
                            payType = WITHDRAW_PAY_TYPE.BANK, manual = False)
 
+    @classmethod
+    def get_processing_via_v3(cls):
+        return cls.objects(status__in = [WithdrawStatus.PROCESSING], payType = WITHDRAW_PAY_TYPE.WECHAT, manual = False)
+
     @classmethod
     def get_failed_records(cls):
         return cls.objects(status = WithdrawStatus.FAILED, refunded = False, manual = False)
@@ -1417,7 +1490,12 @@ class WithdrawRecord(Searchable):
     @staticmethod
     def is_my(order_no):
         try:
-            return order_no[0] == OrderMainType.WITHDRAW and order_no[1] in ROLE.sub_type_list()
+            if order_no[0] == OrderMainType.WITHDRAW and order_no[1] in ROLE.sub_type_list():
+                return True
+            elif order_no[14] == OrderMainType.WITHDRAW and order_no[15] in ROLE.sub_type_list():
+                return True
+            else:
+                return False
         except Exception as e:
             return False
 
@@ -1426,30 +1504,19 @@ class WithdrawRecord(Searchable):
         # type: (str)->None
         return memcache_lock('WithdrawRecord_%s' % order, 'processing')
 
-    @property
-    def source_key(self):
-        if not self.withdrawSourceKey:
-            pay_app_type, occupant_id, tokens = WithdrawGateway.parse_gateway_key(self.withdrawGatewayKey)
-
-            appid, mchid = tokens[0], tokens[1]
-            app = WechatPayApp(appid = appid, mchid = mchid)  # type: WechatPayApp
-            app.occupantId = occupant_id
-
-            return APP_KEY_DELIMITER.join(
-                [WithdrawGateway.LEDGER_PREFIX, PayAppType.WECHAT, getattr(app, '__source_key__')])
-        else:
-            return self.withdrawSourceKey
-
-    @property
-    def is_new_version(self):
-        return self.extras.get('v', 1) == 2
-
     @classmethod
     def count_today(cls, ownerId, role = ROLE.dealer):
-        return cls.objects(
-            ownerId = ownerId, role = role,
-            status__nin = [WithdrawStatus.CLOSED, WithdrawStatus.SUCCEEDED, WithdrawStatus.REFUND],
-            postTime__gte = get_zero_time(datetime.datetime.now())).count()
+        count = 0
+        total = RMB(0)
+
+        for item in cls.objects(
+                ownerId = ownerId, role = role,
+                status__nin = [WithdrawStatus.CLOSED, WithdrawStatus.REFUND],
+                postTime__gte = get_zero_time(datetime.datetime.now())).all():
+            count += 1
+            total += item.amount
+
+        return count, total
 
 
 class PushRecord(Searchable):
@@ -1864,10 +1931,14 @@ class OrderRecordBase(Searchable):
         if hasattr(self, _attr_name):
             return getattr(self, _attr_name)
         else:
-            device = Device.get_dev(self.devNo)
+            device = Device.get_dev_by_logicalCode(self.logicalCode)
             setattr(self, _attr_name, device)
             return device
 
+    @device.setter
+    def device(self, value):
+        setattr(self, '__device__', value)
+
     @property
     def dev_type_code(self):
         if self.devTypeCode:
@@ -2219,3 +2290,343 @@ class WithdrawBankCard(DynamicDocument):
                 phone = phone, provinceCode = provinceCode, province = province,
                 cityCode = cityCode, city = city, branchBankCode = branchBankCode,
                 branchBankName = branchBankName, cnapsCode = cnapsCode)
+
+
+class RefundOrderBase(Searchable):
+    meta = {
+        'abstract': True
+    }
+
+    class Status(object):
+        """ 退款单的状态 正常流程顺序是从上至下 """
+        CREATED = 'created'  # 创建
+        PROCESSING = 'processing'  # 申请中
+        FAILURE = 'failure'  # 申请失败   (彻底失败 需要重新发起)
+        SUCCESS = 'success'  # 退款成功   (异步结果)
+        CLOSED = 'closed'  # 退款失败   (异步结果 不需要重新发起)
+        NOORDER = 'noOrder'  # 单提交错误
+
+        # 订单常见的状态
+        # created ---> processing ---> success  (申请 然后接受成功)
+        # created ---> failure  (直接申请失败)
+        # created ---> processing ---> closed    (申请成功 回调失败)
+        # created ---> processing ---> failure   (申请成功  回调失败 不可以重试)
+
+    rechargeObjId = ObjectIdField(verbose_name = u'对应充值单')
+
+    payAppType = StringField(verbose_name = u'支付应用类型', default = None)
+
+    # refundSeq = IntField(verbose_name=u'多次退款,计算退款序列号', default=1)
+
+    orderNo = StringField(verbose_name = u'商户退款订单号', default = '')
+
+    errorCode = StringField(verbose_name = u"错误代码", default = "")
+    errorDesc = StringField(verbose_name = u"错误描述", default = "")
+
+    money = MonetaryField(verbose_name = u"退款的金钱数额", default = RMB('0.00'))
+
+    status = StringField(verbose_name = u'订单状态', default = Status.CREATED)
+
+    datetimeAdded = DateTimeField(verbose_name = u"退款创建时间", default = None)
+    datetimeUpdated = DateTimeField(verbose_name = u"退款更新时间")
+    finishedTime = DateTimeField(verbose_name = u"退款到账时间")
+
+    extraInfo = DictField(verbose_name = u'额外信息')
+
+    tradeRefundNo = StringField(verbose_name = u"交易机构退款单号", default = None)
+
+    retryCount = IntField(verbose_name = u"重试次数", default = 0)
+
+    def succeed(self, finishedTime, **kwargs):  # type:(datetime.datetime, dict) -> bool
+        """
+        退款成功
+        :param finishedTime:
+        :param kwargs:
+        :return:
+        """
+
+        payload = {
+            'status': self.Status.SUCCESS,
+            'datetimeUpdated': datetime.datetime.now(),
+            'finishedTime': finishedTime
+        }
+
+        if kwargs:
+            payload.update(kwargs)
+
+        result = self.get_collection().update_one(
+            filter = {
+                '_id': ObjectId(self.id),
+                'status': {
+                    '$nin': [self.Status.SUCCESS, self.Status.CLOSED]
+                }},
+            update = {'$set': payload},
+            upsert = False)
+
+        matched = (result.matched_count == 1)
+        if matched:
+            self.reload()
+
+        return matched
+
+    def fail(self, errorCode = "", errorDesc = "", **kwargs):  # type:(str, unicode, dict) -> bool
+        """
+        更新为失败状态 表示退款申请失败 这种情况下 可能就需要售后进行介入
+        1. 订单申请发起时候即失败
+        2. 订单申请通过 但是回调的时候明确失败 并且是不可重试的失败
+        3. 此状态即表示订单没退款 理论上可以重新发起退款
+        """
+
+        payload = {
+            'status': self.Status.FAILURE,
+            'datetimeUpdated': datetime.datetime.now(),
+            'errorCode': errorCode,
+            'errorDesc': errorDesc
+        }
+
+        if kwargs:
+            payload.update(kwargs)
+
+        _filter = {
+            '_id': ObjectId(self.id),
+            'status': {
+                '$nin': [self.Status.CLOSED, self.Status.SUCCESS]
+            }
+        }
+
+        result = self.get_collection().update_one(
+            filter = _filter,
+            update = {'$set': payload},
+            upsert = False)
+
+        return result.matched_count == 1
+
+    def no_order(self, errorCode = "", errorDesc = ""):  # type:(basestring, basestring) -> bool
+        """
+        查询状态无此订单
+        :param errorCode:
+        :param errorDesc:
+        :return:
+        """
+        payload = {
+            'status': self.Status.NOORDER,
+            'datetimeUpdated': datetime.datetime.now(),
+            'errorCode': errorCode,
+            'errorDesc': errorDesc
+        }
+
+        _filter = {
+            '_id': ObjectId(self.id),
+            'status': {
+                '$nin': [self.Status.CLOSED, self.Status.SUCCESS]
+            }
+        }
+
+        result = self.get_collection().update_one(
+            filter = _filter,
+            update = {'$set': payload},
+            upsert = False)
+
+        return result.matched_count == 1
+
+    def closed(self, errorCode = "", errorDesc = "", **kwargs):  # type:(basestring, basestring, dict) -> bool
+        """
+        退款申请成功了 表示合法的退款申请 但是由于某些原因业务进行不下去
+        """
+
+        payload = {
+            'status': self.Status.CLOSED,
+            'datetimeUpdated': datetime.datetime.now(),
+            'errorCode': errorCode,
+            'errorDesc': errorDesc
+        }
+
+        if kwargs:
+            payload.update(kwargs)
+
+        _filter = {
+            '_id': ObjectId(self.id),
+            'status': {
+                '$nin': [self.Status.CLOSED, self.Status.SUCCESS]
+            }
+        }
+
+        result = self.get_collection().update_one(
+            filter = _filter,
+            update = {'$set': payload},
+            upsert = False)
+
+        matched = (result.matched_count == 1)
+        if matched:
+            self.reload()
+
+        return matched
+
+    def processing(self):  # type:() -> bool
+        payload = {
+            'status': self.Status.PROCESSING,
+            'datetimeUpdated': datetime.datetime.now()
+        }
+
+        _filter = {
+            '_id': ObjectId(self.id),
+            'status': {
+                '$nin': [self.Status.CLOSED, self.Status.SUCCESS]
+            }
+        }
+
+        result = self.get_collection().update_one(
+            filter = _filter,
+            update = {'$set': payload},
+            upsert = False)
+
+        return result.matched_count == 1
+
+    def retry_processing(self, changeOrderNo):  # type:(bool)->bool
+        """
+        只有订单处于FAILURE以及NO_ORDER状态的单才会重新调度
+        :param changeOrderNo:
+        :return:
+        """
+        RefundOrderHistory.issue(self)
+
+        if changeOrderNo:
+            identifier = self.orderNo[16:29]
+            orderNo = OrderNoMaker.make_order_no_32(
+                identifier = identifier,
+                main_type = OrderMainType.REFUND,
+                sub_type = RefundSubType.REFUND)
+
+            _payload = {
+                'orderNo': orderNo,
+                'status': self.Status.PROCESSING,
+                'datetimeUpdated': datetime.datetime.now()
+            }
+        else:
+            _payload = {
+                'status': self.Status.PROCESSING,
+                'datetimeUpdated': datetime.datetime.now()
+            }
+
+        _filter = {
+            '_id': ObjectId(self.id),
+            'status': {
+                '$in': [self.Status.FAILURE, self.Status.NOORDER]
+            }
+        }
+
+        result = self.get_collection().update_one(
+            filter = _filter,
+            update = {'$set': _payload, '$inc': {'retryCount': int(1)}},
+            upsert = False)
+
+        return result.matched_count == 1
+
+    @property
+    def is_no_order(self):
+        return self.status == self.Status.NOORDER
+
+    @property
+    def is_fail(self):
+        """ 是否订单失败 """
+        return self.status == self.Status.FAILURE
+
+    @property
+    def is_closed(self):
+        """ 退款单是否已经关闭  目前这个状态没有用 适用于用户手动取消退款申请 """
+        return self.status == self.Status.CLOSED
+
+    @property
+    def is_success(self):
+        """ 退款已经成功 """
+        return self.status == self.Status.SUCCESS
+
+    @property
+    def is_created(self):
+        return self.status == self.Status.CREATED
+
+    @property
+    def is_apply(self):
+        """ 是否已经发出退款申请 """
+        return self.status in [self.Status.CREATED, self.Status.PROCESSING]
+
+    @property
+    def is_processing(self):
+        return self.status == self.Status.PROCESSING
+
+    @property
+    def is_successful(self):
+        """ 保留旧的方法 """
+        return self.status in [self.Status.SUCCESS, self.Status.PROCESSING]
+
+    @classmethod
+    def get_record(cls, **kwargs):
+        return cls.objects(**kwargs).first()
+
+    @property
+    def refund_income_order(self):
+        return None
+
+    @property
+    def pay_app_type(self):
+        raise AttributeError('must implement pay_app_type.')
+
+    @property
+    def pay_sub_order(self):
+        # type: ()->Optional[DealerRechargeRecord,RechargeRecord]
+        raise AttributeError('must implement pay_sub_order.')
+
+    @pay_sub_order.setter
+    def pay_sub_order(self, order):
+        raise AttributeError('must implement pay_sub_order setter.')
+
+    @property
+    def my_payment_gateway(self):
+        if not hasattr(self, '__payment_gateway__'):
+            _payment_gateway = PaymentGateway.clone_from_order(self.pay_sub_order)
+            setattr(self, '__payment_gateway__', _payment_gateway)
+
+        return getattr(self, '__payment_gateway__')
+
+    @property
+    def notify_url(self):
+        raise AttributeError('must implement notify_url.')
+
+
+class RefundOrderHistory(Searchable):
+    """
+    用户退款失败历史记录
+    """
+
+    className = StringField(verbose_name = u'对应退款单表名')
+    refId = ObjectIdField(verbose_name = u'对应退款单')
+
+    orderNo = StringField(verbose_name = u'原单号', default = '')
+
+    errorCode = StringField(verbose_name = u"错误代码", default = "")
+    errorDesc = StringField(verbose_name = u"错误描述", default = "")
+
+    datetimeAdded = DateTimeField(verbose_name = u"创建时间", default = None)
+
+    datetimeUpdated = DateTimeField(verbose_name = u"退款更新时间")
+    finishedTime = DateTimeField(verbose_name = u"退款到账时间")
+    tradeRefundNo = StringField(verbose_name = u"交易机构退款单号", default = None)
+
+    meta = {
+        "collection": "refund_order_history",
+        "db_alias": "logdata",
+    }
+
+    @classmethod
+    def issue(cls, order):
+        # type:(RefundOrderBase)->RefundOrderHistory
+        return cls(
+            className = order.__class__.__name__,
+            refId = order.id,
+            orderNo = order.orderNo,
+            errorCode = order.errorCode,
+            errorDesc = order.errorDesc,
+            datetimeAdded = datetime.datetime.now(),
+            datetimeUpdated = order.datetimeUpdated,
+            finishedTime = order.finishedTime,
+            tradeRefundNo = order.tradeRefundNo).save()

+ 1 - 1
apps/web/common/proxy.py

@@ -862,7 +862,7 @@ class ClientDealerIncomeModelProxy(ModelProxy):
     _META_MODEL = DealerIncomeProxy
 
     @classmethod
-    def get_one(cls, startTime = None, endTime = None, **kwargs):  # type:(str, str, dict) -> Searchable
+    def get_one(cls, startTime = None, endTime = None, **kwargs):  # type:(str, str, dict) -> DealerIncomeProxy
         if 'ref_id' in kwargs:
             return super(ClientDealerIncomeModelProxy, cls).get_one(foreign_id = str(kwargs.get('ref_id')), **kwargs)
         else:

+ 15 - 8
apps/web/common/transaction/__init__.py

@@ -90,7 +90,7 @@ class WithdrawHandler(object):
             self.payee.recover_frozen_balance(
                 self.record.incomeType,
                 self.record.amount,
-                self.record.source_key,
+                self.record.withdrawSourceKey,
                 self.record.order,
                 )
 
@@ -110,10 +110,7 @@ class WithdrawHandler(object):
 
         success = self.record.succeed(**kwargs)
         if success:
-            self.payee.clear_frozen_balance(
-                self.record.incomeType
-            )
-
+            self.payee.clear_frozen_balance(str(self.record.id))
             self.on_approve()
         else:
             raise WithdrawError(u'更新提现状态失败(1002)')
@@ -155,7 +152,7 @@ def translate_withdraw_state(state):
 
 class OrderNoMaker(object):
     @classmethod
-    def _order_prefix(cls, main_type, sub_type):
+    def make_prefix(cls, main_type, sub_type):
         return '{}{}'.format(main_type, sub_type)
 
     @classmethod
@@ -163,13 +160,22 @@ class OrderNoMaker(object):
         # type: (str, str, str)->str
 
         # time:14,prefix:2,identifier:13,resered:3
-
         return '{time}{prefix}{identifier}{reserved}'.format(
             time = datetime.datetime.now().strftime("%Y%m%d%H%M%S"),
-            prefix = cls._order_prefix(main_type, sub_type),
+            prefix = cls.make_prefix(main_type, sub_type),
             identifier = '{:0>13}'.format(identifier[-13:]).upper(),
             reserved = get_random_str(3, string.digits + string.uppercase))
 
+    @classmethod
+    def my_prefix(cls, order_no):
+        return order_no[14:16]
+
+    @classmethod
+    def from_ref_order_no_32(cls, ref_order_no, main_type, sub_type):
+        prefix = cls.make_prefix(main_type, sub_type)
+        order_no = ref_order_no.replace(cls.my_prefix(ref_order_no), prefix)
+        return order_no
+
 
 class OrderMainType(StrEnum):
     PAY = 'P'  # 支付订单
@@ -186,6 +192,7 @@ class OrderMainType(StrEnum):
 
 class RefundSubType(IterConstant):
     REFUND = 'R'
+    REVOKE = 'V'
 
 
 class UserPaySubType(IterConstant):

+ 3 - 35
apps/web/common/transaction/pay/__init__.py

@@ -10,7 +10,6 @@ from django.utils.module_loading import import_string
 from typing import TYPE_CHECKING, cast
 
 from apilib.systypes import StrEnum, Singleton
-from apilib.utils_json import JsonResponse
 from apps import lockCache
 from apps.web.core import PayAppType
 from apps.web.core.payment import PaymentGateway
@@ -27,8 +26,7 @@ if TYPE_CHECKING:
 
     RefundRecordT = Union[RefundDealerRechargeRecord, RefundMoneyRecord]
 
-    from apps.web.core.payment import PaymentGatewayT
-
+    from apps.web.core.payment.type_checking import PaymentGatewayT
 
 logger = logging.getLogger(__name__)
 
@@ -111,7 +109,7 @@ class PayRecordPoller(object):
             logger.error('record<id={}> is not exist.'.format(self.record_id))
             return
 
-        if record.is_success():
+        if record.is_success:
             logger.error('record<id={},order={}> is success.'.format(self.record_id, record.orderNo))
             return
 
@@ -206,7 +204,7 @@ class PayPullUp(object):
 
     @classmethod
     def create(cls, factory_cls, payment_gateway, record, **payload):
-        # type: (cast(PayPullUpFactoryIntf), PaymentGatewayT, RechargeRecordT, dict)->PayPullUp
+        # type: (cast(PayPullUpFactoryIntf, None), PaymentGatewayT, RechargeRecordT, dict)->PayPullUp
 
         return factory_cls.create(payment_gateway, record, **payload)
 
@@ -225,11 +223,6 @@ class PayManager(Singleton):
                 'pullup': import_string('apps.web.common.transaction.pay.wechat.WechatPullUp'),
             },
 
-            PayAppType.WECHAT_MINI: {
-                'poller': import_string('apps.web.common.transaction.pay.wechat.WechatPayRecordPoller'),
-                'notifier': import_string('apps.web.common.transaction.pay.wechat.WechatPayNotifier'),
-            },
-
             PayAppType.ALIPAY: {
                 'poller': import_string('apps.web.common.transaction.pay.alipay.AliPayRecordPoller'),
                 'notifier': import_string('apps.web.common.transaction.pay.alipay.AliPayNotifier'),
@@ -250,28 +243,3 @@ class PayManager(Singleton):
             raise UserServerException(u'第三方支付配置错误,请联系平台客服(1002)')
 
         return self.map_dict[pay_app_type]['pullup']
-
-
-class RefundManager(Singleton):
-    def __init__(self):
-        super(RefundManager, self).__init__()
-        self.map_dict = {
-            PayAppType.WECHAT:
-                {
-                    'poller': import_string('apps.web.common.transaction.refund.wechat.WechatRefundPuller'),
-                    'notifier': import_string('apps.web.common.transaction.refund.wechat.WechatRefundNotifier'),
-                },
-
-            PayAppType.ALIPAY: {
-                'poller': import_string('apps.web.common.transaction.refund.alipay.AliRefundPuller'),
-                'notifier': import_string('apps.web.common.transaction.refund.alipay.AliRefundNotifier'),
-            }
-        }
-
-    def get_poller(self, pay_app_type):
-        assert pay_app_type in self.map_dict, 'not register pay app type'
-        return self.map_dict[pay_app_type]['poller']
-
-    def get_notifier(self, pay_app_type):
-        assert pay_app_type in self.map_dict, 'not register pay app type'
-        return self.map_dict[pay_app_type]['notifier']

+ 2 - 2
apps/web/common/transaction/pay/alipay.py

@@ -23,7 +23,7 @@ from library.alipay import AliValidationError, AliException, AliErrorCode
 from taskmanager.mediator import task_caller
 
 if TYPE_CHECKING:
-    from apps.web.core.payment import PaymentGatewayT
+    from apps.web.core.payment.type_checking import PaymentGatewayT
     from apps.web.core.payment.ali import AliPayGateway, AliPayWithdrawGateway
     from apps.web.common.transaction.pay import RechargeRecordT
     from typing import Dict
@@ -139,7 +139,7 @@ class AliPayNotifier(PayNotifier):
                     'no such record. orderNo = {}'.format(order_no))
                 return HttpResponse('success')
 
-            if record.is_success():
+            if record.is_success:
                 logger.error('record has been finished. orderNo = {}'.format(order_no))
                 return HttpResponse('success')
 

+ 4 - 3
apps/web/common/transaction/pay/wechat.py

@@ -20,7 +20,7 @@ from apps.web.constant import PollRecordDefine
 from apps.web.core.utils import async_operation
 from apps.web.exceptions import UserServerException
 from library.wechatpy.constants import WeChatErrorCode
-from library.wechatbase.exceptions import WeChatException, WechatValidationError
+from library.wechatbase.exceptions import WeChatException, WechatValidationError, InvalidSignatureException
 import simplejson as json
 from apps.web.core.payment.wechat import WechatPaymentGateway
 from taskmanager.mediator import task_caller
@@ -138,7 +138,7 @@ class WechatPayNotifier(PayNotifier):
                 logger.error('no such record. orderNo = {}'.format(out_trade_no))
                 return self.reply("OK", True)
 
-            if record.is_success():
+            if record.is_success:
                 logger.info('recharge record has done. orderNo = {}'.format(out_trade_no))
                 return self.reply("OK", True)
 
@@ -148,7 +148,7 @@ class WechatPayNotifier(PayNotifier):
                 record.pay_app_type)  # type: WechatPaymentGateway
 
             if not payment_gateway.check(raw):
-                raise WeChatException(
+                raise InvalidSignatureException(
                     errCode = WeChatErrorCode.MY_ERROR_SIGNATURE,
                     errMsg = u'支付通知签名错误',
                     client = payment_gateway.client
@@ -224,6 +224,7 @@ class WechatPullUp(PayPullUp):
     """
     def do(self):  # type: ()->HttpResponse
         OrderCacheMgr(self.record).initial()
+
         try:
             data = self.payment_gateway.generate_js_payment_params(
                 payOpenId = self.openId,

+ 192 - 10
apps/web/common/transaction/refund/__init__.py

@@ -1,17 +1,26 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 
+import datetime
 import logging
 
 from django.http import HttpResponse
-from typing import TYPE_CHECKING
+from django.utils.module_loading import import_string
+from typing import TYPE_CHECKING, Optional
 
+from apilib.systypes import Singleton
 from apilib.utils_sys import memcache_lock
-from apps.web.core.payment import PaymentGateway
+from apps.web.core import PayAppType
+from library.alipay import AliPayNetworkException, AliException
+from library.wechatbase.exceptions import WechatNetworkException, WeChatException
 
 if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
-    from apps.web.common.transaction.pay import RefundRecordT, RechargeRecordT
+    from apps.web.common.transaction.pay import RefundRecordT
+    from apilib.monetary import RMB
+    from apps.web.user.models import RechargeRecord
+    from apps.web.dealer.models import DealerRechargeRecord
+    from apps.web.common.models import RefundOrderBase
 
 logger = logging.getLogger(__name__)
 
@@ -37,11 +46,11 @@ class RefundNotifier(object):
         """ 验证解密后的数据是否有效 """
         raise NotImplementedError(u"需要实现")
 
-    def handle_refund_order(self, refundOrder, post_pay):  # type:(RefundRecordT, callable) -> None
+    def handle_refund_order(self, refundOrder, refund_post_callable):  # type:(RefundRecordT, callable) -> None
         """ 具体的处理订单的逻辑 根据回调中的成功与否决定下一步的走向"""
         raise NotImplementedError(u"需要实现")
 
-    def do(self, post_pay):
+    def do(self, post_refund):
         logger.debug(u"refundNotifier {} do work, payload = {}".format(self.__class__.__name__, self.payload))
 
         if not self.verify_payload(self.payload):
@@ -50,7 +59,7 @@ class RefundNotifier(object):
                     self.__class__.__name__, self.payload))
             return HttpResponse(self.successResponse)
 
-        refundOrder = self.refund_order_getter(order_no = self.refund_order_no)  # type: RefundRecordT
+        refundOrder = self.refund_order_getter(self.refund_order_filter)  # type: RefundRecordT
         if not refundOrder:
             logger.info(
                 u"refundNotifier {} record not refund, payload = {}".format(self.__class__.__name__, self.payload))
@@ -62,7 +71,7 @@ class RefundNotifier(object):
                     self.__class__.__name__, refundOrder.id))
                 return
 
-            self.handle_refund_order(refundOrder, post_pay)
+            self.handle_refund_order(refundOrder, post_refund)
 
             return HttpResponse(self.successResponse)
 
@@ -77,7 +86,7 @@ class RefundNotifier(object):
         raise NotImplementedError(u"需要实现")
 
     @property
-    def refund_order_no(self):  # type: ()->str
+    def refund_order_filter(self):  # type: ()->dict
         raise NotImplementedError(u"需要实现")
 
 
@@ -87,5 +96,178 @@ class RefundPuller(object):
     def __init__(self, refundOrder):  # type:(RefundRecordT) -> None
         self._refundOrder = refundOrder
 
-    def pull(self, payGateWay, payOrder, post_pay):  # type:(PaymentGateway, RechargeRecordT, callable) -> None
-        raise NotImplementedError(u"需要实现")
+    def pull(self, refund_post_callable, **kwargs):  # type:(callable, dict) -> bool
+        raise NotImplementedError(u"must implement pull.")
+
+    def parse_error(self, errorCode, errorDesc, refund_post_callable):
+        raise NotImplementedError(u"must implement parse_error.")
+
+
+class RefundCashMixin(object):
+    def __init__(self, rechargeOrder, refundFee):
+        # type:(Optional[RechargeRecord, DealerRechargeRecord], RMB) -> None
+
+        self.paySubOrder = rechargeOrder
+        self.payOrder = self.paySubOrder.payOrder
+
+        self.refundFee = refundFee
+
+        # self._nextSeq = 1
+
+    @property
+    def outTradeNo(self):
+        """
+        交易单号
+        :return:
+        """
+        return self.payOrder.orderNo
+
+    @property
+    def totalFee(self):
+        return self.payOrder.money
+
+    @property
+    def totalCoins(self):
+        return self.payOrder.coins
+
+    @property
+    def subTotalFee(self):
+        return self.paySubOrder.money
+
+    @property
+    def subTotalCoins(self):
+        return self.paySubOrder.coins
+
+    @property
+    def refund_paras(self):
+        return 'mixOrderNo = {} totalFee = {} orderNo = {} subTotalFee = {} refundFee = {}'.format(
+            self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee, self.refundFee)
+
+    def submit_refund(self, refundOrder, partition_map, reason, notify_url, post_refund):
+        # type:(RefundOrderBase, Optional[dict], basestring, basestring, callable)->None
+
+        payGateway = refundOrder.my_payment_gateway
+
+        if payGateway.pay_app_type == PayAppType.ALIPAY:
+            # 支付宝的退款方式
+            # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功
+            # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态
+            try:
+                result = payGateway.refund_to_user(
+                    out_trade_no = self.outTradeNo,
+                    out_refund_no = refundOrder.orderNo,
+                    refund_fee = refundOrder.money,
+                    total_fee = self.totalFee,
+                    refund_reason = reason)
+
+                logger.debug('AliPay Refund request successfully! return = {}'.format(result))
+
+                if result['code'] == '10000':  # 接口调用成功
+                    if result['fund_change'] == 'Y':
+                        if "gmt_refund_pay" in result:
+                            finishedTime = datetime.datetime.strptime(result["gmt_refund_pay"], "%Y-%m-%d %H:%M:%S")
+                        else:
+                            finishedTime = datetime.datetime.now()
+
+                        matched = refundOrder.succeed(tradeRefundNo = result['trade_no'], finishedTime = finishedTime)
+                        if matched:
+                            post_refund(refundOrder, True)
+                elif result['code'] == '20000':  # 20000 服务不可用 稍后重试
+                    refundOrder.fail(errorCode = result.get("code"), errorDesc = result.get("msg"))
+                else:  # 其他都代表失败
+                    if result['code'] == '40004':  # 业务错误
+                        if result['sub_code'] in [
+                            'ACQ.REFUND_AMT_NOT_EQUAL_TOTAL',
+                            'ACQ.REASON_TRADE_REFUND_FEE_ERR',
+                            'ACQ.TRADE_NOT_ALLOW_REFUND',
+                            'ACQ.REFUND_FEE_ERROR',
+                            'ACQ.BUYER_NOT_EXIST',
+                            'ACQ.ONLINE_TRADE_VOUCHER_NOT_ALLOW_REFUND',
+                            'ACQ.TRADE_HAS_FINISHED'
+                        ]:
+                            matched = refundOrder.closed(
+                                errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
+                            if matched:
+                                post_refund(refundOrder, False)
+                        elif result['sub_code'] in ['ACQ.SELLER_BALANCE_NOT_ENOUGH']:
+                            # 40004-ACQ.SELLER_BALANCE_NOT_ENOUGH: 卖家余额不足
+                            refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
+                        else:
+                            refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
+                    else:
+                        refundOrder.fail(errorCode = result.get("sub_code"), errorDesc = result.get("sub_msg"))
+
+            except AliPayNetworkException as e:
+                # 网络调用失败, 无法判定是否成功, 不改变状态, 等拉取订单状态后在更改订单状态
+                logger.warning(
+                    'AliPay Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
+
+            except AliException as e:
+                # 目前只有签名失败
+                logger.warning(
+                    'AliPay Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
+
+                refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
+
+        elif payGateway.pay_app_type in [PayAppType.WECHAT]:
+            try:
+                result = payGateway.refund_to_user(
+                    out_trade_no = self.outTradeNo,
+                    out_refund_no = refundOrder.orderNo,
+                    refund_fee = refundOrder.money,
+                    total_fee = self.totalFee,
+                    refund_reason = reason,
+                    notify_url = notify_url)
+
+                logger.debug('WeChat Refund request successfully! return = {}'.format(result))
+
+            except WechatNetworkException as e:
+                logger.warning(
+                    'WeChat Refund request exception! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
+
+            except WeChatException as e:
+                logger.warning(
+                    'WeChat Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, str(e)))
+
+                puller = RefundManager().get_poller(refundOrder.pay_app_type)
+                puller(refundOrder).parse_error(e.errCode, e.errMsg, post_refund)
+
+        else:
+            matched = refundOrder.closed(errorCode = u'NOT_SUPPORT_REFUND', errorDesc = u"不支持的退款模式")
+            if matched:
+                post_refund(refundOrder, False)
+
+    def check_wallet(self, proxy, order):
+        pass
+
+    def pre_check(self):
+        """
+        退款的预检查
+        :return:
+        """
+        raise NotImplementedError('must implement pre_check')
+
+
+class RefundManager(Singleton):
+    def __init__(self):
+        super(RefundManager, self).__init__()
+        self.map_dict = {
+            PayAppType.WECHAT:
+                {
+                    'poller': import_string('apps.web.common.transaction.refund.wechat.WechatRefundPuller'),
+                    'notifier': import_string('apps.web.common.transaction.refund.wechat.WechatRefundNotifier'),
+                },
+
+            PayAppType.ALIPAY: {
+                'poller': import_string('apps.web.common.transaction.refund.alipay.AliRefundPuller'),
+                'notifier': import_string('apps.web.common.transaction.refund.alipay.AliRefundNotifier'),
+            },
+        }
+
+    def get_poller(self, pay_app_type):
+        assert pay_app_type in self.map_dict, 'not register pay app type'
+        return self.map_dict[pay_app_type]['poller']
+
+    def get_notifier(self, pay_app_type):
+        assert pay_app_type in self.map_dict, 'not register pay app type'
+        return self.map_dict[pay_app_type]['notifier']

+ 30 - 27
apps/web/common/transaction/refund/alipay.py

@@ -10,8 +10,8 @@ from apps.web.common.transaction.refund import RefundNotifier, RefundPuller
 
 if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
-    from apps.web.core.payment import PaymentGatewayT
-    from apps.web.common.transaction.pay import RechargeRecordT, RefundRecordT
+    from apps.web.core.payment.type_checking import PaymentGatewayT
+    from apps.web.common.transaction.pay import RefundRecordT
 
 logger = logging.getLogger(__name__)
 
@@ -21,22 +21,19 @@ class AliRefundNotifier(RefundNotifier):
         return request.POST.dict()  # type: Dict
 
     @property
-    def refund_order_no(self):  # type:() -> str
-        return self.payload['out_biz_no']
+    def refund_order_filter(self):  # type:() -> dict
+        return {'orderNo': self.payload['out_biz_no']}
 
-    def handle_refund_order(self, refundOrder, post_pay):  # type:(RefundRecordT, callable) -> None
+    def handle_refund_order(self, refundOrder, refund_post_callable):  # type:(RefundRecordT, callable) -> None
         """
         只有成功的退款 才会走异步的回调
         """
-
-        refundTime = self.payload["gmt_refund"]
-        datetimeRefund = datetime.datetime.strptime(refundTime[: 19], "%Y-%m-%d %H:%M:%S")
-
-        matched = refundOrder.succeed(tradeRefundNo = None, finishedTime = datetimeRefund)
+        matched = refundOrder.succeed(
+            finishedTime = datetime.datetime.strptime(self.payload["gmt_refund"][: 19], "%Y-%m-%d %H:%M:%S"))
         if not matched:
             return
 
-        return post_pay(refundOrder, datetimeRefund)
+        return refund_post_callable(refundOrder, True)
 
     @property
     def errorResponse(self):  # type:() -> str
@@ -51,23 +48,29 @@ class AliRefundNotifier(RefundNotifier):
 
 
 class AliRefundPuller(RefundPuller):
-    def pull(self, payGateWay, payOrder, post_pay):  # type:(PaymentGatewayT, RechargeRecordT, callable) -> None
-        result = payGateWay.api_refund_query(
-            trade_no = payOrder.wxOrderNo, out_trade_no = payOrder.orderNo, out_request_no = self._refundOrder.orderNo)
+    def pull(self, refund_post_callable, **kwargs):  # type:(callable, dict) -> bool
+        rechargeOrder = self._refundOrder.pay_sub_order
+        payGateway = self._refundOrder.my_payment_gateway  # type: PaymentGatewayT
+
+        result = payGateway.api_refund_query(
+            out_refund_no = self._refundOrder.orderNo, out_trade_no = rechargeOrder.orderNo)
 
-        if result["code"] != "10000" or result["msg"] != "Success" or result["refund_status"] != "REFUND_SUCCESS":
+        if result["code"] != "10000":
+            # 接口错误是无法判断成功还是失败的, 这个情况下不能继续往下处理
             logger.info(
                 "RefundPuller {} pull refund order {}, result code = {}, msg = {}".format(
                     self.__class__.__name__, self._refundOrder, result["code"], result["msg"]))
-            return
-
-        # 对于支付宝来说, 查询的时候通知的时间并没有显示出来, 就用查询的时间代理
-        gmt_refund_pay = result["gmt_refund_pay"]
-        datetimeRefund = datetime.datetime.strptime(gmt_refund_pay, "%Y-%m-%d %H:%M:%S")
-
-        matched = self._refundOrder.succeed(tradeRefundNo = None, finishedTime = datetimeRefund)
-
-        if not matched:
-            return
-
-        post_pay(self._refundOrder, datetimeRefund)
+            return True
+
+        if "refund_status" not in result or result['refund_status'] != 'REFUND_SUCCESS':
+            # 未返回该字段表示退款请求未收到或者退款失败, 可以重试
+            self._refundOrder.fail(errorCode = 'FAIL', errorDesc = 'FAIL')
+            return False
+        else:
+            # 对于支付宝来说,查询的时候通知的时间并没有显示出来,就用查询的时间代替
+            matched = self._refundOrder.succeed(
+                finishedTime = datetime.datetime.strptime(result["gmt_refund_pay"], "%Y-%m-%d %H:%M:%S"))
+            if matched:
+                refund_post_callable(self._refundOrder, True)
+
+            return True

+ 106 - 56
apps/web/common/transaction/refund/wechat.py

@@ -2,21 +2,22 @@
 # !/usr/bin/env python
 
 import datetime
+import logging
 
 import xmltodict
-from mongoengine import DoesNotExist
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
 from apps.web.common.transaction.refund import RefundNotifier, RefundPuller
 from apps.web.core.models import WechatPayApp
-from library.wechatbase.exceptions import WeChatPayException
+from library.wechatbase.exceptions import WeChatException, WechatNetworkException
 from library.wechatpy.pay import WeChatPay
-from apps.web.user.models import RefundMoneyRecord, RechargeRecord
+
+logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
-    from apps.web.core.payment import PaymentGatewayT
-    from apps.web.common.transaction.pay import RechargeRecordT, RefundRecordT
+    from apps.web.core.payment.type_checking import PaymentGatewayT
+    from apps.web.common.transaction.pay import RefundRecordT
 
 
 class WechatRefundNotifier(RefundNotifier):
@@ -33,34 +34,45 @@ class WechatRefundNotifier(RefundNotifier):
         """ 忽略对签名串的校验 """
         return True
 
-    def handle_refund_order(self, refundOrder, post_pay):  # type:(RefundRecordT, callable) -> None
-        if not self.payload:
-            refundOrder.fail(errorDesc = u'通知参数错误')
-            return
-
-        if self.payload["return_code"] != "SUCCESS":
-            refundOrder.fail(errorDesc = self.payload["return_msg"])
+    def handle_refund_order(self, refundOrder, refund_post_callable):  # type:(RefundRecordT, callable) -> None
+        if not self.payload or self.payload["return_code"] != "SUCCESS":
             return
 
         if self.payload["refund_status"] == "SUCCESS":
             payFinishTime = self.payload["success_time"]
             datetimeRefund = datetime.datetime.strptime(payFinishTime, "%Y-%m-%d %H:%M:%S")
 
-            matched = refundOrder.succeed(tradeRefundNo = self.payload["refund_id"], finishedTime = datetimeRefund)
+            matched = refundOrder.succeed(finishedTime = datetimeRefund, **{
+                'tradeRefundNo': self.payload["refund_id"]
+            })
             if not matched:
                 return
 
-            return post_pay(refundOrder, datetimeRefund)
+            return refund_post_callable(refundOrder, True)
 
-        if self.payload["refund_status"] == "REFUNDCLOSE":
-            refundOrder.closed(tradeRefundNo=self.payload["refund_id"], errorDesc = self.payload["return_msg"])
+        elif self.payload["refund_status"] == "REFUNDCLOSE":
+            refundOrder.fail(
+                errorCode = 'REFUNDCLOSE',
+                errorDesc = self.payload["return_msg"],
+                **{
+                    'tradeRefundNo': self.payload.get("refund_id")
+                })
             return
 
-        refundOrder.fail(errorDesc = self.payload["return_msg"])
+        elif self.payload["refund_status"] == "CHANGE":
+            refundOrder.fail(
+                errorCode = 'CHANGE',
+                errorDesc = u'退款异常',
+                **{
+                    'tradeRefundNo': self.payload.get("refund_id")
+                })
+
+        else:
+            pass
 
     @property
-    def refund_order_no(self):  # type:() -> str
-        return self.payload['out_refund_no']
+    def refund_order_filter(self):  # type:() -> dict
+        return {'orderNo': self.payload["out_refund_no"]}
 
     @property
     def errorResponse(self):  # type:() -> str
@@ -74,43 +86,81 @@ class WechatRefundNotifier(RefundNotifier):
 
 
 class WechatRefundPuller(RefundPuller):
-    def pull(self, payGateWay, payOrder, post_pay):  # type:(PaymentGatewayT, RechargeRecordT, callable) -> None
-        try:
-            result = payGateWay.api_refund_query(out_refund_no = self._refundOrder.orderNo)
-        except WeChatPayException:
-            # 接口性质的失败 不做任何处理
-            return
-
-        # 找出退款单号
-        offset = None
-        for _k, _v in result.items():
-            if _v == self._refundOrder.orderNo:
-                offset = _k.rsplit("_", 1)[1]
-
-        if not offset:
-            return
+    def parse_error(self, errorCode, errorDesc, refund_post_callable):
+        if errorCode == 'REFUNDNOTEXIST':
+            self._refundOrder.no_order(errorCode = errorCode, errorDesc = errorDesc)
+
+        elif errorCode in ['TRADE_OVERDUE', 'USER_ACCOUNT_ABNORMAL']:
+            # USER_ACCOUNT_ABNORMAL: 用户账户异常或已注销,不能原路退回,请使用其他方式进行退款。
+            # TRADE_OVERDUE: 超期订单无法退款
+            matched = self._refundOrder.closed(errorCode = errorCode, errorDesc = errorDesc)
+            if matched:
+                refund_post_callable(self._refundOrder, False)
+
+            return True
+        else:
+            if errorCode in ['NOTENOUGH']:
+                # NOTENOUGH: 基本账户余额不足,请充值后重新发起
+                self._refundOrder.fail(errorCode = errorCode, errorDesc = errorDesc)
+            else:
+                # 其他暂时不处理,逐步补充
+                logger.warning('RefundOrder<orderNo={}>, errorCode = {}, errorDesc = {}'.format(
+                    self._refundOrder.orderNo, errorCode, errorDesc))
+                return True
 
-        refundStatus = result["refund_status_{}".format(offset)]
-        if refundStatus == "SUCCESS":
-            refundTime = result["refund_success_time_{}".format(offset)]
-            datetimeRefund = datetime.datetime.strptime(refundTime, "%Y-%m-%d %H:%M:%S")
+        return False
 
-            matched = self._refundOrder.succeed(result["refund_id_{}".format(offset)], finishedTime = datetimeRefund)
-            if not matched:
-                return
+    def pull(self, refund_post_callable, **kwargs):  # type:(callable, dict) -> bool
+        payGateway = self._refundOrder.my_payment_gateway  # type: PaymentGatewayT
 
-            post_pay(self._refundOrder, datetimeRefund)
-        elif refundStatus == "REFUNDCLOSE":
-            self._refundOrder.closed(
-                result["refund_id_{}".format(offset)],
-                result["err_code"], result["err_code_des"]
-            )
-        elif refundStatus == "PROCESSING":
-            if not (self._refundOrder.is_processing or self._refundOrder.is_closed or self._refundOrder.is_success):
-                self._refundOrder.processing()
-        elif refundStatus == "CHANGE":
-            self._refundOrder.fail(result["err_code"], result["err_code_des"])
+        try:
+            result = payGateway.api_refund_query(out_refund_no = self._refundOrder.orderNo)
+        except WechatNetworkException as e:
+            # return_code不为SUCCESS的情况下
+            raise e
+        except WeChatException as e:
+            # result_code不为SUCCESS的情况下, 抛出异常
+            return self.parse_error(e.errCode, e.errMsg, refund_post_callable)
         else:
-            pass
-
-        return
+            # 找出退款单号
+            offset = None
+            for _k, _v in result.items():
+                if _v == self._refundOrder.orderNo:
+                    offset = _k.rsplit("_", 1)[1]
+                    break
+
+            if not offset:
+                return False
+
+            refundStatus = result["refund_status_{}".format(offset)]
+            if refundStatus == "SUCCESS":
+                refundTime = result["refund_success_time_{}".format(offset)]
+                datetimeRefund = datetime.datetime.strptime(refundTime, "%Y-%m-%d %H:%M:%S")
+
+                matched = self._refundOrder.succeed(
+                    finishedTime = datetimeRefund, tradeRefundNo = result["refund_id_{}".format(offset)])
+                if matched:
+                    refund_post_callable(self._refundOrder, True)
+                return True
+            elif refundStatus == "REFUNDCLOSE":
+                self._refundOrder.fail(
+                    errorCode = "REFUNDCLOSE",
+                    errorDesc = '{}({})'.format(result.get("err_code_des"), result.get("err_code")))
+            elif refundStatus == "PROCESSING":
+                if not (self._refundOrder.is_processing or self._refundOrder.is_closed or self._refundOrder.is_success):
+                    self._refundOrder.processing()
+                return True
+            elif refundStatus == "CHANGE":
+                matched = self._refundOrder.closed(
+                    errorCode = 'CHANGE',
+                    errorDesc = '{}({})'.format(result.get("err_code_des"), result.get("err_code")),
+                    **{
+                        'tradeRefundNo': result.get("refund_id_{}".format(offset))
+                    })
+                if matched:
+                    refund_post_callable(self._refundOrder, False)
+                return True
+            else:
+                pass
+
+            return False

+ 71 - 66
apps/web/common/transaction/withdraw.py

@@ -7,7 +7,7 @@ import traceback
 
 import arrow
 from django.conf import settings
-from typing import TYPE_CHECKING, Callable, Union, Optional, cast
+from typing import TYPE_CHECKING, Callable, Union, Optional
 
 from apilib.monetary import RMB
 from apilib.utils_sys import memcache_lock
@@ -22,14 +22,14 @@ from apps.web.core.payment.wechat import WechatWithdrawGateway
 from apps.web.exceptions import WithdrawOrderNotExist
 from library.alipay import AliPayGatewayException
 from library.alipay import AliPayServiceException
-from library.wechatbase.exceptions import WechatNetworkException, WeChatPayException
+from library.wechatbase.exceptions import WechatNetworkException, WeChatException
 
 if TYPE_CHECKING:
     from contextlib import GeneratorContextManager
     from apps.web.common.models import CapitalUser
     from apps.web.common.models import WithdrawRecord
     from apps.web.core.payment.wechat import WechatWithdrawQueryResult
-    from apps.web.core.payment import WithdrawGatewayT
+    from apps.web.core.payment.type_checking import WithdrawGatewayT
 
 logger = logging.getLogger(__name__)
 
@@ -214,7 +214,8 @@ def withdraw_via_bank_in_wechat(gateway, record, bankcard):
         else:
             return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
 
-    except WeChatPayException as e:
+    except WeChatException as e:
+        # 接口性质的失败
         logger.error(repr(e))
         return error_handler(e.errCode, e.errMsg)
 
@@ -301,7 +302,7 @@ def withdraw_via_wechat(gateway, record, payOpenId, real_user_name):
             else:
                 return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
 
-        except WeChatPayException as e:
+        except WeChatException as e:
             logger.error(repr(e))
             return error_handler(e.errCode, e.errMsg)
 
@@ -359,7 +360,7 @@ def withdraw_via_wechat(gateway, record, payOpenId, real_user_name):
             else:
                 return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
 
-        except WeChatPayException as e:
+        except WeChatException as e:
             logger.error(repr(e))
             return error_handler(e.errCode, e.errMsg)
 
@@ -393,19 +394,6 @@ class WithdrawService(object):
                 logger.info('test environment do not support withdraw.')
                 raise ServiceException({'result': 0, 'description': u'测试环境不允许提现', 'payload': {}})
 
-            if self.amount < RMB(settings.WITHDRAW_MINIMUM):
-                raise ServiceException(
-                    {'result': 0, 'description': u"提现金额不能少于%s元" % (settings.WITHDRAW_MINIMUM,), 'payload': {}})
-
-            if self.amount > RMB(settings.WITHDRAW_MAXIMUM):
-                raise ServiceException(
-                    {'result': 0, 'description': u"单次提现金额不得大于%s元" % (settings.WITHDRAW_MAXIMUM,), 'payload': {}})
-
-            #: 大额提现单预警
-            if self.amount >= RMB(settings.WHALE_WITHDRAWAL_ORDER_AMOUNT):
-                from taskmanager.mediator import task_caller
-                task_caller('whale_withdraw_order_alert')
-
             if self.payee.no_withdraw:
                 raise ServiceException(
                     {'result': 0, 'description': u"您的账号权限不足或者异常,暂时不能提现。", 'payload': {}})
@@ -413,24 +401,36 @@ class WithdrawService(object):
             if self.payee.abnormal:
                 raise ServiceException({'result': 0, 'description': u'该帐号资金异常,请联系客服处理', 'payload': {}})
 
-            if not self.payee.can_withdraw_today:
-                raise ServiceException(
-                    {'result': 0, 'description': u"超过每日最大提现次数,请明天再试。", 'payload': {}})
+            self.payee.check_withdraw_min_fee(
+                income_type = self.income_type, amount = self.amount, pay_type = self.pay_type)
+
+            self.payee.can_withdraw_today(self.amount)
+
+            #: 大额提现单预警
+            if self.amount >= RMB(settings.WHALE_WITHDRAWAL_ORDER_AMOUNT):
+                from taskmanager.mediator import task_caller
+                task_caller('whale_withdraw_order_alert')
 
             with withdraw_lock(self.payee.role, str(self.payee.id)) as acquired:
                 if acquired:
                     if self.payee.sub_balance(self.income_type, source_key) < self.amount:
                         raise ServiceException({'result': 0, 'description': u'余额不足', 'payload': {}})
 
-                    is_ledger, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
+                    is_ledger, agent, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
 
                     if not is_ledger:
                         raise ServiceException({'result': 0, 'description': u'暂时不支持提现(1001)', 'payload': {}})
 
                     if self.pay_type == WITHDRAW_PAY_TYPE.WECHAT:
                         if withdraw_gateway_list['wechat'].manual_withdraw:
-                            logger.debug('gateway<{}> is manual withdraw.'.format(repr(withdraw_gateway_list['wechat'])))
-                            raise ServiceException({'result': 0, 'description': u'暂时不支持提现(1002)', 'payload': {}})
+                            logger.debug(
+                                'gateway<{}> is manual withdraw.'.format(repr(withdraw_gateway_list['wechat'])))
+                            raise ServiceException(
+                                {'result': 0, 'description': u'不支持手动提现到微信,请选择提现到银行卡。', 'payload': {}})
+
+                        if not withdraw_gateway_list['wechat'].support_withdraw:
+                            raise ServiceException(
+                                {'result': 0, 'description': u'由于微信接口变更,提现到微信功能已经下线。请您选择提现到支付宝或者银行卡', 'payload': {}})
 
                         pay_entity = WithdrawBankCard(
                             accountCode = self.payee.withdraw_open_id,
@@ -454,11 +454,12 @@ class WithdrawService(object):
 
                         handler = self.payee.new_withdraw_handler(self.record)
 
-                        updated = self.payee.freeze_balance(self.record.incomeType,
-                                                            self.record.amount,
-                                                            self.record.source_key,
-                                                            self.record.order,
-                                                            )
+                        updated = self.payee.freeze_balance(
+                            self.record.incomeType,
+                            self.record.amount,
+                            self.record.withdrawSourceKey,
+                            self.record.order,
+                        )
                         if not updated:
                             handler.revoke(remarks = u'扣款失败', description = u'扣款失败')
                             raise ServiceException({'result': 0, 'description': u'扣款失败', 'payload': {}})
@@ -474,7 +475,11 @@ class WithdrawService(object):
                                 self.record.order, repr(withdraw_result)))
 
                         if withdraw_result.result is True:
-                            handler.approve(finishedTime=datetime.datetime.now())
+                            if wechat_withdraw_gateway.version == 'v3':
+                                handler.processing(remarks = u'提现申请已经受理', description = u'提现申请已经受理')
+                            else:
+                                handler.approve(finishedTime = datetime.datetime.now())
+
                             return {'result': 1, 'description': withdraw_result.show_message,
                                     'payload': {'paymentId': str(self.record.id)}}
                         else:
@@ -503,37 +508,38 @@ class WithdrawService(object):
                         manual = False
                         withdraw_gateway = withdraw_gateway_list['wechat']
 
-                        if bank_card.manual:
+                        if bank_card.manual or withdraw_gateway_list['wechat'].manual_withdraw:
                             manual = True
 
                         elif bank_card.accountType == WithdrawBankCard.AccountType.PUBLIC:
-                            if withdraw_gateway_list['alipay']:
+                            if withdraw_gateway_list['alipay'].support_withdraw_bank:
                                 withdraw_gateway = withdraw_gateway_list['alipay']  # type: WithdrawGateway
                                 if not WithdrawBanks.support(bank_card.bankName):
                                     raise ServiceException({
                                         'result': 0,
                                         'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1001)',
                                         'payload': {}})
+
                             else:
-                                manual = True
+                                raise ServiceException({
+                                    'result': 0,
+                                    'description': u'不支持提现到对公银行卡, 请联系平台客服(1001)',
+                                    'payload': {}})
                         else:
-                            # if withdraw_gateway_list['alipay'] and self.payee.supports('withdraw_alipay'):
-                            if withdraw_gateway_list['alipay']:
-                                withdraw_gateway = withdraw_gateway_list['alipay']  # type: WithdrawGateway
-                                if not WithdrawBanks.support(bank_card.bankName):
-                                    raise ServiceException({
-                                        'result': 0,
-                                        'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1002)',
-                                        'payload': {}})
-                            else:
-                                if withdraw_gateway_list['wechat'].manual_withdraw:
-                                    manual = True
-                                else:
+                            while True:
+                                if withdraw_gateway_list['alipay'].support_withdraw_bank:
+                                    if WithdrawBanks.support(bank_card.bankName):
+                                        withdraw_gateway = withdraw_gateway_list['alipay']  # type: WithdrawGateway
+                                        break
+
+                                if withdraw_gateway_list['wechat'].support_withdraw_bank:
                                     wechat_bank_code = WithdrawBanks.get_wechat_bank_code(bank_card.bankName)
-                                    if not wechat_bank_code:
-                                        raise ServiceException({
+                                    if wechat_bank_code:
+                                        break
+
+                                raise ServiceException({
                                             'result': 0,
-                                            'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1002)',
+                                            'description': u'不支持提现到银行卡, 请联系平台客服(1002)',
                                             'payload': {}})
 
                         self.record = self.payee.new_withdraw_record(
@@ -554,7 +560,7 @@ class WithdrawService(object):
                         updated = self.payee.freeze_balance(
                             self.record.incomeType,
                             self.record.amount,
-                            self.record.source_key,
+                            self.record.withdrawSourceKey,
                             self.record.order
                         )
                         if not updated:
@@ -602,6 +608,13 @@ class WithdrawService(object):
                                      'payload': {}})
 
                     elif self.pay_type == WITHDRAW_PAY_TYPE.ALIPAY:
+                        withdraw_gateway = withdraw_gateway_list['alipay']  # type: WithdrawGatewayT
+                        if not withdraw_gateway.support_withdraw:
+                            raise ServiceException({
+                                'result': 0,
+                                'description': u'不支持提现到支付宝,请联系平台客服(1002)',
+                                'payload': {}})
+
                         pay_entity = WithdrawBankCard(
                             accountCode = self.payee.withdraw_alipay_login_id,
                             accountName = self.payee.withdraw_alipay_real_name,
@@ -611,14 +624,7 @@ class WithdrawService(object):
                         if not pay_entity.accountCode:
                             raise ServiceException({
                                 'result': 0,
-                                'description': u'不支持提现到支付宝账号或者是配置错误(1001)',
-                                'payload': {}})
-
-                        withdraw_gateway = withdraw_gateway_list['alipay']  # type: WithdrawGatewayT
-                        if not withdraw_gateway:
-                            raise ServiceException({
-                                'result': 0,
-                                'description': u'不支持提现到支付宝账号或者是配置错误(1002)',
+                                'description': u'您还没有配置提现支付宝账号',
                                 'payload': {}})
 
                         self.record = self.payee.new_withdraw_record(
@@ -639,7 +645,7 @@ class WithdrawService(object):
                         updated = self.payee.freeze_balance(
                             self.record.incomeType,
                             self.record.amount,
-                            self.record.source_key,
+                            self.record.withdrawSourceKey,
                             self.record.order,
                         )
                         if not updated:
@@ -682,7 +688,7 @@ class WithdrawService(object):
                 else:
                     raise ServiceException({'result': 0, 'description': u'操作频繁,请稍后再试', 'payload': {}})
         except ServiceException as e:
-            logger.exception(e)
+            logger.warning(str(e))
 
             if self.record:
                 if 'payload' in e.result:
@@ -818,9 +824,8 @@ class WithdrawRetryService(object):
 
                 payee.freeze_balance(self.record.incomeType,
                                      self.record.amount,
-                                     self.record.source_key,
-                                     self.record.order,
-                                     self.record.is_new_version)
+                                     self.record.withdrawSourceKey,
+                                     self.record.order)
 
                 withdraw_result = withdraw_via_wechat(withdraw_gateway,
                                                       self.record,
@@ -901,7 +906,7 @@ class WithdrawRetryService(object):
 
                 payee.freeze_balance(self.record.incomeType,
                                      self.record.amount,
-                                     self.record.source_key,
+                                     self.record.withdrawSourceKey,
                                      self.record.order,
                                      )
 
@@ -971,7 +976,7 @@ class WithdrawRetryService(object):
 
                 updated = payee.freeze_balance(self.record.incomeType,
                                                self.record.amount,
-                                               self.record.source_key,
+                                               self.record.withdrawSourceKey,
                                                self.record.order,
                                                )
                 if not updated:
@@ -1017,7 +1022,7 @@ class WithdrawRetryService(object):
         except WechatNetworkException as e:
             logger.exception(e)
             return {'result': 0, 'description': e.errMsg, 'payload': {}}
-        except WeChatPayException as e:
+        except WeChatException as e:
             logger.exception(e)
             return {'result': 0, 'description': e.errMsg, 'payload': {}}
         except ServiceException as e:

+ 2 - 2
apps/web/common/views.py

@@ -13,7 +13,6 @@ import urllib2
 import simplejson as json
 import xmltodict
 from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
-from django.views.decorators.gzip import gzip_page
 from django.views.generic.base import View
 from pymongo.results import UpdateResult
 from typing import TYPE_CHECKING, Iterable, Dict
@@ -38,7 +37,7 @@ from apps.web.core import ROLE
 from apps.web.core.exceptions import InvalidFileSize, InvalidFileName
 from apps.web.core.file import AliOssFileUploader, WechatSubscriptionAccountVerifyFileUploader
 from apps.web.core.models import WechatPayApp, AliApp
-from apps.web.core.payment.ali import AliPayGateway
+
 from apps.web.core.utils import DefaultJsonErrorResponse, JsonOkResponse, JsonErrorResponse
 from apps.web.dealer.models import Dealer
 from apps.web.device.models import Device
@@ -56,6 +55,7 @@ if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
     from apps.web.device.models import DeviceDict
     from apps.web.core.adapter.bolai_gateway import ChargingGatewayBox
+    from apps.web.core.payment.ali import AliPayGateway
 
 logger = logging.getLogger(__name__)
 

+ 23 - 78
apps/web/constant.py

@@ -602,61 +602,6 @@ Const.EVENT_CODE_DESC = {
 
 Const.USER_SEX = enum(UNKNOWN = 0, MALE = 1, FEMALE = 2, ALL = -1)
 
-Const.ADDRESS_TYPE = [
-    {
-        'value': 'school',
-        'label': u'学校'
-    },
-    {
-        'value': 'apartment',
-        'label': u'公寓'
-    },
-    {
-        'value': 'workshop',
-        'label': u'工厂'
-    },
-    {
-        'value': 'others',
-        'label': u'其他'
-    },
-    {
-        'value': 'mall',
-        'label': u'商场'
-    },
-    {
-        'value': 'hospital',
-        'label': u'医院'
-    },
-    {
-        'value': 'cafe',
-        'label': u'咖啡厅'
-    },
-    {
-        'value': 'KTV',
-        'label': u'ktv',
-    },
-    {
-        'value': 'hotel',
-        'label': u'宾馆'
-    },
-    {
-        'value': 'bar',
-        'label': u'酒吧'
-    },
-    {
-        'value': 'parking_lot',
-        'label': u'停车场'
-    },
-    {
-        'value': 'square',
-        'label': u'广场'
-    },
-    {
-        'value': 'bath_center',
-        'label': u'洗浴中心'
-    }
-]
-
 Const.CELERY_TASK_RESULT_TRANSLATION = {
     'PENDING': u'处理中',
     'STARTED': u'已开启',
@@ -666,18 +611,6 @@ Const.CELERY_TASK_RESULT_TRANSLATION = {
     'UNKNOWN': u'未知'
 }
 
-# : 默认的消息推送模版
-Const.DEFAULT_WECHAT_USER_PUSH_MESSAGE_ID_MAP = {
-    'feedback_process': '',
-    'service_complete': '',
-    'refund_coins': '',
-    'device_fault': '',
-    'less_balance': '',
-    'consume_notify': '',
-    'service_expired': '',
-    'device_warnning': ''
-}
-
 Const.TRANSLATION = {
     'feedback_process': u'故障处理完成提醒',
     'feedback': u"用户报障",
@@ -697,6 +630,8 @@ Const.TRANSLATION = {
     'exchange_order_notify': u'售后服务通知',
     'service_start': u'设备启动通知',
     'dev_start': u'启动通知',
+    'charge_order_complete': u'充电订单完成提醒',
+    'common_order_complete': u'订单完成提醒'
 }
 
 
@@ -865,7 +800,10 @@ class RechargeRecordVia(StrEnum):
     Insurance = "insurance"  # 支付保险的钱
     Redpack = "redpack"  # 红包抵扣
     Mix = "mix"  # 混合单 (保险+充电)(红包抵扣+充电)
-    RefundCash = "refundCash"  # 退款订单
+    RefundCash = "refundCash"  # 现金退款
+
+    RevokeRefundCash = 'revokeRefundCash'  # 现金退款退单
+
     Swap = 'swap'  # 互联互通
     AutoSim = 'autoSim'
 
@@ -880,7 +818,8 @@ RECHARGE_RECORD_VIA_TRANSLATION = {
     RechargeRecordVia.MonthlyPackage: u"包月卡充值",
     RechargeRecordVia.Insurance: u"保险",
     RechargeRecordVia.Redpack: u"红包抵扣",
-    RechargeRecordVia.RefundCash: u'退费',
+    RechargeRecordVia.RefundCash: u'现金退款',
+    RechargeRecordVia.RevokeRefundCash: u'现金退款退单',
     RechargeRecordVia.Swap: u'互联互通',
     RechargeRecordVia.AutoSim: u"流量卡自动充值"
 }
@@ -910,18 +849,22 @@ class USER_RECHARGE_TYPE(IterConstant):
     RECHARGE_REDPACK = RechargeRecordVia.Redpack
     RECHARGE_MIX = RechargeRecordVia.Mix
     REFUND_CASH = RechargeRecordVia.RefundCash
+    REVOKE_REFUND_CASH = RechargeRecordVia.RevokeRefundCash
 
     SWAP = RechargeRecordVia.Swap
 
 
 class DEALER_CONSUMPTION_AGG_KIND(IterConstant):
     # : 消费聚合类别
+
     COIN = 'coin'
+    SPEND_MONEY = 'spendMoney'  # 实际花费
+
     PACKAGE = 'package'  # 纸巾包
     REFUNDED_COINS = 'refundedMoney'  # 退费金额(金币)
     REFUNDED_CASH = 'refundedCash'  # 退费金额(元)
     ELEC = 'elec'  # 消耗电量
-    SPEND_MONEY = 'spendMoney'  # 实际花费
+
     CONSUME_CARD = 'consumeCard'  # 刷卡消费额
     REFUND_CARD = 'refundCard'  # 刷卡退费额
     DURATION = 'duration'  # 充电时间
@@ -930,8 +873,8 @@ class DEALER_CONSUMPTION_AGG_KIND(IterConstant):
     TOTAL_COUNT = 'totalCount'  # 线下投币
     SERVICEFEE = 'serviceFee' # 服务费
 
-    SERVICE_CHARGE = 'serviceCharge'
-    ELEC_CHARGE = 'elecCharge'
+    SERVICE_CHARGE = 'serviceCharge' # 同SERVICEFEE,兼容需要,后续不在使用
+    ELEC_CHARGE = 'elecCharge' # ELECFEE,兼容需要,后续不在使用
 
 
 DEALER_CONSUMPTION_AGG_KIND_TRANSLATION = \
@@ -950,8 +893,8 @@ DEALER_CONSUMPTION_AGG_KIND_TRANSLATION = \
         DEALER_CONSUMPTION_AGG_KIND.TOTAL_COUNT: u'线下投币次数',
         DEALER_CONSUMPTION_AGG_KIND.SERVICEFEE: u'服务费',
 
-        DEALER_CONSUMPTION_AGG_KIND.SERVICE_CHARGE: u'服务费',
-        DEALER_CONSUMPTION_AGG_KIND.ELEC_CHARGE: u'电费'
+        DEALER_CONSUMPTION_AGG_KIND.SERVICE_CHARGE: u'服务费', # 兼容需要,后续不在使用
+        DEALER_CONSUMPTION_AGG_KIND.ELEC_CHARGE: u'电费' # 兼容需要,后续不在使用
     }
 
 DEALER_CONSUMPTION_AGG_KIND_UNIT = \
@@ -970,8 +913,8 @@ DEALER_CONSUMPTION_AGG_KIND_UNIT = \
         DEALER_CONSUMPTION_AGG_KIND.REFUND_CARD: u'元',
         DEALER_CONSUMPTION_AGG_KIND.SERVICEFEE: u'元',
 
-        DEALER_CONSUMPTION_AGG_KIND.SERVICE_CHARGE: u'元',
-        DEALER_CONSUMPTION_AGG_KIND.ELEC_CHARGE: u'元',
+        DEALER_CONSUMPTION_AGG_KIND.SERVICE_CHARGE: u'元', # 兼容需要,后续不在使用
+        DEALER_CONSUMPTION_AGG_KIND.ELEC_CHARGE: u'元', # 兼容需要,后续不在使用
     }
 
 DEALER_CONSUMPTION_AGG_KIND_UNIT_PRECISION = \
@@ -989,8 +932,8 @@ DEALER_CONSUMPTION_AGG_KIND_UNIT_PRECISION = \
         DEALER_CONSUMPTION_AGG_KIND.TOTAL_COUNT: '1',
         DEALER_CONSUMPTION_AGG_KIND.SERVICEFEE: '0.01',
 
-        DEALER_CONSUMPTION_AGG_KIND.SERVICE_CHARGE: '0.01',
-        DEALER_CONSUMPTION_AGG_KIND.ELEC_CHARGE: '0.01',
+        DEALER_CONSUMPTION_AGG_KIND.SERVICE_CHARGE: '0.01', # 兼容需要,后续不在使用
+        DEALER_CONSUMPTION_AGG_KIND.ELEC_CHARGE: '0.01', # 兼容需要,后续不在使用
     }
 
 MONTH_DATE_KEY = '{year:d}-{month:02d}'
@@ -1569,3 +1512,5 @@ class OneCardGateAction(object):
 
     def choice(self):
         return [self.ENTER, self.OUT]
+
+Const.DB_BATCH_ROW_READ = 5000

+ 1 - 4
apps/web/core/__init__.py

@@ -26,6 +26,7 @@ class PayAppType(IterConstant):
     PLATFORM_PROMOTION = "platform_promotion"
     PLATFORM_WALLET = 'platform_wallet'
     PLATFORM_RECONCILE ='platform_reconcile'
+    JD_OPEN = 'jdopen'
     MANUAL = 'manual'
 
     SWAP = 'swap'
@@ -195,10 +196,6 @@ class AlipayMixin(_BaseMixin):
     def signKeyType(self):
         return self.__app_for_inner__.signKeyType
 
-    @property
-    def __gateway_type__(self):
-        return AppPlatformType.ALIPAY
-
     @property
     def __bound_openid_key__(self):
         # 支付宝用户ID是唯一的,直接以平台类型区分

+ 2 - 5
apps/web/core/bridge/alipay/__init__.py

@@ -1,5 +1,2 @@
-# coding=utf-8
-
-from apps.web.core.bridge.alipay.authorder import AliPayAuthorProxy
-
-
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python

+ 0 - 100
apps/web/core/bridge/alipay/authorder.py

@@ -1,100 +0,0 @@
-# coding=utf-8
-import os
-
-from alipay.aop.api.FileItem import FileItem
-from alipay.aop.api.request.AlipayMerchantIndirectAuthorderCloseRequest import AlipayMerchantIndirectAuthorderCloseRequest
-from alipay.aop.api.request.AlipayMerchantIndirectAuthorderCreateRequest import AlipayMerchantIndirectAuthorderCreateRequest
-from alipay.aop.api.request.AlipayMerchantIndirectAuthorderQuerystatusRequest import AlipayMerchantIndirectAuthorderQuerystatusRequest
-from alipay.aop.api.request.AlipayMerchantIndirectSmidbindQueryRequest import AlipayMerchantIndirectSmidbindQueryRequest
-from alipay.aop.api.request.AntMerchantExpandIndirectImageUploadRequest import AntMerchantExpandIndirectImageUploadRequest
-from alipay.aop.api.response.AlipayMerchantIndirectAuthorderCloseResponse import AlipayMerchantIndirectAuthorderCloseResponse
-from alipay.aop.api.response.AlipayMerchantIndirectAuthorderCreateResponse import AlipayMerchantIndirectAuthorderCreateResponse
-from alipay.aop.api.response.AlipayMerchantIndirectAuthorderQuerystatusResponse import AlipayMerchantIndirectAuthorderQuerystatusResponse
-from alipay.aop.api.response.AlipayMerchantIndirectSmidbindQueryResponse import AlipayMerchantIndirectSmidbindQueryResponse
-from alipay.aop.api.response.AlipayOfflineMaterialImageUploadResponse import AlipayOfflineMaterialImageUploadResponse
-
-from apps.web.core.bridge.alipay.base import AliApiProxy
-from apps.web.core.file import AliOssFileUploader
-
-
-class AliPayAuthorProxy(AliApiProxy):
-
-    def upload_image_from_oss(self, url):
-        content = AliOssFileUploader.load(url)
-        return self.upload(os.path.basename(url), content)
-
-    def upload(self, imageName, imageContent):
-        """
-        上传图片
-        """
-        _type = imageName.rsplit(".")[-1]
-        image = FileItem(file_name=imageName, file_content=imageContent)
-
-        request = AntMerchantExpandIndirectImageUploadRequest()
-        request.image_type, request.image_content = _type, image
-
-        response = self.request(request, AlipayOfflineMaterialImageUploadResponse())    # type: AlipayOfflineMaterialImageUploadResponse
-        return {
-            "media_id": response.image_id
-        }
-
-    def query(self, subMerchantId):     # type: (str) -> dict
-        request = AlipayMerchantIndirectSmidbindQueryRequest()
-        request.biz_content = {"sub_merchant_id": subMerchantId}
-
-        response = self.request(request, AlipayMerchantIndirectSmidbindQueryResponse())     # type: AlipayMerchantIndirectSmidbindQueryResponse
-
-        return {
-            "checkResult": response.check_result
-        }
-
-    def close(self, applyId=None, busCode=None):
-        """
-        关闭申请单
-        """
-        request = AlipayMerchantIndirectAuthorderCloseRequest()
-        if applyId:
-            request.biz_content = {"order_no": applyId}
-        else:
-            request.biz_content = {"out_biz_no": busCode}
-
-        response = self.request(request, AlipayMerchantIndirectAuthorderCloseResponse())
-
-        return dict()
-
-    def query_status(self, applyId=None, busCode=None):
-        """
-        查询申请状态
-        """
-        request = AlipayMerchantIndirectAuthorderQuerystatusRequest()
-        if applyId:
-            request.biz_content = {"order_no": applyId}
-        else:
-            request.biz_content = {"out_biz_no": busCode}
-
-        response = self.request(request, AlipayMerchantIndirectAuthorderQuerystatusResponse())  # type: AlipayMerchantIndirectAuthorderQuerystatusResponse
-        data = {
-            "applyId": response.order_no,
-            "auditStatus": response.order_status,
-            "qrCode": response.qr_code
-        }
-
-        if response.verify_list:
-            data["errorMsg"] = "{}-{}".format(response.verify_list[0]["fail_param"], response.verify_list[0]["fail_reason"])
-
-        return data
-
-    def create(self, applier):
-        request = AlipayMerchantIndirectAuthorderCreateRequest()
-
-        request.biz_content = applier.to_ali_auth()
-
-        response = self.request(request, AlipayMerchantIndirectAuthorderCreateResponse())       # type: AlipayMerchantIndirectAuthorderCreateResponse
-        return {
-            "applyId": response.order_no,
-            "auditStatus": response.order_status
-        }
-
-
-
-

+ 2 - 65
apps/web/core/bridge/alipay/base.py

@@ -1,65 +1,2 @@
-# coding=utf-8
-
-import logging
-
-from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
-from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
-from typing import TYPE_CHECKING
-
-from library.alipay import AliException
-
-if TYPE_CHECKING:
-    from apps.web.core.models import AliApp
-
-logger = logging.getLogger(__name__)
-
-
-class AliApiProxy(object):
-
-    URL = "https://openapi.alipay.com/gateway.do"
-    URL_DEBUG = "https://openapi.alipaydev.com/gateway.do"
-
-    def __init__(self, provider, debug=False):   # type:(AliApp, bool) -> None
-        self._url = self.URL if not debug else self.URL_DEBUG
-        self._appid = str(provider.appid)
-        self._app_private_key = provider.app_private_key_string
-        self._alipay_public_key = provider.public_key_string
-
-        self._client = None
-
-    def __str__(self):
-        return "AliApiProxy <{}_{}>".format(self.__class__.__name__, self._appid)
-
-    @property
-    def client(self):
-        if not self._client:
-            _config = AlipayClientConfig()
-            _config.server_url = self._url
-            _config.app_id = self._appid
-            _config.app_private_key, _config.alipay_public_key = self._app_private_key, self._alipay_public_key
-
-            self._client = DefaultAlipayClient(_config, logger=logger)
-
-        return self._client
-
-    def request(self, request, response):
-        try:
-            responseContent = self.client.execute(request)
-        except Exception as e:
-            logger.error(e.message)
-            raise AliException(errCode="", errMsg=e.message)
-
-        response.parse_response_content(responseContent)
-        if not response.is_success():
-            raise AliException(errCode=response.code, errMsg=response.msg)
-
-        return response
-
-
-
-
-
-
-
-
-
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python

+ 1 - 1
apps/web/core/device_define/changyuan.py

@@ -166,7 +166,7 @@ class CYCardMixin(object):
             logger.error("dev <{}> don't get a recharge record by id <{}>".format(self.device.devNo, rechargeRcdId))
             return
 
-        if not record.is_success():
+        if not record.is_success:
             logger.debug('{} state is {}.'.format(str(record), record.result))
             raise ValueError(u'订单不能为未支付状态')
 

+ 0 - 12
apps/web/core/payment/__init__.py

@@ -1,17 +1,5 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 
-from typing import Union, TYPE_CHECKING
-
 from apps.web.core.payment.base import PaymentGateway
 from apps.web.core.payment.base import WithdrawGateway
-
-if TYPE_CHECKING:
-    from apps.web.core.payment.ali import AliPayGateway, AliPayWithdrawGateway
-    from apps.web.core.payment.wechat import WechatPaymentGateway, WechatWithdrawGateway
-
-    PaymentGatewayT = Union[AliPayGateway, WechatPaymentGateway]
-
-    WechatMiniPaymentGatewayT = Union[WechatPaymentGateway]
-
-    WithdrawGatewayT = Union[AliPayWithdrawGateway, WechatWithdrawGateway]

+ 12 - 8
apps/web/core/payment/ali.py

@@ -11,10 +11,11 @@ from typing import TYPE_CHECKING
 from apilib.monetary import RMB
 from apilib.monetary import quantize
 from apilib.utils_string import cn
+from apps.web.constant import AppPlatformType
 from apps.web.exceptions import WithdrawOrderNotExist
 from apps.web.common.models import WithdrawBankCard
 from apps.web.core import AlipayMixin
-from apps.web.core.payment.base import PaymentGateway, WithdrawGateway
+from apps.web.core.payment import PaymentGateway, WithdrawGateway
 from apps.web.utils import testcase_point
 from library.alipay import AliPayGatewayException, AliErrorCode, AliException
 from library.alipay import AliPayServiceException
@@ -90,10 +91,12 @@ class AliPayGateway(PaymentGateway, AlipayMixin):
         Alipay 支付网关,扩展原库没有的接口 ``alipay.trade.create``
     """
 
-    def __init__(self, app):
-        # type: (AliApp)->None
+    def __init__(self, app, gateway_type = AppPlatformType.ALIPAY):
+        # type: (AliApp, AppPlatformType)->None
         super(AliPayGateway, self).__init__(app)
 
+        self.__gateway_type__ = gateway_type
+
     def __repr__(self):
         return '<AliPayPaymentGateway(appid=%s, debug=%s)>' % (self.appid, self.debug)
 
@@ -199,14 +202,15 @@ class AliPayGateway(PaymentGateway, AlipayMixin):
         return self.client.api_alipay_data_dataservice_bill_downloadurl_query(bill_type = bill_type,
                                                                               bill_date = bill_date)
 
-    def api_refund_query(self, trade_no, out_trade_no, out_request_no):
-        return self.client.api_alipay_trade_refund_order_query(trade_no, out_trade_no, out_request_no, ["gmt_refund_pay"])
+    def api_refund_query(self, out_refund_no, out_trade_no):
+        return self.client.api_alipay_trade_refund_order_query(out_trade_no, out_refund_no, ["gmt_refund_pay"])
 
 
 class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
-    def __init__(self, app, is_ledger = True):
-        # type: (AliApp, bool)->None
-        super(AliPayWithdrawGateway, self).__init__(app, is_ledger)
+    def __init__(self, app):
+        # type: (AliApp)->None
+        super(AliPayWithdrawGateway, self).__init__(app)
+        self.__gateway_type__ = AppPlatformType.WITHDRAW
 
     def __repr__(self):
         return '<AliPayWithdrawGateway(appid=%s, debug=%s)>' % (self.appid, self.debug)

+ 17 - 14
apps/web/core/payment/base.py

@@ -14,7 +14,7 @@ from apps.web.core import PAY_APP_MAP, APP_KEY_DELIMITER, PayAppType, BaseAppPro
 
 if TYPE_CHECKING:
     from apps.web.core.models import PayAppBase
-    from apps.web.core.payment import PaymentGatewayT
+    from apps.web.core.payment.type_checking import PaymentGatewayT
     from apps.web.common.transaction.pay import RechargeRecordT
 
 
@@ -125,6 +125,11 @@ class PaymentGateway(_PaymentGateway):
         return self.occupant.withdraw_source_key(self.app)
 
 
+
+    def api_refund_query(self, out_refund_no, out_trade_no = None):
+        raise NotImplementedError('must implement api_refund_query.')
+
+
 class WithdrawGateway(_PaymentGateway):
     """
     提现网关基类
@@ -133,30 +138,28 @@ class WithdrawGateway(_PaymentGateway):
     LEDGER_PREFIX = 'ledger'
     NO_LEDGER_PREFIX = 'noledger'
 
-    _WITHDRAW_MAP = {
-        # 只支持微信提现
-        PayAppType.WECHAT: 'apps.web.core.payment.wechat.WechatWithdrawGateway'
-    }
-
-    def __init__(self, app, ledger = False):
-        # type: (cast(PayAppBase), bool)->None
+    def __init__(self, app):
+        # type: (cast(PayAppBase))->None
         super(WithdrawGateway, self).__init__(app)
-        self._ledger = ledger
-
-    @property
-    def ledger(self):
-        return self._ledger
 
     @classmethod
     def is_ledger(cls, source_key):
         # type: (str)->bool
         return source_key.startswith(cls.LEDGER_PREFIX)
 
+    @property
+    def support_withdraw(self):
+        return self.app.supportWithdraw
+
+    @property
+    def support_withdraw_bank(self):
+        return self.app.supportWithdrawBank
+
     @classmethod
     def from_withdraw_gateway_key(cls, withdraw_gateway_key, gateway_version):
         pay_app = cls.get_app_from_gateway_key(gateway_key = withdraw_gateway_key,
                                                default_pay_app_type = PayAppType.WECHAT)  # type: cast(PayAppBase)
-        return pay_app.new_withdraw_gateway(is_ledger = True, gateway_version = gateway_version)
+        return pay_app.new_withdraw_gateway(gateway_version = gateway_version)
 
     @property
     def manual_withdraw(self):

+ 12 - 9
apps/web/core/payment/wechat.py

@@ -32,7 +32,7 @@ from apps.web.core.payment import PaymentGateway, WithdrawGateway
 from apps.web.utils import testcase_point
 from library.wechatpayv3 import WechatClientV3
 from library.wechatpy.pay import WeChatPay
-from library.wechatbase.exceptions import WeChatPayException
+from library.wechatbase.exceptions import WeChatException
 
 if TYPE_CHECKING:
     pass
@@ -62,7 +62,10 @@ class WechatWithdrawQueryResult(dict):
     @property
     def is_failed(self):
         assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS'
-        return self.get('status') in frozenset([WECHAT_WITHDRAW_STATUS.FAILED, WECHAT_WITHDRAW_STATUS.BANK_FAIL])
+        return self.get('status') in frozenset([
+            WECHAT_WITHDRAW_STATUS.FAILED,
+            WECHAT_WITHDRAW_STATUS.BANK_FAIL,
+            WECHAT_WITHDRAW_STATUS.FAIL])
 
     @property
     def is_processing(self):
@@ -238,17 +241,17 @@ class WechatPaymentGateway(PaymentGateway, WechatPayMixin):
             refund_desc = refund_reason,
             notify_url=kwargs.get('notify_url'))
 
-    def api_refund_query(self, out_refund_no=None, refund_no=None):
+    def api_refund_query(self, out_refund_no, out_trade_no=None):
         """
+        :param out_trade_no:
         :param out_refund_no: 自己的退款单号
-        :param refund_no: 微信的退款单号
         """
-        return self.client.refund.query(refund_no, out_refund_no)
+        return self.client.refund.query(out_refund_no = out_refund_no)
 
 
 class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin):
-    def __init__(self, app, gateway_version = "v3", is_ledger = True):  # type: (WechatPayApp, str, bool)->None
-        super(WechatWithdrawGateway, self).__init__(app, is_ledger)
+    def __init__(self, app, gateway_version = "v3"):  # type: (WechatPayApp, str)->None
+        super(WechatWithdrawGateway, self).__init__(app)
 
         self.version = gateway_version
         self.__gateway_type__ = AppPlatformType.WITHDRAW
@@ -328,7 +331,7 @@ class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin):
         """
         try:
             return WechatWithdrawQueryResult(self.client.transfer.query_bankcard(out_trade_no = order_no))
-        except WeChatPayException as e:
+        except WeChatException as e:
             if e.errCode in ['NOT_FOUND', 'ORDERNOTEXIST']:
                 raise WithdrawOrderNotExist()
             else:
@@ -351,7 +354,7 @@ class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin):
                 return WechatWithdrawQueryResult(rv)
             else:
                 return WechatWithdrawQueryResult(self.client.transfer.query(out_trade_no = order_no))
-        except WeChatPayException as e:
+        except WeChatException as e:
             if e.errCode in ['NOT_FOUND', 'ORDERNOTEXIST']:
                 raise WithdrawOrderNotExist()
             else:

+ 17 - 4
apps/web/dealer/define.py

@@ -28,6 +28,8 @@ class DEALER_INCOME_SOURCE(IterConstant):
     RECHARGE_MONTHLY_PACKAGE = USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE
 
     REFUND_CASH = USER_RECHARGE_TYPE.REFUND_CASH
+    REVOKE_REFUND_CASH = USER_RECHARGE_TYPE.REVOKE_REFUND_CASH
+
     AUTO_SIM = RechargeRecordVia.AutoSim
 
     INSURANCE = USER_RECHARGE_TYPE.RECHARGE_INSURANCE
@@ -48,7 +50,8 @@ DEALER_INCOME_SOURCE_TRANSLATION = \
         DEALER_INCOME_SOURCE.REDPACK: u'平台补贴',
         DEALER_INCOME_SOURCE.REFUND_CASH: u'现金退费',
         DEALER_INCOME_SOURCE.AUTO_SIM: u'流量卡自动充值',
-        DEALER_INCOME_SOURCE.LEDGER_CONSUME: u"分润"
+        DEALER_INCOME_SOURCE.LEDGER_CONSUME: u"分润",
+        DEALER_INCOME_SOURCE.REVOKE_REFUND_CASH: u'现金退款退单',
     }
 
 
@@ -73,7 +76,9 @@ DealerConst.MAP_USER_SOURCE_TO_DEALER_SOURCE = {
 
     USER_RECHARGE_TYPE.RECHARGE_INSURANCE: DEALER_INCOME_SOURCE.INSURANCE,
 
-    USER_RECHARGE_TYPE.REFUND_CASH: DEALER_INCOME_SOURCE.REFUND_CASH
+    USER_RECHARGE_TYPE.REFUND_CASH: DEALER_INCOME_SOURCE.REFUND_CASH,
+
+    USER_RECHARGE_TYPE.REVOKE_REFUND_CASH: DEALER_INCOME_SOURCE.REVOKE_REFUND_CASH
 }
 
 # 经销商的收益类型转换为后台的字段
@@ -83,14 +88,15 @@ DealerConst.MAP_SOURCE_TO_TYPE = \
         DEALER_INCOME_SOURCE.RECHARGE_CARD: DEALER_INCOME_TYPE.DEVICE_INCOME,
         DEALER_INCOME_SOURCE.RECHARGE_VIRTUAL_CARD: DEALER_INCOME_TYPE.DEVICE_INCOME,
         DEALER_INCOME_SOURCE.AD: DEALER_INCOME_TYPE.AD_INCOME,
-        DEALER_INCOME_SOURCE.REDPACK: DEALER_INCOME_TYPE.DEVICE_INCOME,
+        DEALER_INCOME_SOURCE.REDPACK: DEALER_INCOME_TYPE.AD_INCOME,
 
         # 保险的单独算作一栏 仅仅为了和经销商统一 实际不会有这个收益目前收银策略下
-        DEALER_INCOME_SOURCE.INSURANCE: DEALER_INCOME_TYPE.DEVICE_INCOME,
+        DEALER_INCOME_SOURCE.INSURANCE: DEALER_INCOME_TYPE.AD_INCOME,
 
         DEALER_INCOME_SOURCE.RECHARGE_MONTHLY_PACKAGE: DEALER_INCOME_TYPE.DEVICE_INCOME,
 
         DEALER_INCOME_SOURCE.REFUND_CASH: DEALER_INCOME_TYPE.DEVICE_INCOME,
+        DEALER_INCOME_SOURCE.REVOKE_REFUND_CASH: DEALER_INCOME_TYPE.DEVICE_INCOME,
         DEALER_INCOME_SOURCE.AUTO_SIM: DEALER_INCOME_TYPE.DEVICE_INCOME,
         DEALER_INCOME_SOURCE.LEDGER_CONSUME: DEALER_INCOME_TYPE.LEDGER_CONSUME,
     }
@@ -106,19 +112,26 @@ if settings.DEBUG and get_test_point('dealer', 'PAY_NOTIFY_URL'):
     class PAY_NOTIFY_URL(IterConstant):
         WECHAT_PAY_BACK = concat_server_end_url(
             uri = '/dealer/{}/{}_finishedPay'.format(PayAppType.WECHAT, get_test_point('dealer', 'PAY_NOTIFY_URL')))
+        JD_OPEN_PAY_BACK = concat_server_end_url(
+            uri = '/dealer/{}/{}_finishedPay'.format(PayAppType.JD_OPEN, get_test_point('dealer', 'PAY_NOTIFY_URL')))
 else:
     class PAY_NOTIFY_URL(IterConstant):
         WECHAT_PAY_BACK = concat_server_end_url(uri = '/dealer/{}/finishedPay'.format(PayAppType.WECHAT))
 
+        JD_OPEN_PAY_BACK = concat_server_end_url(uri = '/dealer/{}/finishedPay'.format(PayAppType.JD_OPEN))
 
 if settings.DEBUG and get_test_point('dealer', 'REFUND_NOTIFY_URL'):
     class REFUND_NOTIFY_URL(IterConstant):
         WECHAT_REFUND_BACK = concat_server_end_url(
             uri = '/dealer/{}/{}_refundOrderNotifier'.format(
                 PayAppType.WECHAT, get_test_point('dealer', 'REFUND_NOTIFY_URL')))
+        JDOPEN_REFUND_BACK = concat_server_end_url(
+            uri = '/dealer/{}/{}_refundOrderNotifier'.format(
+                PayAppType.JD_OPEN, get_test_point('dealer', 'REFUND_NOTIFY_URL')))
 else:
     class REFUND_NOTIFY_URL(IterConstant):
         WECHAT_REFUND_BACK = concat_server_end_url(uri = '/dealer/{}/refundOrderNotifier'.format(PayAppType.WECHAT))
+        JDOPEN_REFUND_BACK = concat_server_end_url(uri = '/dealer/{}/refundOrderNotifier'.format(PayAppType.JD_OPEN))
 
 DEALER_BIND_WECHAT_URL = concat_server_end_url(uri = '/dealer/verifyNewWechatBindingCallback')
 DEALER_BIND_WALLET_WECHAT_URL = concat_server_end_url(uri = '/dealer/verifyNewWalletWechatBindingCallback')

+ 105 - 198
apps/web/dealer/models.py

@@ -312,26 +312,30 @@ class Dealer(CapitalUser):
         return super(Dealer, self).save(**kwargs)
 
     def update(self, **kwargs):
-        ruleDict = kwargs.pop('defaultDiscountConfig', None)
-        if ruleDict is not None:
-            newRuleDict = {}
-            for k, v in ruleDict.items():
-                newRuleDict[k.replace('.', '-')] = v
-            kwargs.update({'defaultDiscountConfig': newRuleDict})
-
-        cardRuleDict = kwargs.pop('defaultCardDiscountConfig', None)
-        if cardRuleDict is not None:
-            newCardRuleDict = {}
-            for k, v in cardRuleDict.items():
-                newCardRuleDict[k.replace('.', '-')] = v
-            kwargs.update({'defaultCardDiscountConfig': newCardRuleDict})
-
-        updated = super(Dealer, self).update(**kwargs)
+        try:
+            ruleDict = kwargs.pop('defaultDiscountConfig', None)
+            if ruleDict is not None:
+                newRuleDict = {}
+                for k, v in ruleDict.items():
+                    newRuleDict[k.replace('.', '-')] = v
+                kwargs.update({'defaultDiscountConfig': newRuleDict})
+
+            cardRuleDict = kwargs.pop('defaultCardDiscountConfig', None)
+            if cardRuleDict is not None:
+                newCardRuleDict = {}
+                for k, v in cardRuleDict.items():
+                    newCardRuleDict[k.replace('.', '-')] = v
+                kwargs.update({'defaultCardDiscountConfig': newCardRuleDict})
+
+            updated = super(Dealer, self).update(**kwargs)
+
+            if not updated:
+                raise UpdateError('failed to update dealer query kwargs(%s)' % (kwargs,))
+            else:
+                return updated
 
-        if not updated:
-            raise UpdateError('failed to update dealer query kwargs(%s)' % (kwargs,))
-        else:
-            return updated
+        finally:
+            self.invalid_cache(str(self.id))
 
     @property
     def permissionList(self):
@@ -656,6 +660,19 @@ class Dealer(CapitalUser):
         from apps.web.dealer.withdraw import DealerWithdrawHandler
         return DealerWithdrawHandler(self, record)
 
+    def check_withdraw_min_fee(self, income_type, pay_type, amount):
+        if pay_type == WITHDRAW_PAY_TYPE.BANK:
+            minimum = RMB(settings.WITHDRAW_MINIMUM)
+        else:
+            if income_type == DEALER_INCOME_TYPE.AD_INCOME:
+                minimum = RMB(settings.AD_WITHDRAW_MINIMUM)
+            else:
+                minimum = RMB(settings.WITHDRAW_MINIMUM)
+
+        if amount < minimum:
+            raise ServiceException(
+                {'result': 0, 'description': u"提现实际到账金额不能少于%s元" % (settings.WITHDRAW_MINIMUM,), 'payload': {}})
+
     def new_withdraw_record(self, withdraw_gateway, pay_entity, source_key, income_type, amount, pay_type, manual, recurrent):
         """
         创建自动提现的单子
@@ -679,9 +696,14 @@ class Dealer(CapitalUser):
                     'payload': {}
                 })
 
-        agentFeeRation = agent.withdrawFeeRatio
-        managerFeeRatio = agent.withdrawFeeRatioCost
-        dealerFeeRatio = self.withdrawFeeRatio
+        if income_type == DEALER_INCOME_TYPE.AD_INCOME:
+            agentFeeRation = Permillage('0')
+            managerFeeRatio = Permillage('0')
+            dealerFeeRatio = Permillage('0')
+        else:
+            agentFeeRation = agent.withdrawFeeRatio
+            managerFeeRatio = agent.withdrawFeeRatioCost
+            dealerFeeRatio = self.withdrawFeeRatio
 
         # 微信或者支付宝平台服务费
         serviceFee = amount * (dealerFeeRatio.as_ratio)
@@ -703,15 +725,7 @@ class Dealer(CapitalUser):
 
         actualPay = amount - serviceFee - bank_trans_fee
 
-        if actualPay < RMB(settings.WITHDRAW_MINIMUM):
-            logger.error('amount is not enough.')
-            raise ServiceException(
-                {
-                    'result': 0,
-                    'description': u"提现实际到账金额不能少于{}元".format(RMB(settings.WITHDRAW_MINIMUM)),
-                    'payload': {}
-                }
-            )
+        self.check_withdraw_min_fee(income_type, pay_type, actualPay)
 
         if dealerFeeRatio > managerFeeRatio:
             earned = amount * ((dealerFeeRatio - managerFeeRatio).as_ratio)
@@ -1201,16 +1215,6 @@ class Dealer(CapitalUser):
         else:
             return self.username
 
-    @property
-    def can_withdraw_today(self):
-        if self.supports('in_withdraw_whitelist'):
-            return True
-
-        if self.devCount >= 5:
-            return True
-
-        return WithdrawRecord.count_today(ownerId = str(self.id), role = self.role) <= 50
-
     @property
     def my_agent(self):
         # type:()->Agent
@@ -1550,8 +1554,9 @@ class DealerRechargeRecord(Searchable):
         payload['status'] = cls.PayState.UnPaid
 
         identifier = identifier if identifier else str(user.id)
-
-        payload['orderNo'] = OrderNoMaker.make_order_no_32(
+        
+        if 'orderNo' not in payload:
+            payload['orderNo'] = OrderNoMaker.make_order_no_32(
                 identifier = identifier, main_type = cls.MY_MAIN_TYPE, sub_type = sub_type)
 
         record = cls(**payload)
@@ -1567,20 +1572,20 @@ class DealerRechargeRecord(Searchable):
         record = cls.objects(orderNo = order_no).first()
         return record
 
-    def succeed(self, wxOrderNo, **kwargs):
-        # 这里和数据库强相关, 通过判断是否正确更新记录
+    def succeed(self, **kwargs):
         payload = {
-            'wxOrderNo': wxOrderNo
+            'status': self.PayState.Paid
         }
 
         if kwargs:
             payload.update(kwargs)
 
-        payload.update({'finishedTime': datetime.datetime.now()})
-        payload.update({'status': self.PayState.Paid})
+        if 'finishedTime' not in payload:
+            payload.update({'finishedTime': datetime.datetime.now()})
 
         result = DealerRechargeRecord.get_collection().update_one(
-            filter = {'_id': ObjectId(self.id), 'status': {'$ne': self.PayState.Paid}},
+            filter = {'_id': ObjectId(self.id),
+                      'status': {'$nin': [self.PayState.Paid, self.PayState.Close, self.PayState.Cancel]}},
             update = {'$set': payload},
             upsert = False
         )
@@ -1588,13 +1593,8 @@ class DealerRechargeRecord(Searchable):
         return result.matched_count == 1
 
     def fail(self, **kwargs):
-        self.status = self.PayState.Failure
-        self.finishedTime = datetime.datetime.now()
-
-        for key, value in kwargs.items():
-            setattr(self, key, value)
-        self.save()
-        return self
+        self.update(status = self.PayState.Failure, finishedTime = datetime.datetime.now(), **kwargs)
+        return self.reload()
 
     def cancel(self):
         result = DealerRechargeRecord.get_collection().update_one(
@@ -1611,23 +1611,40 @@ class DealerRechargeRecord(Searchable):
         self.save()
         return self
 
-    def close(self):
-        self.status = self.PayState.Close
-        self.finishedTime = datetime.datetime.now()
-        return self
+    def close(self, **kwargs):
+        payload = {
+            'status': self.PayState.Close
+        }
+
+        if kwargs:
+            payload.update(kwargs)
 
+        result = self.get_collection().update_one(
+            filter = {'_id': ObjectId(self.id),
+                      'status': {'$nin': [self.PayState.Paid, self.PayState.Close, self.PayState.Cancel]}},
+            update = {'$set': kwargs},
+            upsert = False
+        )
+
+        return result.matched_count == 1
+
+    @property
     def is_success(self):
         return self.status == self.PayState.Paid
 
+    @property
     def is_fail(self):
         return self.status == self.PayState.Failure
 
+    @property
     def is_cancel(self):
         return self.status == self.PayState.Cancel
 
+    @property
     def is_unpay(self):
         return self.status == self.PayState.UnPaid
 
+    @property
     def is_close(self):
         return self.status == self.PayState.Close
 
@@ -1659,40 +1676,7 @@ class DealerRechargeRecord(Searchable):
     def withdraw_source_key(self):
         return self.withdrawSourceKey
 
-
-class RefundDealerRechargeRecord(Searchable):
-    class Status(object):
-        """ 退款单的状态 正常流程顺序是从上至下 """
-        CREATED = 'created'  # 创建
-        PROCESSING = 'processing'  # 申请中
-        FAILURE = 'failure'  # 申请失败   (彻底失败 需要重新发起)
-        SUCCESS = 'success'  # 退款成功   (异步结果)
-        CLOSED = 'closed'  # 退款失败   (异步结果 不需要重新发起)
-
-        # 订单常见的状态
-        # created ---> processing ---> success  (申请 然后接受成功)
-        # created ---> failure  (直接申请失败)
-        # created ---> processing ---> closed    (申请成功 回调失败)
-        # created ---> processing ---> failure   (申请成功  回调失败 不可以重试)
-
-    rechargeObjId = ObjectIdField(verbose_name = u'充值单')
-
-    orderNo = StringField(verbose_name = u'商户退款订单号', default = '')
-    # refundSeq = IntField(verbose_name=u'多次退款,计算退款序列号', default=1)
-
-    errorCode = StringField(verbose_name = u"错误代码", default = "")
-    errorDesc = StringField(verbose_name = u"错误描述", default = "")
-
-    money = MonetaryField(verbose_name = u"退款的金钱数额", default = RMB('0.00'))
-
-    status = StringField(verbose_name = u'订单状态', default = 'created')
-
-    datetimeAdded = DateTimeField(verbose_name = u"退款创建时间", default = datetime.datetime.now)
-    datetimeUpdated = DateTimeField(verbose_name = u"退款更新时间")
-    finishedTime = DateTimeField(verbose_name = u"退款到账时间")
-
-    tradeRefundNo = StringField(verbose_name = u"交易机构退款单号", default = "")
-
+class RefundDealerRechargeRecord(RefundOrderBase):
     meta = {
         'collection': 'refund_dealer_recharge_record',
         'db_alias': 'default'
@@ -1712,120 +1696,43 @@ class RefundDealerRechargeRecord(Searchable):
             # refundSeq=next_seq,
             orderNo = refund_order_no,
             money = refundCash,
-            status = cls.Status.CREATED).save()
-
-    def succeed(self, tradeRefundNo, finishedTime):  # type:(Optional[str], datetime.datetime) -> bool
-        """ 更新为成功状态 已经成功退款 """
-        result = self.__class__.get_collection().update_one(
-            {
-                'orderNo': self.orderNo,
-                'status': self.Status.PROCESSING
-            },
-            {
-                '$set': {
-                    'status': self.Status.SUCCESS,
-                    'datetimeUpdated': datetime.datetime.now(),
-                    'tradeRefundNo': tradeRefundNo,
-                    'finishedTime': finishedTime
-                }
-            }
-        )
-
-        return result.matched_count == 1
-
-    def fail(self, errorCode = "", errorDesc = ""):  # type:(str, unicode) -> bool
-        """
-        更新为失败状态 表示退款申请失败 这种情况下 可能就需要售后进行介入
-        1. 订单申请发起时候即失败
-        2. 订单申请通过 但是回调的时候明确失败 并且是不可重试的失败
-        3. 此状态即表示订单没退款 理论上可以重新发起退款
-        """
-        return self.__class__.objects.filter(
-            orderNo = self.orderNo,
-            status__in = [self.Status.PROCESSING, self.Status.CREATED]
-        ).update(
-            status = self.Status.FAILURE,
-            datetimeUpdated = datetime.datetime.now(),
-            errorCode = errorCode,
-            errorDesc = errorDesc
-        )
-
-    def closed(self, tradeRefundNo, errorCode = "", errorDesc = ""):  # type:(str, str, str) -> bool
-        """
-        退款申请成功了 表示合法的退款申请 但是由于某些原因业务进行不下去
-
-        """
-        result = self.__class__.objects.filter(
-            orderNo = self.orderNo,
-            status = self.Status.PROCESSING
-        ).update(
-            status = self.Status.CLOSED,
-            datetimeUpdated = datetime.datetime.now(),
-            tradeRefundNo = tradeRefundNo,
-            errorCode = errorCode,
-            errorDesc = errorDesc
-        )
-
-        return result
-
-    def processing(self):  # type:() -> bool
-        result = self.__class__.objects.filter(
-            orderNo = self.orderNo,
-            status__in = [self.Status.CLOSED, self.Status.CREATED]
-        ).update(
-            status = self.Status.PROCESSING,
-            datetimeUpdated = datetime.datetime.now()
-        )
-        return result
-
-    @property
-    def is_fail(self):
-        """ 是否订单失败 """
-        return self.status == self.Status.FAILURE
-
-    @property
-    def is_closed(self):
-        """ 退款单是否已经关闭  目前这个状态没有用 适用于用户手动取消退款申请 """
-        return self.status == self.Status.CLOSED
-
-    @property
-    def is_success(self):
-        """ 退款已经成功 """
-        return self.status == self.Status.SUCCESS
-
-    @property
-    def is_apply(self):
-        """ 是否已经发出退款申请 """
-        return self.status in [self.Status.CREATED, self.Status.PROCESSING]
-
-    @property
-    def is_processing(self):
-        return self.status == self.Status.PROCESSING
-
-    @property
-    def is_successful(self):
-        """ 保留旧的方法 """
-        return self.status in [self.Status.SUCCESS, self.Status.PROCESSING]
-
+            status = cls.Status.CREATED,
+            payAppType = order.pay_app_type).save()
+    
+    @property 
+    def pay_app_type(self):
+        return self.payAppType
+    
     @property
-    def pay_order(self):
+    def pay_sub_order(self):
         # type: ()->DealerRechargeRecord
 
-        if not hasattr(self, '__pay_order__'):
+        if not hasattr(self, '__pay_sub_order__'):
             pay_order = DealerRechargeRecord.objects(id = str(self.rechargeObjId)).first()
-            setattr(self, '__pay_order__', pay_order)
+            setattr(self, '__pay_sub_order__', pay_order)
 
-        return getattr(self, '__pay_order__')
+        return getattr(self, '__pay_sub_order__')
 
-    @pay_order.setter
-    def pay_order(self, order):
-        setattr(self, '__pay_order__', order)
+    @pay_sub_order.setter
+    def pay_sub_order(self, order):
+        setattr(self, '__pay_sub_order__', order)
 
     @classmethod
-    def get_record(cls, order_no):
-        return cls.objects(orderNo = order_no).first()
+    def get_record(cls, **kwargs):
+        return cls.objects(**kwargs).first()
 
+    @property
+    def notify_url(self):
+        if self.pay_app_type in [PayAppType.WECHAT, PayAppType.WECHAT_MINI]:
+            return REFUND_NOTIFY_URL.WECHAT_REFUND_BACK
+        elif self.pay_app_type == PayAppType.JD_AGGR:
+            return REFUND_NOTIFY_URL.JD_AGGRE_REFUND_BACK
+        elif self.pay_app_type == PayAppType.JD_OPEN:
+            return REFUND_NOTIFY_URL.JDOPEN_REFUND_BACK
+        else:
+            return None
 
+    
 class OnSale(DynamicDocument):
     """
     优惠活动管理

+ 4 - 7
apps/web/dealer/tasks.py

@@ -1801,7 +1801,6 @@ def dealer_auto_withdraw():
         else:
             logger.error('{} auto withdraw strategy type <{}> error'.format(repr(dealer), strategy['type']))
 
-    # 建单
     for dealer in needExecuteDealers:  # type: Dealer
         try:
             for incomeType in DEALER_INCOME_TYPE.choices():
@@ -1919,7 +1918,7 @@ def dealer_auto_charge_sim_card():
                     if result:
                         logger.info('auto charge sim of {} success.'.format(devObj))
 
-                        rcd.succeed(rcd.orderNo)
+                        rcd.succeed(wxOrderNo = rcd.orderNo)
                         post_sim_recharge(rcd)
 
                         if devObj.groupId not in groupMap:
@@ -1950,9 +1949,6 @@ def dealer_auto_charge_sim_card():
 
                         rcd.cancel()
                         break
-                except InsufficientFundsError:
-                    logger.debug('auto charge sim of {} failure because no enough balance.'.format(devObj))
-                    break
                 except Exception, e:
                     logger.exception(e)
 
@@ -1980,6 +1976,7 @@ def auto_charge_sim_card(dealerId):
 
         result = Dealer.get_collection().update_one(filter, update, upsert = False)
         if result.matched_count == 1 and result.modified_count == 1:
+            logger.debug('dec dealer<id={}> money = {}, fundKey = {}'.format(str(dealer.id), cost, fundKey))
             return True
         else:
             return False
@@ -2035,7 +2032,7 @@ def auto_charge_sim_card(dealerId):
 
                     result = consume_dealer_balance(pay_gateway, income_type, dealer, costMoney)
                     if result:
-                        rcd.succeed(rcd.orderNo)
+                        rcd.succeed(wxOrderNo = rcd.orderNo)
                         post_sim_recharge(rcd)
                         logger.info('charge sim card success,devNo=%s' % devObj.devNo)
 
@@ -2052,7 +2049,7 @@ def auto_charge_sim_card(dealerId):
                                 {
                                     "money": (-costMoney).mongo_amount,
                                     "role": "owner",
-                                    "share": Percent("100.0"),
+                                    "share": Percent("100.0").mongo_amount,
                                     "id": str(dealer.id)
                                 }
                             ],

+ 12 - 14
apps/web/dealer/transaction.py

@@ -10,7 +10,7 @@ from typing import Optional
 from apilib.monetary import RMB
 from apps.web.agent.proxy import record_agent_traffic_card_earning, record_agent_api_quota_earning, \
     record_agent_disable_ad_earning
-from apps.web.dealer.models import DealerRechargeRecord, Dealer, RefundDealerRechargeRecord
+from apps.web.dealer.models import DealerRechargeRecord, Dealer
 from apps.web.device.models import Device
 
 logger = logging.getLogger(__name__)
@@ -19,6 +19,12 @@ logger = logging.getLogger(__name__)
 def post_pay(record, verifyAccount = False):
     # type: (DealerRechargeRecord, bool)->None
 
+    record.reload()
+
+    if record.is_close:
+        logger.warning('{} check close.'.format(repr(record)))
+        return
+
     if record.product == DealerRechargeRecord.ProductType.SimCard:
         post_sim_recharge(record, verifyAccount)
     elif record.product == DealerRechargeRecord.ProductType.ApiCost:
@@ -49,16 +55,17 @@ def post_sim_recharge(record, verifyAccount = False):
         logger.exception('save recharge record failure. error = %s; record = %s' % (e, repr(record)))
 
     dev_no_list = [item['devNo'] for item in record.items]
-
+  
     try:
         Device.get_collection().update_many({'devNo': {'$in': dev_no_list}},
                                             {'$set': {'simStatus': 'chargedUnupdated'}}, upsert = False)
     except Exception as e:
         logger.exception(e)
-
+    
+    
     finally:
         Device.invalid_many_device_cache(dev_no_list)
-
+     
 
 def post_sim_verify_order(dealer, verify_order, verifyAccount = False):
     for item in verify_order.settleInfo['partition']:
@@ -72,6 +79,7 @@ def post_sim_verify_order(dealer, verify_order, verifyAccount = False):
                                               'agent_earning': RMB.fen_to_yuan(item['earned'])
                                           })
 
+
 def post_ApiCost_recharge(record, verifyAccount = False):
     # type: (DealerRechargeRecord, bool)->None
 
@@ -138,13 +146,3 @@ def post_disableAd_recharge(record, verifyAccount = False):
         logger.exception(e)
 
 
-def refund_post_pay(refundOrder, finishedTime):
-    pass
-    # refund_recharge_order = refundOrder.refund_order  # type: RefundDealerRechargeRecord
-
-    # 退款实际结束, 更新状态和时间
-    # refund_recharge_order.finishedTime = refundOrder.finishedTime
-    # refund_recharge_order.result = RefundDealerRechargeRecord.Status.SUCCESS
-    # refund_recharge_order.save()
-
-    # TODO: 代理商收益调整

+ 3 - 127
apps/web/dealer/transaction_deprecated.py

@@ -3,139 +3,15 @@
 
 import logging
 
-from django.conf import settings
 from typing import TYPE_CHECKING
 
-from apilib.monetary import RMB
-from apilib.utils_sys import memcache_lock
-from apps.web.core import PayAppType
-from apps.web.core.exceptions import ServiceException
-from apps.web.core.payment import PaymentGateway
-from apps.web.dealer.define import REFUND_NOTIFY_URL
-from apps.web.dealer.models import RefundDealerRechargeRecord, DealerRechargeRecord
-from apps.web.exceptions import UserServerException
-from library.wechatbase.exceptions import WeChatPayException
-
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
     pass
 
 
-def refund_cash_to_dealer(dealer_recharge_record, refundFee):
-    if dealer_recharge_record.product == DealerRechargeRecord.ProductType.SimCard:
-        return RefundSimRecharge(dealer_recharge_record, refundFee).execute()
-    else:
-        raise UserServerException(u'目前不支持该种类型订单退款')
-
-
-class RefundCash(object):
-    # 最长的查询分账时间
-    MAX_LEDGER_CHECK_TIME = 15
-
-    def __init__(self, rechargeOrder, refundFee):  # type:(DealerRechargeRecord, RMB) -> None
-        self._payOrder = rechargeOrder
-        self.refundFee = refundFee
-
-    @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 execute(self):
-        """
-        执行退款的动作
-        :return:
-        """
-
-        lockKey = "refund_dealer_recharge_cash_{}".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 paras, 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)))
-
-            payGateway = PaymentGateway.clone_from_order(self.payOrder)  # type: PaymentGateway
-
-            refundOrder.processing()
-
-            try:
-                if payGateway.pay_app_type == PayAppType.WECHAT:
-                    # 微信的退款方式
-                    try:
-                        result = payGateway.refund_to_user(
-                            out_trade_no = self.outTradeNo,
-                            out_refund_no = refundOrder.orderNo,
-                            refund_fee = self.refundFee,
-                            total_fee = self.totalFee,
-                            refund_reason = u'退费',
-                            notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK)
-                    except WeChatPayException as e:
-                        logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e))
-                        refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
-                        raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
-
-                    logger.info('WECHAT Refund request successfully! return = {}'.format(result))
-
-                else:
-                    refundOrder.fail(errorDesc = u"不支持的退款模式")
-                    raise UserServerException(u"不支持的退款模式")
-
-            except ServiceException as se:
-                raise se
-            except Exception as ee:
-                # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新
-                logger.exception(ee)
-                raise UserServerException(u'未知异常')
-
-            return refundOrder
-
-
-class RefundSimRecharge(RefundCash):
-    def pre_check(self):
-        super(RefundSimRecharge, self).pre_check()
+def refund_post_pay(refundOrder, success):
+    # TODO: 代理商收益调整
+    pass
 
-        for partition in self.payOrder.settleInfo['partition']:
-            if partition['id'] != settings.MY_PRIMARY_AGENT_ID and partition['earned'] > 0:
-                raise UserServerException(u'目前仅支持不分账情况下退账。')

+ 50 - 43
apps/web/dealer/views.py

@@ -53,7 +53,8 @@ from apps.web.common.models import District, DEFAULT_DEALER_FEATURES, WithdrawRe
 from apps.web.common.proxy import ClientRechargeModelProxy, ClientConsumeModelProxy, DealerDailyStatsModelProxy, \
     GroupDailyStatsModelProxy, DeviceDailyStatsModelProxy, ClientDealerIncomeModelProxy, DealerReportModelProxy
 from apps.web.common.transaction import WithdrawStatus, WITHDRAW_PAY_TYPE, translate_withdraw_state
-from apps.web.common.transaction.pay import OrderCacheMgr, PayManager, RefundManager
+from apps.web.common.transaction.pay import OrderCacheMgr, PayManager
+from apps.web.common.transaction.refund import RefundManager
 from apps.web.common.validation import NAME_RE, PHONE_NUMBER_RE
 from apps.web.constant import (
     Const,
@@ -99,7 +100,8 @@ from apps.web.dealer.models import (
     SubAccount, DealerMessage, Complaint, AdjustUserVirtualCardRecord, ExchangeOrder, DealerAddr, UpCardScoreRecord,
     PermissionRole, PermissionRule, TodoMessage, ApiAppInfo, RefundDealerRechargeRecord)
 from apps.web.dealer.proxy import DealerIncomeProxy
-from apps.web.dealer.transaction import post_pay, refund_post_pay
+from apps.web.dealer.transaction import post_pay
+from apps.web.dealer.transaction_deprecated import refund_post_pay
 from apps.web.dealer.utils import gen_login_response, gen_home_response, gen_subaccount_login_response, \
     VirtualCardBuilder, create_dealer_sim_charge_order, DealerSessionBuilder, MyToken, \
     create_dealer_charge_order_for_api, create_dealer_charge_order_for_disable_ad
@@ -4586,7 +4588,7 @@ def cancelSimRechargeOrder(request):
         if not order:
             return JsonErrorResponse(u'订单不存在,请刷新后再试')
 
-        if not order.is_unpay():
+        if not order.is_unpay:
             return JsonErrorResponse(u'订单已经处理,无法取消')
 
         if order.cancel():
@@ -8670,58 +8672,71 @@ def checkAlarm(request):
 
 
 @permission_required(ROLE.dealer)
-@error_tolerate(logger=logger, nil=JsonErrorResponse(u'系统错误,请稍后再试'))
+@error_tolerate(logger = logger, nil = JsonErrorResponse(u'系统错误,请稍后再试'))
 def withdrawEntry(request):
     # type: (WSGIRequest)->JsonResponse
 
     user = request.user  # type: Dealer
 
     if not check_role(user, ROLE.dealer):
-        return ErrorResponseRedirect(error=u'权限错误')
+        return ErrorResponseRedirect(error = u'权限错误')
+
+    # 检查是否有非法订单,如果有,不允许提现直接返回错误
+    if RechargeRecord.have_illegal_order(str(user.id), user.maxPackagePrice):
+        return ErrorResponseRedirect(error = u'系统检测到部分存疑订单,暂时不能提现。请联系平台客服确认')
 
     source_key = request.GET.get('sourceId')
     if not WithdrawGateway.is_ledger(source_key):
-        return ErrorResponseRedirect(error=u'系统配置错误,请联系平台客服(10003)')
+        return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10003)')
 
     source_type = request.GET.get('sourceType')
     assert source_type in DEALER_INCOME_TYPE.choices(), 'invalid dealer income type'
 
     if source_key not in user.balance_dict(source_type):
-        return ErrorResponseRedirect(error=u'提现参数错误,请刷新后重试')
+        return ErrorResponseRedirect(error = u'提现参数错误,请刷新后重试')
 
-    is_ledger, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
+    is_ledger, agent, withdraw_gateway_list = Agent.withdraw_gateway_list(source_key)
     if not is_ledger:
         return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10005)')
 
-    wechat_withdraw_gateway = withdraw_gateway_list['wechat']
-    if not wechat_withdraw_gateway:
-        return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10006)')
+    wechat_withdraw_gateway = withdraw_gateway_list['wechat']  # type: WithdrawGateway
+    if wechat_withdraw_gateway.support_withdraw and not wechat_withdraw_gateway.manual_withdraw:
+        code = request.GET.get('code', None)
+        if not code:
+            redirect = request.GET.get('redirect')
+            return ExternalResponseRedirect(
+                WechatAuthBridge(wechat_withdraw_gateway.app).generate_auth_url_base_scope(
+                    concat_server_end_url(
+                        uri = '/dealer/withdraw/entry?sourceType={source_type}&sourceId={source_key}'.format(
+                            source_type = source_type,
+                            source_key = source_key
+                        )), payload = base64.b64encode(redirect)))
+        else:
+            auth_bridge = WechatAuthBridge(wechat_withdraw_gateway.app)
+            openId = auth_bridge.authorize(code)
+            if openId is not None:
+                redirect = base64.b64decode(request.GET.get('payload'))
+
+                redirect = add_query(redirect, {
+                    'sourceType': source_type,
+                    'sourceId': source_key,
+                    'openId': openId
+                })
 
-    code = request.GET.get('code', None)
-    if not code:
+                return FrontEndResponseRedirect(redirect)
+            else:
+                return ErrorResponseRedirect(error = u'微信授权失败,请刷新后重试')
+    else:
         redirect = request.GET.get('redirect')
-        return ExternalResponseRedirect(
-            WechatAuthBridge(wechat_withdraw_gateway.app).generate_auth_url_base_scope(
-                concat_server_end_url(
-                    uri='/dealer/withdraw/entry?sourceType={source_type}&sourceId={source_key}'.format(
-                        source_type=source_type,
-                        source_key=source_key
-                    )), payload=base64.b64encode(redirect)))
-    else:
-        auth_bridge = WechatAuthBridge(wechat_withdraw_gateway.app)
-        openId = auth_bridge.authorize(code)
-        if openId is not None:
-            redirect = base64.b64decode(request.GET.get('payload'))
 
-            redirect = add_query(redirect, {
-                'sourceType': source_type,
-                'sourceId': source_key,
-                'openId': openId
-            })
+        redirect = add_query(redirect, {
+            'sourceType': source_type,
+            'sourceId': source_key,
+            'openId': ''
+        })
+
+        return FrontEndResponseRedirect(redirect)
 
-            return FrontEndResponseRedirect(redirect)
-        else:
-            return ErrorResponseRedirect(error=u'微信授权失败,请刷新后重试')
 
 @permission_required(ROLE.dealer)
 def ActivateUser(request):
@@ -10532,14 +10547,6 @@ def payNotify(request, pay_app_type):
     :param request:
     :return: HttpResponse
     """
-
-    if pay_app_type == PayAppType.ALIPAY:
-        payload = request.POST.dict()
-        if 'refund_fee' in payload and 'gmt_refund' in payload:
-            notifier_cls = RefundManager().get_notifier(pay_app_type)
-            return notifier_cls(request, lambda order_no: RefundDealerRechargeRecord.get_record(order_no)).do(
-                refund_post_pay)
-
     recharge_cls_factory = lambda order_no: DealerRechargeRecord
     notifier_cls = PayManager().get_notifier(pay_app_type = pay_app_type)
     response = notifier_cls(request, recharge_cls_factory).do(post_pay)
@@ -13416,7 +13423,7 @@ def refundOrder(request):
     currency = rechargeOrder.myuser.calc_currency_balance(rechargeOrder.owner, rechargeOrder.group)
     if currency < deductCoins:
         return JsonErrorResponse(description = u"扣除金币数大于用户目前余额")
-    if not rechargeOrder.is_success():
+    if not rechargeOrder.is_success:
         return JsonErrorResponse(description = u"退款失败,订单状态非成功,请联系平台客服")
 
     if not rechargeOrder.is_ledgered:
@@ -14859,6 +14866,6 @@ def refundOrderNotifier(request, pay_app_type):
     assert pay_app_type in PayAppType.choices(), 'not support this pay app type({})'.format(pay_app_type)
 
     notifier_cls = RefundManager().get_notifier(pay_app_type)
-    response = notifier_cls(request, lambda order_no: RefundDealerRechargeRecord.get_record(order_no)).do(
+    response = notifier_cls(request, lambda filter: RefundDealerRechargeRecord.get_record(**filter)).do(
         refund_post_pay)
     return response

+ 1 - 1
apps/web/dealer/withdraw.py

@@ -39,7 +39,7 @@ class DealerWithdrawHandler(WithdrawHandler):
                     'withdrawFeeRatio': self.record.withdrawFeeRatio
                 }
 
-                record_agent_withdraw_fee(agentId = agent_id, source_key = self.record.source_key,
+                record_agent_withdraw_fee(agentId = agent_id, source_key = self.record.withdrawSourceKey,
                                           amount = earned, detail = detail)
         except Exception as e:
             logging.exception(e)

+ 79 - 60
apps/web/device/timescale.py

@@ -4,6 +4,7 @@
 import datetime
 import logging
 import socket
+from decimal import Decimal
 
 import arrow
 import simplejson as json
@@ -12,6 +13,7 @@ from influxdb import InfluxDBClient
 from influxdb.resultset import ResultSet
 from django.core.cache import cache
 
+from apilib.numerics import quantize
 from apilib.utils_datetime import local2utc
 from apps.web.core.sysparas import SysParas
 
@@ -20,9 +22,16 @@ logger = logging.getLogger(__name__)
 
 class FluentedEngine(object):
     def in_signal_udp(self, devNo, ts, signal, cmd):
-        logger.debug(
-            '[in_signal_udp] devNo = {}, ts = {}, signal = {}, cmd = {}, ip = {}'.format(
-                devNo, ts, signal, cmd, SysParas.get_private_ip(settings.FLUENTED_IP)))
+        import time
+        if ts < (int(time.time()) - 3 * 24 * 3600):
+            logger.warn(
+                '[in_signal_udp] ignore of ts. devNo = {}, ts = {}, signal = {}, cmd = {}, ip = {}'.format(
+                    devNo, ts, signal, cmd, SysParas.get_private_ip(settings.FLUENTED_IP)))
+            return
+        else:
+            logger.debug(
+                '[in_signal_udp] devNo = {}, ts = {}, signal = {}, cmd = {}, ip = {}'.format(
+                    devNo, ts, signal, cmd, SysParas.get_private_ip(settings.FLUENTED_IP)))
 
         ip_port = (SysParas.get_private_ip(settings.FLUENTED_IP), settings.FLUENTED_SIGNAL_PORT)
 
@@ -35,10 +44,18 @@ class FluentedEngine(object):
         client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         client.sendto(json.dumps(point), ip_port)
 
-    def in_power_udp(self, devNo, port, ts, power, voltage, current):
-        logger.debug(
-            '[in_power_udp] devNo = {}, port = {}, ts = {}, power = {}, voltage = {}, current = {}, ip = {}'.format(
-                devNo, port, ts, power, voltage, current, SysParas.get_private_ip(settings.FLUENTED_IP)))
+    def in_power_udp(self, devNo, port, ts, power, voltage, current, **kwargs):
+        import time
+
+        if ts < (int(time.time()) - 3 * 24 * 3600):
+            logger.warn(
+                '[in_power_udp] ignore of ts. devNo = {}, port = {}, ts = {}, power = {}, voltage = {}, current = {}, ip = {}, kwargs = {}'.format(
+                    devNo, port, ts, power, voltage, current, SysParas.get_private_ip(settings.FLUENTED_IP), kwargs))
+            return
+        else:
+            logger.debug(
+                '[in_power_udp] devNo = {}, port = {}, ts = {}, power = {}, voltage = {}, current = {}, ip = {}, kwargs = {}'.format(
+                    devNo, port, ts, power, voltage, current, SysParas.get_private_ip(settings.FLUENTED_IP), kwargs))
 
         ip_port = (SysParas.get_private_ip(settings.FLUENTED_IP), settings.FLUENTED_POWER_PORT)
 
@@ -55,13 +72,24 @@ class FluentedEngine(object):
         if current is not None:
             point.update({'current2': float(current)})
 
+        if kwargs:
+            point.update(kwargs)
+
         client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         client.sendto(json.dumps(point), ip_port)
 
     def in_put_coins_udp(self, devNo, ts, coins, mode, port = None):
-        logger.debug(
-            '[in_put_coins_udp] devNo = {}, ts = {}, coins = {}, mode = {}, port = {}, ip = {}'.format(
-                devNo, ts, coins, mode, port, SysParas.get_private_ip(settings.FLUENTED_IP)))
+        import time
+
+        if ts < (int(time.time()) - 3 * 24 * 3600):
+            logger.warn(
+                '[in_put_coins_udp] ignore of ts. devNo = {}, ts = {}, coins = {}, mode = {}, port = {}, ip = {}'.format(
+                    devNo, ts, coins, mode, port, SysParas.get_private_ip(settings.FLUENTED_IP)))
+            return
+        else:
+            logger.debug(
+                '[in_put_coins_udp] devNo = {}, ts = {}, coins = {}, mode = {}, port = {}, ip = {}'.format(
+                    devNo, ts, coins, mode, port, SysParas.get_private_ip(settings.FLUENTED_IP)))
 
         ip_port = (SysParas.get_private_ip(settings.FLUENTED_IP), settings.FLUENTED_OFFLINE_PORT)
         point = {
@@ -176,41 +204,46 @@ class PowerManager(object):
     def __init__(self, enginer):
         self.enginer = enginer
 
-    def get(self, devNo, port, sTime, eTime, interval = 300):
+    def get(self, devNo, port, sTime, eTime, interval = 300,
+            fields = [{'field': 'power2', 'scale': '1.0', 'precision': '0.01'},
+                      {'field': 'voltage2', 'scale': '1.0', 'precision': '0.01'},
+                      {'field': 'current2', 'scale': '1.0', 'precision': '0.01'}]):
         def format_date(dateStr):
             return arrow.get(str(dateStr)).to(settings.TIME_ZONE).naive
 
-        def format_point(point):
+        def format_point(point, fields):
             rv = {
                 'time': format_date(point['time'])
             }
 
-            if 'power2' in point and point['power2'] is not None:
-                rv['power'] = round(point['power2'], 2)
-            else:
-                rv['power'] = round(point['power'], 2)
-
-            if 'voltage2' in point and point['voltage2'] is not None and point['voltage2'] != '-':
-                rv['voltage'] = round(point['voltage2'], 2)
-            elif 'voltage' in point and point['voltage'] is not None and point['voltage'] != '-':
-                rv['voltage'] = round(point['voltage'], 2)
-            else:
-                rv['voltage'] = '-'
-
-            if 'current2' in point and point['current2'] is not None and point['current2'] != '-':
-                rv['current'] = round(point['current2'], 2)
-            elif 'current' in point and point['current'] is not None and point['current'] != '-':
-                rv['current'] = round(point['current'], 2)
-            else:
-                rv['current'] = '-'
+            for item in fields:
+                field = item['field']
+                scale = Decimal(item['scale'])
+                precision = item['precision']
+
+                if field in point and point[field] is not None:
+                    rv[field] = quantize(Decimal(str(point[field])) * scale, places = precision)
+                else:
+                    rv[field] = '-'
+
+            return rv
 
+        def format_null_point(_time, fields):
+            rv = {
+                'time': _time
+            }
+            for item in fields:
+                field = item['field']
+                rv[field] = '-'
             return rv
 
         query_start_time = sTime - datetime.timedelta(seconds = interval)
         query_end_time = eTime + datetime.timedelta(seconds = interval)
 
-        selectStr = "select time, power, power2, voltage, voltage2, current, current2 from %s where devno = '%s' and port = '%s' and time >= '%s' and time <= '%s' order by time" % (
-            self.enginer.full_qualify_meaurement, devNo, port, local2utc(query_start_time), local2utc(query_end_time))
+        selectStr = "select time, {} from {} where devno = '{}' and port = '{}' and time >= '{}' and time <= '{}' order by time".format(
+            ','.join([item['field'] for item in fields]), self.enginer.full_qualify_meaurement, devNo, port,
+            local2utc(query_start_time),
+            local2utc(query_end_time))
 
         points = self.enginer.read_data(selectStr)
 
@@ -237,12 +270,8 @@ class PowerManager(object):
 
             while True:
                 next_time = (next_time - datetime.timedelta(seconds = interval))
-                left_rv.append({
-                    'time': next_time,
-                    'power': '-',
-                    'voltage': '-',
-                    'current': '-'
-                })
+
+                left_rv.append(format_null_point(next_time, fields))
 
                 if next_time < sTime:
                     break
@@ -254,7 +283,7 @@ class PowerManager(object):
         middle_section = None
         if max_time > min_time:
             # now_point =
-            now = format_point(points[0])
+            now = format_point(points[0], fields)
             # now = {
             #     'time': format_date(now_point['time']),
             #     'power': now_point['power'],
@@ -267,7 +296,7 @@ class PowerManager(object):
 
             # right_point = points[idx]
 
-            right = format_point(points[idx])
+            right = format_point(points[idx], fields)
             #     {
             #     'time': format_date(right_point['time']),
             #     'power': right_point['power'],
@@ -288,7 +317,7 @@ class PowerManager(object):
                     if idx >= total:
                         break
                     else:
-                        right = format_point(points[idx])
+                        right = format_point(points[idx], fields)
                         # {
                         #     'time': format_date(points[idx]['time']),
                         #     'power': points[idx]['power'],
@@ -302,13 +331,7 @@ class PowerManager(object):
                         # print('before: now = {}; right = {}'.format(now, right))
 
                         last_now = now
-
-                        now = {
-                            'time': (last_now['time'] + datetime.timedelta(seconds = interval)),
-                            'power': '-',
-                            'voltage': '-',
-                            'current': '-'
-                        }
+                        now = format_null_point((last_now['time'] + datetime.timedelta(seconds = interval)), fields)
                         rv.append(now)
 
                         # print('after: now = {}; right = {}'.format(now, right))
@@ -317,9 +340,9 @@ class PowerManager(object):
 
                         rv.append({
                             'time': (now['time'] + datetime.timedelta(seconds = diff / 2)),
-                            'power': now['power'],
-                            'voltage': now['voltage'],
-                            'current': now['current']
+                            'power2': now['power2'],
+                            'voltage2': now['voltage2'],
+                            'current2': now['current2']
                         })
                         rv.append(right)
 
@@ -329,7 +352,7 @@ class PowerManager(object):
                         if idx >= total:
                             break
                         else:
-                            right = format_point(points[idx])
+                            right = format_point(points[idx], fields)
 
                         # print('after: now = {}; right = {}'.format(now, right))
 
@@ -340,12 +363,8 @@ class PowerManager(object):
 
             while True:
                 next_time = (next_time + datetime.timedelta(seconds = interval))
-                right_rv.append({
-                    'time': next_time,
-                    'power': '-',
-                    'voltage': '-',
-                    'current': '-'
-                })
+
+                right_rv.append(format_null_point(next_time, fields))
 
                 if next_time >= eTime:
                     break
@@ -359,7 +378,7 @@ class PowerManager(object):
         def format_date(dateStr):
             return arrow.get(str(dateStr)).to(settings.TIME_ZONE).naive
 
-        selectStr = "select time, power, power2, voltage, voltage2, current, current2 from %s where devno = '%s' and port = '%s' and time >= '%s' and time <= '%s' order by time" % (
+        selectStr = "select time, power2, voltage2, current2 from %s where devno = '%s' and port = '%s' and time >= '%s' and time <= '%s' order by time" % (
             self.enginer.full_qualify_meaurement, devNo, port, local2utc(sTime), local2utc(eTime))
 
         points = self.enginer.read_data(selectStr)
@@ -480,7 +499,7 @@ class OfflineManager(object):
         else:
             offlineNotifyTime = 1
 
-        if not offlineNotifyTime:
+        if not offlineNotifyTime or offlineNotifyTime <= 0:
             return
 
         from taskmanager.mediator import task_caller

+ 2 - 6
apps/web/eventer/DcFastCharge.py

@@ -212,9 +212,7 @@ class MyComNetPayAckEvent(ComNetPayAckEvent):
             for sub_order in sub_orders[::-1]:
                 need_back_coins, need_consume_coins, backCoins = self._calc_refund_info(backCoins, sub_order.coin)
 
-                user.clear_frozen_balance(str(sub_order.id), sub_order.paymentInfo['deduct'],
-                                          back_coins=need_back_coins,
-                                          consume_coins=need_consume_coins)
+                user.clear_frozen_balance(str(sub_order.id), sub_order.paymentInfo['deduct'], need_back_coins)
 
                 sub_order.update_service_info({
                     DEALER_CONSUMPTION_AGG_KIND.COIN: sub_order.coin.mongo_amount,
@@ -223,9 +221,7 @@ class MyComNetPayAckEvent(ComNetPayAckEvent):
 
             need_back_coins, need_consume_coins, backCoins = self._calc_refund_info(backCoins, master_order.coin)
 
-            user.clear_frozen_balance(transaction_id=str(master_order.id),
-                                      deduct_list=master_order.paymentInfo['deduct'], back_coins=need_back_coins,
-                                      consume_coins=need_consume_coins)
+            user.clear_frozen_balance(str(master_order.id), master_order.paymentInfo['deduct'], need_back_coins)
 
             consumeDict.update({
                 DEALER_CONSUMPTION_AGG_KIND.COIN: master_order.coin.mongo_amount,

+ 1 - 1
apps/web/eventer/duibiji.py

@@ -94,7 +94,7 @@ class DuibijiWorkEvent(WorkEvent):
                 return
 
             # 先分账,然后在处理退款
-            if not record.is_success():
+            if not record.is_success:
                 logger.debug('{} state is {}'.format(str(record), record.result))
                 return
 

+ 4 - 8
apps/web/eventer/gaoborui.py

@@ -1482,11 +1482,9 @@ class MyComNetPayAckEvent(ComNetPayAckEvent):
             extra.append({u'虚拟卡号': vCard.cardNo})
 
         elif master_order.paymentInfo['via'] in ['netPay', 'coins', 'cash', 'coin']:
-            user.clear_frozen_balance(str(master_order.id), master_order.paymentInfo['deduct'], back_coins,
-                                      (coins - back_coins))
+            user.clear_frozen_balance(str(master_order.id), master_order.paymentInfo['deduct'], back_coins)
             for sub_order in sub_orders:
-                user.clear_frozen_balance(str(sub_order.id), sub_order.paymentInfo['deduct'], VirtualCoin(0),
-                                          VirtualCoin(0))
+                user.clear_frozen_balance(str(sub_order.id), sub_order.paymentInfo['deduct'], VirtualCoin(0))
 
             if back_coins > VirtualCoin(0):
                 consumeDict['refundedMoney'] = str(back_coins)
@@ -1612,11 +1610,9 @@ class MyComNetPayAckEvent(ComNetPayAckEvent):
                         logger.exception(e)
 
             elif master_order.paymentInfo['via'] in ['netPay', 'coins', 'cash', 'coin']:
-                user.clear_frozen_balance(str(master_order.id), master_order.paymentInfo['deduct'], back_coins,
-                                          (coins - back_coins))
+                user.clear_frozen_balance(str(master_order.id), master_order.paymentInfo['deduct'], back_coins)
                 for sub_order in sub_orders:
-                    user.clear_frozen_balance(str(sub_order.id), sub_order.paymentInfo['deduct'], VirtualCoin(0),
-                                              VirtualCoin(0))
+                    user.clear_frozen_balance(str(sub_order.id), sub_order.paymentInfo['deduct'], VirtualCoin(0))
 
                 if back_coins > VirtualCoin(0):
                     consumeDict.update({"refundedMoney": str(back_coins)})

+ 5 - 1
apps/web/exceptions.py

@@ -55,4 +55,8 @@ class DuplicatedOperationError(Exception):
 
 
 class WithdrawOrderNotExist(UserServerException):
-    pass
+    pass
+
+
+class LimitRateException(UserServerException):
+    pass

+ 64 - 7
apps/web/helpers.py

@@ -1,13 +1,16 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
-
+import contextlib
+import inspect
 import logging
+import uuid
+from copy import deepcopy
 from functools import wraps, partial
 
 import simplejson as json
 from django.http import JsonResponse
 from django.utils.module_loading import import_string
-
+from mongoengine.base import BaseField
 from typing import Union, Tuple, TYPE_CHECKING, Optional, Mapping
 
 from apps import serviceCache
@@ -15,11 +18,11 @@ from apps.web.constant import APP_TYPE, AppPlatformType
 from apps.web.core import ROLE
 from apps.web.core.auth.ali import AlipayAuthBridge
 from apps.web.core.auth.wechat import WechatAuthBridge
-from apps.web.core.bridge import WechatClientProxy
+from apps.web.core.bridge.wechat import WechatClientProxy
 from apps.web.core.datastructures import BaseVisitor
-
+from apps.web.core.models import SystemSettings, ManualPayApp
+from apps.web.models import MongoTempTable
 from apps.web.utils import is_user, is_dealer, is_agent, is_anonymous
-from apps.web.core.models import WechatPayApp, AliApp, SystemSettings, ManualPayApp
 
 logger = logging.getLogger(__name__)
 
@@ -38,10 +41,13 @@ if TYPE_CHECKING:
     from apps.web.management.models import Manager
     SourceT = Union[Agent, Dealer, Device, Manager, DeviceDict, WithdrawRecord]
 
-    from apps.web.core.payment import WechatMiniPaymentGatewayT
+    from apps.web.core.models import WechatPayApp, AliApp
 
     WechatAPP_T = Union[WechatPayApp, WechatManagerApp, WechatAuthApp]
 
+    PayApp_T = Union[WechatPayApp, AliApp]
+    AuthApp_T = Union[WechatManagerApp, WechatAuthApp]
+
 
 class DeviceTypeVisitor(BaseVisitor):
     def entry(self, node):
@@ -564,4 +570,55 @@ def remove_some_desc_for_consume(oldDesc, needRemove):
     except Exception, e:
         return oldDesc
     vList[sIndex], vList[sIndex + 1], vList[sIndex + 2] = '', '', ''
-    return ' '.join(vList)
+    return ' '.join(vList)
+
+
+def copy_temp_document_classes(clazz, new_model_name):
+    exclude = ['_fields', '_db_field_map', '_reverse_db_field_map', '_fields_ordered', '_is_document',
+               'MultipleObjectsReturned', '_superclasses', '_subclasses', '_types', '_class_name',
+               '_meta', '__doc__', '__module__', '_collection', '_is_base_cls', '_auto_id_field', 'id',
+               'DoesNotExist', 'objects', '_cached_reference_fields']
+
+    dicts = {'meta': deepcopy(clazz._origin_meta)}
+
+    dicts['meta'].pop('indexes', None)
+    dicts['meta']['index_background'] = True
+    dicts['meta']['auto_create_index'] = False
+
+    dicts['__module__'] = clazz.__module__
+
+    new_cls = type(new_model_name, clazz.__bases__, dicts)
+
+    for name, field in clazz.__dict__.iteritems():
+        if name in exclude:
+            continue
+        else:
+            field = getattr(clazz, name)
+            if isinstance(field, BaseField):
+                setattr(new_cls, name, field)
+            else:
+                if inspect.ismethod(field):
+                    if not field.im_self:
+                        setattr(new_cls, name, field.im_func)
+                    else:
+                        setattr(new_cls, name, classmethod(field.im_func))
+                elif inspect.isfunction(field):
+                    setattr(new_cls, name, staticmethod(field))
+                else:
+                    setattr(new_cls, name, field)
+
+    return new_cls
+
+
+@contextlib.contextmanager
+def mongo_temp_table():
+    tempModel = copy_temp_document_classes(
+        MongoTempTable,
+        '{}_{}'.format(MongoTempTable.__name__, str(uuid.uuid1())))
+
+    yield tempModel
+
+    try:
+        tempModel.get_collection().drop()
+    except Exception:
+        pass

+ 60 - 58
apps/web/management/tasks.py

@@ -10,7 +10,7 @@ from bson import ObjectId
 from celery.utils.log import get_task_logger
 from django.conf import settings
 from django.core.cache import cache
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from apilib.utils_datetime import timestamp_to_dt, to_datetime
 from apps.web.agent.models import Agent
@@ -21,7 +21,7 @@ from apps.web.common.transaction import WithdrawStatus
 from apps.web.constant import Const, MONTH_DATE_KEY, MANAGER_EXPORT_EXCEL_TYPE, DEALER_CONSUMPTION_AGG_KIND_TRANSLATION
 from apps.web.core import ROLE
 from apps.web.core.payment import WithdrawGateway
-from apps.web.core.payment.ali import AliPayWithdrawGateway
+from apps.web.core.payment.ali import AliPayWithdrawGateway, AlipayWithdrawQueryResult
 from apps.web.core.payment.wechat import WechatWithdrawQueryResult, WechatWithdrawGateway
 from apps.web.core.sysparas import SysParas
 from apps.web.core.utils import generate_excel_report
@@ -29,6 +29,7 @@ from apps.web.dealer.models import DealerRechargeRecord, Dealer
 from apps.web.dealer.proxy import DealerIncomeProxy
 from apps.web.dealer.withdraw import DealerWithdrawRetryService
 from apps.web.device.models import Device, Group
+from apps.web.exceptions import WithdrawOrderNotExist
 from apps.web.helpers import get_inhouse_wechat_manager_mp_proxy
 from apps.web.management.models import Manager
 from apps.web.management.utils import get_dealerMap_by_managerId, query_device_income, query_device_consumption, \
@@ -36,7 +37,6 @@ from apps.web.management.utils import get_dealerMap_by_managerId, query_device_i
     get_device_being_used_trend
 from apps.web.report.models import DealerMonthlyStat
 from apps.web.user.models import RechargeRecord, CardRechargeOrder, MyUser, ConsumeRecord
-from library.alipay import AliPayServiceException
 
 if TYPE_CHECKING:
     from apps.web.common.models import CapitalUser
@@ -346,9 +346,9 @@ def generate_biz_stats_for_manager(filepath, queryAttrs):
     generate_excel_report(filepath, records)
 
 
-def check_wechat_withdraw_via_bank():
+def check_withdraw_via_bank():
     """
-    由于不能实时获取是否微信成功给银行卡转账了,需要每日进行查询并更新状态
+    由于不能实时获取是否微信或者支付宝成功给银行卡转账了,需要每日进行查询并更新状态
     主要处理银行退单的情况,则需要将款项返还给经销商/代理商
     :return:
     """
@@ -356,65 +356,63 @@ def check_wechat_withdraw_via_bank():
     def process_withdraw(record):
         # type: (WithdrawRecord)->None
 
-        check_days = 2
-
         try:
             payee = ROLE.from_role_id(record.role, str(record.ownerId))  # type: CapitalUser
 
             handler = payee.new_withdraw_handler(record)
 
-            gateway = WithdrawGateway.from_withdraw_gateway_key(
-                record.withdrawGatewayKey,
-                record.extras.get(
-                    'gateway_version', 'v1'))  # type: Optional[WechatWithdrawGateway, AliPayWithdrawGateway]
-
-            if isinstance(gateway, WechatWithdrawGateway):
-                result = WechatWithdrawQueryResult(gateway.get_transfer_result_via_bank(record.order))
-                if result.is_failed:
-                    handler.revoke(remarks = result['reason'], description = u'提现失败')
-                elif result.is_successful:
-                    handler.approve()
-            elif isinstance(gateway, AliPayWithdrawGateway):
-                try:
-                    if record.status == WithdrawStatus.BANK_PROCESSION:
-                        pay_date = arrow.get(record.extras['pay_date'], tzinfo = settings.TIME_ZONE)
-                        if pay_date.shift(days = check_days) < arrow.now():
-                            handler.approve(finishedTime = pay_date.naive)
-                    else:
-                        result = gateway.get_transfer_result_via_bank(record.order)
-                        if 'order_id' not in result:
-                            logger.warn('query is failure.')
-                            return
-
-                        if result['status'] == 'FAIL':
-                            error_code = result.get('error_code', u'提现失败')
-                            fail_reason = result.get('fail_reason', u'提现失败')
-                            handler.revoke(remarks = error_code, description = fail_reason)
-                        elif result['status'] == 'REFUND':
-                            handler.revoke(remarks = u'银行退票', description = u'银行退票')
-                        elif result['status'] == 'SUCCESS':
-                            if 'pay_fund_order_id' not in result or not result['pay_fund_order_id']:
-                                logger.debug('pay_fund_order_id not exist. ignore.')
-                                return
-
-                            if 'pay_date' not in result or not result['pay_date']:
-                                logger.debug('pay_date not exist. ignore.')
-                                return
-
-                            pay_date = arrow.get(result['pay_date'], tzinfo = settings.TIME_ZONE)
-
-                            if pay_date.shift(days = check_days) < arrow.now():
-                                handler.approve(finishedTime = pay_date.naive)
-                            else:
-                                handler.bank_processing(**{
-                                    'order_id': result['order_id'],
-                                    'pay_fund_order_id': result['pay_fund_order_id'],
-                                    'pay_date': result['pay_date']
-                                })
-                except AliPayServiceException as e:
-                    if e.errCode == 'ORDER_NOT_EXIST':
-                        handler.revoke(remarks = u'订单不存在', description = u'订单不存在')
+            try:
+                gateway = WithdrawGateway.from_withdraw_gateway_key(
+                    record.withdrawGatewayKey,
+                    record.extras.get(
+                        'gateway_version', 'v1'))  # type: Optional[WechatWithdrawGateway, AliPayWithdrawGateway]
+
+                query_result = gateway.get_transfer_result_via_bank(
+                    record.order)  # type: Union[WechatWithdrawQueryResult, AlipayWithdrawQueryResult]
+
+                errcode, errmsg = query_result.error_desc
+
+                if query_result.is_failed or query_result.is_refund:
+                    handler.revoke(remarks = errcode, description = errmsg)
+                elif query_result.is_successful:
+                    handler.approve(finishedTime = query_result.finished_time, extra = query_result.extra)
+            except WithdrawOrderNotExist:
+                logger.warning('withdraw order<orderNo={}> is not exist.'.format(record.order))
+                handler.revoke(remarks = u'订单不存在', description = u'提现失败')
+        except Exception as e:
+            logger.exception(e)
+
+    def process_v3_withdraw(record):
+        # type: (WithdrawRecord)->None
+
+        try:
+            version = record.extras.get('gateway_version', 'v1')
+            if version != 'v3':
+                logger.warning('record<id={}> is not v3.'.format(str(record.id)))
+                return
 
+            payee = ROLE.from_role_id(record.role, str(record.ownerId))  # type: CapitalUser
+
+            handler = payee.new_withdraw_handler(record)
+
+            try:
+                gateway = WithdrawGateway.from_withdraw_gateway_key(
+                    record.withdrawGatewayKey,
+                    record.extras.get(
+                        'gateway_version', 'v1'))  # type: WechatWithdrawGateway
+
+                query_result = gateway.get_transfer_result_via_changes(
+                    record.order)  # type: WechatWithdrawQueryResult
+
+                errcode, errmsg = query_result.error_desc
+
+                if query_result.is_failed or query_result.is_refund:
+                    handler.revoke(remarks = errcode, description = errmsg)
+                elif query_result.is_successful:
+                    handler.approve(finishedTime = query_result.finished_time, extra = query_result.extra)
+            except WithdrawOrderNotExist:
+                logger.warning('withdraw order<orderNo={}> is not exist.'.format(record.order))
+                handler.revoke(remarks = u'订单不存在', description = u'提现失败')
         except Exception as e:
             logger.exception(e)
 
@@ -422,6 +420,10 @@ def check_wechat_withdraw_via_bank():
     for record in records:
         process_withdraw(record)
 
+    records = list(WithdrawRecord.get_processing_via_v3())
+    for record in records:
+        process_v3_withdraw(record)
+
 
 def check_and_retry_withdraw():
     def process_dealer_withdraw(record):

+ 13 - 0
apps/web/models.py

@@ -2,6 +2,10 @@
 # !/usr/bin/env python
 
 from mongoengine import DynamicDocument, StringField, IntField, BooleanField, DoesNotExist
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pymongo.collection import Collection
 
 
 class ArchivedDatabaseConfig(DynamicDocument):
@@ -57,3 +61,12 @@ class ArchivedModelProxyConfig(DynamicDocument):
                 return '2017-07-01'
             else:
                 return cls.get_model_hot_data_start_day('default')
+
+
+class MongoTempTable(DynamicDocument):
+    _origin_meta = {'db_alias': 'logdata'}
+
+    @classmethod
+    def get_collection(cls):
+        # type: ()->Collection
+        return cls._get_collection()

+ 1 - 1
apps/web/report/ledger.py

@@ -70,7 +70,7 @@ class Ledger(object):
                 )
             )
 
-        if not self.record.is_success():
+        if not self.record.is_success:
             logger.debug('{} is not success.'.format(repr(self.record)))
             return
 

+ 1 - 1
apps/web/services/bluetooth/service.py

@@ -59,7 +59,7 @@ class StartAction(object):
 
         # 校验业务配置
         if 'washConfig' not in self._smartBox:
-            raise ServiceException({'result': 2, 'description': u'未配置该业务,请联系经销商'})
+            raise ServiceException({'result': 2, 'description': u'未配置该业务,请联系经销商(1002)'})
 
         # 校验套餐ID的有效性
         package = self._smartBox['washConfig'].get(self._packageId)

+ 10 - 3
apps/web/superadmin/views.py

@@ -446,14 +446,21 @@ def refundCashFromRecharge(request):
     if float(money) > float(str(rcd.money)):
         return JsonErrorResponse(description=u"退款金额大于订单金额,退款失败")
 
-    if not rcd.is_success():
+    if not rcd.is_success:
         return JsonErrorResponse(description = u"退款失败,订单状态非成功,请联系平台客服")
 
     if not rcd.is_ledgered:
         return JsonErrorResponse(description=u"订单还没有分账,无法退款")
 
     try:
-        refund_cash(recharge_record=rcd, refundFee=RMB(money), deductCoins=VirtualCoin(rcd.coins))
+        deductCoins = VirtualCoin(rcd.coins) * Ratio(float(RMB(money)) / float(RMB(rcd.money)))
+
+        refund_cash(recharge_record = rcd, refundFee = RMB(money), **{
+            'deductCoins': deductCoins,
+            'frozenCoins': deductCoins,
+            'operatorId': request.user.identity_id,
+            'checkWallet': True
+        })
         return JsonOkResponse()
     except ServiceException as e:
         logger.error(e.result)
@@ -2576,7 +2583,7 @@ def manualRechargeSimCard(request):
             return JsonErrorResponse(
                 description = u'创建订单失败,部分设备线下续费失败,请检查后重试(设备号{})。'.format(device.logicalCode))
 
-        rcd.succeed(rcd.orderNo)
+        rcd.succeed(wxOrderNo = rcd.orderNo)
 
         device.simStatus = u'chargedUnupdated'
         device.save()

+ 338 - 292
apps/web/user/models.py

@@ -35,11 +35,11 @@ from apilib.quantity import Quantity
 from apilib.systypes import IterConstant
 from apilib.utils import flatten
 from apilib.utils_datetime import generate_timestamp_ex, today_format_str, to_datetime, get_tomorrow_zero_time
-from apilib.utils_mongo import BulkHandlerEx
+from apilib.utils_mongo import BulkHandlerEx, dict_field_with_money
 from apilib.utils_string import get_random_str
 from apps import serviceCache
 from apps.web.agent.models import Agent, MoniApp
-from apps.web.common.models import OrderRecordBase
+from apps.web.common.models import OrderRecordBase, RefundOrderBase
 
 from apps.web.common.transaction import OrderNoMaker, OrderMainType, UserPaySubType, UserConsumeSubType, RefundSubType
 from apps.web.common.validation import CARD_NO_RE
@@ -62,6 +62,7 @@ from apps.web.device.models import Group, Device, DeviceType
 from apps.web.exceptions import UserServerException, PostPayOrderError
 from apps.web.report.ledger import AgentLedgerFirst, PartnerLedgerFirst
 from apps.web.report.utils import record_consumption_stats
+from apps.web.user.conf import REFUND_NOTIFY_URL
 from apps.web.user.constant2 import StartDeviceType, PackageCategory, CONSUME_ORDER_PAY_TIMEOUT, ConsumeOrderServiceItem, UserBalanceChangeCategory
 from apps.web.utils import concat_front_end_url, concat_user_login_entry_url, concat_user_center_entry_url, set_or_incr_cache, concat_count_down_page_url
 from library.idgen import IDGenService
@@ -510,6 +511,16 @@ class MyUser(RoleBaseDocument):
         if valid_to_pay:
             raise NotImplementedError()
 
+    def refund(self, payCount):
+        try:
+            self.update(inc__balance = payCount.amount, inc__total_consumed = -payCount.amount)
+        except Exception as e:
+            logger.exception(
+                'refund error = %s, openId = %s, money = %s' % (e, self.openId, payCount))
+            return 0, u'退金币失败'
+
+        return 1, ''
+
     def update_total_recharge(self, money):
         # type: (RMB)->int
         assert isinstance(money, RMB), 'money has to be RMB'
@@ -1235,6 +1246,43 @@ class MyUser(RoleBaseDocument):
         result = self.get_collection().update_one(query, update, upsert = False)  # type: UpdateResult
         return bool(result.modified_count == 1)
 
+    def revoke_refund_cash(self, refund_order):
+        # type:(RefundMoneyRecord)->bool
+
+        sync_key = 'r_{}'.format(str(refund_order.id))
+
+        ongoingItem = None
+        for item in self.ongoingList:
+            if item['transaction_id'] == sync_key:
+                ongoingItem = item
+                break
+
+        if not ongoingItem:
+            return False
+
+        query = {
+            '_id': self.id,
+            'ongoingList': {
+                '$elemMatch': {
+                    'transaction_id': sync_key
+                }
+            }
+        }
+
+        update = {
+            '$inc': {
+                'balance': VirtualCoin(ongoingItem['coins']).mongo_amount,
+                'total_recharged': refund_order.money.mongo_amount,
+            },
+
+            '$pull': {
+                'ongoingList': {'transaction_id': 'r_{}'.format(str(refund_order.id))}
+            }
+        }
+
+        result = self.get_collection().update_one(query, update, upsert = False)  # type: UpdateResult
+        return bool(result.modified_count == 1)
+
     @property
     def isNormal(self):
         return True
@@ -1493,16 +1541,27 @@ class RechargeRecordDict(dict):
 
 
 class RechargeRecord(OrderRecordBase):
+    """
+    三个作用:
+    1、经销商收益记录(特殊情况包括平台的收益记录,例如1毛保险单情况下)
+    2、充值订单,模型增加refundInfo信息
+    3、商家派送(金币,红包等)订单
+    """
+
     class PayResult(IterConstant):
         UNPAY = 'unPay'
+
         SUCCESS = 'success'
         FAILED = 'failed'
-        REFUNDING = 'refunding'
+
         CANCEL = 'cancel'
 
-    orderNo = StringField(verbose_name=u"订单号", unique=True)
-    wxOrderNo = StringField(verbose_name=u"渠道订单号")
-    transactionId = StringField(verbose_name=u"微信或者支付宝订单号", default=None)
+        REFUNDING = 'refunding'  # 退费处理中
+        CLOSE = 'close'  # 退款订单用, 被退单
+
+    orderNo = StringField(verbose_name = u"订单号", unique = True)
+    wxOrderNo = StringField(verbose_name = u"渠道订单号. 聚合商户,是聚合平台商户订单号;直连商户则是微信或支付宝订单号。互联互通订单号也用这个")
+    transactionId = StringField(verbose_name = u"微信或者支付宝订单号", default = None)
 
     ownerId = StringField(verbose_name = u"设备的owner", default = "")
 
@@ -1518,25 +1577,28 @@ class RechargeRecord(OrderRecordBase):
     extraInfo = DictField(verbose_name = u"支付订单模型信息", default = {})
     description = StringField(verbose_name = u"结果描述,一般为第三方错误码", default = None)
 
+    # 以下字段为充值订单的模型字段, MODEL中不在体现
+    #: 当用户余额为0,点击投币,生成充值订单,用户完成后直接自动投相应币数
+    #: 此举为优化用户体验,大多数情况用户对充值并不优先选择,使其能够缩短支付使用流程,用完即走
     isQuickPay = BooleanField(verbose_name = u"是否直接支付使用,默认为否", default = False)
     selectedQuickPayPackageId = StringField(verbose_name = u"快捷支付所选的投币套餐", default = None)
 
-    via = StringField(verbose_name = u"充值途径 =: (recharge|sendcoin|refund|chargeCard|swap)", default='recharge')
+    via = StringField(verbose_name = u"充值途径 =: (recharge|sendcoin|refund|chargeCard|swap)", default = 'recharge')
 
-    finishedTime = DateTimeField(verbose_name=u"支付完成时间")
+    finishedTime = DateTimeField(default = None, verbose_name = u"支付完成时间")
 
     # 派币需要记录信息
-    operator = StringField(verbose_name=u"操作员")
+    operator = StringField(verbose_name = u"操作员", default = None)
 
-    attachParas = DictField(verbose_name=u"消费订单信息", default={})
+    attachParas = DictField(verbose_name = u"消费订单信息", default = {})
 
-    gateway = StringField(verbose_name=u'支付网关类型', default='')
+    gateway = StringField(verbose_name = u'支付网关类型', default = '')
 
-    payAppType = StringField(verbose_name=u'支付应用类型', default=None)
-    payGatewayKey = StringField(verbose_name=u'支付网关key', default=None)
+    payAppType = StringField(verbose_name = u'支付应用类型', default = None)
+    payGatewayKey = StringField(verbose_name = u'支付网关key', default = None)
 
-    withdrawSourceKey = StringField(verbose_name=u'提现商户平台标识,资金池模式下和代理商相同')
-    isAllocatedCardMoney = BooleanField(verbose_name=u'是否分钱了', default=False)
+    withdrawSourceKey = StringField(verbose_name = u'提现商户平台标识,资金池模式下和代理商相同', default = None)
+    isAllocatedCardMoney = BooleanField(verbose_name = u'是否分钱了', default = None)
 
     search_fields = ('orderNo', 'wxOrderNo')
 
@@ -1607,6 +1669,7 @@ class RechargeRecord(OrderRecordBase):
         if self.isSubOrder:
             _id = self.attachParas.get("tradeOrderId", None) or self.extraInfo.get('tradeOrderId', None)
             return self.__class__.objects.get(id = _id)
+
         return self
 
     @property
@@ -1617,6 +1680,27 @@ class RechargeRecord(OrderRecordBase):
     def card_no(self):
         return self.attachParas.get('cardNo', None)
 
+    @property
+    def myuser(self):
+        _attr_name = '__my_user__'
+
+        if hasattr(self, _attr_name):
+            return getattr(self, _attr_name)
+        else:
+            _myuser = MyUser.get_or_create(
+                open_id = self.openId,
+                group_id = self.groupId,
+                app_platform_type = self.gateway)  # type: MyUser
+            setattr(self, _attr_name, _myuser)
+            return _myuser
+
+    @property
+    def my_subject(self):
+        if self.subject:
+            return self.subject
+        else:
+            return RECHARGE_RECORD_VIA_TRANSLATION.get(self.via)
+
     @property
     def my_description(self):
         if self.description:
@@ -1640,10 +1724,6 @@ class RechargeRecord(OrderRecordBase):
     def partition_map(self):
         return self.to_dict_obj.partition_map
 
-    @property
-    def is_cancel(self):
-        return self.result == self.PayResult.CANCEL
-
     @property
     def fen_total_fee(self):
         return int(self.money * 100)
@@ -2009,19 +2089,34 @@ class RechargeRecord(OrderRecordBase):
     def to_json_dict(self):
         return self.to_dict(json_safe = True)
 
-    def succeed(self, wxOrderNo, **kwargs):
-        # 这里和数据库强相关, 通过判断是否正确更新记录
+    def succeed(self, **kwargs):
         payload = {
-            'wxOrderNo': wxOrderNo
+            'result': self.PayResult.SUCCESS
         }
 
         if kwargs:
             payload.update(kwargs)
 
-        payload.update({'result': self.PayResult.SUCCESS})
+        result = self.get_collection().update_one(
+            filter = {'_id': ObjectId(self.id),
+                      'result': {'$nin': [self.PayResult.SUCCESS, self.PayResult.CLOSE]}},
+            update = {'$set': payload},
+            upsert = False)
+
+        return result.matched_count == 1
+
+
+    def close(self, **kwargs):
+        payload = {
+            'result': self.PayResult.CLOSE
+        }
+
+        if kwargs:
+            payload.update(kwargs)
 
         result = self.get_collection().update_one(
-            filter = {'_id': ObjectId(self.id), 'result': {'$ne': self.PayResult.SUCCESS}},
+            filter = {'_id': ObjectId(self.id),
+                      'result': {'$nin': [self.PayResult.SUCCESS, self.PayResult.CLOSE]}},
             update = {'$set': payload},
             upsert = False)
 
@@ -2043,12 +2138,26 @@ class RechargeRecord(OrderRecordBase):
 
         return result.matched_count == 1
 
+    @property
     def is_success(self):
         return self.result == self.PayResult.SUCCESS
 
+    @property
+    def is_refunding(self):
+        return self.result == self.PayResult.REFUNDING
+
+    @property
     def is_fail(self):
         return self.result == self.PayResult.FAILED
 
+    @property
+    def is_cancel(self):
+        return self.result == self.PayResult.CANCEL
+
+    @property
+    def is_close(self):
+        return self.result == self.PayResult.CLOSE
+
     def to_detail(self):
         # type: ()->dict
         """
@@ -3017,6 +3126,126 @@ class Card(Searchable):
             order=order
         )
 
+
+
+    @classmethod
+    def fake_one(cls, groupId, cardId='fake_id', cardNo='fake'):
+        return cls(id=cardId, cardNo=cardNo, openId='', productAgentId='', groupId=groupId)
+
+    @staticmethod
+    def get_card_status(cardId):
+        status = serviceCache.get(cardId, 'idle')
+        return status
+
+    @staticmethod
+    def set_card_status(cardId, status):
+        serviceCache.set(cardId, status, 10)
+
+    @staticmethod
+    def get_dev_cur_card(devNo):
+        return serviceCache.get('%s_cardId' % devNo, None)
+
+    @staticmethod
+    def set_dev_cur_card(devNo, cardInfo):
+        serviceCache.set('%s_cardId' % devNo, cardInfo, 90)
+
+    @property
+    def bound_virtual_card(self):
+        # type: ()->Optional[UserVirtualCard]
+
+        if self.boundVirtualCardId:
+            now = datetime.datetime.now()
+            virtual_card = UserVirtualCard.objects(id = self.boundVirtualCardId,
+                                                   expiredTime__gte = now,
+                                                   startTime__lte = now).first()
+            return virtual_card
+        else:
+            return None
+
+    def bind_virtual_card(self, card):
+        # type:(UserVirtualCard)->None
+        return self.update(boundVirtualCardId = card.id)
+
+    def unbind_virtual_card(self, card):
+        # type:(UserVirtualCard)->None
+        """
+        以后也许会支持多张卡
+        :param card:
+        :return:
+        """
+        return self.update(boundVirtualCardId = None)
+
+    # 这个那种有余额的卡,才会调用
+    @staticmethod
+    def update_balance(cardId, balance):
+        card = Card.objects(id=cardId).get()
+        result = card.update(balance = balance)
+        if not result:
+            logger.error('update error, cardId=%s, balance=%s' % (cardId, balance))
+        return result
+
+    @staticmethod
+    def check_card_no(cardNo):
+        """
+        检查实体卡卡号的合法性
+        :param cardNo:
+        :return:
+        """
+        if CARD_NO_RE.match(cardNo):
+            return True
+        return False
+
+    @classmethod
+    def check_swap_card_no(cls, cardNo, dealerId, agentId):
+        try:
+            card = cls.objects.get(cardNo=cardNo, agentId=agentId)
+        except DoesNotExist:
+            return True, ""
+
+        if card.dealerId and card.dealerId != dealerId:
+            return False, u"该卡已经被其他经销商绑定,请确认卡号无误"
+
+        if card.openId:
+            return False, u"该卡号已经被其他用户绑定,请确认卡号无误"
+
+        if CardConsumeRecord.objects.filter(cardId=str(card.id)):
+            return False, u"该卡号存在用户使用记录, 请确认卡号无误"
+
+        card.delete()
+        return True, ""
+
+    @staticmethod
+    def record_dev_card_no(devNo, cardNo):
+        serviceCache.set('%s-cardno' % devNo, cardNo, 2 * 60)
+
+    @staticmethod
+    def get_dev_card_no(devNo):
+        return serviceCache.get('%s-cardno' % devNo, None)
+
+    @staticmethod
+    def clear_dev_card_no(devNo):
+        serviceCache.delete('%s-cardno' % devNo)
+
+    def to_dict(self):
+        data = {
+            "cardId": str(self.id),
+            "cardNo": self.cardNo,
+            "cardName": self.cardName,
+            "phone": self.phone,
+            "cardType": self.cardType,
+            "remarks": self.remarks,
+            "frozen": self.frozen,
+            "balance": self.balance,
+            "lastMaxBalance": self.lastMaxBalance,
+            "bindStatus": self.isBinded,
+        }
+
+        return data
+
+    def freeze_transaction_id(self, freezeType):
+        return '{}_{}'.format(freezeType, str(self.id))
+
+
     @classmethod
     def freeze_balance(cls, transaction_id, payment):
         bulker = BulkHandlerEx(cls.get_collection())  # type: BulkHandlerEx
@@ -3059,7 +3288,7 @@ class Card(Searchable):
                 return True
 
     @classmethod
-    def clear_frozen_balance(cls, transaction_id, payment, refund):
+    def clear_frozen_balance(cls, transaction_id, refund):
         try:
             bulker = BulkHandlerEx(cls.get_collection())  # type: BulkHandlerEx
             chargeBalanceField = cls.chargeBalance.name
@@ -3136,122 +3365,6 @@ class Card(Searchable):
                 return True
 
 
-
-
-    @classmethod
-    def fake_one(cls, groupId, cardId='fake_id', cardNo='fake'):
-        return cls(id=cardId, cardNo=cardNo, openId='', productAgentId='', groupId=groupId)
-
-    @staticmethod
-    def get_card_status(cardId):
-        status = serviceCache.get(cardId, 'idle')
-        return status
-
-    @staticmethod
-    def set_card_status(cardId, status):
-        serviceCache.set(cardId, status, 10)
-
-    @staticmethod
-    def get_dev_cur_card(devNo):
-        return serviceCache.get('%s_cardId' % devNo, None)
-
-    @staticmethod
-    def set_dev_cur_card(devNo, cardInfo):
-        serviceCache.set('%s_cardId' % devNo, cardInfo, 90)
-
-    @property
-    def bound_virtual_card(self):
-        # type: ()->Optional[UserVirtualCard]
-
-        if self.boundVirtualCardId:
-            now = datetime.datetime.now()
-            virtual_card = UserVirtualCard.objects(id = self.boundVirtualCardId,
-                                                   expiredTime__gte = now,
-                                                   startTime__lte = now).first()
-            return virtual_card
-        else:
-            return None
-
-    def bind_virtual_card(self, card):
-        # type:(UserVirtualCard)->None
-        return self.update(boundVirtualCardId = card.id)
-
-    def unbind_virtual_card(self, card):
-        # type:(UserVirtualCard)->None
-        """
-        以后也许会支持多张卡
-        :param card:
-        :return:
-        """
-        return self.update(boundVirtualCardId = None)
-
-    # 这个那种有余额的卡,才会调用
-    @staticmethod
-    def update_balance(cardId, balance):
-        card = Card.objects(id=cardId).get()
-        result = card.update(balance = balance)
-        if not result:
-            logger.error('update error, cardId=%s, balance=%s' % (cardId, balance))
-        return result
-
-    @staticmethod
-    def check_card_no(cardNo):
-        """
-        检查实体卡卡号的合法性
-        :param cardNo:
-        :return:
-        """
-        if CARD_NO_RE.match(cardNo):
-            return True
-        return False
-
-    @classmethod
-    def check_swap_card_no(cls, cardNo, dealerId, agentId):
-        try:
-            card = cls.objects.get(cardNo=cardNo, agentId=agentId)
-        except DoesNotExist:
-            return True, ""
-
-        if card.dealerId and card.dealerId != dealerId:
-            return False, u"该卡已经被其他经销商绑定,请确认卡号无误"
-
-        if card.openId:
-            return False, u"该卡号已经被其他用户绑定,请确认卡号无误"
-
-        if CardConsumeRecord.objects.filter(cardId=str(card.id)):
-            return False, u"该卡号存在用户使用记录, 请确认卡号无误"
-
-        card.delete()
-        return True, ""
-
-    @staticmethod
-    def record_dev_card_no(devNo, cardNo):
-        serviceCache.set('%s-cardno' % devNo, cardNo, 2 * 60)
-
-    @staticmethod
-    def get_dev_card_no(devNo):
-        return serviceCache.get('%s-cardno' % devNo, None)
-
-    @staticmethod
-    def clear_dev_card_no(devNo):
-        serviceCache.delete('%s-cardno' % devNo)
-
-    def to_dict(self):
-        data = {
-            "cardId": str(self.id),
-            "cardNo": self.cardNo,
-            "cardName": self.cardName,
-            "phone": self.phone,
-            "cardType": self.cardType,
-            "remarks": self.remarks,
-            "frozen": self.frozen,
-            "balance": self.balance,
-            "lastMaxBalance": self.lastMaxBalance,
-            "bindStatus": self.isBinded,
-        }
-
-        return data
-
     def clear_card(self):
         return self.update(
             cardType = '',
@@ -3276,44 +3389,13 @@ class Card(Searchable):
         )
 
 
-class RefundMoneyRecord(Searchable):
+class RefundMoneyRecord(RefundOrderBase):
     """
     用户退款记录
     """
 
-    class Status(object):
-        """ 退款单的状态 正常流程顺序是从上至下 """
-        CREATED = 'created'  # 创建
-        PROCESSING = 'processing'  # 申请中
-        FAILURE = 'failure'  # 申请失败   (彻底失败 需要重新发起)
-        SUCCESS = 'success'  # 退款成功   (异步结果)
-        CLOSED = 'closed'  # 退款失败   (异步结果 不需要重新发起)
-
-        # 订单常见的状态
-        # created ---> processing ---> success  (申请 然后接受成功)
-        # created ---> failure  (直接申请失败)
-        # created ---> processing ---> closed    (申请成功 回调失败)
-        # created ---> processing ---> failure   (申请成功  回调失败 不可以重试)
-
-    rechargeObjId = ObjectIdField(verbose_name = u'对应充值单')
-
-    # refundSeq = IntField(verbose_name=u'多次退款,计算退款序列号', default=1)
-
-    orderNo = StringField(verbose_name = u'商户退款订单号', default = '')
-
-    errorCode = StringField(verbose_name = u"错误代码", default = "")
-    errorDesc = StringField(verbose_name = u"错误描述", default = "")
-
-    money = MonetaryField(verbose_name = u"退款的金钱数额", default = RMB('0.00'))
-    coins = MonetaryField(verbose_name = u"清理用户金币数", default = VirtualCoin('0.00'))
-
-    status = StringField(verbose_name = u'订单状态', default = Status.CREATED)
-
-    datetimeAdded = DateTimeField(verbose_name = u"退款创建时间", default = datetime.datetime.now)
-    datetimeUpdated = DateTimeField(verbose_name = u"退款更新时间")
-    finishedTime = DateTimeField(verbose_name = u"退款到账时间")
-
-    tradeRefundNo = StringField(verbose_name = u"交易机构退款单号", default = "")
+    refIncomeOrder = ObjectIdField(verbose_name = u'对应收益单', default = None)
+    coins = MonetaryField(verbose_name = u"清理用户金币数", default = None)
 
     meta = {
         "collection": "RefundMoneyRecord",
@@ -3321,11 +3403,20 @@ class RefundMoneyRecord(Searchable):
     }
 
     @classmethod
-    def issue(cls, order, refundCash, deductCoins):
-        # type:(RechargeRecord, RMB, VirtualCoin)->RefundMoneyRecord
+    def issue(cls, order, refundCash, **extraInfo):
+        # type:(RechargeRecord, RMB, dict)->RefundMoneyRecord
+
+        extraInfo.update({'v': 2})
+
+        if order.via in [USER_RECHARGE_TYPE.RECHARGE_CARD, USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD]:
+            identifier = order.attachParas['cardNo']
+        elif order.via in [USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE]:
+            identifier = order.attachParas.get('cardId')
+        else:
+            identifier = order.logicalCode
 
         refund_order_no = OrderNoMaker.make_order_no_32(
-            identifier = order.logicalCode,
+            identifier = identifier,
             main_type = OrderMainType.REFUND,
             sub_type = RefundSubType.REFUND)
 
@@ -3334,115 +3425,19 @@ class RefundMoneyRecord(Searchable):
             # refundSeq=next_seq,
             orderNo = refund_order_no,
             money = refundCash,
-            coins = deductCoins,
-            status = cls.Status.PROCESSING).save()
-
-    def succeed(self, tradeRefundNo, finishedTime):  # type:(str, datetime.datetime) -> bool
-        """ 更新为成功状态 已经成功退款 """
-        result = self.__class__.get_collection().update_one(
-            {
-                'orderNo': self.orderNo,
-                'status': self.Status.PROCESSING
-            },
-            {
-                '$set': {
-                    'status': self.Status.SUCCESS,
-                    'datetimeUpdated': datetime.datetime.now(),
-                    'tradeRefundNo': tradeRefundNo,
-                    'finishedTime': finishedTime
-                }
-            }
-        )
-
-        return result.matched_count == 1
-
-    def fail(self, errorCode="", errorDesc=""):     # type:(str, unicode) -> bool
-        """
-        更新为失败状态 表示退款申请失败 这种情况下 可能就需要售后进行介入
-        1. 订单申请发起时候即失败
-        2. 订单申请通过 但是回调的时候明确失败 并且是不可重试的失败
-        3. 此状态即表示订单没退款 理论上可以重新发起退款
-        """
-        result = self.__class__.objects.filter(
-            orderNo=self.orderNo,
-            status__in=[self.Status.PROCESSING, self.Status.CREATED]
-        ).update(
-            status=self.Status.FAILURE,
-            datetimeUpdated=datetime.datetime.now(),
-            errorCode=errorCode,
-            errorDesc=errorDesc
-        )
-
-        # TODO 这种情况需不需要将对于经销商的分账重新执行
-        return result
-
-    def closed(self, tradeRefundNo, errorCode="", errorDesc=""):  # type:(str, str, str) -> bool
-        """
-        退款申请成功了 表示合法的退款申请 但是由于某些原因业务进行不下去
-
-        """
-        result = self.__class__.objects.filter(
-            orderNo=self.orderNo,
-            status=self.Status.PROCESSING
-        ).update(
-            status=self.Status.CLOSED,
-            datetimeUpdated=datetime.datetime.now(),
-            tradeRefundNo=tradeRefundNo,
-            errorCode=errorCode,
-            errorDesc=errorDesc
-        )
-
-        return result
-
-    def processing(self):   # type:() -> bool
-        result = self.__class__.objects.filter(
-            orderNo=self.orderNo,
-            status__in=[self.Status.CLOSED, self.Status.CREATED, self.Status.FAILURE]
-        ).update(
-            status=self.Status.PROCESSING,
-            datetimeUpdated=datetime.datetime.now()
-        )
-        return result
-
-    @property
-    def is_fail(self):
-        """ 是否订单失败 """
-        return self.status == self.Status.FAILURE
-
-    @property
-    def is_closed(self):
-        """ 退款单是否已经关闭  目前这个状态没有用 适用于用户手动取消退款申请 """
-        return self.status == self.Status.CLOSED
-
-    @property
-    def is_success(self):
-        """ 退款已经成功 """
-        return self.status == self.Status.SUCCESS
-
-    @property
-    def is_created(self):
-        return self.status == self.Status.CREATED
-
-    @property
-    def is_apply(self):
-        """ 是否已经发出退款申请 """
-        return self.status in [self.Status.CREATED, self.Status.PROCESSING]
-
-    @property
-    def is_processing(self):
-        return self.status == self.Status.PROCESSING
-
-    @property
-    def is_successful(self):
-        """ 保留旧的方法 """
-        return self.status in [self.Status.SUCCESS, self.Status.PROCESSING]
+            status = cls.Status.PROCESSING,
+            datetimeAdded = datetime.datetime.now(),
+            extraInfo = dict_field_with_money(extraInfo),
+            payAppType = order.pay_app_type
+        ).save()
 
     @property
     def pay_sub_order(self):
         # type: ()->RechargeRecord
 
         if not hasattr(self, '__pay_sub_order__'):
-            pay_order = RechargeRecord.objects(id = str(self.rechargeObjId)).first()
+            from apps.web.common.proxy import ClientRechargeModelProxy
+            pay_order = ClientRechargeModelProxy.get_one(id = str(self.rechargeObjId))  # type: RechargeRecord
             setattr(self, '__pay_sub_order__', pay_order)
 
         return getattr(self, '__pay_sub_order__')
@@ -3452,26 +3447,77 @@ class RefundMoneyRecord(Searchable):
         setattr(self, '__pay_sub_order__', order)
 
     @property
-    def refund_order_record(self):
-        if not hasattr(self, '__refund_order_record__'):
-            order = RechargeRecord.objects(orderNo = self.orderNo).first()
-            setattr(self, '__refund_order_record__', order)
+    def pay_app_type(self):
+        if self.payAppType:
+            return self.payAppType
+        else:
+            return self.pay_sub_order.pay_app_type
+
+    @property
+    def refund_income_order(self):
+        if not hasattr(self, '__refund_income_order__'):
+            if self.refIncomeOrder:
+                order = RechargeRecord.objects(
+                    id = self.refIncomeOrder).first()
+            else:
+                # 对老的方式的兼容
+                order = RechargeRecord.objects(
+                    id = self.pay_sub_order.extraInfo['refRefund'][0]['objId']).first()
 
-        return getattr(self, '__refund_order_record__')
+            setattr(self, '__refund_income_order__', order)
 
-    @refund_order_record.setter
-    def refund_order_record(self, refund_order_record):
-        setattr(self, '__refund_order_record__', refund_order_record)
+        return getattr(self, '__refund_income_order__')
 
-    @classmethod
-    def get_record(cls, order_no):
-        return cls.objects(orderNo = order_no).first()
+    @refund_income_order.setter
+    def refund_income_order(self, obj):
+        setattr(self, '__refund_income_order__', obj)
 
     @property
     def user(self):
         # type: ()->MyUser
         return self.pay_sub_order.user
 
+    @property
+    def notify_url(self):
+        if self.pay_app_type in [PayAppType.WECHAT]:
+            return REFUND_NOTIFY_URL.WECHAT_REFUND_BACK
+        else:
+            return None
+
+    @property
+    def checkWallet(self):
+        return self.extraInfo.get('checkWallet', False)
+
+    @property
+    def operatorId(self):
+        return self.extraInfo.get('operatorId', None)
+
+    @property
+    def is_new_version(self):
+        return self.extraInfo.get('v', 1) > 1
+
+    @property
+    def deductCoins(self):
+        """
+        兼容以前数据.退现金同时被扣费的钱包余额。后续需要建模为被扣的描述(钱包就是被扣金额, 月卡等有自己的描述)
+        :return:
+        """
+        if self.is_new_version:
+            return VirtualCoin(self.extraInfo.get('deductCoins', 0))
+        else:
+            return self.coins
+
+    @property
+    def frozenCoins(self):
+        """
+        不退现金情况下应该退的钱包余额
+        :return:
+        """
+        if self.is_new_version:
+            return VirtualCoin(self.extraInfo.get('frozenCoins', 0))
+        else:
+            return self.coins
+
 
 class VCardConsumeRecord(OrderRecordBase):
     orderNo = StringField(verbose_name = "订单号", default = "")

+ 5 - 5
apps/web/user/tasks.py

@@ -10,7 +10,8 @@ from mongoengine import DoesNotExist
 from typing import Optional, TYPE_CHECKING
 
 from apps.web.agent.models import Agent
-from apps.web.common.transaction.pay import PayManager, RefundManager
+from apps.web.common.transaction.pay import PayManager
+from apps.web.common.transaction.refund import RefundManager
 from apps.web.constant import Const
 from apps.web.core.bridge import WechatClientProxy
 from apps.web.core.helpers import ActionDeviceBuilder
@@ -20,17 +21,16 @@ from apps.web.dealer.models import Dealer, VirtualCard
 from apps.web.device.models import Group, Device
 from apps.web.helpers import get_wechat_user_manager_mp_proxy, get_wechat_user_sub_manager_mp_proxy, \
     get_wechat_user_messager_app
-from apps.web.user.models import RechargeRecord, UserVirtualCard, RefundMoneyRecord
 from apps.web.promotion.models import InsuranceOrder, Insurance
+from apps.web.user.models import RechargeRecord, UserVirtualCard, RefundMoneyRecord
 from apps.web.user.transaction import post_pay
 from apps.web.user.transaction_deprecated import refund_post_pay
-from apps.web.user.utils import get_consume_order
 from apps.web.utils import concat_front_end_url
 
 if TYPE_CHECKING:
     from apps.web.common.transaction.pay import PayRecordPoller
-    from apps.web.user.models import MyUser, ConsumeRecord
-    from apps.web.core.payment import PaymentGatewayT
+    from apps.web.user.models import MyUser
+    from apps.web.core.payment.type_checking import PaymentGatewayT
 
 logger = get_task_logger('user.tasks')
 

+ 415 - 253
apps/web/user/transaction_deprecated.py

@@ -6,28 +6,27 @@ import logging
 import time
 import uuid
 
-from pymongo.errors import DuplicateKeyError
-from typing import TYPE_CHECKING, Dict, Any
+from bson import ObjectId
+from mongoengine import NotUniqueError
+from typing import TYPE_CHECKING, Dict
 
 from apilib.monetary import VirtualCoin, RMB
-from apps.web.common.transaction.pay import RefundManager
-from apps.web.constant import USER_RECHARGE_TYPE, RechargeRecordVia
+from apilib.utils import flatten
+from apps.web.common.proxy import ClientDealerIncomeModelProxy
+from apps.web.common.transaction.refund import RefundCashMixin, RefundManager
+from apps.web.constant import USER_RECHARGE_TYPE, PARTITION_ROLE, RechargeRecordVia, AppPlatformType
 from apps.web.core import PayAppType, ROLE
 from apps.web.core.exceptions import ParameterError
-from apps.web.core.payment import PaymentGateway
-from apps.web.dealer.define import DEALER_INCOME_SOURCE
-from apps.web.dealer.proxy import DealerIncomeProxy
+from apps.web.core.payment import WithdrawGateway
+from apps.web.dealer.define import DEALER_INCOME_SOURCE, DEALER_INCOME_TYPE
 from apps.web.device.models import Group
 from apps.web.exceptions import UserServerException
-from apps.web.user.conf import REFUND_NOTIFY_URL
-from apps.web.user.models import MyUser, RechargeRecord, RefundMoneyRecord
-from library.alipay import AliException
-from library.wechatbase.exceptions import WeChatPayException
+from apps.web.user.models import MyUser, RechargeRecord, RefundMoneyRecord, Card, UserVirtualCard, MonthlyPackage
 
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
-    pass
+    from apps.web.dealer.proxy import DealerIncomeProxy
 
 
 def refund_money(device, money, openId):
@@ -63,61 +62,74 @@ def refund_money(device, money, openId):
         logger.exception('update record for feedback coins error=%s,orderNo=%s' % (e, orderNo))
 
 
-def refund_cash(recharge_record, refundFee, deductCoins, **kwargs):
-    # type:(RechargeRecord, RMB, VirtualCoin, Dict[str, Any])->RefundMoneyRecord
+def refund_cash(recharge_record, refundFee, **kwargs):
+    # type:(RechargeRecord, RMB, Dict)->RefundMoneyRecord
 
     """
     新的执行退款 为了保持导包顺序不变
-    :param deductCoins:
+
     :param recharge_record:
     :param refundFee:
-    :param kwargs 用户为资金实体的情况下, 传入user和minus_total_consume参数
+    :type kwargs: object
     :return:
     """
 
-    if recharge_record.via in [RechargeRecordVia.Balance, RechargeRecordVia.Cash, RechargeRecordVia.StartDevice]:
-        return RefundCash(recharge_record, refundFee, deductCoins).execute(
-            frozen_callable = frozen_refund_for_balance, **kwargs)
+    if recharge_record.via in [
+        RechargeRecordVia.Balance,
+        RechargeRecordVia.Cash,
+        RechargeRecordVia.Card,
+        RechargeRecordVia.VirtualCard,
+        RechargeRecordVia.MonthlyPackage,
+    ]:
+        return RefundCash(recharge_record, refundFee, **kwargs).execute(
+            frozen_callable = frozen_refund_func, refund_callable = refund_post_pay)
     else:
         raise UserServerException(u'不支持该类型订单退款')
 
 
-class RefundCash(object):
-    # 最长的查询分账时间
-    MAX_LEDGER_CHECK_TIME = 15
+class RefundCash(RefundCashMixin):
+    MAX_LEDGER_CHECK_TIME = 15  # 最长的查询分账时间
 
-    def __init__(self, rechargeOrder, refundFee, deductCoins):  # type:(RechargeRecord, RMB, VirtualCoin) -> None
-        self.paySubOrder = rechargeOrder
-        self.payOrder = self.paySubOrder.payOrder
+    def __init__(self, rechargeOrder, refundFee, **kwargs):
+        # type:(RechargeRecord, RMB, dict) -> None
 
-        self.refundFee = refundFee
-        self.deductCoins = deductCoins
+        super(RefundCash, self).__init__(rechargeOrder, refundFee)
 
-        # self._nextSeq = 1
+        self.extraInfo = kwargs
 
-    @property
-    def outTradeNo(self):
-        """
-        交易单号
-        :return:
-        """
-        return self.payOrder.orderNo
+        # self._nextSeq = 1
 
-    @property
-    def totalFee(self):
-        return self.payOrder.money
+    def check_wallet(self, proxy, order):
+        partition_map = proxy.partition_map
+        for partition in list(flatten(partition_map.values())):
+            if partition['role'] == PARTITION_ROLE.OWNER:
+                leftBalance = order.owner.sub_balance(
+                    income_type = DEALER_INCOME_TYPE.DEVICE_INCOME,
+                    source_key = order.withdraw_source_key,
+                    only_ledger = True)
+                if abs(RMB(partition['money'])) > leftBalance:
+                    raise UserServerException(u"您的钱包余额不足,无法退款。")
+
+            elif partition['role'] == PARTITION_ROLE.PARTNER:
+                from apps.web.dealer.models import Dealer
+                dealer = Dealer.objects(id = partition['id']).first()
+                leftBalance = dealer.sub_balance(
+                    income_type = DEALER_INCOME_TYPE.DEVICE_INCOME,
+                    source_key = order.withdraw_source_key)
+                if abs(RMB(partition['money'])) > leftBalance:
+                    raise UserServerException(u"您的分账合伙人钱包余额不足,无法退款(1001)。")
+            else:
+                from apps.web.agent.models import Agent
+                from apps.web.agent.define import AGENT_INCOME_TYPE
 
-    @property
-    def totalCoins(self):
-        return self.payOrder.coins
+                agent = Agent.objects(id = partition['id']).first()
 
-    @property
-    def subTotalFee(self):
-        return self.paySubOrder.money
+                leftBalance = agent.sub_balance(
+                    income_type = AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
+                    source_key = order.withdraw_source_key)
 
-    @property
-    def subTotalCoins(self):
-        return self.paySubOrder.coins
+                if abs(RMB(partition['money'])) > leftBalance:
+                    raise UserServerException(u"您的分账合伙人钱包余额不足,无法退款(1002)。")
 
     def pre_check(self):
         """
@@ -125,38 +137,51 @@ class RefundCash(object):
         :return:
         """
 
-        if self.refundFee <= RMB(0) or self.refundFee > self.totalFee or self.refundFee > self.subTotalFee:
+        if self.refundFee <= RMB(0) or self.refundFee > self.subTotalFee:
             raise ParameterError(u"退费金额错误")
 
-        if self.deductCoins < VirtualCoin(
-                0) or self.deductCoins > self.totalCoins or self.deductCoins > self.subTotalCoins:
-            raise ParameterError(u"扣除用户金币数目错误")
+        if 'deductCoins' in self.extraInfo:
+            deductCoins = self.extraInfo['deductCoins']
+            if deductCoins < VirtualCoin(
+                    0) or deductCoins > self.subTotalCoins:
+                raise ParameterError(u"扣除用户金币数目错误")
 
         check_end_time = int(time.time()) + self.MAX_LEDGER_CHECK_TIME
 
-        if not self.paySubOrder.is_success():
+        if not self.paySubOrder.is_success:
             raise UserServerException(u'非成功订单无法进行退款')
 
         while not self.paySubOrder.is_ledgered and int(time.time()) < check_end_time:
             logger.debug('{} is not allocated. wait to be allocated.'.format(repr(self.paySubOrder)))
 
-            # TODO  考虑回调的方式进行
             time.sleep(5)
             self.paySubOrder.reload()
 
-        proxy = DealerIncomeProxy.objects.filter(
-            ref_id = self.paySubOrder.id).first()  # type: DealerIncomeProxy
+        proxy = ClientDealerIncomeModelProxy.get_one(ref_id = self.paySubOrder.id)  # type: DealerIncomeProxy
         if not proxy:
-            raise UserServerException(u"订单尚未分账,无法退款")
+            raise UserServerException(u"订单尚未分账,无法退款(10002)")
+
+        if self.paySubOrder.gateway == AppPlatformType.ALIPAY:
+            over_time = 90 * 24 * 60 * 60
+        else:
+            over_time = 365 * 24 * 60 * 60
+
+        if (datetime.datetime.now() - self.paySubOrder.finishedTime).total_seconds() >= over_time:
+            raise UserServerException(u'超期订单不允许退款')
+
+        checkWallet = self.extraInfo.get('checkWallet', False)
+        if checkWallet and WithdrawGateway.is_ledger(source_key = self.paySubOrder.withdraw_source_key):
+            self.check_wallet(proxy, self.paySubOrder)
 
         return proxy
 
-    def create_refund_order(self):
-        refundOrder = RefundMoneyRecord.issue(self.paySubOrder, self.refundFee, self.deductCoins)
+    def create_refund_order(self, **extraInfo):
+        refundOrder = RefundMoneyRecord.issue(
+            self.paySubOrder, self.refundFee, **extraInfo)
         refundOrder.pay_sub_order = self.paySubOrder
         return refundOrder
 
-    def execute(self, frozen_callable, **kwargs):
+    def execute(self, frozen_callable, refund_callable, notify_url = None):
         """
         执行退款的动作
         对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额
@@ -166,114 +191,54 @@ class RefundCash(object):
 
         proxy = self.pre_check()
 
-        payGateway = PaymentGateway.clone_from_order(self.payOrder)  # type: PaymentGateway
-
         try:
-            refundOrder = self.create_refund_order()  # type: RefundMoneyRecord
-        except DuplicateKeyError:
+            refundOrder = self.create_refund_order(**self.extraInfo)  # type: RefundMoneyRecord
+        except NotUniqueError:
             raise UserServerException(u'已经有退款订单正在进行')
 
-        if str(self.paySubOrder.id) == str(self.payOrder.id):
-            logger.info(
-                'refund paras, orderNo = {} refundOrderNo = {} refundFee = {} totalFee = {}'.format(
-                    self.paySubOrder.orderNo, refundOrder.orderNo, self.refundFee, self.subTotalFee)
-            )
-        else:
-            logger.info(
-                'refund paras, mix<orderNo = {}, totalFee={}>, sub<orderNo = {}, totalFee={}> '
-                'refundOrderNo = {} refundFee = {} '.format(
-                    self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee,
-                    refundOrder.orderNo, self.refundFee)
-            )
+        logger.info('refund paras: {} {}'.format(refundOrder.orderNo, self.refund_paras))
+
+        split_map = proxy.partition_map
 
-        refund_recharge_order = self.paySubOrder.new_refund_cash_order(refundOrder)  # type: RechargeRecord
+        refund_income_order = self.paySubOrder.issue_refund_income_order(
+                refundOrder, split_map)  # type: RechargeRecord
 
-        frozen_callable(refundOrder, **kwargs)  # 对资金实体进行退款(用户余额,卡余额等)
+        frozen_callable(refundOrder)  # 对资金实体进行退款冻结(用户余额,卡余额等)
 
         try:
-            if payGateway.pay_app_type == PayAppType.ALIPAY:
-                # 支付宝的退款方式
-                # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功
-                # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态
-                try:
-                    result = payGateway.refund_to_user(
-                        out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo,
-                        refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费')
-                except AliException as e:
-                    logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e))
-                    raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
-
-                if result["code"] != "10000":
-                    refundOrder.fail(errorCode = "{}-{}".format(result["code"], result.get("sub_code")),
-                                     errorDesc = "{}-{}".format(result["msg"], result.get("sub_msg")))
-
-                logger.info('ALIPAY Refund request successfully! return = {}'.format(result))
-
-            elif payGateway.pay_app_type in [PayAppType.WECHAT, PayAppType.WECHAT_MINI]:
-                try:
-                    result = payGateway.refund_to_user(
-                        out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo,
-                        refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费',
-                        notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK)
-                except WeChatPayException as e:
-                    logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e))
-                    refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
-                    raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
-
-                logger.info('WECHAT Refund request successfully! return = {}'.format(result))
-
-        except UserServerException as se:
-            logger.error(se.message)
-            raise se
-        except Exception as ee:
-            # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新
-            logger.exception(ee)
-            raise UserServerException(ee.message)
+            self.submit_refund(
+                    refundOrder, refund_income_order.partition_map, u'现金退款',
+                    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:
-            # 资金池方式下,直接记录负单.所有对账时间都以系统内时间为准
-            if payGateway.occupant.role == ROLE.agent:
+            refundOrder.reload()
+
+            if refundOrder.is_closed or refundOrder.is_success:
+                # 终态已经调用了post_pay, 所以不在做任何处理
+                pass
+
+            elif refundOrder.my_payment_gateway.occupant.role == ROLE.agent:
+                # 资金池情况下认为成功, 冻结运营商金额
+                refund_income_order.result = RechargeRecord.PayResult.SUCCESS
+                refund_income_order.finishedTime = datetime.datetime.now()
+                refund_income_order.save()
+
                 from apps.web.report.ledger import Ledger
-                ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_recharge_order)
-                ledger.execute(journal = False, stats = True, check = False)
+                ledger = Ledger(refund_income_order.via, refund_income_order)
+                ledger.execute(stats = True)
 
         return refundOrder
 
 
-class RetryRefundCash(object):
+class RetryRefundCash(RefundCashMixin):
     def __init__(self, refundOrder):  # type:(RefundMoneyRecord) -> None
-        self.paySubOrder = refundOrder.pay_sub_order
-        self.payOrder = self.paySubOrder.payOrder
-
-        self.refundFee = refundOrder.money
-        self.deductCoins = refundOrder.coins
+        super(RetryRefundCash, self).__init__(refundOrder.pay_sub_order, refundOrder.money)
 
         self.refundOrder = refundOrder
-        # self._nextSeq = 1
-
-    @property
-    def outTradeNo(self):
-        """
-        交易单号
-        :return:
-        """
-        return self.payOrder.orderNo
-
-    @property
-    def totalFee(self):
-        return self.payOrder.money
-
-    @property
-    def totalCoins(self):
-        return self.payOrder.coins
-
-    @property
-    def subTotalFee(self):
-        return self.paySubOrder.money
-
-    @property
-    def subTotalCoins(self):
-        return self.paySubOrder.coins
 
     def pre_check(self):
         """
@@ -281,24 +246,26 @@ class RetryRefundCash(object):
         :return:
         """
 
-        if self.refundFee <= RMB(0) or self.refundFee > self.totalFee or self.refundFee > self.subTotalFee:
+        if self.refundFee <= RMB(0) or self.refundFee > self.subTotalFee:
             raise ParameterError(u"退费金额错误")
 
-        if self.deductCoins < VirtualCoin(
-                0) or self.deductCoins > self.totalCoins or self.deductCoins > self.subTotalCoins:
+        deductCoins = self.refundOrder.deductCoins
+        if deductCoins < VirtualCoin(0) or deductCoins > self.subTotalCoins:
             raise ParameterError(u"扣除用户金币数目错误")
 
-        if self.refundOrder.is_closed or self.refundOrder.is_success:
-            raise UserServerException(u'已经完结订单不能重试')
+        if not self.refundOrder.is_fail and not self.refundOrder.is_no_order:
+            raise UserServerException(u'状态非错误的订单不能重试')
 
-        proxy = DealerIncomeProxy.objects.filter(
-            ref_id = self.paySubOrder.id).first()  # type: DealerIncomeProxy
+        proxy = ClientDealerIncomeModelProxy.get_one(ref_id = self.paySubOrder.id)
         if not proxy:
-            raise UserServerException(u"订单尚未分账,无法退款")
+            raise UserServerException(u"订单尚未分账,无法退款(10002)")
+
+        if self.refundOrder.checkWallet and WithdrawGateway.is_ledger(source_key = self.paySubOrder.withdraw_source_key):
+            self.check_wallet(proxy, self.paySubOrder)
 
         return proxy
 
-    def execute(self, frozen_callable, **kwargs):
+    def execute(self, frozen_callable, refund_callable, notify_url = None):
         """
         执行退款的动作
         对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额
@@ -308,126 +275,321 @@ class RetryRefundCash(object):
 
         proxy = self.pre_check()
 
-        payGateway = PaymentGateway.clone_from_order(self.payOrder)  # type: PaymentGateway
-
-        if str(self.paySubOrder.id) == str(self.payOrder.id):
-            logger.info(
-                'retry refund paras, orderNo = {} refundOrderNo = {} refundFee = {} totalFee = {}'.format(
-                    self.paySubOrder.orderNo, self.refundOrder.orderNo, self.refundFee, self.subTotalFee)
-            )
-        else:
-            logger.info(
-                'retry refund paras, mix<orderNo = {}, totalFee={}>, sub<orderNo = {}, totalFee={}> '
-                'refundOrderNo = {} refundFee = {} '.format(
-                    self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee,
-                    self.refundOrder.orderNo, self.refundFee)
-            )
+        logger.info('retry refund paras: {} {}'.format(self.refundOrder.orderNo, self.refund_paras))
 
-        puller = RefundManager().get_poller(payGateway.pay_app_type)
-        puller(self.refundOrder).pull(payGateway, self.payOrder, refund_post_pay)
+        puller = RefundManager().get_poller(self.refundOrder.pay_app_type)
+        done = puller(self.refundOrder).pull(refund_post_pay)
+        if done:
+            return
 
         self.refundOrder.reload()
 
-        if self.refundOrder.is_success or self.refundOrder.is_closed:
-            logger.debug('refund order {} has been finished.'.format(str(self.refundOrder)))
+        if not self.refundOrder.is_fail and not self.refundOrder.is_no_order:
+            logger.debug('refund order {} is not in fail status.'.format(str(self.refundOrder)))
             return
 
-        refund_order_record = self.refundOrder.refund_order_record
-        if not refund_order_record:
-            split_map = proxy.partition_map
-            refund_order_record = self.paySubOrder.new_refund_cash_order(
+        if self.refundOrder.retryCount > 10:
+            matched = self.refundOrder.closed(errorCode = 'TIMEOUT', errorDesc = u'重试次数超限,退款失败')
+            if matched:
+                return refund_post_pay(self.refundOrder, False)
+
+        refund_income_order = self.refundOrder.refund_income_order
+        if not refund_income_order:
+            if 'billSplitOfOwner' in self.paySubOrder.attachParas:  # 老的商户分账模式
+                if "billSplitList" in self.paySubOrder.attachParas:
+                    owner_split = self.paySubOrder.attachParas['billSplitOfOwner']
+                    owner_split['merchantId'] = owner_split.pop('splitBillMerchantEmail')
+                    owner_split['money'] = owner_split.pop('splitBillAmount')
+
+                    split_map = {
+                        PARTITION_ROLE.OWNER: [owner_split],
+                        PARTITION_ROLE.AGENT: [],
+                        PARTITION_ROLE.PARTNER: []
+                    }
+
+                    for spliter in self.paySubOrder.attachParas['billSplitList']:
+                        spliter['merchantId'] = spliter.pop('splitBillMerchantEmail')
+                        spliter['money'] = spliter.pop('splitBillAmount')
+
+                        if spliter['role'] == PARTITION_ROLE.AGENT:
+                            split_map[PARTITION_ROLE.AGENT].append(spliter)
+                        elif spliter['role'] == PARTITION_ROLE.PARTNER:
+                            split_map[PARTITION_ROLE.PARTNER].append(spliter)
+                        else:
+                            raise UserServerException(u'错误的分账角色')
+                else:
+                    owner_split = self.paySubOrder.attachParas['billSplitOfOwner']
+                    owner_split['merchantId'] = owner_split.pop('splitBillMerchantEmail')
+                    owner_split['money'] = owner_split.pop('splitBillAmount')
+
+                    split_map = {
+                        PARTITION_ROLE.OWNER: [owner_split],
+                        PARTITION_ROLE.AGENT: [],
+                        PARTITION_ROLE.PARTNER: []
+                    }
+            else:
+                split_map = proxy.partition_map
+
+            refund_income_order = self.paySubOrder.issue_refund_income_order(
                 self.refundOrder, split_map)  # type: RechargeRecord
 
-        # 将订单的状态切换为 正在处理中
-        self.refundOrder.processing()
+        succeed = self.refundOrder.retry_processing(
+            changeOrderNo = (not self.refundOrder.is_no_order) and self.refundOrder.pay_app_type in [PayAppType.JD_OPEN])
+        if not succeed:
+            logger.info(
+                'refund ignored. refund orderNo = {} reason = unique check failure.'.format(self.refundOrder.orderNo))
+            return
 
-        # 扣除实体的金额(用户或者实体卡)
-        frozen_callable(self.refundOrder, **kwargs)
+        self.refundOrder.reload()
 
-        try:
-            if payGateway.pay_app_type == PayAppType.ALIPAY:
-                # 支付宝的退款方式
-                # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功
-                # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态
-                try:
-                    result = payGateway.refund_to_user(
-                        out_trade_no = self.outTradeNo, out_refund_no = self.refundOrder.orderNo,
-                        refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费')
-                except AliException as e:
-                    logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo, e))
-                    raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
-
-                if result["code"] != "10000":
-                    self.refundOrder.fail(errorCode = "{}-{}".format(result["code"], result.get("sub_code")),
-                                     errorDesc = "{}-{}".format(result["msg"], result.get("sub_msg")))
-
-                logger.info('ALIPAY Refund request successfully! return = {}'.format(result))
-
-            elif payGateway.pay_app_type == PayAppType.WECHAT:
-                try:
-                    result = payGateway.refund_to_user(
-                        out_trade_no = self.outTradeNo, out_refund_no = self.refundOrder.orderNo,
-                        refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费',
-                        notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK)
-                except WeChatPayException as e:
-                    logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo, e))
-                    self.refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
-                    raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
-
-                logger.info('WECHAT Refund request successfully! return = {}'.format(result))
-            else:
-                self.refundOrder.fail(errorDesc = u"不支持的退款模式")
-                raise UserServerException(u"不支持的退款模式")
+        frozen_callable(self.refundOrder)
 
-        except UserServerException as se:
-            logger.error(se.message)
-            raise se
-        except Exception as ee:
-            # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新
-            logger.exception(ee)
-            raise UserServerException(ee.message)
+        try:
+            self.submit_refund(
+                self.refundOrder, refund_income_order.partition_map, u'现金退款',
+                notify_url or self.refundOrder.notify_url, refund_callable)
+        except Exception:
+            import traceback
+            logger.warning(
+                'Refund request failure! orderNo = {}; e = {}'.format(self.refundOrder, traceback.format_exc()))
 
         finally:
-            # 资金池方式下,直接记录负单.所有对账时间都以系统内时间为准
-            if payGateway.occupant.role == ROLE.agent:
-                from apps.web.report.ledger import Ledger
-                ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_order_record)
-                ledger.execute(journal = False, stats = True, check = False)
+            self.refundOrder.reload()
+
+            if self.refundOrder.is_closed or self.refundOrder.is_success:
+                # 终态已经调用了post_pay, 所以不在做任何处理
+                pass
+
+            elif self.refundOrder.my_payment_gateway.occupant.role == ROLE.agent:
+                # 资金池情况下认为成功, 冻结运营商金额
+                if refund_income_order.result != RechargeRecord.PayResult.SUCCESS:
+                    refund_income_order.result = RechargeRecord.PayResult.SUCCESS
+                    refund_income_order.finishedTime = datetime.datetime.now()
+                    refund_income_order.save()
+
+                if not refund_income_order.is_ledgered:
+                    from apps.web.report.ledger import Ledger
+                    ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_income_order)
+                    ledger.execute(journal = False, stats = True, check = False)
 
         return self.refundOrder
 
 
-def refund_post_pay(refundOrder, finishedTime):
-    # type: (RefundMoneyRecord, datetime)->None
+def refund_post_pay(refundOrder, success):
+    # type: (RefundMoneyRecord, bool)->None
+    try:
+        refund_income_order = refundOrder.refund_income_order # type: RechargeRecord
 
-    refundOrder.user.commit_refund_cash(refundOrder)
+        try:
+            refPay = refund_income_order.extraInfo['refPay']
+            if isinstance(refund_income_order.extraInfo['refPay'], dict):
+                refund_income_order.extraInfo['refPay'] = ObjectId(refPay.pop('objId'))
+                refund_income_order.save()
+        except:
+            pass
+        if success:
+            refund_success_callback(refundOrder, refundOrder.finishedTime, refund_income_order)
+        else:
+            refund_fail_callback(refundOrder, refundOrder.finishedTime, refund_income_order)
+    except Exception:
+        import traceback
+        logger.warning(
+            'Refund callback failure. orderNo = {}; e = {}'.format(refundOrder.orderNo, traceback.format_exc()))
 
-    refund_order_record = refundOrder.refund_order_record  # type: RechargeRecord
 
-    refund_order_record.finishedTime = finishedTime
-    refund_order_record.result = RechargeRecord.PayResult.SUCCESS
-    refund_order_record.save()
+def refund_success_callback(refundOrder, finishedTime, refund_income_order):
+    # type: (RefundMoneyRecord, datetime, RechargeRecord)->None
+
+    rechargeOrder = refundOrder.pay_sub_order
+
+    if rechargeOrder.via in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]:
+        refundOrder.user.commit_refund_cash(refundOrder)
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_CARD:
+        if rechargeOrder.attachParas.get('terminalRecharge', False):
+            pass
+        else:
+            card = Card.objects(id = rechargeOrder.attachParas['cardId']).first()  # type: Card
+            card.clear_frozen_balance(card.freeze_transaction_id('r'), VirtualCoin(0))
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD:
+        userVirtualCard = UserVirtualCard.objects(id = rechargeOrder.attachParas['cardId']).first()
+        userVirtualCard.commit_refund(refundOrder)
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE:
+        pass
+    else:
+        logger.debug('via({}) is not support.'.format(rechargeOrder.via))
+        return
+
+    if not refund_income_order.is_ledgered:  # 记录收益
+        refund_income_order.finishedTime = finishedTime
+        refund_income_order.result = RechargeRecord.PayResult.SUCCESS
+        refund_income_order.save()
 
-    # 记录资金池的变动
-    if not refund_order_record.is_ledgered:
         from apps.web.report.ledger import Ledger
-        ledger = Ledger(DEALER_INCOME_SOURCE.REFUND_CASH, refund_order_record)
+        ledger = Ledger(refund_income_order.via, refund_income_order)
         ledger.execute(stats=True)
+    else:
+        # FIX预扣单状态.不是SUCCESS先改成SUCCESS
+        if refund_income_order.result != RechargeRecord.PayResult.SUCCESS:
+            refund_income_order.finishedTime = refund_income_order.dateTimeAdded
+            refund_income_order.result = RechargeRecord.PayResult.SUCCESS
+            refund_income_order.save()
+
+    for item in rechargeOrder.extraInfo['refRefund']:
+        if 'deductId' in item:
+            if str(item['deductId']) == str(refund_income_order.id):
+                item['status'] = RefundMoneyRecord.Status.SUCCESS
+                item['finishedTime'] = refundOrder.finishedTime
+                rechargeOrder.save()
+                break
+        elif str(item['objId']) == str(refund_income_order.id):
+            item['status'] = RefundMoneyRecord.Status.SUCCESS
+            item['deductId'] = ObjectId(item.pop('objId'))
+            item['finishedTime'] = refundOrder.finishedTime
+            rechargeOrder.save()
+            break
+
+
+def refund_fail_callback(refundOrder, finishedTime, refund_income_order):
+    rechargeOrder = refundOrder.pay_sub_order
+
+    if rechargeOrder.via in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]:
+        user = refundOrder.pay_sub_order.myuser  # type: MyUser
+        user.revoke_refund_cash(refundOrder)
+
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_CARD:
+        if rechargeOrder.attachParas.get('terminalRecharge', False):
+            pass
+        else:
+            card = Card.objects(id = rechargeOrder.attachParas['cardId']).first()
+            if not card:
+                raise UserServerException(u'充值卡不存在')
 
+            card.recover_frozen_balance(transaction_id = card.freeze_transaction_id('r'), fee = refundOrder.deductCoins)
 
-def frozen_refund_for_balance(refundOrder, user = None, minus_total_consume = VirtualCoin(0)):
-    # type: (RefundMoneyRecord, MyUser, VirtualCoin)->bool
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD:
+        userVirtualCard = UserVirtualCard.objects(id = rechargeOrder.attachParas['cardId']).first()
+        if not userVirtualCard:
+            raise UserServerException(u'虚拟卡不存在')
+
+        userVirtualCard.revoke_refund(refundOrder)
+
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE:
+        monthlyPackage = MonthlyPackage.objects(id = rechargeOrder.attachParas['cardId']).first()
+        if not monthlyPackage:
+            raise UserServerException(u'包月套餐不存在')
+
+        monthlyPackage.toggle_disable(isDisable = 0)
+
+    else:
+        raise UserServerException(u'不支持的退款订单类型')
+
+    if refund_income_order.is_ledgered:
+        # 如果已经扣款分账则建立一个退单收益单
+
+        # FIX预扣单状态.不是SUCCESS先改成SUCCESS
+        if refund_income_order.result != RechargeRecord.PayResult.SUCCESS:
+            refund_income_order.finishedTime = refund_income_order.dateTimeAdded
+            refund_income_order.result = RechargeRecord.PayResult.SUCCESS
+            refund_income_order.save()
+
+        revoke_income_order = refund_income_order.issue_refund_revoke_order()
+
+        from apps.web.report.ledger import Ledger
+        ledger = Ledger(DEALER_INCOME_SOURCE.REVOKE_REFUND_CASH, revoke_income_order)
+        ledger.execute(journal = False, stats = True, check = False)
+
+        for item in rechargeOrder.extraInfo['refRefund']:
+            if 'deductId' in item:
+                if str(item['deductId']) == str(refund_income_order.id):
+                    item['status'] = RefundMoneyRecord.Status.CLOSED
+                    item['revokeId'] = revoke_income_order.id
+                    item['finishedTime'] = refundOrder.finishedTime
+                    rechargeOrder.save()
+                    break
+            elif str(item['objId']) == str(refund_income_order.id):
+                item['status'] = RefundMoneyRecord.Status.CLOSED
+                item['deductId'] = ObjectId(item.pop('objId'))
+                item['revokeId'] = revoke_income_order.id
+                item['finishedTime'] = refundOrder.finishedTime
+                rechargeOrder.save()
+                break
 
-    if user:
-        if user.openId != refundOrder.pay_sub_order.openId:
-            raise UserServerException(u"用户参数错误")
-        else:
-            if user.groupId != refundOrder.pay_sub_order.groupId:
-                user = refundOrder.pay_sub_order.user
     else:
-        user = refundOrder.pay_sub_order.user
+        refund_income_order.finishedTime = finishedTime
+        refund_income_order.result = RechargeRecord.PayResult.CANCEL
+        refund_income_order.save()
+
+        for item in rechargeOrder.extraInfo['refRefund']:
+            if 'deductId' in item:
+                if str(item['deductId']) == str(refund_income_order.id):
+                    item['status'] = RefundMoneyRecord.Status.CLOSED
+                    item['finishedTime'] = refundOrder.finishedTime
+                    rechargeOrder.save()
+                    break
+            elif str(item['objId']) == str(refund_income_order.id):
+                item['status'] = RefundMoneyRecord.Status.CLOSED
+                item['deductId'] = item.pop('objId')
+                item['finishedTime'] = refundOrder.finishedTime
+                rechargeOrder.save()
+                break
+
+
+def frozen_refund_func(refundOrder):
+    # type: (RefundMoneyRecord)->None
+
+    """
+    :param refundOrder:
+    :return:
+    """
+
+    rechargeOrder = refundOrder.pay_sub_order
+
+    if rechargeOrder.via in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]:
+        user = refundOrder.pay_sub_order.myuser
+
+        if not user:
+            raise UserServerException(u'用户不存在')
+
+        minus_total_consume = VirtualCoin(refundOrder.extraInfo.get('minus_total_consume', 0))
 
-    if not user:
-        raise UserServerException(u'用户不存在')
+        deduct_coins = refundOrder.deductCoins
+        frozen_coins = refundOrder.frozenCoins
 
-    return user.prepare_refund_cash(refundOrder, minus_total_consume)
+        logger.debug('MyUser<id={}> prepare refund cash. money = {}, coins = {}, before = {}'.format(
+            str(user.id), refundOrder.money, deduct_coins, user.balance
+        ))
+
+        user.prepare_refund_cash(refundOrder, deduct_coins, frozen_coins, minus_total_consume)
+
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_CARD:
+        if rechargeOrder.attachParas.get('terminalRecharge', False):
+            logger.debug('Card<id={}> prepare refund cash. money = {}'.format(
+                'terminalRecharge', refundOrder.money
+            ))
+        else:
+            card = Card.objects(id = rechargeOrder.attachParas['cardId']).first()
+            if not card:
+                raise UserServerException(u'充值卡不存在')
+
+            logger.debug('Card<id={}> prepare refund cash. money = {}, coins = {}, before = {}'.format(
+                str(card.id), refundOrder.money, refundOrder.deductCoins, card.balance
+            ))
+
+            card.freeze_balance(transaction_id = card.freeze_transaction_id('r'), fee = refundOrder.deductCoins)
+
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD:
+        userVirtualCard = UserVirtualCard.objects(id=rechargeOrder.attachParas['cardId']).first()
+        if not userVirtualCard:
+            raise UserServerException(u'虚拟卡不存在')
+
+        userVirtualCard.prepare_refund(refundOrder)
+
+    elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE:
+        monthlyPackage = MonthlyPackage.objects(id = rechargeOrder.attachParas['cardId']).first()
+        if not monthlyPackage:
+            raise UserServerException(u'包月套餐不存在')
+
+        monthlyPackage.toggle_disable(isDisable = 1)
+
+    else:
+        logger.debug('via({}) is not support.'.format(rechargeOrder.via))

+ 1 - 3
apps/web/user/utils.py

@@ -69,10 +69,8 @@ if TYPE_CHECKING:
 
     from django.http.response import HttpResponse, HttpResponseRedirect
 
-    from apps.web.core.payment import PaymentGateway, WechatMiniPaymentGatewayT
     from django.core.handlers.wsgi import WSGIRequest
     from apps.web.core import PayAppBase
-    from apps.web.dealer.models import OnSale
 
 
 def get_homepage_response(platform_type, user, dev, chargeIndex, product_agent):
@@ -835,7 +833,7 @@ def clear_frozen_user_balance(device, order, usedTime, spendElec, backCoins, use
             user = MyUser.objects(openId = order.openId, groupId = order.groupId).first()  # type: MyUser
 
         if user:
-            user.clear_frozen_balance(str(order.id), order.paymentInfo['deduct'], backCoins, order.coin)
+            user.clear_frozen_balance(str(order.id), order.paymentInfo['deduct'], backCoins)
 
     elif order.paymentInfo['via'] == 'virtualCard':
         if not virtual_card:

+ 6 - 6
apps/web/user/views.py

@@ -37,7 +37,8 @@ from apps.common.utils import coordinateHandler
 from apps.web.ad.models import AdRecord, DevRegToAli
 from apps.web.agent.models import Agent, MoniApp
 from apps.web.common.proxy import ClientRechargeModelProxy, ClientConsumeModelProxy
-from apps.web.common.transaction.pay import OrderCacheMgr, PayManager, RefundManager
+from apps.web.common.transaction.pay import OrderCacheMgr, PayManager
+from apps.web.common.transaction.refund import RefundManager
 from apps.web.common.utils import UserConsumeFilter
 from apps.web.common.validation import check_phone_number, check_entity_name
 from apps.web.constant import Const, GPS_TYPE, START_DEVICE_STATUS, ErrorCode, RECHARGE_CARD_TYPE, APP_TYPE, \
@@ -4417,7 +4418,7 @@ def getOrderStatus(request):
         order_id = str(payload.get('orderId'))
         if OrderCacheMgr(order_id, cls_name = RechargeRecord.__name__).has_done():
             record = RechargeRecord.objects(id = str(payload.get('orderId'))).first()  # type: RechargeRecord
-            if record.is_success():
+            if record.is_success:
                 return JsonResponse({'result': 1, 'description': '', 'payload': {'status': 'success'}})
             else:
                 return JsonResponse({'result': 1, 'description': '', 'payload': {'status': 'fail'}})
@@ -5164,8 +5165,7 @@ def payGateway(request):
         def create_by_wechat(cls, payment_gateway, payParam, record):
             current_user = payParam.curUser
             open_id = current_user.get_bound_pay_openid(payment_gateway.bound_openid_key)
-            from pprint import pprint 
-            pprint(open_id)
+
             pull_up_cls = PayManager().get_pull_up(payment_gateway.pay_app_type)
 
             return pull_up_cls(open_id, payment_gateway, record, **{
@@ -5259,7 +5259,7 @@ def payNotify(request, pay_app_type):
         payload = request.POST.dict()
         if 'refund_fee' in payload and 'gmt_refund' in payload:
             notifier_cls = RefundManager().get_notifier(pay_app_type)
-            return notifier_cls(request, lambda order_no: RefundMoneyRecord.get_record(order_no)).do(refund_post_pay)
+            return notifier_cls(request, lambda filter: RefundMoneyRecord.get_record(**filter)).do(refund_post_pay)
 
     recharge_cls_factory = lambda order_no: RechargeRecord
     notifier_cls = PayManager().get_notifier(pay_app_type = pay_app_type)
@@ -6352,7 +6352,7 @@ def refundOrderNotifier(request, pay_app_type):
     assert pay_app_type in PayAppType.choices(), 'not support this pay app type({})'.format(pay_app_type)
 
     notifier_cls = RefundManager().get_notifier(pay_app_type)
-    response = notifier_cls(request, lambda order_no: RefundMoneyRecord.get_record(order_no)).do(refund_post_pay)
+    response = notifier_cls(request, lambda filter: RefundMoneyRecord.get_record(**filter)).do(refund_post_pay)
     return response
 
 

+ 0 - 5
apps/web/utils.py

@@ -707,11 +707,6 @@ def get_start_key_status(start_key):
         }
     return start_key_status
 
-
-def private_file_path(path):
-    return os.path.join(settings.PRIVATE_MEDIA_ROOT, path)
-
-
 def concat_url(base_url, uri, add_version):
     # type:(str, str, bool)->str
 

+ 1 - 1
apps/web/wechat3rd/views.py

@@ -12,7 +12,7 @@ from django.http import HttpResponse
 from django.views.decorators.http import require_POST
 from typing import TYPE_CHECKING
 
-from apps.web.core.bridge.wechat.WechatClientProxy import MyWeChatComponent, MyWeChatComponentClient
+from apps.web.core.bridge.wechat import MyWeChatComponent, MyWeChatComponentClient
 from library.wechatpy.messages import ComponentVerifyTicketMessage, ComponentUnauthorizedMessage, \
     ComponentAuthorizedMessage, ComponentUpdateAuthorizedMessage, ComponentUnknownMessage, TextMessage
 from library.wechatpy.messages import BaseMessage

+ 5 - 1
configs/base.py

@@ -359,6 +359,8 @@ UNIVERSAL_PASSWORD = env('UNIVERSAL_PASSWORD')
 
 # 提现所需最小值
 WITHDRAW_MINIMUM = 10
+AD_WITHDRAW_MINIMUM = 1
+SIM_INCOME_WITHDRAW_MINIMUM = 10
 
 # 提现所需最大值
 WITHDRAW_MAXIMUM = 20000
@@ -695,4 +697,6 @@ ALIPAY_CHANNEL_ID_V3 = '619191980501444609'  # 主体ID
 ALIPAY_MEDIA_ID_V3 = '707566730122012672'  # 媒体ID
 ALIPAY_IMP_CPM_V3 = '755796924816712705'
 ALIPAY_IMP_CPA_RUHUI_V3 = '773604407794823173'
-ALIPAY_IMP_CPA_LAXIN_V3 = '773837769738319876'
+ALIPAY_IMP_CPA_LAXIN_V3 = '773837769738319876'
+
+CELERY_PUBLISH_METHOD = os.environ.get('CELERY_PUBLISH_METHOD', 'gevent')

+ 1 - 1
configs/servers.py

@@ -7,7 +7,7 @@ PUBLIC_MAP_PRIVATE = {
     "121.40.97.20": "172.16.69.136",
     "121.43.232.118": "172.16.145.63",
     "121.41.75.233": "172.16.69.137",
-    "120.26.227.50": "10.117.57.12",
+    "120.26.227.50": "172.16.69.138",
     "47.98.45.35": "172.16.145.61",
     "114.55.107.231": "172.16.247.184",
     "47.110.242.34": "172.16.69.131",

Diff do ficheiro suprimidas por serem muito extensas
+ 535 - 0
library/RuralCreditUnion/pay.py


+ 2 - 3
library/alipay/__init__.py

@@ -693,10 +693,9 @@ class BaseAliPay(object):
             raw_string, "alipay_system_oauth_token_response"
         )
 
-    def api_alipay_trade_refund_order_query(self, trade_no, out_trade_no, out_request_no, query_options=None):
-        # type:(str, str, str, dict) -> dict
+    def api_alipay_trade_refund_order_query(self, out_trade_no, out_request_no, query_options=None):
+        # type:(str, str, dict) -> dict
         biz_content = {
-            "trade_no":trade_no,
             "out_trade_no": out_trade_no,
             "out_request_no": out_request_no,
         }

+ 0 - 1
library/jd/__init__.py

@@ -2,6 +2,5 @@
 # !/usr/bin/env python
 
 from .base import JDErrorCode
-from .exceptions import JDException, JDAuthException, JDValidationError
 from .pay import JDAggrePay
 from .oauth import JDOAuth

+ 1 - 44
library/jd/exceptions.py

@@ -1,52 +1,9 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
-from typing import Optional
 
-from library.jd import JDErrorCode
-
-
-class JDException(Exception):
-    def __init__(self, errCode, errMsg, client = None, request = None, response = None):
-        self.errCode = errCode
-        self.errMsg = errMsg
-        self.client = client
-        self.request = request
-        self.response = response
-        super(JDException, self).__init__(errMsg)
-
-    def __str__(self):
-        if self.client:
-            return '{kclass}(client: {client}, errCode: {errCode}, errMsg: {errMsg})'.format(
-                kclass = self.__class__.__name__,
-                client = repr(self.client),
-                errCode = self.errCode,
-                errMsg = self.errMsg)
-        else:
-            return '{kclass}(errCode: {errCode}, errMsg: {errMsg})'.format(
-                kclass = self.__class__.__name__,
-                errCode = self.errCode,
-                errMsg = self.errMsg)
-
-    def __repr__(self):
-        return str(self)
+from library.jdbase.exceptions import JDException
 
 
 class JDAuthException(JDException):
     pass
 
-
-class JDPayException(JDException):
-    pass
-
-
-class JDCommuException(JDException):
-    pass
-
-
-class JDValidationError(JDException):
-    def __init__(self, tips, lvalue, rvalue, client = None):
-        # type: (basestring, basestring, basestring, Optional[object])->None
-        super(JDValidationError, self).__init__(
-            errCode = str(JDErrorCode.MY_VALID_ERROR),
-            errMsg = '{}({} != {})'.format(tips, lvalue, rvalue),
-            client = client)

+ 89 - 11
library/jd/pay.py

@@ -5,16 +5,14 @@
 from __future__ import unicode_literals
 
 import base64
-import hashlib
-
 import datetime
+import hashlib
 import logging
 from binascii import hexlify, unhexlify
 from collections import OrderedDict
 
 import requests
 import simplejson as json
-
 import six
 from Crypto.Cipher import DES3
 from typing import Union, Dict, Optional
@@ -23,7 +21,7 @@ from apilib.systypes import IterConstant
 from apilib.utils_url import add_query
 from library import to_binary, to_text
 from library.jd import JDErrorCode
-from library.jd.exceptions import JDPayException, JDValidationError, JDCommuException
+from library.jdbase.exceptions import JDNetworkException, JDException, JDSignError, JDValidationError, JDParameterError
 
 logger = logging.getLogger(__name__)
 
@@ -175,7 +173,7 @@ class JDAggrePay(object):
     def decrypt_response(self, payload):
         # 接口调用失败, SUCCESS/FAIL
         if not payload['success'] or ('errCode' in payload and payload['errCode'] != '000000'):
-            raise JDCommuException(
+            raise JDException(
                 errCode = payload['errCode'] if 'errCode' in payload else -1,
                 errMsg = payload['errCodeDes'] if 'errCodeDes' in payload else u'未知错误',
                 client = self,
@@ -186,7 +184,7 @@ class JDAggrePay(object):
         decrypt_data = json.loads(self.decrypt(payload['cipherJson']))
 
         if sign != self.sign(decrypt_data, ['sign']):
-            raise JDPayException(
+            raise JDSignError(
                 errCode = JDErrorCode.MY_ERROR_SIGNATURE,
                 errMsg = u'签名不一致',
                 client = self)
@@ -214,7 +212,7 @@ class JDAggrePay(object):
         logger.debug('Response from JDAggre decrypt payload: %s', decrypt_data)
 
         if decrypt_data['resultCode'] != 'SUCCESS':
-            raise JDPayException(
+            raise JDException(
                 errCode = decrypt_data['errCode'],
                 errMsg = decrypt_data['errCodeDes'],
                 client = self)
@@ -253,7 +251,7 @@ class JDAggrePay(object):
             try:
                 res.raise_for_status()
             except requests.RequestException as reqe:
-                raise JDPayException(
+                raise JDNetworkException(
                     errCode = 'HTTP{}'.format(res.status_code),
                     errMsg = reqe.message,
                     client = self,
@@ -336,11 +334,13 @@ class JDAggrePay(object):
                       expire = 300, returnParams = None, client_ip = '127.0.0.1', openId = None,
                       gatewayMethod = GatewayMethod.SUBSCRIPTION, **kwargs):
         # type: (str, str, str, str, basestring, int, Optional[Dict], str, Optional[str], str, Dict)->Union[str, Dict]
+
         """
         统一下单
         """
+
         if piType in [PiType.ALIPAY, PiType.WX] and not openId:
-            raise JDPayException(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'缺少openid参数')
+            raise JDParameterError(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'缺少openid参数')
 
         params = {
             'systemId': self.systemId,
@@ -399,7 +399,7 @@ class JDAggrePay(object):
         assert callback_url is not None
 
         if piType not in [PiType.ALIPAY, PiType.WX]:
-            raise JDPayException(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'必须是支付宝或者微信')
+            raise JDParameterError(errCode = JDErrorCode.MY_INVALID_PARAMETER, errMsg = u'必须是支付宝或者微信')
 
         if payload:
             callback_url = add_query(callback_url, {'payload': payload})
@@ -422,7 +422,6 @@ class JDAggrePay(object):
                                              sign = sign,
                                              systemId = self.systemId)
 
-
     def api_trade_query(self, out_trade_no = None, trade_no = None):
         assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time'
 
@@ -503,3 +502,82 @@ class JDAggrePay(object):
 
         result = self.post(endpoint = '/m/queryrefund', data = data)
         return result
+
+
+class JDJosPay(JDAggrePay):
+    """
+    JD JOS 支付的相关接口 加解密方式和JDAGGRE一直 但是部分字段实现不一致
+    """
+    def api_trade_query(self, out_trade_no=None, trade_no=None):
+        shopInfo = getattr(self, "shopInfo", None)
+        assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time'
+        assert shopInfo, "shop info must be confided to josPayApp"
+
+        businessData = OrderedDict([
+            ("brandId", shopInfo.brandId),
+            ("brandName", shopInfo.brandName),
+            ("tradeName", shopInfo.tradeName),
+            ("bizId", shopInfo.bizId)
+        ])
+
+        params = {
+            'merchantNo': str(self.merchant_no),
+            'businessCode': 'MEMBER',
+            'shopId': shopInfo.exStoreId,
+            'version': 'V3.0',
+            'businessData': json.dumps(businessData, separators=(",", ":")),
+            'outTradeNo': str(out_trade_no),
+        }
+
+        if trade_no:
+            params.update({'trandNo': trade_no})
+
+        sign = self.sign(params, ['systemId', 'sign'])
+        cipher_json = self.encrypt(params, ['systemId', 'sign'])
+
+        data = {
+            'systemId': self.systemId,
+            'merchantNo': self.merchant_no,
+            'cipherJson': cipher_json,
+            'sign': sign
+        }
+
+        result = self.post(endpoint='/m/querytrade', data=data)
+        return result
+
+
+    def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs):
+
+        shopInfo = getattr(self, "shopInfo", None)
+        assert outTradeNo and outRefundNo, 'outTradeNo and tradeNo must not be empty'
+        assert shopInfo, "shop info must be confided to josPayApp"
+
+        businessData = OrderedDict([
+            ("brandId", shopInfo.brandId),
+            ("brandName", shopInfo.brandName),
+            ("tradeName", shopInfo.tradeName),
+            ("bizId", shopInfo.bizId)
+        ])
+
+        params = {
+            "merchantNo": str(self.merchant_no),
+            "businessCode": "MEMBER",
+            "version": "V3.0",
+            "outTradeNo": str(outTradeNo),
+            "outRefundNo": str(outRefundNo),
+            "amount": amount,
+            "operId": kwargs.get("operId") or "SYSTEM_AUTO",
+            "shopId": shopInfo.exStoreId,
+            'businessData': json.dumps(businessData, separators=(",", ":")),
+        }
+        sign = self.sign(params, ['systemId', "sign"])
+        cipher_json = self.encrypt(params, ["systemId", "sign"])
+
+        data = {
+            'systemId': self.systemId,
+            'merchantNo': self.merchant_no,
+            'cipherJson': cipher_json,
+            'sign': sign
+        }
+        result = self.post(endpoint='/m/refund', data=data)
+        return result

+ 1 - 2
library/jdopen/__init__.py

@@ -2,5 +2,4 @@
 # !/usr/bin/env python
 
 
-from .base import JDOpenErrorCode, BankType
-from .exceptions import JdOpenException
+from .base import JDOpenErrorCode, BankType, SettleWay, WithdrawMode

+ 18 - 2
library/jdopen/base.py

@@ -3,7 +3,6 @@
 
 
 from enum import unique, IntEnum
-from apilib.systypes import IterConstant
 
 
 @unique
@@ -15,8 +14,25 @@ class JDOpenErrorCode(IntEnum):
     MY_INVALID_PARAMETER = -103
 
 
-class BankType(IterConstant):
+class BankType(object):
     WX = 'WX'
     ALIPAY = 'ALIPAY'
     JDPAY = 'JDPAY'
 
+
+class SettleWay(object):
+    MANUAL = 'MANUAL'
+    AUTOMATIC = 'AUTOMATIC'
+
+    @classmethod
+    def choices(cls):
+        return [cls.MANUAL, cls.AUTOMATIC]
+
+
+class WithdrawMode(object):
+    D0 = 'D0'
+    D1 = 'D1'
+
+    @classmethod
+    def choices(cls):
+        return [cls.D0, cls.D1]

+ 2 - 1
library/jdopen/client/__init__.py

@@ -7,10 +7,11 @@ from library.jdopen.client.base import JdOpenBaseClient
 
 class JdOpenMerchantClient(JdOpenBaseClient):
     API_BASE_URL = "https://openapi.duolabao.com/"
+    # API_BASE_URL = 'https://openapi-uat.duolabao.com/'
 
     support = api.JdOpenSupport         # type: api.JdOpenSupport
     customer = api.JdOpenCustomer       # type: api.JdOpenCustomer
-    settle = api.JdOpenSettleAccount    # type: api.JdOpenSettleAccount
+    settle = api.JdOpenSettle           # type: api.JdOpenSettle
     shop = api.JdOpenShop               # type: api.JdOpenShop
     attach = api.JdOpenAttach           # type: api.JdOpenAttach
     complete = api.JdOpenComplete       # type: api.JdOpenComplete

+ 1 - 1
library/jdopen/client/api/__init__.py

@@ -3,7 +3,7 @@
 
 from library.jdopen.client.api.support import JdOpenSupport
 from library.jdopen.client.api.customer import JdOpenCustomer
-from library.jdopen.client.api.settleAccount import JdOpenSettleAccount
+from library.jdopen.client.api.settle import JdOpenSettle
 from library.jdopen.client.api.shop import JdOpenShop
 from library.jdopen.client.api.attach import JdOpenAttach
 from library.jdopen.client.api.complete import JdOpenComplete

+ 1 - 1
library/jdopen/client/api/attach.py

@@ -20,7 +20,7 @@ class JdOpenAttach(BaseJdOpenAPI):
 
         return self._post(url, data=data)
 
-    def modify_attach(self, attachNum, customerNum, attachType, content):
+    def modify_attach(self, customerNum, attachNum, attachType, content):
         """
         修改附件
         """

+ 2 - 2
library/jdopen/client/api/audit.py

@@ -7,10 +7,10 @@ from library.jdopen.client.api.base import BaseJdOpenAPI
 
 class JdOpenAudit(BaseJdOpenAPI):
 
-    def get_audit_result(self, agentNum, customerNum):
+    def get_audit_result(self, customerNum):
         url = "/v1/agent/declare/list"
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum
         }
 

+ 3 - 3
library/jdopen/client/api/complete.py

@@ -19,13 +19,13 @@ class JdOpenComplete(BaseJdOpenAPI):
             "callbackUrl": callbackUrl or defaultCallBackUrl
         }
 
-        sendData = {_k: _v for _k, _v in data.items() if _v}
+        sendData = {_k: _v for _k, _v in data.items() if _v is not None}
         return self._post(url, data=sendData)
 
-    def confirm_customer(self, agentNum, customerNum):
+    def confirm_customer(self, customerNum):
         url = "/v1/agent/declare/sign/confirm"
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum
         }
 

+ 6 - 6
library/jdopen/client/api/customer.py

@@ -7,7 +7,7 @@ from library.jdopen.client.api.base import BaseJdOpenAPI
 class JdOpenCustomer(BaseJdOpenAPI):
 
     def create_customer(
-            self, agentNum, fullName, shortName, industry, province, city, district, linkMan, linkPhone,
+            self, fullName, shortName, industry, province, city, district, linkMan, linkPhone,
             customerType, certificateType, certificateCode, certificateName, certificateStartDate, contactPhoneNum, linkManId,
             email=None, organizationCode=None, accountOpenLicense=None, certificateEndDate=None, postalAddress=None, certType=None, certNum=None
     ):
@@ -16,7 +16,7 @@ class JdOpenCustomer(BaseJdOpenAPI):
         """
         url = "/v2/agent/declare/customerinfo/create"
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "fullName": fullName,
             "shortName": shortName,
             "industry": industry,
@@ -41,11 +41,11 @@ class JdOpenCustomer(BaseJdOpenAPI):
             "email": email
         }
 
-        sendData = {_k: str(_v) for _k, _v in data.items() if _v}
+        sendData = {_k: str(_v) for _k, _v in data.items() if _v is not None}
         return self._post(url=url, data=sendData)
 
     def modify_customer(
-            self, agentNum, customerNum, fullName, shortName, industry, province, city, district, linkMan, linkPhone,
+            self, customerNum, fullName, shortName, industry, province, city, district, linkMan, linkPhone,
             customerType, certificateType, certificateCode, certificateName, certificateStartDate, contactPhoneNum, linkManId,
             email=None, organizationCode=None, accountOpenLicense=None, certificateEndDate=None, postalAddress=None, certType=None, certNum=None
     ):
@@ -54,7 +54,7 @@ class JdOpenCustomer(BaseJdOpenAPI):
         """
         url = "/v2/agent/declare/customerinfo/modify"
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum,
             "fullName": fullName,
             "shortName": shortName,
@@ -80,7 +80,7 @@ class JdOpenCustomer(BaseJdOpenAPI):
             "email": email
         }
 
-        sendData = {_k: str(_v) for _k, _v in data.items() if _v}
+        sendData = {_k: str(_v) for _k, _v in data.items() if _v is not None}
         return self._post(url=url, data=sendData)
 
     def get_customer(self, customer):

+ 0 - 72
library/jdopen/client/api/settleAccount.py

@@ -1,72 +0,0 @@
-# -*- coding: utf-8 -*-
-# !/usr/bin/env python
-
-from library.jdopen.client.api.base import BaseJdOpenAPI
-
-
-class JdOpenSettleAccount(BaseJdOpenAPI):
-
-    def create_account(
-            self, customerNum, bankAccountName, bankAccountNum, province, city, bankName, bankBranchName,
-            settleAmount, payBankList, accountType, phone,
-            privateType=None, settlerCertificateCode=None, settlerCertificateStartDate=None, settlerCertificateEndDate=None
-    ):
-        """
-        创建结算账户
-        """
-        url = "/v2/agent/declare/settleinfo/create"
-        data = {
-            "customerNum": customerNum,
-            "bankAccountName": bankAccountName,
-            "bankAccountNum": bankAccountNum,
-            "province": province,
-            "city": city,
-            "bankName": bankName,
-            "bankBranchName": bankBranchName,
-            "settleAmount": settleAmount,
-            "accountType": accountType,
-            "phone": phone,
-            "privateType": privateType,
-            "settlerCertificateCode": settlerCertificateCode,
-            "settlerCertificateStartDate": settlerCertificateStartDate,
-            "settlerCertificateEndDate": settlerCertificateEndDate
-        }
-        sendData = {_k: str(_v) for _k, _v in data.items() if _v}
-        sendData["payBankList"] = payBankList
-        return self._post(url, data=sendData)
-
-    def modify_account(
-            self, settleNum, customerNum, bankAccountName, bankAccountNum, province, city, bankName, bankBranchName,
-            settleAmount, payBankList, accountType, phone,
-            privateType=None, settlerCertificateCode=None, settlerCertificateStartDate=None, settlerCertificateEndDate=None
-    ):
-        """
-        修改结算账户
-        """
-        url = "/v2/agent/declare/settleinfo/modify"
-        data = {
-            "settleNum": settleNum,
-            "customerNum": customerNum,
-            "bankAccountName": bankAccountName,
-            "bankAccountNum": bankAccountNum,
-            "province": province,
-            "city": city,
-            "bankName": bankName,
-            "bankBranchName": bankBranchName,
-            "settleAmount": settleAmount,
-            "accountType": accountType,
-            "phone": phone,
-            "privateType": privateType,
-            "settlerCertificateCode": settlerCertificateCode,
-            "settlerCertificateStartDate": settlerCertificateStartDate,
-            "settlerCertificateEndDate": settlerCertificateEndDate
-        }
-        sendData = {_k: str(_v) for _k, _v in data.items() if _v}
-        sendData["payBankList"] = payBankList
-        return self._post(url, data=sendData)
-
-    def get_account(self, settleNum):
-        """
-        获取结算账户信息
-        """
-        return self._post("/v1/agent/declare/settleinfo/{}".format(settleNum))

+ 7 - 7
library/jdopen/client/api/shop.py

@@ -7,7 +7,7 @@ from library.jdopen.client.api.base import BaseJdOpenAPI
 class JdOpenShop(BaseJdOpenAPI):
 
     def create_shop(
-            self, agentNum, customerNum, shopName, address, oneIndustry, twoIndustry,
+            self, customerNum, shopName, address, oneIndustry, twoIndustry,
             mobilePhone,  mapLng=None, mapLat=None, microBizType=None
     ):
         """
@@ -15,7 +15,7 @@ class JdOpenShop(BaseJdOpenAPI):
         """
         url = "/v1/agent/declare/shopinfo/create"
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum,
             "shopName": shopName,
             "address": address,
@@ -27,12 +27,12 @@ class JdOpenShop(BaseJdOpenAPI):
             "microBizType": microBizType
         }
 
-        sendData = {_k: _v for _k, _v in data.items() if _v}
+        sendData = {_k: _v for _k, _v in data.items() if _v is not None}
         return self._post(url=url, data=sendData)
 
     def modify_shop(
-            self, shopNum, agentNum, customerNum, shopName, address, oneIndustry, twoIndustry,
-            mobilePhone,  mapLng, mapLat, microBizType
+            self, shopNum, customerNum, shopName, address, oneIndustry, twoIndustry,
+            mobilePhone,  mapLng = None, mapLat = None, microBizType = None
     ):
         """
         修改店铺
@@ -40,7 +40,7 @@ class JdOpenShop(BaseJdOpenAPI):
         url = "/v1/agent/declare/shopinfo/modify"
         data = {
             "shopNum": shopNum,
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum,
             "shopName": shopName,
             "address": address,
@@ -52,7 +52,7 @@ class JdOpenShop(BaseJdOpenAPI):
             "microBizType": microBizType
         }
 
-        sendData = {_k: _v for _k, _v in data.items() if _v}
+        sendData = {_k: _v for _k, _v in data.items() if _v is not None}
         return self._post(url=url, data=sendData)
 
     def get_shop(self, customerNum):

+ 120 - 8
library/jdopen/client/api/support.py

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 
+from library.jdbase.exceptions import JDException
 from library.jdopen.client.api.base import BaseJdOpenAPI
 
 
@@ -16,13 +17,13 @@ class JdOpenSupport(BaseJdOpenAPI):
         """
         查询市
         """
-        return self._get("/v1/agent/city/list/code/{code}".format(code=code))
+        return self._get("/v1/agent/city/list/code/{code}".format(code = code))
 
     def query_district(self, code):
         """
         查询区
         """
-        return self._get("/v1/agent/district/list/code/{code}".format(code=code))
+        return self._get("/v1/agent/district/list/code/{code}".format(code = code))
 
     def query_industry(self):
         """
@@ -34,19 +35,20 @@ class JdOpenSupport(BaseJdOpenAPI):
         """
         查询二级行业
         """
-        return self._get("/v1/agent/industry/second/list/code/{num}".format(num=num))
+        return self._get("/v1/agent/industry/second/list/code/{num}".format(num = num))
 
-    def query_bank(self, k=None):
+    def query_bank(self, k = None):
         """
         查询银行
         """
-        return self._get("/v1/agent/bank/list/{k}".format(k=k or u"银行"))
+        return self._get("/v1/agent/bank/list/{k}".format(k = k or u"银行"))
 
-    def query_sub_bank(self, bankCode, subK=None):
+    def query_sub_bank(self, bankCode, subK = None):
         """
         查询支行
         """
-        return self._get("/v1/agent/bankSub/list/{bankCode}/{subK}".format(bankCode=bankCode or u"", subK=subK or u"行"))
+        return self._get(
+            "/v1/agent/bankSub/list/{bankCode}/{subK}".format(bankCode = bankCode or u"", subK = subK or u"行"))
 
     def query_pay_type(self):
         return self._get("/v1/agent/pay/bankinfo/list")
@@ -58,4 +60,114 @@ class JdOpenSupport(BaseJdOpenAPI):
             "payProduct": payProduct
         }
 
-        return self._post(url=url, data=data)
+        return self._post(url = url, data = data)
+
+    def query_sub_channel(self, customerNum, payProduct):
+        payProduct = payProduct if payProduct else "WX"
+
+        result = self.get_product_customer(customerNum, payProduct)
+
+        if not result or 'data' not in result:
+            return None
+
+        for item in result["data"]:
+            if item["status"] == "OPEN":
+                return item["subCustomerNum"]
+
+        return None
+
+    def query_auth_status(self, customerNum, payProduct, subMerchantId):
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
+        url = "/api/queryAuthStatus"
+        data = {
+            'agentNum': self.agentNum,
+            'customerNum': customerNum,
+            'bankType': payProduct,
+            'subCustomerNum': subMerchantId
+        }
+
+        sendData = {_k: str(_v) for _k, _v in data.items() if _v is not None}
+
+        return self._post(url = url, data = sendData, processor = processor)
+
+    def submit_auth(self, customerNum, payProduct, subMerchantId):
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
+        url = "/api/wxCreateAuthorizeInfo"
+        data = {
+            'agentNum': self.agentNum,
+            'customerNum': customerNum,
+            'bankType': payProduct,
+            'subCustomerNum': subMerchantId
+        }
+
+        sendData = {_k: str(_v) for _k, _v in data.items() if _v is not None}
+
+        return self._post(url = url, data = sendData, processor = processor)
+
+    def add_wechat_auth_pay_dir(self, customerNum, auth_pay_dir):
+        """
+        追加商户微信支付目录
+        :param customerNum:
+        :param auth_pay_dir:
+        :return:
+        """
+
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
+        url = '/api/addAuthPayDirsDevConfig'
+
+        data = {
+            'customerNum': customerNum,
+            'authPayDir': auth_pay_dir
+        }
+
+        return self._post(url = url, data = data, processor = processor)
+
+    def query_wechat_auth_pay_dir(self, customerNum, batchNum):
+        """
+        查询商户微信支付目录
+        :param customerNum:
+        :param auth_pay_dir:
+        :return:
+        """
+
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
+        url = '/api/queryAddAuthPayDirsDevConfigByBatchNum'
+
+        data = {
+            'customerNum': customerNum,
+            'batchNum': batchNum
+        }
+
+        return self._post(url = url, data = data, processor = processor)

+ 27 - 25
library/jdopen/client/base.py

@@ -13,8 +13,7 @@ from simplejson import JSONDecodeError
 
 from library.jdopen.client.api.base import BaseJdOpenAPI
 from library.jdopen.constants import JdOpenResultCode
-from library.jdopen.exceptions import JdOpenException
-
+from library.jdbase.exceptions import JDException, JDNetworkException
 
 if typing.TYPE_CHECKING:
     from requests import Response
@@ -36,12 +35,17 @@ class JdOpenBaseClient(object):
 
         return self
 
-    def __init__(self, accessKey, secretKey, timeout=None, auto_retry=True):
+    def __init__(self, agentNum, accessKey, secretKey, timeout=None, auto_retry=True):
+        self._agentNum = agentNum
         self._accessKey = accessKey
         self._secretKey = secretKey
         self._timeout = timeout
         self._auto_retry = auto_retry
 
+    @property
+    def agentNum(self):
+        return self._agentNum
+
     @staticmethod
     def _decode_result(res):    # type:(Response) -> typing.Optional[dict, str]
         """
@@ -94,7 +98,7 @@ class JdOpenBaseClient(object):
         }
         kwargs.setdefault("params", {})
         kwargs.setdefault("timeout", self._timeout)
-        callback = kwargs.pop("callback", None)
+        processor = kwargs.pop("processor", None)
 
         with requests.sessions.Session() as _session:
             res = _session.request(
@@ -107,7 +111,7 @@ class JdOpenBaseClient(object):
                 res.raise_for_status()
             except requests.RequestException as rre:
                 logger.info("[{} send request] error! status code = {}, error = {}".format(self.__class__.__name__, res.status_code, rre))
-                raise JdOpenException(
+                raise JDNetworkException(
                     errCode='HTTP{}'.format(res.status_code),
                     errMsg=rre.message,
                     client=self,
@@ -116,34 +120,32 @@ class JdOpenBaseClient(object):
                 )
 
         return self._handle_result(
-            res, method, url, callback, **kwargs
+            res, method, url, processor, **kwargs
         )
 
-    def _handle_result(self, res, method, url, callback, **kwargs):
-        """
-        主要用户重试
-        """
+    def _handle_result(self, res, method, url, processor, **kwargs):
         result = self._decode_result(res)
 
-        logger.info("[{} handle_result] result = {}".format(self.__class__.__name__, result))
+        logger.info("[{} handle_result] method = {}; url = {}; result = {}".format(
+            self.__class__.__name__, method, url, result))
 
-        if "result" not in result:
-            return result
+        if processor:
+            return processor(self, result)
+        else:
+            if "result" not in result:
+                return result
 
-        # 正常请求
-        if result["result"] == JdOpenResultCode.SUCCESS:
-            return callback(result) if callback else result
+            if result["result"] == JdOpenResultCode.SUCCESS:
+                return processor(result) if processor else result
 
-        # 异常请求
-        error = result["error"]
+            error = result["error"]
 
-        raise JdOpenException(
-            errCode=error.get("errorCode", ''),
-            errMsg=error.get("errorMsg", ''),
-            client=self,
-            request=res.request,
-            response=res
-        )
+            raise JDException(
+                errCode = error.get("errorCode", ''),
+                errMsg = error.get("errorMsg", ''),
+                client = self,
+                request = res.request,
+                response = res)
 
     def get(self, url, **kwargs):
         return self._request("get", url, **kwargs)

+ 1 - 10
library/jdopen/constants.py

@@ -1,16 +1,7 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 
-from enum import unique, IntEnum, Enum
-
-
-OPEN_ACCESS_KEY = "9ec189c179bf4bbfa406922288a3352c9dee3b9a"
-OPEN_SECRET_KEY = "355bc56464d146c0acd827de1af595efc72a2c36"
-
-
-@unique
-class JdOpenErrorCode(IntEnum):
-    pass
+from enum import unique, Enum
 
 
 @unique

+ 9 - 33
library/jdopen/exceptions.py

@@ -3,43 +3,19 @@
 
 from typing import Optional
 
+from library.jdbase.exceptions import JDException
 from library.jdopen import JDOpenErrorCode
 
 
-class JdOpenException(Exception):
-    def __init__(self, errCode, errMsg, client = None,
-                 request = None, response = None):
-        super(JdOpenException, self).__init__(errMsg)
-
-        self.errCode = errCode
-        self.errMsg = errMsg
-        self.client = client
-        self.request = request
-        self.response = response
+class JDOpenValidationError(JDException):
+    def __init__(self, tips, lvalue, rvalue, client = None):
+        # type: (basestring, basestring, basestring, Optional[object])->None
 
-    def __str__(self):
-        if self.client:
-            _repr = '{klass}(client: {client}, errCode: {errCode}, errMsg: {errMsg})'.format(
-                klass = self.__class__.__name__,
-                client = repr(self.client),
-                errCode = self.errCode,
-                errMsg = self.errMsg)
-        else:
-            _repr = '{klass}(errCode: {errCode}, errMsg: {errMsg})'.format(
-                klass = self.__class__.__name__,
-                errCode = self.errCode,
-                errMsg = self.errMsg)
+        super(JDOpenValidationError, self).__init__(
+            errCode = str(JDOpenErrorCode.MY_VALID_ERROR), errMsg = tips, client = client)
 
-        return _repr
+        self.lvalue = lvalue
+        self.rvalue = rvalue
 
     def __repr__(self):
-        return str(self)
-
-
-class JDOpenValidationError(JdOpenException):
-    def __init__(self, tips, lvalue, rvalue, client = None):
-        # type: (basestring, basestring, basestring, Optional[object])->None
-        super(JDOpenValidationError, self).__init__(
-            errCode = str(JDOpenErrorCode.MY_VALID_ERROR),
-            errMsg = '{}({} != {})'.format(tips, lvalue, rvalue),
-            client = client)
+        return '{}({} != {})'.format(self.errMsg, self.lvalue, self.rvalue)

+ 154 - 47
library/jdopen/pay.py

@@ -6,9 +6,11 @@ from __future__ import unicode_literals
 
 import logging
 
+from library import random_string
+
 from apilib.systypes import IterConstant
 from library.jdopen import JDOpenErrorCode
-from library.jdopen.exceptions import JdOpenException
+from library.jdbase.exceptions import JDException, JDRequestException
 from library.jdopen.client import JdOpenBaseClient
 
 logger = logging.getLogger(__name__)
@@ -24,13 +26,12 @@ class SubOrderType(IterConstant):
 class JDOpenPay(JdOpenBaseClient):
     API_BASE_URL = 'https://openapi.duolabao.com'
 
-    def __init__(self, agentNum, accessKey, secretKey, customerNum, shopNum, timeout=None, auto_retry=True):
-        super(JDOpenPay, self).__init__(accessKey, secretKey, timeout, auto_retry)
-        self.agentNum = agentNum
+    def __init__(self, agentNum, accessKey, secretKey, customerNum, shopNum, timeout = None, auto_retry = True):
+        super(JDOpenPay, self).__init__(agentNum, accessKey, secretKey, timeout, auto_retry)
         self.customerNum = customerNum
         self.shopNum = shopNum
 
-    def create_pay_url(self, out_trade_no, total_fee, notify_url, extraInfo=None, **kwargs):
+    def create_pay_url(self, out_trade_no, total_fee, notify_url, extraInfo = None, **kwargs):
         """
         创建主扫链接
         :param out_trade_no:
@@ -60,11 +61,11 @@ class JDOpenPay(JdOpenBaseClient):
 
         data.update(kwargs)
 
-        result = self.post(url=url, data=data)
+        result = self.post(url = url, data = data)
         return result['data']['url']
 
-    def unified_order(self, authId, bankType, requestNum, amount, callbackUrl, subject, ledgerRule=None,
-                      extraInfo=None, **kwargs):
+    def unified_order(self, authId, bankType, requestNum, amount, callbackUrl, subject, ledgerRule = None,
+                      extraInfo = None, **kwargs):
         """
         :param authId:
         :param bankType:
@@ -91,7 +92,17 @@ class JDOpenPay(JdOpenBaseClient):
         }
         :return:
         """
-        url = '/v3/order/pay/create'
+
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("msg"),
+                    client = self)
+            else:
+                return result
+
+        url = '/api/createPayWithCheck'
 
         data = {
             'version': 'V4.0',
@@ -99,12 +110,17 @@ class JDOpenPay(JdOpenBaseClient):
             'customerNum': str(self.customerNum),
             # 'shopNum': str(self.shopNum),
 
-            'authId': str(authId),
+            'authCode': str(authId),
             'bankType': str(bankType),
             'requestNum': str(requestNum),
-            'amount': str(amount),
+            'orderAmount': str(amount),
             'callbackUrl': str(callbackUrl),
-            'payModel': 'ONCE'
+            'payModel': 'ONCE',
+            'orderType': 'SALES',
+            'payType': 'ACTIVE',
+            'bussinessType': 'QRCODE_TRAD',
+            'source': 'API',
+            'paySource': str(bankType)
         }
 
         if self.shopNum:
@@ -115,7 +131,7 @@ class JDOpenPay(JdOpenBaseClient):
         if ledgerRule:
             data.update({
                 'subOrderType': str(SubOrderType.LEDGER),
-                'ledgerRule': ledgerRule
+                'LedgerRequest': ledgerRule
             })
         else:
             data.update({
@@ -129,27 +145,43 @@ class JDOpenPay(JdOpenBaseClient):
                 'extraInfo': extraInfo
             })
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
+
+    def api_trade_query(self, out_trade_no = None):
+        def processor(self, result):
+            if 'success' not in result or not result['success']:
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("msg"),
+                    client = self)
+
+            if 'queryOrderInfoRes' not in result:
+                raise JDException(
+                    errCode = 'QUERY_ORDER_FAILURE',
+                    errMsg = u'查询订单信息失败',
+                    client = self)
 
-    def api_trade_query(self, out_trade_no=None, trade_no=None):
-        assert out_trade_no or trade_no, 'out_trade_no and trade_no must not be empty at the same time'
+            if not result['queryOrderInfoRes']['success']:
+                raise JDException(
+                    errCode = result['queryOrderInfoRes']['code'],
+                    errMsg = result['queryOrderInfoRes']['msg'],
+                    client = self)
 
-        url = '/v3/order/query'
+            return result
+
+        url = '/api/queryOrderPayDetail'
 
         data = {
-            'agentNum': self.agentNum,
             'customerNum': self.customerNum,
-            'shopNum': self.shopNum,
-            'requestNum': out_trade_no,
-            'orderNum': trade_no
         }
 
         if out_trade_no:
             data.update({'requestNum': out_trade_no})
         else:
-            data.update({'orderNum': trade_no})
+            raise JDException(
+                errCode = JDOpenErrorCode.MY_INVALID_PARAMETER, errMsg = u'缺少requestNum参数')
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
     def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs):
         """
@@ -161,57 +193,95 @@ class JDOpenPay(JdOpenBaseClient):
         :return:
         """
 
-        url = '/v3/order/refund/part'
+        def processor(self, result):
+            if 'result' not in result or not result['result'] or result['resultCode'] != 'success':
+                raise JDRequestException(
+                    errCode = result.get('resultCode'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
+        url = '/api/refundByRequestNum'
 
         data = {
-            'version': 'V4.0',
+            'requestVersion': 'V4.0',
             'agentNum': self.agentNum,
             'customerNum': self.customerNum,
-            'shopNum': self.shopNum,
+            # 'shopNum': self.shopNum,
             'requestNum': outTradeNo,
             'refundRequestNum': outRefundNo,
             'refundPartAmount': amount
         }
 
+        if self.shopNum:
+            data.update({
+                'shopNum': self.shopNum
+            })
+
         ledgerInfoList = kwargs.pop('ledgerInfoList', None)
         if ledgerInfoList:
-            data.update({'ledgerInfoList': ledgerInfoList})
+            data.update({'list': ledgerInfoList})
 
         data.update(kwargs)
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
-    def api_refund_query(self, requestNum=None, orderNum=None):
+    def api_refund_query(self, out_refund_no):
         """
         查询退款订单
-        :param requestNum: 原商户订单号
-        :param orderNum: 原订单编号
+        :param out_refund_no: 退款单号
         :return:
         """
 
-        assert not (requestNum and orderNum), u'不能同时提供商户订单号和京东订单号'
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDRequestException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("msg"),
+                    client = self)
+            else:
+                return result
 
-        url = '/v3/order/refund/query'
+        url = '/api/queryRefundOrderByRequestNum'
 
         data = {
             'agentNum': self.agentNum,
             'customerNum': self.customerNum,
-            'shopNum': self.shopNum
+            # 'shopNum': self.shopNum
         }
 
-        if requestNum:
+        if self.shopNum:
             data.update({
-                'requestNum': requestNum
+                'shopNum': self.shopNum
             })
-        elif orderNum:
+
+        if out_refund_no:
             data.update({
-                'orderNum': orderNum
+                'requestNum': out_refund_no
             })
         else:
-            raise JdOpenException(
-                errCode=JDOpenErrorCode.MY_INVALID_PARAMETER, errMsg=u'缺少订单号参数')
+            raise JDException(
+                errCode = JDOpenErrorCode.MY_INVALID_PARAMETER, errMsg = u'缺少退款订单号参数')
+
+        nonce_str = random_string(32)
 
-        return self.post(url=url, data=data)
+        if self.shopNum:
+            _str = 'agentnum={}&customernum={}&nonce_str={}&requestnum={}&shopnum={}&key=xrtbvjJk213W'.format(
+                self.agentNum, self.customerNum, nonce_str, out_refund_no, self.shopNum)
+        else:
+            _str = 'agentnum={}&customernum={}&nonce_str={}&requestnum={}&key=xrtbvjJk213W'.format(
+                self.agentNum, self.customerNum, nonce_str, out_refund_no)
+
+        import hashlib
+        sign = hashlib.md5(_str.encode('utf-8')).hexdigest().upper()
+
+        data.update({
+            'nonce_str': nonce_str,
+            'sign': sign
+        })
+
+        return self.post(url = url, data = data, processor = processor)
 
     def add_wechat_auth_pay_dir(self, auth_pay_dir):
         """
@@ -220,6 +290,15 @@ class JDOpenPay(JdOpenBaseClient):
         :return:
         """
 
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
         url = '/api/addAuthPayDirsDevConfig'
 
         data = {
@@ -227,7 +306,7 @@ class JDOpenPay(JdOpenBaseClient):
             'authPayDir': auth_pay_dir
         }
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
     def query_wechat_auth_pay_dir(self, batch_num):
         """
@@ -236,6 +315,15 @@ class JDOpenPay(JdOpenBaseClient):
         :return:
         """
 
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("message"),
+                    client = self)
+            else:
+                return result
+
         url = '/api/queryAddAuthPayDirsDevConfigByBatchNum'
 
         data = {
@@ -243,10 +331,19 @@ class JDOpenPay(JdOpenBaseClient):
             'batchNum': batch_num
         }
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
     def api_close_order(self, requestNum):
-        url = '/v1/agent/order/close'
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("msg"),
+                    client = self)
+            else:
+                return result
+
+        url = '/api/close'
 
         data = {
             'agentNum': self.agentNum,
@@ -254,10 +351,19 @@ class JDOpenPay(JdOpenBaseClient):
             'requestNum': requestNum
         }
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
     def api_cancel_order(self, requestNum):
-        url = '/v2/agent/order/cancel'
+        def processor(self, result):
+            if 'success' not in result or not result['success'] or result['code'] != 'success':
+                raise JDException(
+                    errCode = result.get('code'),
+                    errMsg = result.get("msg"),
+                    client = self)
+            else:
+                return result
+
+        url = '/api/cancel'
 
         data = {
             'agentNum': self.agentNum,
@@ -265,7 +371,7 @@ class JDOpenPay(JdOpenBaseClient):
             'requestNum': requestNum
         }
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
     def download_bill(self, bill_date):
         url = '/v1/agent/checkaccountfile/download'
@@ -273,6 +379,7 @@ class JDOpenPay(JdOpenBaseClient):
         data = {
             'agentNum': self.agentNum,
             'customerNum': self.customerNum,
+            'billType': 'QRBILL',
             'date': bill_date
         }
 

+ 377 - 0
library/jdpsi/client.py

@@ -1,6 +1,383 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 
+import base64
+import datetime
+import hashlib
+import simplejson as json
 import logging
+import random
+import time
+from binascii import hexlify
+from collections import Iterable
+from urlparse import urljoin
+
+import requests
+from Crypto.Cipher import DES3
+
+from apilib.monetary import JDMerchantPermillage
+from library.jdbase.exceptions import JDNetworkException
+from library.jdpsi.constants import JdPsiErrorCode
+from library.jdpsi.exceptions import JdPsiException
 
 logger = logging.getLogger(__name__)
+
+
+class MyEncoder(json.JSONEncoder):
+
+    def default(self, obj):
+        if isinstance(obj, JDMerchantPermillage):
+            return obj.to_jd_params()
+        return super(MyEncoder, self).default(obj)
+
+
+class JdPsiMerchantClient(object):
+    BASE_URL = "https://psi.jd.com"
+
+    def __init__(self, agentNo, accessKey):
+        self.agentNo = agentNo
+        self.accessKey = accessKey
+
+    @staticmethod
+    def sign(*args):
+        h = hashlib.md5()
+        h.update("".join((str(_) for _ in args)))
+        return h.hexdigest()
+
+    @staticmethod
+    def pad(rawJson):
+        # 转换为字节数组
+        rawBytes = bytes.encode(rawJson, encoding = "utf-8")
+        rawBytesLen = len(rawBytes)
+
+        # 计算补位
+        x = (rawBytesLen + 4) % 8
+        y = 0 if x == 0 else 8 - x
+
+        # 将有效数据长度byte[]添加到原始byte数组的头部
+        resultBytes = bytearray(rawBytesLen + 4 + y)
+        resultBytes[0] = (rawBytesLen >> 24) & 0xFF
+        resultBytes[1] = (rawBytesLen >> 16) & 0xFF
+        resultBytes[2] = (rawBytesLen >> 8) & 0xFF
+        resultBytes[3] = rawBytesLen & 0xFF
+
+        # 填充补位数据
+        for i in range(rawBytesLen):
+            resultBytes[4 + i] = ord(rawBytes[i])
+
+        for i in range(y):
+            resultBytes[rawBytesLen + 4 + i] = 0x00
+
+        return bytes(resultBytes)
+
+    def encrypt(self, raw):
+        plaintext = json.dumps(raw, sort_keys = True, separators = (',', ':'), cls = MyEncoder)
+
+        key = base64.b64decode(self.accessKey)
+
+        cipher = DES3.new(key, DES3.MODE_ECB)
+        encrypt_bytes = cipher.encrypt(JdPsiMerchantClient.pad(plaintext))
+        return hexlify(encrypt_bytes)
+
+    def _handle_result(self, res, **kwargs):
+        try:
+            result = json.loads(res.content.decode('utf-8', 'ignore'), strict = False)
+        except (TypeError, ValueError, json.JSONDecodeError):
+            logger.debug(u'错误的解析结构', exc_info = True)
+            result = res
+
+        if "code" not in result:
+            return result
+
+        if result["code"] == JdPsiErrorCode.SUCCESS:
+            return result
+
+        if result["code"] in []:
+            return self._request(kwargs["path"], kwargs["agentNo"], kwargs["entity"], kwargs["images"],
+                                 kwargs["signField"])
+
+        raise JdPsiException(
+            errCode = '{}'.format(result["code"]),
+            errMsg = result["message"],
+            client = self,
+        )
+
+    def _request(self, path, agentNo, entity, images = None, signField = None):
+
+        # 添加 签名标签
+        if signField and isinstance(signField, Iterable):
+            sign = JdPsiMerchantClient.sign(*(entity.get(_) for _ in signField))
+            entity.update({"sign": sign})
+
+        # 组织报文
+        data = {
+            "agentNo": agentNo,
+            "entity": self.encrypt(entity)
+        }
+
+        # 图片文件
+        if images and isinstance(images, dict):
+            files = {_k: (_k, _v, "image/png") for _k, _v in images.items()}
+        else:
+            files = None
+
+        url = urljoin(JdPsiMerchantClient.BASE_URL, path)
+
+        try:
+            res = requests.post(url = url, data = data, files = files)
+        except requests.Timeout:
+            raise JDNetworkException(
+                errCode = 'timeout',
+                errMsg = "timeout",
+                client = self)
+        else:
+            try:
+                res.raise_for_status()
+            except requests.RequestException as rre:
+                raise JDNetworkException(
+                    errCode = 'HTTP{}'.format(res.status_code),
+                    errMsg = rre.message,
+                    client = self,
+                    request = rre.request,
+                    response = rre.response)
+
+        return self._handle_result(
+            res, path = path, agentNo = agentNo, entity = entity, images = images, signField = signField
+        )
+
+    def create_customer(
+            self, blicUrla, lepUrla, lepUrlb, lepUrlc, img, enterimg, innerimg, cardPhoto, settleManPhotoFront,
+            settleManPhotoBack, settleHoldingIDCard,
+            companyType, serialNo, regEmail, regPhone, blicCardType, blicCompanyName, abMerchantName, indTwoCode,
+            blicProvince,
+            blicCity, blicAddress, blicLongTerm, blicValidityStart, blicValidityEnd, lepCardType, lepName, lepCardNo,
+            lepLongTerm,
+            lepValidityStart, lepValidityEnd, contactName, contactPhone, contactEmail, contactProvince, contactCity,
+            contactAddress,
+            ifPhyStore, storeProvince, storeCity, storeAddress, settleToCard, priatePublic, bankName, subBankCode,
+            bankAccountNo,
+            bankAccountName, settleCardPhone, settlementPeriod, directoryList,
+            occUrla = None, blicUscc = None, blicScope = None, merchantNo = None
+    ):
+        images = {
+            "blicUrla": blicUrla,
+            "lepUrla": lepUrla,
+            "lepUrlb": lepUrlb,
+            "lepUrlc": lepUrlc,
+            "img": img,
+            "enterimg": enterimg,
+            "innerimg": innerimg,
+            "cardPhoto": cardPhoto,
+            "settleManPhotoFront": settleManPhotoFront,
+            "settleManPhotoBack": settleManPhotoBack,
+            "settleHoldingIDCard": settleHoldingIDCard
+        }
+        occUrla and images.update({"occUrla": occUrla})
+
+        entity = {
+            "companyType": companyType,
+            "serialNo": serialNo,
+            "agentNo": self.agentNo,
+            "regEmail": regEmail,
+            "regPhone": regPhone,
+            "blicCardType": blicCardType,
+            "blicCompanyName": blicCompanyName,
+            "abMerchantName": abMerchantName,
+            "indTwoCode": indTwoCode,
+            "blicProvince": blicProvince,
+            "blicCity": blicCity,
+            "blicAddress": blicAddress,
+            "blicLongTerm": blicLongTerm,
+            "blicValidityStart": blicValidityStart,
+            "blicValidityEnd": blicValidityEnd,
+            "lepCardType": lepCardType,
+            "lepName": lepName,
+            "lepCardNo": lepCardNo,
+            "lepLongTerm": lepLongTerm,
+            "lepValidityStart": lepValidityStart,
+            "lepValidityEnd": lepValidityEnd,
+            "contactName": contactName,
+            "contactPhone": contactPhone,
+            "contactEmail": contactEmail,
+            "contactProvince": contactProvince,
+            "contactCity": contactCity,
+            "contactAddress": contactAddress,
+            "ifPhyStore": ifPhyStore,
+            "storeProvince": storeProvince,
+            "storeCity": storeCity,
+            "storeAddress": storeAddress,
+            "settleToCard": settleToCard,
+            "priatePublic": priatePublic,
+            "bankName": bankName,
+            "subBankCode": subBankCode,
+            "bankAccountNo": bankAccountNo,
+            "bankAccountName": bankAccountName,
+            "settleCardPhone": settleCardPhone,
+            "settlementPeriod": settlementPeriod,
+            "directoryList": directoryList
+        }
+        blicUscc and entity.update({"blicUscc": blicUscc})
+        blicScope and entity.update({"blicScope": blicScope})
+        merchantNo and entity.update({"merchantNo": merchantNo})
+
+        logger.info("[create_customer] agentNo={}, entity={}".format(
+            self.agentNo, entity
+        ))
+
+        path = "/merchant/enterSingle"
+        return self._request(path = path, agentNo = self.agentNo, entity = entity, images = images, signField = [
+            "serialNo", "lepCardNo", "bankAccountNo", "settleCardPhone"
+        ])
+
+    def create_product(self, serialNo, merchantNo, productId, payToolId, mfeeType, mfee = None,
+                       ladderList = None):
+        """
+        创建 结算 支付 产品
+        """
+
+        logger.info(
+            "[create_product] agentNo={}, serialNo={}, merchantNo={}, productId={}, "
+            "payToolId={}, mfeeType={}, mfee={}, ladderList={}".format(
+                self.agentNo, serialNo, merchantNo, productId, payToolId, mfeeType, mfee, ladderList
+            ))
+
+        entity = {
+            "agentNo": self.agentNo,
+            "serialNo": serialNo,
+            "merchantNo": merchantNo,
+            "productId": productId,
+            "payToolId": payToolId,
+            "mfeeType": mfeeType
+        }
+        if mfee:
+            entity.update({"mfee": mfee})
+        if ladderList:
+            entity.update({"ladderList": ladderList})
+
+        path = "/merchant/applySingle"
+        return self._request(path = path, agentNo = self.agentNo, entity = entity, signField = [
+            "serialNo", "agentNo", "merchantNo", "productId", "payToolId"
+        ])
+
+    def query_product(self, serialNo, merchantNo):
+        """
+        查询产品信息
+        """
+        logger.info("[query_product] agentNo={}, serialNo={}, merchantNo={}".format(
+            self.agentNo, serialNo, merchantNo
+        ))
+
+        if not merchantNo:
+            raise JdPsiException(
+                errCode = JdPsiErrorCode.PARAMETER_ERROR,
+                errMsg = u'缺少商户编号参数',
+                client = self)
+
+        entity = {
+            "agentNo": self.agentNo,
+            "serialNo": serialNo,
+            "merchantNo": merchantNo
+        }
+        path = "/merchant/status/queryApplySingle"
+        return self._request(path = path, agentNo = self.agentNo, entity = entity, signField = [
+            "serialNo", "merchantNo"
+        ])
+
+    def query_secret_key(self, serialNo, merchantNo):
+        """
+        查询产品秘钥
+        """
+        logger.info("[query_secret_key] agentNo={}, serialNo={}, merchantNo={}".format(
+            self.agentNo, serialNo, merchantNo
+        ))
+
+        entity = {
+            "serialNo": serialNo,
+            "merchantNo": merchantNo,
+        }
+
+        if not merchantNo:
+            raise JdPsiException(
+                errCode = JdPsiErrorCode.PARAMETER_ERROR,
+                errMsg = u'缺少商户编号参数',
+                client = self)
+
+        path = "/merchant/status/queryMerchantKeys"
+        return self._request(path = path, agentNo = self.agentNo, entity = entity, signField = [
+            "serialNo", "merchantNo"
+        ])
+
+    def query_bill_file(self, merchantNo, billDate = None):
+        """
+        查询账单 的下载地址
+        """
+        logger.info("[query_bill_file] billDate={}, merchantNo={}, agentNo={}".format(
+            billDate, merchantNo, self.agentNo))
+
+        billDate = billDate or datetime.datetime.now().strftime("%Y%m%d")
+        entity = {
+            "serialNo": "{}{}".format(int(time.time() * 1000), random.randint(1000, 9999)),
+            "agentNo": self.agentNo,
+            "billDate": billDate,
+            "merchantNo": merchantNo
+        }
+
+        path = "/agentmerchant/download"
+
+        return self._request(path = path, agentNo = self.agentNo, entity = entity, signField = ["billDate", "agentNo"])
+
+    def query_settle(self, startTime, endTime, pageNum = 1, pageSize = 10, orderStatus = "", merchantNo = None):
+        """
+        查询 商户的结算信息
+        """
+        logger.info("[query_settle] merchantNo={}, agentNo={}, startTime={}, endTime={}, orderStatus={}".format(
+            merchantNo, self.agentNo, startTime, endTime, orderStatus
+        ))
+
+        entity = {
+            "agentNo": self.agentNo,
+            "merchantNo": merchantNo,
+            "pageNum": pageNum,
+            "pageSize": pageSize,
+            "queryStartTime": startTime,
+            "queryEndTime": endTime,
+            "orderStatus": orderStatus,
+        }
+
+        path = "/merchant/status/queryMerchantsSettle"
+        return self._request(path = path, agentNo = self.agentNo, entity = entity, signField = [
+            "agentNo", "merchantNo", "queryStartTime", "queryEndTime", "orderStatus", "pageNum", "pageSize"
+        ])
+
+    def query_sub_channel(self, merchantNo, productCode = None):
+        logger.info("[query_sub_channel] merchantNo={}, agentNo={}, productCode={}".format(
+            merchantNo, self.agentNo, productCode
+        ))
+
+        # 查询编号固定是401
+        productCode = productCode or "401"
+
+        entity = {
+            "agentNo": self.agentNo,
+            "merchantNo": merchantNo,
+            "productCode": str(productCode),
+        }
+        path = "/merchant/status/queryMerchantWXNo"
+
+        result = self._request(path = path, agentNo = self.agentNo, entity = entity, signField = [
+            "agentNo", "merchantNo", "productCode"
+        ])
+
+        if 'data' not in result or "hlbWxSubNoResultInfo" not in result["data"]:
+            logger.error("[query_sub_channel] merchantNo={}, agentNo={}, productCode={}, has no data.".format(
+                merchantNo, self.agentNo, productCode
+            ))
+            return None
+
+        channelsInfo = json.loads(result["data"]["hlbWxSubNoResultInfo"])
+
+        try:
+            return channelsInfo["threePartnerNoData"][0]["threePartnerNo"]
+        except Exception:
+            logger.error('query wx sub merchant id failure. result = {}'.format(channelsInfo))
+            return None

+ 3 - 8
library/jdpsi/constants.py

@@ -1,11 +1,6 @@
-# coding=utf-8
-
-
-ACCESS_KEY = "FYMWdtwfDTh29DvcyyPIVN+ny9x/c137"
-
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
 
 class JdPsiErrorCode(object):
     SUCCESS = "0000"
-
-
-
+    PARAMETER_ERROR = "0001"

+ 1 - 1
library/qiben/simmanager.py

@@ -108,7 +108,7 @@ class SimManager(object):
         }
 
         result = self._post(endpoint = 'v1/card/batchQueryCardInfo', data = data)
-        if result['status'] != 0:
+        if 'status' not in result or result['status'] != 0:
             raise QibenException(result['status'], result['message'])
 
         return result['data']

+ 2 - 1
library/sms/ucpaas.py

@@ -14,7 +14,8 @@ from library.sms import SmsSender
 
 
 class Sender(SmsSender):
-    API_URL = 'https://open.ucpaas.com/ol/sms/sendsms'
+    # API_URL = 'https://open.ucpaas.com/ol/sms/sendsms'  # old
+    API_URL = 'https://open2.ucpaas.com/ol/sms/sendsms'  # new
 
     TEMPLATE = {
                    'CAPTCHA_TEMPLATE_ID': '544642',

+ 1 - 1
library/sms/zthy.py

@@ -27,7 +27,7 @@ class Sender(SmsSender):
 
     TEMPLATE = {
         'CAPTCHA_TEMPLATE_ID': u'尊敬的用户,您本次的验证码为:{},2分钟内有效。如非本人操作,请忽略本信息。',
-        'SMS_NOTIFY_EXPIRED_DEVICE_TEMPLATEID': u'尊敬的用户, {}, 届时将无法使用支付宝或微信支付, 请您尽快续费, 祝您生活愉快。已经续费请忽略本消息。',
+        'SMS_NOTIFY_EXPIRED_DEVICE_TEMPLATEID': u'尊敬的用户, {}, 届时将无法使用线上支付功能, 请您尽快续费,已经续费请忽略本消息。祝您生活愉快。',
         'EDIT_MONITOR_ID': u'正在编辑审核人。您本次的验证码为:{},2分钟有效。',
         'DEALER_MONITOR_WITHDRAW_ID': u'尊敬的用户,您监管的账号需要验证码,本次的验证码为:{},2分钟有效。',
         'MERCHANT_NOTIFY': u'尊敬的用户,根据央行相关政策,使用银联、微信支付、支付宝等支付机构收款需商家提交身份证、银行卡等相关证件,'

+ 1 - 0
library/wechatpayv3/client/__init__.py

@@ -11,3 +11,4 @@ class WechatClientV3(BaseWechatClient):
     transfer = api.Transfer()
     media = api.Media()
     goldplan = api.GoldPlan()
+    apply4subject = api.Apply4Subject()

+ 1 - 0
library/wechatpayv3/client/api/__init__.py

@@ -5,3 +5,4 @@ from .complaint import Complaint
 from .goldplan import GoldPlan
 from .media import Media
 from .transfer import Transfer
+from .apply4subject import Apply4Subject

+ 14 - 8
library/wechatpayv3/client/api/complaint.py

@@ -20,10 +20,13 @@ class Complaint(BaseWeChatAPI):
             begin_date = datetime.datetime.now().strftime("%Y-%m-%d")
         if not end_date:
             end_date = begin_date
-        if not complainted_mchid:
-            complainted_mchid = self.client.mchid
-        path = '/v3/merchant-service/complaints-v2?limit=%s&offset=%s&begin_date=%s&end_date=%s&complainted_mchid=%s'
-        path = path % (limit, offset, begin_date, end_date, complainted_mchid)
+
+        path = '/v3/merchant-service/complaints-v2?limit={}&offset={}&begin_date={}&end_date={}'
+        path = path.format(limit, offset, begin_date, end_date)
+
+        if complainted_mchid:
+            path = '{}&complainted_mchid={}'.format(path, complainted_mchid)
+
         return self.client.core.request(path)
 
     def complaint_detail_query(self, complaint_id):
@@ -87,7 +90,8 @@ class Complaint(BaseWeChatAPI):
         path = '/v3/merchant-service/complaint-notifications'
         return self.client.core.request(path, method = RequestType.DELETE)
 
-    def complaint_response(self, complaint_id, response_content, response_images = None, jump_url = None,
+    def complaint_response(self, complaint_id, complainted_mchid, response_content, response_images = None,
+                           jump_url = None,
                            jump_url_text = None):
         """提交投诉回复
         :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000'
@@ -103,7 +107,8 @@ class Complaint(BaseWeChatAPI):
             params.update({'response_content': response_content})
         else:
             raise Exception('response_content is not assigned')
-        params.update({'complainted_mchid': self.client.core._mchid})
+
+        params.update({'complainted_mchid': complainted_mchid})
         if response_images:
             params.update({'response_images': response_images})
         if jump_url:
@@ -113,14 +118,15 @@ class Complaint(BaseWeChatAPI):
         path = '/v3/merchant-service/complaints-v2/%s/response' % complaint_id
         return self.client.core.request(path, method = RequestType.POST, data = params)
 
-    def complaint_complete(self, complaint_id):
+    def complaint_complete(self, complaint_id, complainted_mchid):
         """反馈投诉处理完成
         :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000'
         """
         params = {}
         if not complaint_id:
             raise Exception('complaint_id is not assigned')
-        params.update({'complainted_mchid': self.client.core._mchid})
+
+        params.update({'complainted_mchid': complainted_mchid})
         path = '/v3/merchant-service/complaints-v2/%s/complete' % complaint_id
         return self.client.core.request(path, method = RequestType.POST, data = params)
 

+ 22 - 14
library/wechatpayv3/client/api/media.py

@@ -9,15 +9,22 @@ from ...utils import sha256
 
 
 class Media(BaseWeChatAPI):
-    def _media_upload(self, filepath, filename, path):
-        if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath) and path):
-            raise Exception('filepath is not assigned or not exists')
-        with open(filepath, mode = 'rb') as f:
-            content = f.read()
-        if not filename:
-            filename = os.path.basename(filepath)
-        params = {}
-        params.update({'meta': '{"filename":"%s","sha256":"%s"}' % (filename, sha256(content))})
+    def _media_upload(self, content, filepath, filename, path):
+        if not content:
+            if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath) and path):
+                raise Exception('filepath is not assigned or not exists')
+            with open(filepath, mode = 'rb') as f:
+                content = f.read()
+            if not filename:
+                filename = os.path.basename(filepath)
+        else:
+            if not filename:
+                raise Exception('filename is not assigned or not exists')
+
+        data = {
+            'meta': '{"filename":"%s","sha256":"%s"}' % (filename, sha256(content))
+        }
+
         mimes = {
             '.bmp': 'image/bmp',
             '.jpg': 'image/jpeg',
@@ -37,20 +44,21 @@ class Media(BaseWeChatAPI):
         media_type = os.path.splitext(filename)[-1]
         if media_type not in mimes:
             raise Exception('wechatpayv3 does not support this media type.')
+
         files = [('file', (filename, content, mimes[media_type]))]
         return self.client.core.request(
-            path, method = RequestType.POST, data = params, sign_data = params.get('meta'), files = files)
+            path, method = RequestType.POST, data = data, sign_data = data.get('meta'), files = files)
 
-    def image_upload(self, filepath, filename = None):
+    def image_upload(self, content = None, filepath = None, filename = None):
         """图片上传
         :param filepath: 图片文件路径
         :param filename: 文件名称,未指定则从filepath参数中截取
         """
-        return self._media_upload(filepath, filename, '/v3/merchant/media/upload')
+        return self._media_upload(content, filepath, filename, '/v3/merchant/media/upload')
 
-    def video_upload(self, filepath, filename = None):
+    def video_upload(self, content = None, filepath = None, filename = None):
         """视频上传
         :param filepath: 视频文件路径
         :param filename: 文件名称,未指定则从filepath参数中截取
         """
-        return self._media_upload(filepath, filename, '/v3/merchant/media/video_upload')
+        return self._media_upload(content, filepath, filename, '/v3/merchant/media/video_upload')

+ 11 - 14
library/wechatpayv3/core.py

@@ -9,7 +9,7 @@ from datetime import datetime
 import requests
 
 from library.wechatbase.exceptions import InvalidSignatureException, APILimitedException, \
-    WeChatPayException, WechatNetworkException
+    WechatNetworkException, WeChatException
 from . import update_certificates
 from .type import RequestType, SignType
 from .utils import (aes_decrypt, build_authorization, hmac_sign,
@@ -154,7 +154,7 @@ class Core(object):
             auto_retry = True
 
         if not auto_retry:
-            raise WeChatPayException(
+            raise WeChatException(
                 errCode = errcode,
                 errMsg = errmsg,
                 client = self,
@@ -167,7 +167,7 @@ class Core(object):
                 self._logger.debug('reached the maximum number of retries. url = {}'.format(path))
 
                 self.retry_count = 0
-                raise WeChatPayException(
+                raise WeChatException(
                     errCode = errcode,
                     errMsg = errmsg,
                     client = self,
@@ -199,24 +199,21 @@ class Core(object):
             headers.update({'Content-Type': 'multipart/form-data'})
         else:
             headers.update({'Content-Type': 'application/json'})
+
         headers.update({'Accept': 'application/json'})
-        headers.update({'User-Agent': 'wechatpay v3 api python sdk(https://github.com/minibear2021/wechatpayv3)'})
+        headers.update({'User-Agent': 'weifule/v1.0.0 (https://www.washpayer.com)'})
+
         if cipher_data:
             headers.update({'Wechatpay-Serial': hex(self._last_certificate().serial_number)[2:].upper().rstrip("L")})
 
-        authorization = build_authorization(
-            path,
-            method.value,
-            self._mchid,
-            self._cert_serial_no,
-            self._private_key,
-            data = sign_data if sign_data else data)
-        headers.update({'Authorization': authorization})
+        headers.update({'Authorization': build_authorization(
+            path, method.value, self._mchid, self._cert_serial_no,
+            self._private_key, data = sign_data if sign_data else data)})
 
         self._logger.debug('Request url: %s' % self._gate_way + path)
         self._logger.debug('Request type: %s' % method.value)
         self._logger.debug('Request headers: %s' % headers)
-        self._logger.debug('Request params: %s' % data)
+        self._logger.debug('Request data: %s' % data)
 
         kwargs = {'headers': headers, 'proxies': self._proxy}
 
@@ -271,7 +268,7 @@ class Core(object):
 
             if not skip_verify:
                 if not self._verify_signature(res.headers, res.text):
-                    raise InvalidSignatureException(u'无效签名')
+                    raise InvalidSignatureException()
 
             if res.status_code == 204:
                 return {}

+ 20 - 25
library/wechatpayv3/utils.py

@@ -1,42 +1,42 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 
-import json
+import base64
+import hashlib
+import hmac
+import simplejson as json
 import time
 import uuid
-from base64 import b64decode, b64encode
 
 from cryptography.exceptions import InvalidSignature, InvalidTag
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15
 from cryptography.hazmat.primitives.ciphers.aead import AESGCM
-from cryptography.hazmat.primitives.hashes import SHA1, SHA256, Hash
-from cryptography.hazmat.primitives.hmac import HMAC
+from cryptography.hazmat.primitives.hashes import SHA1, SHA256
 from cryptography.hazmat.primitives.serialization import load_pem_private_key
 from cryptography.x509 import load_pem_x509_certificate
 
 
-def build_authorization(path,
-                        method,
-                        mchid,
-                        serial_no,
-                        private_key,
-                        data = None,
-                        nonce_str = None):
+def build_authorization(path, method, mchid, serial_no, private_key, data = None, nonce_str = None):
     timeStamp = str(int(time.time()))
     nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper()
-    body = data if isinstance(data, str) else json.dumps(data) if data else ''
+
+    data = data or data or ""
+    body = json.dumps(data) if not isinstance(data, basestring) else data
+
     sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body)
     signature = rsa_sign(private_key = private_key, sign_str = sign_str)
+
     authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (
         mchid, nonce_str, signature, timeStamp, serial_no)
     return authorization
 
 
 def rsa_sign(private_key, sign_str):
-    message = sign_str.encode('UTF-8')
+    message = sign_str.encode('utf-8')
+
     signature = private_key.sign(data = message, padding = PKCS1v15(), algorithm = SHA256())
-    sign = b64encode(signature).decode('UTF-8').replace('\n', '')
+    sign = base64.b64encode(signature).decode('utf8').replace('\n', '')
     return sign
 
 
@@ -44,7 +44,7 @@ def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key):
     key_bytes = apiv3_key.encode('UTF-8')
     nonce_bytes = nonce.encode('UTF-8')
     associated_data_bytes = associated_data.encode('UTF-8')
-    data = b64decode(ciphertext)
+    data = base64.b64decode(ciphertext)
     aesgcm = AESGCM(key = key_bytes)
     try:
         result = aesgcm.decrypt(nonce = nonce_bytes, data = data, associated_data = associated_data_bytes).decode(
@@ -84,7 +84,7 @@ def rsa_verify(timestamp, nonce, body, signature, certificate):
     sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body)
     public_key = certificate.public_key()
     message = sign_str.encode('UTF-8')
-    signature = b64decode(signature)
+    signature = base64.b64decode(signature)
     try:
         public_key.verify(signature, message, PKCS1v15(), SHA256())
     except InvalidSignature:
@@ -99,12 +99,12 @@ def rsa_encrypt(text, certificate):
         plaintext = data,
         padding = OAEP(mgf = MGF1(algorithm = SHA1()), algorithm = SHA1(), label = None)
     )
-    return b64encode(cipherbyte).decode('UTF-8')
+    return base64.b64encode(cipherbyte).decode('UTF-8')
 
 
 def rsa_decrypt(ciphertext, private_key):
     data = private_key.decrypt(
-        ciphertext = b64decode(ciphertext),
+        ciphertext = base64.b64decode(ciphertext),
         padding = OAEP(mgf = MGF1(algorithm = SHA1()), algorithm = SHA1(), label = None)
     )
     result = data.decode('UTF-8')
@@ -112,13 +112,8 @@ def rsa_decrypt(ciphertext, private_key):
 
 
 def hmac_sign(key, sign_str):
-    hmac = HMAC(key.encode('UTF-8'), SHA256())
-    hmac.update(sign_str.encode('UTF-8'))
-    sign = hmac.finalize().hex().upper()
-    return sign
+    return hmac.new(key.encode('UTF-8'), msg = sign_str, digestmod = hashlib.sha256).hexdigest().upper()
 
 
 def sha256(data):
-    hash = Hash(SHA256())
-    hash.update(data)
-    return hash.finalize().hex()
+    return hashlib.sha256(data).hexdigest()

+ 2 - 2
library/wechatpy/client/__init__.py

@@ -13,7 +13,7 @@ import requests
 from library.wechatpy.client import api
 from library.wechatpy.client.base import BaseWeChatClient
 from library.wechatpy.utils import WeChatSigner
-from library.wechatbase.exceptions import WeChatException
+from library.wechatbase.exceptions import WeChatException, WechatNetworkException
 
 logger = logging.getLogger(__name__)
 
@@ -128,7 +128,7 @@ class WeChatClient(BaseWeChatClient):
             try:
                 res.raise_for_status()
             except requests.RequestException as reqe:
-                raise WeChatException(
+                raise WechatNetworkException(
                     errCode='HTTP{}'.format(res.status_code),
                     errMsg=reqe.message,
                     client=self,

+ 2 - 2
library/wechatpy/client/base.py

@@ -12,7 +12,7 @@ import six
 from library import to_binary, to_text
 from library.wechatpy.client.api.base import BaseWeChatAPI
 from library.wechatpy.constants import WeChatErrorCode
-from library.wechatbase.exceptions import WeChatException, APILimitedException
+from library.wechatbase.exceptions import WeChatException, APILimitedException, WechatNetworkException
 from library.wechatpy.utils import json
 from library.wechatpy import access_token_key, jsapi_ticket_key
 
@@ -106,7 +106,7 @@ class BaseWeChatClient(object):
             try:
                 res.raise_for_status()
             except requests.RequestException as reqe:
-                raise WeChatException(
+                raise WechatNetworkException(
                     errCode='HTTP{}'.format(res.status_code),
                     errMsg=reqe.message,
                     client=self,

+ 18 - 17
library/wechatpy/component.py

@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
 """
     wechatpy.component
     ~~~~~~~~~~~~~~~
@@ -23,9 +25,9 @@ from library.wechatbase.exceptions import (
     APILimitedException,
     WeChatException,
     WeChatComponentOAuthException,
-    WeChatOAuthException,
+    WeChatOAuthException, WechatNetworkException
 )
-from library.wechatpy.messages import COMPONENT_MESSAGE_TYPES, ComponentUnknownMessage, MESSAGE_TYPES
+from library.wechatpy.messages import COMPONENT_MESSAGE_TYPES, ComponentUnknownMessage
 from library.wechatpy.parser import parse_message
 
 logger = logging.getLogger(__name__)
@@ -36,6 +38,7 @@ NO_RETRY_ERRCODE = [
     '61004'   # access clientip is not registered request
 ]
 
+
 class BaseWeChatComponent(object):
     API_BASE_URL = "https://api.weixin.qq.com/cgi-bin"
 
@@ -83,17 +86,16 @@ class BaseWeChatComponent(object):
         if isinstance(kwargs["data"], dict):
             kwargs["data"] = json.dumps(kwargs["data"])
 
-        res = self._http.request(method=method, url=url, **kwargs)
+        res = self._http.request(method = method, url = url, **kwargs)
         try:
             res.raise_for_status()
         except requests.RequestException as reqe:
-            raise WeChatException(
-                errCode=None,
-                errMsg=None,
-                client=self,
-                request=reqe.request,
-                response=reqe.response,
-            )
+            raise WechatNetworkException(
+                errCode = 'HTTP{}'.format(res.status_code),
+                errMsg = reqe.message,
+                client = self,
+                request = reqe.request,
+                response = reqe.response)
 
         return self._handle_result(res, method, url, **kwargs)
 
@@ -203,13 +205,12 @@ class BaseWeChatComponent(object):
         try:
             res.raise_for_status()
         except requests.RequestException as reqe:
-            raise WeChatException(
-                errCode=None,
-                errMsg=None,
-                client=self,
-                request=reqe.request,
-                response=reqe.response,
-            )
+            raise WechatNetworkException(
+                errCode = 'HTTP{}'.format(res.status_code),
+                errMsg = reqe.message,
+                client = self,
+                request = reqe.request,
+                response = reqe.response)
         result = res.json()
         if "errcode" in result and result["errcode"] != 0:
             raise WeChatException(

+ 3 - 5
library/wechatpy/pay/__init__.py

@@ -19,7 +19,8 @@ from cryptography.hazmat.primitives import serialization
 from optionaldict import optionaldict
 
 from library import random_string, to_binary, to_text
-from library.wechatbase.exceptions import WeChatPayException, InvalidSignatureException, WechatNetworkException
+from library.wechatbase.exceptions import InvalidSignatureException, WechatNetworkException, \
+    WeChatException
 from library.wechatpy.pay import api
 from library.wechatpy.pay.base import BaseWeChatPayAPI
 from library.wechatpy.pay.utils import (
@@ -216,7 +217,7 @@ class WeChatPay(object):
         errmsg = data.get('err_code_des')
 
         if result_code != 'SUCCESS':
-            raise WeChatPayException(
+            raise WeChatException(
                 errCode = errcode,
                 errMsg = errmsg,
                 client = self,
@@ -280,6 +281,3 @@ class WeChatPay(object):
 
         unpad = lambda s: s[:-ord(s[len(s) - 1:])]
         return xmltodict.parse(unpad(dec))["root"]
-
-
-

+ 26 - 22
middlewares/validPermission.py

@@ -9,6 +9,8 @@ from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
 from apilib.utils_string import cn
 from apps.web.dealer.models import Dealer, PermissionRole
 from apps.web.utils import ErrorResponseRedirect
+from middlewares.django_jwt_session_auth import get_authorization_header
+from django.conf import settings as django_settings
 
 logger = logging.getLogger(__name__)
 
@@ -16,28 +18,30 @@ logger = logging.getLogger(__name__)
 class PermissionMiddleware(object):
 
     def process_request(self, request):
-        original_user = request.session.get('_auth_user_id')
-        to_oper_user = request.session.get('oper_id')
-
-        if original_user and to_oper_user:
-            # role = PermissionRole.objects.filter(dealerId=to_oper_user, operId=original_user, isActive=True).first()
-            permissionRule = PermissionRole.get_role_permission(dealerId=to_oper_user, operId=original_user)
-
-            if not permissionRule:
-                request.session.clear()
-
-                return ErrorResponseRedirect(error=cn(u'您无权限进行此操作'))
-
-            # TODO url 过滤
-            url = request.path
-            # result = re.findall(r'password|pwd|verifyNewTel|Wallet|withdraw|paymentInfo|accountInfo', url, re.I)
-            result = re.findall(r'password|pwd|verifyNewTel|getWalletWithdrawInfo', url, re.I)
-            if result:
-                return ErrorResponseRedirect(error=cn(u'您当前账号无权访问,请切换主账号来操作'))
-
-            # 有授权信息
-            request.user = Dealer.objects.get(id=to_oper_user)
-            request.permissions = permissionRule
+        auth_domain, _ = get_authorization_header(request)
+
+        if auth_domain == django_settings.SERVICE_DOMAIN.DEALER:
+            original_user = request.session.get('_auth_user_id')
+            to_oper_user = request.session.get('oper_id')
+            if original_user and to_oper_user:
+                # role = PermissionRole.objects.filter(dealerId=to_oper_user, operId=original_user, isActive=True).first()
+                permissionRule = PermissionRole.get_role_permission(dealerId=to_oper_user, operId=original_user)
+
+                if not permissionRule:
+                    request.session.clear()
+
+                    return ErrorResponseRedirect(error=cn(u'您无权限进行此操作'))
+
+                # TODO url 过滤
+                url = request.path
+                # result = re.findall(r'password|pwd|verifyNewTel|Wallet|withdraw|paymentInfo|accountInfo', url, re.I)
+                result = re.findall(r'password|pwd|verifyNewTel|getWalletWithdrawInfo', url, re.I)
+                if result:
+                    return ErrorResponseRedirect(error=cn(u'您当前账号无权访问,请切换主账号来操作'))
+
+                # 有授权信息
+                request.user = Dealer.objects.get(id=to_oper_user)
+                request.permissions = permissionRule
 
     def process_response(self, request, response):
         return response

+ 0 - 0
patch.py


Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff