581 lines
22 KiB
Python
581 lines
22 KiB
Python
"""
|
||
设备模块视图 - 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='切换成功')
|