mopybird 2 years ago
parent
commit
839624c1fa
100 changed files with 3922 additions and 2157 deletions
  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
 from apilib.numerics import force_decimal, quantize, UnitBase
 
 
+
 class MoneyComparisonError(TypeError):
 class MoneyComparisonError(TypeError):
     # This exception was needed often enough to merit its own
     # This exception was needed often enough to merit its own
     # Exception class.
     # Exception class.
@@ -60,7 +61,7 @@ class Money(UnitBase):
         if isinstance(other, Money):
         if isinstance(other, Money):
             return self.__class__(self._amount - other.amount)
             return self.__class__(self._amount - other.amount)
         else:
         else:
-            return self.__class__(other) - self
+            return self - self.__class__(other)
 
 
     def __mul__(self, other):
     def __mul__(self, other):
         if isinstance(other, Money):
         if isinstance(other, Money):
@@ -131,6 +132,7 @@ class RMB(Money):
     def yuan_to_fen(cls, yuan):
     def yuan_to_fen(cls, yuan):
         return int((yuan * 100))
         return int((yuan * 100))
 
 
+
 class VirtualCoin(Money):
 class VirtualCoin(Money):
     __currency__ = 'VirtualCoin'
     __currency__ = 'VirtualCoin'
 
 
@@ -153,6 +155,7 @@ def sum_virtual_coin(list_):
 def sum_accuracy_rmb(list_):
 def sum_accuracy_rmb(list_):
     return sum(list_, AccuracyRMB(0))
     return sum(list_, AccuracyRMB(0))
 
 
+
 class Ratio(object):
 class Ratio(object):
     def __init__(self, amount):
     def __init__(self, amount):
         if isinstance(amount, Ratio):
         if isinstance(amount, Ratio):

+ 21 - 0
apilib/systypes.py

@@ -91,3 +91,24 @@ class NoEmptyValueDict(dict):
             return super(NoEmptyValueDict, self).setdefault(k, d)
             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):
 def rec_update_dict(d, update_dict, firstLevelOverwrite = False):
     """
     """
     递归地更新字典
     递归地更新字典
-    overwrite特指是否覆盖第一层dict
+    :param firstLevelOverwrite: 特指是否覆盖第一层dict
     :param d:
     :param d:
     :param update_dict:
     :param update_dict:
     :return:
     :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 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(
 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)
     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):
     if isinstance(ts_or_str, basestring):
         return datetime.datetime.strptime(ts_or_str, time_format)
         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):
     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'参数错误'
         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):
     if isinstance(ts_or_str, basestring):
         return dt_to_timestamp(to_datetime(ts_or_str, time_format))
         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):
     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)
     end = datetime.datetime.now() + datetime.timedelta(hours = 1)
 
 
     return begin, end
     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
             rv[k.replace('.', '-')] = v
 
 
     return rv
     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
 from apilib.utils_sys import PY3
 
 
+
 def cn(_): return unicode(_).encode('utf-8')
 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\\n%s:%s%s' % (k,tabs,v)
     result += '\\n'
     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
 # !/usr/bin/env python
 
 
 import logging
 import logging
+import shutil
 import sys
 import sys
+import tempfile
 import threading
 import threading
 import uuid
 import uuid
 
 
@@ -130,7 +132,8 @@ class MemcachedLock(object):
             result = self.mc.delete(self.key)
             result = self.mc.delete(self.key)
             logger.debug('=== MemcachedLock === delete result is: {}'.format(str(result)))
             logger.debug('=== MemcachedLock === delete result is: {}'.format(str(result)))
         else:
         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
     @property
     def locked(self):
     def locked(self):
@@ -139,6 +142,7 @@ class MemcachedLock(object):
 
 
 PY3 = sys.version_info[0] == 3
 PY3 = sys.version_info[0] == 3
 
 
+
 @contextmanager
 @contextmanager
 def MyStringIO():
 def MyStringIO():
     from six import StringIO
     from six import StringIO
@@ -147,3 +151,12 @@ def MyStringIO():
         yield fi
         yield fi
     finally:
     finally:
         fi.close()
         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 datetime
 import math
 import math
 import numbers
 import numbers
+from typing import TYPE_CHECKING, Optional
 
 
 from django.conf import settings
 from django.conf import settings
 from pandas import date_range
 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):
 class CoordinateTrans(object):
     """
     """
@@ -70,9 +77,6 @@ class CoordinateTrans(object):
             pass
             pass
 
 
 
 
-coordinateHandler = CoordinateTrans()
-
-
 class IntToHex(object):
 class IntToHex(object):
     """
     """
     数字转换16进制字符串
     数字转换16进制字符串
@@ -94,18 +98,7 @@ class IntToHex(object):
             return ""
             return ""
         return h[-2:] + IntToHex._reverse_hex(h[:-2])
         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):
         if not isinstance(lens, numbers.Integral):
             raise TypeError("type of lens must be Integral")
             raise TypeError("type of lens must be Integral")
         self._add_trans_number(number)
         self._add_trans_number(number)
@@ -118,6 +111,73 @@ class IntToHex(object):
         return numberHex
         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()
 int_to_hex = IntToHex()
 
 
 
 
@@ -146,7 +206,7 @@ def get_date_range(st, et, freq='D', reverse = True):
     if reverse:
     if reverse:
         return list(reversed(date_range(st, et, freq=freq).tolist()))
         return list(reversed(date_range(st, et, freq=freq).tolist()))
     else:
     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):
 def get_test_point(domain, key):
@@ -164,4 +224,5 @@ def support_test_point(point):
     if test_point == 'yes':
     if test_point == 'yes':
         return True
         return True
     else:
     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 aliyunsdkcore.request import CommonRequest
 from aliyunsdkunimkt.request.v20181207 import PopUpQueryRequest, QueryPromotionRequest
 from aliyunsdkunimkt.request.v20181207 import PopUpQueryRequest, QueryPromotionRequest
 from aliyunsdkunimkt.request.v20181212 import QueryUnionPromotionRequest, GetUnionTaskStatusRequest, \
 from aliyunsdkunimkt.request.v20181212 import QueryUnionPromotionRequest, GetUnionTaskStatusRequest, \
-    CreateProxyBrandUserRequest, QueryUnionSumChannelDataRequest, QueryContentListRequest, QueryTaskRuleLimitRequest, \
-    CreateUnionTaskRequest, EndUnionTaskRequest, QueryUnionTaskInfoRequest, QueryUnionTaskListRequest, \
     RegistDeviceRequest
     RegistDeviceRequest
 from django.conf import settings
 from django.conf import settings
 from requests import ConnectionError
 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'
 _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):
 class DingDingRobot(object):
-    def __init__(self, url=_DINGDING_TALK_URL):
+    def __init__(self, url = _DINGDING_TALK_URL):
         self.url = url
         self.url = url
 
 
     def send_msg(self, msg):
     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
     ~~~~~~~~~ad
 
 
 """
 """
-import logging
-import random
-from collections import OrderedDict
 
 
 import datetime
 import datetime
+import logging
+import sys
+from collections import OrderedDict
 
 
 import simplejson as json
 import simplejson as json
-import sys
 import user_agents
 import user_agents
-from aliyunsdkcore.client import AcsClient
-from aliyunsdkunimkt.request.v20181212 import RegistDeviceRequest, ChargeLaunchRequest
 from bson.objectid import ObjectId
 from bson.objectid import ObjectId
-from django.conf import settings
 from django.contrib.auth.hashers import make_password
 from django.contrib.auth.hashers import make_password
 from django.utils.module_loading import import_string
 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 typing import Union, List, AnyStr, Dict, Any, Optional, TYPE_CHECKING
 
 
 from apilib.utils_datetime import today_format_str, yesterday_format_str
 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.constant import Const, AdType, AdSpace
 from apps.web.core.db import CustomizedSequenceField
 from apps.web.core.db import CustomizedSequenceField
 from apps.web.core.db import Searchable
 from apps.web.core.db import Searchable
-
 from apps.web.device.models import Device
 from apps.web.device.models import Device
-
 from apps.web.utils import CustomizedValidationError, detect_app_from_ua, is_user
 from apps.web.utils import CustomizedValidationError, detect_app_from_ua, is_user
 
 
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from apps.web.dealer.models import Dealer
     from apps.web.dealer.models import Dealer
     from apps.web.user.models import MyUser
     from apps.web.user.models import MyUser
     from apps.web.device.models import DeviceDict
     from apps.web.device.models import DeviceDict
+    from library.mongo_django_auth_backport.auth import User
 
 
 
 
 def nullable(obj, nil=''):
 def nullable(obj, nil=''):
@@ -137,7 +133,7 @@ class Advertisement(UserSearchable):
     startTime = DateTimeField(verbose_name="开始时间", default=datetime.datetime.now)
     startTime = DateTimeField(verbose_name="开始时间", default=datetime.datetime.now)
     endTime = DateTimeField(verbose_name="结束时间", default=datetime.datetime.now() + datetime.timedelta(days=365))
     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="")
     provinceId = StringField(verbose_name="省份ID", default="")
     cityId = StringField(verbose_name="城市ID", default="")
     cityId = StringField(verbose_name="城市ID", default="")
     areaId = StringField(verbose_name="区域ID", default="")
     areaId = StringField(verbose_name="区域ID", default="")
@@ -529,7 +525,7 @@ class Advertisement(UserSearchable):
                 'adShow': 'show',
                 'adShow': 'show',
                 'url': ruhui['url'],
                 'url': ruhui['url'],
                 'showType': 'floatRedpack',
                 'showType': 'floatRedpack',
-                'img': 'https://resource.washpayer.com/ad/redpack2.png',
+                'img': 'https://cdn.washpayer.com/promotion/redpack2.png',
                 'title': '点击入会天猫领取红包',
                 'title': '点击入会天猫领取红包',
             }
             }
             logger.debug(
             logger.debug(
@@ -549,10 +545,10 @@ class Advertisement(UserSearchable):
             return None
             return None
 
 
         elif showType in ['floatJump', 'float']:
         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:
         else:
-            return 'https://resource.washpayer.com/ad/payafter_banner_2.png'
+            return 'https://cdn.washpayer.com/promotion/payafter_banner_2.png'
 
 
     @classmethod
     @classmethod
     def _fetch_payafter_ad(cls, ua, user, dealer, device=None):
     def _fetch_payafter_ad(cls, ua, user, dealer, device=None):
@@ -613,7 +609,7 @@ class Advertisement(UserSearchable):
 
 
     @classmethod
     @classmethod
     def fetch_payafter_ad(cls, ua, user, dealer, device=None):
     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
         ad_dict = None
 
 

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

@@ -290,6 +290,8 @@ def getDealerList(request):
     return JsonResponse({'result': 1, 'description': '', 'payload': dealers})
     return JsonResponse({'result': 1, 'description': '', 'payload': dealers})
 
 
 
 
+
+
 @error_tolerate(nil = JsonErrorResponse(u'获取地址列表失败'))
 @error_tolerate(nil = JsonErrorResponse(u'获取地址列表失败'))
 @permission_required(ROLE.manager)
 @permission_required(ROLE.manager)
 def getAddressList(request):
 def getAddressList(request):
@@ -636,17 +638,18 @@ def exportExcel(request):
                                               user = manager,
                                               user = manager,
                                               **(query.raw))
                                               **(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
     #: make queryAttrs serializable
     queryAttrs = query.attrs
     queryAttrs = query.attrs
     queryAttrs['dateTimeAdded__lte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__lte'])
     queryAttrs['dateTimeAdded__lte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__lte'])
     queryAttrs['dateTimeAdded__gte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__gte'])
     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,
     task_caller(func_name = offline_task.process_func_name,
                 offline_task_id = str(offline_task.id),
                 offline_task_id = str(offline_task.id),
                 filepath = file_path,
                 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_DISABLE_AD = 'dealer_disable_ad'
     DEALER_DEVICE_FEE = 'dealer_device_fee'
     DEALER_DEVICE_FEE = 'dealer_device_fee'
     INSURANCE = 'insurance'
     INSURANCE = 'insurance'
-    REDPACK = 'redpack'
     REFUND_CASH = 'refundCash'
     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_REDPACK: AGENT_INCOME_SOURCE.REDPACK,
 
 
     USER_RECHARGE_TYPE.RECHARGE_INSURANCE: AGENT_INCOME_SOURCE.INSURANCE,
     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 = {
 AgentConst.MAP_SOURCE_TO_TYPE = {
     AGENT_INCOME_SOURCE.AD: AGENT_INCOME_TYPE.AD,
     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_WITHDRAW_FEE: AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE,
     AGENT_INCOME_SOURCE.DEALER_CARD_FEE: AGENT_INCOME_TYPE.DEALER_CARD_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_API_QUOTA: AGENT_INCOME_TYPE.DEALER_API_QUOTA,
     AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
     AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
 
 
     AGENT_INCOME_SOURCE.INSURANCE: AGENT_INCOME_TYPE.INSURANCE,
     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.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 = {
 AgentConst.MAP_TYPE_TO_FIELD = {

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

@@ -363,7 +363,8 @@ class Agent(CapitalUser):
                     "northIp": ip,
                     "northIp": ip,
                     "northPort": port
                     "northPort": port
                 }
                 }
-            except Exception:
+            except Exception as e:
+                logger.error("agent filter got an error = {}".format(e))
                 pass
                 pass
 
 
             item = {
             item = {
@@ -633,16 +634,19 @@ class Agent(CapitalUser):
         为了方便识别,如果获取默认代理商失败,需要报错为默认代理商找不到
         为了方便识别,如果获取默认代理商失败,需要报错为默认代理商找不到
         """
         """
         try:
         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:
         except DoesNotExist:
             raise PrimaryAgentDoesNotExist('failed to get default primary agent')
             raise PrimaryAgentDoesNotExist('failed to get default primary agent')
 
 
     @property
     @property
     def inhouse_prime_agent(self):
     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
             return self
         else:
         else:
             return Agent.get_inhouse_prime_agent()
             return Agent.get_inhouse_prime_agent()
+    @staticmethod
+    def inhouse_prime_agent_id():
+        return str(settings.MY_PRIMARY_AGENT_ID)
 
 
     @property
     @property
     def customizedCashflowAllowable(self):
     def customizedCashflowAllowable(self):
@@ -880,25 +884,26 @@ class Agent(CapitalUser):
     @classmethod
     @classmethod
     def withdraw_gateway_list(cls, source_key):
     def withdraw_gateway_list(cls, source_key):
         is_ledger, pay_app_type, occupant_id, tokens = cls._parse_source_key(source_key)
         is_ledger, pay_app_type, occupant_id, tokens = cls._parse_source_key(source_key)
+
         if not is_ledger:
         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()
         agent = cls.objects(id = occupant_id).first()
         if not agent:
         if not agent:
             raise UserServerException(u'系统配置错误(第三方支付),请联系平台客服(1002)')
             raise UserServerException(u'系统配置错误(第三方支付),请联系平台客服(1002)')
 
 
+        if pay_app_type != PayAppType.WECHAT:
+            raise UserServerException(u'系统配置错误(第三方支付),请联系平台客服(1001)')
+
         if not agent.withdrawApps:
         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:
         else:
             if source_key not in agent.withdrawApps:
             if source_key not in agent.withdrawApps:
@@ -906,19 +911,25 @@ class Agent(CapitalUser):
 
 
             withdraw_entity = agent.withdrawApps[source_key]  # type: WithdrawEntity
             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.occupantId = str(agent.id)
             withdraw_entity.wechat_app.occupant = agent
             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
     @staticmethod
     def get_platform_wechat_manager_app():
     def get_platform_wechat_manager_app():
@@ -1280,11 +1291,26 @@ class Agent(CapitalUser):
         """
         """
         return self.incr_income(source, source_key, -money)
         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,
     def new_withdraw_record(self, withdraw_gateway, pay_entity, source_key, income_type, amount, pay_type, manual,
                             recurrent):
                             recurrent):
         # type: (WithdrawGateway, WithdrawBankCard, str, str, RMB, str, bool, bool) -> WithdrawRecord
         # 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
             withdraw_fee_ratio = Permillage('0.00')  # type: Permillage
         else:
         else:
             withdraw_fee_ratio = Const.PLATFORM_DEFAULT_WITHDRAW_FEE_RATIO  # type: Permillage
             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
         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,
         return WithdrawRecord.create(self,
                                      withdraw_gateway = withdraw_gateway,
                                      withdraw_gateway = withdraw_gateway,
                                      pay_entity = pay_entity,
                                      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):
     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:
     if not is_ledger:
         return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10005)')
         return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10005)')
 
 
     wechat_withdraw_gateway = withdraw_gateway_list['wechat']
     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:
         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'系统错误'))
 @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 apilib.utils_sys import memcache_lock, ThreadLock
 from apps.web import district
 from apps.web import district
 from apps.web.agent.define import AGENT_INCOME_TYPE
 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.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, \
 from apps.web.core.db import Searchable, StrictDictField, MonetaryField, RoleBaseDocument, PermillageField, \
     AccuracyMoneyField, BooleanIntField, BaseDocument
     AccuracyMoneyField, BooleanIntField, BaseDocument
 from apps.web.core.exceptions import ImproperlyConfigured
 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.dealer.define import DEALER_INCOME_TYPE
 from apps.web.utils import LimitAttemptsManager
 from apps.web.utils import LimitAttemptsManager
 from library.misc import BankAPI
 from library.misc import BankAPI
@@ -36,6 +36,8 @@ if TYPE_CHECKING:
     from pymongo.results import UpdateResult
     from pymongo.results import UpdateResult
     from apps.web.device.models import DeviceDict, GroupDict
     from apps.web.device.models import DeviceDict, GroupDict
     from apps.web.core import PayAppBase
     from apps.web.core import PayAppBase
+    from apps.web.user.models import RechargeRecord
+    from apps.web.dealer.models import DealerRechargeRecord
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -691,6 +693,14 @@ class CapitalUser(UserSearchable):
 
 
     bankWithdrawFee = BooleanField(verbose_name = u"银行卡提现手续开关(资金池代理商开关和这个开关为双开关)", default = True)
     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
     @classmethod
     def income_field_name(cls, income_type):
     def income_field_name(cls, income_type):
         # type: (str)->str
         # type: (str)->str
