mopybird %!s(int64=2) %!d(string=hai) anos
achega
144334abef
Modificáronse 100 ficheiros con 16964 adicións e 0 borrados
  1. 43 0
      __init__.py
  2. 2 0
      apilib/__init__.py
  3. 126 0
      apilib/bank_card_utils.py
  4. 16 0
      apilib/constants.py
  5. 43 0
      apilib/exceptions.py
  6. 294 0
      apilib/img_utils.py
  7. 2 0
      apilib/inference/__init__.py
  8. 2 0
      apilib/inference/types.py
  9. 2 0
      apilib/loghelper/__init__.py
  10. 12 0
      apilib/loghelper/filters.py
  11. 37 0
      apilib/loghelper/middleware.py
  12. 15 0
      apilib/loghelper/utils.py
  13. 12 0
      apilib/loghelper/wsgi.py
  14. 310 0
      apilib/monetary.py
  15. 23 0
      apilib/numerics.py
  16. 79 0
      apilib/quantity.py
  17. 93 0
      apilib/systypes.py
  18. 591 0
      apilib/utils.py
  19. 38 0
      apilib/utils_AES.py
  20. 156 0
      apilib/utils_datetime.py
  21. 122 0
      apilib/utils_json.py
  22. 89 0
      apilib/utils_mongo.py
  23. 4 0
      apilib/utils_mqtt.py
  24. 126 0
      apilib/utils_string.py
  25. 149 0
      apilib/utils_sys.py
  26. 206 0
      apilib/utils_url.py
  27. 22 0
      apps/__init__.py
  28. 0 0
      apps/accounting/__init__.py
  29. 67 0
      apps/accounting/reconcile.py
  30. 0 0
      apps/common/__init__.py
  31. 167 0
      apps/common/utils.py
  32. 2 0
      apps/dispatch/__init__.py
  33. 55 0
      apps/dispatch/commands.py
  34. 44 0
      apps/dispatch/common.py
  35. 151 0
      apps/dispatch/tasks.py
  36. 47 0
      apps/patch/__init__.py
  37. 0 0
      apps/provision/__init__.py
  38. 1172 0
      apps/provision/bank.py
  39. 2787 0
      apps/provision/bank_card.py
  40. 4 0
      apps/thirdparties/__init__.py
  41. 38 0
      apps/thirdparties/alioss.py
  42. 384 0
      apps/thirdparties/aliyun.py
  43. 24 0
      apps/thirdparties/dingding.py
  44. 3 0
      apps/thirdparties/pulsar.py
  45. 2 0
      apps/visual/__init__.py
  46. 19 0
      apps/visual/base.py
  47. 14 0
      apps/visual/utils.py
  48. 51 0
      apps/visual/visualizers.py
  49. 4 0
      apps/web/__init__.py
  50. 29 0
      apps/web/ad/__init__.py
  51. 13 0
      apps/web/ad/helpers.py
  52. 1097 0
      apps/web/ad/models.py
  53. 19 0
      apps/web/ad/tasks.py
  54. 73 0
      apps/web/ad/urls.py
  55. 124 0
      apps/web/ad/utils.py
  56. 57 0
      apps/web/ad/validation.py
  57. 837 0
      apps/web/ad/views.py
  58. 2 0
      apps/web/agent/__init__.py
  59. 185 0
      apps/web/agent/api.py
  60. 96 0
      apps/web/agent/define.py
  61. 10 0
      apps/web/agent/errors.py
  62. 1710 0
      apps/web/agent/models.py
  63. 139 0
      apps/web/agent/proxy.py
  64. 177 0
      apps/web/agent/urls.py
  65. 4 0
      apps/web/agent/utils.py
  66. 17 0
      apps/web/agent/validation.py
  67. 2272 0
      apps/web/agent/views.py
  68. 40 0
      apps/web/agent/withdraw.py
  69. 2 0
      apps/web/api/__init__.py
  70. 0 0
      apps/web/api/cy4/__init__.py
  71. 18 0
      apps/web/api/cy4/urls.py
  72. 269 0
      apps/web/api/cy4/views.py
  73. 0 0
      apps/web/api/dc/__init__.py
  74. 15 0
      apps/web/api/dc/urls.py
  75. 148 0
      apps/web/api/dc/views.py
  76. 57 0
      apps/web/api/exceptions.py
  77. 0 0
      apps/web/api/ft_north/__init__.py
  78. 18 0
      apps/web/api/ft_north/constant.py
  79. 63 0
      apps/web/api/ft_north/models.py
  80. 12 0
      apps/web/api/ft_north/urls.py
  81. 398 0
      apps/web/api/ft_north/utils.py
  82. 204 0
      apps/web/api/ft_north/views.py
  83. 0 0
      apps/web/api/jh/__init__.py
  84. 17 0
      apps/web/api/jh/urls.py
  85. 110 0
      apps/web/api/jh/views.py
  86. 0 0
      apps/web/api/jn/__init__.py
  87. 14 0
      apps/web/api/jn/urls.py
  88. 42 0
      apps/web/api/jn/views.py
  89. 0 0
      apps/web/api/jn_north/__init__.py
  90. 13 0
      apps/web/api/jn_north/constant.py
  91. 14 0
      apps/web/api/jn_north/urls.py
  92. 330 0
      apps/web/api/jn_north/utils.py
  93. 372 0
      apps/web/api/jn_north/views.py
  94. 192 0
      apps/web/api/models.py
  95. 0 0
      apps/web/api/openluat/__init__.py
  96. 12 0
      apps/web/api/openluat/urls.py
  97. 72 0
      apps/web/api/openluat/views.py
  98. 0 0
      apps/web/api/swap/__init__.py
  99. 22 0
      apps/web/api/swap/urls.py
  100. 0 0
      apps/web/api/swap/views.py

+ 43 - 0
__init__.py

@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+"""
+
+
+                                                        .-.                 ___
+                                                 .-.   /    \              (   )
+                         ___  ___  ___   .--.   ( __)  | .`. ;   ___  ___   | |    .--.
+                        (   )(   )(   ) /    \  (''")  | |(___) (   )(   )  | |   /    \
+                         | |  | |  | | |  .-. ;  | |   | |_      | |  | |   | |  |  .-. ;
+                         | |  | |  | | |  | | |  | |  (   __)    | |  | |   | |  |  | | |
+                         | |  | |  | | |  |/  |  | |   | |       | |  | |   | |  |  |/  |
+                         | |  | |  | | |  ' _.'  | |   | |       | |  | |   | |  |  ' _.'
+                         | |  ; '  | | |  .'.-.  | |   | |       | |  ; '   | |  |  .'.-.
+                         ' `-'   `-' ' '  `-' /  | |   | |       ' `-'  /   | |  '  `-' /
+                          '.__.'.__.'   `.__.'  (___) (___)       '.__.'   (___)  `.__.'
+
+
+                                                     _||λ||_
+                                                    o111111o
+                                                    ||" . "||
+                                                    (| -_- |)
+                                                     O\ = /O
+                                                 ____/`-λ-'\____
+                                               .   ' \\| |// `.
+                                                / \\||| | |||// \
+                                              / _||||| -λ- |||||- \
+                                                | | \\\ - /// | |
+                                              | \_| ''\---/'' | |
+                                               \ .-\__ `-` ___/-. /
+                                            ___`. .' /--.--\ `. . __
+                                         ."" '< `.___\_<λ>_/___.' >'"".
+                                        | | : `- \`.;`\ _ /`;.`/ - ` : | |
+                                          \ \ `-. \_ __\ /__ _/ .-` / /
+                                  ======`-.____`-.___\_____/___.-`____.-'======
+                                                     `=---='
+
+
+"""
+
+__version__ = '0.2.1'
+

+ 2 - 0
apilib/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python

+ 126 - 0
apilib/bank_card_utils.py

@@ -0,0 +1,126 @@
+# coding=utf-8
+import logging
+import json
+import requests
+from urlparse import urljoin
+
+from django.conf import settings
+
+from apilib.exceptions import AliBankCardParamError, AliBankCardSysError
+from apilib.utils_url import add_query
+from apps.web.utils import LimitAttemptsManager
+
+
+logger = logging.getLogger(__name__)
+
+
+class BankRecognizer(object):
+    """
+    查询银行卡联行号相关信息
+    helps: https://market.aliyun.com/products/57000002/cmapi017567.html?spm=5176.12901015.0.i12901015.5737525cxikPYJ#sku=yuncode1156700000
+    """
+    HOST = "https://cnaps.market.alicloudapi.com"
+
+    def __init__(self, visitor, appCode=None, timeout=None):
+        self._appCode = appCode or settings.ALI_BANK_CARD_APP_CODE
+        self._timeout = timeout or 5
+        self._visitor = visitor or "visitor"
+
+        limiter = LimitAttemptsManager(self._visitor, self.__class__.__name__, maxTimes=30 if not settings.DEBUG else 9999)
+        if limiter.is_exceeded_limit():
+            raise AliBankCardParamError(u"访问频率超限")
+        limiter.incr()
+
+    def _request(self, path, callback=None):
+        """
+        目前使用的两个接口比较简单, request没有必要实现的太过于复杂 get请求足矣
+        """
+        url = urljoin(self.HOST, path)
+        headers = {
+            "Authorization": "APPCODE {}".format(self._appCode)
+        }
+
+
+        try:
+            res = requests.get(url ,headers=headers, timeout=self._timeout)
+            res.raise_for_status()
+        except requests.RequestException as ree:
+            logger.warning("[{} _request] send error request error = {}, path={}, appCode = {}******".format(
+                self.__class__.__name__, ree, path, self._appCode[:-6]
+            ))
+            raise AliBankCardParamError(ree.message)
+
+        return callback(res) if callback else res
+
+    @staticmethod
+    def _handle_response(res):
+        """
+        处理查询支行联行号的回调函数
+        """
+        try:
+            result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
+        except (TypeError, ValueError):
+            logger.debug('[_handle_sub_code] Can not decode response as JSON', exc_info=True)
+            raise AliBankCardSysError(u"请求错误(10000)")
+
+        if "resp" not in result:
+            raise AliBankCardSysError(u"请求错误(10001)")
+        if "RespCode" not in result["resp"]:
+            raise AliBankCardSysError(u"请求错误(10002)")
+
+        respCode = result["resp"]["RespCode"]
+        if respCode != "200":
+            raise AliBankCardParamError(result["resp"]["RespMsg"])
+
+        return result
+
+    def query_sub_code(self, bank=None, card=None, province=None, city=None, queryKey=None, page=None):
+        """
+        多条件查询银行支行信息
+        :param bank:
+        :param card:
+        :param province:
+        :param city:
+        :param queryKey:
+        :param page:
+        """
+        path = "/lianzhuo/searchcnaps"
+        params = {
+            "card": card or "", "bank": str(bank or ""),
+            "province": str(province or ""), "city": str(city or ""), "key": str(queryKey or ""), "page": page or 1
+        }
+        path = add_query(path, params)
+
+        return self._request(path, self._handle_response)
+
+    def query_bank(self, card):
+        """
+        根据银行卡查询总行的信息
+        """
+        path = "/lundroid/querybankno"
+        params = {
+            "bankno": card
+        }
+        return self._request(add_query(path, params), self._handle_response)
+
+    def query_all_bank(self, bank):
+        """
+        查询支持的银行总行
+        这个接口不是很好用 建议不要使用
+        bank为关键字
+        """
+        path = "/lundroid/querybank"
+        params = {
+            "bank": str(bank)
+        }
+        return self._request(add_query(path, params), self._handle_response)
+
+    def query_sub_bank(self, subCode):
+        """
+        根据银行的支行联行号 查询银行
+        """
+        path = "/lundroid/querybankcode"
+        params = {
+            "bankcode": subCode
+        }
+        return self._request(add_query(path, params), self._handle_response)

+ 16 - 0
apilib/constants.py

@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+class _Constant(object):
+    class _ConstantError(TypeError):
+        pass
+
+    def __setattr__(self, name, value):
+        if name in self.__dict__:
+            raise self._ConstantError, "Can't rebind constant(%s)" % name
+        self.__dict__[name] = value
+
+    def __delattr__(self, name):
+        if name in self.__dict__:
+            raise self._ConstantError, "Can't unbind constant(%s)" % name
+        raise NameError, name

+ 43 - 0
apilib/exceptions.py

@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+
+class ParameterError(Exception):
+    pass
+
+
+class BaiDuApiError(Exception):
+    pass
+
+
+class BaiDuApiImageError(BaiDuApiError):
+    """
+    百度图像识别错误
+    """
+    pass
+
+
+class BaiDuApiNetError(BaiDuApiError):
+    """
+    百度网络识别错误
+    """
+    pass
+
+
+class BaiDuApiSysError(BaiDuApiError):
+    """
+    百度识别的系统错误
+    """
+    pass
+
+
+class AliBankCardError(Exception):
+    pass
+
+
+class AliBankCardSysError(AliBankCardError):
+    pass
+
+
+class AliBankCardParamError(AliBankCardError):
+    pass

+ 294 - 0
apilib/img_utils.py

@@ -0,0 +1,294 @@
+# coding=utf-8
+import logging
+
+import simplejson as json
+
+from aip import AipOcr
+from django.conf import settings
+from django.core.files.uploadedfile import UploadedFile
+
+from apilib.exceptions import BaiDuApiImageError, BaiDuApiNetError, BaiDuApiSysError
+from apilib.utils import split_addr
+from apilib.utils_datetime import to_datetime
+from apps.web.utils import LimitAttemptsManager
+
+
+logger = logging.getLogger(__name__)
+
+
+
+
+
+def parse_identify_image(result):
+    if not isinstance(result, dict):
+        raise BaiDuApiImageError(u"图像识别错误")
+
+    # 状态字段一定会有 一定需要判断图像的状态
+    if result["image_status"] != "normal":
+        if result["image_status"] == "reversed_side":
+            raise BaiDuApiImageError(u"身份证正反面颠倒,请确认身份证的国徽面或人像面")
+        if result["image_status"] == "non_idcard":
+            raise BaiDuApiImageError(u"上传的图片中不包含身份证,请确认")
+        if result["image_status"] == "blurred":
+            raise BaiDuApiImageError(u"身份证模糊,请重新上传")
+        if result["image_status"] == "other_type_card":
+            raise BaiDuApiImageError(u"上传证件类型错误,请重新上传")
+        if result["image_status"] == "over_exposure":
+            raise BaiDuApiImageError(u"身份证信息识别错误,请重新上传")
+        if result["image_status"] == "over_dark":
+            raise BaiDuApiImageError(u"身份证图像亮度过低,识别错误,请重新上传")
+        if result["image_status"] == "unknown":
+            raise BaiDuApiImageError(u"识别错误,请重新上传")
+
+    # 证号状态只有在人像面才有
+    if "idcard_number_type" in result and  result["idcard_number_type"] != 1:
+        if result["idcard_number_type"] == -1:
+            raise BaiDuApiImageError(u"身份证人像面信息识别错误,请检查")
+        if result["idcard_number_type"] == 0:
+            raise BaiDuApiImageError(u"身份证证号不合法")
+        if result["idcard_number_type"] == 2:
+            raise BaiDuApiImageError(u"身份证证号和性别、出生信息都不一致,请检查")
+        if result["idcard_number_type"] == 3:
+            raise BaiDuApiImageError(u"身份证证号和出生信息不一致,请检查")
+        if result["idcard_number_type"] == 4:
+            raise BaiDuApiImageError(u"身份证证号和性别信息不一致,请检查")
+
+    # 证件方向 选填
+    if "direction" in result and result["direction"] != 0:
+        raise BaiDuApiImageError(u"请调整上传图像方向,将上传图片摆正(可参考右侧图片示例)。")
+
+    # 证件风险 选填 防止复印件
+    if "risk_type" in result and result["risk_type"] != "normal":
+        if result["risk_type"] == "copy":
+            raise BaiDuApiImageError(u"上传身份证为复印件")
+        if result["risk_type"] == "temporary":
+            raise BaiDuApiImageError(u"上传身份证为临时件")
+        if result["risk_type"] == "screen":
+            raise BaiDuApiImageError(u"上传身份证为翻拍件")
+        if result["risk_type"] == "unknown":
+            raise BaiDuApiImageError(u"上传身份证异常")
+
+    # 证件质量检查
+    if "card_quality" in result:
+        if result["card_quality"]["IsClear"] != 1:
+            raise BaiDuApiImageError(u"身份证图像不清晰")
+        if result["card_quality"]["IsComplete"] != 1:
+            raise BaiDuApiImageError(u"身份证图像不完整")
+        if result["card_quality"]["IsNoCover"] != 1:
+            raise BaiDuApiImageError(u"身份证图像被遮挡")
+
+    # 最后处理下识别的字段结果 注意对各种数据进行下转换
+    data = dict()
+    if u"住址" in result["words_result"]:
+        data["province"], data["city"], data["area"], data["addr"] = split_addr(result["words_result"][u"住址"]["words"])
+    if u"出生" in result["words_result"]:
+        data["birthday"] = result["words_result"][u"出生"]["words"]
+    if u"姓名" in result["words_result"]:
+        data["name"] = result["words_result"][u"姓名"]["words"]
+    if u"公民身份号码" in result["words_result"]:
+        data["code"] = result["words_result"][u"公民身份号码"]["words"]
+    if u"性别" in result["words_result"]:
+        data["sex"] = result["words_result"][u"性别"]["words"]
+    if u"民族" in result["words_result"]:
+        data["nation"] = result["words_result"][u"民族"]["words"]
+
+    if u"签发日期" in result["words_result"]:
+        data["startTime"] = to_datetime(result["words_result"][u"签发日期"]["words"], "%Y%m%d").strftime("%Y-%m-%d")
+    if u"失效日期" in result["words_result"]:
+        try:
+            data["endTime"] = to_datetime(result["words_result"][u"失效日期"]["words"], "%Y%m%d").strftime("%Y-%m-%d")
+        except ValueError:
+            data["endTime"] = result["words_result"][u"失效日期"]["words"]
+
+    return data
+
+
+def parse_bank_image(result):
+    if not isinstance(result, dict):
+        raise BaiDuApiImageError(u"图像识别错误")
+
+    if "direction" in result and result["direction"] != 0:
+        raise BaiDuApiImageError(u"请调整上传图像方向,将上传图片摆正(可参考右侧图片示例)。")
+
+    if result["result"]["bank_card_type"] == 0:
+        raise BaiDuApiImageError(u"无法识别您的银行卡类型,请重新上传")
+
+    data = {
+        "bankCode": "".join(result["result"]["bank_card_number"].split()),
+    }
+
+    return data
+
+
+def parse_business_image(result):
+    if not isinstance(result, dict):
+        raise BaiDuApiImageError(u"图像识别错误")
+
+    if "direction" in result and result["direction"] != 0:
+        raise BaiDuApiImageError(u"请调整上传图像方向,将上传图片摆正(可参考右侧图片示例)。")
+    if "risk_type" in result and result["risk_type"] != "normal":
+        raise BaiDuApiImageError(u"营业执照异常")
+
+    wordsResult = result["words_result"]
+    data = dict()
+    if u"社会信用代码" in wordsResult:
+        data["busCode"] = wordsResult[u"社会信用代码"]["words"]
+    if u"单位名称" in wordsResult:
+        data["busName"] = wordsResult[u"单位名称"]["words"]
+    if u"成立日期" in wordsResult:
+        data["startTime"] = to_datetime(wordsResult[u"成立日期"]["words"], u"%Y年%m月%d日").strftime("%Y-%m-%d")
+    if u"有效期" in wordsResult:
+        try:
+            data["endTime"] = to_datetime(wordsResult[u"有效期"]["words"], u"%Y年%m月%d日").strftime("%Y-%m-%d")
+        except ValueError:
+            data["endTime"] = wordsResult[u"有效期"]["words"]
+    if u"地址" in wordsResult:
+        data["province"], data["city"], data["area"], data["addr"] = split_addr(wordsResult[u"地址"]["words"])
+
+    if u"法人" in wordsResult:
+        data["legalName"] = wordsResult[u"法人"]["words"]
+
+    return data
+
+
+class IDCardSide(object):
+    PERSONAL = "front"
+    EMBLEM = "back"
+
+
+class ImageRecognizer(object):
+    """
+    OCR 识别工具
+    helps: https://cloud.baidu.com/doc/OCR/s/7kibizyfm#%E8%BA%AB%E4%BB%BD%E8%AF%81%E8%AF%86%E5%88%AB
+    当前版本不支持url的形式上传
+    """
+
+    def __init__(self, visitor, appid=None, appKey=None, secretKey=None, imageData=None, inMemFile=None):
+
+        limiter = LimitAttemptsManager(visitor, self.__class__.__name__, maxTimes=30 if not settings.DEBUG else 9999)
+        if limiter.is_exceeded_limit():
+            raise BaiDuApiNetError(u"访问频率超限")
+        limiter.incr()
+
+        self._appid = appid or settings.BAIDU_IMAGE_RECOGNIZE_APP_ID
+        self._appKey = appKey or settings.BAIDU_IMAGE_RECOGNIZE_APP_KEY
+        self._appSecret = secretKey or settings.BAIDU_IMAGE_RECOGNIZE_APP_SECRET
+
+        if not all([self._appid, self._appKey, self._appSecret]):
+            raise BaiDuApiNetError(u"识别初始化失败")
+
+        if imageData and isinstance(imageData, bytes):
+            self._content = imageData
+        elif inMemFile and isinstance(inMemFile, UploadedFile):
+            self._content = b""
+            for _image in inMemFile.chunks():
+                self._content += _image
+        else:
+            self._content = None
+
+        if not self._content:
+            raise BaiDuApiImageError(u"无效的图像数据")
+
+        self._client = None
+
+    @property
+    def client(self):   # type:() -> AipOcr
+        if not self._client:
+            self._client = AipOcr(
+                appId=self._appid, apiKey=self._appKey, secretKey=self._appSecret
+            )
+
+        return self._client
+
+    @staticmethod
+    def _handle_result(result, callback):
+        """
+        对错误作出一定的解析
+        """
+        if not isinstance(result, dict):
+            raise BaiDuApiImageError(u"图像识别失败(10002)")
+        if "error_code" not in result:
+            return callback(result) if callback else result
+
+        error_code = result["error_code"]
+
+        # 此类错误是一般是对方服务器错误,需要通知系统管理员及时提交工单
+        if error_code in [
+            1,
+            100, 110, 111,
+            282000,
+            216100, 216101, 216102, 216103,
+            216110,
+            216630, 216634,
+            282003,
+            282005, 282006,
+            282110, 282111, 282112, 282113, 282114,
+            282808, 282809, 282810
+        ]:
+            raise BaiDuApiImageError(u"图像识别错误,请重新上传({}-{})。".format(error_code, result["error_msg"]))
+
+        if error_code == 216200:
+            raise BaiDuApiImageError(u"未检测到上传图像,请重新上传。")
+
+        if error_code == 216201:
+            raise BaiDuApiImageError(u"图片格式上传错误,请重新上传(支持图像格式为 PNG、JPG、JPEG、BMP)。")
+
+        if error_code == 216202:
+            raise BaiDuApiImageError(u"图片尺寸过大,请重新上传(图像不得超过4Mb, 分辨率不高于4096*4096)。")
+
+        if error_code == 216631:
+            raise BaiDuApiImageError(u"银行卡识别错误,出现此问题的一般原因为:您上传的图片非银行卡正面,上传了异形卡的图片或上传的银行卡正品图片不完整。")
+
+        if error_code == 216633:
+            raise BaiDuApiImageError(u"身份证识别错误,出现此问题的一般原因为:您上传了非身份证图片或您上传的身份证图片不完整")
+
+        # 流量或者QPS 超限的错误
+        if error_code in [17, 18, 19]:
+            raise BaiDuApiSysError(u"额度超出限制。")
+
+        # 其余的错误直接抛出
+        raise BaiDuApiImageError(u"图像识别错误,请重新上传({}-{})。".format(error_code, result["error_msg"]))
+
+    def recognize_identify_card(self, idCardSide, detectDirection=True, detectRisk=False, detectQuality=False, detectPhoto=False, detectCard=False, callback=None):
+        """
+        识别身份证 目前后三个付费功能没有开通
+        :param idCardSide: 自动检测身份证正反面,如果传参指定方向与图片相反,支持正常识别,返回参数image_status字段为"reversed_side"
+        :param detectDirection: 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度
+        :param detectRisk: 是否开启身份证风险类型(身份证复印件、临时身份证、身份证翻拍、修改过的身份证)检测功能
+        :param detectQuality: 是否开启身份证质量类型(边框/四角不完整、头像或关键字段被遮挡/马赛克)检测功能
+        :param detectPhoto: 是否检测头像内容,默认不检测
+        :param detectCard: 是否检测身份证进行裁剪,默认不检测
+        :param callback: 回调处理函数
+        """
+        options = {
+            "detect_direction": json.dumps(detectDirection),
+            "detect_risk": json.dumps(detectRisk),
+            "detect_quality": json.dumps(detectQuality),
+            "detect_photo": json.dumps(detectPhoto),
+            "detect_card": json.dumps(detectCard)
+        }
+
+        result = self.client.idcard(self._content, idCardSide, options)
+        return self._handle_result(result, callback)
+
+    def recognize_bank_card(self, detectDirection=True, callback=None):
+        """
+        识别银行卡
+        两个注意:1不能识别银行开户许可证 2识别出来的银行卡号不连续 需要后续存储的时候去掉空格
+        :param detectDirection: 检测方向
+        :param callback:
+        """
+        options = {
+            "detect_direction": json.dumps(detectDirection)
+        }
+        result = self.client.bankcard(self._content, options)
+        return self._handle_result(result, callback)
+
+    def recognize_business_license(self, detectDirection=True, riskWarn=False, callback=None):
+        options = {
+            "detect_direction": json.dumps(detectDirection),
+            "risk_warn": json.dumps(riskWarn),
+        }
+        result = self.client.businessLicense(self._content, options)
+        return self._handle_result(result, callback)
+

+ 2 - 0
apilib/inference/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python

+ 2 - 0
apilib/inference/types.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python

+ 2 - 0
apilib/loghelper/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python

+ 12 - 0
apilib/loghelper/filters.py

@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+import logging
+
+from .utils import get_current_request_id
+
+
+class RequestIdFilter(logging.Filter):
+    def filter(self, record):
+        record.request_id = get_current_request_id()
+        return True

+ 37 - 0
apilib/loghelper/middleware.py

@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from apps.web.constant import REQUEST_ID_HEADER
+from utils import local, generate_request_id
+
+
+def get_request_id(request):
+    if hasattr(request, 'request_id'):
+        return request.request_id
+    else:
+        return request.META.get(REQUEST_ID_HEADER, generate_request_id())
+
+class RequestIdMiddleware(object):
+    # Code for Django >= 1.10
+    def __init__(self, get_response = None):
+        self.get_response = get_response
+
+    def __call__(self, request):
+        request_id = get_request_id(request)
+        request.request_id = request_id
+        local.request_id = request_id
+
+        response = self.get_response(request)
+
+        del local.request_id
+        return response
+
+    # Compatibility methods for Django <1.10
+    def process_request(self, request):
+        request_id = get_request_id(request)
+        request.request_id = request_id
+        local.request_id = request_id
+
+    def process_response(self, request, response):
+        del local.request_id
+        return response

+ 15 - 0
apilib/loghelper/utils.py

@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+import threading
+from uuid import uuid4
+
+local = threading.local()
+
+
+def generate_request_id():
+    return str(uuid4())
+
+
+def get_current_request_id():
+    return getattr(local, 'request_id', generate_request_id())

+ 12 - 0
apilib/loghelper/wsgi.py

@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+class CustomWSGIWrapper(object):
+    def __init__(self, app):
+        from patch import patch_requests
+        patch_requests()
+
+        self.app = app
+
+    def __call__(self, environ, start_response):
+        return self.app(environ, start_response)

+ 310 - 0
apilib/monetary.py

@@ -0,0 +1,310 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+from decimal import Decimal, ROUND_DOWN
+from bson.decimal128 import Decimal128
+
+from apilib.numerics import force_decimal, quantize, UnitBase
+
+class MoneyComparisonError(TypeError):
+    # This exception was needed often enough to merit its own
+    # Exception class.
+
+    def __init__(self, other):
+        assert not isinstance(other, Money)
+        self.other = other
+
+    def __str__(self):
+        # Note: at least w/ Python 2.x, use __str__, not __unicode__.
+        return "Cannot compare instances of Money and %s" \
+               % self.other.__class__.__name__
+
+
+class Money(UnitBase):
+    """所有与金钱相来往的操作都走这个类,不暴露具体的运算,如取精度等"""
+
+    # 以后或许会有多币种支持
+    __currency__ = None
+
+    __places__ = '0.01'
+
+    def __init__(self, amount):
+
+        if isinstance(amount, Money):
+            self._amount = quantize(amount.amount, places=self.__places__)
+        else:
+            self._amount = quantize(Decimal(str(amount).strip()), places=self.__places__)
+
+    @property
+    def amount(self):
+        return self._amount
+
+    @property
+    def mongo_amount(self):
+        return Decimal128(self.amount)
+
+    @property
+    def precision(self):
+        """精度等级 小数的位数"""
+        return len(self.__places__.split(".")[1])
+
+    def __repr__(self): return '<{currency} {amount:.2f}> '.format(currency=self.__currency__, amount=self._amount)
+
+    def __add__(self, other):
+        if isinstance(other, Money):
+            return self.__class__(self._amount + other.amount)
+        else:
+            return self.__class__(other) + self
+
+    def __sub__(self, other):
+        if isinstance(other, Money):
+            return self.__class__(self._amount - other.amount)
+        else:
+            return self.__class__(other) - self
+
+    def __mul__(self, other):
+        if isinstance(other, Money):
+            raise TypeError('Cannot multiply two Money instances.')
+        elif isinstance(other, Ratio):
+            return self.__class__(amount=(self.amount * other.amount))
+        else:
+            return self.__class__(amount=(self.amount * force_decimal(other)))
+
+    def __abs__(self):
+        return self.__class__(amount=abs(self.amount))
+
+    def __neg__(self):
+        return self.__class__(amount=-self.amount)
+
+    def __eq__(self, other):
+        return (isinstance(other, Money) and
+                (self.amount == other.amount))
+
+    def __ne__(self, other):
+        result = self.__eq__(other)
+        return not result
+
+    def __lt__(self, other):
+        if not isinstance(other, Money):
+            raise MoneyComparisonError(other)
+        return self.amount < other.amount
+
+    def __gt__(self, other):
+        if not isinstance(other, Money):
+            raise MoneyComparisonError(other)
+        return self.amount > other.amount
+
+    def __le__(self, other):
+        if not isinstance(other, Money):
+            raise MoneyComparisonError(other)
+        return self.amount <= other.amount
+
+    def __ge__(self, other):
+        if not isinstance(other, Money):
+            raise MoneyComparisonError(other)
+        return self.amount >= other.amount
+
+    def __str__(self):
+        return str(self._amount)
+
+    def __int__(self):
+        return int(self._amount)
+
+    def __float__(self):
+        return float(self._amount)
+
+
+class RMB(Money):
+    """
+    > RMB(1) + RMB(2) == RMB(3)
+    """
+    __currency__ = 'RMB'
+
+    def __str__(self):
+        return u'{amount}'.format(amount=self._amount)
+
+    @classmethod
+    def fen_to_yuan(cls, fen):
+        return cls(fen) * Decimal('0.01')
+
+    @classmethod
+    def yuan_to_fen(cls, yuan):
+        return int((yuan * 100))
+
+class VirtualCoin(Money):
+    __currency__ = 'VirtualCoin'
+
+    def __str__(self):
+        return u'{amount}'.format(amount=self._amount)
+
+    @property
+    def as_impulses(self):
+        return int(self.amount)
+
+
+def sum_rmb(list_):
+    return sum(list_, RMB(0))
+
+
+def sum_virtual_coin(list_):
+    return sum(list_, VirtualCoin(0))
+
+
+def sum_accuracy_rmb(list_):
+    return sum(list_, AccuracyRMB(0))
+
+class Ratio(object):
+    def __init__(self, amount):
+        if isinstance(amount, Ratio):
+            self._amount = amount.amount
+        else:
+            amount = Decimal(str(amount).strip()) # type: Decimal
+            self._amount = amount
+
+    @property
+    def amount(self):
+        return self._amount
+
+    @property
+    def mongo_amount(self):
+        return Decimal128(self.amount)
+
+    def __eq__(self, other):
+        if isinstance(other, Ratio):
+            return self.amount == other.amount
+        else:
+            raise TypeError('Ratio can only be compared to another Ratio')
+
+    def __ne__(self, other):
+        result = self.__eq__(other)
+        return not result
+
+    def __lt__(self, other):
+        if not isinstance(other, Ratio):
+            raise TypeError('Ratio can only be compared against another ratio')
+        return self.amount < other.amount
+
+    def __gt__(self, other):
+        if not isinstance(other, Ratio):
+            raise TypeError('Ratio can only be compared against another ratio')
+        return self.amount > other.amount
+
+    def __le__(self, other):
+        if not isinstance(other, Ratio):
+            raise TypeError('Ratio can only be compared against another ratio')
+        return self.amount <= other.amount
+
+    def __ge__(self, other):
+        if not isinstance(other, Ratio):
+            raise TypeError('Ratio can only be compared against another ratio')
+        return self.amount >= other.amount
+
+    def __repr__(self):
+        return '<Ratio {amount:.4f}>'.format(amount=self._amount)
+
+    def quantize(self):
+        return self.__class__(self._amount.quantize(Decimal('0.0001'), rounding=ROUND_DOWN))
+
+    def __str__(self):
+        return u'{amount}'.format(amount=self.quantize()._amount)
+
+    def __add__(self, other):
+        if isinstance(other, Ratio):
+            return self.__class__(self._amount + other.amount)
+        else:
+            raise TypeError('Ratio can only be added to a Ratio object')
+
+    def __sub__(self, other):
+        if isinstance(other, Ratio):
+            return self.__class__(self._amount - other.amount)
+        else:
+            raise TypeError('Ratio can only be subtracted to a Ratio object')
+
+    def __mul__(self, other):
+        if isinstance(other, (Money,Ratio)):
+            return other.__class__(amount=(self.amount * other.amount))
+        else:
+            return self.__class__(amount=(self.amount * force_decimal(other)))
+
+    def __div__(self, other):
+        if isinstance(other, (Money, Percent, Ratio)):
+            raise TypeError('Ration objects don\'t support division with (Money, Percent, Ration)')
+        else:
+            return Ratio(self.amount / force_decimal(other))
+
+
+def sum_ratio(ratios):
+    return sum(ratios, Ratio(0))
+
+
+class Percent(Ratio):
+    @property
+    def as_ratio(self):
+        return Ratio(self.amount * Decimal('0.01'))
+
+    def __mul__(self, other):
+        if isinstance(other, Money):
+            return other.__class__(amount=(self.as_ratio.amount * other.amount))
+        elif isinstance(other, Percent):
+            return Percent(self.amount * other.as_ratio.amount)
+        else:
+            return self.__class__(amount=(self.amount * force_decimal(other)))
+
+    def __div__(self, other):
+        if isinstance(other, (Money, Percent, Ratio)):
+            raise TypeError('Percent objects don\'t support division with (Money, Percent, Ration)')
+        else:
+            return Percent(self.amount / force_decimal(other))
+
+    def __repr__(self):
+        return '<Percent {amount:.2f}>'.format(amount = self._amount)
+
+    def __str__(self):
+        return u'{amount}'.format(amount = quantize(self._amount, places='0.01'))
+
+
+class Permillage(Ratio):
+    @property
+    def as_ratio(self):
+        return Ratio(self.amount * Decimal('0.001'))
+
+    def __mul__(self, other):
+        if isinstance(other, Money):
+            return other.__class__(amount=(self.as_ratio.amount * other.amount))
+        elif isinstance(other, Permillage):
+            return Percent(self.amount * other.as_ratio.amount)
+        else:
+            return self.__class__(amount=(self.amount * force_decimal(other)))
+
+    def __div__(self, other):
+        if isinstance(other, Money):
+            raise TypeError('Percent objects don\'t support division with (Money, Percent, Permillage, Ratio)')
+        else:
+            return Percent(self.amount / force_decimal(other))
+
+    def __repr__(self):
+        return '<Permillage {amount:.2f}>'.format(amount = self._amount)
+
+    def __str__(self):
+        return u'{amount}'.format(amount = quantize(self._amount, places='0.01'))
+
+
+class JDMerchantPermillage(Permillage):
+
+    def to_jd_params(self):
+        """
+        打印出来的是百分之多少 比如设定的是JDMerchantPermillage(6) 表示是千分之6
+        则使用str之后 显示的是 0.60
+        :return:
+        """
+        return u'{amount:.2f}'.format(amount=quantize((self * Decimal('0.1'))._amount, places='0.01'))
+
+
+class AccuracyRMB(RMB):
+    """
+    精度等级更高的RMB 算到0.01分 适用于单笔结账
+    """
+    __places__ = "0.001"
+    __currency__ = "AccuracyRMB"
+
+    def __repr__(self): return '<{currency} {amount:.4f}> '.format(currency=self.__currency__, amount=self._amount)

