# 服务器端修改指南 — 设备动态绑定 > 本文档对应手机端文档 `LTY_App_Project_URP/docs/修改指南_手机端.md` > 和设备端文档 `LTY_Project/docs/修改指南_设备端.md` > 三个文档可以交叉验证。 > > **注意**:手机端文档新增了**步骤 0(手机号登录功能)**作为前置基础, > 后续步骤编号与本文档/设备端文档不完全对齐,请以各文档内的交叉引用为准。 --- ## 步骤 1:修改 MAC 登录接口,支持返回绑定用户信息 ### 目标 当前 `MacAddressLoginView` 已经在做正确的事:查找设备 → 检查绑定 → 返回绑定用户的 token。但需要增强返回信息,让设备端能区分"未绑定"和其他错误,并返回更多上下文。 ### 修改文件 `userapp/views.py` — `MacAddressLoginView` 类 ### 当前代码(第 99-142 行) ```python def post(self, request): 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") token = generate_token(user_device.user.id) logger.info(f"Successfully logged in device with MAC: {mac_address}") return success_response( data={'token': token}, message="登录成功" ) except Device.DoesNotExist: logger.warning(f"Device not found: {mac_address}") return error_response(message="Device not found") except Exception as e: logger.error(f"MAC address login failed: {str(e)}") return error_response(message=f"Login failed: {str(e)}") ``` ### 替换为 ```python def post(self, request): 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) # 检查设备是否已绑定给用户 user_device = UserDevice.objects.filter(device=device).first() if not user_device: logger.warning(f"Device not bound to any user: {mac_address}") # 返回特定 code=4010,让设备端可以识别"未绑定"状态 return error_response( 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}, bound to user: {user_device.user.id}") return success_response( 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", 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)}") ``` ### 需要新增的 import(文件头部) 在 `userapp/views.py` 顶部检查是否已有以下 import,如果没有则添加: ```python from django.utils import timezone ``` ### 验证方法 用 curl 或 Postman 测试: ```bash # 测试1:用已绑定设备的 MAC(应返回 token + bound=true) curl -X POST https://qy-lty.airlabs.art/api/user/auth/mac/login/ \ -H "Content-Type: application/json" \ -d '{"mac_address": "00:55:11:44:f3:22"}' # 期望响应: # {"success": true, "code": 200, "data": {"token": "xxx", "user_id": 1, "device_code": "...", "bound": true}} # 测试2:用未绑定设备的 MAC(应返回 code=4010 + bound=false) curl -X POST https://qy-lty.airlabs.art/api/user/auth/mac/login/ \ -H "Content-Type: application/json" \ -d '{"mac_address": "AA:BB:CC:DD:EE:FF"}' # 期望响应: # {"success": false, "code": 4010, "data": {"device_code": "...", "bound": false}} # 测试3:用不存在的 MAC(应返回 code=4011 + registered=false) curl -X POST https://qy-lty.airlabs.art/api/user/auth/mac/login/ \ -H "Content-Type: application/json" \ -d '{"mac_address": "FF:FF:FF:FF:FF:FF"}' # 期望响应: # {"success": false, "code": 4011, "data": {"registered": false}} ``` ### 与其他端的关联 - **设备端步骤 1** 依赖此步骤:设备端会根据 `code=4010` 判断自己未绑定,进入等待绑定状态 - **手机端步骤 0** 不依赖此步骤:手机端使用手机号登录(见手机端步骤 0),登录 API 已存在无需改动 --- ## 步骤 2:新增设备绑定状态查询接口 ### 目标 设备端需要轮询查询自己是否已被手机端绑定。新增一个不需要认证的接口,设备通过 MAC 地址查询绑定状态。 ### 修改文件 `device_interaction/views.py` — 在 `DeviceViewSet` 类中新增 action ### 在 `DeviceViewSet` 类中添加方法 在 `update_status` 方法之后(约第 400 行后),添加: ```python @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: 设备MAC地址(query参数) 返回: - bound: 是否已绑定 - user_id: 绑定用户ID(仅已绑定时返回) - nickname: 设备昵称(仅已绑定时返回) - device_code: 设备编码 """ 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='设备不存在') ``` ### 验证方法 ```bash # 测试1:查询已绑定设备 curl "https://qy-lty.airlabs.art/api/device/devices/bind_status/?mac_address=00:55:11:44:f3:22" # 期望响应: # {"success": true, "data": {"bound": true, "user_id": 1, "nickname": "...", "device_code": "..."}} # 测试2:查询未绑定设备 curl "https://qy-lty.airlabs.art/api/device/devices/bind_status/?mac_address=AA:BB:CC:DD:EE:FF" # 期望响应: # {"success": true, "data": {"bound": false, "device_code": "...", "mac_address": "AA:BB:CC:DD:EE:FF"}} # 测试3:查询不存在的设备 curl "https://qy-lty.airlabs.art/api/device/devices/bind_status/?mac_address=FF:FF:FF:FF:FF:FF" # 期望响应: # {"success": false, "message": "设备不存在"} ``` ### 与其他端的关联 - **设备端步骤 1** 依赖此步骤:设备端轮询此接口等待被绑定 - **手机端步骤 2**(DeviceBindManager)不直接依赖,但绑定后可以用此接口验证绑定结果 --- ## 步骤 3:新增设备自注册接口 ### 目标 设备首次开机时,如果 MAC 地址在数据库中不存在,可以自动注册一个 Device 记录,无需管理员手动在后台录入。 ### 修改文件 **1. `device_interaction/serializers.py`** — 新增 Serializer 在文件末尾添加: ```python 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 ``` **2. `device_interaction/views.py`** — 在 `DeviceViewSet` 中新增 action 在 import 区域补充(如果还没有): ```python from .serializers import ( DeviceTypeSerializer, DeviceBatchSerializer, DeviceSerializer, DeviceCreateSerializer, DeviceBatchCreateSerializer, UserDeviceSerializer, DeviceBindSerializer, DeviceRegisterSerializer ) ``` 在 `DeviceViewSet` 类中(`bind_status` 方法之后)添加: ```python @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) ``` ### 验证方法 ```bash # 测试1:注册新设备 curl -X POST https://qy-lty.airlabs.art/api/device/devices/register/ \ -H "Content-Type: application/json" \ -d '{"mac_address": "AA:BB:CC:DD:EE:01", "device_type_code": "T01"}' # 期望响应: # {"success": true, "code": 201, "data": {"device_code": "T01-AUTO-T01-00001", "is_new": true}} # 测试2:重复注册(应返回已存在) curl -X POST https://qy-lty.airlabs.art/api/device/devices/register/ \ -H "Content-Type: application/json" \ -d '{"mac_address": "AA:BB:CC:DD:EE:01"}' # 期望响应: # {"success": true, "data": {"device_code": "...", "is_new": false}} ``` ### 与其他端的关联 - **设备端步骤 1** 依赖此步骤:设备端首次开机时如果 MAC 登录返回 `code=4011`(设备不存在),会调用此接口自动注册 - **手机端**不依赖此步骤 --- ## 步骤 4:为绑定接口增加 mac_address 返回字段 ### 目标 当前 `UserDeviceSerializer` 没有返回设备的 `mac_address`,手机端需要这个字段来管理设备。 ### 修改文件 `device_interaction/serializers.py` — `UserDeviceSerializer` 类 ### 当前代码(第 65-78 行) ```python class UserDeviceSerializer(serializers.ModelSerializer): device_code = serializers.ReadOnlyField(source='device.device_code') device_type = serializers.ReadOnlyField(source='device.device_type.name') device_status = serializers.ReadOnlyField(source='device.status') battery_level = serializers.ReadOnlyField(source='device.battery_level') firmware_version = serializers.ReadOnlyField(source='device.firmware_version') wifi_name = serializers.ReadOnlyField(source='device.wifi_name') wifi_password = serializers.ReadOnlyField(source='device.wifi_password') brightness = serializers.ReadOnlyField(source='device.brightness') class Meta: model = UserDevice fields = '__all__' read_only_fields = ('user', 'bound_at') ``` ### 替换为 ```python 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') firmware_version = serializers.ReadOnlyField(source='device.firmware_version') wifi_name = serializers.ReadOnlyField(source='device.wifi_name') wifi_password = serializers.ReadOnlyField(source='device.wifi_password') brightness = serializers.ReadOnlyField(source='device.brightness') class Meta: model = UserDevice fields = '__all__' read_only_fields = ('user', 'bound_at') ``` (仅增加了一行 `mac_address`) ### 验证方法 ```bash # 用已登录用户的 token 查询绑定设备列表 curl -H "Authorization: Bearer " \ https://qy-lty.airlabs.art/api/device/user-devices/ # 期望响应中每个设备对象现在包含 mac_address 字段 ``` ### 与其他端的关联 - **手机端步骤 2**(DeviceBindManager)依赖此步骤:手机端解析设备列表时需要 mac_address --- ## 步骤 5:新增 device_info WebSocket 消息类型 ### 目标 设备端连上 WiFi 并建立 WebSocket 后,需要把自己的 MAC 地址、电量、固件版本等信息上报给服务器和手机端。新增 `device_info` 消息类型实现这个功能。 ### 背景:手机端不需要 MAC 地址做通信 手机端通过 `user_id` 标识(来自登录 token),不是 MAC 地址。手机切换 WiFi/4G/5G 不影响 WebSocket 通信。但手机端需要知道设备的状态信息(电量、在线状态等),所以设备端通过 WebSocket 上报 `device_info` 消息。 ### 修改文件 `device_interaction/consumers.py` ### 在 receive 方法中添加 device_info 处理 找到 `receive` 方法中 `elif message_type == 'factory_reset':` 分支(约第 257 行),在它**之前**添加: ```python 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)}' })) ``` ### 在 Consumer 类中添加 device_info 事件处理方法 在 `conversation_subtitle` 方法之后(约第 476 行后),添加: ```python 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)}") ``` ### 在 Consumer 类中添加数据库更新辅助方法 在 `authenticate_with_token` 方法之后添加: ```python @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)}") ``` ### 验证方法 使用 WebSocket 客户端工具(如 wscat 或浏览器控制台)连接 WebSocket 后发送: ```json { "type": "device_info", "message": "{\"mac_address\":\"00:55:11:44:f3:22\",\"battery_level\":85,\"firmware_version\":\"1.0.0\",\"wifi_name\":\"TestWiFi\"}" } ``` 预期: 1. 服务器返回 `{"status": "success", "message": "设备信息已更新"}` 2. 同 group 的其他连接(手机端)收到 `{"type": "device_info", "message": {...}}` 3. 数据库中 Device 记录的 battery_level/firmware_version 等字段已更新 ### 与其他端的关联 - **设备端步骤 3** 依赖此步骤:设备端 WebSocket 连接成功后会发送 `device_info` 消息 - **手机端步骤 3B** 依赖此步骤:手机端需要处理收到的 `device_info` 消息 --- ## 步骤 6:部署验证 & 三端联调检查清单 ### 部署步骤 ```bash # 1. 拉取代码 cd /path/to/qy_lty-main git pull # 2. 检查是否需要 migrate(本次无模型变更,不需要) # 3. 重启服务 # Docker 环境: docker-compose restart # 或 直接 daphne: daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application ``` ### API 验证清单 | # | 接口 | 方法 | 预期 | |---|------|------|------| | 1 | `/api/user/auth/mac/login/` | POST | 已绑定:返回 token+bound=true;未绑定:code=4010;不存在:code=4011 | | 2 | `/api/device/devices/bind_status/` | GET | 返回 bound=true/false | | 3 | `/api/device/devices/register/` | POST | 新设备返回 is_new=true;已存在返回 is_new=false | | 4 | `/api/device/user-devices/bind/` | POST | 传 mac_address 绑定成功 | | 5 | `/api/device/user-devices/` | GET | 返回设备列表,含 mac_address 字段 | | 6 | `/api/device/user-devices/{id}/` | DELETE | 解绑成功 | | 7 | `/api/user/auth/phone/login/` | POST | 手机号登录返回 token(已有,无需改动) | ### 与设备端联调 > 需要**设备端步骤 1-3** 全部完成 1. 设备端开机 → 调用 MAC 登录 → 收到 `code=4010`(未绑定)→ 正确进入等待绑定状态 ✓(设备端步骤 1) 2. 设备端调用 `bind_status` 轮询 → 返回 `bound=false` ✓(设备端步骤 1) 3. 手机端绑定后 → 设备端轮询 `bind_status` → 返回 `bound=true` ✓(设备端步骤 1) 4. 设备端重新 MAC 登录 → 返回 token + `bound=true` → 连接 WebSocket ✓(设备端步骤 1) 5. 设备端连 WebSocket 后上报 `device_info` → 服务器更新数据库 → 手机端收到 ✓(设备端步骤 3) ### 与手机端联调 > 需要**手机端步骤 0-4** 全部完成(步骤 0 为手机号登录功能) 1. 手机端手机号+验证码登录 → 获得 user_token ✓(手机端步骤 0) 2. 手机端调用 `bind` 接口 → 绑定成功 ✓(手机端步骤 2 DeviceBindManager) 3. 手机端查询设备列表 → 含 mac_address 字段 ✓(手机端步骤 2) 4. 手机端连 WebSocket → 进入 `device_{user_id}` group ✓(手机端步骤 1) 5. 手机端发 touch 消息 → 设备端收到 ✓ 6. 手机端收到设备端 `device_info` 上报 ✓(手机端步骤 3B)