فهرست منبع

4G业务兼容移远电信

zhangdongming 5 روز پیش
والد
کامیت
3c471c45fe

+ 50 - 17
AdminController/ServeManagementController.py

@@ -8,6 +8,7 @@ import uuid
 import json
 import logging
 from collections import defaultdict
+from typing import Dict, Any
 
 import paypalrestsdk
 import requests
@@ -27,7 +28,7 @@ from Model.models import VodBucketModel, CDKcontextModel, Store_Meal, Order_Mode
     UID_Bucket, ExperienceContextModel, Lang, CloudLogModel, UidSetModel, Unused_Uid_Meal, \
     Device_Info, DeviceTypeModel, UnicomComboOrderInfo, AiService, CountryModel, CouponLang, CouponConfigModel, \
     CouponCombo, CouponModel, Device_User, AbnormalOrder, DailyReconciliation, StsCrdModel, LogModel, \
-    InAppPurchasePackage, InAppRefund, VodHlsSummary
+    InAppPurchasePackage, InAppRefund, VodHlsSummary, UnicomDeviceInfo
 from Object.AppleInAppPurchaseSubscriptionObject import InAppPurchase
 from Object.Enums.RedisKeyConstant import RedisKeyConstant
 from Object.RedisObject import RedisObject
@@ -36,6 +37,7 @@ from Object.TokenObject import TokenObject
 from Object.UnicomObject import UnicomObjeect
 from Object.utils.PayPalUtil import PayPalService
 from Service.CommonService import CommonService
+from Service.VodHlsOssService import SplitVodHlsOssObject
 from Service.VodHlsService import SplitVodHlsObject
 
 LOGGER = logging.getLogger('info')
@@ -1975,12 +1977,21 @@ class serveManagement(View):
             return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
 
     @classmethod
-    def get_cloud_storage_status_by_uid(cls, request_dict, response):
+    def get_cloud_storage_status_by_uid(cls, request_dict: Dict[str, Any], response: ResponseObject) -> ResponseObject:
         """
-        根据用户ID获取云存储状态
-        :param request_dict: 请求参数,包含用户ID和日期
-        :param response: 响应对象
-        :return: 云存储状态数据
+        根据用户uid获取云存储状态
+
+        Args:
+            request_dict (Dict[str, Any]): 请求参数字典,包含以下字段:
+                - uid (str): 用户唯一标识符
+            response (ResponseObject): 响应对象
+
+        Returns:
+            JsonResponse: 包含云存储状态的JSON响应
+
+        Raises:
+            ValueError: 当参数无效时抛出
+            RuntimeError: 当操作失败时抛出
         """
         # 解析请求参数
         uid = request_dict.get('uid')
@@ -2033,23 +2044,39 @@ class serveManagement(View):
                 res_data['time'] = vod_hls_summary['time']
 
             # 查询aws+oss分表数据
-            now_time = int(time.time())
-            split_vod_hls_obj = SplitVodHlsObject()
             # 获取当天的开始时间戳(00:00:00)
             start_timestamp = int(date_obj.timestamp())
-
             # 获取当天的结束时间戳(23:59:59)
             end_timestamp = int((date_obj + datetime.timedelta(days=1)).timestamp()) - 1
-            vod_hls_qs = split_vod_hls_obj.get_vod_hls_data(
-                uid=uid, end_time__gte=now_time, start_time__range=(start_timestamp, end_timestamp)) \
-                .values("id", "start_time", "sec", "fg", "bucket_id", "channel", "tab_val")
 
-            count = vod_hls_qs.count()
-            if count > 0:
-                res_data['cloud_video_count'] = count
+            # 获取AWS分表数据
+            split_vod_hls_obj = SplitVodHlsObject()
+            vod_hls_qs = split_vod_hls_obj.get_vod_hls_data(
+                uid=uid,
+                start_time__range=(start_timestamp, end_timestamp)
+            ).values("id", "start_time", "sec", "fg", "bucket_id", "channel", "tab_val")
+
+            # 获取Ali分表数据
+            oss_vod_hls_obj = SplitVodHlsOssObject()
+            oss_vod_qs = oss_vod_hls_obj.get_vod_hls_data(
+                uid=uid,
+                start_time__range=(start_timestamp, end_timestamp)
+            ).values("id", "start_time", "sec", "fg", "bucket_id", "channel", "tab_val")
+
+            # 处理AWS数据
+            aws_count = vod_hls_qs.count()
+            if aws_count > 0:
+                res_data['cloud_video_count'] = aws_count
                 sec_values = vod_hls_qs.values_list('sec', flat=True)
                 res_data['total_duration_seconds'] = sum(sec_values) if sec_values else 0
 
+            # 处理OSS数据 - 修复了这里的语法错误
+            oss_count = oss_vod_qs.count()
+            if oss_count > 0:
+                res_data['cloud_video_count'] += oss_count
+                sec_values = oss_vod_qs.values_list('sec', flat=True)
+                res_data['total_duration_seconds'] += sum(sec_values) if sec_values else 0
+
             # 从Redis获取请求计数
             redis_obj = RedisObject()
             key = f'sts_count_{uid}'
@@ -2060,7 +2087,9 @@ class serveManagement(View):
             return response.json(0, res_data)
 
         except Exception as e:
-            LOGGER.error(f'获取云存储状态失败 - uid: {uid}, 错误: {repr(e)}')
+            error_msg: str = f'查询云存状态异常 - UID: {uid}, 错误: {str(e)}'
+            error_line: int = e.__traceback__.tb_lineno
+            LOGGER.error(f'{error_msg} 行号: {error_line}')
             return response.json(500, f'服务器错误: {repr(e)}')
 
 
@@ -2525,8 +2554,12 @@ class serveManagement(View):
                     combo_info_qs = UnicomComboOrderInfo.objects.filter(status=1)
                     if combo_info_qs:
                         return response.json(10059)
+                    sim_card_qs = UnicomDeviceInfo.objects.filter(iccid=iccid).values('card_type')
+                    card_type = 0
+                    if sim_card_qs.exists():
+                        card_type = sim_card_qs[0]['card_type']
                     unicom_api = UnicomObjeect()
-                    usage_flow = unicom_api.get_flow_usage_total(iccid)
+                    usage_flow = unicom_api.get_flow_usage_total(iccid, card_type)
                     today = datetime.datetime.today()
                     year = today.year
                     month = today.month

+ 21 - 7
AdminController/UnicomManageController.py

@@ -35,6 +35,7 @@ from Object.TokenObject import TokenObject
 from Object.UnicomObject import UnicomObjeect
 from Object.WXTechObject import WXTechObject
 from Service.CommonService import CommonService
+from Service.QuecCloudService import QuecCloudService
 from Service.TelecomService import TelecomService
 
 UNICOM_MANAGE_LOCK = 'BASIC:UNICOM:MANAGE:OPERATION:LOCK'
@@ -876,14 +877,14 @@ class UnicomManageControllerView(View):
             return response.json(0, {'packageList': package_list})
         iccid = ud_qs[0]['iccid']
         card_type = ud_qs[0]['card_type']