+ 23 - 0
apilib/numerics.py

@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from decimal import Decimal, ROUND_HALF_UP
+
+
+def force_decimal(amount):
+    """Given an amount of unknown type, type cast it to be a Decimal."""
+    if not isinstance(amount, Decimal):
+        return Decimal(str(amount))
+    return amount
+
+
+def quantize(decimal_value, places = '0.01'):
+    # type:(Decimal, str)->Decimal
+    return decimal_value.quantize(Decimal(places), rounding = ROUND_HALF_UP)
+
+
+class UnitBase(object):
+
+    @classmethod
+    def initial(cls, default = '0'):
+        return cls(default)

+ 79 - 0
apilib/quantity.py

@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+from functools import total_ordering
+
+from bson.decimal128 import Decimal128
+
+from apilib.numerics import force_decimal, UnitBase, quantize
+
+
+@total_ordering
+class Quantity(UnitBase):
+    """
+    主要用来记录消费的数额
+    """
+
+    def __init__(self, amount, places='0.0001'):
+
+        if isinstance(amount, Quantity):
+            self._amount = amount.amount
+
+        self._amount = quantize(force_decimal(amount), places=places)
+
+    @property
+    def amount(self):
+        return self._amount
+
+    @property
+    def mongo_amount(self):
+        return Decimal128(self._amount)
+
+    def _format(self, string):
+        # type:(str)->str
+        return string.format(type=self.__class__.__name__)
+
+    def __str__(self):
+        return u'{amount}'.format(amount=self._amount)
+
+    def __repr__(self):
+        return '{type} {amount}'.format(type=self.__class__.__name__, amount=self._amount)
+
+    def __eq__(self, other):
+        if isinstance(other, Quantity):
+            return self._amount == other._amount
+        else:
+            raise TypeError(self._format('{type} can only be compared(__eq__) with another {type}'))
+
+    def __lt__(self, other):
+        if isinstance(other, Quantity):
+            return self._amount < other._amount
+        else:
+            raise TypeError(self._format('{type} can only be compared(__lt__) with another {type}'))
+
+    def __mul__(self, other):
+        if isinstance(other, Quantity):
+            raise TypeError(self._format('{type} cannot be multiplied by {type}'))
+        else:
+            return self.__class__(self.amount * other)
+
+    def __div__(self, other):
+        if isinstance(other, Quantity):
+            raise TypeError(self._format('{type} cannot be divided by {type}'))
+        else:
+            return self.__class__(self.amount / other)
+
+    def __add__(self, other):
+        if isinstance(other, Quantity):
+            return self.__class__(self._amount + other._amount)
+        else:
+            raise TypeError(self._format('{type} cannot be added to {type}'))
+
+    def __sub__(self, other):
+        if isinstance(other, Quantity):
+            return self.__class__(self._amount - other._amount)
+        else:
+            raise TypeError(self._format('{type} cannot be subtracted to {type}'))
+
+    def __float__(self):
+        return float(self.amount)

+ 93 - 0
apilib/systypes.py

@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+import threading
+from itertools import chain
+
+from enum import IntEnum as _IntEnum, Enum
+from typing import List
+
+
+def enum(**enums):
+    return type('Enum', (), enums)
+
+
+class IntEnum(_IntEnum):
+   @classmethod
+   def choices(cls):
+       return [ _.value for _ in  cls.__members__.values() ]
+
+
+class StrEnum(str, Enum):
+    @classmethod
+    def choices(cls):
+        return [ _.value for _ in  cls.__members__.values() ]
+
+
+class IterConstant(object):
+    """
+    用此类代替枚举的原因在于IDE等工具的可观察性, 一定程度牺牲不可变性
+    """
+    @classmethod
+    def choices(cls):
+        # type: ()->List[str]
+        return map(lambda _: getattr(cls, _),
+                   [_ for _ in dir(cls) if not _.startswith('__') and not callable(getattr(cls, _))])
+
+
+class Singleton(object):
+    objs = {}
+    objs_locker = threading.Lock()
+
+    def __new__(cls, *args, **kv):
+        if cls in cls.objs:
+            return cls.objs[cls]['obj']
+
+        cls.objs_locker.acquire()
+
+        try:
+            if cls in cls.objs:  ## double check locking
+                return cls.objs[cls]['obj']
+
+            obj = object.__new__(cls)
+            cls.objs[cls] = {'obj': obj, 'init': False}
+            setattr(cls, '__init__', cls.decorate_init(cls.__init__))
+
+            return cls.objs[cls]['obj']
+        finally:
+            cls.objs_locker.release()
+
+    @classmethod
+    def decorate_init(cls, fn):
+        def init_wrap(*args):
+            if not cls.objs[cls]['init']:
+                fn(*args)
+                cls.objs[cls]['init'] = True
+            return
+
+        return init_wrap
+
+
+class NoEmptyValueDict(dict):
+    @staticmethod
+    def _process_args(mapping = (), **kwargs):
+        if hasattr(mapping, 'items'):
+            mapping = getattr(mapping, 'items')()
+        return ((k, v) for k, v in chain(mapping, getattr(kwargs, 'items')()) if v)
+
+    def __setitem__(self, i, y):
+        if y:
+            super(NoEmptyValueDict, self).__setitem__(i, y)
+
+    def update(self, E = None, **F):
+        super(NoEmptyValueDict, self).update(self._process_args(E, **F))
+
+    def setdefault(self, k, d=None):
+        if k in self:
+            return super(NoEmptyValueDict, self).setdefault(k, d)
+
+        if not d:
+            return None
+        else:
+            return super(NoEmptyValueDict, self).setdefault(k, d)
+
+

+ 591 - 0
apilib/utils.py

@@ -0,0 +1,591 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+import itertools
+import math
+import re
+import time
+import codecs
+import pickle
+from functools import partial
+
+import addressparser
+import chardet
+import platform
+
+import collections
+from collections import OrderedDict
+from itertools import islice, takewhile
+
+from typing import NamedTuple, Tuple, Optional
+
+from Crypto.PublicKey import RSA
+
+is_windows = any(platform.win32_ver())
+
+
+def xor_encrypt_buffer(buffer, offset, key):
+    import struct
+
+    lkey = len(key)
+
+    step = 0
+
+    num = offset
+    for _ in buffer[offset:]:
+        struct.pack_into('<B', buffer, num, ord(_) ^ ord(key[step % lkey]))
+        num += 1
+        step += 1
+    return buffer
+
+
+def xor_decrypt_buffer(buffer, offset, key):
+    return xor_encrypt_buffer(buffer, offset, key)
+
+
+clock = time.clock if is_windows else time.time
+
+
+class Timer(object):
+
+    def __init__(self, func = clock):
+        self.elapsed = 0.0
+        self._func = func
+        self._start = None
+
+    def start(self):
+        if self._start is not None:
+            raise RuntimeError('Already started')
+        self._start = self._func()
+
+    def stop(self):
+        if self._start is None:
+            raise RuntimeError('Not started')
+        end = self._func()
+        self.elapsed += end - self._start
+        self._start = None
+
+    def reset(self):
+        self.elapsed = 0.0
+
+    @property
+    def running(self):
+        return self._start is not None
+
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, *args):
+        self.stop()
+
+
+def write_file(name, content):
+    # type: (str, bytes)->None
+    with open(name, 'w') as f:
+        f.write(content)
+
+
+KeyPair = NamedTuple('KeyPair', [('public', str), ('private', str)])
+
+
+def generate_RSA_key_pairs(bits = 2048):
+    # type: (int)->KeyPair
+    """
+
+    :param bits:
+    :return:
+    """
+
+    key = RSA.generate(bits)
+    return KeyPair(public = key.publickey().exportKey('PEM').decode('ascii'),
+                   private = key.exportKey('PEM').decode('ascii'))
+
+
+def write_RSA_key_pairs(path, pair = None):
+    # type: (str, bool, KeyPair)->Tuple[str, str]
+
+    if pair is None:
+        pair = generate_RSA_key_pairs()  # type: KeyPair
+
+    def key_name(name, kind): return '{name}_{kind}.pem'.format(name = name, kind = kind)
+
+    public_key_file_name = key_name(path, 'public')
+    private_key_file_name = key_name(path, 'private')
+
+    write_file(public_key_file_name, pair.public)
+    write_file(private_key_file_name, pair.private)
+
+    return public_key_file_name, private_key_file_name
+
+
+def head(iterable, default = None):
+    return next(iter(iterable), default)
+
+
+def first_true(iterable, pred = None, default = None):
+    """Returns the first true value in the iterable.
+    If no true value is found, returns *default*
+    If *pred* is not None, returns the first item
+    for which pred(item) is true."""
+    # first_true([a,b,c], default=x) --> a or b or c or x
+    # first_true([a,b], fn, x) --> a if fn(a) else b if fn(b) else x
+    return next(filter(pred, iterable), default)
+
+
+def nth(iterable, n, default = None):
+    """Returns the nth item of iterable, or a default value"""
+    return next(islice(iterable, n, None), default)
+
+
+def upto(iterable, max_val):
+    """From a monotonically increasing iterable, generate all the values <= max_val."""
+    # Why <= max_val rather than < max_val? In part because that's how Ruby's upto does it.
+    return takewhile(lambda x: x <= max_val, iterable)
+
+
+def ilen(iterable):
+    """Length of any iterable (consumes generators)."""
+    return sum(1 for _ in iterable)
+
+
+count_iterable = ilen
+
+
+def recursive_repr(fill_value = '...'):
+    """
+    back-ported from Python3
+    Decorator to make a repr function return fill_value for a recursive call"""
+
+    from thread import get_ident
+
+    def decorating_function(user_function):
+        repr_running = set()
+
+        def wrapper(self):
+            key = id(self), get_ident()
+            if key in repr_running:
+                return fill_value
+            repr_running.add(key)
+            try:
+                result = user_function(self)
+            finally:
+                repr_running.discard(key)
+            return result
+
+        # Can't use functools.wraps() here because of bootstrap issues
+        wrapper.__module__ = getattr(user_function, '__module__')
+        wrapper.__doc__ = getattr(user_function, '__doc__')
+        wrapper.__name__ = getattr(user_function, '__name__')
+        wrapper.__annotations__ = getattr(user_function, '__annotations__', {})
+        return wrapper
+
+    return decorating_function
+
+
+class ChainMap(collections.MutableMapping):
+    """
+    back-ported from Python3
+    A ChainMap groups multiple dicts (or other mappings) together
+    to create a single, updateable view.
+    The underlying mappings are stored in a list.  That list is public and can
+    be accessed or updated using the *maps* attribute.  There is no other
+    state.
+    Lookups search the underlying mappings successively until a key is found.
+    In contrast, writes, updates, and deletions only operate on the first
+    mapping.
+    """
+
+    def __init__(self, *maps):
+        """Initialize a ChainMap by setting *maps* to the given mappings.
+        If no mappings are provided, a single empty dictionary is used.
+        """
+        self.maps = list(maps) or [{}]  # always at least one map
+
+    def __missing__(self, key):
+        raise KeyError(key)
+
+    def __getitem__(self, key):
+        for mapping in self.maps:
+            try:
+                return mapping[key]  # can't use 'key in mapping' with defaultdict
+            except KeyError:
+                pass
+        return self.__missing__(key)  # support subclasses that define __missing__
+
+    def get(self, key, default = None):
+        return self[key] if key in self else default
+
+    def __len__(self):
+        return len(set().union(*self.maps))  # reuses stored hash values if possible
+
+    def __iter__(self):
+        d = {}
+        for mapping in reversed(self.maps):
+            d.update(mapping)  # reuses stored hash values if possible
+        return iter(d)
+
+    def __contains__(self, key):
+        return any(key in m for m in self.maps)
+
+    def __bool__(self):
+        return any(self.maps)
+
+    @recursive_repr()
+    def __repr__(self):
+        return '{name}({maps})'.format(name = self.__class__.__name__, maps = ", ".join(map(repr, self.maps)))
+
+    @classmethod
+    def fromkeys(cls, iterable, *args):
+        """Create a ChainMap with a single dict created from the iterable."""
+        return cls(dict.fromkeys(iterable, *args))
+
+    def copy(self):
+        """New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]"""
+        return self.__class__(self.maps[0].copy(), *self.maps[1:])
+
+    __copy__ = copy
+
+    def new_child(self, m = None):  # like Django's Context.push()
+        """New ChainMap with a new map followed by all previous maps.
+        If no map is provided, an empty dict is used.
+        """
+        if m is None:
+            m = {}
+        return self.__class__(m, *self.maps)
+
+    @property
+    def parents(self):  # like Django's Context.pop()
+        """New ChainMap from maps[1:]."""
+        return self.__class__(*self.maps[1:])
+
+    def __setitem__(self, key, value):
+        self.maps[0][key] = value
+
+    def __delitem__(self, key):
+        try:
+            del self.maps[0][key]
+        except KeyError:
+            raise KeyError('Key not found in the first mapping: {!r}'.format(key))
+
+    def popitem(self):
+        """Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty."""
+        try:
+            return self.maps[0].popitem()
+        except KeyError:
+            raise KeyError('No keys found in the first mapping.')
+
+    def pop(self, key, *args):
+        """Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0]."""
+        try:
+            return self.maps[0].pop(key, *args)
+        except KeyError:
+            raise KeyError('Key not found in the first mapping: {!r}'.format(key))
+
+    def clear(self):
+        """Clear maps[0], leaving maps[1:] intact."""
+        self.maps[0].clear()
+
+
+class Immutable(object):
+    """
+    Immutable object which adheres to the Mapping and the Sequence protocols.
+    * Attributes are kept in `self._ordered_dict`. NEVER MUTATE THIS!
+    * Instantiate with kwargs or args. Instantiating with args preserves order.
+    * Mapping methods get(), items(), keys(), and values() are also included.
+    * Sequence methods index() and count() are also included.
+    """
+
+    class ImmutableError(Exception):
+        pass
+
+    def __init__(self, *args, **kwargs):
+        """
+        Instantiate an Immutable instance.
+        >>> # tuple instantiation --> Note that this method preserves order!
+        >>> obj = Immutable((('val_0', 0), ('val_1', 1)))
+        >>> # key=value pairs
+        >>> obj1 = Immutable(val_0=0, val_1=1)
+        >>> # same as above, but by upacking a dict
+        >>> attribute_dict = {'val_0': 0, 'val_1': 1}
+        >>> obj2 = Immutable(**attribute_dict)
+        >>> # access via '.' or '[]'
+        >>> obj.val_0
+        >>> obj['val_0']
+        :param args: (<attr>, <val>,) pairs to add *in order*
+        :param kwargs: Allows you to unpack a dict to create this.
+        """
+        reserved_keys = ('get', 'keys', 'values', 'items', 'count', 'index',
+                         '_ordered_dict', '_tuple')
+        ordered_dict = OrderedDict()
+        for key, val in args:
+            if key in kwargs:
+                raise self.ImmutableError('Key in args duplicated in kwargs.')
+            ordered_dict[key] = val
+        ordered_dict.update(kwargs)
+        for key, val in ordered_dict.items():
+            try:
+                hash(key)
+                hash(val)
+            except TypeError:
+                raise self.ImmutableError('Keys and vals must be hashable.')
+            if isinstance(key, int):
+                raise self.ImmutableError('Keys cannot be integers.')
+            if key in reserved_keys:
+                raise self.ImmutableError('Keys cannot be any of these: {}.'
+                                          .format(reserved_keys))
+        self.__dict__['_ordered_dict'] = ordered_dict
+        self.__dict__['_tuple'] = tuple(ordered_dict.values())
+
+    def __contains__(self, item):
+        raise self.ImmutableError('Containment not implemented. Try with '
+                                  'keys(), values(), or items().')
+
+    def __reversed__(self):
+        raise self.ImmutableError('Reversal not implemented. Try with '
+                                  'keys(), values(), or items().')
+
+    def __getitem__(self, key):
+        if isinstance(key, int):
+            return self.__dict__['_tuple'][key]
+        else:
+            return self.__dict__['_ordered_dict'][key]
+
+    def __setitem__(self, key, value):
+        raise self.ImmutableError('Cannot set items on Immutable.')
+
+    def __getattr__(self, key):
+        if key == 'items':
+            return self.__dict__['_ordered_dict'].items
+        elif key == 'keys':
+            return self.__dict__['_ordered_dict'].keys
+        elif key == 'values':
+            return self.__dict__['_ordered_dict'].values
+        elif key == 'index':
+            return self.__dict__['_tuple'].index
+        elif key == 'count':
+            return self.__dict__['_tuple'].count
+        else:
+            return self.__getitem__(key)
+
+    def __setattr__(self, key, value):
+        raise self.ImmutableError('Cannot set attributes on Immutable.')
+
+    def __cmp__(self, other):
+        raise self.ImmutableError('Only equality comparisons implemented.')
+
+    def __eq__(self, other):
+        if not isinstance(other, Immutable):
+            return False
+        return hash(self) == hash(other)
+
+    def __ne__(self, other):
+        if not isinstance(other, Immutable):
+            return True
+        return hash(self) != hash(other)
+
+    def __len__(self):
+        return len(self.__dict__['_tuple'])
+
+    def __iter__(self):
+        raise self.ImmutableError('Iteration not implemented. Try with '
+                                  'keys(), values(), or items().')
+
+    def __hash__(self):
+        return hash(tuple(self._ordered_dict.items()))
+
+    def __str__(self):
+        return bytes('{}'.format(self.__repr__()))
+
+    def __unicode__(self):
+        return '{}'.format(self.__repr__())
+
+    def __repr__(self):
+        keys_repr = ', '.join('{}={}'.format(key, repr(val))
+                              for key, val in self.items())
+        return 'Immutable({})'.format(keys_repr)
+
+    def __dir__(self):
+        return list(self.keys())
+
+
+def guess_encoding(file_path):
+    # type: (str, int)->str
+    """Predict a file's encoding using chardet"""
+
+    # Open the file as binary data
+    with open(file_path, 'rb') as f:
+        # Join binary lines for specified number of lines
+        raw_data = b''.join(f.readlines())
+
+    return chardet.detect(raw_data)['encoding']
+
+
+def convert_encoding(source_file, target_file = None, source_encoding = None, target_encoding = "utf-8"):
+    # type: (str, Optional[str], Optional[str], Optional[str])->str
+    """
+
+    :param source_file:
+    :param target_file:
+    :param source_encoding:
+    :param target_encoding:
+    :return:
+    """
+
+    source_encoding = source_encoding if source_encoding is not None else guess_encoding(source_file)
+
+    if source_encoding in ('gb2312', 'GB2312'):
+        #: gb18030 is a superset of gb2312, it can cover more corner cases
+        source_encoding = 'gb18030'
+
+    if not target_file:
+        filename, suffix = source_file.rsplit('.')
+        target_file = 'converted-{filename}-{source_encoding}-{target_encoding}.{suffix}' \
+            .format(filename = filename,
+                    source_encoding = source_encoding,
+                    target_encoding = target_encoding,
+                    suffix = suffix)
+
+    BLOCK_SIZE = 1048576
+    with codecs.open(source_file, "r", source_encoding) as source:
+        with codecs.open(target_file, "w", target_encoding) as target:
+            while True:
+                contents = source.read(BLOCK_SIZE)
+                if not contents:
+                    break
+                target.write(contents)
+
+    return target_file
+
+
+def rec_update_dict(d, update_dict, firstLevelOverwrite = False):
+    """
+    递归地更新字典
+    overwrite特指是否覆盖第一层dict
+    :param d:
+    :param update_dict:
+    :return:
+    """
+    for k, v in update_dict.iteritems():
+        if firstLevelOverwrite:
+            d[k] = v
+        else:
+            if isinstance(v, collections.Mapping):
+                d[k] = rec_update_dict(d.get(k, {}), v)
+            else:
+                d[k] = v
+    return d
+
+
+flatten = itertools.chain.from_iterable
+
+
+def convert(text):
+    return int(text) if text.isdigit() else text
+
+
+def alphanum_key(key):
+    return [convert(c) for c in re.split('([0-9]+)', key)]
+
+
+def natural_sort(array, key, reverse):
+    return sorted(array, key = lambda d: alphanum_key(d[key]), reverse = reverse)
+
+
+def paginated(dataList, pageIndex, pageSize):
+    return dataList[(pageIndex - 1) * pageSize:pageIndex * pageSize]
+
+
+def is_number(s):
+    try:
+        float(s)
+        return True
+    except ValueError:
+        pass
+
+    try:
+        import unicodedata
+        unicodedata.numeric(s)
+        return True
+    except (TypeError, ValueError, ImportError):
+        pass
+
+    return False
+
+
+def ceil_floor(x): return math.ceil(x) if x < 0 else math.floor(x)
+
+
+def round_n_digits(x, n): return ceil_floor(x * math.pow(10, n)) / math.pow(10, n)
+
+
+round_2_digits = partial(round_n_digits, n = 2)
+
+
+def with_metaclass(meta, *bases):
+    """
+    Function from jinja2/_compat.py. License: BSD.
+    Use it like this::
+        class BaseForm(object):
+            pass
+        class FormType(type):
+            pass
+        class Form(with_metaclass(FormType, BaseForm)):
+            pass
+    This requires a bit of explanation: the basic idea is to make a
+    dummy metaclass for one level of class instantiation that replaces
+    itself with the actual metaclass.  Because of internal type checks
+    we also need to make sure that we downgrade the custom metaclass
+    for one level to something closer to type (that's why __call__ and
+    __init__ comes back from type etc.).
+    This has the advantage over six.with_metaclass of not introducing
+    dummy classes into the final MRO.
+    """
+
+    class Metaclass(meta):
+        __call__ = type.__call__
+        __init__ = type.__init__
+
+        def __new__(cls, name, this_bases, d):
+            if this_bases is None:
+                return type.__new__(cls, name, (), d)
+            return meta(name, bases, d)
+
+    return Metaclass('temporary_class', None, {})
+
+
+def load_pickle(filepath):
+    with open(filepath) as f:
+        return pickle.load(f)
+
+
+def dump_pickle(obj, filepath):
+    with open(filepath, 'w') as f:
+        pickle.dump(obj, f)
+
+
+def split_addr(addr):  # type:(unicode) -> tuple
+    addr = [addr]
+    df = addressparser.transform(addr)
+    df.fillna("", inplace=True)
+    return df.values.tolist()[0]
+
+
+def fix_dict(mydict, check_empty_string = []):
+    # type: (dict, list)->dict
+
+    result = {}
+
+    if not mydict:
+        return result
+
+    for key, value in mydict.iteritems():
+        if key in check_empty_string:
+            if value is None or value == '':
+                continue
+            else:
+                result[key] = value
+        else:
+            if value is not None:
+                result[key] = value
+
+    return result

+ 38 - 0
apilib/utils_AES.py

@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+"""
+AES加解密
+"""
+import base64
+from Crypto.Cipher import AES
+
+class EncryptDate:
+    def __init__(self, key):
+        self.key = key  # 初始化密钥
+        self.length = AES.block_size  # 初始化数据块大小
+        self.aes = AES.new(self.key, AES.MODE_ECB)  # 初始化AES,ECB模式的实例
+        # 截断函数,去除填充的字符
+        self.unpad = lambda date: date[0:-ord(date[-1])]      
+
+    def pad(self, text):
+        """
+        #填充函数,使被加密数据的字节码长度是block_size的整数倍
+        """
+        count = len(text.encode('utf-8'))
+        add = self.length - (count % self.length)
+        entext = text + (chr(add) * add)
+        return entext
+
+    def encrypt(self, encrData):  # 加密函数
+        res = self.aes.encrypt(self.pad(encrData).encode("utf8"))
+        msg = str(base64.b64encode(res))
+        return msg
+
+    def decrypt(self, decrData):  # 解密函数
+        res = base64.decodestring(decrData.encode("utf8"))
+        try:
+            msg = self.aes.decrypt(res).decode("utf8")
+            return self.unpad(msg)
+        except Exception,e:
+            return None

+ 156 - 0
apilib/utils_datetime.py

@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import time
+import datetime
+
+from typing import Union, Tuple
+
+from dateutil.relativedelta import relativedelta
+from apilib.utils_string import basestring_type
+
+from typing import Optional
+
+"""
+由于Python的_strptime会在多线程时出现bug,需要首先引入 import _strptime
+[0] https://stackoverflow.com/questions/2427240/thread-safe-equivalent-to-pythons-time-strptime
+"""
+
+
+def get_zero_time(the_datetime):
+    zero_time = None
+    if isinstance(the_datetime, datetime.date):
+        str_time = the_datetime.strftime("%Y-%m-%d") + " 00:00:00"
+        zero_time = datetime.datetime.strptime(str_time, "%Y-%m-%d %H:%M:%S")
+    return zero_time
+
+
+def get_tomorrow_zero_time(the_datetime):
+    # type: (datetime)->datetime
+    zero_time = None
+    str_time = the_datetime.strftime("%Y-%m-%d") + " 00:00:00"
+    zero_time = datetime.datetime.strptime(str_time, "%Y-%m-%d %H:%M:%S")
+    return zero_time + datetime.timedelta(days = 1)
+
+
+# 根据当前时间获取时间戳,返回整数
+def generate_timestamp():
+    return long(time.time())
+
+
+def generate_timestamp_ex():
+    return long(time.time() * 1000)
+
+
+def date_to_datetime(date, start_or_end):
+    # type: (Union[basestring_type, datetime.date], str)->datetime.datetime
+    if isinstance(date, basestring_type):
+        date = datetime.datetime.strptime(date, '%Y-%m-%d')
+    else:
+        date = date
+    if start_or_end == 'start':
+        time_ = datetime.time.min
+    elif start_or_end == 'end':
+        time_ = datetime.time.max
+    else:
+        raise Exception('only `start` or `end` are allowed for argument `start_or_end` given %s' % (start_or_end,))
+    return datetime.datetime.combine(date, time_)
+
+
+def defaultTodayDate(x): return datetime.datetime.now().strftime('%Y-%m-%d') if x is None else x
+
+
+def date_to_datetime_floor(date = None):
+    if date is None:
+        return datetime.datetime.now()
+    else:
+        return date_to_datetime(date = date, start_or_end = 'start')
+
+
+def date_to_datetime_ceiling(date = None):
+    if date is None:
+        return datetime.datetime.now()
+    else:
+        return date_to_datetime(date = date, start_or_end = 'end')
+
+
+def datetime_range(dt):
+    # type:(datetime.datetime)->Tuple[datetime.datetime, datetime.datetime]
+    assert isinstance(dt, datetime.datetime)
+    combine = datetime.datetime.combine
+    date = dt.date()
+    return combine(date, datetime.time.min), combine(date, datetime.time.max)
+
+
+def today_datetime_range(): return datetime_range(datetime.datetime.now())
+
+
+def dt_to_timestamp(dt): return time.mktime(dt.timetuple())
+
+
+def local2utc(local_time):
+    time_struct = time.mktime(local_time.timetuple())
+    utc_st = datetime.datetime.utcfromtimestamp(time_struct)
+    return utc_st
+
+
+timestamp_to_dt = datetime.datetime.fromtimestamp
+
+datetime_to_timestamp = dt_to_timestamp
+
+
+def today_format_str(): return datetime.datetime.now().strftime("%Y-%m-%d")
+
+
+def yesterday_format_str(): return (datetime.datetime.now() - datetime.timedelta(days = 1)).strftime('%Y-%m-%d')
+
+
+def the_day_before_yesterday_format_str(): return (datetime.datetime.now() - datetime.timedelta(days = 2)).strftime(
+    '%Y-%m-%d')
+
+
+def this_month_format_str(): return datetime.datetime.now().strftime('%Y-%m')
+
+
+def last_month_format_str(): return (datetime.datetime.today() - relativedelta(months = 1)).strftime('%Y-%m')
+
+
+def timestamp_timeformat(timeStamp): return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timeStamp))
+
+
+def first_day_datetime_of_month(date = None, time_format = '%Y-%m-%d %H:%M:%S'):
+    # type: (Optional[datetime], Optional[str])->datetime
+    if not date:
+        date = datetime.datetime.now()
+    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"):
+    if isinstance(ts_or_str, basestring):
+        return datetime.datetime.strptime(ts_or_str, time_format)
+    elif isinstance(ts_or_str, int) or isinstance(ts_or_str, long) or isinstance(ts_or_str, float):
+        return datetime.datetime.fromtimestamp(ts_or_str)
+    elif isinstance(ts_or_str, datetime.datetime):
+        return ts_or_str
+    else:
+        assert False, u'参数错误'
+
+
+def to_timestamp(ts_or_str, time_format="%Y-%m-%d %H:%M:%S"):
+    if isinstance(ts_or_str, basestring):
+        return dt_to_timestamp(to_datetime(ts_or_str, time_format))
+    elif isinstance(ts_or_str, int) or isinstance(ts_or_str, long) or isinstance(ts_or_str, float):
+        return ts_or_str
+    elif isinstance(ts_or_str, datetime.datetime):
+        return dt_to_timestamp(ts_or_str)
+    else:
+        assert False, '参数错误'
+
+
+def datetime_between_months(months = 6):
+    begin = datetime.datetime.now() - relativedelta(months = months)
+    begin = datetime.datetime(begin.year, begin.month, 1)
+
+    end = datetime.datetime.now() + datetime.timedelta(hours = 1)
+
+    return begin, end

