瀏覽代碼

新增自动升级OTA、优化设备管理后台

zhangdongming 2 周之前
父節點
當前提交
f3752ee2f4

+ 123 - 9
AdminController/DeviceManagementController.py

@@ -25,7 +25,7 @@ from Model.models import Device_Info, UidSetModel, LogModel, UID_Bucket, Unused_
     VodHlsModel, ExperienceContextModel, DeviceTypeModel, UidUserModel, ExperienceAiModel, AiService, \
     AppBundle, App_Info, AppDeviceType, DeviceNameLanguage, UIDCompanySerialModel, UidPushModel, \
     CustomCustomerOrderInfo, CustomCustomerDevice, DeviceVersionInfo, VoicePromptModel, DeviceAlgorithmExplain, BaiduBigModelLicense, \
-    Instavision
+    DeviceDailyReport, Instavision
 from Object.AWS.AmazonS3Util import AmazonS3Util
 from Object.Enums.RedisKeyConstant import RedisKeyConstant
 from Object.RedisObject import RedisObject
@@ -125,6 +125,8 @@ class DeviceManagement(View):
                 return self.del_device_ver_info(request_dict, response)
             elif operation == 'syncDeviceVersion':  # 一键同步
                 return self.sync_device_version(request_dict, response)
+            elif operation == 'clearDeviceVersionCache':  # 清除设备型号版本缓存
+                return self.clear_device_version_cache(request_dict, response)
 
             # 设备语音设置
             elif operation == 'getDeviceVoice':  # 获取设备音频
@@ -289,6 +291,9 @@ class DeviceManagement(View):
                                 device_info_list["datas"][k]['fields']['ai_status'] = 0
                         else:
                             device_info_list["datas"][k]['fields']['ai_status'] = 0
+                        # 时间格式化修改
+                        device_info_list["datas"][k]['fields']['update_time'] = device_info_list["datas"][k]['fields']['update_time'].split('.')[0].replace('T', ' ')
+                        device_info_list["datas"][k]['fields']['data_joined'] = device_info_list["datas"][k]['fields']['data_joined'].split('.')[0].replace('T', ' ')
             return response.json(0, {'list': device_info_list, 'total': total})
         except Exception as e:
             print(e)
@@ -1158,8 +1163,8 @@ class DeviceManagement(View):
         try:
             uid_set_qs = UidSetModel.objects.filter(uid=uid).first()
             data = model_to_dict(uid_set_qs)
-            ALGORITHM_COMBO_TYPES = ['移动', '人形', '车型', '宠物', '人脸', '异响', '闯入', '离开', '徘徊',
-                                     '无人', '往来', '哭声', '手势', '火焰', '婴儿', '包裹']
+            ALGORITHM_COMBO_TYPES = ['移动', '人形', '车型', '宠物', '人脸', '异响', '闯入', '离开', '徘徊', '无人',
+                                     '往来', '哭声', '手势', '火焰', '婴儿', '包裹', '暂无', '电动车', '遮挡']
             if data['ai_type'] > 0:
                 num = data['ai_type']
                 result = ""
@@ -1264,8 +1269,10 @@ class DeviceManagement(View):
             return response.json(444, 'Missing required parameters')
         if files is None and files_v2 is None:
             return response.json(444, 'Missing required parameters')
+        LOGGER.info(f"添加图标设备类型 region: {region} -- region")
         regions = region.split(",")  # 将同步区域拆分为列表
         return_value_list = []
+        LOGGER.info(f"添加图标设备类型 regions: {regions} -- regions列表")
         for region in regions:
             if region == 'test':
                 url = SERVER_DOMAIN_TEST
@@ -1277,7 +1284,7 @@ class DeviceManagement(View):
                 url = SERVER_DOMAIN_EUR
             else:
                 return response.json(444, 'Invalid region')