@@ -895,6 +905,35 @@ class CapitalUser(UserSearchable):
             total += self.sub_frozen_balance(income_type)
             total += self.sub_frozen_balance(income_type)
         return total
         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):
     def _freeze_balance(self, income_type, money, source_key, transaction_id, freeze_type):
         assert source_key, 'gateway is null'
         assert source_key, 'gateway is null'
         assert transaction_id, 'transaction id 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")
         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:
         :param transaction_id:
@@ -1006,6 +1045,9 @@ class CapitalUser(UserSearchable):
         # type: () -> basestring
         # type: () -> basestring
         raise NotImplementedError()
         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,
     def new_withdraw_record(self, withdraw_gateway, pay_entity, source_key, income_type, amount, pay_type, manual,
                             recurrent):
                             recurrent):
         # type: (WithdrawGateway, WithdrawBankCard, str, str, RMB, str, bool, bool) -> WithdrawRecord
         # type: (WithdrawGateway, WithdrawBankCard, str, str, RMB, str, bool, bool) -> WithdrawRecord
@@ -1082,8 +1124,33 @@ class CapitalUser(UserSearchable):
     def auto_withdraw_bound_open_id(self):
     def auto_withdraw_bound_open_id(self):
         raise NotImplementedError()
         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
     @property
     def current_wallet_withdraw_source_key(self):
     def current_wallet_withdraw_source_key(self):
@@ -1095,7 +1162,7 @@ class CapitalUser(UserSearchable):
     def withdraw_support(self, source_key):
     def withdraw_support(self, source_key):
         from apps.web.agent.models import Agent
         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:
         if not is_ledger:
             return {
             return {
@@ -1112,22 +1179,24 @@ class CapitalUser(UserSearchable):
                 }
                 }
             }
             }
 
 
-        dealerBankWithdrawFee = withdraw_gateway_list['wechat'].occupant.dealerBankWithdrawFee
-
         rv = {
         rv = {
             'wechat': {
             '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
                 'realName': self.withdraw_wechat_real_name
             },
             },
             'alipay': {
             '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,
                 'realName': self.withdraw_alipay_real_name,
                 'loginId': self.withdraw_alipay_login_id
                 'loginId': self.withdraw_alipay_login_id
             },
             },
             'bank': {
             'bank': {
-                'support': True,
+                'support': withdraw_gateway_list['wechat'].support_withdraw_bank or withdraw_gateway_list[
+                    'alipay'].support_withdraw_bank,
                 'cards': self.withdraw_bank_cards,
                 '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],
         return cls.objects(status__in = [WithdrawStatus.PROCESSING, WithdrawStatus.BANK_PROCESSION],
                            payType = WITHDRAW_PAY_TYPE.BANK, manual = False)
                            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
     @classmethod
     def get_failed_records(cls):
     def get_failed_records(cls):
         return cls.objects(status = WithdrawStatus.FAILED, refunded = False, manual = False)
         return cls.objects(status = WithdrawStatus.FAILED, refunded = False, manual = False)
@@ -1417,7 +1490,12 @@ class WithdrawRecord(Searchable):
     @staticmethod
     @staticmethod
     def is_my(order_no):
     def is_my(order_no):
         try:
         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:
         except Exception as e:
             return False
             return False
 
 
@@ -1426,30 +1504,19 @@ class WithdrawRecord(Searchable):
         # type: (str)->None
         # type: (str)->None
         return memcache_lock('WithdrawRecord_%s' % order, 'processing')
         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
     @classmethod
     def count_today(cls, ownerId, role = ROLE.dealer):
     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):
 class PushRecord(Searchable):
@@ -1864,10 +1931,14 @@ class OrderRecordBase(Searchable):
         if hasattr(self, _attr_name):
         if hasattr(self, _attr_name):
             return getattr(self, _attr_name)
             return getattr(self, _attr_name)
         else:
         else:
-            device = Device.get_dev(self.devNo)
+            device = Device.get_dev_by_logicalCode(self.logicalCode)
             setattr(self, _attr_name, device)
             setattr(self, _attr_name, device)
             return device
             return device
 
 
+    @device.setter
+    def device(self, value):
+        setattr(self, '__device__', value)
+
     @property
     @property
     def dev_type_code(self):
     def dev_type_code(self):
         if self.devTypeCode:
         if self.devTypeCode:
@@ -2219,3 +2290,343 @@ class WithdrawBankCard(DynamicDocument):
                 phone = phone, provinceCode = provinceCode, province = province,
                 phone = phone, provinceCode = provinceCode, province = province,
                 cityCode = cityCode, city = city, branchBankCode = branchBankCode,
                 cityCode = cityCode, city = city, branchBankCode = branchBankCode,
                 branchBankName = branchBankName, cnapsCode = cnapsCode)
                 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
     _META_MODEL = DealerIncomeProxy
 
 
     @classmethod
     @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:
         if 'ref_id' in kwargs:
             return super(ClientDealerIncomeModelProxy, cls).get_one(foreign_id = str(kwargs.get('ref_id')), **kwargs)
             return super(ClientDealerIncomeModelProxy, cls).get_one(foreign_id = str(kwargs.get('ref_id')), **kwargs)
         else:
         else:

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

@@ -90,7 +90,7 @@ class WithdrawHandler(object):
             self.payee.recover_frozen_balance(
             self.payee.recover_frozen_balance(
                 self.record.incomeType,
                 self.record.incomeType,
                 self.record.amount,
                 self.record.amount,
-                self.record.source_key,
+                self.record.withdrawSourceKey,
                 self.record.order,
                 self.record.order,
                 )
                 )
 
 
@@ -110,10 +110,7 @@ class WithdrawHandler(object):
 
 
         success = self.record.succeed(**kwargs)
         success = self.record.succeed(**kwargs)
         if success:
         if success:
-            self.payee.clear_frozen_balance(
-                self.record.incomeType
-            )
-
+            self.payee.clear_frozen_balance(str(self.record.id))
             self.on_approve()
             self.on_approve()
         else:
         else:
             raise WithdrawError(u'更新提现状态失败(1002)')
             raise WithdrawError(u'更新提现状态失败(1002)')
@@ -155,7 +152,7 @@ def translate_withdraw_state(state):
 
 
 class OrderNoMaker(object):
 class OrderNoMaker(object):
     @classmethod
     @classmethod
-    def _order_prefix(cls, main_type, sub_type):
+    def make_prefix(cls, main_type, sub_type):
         return '{}{}'.format(main_type, sub_type)
         return '{}{}'.format(main_type, sub_type)
 
 
     @classmethod
     @classmethod
@@ -163,13 +160,22 @@ class OrderNoMaker(object):
         # type: (str, str, str)->str
         # type: (str, str, str)->str
 
 
         # time:14,prefix:2,identifier:13,resered:3
         # time:14,prefix:2,identifier:13,resered:3
-
         return '{time}{prefix}{identifier}{reserved}'.format(
         return '{time}{prefix}{identifier}{reserved}'.format(
             time = datetime.datetime.now().strftime("%Y%m%d%H%M%S"),
             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(),
             identifier = '{:0>13}'.format(identifier[-13:]).upper(),
             reserved = get_random_str(3, string.digits + string.uppercase))
             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):
 class OrderMainType(StrEnum):
     PAY = 'P'  # 支付订单
     PAY = 'P'  # 支付订单
@@ -186,6 +192,7 @@ class OrderMainType(StrEnum):
 
 
 class RefundSubType(IterConstant):
 class RefundSubType(IterConstant):
     REFUND = 'R'
     REFUND = 'R'
+    REVOKE = 'V'
 
 
 
 
 class UserPaySubType(IterConstant):
 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 typing import TYPE_CHECKING, cast
 
 
 from apilib.systypes import StrEnum, Singleton
 from apilib.systypes import StrEnum, Singleton
-from apilib.utils_json import JsonResponse
 from apps import lockCache
 from apps import lockCache
 from apps.web.core import PayAppType
 from apps.web.core import PayAppType
 from apps.web.core.payment import PaymentGateway
 from apps.web.core.payment import PaymentGateway
@@ -27,8 +26,7 @@ if TYPE_CHECKING:
 
 
     RefundRecordT = Union[RefundDealerRechargeRecord, RefundMoneyRecord]
     RefundRecordT = Union[RefundDealerRechargeRecord, RefundMoneyRecord]
 
 
-    from apps.web.core.payment import PaymentGatewayT
-
+    from apps.web.core.payment.type_checking import PaymentGatewayT
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -111,7 +109,7 @@ class PayRecordPoller(object):
             logger.error('record<id={}> is not exist.'.format(self.record_id))
             logger.error('record<id={}> is not exist.'.format(self.record_id))
             return
             return
 
 
-        if record.is_success():
+        if record.is_success:
             logger.error('record<id={},order={}> is success.'.format(self.record_id, record.orderNo))
             logger.error('record<id={},order={}> is success.'.format(self.record_id, record.orderNo))
             return
             return
 
 
@@ -206,7 +204,7 @@ class PayPullUp(object):
 
 
     @classmethod
     @classmethod
     def create(cls, factory_cls, payment_gateway, record, **payload):
     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)
         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'),
                 '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: {
             PayAppType.ALIPAY: {
                 'poller': import_string('apps.web.common.transaction.pay.alipay.AliPayRecordPoller'),
                 'poller': import_string('apps.web.common.transaction.pay.alipay.AliPayRecordPoller'),
                 'notifier': import_string('apps.web.common.transaction.pay.alipay.AliPayNotifier'),
                 'notifier': import_string('apps.web.common.transaction.pay.alipay.AliPayNotifier'),
@@ -250,28 +243,3 @@ class PayManager(Singleton):
             raise UserServerException(u'第三方支付配置错误,请联系平台客服(1002)')
             raise UserServerException(u'第三方支付配置错误,请联系平台客服(1002)')
 
 
         return self.map_dict[pay_app_type]['pullup']
         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
 from taskmanager.mediator import task_caller
 
 
 if TYPE_CHECKING:
 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.core.payment.ali import AliPayGateway, AliPayWithdrawGateway
     from apps.web.common.transaction.pay import RechargeRecordT
     from apps.web.common.transaction.pay import RechargeRecordT
     from typing import Dict
     from typing import Dict
@@ -139,7 +139,7 @@ class AliPayNotifier(PayNotifier):
                     'no such record. orderNo = {}'.format(order_no))
                     'no such record. orderNo = {}'.format(order_no))
                 return HttpResponse('success')
                 return HttpResponse('success')
 
 
-            if record.is_success():
+            if record.is_success:
                 logger.error('record has been finished. orderNo = {}'.format(order_no))
                 logger.error('record has been finished. orderNo = {}'.format(order_no))
                 return HttpResponse('success')
                 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.core.utils import async_operation
 from apps.web.exceptions import UserServerException
 from apps.web.exceptions import UserServerException
 from library.wechatpy.constants import WeChatErrorCode
 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
 import simplejson as json
 from apps.web.core.payment.wechat import WechatPaymentGateway
 from apps.web.core.payment.wechat import WechatPaymentGateway
 from taskmanager.mediator import task_caller
 from taskmanager.mediator import task_caller
@@ -138,7 +138,7 @@ class WechatPayNotifier(PayNotifier):
                 logger.error('no such record. orderNo = {}'.format(out_trade_no))
                 logger.error('no such record. orderNo = {}'.format(out_trade_no))
                 return self.reply("OK", True)
                 return self.reply("OK", True)
 
 
-            if record.is_success():
+            if record.is_success:
                 logger.info('recharge record has done. orderNo = {}'.format(out_trade_no))
                 logger.info('recharge record has done. orderNo = {}'.format(out_trade_no))
                 return self.reply("OK", True)
                 return self.reply("OK", True)
 
 
@@ -148,7 +148,7 @@ class WechatPayNotifier(PayNotifier):
                 record.pay_app_type)  # type: WechatPaymentGateway
                 record.pay_app_type)  # type: WechatPaymentGateway
 
 
             if not payment_gateway.check(raw):
             if not payment_gateway.check(raw):
-                raise WeChatException(
+                raise InvalidSignatureException(
                     errCode = WeChatErrorCode.MY_ERROR_SIGNATURE,
                     errCode = WeChatErrorCode.MY_ERROR_SIGNATURE,
                     errMsg = u'支付通知签名错误',
                     errMsg = u'支付通知签名错误',
                     client = payment_gateway.client
                     client = payment_gateway.client
@@ -224,6 +224,7 @@ class WechatPullUp(PayPullUp):
     """
     """
     def do(self):  # type: ()->HttpResponse
     def do(self):  # type: ()->HttpResponse
         OrderCacheMgr(self.record).initial()
         OrderCacheMgr(self.record).initial()
+
         try:
         try:
             data = self.payment_gateway.generate_js_payment_params(
             data = self.payment_gateway.generate_js_payment_params(
                 payOpenId = self.openId,
                 payOpenId = self.openId,

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

@@ -1,17 +1,26 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 
 
+import datetime
 import logging
 import logging
 
 
 from django.http import HttpResponse
 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 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:
 if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
     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__)
 logger = logging.getLogger(__name__)
 
 
@@ -37,11 +46,11 @@ class RefundNotifier(object):
         """ 验证解密后的数据是否有效 """
         """ 验证解密后的数据是否有效 """
         raise NotImplementedError(u"需要实现")
         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"需要实现")
         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))
         logger.debug(u"refundNotifier {} do work, payload = {}".format(self.__class__.__name__, self.payload))
 
 
         if not self.verify_payload(self.payload):
         if not self.verify_payload(self.payload):
@@ -50,7 +59,7 @@ class RefundNotifier(object):
                     self.__class__.__name__, self.payload))
                     self.__class__.__name__, self.payload))
             return HttpResponse(self.successResponse)
             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:
         if not refundOrder:
             logger.info(
             logger.info(
                 u"refundNotifier {} record not refund, payload = {}".format(self.__class__.__name__, self.payload))
                 u"refundNotifier {} record not refund, payload = {}".format(self.__class__.__name__, self.payload))
@@ -62,7 +71,7 @@ class RefundNotifier(object):
                     self.__class__.__name__, refundOrder.id))
                     self.__class__.__name__, refundOrder.id))
                 return
                 return
 
 
-            self.handle_refund_order(refundOrder, post_pay)
+            self.handle_refund_order(refundOrder, post_refund)
 
 
             return HttpResponse(self.successResponse)
             return HttpResponse(self.successResponse)
 
 
@@ -77,7 +86,7 @@ class RefundNotifier(object):
         raise NotImplementedError(u"需要实现")
         raise NotImplementedError(u"需要实现")
 
 
     @property
     @property
-    def refund_order_no(self):  # type: ()->str
+    def refund_order_filter(self):  # type: ()->dict
         raise NotImplementedError(u"需要实现")
         raise NotImplementedError(u"需要实现")
 
 
 
 
@@ -87,5 +96,178 @@ class RefundPuller(object):
     def __init__(self, refundOrder):  # type:(RefundRecordT) -> None
     def __init__(self, refundOrder):  # type:(RefundRecordT) -> None
         self._refundOrder = refundOrder
         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:
 if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
     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__)
 logger = logging.getLogger(__name__)
 
 
@@ -21,22 +21,19 @@ class AliRefundNotifier(RefundNotifier):
         return request.POST.dict()  # type: Dict
         return request.POST.dict()  # type: Dict
 
 
     @property
     @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:
         if not matched:
             return
             return
 
 
-        return post_pay(refundOrder, datetimeRefund)
+        return refund_post_callable(refundOrder, True)
 
 
     @property
     @property
     def errorResponse(self):  # type:() -> str
     def errorResponse(self):  # type:() -> str
@@ -51,23 +48,29 @@ class AliRefundNotifier(RefundNotifier):
 
 
 
 
 class AliRefundPuller(RefundPuller):
 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(
             logger.info(
                 "RefundPuller {} pull refund order {}, result code = {}, msg = {}".format(
                 "RefundPuller {} pull refund order {}, result code = {}, msg = {}".format(
                     self.__class__.__name__, self._refundOrder, result["code"], result["msg"]))
                     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
 # !/usr/bin/env python
 
 
 import datetime
 import datetime
+import logging
 
 
 import xmltodict
 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.common.transaction.refund import RefundNotifier, RefundPuller
 from apps.web.core.models import WechatPayApp
 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 library.wechatpy.pay import WeChatPay
-from apps.web.user.models import RefundMoneyRecord, RechargeRecord
+
+logger = logging.getLogger(__name__)
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
     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):
 class WechatRefundNotifier(RefundNotifier):
@@ -33,34 +34,45 @@ class WechatRefundNotifier(RefundNotifier):
         """ 忽略对签名串的校验 """
         """ 忽略对签名串的校验 """
         return True
         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
             return
 
 
         if self.payload["refund_status"] == "SUCCESS":
         if self.payload["refund_status"] == "SUCCESS":
             payFinishTime = self.payload["success_time"]
             payFinishTime = self.payload["success_time"]
             datetimeRefund = datetime.datetime.strptime(payFinishTime, "%Y-%m-%d %H:%M:%S")
             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:
             if not matched:
                 return
                 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
             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
     @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
     @property
     def errorResponse(self):  # type:() -> str
     def errorResponse(self):  # type:() -> str
@@ -74,43 +86,81 @@ class WechatRefundNotifier(RefundNotifier):
 
 
 
 
 class WechatRefundPuller(RefundPuller):
 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:
         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
 import arrow
 from django.conf import settings
 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.monetary import RMB
 from apilib.utils_sys import memcache_lock
 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 apps.web.exceptions import WithdrawOrderNotExist
 from library.alipay import AliPayGatewayException
 from library.alipay import AliPayGatewayException
 from library.alipay import AliPayServiceException
 from library.alipay import AliPayServiceException
-from library.wechatbase.exceptions import WechatNetworkException, WeChatPayException
+from library.wechatbase.exceptions import WechatNetworkException, WeChatException
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from contextlib import GeneratorContextManager
     from contextlib import GeneratorContextManager
     from apps.web.common.models import CapitalUser
     from apps.web.common.models import CapitalUser
     from apps.web.common.models import WithdrawRecord
     from apps.web.common.models import WithdrawRecord
     from apps.web.core.payment.wechat import WechatWithdrawQueryResult
     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__)
 logger = logging.getLogger(__name__)
 
 
