feat: 实现设备动态绑定方案(步骤1-5)

- 步骤1: MacAddressLoginView 增强,返回 code=4010(未绑定)/4011(不存在),支持自动激活
- 步骤2: 新增 bind_status 接口,设备端轮询查询绑定状态(无需认证)
- 步骤3: 新增 register 设备自注册接口,首次开机自动注册(无需认证)
- 步骤4: UserDeviceSerializer 增加 mac_address 字段
- 步骤5: WebSocket 新增 device_info 消息类型,支持设备状态上报和广播

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
pmc 2026-03-20 15:53:33 +08:00
parent 8b16eb32bf
commit afa88c142b
4 changed files with 276 additions and 23 deletions

View File

@ -100,6 +100,29 @@ class DeviceConsumer(AsyncWebsocketConsumer):
logger.error(f"Token authentication error: {str(e)}") logger.error(f"Token authentication error: {str(e)}")
return None return None
@database_sync_to_async
def update_device_status(self, mac_address, device_data):
"""
根据设备上报信息更新数据库中的设备状态
"""
try:
from .models import Device
device = Device.objects.filter(mac_address=mac_address).first()
if device:
if 'battery_level' in device_data:
device.battery_level = device_data['battery_level']
if 'firmware_version' in device_data:
device.firmware_version = device_data['firmware_version']
if 'wifi_name' in device_data:
device.wifi_name = device_data['wifi_name']
if 'brightness' in device_data:
device.brightness = device_data['brightness']
device.status = 'connected'
device.save()
logger.info(f"Updated device status for MAC: {mac_address}")
except Exception as e:
logger.error(f"Failed to update device status: {str(e)}")
async def disconnect(self, close_code): async def disconnect(self, close_code):
try: try:
# 只有在用户已认证且已加入组的情况下才执行移除操作 # 只有在用户已认证且已加入组的情况下才执行移除操作
@ -254,6 +277,36 @@ class DeviceConsumer(AsyncWebsocketConsumer):
'status': 'success', 'status': 'success',
'message': '对话状态消息已发送' 'message': '对话状态消息已发送'
})) }))
elif message_type == 'device_info':
# 处理设备信息上报设备连WiFi后发送MAC、电量等
try:
device_data = json.loads(message) if isinstance(message, str) else message
# 更新数据库中的设备状态
mac_address = device_data.get('mac_address')
if mac_address:
await self.update_device_status(mac_address, device_data)
# 广播到 group让手机端也能收到
await self.channel_layer.group_send(
self.group_name,
{
'type': 'device_info',
'message': device_data,
'user_id': self.user_id
}
)
await self.send(text_data=json.dumps({
'status': 'success',
'message': '设备信息已更新'
}))
except Exception as e:
logger.error(f"Error processing device_info: {str(e)}")
await self.send(text_data=json.dumps({
'status': 'error',
'message': f'设备信息处理失败: {str(e)}'
}))
elif message_type == 'factory_reset': elif message_type == 'factory_reset':
# 处理恢复出厂设置消息 # 处理恢复出厂设置消息
await self.channel_layer.group_send( await self.channel_layer.group_send(
@ -458,6 +511,24 @@ class DeviceConsumer(AsyncWebsocketConsumer):
except Exception as e: except Exception as e:
logger.error(f"Error in factory_reset: {str(e)}") logger.error(f"Error in factory_reset: {str(e)}")
async def device_info(self, event):
"""
处理设备信息上报消息
将设备信息转发给 WebSocket手机端会收到
"""
try:
message = event['message']
user_id = event.get('user_id', '')
await self.send(text_data=json.dumps({
'type': 'device_info',
'message': message,
'user_id': user_id
}))
logger.info(f"Sent device_info to WebSocket: mac={message.get('mac_address', 'unknown')}")
except Exception as e:
logger.error(f"Error in device_info: {str(e)}")
async def conversation_subtitle(self, event): async def conversation_subtitle(self, event):
""" """
处理对话字幕消息 处理对话字幕消息

View File

@ -64,6 +64,7 @@ class DeviceBatchCreateSerializer(serializers.Serializer):
class UserDeviceSerializer(serializers.ModelSerializer): class UserDeviceSerializer(serializers.ModelSerializer):
device_code = serializers.ReadOnlyField(source='device.device_code') device_code = serializers.ReadOnlyField(source='device.device_code')
mac_address = serializers.ReadOnlyField(source='device.mac_address')
device_type = serializers.ReadOnlyField(source='device.device_type.name') device_type = serializers.ReadOnlyField(source='device.device_type.name')
device_status = serializers.ReadOnlyField(source='device.status') device_status = serializers.ReadOnlyField(source='device.status')
battery_level = serializers.ReadOnlyField(source='device.battery_level') battery_level = serializers.ReadOnlyField(source='device.battery_level')
@ -78,6 +79,22 @@ class UserDeviceSerializer(serializers.ModelSerializer):
read_only_fields = ('user', 'bound_at') read_only_fields = ('user', 'bound_at')
class DeviceRegisterSerializer(serializers.Serializer):
mac_address = serializers.CharField(max_length=17)
device_type_code = serializers.CharField(max_length=10, required=False, default='T01')
firmware_version = serializers.CharField(max_length=50, required=False, default='')
def validate_mac_address(self, value):
if Device.objects.filter(mac_address=value).exists():
raise serializers.ValidationError("设备已注册")
return value
def validate_device_type_code(self, value):
if not DeviceType.objects.filter(code=value).exists():
raise serializers.ValidationError(f"设备类型 {value} 不存在")
return value
class DeviceBindSerializer(serializers.Serializer): class DeviceBindSerializer(serializers.Serializer):
mac_address = serializers.CharField(max_length=17) mac_address = serializers.CharField(max_length=17)
nickname = serializers.CharField(max_length=100, required=False) nickname = serializers.CharField(max_length=100, required=False)

View File

@ -24,7 +24,7 @@ from .models import DeviceType, DeviceBatch, Device, UserDevice
from .serializers import ( from .serializers import (
DeviceTypeSerializer, DeviceBatchSerializer, DeviceSerializer, DeviceTypeSerializer, DeviceBatchSerializer, DeviceSerializer,
DeviceCreateSerializer, DeviceBatchCreateSerializer, DeviceCreateSerializer, DeviceBatchCreateSerializer,
UserDeviceSerializer, DeviceBindSerializer UserDeviceSerializer, DeviceBindSerializer, DeviceRegisterSerializer
) )
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
@ -415,6 +415,145 @@ class DeviceViewSet(viewsets.ModelViewSet):
) )
@swagger_auto_schema(
operation_summary="查询设备绑定状态",
operation_description="通过MAC地址查询设备是否已被用户绑定不需要认证",
manual_parameters=[mac_address_param],
responses={
200: openapi.Response('查询成功', get_standardized_response_schema()),
400: openapi.Response('参数错误', get_standardized_response_schema()),
404: openapi.Response('设备不存在', get_standardized_response_schema())
}
)
@action(detail=False, methods=['get'], permission_classes=[AllowAny], authentication_classes=[])
def bind_status(self, request):
"""
查询设备绑定状态
通过MAC地址查询设备是否已被用户绑定
此接口不需要认证供设备端轮询使用
"""
mac_address = request.query_params.get('mac_address')
if not mac_address:
return error_response(message='MAC地址不能为空', code=status.HTTP_400_BAD_REQUEST)
try:
device = Device.objects.get(mac_address=mac_address)
user_device = UserDevice.objects.filter(device=device).first()
if user_device:
return success_response(
data={
'bound': True,
'user_id': user_device.user.id,
'nickname': user_device.nickname or '',
'device_code': device.device_code,
'is_primary': user_device.is_primary
},
message='设备已绑定'
)
else:
return success_response(
data={
'bound': False,
'device_code': device.device_code,
'mac_address': mac_address
},
message='设备未绑定'
)
except Device.DoesNotExist:
return not_found_response(message='设备不存在')
@swagger_auto_schema(
operation_summary="设备自注册",
operation_description="设备首次开机时自动注册(不需要认证)",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['mac_address'],
properties={
'mac_address': openapi.Schema(type=openapi.TYPE_STRING, description='设备MAC地址'),
'device_type_code': openapi.Schema(type=openapi.TYPE_STRING, description='设备类型代码默认T01'),
'firmware_version': openapi.Schema(type=openapi.TYPE_STRING, description='固件版本号'),
}
),
responses={
201: openapi.Response('注册成功', get_standardized_response_schema()),
400: openapi.Response('参数错误', get_standardized_response_schema()),
}
)
@action(detail=False, methods=['post'], permission_classes=[AllowAny], authentication_classes=[])
def register(self, request):
"""
设备自注册
设备首次开机时如果MAC地址不存在则自动注册设备
如果MAC地址已存在返回已存在的设备信息
此接口不需要认证
"""
mac_address = request.data.get('mac_address')
if not mac_address:
return error_response(message='MAC地址不能为空', code=status.HTTP_400_BAD_REQUEST)
# 如果设备已存在,直接返回设备信息
existing_device = Device.objects.filter(mac_address=mac_address).first()
if existing_device:
return success_response(
data={
'device_code': existing_device.device_code,
'mac_address': existing_device.mac_address,
'is_new': False
},
message='设备已注册'
)
device_type_code = request.data.get('device_type_code', 'T01')
firmware_version = request.data.get('firmware_version', '')
try:
device_type = DeviceType.objects.get(code=device_type_code)
except DeviceType.DoesNotExist:
return error_response(message=f'设备类型 {device_type_code} 不存在', code=status.HTTP_400_BAD_REQUEST)
# 获取或创建一个默认批次
batch, _ = DeviceBatch.objects.get_or_create(
device_type=device_type,
batch_number=f'AUTO-{device_type_code}',
defaults={
'production_date': timezone.now().date(),
'quantity': 99999,
'description': '设备自动注册批次'
}
)
# 生成序列号
existing_count = Device.objects.filter(batch=batch).count()
serial_number = f"{existing_count + 1:05d}"
try:
device = Device(
device_type=device_type,
batch=batch,
serial_number=serial_number,
mac_address=mac_address,
firmware_version=firmware_version
)
device.save()
return created_response(
data={
'device_code': device.device_code,
'mac_address': device.mac_address,
'is_new': True
},
message='设备注册成功'
)
except Exception as e:
logger.error(f"Device registration failed: {str(e)}")
return error_response(message=f'注册失败: {str(e)}', code=status.HTTP_500_INTERNAL_SERVER_ERROR)
class UserDeviceViewSet(viewsets.ModelViewSet): class UserDeviceViewSet(viewsets.ModelViewSet):
""" """
用户设备管理接口 用户设备管理接口

