mopybird 2 anni fa
parent
commit
7697345818

+ 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,
+        }
+
+

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

@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+from django.conf.urls import patterns, url
+
+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/zhejiang/__init__.py


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

@@ -0,0 +1,18 @@
+# coding=utf-8
+
+class EventDeviceCategory(object):
+    DEVICE = 1
+    PART = 2
+    OTHER = 999
+
+
+class FaultCategory(object):
+    POWER = 1
+    OFFLINE = 3
+    OTHER = 999
+
+
+class AlarmCategory(object):
+    TEMP = 1
+    SMOKE = 2
+    OTHER = 999

+ 5 - 0
apps/web/api/zhejiang/exceptions.py

@@ -0,0 +1,5 @@
+# coding=utf-8
+
+
+class ZheJiangNotifierException(Exception):
+    pass

+ 46 - 0
apps/web/api/zhejiang/models.py

@@ -0,0 +1,46 @@
+# coding=utf-8
+import datetime
+
+from mongoengine import StringField, DateTimeField, IntField, DictField
+
+from apps.web.core.db import Searchable
+from apps.web.dealer.models import Dealer
+
+
+class ZheJiangFireFight(Searchable):
+    """
+    德力西浙江玉环所需最小单位信息
+    """
+    ak = StringField(verbose_name=u"接入ID")
+    sk = StringField(verbose_name=u"接入匹配字段")
+    url = StringField(verbose_name=u"对接平台地址")
+
+    parentId = StringField(verbose_name=u"运营服务机构唯一代码")
+    dealerId = StringField(verbose_name=u"绑定的经销商")
+
+    companyName = StringField(verbose_name=u"联网单位名称")
+    companyCode = StringField(verbose_name=u"统一社会信用码")
+    address = StringField(verbose_name=u"地址信息")
+    regionCode = StringField(verbose_name=u"行政区编码")
+    companyCategory = StringField(verbose_name=u"单位类别", db_field="cCategory")
+    companyType = IntField(verbose_name=u"单位类型", db_field="cType")
+    industryType = StringField(verbose_name=u"行业类型", db_field="iType")
+    fireManager = StringField(verbose_name=u"消防安全管理人", db_field="FM")
+    fireManagerTel = StringField(verbose_name=u"消防安全管理人电话", db_field="FMT")
+    fireLiable = StringField(verbose_name=u"消防安全责任人", db_field="FL")
+    fireLiableTel = StringField(verbose_name=u"消防安全责任人电话", db_field="FLT")
+
+    deviceCreatorMap = DictField(verbose_name=u"设备厂家映射表code-厂家", default=dict)
+
+    createTime = DateTimeField(verbose_name=u"创建时间", default=datetime.datetime.now)
+    updateTime = DateTimeField(verbose_name=u"更新时间", default=datetime.datetime.now)
+
+    @property
+    def dealer(self):
+        dealer = getattr(self, "_dealer", None)
+        if not dealer:
+            dealer = Dealer.objects.get(id=self.dealerId)
+            setattr(self, "_dealer", dealer)
+
+        return dealer
+

+ 439 - 0
apps/web/api/zhejiang/utils.py