@@ -214,7 +214,8 @@ def withdraw_via_bank_in_wechat(gateway, record, bankcard):
         else:
         else:
             return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
             return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
 
 
-    except WeChatPayException as e:
+    except WeChatException as e:
+        # 接口性质的失败
         logger.error(repr(e))
         logger.error(repr(e))
         return error_handler(e.errCode, e.errMsg)
         return error_handler(e.errCode, e.errMsg)
 
 
@@ -301,7 +302,7 @@ def withdraw_via_wechat(gateway, record, payOpenId, real_user_name):
             else:
             else:
                 return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
                 return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
 
 
-        except WeChatPayException as e:
+        except WeChatException as e:
             logger.error(repr(e))
             logger.error(repr(e))
             return error_handler(e.errCode, e.errMsg)
             return error_handler(e.errCode, e.errMsg)
 
 
@@ -359,7 +360,7 @@ def withdraw_via_wechat(gateway, record, payOpenId, real_user_name):
             else:
             else:
                 return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
                 return WithdrawResult(False, 0, False, u'微信通讯错误', u'微信通讯错误,请联系客服确认提现是否成功。')
 
 
-        except WeChatPayException as e:
+        except WeChatException as e:
             logger.error(repr(e))
             logger.error(repr(e))
             return error_handler(e.errCode, e.errMsg)
             return error_handler(e.errCode, e.errMsg)
 
 
@@ -393,19 +394,6 @@ class WithdrawService(object):
                 logger.info('test environment do not support withdraw.')
                 logger.info('test environment do not support withdraw.')
                 raise ServiceException({'result': 0, 'description': u'测试环境不允许提现', 'payload': {}})
                 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:
             if self.payee.no_withdraw:
                 raise ServiceException(
                 raise ServiceException(
                     {'result': 0, 'description': u"您的账号权限不足或者异常,暂时不能提现。", 'payload': {}})
                     {'result': 0, 'description': u"您的账号权限不足或者异常,暂时不能提现。", 'payload': {}})
@@ -413,24 +401,36 @@ class WithdrawService(object):
             if self.payee.abnormal:
             if self.payee.abnormal:
                 raise ServiceException({'result': 0, 'description': u'该帐号资金异常,请联系客服处理', 'payload': {}})
                 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:
             with withdraw_lock(self.payee.role, str(self.payee.id)) as acquired:
                 if acquired:
                 if acquired:
                     if self.payee.sub_balance(self.income_type, source_key) < self.amount:
                     if self.payee.sub_balance(self.income_type, source_key) < self.amount:
                         raise ServiceException({'result': 0, 'description': u'余额不足', 'payload': {}})
                         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:
                     if not is_ledger:
                         raise ServiceException({'result': 0, 'description': u'暂时不支持提现(1001)', 'payload': {}})
                         raise ServiceException({'result': 0, 'description': u'暂时不支持提现(1001)', 'payload': {}})
 
 
                     if self.pay_type == WITHDRAW_PAY_TYPE.WECHAT:
                     if self.pay_type == WITHDRAW_PAY_TYPE.WECHAT:
                         if withdraw_gateway_list['wechat'].manual_withdraw:
                         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(
                         pay_entity = WithdrawBankCard(
                             accountCode = self.payee.withdraw_open_id,
                             accountCode = self.payee.withdraw_open_id,
@@ -454,11 +454,12 @@ class WithdrawService(object):
 
 
                         handler = self.payee.new_withdraw_handler(self.record)
                         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:
                         if not updated:
                             handler.revoke(remarks = u'扣款失败', description = u'扣款失败')
                             handler.revoke(remarks = u'扣款失败', description = u'扣款失败')
                             raise ServiceException({'result': 0, 'description': u'扣款失败', 'payload': {}})
                             raise ServiceException({'result': 0, 'description': u'扣款失败', 'payload': {}})
@@ -474,7 +475,11 @@ class WithdrawService(object):
                                 self.record.order, repr(withdraw_result)))
                                 self.record.order, repr(withdraw_result)))
 
 
                         if withdraw_result.result is True:
                         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,
                             return {'result': 1, 'description': withdraw_result.show_message,
                                     'payload': {'paymentId': str(self.record.id)}}
                                     'payload': {'paymentId': str(self.record.id)}}
                         else:
                         else:
@@ -503,37 +508,38 @@ class WithdrawService(object):
                         manual = False
                         manual = False
                         withdraw_gateway = withdraw_gateway_list['wechat']
                         withdraw_gateway = withdraw_gateway_list['wechat']
 
 
-                        if bank_card.manual:
+                        if bank_card.manual or withdraw_gateway_list['wechat'].manual_withdraw:
                             manual = True
                             manual = True
 
 
                         elif bank_card.accountType == WithdrawBankCard.AccountType.PUBLIC:
                         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
                                 withdraw_gateway = withdraw_gateway_list['alipay']  # type: WithdrawGateway
                                 if not WithdrawBanks.support(bank_card.bankName):
                                 if not WithdrawBanks.support(bank_card.bankName):
                                     raise ServiceException({
                                     raise ServiceException({
                                         'result': 0,
                                         'result': 0,
                                         'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1001)',
                                         'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1001)',
                                         'payload': {}})
                                         'payload': {}})
+
                             else:
                             else:
-                                manual = True
+                                raise ServiceException({
+                                    'result': 0,
+                                    'description': u'不支持提现到对公银行卡, 请联系平台客服(1001)',
+                                    'payload': {}})
                         else:
                         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)
                                     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,
                                             'result': 0,
