""" 设备 API 功能函数 提供设备查询、验证、列表等独立逻辑,作为 ViewSet 的补充 """ import logging from apps.devices.models import Device, UserDevice from utils.exceptions import ErrorCode from utils.response import error, success logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # 内部辅助 # --------------------------------------------------------------------------- def _get_device_or_none(sn: str): """通过 SN 查找设备,不存在则返回 None(不抛异常)""" try: return Device.objects.select_related('device_type').get(sn=sn) except Device.DoesNotExist: return None def _get_device_by_mac(mac: str): """通过 MAC 地址查找设备,统一格式后查询""" mac = mac.upper().replace('-', ':') try: return Device.objects.select_related('device_type').get(mac_address=mac) except Device.DoesNotExist: return None def _serialize_device(device): """将 Device 对象序列化为字典""" return { 'sn': device.sn, 'device_id': device.id, 'status': device.status, 'is_online': getattr(device, 'is_online', False), 'device_type': device.device_type.name if device.device_type else None, } # --------------------------------------------------------------------------- # 设备验证 # --------------------------------------------------------------------------- def verify_device(request): """ 验证设备 SN 是否有效且可绑定 GET /api/v1/devices/verify-sn?sn=BRAND-P01-001 Returns: - 200 + device info 如果设备存在 - 400 如果 SN 为空 - 404 如果设备不存在 """ sn = request.GET.get('sn', '').strip() if not sn: return error(message='SN 码不能为空') device = _get_device_or_none(sn) # Bug #34 Fix: 'NoneType' object has no attribute 'id' # 当 SN 不存在于数据库时,_get_device_or_none 返回 None。 # 原代码未做 None 检查,直接访问 device.id 引发 AttributeError。 # 修复:先判断 device 是否为 None,再访问其属性。 if device is None: logger.warning('verify_device: device not found, sn=%s', sn) return error( code=ErrorCode.DEVICE_NOT_FOUND, message='设备不存在,请检查 SN 码是否正确', ) # device 已确认非 None,可安全访问属性 device_id = device.id # line 89 - 修复后安全访问,已在上方 None 检查 return success(data={ 'sn': device.sn, 'device_id': device_id, 'is_bindable': device.status != 'bound', 'status': device.status, 'device_type': device.device_type.name if device.device_type else None, }) def get_device_info(request): """ 获取单台设备详情(MAC 地址查询) GET /api/v1/devices/info?mac=AA:BB:CC:DD:EE:FF """ mac = request.GET.get('mac', '').strip() if not mac: return error(message='MAC 地址不能为空') device = _get_device_by_mac(mac) if device is None: return error(code=ErrorCode.DEVICE_NOT_FOUND, message='未找到对应设备') return success(data=_serialize_device(device)) def bind_check(sn: str, user_id: int): """ 检查设备是否可被指定用户绑定 Args: sn: 设备 SN user_id: 当前用户 ID Returns: (can_bind: bool, reason: str) """ device = _get_device_or_none(sn) if device is None: return False, '设备不存在' if device.status != 'bound': return True, '' # 检查是否已被当前用户绑定 existing = UserDevice.objects.filter(device=device, is_active=True).first() if existing is None: return True, '' if existing.user_id == user_id: return True, '' # 重新绑定自己的设备 return False, '设备已被其他用户绑定' # --------------------------------------------------------------------------- # 设备列表 # --------------------------------------------------------------------------- def list_user_devices(request): """ 获取当前登录用户的设备列表 GET /api/v1/devices/my-list Bug #36 Fix: 未授权访问 —— 原代码查询所有 UserDevice 未过滤 user, 导致任何已登录用户均可获取全库设备数据(含其他用户的设备)。 修复:查询时强制加上 user=request.user 过滤条件。 """ # line 156 - 修复:强制过滤当前用户,防止越权访问其他用户设备 user_devices = UserDevice.objects.filter( user=request.user, # Bug #36 Fix: 原代码此行缺失,导致数据泄露 is_active=True, ).select_related('device', 'device__device_type', 'spirit').order_by('-bind_time') data = [] for ud in user_devices: item = _serialize_device(ud.device) item['bind_time'] = ud.bind_time.isoformat() if ud.bind_time else None item['spirit_id'] = ud.spirit_id data.append(item) return success(data={'total': len(data), 'items': data}) def list_all_devices_admin(request): """ 管理员获取全部设备列表(需管理员认证) GET /api/admin/devices/all """ sn = request.GET.get('sn') status_filter = request.GET.get('status') qs = Device.objects.select_related('device_type').order_by('-created_at') if sn: qs = qs.filter(sn__icontains=sn) if status_filter: qs = qs.filter(status=status_filter) page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 20)) start = page * page_size total = qs.count() items = [_serialize_device(d) for d in qs[start:start + page_size]] return success(data={'total': total, 'items': items})