Browse Source

新增短信国内使用微瞳、设备电池电量上报、定制客户获取UID

zhangdongming 3 months ago
parent
commit
a4df9729db

+ 31 - 0
AgentModel/models.py

@@ -186,3 +186,34 @@ class ApplyAgent(models.Model):
         verbose_name = '申请代理表'
         verbose_name = '申请代理表'
         verbose_name_plural = verbose_name
         verbose_name_plural = verbose_name
         app_label = 'AgentModel'
         app_label = 'AgentModel'
+
+
+class CustomUIDPool(models.Model):
+    id = models.AutoField(primary_key=True, verbose_name='自增标记ID')
+    uid = models.CharField(default='', db_index=True, max_length=32, unique=True, verbose_name='设备ID')
+    type = models.SmallIntegerField(default=1, verbose_name=u'外销客户UID')
+    customer_name = models.CharField(default='', max_length=32, verbose_name='客户id')
+    status = models.IntegerField(default=0, verbose_name='0:未绑,1:绑定,2:弃用不再使用')
+    created_time = models.IntegerField(default=0, verbose_name='创建时间')
+    updated_time = models.IntegerField(default=0, verbose_name='更新时间')
+
+    class Meta:
+        db_table = 'custom_uid_pool'
+        verbose_name = '定制UID池'
+        verbose_name_plural = verbose_name
+        app_label = 'AgentModel'
+
+
+class DeviceCustomUID(models.Model):
+    id = models.AutoField(primary_key=True, verbose_name='自增标记ID')
+    uid = models.CharField(default='', db_index=True, max_length=32, unique=True, verbose_name='设备ID')
+    device_mac = models.CharField(default='', db_index=True, max_length=64, unique=True, verbose_name='mac地址')
+    status = models.IntegerField(default=0, verbose_name='1:绑定,2:失败')
+    created_time = models.IntegerField(default=0, verbose_name='创建时间')
+    updated_time = models.IntegerField(default=0, verbose_name='更新时间')
+
+    class Meta:
+        db_table = 'device_custom_uid'
+        verbose_name = '设备关联定制UID'
+        verbose_name_plural = verbose_name
+        app_label = 'AgentModel'

+ 2 - 0
Ansjer/server_urls/algorithm_shop_url.py

@@ -12,12 +12,14 @@ from AdminController import AlgorithmShopManageController
 from Controller.AlgorithmShop import AlgorithmShopController
 from Controller.AlgorithmShop import AlgorithmShopController
 from Controller.CloudPhoto import CloudPhotoController
 from Controller.CloudPhoto import CloudPhotoController
 from Controller.Cron import CronCloudPhotoController
 from Controller.Cron import CronCloudPhotoController