-                                            'description': u'不支持提现到此银行卡或者银行名称错误, 请联系平台客服(1002)',
+                                            'description': u'不支持提现到银行卡, 请联系平台客服(1002)',
                                             'payload': {}})
                                             'payload': {}})
 
 
                         self.record = self.payee.new_withdraw_record(
                         self.record = self.payee.new_withdraw_record(
@@ -554,7 +560,7 @@ class WithdrawService(object):
                         updated = self.payee.freeze_balance(
                         updated = self.payee.freeze_balance(
                             self.record.incomeType,
                             self.record.incomeType,
                             self.record.amount,
                             self.record.amount,
-                            self.record.source_key,
+                            self.record.withdrawSourceKey,
                             self.record.order
                             self.record.order
                         )
                         )
                         if not updated:
                         if not updated:
@@ -602,6 +608,13 @@ class WithdrawService(object):
                                      'payload': {}})
                                      'payload': {}})
 
 
                     elif self.pay_type == WITHDRAW_PAY_TYPE.ALIPAY:
                     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(
                         pay_entity = WithdrawBankCard(
                             accountCode = self.payee.withdraw_alipay_login_id,
                             accountCode = self.payee.withdraw_alipay_login_id,
                             accountName = self.payee.withdraw_alipay_real_name,
                             accountName = self.payee.withdraw_alipay_real_name,
@@ -611,14 +624,7 @@ class WithdrawService(object):
                         if not pay_entity.accountCode:
                         if not pay_entity.accountCode:
                             raise ServiceException({
                             raise ServiceException({
                                 'result': 0,
                                 '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': {}})
                                 'payload': {}})
 
 
                         self.record = self.payee.new_withdraw_record(
                         self.record = self.payee.new_withdraw_record(
@@ -639,7 +645,7 @@ class WithdrawService(object):
                         updated = self.payee.freeze_balance(
                         updated = self.payee.freeze_balance(
                             self.record.incomeType,
                             self.record.incomeType,
                             self.record.amount,
                             self.record.amount,
-                            self.record.source_key,
+                            self.record.withdrawSourceKey,
                             self.record.order,
                             self.record.order,
                         )
                         )
                         if not updated:
                         if not updated:
@@ -682,7 +688,7 @@ class WithdrawService(object):
                 else:
                 else:
                     raise ServiceException({'result': 0, 'description': u'操作频繁,请稍后再试', 'payload': {}})
                     raise ServiceException({'result': 0, 'description': u'操作频繁,请稍后再试', 'payload': {}})
         except ServiceException as e:
         except ServiceException as e:
-            logger.exception(e)
+            logger.warning(str(e))
 
 
             if self.record:
             if self.record:
                 if 'payload' in e.result:
                 if 'payload' in e.result:
@@ -818,9 +824,8 @@ class WithdrawRetryService(object):
 
 
                 payee.freeze_balance(self.record.incomeType,
                 payee.freeze_balance(self.record.incomeType,
                                      self.record.amount,
                                      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,
                 withdraw_result = withdraw_via_wechat(withdraw_gateway,
                                                       self.record,
                                                       self.record,
@@ -901,7 +906,7 @@ class WithdrawRetryService(object):
 
 
                 payee.freeze_balance(self.record.incomeType,
                 payee.freeze_balance(self.record.incomeType,
                                      self.record.amount,
                                      self.record.amount,
-                                     self.record.source_key,
+                                     self.record.withdrawSourceKey,
                                      self.record.order,
                                      self.record.order,
                                      )
                                      )
 
 
@@ -971,7 +976,7 @@ class WithdrawRetryService(object):
 
 
                 updated = payee.freeze_balance(self.record.incomeType,
                 updated = payee.freeze_balance(self.record.incomeType,
                                                self.record.amount,
                                                self.record.amount,
-                                               self.record.source_key,
+                                               self.record.withdrawSourceKey,
                                                self.record.order,
                                                self.record.order,
                                                )
                                                )
                 if not updated:
                 if not updated:
@@ -1017,7 +1022,7 @@ class WithdrawRetryService(object):
         except WechatNetworkException as e:
         except WechatNetworkException as e:
             logger.exception(e)
             logger.exception(e)
             return {'result': 0, 'description': e.errMsg, 'payload': {}}
             return {'result': 0, 'description': e.errMsg, 'payload': {}}
-        except WeChatPayException as e:
+        except WeChatException as e:
             logger.exception(e)
             logger.exception(e)
             return {'result': 0, 'description': e.errMsg, 'payload': {}}
             return {'result': 0, 'description': e.errMsg, 'payload': {}}
         except ServiceException as e:
         except ServiceException as e:

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

@@ -13,7 +13,6 @@ import urllib2
 import simplejson as json
 import simplejson as json
 import xmltodict
 import xmltodict
 from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
 from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
-from django.views.decorators.gzip import gzip_page
 from django.views.generic.base import View
 from django.views.generic.base import View
 from pymongo.results import UpdateResult
 from pymongo.results import UpdateResult
 from typing import TYPE_CHECKING, Iterable, Dict
 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.exceptions import InvalidFileSize, InvalidFileName
 from apps.web.core.file import AliOssFileUploader, WechatSubscriptionAccountVerifyFileUploader
 from apps.web.core.file import AliOssFileUploader, WechatSubscriptionAccountVerifyFileUploader
 from apps.web.core.models import WechatPayApp, AliApp
 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.core.utils import DefaultJsonErrorResponse, JsonOkResponse, JsonErrorResponse
 from apps.web.dealer.models import Dealer
 from apps.web.dealer.models import Dealer
 from apps.web.device.models import Device
 from apps.web.device.models import Device
@@ -56,6 +55,7 @@ if TYPE_CHECKING:
     from django.core.handlers.wsgi import WSGIRequest
     from django.core.handlers.wsgi import WSGIRequest
     from apps.web.device.models import DeviceDict
     from apps.web.device.models import DeviceDict
     from apps.web.core.adapter.bolai_gateway import ChargingGatewayBox
     from apps.web.core.adapter.bolai_gateway import ChargingGatewayBox
+    from apps.web.core.payment.ali import AliPayGateway
 
 
 logger = logging.getLogger(__name__)
 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.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 = {
 Const.CELERY_TASK_RESULT_TRANSLATION = {
     'PENDING': u'处理中',
     'PENDING': u'处理中',
     'STARTED': u'已开启',
     'STARTED': u'已开启',
@@ -666,18 +611,6 @@ Const.CELERY_TASK_RESULT_TRANSLATION = {
     'UNKNOWN': u'未知'
     '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 = {
 Const.TRANSLATION = {
     'feedback_process': u'故障处理完成提醒',
     'feedback_process': u'故障处理完成提醒',
     'feedback': u"用户报障",
     'feedback': u"用户报障",
@@ -697,6 +630,8 @@ Const.TRANSLATION = {
     'exchange_order_notify': u'售后服务通知',
     'exchange_order_notify': u'售后服务通知',
     'service_start': u'设备启动通知',
     'service_start': u'设备启动通知',
     'dev_start': u'启动通知',
     'dev_start': u'启动通知',
+    'charge_order_complete': u'充电订单完成提醒',
+    'common_order_complete': u'订单完成提醒'
 }
 }
 
 
 
 
@@ -865,7 +800,10 @@ class RechargeRecordVia(StrEnum):
     Insurance = "insurance"  # 支付保险的钱
     Insurance = "insurance"  # 支付保险的钱
     Redpack = "redpack"  # 红包抵扣
     Redpack = "redpack"  # 红包抵扣
     Mix = "mix"  # 混合单 (保险+充电)(红包抵扣+充电)
     Mix = "mix"  # 混合单 (保险+充电)(红包抵扣+充电)
-    RefundCash = "refundCash"  # 退款订单
+    RefundCash = "refundCash"  # 现金退款
+
+    RevokeRefundCash = 'revokeRefundCash'  # 现金退款退单
+
     Swap = 'swap'  # 互联互通
     Swap = 'swap'  # 互联互通
     AutoSim = 'autoSim'
     AutoSim = 'autoSim'
 
 
@@ -880,7 +818,8 @@ RECHARGE_RECORD_VIA_TRANSLATION = {
     RechargeRecordVia.MonthlyPackage: u"包月卡充值",
     RechargeRecordVia.MonthlyPackage: u"包月卡充值",
     RechargeRecordVia.Insurance: u"保险",
     RechargeRecordVia.Insurance: u"保险",
     RechargeRecordVia.Redpack: u"红包抵扣",
     RechargeRecordVia.Redpack: u"红包抵扣",
-    RechargeRecordVia.RefundCash: u'退费',
+    RechargeRecordVia.RefundCash: u'现金退款',
+    RechargeRecordVia.RevokeRefundCash: u'现金退款退单',
     RechargeRecordVia.Swap: u'互联互通',
     RechargeRecordVia.Swap: u'互联互通',
     RechargeRecordVia.AutoSim: u"流量卡自动充值"
     RechargeRecordVia.AutoSim: u"流量卡自动充值"
 }
 }
@@ -910,18 +849,22 @@ class USER_RECHARGE_TYPE(IterConstant):
     RECHARGE_REDPACK = RechargeRecordVia.Redpack
     RECHARGE_REDPACK = RechargeRecordVia.Redpack
     RECHARGE_MIX = RechargeRecordVia.Mix
     RECHARGE_MIX = RechargeRecordVia.Mix
     REFUND_CASH = RechargeRecordVia.RefundCash
     REFUND_CASH = RechargeRecordVia.RefundCash
+    REVOKE_REFUND_CASH = RechargeRecordVia.RevokeRefundCash
 
 
     SWAP = RechargeRecordVia.Swap
     SWAP = RechargeRecordVia.Swap
 
 
 
 
 class DEALER_CONSUMPTION_AGG_KIND(IterConstant):
 class DEALER_CONSUMPTION_AGG_KIND(IterConstant):
     # : 消费聚合类别
     # : 消费聚合类别
+
     COIN = 'coin'
     COIN = 'coin'
+    SPEND_MONEY = 'spendMoney'  # 实际花费
+
     PACKAGE = 'package'  # 纸巾包
     PACKAGE = 'package'  # 纸巾包
     REFUNDED_COINS = 'refundedMoney'  # 退费金额(金币)
     REFUNDED_COINS = 'refundedMoney'  # 退费金额(金币)
     REFUNDED_CASH = 'refundedCash'  # 退费金额(元)
     REFUNDED_CASH = 'refundedCash'  # 退费金额(元)
     ELEC = 'elec'  # 消耗电量
     ELEC = 'elec'  # 消耗电量
-    SPEND_MONEY = 'spendMoney'  # 实际花费
+
     CONSUME_CARD = 'consumeCard'  # 刷卡消费额
     CONSUME_CARD = 'consumeCard'  # 刷卡消费额
     REFUND_CARD = 'refundCard'  # 刷卡退费额
     REFUND_CARD = 'refundCard'  # 刷卡退费额
     DURATION = 'duration'  # 充电时间
     DURATION = 'duration'  # 充电时间
@@ -930,8 +873,8 @@ class DEALER_CONSUMPTION_AGG_KIND(IterConstant):
     TOTAL_COUNT = 'totalCount'  # 线下投币
     TOTAL_COUNT = 'totalCount'  # 线下投币
     SERVICEFEE = 'serviceFee' # 服务费
     SERVICEFEE = 'serviceFee' # 服务费
 
 
-    SERVICE_CHARGE = 'serviceCharge'
-    ELEC_CHARGE = 'elecCharge'
+    SERVICE_CHARGE = 'serviceCharge' # 同SERVICEFEE,兼容需要,后续不在使用
+    ELEC_CHARGE = 'elecCharge' # ELECFEE,兼容需要,后续不在使用
 
 
 
 
 DEALER_CONSUMPTION_AGG_KIND_TRANSLATION = \
 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.TOTAL_COUNT: u'线下投币次数',
         DEALER_CONSUMPTION_AGG_KIND.SERVICEFEE: 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 = \
 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.REFUND_CARD: u'元',
         DEALER_CONSUMPTION_AGG_KIND.SERVICEFEE: 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 = \
 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.TOTAL_COUNT: '1',
         DEALER_CONSUMPTION_AGG_KIND.SERVICEFEE: '0.01',
         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}'
 MONTH_DATE_KEY = '{year:d}-{month:02d}'
@@ -1569,3 +1512,5 @@ class OneCardGateAction(object):
 
 
     def choice(self):
     def choice(self):
         return [self.ENTER, self.OUT]
         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_PROMOTION = "platform_promotion"
     PLATFORM_WALLET = 'platform_wallet'
     PLATFORM_WALLET = 'platform_wallet'
     PLATFORM_RECONCILE ='platform_reconcile'
     PLATFORM_RECONCILE ='platform_reconcile'
+    JD_OPEN = 'jdopen'
     MANUAL = 'manual'
     MANUAL = 'manual'
 
 
     SWAP = 'swap'
     SWAP = 'swap'
@@ -195,10 +196,6 @@ class AlipayMixin(_BaseMixin):
     def signKeyType(self):
     def signKeyType(self):
         return self.__app_for_inner__.signKeyType
         return self.__app_for_inner__.signKeyType
 
 
-    @property
-    def __gateway_type__(self):
-        return AppPlatformType.ALIPAY
-
     @property
     @property
     def __bound_openid_key__(self):
     def __bound_openid_key__(self):
         # 支付宝用户ID是唯一的,直接以平台类型区分
         # 支付宝用户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))
             logger.error("dev <{}> don't get a recharge record by id <{}>".format(self.device.devNo, rechargeRcdId))
             return
             return
 
 
-        if not record.is_success():
+        if not record.is_success:
             logger.debug('{} state is {}.'.format(str(record), record.result))
             logger.debug('{} state is {}.'.format(str(record), record.result))
             raise ValueError(u'订单不能为未支付状态')
             raise ValueError(u'订单不能为未支付状态')
 
 

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

@@ -1,17 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/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 PaymentGateway
 from apps.web.core.payment.base import WithdrawGateway
 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 RMB
 from apilib.monetary import quantize
 from apilib.monetary import quantize
 from apilib.utils_string import cn
 from apilib.utils_string import cn
+from apps.web.constant import AppPlatformType
 from apps.web.exceptions import WithdrawOrderNotExist
 from apps.web.exceptions import WithdrawOrderNotExist
 from apps.web.common.models import WithdrawBankCard
 from apps.web.common.models import WithdrawBankCard
 from apps.web.core import AlipayMixin
 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 apps.web.utils import testcase_point
 from library.alipay import AliPayGatewayException, AliErrorCode, AliException
 from library.alipay import AliPayGatewayException, AliErrorCode, AliException
 from library.alipay import AliPayServiceException
 from library.alipay import AliPayServiceException
@@ -90,10 +91,12 @@ class AliPayGateway(PaymentGateway, AlipayMixin):
         Alipay 支付网关,扩展原库没有的接口 ``alipay.trade.create``
         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)
         super(AliPayGateway, self).__init__(app)
 
 
+        self.__gateway_type__ = gateway_type
+
     def __repr__(self):
     def __repr__(self):
         return '<AliPayPaymentGateway(appid=%s, debug=%s)>' % (self.appid, self.debug)
         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,
         return self.client.api_alipay_data_dataservice_bill_downloadurl_query(bill_type = bill_type,
                                                                               bill_date = bill_date)
                                                                               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):
 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):
     def __repr__(self):
         return '<AliPayWithdrawGateway(appid=%s, debug=%s)>' % (self.appid, self.debug)
         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:
 if TYPE_CHECKING:
     from apps.web.core.models import PayAppBase
     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
     from apps.web.common.transaction.pay import RechargeRecordT
 
 
 
 
@@ -125,6 +125,11 @@ class PaymentGateway(_PaymentGateway):
         return self.occupant.withdraw_source_key(self.app)
         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):
 class WithdrawGateway(_PaymentGateway):
     """
     """
     提现网关基类
     提现网关基类
@@ -133,30 +138,28 @@ class WithdrawGateway(_PaymentGateway):
     LEDGER_PREFIX = 'ledger'
     LEDGER_PREFIX = 'ledger'
     NO_LEDGER_PREFIX = 'noledger'
     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)
         super(WithdrawGateway, self).__init__(app)
-        self._ledger = ledger
-
-    @property
-    def ledger(self):
-        return self._ledger
 
 
     @classmethod
     @classmethod
     def is_ledger(cls, source_key):
     def is_ledger(cls, source_key):
         # type: (str)->bool
         # type: (str)->bool
         return source_key.startswith(cls.LEDGER_PREFIX)
         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
     @classmethod
     def from_withdraw_gateway_key(cls, withdraw_gateway_key, gateway_version):
     def from_withdraw_gateway_key(cls, withdraw_gateway_key, gateway_version):
         pay_app = cls.get_app_from_gateway_key(gateway_key = withdraw_gateway_key,
         pay_app = cls.get_app_from_gateway_key(gateway_key = withdraw_gateway_key,
                                                default_pay_app_type = PayAppType.WECHAT)  # type: cast(PayAppBase)
                                                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
     @property
     def manual_withdraw(self):
     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 apps.web.utils import testcase_point
 from library.wechatpayv3 import WechatClientV3
 from library.wechatpayv3 import WechatClientV3
 from library.wechatpy.pay import WeChatPay
 from library.wechatpy.pay import WeChatPay
-from library.wechatbase.exceptions import WeChatPayException
+from library.wechatbase.exceptions import WeChatException
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     pass
     pass
@@ -62,7 +62,10 @@ class WechatWithdrawQueryResult(dict):
     @property
     @property
     def is_failed(self):
     def is_failed(self):
         assert self.get('return_code') == 'SUCCESS' and self.get('result_code') == 'SUCCESS'
         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
     @property
     def is_processing(self):
     def is_processing(self):
@@ -238,17 +241,17 @@ class WechatPaymentGateway(PaymentGateway, WechatPayMixin):
             refund_desc = refund_reason,
             refund_desc = refund_reason,
             notify_url=kwargs.get('notify_url'))
             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 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):
 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.version = gateway_version
         self.__gateway_type__ = AppPlatformType.WITHDRAW
         self.__gateway_type__ = AppPlatformType.WITHDRAW
@@ -328,7 +331,7 @@ class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin):
         """
         """
         try:
         try:
             return WechatWithdrawQueryResult(self.client.transfer.query_bankcard(out_trade_no = order_no))
             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']:
             if e.errCode in ['NOT_FOUND', 'ORDERNOTEXIST']:
                 raise WithdrawOrderNotExist()
                 raise WithdrawOrderNotExist()
             else:
             else:
@@ -351,7 +354,7 @@ class WechatWithdrawGateway(WithdrawGateway, WechatPayMixin):
                 return WechatWithdrawQueryResult(rv)
                 return WechatWithdrawQueryResult(rv)
             else:
             else:
                 return WechatWithdrawQueryResult(self.client.transfer.query(out_trade_no = order_no))
                 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']:
             if e.errCode in ['NOT_FOUND', 'ORDERNOTEXIST']:
                 raise WithdrawOrderNotExist()
                 raise WithdrawOrderNotExist()
             else:
             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
     RECHARGE_MONTHLY_PACKAGE = USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE
 
 
     REFUND_CASH = USER_RECHARGE_TYPE.REFUND_CASH
     REFUND_CASH = USER_RECHARGE_TYPE.REFUND_CASH
+    REVOKE_REFUND_CASH = USER_RECHARGE_TYPE.REVOKE_REFUND_CASH
+
     AUTO_SIM = RechargeRecordVia.AutoSim
     AUTO_SIM = RechargeRecordVia.AutoSim
 
 
     INSURANCE = USER_RECHARGE_TYPE.RECHARGE_INSURANCE
     INSURANCE = USER_RECHARGE_TYPE.RECHARGE_INSURANCE
@@ -48,7 +50,8 @@ DEALER_INCOME_SOURCE_TRANSLATION = \
         DEALER_INCOME_SOURCE.REDPACK: u'平台补贴',
         DEALER_INCOME_SOURCE.REDPACK: u'平台补贴',
         DEALER_INCOME_SOURCE.REFUND_CASH: u'现金退费',
         DEALER_INCOME_SOURCE.REFUND_CASH: u'现金退费',
         DEALER_INCOME_SOURCE.AUTO_SIM: 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.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_CARD: DEALER_INCOME_TYPE.DEVICE_INCOME,
         DEALER_INCOME_SOURCE.RECHARGE_VIRTUAL_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.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.RECHARGE_MONTHLY_PACKAGE: DEALER_INCOME_TYPE.DEVICE_INCOME,
 
 
         DEALER_INCOME_SOURCE.REFUND_CASH: 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.AUTO_SIM: DEALER_INCOME_TYPE.DEVICE_INCOME,
         DEALER_INCOME_SOURCE.LEDGER_CONSUME: DEALER_INCOME_TYPE.LEDGER_CONSUME,
         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):
     class PAY_NOTIFY_URL(IterConstant):
         WECHAT_PAY_BACK = concat_server_end_url(
         WECHAT_PAY_BACK = concat_server_end_url(
             uri = '/dealer/{}/{}_finishedPay'.format(PayAppType.WECHAT, get_test_point('dealer', 'PAY_NOTIFY_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:
 else:
     class PAY_NOTIFY_URL(IterConstant):
     class PAY_NOTIFY_URL(IterConstant):
         WECHAT_PAY_BACK = concat_server_end_url(uri = '/dealer/{}/finishedPay'.format(PayAppType.WECHAT))
         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'):
 if settings.DEBUG and get_test_point('dealer', 'REFUND_NOTIFY_URL'):
     class REFUND_NOTIFY_URL(IterConstant):
     class REFUND_NOTIFY_URL(IterConstant):
         WECHAT_REFUND_BACK = concat_server_end_url(
         WECHAT_REFUND_BACK = concat_server_end_url(
             uri = '/dealer/{}/{}_refundOrderNotifier'.format(
             uri = '/dealer/{}/{}_refundOrderNotifier'.format(
                 PayAppType.WECHAT, get_test_point('dealer', 'REFUND_NOTIFY_URL')))
                 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:
 else:
     class REFUND_NOTIFY_URL(IterConstant):
     class REFUND_NOTIFY_URL(IterConstant):
         WECHAT_REFUND_BACK = concat_server_end_url(uri = '/dealer/{}/refundOrderNotifier'.format(PayAppType.WECHAT))
         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_WECHAT_URL = concat_server_end_url(uri = '/dealer/verifyNewWechatBindingCallback')
 DEALER_BIND_WALLET_WECHAT_URL = concat_server_end_url(uri = '/dealer/verifyNewWalletWechatBindingCallback')
 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)
         return super(Dealer, self).save(**kwargs)
 
 
     def update(self, **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
     @property
     def permissionList(self):
     def permissionList(self):
@@ -656,6 +660,19 @@ class Dealer(CapitalUser):
         from apps.web.dealer.withdraw import DealerWithdrawHandler
         from apps.web.dealer.withdraw import DealerWithdrawHandler
         return DealerWithdrawHandler(self, record)
         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):
     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': {}
                     '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)
         serviceFee = amount * (dealerFeeRatio.as_ratio)
@@ -703,15 +725,7 @@ class Dealer(CapitalUser):
 
 
         actualPay = amount - serviceFee - bank_trans_fee
         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:
         if dealerFeeRatio > managerFeeRatio:
             earned = amount * ((dealerFeeRatio - managerFeeRatio).as_ratio)
             earned = amount * ((dealerFeeRatio - managerFeeRatio).as_ratio)
@@ -1201,16 +1215,6 @@ class Dealer(CapitalUser):
         else:
         else:
             return self.username
             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
     @property
     def my_agent(self):
     def my_agent(self):
         # type:()->Agent
         # type:()->Agent
@@ -1550,8 +1554,9 @@ class DealerRechargeRecord(Searchable):
         payload['status'] = cls.PayState.UnPaid
         payload['status'] = cls.PayState.UnPaid
 
 
         identifier = identifier if identifier else str(user.id)
         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)
                 identifier = identifier, main_type = cls.MY_MAIN_TYPE, sub_type = sub_type)
 
 
         record = cls(**payload)
         record = cls(**payload)
@@ -1567,20 +1572,20 @@ class DealerRechargeRecord(Searchable):
         record = cls.objects(orderNo = order_no).first()
         record = cls.objects(orderNo = order_no).first()
         return record
         return record
 
 
-    def succeed(self, wxOrderNo, **kwargs):
-        # 这里和数据库强相关, 通过判断是否正确更新记录
+    def succeed(self, **kwargs):
         payload = {
         payload = {
-            'wxOrderNo': wxOrderNo
+            'status': self.PayState.Paid
         }
         }
 
 
         if kwargs:
         if kwargs:
             payload.update(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(
         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},
             update = {'$set': payload},
             upsert = False
             upsert = False
         )
         )
@@ -1588,13 +1593,8 @@ class DealerRechargeRecord(Searchable):
         return result.matched_count == 1
         return result.matched_count == 1
 
 
     def fail(self, **kwargs):
     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):
     def cancel(self):
         result = DealerRechargeRecord.get_collection().update_one(
         result = DealerRechargeRecord.get_collection().update_one(
@@ -1611,23 +1611,40 @@ class DealerRechargeRecord(Searchable):
         self.save()
         self.save()
         return self
         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):
     def is_success(self):
         return self.status == self.PayState.Paid
         return self.status == self.PayState.Paid
 
 
+    @property
     def is_fail(self):
     def is_fail(self):
         return self.status == self.PayState.Failure
         return self.status == self.PayState.Failure
 
 
+    @property
     def is_cancel(self):
     def is_cancel(self):
         return self.status == self.PayState.Cancel
         return self.status == self.PayState.Cancel
 
 
+    @property
     def is_unpay(self):
     def is_unpay(self):
         return self.status == self.PayState.UnPaid
         return self.status == self.PayState.UnPaid
 
 
+    @property
     def is_close(self):
     def is_close(self):
         return self.status == self.PayState.Close
         return self.status == self.PayState.Close
 
 
@@ -1659,40 +1676,7 @@ class DealerRechargeRecord(Searchable):
     def withdraw_source_key(self):
     def withdraw_source_key(self):
         return self.withdrawSourceKey
         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 = {
     meta = {
         'collection': 'refund_dealer_recharge_record',
         'collection': 'refund_dealer_recharge_record',
         'db_alias': 'default'
         'db_alias': 'default'
@@ -1712,120 +1696,43 @@ class RefundDealerRechargeRecord(Searchable):
             # refundSeq=next_seq,
             # refundSeq=next_seq,
             orderNo = refund_order_no,
             orderNo = refund_order_no,
             money = refundCash,
             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
     @property
-    def pay_order(self):
+    def pay_sub_order(self):
         # type: ()->DealerRechargeRecord
         # type: ()->DealerRechargeRecord
 
 
-        if not hasattr(self, '__pay_order__'):
+        if not hasattr(self, '__pay_sub_order__'):
             pay_order = DealerRechargeRecord.objects(id = str(self.rechargeObjId)).first()
             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
     @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):
 class OnSale(DynamicDocument):
     """
     """
     优惠活动管理
     优惠活动管理

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

@@ -1801,7 +1801,6 @@ def dealer_auto_withdraw():
         else:
         else:
             logger.error('{} auto withdraw strategy type <{}> error'.format(repr(dealer), strategy['type']))
             logger.error('{} auto withdraw strategy type <{}> error'.format(repr(dealer), strategy['type']))
 
 
-    # 建单
     for dealer in needExecuteDealers:  # type: Dealer
     for dealer in needExecuteDealers:  # type: Dealer
         try:
         try:
             for incomeType in DEALER_INCOME_TYPE.choices():
             for incomeType in DEALER_INCOME_TYPE.choices():
@@ -1919,7 +1918,7 @@ def dealer_auto_charge_sim_card():
                     if result:
                     if result:
                         logger.info('auto charge sim of {} success.'.format(devObj))
                         logger.info('auto charge sim of {} success.'.format(devObj))
 
 
-                        rcd.succeed(rcd.orderNo)
+                        rcd.succeed(wxOrderNo = rcd.orderNo)
                         post_sim_recharge(rcd)
                         post_sim_recharge(rcd)
 
 
                         if devObj.groupId not in groupMap:
                         if devObj.groupId not in groupMap:
@@ -1950,9 +1949,6 @@ def dealer_auto_charge_sim_card():
 
 
                         rcd.cancel()
                         rcd.cancel()
                         break
                         break
-                except InsufficientFundsError:
-                    logger.debug('auto charge sim of {} failure because no enough balance.'.format(devObj))
-                    break
                 except Exception, e:
                 except Exception, e:
                     logger.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)
         result = Dealer.get_collection().update_one(filter, update, upsert = False)
         if result.matched_count == 1 and result.modified_count == 1:
         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
             return True
         else:
         else:
             return False
             return False
@@ -2035,7 +2032,7 @@ def auto_charge_sim_card(dealerId):
 
 
                     result = consume_dealer_balance(pay_gateway, income_type, dealer, costMoney)
                     result = consume_dealer_balance(pay_gateway, income_type, dealer, costMoney)
                     if result:
                     if result:
-                        rcd.succeed(rcd.orderNo)
+                        rcd.succeed(wxOrderNo = rcd.orderNo)
                         post_sim_recharge(rcd)
                         post_sim_recharge(rcd)
                         logger.info('charge sim card success,devNo=%s' % devObj.devNo)
                         logger.info('charge sim card success,devNo=%s' % devObj.devNo)
 
 
@@ -2052,7 +2049,7 @@ def auto_charge_sim_card(dealerId):
                                 {
                                 {
                                     "money": (-costMoney).mongo_amount,
                                     "money": (-costMoney).mongo_amount,
                                     "role": "owner",
                                     "role": "owner",
-                                    "share": Percent("100.0"),
+                                    "share": Percent("100.0").mongo_amount,
                                     "id": str(dealer.id)
                                     "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 apilib.monetary import RMB
 from apps.web.agent.proxy import record_agent_traffic_card_earning, record_agent_api_quota_earning, \
 from apps.web.agent.proxy import record_agent_traffic_card_earning, record_agent_api_quota_earning, \
     record_agent_disable_ad_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
 from apps.web.device.models import Device
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -19,6 +19,12 @@ logger = logging.getLogger(__name__)
 def post_pay(record, verifyAccount = False):
 def post_pay(record, verifyAccount = False):
     # type: (DealerRechargeRecord, bool)->None
     # type: (DealerRechargeRecord, bool)->None
 
 
+    record.reload()
+
+    if record.is_close:
+        logger.warning('{} check close.'.format(repr(record)))
+        return
+
     if record.product == DealerRechargeRecord.ProductType.SimCard:
     if record.product == DealerRechargeRecord.ProductType.SimCard:
         post_sim_recharge(record, verifyAccount)
         post_sim_recharge(record, verifyAccount)
     elif record.product == DealerRechargeRecord.ProductType.ApiCost:
     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)))
         logger.exception('save recharge record failure. error = %s; record = %s' % (e, repr(record)))
 
 
     dev_no_list = [item['devNo'] for item in record.items]
     dev_no_list = [item['devNo'] for item in record.items]
-
+  
     try:
     try:
         Device.get_collection().update_many({'devNo': {'$in': dev_no_list}},
         Device.get_collection().update_many({'devNo': {'$in': dev_no_list}},
                                             {'$set': {'simStatus': 'chargedUnupdated'}}, upsert = False)
                                             {'$set': {'simStatus': 'chargedUnupdated'}}, upsert = False)
     except Exception as e:
     except Exception as e:
         logger.exception(e)
         logger.exception(e)
-
+    
+    
     finally:
     finally:
         Device.invalid_many_device_cache(dev_no_list)
         Device.invalid_many_device_cache(dev_no_list)
-
+     
 
 
 def post_sim_verify_order(dealer, verify_order, verifyAccount = False):
 def post_sim_verify_order(dealer, verify_order, verifyAccount = False):
     for item in verify_order.settleInfo['partition']:
     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'])
                                               'agent_earning': RMB.fen_to_yuan(item['earned'])
                                           })
                                           })
 
 