View File

@ -18,6 +18,7 @@ from .authentication import RedisTokenAuthentication
from rest_framework.views import APIView from rest_framework.views import APIView
from django.conf import settings from django.conf import settings
from django.utils import timezone
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils import translation from django.utils import translation
@ -27,7 +28,7 @@ from drf_yasg import openapi
from rest_framework import serializers from rest_framework import serializers
# 引入标准化响应工具 # 引入标准化响应工具
from common.responses import success_response, error_response, created_response from common.responses import success_response, error_response, created_response, api_response
from common.swagger_utils import get_standardized_response_schema, StandardizedResponseSchema from common.swagger_utils import get_standardized_response_schema, StandardizedResponseSchema
import logging import logging
@ -112,32 +113,57 @@ class MacAddressLoginView(APIView):
logger.info(f"Attempting MAC address login for device: {mac_address}") logger.info(f"Attempting MAC address login for device: {mac_address}")
try: try:
# 查找设备
device = Device.objects.get(mac_address=mac_address) device = Device.objects.get(mac_address=mac_address)
# 检查设备是否已激活
if not device.is_active:
logger.warning(f"Device not active: {mac_address}")
return error_response(message="Device is not active")
# 检查设备是否已绑定给用户 # 检查设备是否已绑定给用户
user_device = UserDevice.objects.filter(device=device).first() user_device = UserDevice.objects.filter(device=device).first()
if not user_device: if not user_device:
logger.warning(f"Device not bound to any user: {mac_address}") logger.warning(f"Device not bound to any user: {mac_address}")
return error_response(message="Device is not bound to any user") # 返回特定 code=4010让设备端可以识别"未绑定"状态
return api_response(
success=False,
message="Device is not bound to any user",
code=4010,
data={
'device_code': device.device_code,
'mac_address': mac_address,
'bound': False
}
)
# 生成认证令牌 # 如果设备未激活,在登录时自动激活
if not device.is_active:
device.is_active = True
device.activated_at = timezone.now()
device.save()
logger.info(f"Device auto-activated on login: {mac_address}")
# 生成绑定用户的 token
token = generate_token(user_device.user.id) token = generate_token(user_device.user.id)
logger.info(f"Successfully logged in device with MAC: {mac_address}") logger.info(f"Successfully logged in device with MAC: {mac_address}, bound to user: {user_device.user.id}")
return success_response( return success_response(
data={'token': token}, data={
'token': token,
'user_id': user_device.user.id,
'device_code': device.device_code,
'mac_address': mac_address,
'bound': True
},
message="登录成功" message="登录成功"
) )
except Device.DoesNotExist: except Device.DoesNotExist:
logger.warning(f"Device not found: {mac_address}") logger.warning(f"Device not found: {mac_address}")
return error_response(message="Device not found") return api_response(
success=False,
message="Device not found",
code=4011,
data={
'mac_address': mac_address,
'registered': False
}
)
except Exception as e: except Exception as e:
logger.error(f"MAC address login failed: {str(e)}") logger.error(f"MAC address login failed: {str(e)}")
return error_response(message=f"Login failed: {str(e)}") return error_response(message=f"Login failed: {str(e)}")