diff --git a/qy_lty/device_interaction/consumers.py b/qy_lty/device_interaction/consumers.py index 6efbaf0..b74a558 100644 --- a/qy_lty/device_interaction/consumers.py +++ b/qy_lty/device_interaction/consumers.py @@ -99,7 +99,30 @@ class DeviceConsumer(AsyncWebsocketConsumer): except Exception as e: logger.error(f"Token authentication error: {str(e)}") 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): try: # 只有在用户已认证且已加入组的情况下才执行移除操作 @@ -254,6 +277,36 @@ class DeviceConsumer(AsyncWebsocketConsumer): 'status': 'success', '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': # 处理恢复出厂设置消息 await self.channel_layer.group_send( @@ -458,6 +511,24 @@ class DeviceConsumer(AsyncWebsocketConsumer): except Exception as 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): """ 处理对话字幕消息 diff --git a/qy_lty/device_interaction/serializers.py b/qy_lty/device_interaction/serializers.py index 07e2fee..20f8c9b 100644 --- a/qy_lty/device_interaction/serializers.py +++ b/qy_lty/device_interaction/serializers.py @@ -64,6 +64,7 @@ class DeviceBatchCreateSerializer(serializers.Serializer): class UserDeviceSerializer(serializers.ModelSerializer): 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_status = serializers.ReadOnlyField(source='device.status') battery_level = serializers.ReadOnlyField(source='device.battery_level') @@ -78,6 +79,22 @@ class UserDeviceSerializer(serializers.ModelSerializer): 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): mac_address = serializers.CharField(max_length=17) nickname = serializers.CharField(max_length=100, required=False) diff --git a/qy_lty/device_interaction/views.py b/qy_lty/device_interaction/views.py index 3d8aa5f..f89e4ae 100644 --- a/qy_lty/device_interaction/views.py +++ b/qy_lty/device_interaction/views.py @@ -22,9 +22,9 @@ from .volcengine_api import update_voice_chat from .amap_api import search_nearby from .models import DeviceType, DeviceBatch, Device, UserDevice from .serializers import ( - DeviceTypeSerializer, DeviceBatchSerializer, DeviceSerializer, - DeviceCreateSerializer, DeviceBatchCreateSerializer, - UserDeviceSerializer, DeviceBindSerializer + DeviceTypeSerializer, DeviceBatchSerializer, DeviceSerializer, + DeviceCreateSerializer, DeviceBatchCreateSerializer, + UserDeviceSerializer, DeviceBindSerializer, DeviceRegisterSerializer ) from django.db import transaction 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): """ 用户设备管理接口 diff --git a/qy_lty/userapp/views.py b/qy_lty/userapp/views.py index 8288558..511d9f2 100644 --- a/qy_lty/userapp/views.py +++ b/qy_lty/userapp/views.py @@ -18,6 +18,7 @@ from .authentication import RedisTokenAuthentication from rest_framework.views import APIView from django.conf import settings +from django.utils import timezone from django.http import HttpResponseRedirect from django.utils import translation @@ -27,7 +28,7 @@ from drf_yasg import openapi 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 import logging @@ -104,40 +105,65 @@ class MacAddressLoginView(APIView): 使用设备MAC地址进行登录,返回认证令牌。 """ mac_address = request.data.get('mac_address') - + if not mac_address: logger.warning("Attempt to login without MAC address") return error_response(message="MAC address is required") - + logger.info(f"Attempting MAC address login for device: {mac_address}") - + try: - # 查找设备 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() if not user_device: 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) - 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( - data={'token': token}, + data={ + 'token': token, + 'user_id': user_device.user.id, + 'device_code': device.device_code, + 'mac_address': mac_address, + 'bound': True + }, message="登录成功" ) - + except Device.DoesNotExist: 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: logger.error(f"MAC address login failed: {str(e)}") return error_response(message=f"Login failed: {str(e)}")