+
 def post_ApiCost_recharge(record, verifyAccount = False):
 def post_ApiCost_recharge(record, verifyAccount = False):
     # type: (DealerRechargeRecord, bool)->None
     # type: (DealerRechargeRecord, bool)->None
 
 
@@ -138,13 +146,3 @@ def post_disableAd_recharge(record, verifyAccount = False):
         logger.exception(e)
         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
 import logging
 
 
-from django.conf import settings
 from typing import TYPE_CHECKING
 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__)
 logger = logging.getLogger(__name__)
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     pass
     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, \
 from apps.web.common.proxy import ClientRechargeModelProxy, ClientConsumeModelProxy, DealerDailyStatsModelProxy, \
     GroupDailyStatsModelProxy, DeviceDailyStatsModelProxy, ClientDealerIncomeModelProxy, DealerReportModelProxy
     GroupDailyStatsModelProxy, DeviceDailyStatsModelProxy, ClientDealerIncomeModelProxy, DealerReportModelProxy
 from apps.web.common.transaction import WithdrawStatus, WITHDRAW_PAY_TYPE, translate_withdraw_state
 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.common.validation import NAME_RE, PHONE_NUMBER_RE
 from apps.web.constant import (
 from apps.web.constant import (
     Const,
     Const,
@@ -99,7 +100,8 @@ from apps.web.dealer.models import (
     SubAccount, DealerMessage, Complaint, AdjustUserVirtualCardRecord, ExchangeOrder, DealerAddr, UpCardScoreRecord,
     SubAccount, DealerMessage, Complaint, AdjustUserVirtualCardRecord, ExchangeOrder, DealerAddr, UpCardScoreRecord,
     PermissionRole, PermissionRule, TodoMessage, ApiAppInfo, RefundDealerRechargeRecord)
     PermissionRole, PermissionRule, TodoMessage, ApiAppInfo, RefundDealerRechargeRecord)
 from apps.web.dealer.proxy import DealerIncomeProxy
 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, \
 from apps.web.dealer.utils import gen_login_response, gen_home_response, gen_subaccount_login_response, \
     VirtualCardBuilder, create_dealer_sim_charge_order, DealerSessionBuilder, MyToken, \
     VirtualCardBuilder, create_dealer_sim_charge_order, DealerSessionBuilder, MyToken, \
     create_dealer_charge_order_for_api, create_dealer_charge_order_for_disable_ad
     create_dealer_charge_order_for_api, create_dealer_charge_order_for_disable_ad
@@ -4586,7 +4588,7 @@ def cancelSimRechargeOrder(request):
         if not order:
         if not order:
             return JsonErrorResponse(u'订单不存在,请刷新后再试')
             return JsonErrorResponse(u'订单不存在,请刷新后再试')
 
 
-        if not order.is_unpay():
+        if not order.is_unpay:
             return JsonErrorResponse(u'订单已经处理,无法取消')
             return JsonErrorResponse(u'订单已经处理,无法取消')
 
 
         if order.cancel():
         if order.cancel():
@@ -8670,58 +8672,71 @@ def checkAlarm(request):
 
 
 
 
 @permission_required(ROLE.dealer)
 @permission_required(ROLE.dealer)
-@error_tolerate(logger=logger, nil=JsonErrorResponse(u'系统错误,请稍后再试'))
+@error_tolerate(logger = logger, nil = JsonErrorResponse(u'系统错误,请稍后再试'))
 def withdrawEntry(request):
 def withdrawEntry(request):
     # type: (WSGIRequest)->JsonResponse
     # type: (WSGIRequest)->JsonResponse
 
 
     user = request.user  # type: Dealer
     user = request.user  # type: Dealer
 
 
     if not check_role(user, ROLE.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')
     source_key = request.GET.get('sourceId')
     if not WithdrawGateway.is_ledger(source_key):
     if not WithdrawGateway.is_ledger(source_key):
-        return ErrorResponseRedirect(error=u'系统配置错误,请联系平台客服(10003)')
+        return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10003)')
 
 
     source_type = request.GET.get('sourceType')
     source_type = request.GET.get('sourceType')
     assert source_type in DEALER_INCOME_TYPE.choices(), 'invalid dealer income type'
     assert source_type in DEALER_INCOME_TYPE.choices(), 'invalid dealer income type'
 
 
     if source_key not in user.balance_dict(source_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:
     if not is_ledger:
         return ErrorResponseRedirect(error = u'系统配置错误,请联系平台客服(10005)')
         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')
         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)
 @permission_required(ROLE.dealer)
 def ActivateUser(request):
 def ActivateUser(request):
@@ -10532,14 +10547,6 @@ def payNotify(request, pay_app_type):
     :param request:
     :param request:
     :return: HttpResponse
     :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
     recharge_cls_factory = lambda order_no: DealerRechargeRecord
     notifier_cls = PayManager().get_notifier(pay_app_type = pay_app_type)
     notifier_cls = PayManager().get_notifier(pay_app_type = pay_app_type)
     response = notifier_cls(request, recharge_cls_factory).do(post_pay)
     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)
     currency = rechargeOrder.myuser.calc_currency_balance(rechargeOrder.owner, rechargeOrder.group)
     if currency < deductCoins:
     if currency < deductCoins:
         return JsonErrorResponse(description = u"扣除金币数大于用户目前余额")
         return JsonErrorResponse(description = u"扣除金币数大于用户目前余额")
-    if not rechargeOrder.is_success():
+    if not rechargeOrder.is_success:
         return JsonErrorResponse(description = u"退款失败,订单状态非成功,请联系平台客服")
         return JsonErrorResponse(description = u"退款失败,订单状态非成功,请联系平台客服")
 
 
     if not rechargeOrder.is_ledgered:
     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)
     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)
     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)
         refund_post_pay)
     return response
     return response

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

@@ -39,7 +39,7 @@ class DealerWithdrawHandler(WithdrawHandler):
                     'withdrawFeeRatio': self.record.withdrawFeeRatio
                     '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)
                                           amount = earned, detail = detail)
         except Exception as e:
         except Exception as e:
             logging.exception(e)
             logging.exception(e)

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

@@ -4,6 +4,7 @@
 import datetime
 import datetime
 import logging
 import logging
 import socket
 import socket
+from decimal import Decimal
 
 
 import arrow
 import arrow
 import simplejson as json
 import simplejson as json
@@ -12,6 +13,7 @@ from influxdb import InfluxDBClient
 from influxdb.resultset import ResultSet
 from influxdb.resultset import ResultSet
 from django.core.cache import cache
 from django.core.cache import cache
 
 
+from apilib.numerics import quantize
 from apilib.utils_datetime import local2utc
 from apilib.utils_datetime import local2utc
 from apps.web.core.sysparas import SysParas
 from apps.web.core.sysparas import SysParas
 
 
@@ -20,9 +22,16 @@ logger = logging.getLogger(__name__)
 
 
 class FluentedEngine(object):
 class FluentedEngine(object):
     def in_signal_udp(self, devNo, ts, signal, cmd):
     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)
         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 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         client.sendto(json.dumps(point), ip_port)
         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)
         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:
         if current is not None:
             point.update({'current2': float(current)})
             point.update({'current2': float(current)})
 
 
+        if kwargs:
+            point.update(kwargs)
+
         client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         client.sendto(json.dumps(point), ip_port)
         client.sendto(json.dumps(point), ip_port)
 
 
     def in_put_coins_udp(self, devNo, ts, coins, mode, port = None):
     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)
         ip_port = (SysParas.get_private_ip(settings.FLUENTED_IP), settings.FLUENTED_OFFLINE_PORT)
         point = {
         point = {
@@ -176,41 +204,46 @@ class PowerManager(object):
     def __init__(self, enginer):
     def __init__(self, enginer):
         self.enginer = 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):
         def format_date(dateStr):
             return arrow.get(str(dateStr)).to(settings.TIME_ZONE).naive
             return arrow.get(str(dateStr)).to(settings.TIME_ZONE).naive
 
 
-        def format_point(point):
+        def format_point(point, fields):
             rv = {
             rv = {
                 'time': format_date(point['time'])
                 '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
             return rv
 
 
         query_start_time = sTime - datetime.timedelta(seconds = interval)
         query_start_time = sTime - datetime.timedelta(seconds = interval)
         query_end_time = eTime + 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)
         points = self.enginer.read_data(selectStr)
 
 
@@ -237,12 +270,8 @@ class PowerManager(object):
 
 
             while True:
             while True:
                 next_time = (next_time - datetime.timedelta(seconds = interval))
                 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:
                 if next_time < sTime:
                     break
                     break
@@ -254,7 +283,7 @@ class PowerManager(object):
         middle_section = None
         middle_section = None
         if max_time > min_time:
         if max_time > min_time:
             # now_point =
             # now_point =
-            now = format_point(points[0])
+            now = format_point(points[0], fields)
             # now = {
             # now = {
             #     'time': format_date(now_point['time']),
             #     'time': format_date(now_point['time']),
             #     'power': now_point['power'],
             #     'power': now_point['power'],
@@ -267,7 +296,7 @@ class PowerManager(object):
 
 
             # right_point = points[idx]
             # right_point = points[idx]
 
 
-            right = format_point(points[idx])
+            right = format_point(points[idx], fields)
             #     {
             #     {
             #     'time': format_date(right_point['time']),
             #     'time': format_date(right_point['time']),
             #     'power': right_point['power'],
             #     'power': right_point['power'],
@@ -288,7 +317,7 @@ class PowerManager(object):
                     if idx >= total:
                     if idx >= total:
                         break
                         break
                     else:
                     else:
-                        right = format_point(points[idx])
+                        right = format_point(points[idx], fields)
                         # {
                         # {
                         #     'time': format_date(points[idx]['time']),
                         #     'time': format_date(points[idx]['time']),
                         #     'power': points[idx]['power'],
                         #     'power': points[idx]['power'],
@@ -302,13 +331,7 @@ class PowerManager(object):
                         # print('before: now = {}; right = {}'.format(now, right))
                         # print('before: now = {}; right = {}'.format(now, right))
 
 
                         last_now = now
                         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)
                         rv.append(now)
 
 
                         # print('after: now = {}; right = {}'.format(now, right))
                         # print('after: now = {}; right = {}'.format(now, right))
@@ -317,9 +340,9 @@ class PowerManager(object):
 
 
                         rv.append({
                         rv.append({
                             'time': (now['time'] + datetime.timedelta(seconds = diff / 2)),
                             '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)
                         rv.append(right)
 
 
@@ -329,7 +352,7 @@ class PowerManager(object):
                         if idx >= total:
                         if idx >= total:
                             break
                             break
                         else:
                         else:
-                            right = format_point(points[idx])
+                            right = format_point(points[idx], fields)
 
 
                         # print('after: now = {}; right = {}'.format(now, right))
                         # print('after: now = {}; right = {}'.format(now, right))
 
 
@@ -340,12 +363,8 @@ class PowerManager(object):
 
 
             while True:
             while True:
                 next_time = (next_time + datetime.timedelta(seconds = interval))
                 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:
                 if next_time >= eTime:
                     break
                     break
@@ -359,7 +378,7 @@ class PowerManager(object):
         def format_date(dateStr):
         def format_date(dateStr):
             return arrow.get(str(dateStr)).to(settings.TIME_ZONE).naive
             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))
             self.enginer.full_qualify_meaurement, devNo, port, local2utc(sTime), local2utc(eTime))
 
 
         points = self.enginer.read_data(selectStr)
         points = self.enginer.read_data(selectStr)
@@ -480,7 +499,7 @@ class OfflineManager(object):
         else:
         else:
             offlineNotifyTime = 1
             offlineNotifyTime = 1
 
 
-        if not offlineNotifyTime:
+        if not offlineNotifyTime or offlineNotifyTime <= 0:
             return
             return
 
 
         from taskmanager.mediator import task_caller
         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]:
             for sub_order in sub_orders[::-1]:
                 need_back_coins, need_consume_coins, backCoins = self._calc_refund_info(backCoins, sub_order.coin)
                 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({
                 sub_order.update_service_info({
                     DEALER_CONSUMPTION_AGG_KIND.COIN: sub_order.coin.mongo_amount,
                     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)
             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({
             consumeDict.update({
                 DEALER_CONSUMPTION_AGG_KIND.COIN: master_order.coin.mongo_amount,
                 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
                 return
 
 
             # 先分账,然后在处理退款
             # 先分账,然后在处理退款
-            if not record.is_success():
+            if not record.is_success:
                 logger.debug('{} state is {}'.format(str(record), record.result))
                 logger.debug('{} state is {}'.format(str(record), record.result))
                 return
                 return
 
 

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

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

+ 5 - 1
apps/web/exceptions.py

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

+ 64 - 7
apps/web/helpers.py

@@ -1,13 +1,16 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
-
+import contextlib
+import inspect
 import logging
 import logging
+import uuid
+from copy import deepcopy
 from functools import wraps, partial
 from functools import wraps, partial
 
 
 import simplejson as json
 import simplejson as json
 from django.http import JsonResponse
 from django.http import JsonResponse
 from django.utils.module_loading import import_string
 from django.utils.module_loading import import_string
-
+from mongoengine.base import BaseField
 from typing import Union, Tuple, TYPE_CHECKING, Optional, Mapping
 from typing import Union, Tuple, TYPE_CHECKING, Optional, Mapping
 
 
 from apps import serviceCache
 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 import ROLE
 from apps.web.core.auth.ali import AlipayAuthBridge
 from apps.web.core.auth.ali import AlipayAuthBridge
 from apps.web.core.auth.wechat import WechatAuthBridge
 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.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.utils import is_user, is_dealer, is_agent, is_anonymous
-from apps.web.core.models import WechatPayApp, AliApp, SystemSettings, ManualPayApp
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -38,10 +41,13 @@ if TYPE_CHECKING:
     from apps.web.management.models import Manager
     from apps.web.management.models import Manager
     SourceT = Union[Agent, Dealer, Device, Manager, DeviceDict, WithdrawRecord]
     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]
     WechatAPP_T = Union[WechatPayApp, WechatManagerApp, WechatAuthApp]
 
 
+    PayApp_T = Union[WechatPayApp, AliApp]
+    AuthApp_T = Union[WechatManagerApp, WechatAuthApp]
+
 
 
 class DeviceTypeVisitor(BaseVisitor):
 class DeviceTypeVisitor(BaseVisitor):
     def entry(self, node):
     def entry(self, node):
@@ -564,4 +570,55 @@ def remove_some_desc_for_consume(oldDesc, needRemove):
     except Exception, e:
     except Exception, e:
         return oldDesc
         return oldDesc
     vList[sIndex], vList[sIndex + 1], vList[sIndex + 2] = '', '', ''
     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 celery.utils.log import get_task_logger
 from django.conf import settings
 from django.conf import settings
 from django.core.cache import cache
 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 apilib.utils_datetime import timestamp_to_dt, to_datetime
 from apps.web.agent.models import Agent
 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.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 import ROLE
 from apps.web.core.payment import WithdrawGateway
 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.payment.wechat import WechatWithdrawQueryResult, WechatWithdrawGateway
 from apps.web.core.sysparas import SysParas
 from apps.web.core.sysparas import SysParas
 from apps.web.core.utils import generate_excel_report
 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.proxy import DealerIncomeProxy
 from apps.web.dealer.withdraw import DealerWithdrawRetryService
 from apps.web.dealer.withdraw import DealerWithdrawRetryService
 from apps.web.device.models import Device, Group
 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.helpers import get_inhouse_wechat_manager_mp_proxy
 from apps.web.management.models import Manager
 from apps.web.management.models import Manager
 from apps.web.management.utils import get_dealerMap_by_managerId, query_device_income, query_device_consumption, \
 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
     get_device_being_used_trend
 from apps.web.report.models import DealerMonthlyStat
 from apps.web.report.models import DealerMonthlyStat
 from apps.web.user.models import RechargeRecord, CardRechargeOrder, MyUser, ConsumeRecord
 from apps.web.user.models import RechargeRecord, CardRechargeOrder, MyUser, ConsumeRecord
-from library.alipay import AliPayServiceException
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from apps.web.common.models import CapitalUser
     from apps.web.common.models import CapitalUser
@@ -346,9 +346,9 @@ def generate_biz_stats_for_manager(filepath, queryAttrs):
     generate_excel_report(filepath, records)
     generate_excel_report(filepath, records)
 
 
 
 
-def check_wechat_withdraw_via_bank():
+def check_withdraw_via_bank():
     """
     """
-    由于不能实时获取是否微信成功给银行卡转账了,需要每日进行查询并更新状态
+    由于不能实时获取是否微信或者支付宝成功给银行卡转账了,需要每日进行查询并更新状态
     主要处理银行退单的情况,则需要将款项返还给经销商/代理商
     主要处理银行退单的情况,则需要将款项返还给经销商/代理商
     :return:
     :return:
     """
     """
@@ -356,65 +356,63 @@ def check_wechat_withdraw_via_bank():
     def process_withdraw(record):
     def process_withdraw(record):
         # type: (WithdrawRecord)->None
         # type: (WithdrawRecord)->None
 
 
-        check_days = 2
-
         try:
         try:
             payee = ROLE.from_role_id(record.role, str(record.ownerId))  # type: CapitalUser
             payee = ROLE.from_role_id(record.role, str(record.ownerId))  # type: CapitalUser
 
 
             handler = payee.new_withdraw_handler(record)
             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:
         except Exception as e:
             logger.exception(e)
             logger.exception(e)
 
 
@@ -422,6 +420,10 @@ def check_wechat_withdraw_via_bank():
     for record in records:
     for record in records:
         process_withdraw(record)
         process_withdraw(record)
 
 
+    records = list(WithdrawRecord.get_processing_via_v3())
+    for record in records:
+        process_v3_withdraw(record)
+
 
 
 def check_and_retry_withdraw():
 def check_and_retry_withdraw():
     def process_dealer_withdraw(record):
     def process_dealer_withdraw(record):

+ 13 - 0
apps/web/models.py

@@ -2,6 +2,10 @@
 # !/usr/bin/env python
 # !/usr/bin/env python
 
 
 from mongoengine import DynamicDocument, StringField, IntField, BooleanField, DoesNotExist
 from mongoengine import DynamicDocument, StringField, IntField, BooleanField, DoesNotExist
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pymongo.collection import Collection
 
 
 
 
 class ArchivedDatabaseConfig(DynamicDocument):
 class ArchivedDatabaseConfig(DynamicDocument):
@@ -57,3 +61,12 @@ class ArchivedModelProxyConfig(DynamicDocument):
                 return '2017-07-01'
                 return '2017-07-01'
             else:
             else:
                 return cls.get_model_hot_data_start_day('default')
                 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)))
             logger.debug('{} is not success.'.format(repr(self.record)))
             return
             return
 
 

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

@@ -59,7 +59,7 @@ class StartAction(object):
 
 
         # 校验业务配置
         # 校验业务配置
         if 'washConfig' not in self._smartBox:
         if 'washConfig' not in self._smartBox:
-            raise ServiceException({'result': 2, 'description': u'未配置该业务,请联系经销商'})
+            raise ServiceException({'result': 2, 'description': u'未配置该业务,请联系经销商(1002)'})
 
 
         # 校验套餐ID的有效性
         # 校验套餐ID的有效性
         package = self._smartBox['washConfig'].get(self._packageId)
         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)):
     if float(money) > float(str(rcd.money)):
         return JsonErrorResponse(description=u"退款金额大于订单金额,退款失败")
         return JsonErrorResponse(description=u"退款金额大于订单金额,退款失败")
 
 
-    if not rcd.is_success():
+    if not rcd.is_success:
         return JsonErrorResponse(description = u"退款失败,订单状态非成功,请联系平台客服")
         return JsonErrorResponse(description = u"退款失败,订单状态非成功,请联系平台客服")
 
 
     if not rcd.is_ledgered:
     if not rcd.is_ledgered:
         return JsonErrorResponse(description=u"订单还没有分账,无法退款")
         return JsonErrorResponse(description=u"订单还没有分账,无法退款")
 
 
     try:
     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()
         return JsonOkResponse()
     except ServiceException as e:
     except ServiceException as e:
         logger.error(e.result)
         logger.error(e.result)
@@ -2576,7 +2583,7 @@ def manualRechargeSimCard(request):
             return JsonErrorResponse(
             return JsonErrorResponse(
                 description = u'创建订单失败,部分设备线下续费失败,请检查后重试(设备号{})。'.format(device.logicalCode))
                 description = u'创建订单失败,部分设备线下续费失败,请检查后重试(设备号{})。'.format(device.logicalCode))
 
 
-        rcd.succeed(rcd.orderNo)
+        rcd.succeed(wxOrderNo = rcd.orderNo)
 
 
         device.simStatus = u'chargedUnupdated'
         device.simStatus = u'chargedUnupdated'
         device.save()
         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.systypes import IterConstant
 from apilib.utils import flatten
 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_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 apilib.utils_string import get_random_str
 from apps import serviceCache
 from apps import serviceCache
 from apps.web.agent.models import Agent, MoniApp
 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.transaction import OrderNoMaker, OrderMainType, UserPaySubType, UserConsumeSubType, RefundSubType
 from apps.web.common.validation import CARD_NO_RE
 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.exceptions import UserServerException, PostPayOrderError
 from apps.web.report.ledger import AgentLedgerFirst, PartnerLedgerFirst
 from apps.web.report.ledger import AgentLedgerFirst, PartnerLedgerFirst
 from apps.web.report.utils import record_consumption_stats
 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.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 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
 from library.idgen import IDGenService
@@ -510,6 +511,16 @@ class MyUser(RoleBaseDocument):
         if valid_to_pay:
         if valid_to_pay:
             raise NotImplementedError()
             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):
     def update_total_recharge(self, money):
         # type: (RMB)->int
         # type: (RMB)->int
         assert isinstance(money, RMB), 'money has to be RMB'
         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
         result = self.get_collection().update_one(query, update, upsert = False)  # type: UpdateResult
         return bool(result.modified_count == 1)
         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
     @property
     def isNormal(self):
     def isNormal(self):
         return True
         return True
@@ -1493,16 +1541,27 @@ class RechargeRecordDict(dict):
 
 
 
 
 class RechargeRecord(OrderRecordBase):
 class RechargeRecord(OrderRecordBase):
+    """
+    三个作用:
+    1、经销商收益记录(特殊情况包括平台的收益记录,例如1毛保险单情况下)
+    2、充值订单,模型增加refundInfo信息
+    3、商家派送(金币,红包等)订单
+    """
+
     class PayResult(IterConstant):
     class PayResult(IterConstant):
         UNPAY = 'unPay'
         UNPAY = 'unPay'
+
         SUCCESS = 'success'
         SUCCESS = 'success'
         FAILED = 'failed'
         FAILED = 'failed'
-        REFUNDING = 'refunding'
+
         CANCEL = 'cancel'
         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 = "")
     ownerId = StringField(verbose_name = u"设备的owner", default = "")
 
 