+ 122 - 0
apilib/utils_json.py

@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+import json
+
+from bson import ObjectId, Decimal128
+from django.core.serializers.json import DjangoJSONEncoder
+from django.http import HttpResponse
+
+from apilib.monetary import RMB, VirtualCoin, Ratio, Percent
+from apilib.quantity import Quantity
+
+
+from apilib.systypes import StrEnum
+
+
+class DescriptiveJSONEncoder(DjangoJSONEncoder):
+    def default(self, o):
+        import datetime
+
+        if isinstance(o, ObjectId):
+            return str(o)
+        elif isinstance(o, Decimal128):
+            return str(o)
+        elif isinstance(o, (RMB, VirtualCoin, Ratio, Percent)):
+            return str(o)
+        elif isinstance(o, Quantity):
+            return str(o)
+        elif isinstance(o, datetime.datetime):
+            return o.strftime('%Y-%m-%d %H:%M:%S')
+        return super(DescriptiveJSONEncoder, self).default(o)
+
+
+class CustomizedType(StrEnum):
+    ObjectId = 'ObjectId'
+    Decimal128 = 'Decimal128'
+    RMB = 'RMB'
+    VirtualCoin = 'VirtualCoin'
+    Ratio = 'Ratio'
+    Percent = 'Percent'
+    Quantity = 'Quantity'
+    datetime = 'datetime'
+
+
+class CustomizedTypeJSONEncoder(DjangoJSONEncoder):
+    def default(self, o):
+        import datetime
+
+        if isinstance(o, ObjectId):
+            return {'_spec_type': CustomizedType.ObjectId, 'val': str(o)}
+        elif isinstance(o, Decimal128):
+            return {'_spec_type': CustomizedType.Decimal128, 'val': str(o)}
+        elif isinstance(o, RMB):
+            return {'_spec_type': CustomizedType.RMB, 'val': str(o)}
+        elif isinstance(o, VirtualCoin):
+            return {'_spec_type': CustomizedType.VirtualCoin, 'val': str(o)}
+        elif isinstance(o, Ratio):
+            return {'_spec_type': CustomizedType.Ratio, 'val': str(o)}
+        elif isinstance(o, Percent):
+            return {'_spec_type': CustomizedType.Percent, 'val': str(o)}
+        elif isinstance(o, Quantity):
+            return {'_spec_type': CustomizedType.Quantity, 'val': str(o)}
+        elif isinstance(o, datetime.datetime):
+            return {'_spec_type': CustomizedType.datetime, 'val': o.strftime('%Y-%m-%d %H:%M:%S')}
+
+        return super(CustomizedTypeJSONEncoder, self).default(o)
+
+
+def object_hook(obj):
+    _spec_type = obj.get('_spec_type')
+    if not _spec_type:
+        return obj
+
+    if _spec_type == CustomizedType.ObjectId:
+        return ObjectId(obj.get('val'))
+
+    if _spec_type == CustomizedType.RMB:
+        return RMB(obj.get('val'))
+
+    elif _spec_type == CustomizedType.VirtualCoin:
+        return VirtualCoin(obj.get('val'))
+
+    elif _spec_type == CustomizedType.Ratio:
+        return Ratio(obj.get('val'))
+
+    elif _spec_type == CustomizedType.Percent:
+        return Percent(obj.get('val'))
+
+    elif _spec_type == CustomizedType.Quantity:
+        return Quantity(obj.get('val'))
+
+    elif _spec_type == CustomizedType.datetime:
+        import datetime
+        return datetime.datetime.strptime(obj.get('val'), '%Y-%m-%d %H:%M:%S')
+
+    elif _spec_type == CustomizedType.Decimal128:
+        return Decimal128(str(obj.get('val')))
+
+    raise Exception('Unknown {}'.format(_spec_type))
+
+
+def json_dumps(data, sort_keys = False, serialize_type = False):
+    if not serialize_type:
+        return json.dumps(data, sort_keys = sort_keys, cls = DescriptiveJSONEncoder)
+    else:
+        return json.dumps(data, sort_keys = sort_keys, cls = CustomizedTypeJSONEncoder)
+
+
+def json_loads(data):
+    return json.loads(data, object_hook = object_hook)
+
+
+class JsonResponse(HttpResponse):
+    def __init__(self, data, safe = True, ordered = False, **kwargs):
+        if safe and not isinstance(data, dict):
+            raise TypeError('In order to allow non-dict objects to be '
+                            'serialized set the safe parameter to False')
+        kwargs.setdefault('content_type', 'application/json')
+        data = json_dumps(data, sort_keys = ordered)
+        super(JsonResponse, self).__init__(content = data, **kwargs)
+
+    def json(self):
+        return json.loads(self.content)

+ 89 - 0
apilib/utils_mongo.py

@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from pymongo import InsertOne, UpdateOne, DeleteOne
+from pymongo.errors import BulkWriteError
+from typing import Dict
+
+
+class BulkHandler(object):
+    def __init__(self, collection):
+        self.collection = collection
+        self.bulk = collection.initialize_unordered_bulk_op()
+
+    def insert(self, insert_dict):
+        self.bulk.insert(insert_dict)
+
+    def update(self, query_dict, update_dict):
+        self.bulk.find(query_dict).update(update_dict)
+
+    def upsert(self, query_dict, update_dict):
+        self.bulk.find(query_dict).upsert().update(update_dict)
+
+    def delete(self, query_dict):
+        self.bulk.find(query_dict).remove()
+
+    def execute(self):
+        result = {'success': 0, 'info': 0}
+        try:
+            if len(self.bulk._BulkOperationBuilder__bulk.ops) != 0:
+                result['info'] = self.bulk.execute()
+                result['success'] = 1
+            else:
+                result['info'] = 'no operation to execute'
+                result['success'] = 1
+        except Exception as e:
+            result['info'] = e
+            result['success'] = 0
+
+        return result
+
+
+class BulkHandlerEx(object):
+    def __init__(self, collection):
+        self.collection = collection
+        self.requests = []
+
+    def insert(self, insert_dict):
+        self.requests.append(InsertOne(insert_dict))
+
+    def update(self, query_dict, update_dict):
+        self.requests.append(UpdateOne(query_dict, update_dict, upsert = False))
+
+    def upsert(self, query_dict, update_dict):
+        self.requests.append(UpdateOne(query_dict, update_dict, upsert = True))
+
+    def delete(self, query_dict):
+        self.requests.append(DeleteOne(query_dict))
+
+    def execute(self, ordered = False):
+        result = {'success': 0, 'info': 0}
+        try:
+            if len(self.requests) != 0:
+                reuslt = self.collection.bulk_write(self.requests, ordered = ordered)
+                result['info'] = reuslt.bulk_api_result
+                result['success'] = 1
+            else:
+                result['info'] = {'writeErrors': list(), 'desc': 'no operation to execute'}
+                result['success'] = 2
+        except BulkWriteError as e:
+            result['info'] = e.details
+            result['success'] = 1
+        except Exception as e:
+            result['info'] = e
+            result['success'] = 0
+
+        return result
+
+
+def format_dot_key(rule_dict, to_dot = False):
+    # type: (Dict, bool)->Dict
+    rv = {}
+
+    for k, v in rule_dict.items():
+        if to_dot:
+            rv[k.replace('-', '.')] = v
+        else:
+            rv[k.replace('.', '-')] = v
+
+    return rv

+ 4 - 0
apilib/utils_mqtt.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+get_share_prefix = lambda group, prefix: '%s%s' % (group, prefix)

+ 126 - 0
apilib/utils_string.py

@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+"""
+字符处理的工具类
+"""
+
+import hashlib
+import random
+import string
+
+# 错误信息重定向
+from typing import Iterable
+
+from apilib.utils_sys import PY3
+
+def cn(_): return unicode(_).encode('utf-8')
+
+
+def get_random_string(length = 8):
+    src_digits = string.digits
+    src_uppercase = string.ascii_uppercase
+    src_lowercase = string.ascii_lowercase
+
+    digits_num = random.randint(1, 6)
+    uppercase_num = random.randint(1, length - digits_num - 1)
+    lowercase_num = length - (digits_num + uppercase_num)
+
+    random_string = random.sample(src_digits, digits_num) + random.sample(src_uppercase, uppercase_num) + random.sample(
+        src_lowercase, lowercase_num)
+    random.shuffle(random_string)
+    return ''.join(random_string)
+
+
+def get_random_str(num, seq=string.ascii_letters):
+    ret_list = []
+    for i in range(1, num + 1):
+        ret_list.append("".join(random.sample(seq, 1)))
+
+    return "".join(ret_list)
+
+
+def generate_random_seq(size = 32):
+    char = string.ascii_letters + string.digits
+    return "".join(random.choice(char) for _ in range(size))
+
+
+if PY3:
+    unicode_type = str
+    basestring_type = (str, bytes)
+else:
+    unicode_type = unicode
+    basestring_type = basestring
+
+
+def encode(s):
+    return s.encode('utf-8') if isinstance(s, unicode_type) else s
+
+
+def decode(s):
+    return s.decode('utf-8') if isinstance(s, bytes) else s
+
+
+def md5(_): return hashlib.md5(_).hexdigest()
+
+
+# TODO: 暂时放这儿吧
+def encrypt_display(str): return '******'
+
+
+def split_str(s, startIndex=0, lens=None, toInt=False):
+    """
+    分割字符串的函数,用于将协议返还的串口数据分割成指定的长度
+    ex:
+    s = "660000FFFF00A1"
+    startIndex = 2
+    lens = "4422"
+    返还 ["0000", "FFFF", "00", "A1"]
+    :param s: 被分割的字符串
+    :param startIndex: 被分割起始位置
+    :param lens: 要被分割的长度
+    :param toInt: 分割后是否转为 int 数值
+    :return: list
+    """
+    if lens is None:
+        lens = list()
+    if not isinstance(lens, Iterable):
+        raise TypeError("indexes mast be Iterable")
+
+    result = list()
+    for _len in lens:
+        endIndex = int(startIndex) + int(_len)
+
+        tempS = s[startIndex: endIndex]
+        if toInt:
+            tempS = int(tempS, 16)
+        result.append(tempS)
+
+        startIndex = endIndex
+
+    return result
+
+def make_title_from_dict(valueDictList):
+    result = ''
+    keys = []
+    for kd in valueDictList:
+        keys.extend(kd.keys())
+        
+    maxLen = 0
+    for k in keys:
+        if len(k) > maxLen:
+            maxLen = len(k)
+            
+    for valueDict in valueDictList:
+        for k,v in valueDict.items():
+            if k == '':
+                result += u'{}\\n'.format(v)
+            else:
+                needTab = 2 + (maxLen - len(k))/2
+                tabs = ''
+                for ii in range(needTab):
+                    tabs += '\\t'
+
+                result += '\\n\\n%s:%s%s' % (k,tabs,v)
+    result += '\\n'
+    return result

+ 149 - 0
apilib/utils_sys.py

@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+import sys
+import threading
+import uuid
+
+from typing import Union, TYPE_CHECKING, Optional
+
+from contextlib import contextmanager
+
+from django.core.cache import CacheHandler, DefaultCacheProxy, caches
+from kombu.five import monotonic
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    from contextlib import GeneratorContextManager
+
+
+class ThreadLock(object):
+    def __init__(self):
+        self._lock = None
+
+    def acquire_lock(self):
+        """
+        Acquire the module-level lock for serializing access to shared data.
+        This should be released with _releaseLock().
+        """
+        if not self._lock:
+            self._lock = threading.RLock()
+        if self._lock:
+            self._lock.acquire()
+
+    def release_lock(self):
+        """
+        Release the module-level lock acquired by calling _acquireLock().
+        """
+        if self._lock:
+            self._lock.release()
+
+
+@contextmanager
+def memcache_lock(key, value, expire = 60 * 10, mc = caches['lock']):
+    # type: (str, Optional[str,int], int, Union[DefaultCacheProxy, CacheHandler])->GeneratorContextManager
+
+    """
+    Example usage
+    ```
+     with memcache_lock(cache, lock_id, self.app.oid) as acquired:
+        if acquired:
+            return Feed.objects.import_feed(feed_url).url
+            logger.debug(
+                'Feed %s is already being imported by another worker', feed_url)
+    ```
+    :param mc:
+    :param key:
+    :param value:
+    :param expire
+    :return:
+    """
+
+    value = str(value)
+
+    timeout_at = monotonic() + expire - 3
+
+    status = mc.add(key, value, expire)
+
+    logger.debug('memcache_lock add result is:  {}; key is: {}'.format(status, key))
+
+    try:
+        yield status
+    finally:
+        # memcache delete is very slow, but we have to use it to take
+        # advantage of using add() for atomic locking
+        if monotonic() < timeout_at and status:
+            # don't release the lock if we exceeded the timeout
+            # to lessen the chance of releasing an expired lock
+            # owned by someone else
+            # also don't release the lock if we didn't acquire it
+            result = mc.delete(key)
+
+            logger.debug('memcache_lock delete result is:  {}; key is: {}'.format(result, key))
+
+
+class MemcachedLock(object):
+    """
+    Try to do same as threading.Lock, but using Memcached to store lock instance to do a distributed lock
+    """
+
+    def __init__(self, key, value, expire = 360, mc = caches['lock']):
+        # type: (str, str, int, Union[DefaultCacheProxy, CacheHandler])->MemcachedLock
+
+        self.key = key
+        self.mc = mc
+        self.timeout = expire
+        self.instance_id = '{}:{}'.format(uuid.uuid1().hex, value)
+        self.timeout_at = monotonic() + expire - 3
+
+    def __repr__(self):
+        return 'MemcachedLock<key={}, id={}>'.format(self.key, self.instance_id)
+
+    def acquire(self):
+        logger.debug('=== MemcachedLock === try to acquire memcache lock {}'.format(repr(self)))
+
+        added = self.mc.add(self.key, self.instance_id, self.timeout)
+
+        logger.debug("=== MemcachedLock === Added=%s" % repr(added))
+
+        if added:
+            logger.debug('=== MemcachedLock === acquired memcache lock {}'.format(repr(self)))
+            return True
+
+        if added == 0 and not (added is False):
+            raise RuntimeError(
+                u"=== MemcachedLock === Error calling memcached add! Is memcached up and configured? memcached_client.add returns %s" % repr(
+                    added))
+
+        return False
+
+    def release(self):
+        logger.debug('=== MemcachedLock === try to release memcache lock {}'.format(repr(self)))
+
+        value = self.mc.get(self.key)
+
+        if value == self.instance_id:
+            # Avoid short timeout, because if key expires, after GET, and another lock occurs, memcached remove
+            # below can delete another lock! There is no way to solve this in memcached
+            result = self.mc.delete(self.key)
+            logger.debug('=== MemcachedLock === delete result is: {}'.format(str(result)))
+        else:
+            logger.warn("=== MemcachedLock === no lock to release {}. Increase TIMEOUT of lock operations".format(repr(self)))
+
+    @property
+    def locked(self):
+        return True if self.mc.get(self.key) is not None else False
+
+
+PY3 = sys.version_info[0] == 3
+
+@contextmanager
+def MyStringIO():
+    from six import StringIO
+    try:
+        fi = StringIO()
+        yield fi
+    finally:
+        fi.close()

+ 206 - 0
apilib/utils_url.py

@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from six.moves.urllib import parse
+import simplejson as json
+
+
+class UrlHandler(object):
+    DEFAULT_NEXT_HANDLER = None
+
+    def __init__(self, client):
+        self._client = client
+        self._next = UrlHandler.DEFAULT_NEXT_HANDLER
+
+    @property
+    def endHandler(self):
+        if not self.nextHandler:
+            return self
+
+        return self.nextHandler.endHandler
+
+    @property
+    def nextHandler(self):
+        return self._next
+
+    @nextHandler.setter
+    def nextHandler(self, handler):
+        if not isinstance(handler, UrlHandler):
+            raise ValueError("invalid handler for nextHandler(): {}".format(handler))
+
+        self._next = handler
+
+    def parse(self):
+        """
+        对 url 的每一部分都做解析 将各部分的零件都放到该放的位置上去
+        """
+        if self.nextHandler:
+            self.nextHandler.parse()
+
+    def add_query(self, added_query):
+
+        if self.nextHandler:
+            self.nextHandler.add_query(added_query)
+
+
+class SchemeHandler(UrlHandler):
+    pass
+
+
+class NetLocHandler(UrlHandler):
+    pass
+
+
+class PathHandler(UrlHandler):
+    pass
+
+
+class ParamsHandler(UrlHandler):
+    pass
+
+
+class QueryHandler(UrlHandler):
+    def parse(self):
+        pass
+
+    def add_query(self, added_query):
+        if not self._client.has_fragment:
+            query = parse.parse_qs(qs = self._client.parseResult.query, keep_blank_values = True)
+            new_query = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in added_query.items()}
+            query.update(new_query)
+            self._client.parseResult = self._client.parseResult._replace(query = parse.urlencode(query, True))
+        else:
+            pass
+
+        super(QueryHandler, self).add_query(added_query)
+
+
+class FragmentHandler(UrlHandler):
+    def parse(self):
+        pass
+
+    def add_query(self, added_query):
+        if self._client.has_fragment:
+            old_framgent = self._client.parseResult.fragment
+
+            new_query = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in added_query.items()}
+
+            if not old_framgent:
+                fragment_path = ''
+            else:
+                tokens = self._client.parseResult.fragment.split('?')
+                fragment_path = tokens[0]
+                new_query.update(parse.parse_qs(qs = '&'.join(tokens[1:]), keep_blank_values = True))
+
+            self._client.parseResult = self._client.parseResult._replace(
+                fragment = '{}?{}'.format(fragment_path, parse.urlencode(new_query, True)))
+        else:
+            pass
+
+        super(FragmentHandler, self).add_query(added_query)
+
+
+class UrlClient(object):
+    """
+    处理url 每一部分单独处理自己职责内的问题 调用顺序依次是
+    scheme netloc path params query fragment
+    """
+
+    def __init__(self, url):
+        self._url = url
+
+        self._parseResult = parse.urlparse(self._url)
+
+        # 添加节点
+        self.urlHeadHandler = SchemeHandler(self)
+        self.urlHeadHandler.endHandler.nextHandler = NetLocHandler(self)
+        self.urlHeadHandler.endHandler.nextHandler = PathHandler(self)
+        self.urlHeadHandler.endHandler.nextHandler = ParamsHandler(self)
+        self.urlHeadHandler.endHandler.nextHandler = QueryHandler(self)
+        self.urlHeadHandler.endHandler.nextHandler = FragmentHandler(self)
+
+        self.urlHeadHandler.parse()
+
+    @property
+    def has_fragment(self):
+        if '#' in self._url:
+            return True
+        else:
+            return False
+
+    @property
+    def parseResult(self):
+        return self._parseResult
+
+    @parseResult.setter
+    def parseResult(self, value):
+        """
+        TODO value 类型的检查
+        """
+        self._parseResult = value
+
+    def add_query(self, added_query):
+        """
+        对 url 中的query 部分进行更新
+        """
+        self.urlHeadHandler.add_query(added_query)
+        return self
+
+    def getUrl(self):
+        return parse.urlunparse(self.parseResult)
+
+
+def before_frag_add_query(uri, added_query):
+    urlClient = UrlClient(uri)
+    urlClient.add_query(added_query)
+    return urlClient.getUrl()
+
+
+def add_query(uri, added_query):
+    bits = list(parse.urlparse(uri))
+    qs = parse.parse_qs(qs = bits[4], keep_blank_values = True)
+    new_query = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in added_query.items()}
+    qs.update(new_query)
+    bits[4] = parse.urlencode(qs, True)
+    return parse.urlunparse(bits)
+
+
+if __name__ == '__main__':
+    a = "https://www.washpayer.com/user/index.html?l=123456"
+    b = "https://www.washpayer.com/user/index.html?l=123456&chargeIndex=1"
+
+    c = "https://www.washpayer.com/user/index.html?l=123456#/pay"
+    d = "https://www.washpayer.com/user/index.html?l=123456&chargeIndex=1#/pay"
+
+    e = "https://www.washpayer.com/user/index.html#/pay?l=123456"
+    f = "https://www.washpayer.com/user/index.html#/pay?l=123456&chargeIndex=1"
+
+    g = "https://www.washpayer.com/user/index.html?isTest=1#/pay?l=123456"
+    h = "https://www.washpayer.com/user/index.html?isTest=1#/pay?l=123456&chargeIndex=1"
+
+    redirectQuery = {"redirect": "/user/index.html#/pay", "v": "1.0.02"}
+
+    print before_frag_add_query(a, redirectQuery)
+    print before_frag_add_query(b, redirectQuery)
+    print before_frag_add_query(c, redirectQuery)
+    print before_frag_add_query(d, redirectQuery)
+    print before_frag_add_query(e, redirectQuery)
+    print before_frag_add_query(f, redirectQuery)
+    print before_frag_add_query(g, redirectQuery)
+    print before_frag_add_query(h, redirectQuery)
+
+    t1 = 'https://www.washpayer.com/user/index.html#/pay?l=123456'
+    _add_query = {'chargeIndex': 1}
+    print add_query(before_frag_add_query(t1, added_query = _add_query), added_query = {'v': '1.0.0'})
+
+    t2 = 'https://www.washpayer.com/user/index.html#/pay'
+    _add_query = {'l': '123456', 'chargeIndex': 1}
+    print add_query(before_frag_add_query(t2, added_query = _add_query), added_query = {'v': '1.0.0'})
+
+    t3 = 'https://www.washpayer.com/user/index.html#/pay'
+    _add_query = {
+        'l': '123456',
+        'chargeIndex': 1,
+        'redirect': "/user/index.html#/pay"
+    }
+    print add_query(before_frag_add_query(t3, added_query = _add_query), added_query = {'v': '1.0.0'})

+ 22 - 0
apps/__init__.py

@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+import signal
+
+from django.core.cache import caches
+
+serviceCache = caches['service']
+reportCache = caches['report']
+lockCache = caches['lock']
+
+
+def handle_usr1(sig, frame):
+    from apps.web.core.models import SystemSettings
+    SystemSettings.reload_settings()
+
+
+def init_signals():
+    if hasattr(signal, "SIGUSR1"):
+        signal.signal(signal.SIGUSR1, handle_usr1)
+
+
+init_signals()

+ 0 - 0
apps/accounting/__init__.py


+ 67 - 0
apps/accounting/reconcile.py

@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+import datetime
+
+from typing import Tuple
+
+from dateutil.parser import parse
+
+from apps.web.user.models import RechargeRecord
+
+
+def dt_range(date_string):
+    # type:(str)->Tuple[datetime.datetime, datetime.datetime]
+    start = parse(date_string)
+    return start, datetime.datetime.combine(start, datetime.time.max)
+
+
+def cn(unicode_):
+    assert isinstance(unicode_, unicode)
+    return unicode_.encode('gb18030')
+
+
+def mask(df, key, value): return df[df[key] == value]
+
+
+def df_in(df, key, items): return df[df[key].isin(items)]
+
+
+def withdraw_succeeded_mask(df):
+    return mask(df, key = "付款状态", value = "付款成功")
+
+
+def download_alipay_statement():
+    #: TODO
+    pass
+
+
+def read_alipay_statement(file_name):
+    """
+
+    :param file_name:
+    :return:
+    """
+    #: 跳过支付宝的csv表头注释
+    #: TODO change file encoding
+    import pandas as pd
+    return pd.read_csv(file_name, skiprows = 2)
+
+
+def reconcile_alipay(file_name, date_str):
+    # type:(str, str)->list
+    """
+
+    :param file_name:
+    :param date_str:
+    :return:
+    """
+    df = mask(df = read_alipay_statement(file_name), key = cn(u'交易状态'), value = cn(u'成功'))
+    start, end = dt_range(date_str)
+    records = RechargeRecord.objects(gateway = 'alipay', result = 'unPay', dateTimeAdded__gte = start,
+                                     dateTimeAdded__lte = end)
+    df = df[df[cn(u'商户订单号')].isin([str(_.orderNo) for _ in records])]
+    return list(df[cn(u'商户订单号')])
+
+
+def reset_today_income():
+    pass

+ 0 - 0
apps/common/__init__.py


+ 167 - 0
apps/common/utils.py

@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+import calendar
+import datetime
+import math
+import numbers
+
+from django.conf import settings
+from pandas import date_range
+
+
+class CoordinateTrans(object):
+    """
+    坐标系转换
+    """
+    def __init__(self, lng=0.0, lat=0.0):
+        """
+        :param lng: 经度
+        :param lat: 维度
+        """
+        self._x_pi = 3.14159265358979324 * 3000.0 / 180.0
+        self._lng = lng
+        self._lat = lat
+
+    def bd09_to_gcj02(self):
+        """
+        百度坐标系到google坐标系的经纬度转换
+        :return:
+        """
+        if self._lng == 0.0 or self._lat == 0.0:
+            return self._lng, self._lat
+
+        x = self._lng - 0.0065
+        y = self._lat - 0.006
+        z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * self._x_pi)
+        theta = math.atan2(y, x) - 0.000003 * math.cos(x * self._x_pi)
+        gg_lng = z * math.cos(theta)
+        gg_lat = z * math.sin(theta)
+        return [gg_lng, gg_lat]
+
+    def gcj02_to_bd09(self):
+        """
+        高德/谷歌 坐标系 转百度
+        :return:
+        """
+        if self._lng == 0.0 or self._lat == 0.0:
+            return self._lng, self._lat
+
+        z = math.sqrt(self._lng * self._lng + self._lat * self._lat) + 0.00002 * math.sin(self._lat * self._x_pi)
+        theta = math.atan2(self._lat, self._lng) + 0.000003 * math.cos(self._lng * self._x_pi)
+        bd_lng = z * math.cos(theta) + 0.0065
+        bd_lat = z * math.sin(theta) + 0.006
+        return [bd_lng, bd_lat]
+
+    def __call__(self, lng, lat, _type="b_to_g"):
+        """
+        :param lng:
+        :param lat:
+        :param _type: b_to_g 百度转高德   g_to_b 高德转百度
+        :return:
+        """
+        self._lng = lng
+        self._lat = lat
+
+        if _type == "b_to_g":
+            return self.bd09_to_gcj02()
+        elif _type == "g_to_b":
+            return self.gcj02_to_bd09()
+        else:
+            pass
+
+
+coordinateHandler = CoordinateTrans()
+
+
+class IntToHex(object):
+    """
+    数字转换16进制字符串
+    """
+    def __init__(self):
+        self._number = None
+
+    def _add_trans_number(self, number):
+        if isinstance(number, numbers.Integral):
+            self._number = number
+        elif isinstance(number, str) and number.isdigit():
+            self._number = int(number)
+        else:
+            raise TypeError("type of number must in <Integral,str>")
+
+    @staticmethod
+    def _reverse_hex(h):
+        if not len(h):
+            return ""
+        return h[-2:] + IntToHex._reverse_hex(h[:-2])
+
+    def __call__(self, number, lens=4, reverse=False):
+        """
+
+        :param number:  带转换的数字
+        :type number: int
+        :param lens: 转换后的长度
+        :type lens: int
+        :param reverse: 是否高低位互换
+        :type reverse: bool
+        :return:
+        :rtype: str
+        """
+        if not isinstance(lens, numbers.Integral):
+            raise TypeError("type of lens must be Integral")
+        self._add_trans_number(number)
+
+        formatStr = "{:0>%sX}" % lens
+        numberHex = formatStr.format(self._number)
+        if reverse:
+            return IntToHex._reverse_hex(numberHex)
+
+        return numberHex
+
+
+int_to_hex = IntToHex()
+
+
+def get_start_and_end_by_month(monthStr):
+    # type:(str) -> (str, str)
+    """根据月份获取 当月月初和月末"""
+    year, month = map(lambda x: int(x), monthStr.split("-"))
+    firstWeekDay, lastDay = calendar.monthrange(year, month)
+
+    startTime = datetime.date(year, month, 1).strftime("%Y-%m-%d")
+    endTime = datetime.date(year, month, lastDay).strftime("%Y-%m-%d")
+
+    return startTime, endTime
+
+
+def get_start_and_end_by_year(yearStr):
+    """
+    获取年初 和年尾
+    :param yearStr:
+    :return:
+    """
+    return "{}-01-01".format(yearStr), "{}-12-31".format(yearStr)
+
+
+def get_date_range(st, et, freq='D', reverse = True):
+    if reverse:
+        return list(reversed(date_range(st, et, freq=freq).tolist()))
+    else:
+        return list(date_range(st, et, freq = freq).tolist())
+
+
+def get_test_point(domain, key):
+    import os
+    return os.environ.get('test_{}_{}'.format(domain, key), None)
+
+
+def support_test_point(point):
+    if not settings.DEBUG:
+        return False
+
+    import os
+    test_point = os.environ.get(point, None)
+
+    if test_point == 'yes':
+        return True
+    else:
+        return False

+ 2 - 0
apps/dispatch/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python

+ 55 - 0
apps/dispatch/commands.py

@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+import json
+import logging
+
+from apps.web.core.exceptions import InvalidParameter
+
+from apps.dispatch.common import get_result_topic, get_query_topic
+
+logger = logging.getLogger(__name__)
+
+
+class TopicCommand(object):
+
+    def __init__(self, cmdNo, devNo, params=None, prefix=''):
+        self.cmdNo = cmdNo
+        self.devNo = devNo
+
+        if params is None:
+            self.params = {}
+
+        elif isinstance(params, list):
+            self.params = { _["name"] : {_["key"]: _["value"]} for _ in params }
+
+        else:
+            self.params = params
+
+        self.prefix = prefix
+
+    def __str__(self):
+        return '{prefix}/{devNo}/{cmdNo}'.format(prefix = self.prefix,
+                                                 devNo = self.devNo,
+                                                 cmdNo = self.cmdNo)
+
+    @property
+    def result_topic(self):
+        return get_result_topic(prefix=self.prefix, cmdNo=self.cmdNo, devNo=self.devNo)
+
+    @property
+    def query_topic(self):
+        return get_query_topic(prefix=self.prefix, cmdNo=self.cmdNo, devNo=self.devNo)
+
+    @property
+    def query_payload(self):
+        return json.dumps(self.params)
+
+    @property
+    def result_payload(self):
+        try:
+            payload = self.params
+            payload['IMEI'] = self.devNo
+            return payload
+        except ValueError:
+            raise InvalidParameter('params has to be json-like, params was = %s' % (self.params,))
+

+ 44 - 0
apps/dispatch/common.py

@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+import os
+
+from dotenv import load_dotenv
+from parse import parse
+
+load_dotenv('.env.dispatch')
+
+from apps.web.core.exceptions import FormatError
+
+QUERY_PREFIX = 'query'
+RESULT_PREFIX = 'result'
+
+HANDLER_MQTT_HOST = os.environ.get('MQTT_HOSTNAME', 'test.mosquitto.org')
+HANDLER_MQTT_PORT = int(os.environ.get('MQTT_PORT', 8080))
+
+
+TOPIC_FMT = '/{type}/{prefix}/{cmdNo}/{devNo}'
+
+
+def get_result_topic(prefix, cmdNo, devNo):
+    return TOPIC_FMT.format(type=RESULT_PREFIX, prefix=prefix, cmdNo=cmdNo, devNo=devNo)
+
+def get_query_topic(prefix, cmdNo, devNo):
+    return TOPIC_FMT.format(type=QUERY_PREFIX, prefix=prefix, cmdNo=cmdNo, devNo=devNo)
+
+
+ALL_QUERY_TOPICS = '/{prefix}/#'.format(prefix=QUERY_PREFIX)
+
+
+def parse_topic(string, format_=TOPIC_FMT):
+    return parse(format=format_, string=string).named
+
+
+class BrokerUrl(object):
+
+    def __init__(self, url):
+        self.url = url
+        try:
+            self.host, self.port = url.split(':')
+            self.port = int(self.port)
+        except (ValueError, AttributeError):
+            raise FormatError('url has to be a string and format has to be host:port')

+ 151 - 0
apps/dispatch/tasks.py