+from Controller.UserDevice import DeviceReportController
 
 
 urlpatterns = [
 urlpatterns = [
     re_path(r'^api/(?P<operation>.*)$', AlgorithmShopController.AlgorithmShopView.as_view()),
     re_path(r'^api/(?P<operation>.*)$', AlgorithmShopController.AlgorithmShopView.as_view()),
     re_path(r'^cron/(?P<operation>.*)$', CronCloudPhotoController.CronCloudPhotoView.as_view()),
     re_path(r'^cron/(?P<operation>.*)$', CronCloudPhotoController.CronCloudPhotoView.as_view()),
     re_path(r'^photo/(?P<operation>.*)$', CloudPhotoController.CronCloudPhotoView.as_view()),
     re_path(r'^photo/(?P<operation>.*)$', CloudPhotoController.CronCloudPhotoView.as_view()),
     re_path(r'^open/(?P<operation>.*)$', AlgorithmShopController.AlgorithmShopView.as_view()),
     re_path(r'^open/(?P<operation>.*)$', AlgorithmShopController.AlgorithmShopView.as_view()),
+    re_path(r'^report/(?P<operation>.*)$', DeviceReportController.DeviceReportView.as_view()),
     re_path(r'^(?P<apiVersion>[a-zA-Z0-9]+)/manage/(?P<operation>.*)$',
     re_path(r'^(?P<apiVersion>[a-zA-Z0-9]+)/manage/(?P<operation>.*)$',
             AlgorithmShopManageController.AlgorithmShopManageView.as_view()),
             AlgorithmShopManageController.AlgorithmShopManageView.as_view()),
 
 

+ 2 - 1
Ansjer/urls.py

@@ -31,7 +31,7 @@ from Controller.CustomCustomer import CustomCustomerController
 from Controller.MessagePush import EquipmentMessagePush
 from Controller.MessagePush import EquipmentMessagePush
 from Controller.Surveys import CloudStorageController
 from Controller.Surveys import CloudStorageController
 from Controller.UserDevice import UserSubscriptionController, DeviceVersionInfoController, APNConfigController, \
 from Controller.UserDevice import UserSubscriptionController, DeviceVersionInfoController, APNConfigController, \
-    ActivityCenterController
+    ActivityCenterController, DeviceCustomUIDController
 from Controller.CampaignController import AdDepartmentController
 from Controller.CampaignController import AdDepartmentController
 from Controller.SensorGateway import SensorGatewayController, EquipmentFamilyController
 from Controller.SensorGateway import SensorGatewayController, EquipmentFamilyController
 from django.urls import include
 from django.urls import include
@@ -391,6 +391,7 @@ urlpatterns = [
     re_path('open/device/configuration/(?P<operation>.*)', DeviceVersionInfoController.DeviceVersionInfoView.as_view()),
     re_path('open/device/configuration/(?P<operation>.*)', DeviceVersionInfoController.DeviceVersionInfoView.as_view()),
     # 获取APN配置信息
     # 获取APN配置信息
     re_path('APNConfig/(?P<operation>.*)', APNConfigController.APNConfigView.as_view()),
     re_path('APNConfig/(?P<operation>.*)', APNConfigController.APNConfigView.as_view()),
+    re_path(r'^api/device/custom/(?P<operation>.*)$', DeviceCustomUIDController.DeviceCustomUIDView.as_view()),
     # roomumy
     # roomumy
     re_path(r'^roomumy/', include("Roomumy.server_urls.roomumy_url")),
     re_path(r'^roomumy/', include("Roomumy.server_urls.roomumy_url")),
 
 

+ 157 - 0
Controller/UserDevice/DeviceCustomUIDController.py

@@ -0,0 +1,157 @@
+import json
+import time
+
+from django.db import transaction
+from django.http import QueryDict
+from django.views import View
+
+from AgentModel.models import DeviceCustomUID, CustomUIDPool
+from Ansjer.config import LOGGER
+from Model.models import LogModel
+from Object.RedisObject import RedisObject
+from Object.ResponseObject import ResponseObject
+from Service.CommonService import CommonService
+
+
+class DeviceCustomUIDView(View):
+    def get(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        return self.validation(request.GET, request, operation)
+
+    def post(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        return self.validation(request.POST, request, operation)
+
+    def delete(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        delete = QueryDict(request.body)
+        if not delete:
+            delete = request.GET
+        return self.validation(delete, request, operation)
+
+    def put(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        put = QueryDict(request.body)
+        return self.validation(put, request, operation)
+
+    def validation(self, request_dict, request, operation):
+        response = ResponseObject('en')
+        if operation == 'getUID':  # 获取电池电量列表
+            return self.get_custom_uid(request, request_dict, response)
+
+    @staticmethod
+    def get_custom_uid(request, request_dict, response):
+        ip = CommonService.get_ip_address(request)
+        LOGGER.info(f'获取定制客户UID:{request_dict},ip:{ip}')
+        mac = request_dict.get('mac')
+        token = request_dict.get('token')
+        time_stamp = request_dict.get('time_stamp')
+
+        # 参数校验
+        if not all([mac, token, time_stamp]):
+            LOGGER.error(f'{mac}请求绑定uid参数缺失')
+            return response.json(444)
+
+        # 时间戳token校验
+        if not CommonService.check_time_stamp_token(token, time_stamp):
+            LOGGER.error(f'{mac}时间戳校验失败time:{time_stamp},tk:{token}')
+            return response.json(13)
+
+        redis = RedisObject(db=3)
+        lock_key = f"MAC:ASSIGN:{mac}"
+        request_id = f"{mac}_{int(time.time())}"
+
+        try:
+            # 预检查绑定状态(缓存优化)
+            cached_uid = redis.get_data(f"DEVICE:MAC:{mac}")
+            if cached_uid:
+                return response.json(0, data={'uid': cached_uid})
+
+            # 分布式锁配置(增加锁持有检测)
+            acquired = False
+            for _ in range(3):
+                if redis.try_lock(lock_key, request_id, expire=5, time_unit_second=1):
+                    acquired = True
+                    break
+                time.sleep(0.3)
+            if not acquired:
+                LOGGER.warning(f'{mac}系统繁忙请稍后重试')
+                return response.json(5)
+            with transaction.atomic():
+                # 二次检查(带数据库查询)
+                existing = DeviceCustomUID.objects.filter(
+                    device_mac=mac,
+                    status=1
+                ).only('uid').first()
+                if existing:
+                    # 更新缓存
+                    redis.set_ex_data(f"DEVICE:MAC:{mac}", existing.uid, 10)
+                    return response.json(0, {'uid': existing.uid})
+                count = 0
+                while count < 5:
+                    available_uid = (
+                        CustomUIDPool.objects
+                            .filter(status=0)
+                            .order_by('updated_time')
+                            .first()
+                    )
+
+                    if not available_uid:
+                        LOGGER.error(f'{mac}UID池资源耗尽')
+                        return response.json(173)
+                    # 查询到未被绑定的UID进行redis加锁
+                    uid_lock = redis.try_lock(f"DEVICE:UID:{available_uid.uid}",
+                                              request_id, expire=3, time_unit_second=1)
+                    if not uid_lock:
+                        count += 1
+                        continue
+                    # 原子操作更新
+                    rows_updated = CustomUIDPool.objects.filter(
+                        id=available_uid.id,
+                        status=0  # 乐观锁校验
+                    ).update(
+                        status=1,
+                        updated_time=int(time.time())
+                    )
+
+                    if int(rows_updated) <= 0:  # 更新失败
+                        count += 1
+                        continue
+
+                    if not rows_updated:
+                        LOGGER.warning(f'{mac}UID并发竞争更新失败')
+                        return response.json(14)
+                    # 创建设备关联记录
+                    DeviceCustomUID.objects.create(
+                        device_mac=mac,
+                        uid=available_uid.uid,
+                        status=1,
+                        updated_time=int(time.time()),
+                        created_time=int(time.time())
+                    )
+
+                    content = json.loads(json.dumps(request_dict))
+                    log = {
+                        'ip': ip,
+                        'user_id': 1,
+                        'status': 200,
+                        'time': int(time.time()),
+                        'content': json.dumps(content),
+                        'url': 'api/device/custom/getUID',
+                        'operation': '定制设备mac{}绑定uid: {}'.format(mac, available_uid.uid),
+                    }
+                    LogModel.objects.create(**log)
+
+                    # 设置缓存
+                    redis.set_ex_data(f"MAC_UID_CACHE:{mac}", available_uid.uid, 10)
+                    return response.json(0, {'uid': available_uid.uid})
+
+        except Exception as e:
+            LOGGER.error(f'mac:{mac}系统异常: {str(e)}', exc_info=True)
+            return response.json(500)
+        finally:
+            redis.release_lock(lock_key, request_id)

+ 184 - 0
Controller/UserDevice/DeviceReportController.py

@@ -0,0 +1,184 @@
+# -*- encoding: utf-8 -*-
+"""
+@File    : DeviceReportController.py
+@Time    : 2025/4/3 16:20
+@Author  : stephen
+@Email   : zhangdongming@asj6.wecom.work
+@Software: PyCharm
+"""
+import time
+from datetime import timedelta, datetime
+
+import pytz
+from django.db.models import Sum, Value
+from django.db.models.functions import Coalesce
+from django.http import QueryDict
+from django.views import View
+
+from Ansjer.config import LOGGER
+from Model.models import DeviceDailyReport
+from Object.ResponseObject import ResponseObject
+from Object.TokenObject import TokenObject
+
+
+class DeviceReportView(View):
+    def get(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        return self.validation(request.GET, request, operation)
+
+    def post(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        return self.validation(request.POST, request, operation)
+
+    def delete(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        delete = QueryDict(request.body)
+        if not delete:
+            delete = request.GET
+        return self.validation(delete, request, operation)
+
+    def put(self, request, *args, **kwargs):
+        request.encoding = 'utf-8'
+        operation = kwargs.get('operation')
+        put = QueryDict(request.body)
+        return self.validation(put, request, operation)
+
+    def validation(self, request_dict, request, operation):
+        response = ResponseObject('cn')
+        tko = TokenObject(request.META.get('HTTP_AUTHORIZATION'))
+        if tko.code != 0:
+            return response.json(tko.code)
+        response.lang = tko.lang
+        userID = tko.userID
+        if operation == 'getBatteryCapacityList':  # 获取电池电量列表
+            return self.get_battery_capacity_list(userID, request, request_dict, response)
+        elif operation == 'getWakeSleepData':
+            return self.get_wake_sleep_data(userID, request_dict, response)
+
+    @classmethod
+    def get_wake_sleep_data(cls, userID, request_dict, response):
+        """
+        获取设备睡眠唤醒统计数据
+        :param userID: 响应对象
+        :param request_dict: 参数对象
+        :param response: 响应对象
+        :return: 包含7天汇总统计和30天明细数据的JSON响应
+        """
+        try:
+            # region 时间参数计算
+            uid = request_dict.get('device_id', None)
+            if not uid:
+                return response.json(444)
+            current_time = int(time.time())
+            SECONDS_PER_DAY = 24 * 60 * 60  # 单日秒数常量
+
+            # 计算时间范围(秒级时间戳)
+            seven_days_ago = current_time - 7 * SECONDS_PER_DAY  # 7天前
+            thirty_days_ago = current_time - 30 * SECONDS_PER_DAY  # 30天前
+            # endregion
+
+            # region 7天汇总统计 (使用Coalesce避免空值)
+
+            seven_days_stats = DeviceDailyReport.objects.filter(
+                device_id=uid,
+                report_time__range=(seven_days_ago, current_time)  # 时间范围查询
+            ).aggregate(
+                total_human_detection=Coalesce(Sum('human_detection'), Value(0)),
+                total_working_hours=Coalesce(Sum('working_hours'), Value(0)),
+                total_wake_sleep=Coalesce(Sum('wake_sleep'), Value(0))
+            )
+            # endregion
+
+            # region 30天明细数据(按时间倒序)
+            thirty_days_details = DeviceDailyReport.objects.filter(
+                device_id=uid,
+                report_time__gte=thirty_days_ago,
+                report_time__lte=current_time
+            ).order_by('-report_time').values_list(
+                'human_detection',
+                'working_hours',
+                'wake_sleep',
+                'battery_level',
+                'report_time',
+                named=True  # 使用命名元组方便访问
+            )
+            # endregion
+
+            # 组合返回数据结构
+            result_data = {
+                **seven_days_stats,
+                'detail_report': list(thirty_days_details)  # 转换查询集为列表
+            }
+
+            return response.json(0, result_data)
+
+        except Exception as e:
+            error_msg = f"查询异常: {str(e)},userID: {userID}"
+            error_line = e.__traceback__.tb_lineno
+            LOGGER.error(f"{error_msg} 行号: {error_line}")
+
+            # 返回安全空数据
+            return response.json(500, {
+                'total_human_detection': 0,
+                'total_working_hours': 0,
+                'total_wake_sleep': 0,
+                'detail_report': []})
+
+    @classmethod
+    def get_battery_capacity_list(cls, userID, request, request_dict, response):
+        try:
+            # 参数校验
+            device_id = request_dict.get('device_id')
+            if not device_id:
+                return response.json(444, "设备ID不能为空")
+            tz_offset = float(request_dict.get('tz', 0))  # 允许小数时区如5.5
+            if not (-12 <= tz_offset <= 14):
+                raise ValueError
+            tz = pytz.FixedOffset(int(tz_offset * 60))  # 转换为分钟偏移
+
+            # 计算日期范围(关键修改:end_time取昨日23:59:59)
+            query_days = int(request_dict.get('days', 7))
+            client_now = datetime.now(tz)
+            end_time = client_now.replace(
+                hour=23, minute=59, second=59
+            ) - timedelta(days=1)  # 截止到客户端昨日
+
+            start_time = end_time - timedelta(days=query_days - 1)
+            start_time = start_time.replace(hour=0, minute=0, second=0)
+
+            # 转换时间范围到UTC时间戳
+            start_utc_timestamp = int(start_time.astimezone(pytz.utc).timestamp())
+            end_utc_timestamp = int(end_time.astimezone(pytz.utc).timestamp())
+
+            # 查询数据库
+            records = DeviceDailyReport.objects.filter(
+                device_id=device_id,
+                type=1,
+                report_time__gte=start_utc_timestamp,
+                report_time__lte=end_utc_timestamp
+            ).order_by('report_time')
+
+            # 构建日期-电量映射(按客户端时区)
+            date_battery_map = {}
+            for record in records:
+                local_date = datetime.fromtimestamp(record.report_time, tz).date()
+                date_str = local_date.strftime("%Y-%m-%d")
+                date_battery_map[date_str] = record.battery_level
+
+            # 生成完整日期序列
+            report_data = []
+            for day_offset in range(query_days):
+                current_date = (end_time.date() - timedelta(days=query_days - 1 - day_offset))
+                report_data.append({
+                    "index": current_date.day - 1,  # 日期下标从0开始
+                    "battery": date_battery_map.get(current_date.strftime("%Y-%m-%d"), 0),
+                    "time": current_date.strftime("%Y-%m-%d")
+                })
+
+            return response.json(0, report_data)
+        except Exception as e:
+            LOGGER.error('查询设备电量上报列表异常error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
+            return response.json(0, {})

+ 22 - 0
Model/models.py

@@ -5513,3 +5513,25 @@ class DeviceNetInfo(models.Model):
         db_table = 'device_net_info'
         db_table = 'device_net_info'
         verbose_name = '设备网络信息'
         verbose_name = '设备网络信息'
 
 
+
+class DeviceDailyReport(models.Model):
+    id = models.AutoField(primary_key=True, verbose_name='自增标记ID')
+    device_id = models.CharField(default='', max_length=32, verbose_name='设备ID')
+    type = models.SmallIntegerField(default=0, verbose_name=u'统计类型0:默认1:电耗统计')
+    battery_level = models.IntegerField(default=0, verbose_name='23:59电量百分比')
+    report_date = models.DateTimeField(blank=True, null=True, verbose_name=u'统计日期时间')
+    report_time = models.IntegerField(default=0, verbose_name='设备上报时间戳')
+    human_detection = models.IntegerField(default=0, verbose_name='人形检测次数')
+    working_hours = models.IntegerField(default=0, verbose_name='工作时长/秒')
+    wake_sleep = models.IntegerField(default=0, verbose_name='唤醒休眠次数')
+    pir_wakeup_count = models.IntegerField(default=0, verbose_name='PIR唤醒次数')
+    mqtt_wakeup_count = models.IntegerField(default=0, verbose_name='mqtt唤醒次数')
+    channel = models.IntegerField(default=1, blank=True, verbose_name=u'设备通道')
+    created_time = models.IntegerField(default=0, verbose_name='创建时间')
+    updated_time = models.IntegerField(default=0, verbose_name='更新时间')
+
+    class Meta:
+        db_table = 'device_daily_report'
+        verbose_name = '设备日报记录表'
+        verbose_name_plural = verbose_name
+        app_label = 'PushModel'

+ 1 - 5
Service/CommonService.py

@@ -905,11 +905,7 @@ GCqvlyw5dfxNA+EtxNE2wCW/LW7ENJlACgcfgPlBZtpLheWoZB/maw4=
             sign_name = '周视'
             sign_name = '周视'
         elif sign_name == 'vsees':
         elif sign_name == 'vsees':
             # 微瞳移动号码使用 Ansjer 签名
             # 微瞳移动号码使用 Ansjer 签名
-            is_china_mobile = cls.is_china_mobile(phone)
-            if is_china_mobile:
-                sign_name = 'Ansjer'
-            else:
-                sign_name = '微瞳'
+            sign_name = '微瞳'
         else:
         else:
             sign_name = 'Ansjer'
             sign_name = 'Ansjer'
         return sign_name
         return sign_name