@@ -1518,25 +1577,28 @@ class RechargeRecord(OrderRecordBase):
     extraInfo = DictField(verbose_name = u"支付订单模型信息", default = {})
     extraInfo = DictField(verbose_name = u"支付订单模型信息", default = {})
     description = StringField(verbose_name = u"结果描述,一般为第三方错误码", default = None)
     description = StringField(verbose_name = u"结果描述,一般为第三方错误码", default = None)
 
 
+    # 以下字段为充值订单的模型字段, MODEL中不在体现
+    #: 当用户余额为0,点击投币,生成充值订单,用户完成后直接自动投相应币数
+    #: 此举为优化用户体验,大多数情况用户对充值并不优先选择,使其能够缩短支付使用流程,用完即走
     isQuickPay = BooleanField(verbose_name = u"是否直接支付使用,默认为否", default = False)
     isQuickPay = BooleanField(verbose_name = u"是否直接支付使用,默认为否", default = False)
     selectedQuickPayPackageId = StringField(verbose_name = u"快捷支付所选的投币套餐", default = None)
     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')
     search_fields = ('orderNo', 'wxOrderNo')
 
 
@@ -1607,6 +1669,7 @@ class RechargeRecord(OrderRecordBase):
         if self.isSubOrder:
         if self.isSubOrder:
             _id = self.attachParas.get("tradeOrderId", None) or self.extraInfo.get('tradeOrderId', None)
             _id = self.attachParas.get("tradeOrderId", None) or self.extraInfo.get('tradeOrderId', None)
             return self.__class__.objects.get(id = _id)
             return self.__class__.objects.get(id = _id)
+
         return self
         return self
 
 
     @property
     @property
@@ -1617,6 +1680,27 @@ class RechargeRecord(OrderRecordBase):
     def card_no(self):
     def card_no(self):
         return self.attachParas.get('cardNo', None)
         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
     @property
     def my_description(self):
     def my_description(self):
         if self.description:
         if self.description:
@@ -1640,10 +1724,6 @@ class RechargeRecord(OrderRecordBase):
     def partition_map(self):
     def partition_map(self):
         return self.to_dict_obj.partition_map
         return self.to_dict_obj.partition_map
 
 
-    @property
-    def is_cancel(self):
-        return self.result == self.PayResult.CANCEL
-
     @property
     @property
     def fen_total_fee(self):
     def fen_total_fee(self):
         return int(self.money * 100)
         return int(self.money * 100)
@@ -2009,19 +2089,34 @@ class RechargeRecord(OrderRecordBase):
     def to_json_dict(self):
     def to_json_dict(self):
         return self.to_dict(json_safe = True)
         return self.to_dict(json_safe = True)
 
 
-    def succeed(self, wxOrderNo, **kwargs):
-        # 这里和数据库强相关, 通过判断是否正确更新记录
+    def succeed(self, **kwargs):
         payload = {
         payload = {
-            'wxOrderNo': wxOrderNo
+            'result': self.PayResult.SUCCESS
         }
         }
 
 
         if kwargs:
         if kwargs:
             payload.update(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(
         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},
             update = {'$set': payload},
             upsert = False)
             upsert = False)
 
 
@@ -2043,12 +2138,26 @@ class RechargeRecord(OrderRecordBase):
 
 
         return result.matched_count == 1
         return result.matched_count == 1
 
 
+    @property
     def is_success(self):
     def is_success(self):
         return self.result == self.PayResult.SUCCESS
         return self.result == self.PayResult.SUCCESS
 
 
+    @property
+    def is_refunding(self):
+        return self.result == self.PayResult.REFUNDING
+
+    @property
     def is_fail(self):
     def is_fail(self):
         return self.result == self.PayResult.FAILED
         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):
     def to_detail(self):
         # type: ()->dict
         # type: ()->dict
         """
         """
@@ -3017,6 +3126,126 @@ class Card(Searchable):
             order=order
             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
     @classmethod
     def freeze_balance(cls, transaction_id, payment):
     def freeze_balance(cls, transaction_id, payment):
         bulker = BulkHandlerEx(cls.get_collection())  # type: BulkHandlerEx
         bulker = BulkHandlerEx(cls.get_collection())  # type: BulkHandlerEx
@@ -3059,7 +3288,7 @@ class Card(Searchable):
                 return True
                 return True
 
 
     @classmethod
     @classmethod
-    def clear_frozen_balance(cls, transaction_id, payment, refund):
+    def clear_frozen_balance(cls, transaction_id, refund):
         try:
         try:
             bulker = BulkHandlerEx(cls.get_collection())  # type: BulkHandlerEx
             bulker = BulkHandlerEx(cls.get_collection())  # type: BulkHandlerEx
             chargeBalanceField = cls.chargeBalance.name
             chargeBalanceField = cls.chargeBalance.name
@@ -3136,122 +3365,6 @@ class Card(Searchable):
                 return True
                 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):
     def clear_card(self):
         return self.update(
         return self.update(
             cardType = '',
             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 = {
     meta = {
         "collection": "RefundMoneyRecord",
         "collection": "RefundMoneyRecord",
@@ -3321,11 +3403,20 @@ class RefundMoneyRecord(Searchable):
     }
     }
 
 
     @classmethod
     @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(
         refund_order_no = OrderNoMaker.make_order_no_32(
-            identifier = order.logicalCode,
+            identifier = identifier,
             main_type = OrderMainType.REFUND,
             main_type = OrderMainType.REFUND,
             sub_type = RefundSubType.REFUND)
             sub_type = RefundSubType.REFUND)
 
 
@@ -3334,115 +3425,19 @@ class RefundMoneyRecord(Searchable):
             # refundSeq=next_seq,
             # refundSeq=next_seq,
             orderNo = refund_order_no,
             orderNo = refund_order_no,
             money = refundCash,
             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
     @property
     def pay_sub_order(self):
     def pay_sub_order(self):
         # type: ()->RechargeRecord
         # type: ()->RechargeRecord
 
 
         if not hasattr(self, '__pay_sub_order__'):
         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)
             setattr(self, '__pay_sub_order__', pay_order)
 
 
         return getattr(self, '__pay_sub_order__')
         return getattr(self, '__pay_sub_order__')
@@ -3452,26 +3447,77 @@ class RefundMoneyRecord(Searchable):
         setattr(self, '__pay_sub_order__', order)
         setattr(self, '__pay_sub_order__', order)
 
 
     @property
     @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
     @property
     def user(self):
     def user(self):
         # type: ()->MyUser
         # type: ()->MyUser
         return self.pay_sub_order.user
         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):
 class VCardConsumeRecord(OrderRecordBase):
     orderNo = StringField(verbose_name = "订单号", default = "")
     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 typing import Optional, TYPE_CHECKING
 
 
 from apps.web.agent.models import Agent
 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.constant import Const
 from apps.web.core.bridge import WechatClientProxy
 from apps.web.core.bridge import WechatClientProxy
 from apps.web.core.helpers import ActionDeviceBuilder
 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.device.models import Group, Device
 from apps.web.helpers import get_wechat_user_manager_mp_proxy, get_wechat_user_sub_manager_mp_proxy, \
 from apps.web.helpers import get_wechat_user_manager_mp_proxy, get_wechat_user_sub_manager_mp_proxy, \
     get_wechat_user_messager_app
     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.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 import post_pay
 from apps.web.user.transaction_deprecated import refund_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
 from apps.web.utils import concat_front_end_url
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from apps.web.common.transaction.pay import PayRecordPoller
     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')
 logger = get_task_logger('user.tasks')
 
 

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

@@ -6,28 +6,27 @@ import logging
 import time
 import time
 import uuid
 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 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 import PayAppType, ROLE
 from apps.web.core.exceptions import ParameterError
 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.device.models import Group
 from apps.web.exceptions import UserServerException
 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__)
 logger = logging.getLogger(__name__)
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
-    pass
+    from apps.web.dealer.proxy import DealerIncomeProxy
 
 
 
 
 def refund_money(device, money, openId):
 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))
         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 recharge_record:
     :param refundFee:
     :param refundFee:
-    :param kwargs 用户为资金实体的情况下, 传入user和minus_total_consume参数
+    :type kwargs: object
     :return:
     :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:
     else:
         raise UserServerException(u'不支持该类型订单退款')
         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):
     def pre_check(self):
         """
         """
@@ -125,38 +137,51 @@ class RefundCash(object):
         :return:
         :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"退费金额错误")
             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
         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'非成功订单无法进行退款')
             raise UserServerException(u'非成功订单无法进行退款')
 
 
         while not self.paySubOrder.is_ledgered and int(time.time()) < check_end_time:
         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)))
             logger.debug('{} is not allocated. wait to be allocated.'.format(repr(self.paySubOrder)))
 
 
-            # TODO  考虑回调的方式进行
             time.sleep(5)
             time.sleep(5)
             self.paySubOrder.reload()
             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:
         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
         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
         refundOrder.pay_sub_order = self.paySubOrder
         return refundOrder
         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()
         proxy = self.pre_check()
 
 
-        payGateway = PaymentGateway.clone_from_order(self.payOrder)  # type: PaymentGateway
-
         try:
         try:
-            refundOrder = self.create_refund_order()  # type: RefundMoneyRecord
-        except DuplicateKeyError:
+            refundOrder = self.create_refund_order(**self.extraInfo)  # type: RefundMoneyRecord
+        except NotUniqueError:
             raise UserServerException(u'已经有退款订单正在进行')
             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:
         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:
         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
                 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
         return refundOrder
 
 
 
 
-class RetryRefundCash(object):
+class RetryRefundCash(RefundCashMixin):
     def __init__(self, refundOrder):  # type:(RefundMoneyRecord) -> None
     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.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):
     def pre_check(self):
         """
         """
@@ -281,24 +246,26 @@ class RetryRefundCash(object):
         :return:
         :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"退费金额错误")
             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"扣除用户金币数目错误")
             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:
         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
         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()
         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()
         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
             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, 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:
         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
         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
         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)
         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:
     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 django.http.response import HttpResponse, HttpResponseRedirect
 
 
-    from apps.web.core.payment import PaymentGateway, WechatMiniPaymentGatewayT
     from django.core.handlers.wsgi import WSGIRequest
     from django.core.handlers.wsgi import WSGIRequest
     from apps.web.core import PayAppBase
     from apps.web.core import PayAppBase
-    from apps.web.dealer.models import OnSale
 
 
 
 
 def get_homepage_response(platform_type, user, dev, chargeIndex, product_agent):
 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
             user = MyUser.objects(openId = order.openId, groupId = order.groupId).first()  # type: MyUser
 
 
         if user:
         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':
     elif order.paymentInfo['via'] == 'virtualCard':
         if not virtual_card:
         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.ad.models import AdRecord, DevRegToAli
 from apps.web.agent.models import Agent, MoniApp
 from apps.web.agent.models import Agent, MoniApp
 from apps.web.common.proxy import ClientRechargeModelProxy, ClientConsumeModelProxy
 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.utils import UserConsumeFilter
 from apps.web.common.validation import check_phone_number, check_entity_name
 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, \
 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'))
         order_id = str(payload.get('orderId'))
         if OrderCacheMgr(order_id, cls_name = RechargeRecord.__name__).has_done():
         if OrderCacheMgr(order_id, cls_name = RechargeRecord.__name__).has_done():
             record = RechargeRecord.objects(id = str(payload.get('orderId'))).first()  # type: RechargeRecord
             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'}})
                 return JsonResponse({'result': 1, 'description': '', 'payload': {'status': 'success'}})
             else:
             else:
                 return JsonResponse({'result': 1, 'description': '', 'payload': {'status': 'fail'}})
                 return JsonResponse({'result': 1, 'description': '', 'payload': {'status': 'fail'}})