@@ -0,0 +1,151 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+from urlparse import urlparse
+
+from celery.utils.log import get_task_logger
+
+from library.paho.mqtt.publish import _on_publish, _on_connect
+
+from django.conf import settings
+
+from apps.web.core.mqtt_client import get_client_id, MqttClient
+from apps.web.device.models import Device
+
+from apps.web.core.networking import MessageSender
+
+from apps.dispatch.commands import TopicCommand
+
+logger = get_task_logger(__name__)
+
+def publish_via_websockets(msgs, host="localhost", port=1883, path='/ws', client_id="", auth=None, tls=None):
+    """Publish multiple messages to a broker, then disconnect cleanly.
+
+    This function creates an MQTT client, connects to a broker and publishes a
+    list of messages. Once the messages have been delivered, it disconnects
+    cleanly from the broker.
+
+    msgs : a list of messages to publish. Each message is either a dict or a
+           tuple.
+
+           If a dict, only the topic must be present. Default values will be
+           used for any missing arguments. The dict must be of the form:
+
+           msg = {'topic':"<topic>", 'payload':"<payload>", 'qos':<qos>,
+           'retain':<retain>}
+           topic must be present and may not be empty.
+           If payload is "", None or not present then a zero length payload
+           will be published.
+           If qos is not present, the default of 0 is used.
+           If retain is not present, the default of False is used.
+
+           If a tuple, then it must be of the form:
+           ("<topic>", "<payload>", qos, retain)
+
+    host : a string containing the address of the broker to connect to.
+               Defaults to localhost.
+
+    port : the port to connect to the broker on. Defaults to 1883.
+
+    client_id : the MQTT client id to use. If "" or None, the Paho library will
+                generate a client id automatically.
+
+    keepalive : the keepalive timeout value for the client. Defaults to 60
+                seconds.
+
+    will : a dict containing will parameters for the client: will = {'topic':
+           "<topic>", 'payload':"<payload">, 'qos':<qos>, 'retain':<retain>}.
+           Topic is required, all other parameters are optional and will
+           default to None, 0 and False respectively.
+           Defaults to None, which indicates no will should be used.
+
+    auth : a dict containing authentication parameters for the client:
+           auth = {'username':"<username>", 'password':"<password>"}
+           Username is required, password is optional and will default to None
+           if not provided.
+           Defaults to None, which indicates no authentication is to be used.
+
+    tls : a dict containing TLS configuration parameters for the client:
+          dict = {'ca_certs':"<ca_certs>", 'certfile':"<certfile>",
+          'keyfile':"<keyfile>", 'tls_version':"<tls_version>",
+          'ciphers':"<ciphers">}
+          ca_certs is required, all other parameters are optional and will
+          default to None if not provided, which results in the client using
+          the default behaviour - see the paho.mqtt.client documentation.
+          Alternatively, tls input can be an SSLContext object, which will be
+          processed using the tls_set_context method.
+          Defaults to None, which indicates that TLS should not be used.
+
+    transport : set to "tcp" to use the default setting of transport which is
+          raw TCP. Set to "websockets" to use WebSockets as the transport.
+    """
+
+    if not isinstance(msgs, list):
+        raise ValueError('msgs must be a list')
+
+    client = MqttClient(client_id=client_id, userdata=msgs, transport='websockets')
+
+    client.on_publish = _on_publish
+    client.on_connect = _on_connect
+
+    client.ws_set_options(path=path)
+
+    if auth:
+        username = auth.get('username')
+        if username:
+            password = auth.get('password')
+            client.username_pw_set(username, password)
+        else:
+            raise KeyError("The 'username' key was not found, this is "
+                           "required for auth")
+
+    if tls is not None:
+        if isinstance(tls, dict):
+            client.tls_set(**tls)
+        else:
+            # Assume input is SSLContext object
+            client.tls_set_context(tls)
+
+    client.connect(host, port)
+    client.loop_forever()
+
+
+def publish_single_via_websockets(topic, payload, host, port, path, qos=0, retain=False, client_id=""):
+    msgs = [{'topic': topic, 'payload': payload, 'qos': qos, 'retain': retain}]
+    return publish_via_websockets(msgs=msgs, host=host, port=port, path=path, client_id=client_id)
+
+
+def send_topic_command(cmdNo, devNo, params, prefix='diag'):
+
+    command = TopicCommand(cmdNo, devNo, params, prefix)
+
+    dev = Device.get_dev(devNo)
+
+    from apps.web.core.helpers import ActionDeviceBuilder
+
+    box = ActionDeviceBuilder.create_action_device(dev)
+
+    try:
+        result = MessageSender.send(device = dev, cmd = cmdNo,
+                                    payload = command.result_payload)
+        translation = box.translate_server_cmd(result)
+
+    except Exception as e:
+        logger.exception(e)
+        translation = u'未知错误'
+
+    client_id = get_client_id(prefix='result')
+
+    def decompose_url(url):
+        if '//' not in url:
+            url = '//' + url
+        return urlparse(url)
+
+    broker_url = decompose_url(settings.DEFAULT_DEALER_PUSH_BROKER_URL)
+
+    publish_single_via_websockets(topic=command.result_topic,
+                   payload=translation,
+                   host=broker_url.hostname,
+                   port=broker_url.port,
+                   path=broker_url.path,
+                   client_id=client_id)

+ 47 - 0
apps/patch/__init__.py

@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+import threading
+from django.core.cache import CacheHandler, _create_cache, InvalidCacheBackendError
+
+logger = logging.getLogger(__name__)
+
+_getitem = CacheHandler.__getitem__
+
+
+def patch_cache_handler():
+    CacheHandler._poll_cache_lock = threading.Lock()
+
+    def __getitem__(self, alias):
+        from django.conf import settings
+        if alias not in settings.CACHES:
+            raise InvalidCacheBackendError(
+                "Could not find config for '%s' in settings.CACHES" % alias
+            )
+
+        if settings.CACHES[alias].get('BACKEND_TYPE', '') == 'pool':
+            try:
+                return self._poll_caches[alias]
+            except AttributeError:
+                pass
+            except KeyError:
+                pass
+
+            with self._poll_cache_lock:
+                logger.debug('init cache poll<alias={}>'.format(alias))
+
+                if getattr(self, '_poll_caches', None) is None:
+                    self._poll_caches = {}
+
+                cache = _create_cache(alias)
+                self._poll_caches[alias] = cache
+
+                return cache
+        else:
+            return _getitem(self, alias)
+
+    CacheHandler.__getitem__ = __getitem__
+
+
+patch_cache_handler()

+ 0 - 0
apps/provision/__init__.py


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1172 - 0
apps/provision/bank.py


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 2787 - 0
apps/provision/bank_card.py


+ 4 - 0
apps/thirdparties/__init__.py

@@ -0,0 +1,4 @@
+## -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+from .alioss import AliOSS

+ 38 - 0
apps/thirdparties/alioss.py

@@ -0,0 +1,38 @@
+## -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import oss2
+from django.conf import settings
+
+
+class AliOSS(object):
+    def __init__(self,
+                 access_key_id = settings.ALI_OSS_ACCESS_ID,
+                 access_key_secret = settings.ALI_OSS_ACCESS_KEY,
+                 endpoint = settings.ALI_OSS_ENDPOINT,
+                 bucket_name = settings.ALI_OSS_BUKET_NAME):
+        auth = oss2.Auth(access_key_id, access_key_secret)
+        self.bucket = oss2.Bucket(auth, endpoint, bucket_name, enable_crc = False)
+
+    def put_object(self, key, data, headers = {
+        # 'Content-Encoding': 'br',
+        'Content-Disposition': 'inline',
+        'Cache-Control': 'max-age=315360000'
+    }):
+        self.bucket.put_object(key = key, data = data, headers = headers)
+
+    def put_attachment(self, key, data, headers = {
+        'Content-Disposition': 'attachment',
+        'Cache-Control': 'max-age=315360000'
+    }):
+        self.bucket.put_object(key = key, data = data, headers = headers)
+
+    def get_object(self, key, headers={
+        'Content-Disposition': 'attachment',
+        'Cache-Control': 'max-age=315360000'
+    }):
+        content = bytes()
+        for chunk in self.bucket.get_object(key, headers=headers):
+            content += chunk
+
+        return content

+ 384 - 0
apps/thirdparties/aliyun.py

@@ -0,0 +1,384 @@
+## -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+
+import logging
+import random
+import time
+from collections import OrderedDict
+
+import simplejson as json
+from alibabacloud_imarketing20220704.client import Client
+from alibabacloud_imarketing20220704.models import CreateDeviceRequest, ListAdvertisingRequestApp, \
+    ListAdvertisingRequestImp, ListAdvertisingRequestUser, ListAdvertisingRequest, GetUserFinishedAdRequest
+from alibabacloud_tea_openapi.models import Config
+from aliyunsdkafs.request.v20180112 import AuthenticateSigRequest
+from aliyunsdkcore.client import AcsClient
+from aliyunsdkcore.request import CommonRequest
+from aliyunsdkunimkt.request.v20181207 import PopUpQueryRequest, QueryPromotionRequest
+from aliyunsdkunimkt.request.v20181212 import QueryUnionPromotionRequest, GetUnionTaskStatusRequest, \
+    CreateProxyBrandUserRequest, QueryUnionSumChannelDataRequest, QueryContentListRequest, QueryTaskRuleLimitRequest, \
+    CreateUnionTaskRequest, EndUnionTaskRequest, QueryUnionTaskInfoRequest, QueryUnionTaskListRequest, \
+    RegistDeviceRequest
+from django.conf import settings
+from requests import ConnectionError
+from simplejson import JSONDecodeError
+from typing import Union
+
+from apps.web.constant import Const
+
+logger = logging.getLogger(__name__)
+
+
+def create_sign(map, key):
+    # type: (dict, Union[str, None]) -> str
+
+    SECRET = "1234512345123451"
+
+    sign = ''
+
+    # 1 TODO 校验map,并排序
+    Odict = OrderedDict(dict(sorted(map.items())))
+
+    # 2 TODO 拼接字符串
+    sign = ''
+    for k, v in Odict.items():
+        sign += str(k)
+        sign += str(v)
+
+    # 3 TODO 加盐
+    sign = SECRET + sign + SECRET
+
+    ba = bytearray(sign)
+
+    # 4 TODO MD5加密
+    import hashlib
+    sign = hashlib.md5(ba).hexdigest().upper()
+
+    return sign
+
+
+class Aliyun(object):
+    def __init__(self, appid, secret, region_id, product_code, endpoint):
+        self.client = AcsClient(appid, secret)
+        self.client.add_endpoint(region_id, product_code, endpoint)
+
+    @staticmethod
+    def create_sign(map, key):
+        # type: (dict, Union[str, None]) -> str
+
+        SECRET = "1234512345123451"
+
+        sign = ''
+
+        # 1 TODO 校验map,并排序
+        Odict = OrderedDict(dict(sorted(map.items())))
+
+        # 2 TODO 拼接字符串
+        sign = ''
+        for k, v in Odict.items():
+            sign += str(k)
+            sign += str(v)
+
+        # 3 TODO 加盐
+        sign = SECRET + sign + SECRET
+
+        ba = bytearray(sign)
+
+        # 4 TODO MD5加密
+        import hashlib
+        sign = hashlib.md5(ba).hexdigest().upper()
+
+        return sign
+
+    @staticmethod
+    def first_Cap(string):
+        return string[0].upper() + string[1:]
+
+
+class AliyunSlider(Aliyun):
+    def __init__(self, appid = Const.ALI_SLIDER_ACCESSKEY_ID, secret = Const.ALI_SLIDER_ACCESSKEY_SECRET):
+        super(AliyunSlider, self).__init__(
+            appid = appid,
+            secret = secret,
+            region_id = 'cn-hangzhou',
+            product_code = 'afs',
+            endpoint = 'afs.aliyuncs.com')
+
+    def check_validation_results(self, sessionId, sig, token):
+        request = AuthenticateSigRequest.AuthenticateSigRequest()
+        # 会话ID。必填参数,从前端获取,不可更改。
+        request.set_SessionId(sessionId)
+        # 签名串。必填参数,从前端获取,不可更改。
+        request.set_Sig(sig)
+        # 请求唯一标识。必填参数,从前端获取,不可更改。
+        request.set_Token(token)
+        # 场景标识。必填参数,从前端获取,不可更改。
+        request.set_Scene('nc_message_h5')
+        # 应用类型标识。必填参数,后端填写。
+        request.set_AppKey('FFFF0N000000000088F1')
+        # 客户端IP。必填参数,后端填写。
+        request.set_RemoteIp('120.26.227.50')
+        result = self.client.do_action_with_exception(request)
+
+        # 返回code 100表示验签通过,900表示验签失败
+        return result
+
+
+class AliLaXin(Aliyun):
+    def __init__(self, appid=settings.ALIYUN_ACCESS_KEY_ID, secret=settings.ALIYUN_ACCESS_KEY_SECRET):
+        super(AliLaXin, self).__init__(
+            appid=appid,
+            secret=secret,
+            region_id='cn-hangzhou',
+            product_code='UniMkt',
+            endpoint='cloudcode.aliyuncs.com')
+
+    def QueryUnionPromotionRequest(self, param):
+        """
+        :param param: {'alipayOpenId': '2088812351147125','channelId': 'test',}
+        :return:
+        """
+        request = QueryUnionPromotionRequest.QueryUnionPromotionRequest()
+        request.set_AlipayOpenId(param.get('alipayOpenId'))
+        request.set_ChannelId(param.get('channelId') or settings.ALIPAY_LAXIN_CHANNEL_ID)
+
+        sign_dict = {
+            'alipayOpenId': request.get_AlipayOpenId(),
+            'channelId': request.get_ChannelId()
+        }
+
+        sign = self.create_sign(sign_dict, None)
+        request.set_Sign(sign)
+
+        try:
+            resp = self.client.do_action_with_exception(request)
+        except:
+            return {}
+        return json.loads(resp)
+
+    def GetUnionTaskStatusRequest(self, param):
+        """
+        :param param: {'alipayOpenId': '小明','taskId': '1623913226986034',}
+        :return:
+        """
+        request = GetUnionTaskStatusRequest.GetUnionTaskStatusRequest()
+        request.set_AlipayOpenId(param.get('alipayOpenId'))
+        request.set_TaskId(param.get('taskId'))
+        request.add_query_param('ChannelId', param.get('channelId', settings.ALIPAY_LAXIN_CHANNEL_ID))  # 此字段不参与签名
+
+        sign_dict = {
+            'alipayOpenId': request.get_AlipayOpenId(),
+            'taskId': request.get_TaskId(),
+        }
+
+        sign = create_sign(sign_dict, None)
+
+        request.set_Sign(sign)
+
+        try:
+            resp = self.client.do_action_with_exception(request)
+        except:
+            return {}
+        return json.loads(resp)
+
+
+class AliRuHui(Aliyun):
+    def __init__(self, appid=settings.ALIYUN_ACCESS_KEY_ID, secret=settings.ALIYUN_ACCESS_KEY_SECRET):
+        super(AliRuHui, self).__init__(
+            appid=appid,
+            secret=secret,
+            region_id='cn-hangzhou',
+            product_code='UniMkt',
+            endpoint='cloudcode.cn-hangzhou.aliyuncs.com')
+
+    # 入会
+    def PopUpQueryRequest(self, param):
+
+        request = PopUpQueryRequest.PopUpQueryRequest()
+        request.set_UrlId(param.get('urlId'))  # url Id号码
+        request.set_ChannelId(param.get('channelId', settings.ALIPAY_RUHUI_CHANNEL_ID))  # 渠道号
+        request.set_AlipayOpenId(param.get('alipayOpenId'))  # 阿里 openId
+        request.set_OuterCode(param.get('outerCode'))  # 设备号
+        request.set_Extra(param.get('extra', ''))  # 额外字段
+        request.set_OptionType('1')  # 渠道商自由分配 !!! 重要
+
+        try:
+            resp = self.client.do_action_with_exception(request)
+        except:
+            return {}
+        return json.loads(resp)
+
+    def QueryPromotionRequest(self, param):
+        request = QueryPromotionRequest.QueryPromotionRequest()
+        request.set_AlipayOpenId(param.get('alipayOpenId'))  # 阿里 openId
+        request.set_ChannelId(param.get('channelId', settings.ALIPAY_RUHUI_CHANNEL_ID))  # 渠道号
+
+        try:
+            resp = self.client.do_action_with_exception(request)
+        except:
+            return {}
+        return json.loads(resp)
+
+    def RegistDeviceRequest(self):
+        request = RegistDeviceRequest.RegistDeviceRequest()
+        # 固定值
+        request.set_FirstScene('社区')
+        request.set_SecondScene('普通社区')
+        request.set_DeviceType('聚合支付')
+        request.set_ChannelId('QD-VHWFTEST-434497')
+
+        # 设备唯一编号
+        request.set_OuterCode('434497')
+
+        # 设置省份
+        request.set_Province('山东省')
+        # 设置所属城市
+        request.set_City('济南')
+        # 设置所属区域
+        request.set_District('高新区')
+        # 设置详细地址
+        request.set_DetailAddr('中铁财智6号楼')
+        # 设备型号
+        request.set_DeviceModelNumber('设备型号')
+        # 设备名称
+        request.set_DeviceName('猛犸充电桩')
+        # 设备点位名称
+        request.set_LocationName('点位名称')
+        # 楼层名称
+        request.set_Floor('楼层')
+
+        resp = self.client.do_action_with_exception(request)
+        return resp
+
+
+class AliSms(Aliyun):
+    def __init__(self, appid = 'LTAI4GEc1j8pvs4EjFrtL5K9', secret = 'KuCEo8YWRn7tjQaJsXJCcG7P4leBMr'):
+        super(AliSms, self).__init__(
+            appid = appid,
+            secret = secret,
+            region_id = 'cn-hangzhou',
+            product_code = 'Dysmsapi',
+            endpoint = 'dysmsapi.aliyuncs.com')
+
+    def send(self, phoneNumber, templateId, msg, productName, verifyCode = False):
+        """
+        :param phoneNumber:
+        :param templateId:
+        :param msg:
+        :param productName:
+        :return: dict
+        """
+        try:
+            request = CommonRequest()
+            request.set_accept_format('json')
+            request.set_method('POST')
+            request.set_protocol_type('http')  # https | http
+            request.set_version('2017-05-25')
+            request.set_action_name('SendSms')
+
+            request.add_query_param('RegionId', 'cn-hangzhou')
+            request.add_query_param('PhoneNumbers', phoneNumber)
+            # request.add_query_param('SignName', productName)
+            request.add_query_param('SignName', u'微付乐')
+            request.add_query_param('TemplateCode', templateId)
+
+            if verifyCode:
+                request.add_query_param('TemplateParam', {'code': msg})
+            else:
+                request.add_query_param('TemplateParam', {'user': productName, 'detail': msg})
+
+            response = self.client.do_action_with_exception(request)
+
+            result = json.loads(response)
+
+            if result['Code'] == 'OK':
+                return {'result': True, 'msg': 'success'}
+            else:
+                return {'result': False, 'msg': result['Message']}
+        except JSONDecodeError as e:
+            logger.exception(e)
+            return {'result': False, 'msg': u'短信服务器繁忙,请稍后重试'}
+        except ConnectionError as e:
+            logger.exception(e)
+            return {'result': False, 'msg': u'短信服务器不可用,请稍后重试'}
+
+
+class AlipayYunMaV3:
+    def __init__(self, access_key_id=None, access_key_secret=None):
+        self.config = Config(
+            access_key_id=access_key_id or settings.ALIYUN_ACCESS_KEY_ID,
+            access_key_secret=access_key_secret or settings.ALIYUN_ACCESS_KEY_SECRET,
+            region_id='cn-hangzhou',
+        )
+
+    def reg_dev(self, dic):
+        self.config.endpoint = 'imarketing.cn-zhangjiakou.aliyuncs.com'
+
+        dic.update({
+            'channel_id': settings.ALIPAY_CHANNEL_ID_V3,
+            'media_id': settings.ALIPAY_MEDIA_ID_V3,
+        })
+
+        request = CreateDeviceRequest(**dic)
+        client = Client(self.config)
+        response = client.create_device(request)
+        return response
+
+    def get_cpm_body(self, openId, logicalCode):
+
+        cpm_imp_id = 'CPM{:}{:03d}'.format(int(time.time() * 1000), random.randint(1, 1000))
+
+        self.config.endpoint = 'imarketing.aliyuncs.com'
+        app = ListAdvertisingRequestApp(mediaid=settings.ALIPAY_MEDIA_ID_V3, sn=logicalCode)
+        cpm_imp = ListAdvertisingRequestImp(id=cpm_imp_id, tagid=settings.ALIPAY_IMP_CPM_V3)
+        user = ListAdvertisingRequestUser(id=openId, usertype='ALIPAY_OPEN_ID')
+
+        request = ListAdvertisingRequest(app=app, imp=[cpm_imp], user=user)
+        client = Client(self.config)
+        response = client.list_advertising(request)
+        return response
+
+    def get_cpa_ruhui_body(self, openId, logicalCode):
+        rh_imp_id = 'CPARH{:}{:03d}'.format(int(time.time() * 1000), random.randint(1, 1000))
+
+        self.config.endpoint = 'imarketing.aliyuncs.com'
+        app = ListAdvertisingRequestApp(mediaid=settings.ALIPAY_MEDIA_ID_V3, sn=logicalCode)
+        rh_imp = ListAdvertisingRequestImp(id=rh_imp_id, tagid=settings.ALIPAY_IMP_CPA_RUHUI_V3)
+        user = ListAdvertisingRequestUser(id=openId, usertype='ALIPAY_OPEN_ID')
+
+        request = ListAdvertisingRequest(app=app, imp=[rh_imp], user=user, id=rh_imp_id)
+        client = Client(self.config)
+        response = client.list_advertising(request)
+        return response
+
+    def get_cpa_laxin_body(self, openId, logicalCode):
+        lx_imp_id = 'CPALX{:}{:03d}'.format(int(time.time() * 1000), random.randint(1, 1000))
+
+        self.config.endpoint = 'imarketing.aliyuncs.com'
+        app = ListAdvertisingRequestApp(mediaid=settings.ALIPAY_MEDIA_ID_V3, sn=logicalCode)
+        lx_imp = ListAdvertisingRequestImp(id=lx_imp_id, tagid=settings.ALIPAY_IMP_CPA_RUHUI_V3)
+        user = ListAdvertisingRequestUser(id=openId, usertype='ALIPAY_OPEN_ID')
+
+        request = ListAdvertisingRequest(app=app, imp=[lx_imp], user=user, id=lx_imp_id)
+        client = Client(self.config)
+        response = client.list_advertising(request)
+        return response
+
+    def query_task_status(self, clicklink, openId, adid, taskType='RH'):
+        self.config.endpoint = 'imarketing.aliyuncs.com'
+
+        if taskType == 'RH':  # 入会
+            tagid = settings.ALIPAY_IMP_CPA_RUHUI_V3
+        elif taskType == 'LX':  # 拉新
+            tagid = settings.ALIPAY_IMP_CPA_LAXIN_V3
+        elif taskType == 'CPM':
+            tagid = settings.ALIPAY_IMP_CPM_V3
+        else:
+            raise TypeError('task type not in ["CPM", "RH", "LX"]')
+
+        request = GetUserFinishedAdRequest(adid=adid, clicklink=clicklink, mediaid=settings.ALIPAY_MEDIA_ID_V3,
+                                           tagid=tagid, uid=openId)
+        client = Client(self.config)
+        response = client.get_user_finished_ad(request)
+        return response

+ 24 - 0
apps/thirdparties/dingding.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+import simplejson as json
+import requests
+from django.utils.encoding import force_text
+
+logger = logging.getLogger(__name__)
+
+_DINGDING_TALK_URL = 'https://oapi.dingtalk.com/robot/send?access_token=acb44458217bd584bd3c29129c7ac6e59b6484314b441374b8b79dab9181f7ee'
+
+
+class DingDingRobot(object):
+    def __init__(self, url=_DINGDING_TALK_URL):
+        self.url = url
+
+    def send_msg(self, msg):
+        data = {"msgtype": "text", "text": {"content": force_text(msg)}}
+        headers = {'Content-Type': 'application/json;charset=UTF-8'}
+        send_data = json.dumps(data).encode('utf-8')
+        response = requests.post(url=self.url, data=send_data, headers=headers, timeout=15)
+        logger.debug(response.text)

+ 3 - 0
apps/thirdparties/pulsar.py

@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+# !/usr/bin/env python
+

+ 2 - 0
apps/visual/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python

+ 19 - 0
apps/visual/base.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+
+class Visualizer(object):
+
+    def visualized(self):
+        raise NotImplementedError('you cannot use Visualizer base class')
+
+    v = visualized
+
+class ModelProxy(object):
+
+    _meta_model = None
+
+    _query_fields = []
+
+    def __init__(self, **kwargs):
+        self._model = self._meta_model(**kwargs).get()

+ 14 - 0
apps/visual/utils.py

@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+import pprint
+
+class UnicodePrettyPrinter(pprint.PrettyPrinter):
+    """
+    可以方便地打印出unicode
+    """
+    def format(self, object, context, maxlevels, level):
+        ret = pprint.PrettyPrinter.format(self, object, context, maxlevels, level)
+        if isinstance(object, unicode):
+            ret = (object.encode('utf-8'), ret[1], ret[2])
+        return ret

+ 51 - 0
apps/visual/visualizers.py

@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+# from tabulate import tabulate
+
+# from .base import Visualizer, ModelProxy
+#
+# from apps.web.agent.models import Agent
+# from apps.web.dealer.models import Dealer
+# from apps.web.user.models import MyUser
+# from apps.web.device.models import Device, Group
+#
+#
+# class DealerVisualizer(Visualizer, ModelProxy):
+#     """
+#     经销商需要可视化的信息有
+#     :1 经销商的余额,手机号,代理商名字和手机号,创建时间,最近一次登录时间
+#     :2 所拥有的组,所拥有的设备
+#     :3 合伙人情况
+#     """
+#
+#     _meta_model = Dealer
+#
+#     _fields = []
+#
+#     def visualized(self):
+#         m = self._model
+#         a = Agent.objects(id=m.agentId).get()
+#         g = Group.get_group_ids_of_dealer(str(m.id))
+#         mapping = [
+#             ('id', str(m.id)),
+#             (u'姓名', m.nickname),
+#             (u'手机号', m.username),
+#             (u'代理商', '%s(%s)' % (a.nickname, a.username))
+#         ]
+#         table, headers = zip(*mapping)
+#         print tabulate([table], headers)
+#
+# class DeviceVisualizer(Visualizer, ModelProxy):
+#
+#     _meta_model = Device
+#
+#     def visualized(self):
+#         pass
+#
+# class MyUserVisualizer(Visualizer, ModelProxy):
+#
+#     _meta_model = MyUser
+#
+#     def visualized(self):
+#         pass

+ 4 - 0
apps/web/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+

+ 29 - 0
apps/web/ad/__init__.py

@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import json
+from collections import namedtuple
+
+from django.http import HttpResponse
+
+from apilib.systypes import StrEnum
+from apilib.utils_json import DescriptiveJSONEncoder
+
+
+class OfflineTaskType(StrEnum):
+    AD_REPORT = u'广告报表'
+
+
+class MyJsonOkResponse(HttpResponse):
+    def __init__(self, data, safe = True, ordered = False, **kwargs):
+        if safe and not isinstance(data, dict):
+            raise TypeError('In order to allow non-dict objects to be '
+                            'serialized set the safe parameter to False')
+        kwargs.setdefault('content_type', 'application/json')
+        content = json.dumps(data, sort_keys = ordered, cls = DescriptiveJSONEncoder, ensure_ascii = False)
+        super(MyJsonOkResponse, self).__init__(content = content, **kwargs)
+
+    def json(self):
+        return json.loads(self.content, encoding = 'utf-8')
+
+AdUser = namedtuple('AdUser', ['openId', 'feature_keys', 'feature_map'])

+ 13 - 0
apps/web/ad/helpers.py

@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import base64
+
+from apilib.utils_json import json_dumps, json_loads
+
+def encode_tracker_params(params):
+    return base64.b64encode(json_dumps(params))
+
+
+def decode_tracker_params(string):
+    return json_loads(base64.b64decode(string))

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1097 - 0
apps/web/ad/models.py


+ 19 - 0
apps/web/ad/tasks.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from celery.utils.log import get_task_logger
+
+from apilib.utils_datetime import timestamp_to_dt
+from apps.web.ad.models import AdRecord
+from apps.web.core.utils import generate_excel_report
+
+logger = get_task_logger(__name__)
+
+def generate_ad_excel_report(filepath, queryAttrs):
+    #: 此任务IO耗时大,所以将其作为异步任务
+    queryAttrs['dateTimeAdded__lte'] = timestamp_to_dt(queryAttrs['dateTimeAdded__lte'])
+    queryAttrs['dateTimeAdded__gte'] = timestamp_to_dt(queryAttrs['dateTimeAdded__gte'])
+
+    records = [_.to_dict_in_cn() for _ in AdRecord.objects(**queryAttrs)]
+
+    generate_excel_report(filepath, records)

+ 73 - 0
apps/web/ad/urls.py

@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+from django.conf.urls import patterns, url
+from apps.web.ad.views import *
+
+urlpatterns = patterns('',
+                       #
+                       url(r'^getAdPreAllocatedDevice', getAdPreAllocatedDevice, name='getAdPreAllocatedDevice'),
+
+                       # 获取广告列表(查)
+                       url(r'^getAdList$', getAdList, name = 'getAdList'),
+
+                       #: 获取广告分配的设备列表
+                       url(r'^getDevListByAd', getDevListByAd, name='getDevListByAd'),
+
+                       # 增加广告
+                       url(r'^addAd$', addAd, name = 'addAd'),
+
+                       # 修改广告
+                       url(r'^editAd$', editAd, name = 'editAd'),
+
+                       # 删除广告
+                       url(r'^deleteAd$', deleteAd, name = 'deleteAd'),
+
+                       #: 从设备获取该设备被分配的广告列表
+                       url(r'^getDeviceAllocatedAds', getDeviceAllocatedAds, name='getDeviceAllocatedAds'),
+
+                       # 效果报表
+                       url(r'^getAdMoneyTrend$', getAdMoneyTrend, name = 'getAdMoneyTrend'),
+                       url(r'^getAdFansTrend$', getAdFansTrend, name = 'getAdFansTrend'),
+                       url(r'^getDealerList$', getDealerList, name = 'getDealerList'),
+                       url(r'^getAddressList$', getAddressList, name = 'getAddressList'),
+                       url(r'^getDevList$', getDevList, name = 'getDevList'),
+
+                       # 为蓝牙保留, 获取支付后广告链接
+                       url(r'^getPayAfterAd$', getPayAfterAd, name = 'getPayAfterAd'),
+
+                       url(r'^taskList$', taskList, name = 'taskList'),
+
+                       # 用户点击广告
+                       # url(r'^qrcodeAdClick$', qrcodeAdClick, name = 'qrcodeAdClick'),
+
+                       # 广告详细信息
+                       url(r'^getAdFansDetail$', getAdFansDetail, name = 'getAdFansDetail'),
+
+                       # 导出excel表格
+                       url(r'^exportExcel$', exportExcel, name = 'exportExcel'),
+
+                       #: 广告主登录
+                       url(r'^advertiserLogin$', advertiserLogin, name = 'advertiserLogin'),
+
+                       url(r'^changeAdvertiserPassword$', changeAdvertiserPassword, name = 'changeAdvertiserPassword'),
+
+                       #: 获取广告主信息
+                       url(r'^getAdvertiserInfo$', getAdvertiserInfo, name = 'getAdvertiserInfo'),
+
+                       #: 获取广告主信息
+                       url(r'^getDialogAd$', getDialogAd, name = 'getDialogAd'),
+
+                       url(r'^getBannerAd$', getBannerAd, name = 'getBannerAd'),
+
+                       #: 广告语管理
+                       url(r'^adwords$', getAdWords, name='getAdWords'),
+                       url(r'^adword/new$', addAdWord, name='addAdWord'),
+                       url(r'^adword/edit$', editAdword, name='editAdword'),
+                       url(r'^adword/delete$', deleteAdword, name='deleteAdword'),
+                       url(r'^user-adword$', userGetAdword, name='userGetAdword'),
+
+                       #: 阿里3.0回调地址
+                       url(r'^alipay(\w+)/callback$', alipayCallback, name='alipayCallback'),
+
+)

+ 124 - 0
apps/web/ad/utils.py