-        if card_type == 0 or card_type == 3:
+        if card_type == 0 or card_type == 3 or card_type == 6:
             o_qs = UnicomComboOrderInfo.objects.filter(iccid=iccid) \
                 .values('id', 'status', 'flow_total_usage', 'flow_exceed', 'activation_time', 'expire_time',
                         'combo__combo_name', 'combo__flow_total', 'updated_time') \
                 .order_by('created_time')
             if not o_qs:
                 return response.json(0, {'packageList': package_list})
-            return response.json(0, {'package_list': cls.get_unicom_package_list(iccid, o_qs)})
+            return response.json(0, {'package_list': cls.get_unicom_package_list(iccid, o_qs, card_type)})
         if card_type == 1:
             data = {'iccid': iccid, 'operator': 3}
             return response.json(0, {'package_list': cls.get_wx_package_list(**data)})
@@ -910,13 +911,13 @@ class UnicomManageControllerView(View):
             return response(500)
 
     @staticmethod
-    def get_unicom_package_list(iccid, o_qs):
+    def get_unicom_package_list(iccid, o_qs, card_type=0):
         package_list = []
         unicom_api = UnicomObjeect()
         for i, item in enumerate(o_qs):
             package_status = item['status']
             flow_total = item['combo__flow_total']
-            flow_total_usage = float(unicom_api.get_flow_usage_total(iccid))
+            flow_total_usage = float(unicom_api.get_flow_usage_total(iccid, card_type))
             activate_flow = float(item['flow_total_usage']) if item['flow_total_usage'] else 0
             used = 0
             if package_status == 1:
@@ -1009,7 +1010,16 @@ class UnicomManageControllerView(View):
                 if not access_info.exists():
                     return 'N/A'
                 return '已激活' if access_info[0]['action'] == 2 else '已停用'
-
+            elif card_type == 6:
+                VALID_QUEC_STATUSES = {'正常', '测试期正常', '待激活'}
+                quec_info = QuecCloudService.query_quec_cloud_sim_info(iccid)
+                if quec_info:
+                    if quec_info.get('status') in VALID_QUEC_STATUSES and quec_info.get('gprsstatus') == '1':
+                        status = '已激活'
+                    else:
+                        status = '已停用'
+                    return status
+                return 'N/A'
             else:
                 return 'N/A'
         except Exception as e:
@@ -1027,7 +1037,7 @@ class UnicomManageControllerView(View):
         if not serial_number:
             return response.json(444)
         status = int(request_dict.get('status', 0))
-        unicom_qs = UnicomDeviceInfo.objects.filter(serial_no=serial_number, card_type__in=[0, 3])
+        unicom_qs = UnicomDeviceInfo.objects.filter(serial_no=serial_number, card_type__in=[0, 3, 6])
         if not unicom_qs.exists():
             return response.json(173)
         unicom_service = UnicomObjeect()
@@ -1468,8 +1478,12 @@ class UnicomManageControllerView(View):
                     package_info['status'] = 0
                     package_info['flow_total_usage'] = '0'
                 else:
+                    sim_card_qs = UnicomDeviceInfo.objects.filter(iccid=iccid).values('card_type')
+                    card_type = 0
+                    if sim_card_qs.exists():
+                        card_type = sim_card_qs[0]['card_type']
                     package_info['status'] = 1
-                    package_info['flow_total_usage'] = unicom_api.get_flow_usage_total(iccid)
+                    package_info['flow_total_usage'] = unicom_api.get_flow_usage_total(iccid, card_type)
                 UnicomComboOrderInfo.objects.create(**package_info)
                 # 设置旧套餐状态为2
                 u_package_query_set.update(status=2, updated_time=int(time.time()))

+ 5 - 0
Ansjer/cn_config/formal_settings.py

@@ -41,6 +41,11 @@ WX_KEY = os.getenv('WX_KEY', default='')
 # omni key
 OMNI_API_KEY = os.getenv('OMNI_API_KEY', default='')
 
+# quecCloud key
+QUEC_CLOUD_AK = os.getenv('QUEC_CLOUD_AK', default='')
+QUEC_CLOUD_SK = os.getenv('QUEC_CLOUD_SK', default='')
+
+
 SECRET_KEY = 'c7ki2_gkg4#sjfm-u1%$s#&n#szf01f*v69rwv2qsf#-zmm@tl'
 # DEBUG = True
 DEBUG = False

+ 38 - 7
Controller/UnicomCombo/UnicomComboController.py

@@ -33,6 +33,7 @@ from Object.WXTechObject import WXTechObject
 from Object.utils import LocalDateTimeUtil
 from Object.utils.PayUtil import PayService
 from Service.CommonService import CommonService
+from Service.QuecCloudService import QuecCloudService
 
 
 class UnicomComboView(View):
@@ -138,6 +139,7 @@ class UnicomComboView(View):
                 serial_no = unicom_device_info_qs[0].serial_no
                 cls.experience_order_4G(iccid, serial_no, user_id)  # 生成4G体验订单
             unicom_api = UnicomObjeect()
+            card_type = unicom_device_info_qs[0].card_type
 
             # 查询正在生效套餐
             combo_order_qs = UnicomComboOrderInfo.objects.filter(iccid=iccid, status=1, is_del=False) \
@@ -165,7 +167,8 @@ class UnicomComboView(View):
                     'month': combo_order['month'],
                 }
                 activate_flow = float(flow_details['flowTotalUsage'])
-                flow_total_usage = float(unicom_api.get_flow_usage_total(iccid))
+                # 获取卡号历史总流量
+                flow_total_usage = float(unicom_api.get_flow_usage_total(iccid, card_type))
                 flow = 0 if flow_total_usage <= 0 else flow_total_usage - activate_flow
                 # 因APP问题,usableFlow可用流量替换为,已用流量值
                 flow_details['usableFlow'] = 0 if flow == 0 else flow
@@ -410,6 +413,16 @@ class UnicomComboView(View):
                                          ip, request_dict,
                                          '4G序列号{}绑定{},testFlowPackage{}'.format(serial_no, iccid, result))
                 return response.json(0)
+            elif QuecCloudService.is_quec_cloud_sim(iccid):  # 移远电信
+                params['card_type'] = 6
+                params['status'] = 2
+                params['iccid'] = iccid[0:19]
+                UnicomDeviceInfo.objects.create(**params)
+                result = cls.activate_test_flow_package(serial_no)
+                cls.create_operation_log('unicom/api/device-bind',
+                                         ip, request_dict,
+                                         f"移远电信4G序列号{serial_no}绑定{iccid},testFlowPackage{result}")
+                return response.json(0)
             elif cls.is_telecom_sim(iccid):  # 鼎芯电信
                 params['card_type'] = 3
                 params['status'] = 2
@@ -675,7 +688,12 @@ class UnicomComboView(View):
                 order_id = CommonService.createOrderID()
                 rank_id, ai_rank_id = cls.get_cloud_or_ai_combo()
 