@@ -5164,8 +5165,7 @@ def payGateway(request):
         def create_by_wechat(cls, payment_gateway, payParam, record):
         def create_by_wechat(cls, payment_gateway, payParam, record):
             current_user = payParam.curUser
             current_user = payParam.curUser
             open_id = current_user.get_bound_pay_openid(payment_gateway.bound_openid_key)
             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)
             pull_up_cls = PayManager().get_pull_up(payment_gateway.pay_app_type)
 
 
             return pull_up_cls(open_id, payment_gateway, record, **{
             return pull_up_cls(open_id, payment_gateway, record, **{
@@ -5259,7 +5259,7 @@ def payNotify(request, pay_app_type):
         payload = request.POST.dict()
         payload = request.POST.dict()
         if 'refund_fee' in payload and 'gmt_refund' in payload:
         if 'refund_fee' in payload and 'gmt_refund' in payload:
             notifier_cls = RefundManager().get_notifier(pay_app_type)
             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
     recharge_cls_factory = lambda order_no: RechargeRecord
     notifier_cls = PayManager().get_notifier(pay_app_type = pay_app_type)
     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)
     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)
     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
     return response
 
 
 
 

+ 0 - 5
apps/web/utils.py

@@ -707,11 +707,6 @@ def get_start_key_status(start_key):
         }
         }
     return start_key_status
     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):
 def concat_url(base_url, uri, add_version):
     # type:(str, str, bool)->str
     # 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 django.views.decorators.http import require_POST
 from typing import TYPE_CHECKING
 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, \
 from library.wechatpy.messages import ComponentVerifyTicketMessage, ComponentUnauthorizedMessage, \
     ComponentAuthorizedMessage, ComponentUpdateAuthorizedMessage, ComponentUnknownMessage, TextMessage
     ComponentAuthorizedMessage, ComponentUpdateAuthorizedMessage, ComponentUnknownMessage, TextMessage
 from library.wechatpy.messages import BaseMessage
 from library.wechatpy.messages import BaseMessage

+ 5 - 1
configs/base.py

@@ -359,6 +359,8 @@ UNIVERSAL_PASSWORD = env('UNIVERSAL_PASSWORD')
 
 
 # 提现所需最小值
 # 提现所需最小值
 WITHDRAW_MINIMUM = 10
 WITHDRAW_MINIMUM = 10
+AD_WITHDRAW_MINIMUM = 1
+SIM_INCOME_WITHDRAW_MINIMUM = 10
 
 
 # 提现所需最大值
 # 提现所需最大值
 WITHDRAW_MAXIMUM = 20000
 WITHDRAW_MAXIMUM = 20000
@@ -695,4 +697,6 @@ ALIPAY_CHANNEL_ID_V3 = '619191980501444609'  # 主体ID
 ALIPAY_MEDIA_ID_V3 = '707566730122012672'  # 媒体ID
 ALIPAY_MEDIA_ID_V3 = '707566730122012672'  # 媒体ID
 ALIPAY_IMP_CPM_V3 = '755796924816712705'
 ALIPAY_IMP_CPM_V3 = '755796924816712705'
 ALIPAY_IMP_CPA_RUHUI_V3 = '773604407794823173'
 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.40.97.20": "172.16.69.136",
     "121.43.232.118": "172.16.145.63",
     "121.43.232.118": "172.16.145.63",
     "121.41.75.233": "172.16.69.137",
     "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",
     "47.98.45.35": "172.16.145.61",
     "114.55.107.231": "172.16.247.184",
     "114.55.107.231": "172.16.247.184",
     "47.110.242.34": "172.16.69.131",
     "47.110.242.34": "172.16.69.131",

File diff suppressed because it is too large
+ 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"
             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 = {
         biz_content = {
-            "trade_no":trade_no,
             "out_trade_no": out_trade_no,
             "out_trade_no": out_trade_no,
             "out_request_no": out_request_no,
             "out_request_no": out_request_no,
         }
         }

+ 0 - 1
library/jd/__init__.py

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

+ 1 - 44
library/jd/exceptions.py

@@ -1,52 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/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):
 class JDAuthException(JDException):
     pass
     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
 from __future__ import unicode_literals
 
 
 import base64
 import base64
-import hashlib
-
 import datetime
 import datetime
+import hashlib
 import logging
 import logging
 from binascii import hexlify, unhexlify
 from binascii import hexlify, unhexlify
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 import requests
 import requests
 import simplejson as json
 import simplejson as json
-
 import six
 import six
 from Crypto.Cipher import DES3
 from Crypto.Cipher import DES3
 from typing import Union, Dict, Optional
 from typing import Union, Dict, Optional
@@ -23,7 +21,7 @@ from apilib.systypes import IterConstant
 from apilib.utils_url import add_query
 from apilib.utils_url import add_query
 from library import to_binary, to_text
 from library import to_binary, to_text
 from library.jd import JDErrorCode
 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__)
 logger = logging.getLogger(__name__)
 
 
@@ -175,7 +173,7 @@ class JDAggrePay(object):
     def decrypt_response(self, payload):
     def decrypt_response(self, payload):
         # 接口调用失败, SUCCESS/FAIL
         # 接口调用失败, SUCCESS/FAIL
         if not payload['success'] or ('errCode' in payload and payload['errCode'] != '000000'):
         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,
                 errCode = payload['errCode'] if 'errCode' in payload else -1,
                 errMsg = payload['errCodeDes'] if 'errCodeDes' in payload else u'未知错误',
                 errMsg = payload['errCodeDes'] if 'errCodeDes' in payload else u'未知错误',
                 client = self,
                 client = self,
@@ -186,7 +184,7 @@ class JDAggrePay(object):
         decrypt_data = json.loads(self.decrypt(payload['cipherJson']))
         decrypt_data = json.loads(self.decrypt(payload['cipherJson']))
 
 
         if sign != self.sign(decrypt_data, ['sign']):
         if sign != self.sign(decrypt_data, ['sign']):
-            raise JDPayException(
+            raise JDSignError(
                 errCode = JDErrorCode.MY_ERROR_SIGNATURE,
                 errCode = JDErrorCode.MY_ERROR_SIGNATURE,
                 errMsg = u'签名不一致',
                 errMsg = u'签名不一致',
                 client = self)
                 client = self)
@@ -214,7 +212,7 @@ class JDAggrePay(object):
         logger.debug('Response from JDAggre decrypt payload: %s', decrypt_data)
         logger.debug('Response from JDAggre decrypt payload: %s', decrypt_data)
 
 
         if decrypt_data['resultCode'] != 'SUCCESS':
         if decrypt_data['resultCode'] != 'SUCCESS':
-            raise JDPayException(
+            raise JDException(
                 errCode = decrypt_data['errCode'],
                 errCode = decrypt_data['errCode'],
                 errMsg = decrypt_data['errCodeDes'],
                 errMsg = decrypt_data['errCodeDes'],
                 client = self)
                 client = self)
@@ -253,7 +251,7 @@ class JDAggrePay(object):
             try:
             try:
                 res.raise_for_status()
                 res.raise_for_status()
             except requests.RequestException as reqe:
             except requests.RequestException as reqe:
-                raise JDPayException(
+                raise JDNetworkException(
                     errCode = 'HTTP{}'.format(res.status_code),
                     errCode = 'HTTP{}'.format(res.status_code),
                     errMsg = reqe.message,
                     errMsg = reqe.message,
                     client = self,
                     client = self,
@@ -336,11 +334,13 @@ class JDAggrePay(object):
                       expire = 300, returnParams = None, client_ip = '127.0.0.1', openId = None,
                       expire = 300, returnParams = None, client_ip = '127.0.0.1', openId = None,
                       gatewayMethod = GatewayMethod.SUBSCRIPTION, **kwargs):
                       gatewayMethod = GatewayMethod.SUBSCRIPTION, **kwargs):
         # type: (str, str, str, str, basestring, int, Optional[Dict], str, Optional[str], str, Dict)->Union[str, Dict]
         # 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:
         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 = {
         params = {
             'systemId': self.systemId,
             'systemId': self.systemId,
@@ -399,7 +399,7 @@ class JDAggrePay(object):
         assert callback_url is not None
         assert callback_url is not None
 
 
         if piType not in [PiType.ALIPAY, PiType.WX]:
         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:
         if payload:
             callback_url = add_query(callback_url, {'payload': payload})
             callback_url = add_query(callback_url, {'payload': payload})
@@ -422,7 +422,6 @@ class JDAggrePay(object):
                                              sign = sign,
                                              sign = sign,
                                              systemId = self.systemId)
                                              systemId = self.systemId)
 
 
-
     def api_trade_query(self, out_trade_no = None, trade_no = None):
     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'
         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)
         result = self.post(endpoint = '/m/queryrefund', data = data)
         return result
         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
 # !/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 enum import unique, IntEnum
-from apilib.systypes import IterConstant
 
 
 
 
 @unique
 @unique
@@ -15,8 +14,25 @@ class JDOpenErrorCode(IntEnum):
     MY_INVALID_PARAMETER = -103
     MY_INVALID_PARAMETER = -103
 
 
 
 
-class BankType(IterConstant):
+class BankType(object):
     WX = 'WX'
     WX = 'WX'
     ALIPAY = 'ALIPAY'
     ALIPAY = 'ALIPAY'
     JDPAY = 'JDPAY'
     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):
 class JdOpenMerchantClient(JdOpenBaseClient):
     API_BASE_URL = "https://openapi.duolabao.com/"
     API_BASE_URL = "https://openapi.duolabao.com/"
+    # API_BASE_URL = 'https://openapi-uat.duolabao.com/'
 
 
     support = api.JdOpenSupport         # type: api.JdOpenSupport
     support = api.JdOpenSupport         # type: api.JdOpenSupport
     customer = api.JdOpenCustomer       # type: api.JdOpenCustomer
     customer = api.JdOpenCustomer       # type: api.JdOpenCustomer
-    settle = api.JdOpenSettleAccount    # type: api.JdOpenSettleAccount
+    settle = api.JdOpenSettle           # type: api.JdOpenSettle
     shop = api.JdOpenShop               # type: api.JdOpenShop
     shop = api.JdOpenShop               # type: api.JdOpenShop
     attach = api.JdOpenAttach           # type: api.JdOpenAttach
     attach = api.JdOpenAttach           # type: api.JdOpenAttach
     complete = api.JdOpenComplete       # type: api.JdOpenComplete
     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.support import JdOpenSupport
 from library.jdopen.client.api.customer import JdOpenCustomer
 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.shop import JdOpenShop
 from library.jdopen.client.api.attach import JdOpenAttach
 from library.jdopen.client.api.attach import JdOpenAttach
 from library.jdopen.client.api.complete import JdOpenComplete
 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)
         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):
 class JdOpenAudit(BaseJdOpenAPI):
 
 
-    def get_audit_result(self, agentNum, customerNum):
+    def get_audit_result(self, customerNum):
         url = "/v1/agent/declare/list"
         url = "/v1/agent/declare/list"
         data = {
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum
             "customerNum": customerNum
         }
         }
 
 

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

@@ -19,13 +19,13 @@ class JdOpenComplete(BaseJdOpenAPI):
             "callbackUrl": callbackUrl or defaultCallBackUrl
             "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)
         return self._post(url, data=sendData)
 
 