@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+from django.conf import settings
+from typing import TYPE_CHECKING
+
+from apps.web.ad.models import Advertisement, Advertiser
+from apps.web.core.mathematics import get_haversine_by_km
+
+if TYPE_CHECKING:
+    from apps.web.user.models import MyUser
+
+logger = logging.getLogger(__name__)
+
+AD_SET_KEY = 'ads'
+
+INDEX_AD_RECORD_TIMESTAMP = 'idx:ad-record:timestamp'
+
+#: (logicalCode) -> ZSET[adIds]
+def device_ad_match_set_key(logicalCode):
+    # type:(str)->str
+    return 'device:%s:ad-matched' % (logicalCode,)
+
+#: (openId, adId) -> adRecordKey
+ad_record_key = lambda openId, adId: 'user:%s:adId:%s' % (openId, adId)
+
+ad_key = lambda adId: 'ad:adId:%s' % (adId,)
+
+user_ad_converted_set_key = lambda openId: 'user:%s:ad-converted' % (openId,)
+
+user_ad_view_key = lambda openId: 'user:%s:ad-viewed' % (openId,)
+user_ad_match_key = lambda openId: 'user:%s:ad-matched' % (openId,)
+
+#: 广告关联集合,记录其与代理商,经销商,组,设备的关系
+AD_ASSOC_AGENT_UNION = 'ad-assoc:agent'
+AD_ASSOC_DEALER_UNION = 'ad-assoc:dealer'
+AD_ASSOC_GROUP_UNION = 'ad-assoc:group'
+AD_ASSOC_DEVICE_UNION = 'ad-assoc:device'
+
+AD_ASSOC = [AD_ASSOC_AGENT_UNION, AD_ASSOC_DEALER_UNION, AD_ASSOC_GROUP_UNION, AD_ASSOC_DEVICE_UNION]
+
+
+def consume_advertiser_quota(advertiserId, consumed_quota=1):
+    # type: (str, int) -> None
+    if not advertiserId:
+        logger.info('advertiserId is null, skipped consuming quota')
+        return
+    advertiser = Advertiser.objects(id=advertiserId).get()
+    updated = advertiser.update(dec__quota=consumed_quota)
+    logger.info('{0!r} updated quota({1!r}), updated={2}'.format(advertiser, consumed_quota, updated))
+    if advertiser.reload().quota == 0:
+        status_updated = Advertisement.objects(advertiserId=advertiserId).update(status=False)
+        logger.info('{0!r}\'s quota reached 0. set all his ads\' status to be False, updated={1}'
+                    .format(advertiser, status_updated))
+
+
+#: math
+def cpc_to_ecpm(views, clicks, cpc):
+    return 1000. * cpc * clicks / views
+
+
+def cpa_to_ecpm(views, actions, cpa):
+    return 1000. * cpa * actions / views
+
+
+def calc_user_device_distance(device, user):
+    # type: (dict, MyUser) -> int
+    user_location = user.locations.pop().coordinates
+    device_location = (device['lng'], device['lat'])
+    distance = get_haversine_by_km(user_location, device_location)
+    return distance
+
+
+def user_near_device(device, user):
+    # type: (dict, MyUser) -> bool
+    """
+    检测是否用户靠近设备,目前只与广告相关
+    :param device:
+    :param user:
+    :return:
+    """
+    if not settings.AD_LOCATION_LIMIT:
+        return True
+    if not len(user.locations):
+        logger.debug('no user({0!r}) location available'.format(user))
+        return False
+    else:
+        distance = calc_user_device_distance(device, user)
+        if distance > settings.MAX_DISTANCE_TO_SHOW_AD:
+            logger.debug('user({0!r}) is too far from the device(logicalCode={1})'.format(user, device['logicalCode']))
+            return False
+        else:
+            return True
+
+
+def get_available_ads_by_user(device, user):
+    # type: (dict, MyUser) -> list
+    """
+    在设备侧准备好的广告用于匹配用户的特征
+    :param device:
+    :param user:
+    :return:
+    """
+    logger.debug('getting available ads by user, device=(logicalCode=%s), user=%s'
+                 % (device['logicalCode'], repr(user)))
+    if not all([device, user]):
+        return []
+    else:
+        return []
+
+
+def user_has_available_ads(device, user):
+    # type: (dict, MyUser) -> bool
+    """
+    支付前广告
+    为了不影响业务主流程,这里catch所有的异常
+    :param device:
+    :param user:
+    :return:
+    """
+    return False
+

+ 57 - 0
apps/web/ad/validation.py

@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+import datetime
+
+from apps.web.constant import AdType
+from apps.web.core.validation import Schema, Required, Coerce, In, Boolean, Any, Url,  REMOVE_EXTRA
+
+getAdPreAllocatedDevice_query_schema = Schema(
+    {
+        Required("devCondition"): Schema([{"agentIdList": list,"dealerIdList":list,"groupIdList":list}]),
+        Required("addressTypeList"): list,
+        Required("devTypeList"): list
+    })
+
+
+advertisementSchema = Schema(
+    {
+        #: 广告基本属性
+
+        #:: 编辑时需要广告ID
+        'id': basestring,
+        Required('name') : basestring,
+        'adType': In(AdType.choices()),
+        'online': Boolean(),
+        'img': basestring,
+        'word': basestring,
+        'link': Any(Url(), None, ""),
+        'startTime': lambda _: datetime.datetime.strptime(_, "%Y-%m-%d %H:%M:%S"),
+        'endTime': lambda _: datetime.datetime.strptime(_, "%Y-%m-%d %H:%M:%S"),
+        'script': Any(basestring, None),
+        'scriptType': Any(basestring, None),
+
+        #: 过滤器
+        'devCondition': list,
+        'devTypeList': list,
+        'addressTypeList': list,
+
+        #: 接受广告投递的设备
+        'devList': list,
+
+        #:: 面向用户的广告过滤器
+        'targetSex': In([u'男', u'女', '*']),
+        'targetPhoneOS': In([[], [u'iOS'], [u'Android'],[u'iOS', u'Android']]),
+        'gateway': In([[], [u'wechat'], [u'alipay'], [u'wechat', u'alipay']]),
+
+        #: 定价
+        'price': Coerce(float),
+        'agentPrice': Coerce(float),
+        'dealerPrice': Coerce(float),
+
+        #: 配置项
+        'configs': dict,
+        'fansType': In(['person', 'official']),
+        'offlineFansNumber': int,
+        'PIN': basestring,
+
+    }, extra=REMOVE_EXTRA)

+ 837 - 0
apps/web/ad/views.py

