2026-03-02 17:33:56 +08:00

581 lines
22 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.

"""
设备模块视图 - App端
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated, AllowAny
from drf_spectacular.utils import extend_schema
from utils.response import success, error
from utils.exceptions import ErrorCode
from apps.admins.authentication import AppJWTAuthentication
from .models import Device, UserDevice, DeviceType, DeviceSettings, DeviceWifi, RoleMemory
from .serializers import (
DeviceSerializer,
UserDeviceSerializer,
BindDeviceSerializer,
DeviceVerifySerializer,
DeviceTypeSerializer,
DeviceDetailSerializer,
DeviceSettingsUpdateSerializer,
DeviceReportStatusSerializer,
RoleMemorySerializer,
RoleMemorySettingsUpdateSerializer,
RoleMemoryAgentUpdateSerializer,
RoleMemoryMemoryUpdateSerializer,
)
@extend_schema(tags=['设备'])
class DeviceViewSet(viewsets.ViewSet):
"""设备视图集App端"""
authentication_classes = [AppJWTAuthentication]
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'], url_path='query-by-mac',
authentication_classes=[], permission_classes=[AllowAny])
def query_by_mac(self, request):
"""
通过MAC地址查询SN码无需登录
GET /api/v1/devices/query-by-mac?mac=AA:BB:CC:DD:EE:FF
"""
mac = request.query_params.get('mac', '')
if not mac:
return error(message='MAC地址不能为空')
# 统一格式
mac = mac.upper().replace('-', ':')
try:
device = Device.objects.select_related('device_type').get(mac_address=mac)
return success(data={
'sn': device.sn,
'mac_address': device.mac_address,
'device_type': DeviceTypeSerializer(device.device_type).data if device.device_type else None,
'status': device.status,
'is_bound': device.status == 'bound'
})
except Device.DoesNotExist:
return error(
code=404,
message='未找到对应的设备请检查MAC地址是否正确或设备是否已完成入库',
status_code=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['get'])
def latest(self, request):
"""
获取用户最近使用的设备
GET /api/v1/devices/latest
"""
devices = list(
UserDevice.objects.filter(
user=request.user,
is_active=True
).select_related('device', 'device__device_type', 'spirit', 'role_memory', 'role_memory__device_type')
.order_by('-bind_time')[:1]
)
if not devices:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='暂无绑定设备')
latest = devices[0]
return success(data=UserDeviceSerializer(latest).data)
@action(detail=False, methods=['post'])
def verify(self, request):
"""
验证设备SN
POST /api/v1/devices/verify
"""
serializer = DeviceVerifySerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
sn = serializer.validated_data['sn']
try:
device = Device.objects.select_related('device_type').get(sn=sn)
except Device.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='设备不存在')
# 检查是否已被绑定
is_bindable = device.status != 'bound'
return success(data={
'sn': device.sn,
'is_bindable': is_bindable,
'device_type': DeviceTypeSerializer(device.device_type).data if device.device_type else None
})
@action(detail=False, methods=['post'])
def bind(self, request):
"""
绑定设备
POST /api/v1/devices/bind
"""
serializer = BindDeviceSerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
sn = serializer.validated_data['sn']
spirit_id = serializer.validated_data.get('spirit_id')
try:
device = Device.objects.select_related('device_type').get(sn=sn)
except Device.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='设备不存在')
# 检查是否已被绑定
if device.status == 'bound':
# 检查是否是当前用户绑定的
existing = UserDevice.objects.filter(device=device, is_active=True).first()
if existing and existing.user != request.user:
return error(code=ErrorCode.DEVICE_ALREADY_BOUND, message='设备已被其他用户绑定')
# 创建角色记忆(始终创建新的)
role_memory = None
if device.device_type:
role_memory = RoleMemory.objects.create(
user=request.user,
device_type=device.device_type,
is_bound=True,
prompt=device.device_type.default_prompt,
voice_id=device.device_type.default_voice_id,
)
# 创建绑定关系
user_device, created = UserDevice.objects.update_or_create(
user=request.user,
device=device,
defaults={
'spirit_id': spirit_id,
'role_memory': role_memory,
'is_active': True
}
)
# 更新设备状态和可选的设备名称
device_name = request.data.get('device_name')
if device_name:
device.name = device_name
device.status = 'bound'
device.save()
return success(
data=UserDeviceSerializer(user_device).data,
message='绑定成功' if created else '更新绑定成功'
)
@action(detail=False, methods=['get'])
def my_devices(self, request):
"""
我的设备列表
GET /api/v1/devices/my_devices
"""
user_devices = UserDevice.objects.filter(
user=request.user,
is_active=True
).select_related('device', 'device__device_type', 'spirit', 'role_memory', 'role_memory__device_type')
serializer = UserDeviceSerializer(user_devices, many=True)
return success(data=serializer.data)
@action(detail=True, methods=['delete'])
def unbind(self, request, pk=None):
"""
解绑设备
DELETE /api/v1/devices/{id}/unbind
"""
try:
user_device = UserDevice.objects.select_related('role_memory').get(
id=pk, user=request.user
)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
# 更新绑定状态
user_device.is_active = False
user_device.save()
# 将关联的角色记忆标记为闲置
if user_device.role_memory:
user_device.role_memory.is_bound = False
user_device.role_memory.save(update_fields=['is_bound', 'updated_at'])
# 检查设备是否还有其他活跃绑定
active_bindings = UserDevice.objects.filter(device=user_device.device, is_active=True).count()
if active_bindings == 0:
user_device.device.status = 'out_stock'
user_device.device.save()
return success(message='解绑成功')
@action(detail=True, methods=['put'], url_path='update-spirit')
def update_spirit(self, request, pk=None):
"""
更新设备绑定的智能体
PUT /api/v1/devices/{id}/update-spirit
"""
try:
user_device = UserDevice.objects.get(id=pk, user=request.user, is_active=True)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
spirit_id = request.data.get('spirit_id')
user_device.spirit_id = spirit_id
user_device.save()
return success(data=UserDeviceSerializer(user_device).data, message='更新成功')
@action(detail=True, methods=['get'], url_path='detail')
def device_detail(self, request, pk=None):
"""
获取设备详情
GET /api/v1/devices/{user_device_id}/detail/
pk 为 UserDevice 的 ID
"""
try:
user_device = UserDevice.objects.select_related(
'device', 'spirit', 'role_memory', 'role_memory__device_type'
).get(id=pk, user=request.user, is_active=True)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
device = user_device.device
serializer = DeviceDetailSerializer(
device, context={'user_device': user_device}
)
return success(data=serializer.data)
@action(detail=True, methods=['put'], url_path='settings')
def update_settings(self, request, pk=None):
"""
更新设备设置
PUT /api/v1/devices/{user_device_id}/settings/
"""
try:
user_device = UserDevice.objects.select_related('device').get(
id=pk, user=request.user, is_active=True
)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
serializer = DeviceSettingsUpdateSerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
device = user_device.device
settings_obj, _ = DeviceSettings.objects.get_or_create(device=device)
for field, value in serializer.validated_data.items():
setattr(settings_obj, field, value)
settings_obj.save()
return success(message='设置已保存')
@action(detail=True, methods=['post'], url_path='wifi')
def configure_wifi(self, request, pk=None):
"""
配置设备WiFi
POST /api/v1/devices/{user_device_id}/wifi/
"""
try:
user_device = UserDevice.objects.select_related('device').get(
id=pk, user=request.user, is_active=True
)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
ssid = request.data.get('ssid')
if not ssid:
return error(message='WiFi名称不能为空')
device = user_device.device
# 将其他 WiFi 标记为未连接
DeviceWifi.objects.filter(device=device).update(is_connected=False)
# 创建或更新当前 WiFi 记录
wifi, _ = DeviceWifi.objects.update_or_create(
device=device,
ssid=ssid,
defaults={'is_connected': True}
)
# TODO: 通过设备通信协议下发 WiFi 配置password 不存库)
return success(message='WiFi 配置成功')
@action(
detail=False, methods=['get'],
url_path='stories',
authentication_classes=[], permission_classes=[AllowAny]
)
def stories_by_mac(self, request):
"""
获取设备关联用户的随机故事(公开接口,无需认证)
GET /api/v1/devices/stories/?mac_address=AA:BB:CC:DD:EE:FF
供 hw-ws-service 调用。
优先返回用户自己的故事无则兜底返回系统默认故事is_default=True
"""
mac = request.query_params.get('mac_address', '').strip()
if not mac:
return error(message='mac_address 参数不能为空')
mac = mac.upper().replace('-', ':')
try:
device = Device.objects.get(mac_address=mac)
except Device.DoesNotExist:
return error(
code=ErrorCode.DEVICE_NOT_FOUND,
message='未找到对应设备',
status_code=status.HTTP_404_NOT_FOUND
)
user_device = (
UserDevice.objects
.filter(device=device, is_active=True, bind_type='owner')
.select_related('user')
.first()
)
if not user_device:
return error(
code=ErrorCode.NOT_FOUND,
message='该设备尚未绑定用户',
status_code=status.HTTP_404_NOT_FOUND
)
from apps.stories.models import Story
# 优先随机取用户自己有 audio_url 的故事
story = (
Story.objects
.filter(user=user_device.user)
.exclude(audio_url='')
.order_by('?')
.first()
)
# 兜底:用户暂无故事时使用系统默认故事
if not story:
story = (
Story.objects
.filter(is_default=True)
.exclude(audio_url='')
.order_by('?')
.first()
)
if not story:
return error(
code=ErrorCode.STORY_NOT_FOUND,
message='暂无可播放的故事',
status_code=status.HTTP_404_NOT_FOUND
)
return success(data={'title': story.title, 'audio_url': story.audio_url})
@action(detail=False, methods=['post'], url_path='report-status',
authentication_classes=[], permission_classes=[AllowAny])
def report_status(self, request):
"""
设备状态上报(硬件端调用,无需认证)
POST /api/v1/devices/report-status
"""
serializer = DeviceReportStatusSerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
data = serializer.validated_data
mac = data.pop('mac_address')
try:
device = Device.objects.get(mac_address=mac)
except Device.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='设备不存在')
# 更新 Device 表字段
device_fields_updated = False
for field in ('is_online', 'battery', 'firmware_version'):
if field in data:
setattr(device, field, data[field])
device_fields_updated = True
if data.get('is_online'):
from django.utils import timezone
device.last_online_at = timezone.now()
device_fields_updated = True
if device_fields_updated:
device.save()
# 更新 DeviceSettings 表字段(仅 volume / brightness
settings_data = {k: data[k] for k in ('volume', 'brightness') if k in data}
if settings_data:
settings_obj, _ = DeviceSettings.objects.get_or_create(device=device)
for field, value in settings_data.items():
setattr(settings_obj, field, value)
settings_obj.save()
return success(
data={'device_id': device.id, 'sn': device.sn},
message='状态上报成功'
)
# ==================== 角色记忆相关端点 ====================
@action(detail=True, methods=['get'], url_path='role-memory')
def role_memory_detail(self, request, pk=None):
"""
获取当前设备的角色记忆
GET /api/v1/devices/{user_device_id}/role-memory/
"""
try:
user_device = UserDevice.objects.select_related(
'role_memory', 'role_memory__device_type'
).get(id=pk, user=request.user, is_active=True)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
if not user_device.role_memory:
return error(code=ErrorCode.ROLE_MEMORY_NOT_FOUND, message='该设备暂无角色记忆')
return success(data=RoleMemorySerializer(user_device.role_memory).data)
@action(detail=True, methods=['put'], url_path='role-memory/settings')
def role_memory_settings(self, request, pk=None):
"""
更新角色记忆-设备设置
PUT /api/v1/devices/{user_device_id}/role-memory/settings/
"""
try:
user_device = UserDevice.objects.select_related('role_memory').get(
id=pk, user=request.user, is_active=True
)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
if not user_device.role_memory:
return error(code=ErrorCode.ROLE_MEMORY_NOT_FOUND, message='该设备暂无角色记忆')
serializer = RoleMemorySettingsUpdateSerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
rm = user_device.role_memory
for field, value in serializer.validated_data.items():
setattr(rm, field, value)
rm.save()
return success(data=RoleMemorySerializer(rm).data, message='设置已保存')
@action(detail=True, methods=['put'], url_path='role-memory/agent')
def role_memory_agent(self, request, pk=None):
"""
更新角色记忆-Agent信息
PUT /api/v1/devices/{user_device_id}/role-memory/agent/
"""
try:
user_device = UserDevice.objects.select_related('role_memory').get(
id=pk, user=request.user, is_active=True
)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
if not user_device.role_memory:
return error(code=ErrorCode.ROLE_MEMORY_NOT_FOUND, message='该设备暂无角色记忆')
serializer = RoleMemoryAgentUpdateSerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
rm = user_device.role_memory
for field, value in serializer.validated_data.items():
setattr(rm, field, value)
rm.save()
return success(data=RoleMemorySerializer(rm).data, message='Agent信息已更新')
@action(detail=True, methods=['put'], url_path='role-memory/memory')
def role_memory_summary(self, request, pk=None):
"""
更新角色记忆-聊天记忆摘要
PUT /api/v1/devices/{user_device_id}/role-memory/memory/
"""
try:
user_device = UserDevice.objects.select_related('role_memory').get(
id=pk, user=request.user, is_active=True
)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
if not user_device.role_memory:
return error(code=ErrorCode.ROLE_MEMORY_NOT_FOUND, message='该设备暂无角色记忆')
serializer = RoleMemoryMemoryUpdateSerializer(data=request.data)
if not serializer.is_valid():
return error(message=str(serializer.errors))
user_device.role_memory.memory_summary = serializer.validated_data['memory_summary']
user_device.role_memory.save(update_fields=['memory_summary', 'updated_at'])
return success(message='聊天记忆已更新')
@action(detail=False, methods=['get'], url_path='role-memories')
def role_memory_list(self, request):
"""
获取角色记忆列表
GET /api/v1/devices/role-memories/?device_type_id=1&is_bound=false
"""
qs = RoleMemory.objects.filter(user=request.user).select_related('device_type')
device_type_id = request.query_params.get('device_type_id')
if device_type_id:
qs = qs.filter(device_type_id=device_type_id)
is_bound = request.query_params.get('is_bound')
if is_bound is not None:
qs = qs.filter(is_bound=is_bound.lower() == 'true')
return success(data=RoleMemorySerializer(qs, many=True).data)
@action(detail=True, methods=['put'], url_path='switch-role-memory')
def switch_role_memory(self, request, pk=None):
"""
切换设备的角色记忆
PUT /api/v1/devices/{user_device_id}/switch-role-memory/
body: { "role_memory_id": 5 }
"""
try:
user_device = UserDevice.objects.select_related(
'device', 'device__device_type', 'role_memory'
).get(id=pk, user=request.user, is_active=True)
except UserDevice.DoesNotExist:
return error(code=ErrorCode.DEVICE_NOT_FOUND, message='绑定记录不存在')
role_memory_id = request.data.get('role_memory_id')
if not role_memory_id:
return error(message='role_memory_id 不能为空')
try:
new_rm = RoleMemory.objects.get(id=role_memory_id, user=request.user)
except RoleMemory.DoesNotExist:
return error(code=ErrorCode.ROLE_MEMORY_NOT_FOUND, message='该设备暂无角色记忆')
# 校验: 目标记忆必须是同一设备类型
if user_device.device.device_type_id and new_rm.device_type_id != user_device.device.device_type_id:
return error(code=ErrorCode.ROLE_MEMORY_TYPE_MISMATCH, message='只能切换到同类型设备的角色记忆')
# 校验: 目标记忆必须是闲置状态
if new_rm.is_bound:
return error(code=ErrorCode.ROLE_MEMORY_ALREADY_BOUND, message='该角色记忆正在被其他设备使用')
# 执行切换:旧记忆标记闲置,新记忆标记绑定
old_rm = user_device.role_memory
if old_rm:
old_rm.is_bound = False
old_rm.save(update_fields=['is_bound', 'updated_at'])
new_rm.is_bound = True
new_rm.save(update_fields=['is_bound', 'updated_at'])
user_device.role_memory = new_rm
user_device.save(update_fields=['role_memory'])
return success(data=RoleMemorySerializer(new_rm).data, message='切换成功')