-
+            LOGGER.info(f"添加图标设备类型 url: {url} -- 确定url")
             try:
                 form_data = {
                     'name': name,
@@ -1307,6 +1314,7 @@ class DeviceManagement(View):
                     files_for_post['iconV2File'] = ('iconV2.png', io.BytesIO(file_content_v2), files_v2.content_type)
 
                 # 发送 POST 请求调用 add_app_device_type 方法
+                LOGGER.info(f"添加图标设备类型 url: {url} -- 准备发送")
                 response_value = requests.post(url + "/deviceManagement/addAppDeviceType",
                                                data=form_data,
                                                files=files_for_post)
@@ -1588,6 +1596,8 @@ class DeviceManagement(View):
         electricity_statistics = request_dict.get('electricityStatistics', 0)
         supports_pet_tracking = request_dict.get('supportsPetTracking', 0)
         has_4g_cloud = request_dict.get('has4gCloud', -1)
+
+
         if not all([d_code, software_ver, video_code,
                     device_type, supports_alarm,
                     screen_channels, network_type]
@@ -1600,6 +1610,7 @@ class DeviceManagement(View):
                 other_features = json.loads(other_features)
             else:
                 other_features = None
+
             if DeviceVersionInfo.objects.filter(d_code=d_code, software_ver=software_ver).exists():
                 return response.json(174)
 
@@ -1664,19 +1675,21 @@ class DeviceManagement(View):
         supports_pet_tracking = request_dict.get('supportsPetTracking', 0)
         has_4g_cloud = request_dict.get('has4gCloud', -1)
 
+
         if not all([device_ver_id, video_code, device_type, supports_alarm, screen_channels, network_type]):
             return response.json(444)
         try:
             now_time = int(time.time())
             ai_type = int(ai_type)
-            device_version_info_qs = DeviceVersionInfo.objects.filter(id=device_ver_id).values('d_code', 'software_ver')
-            if not device_version_info_qs.exists():
-                return response.json(173)
             if other_features:
                 other_features = json.loads(other_features)
             else:
                 other_features = None
 
+            device_version_info_qs = DeviceVersionInfo.objects.filter(id=device_ver_id).values('d_code', 'software_ver')
+            if not device_version_info_qs.exists():
+                return response.json(173)
+
             d_code = device_version_info_qs[0]['d_code']
             software_ver = device_version_info_qs[0]['software_ver']
             version_key = RedisKeyConstant.ZOSI_DEVICE_VERSION_INFO.value + software_ver + d_code
@@ -1769,7 +1782,7 @@ class DeviceManagement(View):
             if not version_config_qs.exists():
                 return response.json(173)  # 设备版本配置不存在
 
-            # 获取第一条匹配的记录(通常应该只有一条)
+            # 获取第一条匹配的记录
             version_config = version_config_qs.first()
             # 直接使用数据库字段,不需要json.loads
             other_features = ''
@@ -1803,7 +1816,8 @@ class DeviceManagement(View):
                 'networkType': version_config.network_type,
                 'otherFeatures': other_features,
                 'electricityStatistics': version_config.electricity_statistics,
-                'supportsPetTracking': version_config.supports_pet_tracking
+                'supportsPetTracking': version_config.supports_pet_tracking,
+                'has4gCloud': version_config.has_4g_cloud
             }
             config_thread = threading.Thread(target=DeviceManagement.sync_device_version_config, kwargs=req_data)
             config_thread.start()
@@ -2370,3 +2384,103 @@ class DeviceManagement(View):
             return response.json(0)
         except Exception as e:
             return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
+
+    @staticmethod
+    def device_power_display(request_dict, response):
+        """
+        获取设备电量显示
+        @param request_dict: 包含查询参数的字典
+        @param response: 响应对象
+        @return: 分页后的设备日报数据,包含聚合信息(如果条件满足)
+        """
+        device_id = request_dict.get('deviceId', None)
+        start_time = request_dict.get('startTime', None)
+        end_time = request_dict.get('endTime', None)
+        page = int(request_dict.get('page', 1))  # 默认第1页
+        page_size = int(request_dict.get('pageSize', 10))  # 默认每页10条
+
+        device_daily_report_qs = DeviceDailyReport.objects.filter(type=1)
+
+        # 应用过滤条件
+        if device_id:
+            device_daily_report_qs = device_daily_report_qs.filter(device_id=device_id)
+
+        device_daily_report_qs = device_daily_report_qs.filter(report_time__gt=0)
+
+        if start_time and end_time:
+            start_time = int(start_time)
+            end_time = int(end_time)
+
+            device_daily_report_qs = device_daily_report_qs.filter(
+                report_time__gte=start_time,
+                report_time__lte=end_time
+            )
+
+
+        # 计算总数(用于分页)
+        total_count = device_daily_report_qs.count()
+
+        device_daily_report_qs = device_daily_report_qs.order_by('-report_time')
+
+        # 应用分页
+        paginator = Paginator(device_daily_report_qs, page_size)
+        device_daily_report_page = paginator.page(page)
+
+        # 序列化分页数据
+        device_daily_report = list(device_daily_report_page.object_list.values(
+            'device_id',
+            'battery_level',
+            'report_time',
+            'human_detection',
+            'working_hours',
+            'wake_sleep',
+            'pir_wakeup_count',
+            'mqtt_wakeup_count'
+        ))
+
+        # 构建返回数据
+        data = {
+            "list": device_daily_report,
+            "total": total_count,
+        }
+
+        # 如果满足条件(有设备ID和时间范围),计算聚合数据
+        if device_id and start_time and end_time:
+            aggregates = device_daily_report_qs.aggregate(
+                total_human_detection=Sum('human_detection'),
+                total_working_hours=Sum('working_hours'),
+                total_wake_sleep=Sum('wake_sleep')
+            )
+
+            data['data_statistics'] = {
+                'total_human_detection': aggregates.get('total_human_detection', 0),
+                'total_working_hours': aggregates.get('total_working_hours', 0),
+                'total_wake_sleep': aggregates.get('total_wake_sleep', 0)
+            }
+
+        return response.json(0, data)
+
+    @classmethod
+    def clear_device_version_cache(cls, request_dict, response):
+        """清除设备版本的缓存"""
+        try:
+            redis = RedisObject()
+            ver_dcode_qs = DeviceVersionInfo.objects.values('software_ver', 'd_code')
+            del_count = 0
+
+            for info in ver_dcode_qs:
+                ver = info['software_ver']
+                d_code = info['d_code']
+                if not ver or not d_code:
+                    continue
+                cache_key = RedisKeyConstant.ZOSI_DEVICE_VERSION_INFO.value + ver + d_code
+                if redis.del_data(cache_key):
+                    del_count += 1
+
+            if del_count > 0:
+                return response.json(0, f'成功清除{del_count}个设备缓存')
+            else:
+                return response.json(0, '没有缓存需要清理')
+
+        except Exception as e:
+            return response.json(500, f'error_line:{e.__traceback__.tb_lineno}, error_msg:{str(e)}')

+ 338 - 63
AdminController/VersionManagementController.py

@@ -1,19 +1,28 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
-import os
 import hashlib
+import json
+import os
+import threading
 import time
 
 import boto3
 import botocore
+import requests
+from django.core.paginator import Paginator
 from django.db import transaction
 from django.views.generic.base import View
+from packaging import version as pacVer
 
 from Ansjer.config import BASE_DIR, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
-from Object.TokenObject import TokenObject
+from Ansjer.config import LOGGER, CONFIG_TEST, SERVER_DOMAIN, CONFIG_CN, CONFIG_INFO
+from Model.models import Equipment_Version, App_Info, AppSetModel, App_Colophon, Pc_Info, CountryModel, \
+    Device_Info, UidSetModel, Device_User, IPAddr, DeviceVersionInfo, iotdeviceInfoModel
+from Object.RedisObject import RedisObject
 from Object.ResponseObject import ResponseObject
+from Object.TokenObject import TokenObject
+from Object.UrlTokenObject import UrlTokenObject
 from Service.CommonService import CommonService
-from Model.models import Equipment_Version, App_Info, AppSetModel, App_Colophon, Pc_Info
 
 
 class VersionManagement(View):
@@ -70,6 +79,10 @@ class VersionManagement(View):
                 return self.editPcVersion(request_dict, response)
             elif operation == 'deletePcInfo':
                 return self.deletePcInfo(request_dict, response)
+            elif operation == 'getCountryList':
+                return self.getCountryList(request_dict, response)
+            elif operation == 'deviceAutoUpdate':
+                return self.device_auto_update(userID, request_dict, response)
             else:
                 return response.json(404)
 
@@ -87,7 +100,7 @@ class VersionManagement(View):
         line = int(pageSize)
 
         try:
-            equipment_version_qs = Equipment_Version.objects.filter()
+            equipment_version_qs = Equipment_Version.objects.filter().order_by('-update_time')
             if mci:
                 equipment_version_qs = equipment_version_qs.filter(mci=mci)
             if lang:
@@ -98,10 +111,20 @@ class VersionManagement(View):
             total = equipment_version_qs.count()
             equipment_version_qs = equipment_version_qs.values()[(page - 1) * line:page * line]
             equipment_version_list = CommonService.qs_to_list(equipment_version_qs)
+
+            for equipment_version in equipment_version_list:
+                new_equipment_version = equipment_version['version'][1:]
+                d_code = new_equipment_version.rsplit('.', 1)[1]
+                software_ver = new_equipment_version.rsplit('.', 1)[0].replace('V', '')
+                device_ver_info_qs = DeviceVersionInfo.objects.filter(d_code=d_code, software_ver=software_ver)
+                if device_ver_info_qs.exists():
+                    equipment_version['is_hav_dev_ver_info'] = 1
+                else:
+                    equipment_version['is_hav_dev_ver_info'] = 0
             return response.json(0, {'list': equipment_version_list, 'total': total})
         except Exception as e:
             print(e)
-            return response.json(500, repr(e))
+            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
 
     def upLoadFile(self, request, request_dict, response):
         file = request.FILES.get('file', None)
@@ -114,66 +137,75 @@ class VersionManagement(View):
         Description = request_dict.get('Description', '')
         status = request_dict.get('status', 0)
         isPopup = request_dict.get('isPopup', 0)
+        auto_update = request_dict.get('autoUpdate', 0)
+        data_json = request_dict.get('dataJson', None)
 
         if not all([file, mci, lang, ESN, max_ver, channel, resolutionRatio]):
             return response.json(444)
 
         try:
-            nowTime = CommonService.timestamp_to_str(timestamp=int(time.time()))
-            channel = int(channel)
-            resolutionRatio = int(resolutionRatio)
-            status = int(status)
-            isPopup = int(isPopup)
-            # 文件名为设备版本,最后一个'.'的前面为软件版本,后面为设备规格名称
-            # V2.2.4.16E201252CA,软件版本:2.2.4,设备规格名称:16E201252CA
-            # V1.7.2.36C11680X30411F000600000150001Z,软件版本:1.7.2,设备规格名称:36C11680X30411F000600000150001Z
-            file_name = str(file)  # 文件名
-            # .img和.tar.gz文件
-            file_type_index = file_name.find('.img')
-            if file_type_index == -1:
-                file_type_index = file_name.find('.tar')
+            with transaction.atomic():
+                nowTime = CommonService.timestamp_to_str(timestamp=int(time.time()))
+                channel = int(channel)
+                resolutionRatio = int(resolutionRatio)
+                status = int(status)
+                isPopup = int(isPopup)
+                if data_json:
+                    data_json = eval(data_json)
+                # 文件名为设备版本,最后一个'.'的前面为软件版本,后面为设备规格名称
+                # V2.2.4.16E201252CA,软件版本:2.2.4,设备规格名称:16E201252CA
+                # V1.7.2.36C11680X30411F000600000150001Z,软件版本:1.7.2,设备规格名称:36C11680X30411F000600000150001Z
+                file_name = str(file)  # 文件名
+                # .img和.tar.gz文件
+                file_type_index = file_name.find('.img')
                 if file_type_index == -1:
-                    return response.json(903)
-            version = file_name[:file_type_index]  # 设备版本
-            version_index = version.rindex('.')
-            softwareVersion = version[1:version_index]  # 软件版本
-            code = version[version_index + 1:]  # 设备规格名称
-            chipModelList2Code = code[:4]  # 主芯片
-            type = code[8:10]  # 设备机型
-            companyCode = code[-1:]  # 公司代码
-            fileSize = file.size
-            filePath = '/'.join(('static/otapack', mci, lang, file_name))
-            file_data = file.read()
-            fileMd5 = hashlib.md5(file_data).hexdigest()
-            data_dict = {'mci': mci, 'lang': lang, 'ESN': ESN, 'max_ver': max_ver, 'channel': channel,
-                         'resolutionRatio': resolutionRatio, 'Description': Description, 'status': status,
-                         'version': version, 'softwareVersion': softwareVersion, 'code': code,
-                         'chipModelList2Code': chipModelList2Code, 'type': type, 'companyCode': companyCode,
-                         'fileSize': fileSize, 'filePath': filePath, 'fileMd5': fileMd5, 'update_time': nowTime,
-                         'is_popup': isPopup}
-            # Equipment_Version表创建或更新数据
-            equipment_version_qs = Equipment_Version.objects.filter(code=code, lang=lang)
-            if not equipment_version_qs.exists():
-                Equipment_Version.objects.create(eid=CommonService.getUserID(getUser=False, setOTAID=True),
-                                                 **data_dict)
-            else:
-                equipment_version_qs.update(**data_dict)
-
-            # 上传文件到服务器
-            upload_path = '/'.join((BASE_DIR, 'static/otapack', mci, lang)).replace('\\', '/') + '/'
-            if not os.path.exists(upload_path):  # 上传目录不存在则创建
-                os.makedirs(upload_path)
-            # 文件上传
-            full_name = upload_path + file_name
-            if os.path.exists(full_name):   # 删除同名文件
-                os.remove(full_name)
-            with open(full_name, 'wb+') as write_file:
-                for chunk in file.chunks():
-                    write_file.write(chunk)
-            return response.json(0)
+                    file_type_index = file_name.find('.tar')
+                    if file_type_index == -1:
+                        return response.json(903)
+                version = file_name[:file_type_index]  # 设备版本
+                version_index = version.rindex('.')
+                softwareVersion = version[1:version_index]  # 软件版本
+                code = version[version_index + 1:]  # 设备规格名称
+                chipModelList2Code = code[:4]  # 主芯片
+                type = code[8:10]  # 设备机型
+                companyCode = code[-1:]  # 公司代码
+                fileSize = file.size
+                filePath = '/'.join(('static/otapack', mci, lang, file_name))
+                file_data = file.read()
+                fileMd5 = hashlib.md5(file_data).hexdigest()
+                data_dict = {'mci': mci, 'lang': lang, 'ESN': ESN, 'max_ver': max_ver, 'channel': channel,
+                             'resolutionRatio': resolutionRatio, 'Description': Description, 'status': status,
+                             'is_popup': isPopup, 'version': version, 'softwareVersion': softwareVersion, 'code': code,
+                             'chipModelList2Code': chipModelList2Code, 'type': type, 'companyCode': companyCode,
+                             'fileSize': fileSize, 'filePath': filePath, 'fileMd5': fileMd5, 'update_time': nowTime,
+                             'data_json': data_json, 'auto_update': auto_update}
+                # Equipment_Version表创建或更新数据
+                equipment_version_qs = Equipment_Version.objects.filter(code=code, lang=lang)
+                if not equipment_version_qs.exists():
+                    Equipment_Version.objects.create(eid=CommonService.getUserID(getUser=False, setOTAID=True),
+                                                     **data_dict)
+                else:
+                    equipment_version_qs.update(**data_dict)
+
+                # 上传文件到服务器
+                upload_path = '/'.join((BASE_DIR, 'static/otapack', mci, lang)).replace('\\', '/') + '/'
+                if not os.path.exists(upload_path):  # 上传目录不存在则创建
+                    os.makedirs(upload_path)
+                # 文件上传
+                full_name = upload_path + file_name
+                if os.path.exists(full_name):  # 删除同名文件
+                    os.remove(full_name)
+                with open(full_name, 'wb+') as write_file:
+                    for chunk in file.chunks():
+                        write_file.write(chunk)
+                LOGGER.info('versionManagement/upLoadFile成功上传{}'.format(file_name))
+                if not DeviceVersionInfo.objects.filter(d_code=code, software_ver=softwareVersion).exists():
+                    return response.json(0, "该版本尚未添加设备版本信息")
+                return response.json(0)
         except Exception as e:
-            print(e)
-            return response.json(500, repr(e))
+            LOGGER.info(
+                'versionManagement/upLoadFile接口异常,errLine:{}, errMsg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
+            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
 
     def editVersionInformation(self, request_dict, response):
         eid = request_dict.get('eid', None)
@@ -184,22 +216,26 @@ class VersionManagement(View):
         resolutionRatio = request_dict.get('resolutionRatio', '')
         Description = request_dict.get('Description', '')
         is_popup = request_dict.get('is_popup', '')
+        auto_update = request_dict.get('autoUpdate', 0)
+        data_json = request_dict.get('dataJson', None)
 
         if not eid:
             return response.json(444)
         status = 1 if status == 'true' else 0
+        if data_json:
+            data_json = eval(data_json)
         try:
             equipment_version_qs = Equipment_Version.objects.filter(eid=eid)
             if not equipment_version_qs.exists():
                 return response.json(173)
             data_dict = {'ESN': ESN, 'max_ver': max_ver, 'status': status, 'channel': channel,
-                         'resolutionRatio': resolutionRatio, 'Description': Description,
-                         'is_popup': is_popup}
+                         'auto_update': auto_update, 'data_json': data_json,
+                         'resolutionRatio': resolutionRatio, 'Description': Description, 'is_popup': is_popup}
             equipment_version_qs.update(**data_dict)
             return response.json(0)
         except Exception as e:
             print(e)
-            return response.json(500, repr(e))
+            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
 
     def deleteEquipmentVersion(self, request_dict, response):
         eid = request_dict.get('eid', None)
@@ -218,7 +254,7 @@ class VersionManagement(View):
             return response.json(0)
         except Exception as e:
             print(e)
-            return response.json(500, repr(e))
+            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
 
     def getAppVersionList(self, request_dict, response):
         app_type = request_dict.get('app_type', None)
@@ -635,4 +671,243 @@ class VersionManagement(View):
                 return response.json(0)
         except Exception as e:
             print(e)
-            return response.json(500, repr(e))
+            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
+
+    def getCountryList(self, request_dict, response):
+        try:
+            country_qs = CountryModel.objects.all().values_list('country_name', flat=True)
+            return response.json(0, list(country_qs))
+        except Exception as e:
+            print(e)
+            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
+
+    @classmethod
+    def device_auto_update(cls, user_id, request_dict, response):
+        verId = request_dict.get('verId', None)
+        if not verId:
+            return response.json(444)
+        LOGGER.info(f'版本ID:{verId},操作用户{user_id}')
+        version_qs = Equipment_Version.objects.filter(eid=verId, auto_update=True, status=1)
+        if not version_qs.exists():
+            return response.json(173)
+
+        agent_thread = threading.Thread(
+            target=VersionManagement().device_async_update,
+            args=(version_qs.first(),)  # 取单个对象
+        )
+        agent_thread.start()
+        return response.json(0)
+
+    @staticmethod
+    def device_async_update(version_qs):
+        code = version_qs.code
+        version = version_qs.softwareVersion
+        ver_data = version_qs.data_json
+        uid_set_qs = UidSetModel.objects.filter(ucode=code).exclude(version=version) \
+            .values('uid', 'ip', 'version')
+        LOGGER.info(f'通知设备自动升级查询符合数量{uid_set_qs.count()}')
+        if not uid_set_qs.exists():
+            return
+        try:
+            # 创建分页器,每页100条数据
+            paginator = Paginator(uid_set_qs, 100)
+            # 遍历每一页数据
+            for page_num in range(1, paginator.num_pages + 1):
+                # 获取当前页的数据
+                page_data = paginator.page(page_num)
+
+                # 遍历当前页的每一条数据
+                for data in page_data:
+                    uid = data['uid']
+
+                    result = VersionManagement.check_version_auto_update(uid, '', ver_data)
+                    if not result:
+                        LOGGER.info(f'{uid}判断是否符合自动升级:{result}')
+                        continue
+                    serial_number = CommonService.get_serial_number_by_uid(uid)
+                    now_ver = data['version']
+                    # 将版本号带V字符换成空
+                    now_ver_clean = now_ver.replace("V", "")
+                    # 采用packaging模块中的Version类进行版本号比较
+                    now_version = pacVer.Version(now_ver_clean)
+                    new_version = pacVer.Version(version)
+
+                    if new_version > now_version:  # 新版本大于当前设备版本执行MQTT发送升级
+                        VersionManagement.send_auto_update_url(version_qs, uid, serial_number, code, now_ver)
+                    LOGGER.info(f'uid={uid},ucode={code},当前版本{now_ver},最新版本{version}')
+        except Exception as e:
+            LOGGER.error(f'异步自动升级异常:ucode:{code},error:{repr(e)}')
+
+    @classmethod
+    def check_version_auto_update(cls, uid, user_id, ver_data):
+        """
+
+        :param uid: 设备UID
+        :param user_id: 用户id
+        :param ver_data: 版本指定升级数据
+        :return: True | False
+        """
+        try:
+            if not ver_data:  # 过滤值默认当前版本所有设备自动升级
+                return True
+
+            if uid and ver_data['uid_list']:  # 当前版本指定UID
+                return uid in ver_data['uid_list']
+            if user_id and ver_data['country_list'] and CONFIG_INFO not in [CONFIG_CN, CONFIG_TEST]:  # 当前版本指定用户国家
+                user_qs = Device_User.objects.filter(userID=user_id).values('region_country')
+                if not user_qs.exists():  # 用户不存在不升级
+                    return False
+                if user_qs[0]['region_country'] == 0:  # 用户未选择国家 不进行升级提示
+                    return False
+                return user_qs[0]['region_country'] in ver_data['country_list']
+            if CONFIG_INFO in [CONFIG_CN, CONFIG_TEST] and ver_data['addr']:  # 中国区可指定城市
+                city_list = ver_data['addr']['city_list']
+                if not city_list:
+                    return True
+                uid_set_qs = UidSetModel.objects.filter(uid=uid).values('ip')
+                if not uid_set_qs:
+                    return False
+                ip_qs = IPAddr.objects.filter(ip=uid_set_qs[0]['ip']).values('city').order_by('-id')
+                if not ip_qs:
+                    return False
+                return ip_qs[0]['city'] in city_list
+            return False
+        except Exception as e:
+            LOGGER.error(f'检测是否符合自动升级{repr(e)}')
+            return False
+
+    @staticmethod
+    def send_auto_update_url(version_qs, uid, serial_number, code, now_ver):
+        """
+        发送自动升级URL
+        @param version_qs: 最新版本querySet
+        @param uid: 设备UID
+        @param serial_number: 设备序列号
+        @param code: 设备规格码
+        @param now_ver: 设备当前版本好
+        @return:
+        """
+        try:
+            file_path = version_qs.filePath
+            version = version_qs.version
+            mci = version_qs.mci
+            ver = version_qs.softwareVersion
+            max_ver = version_qs.max_ver
+
+            if ver <= max_ver:
+                user_qs = Device_Info.objects.filter(UID=uid, isShare=False).values('userID_id')
+                if not user_qs.exists():
+                    LOGGER.info(f'{uid}未添加该设备返回不发MQTT')
+                    return False
+                user_id = user_qs[0]['userID_id']
+                # 创建url的token
+                param_url = "ansjer/" + CommonService.RandomStr(6) + "/" + file_path
+                data = {'Url': param_url, 'user_id': user_id,
+                        'uid': uid, 'serial_number': serial_number, 'old_version': "V" + now_ver + "." + code,
+                        'new_version': version, 'mci': mci}
+                device_info_value = json.dumps(data)
+                expire = 600
+
+                str_uuid = str(user_id)
+                if serial_number and serial_number != 'null':
+                    str_uuid += serial_number
+                elif uid and uid != 'null':
+                    str_uuid += uid
+                str_uuid += now_ver
+                device_info_key = 'ASJ:SERVER:VERSION:{}'.format(str_uuid)
+                LOGGER.info('缓存key={}'.format(device_info_key))
+                redisObject = RedisObject()
+                redisObject.set_data(device_info_key, device_info_value, expire)
+
+                url_tko = UrlTokenObject()
+                file_path = url_tko.generate(data={'uid': str_uuid})
+
+                url = SERVER_DOMAIN + 'dlotapack/' + file_path
+                # 主题名称
+                topic_name = f'ansjer/generic/{serial_number}'
+                # 发布消息内容
+                msg = f'{{"commandCode":1,"data":{{"url":"{url}"}}}}'
+
+                result = VersionManagement.publish_to_aws_iot_mqtt(serial_number, topic_name, msg)
+                LOGGER.info(f'uid={uid}发送数据msg={msg},发送MQTT结果={result}')
+                return True
+            return False
+        except Exception as e:
+            LOGGER.error(f'自动升级异常{repr(e)}')
+            return False
+
+    @staticmethod
+    def publish_to_aws_iot_mqtt(identification_code, topic_name, msg, qos=1):
+        """
+        发布消息到AWS IoT MQTT(仅尝试一次,无重试)
+        @param identification_code: 标识码
+        @param topic_name: 主题名
+        @param msg: 消息内容(JSON字符串)
+        @param qos: QoS等级(0或1)
+        @return: 成功返回True,失败返回False
+        """
+        # 入参校验
+        if not isinstance(identification_code, str) or not identification_code.strip():
+            LOGGER.error("标识码为空或无效")
+            return False
+        if not isinstance(topic_name, str) or not topic_name.strip():
+            LOGGER.error("主题名为空或无效")
+            return False
+        if qos not in (0, 1):
+            LOGGER.warning("QoS不合法,默认使用1")
+            qos = 1
+
+        # 生成ThingName
+        thing_name = f'LC_{identification_code}' if identification_code.endswith(
+            '11L') else f'Ansjer_Device_{identification_code}'
+
+        try:
+            # 查询设备信息
+            iot_device = iotdeviceInfoModel.objects.filter(thing_name=thing_name).values('endpoint',
+                                                                                         'token_iot_number').first()
+            if not iot_device:
+                LOGGER.error(f"未查询到设备信息:{thing_name}")
+                return False
+
+            endpoint = iot_device.get('endpoint', '').strip()
+            token = iot_device.get('token_iot_number', '').strip()
+            if not endpoint or not token:
+                LOGGER.error("设备信息不完整(endpoint或token缺失)")
+                return False
+
+            # 构造请求
+            encoded_topic = requests.utils.quote(topic_name, safe='')
+            request_url = f"https://{endpoint}/topics/{encoded_topic}?qos={qos}"
+            signature = CommonService.rsa_sign(token)
+            if not signature:
+                LOGGER.error("Token签名失败")
+                return False
+
+            headers = {
+                'x-amz-customauthorizer-name': 'Ansjer_Iot_Auth',
+                'Token': token,
+                'x-amz-customauthorizer-signature': signature
+            }
+
+            # 发送请求(仅一次)
+            response = requests.post(
+                url=request_url,
+                headers=headers,
+                data=msg,
+                timeout=10  # 10秒超时
+            )
+
+            # 结果判断
+            if response.status_code == 200:
+                res_json = response.json()
+                if res_json.get('message') == 'OK':
+                    LOGGER.info(f"发布成功:{topic_name}")
+                    return True
+                LOGGER.error(f"响应异常:{res_json}")
+            else:
+                LOGGER.error(f"请求失败,状态码:{response.status_code}")
+            return False
+
+        except Exception as e:
+            LOGGER.error(f"发布失败:{str(e)}")
+            return False

+ 28 - 4
Controller/UserDevice/DeviceVersionInfoController.py

@@ -146,18 +146,40 @@ class DeviceVersionInfoView(View):
                 return response.json(173)  # 错误代码:未找到设备版本信息
 
             # 从QuerySet中获取设备信息
-            device_info = device_version_qs
+            device_info = device_version_qs.first()
+
+            other_features_fields = [
+                'supports_lighting',
+                'is_support_multi_speed',
+                'supports_algorithm_switch',
+                'supports_playback_filtering'
+            ]
+
+            if not device_info.get("other_features"):
+                device_info["other_features"] = {field: -1 for field in other_features_fields}
+            else:
+                try:
+                    if isinstance(device_info["other_features"], str):
+                        device_info["other_features"] = json.loads(device_info["other_features"])
+
+                    for field in other_features_fields:
+                        if field not in device_info["other_features"] or device_info["other_features"][field] in (None,
+                                                                                                                  ''):
+                            device_info["other_features"][field] = -1
+                except json.JSONDecodeError:
+                    device_info["other_features"] = {field: -1 for field in other_features_fields}
 
             # 将设备信息序列化为JSON
-            device_json = json.dumps(device_info[0])
+            device_json = json.dumps(device_info)
 
             # 将数据写入Redis,以便后续使用
             redis.set_data(version_key, device_json, 60 * 60 * 24)  # 设置TTL为24小时
             # 返回设备信息
-            return response.json(0, device_info[0])
+            return response.json(0, device_info)
 
         except Exception as e:
-            return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
+            LOGGER.error('uid:{}返回173,error_line:{}, error_msg:{}'.format(uid, e.__traceback__.tb_lineno, repr(e)))
+            return response.json(173)
 
     @classmethod
     def cache_device_version_info(cls, d_code, ver):
@@ -263,6 +285,7 @@ class DeviceVersionInfoView(View):
         try:
             d_code = request_dict.get('dCode', '')
             ver = request_dict.get('softwareVer', '')
+
             # 提取参数并设置默认值(添加必要字段的类型转换)
             params = {
                 'd_code': d_code,  # 字符串类型,添加默认空字符串
@@ -290,6 +313,7 @@ class DeviceVersionInfoView(View):
                     'otherFeatures') else None,  # 保持不变
                 'electricity_statistics': int(request_dict.get('electricityStatistics', 0)),  # 使用get()添加默认值0
                 'supports_pet_tracking': int(request_dict.get('supportsPetTracking', 0)),  # 使用get()添加默认值0
+                'has_4g_cloud': int(request_dict.get('has4gCloud', -1)),  # 使用get()添加默认值0
             }
 
             now_time = int(time.time())