rtc_backend/app/api/device_api.py
2026-02-25 14:07:32 +08:00

182 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
设备 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 - 1) * page_size
total = qs.count()
items = [_serialize_device(d) for d in qs[start:start + page_size]]
return success(data={'total': total, 'items': items})