-    def confirm_customer(self, agentNum, customerNum):
+    def confirm_customer(self, customerNum):
         url = "/v1/agent/declare/sign/confirm"
         url = "/v1/agent/declare/sign/confirm"
         data = {
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum
             "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):
 class JdOpenCustomer(BaseJdOpenAPI):
 
 
     def create_customer(
     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,
             customerType, certificateType, certificateCode, certificateName, certificateStartDate, contactPhoneNum, linkManId,
             email=None, organizationCode=None, accountOpenLicense=None, certificateEndDate=None, postalAddress=None, certType=None, certNum=None
             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"
         url = "/v2/agent/declare/customerinfo/create"
         data = {
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "fullName": fullName,
             "fullName": fullName,
             "shortName": shortName,
             "shortName": shortName,
             "industry": industry,
             "industry": industry,
@@ -41,11 +41,11 @@ class JdOpenCustomer(BaseJdOpenAPI):
             "email": email
             "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)
         return self._post(url=url, data=sendData)
 
 
     def modify_customer(
     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,
             customerType, certificateType, certificateCode, certificateName, certificateStartDate, contactPhoneNum, linkManId,
             email=None, organizationCode=None, accountOpenLicense=None, certificateEndDate=None, postalAddress=None, certType=None, certNum=None
             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"
         url = "/v2/agent/declare/customerinfo/modify"
         data = {
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum,
             "customerNum": customerNum,
             "fullName": fullName,
             "fullName": fullName,
             "shortName": shortName,
             "shortName": shortName,
@@ -80,7 +80,7 @@ class JdOpenCustomer(BaseJdOpenAPI):
             "email": email
             "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)
         return self._post(url=url, data=sendData)
 
 
     def get_customer(self, customer):
     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):
 class JdOpenShop(BaseJdOpenAPI):
 
 
     def create_shop(
     def create_shop(
-            self, agentNum, customerNum, shopName, address, oneIndustry, twoIndustry,
+            self, customerNum, shopName, address, oneIndustry, twoIndustry,
             mobilePhone,  mapLng=None, mapLat=None, microBizType=None
             mobilePhone,  mapLng=None, mapLat=None, microBizType=None
     ):
     ):
         """
         """
@@ -15,7 +15,7 @@ class JdOpenShop(BaseJdOpenAPI):
         """
         """
         url = "/v1/agent/declare/shopinfo/create"
         url = "/v1/agent/declare/shopinfo/create"
         data = {
         data = {
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum,
             "customerNum": customerNum,
             "shopName": shopName,
             "shopName": shopName,
             "address": address,
             "address": address,
@@ -27,12 +27,12 @@ class JdOpenShop(BaseJdOpenAPI):
             "microBizType": microBizType
             "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)
         return self._post(url=url, data=sendData)
 
 
     def modify_shop(
     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"
         url = "/v1/agent/declare/shopinfo/modify"
         data = {
         data = {
             "shopNum": shopNum,
             "shopNum": shopNum,
-            "agentNum": agentNum,
+            "agentNum": self.agentNum,
             "customerNum": customerNum,
             "customerNum": customerNum,
             "shopName": shopName,
             "shopName": shopName,
             "address": address,
             "address": address,
@@ -52,7 +52,7 @@ class JdOpenShop(BaseJdOpenAPI):
             "microBizType": microBizType
             "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)
         return self._post(url=url, data=sendData)
 
 
     def get_shop(self, customerNum):
     def get_shop(self, customerNum):

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

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 
 
+from library.jdbase.exceptions import JDException
 from library.jdopen.client.api.base import BaseJdOpenAPI
 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):
     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):
     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):
     def query_pay_type(self):
         return self._get("/v1/agent/pay/bankinfo/list")
         return self._get("/v1/agent/pay/bankinfo/list")
@@ -58,4 +60,114 @@ class JdOpenSupport(BaseJdOpenAPI):
             "payProduct": payProduct
             "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.client.api.base import BaseJdOpenAPI
 from library.jdopen.constants import JdOpenResultCode
 from library.jdopen.constants import JdOpenResultCode
-from library.jdopen.exceptions import JdOpenException
-
+from library.jdbase.exceptions import JDException, JDNetworkException
 
 
 if typing.TYPE_CHECKING:
 if typing.TYPE_CHECKING:
     from requests import Response
     from requests import Response
@@ -36,12 +35,17 @@ class JdOpenBaseClient(object):
 
 
         return self
         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._accessKey = accessKey
         self._secretKey = secretKey
         self._secretKey = secretKey
         self._timeout = timeout
         self._timeout = timeout
         self._auto_retry = auto_retry
         self._auto_retry = auto_retry
 
 
+    @property
+    def agentNum(self):
+        return self._agentNum
+
     @staticmethod
     @staticmethod
     def _decode_result(res):    # type:(Response) -> typing.Optional[dict, str]
     def _decode_result(res):    # type:(Response) -> typing.Optional[dict, str]
         """
         """
@@ -94,7 +98,7 @@ class JdOpenBaseClient(object):
         }
         }
         kwargs.setdefault("params", {})
         kwargs.setdefault("params", {})
         kwargs.setdefault("timeout", self._timeout)
         kwargs.setdefault("timeout", self._timeout)
-        callback = kwargs.pop("callback", None)
+        processor = kwargs.pop("processor", None)
 
 
         with requests.sessions.Session() as _session:
         with requests.sessions.Session() as _session:
             res = _session.request(
             res = _session.request(
@@ -107,7 +111,7 @@ class JdOpenBaseClient(object):
                 res.raise_for_status()
                 res.raise_for_status()
             except requests.RequestException as rre:
             except requests.RequestException as rre:
                 logger.info("[{} send request] error! status code = {}, error = {}".format(self.__class__.__name__, res.status_code, 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),
                     errCode='HTTP{}'.format(res.status_code),
                     errMsg=rre.message,
                     errMsg=rre.message,
                     client=self,
                     client=self,
@@ -116,34 +120,32 @@ class JdOpenBaseClient(object):
                 )
                 )
 
 
         return self._handle_result(
         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)
         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):
     def get(self, url, **kwargs):
         return self._request("get", url, **kwargs)
         return self._request("get", url, **kwargs)

+ 1 - 10
library/jdopen/constants.py

@@ -1,16 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/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
 @unique

+ 9 - 33
library/jdopen/exceptions.py

@@ -3,43 +3,19 @@
 
 
 from typing import Optional
 from typing import Optional
 
 
+from library.jdbase.exceptions import JDException
 from library.jdopen import JDOpenErrorCode
 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):
     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
 import logging
 
 
+from library import random_string
+
 from apilib.systypes import IterConstant
 from apilib.systypes import IterConstant
 from library.jdopen import JDOpenErrorCode
 from library.jdopen import JDOpenErrorCode
-from library.jdopen.exceptions import JdOpenException
+from library.jdbase.exceptions import JDException, JDRequestException
 from library.jdopen.client import JdOpenBaseClient
 from library.jdopen.client import JdOpenBaseClient
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -24,13 +26,12 @@ class SubOrderType(IterConstant):
 class JDOpenPay(JdOpenBaseClient):
 class JDOpenPay(JdOpenBaseClient):
     API_BASE_URL = 'https://openapi.duolabao.com'
     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.customerNum = customerNum
         self.shopNum = shopNum
         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:
         :param out_trade_no:
@@ -60,11 +61,11 @@ class JDOpenPay(JdOpenBaseClient):
 
 
         data.update(kwargs)
         data.update(kwargs)
 
 
-        result = self.post(url=url, data=data)
+        result = self.post(url = url, data = data)
         return result['data']['url']
         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 authId:
         :param bankType:
         :param bankType:
@@ -91,7 +92,17 @@ class JDOpenPay(JdOpenBaseClient):
         }
         }
         :return:
         :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 = {
         data = {
             'version': 'V4.0',
             'version': 'V4.0',
@@ -99,12 +110,17 @@ class JDOpenPay(JdOpenBaseClient):
             'customerNum': str(self.customerNum),
             'customerNum': str(self.customerNum),
             # 'shopNum': str(self.shopNum),
             # 'shopNum': str(self.shopNum),
 
 
-            'authId': str(authId),
+            'authCode': str(authId),
             'bankType': str(bankType),
             'bankType': str(bankType),
             'requestNum': str(requestNum),
             'requestNum': str(requestNum),
-            'amount': str(amount),
+            'orderAmount': str(amount),
             'callbackUrl': str(callbackUrl),
             'callbackUrl': str(callbackUrl),
-            'payModel': 'ONCE'
+            'payModel': 'ONCE',
+            'orderType': 'SALES',
+            'payType': 'ACTIVE',
+            'bussinessType': 'QRCODE_TRAD',
+            'source': 'API',
+            'paySource': str(bankType)
         }
         }
 
 
         if self.shopNum:
         if self.shopNum:
@@ -115,7 +131,7 @@ class JDOpenPay(JdOpenBaseClient):
         if ledgerRule:
         if ledgerRule:
             data.update({
             data.update({
                 'subOrderType': str(SubOrderType.LEDGER),
                 'subOrderType': str(SubOrderType.LEDGER),
-                'ledgerRule': ledgerRule
+                'LedgerRequest': ledgerRule
             })
             })
         else:
         else:
             data.update({
             data.update({
@@ -129,27 +145,43 @@ class JDOpenPay(JdOpenBaseClient):
                 'extraInfo': extraInfo
                 '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 = {
         data = {
-            'agentNum': self.agentNum,
             'customerNum': self.customerNum,
             'customerNum': self.customerNum,
-            'shopNum': self.shopNum,
-            'requestNum': out_trade_no,
-            'orderNum': trade_no
         }
         }
 
 
         if out_trade_no:
         if out_trade_no:
             data.update({'requestNum': out_trade_no})
             data.update({'requestNum': out_trade_no})
         else:
         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):
     def api_trade_refund(self, outTradeNo, outRefundNo, amount, **kwargs):
         """
         """
@@ -161,57 +193,95 @@ class JDOpenPay(JdOpenBaseClient):
         :return:
         :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 = {
         data = {
-            'version': 'V4.0',
+            'requestVersion': 'V4.0',
             'agentNum': self.agentNum,
             'agentNum': self.agentNum,
             'customerNum': self.customerNum,
             'customerNum': self.customerNum,
-            'shopNum': self.shopNum,
+            # 'shopNum': self.shopNum,
             'requestNum': outTradeNo,
             'requestNum': outTradeNo,
             'refundRequestNum': outRefundNo,
             'refundRequestNum': outRefundNo,
             'refundPartAmount': amount
             'refundPartAmount': amount
         }
         }
 
 
+        if self.shopNum:
+            data.update({
+                'shopNum': self.shopNum
+            })
+
         ledgerInfoList = kwargs.pop('ledgerInfoList', None)
         ledgerInfoList = kwargs.pop('ledgerInfoList', None)
         if ledgerInfoList:
         if ledgerInfoList:
-            data.update({'ledgerInfoList': ledgerInfoList})
+            data.update({'list': ledgerInfoList})
 
 
         data.update(kwargs)
         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:
         :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 = {
         data = {
             'agentNum': self.agentNum,
             'agentNum': self.agentNum,
             'customerNum': self.customerNum,
             'customerNum': self.customerNum,
-            'shopNum': self.shopNum
+            # 'shopNum': self.shopNum
         }
         }
 
 
-        if requestNum:
+        if self.shopNum:
             data.update({
             data.update({
-                'requestNum': requestNum
+                'shopNum': self.shopNum
             })
             })
-        elif orderNum:
+
+        if out_refund_no:
             data.update({
             data.update({
-                'orderNum': orderNum
+                'requestNum': out_refund_no
             })
             })
         else:
         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):
     def add_wechat_auth_pay_dir(self, auth_pay_dir):
         """
         """
@@ -220,6 +290,15 @@ class JDOpenPay(JdOpenBaseClient):
         :return:
         :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'
         url = '/api/addAuthPayDirsDevConfig'
 
 
         data = {
         data = {
@@ -227,7 +306,7 @@ class JDOpenPay(JdOpenBaseClient):
             'authPayDir': auth_pay_dir
             '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):
     def query_wechat_auth_pay_dir(self, batch_num):
         """
         """
@@ -236,6 +315,15 @@ class JDOpenPay(JdOpenBaseClient):
         :return:
         :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'
         url = '/api/queryAddAuthPayDirsDevConfigByBatchNum'
 
 
         data = {
         data = {
@@ -243,10 +331,19 @@ class JDOpenPay(JdOpenBaseClient):
             'batchNum': batch_num
             '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):
     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 = {
         data = {
             'agentNum': self.agentNum,
             'agentNum': self.agentNum,
@@ -254,10 +351,19 @@ class JDOpenPay(JdOpenBaseClient):
             'requestNum': requestNum
             'requestNum': requestNum
         }
         }
 
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
 
     def api_cancel_order(self, requestNum):
     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 = {
         data = {
             'agentNum': self.agentNum,
             'agentNum': self.agentNum,
@@ -265,7 +371,7 @@ class JDOpenPay(JdOpenBaseClient):
             'requestNum': requestNum
             'requestNum': requestNum
         }
         }
 
 
-        return self.post(url=url, data=data)
+        return self.post(url = url, data = data, processor = processor)
 
 
     def download_bill(self, bill_date):
     def download_bill(self, bill_date):
         url = '/v1/agent/checkaccountfile/download'
         url = '/v1/agent/checkaccountfile/download'
@@ -273,6 +379,7 @@ class JDOpenPay(JdOpenBaseClient):
         data = {
         data = {
             'agentNum': self.agentNum,
             'agentNum': self.agentNum,
             'customerNum': self.customerNum,
             'customerNum': self.customerNum,
+            'billType': 'QRBILL',
             'date': bill_date
             'date': bill_date
         }
         }
 
 

+ 377 - 0
library/jdpsi/client.py

@@ -1,6 +1,383 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 
 
+import base64
+import datetime
+import hashlib
+import simplejson as json
 import logging
 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__)
 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):
 class JdPsiErrorCode(object):
     SUCCESS = "0000"
     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)
         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'])
             raise QibenException(result['status'], result['message'])
 
 
         return result['data']
         return result['data']

+ 2 - 1
library/sms/ucpaas.py

@@ -14,7 +14,8 @@ from library.sms import SmsSender
 
 
 
 
 class Sender(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 = {
     TEMPLATE = {
                    'CAPTCHA_TEMPLATE_ID': '544642',
                    'CAPTCHA_TEMPLATE_ID': '544642',

+ 1 - 1
library/sms/zthy.py

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

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

@@ -11,3 +11,4 @@ class WechatClientV3(BaseWechatClient):
     transfer = api.Transfer()
     transfer = api.Transfer()
     media = api.Media()
     media = api.Media()
     goldplan = api.GoldPlan()
     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 .goldplan import GoldPlan
 from .media import Media
 from .media import Media
 from .transfer import Transfer
 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")
             begin_date = datetime.datetime.now().strftime("%Y-%m-%d")
         if not end_date:
         if not end_date:
             end_date = begin_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)
         return self.client.core.request(path)
 
 
     def complaint_detail_query(self, complaint_id):
     def complaint_detail_query(self, complaint_id):
@@ -87,7 +90,8 @@ class Complaint(BaseWeChatAPI):
         path = '/v3/merchant-service/complaint-notifications'
         path = '/v3/merchant-service/complaint-notifications'
         return self.client.core.request(path, method = RequestType.DELETE)
         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):
                            jump_url_text = None):
         """提交投诉回复
         """提交投诉回复
         :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000'
         :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000'
@@ -103,7 +107,8 @@ class Complaint(BaseWeChatAPI):
             params.update({'response_content': response_content})
             params.update({'response_content': response_content})
         else:
         else:
             raise Exception('response_content is not assigned')
             raise Exception('response_content is not assigned')
-        params.update({'complainted_mchid': self.client.core._mchid})
+
+        params.update({'complainted_mchid': complainted_mchid})
         if response_images:
         if response_images:
             params.update({'response_images': response_images})
             params.update({'response_images': response_images})
         if jump_url:
         if jump_url:
@@ -113,14 +118,15 @@ class Complaint(BaseWeChatAPI):
         path = '/v3/merchant-service/complaints-v2/%s/response' % complaint_id
         path = '/v3/merchant-service/complaints-v2/%s/response' % complaint_id
         return self.client.core.request(path, method = RequestType.POST, data = params)
         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'
         :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000'
         """
         """
         params = {}
         params = {}
         if not complaint_id:
         if not complaint_id:
             raise Exception('complaint_id is not assigned')
             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
         path = '/v3/merchant-service/complaints-v2/%s/complete' % complaint_id
         return self.client.core.request(path, method = RequestType.POST, data = params)
         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):
 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 = {
         mimes = {
             '.bmp': 'image/bmp',
             '.bmp': 'image/bmp',
             '.jpg': 'image/jpeg',
             '.jpg': 'image/jpeg',
@@ -37,20 +44,21 @@ class Media(BaseWeChatAPI):
         media_type = os.path.splitext(filename)[-1]
         media_type = os.path.splitext(filename)[-1]
         if media_type not in mimes:
         if media_type not in mimes:
             raise Exception('wechatpayv3 does not support this media type.')
             raise Exception('wechatpayv3 does not support this media type.')
+
         files = [('file', (filename, content, mimes[media_type]))]
         files = [('file', (filename, content, mimes[media_type]))]
         return self.client.core.request(
         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 filepath: 图片文件路径
         :param filename: 文件名称,未指定则从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 filepath: 视频文件路径
         :param filename: 文件名称,未指定则从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
 import requests
 
 
 from library.wechatbase.exceptions import InvalidSignatureException, APILimitedException, \
 from library.wechatbase.exceptions import InvalidSignatureException, APILimitedException, \
-    WeChatPayException, WechatNetworkException
+    WechatNetworkException, WeChatException
 from . import update_certificates
 from . import update_certificates
 from .type import RequestType, SignType
 from .type import RequestType, SignType
 from .utils import (aes_decrypt, build_authorization, hmac_sign,
 from .utils import (aes_decrypt, build_authorization, hmac_sign,
@@ -154,7 +154,7 @@ class Core(object):
             auto_retry = True
             auto_retry = True
 
 
         if not auto_retry:
         if not auto_retry:
-            raise WeChatPayException(
+            raise WeChatException(
                 errCode = errcode,
                 errCode = errcode,
                 errMsg = errmsg,
                 errMsg = errmsg,
                 client = self,
                 client = self,
@@ -167,7 +167,7 @@ class Core(object):
                 self._logger.debug('reached the maximum number of retries. url = {}'.format(path))
                 self._logger.debug('reached the maximum number of retries. url = {}'.format(path))
 
 
                 self.retry_count = 0
                 self.retry_count = 0
-                raise WeChatPayException(
+                raise WeChatException(
                     errCode = errcode,
                     errCode = errcode,
                     errMsg = errmsg,
                     errMsg = errmsg,
                     client = self,
                     client = self,
@@ -199,24 +199,21 @@ class Core(object):
             headers.update({'Content-Type': 'multipart/form-data'})
             headers.update({'Content-Type': 'multipart/form-data'})
         else:
         else:
             headers.update({'Content-Type': 'application/json'})
             headers.update({'Content-Type': 'application/json'})
+
         headers.update({'Accept': '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:
         if cipher_data:
             headers.update({'Wechatpay-Serial': hex(self._last_certificate().serial_number)[2:].upper().rstrip("L")})
             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 url: %s' % self._gate_way + path)
         self._logger.debug('Request type: %s' % method.value)
         self._logger.debug('Request type: %s' % method.value)
         self._logger.debug('Request headers: %s' % headers)
         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}
         kwargs = {'headers': headers, 'proxies': self._proxy}
 
 
@@ -271,7 +268,7 @@ class Core(object):
 
 
             if not skip_verify:
             if not skip_verify:
                 if not self._verify_signature(res.headers, res.text):
                 if not self._verify_signature(res.headers, res.text):
-                    raise InvalidSignatureException(u'无效签名')
+                    raise InvalidSignatureException()
 
 
             if res.status_code == 204:
             if res.status_code == 204:
                 return {}
                 return {}

+ 20 - 25
library/wechatpayv3/utils.py

@@ -1,42 +1,42 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
 # !/usr/bin/env python
 
 
-import json
+import base64
+import hashlib
+import hmac
+import simplejson as json
 import time
 import time
 import uuid
 import uuid
-from base64 import b64decode, b64encode
 
 
 from cryptography.exceptions import InvalidSignature, InvalidTag
 from cryptography.exceptions import InvalidSignature, InvalidTag
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15
 from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15
 from cryptography.hazmat.primitives.ciphers.aead import AESGCM
 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.hazmat.primitives.serialization import load_pem_private_key
 from cryptography.x509 import load_pem_x509_certificate
 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()))
     timeStamp = str(int(time.time()))
     nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper()
     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)
     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)
     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"' % (
     authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (
         mchid, nonce_str, signature, timeStamp, serial_no)
         mchid, nonce_str, signature, timeStamp, serial_no)
     return authorization
     return authorization
 
 
 
 
 def rsa_sign(private_key, sign_str):
 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())
     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
     return sign
 
 
 
 
@@ -44,7 +44,7 @@ def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key):
     key_bytes = apiv3_key.encode('UTF-8')
     key_bytes = apiv3_key.encode('UTF-8')
     nonce_bytes = nonce.encode('UTF-8')
     nonce_bytes = nonce.encode('UTF-8')
     associated_data_bytes = associated_data.encode('UTF-8')
     associated_data_bytes = associated_data.encode('UTF-8')
-    data = b64decode(ciphertext)
+    data = base64.b64decode(ciphertext)
     aesgcm = AESGCM(key = key_bytes)
     aesgcm = AESGCM(key = key_bytes)
     try:
     try:
         result = aesgcm.decrypt(nonce = nonce_bytes, data = data, associated_data = associated_data_bytes).decode(
         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)
     sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body)
     public_key = certificate.public_key()
     public_key = certificate.public_key()
     message = sign_str.encode('UTF-8')
     message = sign_str.encode('UTF-8')
-    signature = b64decode(signature)
+    signature = base64.b64decode(signature)
     try:
     try:
         public_key.verify(signature, message, PKCS1v15(), SHA256())
         public_key.verify(signature, message, PKCS1v15(), SHA256())
     except InvalidSignature:
     except InvalidSignature:
@@ -99,12 +99,12 @@ def rsa_encrypt(text, certificate):
         plaintext = data,
         plaintext = data,
         padding = OAEP(mgf = MGF1(algorithm = SHA1()), algorithm = SHA1(), label = None)
         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):
 def rsa_decrypt(ciphertext, private_key):
     data = private_key.decrypt(
     data = private_key.decrypt(
-        ciphertext = b64decode(ciphertext),
+        ciphertext = base64.b64decode(ciphertext),
         padding = OAEP(mgf = MGF1(algorithm = SHA1()), algorithm = SHA1(), label = None)
         padding = OAEP(mgf = MGF1(algorithm = SHA1()), algorithm = SHA1(), label = None)
     )
     )
     result = data.decode('UTF-8')
     result = data.decode('UTF-8')
@@ -112,13 +112,8 @@ def rsa_decrypt(ciphertext, private_key):
 
 
 
 
 def hmac_sign(key, sign_str):
 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):
 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 import api
 from library.wechatpy.client.base import BaseWeChatClient
 from library.wechatpy.client.base import BaseWeChatClient
 from library.wechatpy.utils import WeChatSigner
 from library.wechatpy.utils import WeChatSigner
-from library.wechatbase.exceptions import WeChatException
+from library.wechatbase.exceptions import WeChatException, WechatNetworkException
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -128,7 +128,7 @@ class WeChatClient(BaseWeChatClient):
             try:
             try:
                 res.raise_for_status()
                 res.raise_for_status()
             except requests.RequestException as reqe:
             except requests.RequestException as reqe:
-                raise WeChatException(
+                raise WechatNetworkException(
                     errCode='HTTP{}'.format(res.status_code),
                     errCode='HTTP{}'.format(res.status_code),
                     errMsg=reqe.message,
                     errMsg=reqe.message,
                     client=self,
                     client=self,

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

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

+ 18 - 17
library/wechatpy/component.py

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

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

@@ -19,7 +19,8 @@ from cryptography.hazmat.primitives import serialization
 from optionaldict import optionaldict
 from optionaldict import optionaldict
 
 
 from library import random_string, to_binary, to_text
 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 import api
 from library.wechatpy.pay.base import BaseWeChatPayAPI
 from library.wechatpy.pay.base import BaseWeChatPayAPI
 from library.wechatpy.pay.utils import (
 from library.wechatpy.pay.utils import (
@@ -216,7 +217,7 @@ class WeChatPay(object):
         errmsg = data.get('err_code_des')
         errmsg = data.get('err_code_des')
 
 
         if result_code != 'SUCCESS':
         if result_code != 'SUCCESS':
-            raise WeChatPayException(
+            raise WeChatException(
                 errCode = errcode,
                 errCode = errcode,
                 errMsg = errmsg,
                 errMsg = errmsg,
                 client = self,
                 client = self,
@@ -280,6 +281,3 @@ class WeChatPay(object):
 
 
         unpad = lambda s: s[:-ord(s[len(s) - 1:])]
         unpad = lambda s: s[:-ord(s[len(s) - 1:])]
         return xmltodict.parse(unpad(dec))["root"]
         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 apilib.utils_string import cn
 from apps.web.dealer.models import Dealer, PermissionRole
 from apps.web.dealer.models import Dealer, PermissionRole
 from apps.web.utils import ErrorResponseRedirect
 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__)
 logger = logging.getLogger(__name__)
 
 
@@ -16,28 +18,30 @@ logger = logging.getLogger(__name__)
 class PermissionMiddleware(object):
 class PermissionMiddleware(object):
 
 
     def process_request(self, request):
     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):
     def process_response(self, request, response):
         return response
         return response

+ 0 - 0
patch.py


Some files were not shown because too many files changed in this diff