-                order_type = 2 if unicom_device_qs['card_type'] == 0 else 5
+                CARD_TO_ORDER_TYPE = {
+                    0: 2,
+                    6: 6
+                }
+                # 从字典获取,默认返回5(处理其他所有情况)
+                order_type = CARD_TO_ORDER_TYPE.get(unicom_device_qs['card_type'], 5)
                 order_dict = {'rank_id': rank_id, 'ai_rank_id': ai_rank_id, 'orderID': order_id, 'UID': device_uid,
                               'userID_id': user_id, 'desc': unicom_combo_qs['combo_name'], 'payType': pay_type,
                               'payTime': now_time, 'price': price, 'currency': 'CNY', 'addTime': now_time,
@@ -743,8 +761,12 @@ class UnicomComboView(View):
         iccid = request_dict.get('iccid', None)
         if not iccid:
             return response.json(444)
+        sim_card_qs = UnicomDeviceInfo.objects.filter(iccid=iccid).values('card_type')
+        card_type = 0
+        if sim_card_qs.exists():
+            card_type = sim_card_qs[0]['card_type']
         unicom_api = UnicomObjeect()
-        flow = unicom_api.get_flow_usage_total(iccid)
+        flow = unicom_api.get_flow_usage_total(iccid, card_type)
         return response.json(0, flow)
 
     @staticmethod
@@ -908,9 +930,11 @@ class UnicomComboView(View):
                         if card_info[0]['card_type'] == 3:
                             reason = '激活' + unicom_combo['combo_name']
                             unicom_api.change_device_to_activate(iccid, 3, card_info[0]['access_number'], reason)
+                        elif card_info[0]['card_type'] == 6:  # 判断移远卡状态
+                            QuecCloudService.resume_card(iccid=iccid)
                         else:
                             unicom_api.change_device_to_activate(iccid)
-                        flow_total_usage = unicom_api.get_flow_usage_total(iccid, card_info[0]['card_type'])
+                        flow_total_usage = unicom_api.get_flow_usage_total(iccid, card_info[0]['card_type'])  # 获取套餐总流量
                         if flow_total_usage > 0:
                             flow_total_usage = Decimal(flow_total_usage)
                             flow_total_usage = flow_total_usage.quantize(Decimal('0.00'))
@@ -921,7 +945,8 @@ class UnicomComboView(View):
                     logger.info('保存套餐支付信息success')
                 return True
         except Exception as e:
-            print(e)
+            error_msg = f"{iccid}创建4G订单套餐记录失败: {str(e)}"
+            LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
             return False
 
     @staticmethod
@@ -1095,7 +1120,7 @@ class UnicomComboView(View):
         """
         try:
             # 查询珠海联通与鼎芯电信4G卡
-            u_device_info_qs = UnicomDeviceInfo.objects.filter(serial_no=serial_number, card_type__in=[0, 3])
+            u_device_info_qs = UnicomDeviceInfo.objects.filter(serial_no=serial_number, card_type__in=[0, 3, 6])
             if not u_device_info_qs.exists():
                 LOGGER.info(f'{serial_number}生成联通流量套餐iccid未绑定')
                 return False
@@ -1114,7 +1139,13 @@ class UnicomComboView(View):
             if u_device_info.user_id:
                 user_id = u_device_info.user_id
             order_id = CommonService.createOrderID()
-            order_type = 2 if u_device_info.card_type == 0 else 5
+            # 定义映射关系,键为card_type,值为对应的order_type
+            CARD_TO_ORDER_TYPE = {
+                0: 2,
+                6: 6
+            }
+            # 从字典获取,默认返回5(处理其他所有情况)
+            order_type = CARD_TO_ORDER_TYPE.get(u_device_info.card_type, 5)
 
             order_dict = {'orderID': order_id, 'UID': uid, 'rank_id': rank_id, 'ai_rank_id': ai_rank_id,
                           'userID_id': user_id, 'desc': combo_info_vo['combo_name'], 'payType': 10,

+ 26 - 16
Controller/UnicomCombo/UnicomComboTaskController.py

@@ -114,7 +114,11 @@ class UnicomComboTaskView(View):
                         if not combo_qs.exists():
                             continue
                         # 查询当月用量情况
-                        flow_total_usage = unicom_api.get_flow_usage_total(item['iccid'])
+                        sim_card_qs = UnicomDeviceInfo.objects.filter(iccid=iccid).values('card_type')
+                        card_type = 0
+                        if sim_card_qs.exists():
+                            card_type = sim_card_qs[0]['card_type']
+                        flow_total_usage = unicom_api.get_flow_usage_total(item['iccid'], card_type)
                         flow_total_usage = Decimal(flow_total_usage).quantize(
                             Decimal('0.00')) if flow_total_usage > 0 else 0
                         flow_total_usage = str(flow_total_usage)
@@ -185,7 +189,7 @@ class UnicomComboTaskView(View):
                     cls.flow_warning_push(u_device_info_qs.user_id, u_device_info_qs.serial_no, item['id'], flow_total,
                                           usage)
 
-                # 检查是否有当月未使用套餐 没有则停卡
+                # 已达量,检查是否有当月未使用套餐 没有则停卡
                 if is_expire:
                     flow_exceed = flow_total_usage - flow
                     UnicomComboOrderInfo.objects.filter(id=item['id']) \
@@ -198,7 +202,7 @@ class UnicomComboTaskView(View):
                             TelecomService().update_access_number_network(iccid, u_device_info_qs.access_number, 'ADD',
                                                                           '套餐流量已用完')
                         else:
-                            unicom_api.change_device_to_disable(iccid=iccid, reason='套餐流量已用完')
+                            unicom_api.change_device_to_disable(iccid=iccid, card_type=card_type, reason='套餐流量已用完')
 
         except Exception as e:
             logger.info('异步~检测流量用量详情异常,errLine:{}, errMsg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
@@ -306,35 +310,38 @@ class UnicomComboTaskView(View):
         year = today.year
         month = today.month
 
-        iccid_list = []
+        iccid_with_type = []
         for item in combo_order_qs:
             try:
                 icc_id = item['iccid']
                 um_device_qs = UnicomDeviceInfo.objects.filter(iccid=icc_id)
                 if not um_device_qs.exists():
                     continue
+                um_device = um_device_qs.first()
+                card_type = um_device.card_type  # 这里获取card_type
                 UnicomComboOrderInfo.objects.filter(id=item['id']).update(status=2, updated_time=now_time)
-                iccid_list.append(icc_id)
+                iccid_with_type.append((icc_id, card_type))  # 存入元组
                 logger.info('修改套餐已失效成功,iccid:{}'.format(icc_id))
             except Exception as e:
                 logger.info('async_deactivate_expire_package套餐过期修改失效异常,{}'
                             'errLine:{}, errMsg:{}'.format(item['iccid'], e.__traceback__.tb_lineno, repr(e)))
                 continue
         # set无序不重复元素集
-        iccid_list = list(set(iccid_list))
+        unique_iccid_type = {iccid: card_type for iccid, card_type in iccid_with_type}
+        iccid_with_type = list(unique_iccid_type.items())
         unicom_api = UnicomObjeect()
-        for item in iccid_list:
+        for item, card_type in iccid_with_type:
             try:
                 activate_combo_qs = UnicomComboOrderInfo.objects.filter(iccid=item, status=1, expire_time__gt=now_time,
                                                                         is_del=False).values()
                 if activate_combo_qs.exists():
                     continue
-                usage_flow = unicom_api.get_flow_usage_total(item)
+                usage_flow = unicom_api.get_flow_usage_total(item, card_type)
                 # 查询是否有未使用套餐,有则进行激活 否 则调用API停卡
                 result = UnicomComboTaskView().query_unused_combo_and_activate(item, year, month, usage_flow)
                 if not result:  # 没有可用套餐进行停卡
                     # 停用设备
-                    unicom_api.change_device_to_disable(iccid=item, reason='没有可用套餐')
+                    unicom_api.change_device_to_disable(iccid=item, card_type=card_type, reason='没有可用套餐')
                     logger.info('调用停卡API successful,iccid:{}'.format(item))
                     combo_order_info_qs = UnicomComboOrderInfo.objects.filter(iccid=item, status=2) \
                         .values('id').order_by('-updated_time')
@@ -381,7 +388,7 @@ class UnicomComboTaskView(View):
         device_count = UnicomDeviceInfo.objects.filter(card_type=0).count()
         total_pages = device_count // page_size + (device_count % page_size > 0)  # 计算总页数
         for page_number in range(1, total_pages + 1):
-            u_device_qs = UnicomDeviceInfo.objects.filter(card_type=0).values('id', 'iccid', 'sim_used_flow').order_by(
+            u_device_qs = UnicomDeviceInfo.objects.filter(card_type=0).values('id', 'iccid', 'sim_used_flow', 'card_type').order_by(
                 '-created_time')[(page_number - 1) * page_size:page_number * page_size]
             asy = threading.Thread(target=UnicomComboTaskView.thread_collect_flow_used, args=(u_device_qs,))
             asy.start()
@@ -394,7 +401,7 @@ class UnicomComboTaskView(View):
                 unicom_api = UnicomObjeect()
                 n_time = int(time.time())
                 # 队列已使用总流量总量
-                flow_total_usage = unicom_api.get_flow_usage_total(item['iccid'])
+                flow_total_usage = unicom_api.get_flow_usage_total(item['iccid'], item['card_type'])
                 UnicomDeviceInfo.objects.filter(id=item['id']).update(updated_time=n_time,
                                                                       sim_used_flow=flow_total_usage)
             except Exception as e:
@@ -524,7 +531,7 @@ class UnicomComboTaskView(View):
         """
         更新设备账期流量接口
         """
-        card_qs = UnicomDeviceInfo.objects.filter(card_type=0, status=2).values('iccid')
+        card_qs = UnicomDeviceInfo.objects.filter(card_type=0, status=2).values('iccid', 'card_type')
         if not card_qs.exists():
             return response.json(0)
         logger.info('总数:{}'.format(card_qs.count()))
@@ -542,13 +549,12 @@ class UnicomComboTaskView(View):
             unicom_api = UnicomObjeect()
             for item in qs:
                 try:
-                    unicom_api.get_flow_usage_total(item['iccid'])
+                    unicom_api.get_flow_usage_total(item['iccid'], item['card_type'])
                 except Exception as e:
-                    print(repr(e))
+                    logger.error(f"{item['iccid']}异步更新设备账期流量异常,errLine:{e.__traceback__.tb_lineno}, errMsg:{str(e)}")
                     continue
         except Exception as e:
             logger.info('更新账期流量异常,errLine:{}, errMsg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
-            return None
 
     @staticmethod
     def get_access_number_change_task(response):
@@ -652,7 +658,11 @@ class UnicomComboTaskView(View):
             for item in o_combo_qs:
                 iccid = item['iccid']
                 try:
-                    flow = float(unicom_api.get_flow_usage_total(iccid))
+                    sim_card_qs = UnicomDeviceInfo.objects.filter(iccid=iccid).values('card_type')
+                    card_type = 0
+                    if sim_card_qs.exists():
+                        card_type = sim_card_qs[0]['card_type']
+                    flow = float(unicom_api.get_flow_usage_total(iccid, card_type))
                     if flow == 0:
                         continue
                     old_flow = float(item['flow_total_usage'])

+ 645 - 0
Object/QuecCloudObject.py

@@ -0,0 +1,645 @@
+# -*- encoding: utf-8 -*-
+"""
+@File    : QuecCloudObject.py
+@Time    : 2025/11/13 14:31
+@Author  : stephen
+@Email   : zhangdongming@asj6.wecom.work
+@Software: PyCharm
+"""
+import hashlib
+import logging
+# 1. 标准库导入
+import time
+from typing import Dict, Any, Optional, Final, List
+
+# 2. 第三方库导入
+import requests
+from requests.exceptions import RequestException
+
+# 3. 本地应用导入(假设日志配置在项目config模块中)
+from Ansjer.config import LOGGER
+
+# 类型注解日志对象
+LOGGER: logging.Logger
+
+
+class QuecCloudApiClient:
+    """
+    移远通信安防按量计费OpenAPI工具类
+    封装系统级参数处理、签名算法及流量池相关业务查询接口
+    遵循项目代码规范,支持可扩展、可复用的API调用
+    """
+    # 基础配置常量(全大写蛇形命名)
+    BASE_URL: Final[str] = "https://api.quectel.com/openapi/router"
+    TIME_DIFF_THRESHOLD: Final[int] = 300  # UTC时间戳最大误差(5分钟)
+    DEFAULT_TIMEOUT: Final[int] = 10  # 请求超时时间(秒)
+    MAX_PAGE_SIZE: Final[int] = 100  # 分页查询最大每页条数
+
+    def __init__(self, app_key: str, secret: str):
+        """
+        初始化API客户端
+
+        Args:
+            app_key (str): 应用键(从移远平台API管理页面获取)
+            secret (str): 应用密钥(与appKey对应,需妥善保管)
+        """
+        self.app_key = app_key
+        self.secret = secret
+        self.session = requests.Session()  # 复用会话提升性能
+
+    def _generate_sign(self, params: Dict[str, Any]) -> str:
+        """
+        生成签名串(遵循文档1.6签名算法规范)
+
+        Args:
+            params (Dict[str, Any]): 待签名的所有参数(系统级+业务级)
+
+        Returns:
+            str: 十六进制SHA1签名字符串
+
+        Raises:
+            ValueError: 当参数为空或签名过程失败时抛出
+        """
+        try:
+            # 1. 过滤空值参数,按参数名ASCII升序排列
+            # 注意:必须将所有参数值转换为字符串,并过滤None值
+            filtered_params = {}
+            for k, v in params.items():
+                if v is not None:
+                    # 将所有值转换为字符串
+                    filtered_params[k] = str(v)
+
+            # 按参数名升序排序
+            sorted_params = sorted(filtered_params.items(), key=lambda x: x[0])
+
+            # 2. 拼接参数名和参数值
+            param_str = "".join([f"{k}{v}" for k, v in sorted_params])
+
+            # 3. 首尾添加secret
+            sign_str = f"{self.secret}{param_str}{self.secret}"
+
+            # 调试:打印签名相关信息
+            LOGGER.debug(f"参与签名的参数: {sorted_params}")
+            LOGGER.debug(f"签名字符串: {sign_str}")
+
+            # 4. SHA1加密并转为十六进制字符串
+            sha1 = hashlib.sha1()
+            sha1.update(sign_str.encode("UTF-8"))
+            signature = sha1.hexdigest().upper()
+
+            LOGGER.debug(f"生成的签名: {signature}")
+            return signature
+
+        except Exception as e:
+            error_msg = f"签名生成失败: {str(e)}"
+            LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
+            raise ValueError(error_msg) from e
+
+    def _get_utc_timestamp(self) -> int:
+        """
+        获取当前UTC时间戳(秒),确保与服务器时间误差不超过5分钟
+
+        Returns:
+            int: UTC时间戳
+
+        Raises:
+            RuntimeError: 当系统时间异常时抛出
+        """
+        timestamp = int(time.time())
+        # 此处可添加时间校准逻辑(如对接NTP服务器),避免本地时间偏差
+        return timestamp
+
+    def _send_request(self, method: str, business_params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+        """
+        发送API请求(统一请求处理逻辑)
+
+        Args:
+            method (str): 服务方法名(如fc.function.pool.package.getall)
+            business_params (Optional[Dict[str, Any]]): 业务级参数,可选
+
+        Returns:
+            Dict[str, Any]: 接口返回的JSON数据
+
+        Raises:
+            RequestException: 网络请求异常时抛出
+            ValueError: 接口返回错误时抛出
+        """
+        # 1. 构建系统级参数
+        system_params = {
+            "appKey": self.app_key,
+            "t": self._get_utc_timestamp(),
+            "method": method
+            # 注意:sign参数不参与签名生成
+        }
+
+        # 2. 合并所有参数(过滤空值)
+        all_params = system_params.copy()
+        if business_params:
+            # 过滤掉业务参数中的None值
+            all_params.update({k: v for k, v in business_params.items() if v is not None})
+
+        # 3. 生成签名(使用所有参数,但不包括sign本身)
+        sign = self._generate_sign(all_params)
+
+        # 4. 将签名添加到请求参数中
+        all_params["sign"] = sign
+
+        LOGGER.info(f"API请求参数 - 方法: {method}, 时间戳: {all_params['t']}")
+        LOGGER.debug(f"完整请求参数: {all_params}")
+
+        try:
+            # 5. 发送POST请求(支持form-data格式)
+            response = self.session.post(
+                url=self.BASE_URL,
+                data=all_params,
+                timeout=self.DEFAULT_TIMEOUT,
+                headers={"Content-Type": "application/x-www-form-urlencoded"}
+            )
+            response.raise_for_status()  # 抛出HTTP状态码异常
+            result = response.json()
+            LOGGER.info(f"API响应结果 - 方法: {method}, 状态: {result.get('resultCode')}")
+
+            # 6. 校验接口返回状态
+            result_code = result.get("resultCode", -1)
+            if result_code != 0:
+                error_msg = f"接口调用失败 - 错误码: {result_code}, 错误信息: {result.get('errorMessage', '未知错误')}"
+                LOGGER.error(error_msg)
+                raise ValueError(error_msg)
+
+            return result
+        except RequestException as e:
+            error_msg = f"网络请求异常 - 方法: {method}, 错误: {str(e)}"
+            LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
+            raise RequestException(error_msg) from e
+        except ValueError as e:
+            # 已记录日志,直接向上抛出
+            raise
+        except Exception as e:
+            error_msg = f"请求处理异常 - 方法: {method}, 错误: {str(e)}"
+            LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
+            raise RuntimeError(error_msg) from e
+
+    def query_enterprise_pool_packages(self) -> List[Dict[str, Any]]:
+        """
+        3.1.1 企业套餐查询接口
+        查询企业与移远约定的流量池计费套餐列表
+
+        Returns:
+            List[Dict[str, Any]]: 套餐信息列表,包含id、name、flowsize等字段
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> packages = client.query_enterprise_pool_packages()
+            >>> print(packages[0]["name"])
+            "100G/360天"
+        """
+        method = "fc.function.pool.package.getall"
+        result = self._send_request(method=method)
+        # 按文档规范,返回数据在data字段中
+        return result.get("data", [])
+
+    def query_enterprise_pools(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None,
+            poolcode: Optional[str] = None,
+            page_no: Optional[int] = 1,
+            page_size: Optional[int] = 10
+    ) -> Dict[str, Any]:
+        """
+        3.1.2 企业流量池查询接口
+        查询企业账户下的流量池列表,支持多条件过滤和分页
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+            poolcode (Optional[str]): 指定流量池编号,精准查询
+            page_no (Optional[int]): 分页页码,默认1
+            page_size (Optional[int]): 分页大小,默认10,最大100
+
+        Returns:
+            Dict[str, Any]: 流量池查询结果,包含分页信息和流量池列表
+
+        Raises:
+            ValueError: 当分页参数无效时抛出
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> result = client.query_enterprise_pools(poolcode="PC1629946707")
+            >>> print(result["data"][0]["poolname"])
+            "测试流量池"
+        """
+        # 参数校验
+        if page_size and page_size > self.MAX_PAGE_SIZE:
+            raise ValueError(f"分页大小不能超过{self.MAX_PAGE_SIZE}")
+
+        # 构建业务参数
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid,
+            "poolcode": poolcode,
+            "pageNo": page_no,
+            "pageSize": page_size
+        }
+
+        method = "fc.function.pool.list"
+        result = self._send_request(method=method, business_params=business_params)
+        return result
+
+    def query_single_card_info(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None
+    ) -> Dict[str, Any]:
+        """
+        4.1.1 单卡信息查询接口
+        查询物联卡的详细信息,包括基本状态、流量使用、定向信息等
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+
+        Returns:
+            Dict[str, Any]: 单卡详细信息
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> card_info = client.query_single_card_info(iccid="89860446091891282224")
+            >>> print(card_info["msisdn"], card_info["status"])
+        """
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid
+        }
+
+        method = "fc.function.card.info"
+        result = self._send_request(method=method, business_params=business_params)
+
+        # 移除resultCode和errorMessage,直接返回数据主体
+        result.pop("resultCode", None)
+        result.pop("errorMessage", None)
+        return result
+
+    def query_single_card_realtimestatus(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None
+    ) -> Dict[str, Any]:
+        """
+        4.1.8.资产实时状态
+        查询资产的实时状态
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+
+        Returns:
+            Dict[str, Any]: 单卡详细信息
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+        """
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid
+        }
+
+        method = "fc.function.card.realtimestatus"
+        result = self._send_request(method=method, business_params=business_params)
+
+        # 移除resultCode和errorMessage,直接返回数据主体
+        result.pop("resultCode", None)
+        result.pop("errorMessage", None)
+        return result
+
+    def query_section_day_flow(
+            self,
+            section: Optional[str] = None,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None
+    ) -> Dict[str, Any]:
+        """
+        4.1.7.单卡日流量查询
+        资产指定时间段的流量查询
+
+        Args:
+            section (Optional[str]): 查询时间段,20201020
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+
+        Returns:
+            Dict[str, Any]: 单卡详细信息
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+        """
+        if not section:
+            raise ValueError("section参数必须提供")
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        business_params = {
+            "section": section,
+            "msisdn": msisdn,
+            "iccid": iccid
+        }
+
+        method = "fc.function.billing.section.dayflow"
+        result = self._send_request(method=method, business_params=business_params)
+
+        # 移除resultCode和errorMessage,直接返回数据主体
+        result.pop("resultCode", None)
+        result.pop("errorMessage", None)
+        return result
+
+    def query_orientation_info(
+            self,
+            strategycode: Optional[str] = None
+    ) -> List[Dict[str, Any]]:
+        """
+        4.1.11 定向信息查询接口
+        查询企业名下的定向策略信息
+
+        Args:
+            strategycode (Optional[str]): 定向策略编号,不传则查询所有定向信息
+
+        Returns:
+            List[Dict[str, Any]]: 定向策略信息列表
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> strategies = client.query_orientation_info()
+            >>> print(f"查询到{len(strategies)}个定向策略")
+        """
+        business_params = {
+            "strategycode": strategycode
+        }
+
+        method = "fc.function.orientation.info"
+        result = self._send_request(method=method, business_params=business_params)
+        return result.get("data", [])
+
+    def manage_card_flow(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None,
+            notify_emails: Optional[str] = None,
+            alarm_threshold1: Optional[str] = None,
+            alarm_threshold2: Optional[str] = None,
+            alarm_threshold3: Optional[str] = None,
+            alarm_type: Optional[int] = None,
+            alarm_status: Optional[int] = None,
+            pause_flow_threshold: Optional[int] = None,
+            pause_status: Optional[int] = None,
+            pause_type: Optional[int] = None
+    ) -> bool:
+        """
+        4.2.1 单卡流量管理接口
+        支持对单卡的流量使用设置预警、阈值,达到预警进行告警,达到阈值进行停机
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+            notify_emails (Optional[str]): 通知邮箱,多个邮箱以英文逗号隔开
+            alarm_threshold1 (Optional[str]): 预警阶段1
+            alarm_threshold2 (Optional[str]): 预警阶段2
+            alarm_threshold3 (Optional[str]): 预警阶段3
+            alarm_type (Optional[int]): 预警阶段类型,1百分比,2数值
+            alarm_status (Optional[int]): 预警启用状态,0停用,1启用
+            pause_flow_threshold (Optional[int]): 停机阈值(MB)
+            pause_status (Optional[int]): 停机阈值启用状态,0停用,1启用
+            pause_type (Optional[int]): 停机类型,1当月流量,2当月流量池内流量
+
+        Returns:
+            bool: 操作是否成功
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> success = client.manage_card_flow(
+            ...     iccid="89860446091891282224",
+            ...     notify_emails="admin@example.com",
+            ...     alarm_threshold1="1024",
+            ...     alarm_type=2,
+            ...     alarm_status=1,
+            ...     pause_flow_threshold=20480,
+            ...     pause_status=1,
+            ...     pause_type=1
+            ... )
+        """
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        # 构建流量管理参数JSON
+        param_json = {}
+
+        if notify_emails is not None:
+            param_json["notifyemails"] = notify_emails
+        if alarm_threshold1 is not None:
+            param_json["alarmthreshold1"] = alarm_threshold1
+        if alarm_threshold2 is not None:
+            param_json["alarmthreshold2"] = alarm_threshold2
+        if alarm_threshold3 is not None:
+            param_json["alarmthreshold3"] = alarm_threshold3
+        if alarm_type is not None:
+            param_json["alarmtype"] = alarm_type
+        if alarm_status is not None:
+            param_json["alarmstatus"] = alarm_status
+        if pause_flow_threshold is not None:
+            param_json["pauseflowthreshold"] = pause_flow_threshold
+        if pause_status is not None:
+            param_json["pausestatus"] = pause_status
+        if pause_type is not None:
+            param_json["pausetype"] = pause_type
+
+        import json
+        param_json_str = json.dumps(param_json, ensure_ascii=False)
+
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid,
+            "paramJson": param_json_str
+        }
+
+        method = "fc.function.billing.card.alarm.new"
+        self._send_request(method=method, business_params=business_params)
+        return True
+
+    def suspend_single_card(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None
+    ) -> bool:
+        """
+        4.2.2 单卡停机接口
+        主动停机处于正常状态的卡
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+
+        Returns:
+            bool: 操作是否成功
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> success = client.suspend_single_card(iccid="89860446091891282224")
+        """
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid
+        }
+
+        method = "fc.function.card.pause"
+        self._send_request(method=method, business_params=business_params)
+        return True
+
+    def resume_single_card(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None
+    ) -> bool:
+        """
+        4.2.3 单卡复机接口
+        主动复机处于停机状态的卡
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+
+        Returns:
+            bool: 操作是否成功
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> success = client.resume_single_card(iccid="89860446091891282224")
+        """
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid
+        }
+
+        method = "fc.function.card.resume"
+        self._send_request(method=method, business_params=business_params)
+        return True
+
+    def close_gprs_service(self, msisdn: Optional[str] = None, iccid: Optional[str] = None) -> bool:
+        """
+        4.2.4 单卡GPRS关闭接口
+        主动关闭物联卡的GPRS(数据服务)。只能处理隶属于该企业下的卡。
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid任选其一。
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn任选其一。
+
+        Returns:
+            Dict[str, Any]: 接口返回的JSON数据,包含resultCode和errorMessage字段。
+
+        Raises:
+            ValueError: 当msisdn和iccid均为空时抛出。
+        """
+        try:
+            if not msisdn and not iccid:
+                raise ValueError("必须提供msisdn或iccid参数")
+
+            method = "fc.function.card.gprs.close"
+            business_params = {}
+            if msisdn:
+                business_params["msisdn"] = msisdn
+            if iccid:
+                business_params["iccid"] = iccid
+            self._send_request(method=method, business_params=business_params)
+            return True
+        except Exception as e:
+            LOGGER.error(f"{iccid}关闭GPRS服务失败: {str(e)}")
+            return False
+
+    def open_gprs_service(self, msisdn: Optional[str] = None, iccid: Optional[str] = None) -> bool:
+        """
+        4.2.5 单卡GPRS开启接口
+        主动开启物联卡的GPRS(数据服务)。只能处理隶属于该企业下的卡。
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid任选其一。
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn任选其一。
+
+        Returns:
+            Dict[str, Any]: 接口返回的JSON数据,包含resultCode和errorMessage字段。
+
+        Raises:
+            ValueError: 当msisdn和iccid均为空时抛出。
+        """
+
+        try:
+            if not msisdn and not iccid:
+                raise ValueError("必须提供msisdn或iccid参数")
+
+            method = "fc.function.card.gprs.open"
+            business_params = {}
+            if msisdn:
+                business_params["msisdn"] = msisdn
+            if iccid:
+                business_params["iccid"] = iccid
+            self._send_request(method=method, business_params=business_params)
+            return True
+        except Exception as e:
+            LOGGER.error(f"{iccid}开启GPRS服务失败: {str(e)}")
+            return False
+
+    def activate_flow_card(
+            self,
+            msisdn: Optional[str] = None,
+            iccid: Optional[str] = None
+    ) -> bool:
+        """
+        4.2.6 流量卡激活接口
+        激活名下的物联卡(同一卡号30分钟内不能重复办理)
+
+        Args:
+            msisdn (Optional[str]): 物联卡号码,与iccid二选一
+            iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
+
+        Returns:
+            bool: 操作是否成功
+
+        Raises:
+            ValueError: 当msisdn和iccid都未提供时抛出
+
+        Example:
+            >>> client = QuecCloudApiClient("appKey", "secret")
+            >>> success = client.activate_flow_card(iccid="89860446091891282224")
+        """
+        if not msisdn and not iccid:
+            raise ValueError("msisdn和iccid参数必须至少提供一个")
+
+        business_params = {
+            "msisdn": msisdn,
+            "iccid": iccid
+        }
+
+        method = "fc.function.card.activate"
+        self._send_request(method=method, business_params=business_params)
+        return True

+ 7 - 0
Object/UnicomObject.py

@@ -21,6 +21,7 @@ from Model.models import UnicomDeviceInfo
 from Object.RedisObject import RedisObject
 from Object.utils import SM3Util
 from Object.utils.SymmetricCryptoUtil import AESencrypt
+from Service.QuecCloudService import QuecCloudService
 from Service.TelecomService import TelecomService
 
 """
@@ -313,6 +314,8 @@ class UnicomObjeect:
         expire_time = 60 * 10 + 60
         if card_type == 3:  # 鼎芯电信
             return TelecomService().query_total_usage_by_access_number(iccid, flow_key.format(iccid), expire_time)
+        elif card_type == 6:  # 移远电信
+            return QuecCloudService().get_flow_total_usage(flow_key.format(iccid), expire_time, **usage_data)
         # 珠海联通
         return UnicomObjeect().get_flow_total_usage(flow_key.format(iccid), expire_time, **usage_data)
 
@@ -334,6 +337,8 @@ class UnicomObjeect:
         if card_info[0]['card_type'] == 3:
             TelecomService.update_access_number_network(iccid, card_info[0]['access_number'], 'DEL', reason)
             return True
+        elif card_info[0]['card_type'] == 6:
+            return QuecCloudService.resume_card(iccid)
         if iccid:
             re_data = {'iccid': iccid}
             result = UnicomObjeect().query_device_status(**re_data)
@@ -363,6 +368,8 @@ class UnicomObjeect:
         if card_info[0]['card_type'] == 3:
             TelecomService.update_access_number_network(iccid, card_info[0]['access_number'], 'ADD', reason)
             return True
+        elif card_info[0]['card_type'] == 6:
+            return QuecCloudService.suspend_single_card(iccid)
         if iccid:
             re_data = {"iccid": iccid, "status": 3}
             response = UnicomObjeect().update_device_state(**re_data)

+ 265 - 0
Service/QuecCloudService.py

@@ -0,0 +1,265 @@
+# -*- encoding: utf-8 -*-
+"""
+@File    : QuecCloudService.py
+@Time    : 2025/11/19 15:13
+@Author  : stephen
+@Email   : zhangdongming@asj6.wecom.work
+@Software: PyCharm
+"""
+import json
+import time
+from decimal import Decimal
+from typing import Any, Optional
+
+from django.conf import settings
+
+from Ansjer.config import LOGGER
+from Model.models import UnicomDeviceInfo
+from Object.QuecCloudObject import QuecCloudApiClient
+from Object.RedisObject import RedisObject
+
+# 移远AK/SK
+QUEC_CLOUD_AK = settings.QUEC_CLOUD_AK
+QUEC_CLOUD_SK = settings.QUEC_CLOUD_SK
+QUEC_CLIENT = QuecCloudApiClient(
+    app_key=QUEC_CLOUD_AK,
+    secret=QUEC_CLOUD_SK
+)
+# 定义统一的 Key
+REDIS_FAIL_LOG_KEY = "asj:sim_card:op_failures"
+# 实例化你的 Redis 类 (确保配置正确)
+redis_client = RedisObject()
+MAX_LOG_LENGTH = 1000
+
+
+class QuecCloudService:
+
+    @staticmethod
+    def get_flow_total_usage(key: str, expire: int = 600, **usage_data: Any) -> int | Decimal:
+        """
+        获取设备当前队列的流量用量详情(带缓存功能)
+
+        该函数用于查询指定设备的流量使用情况,并通过缓存机制减少重复查询,
+        提高系统性能。缓存时效由expire参数控制,** usage_data用于传递查询所需的额外参数。
+
+        Args:
+            key (str): 缓存的唯一标识键,通常与设备唯一ID相关联
+            expire (int, optional): 缓存失效时间(秒),默认值为600秒(10分钟)
+            **usage_data (Any): 可变关键字参数,用于传递额外查询参数,如iccid、imei等
+
+        Returns:
+            Optional[Decimal]: 流量用量详情,用量数据(如已用流量),
+                查询失败或无数据时返回数据库已历史流量记录
+
+        Example:
+            >>> QuecCloudService.get_flow_total_usage("device_123", 600, iccid="8986112321603574202")
+             return "39.13"
+        """
+        redis = RedisObject()
+        sim_flow_used_total = redis.get_data(key)
+        if sim_flow_used_total:
+            return Decimal(sim_flow_used_total).quantize(Decimal('0.00'))
+        else:
+            # 检查 iccid 是否存在
+            if 'iccid' not in usage_data:
+                LOGGER.error('缺少必要参数: iccid')
+                return Decimal('0.00')
+
+            # 查询SIM卡信息
+            sim_qs = UnicomDeviceInfo.objects.filter(iccid=usage_data['iccid'])
+            if not sim_qs:
+                return Decimal('0.00')
+            sim_vo = sim_qs.first()
+            try:
+                # 异步调用占位(需根据实际框架扩展)
+                card_info = QUEC_CLIENT.query_single_card_info(iccid=usage_data['iccid'])
+                cycle_total = card_info.get('usedflow')
+                if cycle_total is None:
+                    LOGGER.error(f'移远查询流量异常: 未返回 usedflow 字段, iccid:{usage_data["iccid"]}')
+                    return sim_vo.sim_used_flow + sim_vo.sim_cycle_used_flow
+            except Exception as e:
+                LOGGER.error(f'移远查询流量异常,iccid:{usage_data["iccid"]},error:{str(e)}')
+                return sim_vo.sim_used_flow + sim_vo.sim_cycle_used_flow
+
+            cycle_total = Decimal(cycle_total).quantize(Decimal('0.00'))
+
+            n_time = int(time.time())
+
+            # 判断数据库周期流量用量 是否大于API查询出来的周期用量 如果是则判定进入了下一个周期
+            if sim_vo.sim_cycle_used_flow != 0 and sim_vo.sim_cycle_used_flow > cycle_total:
+                sim_used_flow = sim_vo.sim_used_flow + sim_vo.sim_cycle_used_flow
+                sim_qs.update(
+                    sim_used_flow=sim_used_flow,
+                    sim_cycle_used_flow=cycle_total,
+                    updated_time=n_time
+                )
+                # 队列用量历史总量 + 上一个周期流量 + 当前周期流量 = 总消耗流量
+                sim_flow_used_total = sim_used_flow + cycle_total
+            elif cycle_total > sim_vo.sim_cycle_used_flow:  # API周期用量大于当前数据库用量则更新记录
+                sim_qs.update(sim_cycle_used_flow=cycle_total, updated_time=n_time)
+                # 队列用量历史总量 + 当前周期流量 = 总消耗流量
+                sim_flow_used_total = sim_vo.sim_used_flow + cycle_total
+            else:
+                sim_flow_used_total = sim_vo.sim_used_flow + sim_vo.sim_cycle_used_flow
+            redis.CONN.setnx(key, str(sim_flow_used_total))
+            redis.CONN.expire(key, expire)
+        return sim_flow_used_total
+
+    @staticmethod
+    def resume_card(iccid: str) -> Optional[bool]:
+        """
+        恢复 SIM 卡功能(激活已停用的 SIM 卡)
+        """
+        action_name = "激活"
+        try:
+            VALID_QUEC_STATUSES = {'正常', '测试期正常', '待激活'}
+            quec_card_info = QUEC_CLIENT.query_single_card_info(iccid=iccid)
+
+            if quec_card_info:
+                quec_status = quec_card_info.get('status')
+                gprsstatus = quec_card_info.get('gprsstatus')
+
+                if quec_status not in VALID_QUEC_STATUSES:
+                    QUEC_CLIENT.resume_single_card(iccid=iccid)
+                elif gprsstatus == '0':
+                    result = QUEC_CLIENT.open_gprs_service(iccid=iccid)
+                    LOGGER.info(f"{iccid}移远卡GPRS启动结果: {result}")
+                    # 如果 API 返回 False/None,视作失败进行记录
+                    if not result:
+                        QuecCloudService.record_sim_failure(iccid, action_name, "API返回操作失败或未响应")
+            return True
+
+        except Exception as e:
+            error_msg = str(e)
+            LOGGER.error(f"{iccid}移远卡恢复失败: {error_msg}")
+
+            # === 【新增】记录到 Redis 供运营查看 ===
+            QuecCloudService.record_sim_failure(iccid, action_name, error_msg)
+
+            return False
+
+    @staticmethod
+    def suspend_single_card(iccid: str) -> Optional[bool]:
+        """
+        停用 SIM 卡功能(停用 SIM 卡)
+        """
+        action_name = "停用"
+        try:
+            result = QUEC_CLIENT.close_gprs_service(iccid=iccid)
+            LOGGER.info(f"{iccid}移远卡GPRS停机结果: {result}")
+
+            # 如果 API 返回 False/None,视作失败进行记录
+            if not result:
+                QuecCloudService.record_sim_failure(iccid, action_name, "API返回操作失败或未响应")
+
+            return result
+
+        except Exception as e:
+            error_msg = str(e)
+            LOGGER.error(f"{iccid}移远卡停机失败: {error_msg}")
+
+            # === 【新增】记录到 Redis 供运营查看 ===
+            QuecCloudService.record_sim_failure(iccid, action_name, error_msg)
+
+            return False
+
+    @staticmethod
+    def is_quec_cloud_sim(iccid: Optional[str] = None):
+        """
+                单卡查询信息接口
+                Args:
+                    iccid (Optional[str]): 集成电路卡识别码
+                Returns:
+                    bool: 操作是否成功
+                Raises:
+                    ValueError: 当msisdn和iccid都未提供时抛出
+        """
+        try:
+            iccid = iccid[0:19]
+            card_info = QUEC_CLIENT.query_single_card_info(iccid=iccid)
+            LOGGER.info(f"{iccid}查询是否是QUEC云卡 状态:{card_info.get('status')},"
+                        f"已用流量:{card_info.get('usedflow')},套餐类型:{card_info.get('setmealType')}")
+            return True
+        except Exception as e:
+            LOGGER.error(f"{iccid}单卡信息查询失败: {str(e)}")
+            return False
+
+    @staticmethod
+    def query_quec_cloud_sim_info(iccid: Optional[str] = None) -> Optional[dict]:
+        """
+                单卡查询信息接口
+                Args:
+                    iccid (Optional[str]): 集成电路卡识别码
+                Raises:
+                    ValueError: 当msisdn和iccid都未提供时抛出
+        """
+        try:
+            iccid = iccid[0:19]
+            return QUEC_CLIENT.query_single_card_info(iccid=iccid)
+        except Exception as e:
+            LOGGER.error(f"{iccid}单卡信息查询失败: {str(e)}")
+            return None
+
+    @staticmethod
+    def record_sim_failure(iccid: str, action_type: str, error_msg: str):
+        """
+        记录失败操作到 Redis List,并自动清理旧数据
+        """
+        try:
+            # 1. 准备数据
+            current_time = int(time.time())
+            log_data = {
+                "iccid": iccid,
+                "action": action_type,
+                "error": str(error_msg),
+                "time": current_time,
+                "status": "pending"
+            }
+            val = json.dumps(log_data, ensure_ascii=False)
+
+            # 2. 写入 Redis (rpush: 从右边插入)
+            # 注意:这里使用你 RedisObject 实例化的对象,假设变量名叫 redis_client
+            redis_client.rpush(REDIS_FAIL_LOG_KEY, val)
+
+            # ==========================================
+            # 3. 防爆逻辑 (放在这里)
+            # ==========================================
+            # 获取当前长度
+            current_len = redis_client.llen(REDIS_FAIL_LOG_KEY)
+
+            # 如果超过最大长度,从左边弹出最老的数据 (lpop)
+            if current_len and current_len > MAX_LOG_LENGTH:
+                # 循环弹出,直到长度符合要求(防止并发写入时瞬间超出很多)
+                # 这里简单点只弹出一个也可以,通常够用了
+                redis_client.lpop(REDIS_FAIL_LOG_KEY)
+
+        except Exception as e:
+            # 记录日志本身如果报错,打印一下,不要影响主业务
+            LOGGER.error(f"写入Redis失败日志出错: {str(e)}")
+
+    @staticmethod
+    def get_failure_logs(page=1, page_size=20):
+        """
+        获取失败记录列表(支持简单分页)
+        Redis List 下标从 0 开始
+        """
+        start = (page - 1) * page_size
+        end = start + page_size - 1
+
+        # 使用 RedisObject 的 lrange 方法
+        # 注意:lrange 获取的是 字节(bytes) 或 字符串,需要反序列化
+        raw_list = redis_client.lrange(REDIS_FAIL_LOG_KEY, start, end)
+
+        formatted_list = []
+        if raw_list:
+            for item in raw_list:
+                try:
+                    # 如果 item 是 bytes,需要 decode,如果是 str 则不需要
+                    if isinstance(item, bytes):
+                        item = item.decode('utf-8')
+                    formatted_list.append(json.loads(item))
+                except Exception as e:
+                    LOGGER.error(f"反序列化失败日志出错: {str(e)}")
+                    continue
+
+        return formatted_list