@@ -0,0 +1,439 @@
+# coding=utf-8
+import datetime
+import json
+import logging
+import base64
+import hmac
+import time
+import uuid
+
+import typing
+from hashlib import sha256
+from urlparse import urljoin
+
+import requests
+
+from apps.web.api.zhejiang.constant import EventDeviceCategory
+from apps.web.api.zhejiang.models import ZheJiangFireFight
+from apps.web.api.zhejiang.exceptions import ZheJiangNotifierException
+from apps.web.common.models import District
+
+if typing.TYPE_CHECKING:
+    from apps.web.device.models import DeviceDict, Part
+
+
+logger = logging.getLogger(__name__)
+
+
+def get_client_token(ak, sk, timestamp):
+    """
+    获取客户端的token
+    """
+    # 时间戳为毫秒数
+    timestamp *= 1000
+    ak = str(ak)
+    sk = str(sk)
+
+    logger.info("[get_client_token] ak = {}-{}, sk = {}-{}, timestamp = {}-{}".format(type(ak), ak, type(sk), sk, type(timestamp), timestamp))
+    # 获取认证字符串
+    authStr = base64.b64encode("{}{}".format(ak, timestamp))
+
+    # 获取认证摘要字符串 注意要是16进制的字符串
+    auth = hmac.new(key=sk, msg=authStr, digestmod=sha256).hexdigest()
+
+    # 获取clientToken
+    clientToken = base64.b64encode("{}:{}:{}".format(auth, ak, timestamp))
+    logger.info("[get_client_token] token = {}".format(clientToken))
+    return clientToken
+
+
+def verify_client_token(clientToken, ak, sk):
+    ak = str(ak)
+    sk = str(sk)
+
+    vAuth, vAk, vt = clientToken.split(":")
+
+    if ak != vAk:
+        return False
+
+    authStr = base64.b64encode("{}{}".format(ak, vt))
+    auth = base64.b64encode(hmac.new(key=sk, msg=authStr, digestmod=sha256).hexdigest())
+
+    if auth != vAuth:
+        return False
+
+    # 时间戳的单位是毫秒
+    if int(time.time() * 1000) > int(vt):
+        return False
+
+    return True
+
+
+def check_none_value(iterable):
+    """
+    检查必传参数是否有空值 注意和0以及空串区分开
+    """
+    return all(filter(lambda x: x is None, iterable))
+
+
+class ZheJiangNotifier(object):
+
+    URL = ""
+    EXPIRE_TIME = 1000
+
+    def __init__(self, ak, sk, pid, **kwargs):
+        self._ak = ak
+        self._sk = sk
+        self._pid = pid
+
+        if "url" in kwargs:
+            self._baseUrl = kwargs["url"]
+        else:
+            self._baseUrl = self.URL
+
+        if "expire" in kwargs:
+            self._expire = kwargs["expire"]
+        else:
+            self._expire = self.EXPIRE_TIME
+
+    def __str__(self):
+        return "[ZheJiangNotifier]-<{}>".format(self._pid)
+
+    def _get_header(self, **kwargs):
+        """
+        附加请求头
+        """
+        timeStamp = kwargs.pop("timeStamp", None) or (int(time.time()) + self._expire)
+        headers = {
+            "Client-Token": get_client_token(self._ak, self._sk, timeStamp),
+            "Content-Type": "application/json"
+        }
+
+        for _k, _v in kwargs.items():
+            headers[_k] = _v
+
+        return headers
+
+    def _request(self, **kwargs):
+        headers = kwargs.pop("headers", dict())
+        headers = self._get_header(**headers)
+
+        path = kwargs.pop("path")
+        url = urljoin(self._baseUrl, path)
+
+        # 找出操作数据的类型 1---修改或者新增 2----删除
+        payload = {
+            "opt_type": kwargs.pop("opt", 0),
+            "lists": kwargs["lists"]
+        }
+
+        logger.info("[ZheJiangNotifier _request], notifier = {}, headers = {}, url = {}, push payload = {}".format(self, headers, url, json.dumps(payload, indent=4)))
+        try:
+            response = requests.post(url=url, headers=headers, json=payload, verify=False, timeout=10)
+        except requests.Timeout:
+            raise ZheJiangNotifierException(u"请求超时")
+
+        return self._handle_response(response)
+
+    def _handle_response(self, response):
+        try:
+            response.raise_for_status()
+        except requests.RequestException as ree:
+            raise ZheJiangNotifierException(ree.message)
+
+        result = response.json()
+        logger.info("[ZheJiangNotifier _handle_response] notifier = {} result = {}".format(self, json.dumps(result, indent=4)))
+        # TODO 根据code的定义 有可能重新发出请求
+        code = result["code"]
+
+        return result
+
+    def push_company(self, *company):
+        """
+        推送公司信息 保持最小信息推送原则
+        """
+        lists = list()
+        for _item in company:
+            data = {
+                "company_id": _item.pop("companyId", None),
+                "company_name": _item.pop("companyName", None),
+                "parent_id": self._pid,
+                "company_code": _item.pop("companyCode", None),
+                "address": _item.pop("address", None),
+                "region_code": _item.pop("regionCode", None),
+                "company_category": _item.pop("companyCategory", None),
+                "company_type": _item.pop("companyType", None),
+                "industry_type": _item.pop("industryType", None),
+                "fire_manager": _item.pop("fireManager", None),
+                "fire_manager_tel": _item.pop("fireManagerTel", None),
+                "fire_liable": _item.pop("fireLiable", None),
+                "fire_liable_tel": _item.pop("fireLiableTel", None),
+                "create_time": _item.pop("createTime", None),
+                "update_time": _item.pop("updateTime", None),
+            }
+
+            if not check_none_value(data.values()):
+                raise ZheJiangNotifierException(u"推送单位参数错误")
+            lists.append(data)
+
+        logger.info("[ZheJiangNotifier push_company], notifier = {}, push data = {}".format(self, json.dumps(lists, indent=4)))
+        response = self._request(opt=0, lists=lists, path="fire/company/update")
+        return response
+
+    def push_device(self, *device):
+        """
+        推送设备的信息 推送设备类型固定为 小型充电桩(非汽车)
+        """
+        lists = list()
+
+        for _item in device:
+            data = {
+                "device_id": _item.pop("deviceId"),
+                "device_name": _item.pop("logicalCode"),
+                "parentId": self._pid,
+                "device_code": _item.pop("devNo"),
+                "location": _item.pop("location"),
+                "device_manufactory": _item.pop("creator"),
+                "device_type": "44",
+                "relation_type": "1",
+                "relation_id": _item.pop("companyId"),
+                "create_time": _item.pop("createTime"),
+                "update_time": _item.pop("updateTime")
+            }
+
+            if not check_none_value(data.values()):
+                raise ZheJiangNotifierException(u"推送设备参数错误")
+            lists.append(data)
+
+        logger.info("[ZheJiangNotifier push_device], notifier = {}, push data = {}".format(self, json.dumps(lists, indent=4)))
+        response = self._request(opt=0, lists=lists, path="fire/device/update")
+        return response
+
+    def push_part(self, *part):
+        """
+        推送部件的时候 推送部件类型固定为充电口
+        """
+        lists = list()
+
+        for _item in part:
+            data = {
+                "part_id": _item.pop("partId"),
+                "part_name": _item.pop("partName"),
+                "parent_id": self._pid,
+                "sensor_code": _item.pop("sensorCode"),
+                "address": _item.pop("address"),
+                "parts_type": "137",
+                "relation_type": "1",
+                "relation_id": _item.pop("companyId"),
+                "device_id": _item.pop("deviceId"),
+                "create_time": _item.pop("createTime"),
+                "update_time": _item.pop("updateTime")
+            }
+
+            if not check_none_value(data.values()):
+                raise ZheJiangNotifierException(u"推送部件参数错误")
+            lists.append(data)
+
+        logger.info("[ZheJiangNotifier push_part], notifier = {}, push data = {}".format(self, json.dumps(lists, indent=4)))
+        response = self._request(opt=0, lists=lists, path="fire/part/update")
+        return response
+
+    def push_device_state(self, *state):
+        """
+        推送设备的运行状态 应该是设备上线或者设备离线的时候使用
+        """
+        lists = list()
+
+        for _item in state:
+            data = {
+                "event_id": _item.pop("eventId"),
+                "device_category": int(_item.pop("deviceCategory")),
+                "device_id": _item.pop("deviceId"),
+                "parent_id": self._pid,
+                "online_status": int(_item.pop("onlineStatus")),
+                "work_status": int(_item.pop("workStatus")),
+                "event_time": _item.pop("eventTime")
+            }
+
+            if not check_none_value(data.values()):
+                raise ZheJiangNotifierException(u"推送设备运行状态错误")
+            lists.append(data)
+
+        logger.info("[ZheJiangNotifier push_device_state], notifier = {}, push data = {}".format(self, json.dumps(lists, indent=4)))
+        response = self._request(opt=0, lists=lists, path="fire/devicestate/report")
+        return response
+
+    def push_fault(self, **kwargs):
+        """
+        推送故障信息
+        """
+        data = {
+            "event_id": kwargs.pop("eventId"),
+            "device_category": int(kwargs.pop("deviceCategory")),
+            "device_id": kwargs.pop("deviceId"),
+            "parent_id": self._pid,
+            "fault_type": int(kwargs.pop("faultType")),
+            "event_time": kwargs.pop("eventTime")
+        }
+
+        logger.info("[ZheJiangNotifier push_fault], notifier = {}, push data = {}".format(self, json.dumps(data, indent=4)))
+
+        if not check_none_value(data.values()):
+            raise ZheJiangNotifierException(u"推送设备故障状态错误,缺少参数")
+        response = self._request(opt=0, lists=[data], path="fire/fault/report")
+        return response
+
+    def push_fault_handle(self, **kwargs):
+        data = {
+            "event_id": kwargs.pop("eventId"),
+            "device_category": int(kwargs.pop("deviceCategory")),
+            "device_id": kwargs.pop("deviceId"),
+            "parent_id": self._pid,
+            "fault_type": int(kwargs.pop("faultType")),
+            "happen_time": kwargs.pop("happenTime"),
+            "process_type": kwargs.pop("processType")
+        }
+
+        if data["process_type"] == "0":
+            data.update({
+                "fault_content": kwargs.pop("faultContent"),
+                "reportperson_name": kwargs.pop("reportpersonName")
+            })
+
+        logger.info("[ZheJiangNotifier push_fault_handle], notifier = {}, push data = {}".format(self, json.dumps(data, indent=4)))
+
+        if not check_none_value(data.values()):
+            raise ZheJiangNotifierException(u"推送设备信息故障操作信息失败,缺少参数")
+        response = self._request(opt=0, lists=[data], path="fire/faultprocess/report")
+        return response
+
+    def push_alarm(self, **kwargs):
+        """
+        推送火警信息
+        """
+        data = {
+            "event_id": kwargs.pop("eventId"),
+            "device_category": int(kwargs.pop("deviceCategory")),
+            "device_id": kwargs.pop("deviceId"),
+            "parent_id": self._pid,
+            "alarm_type": kwargs.pop("alarmType"),
+            "event_time": kwargs.pop("eventTime")
+        }
+
+        logger.info("[ZheJiangNotifier push_alarm], notifier = {}, push data = {}".format(self, json.dumps(data, indent=4)))
+
+        if not check_none_value(data.values()):
+            raise ZheJiangNotifierException(u"推送设备火警状态错误,缺少参数")
+        response = self._request(opt=0, lists=[data], path="fire/firealarm/report")
+        return response
+
+    def push_alarm_handle(self, **kwargs):
+        data = {
+            "event_id": kwargs.pop("eventId"),
+            "device_category": int(kwargs.pop("deviceCategory")),
+            "device_id": kwargs.pop("deviceId"),
+            "parent_id": self._pid,
+            "alarm_type": int(kwargs.pop("alarmType")),
+            "check_time": kwargs.pop("checkTime"),
+            "handle_time": kwargs.pop("handleTime"),
+            "process_type": kwargs.pop("processType"),
+            "handle_status": kwargs.pop("handleStatus")
+        }
+
+        logger.info("[ZheJiangNotifier push_alarm_handle], notifier = {}, push data = {}".format(self, json.dumps(data, indent=4)))
+
+        if not check_none_value(data.values()):
+            raise ZheJiangNotifierException(u"推送设备火警信息故障操作信息失败,缺少参数")
+        response = self._request(opt=0, lists=[data], path="fire/firealarmprocess/report")
+        return response
+
+
+class ZheJiangNorther(object):
+
+    def __init__(self, fight):   # type:(ZheJiangFireFight) -> None
+        self._firefight = fight
+        self._notifier = ZheJiangNotifier(
+            ak=fight.ak, sk=fight.sk, pid=fight.parentId, url=fight.url
+        )
+
+    def _report_company(self):
+        self._notifier.push_company({
+            "companyId": str(self._firefight.id),
+            "companyName": self._firefight.companyName,
+            "companyCode": self._firefight.companyCode,
+            "address": self._firefight.address,
+            "regionCode": self._firefight.regionCode,
+            "companyCategory": self._firefight.companyCategory,
+            "companyType": self._firefight.companyType,
+            "industryType": self._firefight.industryType,
+            "fireManager": self._firefight.fireManager,
+            "fireManagerTel": self._firefight.fireManagerTel,
+            "fireLiable": self._firefight.fireLiable,
+            "fireLiableTel": self._firefight.fireLiableTel,
+            "createTime": self._firefight.createTime.strftime("%Y-%m-%d %H:%M:%S"),
+            "updateTime": self._firefight.updateTime.strftime("%Y-%m-%d %H:%M:%S")
+        })
+
+    def _report_devices(self):
+        dealer = self._firefight.dealer
+        devices = dealer.get_own_devices()
+
+        dataList = list()
+        dataStateList = list()
+
+        # 逐条推送设备的信息
+        for _dev in devices:    # type: DeviceDict
+            devObj = _dev.my_obj
+
+            dataList.append({
+                "deviceId": str(devObj.id),
+                "logicalCode": _dev.logicalCode,
+                "devNo": _dev.devNo,
+                "location": District.get_district(devObj.districtId),
+                "creator": devObj.mf,
+                "companyId": str(self._firefight.id),
+                "createTime": _dev.dateTimeBinded,
+                "updateTime": _dev.lastRegisterTime
+            })
+
+            # 同时推送一次设备的状态
+            dataStateList.append({
+                "eventId": "".join(str(uuid.uuid4()).split("-")),
+                "deviceCategory": EventDeviceCategory.DEVICE,
+                "deviceId": str(devObj.id),
+                "onlineStatus": 1,
+                "workStatus": 1,
+                "eventTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+            })
+
+        self._notifier.push_device(*dataList)
+        self._notifier.push_device_state(*dataStateList)
+
+    def _report_parts(self):
+        dealer = self._firefight.dealer
+        devices = dealer.get_own_devices()
+
+        for _dev in devices:    # type: DeviceDict
+            _parts = _dev.parts
+
+            partsList = list()
+            for _part in _parts:    # type: Part
+                partsList.append({
+                    "partId": str(_part.id),
+                    "partName": _part.partName,
+                    "sensorCode": "",
+                    "address": "",
+                    "companyId": str(self._firefight.id),
+                    "deviceId": _dev.id,
+                    "createTime": _part.dateTimeAdded.strftime("%Y-%m-%d %H:%M:%S"),
+                    "updateTime": _part.dateTimeUpdated.strftime("%Y-%m-%d %H:%M:%S")
+                })
+
+            self._notifier.push_part(*partsList)
+
+    def report(self):
+        self._report_company()
+
+        self._report_devices()
+
+        self._report_parts()

+ 26 - 0
apps/web/core/bridge/wechat/WechatClientV3.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# !/usr/bin/env python
+
+import logging
+
+from typing import TYPE_CHECKING, Optional
+
+from library.wechatpayv3 import WechatClientV3
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+    from apps.web.core.models import WechatPayApp, WechatChannelApp
+
+
+class MyWechatClientV3(WechatClientV3):
+    def __init__(self, app):
+        # type: (Optional[WechatPayApp, WechatChannelApp])->None
+
+        super(MyWechatClientV3, self).__init__(
+            appid = app.appid,
+            mchid = app.mchid,
+            private_key = app.ssl_key,
+            cert_serial_no = app.app_serial_number,
+            apiv3_key = app.apikey_v3,
+            certificate_str_list = app.platform_certificates)