@@ -0,0 +1,837 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+"""
+    web.ad.views
+    ~~~~~~~~~
+"""
+
+import datetime
+import itertools
+import logging
+import random
+import traceback
+
+import simplejson as json
+import xmltodict
+from bson.objectid import ObjectId
+from django.core.handlers.wsgi import WSGIRequest
+from django.views.decorators.http import require_POST, require_GET
+from mongoengine.errors import NotUniqueError
+from typing import List, Union, Optional, Iterable, cast, TYPE_CHECKING, Dict
+
+from apilib import utils_datetime
+from apilib.utils import flatten
+from apilib.utils_datetime import dt_to_timestamp, date_to_datetime_floor, date_to_datetime_ceiling
+from apilib.utils_json import JsonResponse
+from apps import serviceCache
+from apps.web.ad import OfflineTaskType, MyJsonOkResponse
+from apps.web.ad.models import Advertisement, AdRecord, Advertiser, AdWord, AliAdLog
+from apps.web.ad.utils import (get_available_ads_by_user)
+from apps.web.agent.models import Agent
+from apps.web.common.validation import PASSWORD_RE
+from apps.web.core import ROLE
+from apps.web.core.db import prepare_conditions, prepare_query
+from apps.web.core.exceptions import InvalidPermission
+from apps.web.core.models import OfflineTask
+from apps.web.core.utils import DefaultJsonErrorResponse, parse_json_payload
+from apps.web.core.utils import JsonErrorResponse, JsonOkResponse
+from apps.web.dealer.models import Dealer
+from apps.web.device.models import Device
+from apps.web.device.models import Group
+from apps.web.management.models import Manager
+from apps.web.user.models import MyUser, Redpack
+from apps.web.user.utils import RedpackBuilder
+from apps.web.utils import advertiser_login, \
+    is_advertiser, is_manager, is_advertisement
+from apps.web.utils import error_tolerate, permission_required
+from taskmanager.mediator import task_caller
+
+if TYPE_CHECKING:
+    from apps.web.common.models import UserSearchable
+
+
+logger = logging.getLogger(__name__)
+
+
+@error_tolerate(nil = DefaultJsonErrorResponse)
+@require_POST
+@permission_required(ROLE.manager, ROLE.advertiser)
+def getAdPreAllocatedDevice(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    根据多种多样的查询条件获取供分配设备列表List[Dict]
+
+    {"devCondition":[{"agentIdList":[],"dealerIdList":[],"groupIdList":[]}],"addressTypeList":[],"devTypeList":[]}
+
+    get_devices query =
+          dealerIdList => Device.objects(ownerId__in=dealerIdList)
+        | agentIdList => Device.objects(Dealer.objects(agentId__=agentIdList)
+        | groupIdList => Device.objects(groupId__in=groupIdList)
+
+    devices.filter( _ =>  _.devType in devTypeList &  _ => _.group.addressType in addressTypeList)
+
+    return [ { 'logicalCode': x,  'devType': 'y' } ]
+    TODO:
+    优化方向,
+        0. 优化解析过程
+        1. 建立query解析缓存
+        2. 运用mongodb pipeline
+        3. 本地化部分数据(将厂商ID绑定到设备端,解绑记得清除,将地址类型也绑定到设备,解绑记得清除)
+    :param request:
+    :return:
+    :rtype: JsonResponse
+    """
+    query = json.loads(request.body)
+
+    currentUser = request.user #type: cast(Union[Manager, Advertiser])
+
+
+    if is_manager(currentUser):
+        devCondition, addressTypeList, devTypeList  = query['devCondition'], query['addressTypeList'], query['devTypeList']
+    elif is_advertiser(currentUser):
+        devCondition, addressTypeList, devTypeList = currentUser.preAllocatedDeviceConditions, query['addressTypeList'], query['devTypeList']
+    else:
+        raise InvalidPermission(u'只有广告主或厂商才能查询广告投放设备')
+
+    hasDevCondition = bool(len(list(itertools.ifilter(lambda _ : _ != [], flatten(_.values() for _ in devCondition)))))
+    hasAddressTypeList = bool(len(addressTypeList))
+    hasDevTypeList = bool(len(devTypeList))
+
+    def imerge_dicts(iterable):
+        """
+        Given any number of dicts, shallow copy and merge into a new dict,
+        precedence goes to key value pairs in latter dicts.
+        """
+        result = {}
+        for dictionary in iterable:
+            result.update(dictionary)
+        return result
+
+    to_query = lambda _ : imerge_dicts( x.to_query(None) for x in prepare_conditions(_) )
+
+    def acc_or_append(q, data):
+        """
+
+        :param q: [{}]
+        :param data:
+        :return:
+        """
+        if not len(q):
+            q.append(data)
+        else:
+            for item in q:
+
+                if data == item: break
+
+                elif item['field'] == data['field'] and item['operator'] == data['operator']:
+                    item['value'] = list(set(item['value'] + data['value']))
+                    break
+                else:
+                    q.append(data)
+                    break
+
+    def parse_devCondition(conditions):
+        """
+        :param conditions: [{..conditions}]
+        :return:
+        """
+        queries = {
+            'device': [],
+            'dealer': [],
+            'group': []
+        }
+
+        for cond in conditions:
+            # 优先级 groupIdList > dealerIdList > agentIdList
+            if cond['groupIdList']:
+                acc_or_append(queries['group'],  {"field": "_id", "operator": "in", "value": [ ObjectId(_) for _ in cond['groupIdList']] })
+                continue
+            elif cond['dealerIdList']:
+                acc_or_append(queries['group'], {"field": "ownerId", "operator": "in", "value": map(str, cond['dealerIdList']) })
+                continue
+            elif cond['agentIdList']:
+                acc_or_append(queries['dealer'], {"field": "agentId", "operator": "in", "value": cond['agentIdList']})
+                continue
+
+        return queries
+
+    #: 生成,拼装请求,延迟请求Device到最后, 减少IO
+    queries = parse_devCondition(devCondition)
+
+    #: 默认只能选择某厂商下的所有设备
+    #acc_or_append(queries['device'],{"field": "managerId", "operator": "", "value": managerId})
+
+    #: 不选择的时候,选择所有组的设备
+    if not hasDevCondition:
+
+        if is_manager(currentUser):
+            agentIds = [ str(_['_id']) for _  in Agent.get_collection().find({'managerId': str(currentUser.id)})]
+            acc_or_append(queries['dealer'], {"field": "agentId", "operator": "in", "value": agentIds})
+
+        elif is_advertiser(currentUser):
+            agentIds = [ str(_.id) for _  in Agent.objects(managerId=currentUser.managerId)  ]
+            acc_or_append(queries['dealer'], {"field": "agentId", "operator": "in", "value": agentIds})
+
+
+    if hasDevTypeList:
+        acc_or_append(queries['device'], {"field": "devType.id", "operator": "in", "value": devTypeList})
+
+    if hasAddressTypeList:
+        acc_or_append(queries['group'], {"field": "addressType", "operator": "in", "value": addressTypeList})
+
+    if queries['dealer']:
+        dealers = [ str(_['_id']) for _ in Dealer.get_collection().find(to_query(queries['dealer']), {'_id': 1}) ]
+        acc_or_append(queries['group'], {"field": "ownerId", "operator": "in", "value": dealers})
+
+    if queries['group']:
+        groups = [ str(_['_id']) for _ in Group.get_collection().find(to_query(queries['group']), {'_id': 1}) ]
+        acc_or_append(queries['device'], {"field": "groupId", "operator": "in", "value": groups})
+
+    if queries['device']:
+
+        dataList = [ {'logicalCode': _['logicalCode'], 'devTypeName' : _['devType'].get('name')} for _ in
+                        Device.get_collection()
+                              .find(to_query(queries['device']), {'logicalCode': 1, 'devType.name': 1})]
+
+        return JsonResponse({'result': 1, 'payload': {'dataList': dataList}})
+
+    return JsonResponse({'result': 1, 'payload': {'dataList': []}})
+
+
+###: 广告增删查改
+
+@require_GET
+@error_tolerate(nil = DefaultJsonErrorResponse)
+@permission_required(ROLE.manager, ROLE.advertisement, ROLE.advertiser)
+def getAdList(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    获取广告列表
+    :param request:
+    :return:
+    """
+
+    return JsonResponse({'result': 1, 'description': '', 'payload': {'total': 0, 'dataList': []}})
+
+
+@require_GET
+@error_tolerate(nil=JsonErrorResponse(u'获取广告分配设备列表失败'))
+@permission_required(ROLE.manager, ROLE.advertiser)
+def getDevListByAd(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    为了加快广告加载速度,不默认返回设备列表,只有在用户点击广告的时候才会请求设备列表
+    :param request:
+    :return:
+    """
+    adId = request.GET.get('adId')
+    devList = Advertisement.get_by_adId(adId=adId).devList
+    return JsonResponse({'result': 1, 'description': '', 'payload': {'total': len(devList), 'dataList': devList }})
+
+
+@require_POST
+@error_tolerate(nil = JsonErrorResponse(u'添加广告失败'))
+@permission_required(ROLE.manager, ROLE.advertiser)
+def addAd(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    添加广告
+    广告主和厂商都可以添加广告
+    广告主只可配置 publicAdAvailable的广告主
+    :param request:
+    :return:
+    """
+    return JsonResponse({'result': 1, 'description': '', 'payload': {}})
+
+@require_POST
+@error_tolerate(nil = JsonErrorResponse(u'编辑广告失败'))
+@permission_required(ROLE.manager, ROLE.advertiser)
+def editAd(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    编辑广告
+    :param request:
+    :return:
+    """
+    return JsonResponse({'result': 1, 'description': '', 'payload': {}})
+
+
+@require_POST
+@error_tolerate(nil = JsonErrorResponse(u'删除广告失败'))
+@permission_required(ROLE.manager, ROLE.advertiser)
+def deleteAd(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    删除广告
+    :param request:
+    :return:
+    """
+    return JsonResponse({'result': 1, 'description': '', 'payload': {}})
+
+
+###: 各种过滤器
+
+@error_tolerate(nil = JsonErrorResponse(u'获取经销商列表失败'))
+@permission_required(ROLE.manager)
+def getDealerList(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    获取经销商列表
+    :param request:
+    :return:
+    """
+    agentId = request.GET.get('agentId', None)
+    if agentId is None:
+        return JsonResponse({'result': 0, 'description': u'请传入正确的参数', 'payload': {}})
+    dealers = [{'value': str(dealer.id), 'label': dealer.nickname} for dealer in Dealer.objects(agentId = str(agentId))]
+    return JsonResponse({'result': 1, 'description': '', 'payload': dealers})
+
+
+@error_tolerate(nil = JsonErrorResponse(u'获取地址列表失败'))
+@permission_required(ROLE.manager)
+def getAddressList(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    dealer->group.address
+    :param request:
+    :return:
+    """
+    dealerId = request.GET.get('id')
+    groupIds = Group.get_group_ids_of_dealer(dealerId)
+    groups = Group.get_groups_by_group_ids(groupIds)
+    if groups is None:
+        return JsonResponse({'result': 1, 'description': '', 'payload': []})
+    else:
+        groupIdAddressPair = [{'value': k, 'label': v['address']} for k, v in groups.iteritems()]
+        return JsonResponse({'result': 1, 'description': '', 'payload': groupIdAddressPair})
+
+
+@error_tolerate(nil = JsonErrorResponse(u'获取设备列表失败'))
+@permission_required(ROLE.manager)
+def getDevList(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    广告系统获取设备列表
+    :param request:
+    :return:
+    """
+    groupId = request.GET.get('id')
+    deviceNos = Device.get_devNos_by_group([groupId])
+    devs = Device.get_dev_by_nos(deviceNos)
+    return JsonResponse(
+        {
+            'result': 1,
+            'description': '',
+            'payload':
+                [
+                    {'value': deviceNo, 'label': dev['logicalCode']}
+                    for deviceNo, dev in devs.items()
+                ] if deviceNos else []
+        }
+    )
+
+
+### 查看设备被分配的广告列表
+@error_tolerate(nil = JsonErrorResponse(u'查看设备广告列表失败'))
+@permission_required(ROLE.manager)
+def getDeviceAllocatedAds(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+
+    :param request:
+    :return:
+    """
+    logicalCode = request.GET.get('logicalCode')
+    return JsonResponse({'result': 1, 'description': '', 'payload': {'total': 0, 'dataList': []}})
+
+
+### 广告数据报表
+@error_tolerate(nil = JsonErrorResponse(u'获取趋势列表失败'))
+@permission_required(ROLE.manager, ROLE.advertisement, ROLE.advertiser)
+def getAdFansTrend(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    获取广告效果趋势
+    :param request:
+    :return:
+    """
+
+    currentUser = request.user  # type: cast(Union[Advertisement, Advertiser, Manager])
+
+    query = prepare_query(request.GET)
+    queryDict = query.raw
+
+    delta = query.attrs['dateTimeAdded__lte'] - query.attrs['dateTimeAdded__gte']  # type: datetime.timedelta
+    if delta.days > 90:
+        return JsonErrorResponse(u'只支持查询最远至90天前的数据')
+
+    #: TODO to refactor 引入校验schema
+    if 'adId' in queryDict:
+        queryDict['adId'] = int(queryDict['adId'])
+
+    if is_manager(currentUser):
+        queryDict['managerId'] = str(currentUser.id)
+    elif is_advertiser(currentUser):
+        queryDict['advertiserId'] = str(currentUser.id)
+    elif is_advertisement(currentUser):
+        queryDict['adId'] = int(currentUser.adId)
+
+    matchStage = {
+        'createdDate':
+            {
+                '$gte': queryDict.pop('startTime'),
+                '$lte': queryDict.pop('endTime')
+            },
+    }
+    matchStage.update(queryDict)
+
+    projectStage = {
+        '_id': 0,
+        'clicks': 1,
+        'createdDate': 1,
+        'fans': {'$cond': ['$converted', 1, 0]},
+        'maleFans': '$features.sex.is_male',
+        'femaleFans': '$features.sex.is_female'
+    }
+
+    projectStage.update(dict.fromkeys(AdRecord.filter_fields(), 1))
+
+    groupStage = {
+        '_id': '$createdDate',
+        'clicks': {'$sum': '$clicks'},
+        'fans': {'$sum': '$fans'},
+        'show': {'$sum': '$clicks'},
+        'maleFans': {'$sum': '$maleFans'},
+        'femaleFans': {'$sum': '$femaleFans'}
+    }
+
+    aggregates = AdRecord.get_collection().aggregate(
+        [
+            {'$project': projectStage},
+            {'$match': matchStage},
+            {'$group': groupStage},
+            {'$addFields': {'time': '$_id'}},
+            {'$sort': {'time': 1}},
+        ]
+    )
+
+    return JsonResponse({'result': 1, 'description': '', 'payload': list(aggregates)})
+
+
+@permission_required(ROLE.manager)
+def getAdMoneyTrend(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    收益趋势
+    :param request:
+    :return:
+    """
+    # TODO 待讨论
+    return JsonResponse({'result': 1, 'description': '', 'payload': []})
+
+
+@error_tolerate(nil = DefaultJsonErrorResponse)
+@permission_required(ROLE.manager, ROLE.advertisement, ROLE.advertiser)
+def getAdFansDetail(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    获取广告效果详情列表
+    :param request:
+    :return:
+    """
+
+    query = prepare_query(request.GET)
+
+    currentUser = request.user # type: cast(Union[Advertisement, Advertiser, Manager])
+
+    if is_manager(currentUser):
+        query.attrs['managerId'] = str(currentUser.id)
+    elif is_advertiser(currentUser):
+        query.attrs['advertiserId'] = str(currentUser.id)
+    elif is_advertisement(currentUser):
+        query.attrs['adId'] = int(currentUser.adId)
+
+    cursor = AdRecord.objects(converted=True, **query.attrs)
+    total = cursor.count()
+
+    pagedRecords = cursor.order_by('-dateTimeAdded').paginate(pageIndex=query.pageIndex, pageSize=query.pageSize) # type: List[AdRecord]
+    records = [ r.to_dict() for r in pagedRecords ]
+
+    return JsonResponse({'result': 1, 'description': None, 'data': {'total': total, 'dataList': records}})
+
+
+@error_tolerate(nil=DefaultJsonErrorResponse)
+def getPayAfterAd(request):
+    # type:(WSGIRequest)->JsonResponse
+    """
+    获取支付后广告链接(蓝牙payAfter页面)
+    :param request:
+    :return:
+    """
+
+    logicalCode = request.GET.get('logicalCode', None)
+    if not logicalCode:
+        logger.debug('no logical code.')
+        return JsonOkResponse(description='no ads available')
+
+    device = Device.get_dev_by_logicalCode(logicalCode)
+    if not device:
+        logger.debug('no device find.')
+        return JsonOkResponse(description='no ads available')
+
+    ua = request.META.get('HTTP_USER_AGENT', '')
+
+    user = request.user  # type: MyUser
+
+    ad_dict = Advertisement.fetch_payafter_ad(ua, user, device.owner, device)
+
+    logger.debug('ad dict is {}'.format(ad_dict))
+
+    if not ad_dict:
+        return JsonOkResponse(description='no ads available')
+    else:
+        return JsonResponse({'result': 1, 'description': u'', 'payload': ad_dict})
+
+
+def taskList(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    广告适配系统
+    :param request:
+    :return:
+    """
+    try:
+        user = request.user # type: cast(MyUser)
+        devNo = request.GET.get('devNo')
+
+        if not devNo:
+            return JsonResponse({'result': 0, 'description': u'请首先扫码', 'payload': {}})
+
+        device = Device.get_dev(devNo)
+        if not device:
+            return JsonResponse({'result': 0, 'description': u'设备不存在', 'payload': {}})
+
+        ads = get_available_ads_by_user(device, user)
+        logger.debug('%s got ads(%s) on device(logicalCode=%s)' % (repr(user), ads, device['logicalCode']))
+
+        allocated_ads = [{'id': ad.adId, 'name': ad.name, 'word': ad.word, 'qrcode': ad.img} for ad in ads]
+        random.shuffle(allocated_ads)
+        return JsonResponse({'result': 1, 'description': None, 'payload': {'taskList': allocated_ads}})
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({'result': 1, 'description': None, 'payload': {'taskList': []}})
+
+
+### 广告主
+@error_tolerate(nil = JsonErrorResponse(u'注册失败'))
+def registerAdvertiser(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    注册成为广告主
+    :param request:
+    :return:
+    """
+    payload = json.loads(request.body)
+    try:
+        Advertiser.create_user(**payload)
+    except NotUniqueError:
+        return JsonErrorResponse(description = u'该手机号已注册')
+    return JsonResponse({'result': 1, 'description': None, 'payload': {}})
+
+
+@error_tolerate(nil = JsonErrorResponse(u'登录失败'))
+def advertiserLogin(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    广告主登录
+    :param request:
+    :return:
+    """
+    payload = json.loads(request.body)
+    username, password = payload['username'], payload['password']
+    return advertiser_login(request, logger, username, password)
+
+
+@error_tolerate(nil = JsonErrorResponse(u'修改密码失败'))
+@permission_required(ROLE.advertiser)
+def changeAdvertiserPassword(request):
+    # type: (WSGIRequest)->JsonResponse
+
+    currentAdvertiser = request.user # type: cast(Advertiser)
+    payload = json.loads(request.body)
+    password = payload['password']
+    if PASSWORD_RE.match(password) is None:
+        return  JsonErrorResponse(description=u'密码必须大于8位,只能是数字或字母或常见特殊符号')
+    currentAdvertiser.set_password(payload['password'])
+    return JsonResponse({'result': 1})
+
+
+@error_tolerate(nil = JsonErrorResponse(u'获取广告主信息失败'))
+@permission_required(ROLE.advertiser)
+def getAdvertiserInfo(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    获取广告主信息
+    :param request:
+    :return:
+    """
+    user = request.user # type: cast(Advertiser)
+    return JsonResponse(
+        {
+            'result': 1,
+            'description': '',
+            'payload': {'nickname': user.nickname, 'brandName': '', 'quota': user.quota, 'role': ROLE.advertiser }
+        }
+    )
+
+
+### others
+@error_tolerate(nil = JsonErrorResponse(u'生成报表失败'))
+@permission_required(ROLE.manager)
+def exportExcel(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    """
+    广告效果报表生成excel
+    :param request:
+    :return:
+    """
+    manager = request.user  # type: cast(Manager)
+
+    def get_offline_task_name(task_type, user, **kwargs):
+        # type: (basestring, UserSearchable, Dict)->basestring
+
+        tmp_list = [task_type, user.username]
+
+        if 'adId' in kwargs and kwargs['adId']:
+            tmp_list.append(u'广告_%s' % (kwargs['adId'],))
+        elif 'devNo' in kwargs:
+            logicalCode = Device.get_logicalCode_by_devNo(kwargs['devNo'])
+            tmp_list.append(u'设备号_%s' % (logicalCode,))
+        elif 'groupId' in kwargs:
+            address = Group.get_group(kwargs['groupId']).get('address', '')
+            tmp_list.append(u'地址_%s' % (address,))
+        elif 'dealerId' in kwargs:
+            dealerName = Dealer.objects(id = str(kwargs['dealerId'])).get().nickname
+            tmp_list.append(u'经销商_%s' % (dealerName,))
+        elif 'agentId' in kwargs:
+            agentName = Agent.objects(id = str(kwargs['agentId'])).get().nickname
+            tmp_list.append(u'代理商_%s' % (agentName,))
+
+        tmp_list.append(u'%s至%s' % (kwargs['startTime'], kwargs['endTime']))
+        tmp_list.append(str(utils_datetime.generate_timestamp_ex()))
+
+        return '-'.join(tmp_list).replace("/", "_")
+
+    query = prepare_query(request.GET)
+
+    offline_task_name = get_offline_task_name(task_type = OfflineTaskType.AD_REPORT,
+                                              user = manager,
+                                              **(query.raw))
+
+    file_path, offline_task = OfflineTask.issue_export_report(offline_task_name = offline_task_name,
+                                                              process_func_name = 'generate_ad_excel_report',
+                                                              task_type = OfflineTaskType.AD_REPORT,
+                                                              userid = str(manager.id),
+                                                              role = ROLE.manager)
+
+    #: make queryAttrs serializable
+    queryAttrs = query.attrs
+    queryAttrs['dateTimeAdded__lte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__lte'])
+    queryAttrs['dateTimeAdded__gte'] = dt_to_timestamp(queryAttrs['dateTimeAdded__gte'])
+
+    task_caller(func_name = offline_task.process_func_name,
+                offline_task_id = str(offline_task.id),
+                filepath = file_path,
+                queryAttrs = queryAttrs)
+
+    return JsonResponse({
+        'result': 1,
+        'description': u"请前往离线任务查看任务处理情况",
+        'payload': str(offline_task.id)})
+
+
+def getDialogAd(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    # TODO 支付前广告,待补充
+    return JsonResponse({
+        'result': 1,
+        'description': '',
+        'payload': {'dataList': []}})
+
+
+def getBannerAd(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    logicalCode = request.GET.get('logicalCode')
+    dev = Device.get_dev_by_logicalCode(logicalCode)
+
+    dealer = Dealer.objects(id=str(dev['ownerId'])).only('agentId').get()  # type: Dealer
+    agent = Agent.objects(id=str(dealer.agentId)).only('managerId').get()  # type: Agent
+
+    ad = Advertisement.filter_by_top_show(managerId=agent.managerId).first()  # type: Optional[Advertisement]
+
+    if not ad:
+        return JsonOkResponse(payload={})
+    else:
+        return JsonOkResponse(payload={'link': ad.link, 'img': ad.img})
+
+
+@permission_required(ROLE.supermanager)
+@error_tolerate(nil = DefaultJsonErrorResponse)
+def getAdWords(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    pageIndex = int(request.GET.get('pageIndex', 1))
+    pageSize = int(request.GET.get('pageSize', 10))
+    searchKey = request.GET.get('searchKey')
+    startTime = request.GET.get('startTime')
+    endTime = request.GET.get('endTime')
+
+    query = {}
+
+    if startTime is not None and endTime is not None:
+        query['dateTimeAdded__lte'] = date_to_datetime_ceiling(endTime)
+        query['dateTimeAdded__gte'] = date_to_datetime_floor(startTime)
+
+    total = AdWord.objects(**query).search(searchKey).count()
+    qs = AdWord.objects(**query).search(searchKey).paginate(pageIndex=pageIndex, pageSize=pageSize) # type: Iterable[AdWord]
+
+    return JsonOkResponse(payload={'dataList': [ _.to_dict() for _ in qs ], 'total': total})
+
+
+@permission_required(ROLE.supermanager)
+@error_tolerate(nil = DefaultJsonErrorResponse)
+def addAdWord(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    payload = parse_json_payload(request.body)
+    AdWord(**payload).save()
+    return JsonOkResponse()
+
+
+@permission_required(ROLE.supermanager)
+@error_tolerate(nil = DefaultJsonErrorResponse)
+def editAdword(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    payload = parse_json_payload(request.body)
+    ad_word = AdWord.objects(id=payload['id']).get()
+    updated = ad_word.update(**payload)
+    if updated:
+        return JsonOkResponse(payload={'description': u'更新成功'})
+    else:
+        return JsonErrorResponse(payload={'description': u'更新失败'})
+
+
+@permission_required(ROLE.supermanager)
+@error_tolerate(nil = DefaultJsonErrorResponse)
+def deleteAdword(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    payload = json.loads(request.body)
+
+    ids = payload.get('ids')
+
+    if not ids:
+        return JsonErrorResponse(description=u'')
+
+    AdWord.objects(id__in=ids).delete() # type: AdWord
+
+    return JsonOkResponse()
+
+
+@error_tolerate(nil = DefaultJsonErrorResponse)
+def userGetAdword(request):
+    # type:(WSGIRequest)->JsonResponse
+
+    try:
+        user = request.user  # type: cast(MyUser)
+
+        ad_word = AdWord.get_random_one()  # type: AdWord
+
+        if ad_word is not None:
+            ad_word.incr_count(1)
+
+            title = ad_word.title
+
+            if not ad_word.autoPass:
+                return MyJsonOkResponse({'result': 1, 'description': '', 'payload': {'content': ad_word.promotionUrl}})
+
+            tbkCode = serviceCache.get('tbk_{}'.format(str(ad_word.id)))
+            if not tbkCode:
+                from library.aliopen.tbkapi import CreatTaokoulingReq
+
+                request = CreatTaokoulingReq(appKey = '33175347', appSecret = 'c4e2d8426d1d08ec49ef00bc40ac94d5')
+                request.setParams(url = ad_word.promotionUrl)
+                rv = xmltodict.parse(request.getResponse().content)
+
+                logger.debug('tbk code rv for {} is: {}'.format(str(ad_word.id), str(rv)))
+
+                tokens = rv['tbk_tpwd_create_response']['data']['model'].split(' ')
+                tbkCode = '{} {} {}'.format(tokens[0], tokens[1], title)
+
+                logger.debug('new tbk code is: {}'.format(tbkCode))
+
+                serviceCache.set('tbk_{}'.format(str(ad_word.id)), tbkCode, 60 * 60 * 2)
+
+                return MyJsonOkResponse({'result': 1, 'description': '', 'payload': {'content': tbkCode}})
+            else:
+                logger.debug('cache tbk code is: {}'.format(tbkCode))
+
+                return MyJsonOkResponse({'result': 1, 'description': '', 'payload': {'content': tbkCode}})
+    except Exception as e:
+        logger.error('exception = {}; id = {}'.format(traceback.format_exc(), str(ad_word.id)))
+
+    return MyJsonOkResponse({'result': 1, 'description': '', 'payload': {'content': ''}})
+
+
+@error_tolerate(nil = DefaultJsonErrorResponse)
+def alipayCallback(request, uidType):
+    # type:(WSGIRequest, str)->JsonResponse
+
+    try:
+        uidType = uidType.upper()
+
+        payload = json.loads(request.body)
+
+        if uidType == 'CPM':
+            # uid = payload.get('uid')
+            # bid = payload.get('bid')
+            # try:
+            #     AdLog.objects.get(openId=uid, bidid=bid, status__ne='finished').update(status='finished', finishedData=request.body, finishedTime=datetime.datetime.now())
+            # except:
+            #     logger.info(traceback.format_exc())
+            pass
+
+        elif uidType == 'RH':
+
+            uid = payload.get('uid')
+            urlId = payload.get('ext', {}).get('id')
+
+            redpack = Redpack.take_effect(openId = uid, urlId = urlId, **{'RHCallback': request.body})
+            if redpack:
+                RedpackBuilder._set_alipay_key(uid, redpack.factoryCode, urlId, redpack.money.mongo_amount,
+                                               showType = redpack.showType)
+
+            return JsonResponse({"status": 1, "alipayOrderId": urlId})
+
+        elif uidType == 'LX':
+            pass
+
+        try:
+            AliAdLog.objects(campaignId = payload.get('campaignId'), bid = payload.get('bid')).upsert_one(
+                uid = payload.get('uid'), uidType = payload.get('uidtype'), campaignId = payload.get('campaignId'),
+                bid = payload.get('bid'), tradeTS = payload.get('tradetimestamp'), commission = payload.get('commission'))
+        except Exception as e:
+            logger.error(e)
+    finally:
+        return JsonResponse({'success': True, 'errorMsg': '', 'result': True})

+ 2 - 0
apps/web/agent/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python

+ 185 - 0
apps/web/agent/api.py

@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import base64
+import datetime
+
+from apps import serviceCache
+from apps.web.agent.models import Agent
+from apps.web.core.models import AliApp, WechatPayApp, WechatUserManagerApp, WechatManagerApp, \
+    WechatUserSubscribeManagerApp, WechatDealerSubscribeManagerApp
+from apps.web.core.utils import JsonErrorResponse, JsonOkResponse
+from apps.web.core.validation import wechatManagerAppSchema, MultipleInvalid
+
+
+def bindAliApp(payload):
+    def alipay_public_key(text):
+        return '-----BEGIN PUBLIC KEY-----\n' + text + '\n-----END PUBLIC KEY-----'
+
+    agent = Agent.objects(id = str(payload['id'])).get()  # type: Agent
+
+    customizedAlipayCashflowAllowable = bool(payload.pop('customizedAlipayCashflowAllowable', False))
+    if customizedAlipayCashflowAllowable and not agent.customizedWechatCashflowAllowable:
+        return JsonErrorResponse(description = u'自主支付宝开启必须首先开启微信支付(提现必须通过微信)')
+
+    if customizedAlipayCashflowAllowable:
+        params = payload['aliPayApp']
+
+        if 'appid' not in params or not params['appid']:
+            return JsonErrorResponse(description = u'支付宝支付定制情况下支付公众号不能为空')
+
+        ca_type = params.get('signKeyType', 'normal')
+        app_name = params.get('appName', '')
+        company_name = params.get('companyName', '')
+        shadow = params.get('shadow', False)
+
+        new_flag = False
+
+        
+        current_app = AliApp.objects(appid = params['appid']).first()  # type: AliApp
+
+        if not current_app:
+            new_flag = True
+            current_app = AliApp(appid = params['appid']).save()
+
+        current_app.companyName = company_name
+        current_app.appName = app_name
+        current_app.signKeyType = ca_type
+        current_app.shadow = params.get('shadow')
+        current_app.dateTimeUpdated = datetime.datetime.now()
+
+        if ca_type == 'normal':
+            if 'alipayPublicKeyContent' in params and params['alipayPublicKeyContent']:
+                if current_app.alipayPublicKeyContent != params['alipayPublicKeyContent']:
+                    current_app.alipayPublicKeyContent = params['alipayPublicKeyContent']
+
+                alipay_public_key_string = alipay_public_key(current_app.alipayPublicKeyContent)
+                current_app.alipayPublicKey = alipay_public_key_string
+
+            if 'appPublicKeyContent' in params and params['appPublicKeyContent']:
+                current_app.appPublicKeyContent = params['appPublicKeyContent']
+
+            if 'app_private_key_path' in params and params['app_private_key_path']:
+                app_private_key_string = serviceCache.get(params['app_private_key_path'])
+                if not app_private_key_string:
+                    return JsonErrorResponse(description = u'私钥文件不存在,请重新生成')
+
+                current_app.appPrivateKey = app_private_key_string
+
+            if not current_app.appPrivateKey:
+                return JsonErrorResponse(description = u'私钥不能为空')
+
+            if not current_app.alipayPublicKey:
+                return JsonErrorResponse(description = u'支付宝公钥不能为空')
+        else:
+            if 'app_publickey_cert' in params and params['app_publickey_cert']:
+                app_publickey_cert_string = params['app_publickey_cert']
+
+                if not app_publickey_cert_string:
+                    return JsonErrorResponse(description = u'应用公钥证书不能为空')
+
+                current_app.appPublicKeyCert = app_publickey_cert_string
+
+            if 'publickey_cert' in params and params['publickey_cert']:
+                publickey_cert_string = params['publickey_cert']
+
+                if not publickey_cert_string:
+                    return JsonErrorResponse(description = u'支付宝公钥证书不能为空')
+
+                current_app.publicKeyCert = publickey_cert_string
+
+            if 'root_cert' in params and params['root_cert']:
+                root_cert_string = params['root_cert']
+
+                if not root_cert_string:
+                    return JsonErrorResponse(description = u'支付宝根证书不能为空')
+
+                current_app.rootCert = root_cert_string
+
+            if not current_app.appPublicKeyCert:
+                return JsonErrorResponse(description = u'应用公钥证书不能为空')
+
+            if not current_app.publicKeyCert:
+                return JsonErrorResponse(description = u'支付宝公钥证书不能为空')
+
+            if not current_app.rootCert:
+                return JsonErrorResponse(description = u'支付宝根证书不能为空')
+
+        current_app.save()
+
+        agent.payAppAli = current_app
+        agent.customizedAlipayCashflowAllowable = True
+        agent.save()
+    else:
+        agent.customizedAlipayCashflowAllowable = False
+        agent.save()
+
+    return JsonOkResponse(payload = {})
+
+
+def bindWechatFund(payload):
+    agent = Agent.objects(id = str(payload['id'])).get()  # type: Agent
+    customizedWechatCashflowAllowable = bool(payload.get('customizedWechatCashflowAllowable', False))
+    if customizedWechatCashflowAllowable:
+        params = payload['wechatPayApp']
+
+        if 'appid' not in params or 'mchid' not in params:
+            return JsonErrorResponse(description = u'微信支付定制情况下支付公众号不能为空')
+
+        if 'ssl_cert' not in params:
+            return JsonErrorResponse(description = u'证书不能为空')
+
+        if 'ssl_key' not in params:
+            return JsonErrorResponse(description = u'SSL公钥不能为空')
+
+        params['sslCert'] = base64.b64decode(params.pop('ssl_cert'))
+        params['sslKey'] = base64.b64decode(params.pop('ssl_key'))
+
+        params['dateTimeUpdated'] = datetime.datetime.now()
+
+        agent.payAppWechat = WechatPayApp.objects(appid = params['appid'], mchid = params['mchid']).upsert_one(
+            **params)
+        agent.customizedWechatCashflowAllowable = True
+        agent.save()
+    else:
+        agent.customizedWechatCashflowAllowable = False
+        agent.save()
+    return JsonOkResponse(payload = {})
+
+
+def bindWechatCust(payload):
+    try:
+        agent = Agent.objects(id = str(payload['id'])).get()  # type: Agent
+
+        user_allowable = bool(payload.pop('customizedUserGzhAllowable', False))
+        payload.update({'customizedUserGzhAllowable': user_allowable})
+        if user_allowable:
+            user_app_dict = wechatManagerAppSchema(payload.pop('user', None))
+            payload.update({'wechatUserManagerialApp': WechatUserManagerApp.create(**user_app_dict)})
+
+        dealer_allowable = bool(payload.pop('customizedDealerGzhAllowable', False))
+        payload.update({'customizedDealerGzhAllowable': dealer_allowable})
+        if dealer_allowable:
+            dealer_app_dict = wechatManagerAppSchema(payload.pop('dealer', None))
+            payload.update({'wechatDealerManagerialApp': WechatManagerApp.create(**dealer_app_dict)})
+
+        user_sub_allowable = bool(payload.pop('customizedUserSubGzhAllowable', False))
+        payload.update({'customizedUserSubGzhAllowable': user_sub_allowable})
+        if user_sub_allowable:
+            user_sub_app_dict = wechatManagerAppSchema(payload.pop('user_sub', None))
+            payload.update({'wechatUserSubscribeManagerApp': WechatUserSubscribeManagerApp.create(**user_sub_app_dict)})
+
+        dealer_sub_allowable = bool(payload.pop('customizedDealerSubGzhAllowable', False))
+        payload.update({'customizedDealerSubGzhAllowable': dealer_sub_allowable})
+        if dealer_sub_allowable:
+            dealer_sub_app_dict = wechatManagerAppSchema(payload.pop('dealer_sub', None))
+            payload.update({'wechatDealerSubscribeManagerApp': WechatDealerSubscribeManagerApp.create(**dealer_sub_app_dict)})
+
+
+        updated = agent.update(**payload)
+        if updated:
+            return JsonOkResponse()
+        else:
+            return JsonErrorResponse(description = u'绑定自定义公众号失败')
+    except MultipleInvalid as e:
+        return JsonErrorResponse(description = u'信息填写有误,请检查')

+ 96 - 0
apps/web/agent/define.py

@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from apilib.constants import _Constant
+from apilib.systypes import IterConstant
+from apps.web.utils import concat_server_end_url
+from apps.web.constant import USER_RECHARGE_TYPE
+
+
+AgentConst = _Constant()
+
+
+class AGENT_INCOME_SOURCE(IterConstant):
+    """
+    代理商收益来源分类 展示在前端的字段
+    """
+    AD = 'ad'
+    DEALER_WITHDRAW_FEE = 'dealer_withdraw_fee'
+    DEALER_CARD_FEE = 'dealer_card_fee'
+    DEALER_API_QUOTA = 'dealer_api_quota'
+    DEALER_DISABLE_AD = 'dealer_disable_ad'
+    DEALER_DEVICE_FEE = 'dealer_device_fee'
+    INSURANCE = 'insurance'
+    REDPACK = 'redpack'
+    REFUND_CASH = 'refundCash'
+
+
+# 代理商的收益来源的翻译(前台展示使用字段的名称)
+# 暂时可能没用上 代理商的前端翻译好像是固定写死的 先保持和经销商的结构统一 便于理解
+AGENT_INCOME_SOURCE_TRANSLATION = \
+    {
+        AGENT_INCOME_SOURCE.AD: u'广告收益',
+        AGENT_INCOME_SOURCE.DEALER_WITHDRAW_FEE: u'提现收益',
+        AGENT_INCOME_SOURCE.DEALER_CARD_FEE: u"流量收益",
+        AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE: u"设备收益",
+        AGENT_INCOME_SOURCE.INSURANCE: u'保险收益',
+        AGENT_INCOME_SOURCE.REDPACK: u'第三方红包',
+
+        AGENT_INCOME_SOURCE.DEALER_DISABLE_AD: u'纯净计划',
+
+        AGENT_INCOME_SOURCE.REFUND_CASH: u'现金退款'
+    }
+
+
+# TYPE和数据库字段一致
+class AGENT_INCOME_TYPE(IterConstant):
+    AD = 'ad'
+    DEALER_DEVICE_FEE = 'device'
+    INSURANCE = "insurance"
+    # 代理商比经销商要多的两种收益方式  提现收益(资金池)流量卡收益
+    DEALER_WITHDRAW_FEE = 'withdraw'
+    DEALER_CARD_FEE = 'traffic'
+    DEALER_API_QUOTA = 'apiQuate'
+    DEALER_DISABLE_AD = 'disableAd'
+
+
+# 代理商的统计维度和经销商不通 对于用户的订单来说 代理商统统算入设备收益大类
+AgentConst.MAP_USER_SOURCE_TO_DEALER_SOURCE = {
+    USER_RECHARGE_TYPE.RECHARGE: AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE,
+    USER_RECHARGE_TYPE.RECHARGE_CARD: AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE,
+    USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD: AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE,
+    USER_RECHARGE_TYPE.RECHARGE_CASH: AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE,
+    USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE: AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE,
+    USER_RECHARGE_TYPE.RECHARGE_REDPACK: AGENT_INCOME_SOURCE.REDPACK,
+
+    USER_RECHARGE_TYPE.RECHARGE_INSURANCE: AGENT_INCOME_SOURCE.INSURANCE,
+    USER_RECHARGE_TYPE.REFUND_CASH: AGENT_INCOME_SOURCE.REFUND_CASH
+}
+
+
+AgentConst.MAP_SOURCE_TO_TYPE = {
+    AGENT_INCOME_SOURCE.AD: AGENT_INCOME_TYPE.AD,
+    AGENT_INCOME_SOURCE.DEALER_WITHDRAW_FEE: AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE,
+    AGENT_INCOME_SOURCE.DEALER_CARD_FEE: AGENT_INCOME_TYPE.DEALER_CARD_FEE,
+    AGENT_INCOME_SOURCE.DEALER_API_QUOTA: AGENT_INCOME_TYPE.DEALER_API_QUOTA,
+    AGENT_INCOME_SOURCE.DEALER_DEVICE_FEE: AGENT_INCOME_TYPE.DEALER_DEVICE_FEE,
+
+    AGENT_INCOME_SOURCE.INSURANCE: AGENT_INCOME_TYPE.INSURANCE,
+
+    AGENT_INCOME_SOURCE.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 = {
+    AGENT_INCOME_TYPE.AD: 'adBalance',
+    AGENT_INCOME_TYPE.DEALER_DEVICE_FEE: 'deviceBalance',
+    AGENT_INCOME_TYPE.DEALER_WITHDRAW_FEE: 'withdrawBalance',
+    AGENT_INCOME_TYPE.DEALER_CARD_FEE: 'trafficBalance',
+    AGENT_INCOME_TYPE.INSURANCE: 'insuranceBalance',
+    AGENT_INCOME_TYPE.DEALER_API_QUOTA: 'apiQuotaBalance',
+    AGENT_INCOME_TYPE.DEALER_DISABLE_AD: 'disableAdBalance'
+}
+
+AGENT_BIND_WECHAT_URL = concat_server_end_url(uri='/agent/wechat/bind')

+ 10 - 0
apps/web/agent/errors.py

@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+
+from apps.web.exceptions import UserServerException
+
+
+class PrimaryAgentDoesNotExist(UserServerException):
+    """
+       找不到主代理商
+    """

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1710 - 0
apps/web/agent/models.py


+ 139 - 0
apps/web/agent/proxy.py

@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+from typing import Any
+
+from apilib.monetary import RMB
+from apilib.utils_json import json_dumps
+from apps.web.agent.define import AGENT_INCOME_SOURCE
+from apps.web.agent.models import AgentIncomeReport, Agent
+
+logger = logging.getLogger(__name__)
+
+
+def record_agent_income(agentId, source, source_key, amount, detail, date=None, **kwargs):
+    # type:(str, str, str, RMB, dict, str, **Any)->bool
+    """
+    记录代理商的收入
+    :param agentId:
+    :param detail:
+    :param source:
+    :param amount:
+    :param date:
+    :return:
+    """
+    try:
+        if amount == RMB(0):
+            logger.warn('`record_agent_income` agent(id=%s) amount=0 found' % (agentId,))
+            return False
+
+        record_id = detail.get('recharge_record_id')
+        if not record_id:
+            logger.error('no recharge_record_id provided')
+            return False
+
+        logger.info('[recordAgentIncome] agent(id={}), amount={}, detail={}, source={}'
+                    .format(agentId, amount, json_dumps(detail), source))
+
+        transaction_id = 'incr_{}_{}'.format(source, str(record_id))
+
+        success = Agent.prepare_sellet_income(agentId, transaction_id, source, source_key, amount, True)
+        if not success:
+            logger.error(
+                '[recordAgentIncome] duplicate operation({}) or agent<id={}> not exists.'.format(
+                    transaction_id, agentId))
+            return False
+
+        if date:
+            AgentIncomeReport(agentId=agentId, source=source, amount=amount, date=date, detail=detail,
+                              **kwargs).save()
+        else:
+            AgentIncomeReport(agentId=agentId, source=source, amount=amount, detail=detail, **kwargs).save()
+
+        return Agent.commit_sellet_income(agentId, transaction_id)
+    except Exception as e:
+        logger.exception(e)
+        return False
+
+
+def update_agent_income_stats(recordId, agentId, amount):
+    """
+    更新代理商的收益报表
+    :param recordId:
+    :param agentId:
+    :param amount:
+    :return:
+    """
+    AgentIncomeReport.get_collection().update_one(
+        {'detail.recharge_record_id': recordId, 'agentId': agentId},
+        {'$inc': {'amount': amount.mongo_amount}})
+
+
+def record_agent_withdraw_fee(agentId, source_key, amount, detail):
+    # type:(str, str, RMB, dict)->bool
+
+    """
+    记录代理商旗下经销商的提现手续费差收入
+    """
+
+    return record_agent_income(agentId=agentId, source=AGENT_INCOME_SOURCE.DEALER_WITHDRAW_FEE,
+                               source_key=source_key, amount=amount, detail=detail)
+
+
+def record_agent_traffic_card_earning(agentId, source_key, detail):
+    # type:(str, str, dict)->bool
+
+    try:
+        if 'agent_earning' not in detail:
+            logger.error('no earned provided')
+            return False
+
+        earned = RMB(detail['agent_earning'])
+        if earned <= RMB(0):
+            return True
+
+        return record_agent_income(agentId=agentId, source=AGENT_INCOME_SOURCE.DEALER_CARD_FEE,
+                                   source_key=source_key, amount=earned, detail=detail)
+    except Exception as e:
+        logger.exception(e)
+        return False
+
+
+def record_agent_api_quota_earning(agentId, source_key, detail):
+    # type:(str, str, dict)->bool
+
+    try:
+        if 'agent_earning' not in detail:
+            logger.error('no earned provided')
+            return False
+
+        earned = RMB(detail['agent_earning'])
+        if earned <= RMB(0):
+            return True
+
+        return record_agent_income(agentId=agentId, source=AGENT_INCOME_SOURCE.DEALER_API_QUOTA,
+                                   source_key=source_key, amount=earned, detail=detail)
+    except Exception as e:
+        logger.exception(e)
+        return False
+
+
+def record_agent_disable_ad_earning(agentId, source_key, detail):
+    # type:(str, str, dict)->bool
+
+    try:
+        if 'agent_earning' not in detail:
+            logger.error('no earned provided')
+            return False
+
+        earned = RMB(detail['agent_earning'])
+        if earned <= RMB(0):
+            return True
+
+        return record_agent_income(agentId=agentId, source=AGENT_INCOME_SOURCE.DEALER_DISABLE_AD,
+                                   source_key=source_key, amount=earned, detail=detail)
+    except Exception as e:
+        logger.exception(e)
+        return False

+ 177 - 0
apps/web/agent/urls.py

@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from django.conf.urls import patterns, url
+
+from apps.web.agent.views import *
+
+urlpatterns = patterns(
+    '',
+    # 获取代理商列表
+    url(r'^getAgentsDetailList$', getAgentsDetailList, name = 'getAgentsDetailList'),
+
+    # 退出登录
+    url(r'^logout$', logout, name = 'logout'),
+
+    # 获取配置套餐
+    url(r'^getDefaultPackageList$',
+        getPackageList, name = 'getDefaultPackageList'),
+
+    # 管理经销商
+    url(r'^login$', login, name = 'agentLogin'),
+
+    # 代理商注册|管理经销商
+    url(r'^verifyRegisterCode$', verifyRegisterCode, name = 'verifyRegisterCode'),
+
+    url(r'^getDealerList$', getDealerList, name = 'getDealerList'),
+
+    url(r'^getCustomerList$', getReferralDealerList, name = 'getReferralDealerList'),
+
+    # 代理商个人信息
+    url(r'^accountInfo$', accountInfo, name = 'agentAccountInfo'),
+
+    url(r'^homepageData$', homePageData, name = 'agentHomePageData'),
+
+    url(r'^getIncome$', getIncome, name = 'getIncome'),
+
+    url(r'^cardFeeIncome$', cardFeeIncome, name = 'cardFeeIncome'),
+
+    url(r'^cardFeeIncomeList$', cardFeeIncomeList, name = 'cardFeeIncomeList'),
+
+    url(r'^withdrawIncome$', withdrawIncome, name = 'withdrawIncome'),
+
+    url(r'^withdrawIncomeList$', withdrawIncomeList, name = 'withdrawIncomeList'),
+
+    url(r'^adIncome$', adIncome, name = 'adIncome'),
+
+    url(r'^adIncomeList$', adIncomeList, name = 'adIncomeList'),
+
+    url(r'^resetPassword$', resetPassword, name = 'agentResetPassword'),
+
+    url(r'^getIncomeStatistics', getIncomeStatistics, name = 'getIncomeStatistics'),
+
+    url(r'^getIncomeList', getIncomeList, name = 'getIncomeList'),
+
+    # 代理商上传logo
+    url(r'^uploadLogo$', uploadLogo, name = 'uploadLogo'),
+
+    # 代理商上传服务二维码
+    url(r'^uploadServiceQrcodeUrl$', uploadServiceQrcodeUrl, name = 'uploadServiceQrcodeUrl'),
+
+    # 代理商上传服务二维码
+    url(r'^uploadServiceGzhQrcodeUrl$', uploadServiceGzhQrcodeUrl,
+        name = 'uploadServiceGzhQrcodeUrl'),
+
+    url(r'^getAgentInfo$', getAgentInfo, name = 'getAgentInfo'),
+
+    url(r'^saveAgentInfo$', saveAgentInfo, name = 'saveAgentInfo'),
+
+    # 获取经销商信息
+    url(r'^getDealerInfo$', getDealerInfo, name = 'getDealerInfo'),
+
+    # 设置经销商提现比率
+    url(r'^setDealerWithdrawFeeRatio$', setDealerWithdrawFeeRatio,
+        name = 'setDealerWithdrawFeeRatio'),
+
+    # 设置经销商年卡费用
+    url(r'^setDealerAnnualTrafficCost$', setDealerAnnualTrafficCost,
+        name = 'setDealerAnnualTrafficCost'),
+
+    # 批量设置经销商的年卡费用
+    url(r'^setBatchDealerAnnualTrafficCost$', setBatchDealerAnnualTrafficCost,
+        name = 'setBatchDealerAnnualTrafficCost'),
+
+    # 代理商远程上分
+    url(r'^onPoints$', onPoints, name = 'agentOnPoints'),
+
+    # 标记测试
+    url(r'^labelBadDevice$', labelBadDevice, name = 'labelBadDevice'),
+
+    # 查询设备信息
+    url(r'^equipmentList$', equipmentList, name = 'equipmentList'),
+
+    # wxconfig过期再取
+    url(r'^wxconfig$', wxconfig, name = 'wxconfig'),
+
+    url(r'^toggleSwitches$', toggleSwitches, name = 'toggleSwitches'),
+
+    # 代理商获取短信验证码
+    url(r'^getCheckCode', getCheckCode, name = 'getCheckCode'),
+
+    # 代理商验证验证码
+    url(r'^verifyForgetCode$', verifyForgetCode, name = 'verifyForgetCode'),
+
+    # 查询钱包
+    url(r'^walletData$', walletData, name = 'walletData'),
+
+    # 查询银行卡信息
+    url(r'^getWalletBank$', getWalletBank, name = 'getWalletBank'),
+
+    # 保存银行卡信息
+    url(r'^saveWalletBank$', saveWalletBank, name = 'saveWalletBank'),
+
+    # 解绑银行卡
+    url(r'^bankCardUnbind$', bankCardUnbind, name = 'bankCardUnbind'),
+
+    # 代理商获取提现短信验证码
+    url(r'^getWithdrawCode$', getWithdrawCode, name = 'getWithdrawCode'),
+
+    # 代理商提现
+    url(r'^withdraw$', agentWithdraw, name = 'agentWithdraw'),
+
+    # 查询提现记录
+    url(r'^withdrawalsHistoryList$', withdrawalsHistoryList, name = 'withdrawalsHistoryList'),
+
+    # 提现明细详情
+    url(r'^paymentInfo$', paymentInfo, name = 'paymentInfo'),
+
+    # 设置代理商设备分成比例 分为商户收款和资金池收款
+    url(r'^setDealerAgentProfitShare$', setDealerAgentProfitShare, name = 'setDealerAgentProfitShare'),
+    url(r'^setDealerAgentMerProfitShare$', setDealerAgentMerProfitShare, name='setDealerAgentMerProfitShare'),
+
+    url(r'^getWalletWithdrawInfo$', getWalletWithdrawInfo, name = 'getWalletWithdrawInfo'),
+
+    url(r'^updateInfo$', updateInfo, name = 'updateInfo'),
+
+    # 提现处理列表
+    url(r'^withdrawList$', getWithdrawList, name = 'getManualWithdrawListOfDealers'),
+
+    # 确认提现
+    url(r'^adminAgreeWallet$', adminAgreeWallet, name = 'adminAgreeWallet'),
+
+    # 后台退单
+    url(r'^revokeWithdrawApplication$', revokeWithdrawApplication,
+        name = 'revokeWithdrawApplication'),
+
+    url(r'^getFeatureList$', getFeatureList, name = 'getFeatureList'),
+
+    url(r'^withdrawDetail$', getWithdrawDetail, name = 'getDealerWithdrawalDetail'),
+
+    url(r'^setLimitDevNum$', setLimitDevNum, name = 'setLimitDevNum'),
+
+    url(r'^withdraw/entry$', withdrawEntry, name = 'withdrawEntry'),
+
+    url(r'^wechat/bind$', bindWechatEntry, name = 'bindWechatEntry'),
+
+    url(r'^wechat/getBindInfo$', getBoundWechat, name = 'getBoundWechat'),
+
+    url(r'^card/batchBind$', getBoundWechat, name = 'getBoundWechat'),
+
+    url(r'^batchEntryCard$', batchBindCard, name = 'batchBindCard'),
+
+    url(r'^getUserCardList$', getUserCardList, name='getUserCardList'),
+
+    url(r'^clearCardBind$', clearCardBind, name='clearCardBind'),
+
+    url(r'^clearCardVirtualBind$', clearCardVirtualBind, name='clearCardVirtualBind'),
+
+    url(r'^setShowBanner$', setShowBanner, name='setShowBanner'),
+
+    url(r'^setHasTempPackageSwitch$', setHasTempPackageSwitch, name='setHasTempPackageSwitch'),
+
+    url(r'^setBankWithdrawFee$', setBankWithdrawFee, name='setBankWithdrawFee'),
+
+    url(r'^dealer/setProxyServicePhone$', setAgentProxyServicePhone, name='setAgentProxyServicePhone'),
+
+    url(r'^dealer/findDevTypeCandidate$', findDevTypeCandidate, name='findDevTypeCandidate'),
+)

+ 4 - 0
apps/web/agent/utils.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+

+ 17 - 0
apps/web/agent/validation.py

@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+from apilib.monetary import RMB, Percent, Permillage
+
+from apps.web.core.validation import Schema, Coerce, ALLOW_EXTRA
+
+agentSchema = Schema(
+    {
+        'username': basestring,
+        'password': basestring,
+        'nickname': basestring,
+
+        'annualTrafficCost': Coerce(RMB),
+        'managerProfitShare': Coerce(Percent),
+        'withdrawFeeRatio': Coerce(Permillage)
+
+    }, extra=ALLOW_EXTRA)

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 2272 - 0
apps/web/agent/views.py


+ 40 - 0
apps/web/agent/withdraw.py

@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from typing import TYPE_CHECKING
+
+from apps.web.common.models import WithdrawRecord
+from apps.web.common.transaction.withdraw import WithdrawService, WithdrawRetryService
+from apps.web.common.transaction import WithdrawStatus, WithdrawHandler
+from apps.web.core.exceptions import ServiceException
+
+if TYPE_CHECKING:
+    pass
+
+
+def check_record_when_revoke_or_approve(withdraw_record, agent_id):
+    # type: (WithdrawRecord, str)->None
+    if withdraw_record is None:
+        raise ServiceException({'result': 0, 'description': u'提现申请不存在', 'payload': {}})
+
+    if withdraw_record.manual:
+        if withdraw_record.status != WithdrawStatus.PROCESSING:
+            raise ServiceException({'result': 0, 'description': u'订单状态错误,请刷新', 'payload': {}})
+    else:
+        if withdraw_record.status != WithdrawStatus.FAILED:
+            raise ServiceException({'result': 0, 'description': u'订单状态错误,请刷新', 'payload': {}})
+
+    if withdraw_record.payAgentId != agent_id:
+        raise ServiceException({'result': 0, 'description': u'不是有效的提现申请(1001)', 'payload': {}})
+
+
+class AgentWithdrawHandler(WithdrawHandler):
+    pass
+
+
+class AgentWithdrawService(WithdrawService):
+    pass
+
+
+class AgentWithdrawRetryService(WithdrawRetryService):
+    pass

+ 2 - 0
apps/web/api/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python

+ 0 - 0
apps/web/api/cy4/__init__.py


+ 18 - 0
apps/web/api/cy4/urls.py

@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from django.conf.urls import patterns, url
+
+from .views import *
+
+
+urlpatterns = patterns('',
+    url(r'^startDeviceForCy4$', startDeviceForCy4, name='startDeviceForCy4'),
+    url(r'^getDevSettingsFromCy4$', getDevSettingsFromCy4, name='getDevSettingsFromCy4'),
+    url(r'^setDevSettingsFromCy4$', setDevSettingsFromCy4, name='setDevSettingsFromCy4'),
+    url(r'^stopChargingPortForCy4$', stopChargingPortForCy4, name='stopChargingPortForCy4'),
+    url(r'^getPortStatusFromCy4$', getPortStatusFromCy4, name='getPortStatusFromCy4'),
+    url(r'^getPortInfoFromCy4$', getPortInfoFromCy4, name='getPortInfoFromCy4'),
+    url(r'^getConsumeRecordsFromCy4ByDeviceCode$', getConsumeRecordsFromCy4ByDeviceCode, name='getConsumeRecordsFromCy4ByDeviceCode'),
+    url(r'^getConsumeRecordsFromCy4ByOrderNo$', getConsumeRecordsFromCy4ByOrderNo, name='getConsumeRecordsFromCy4ByOrderNo'),
+)

+ 269 - 0
apps/web/api/cy4/views.py

@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+import simplejson as json
+
+from typing import TYPE_CHECKING
+
+from apps.web.api.exceptions import ApiParameterError
+from apps.web.api.models import APIServiceStartRecord
+from apps.web.api.utils import api_call, api_ok_response, api_exception_response
+from apps.web.api.views import send_api_order
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    from django.core.handlers.wsgi import WSGIRequest
+    from apilib.utils_json import JsonResponse
+    from apps.web.dealer.models import Dealer
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def startDeviceForCy4(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    # default_params_list = ['deviceCode', 'price', 'discount', 'port']
+    # params_list = payload.keys()
+
+    # # 1. 判断差集
+    # different_params_list = list(set(default_params_list) - set(params_list))
+    # if len(different_params_list) != 0:
+    #     raise ApiParameterError(errmsg = u'参数内容错误')
+    #
+    # # 2. 判断个数
+    # if len(params_list) != len(default_params_list):
+    #     raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiStartDeviceForCy4')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getDevSettingsFromCy4(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetDevSettingsFromCy4')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def setDevSettingsFromCy4(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'time1', 'time2', 'time3', 'time4', 'powerMax1', 'powerMax2', 'powerMax3',
+                           'powerMax4', 'elecCheckMin', 'elecCheckTime', 'voice']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiSetDevSettingsFromCy4')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def stopChargingPortForCy4(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiStopChargingPortForCy4')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortStatusFromCy4(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortStatusFromCy4')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortInfoFromCy4(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortInfoFromCy4')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getConsumeRecordsFromCy4ByDeviceCode(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'dataSize', 'reverse']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = {
+        "status": 0,
+        "description": "",
+        "errcode": 0,
+        "result": {},
+        "errmsg": "SUCCESS",
+        "desc": ""
+    }
+    apis = APIServiceStartRecord.objects(logicalCode = payload['deviceCode'])
+    if apis.count() == 0:
+        dataList = []
+    else:
+        apis = apis[0:payload['dataSize']]
+        dataList = [{
+            'orderNo': _.orderNo,
+            'logicalCode': _.logicalCode,
+            'port': _.port,
+            'money': _.money,
+            'needTime': _.needTime,
+            'consumeType': _.consumeType,
+            'cardNo': _.cardNo,
+            'status': _.status,
+            'startTime': _.startTime,
+            'finishReason': _.finishReason,
+            'leftTime': _.leftTime,
+            'duration': _.duration,
+            'spendElec': _.spendElec,
+            'endTime': _.endTime
+        } for _ in apis]
+
+    if payload['reverse'] is True:
+        dataList.sort(key = lambda x: x['startTime'], reverse = True)
+    else:
+        dataList.sort(key = lambda x: x['startTime'])
+
+    resultDict['result'].update({'dataList': dataList})
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getConsumeRecordsFromCy4ByOrderNo(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['orderNo']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = {
+        "status": 0,
+        "description": "",
+        "errcode": 0,
+        "result": {},
+        "errmsg": "SUCCESS",
+        "desc": ""
+    }
+    api = APIServiceStartRecord.objects(orderNo = payload['orderNo']).first()
+    if api is None:
+        dataList = []
+    else:
+        dataList = [{
+            'orderNo': api.orderNo,
+            'logicalCode': api.logicalCode,
+            'port': api.port,
+            'money': api.money,
+            'needTime': api.needTime,
+            'consumeType': api.consumeType,
+            'cardNo': api.cardNo,
+            'status': api.status,
+            'startTime': api.startTime,
+            'finishReason': api.finishReason,
+            'leftTime': api.leftTime,
+            'duration': api.duration,
+            'spendElec': api.spendElec,
+            'endTime': api.endTime
+        }]
+
+    resultDict['result'].update({'dataList': dataList})
+
+    return api_ok_response(payload = resultDict)

+ 0 - 0
apps/web/api/dc/__init__.py


+ 15 - 0
apps/web/api/dc/urls.py

@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from django.conf.urls import patterns, url
+
+from .views import *
+
+
+urlpatterns = patterns('',
+                       url(r'^startDeviceForDc$', startDeviceForDc, name = 'startDeviceForDc'),
+                       url(r'^getPortStatusFromDc$', getPortStatusFromDc, name = 'getPortStatusFromDc'),
+                       url(r'^getPortInfoFromDc$', getPortInfoFromDc, name = 'getPortInfoFromDc'),
+                       url(r'^getDevSettingsFromDc$', getDevSettingsFromDc, name='getDevSettingsFromDc'),
+                       url(r'^setDevSettingsFromDc$', setDevSettingsFromDc, name='setDevSettingsFromDc')
+                       )

+ 148 - 0
apps/web/api/dc/views.py

@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+import simplejson as json
+from typing import TYPE_CHECKING
+
+from apps.web.api.exceptions import ApiParameterError
+from apps.web.api.utils import api_call, api_ok_response, api_exception_response
+from apps.web.api.views import send_api_order
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    from django.core.handlers.wsgi import WSGIRequest
+    from apps.web.dealer.models import Dealer
+    from apilib.utils_json import JsonResponse
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortStatusFromDc(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortStatusFromDc')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getDevSettingsFromDc(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetDevSettingsFromDc')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def setDevSettingsFromDc(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = [
+        "deviceCode",
+        "power1",
+        "power2",
+        "power3",
+        "power1ratio",
+        "power2ratio",
+        "power3ratio",
+        "fuchongTime",
+        "fuchongPower",
+        "maxPower",
+        "icMoney",
+        "time1",
+        "time2",
+        "time3"
+    ]
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apisetDevSettingsFromDc')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortInfoFromDc(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortInfoFromDc')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def startDeviceForDc(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port', 'price', 'time']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiStartDeviceForDc')
+
+    return api_ok_response(payload = resultDict)

+ 57 - 0
apps/web/api/exceptions.py

@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from apps.web.constant import ErrorCode
+
+class ApiException(Exception):
+    def __init__(self, errcode, errmsg, payload = {}):
+        self.errcode = errcode
+        self.payload = payload
+        self.errmsg = errmsg
+
+    def __repr__(self):
+        return 'api exception. errcode = {}, errmsg = {}, payload = {}'.format(
+            self.errcode, self.errmsg, str(self.payload))
+
+    def __str__(self):
+        return self.__repr__()
+
+    def to_response(self):
+        from apps.web.api.utils import api_error_response
+        return api_error_response(errcode = self.errcode, errmsg = self.errmsg, payload = self.payload)
+
+
+class ApiParameterError(ApiException):
+    def __init__(self, errmsg):
+        super(ApiParameterError, self).__init__(
+            errcode = ErrorCode.API_PARAMETER_ERROR, errmsg = errmsg)
+
+
+class ApiAuthException(ApiException):
+    def __init__(self, errmsg):
+        super(ApiAuthException, self).__init__(
+            errcode = ErrorCode.API_AUTH_ERROR, errmsg = errmsg, payload = {'auth_result': 1})
+
+
+class ApiAuthDeviceException(ApiException):
+    def __init__(self):
+        super(ApiAuthDeviceException, self).__init__(
+            errcode = ErrorCode.API_AUTH_DEVICE_ERROR, errmsg = u'无权操作设备', payload = {'auth_result': 3})
+
+
+class ApiNoDeviceException(ApiException):
+    def __init__(self):
+        super(ApiNoDeviceException, self).__init__(
+            errcode = ErrorCode.API_NO_DEVICE, errmsg = u'设备不存在')
+
+class ApiDeviceModeException(ApiException):
+    def __init__(self):
+        super(ApiDeviceModeException, self).__init__(
+            errcode = ErrorCode.API_NO_DEVICE, errmsg = u'当前设备不在API模式, 请切换API模式(经销商后台->API接口管理->API设备管理)')
+
+class ApiDeviceTypeError(ApiException):
+    def __init__(self, errmsg):
+        super(ApiDeviceTypeError, self).__init__(
+            errcode = ErrorCode.API_ERROR_DEVICE_TYPE, errmsg = errmsg)
+
+

+ 0 - 0
apps/web/api/ft_north/__init__.py


+ 18 - 0
apps/web/api/ft_north/constant.py

@@ -0,0 +1,18 @@
+# coding=utf-8
+
+
+class RESPONSE_CODE(object):
+    SYS_BUSY = -1
+    SYS_ERROR = 500
+
+    SUCCESS = 0
+
+    ERROR_SIGNATURE = 4000
+    ERROR_TOKEN = 4001
+    ERROR_POST = 4002
+    ERROR_PARAM = 4003
+
+class PORT_WORK_STATUS(object):
+    PORT_IDLE = 0
+    PORT_WORKING = 1
+    PORT_FAULT = 2

+ 63 - 0
apps/web/api/ft_north/models.py

@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import base64
+import hashlib
+import hmac
+import logging
+import time
+from urllib import quote
+
+from mongoengine import StringField, IntField
+
+from apps.web.core.db import Searchable
+
+logger = logging.getLogger(__name__)
+
+
+class FengTuTechnologyNorther(Searchable):
+    northPort = StringField(verbose_name="平台的 域名端口 推送数据给对方平台的时候 服务器的地址", default="")
+    appId = StringField(vebose_name="App key,由丰图提供")
+    appSecret = StringField(vebose_name="签名密钥 可以理解为 我方密码")
+    northToken = StringField(vebose_name="northToken 通过getToken接口获取 ")    # 我们像对方平台获取到的token
+    northTokenExpiredTime = IntField(verbose_name=u"northToken的过期时间", default=time.time)
+    northAppId = StringField(vebose_name="平台ID,由我们提供")
+    meta = {
+        "collection": "feng_tu_norther",
+        "db_alias": "default"
+    }
+
+    def get_sig(self, ts):
+        """
+        生成签名字符串
+        :return:
+        """
+        payload  = {
+            'appid' : str(self.appId),
+            'timestamp' : str(ts)
+        }
+
+        raw = [(k, payload[k]) for k in sorted(payload.keys())]
+        s = str('&'.join('='.join(kv) for kv in raw if kv[1]))
+
+        sigSecret = str(self.appSecret)
+        sigDate = hmac.new(sigSecret,s, hashlib.md5).digest()
+        sig = base64.b64encode(sigDate)
+
+        return quote(sig)
+
+    def join_url(self,path):
+        return "{ipPort}/{path}".format(
+            ipPort=self.northPort,
+            path=path
+        )
+
+    def get_token_data(self):
+        """
+        获取token的载数据 身份验证以平台为维度获取 那么token的范围也以 平台为准 即 northOperatorID
+        """
+        return {
+            "northAppId": self.northAppId,
+        }
+
+

+ 12 - 0
apps/web/api/ft_north/urls.py

@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from django.conf.urls import patterns, url, include
+from apps.web.api.ft_north.views import getOrder,getDeviceInfo,getCoding,getToken
+
+urlpatterns = patterns('',*[
+    url(r'^get/token$', getToken, name = 'getToken'),
+    url(r'^get/order$', getOrder, name = 'getOrder'),
+    url(r'^get/deviceInfo$', getDeviceInfo, name = 'getDeviceInfo'),
+    url(r'^get/coding$', getCoding, name = 'getDeviceInfo'),
+])

+ 398 - 0
apps/web/api/ft_north/utils.py

@@ -0,0 +1,398 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import base64
+import time
+import itsdangerous
+
+import requests
+import logging
+
+from django.conf import settings
+
+from apps.web.api.ft_north.models import FengTuTechnologyNorther
+from apps.web.constant import DeviceOnlineStatus
+from apps.web.device.models import Device, Group
+from apps.web.api.ft_north.constant import PORT_WORK_STATUS
+from apps.web.api.utils import bd09_to_gcj02, get_coordinates_and_nums
+
+
+logger = logging.getLogger(__name__)
+
+def get_coding_info_by_device(devNo):
+    dev = Device.get_dev(devNo)
+    box = dev.deviceAdapter
+    try:
+        devStatusDict = box.get_port_status()
+    except:
+        return
+    codingInfos = list()
+    for k,v in devStatusDict.items():
+        codingInfo = {}
+        codingInfo['coding'] = k
+        if v['status'] != PORT_WORK_STATUS.PORT_WORKING and v['status'] != PORT_WORK_STATUS.PORT_WORKING:
+            v['status'] = PORT_WORK_STATUS.PORT_FAULT
+        codingInfo['status'] = v['status']
+        codingInfos.append(codingInfo)
+    return codingInfos
+
+def get_coding_info(devNo):
+    """
+    获取端口状态,先找缓存,缓存中没有直接从设备里拿
+    :param devNo:
+    :return:
+    """
+    ctrInfo = Device.get_dev_control_cache(devNo)
+    codingInfo = []
+    if 'allPorts' not in ctrInfo:
+        ctrInfo = get_coding_info_by_device(devNo)
+        if ctrInfo:
+            return ctrInfo
+        else:
+            return [{"coding":"0","status":0}]
+    else:
+        allPorts = ctrInfo.get('allPorts', 10)
+        # allPorts 有的时候会为0, 这个时候强制设置为10
+        if allPorts == 0:
+            allPorts = 10
+        for ii in range(allPorts):
+            statusDict = {}
+            tempDict = ctrInfo.get(str(ii + 1), {})
+            if tempDict.has_key('status'):
+                statusDict['coding'] = str(ii + 1)
+                statusDict['status'] = tempDict.get('status')
+            elif tempDict.has_key('isStart'):
+                if tempDict['isStart']:
+                    statusDict['coding'] = str(ii + 1)
+                    statusDict['status'] = PORT_WORK_STATUS.PORT_WORKING
+                else:
+                    statusDict['coding'] = str(ii + 1)
+                    statusDict['status'] = PORT_WORK_STATUS.PORT_IDLE
+            else:
+                statusDict['coding'] = str(ii + 1)
+                statusDict['status'] = PORT_WORK_STATUS.PORT_IDLE
+            codingInfo.append(statusDict)
+
+        return codingInfo
+
+def get_device_status(devNo):
+    """
+    获取设备状态
+    :param devNo:
+    :return:
+    """
+    dev = Device.get_dev(devNo)
+    devOnline = dev.online
+    if devOnline == DeviceOnlineStatus.DEV_STATUS_ONLINE:
+        devStatus = '1'
+    elif devOnline == DeviceOnlineStatus.DEV_STATUS_OFFLINE:
+        devStatus = '0'
+    else:
+        devStatus = '-1'
+
+    return devStatus
+
+def get_station_Info(groupIds):
+    """
+    推送站点信息
+    :param groupIds:
+    :return:
+    """
+
+    stationInfoList = list()
+
+    groups = Group.get_groups_by_group_ids(groupIds)
+
+    for group in groups.values():
+        # 获取经纬度 获取设备数量 经纬度使用火星坐标系转换
+        lng, lat, count = get_coordinates_and_nums(group.groupId)
+        lng, lat = bd09_to_gcj02(lng, lat)
+
+        stationInfoDict = {
+            "stationId" : group.groupId, # 厂商站点ID
+            "name" : group.groupName, # 充电站名称
+            "brand" : "", # 品牌名称
+            "longitude" : "{:.6f}".format(float(lng)),  # 经度(6位小数),
+            "latitude" : "{:.6f}".format(float(lat)),  # 维度(6位小数)
+            "address" : group.address, # 具体地址
+            "socketNumber" : 0, # 设备路数
+            "hasRainshed" : "", # 是否有雨棚
+            "hasMonitor" : "", # 是否覆盖视频监控
+            "fireFacilities" : "", # 是否配置消防设施
+            "buildingTime" : group.get('dateTimeAdded').get('val').split(" ")[0], # 建设时间
+            "picture" : "", # 站点图片
+            "deviceList" : get_dev_info([group.groupId],group), # 站点下的设备列表
+        }
+        stationInfoList.append(stationInfoDict)
+
+    return stationInfoList
+
+def get_dev_info(groupIds,group):
+    devices = Device.get_devices_by_group(groupIds)
+    deviceInfo = list()
+    for devNo,devInfo in devices.items():
+        devDict = {
+            'stationId':groupIds[0], # 厂商站点ID
+            'deviceNo':devNo, # 设备编号
+            'deviceType':devInfo['devType']["majorDeviceType"], # 设备类型:充电柜、充电桩、桩柜一体、摄像头、烟感、消防器(栓)、喷淋
+            'simCardNo':"", # SIM卡号
+            'wateringPortNum':"", # 浇水口数量
+            'brand':devInfo['devType']["name"], # 品牌名称
+            'useStationInfo':1, # 是否与充电站公用一个位置: 0.否, 1.是
+            'deviceLocation':group.address, # 设备地址,省市区街道+详细地址
+            'longitude':devInfo['lng'], # 径度
+            'latitude':devInfo['lat'], # 纬度
+            'socketNumber':Device.get_dev_control_cache(devNo).get('allPorts'), # 设备路数,充电口数量
+            'buildingYear':devInfo['dateTimeAdded'].split('-')[0] # 建设年份,格式:yyyy
+        }
+        deviceInfo.append(devDict)
+    return deviceInfo
+
+def send_request(url,mode='POST',**kwargs):
+    """
+    主动发送HTTP请求获取数据 密钥及签名
+    :return:
+    """
+
+    headers = {"Content-Type": "application/json;charset=utf-8"}
+    token = kwargs.pop("token", None)
+
+    if token:headers.update({"Authorization": "Basic {}".format(token)})
+    timeout = kwargs.pop("timeout", 5)
+
+    data = kwargs
+
+
+
+    try:
+        if mode == 'POST':
+            response = requests.post(url=url, json=data, headers=headers, timeout=timeout)
+        else:
+            response = requests.get(url=url, headers=headers, timeout=timeout)
+    except requests.Timeout:
+        return dict()
+    except Exception as e:
+        logger.exception(e)
+        return dict()
+
+    if response.status_code != 200:
+        return dict()
+
+    return response.json()
+
+def get_token(norther):
+
+
+    # if norther.token and norther.tokenExpiredTime > int(time.time()) * 1000:
+    #     return norther.token
+
+    timestamp = int(time.time()*1000)
+
+    url = norther.join_url("getToken")
+    sign = norther.get_sig(timestamp)
+
+
+
+    result = send_request(url,appid=norther.appId,timestamp=timestamp,sign=sign)
+    ret = result.get("code")
+    logger.info(ret)
+    if ret != 200:
+        return
+
+    responseJson = result.get("data")
+    tokenAvailableTime = responseJson.get("expireTime", 120)
+
+    token = responseJson.get("token", "")
+
+    # 数据库更新
+    # norther.update(
+    #     token=token,
+    #     tokenExpiredTime=tokenAvailableTime
+    # )
+    #
+    # norther.save()
+    return token
+
+def get_station_Info_manage(groupId):
+    """
+    推送站点信息
+    :param groupId:
+    :return:
+    """
+
+    group = Group.get_group(groupId)
+
+    # 获取经纬度 获取设备数量 经纬度使用火星坐标系转换
+    lng, lat, count = get_coordinates_and_nums(group.groupId)
+    lng, lat = bd09_to_gcj02(lng, lat)
+
+    stationInfoDict = {
+        "stationId" : group.groupId, # 厂商站点ID
+        "name" : group.groupName, # 充电站名称
+        "chargingType":"", # 充电站类型
+        "brand":"",
+        "longitude":lng,
+        "latitude":lat,
+        "address":group.address,
+        "socketNumber":0,
+        "hasRainshed":"",
+        "hasMonitor":"",
+        "fireFacilities":"",
+        "buildingTime":group.get('dateTimeAdded').strftime('%Y-%m-%d'),
+        "picture":"",
+    }
+
+    return stationInfoDict
+
+def get_device_info_manage(devNo):
+    dev = Device.get_dev(devNo)
+    deviceInfo = {
+        "stationId":dev["groupId"], # 厂商站点ID
+        "deviceNo":devNo, # 设备编号
+        "deviceType":dev['devType']["majorDeviceType"], # 设备类型
+        "brand":dev['devType']["name"], # 品牌名称
+        "deviceLocation":Group.get_group(dev["groupId"]).get("address"), # 设备地址
+        "longitude":dev['lng'], # 经度
+        "latitude":dev['lat'], # 纬度
+        "socketNumber":Device.get_dev_control_cache(devNo).get('allPorts',10), # 设备路数
+    }
+    return deviceInfo
+
+def get_alarm_report(devNo,alarmType):
+    dev = Device.get_dev(devNo)
+    data = {
+        "stationId":dev.get('groupId'), # 充电站编号
+        "deviceNo" : devNo,
+        "codingNo" : "",
+        "eventId" : base64.b64encode(dev.get('groupId')+devNo+str(time.time())),
+        "outSourceId" : "",
+        "alarmType":alarmType
+
+    }
+    return data
+
+def generate_json_token(data, expire=None):
+    salt = settings.FENG_TU_TOKEN_SECRET
+    its = itsdangerous.TimedJSONWebSignatureSerializer(salt, expire)
+
+    return its.dumps(data)
+
+def parse_json_token(s, expire=None):
+    salt = settings.FENG_TU_TOKEN_SECRET
+    its = itsdangerous.TimedJSONWebSignatureSerializer(salt, expire)
+
+    try:
+        result = its.loads(s)
+    except itsdangerous.BadData:
+        return dict()
+
+    return result
+
+
+def batchStationInfoReport(groupIds):
+    """
+    主动推送站点信息
+    :param groupIds:
+    :return:
+    """
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = get_station_Info(groupIds)
+        url = north.join_url("batchStationInfo")
+        token = get_token(north)
+        return send_request(url=url, token=token, stationInfoList=data)
+    except Exception as e:
+        logger.exception(e)
+
+
+def addStationReport(groupId):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = get_station_Info_manage(groupId)
+        url = north.join_url("station/add")
+        token = get_token(north)
+        return send_request(url=url, token=token, **data)
+    except Exception as e:
+        logger.exception(e)
+
+
+def deleteStationReport(groupId):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = groupId
+        url = north.join_url("station/delete/{}".format(data))
+        token = get_token(north)
+        return send_request(url=url, mode='GET', token=token)
+    except Exception as e:
+        logger.exception(e)
+
+
+def updateStationReport(groupId):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = get_station_Info_manage(groupId)
+        url = north.join_url("station/update")
+        token = get_token(north)
+        return send_request(url=url, token=token, **data)
+    except Exception as e:
+        logger.exception(e)
+
+
+def addDeviceReport(devNo):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = get_device_info_manage(devNo)
+        url = north.join_url("device/add")
+        token = get_token(north)
+        return send_request(url=url, token=token, **data)
+    except Exception as e:
+        logger.exception(e)
+
+
+def deleteDeviceReport(devNo):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        groupId = Device.get_dev(devNo).get("groupId")
+        url = north.join_url("device/delete/{}/{}".format(groupId,devNo))
+        token = get_token(north)
+        return send_request(url=url, mode='GET', token=token)
+    except Exception as e:
+        logger.exception(e)
+
+
+def updateDeviceReport(devNo):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = get_device_info_manage(devNo)
+        url = north.join_url("device/update")
+        token = get_token(north)
+        return send_request(url=url, token=token, **data)
+    except Exception as e:
+        logger.exception(e)
+
+
+def devHeartbeatReport(devNo):
+    try:
+        data = {
+            "deviceNo":devNo,
+            "deviceStatus":get_device_status(devNo),
+            "codings":get_coding_info(devNo),
+            "eventId": base64.b64encode(devNo+str(time.time()))
+        }
+        north = FengTuTechnologyNorther.objects.filter().first()
+        url = north.join_url("device/heartbeat")
+        token = get_token(north)
+        send_request(url=url, token=token, **data)
+    except Exception as e:
+        logger.exception(e)
+
+
+def alarmReport(devNo,faultName):
+    try:
+        north = FengTuTechnologyNorther.objects.filter().first()
+        data = get_alarm_report(devNo,faultName)
+        url = north.join_url("alarm")
+        token = get_token(north)
+        send_request(url=url, token=token, **data)
+    except Exception as e:
+        logger.exception(e)

+ 204 - 0
apps/web/api/ft_north/views.py

@@ -0,0 +1,204 @@
+# coding=utf-8
+import datetime
+import logging
+import json
+import random
+import time
+
+
+from mongoengine import DoesNotExist
+from django.views.decorators.http import require_POST
+
+
+from apilib.utils_json import JsonResponse
+
+from apps.web.api.ft_north.constant import RESPONSE_CODE
+from apps.web.api.ft_north.models import FengTuTechnologyNorther
+from apps.web.api.ft_north.utils import get_coding_info , get_device_status, generate_json_token,parse_json_token
+
+
+
+logger = logging.getLogger(__name__)
+
+@require_POST
+def getToken(request):
+    """
+    通过账号密码获取token
+    """
+    logger.debug("[queryToken] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"success": False,"code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1001)"})
+
+    try:
+        data = json.loads(request.body).get("data")
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"success": False,"code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1002)"})
+
+    appid = data.get("appid")
+    appSecret = data.get("appSecret")
+
+    logger.debug("[queryToken] appid = {}, appSecret = {}".format(appid, appSecret))
+
+    if not all((appid, appSecret)):
+        return JsonResponse({"success": False,"code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1003)"})
+
+    try:
+        norther = FengTuTechnologyNorther.objects.filter(appId=appid, appSecret=appSecret).first()   # type: FengTuTechnologyNorther
+    except DoesNotExist:
+        return JsonResponse({"success": False,"code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1004)"})
+    except Exception as e:
+        return JsonResponse({"success": False,"code": RESPONSE_CODE.SYS_ERROR, "message": u"系统错误"})
+
+    expire = int(time.time() + 60 * 60 * 24) * 1000
+
+    token = generate_json_token(data=norther.get_token_data(), expire=expire)
+
+    resultData = {
+        "token": token,
+        "expiresTime": expire
+    }
+    return JsonResponse({
+        "success": True,
+        "code": 200,
+        "message": "请求成功",
+        "data": resultData,
+    })
+
+def getOrder(request):
+    """
+    查询订单数量
+    :param request:
+    :return:
+    """
+    # 验证身份
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[getOrder] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"success":False, "code": RESPONSE_CODE.ERROR_TOKEN, "message": u"请求参数错误(1001)"})
+
+    try:
+        data = json.loads(request.body).get("data")
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"success":False, "code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1004)"})
+
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"success":False, "code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1003)"})
+
+    # 查询参数
+    deviceNo = str(data.get('deviceNo'))
+    startTime = data.get('startTime',"2015-01-01 00:00:00")
+    endTime = data.get("endTime",datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+
+
+    result = {
+        "orderNum": random.randint(10, 200)
+    }
+
+    resultData = json.dumps(result)
+
+    return JsonResponse({
+        "success": True,
+        "message": u"请求成功",
+        "code": u"200",
+        "data": resultData,
+    })
+
+def getCoding(request):
+    """
+    查询端口状态
+    :param request:
+    :return:
+    """
+    # 验证身份
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[getOrder] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"success": False, "code": RESPONSE_CODE.ERROR_TOKEN, "message": u"请求参数错误(1001)"})
+
+    try:
+        data = json.loads(request.body).get("data")
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"success": False, "code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1004)"})
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"success": False, "code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1003)"})
+
+    devNo = str(data.get('deviceNo'))
+
+    codingInfo = get_coding_info(devNo)
+    if not codingInfo:
+        return JsonResponse({
+            "success": False,
+            "message": u"设备未启动",
+            "code": u"303"
+        })
+
+    resultData = json.dumps(codingInfo)
+
+    return JsonResponse({
+        "success": True,
+        "message": u"请求成功",
+        "code": u"200",
+        "data": resultData,
+    })
+
+def getDeviceInfo(request):
+    """
+    查询设备状态
+    :param request:
+    :return:
+    """
+
+    # 验证身份
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[getOrder] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"success": False, "code": RESPONSE_CODE.ERROR_TOKEN, "message": u"请求参数错误(1001)"})
+
+    try:
+        data = json.loads(request.body).get("data")
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"success": False, "code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1004)"})
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"success": False, "code": RESPONSE_CODE.ERROR_POST, "message": u"请求参数错误(1003)"})
+
+    devNo = str(data.get('deviceNo'))
+    deviceStatus = get_device_status(devNo)
+
+    result = {
+        "deviceStatus": deviceStatus
+    }
+
+    resultData = json.dumps(result)
+
+    return JsonResponse({
+        "success": True,
+        "message": u"请求成功",
+        "code": u"200",
+        "data": resultData,
+    })

+ 0 - 0
apps/web/api/jh/__init__.py


+ 17 - 0
apps/web/api/jh/urls.py

@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+"""
+久恒接口对接
+"""
+
+from django.conf.urls import patterns, url
+
+from .views import *
+
+urlpatterns = patterns('',
+                       url(r'^getPortStatusFromJh$', getPortStatusFromJh, name = 'getPortStatusFromJh'),
+                       url(r'^startDeviceForJh$', startDeviceForJh, name = 'startDeviceForJh'),
+                       url(r'^getPortInfoFromJh$', getPortInfoFromJh, name = 'getPortInfoFromJh'),
+                       url(r'^stopChargingPortForJh$', stopChargingPortForJh, name = 'stopChargingPortForJh'),
+                       )

+ 110 - 0
apps/web/api/jh/views.py

@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+import simplejson as json
+from typing import TYPE_CHECKING
+
+from apps.web.api.exceptions import ApiParameterError
+from apps.web.api.utils import api_call, api_ok_response, api_exception_response
+from apps.web.api.views import send_api_order
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    from django.core.handlers.wsgi import WSGIRequest
+    from apps.web.dealer.models import Dealer
+    from apilib.utils_json import JsonResponse
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortStatusFromJh(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortStatusFromJh')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def startDeviceForJh(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port', 'time', 'elec']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiStartDeviceForJh')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def stopChargingPortForJh(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiStopChargingPortForJh')
+
+    return api_ok_response(payload = resultDict)
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortInfoFromJh(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortInfoFromJh')
+
+    return api_ok_response(payload = resultDict)

+ 0 - 0
apps/web/api/jn/__init__.py


+ 14 - 0
apps/web/api/jn/urls.py

@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+"""
+劲能接口对接
+"""
+
+from django.conf.urls import patterns, url
+
+from .views import *
+
+urlpatterns = patterns('',
+                       url(r'^getPortStatusFromJn$', getPortStatusFromJn, name = 'getPortStatusFromJn'),
+                       )

+ 42 - 0
apps/web/api/jn/views.py

@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+import simplejson as json
+from typing import TYPE_CHECKING
+
+from apps.web.api.exceptions import ApiParameterError
+from apps.web.api.utils import api_call, api_ok_response, api_exception_response
+from apps.web.api.views import send_api_order
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    from django.core.handlers.wsgi import WSGIRequest
+    from apps.web.dealer.models import Dealer
+    from apilib.utils_json import JsonResponse
+
+
+@api_call(logger = logger, nil = api_exception_response())
+def getPortStatusFromJn(request, dealer):
+    # type: (WSGIRequest, Dealer)->JsonResponse
+    payload = json.loads(request.body) if request.body else {}
+
+    default_params_list = ['deviceCode', 'port']
+    params_list = payload.keys()
+
+    # 1. 判断差集
+    different_params_list = list(set(default_params_list) - set(params_list))
+    if len(different_params_list) != 0:
+        raise ApiParameterError(errmsg = u'参数内容错误')
+
+    # 2. 判断个数
+    if len(params_list) != len(default_params_list):
+        raise ApiParameterError(errmsg = u'参数错误')
+
+    # 3. 调用api
+    resultDict = send_api_order(payload = payload, dealer = dealer, func = 'apiGetPortStatusFromJn')
+
+    return api_ok_response(payload = resultDict)
+

+ 0 - 0
apps/web/api/jn_north/__init__.py


+ 13 - 0
apps/web/api/jn_north/constant.py

@@ -0,0 +1,13 @@
+# coding=utf-8
+
+
+class RESPONSE_CODE(object):
+    SYS_BUSY = -1
+    SYS_ERROR = 500
+
+    SUCCESS = 0
+
+    ERROR_SIGNATURE = 4000
+    ERROR_TOKEN = 4001
+    ERROR_POST = 4002
+    ERROR_PARAM = 4003

+ 14 - 0
apps/web/api/jn_north/urls.py

@@ -0,0 +1,14 @@
+# coding=utf-8
+
+from django.conf.urls import patterns, url
+
+from apps.web.api.jn_north.views import queryToken, queryStationsInfo, queryStationStats, queryStationStatus, queryEquipBusinessPolicy
+
+
+urlpatterns = patterns('',*[
+    url(r'^query_token$', queryToken, name = 'queryToken'),
+    url(r'^query_stations_info', queryStationsInfo, name='queryStationsInfo'),
+    url(r'^query_station_stats', queryStationStats, name='queryStationStats'),
+    url(r'^query_station_status', queryStationStatus, name='queryStationStatus'),
+    url(r'^query_equip_business_policy', queryEquipBusinessPolicy, name='queryEquipBusinessPolicy'),
+])

+ 330 - 0
apps/web/api/jn_north/utils.py

@@ -0,0 +1,330 @@
+# coding=utf-8
+
+from apps.web.agent.models import Agent
+from apps.web.api.utils import bd09_to_gcj02, get_coordinates_and_nums
+from apps.web.common.models import District
+from apps.web.constant import Const
+from apps.web.south_intf.shangdong_platform import ShanDongNorther, GroupIdMap, GB2260
+from apps.web.device.models import Part, DeviceDict, GroupDict, Device
+from apps.web.user.models import ConsumeRecord
+
+
+def get_connector(part, norther):     # type:(Part, ShanDongNorther) -> dict
+    """
+    获取部件信息
+    """
+    return {
+        "ConnectorID": str(part.id),
+        "ConnectorName": part.partName,
+        "ConnectorType": 2,
+        "VoltageUpperLimits": 220,
+        "VoltageLowerLimits": 220,
+        "Current": part.attachParas.get("current", 16),
+        "Power": part.attachParas.get("power", 7),
+        "ParkNo": "-",
+        "NationalStandard": 3,
+    }
+
+
+def get_equipment(dev, norther):    # type:(DeviceDict, ShanDongNorther) -> dict
+    agent = Agent.objects.get(id=dev.owner.agentId)
+
+    ConnectorInfo = list()
+    for part in dev.parts:
+        ConnectorInfo.append(get_connector(part, norther))
+
+    data = {
+        "EquipmentID": dev.devNo,
+        "ManufacturerName": agent.productName,
+        "EquipmentModel": dev.get("devType", dict()).get("name", ""),
+        "EquipmentName": dev.get("logicalCode"),
+        "EquipmentType": 2,
+        "EquipmentStatus": 50,
+        "EquipmentPower": 7,
+        "NewNationalStandard": 1,
+        "ConnectorInfos": ConnectorInfo,
+    }
+
+    return data
+
+
+def get_stations_info(group, norther):  # type:(GroupDict, ShanDongNorther) -> dict
+    """
+    获取站点的信息
+    """
+    # 获取经纬度 获取设备数量 经纬度使用火星坐标系转换
+    lng, lat, count = get_coordinates_and_nums(group.groupId)
+    lng, lat = bd09_to_gcj02(lng, lat)
+
+    # 获取设备信息
+    EquipmentInfos = list()
+    devNos = Device.get_devNos_by_group([group.groupId])
+    for devNo in devNos:
+        dev = Device.get_dev(devNo) # type: DeviceDict
+        if dev.majorDeviceType != u"汽车充电桩":
+            continue
+
+        EquipmentInfos.append(get_equipment(dev, norther))
+
+    # 充电站信息
+    stationId = GroupIdMap.get_stationId(group.groupId)
+    dealer = group.owner
+
+    data = {
+        "StationID": stationId,  # 充电站ID 20
+        "OperatorID": norther.agentOperatorID,  # 组织机构代码 9
+        "EquipmentOwnerID": norther.equipOperatorID,  # 设备所属方组织机构代码 9
+        "StationName": group.groupName,  # 充电站名称描述  50
+
+        "CountryCode": "CN",  # 国家代码 固定
+        "AreaCode": GB2260.get_code(District.get_area(group.get("districtId"))),  # 地区编码 20
+        "Address": group.address,  # 详细地址 50
+
+        "StationTel": '-',  # 站点责任人电话,
+        "ServiceTel": '-',  # 站点服务电话
+
+        "StationType": 1,  # 站点类型
+        "StationStatus": 50,  # 站点状态
+
+        "ParkNums": 0,  # 车位数量 0代表未知
+
+        "StationLng": "{:.6f}".format(float(lng)),  # 精度(6位小数)
+        "StationLat": "{:.6f}".format(float(lat)),  # 维度(6位小数)
+
+        "Construction": 255,  # 建设场所
+        "ParkInfo": "-",
+        "ParkName": "-",
+
+        "OpenAllDay": 1,  # 是否全天开放
+
+        "BusineHours": u"24小时全天服务",  # 营业时间描述
+
+        "MinElectricityPric": 0.0,  # 最低充电费率 浮点型
+        "ElectricityFee": "-",  # 充电电费描述,
+        "ServiceFee": "-",  # 服务费率描述
+        "ParkFree": 0,  # 是否停车免费
+        "ParkFee": "-",  # 停车费率描述
+
+        "SupportOrder": 0,  # 是否支持预约
+        "EquipmentInfos": EquipmentInfos,  # 充电站信息,
+        "ElectricityTax": 0.0,
+        "ServiceTax": 0.0
+    }
+
+    return data
+
+def get_get_station_state(stationID,startTime, endTime):
+    groupId = GroupIdMap.get_groupId(stationID)
+    EquipmentStatsInfos = list()
+    devNos = Device.get_devNos_by_group([groupId])
+    elec = float(0)
+    for devNo in devNos:
+        tempState = get_equipment_state(devNo, startTime, endTime)
+        elec += tempState.get("EquipmentElectricity", 0.0)
+        EquipmentStatsInfos.append(tempState)
+    data = {
+        "StationElectricity": elec,
+        "EquipmentStatsInfos": EquipmentStatsInfos
+    }
+
+    return data
+
+def get_equipment_state(devNo, startTime, endTime):
+    """
+    获取充电设备的 一段时间内的统计信息 主要是电量
+    :param devNo:
+    :param startTime:
+    :param endTime:
+    :return:
+    """
+    device = Device.get_dev(devNo)
+    ConnectorStatsInfo = list()
+    parts = Part.objects.filter(logicalCode = device.get("logicalCode"))
+    elec = float(0)
+    for part in parts:
+        tempState = get_part_state(devNo, startTime, endTime, part)
+        elec += tempState.get("ConnectorElectricity", 0.0)
+        ConnectorStatsInfo.append(tempState)
+
+    return {
+        "EquipmentID": str(devNo),
+        "EquipmentElectricity": elec,
+        "ConnectorStatsInfo": ConnectorStatsInfo
+    }
+
+def get_part_state(devNo, startTime, endTime, part):
+    """
+    获取充电端口的 一段时间内的统计信息 主要是电量
+    :param devNo:
+    :param part:
+    :param startTime:
+    :param endTime:
+    :return:
+    """
+    filters = {
+        "devNo": devNo,
+        "finishedTime__gte": startTime,
+        "finishedTime__lte": endTime,
+    }
+    device = Device.get_dev(devNo)
+    if device.get("devType", dict()).get("code") != Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY:
+        filters.update({"attachParas__chargeIndex": part.partNo})
+
+    records = ConsumeRecord.objects.filter(**filters).only("servicedInfo")
+
+    elec = float(0)
+    for item in records:
+        elec += item.servicedInfo.get("elec", 0.0)
+
+    return {
+        "ConnectorID": str(part.id),
+        "ConnectorElectricity": float("{:.1f}".format(float(elec)))
+    }
+
+
+def get_station_status(groupId):  # type:(DeviceDict, ShanDongNorther) -> dict
+    """
+    获取充电站的当前状态
+    """
+    ConnectorStatusInfos = list()
+    devNos = Device.get_devNos_by_group([groupId])
+    for devNo in devNos:
+        device = Device.get_dev(devNo)
+        parts = Part.objects.filter(logicalCode = device.logicalCode)
+        devCache = Device.get_dev_control_cache(devNo)
+
+        online = device.get("online", True)
+
+        for part in parts:
+            if device.get("devType", dict()).get("code") not in [
+                Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY,
+                Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY_V2
+            ]:
+                partNo = part.partNo
+                if partNo in ['allPorts','usedPorts','usePorts']:
+                    continue
+                portCache = devCache.get(part.partNo) or dict()
+            else:
+                portCache = devCache or dict()
+
+            # 判断端口当前状态
+            if not online:
+                status = 0
+            else:
+                status = 3 if portCache.get("isStart") else 1
+
+            data = {
+                "ConnectorID": str(part.id),
+                "Status": status,
+                "CurrentA": 0,
+                "VoltageA": 0,
+                "BeginTime": portCache.get("startTime","-"),  # 开始时间
+                "SOC": 0.0,  # 剩余电量
+                "CurrentKwh": 0.0,  # 已充电量
+                "CurrentMeter": 0.0,  # 当前电表读数
+                "BmsReqVoltage": 0.0,  # BMS需求电压
+                "BmsReqCurrent": 0.0  # BMS需求电流
+
+            }
+
+            ConnectorStatusInfos.append(data)
+
+    return ConnectorStatusInfos
+
+
+def get_policy_info(partId):
+    """
+    获取 端口的计费信息
+    :param partId:
+    :return:
+    """
+    DEFAULT_ELEC_PRICE = 1.500
+
+    ELEC_FUNCS = [get_elec_price_by_package, get_elec_price_by_conf,
+                  get_elec_price_by_consume]
+
+    part = Part.objects.filter(id = partId).first()
+    if not part:
+        return
+
+    devNo = Device.get_devNo_by_logicalCode(part.logicalCode)
+
+    for func in ELEC_FUNCS:
+        try:
+            elecPrice = func(devNo)
+        except Exception:
+            elecPrice = None
+
+        if elecPrice:
+            break
+    else:
+        elecPrice = DEFAULT_ELEC_PRICE
+
+    data = {
+        "StartTime": "000000",
+        "ElecPrice": elecPrice,
+        "SevicePrice": 0.0,
+        "DiscountElecPrice":elecPrice, # 协议电费价格
+        "DiscountServicePrice":0.0 # 协议服务费价格
+    }
+
+    return data
+
+def get_elec_price_by_package(devNo):
+    """
+    通过套餐获取电费
+    :param devNo:
+    :return:
+    """
+    device = Device.get_dev(devNo)
+    package = device.get("washConfig", dict()).get("1", dict())
+    if not package:
+        return
+
+    price = package.get("price")
+    time = package.get("time")
+    unit = package.get("unit")
+    if unit != u"度":
+        return
+    if not all([price, time]):
+        return
+    try:
+        elecPrice = float("{:.4f}".format(float(price) / float(time)))
+    except ZeroDivisionError:
+        return
+
+    return elecPrice
+
+
+def get_elec_price_by_conf(devNo):
+    """
+    通过设备设置设置电费
+    :param devNo:
+    :return:
+    """
+    device = Device.get_dev(devNo)
+    elecPrice = device.get("otherConf", dict()).get("elecPrice")
+    return float("{:.4f}".format(float(elecPrice)))
+
+def get_elec_price_by_consume(devNo):
+    """
+    通过 最近一次的消费记录获取电费
+    :param devNo:
+    :return:
+    """
+    record = ConsumeRecord.objects.filter(devNo=devNo).sort("-id").first()
+    if not record or not record.servicedInfo:
+        return
+
+    elec = record.servicedInfo.get("elec")
+    spend = record.servicedInfo.get("spend")
+
+    if not all([elec, spend]):
+        return
+
+    try:
+        elecPrice = float("{:.4f}".format(float(spend) / float(elec)))
+    except ZeroDivisionError:
+        return
+
+    return elecPrice

+ 372 - 0
apps/web/api/jn_north/views.py

@@ -0,0 +1,372 @@
+# coding=utf-8
+import json
+import logging
+import datetime
+
+from mongoengine import DoesNotExist
+
+from django.views.decorators.http import require_POST
+
+from apilib.utils_datetime import to_datetime
+from apilib.utils_json import JsonResponse
+from apps.web.api.jn_north.constant import RESPONSE_CODE
+from apps.web.api.jn_north.utils import get_stations_info,get_station_status,get_get_station_state,get_policy_info
+from apps.web.api.utils import AES_CBC_PKCS5padding_encrypt, AES_CBC_PKCS5padding_decrypt, generate_json_token, parse_json_token
+from apps.web.device.models import Group, Device
+from apps.web.south_intf.shangdong_platform import ShanDongNorther, GroupIdMap
+
+logger = logging.getLogger(__name__)
+
+
+@require_POST
+def queryToken(request):
+    """
+    通过账号密码获取token
+    """
+    logger.debug("[queryToken] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1001)"})
+
+    try:
+        Data = json.loads(request.body).get("Data")
+        data = json.loads(AES_CBC_PKCS5padding_decrypt(Data))
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1002)"})
+
+    OperatorID = data.get("OperatorID")
+    OperatorSecret = data.get("OperatorSecret")
+
+    logger.debug("[queryToken] OperatorID = {}, OperatorSecret = {}".format(OperatorID, OperatorSecret))
+
+    if not all((OperatorID, OperatorSecret)):
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1003)"})
+
+    try:
+        norther = ShanDongNorther.objects.filter(northOperatorID=OperatorID, northOperatorSecret=OperatorSecret).first()   # type: ShanDongNorther
+    except DoesNotExist:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1004)"})
+    except Exception as e:
+        return JsonResponse({"Ret": RESPONSE_CODE.SYS_ERROR, "Msg": u"系统错误"})
+
+    expire = 60 * 60 * 24 * 7
+    result = {
+        "OperatorID": norther.agentOperatorID,
+        "SuccStat": 0,
+        "AccessToken": generate_json_token(data=norther.get_token_data(), expire=expire),
+        "TokenAvailableTime": expire,
+        "FailReason": None
+    }
+    logger.debug("[queryToken] return result = {}".format(result))
+
+    # 拉取的时候加密 显式指明加密秘钥为 pull
+    resultData = AES_CBC_PKCS5padding_encrypt(
+        json.dumps(result),
+        dataSecret=norther.pullDataSecret,
+        dataSecretIV=norther.pullDataSecretIV
+    )
+    sig = norther.get_sig(resultData)
+
+    return JsonResponse({
+        "Ret": RESPONSE_CODE.SUCCESS,
+        "Msg": u"请求成功",
+        "Data": resultData,
+        "Sig": sig
+    })
+
+def queryStationsInfo(request):
+    """
+    查询充电站的信息
+    """
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[queryStationsInfo] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_TOKEN, "Msg": u"请求参数错误(1001)"})
+
+    # 获取这个平台下面的所有的northers记录
+    northers = ShanDongNorther.get_norther(**tokenData)
+    if not northers:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_PARAM, "Msg": u"请求参数错误(1002)"})
+
+    # 准备token所获取的参数信息
+    norther = northers.first()
+    pullDataSecret = norther.pullDataSecret
+    pullDataSecretIV = norther.pullDataSecretIV
+
+    # 这个平台下的所有对接过的经销商
+    dealerIds = [_.dealerId for _ in northers]
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1003)"})
+
+    try:
+        Data = json.loads(request.body).get("Data")
+        data = json.loads(AES_CBC_PKCS5padding_decrypt(
+            Data,
+            dataSecret=pullDataSecret,
+            dataSecretIV=pullDataSecretIV
+        ))
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1004)"})
+
+    # 分页以及查询参数
+    pageNo = int(data.get('PageNo', 1))
+    pageSize = int(data.get('PageSize', 10))
+    lastQueryTime = data.get("LastQueryTime")
+
+    # 查找出所有符合条件的地质组的信息
+    query = Group.objects.filter(ownerId__in=dealerIds)
+    if lastQueryTime:
+        dateTime = to_datetime(lastQueryTime)
+        query = query.filter(dateTimeAdded__gte=dateTime)
+
+    # 过滤出有设备的地址组
+    StationInfos = list()
+    groupIds = [str(item.id) for item in query.only("id") if Device.objects.filter(groupId = str(item.id)).count()]
+    groupQuery = Group.objects.filter(id__in=groupIds).only("id").paginate(pageNo, pageSize)
+    for item in groupQuery:
+        # 获取站点信息
+        groupId = str(item.id)
+
+        # 我们的GroupID是24位,省平台的是20位,尝试添加映射
+        GroupIdMap.add(groupId)
+        StationInfos.append(get_stations_info(Group.get_group(item.id), norther))
+
+    result = {
+        "PageNo": pageNo,
+        "ItemSize": pageSize,
+        "PageCount": groupQuery.count(),
+        "StationInfos": StationInfos
+    }
+    logger.debug("[queryStationsInfo] return result = {}".format(result))
+
+    resultData = AES_CBC_PKCS5padding_encrypt(
+        json.dumps(result),
+        dataSecret=pullDataSecret,
+        dataSecretIV=pullDataSecretIV
+    )
+    sig = norther.get_sig(resultData)
+    logger.debug("groupId = {}".format(groupIds[0]))
+    return JsonResponse({
+        "Ret": 0,
+        "Msg": u"请求成功",
+        "Data": resultData,
+        "Sig": sig
+    })
+
+def queryStationStats(request):
+    """
+    取每个充电站在某个周期内的统计信息
+    :param request:
+    :return:
+    """
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[queryStationsInfo] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_TOKEN, "Msg": u"请求参数错误(1001)"})
+    northers = ShanDongNorther.get_norther(**tokenData)
+    if not northers:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_PARAM, "Msg": u"请求参数错误(1002)"})
+
+    # 准备token所获取的参数信息
+    norther = northers.first()
+    pullDataSecret = norther.pullDataSecret
+    pullDataSecretIV = norther.pullDataSecretIV
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1003)"})
+
+    try:
+        Data = json.loads(request.body).get("Data")
+        data = json.loads(AES_CBC_PKCS5padding_decrypt(
+            Data,
+            dataSecret=pullDataSecret,
+            dataSecretIV=pullDataSecretIV
+        ))
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1004)"})
+
+    stationID = data.get("StationID")
+    startTime = data.get("StartTime")
+    endTime = data.get("EndTime")
+
+    startTimeObj = datetime.datetime.strptime(startTime, "%Y-%m-%d")
+    endTimeObj = datetime.datetime.strptime(endTime, "%Y-%m-%d")
+
+    res = get_get_station_state(stationID,startTimeObj, endTimeObj)
+
+    res.update({
+        "StationID": stationID,
+        "StartTime": startTime,
+        "EndTime": endTime
+    })
+
+    result = {"StationStats": res}
+    resultData = AES_CBC_PKCS5padding_encrypt(
+        json.dumps(result),
+        dataSecret=pullDataSecret,
+        dataSecretIV=pullDataSecretIV
+    )
+    sig = norther.get_sig(resultData)
+    logger.debug("[queryStationStats] return result = {}".format(result))
+
+    return JsonResponse({
+        "Ret": 0,
+        "Msg": u"请求成功",
+        "Data": resultData,
+        "Sig": sig
+    })
+
+def queryStationStatus(request):
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[queryStationsInfo] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_TOKEN, "Msg": u"请求参数错误(1001)"})
+    northers = ShanDongNorther.get_norther(**tokenData)
+    if not northers:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_PARAM, "Msg": u"请求参数错误(1002)"})
+
+    # 准备token所获取的参数信息
+    norther = northers.first()
+    pullDataSecret = norther.pullDataSecret
+    pullDataSecretIV = norther.pullDataSecretIV
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1003)"})
+
+    try:
+        Data = json.loads(request.body).get("Data")
+        data = json.loads(AES_CBC_PKCS5padding_decrypt(
+            Data,
+            dataSecret=pullDataSecret,
+            dataSecretIV=pullDataSecretIV
+        ))
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1004)"})
+
+    stationIDs = data.get("StationIDs")
+    if not isinstance(stationIDs, list):
+        return JsonResponse({"Ret": 4004, "Msg": u"系统错误"})
+
+    StationStatusInfos = list()
+    for stationID in stationIDs:
+        groupId = GroupIdMap.get_groupId(stationID)
+
+        StationStatusInfos.append(
+            {
+                "StationID": stationID,
+                "ConnectorStatusInfos": get_station_status(groupId)
+            }
+        )
+
+    result = {
+        "StationStatusInfos": StationStatusInfos
+    }
+    resultData = AES_CBC_PKCS5padding_encrypt(
+        json.dumps(result),
+        dataSecret=pullDataSecret,
+        dataSecretIV=pullDataSecretIV
+    )
+    sig = norther.get_sig(resultData)
+    logger.debug("[queryStationStatus] return result = {}".format(result))
+
+    return JsonResponse({
+        "Ret": 0,
+        "Msg": u"请求成功",
+        "Data": resultData,
+        "Sig": sig
+    })
+
+def queryEquipBusinessPolicy(request):
+    """
+    用于查询运营商的充电设备接口计费模型信息
+    :param request:
+    :return:
+    """
+    token = request.META.get('HTTP_AUTHORIZATION', "").replace("Bearer", "").strip()
+    logger.info('[queryStationsInfo] , token = {}'.format(token))
+
+    # 验证身份
+    tokenData = parse_json_token(token)
+    if not tokenData:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_TOKEN, "Msg": u"请求参数错误(1001)"})
+    northers = ShanDongNorther.get_norther(**tokenData)
+    if not northers:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_PARAM, "Msg": u"请求参数错误(1002)"})
+
+    # 准备token所获取的参数信息
+    norther = northers.first()
+    pullDataSecret = norther.pullDataSecret
+    pullDataSecretIV = norther.pullDataSecretIV
+
+    # 验证参数
+    logger.debug("[queryStationsInfo] request body = {}".format(request.body))
+
+    if not request.body:
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1003)"})
+
+    try:
+        Data = json.loads(request.body).get("Data")
+        data = json.loads(AES_CBC_PKCS5padding_decrypt(
+            Data,
+            dataSecret=pullDataSecret,
+            dataSecretIV=pullDataSecretIV
+        ))
+    except Exception as e:
+        logger.exception(e)
+        return JsonResponse({"Ret": RESPONSE_CODE.ERROR_POST, "Msg": u"请求参数错误(1004)"})
+
+    equipBizSeq = data.get("EquipBizSeq")
+    connectorID = data.get("ConnectorID")
+
+    policyInfo = get_policy_info(connectorID)
+
+    result = {
+        "EquipBizSeq": equipBizSeq,
+        "ConnectorID": connectorID,
+        "SuccStat": 0 if policyInfo else 1,
+        "FailReason": 0 if policyInfo else 1,
+        "SumPeriod": 1,
+        "PolicyInfos": [policyInfo] if policyInfo else "-"
+    }
+    resultData = AES_CBC_PKCS5padding_encrypt(
+        json.dumps(result),
+        dataSecret=pullDataSecret,
+        dataSecretIV=pullDataSecretIV
+    )
+    sig = norther.get_sig(resultData)
+
+    logger.debug("[queryEquipBusinessPolicy] return result = {}".format(result))
+
+    return JsonResponse({
+        "Ret": 0,
+        "Msg": u"请求成功",
+        "Data": resultData,
+        'Sig':sig
+    })
+
+
+
+

+ 192 - 0
apps/web/api/models.py

@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import datetime
+import logging
+
+from mongoengine import BooleanField, StringField, ListField, FloatField, DictField, DateTimeField
+from mongoengine.fields import IntField
+from typing import Optional
+
+from apps.web.constant import Const, ErrorCode
+from apps.web.core.db import Searchable
+
+logger = logging.getLogger(__name__)
+
+
+class DeviceVersionConfig(Searchable):
+    moduleName = StringField(verbose_name = u'设备模块名称', default = '')
+    version = StringField(verbose_name = u'最新版本号', default = '')
+    matchVersions = ListField(verbose_name = u'匹配版本号', default = [])
+    filePath = StringField(verbose_name = u'程序路径', default = '')
+    allowable = BooleanField(verbose_name = u'是否允许升级', default = False)
+
+    meta = {'collection': 'device_upgrade_config', 'db_alias': 'logdata'}
+
+
+class APIServiceStartRecord(Searchable):
+    orderNo = StringField(verbose_name = u'订单编号', default = None, unique = True)
+    devNo = StringField(verbose_name = u'设备编号', default = u'')
+    logicalCode = StringField(verbose_name = u'二维码编号', default = u'')
+    port = StringField(verbose_name = u'端口号', default = u'')
+    money = FloatField(verbose_name = u'消费金额', default = None)
+    needTime = IntField(verbose_name = u'订购时长', default = None)
+    consumeType = StringField(verbose_name = u'启动方式', default = u'')
+    openId = StringField(verbose_name = u'openId', default = u'')
+    cardNo = StringField(verbose_name = u'离线卡号', default = u'')
+    status = StringField(verbose_name = u'订单状态', default = u'')
+    startTime = StringField(verbose_name = u'订单开始时间', default = None)
+    finishReason = StringField(verbose_name = u'订单结束原因', default = u'')
+    leftTime = IntField(verbose_name = u'剩余时间', default = None)
+    duration = IntField(verbose_name = u'使用时长', default = None)
+    spendElec = FloatField(verbose_name = u'消耗电量', default = None)
+    endTime = StringField(verbose_name = u'订单结束时间', default = None)
+
+    extOrderNo = StringField(verbose_name = u'外部订单编号', default = u'')
+
+    meta = {'collection': 'api_service_start_record', 'db_alias': 'default'}
+
+    search_fields = ('openId', 'devNo', 'port', 'status', 'cardNo', 'consumeType')
+
+    @classmethod
+    def record_4_cy4_card(cls, data):
+        if data.get('orderNo', '') != '':
+            cls(
+                orderNo = data['orderNo'],
+                devNo = data['devNo'],
+                logicalCode = data['logicalCode'],
+                port = data['port'],
+                money = data['money'],
+                needTime = data['needTime'],
+                consumeType = data['consumeType'],
+                openId = data['openId'],
+                cardNo = data['cardNo'],
+                status = data['status'],
+                startTime = data['startTime']
+            ).save()
+        else:
+            apiRecord = cls.objects(
+                status = 'PENDING',
+                port = data['port'],
+                devNo = data['devNo'],
+                cardNo = data['cardNo'],
+                startTime__gte = (datetime.datetime.now() - datetime.timedelta(days = 2)).strftime(Const.DATE_FMT)
+            ).order_by('-startTime').first()
+
+            if apiRecord is None:
+                return
+
+            apiRecord.finishReason = data['finishReason']
+            apiRecord.leftTime = data['leftTime']
+            apiRecord.spendElec = data['spendElec']
+            apiRecord.status = data['status']
+            apiRecord.duration = data['duration']
+            apiRecord.endTime = data['endTime']
+            apiRecord.save()
+
+    @classmethod
+    def record_4_cy4_mobile(cls, data):
+        if data.get('orderNo', '') != '':
+            cls(
+                orderNo = data['orderNo'],
+                devNo = data['devNo'],
+                logicalCode = data['logicalCode'],
+                port = data['port'],
+                money = data['money'],
+                needTime = data['needTime'],
+                consumeType = data['consumeType'],
+                status = data['status'],
+                startTime = data['startTime'],
+                extOrderNo = data['extOrderNo']
+            ).save()
+        else:
+            apiRecord = cls.objects(
+                status = 'PENDING',
+                port = data['port'],
+                devNo = data['devNo'],
+                startTime__gte = (datetime.datetime.now() - datetime.timedelta(days = 2)).strftime(Const.DATE_FMT)
+            ).order_by('-startTime').first()
+
+            if apiRecord is None:
+                return
+
+            apiRecord.finishReason = data['finishReason']
+            apiRecord.leftTime = data['leftTime']
+            apiRecord.spendElec = data['spendElec']
+            apiRecord.status = data['status']
+            apiRecord.duration = data['duration']
+            apiRecord.endTime = data['endTime']
+            apiRecord.save()
+
+
+class APIStartDeviceRecord(Searchable):
+    orderNo = StringField(verbose_name = "订单号(extOrderNo)", default = "", max_length = 100)
+    userId = StringField(verbose_name = "用户ID", default = "")
+    ownerId = StringField(verbose_name = '经销商ID', default = "")
+    createTime = StringField(verbose_name = "调用方创建订单时间", default = "")
+    channel = StringField(verbose_name = "channel")
+    deviceCode = StringField(verbose_name = "设备逻辑编码(=logicalCode)", default = "")
+    devNo = StringField(verbose_name = "设备ID", default = "")
+    package = DictField(verbose_name = "套餐", default = {})
+
+    notifyUrl = StringField(verbose_name = "回调通知地址", default = '')
+
+    apiConf = DictField(verbose_name = "api配置", default = {})
+
+    postActionResult = DictField(verbose_name = 'API后续处理', default = {})
+    postActionTriggered = BooleanField(default = False)
+
+    errCode = IntField(verbose_name = '错误码', default = ErrorCode.EXCEPTION)
+    errMsg = StringField(verbose_name = '错误描述', default = u'系统错误')
+
+    attachParas = DictField(verbose_name = '可变参数', default = {})
+    servicedInfo = DictField(verbose_name = '服务信息', default = {})
+
+    datetimeAdded = DateTimeField(verbose_name = "时间", default = datetime.datetime.now)
+
+    meta = {
+        "collection": "APIStartDeviceRecord",
+        "db_alias": "logdata",
+        # 'shard_key': ('orderNo',)
+    }
+
+    def __repr__(self):
+        return 'APIStartDeviceRecord<id={},orderNo={}> user({}) use api to start device({}) with package({})'.format(
+            str(self.id), self.orderNo, self.userId, self.deviceCode, self.package)
+
+    def __str__(self):
+        return self.__repr__()
+
+    @property
+    def used_port(self):
+        if self.attachParas and 'chargeIndex' in self.attachParas and self.attachParas['chargeIndex']:
+            return str(self.attachParas['chargeIndex'])
+
+        return ''
+
+    @property
+    def openId(self):
+        return self.userId
+
+    @classmethod
+    def get_api_record(cls, logicalCode, ext_order_no):
+        # type:(str, str)->Optional[APIStartDeviceRecord]
+        record = (cls.objects(orderNo = ext_order_no, deviceCode = logicalCode)
+                  .order_by('-createdTime')
+                  .first())  # type: APIStartDeviceRecord
+        return record
+
+    @property
+    def is_from_api(self):
+        return True
+
+    @property
+    def is_on_point(self):
+        return False
+
+    @property
+    def is_from_user(self):
+        return False
+
+    def my_package(self, device):
+        return dict(self.package)

+ 0 - 0
apps/web/api/openluat/__init__.py


+ 12 - 0
apps/web/api/openluat/urls.py

@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from django.conf.urls import patterns, url
+
+from .views import *
+
+urlpatterns = patterns('',
+                       # 合宙在1.2.2版本之前不能自定义升级路径
+                       url(r'^firmware_upgrade$', get_firmware_upgrade_file,
+                           name = 'get_firmware_upgrade_file'),
+                       )

+ 72 - 0
apps/web/api/openluat/views.py

@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+import os
+
+from django.http import StreamingHttpResponse
+from typing import TYPE_CHECKING
+
+from apilib.utils_json import JsonResponse
+from apps.web.api.models import DeviceVersionConfig
+from apps.web.device.models import Device
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    pass
+
+DEVICE_VERSION_BASE_PATH = '/var/www/uploaded/version'
+DEVICE_VERSION_BUFFER = dict()
+
+
+def get_firmware_upgrade_file(request):
+    def file_iterator(file_name, chunk_size = 512):
+        with open(file_name) as f:
+            while True:
+                c = f.read(chunk_size)
+                if c:
+                    yield c
+                else:
+                    break
+
+    project_key = request.GET.get('project_key')
+    dev_no = request.GET.get('imei')
+    version = str(request.GET.get('version'))
+
+    logger.debug('project key = {}; imei = {}; version = {}'.format(project_key, dev_no, version))
+
+    dev = Device.get_dev(dev_no)
+    if not dev:
+        logger.debug('{} is not exists.'.format(dev_no))
+        return JsonResponse({"payload": {}}, status = 404)
+
+    if project_key not in ['vivostone', 'viveston']:
+        logger.debug('project key {} is not mine.'.format(project_key))
+        return JsonResponse({"payload": {}}, status = 404)
+
+    tokens = version.split('.')
+    match_version = '{}.{}.'.format(tokens[0], tokens[1])
+
+    config = DeviceVersionConfig.objects(matchVersions = match_version,
+                                         allowable = True).first()  # type: DeviceVersionConfig
+    if not config:
+        logger.debug('config is not exits.')
+        return JsonResponse({"payload": {}}, status = 404)
+
+    version_path = '{}/{}'.format(DEVICE_VERSION_BASE_PATH, config.filePath).format(dev['driverCode'])
+    if version_path not in DEVICE_VERSION_BUFFER:
+        if not os.path.isfile(version_path):
+            return JsonResponse({"payload": {}}, status = 404)
+
+        content = file_iterator(version_path)
+        DEVICE_VERSION_BUFFER[version_path] = content
+    else:
+        content = DEVICE_VERSION_BUFFER[version_path]
+
+    response = StreamingHttpResponse(content)
+    response['Content-Type'] = 'application/octet-stream'
+    response['Content-Disposition'] = 'attachment;filename=%s' % version_path
+    response['Content-Length'] = os.path.getsize(version_path)
+
+    return response

+ 0 - 0
apps/web/api/swap/__init__.py


+ 22 - 0
apps/web/api/swap/urls.py

@@ -0,0 +1,22 @@
+# coding=utf-8
+
+from django.conf.urls import patterns, url
+
+from apps.web.api.swap.views import queryToken, queryStationsInfo, queryStationStats, queryStationStatus, \
+queryEquipAuth,queryEquipBusinessPolicy,query_start_charge,query_equip_charge_status,query_stop_charge
+
+
+urlpatterns = patterns('',*[
+    url(r'^wfl_kd/query_token$', queryToken, name = 'queryToken'),
+    url(r'^wfl_kd/query_stations_info$', queryStationsInfo, name='queryStationsInfo'),
+    url(r'^wfl_kd/query_station_stats$', queryStationStats, name='queryStationStats'),
+    url(r'^wfl_kd/query_station_status$', queryStationStatus, name='queryStationStatus'),
+    url(r'^wfl_kd/query_equip_auth$', queryEquipAuth, name='queryEquipAuth'),
+    
+    url(r'^wfl_kd/query_equip_business_policy$', queryEquipBusinessPolicy, name='queryEquipBusinessPolicy'),
+    
+    url(r'^wfl_kd/query_start_charge$', query_start_charge, name='query_start_charge'),
+    url(r'^wfl_kd/query_equip_charge_status$', query_equip_charge_status, name='query_equip_charge_status'),
+    url(r'^wfl_kd/query_stop_charge$', query_stop_charge, name='query_stop_charge'),
+
+])

+ 0 - 0
apps/web/api/swap/views.py


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio