Some checks failed
continuous-integration/drone/push Build is failing
1. 课程学习进度追踪
- 新增 UserCourseProgress 和 UserMaterialProgress 模型
- 新增 /api/v1/progress/* 进度追踪 API
- 更新 admin.py 使用真实课程完成率数据
2. 路由权限检查完善
- 新增前端 permissionChecker.ts 权限检查工具
- 更新 router/guard.ts 实现团队和课程权限验证
- 新增后端 permission_service.py
3. AI 陪练音频转文本
- 新增 speech_recognition.py 语音识别服务
- 新增 /api/v1/speech/* API
- 更新 ai-practice-coze.vue 支持语音输入
4. 双人对练报告生成
- 更新 practice_room_service.py 添加报告生成功能
- 新增 /rooms/{room_code}/report API
- 更新 duo-practice-report.vue 调用真实 API
5. 学习提醒推送
- 新增 notification_service.py 通知服务
- 新增 scheduler_service.py 定时任务服务
- 支持钉钉、企微、站内消息推送
6. 智能学习推荐
- 新增 recommendation_service.py 推荐服务
- 新增 /api/v1/recommendations/* API
- 支持错题、能力、进度、热门多维度推荐
7. 安全问题修复
- DEBUG 默认值改为 False
- 添加 SECRET_KEY 安全警告
- 新增 check_security_settings() 检查函数
8. 证书 PDF 生成
- 更新 certificate_service.py 添加 PDF 生成
- 添加 weasyprint、Pillow、qrcode 依赖
- 更新下载 API 支持 PDF 和 PNG 格式
714 lines
24 KiB
Python
714 lines
24 KiB
Python
"""
|
||
双人对练房间服务
|
||
|
||
功能:
|
||
- 房间创建、加入、退出
|
||
- 房间状态管理
|
||
- 消息广播
|
||
- 对练结束处理
|
||
"""
|
||
import logging
|
||
import random
|
||
import string
|
||
from datetime import datetime
|
||
from typing import Optional, List, Dict, Any
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, update, and_
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.models.practice_room import PracticeRoom, PracticeRoomMessage
|
||
from app.models.practice import PracticeDialogue, PracticeSession
|
||
from app.models.user import User
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class PracticeRoomService:
|
||
"""双人对练房间服务"""
|
||
|
||
# 房间状态常量
|
||
STATUS_WAITING = "waiting" # 等待加入
|
||
STATUS_READY = "ready" # 准备就绪
|
||
STATUS_PRACTICING = "practicing" # 对练中
|
||
STATUS_COMPLETED = "completed" # 已完成
|
||
STATUS_CANCELED = "canceled" # 已取消
|
||
|
||
# 消息类型常量
|
||
MSG_TYPE_CHAT = "chat" # 聊天消息
|
||
MSG_TYPE_SYSTEM = "system" # 系统消息
|
||
MSG_TYPE_JOIN = "join" # 加入消息
|
||
MSG_TYPE_LEAVE = "leave" # 离开消息
|
||
MSG_TYPE_START = "start" # 开始消息
|
||
MSG_TYPE_END = "end" # 结束消息
|
||
|
||
def __init__(self, db: AsyncSession):
|
||
self.db = db
|
||
|
||
# ==================== 房间管理 ====================
|
||
|
||
async def create_room(
|
||
self,
|
||
host_user_id: int,
|
||
scene_id: Optional[int] = None,
|
||
scene_name: Optional[str] = None,
|
||
scene_type: Optional[str] = None,
|
||
scene_background: Optional[str] = None,
|
||
role_a_name: str = "销售顾问",
|
||
role_b_name: str = "顾客",
|
||
role_a_description: Optional[str] = None,
|
||
role_b_description: Optional[str] = None,
|
||
host_role: str = "A",
|
||
room_name: Optional[str] = None
|
||
) -> PracticeRoom:
|
||
"""
|
||
创建对练房间
|
||
|
||
Args:
|
||
host_user_id: 房主用户ID
|
||
scene_id: 场景ID(可选)
|
||
scene_name: 场景名称
|
||
scene_type: 场景类型
|
||
scene_background: 场景背景
|
||
role_a_name: 角色A名称
|
||
role_b_name: 角色B名称
|
||
role_a_description: 角色A描述
|
||
role_b_description: 角色B描述
|
||
host_role: 房主选择的角色(A或B)
|
||
room_name: 房间名称
|
||
|
||
Returns:
|
||
PracticeRoom: 创建的房间对象
|
||
"""
|
||
# 生成唯一的6位房间码
|
||
room_code = await self._generate_unique_room_code()
|
||
|
||
# 创建房间
|
||
room = PracticeRoom(
|
||
room_code=room_code,
|
||
room_name=room_name or f"{scene_name or '双人对练'}房间",
|
||
scene_id=scene_id,
|
||
scene_name=scene_name,
|
||
scene_type=scene_type,
|
||
scene_background=scene_background,
|
||
role_a_name=role_a_name,
|
||
role_b_name=role_b_name,
|
||
role_a_description=role_a_description,
|
||
role_b_description=role_b_description,
|
||
host_user_id=host_user_id,
|
||
host_role=host_role,
|
||
status=self.STATUS_WAITING
|
||
)
|
||
|
||
self.db.add(room)
|
||
await self.db.commit()
|
||
await self.db.refresh(room)
|
||
|
||
logger.info(f"创建房间成功: room_code={room_code}, host_user_id={host_user_id}")
|
||
return room
|
||
|
||
async def join_room(
|
||
self,
|
||
room_code: str,
|
||
user_id: int
|
||
) -> PracticeRoom:
|
||
"""
|
||
加入房间
|
||
|
||
Args:
|
||
room_code: 房间码
|
||
user_id: 用户ID
|
||
|
||
Returns:
|
||
PracticeRoom: 房间对象
|
||
|
||
Raises:
|
||
ValueError: 房间不存在、已满或状态不允许加入
|
||
"""
|
||
# 查询房间
|
||
room = await self.get_room_by_code(room_code)
|
||
if not room:
|
||
raise ValueError("房间不存在或已过期")
|
||
|
||
# 检查是否是房主(房主重新进入)
|
||
if room.host_user_id == user_id:
|
||
return room
|
||
|
||
# 检查房间状态
|
||
if room.status not in [self.STATUS_WAITING, self.STATUS_READY]:
|
||
raise ValueError("房间已开始对练或已结束,无法加入")
|
||
|
||
# 检查是否已满
|
||
if room.guest_user_id and room.guest_user_id != user_id:
|
||
raise ValueError("房间已满")
|
||
|
||
# 加入房间
|
||
room.guest_user_id = user_id
|
||
room.status = self.STATUS_READY
|
||
|
||
await self.db.commit()
|
||
await self.db.refresh(room)
|
||
|
||
# 发送系统消息
|
||
await self._add_system_message(room.id, f"用户已加入房间", self.MSG_TYPE_JOIN, user_id)
|
||
|
||
logger.info(f"用户加入房间: room_code={room_code}, user_id={user_id}")
|
||
return room
|
||
|
||
async def leave_room(
|
||
self,
|
||
room_code: str,
|
||
user_id: int
|
||
) -> bool:
|
||
"""
|
||
离开房间
|
||
|
||
Args:
|
||
room_code: 房间码
|
||
user_id: 用户ID
|
||
|
||
Returns:
|
||
bool: 是否成功离开
|
||
"""
|
||
room = await self.get_room_by_code(room_code)
|
||
if not room:
|
||
return False
|
||
|
||
# 如果是房主离开,取消房间
|
||
if room.host_user_id == user_id:
|
||
room.status = self.STATUS_CANCELED
|
||
await self._add_system_message(room.id, "房主离开,房间已关闭", self.MSG_TYPE_LEAVE, user_id)
|
||
# 如果是嘉宾离开
|
||
elif room.guest_user_id == user_id:
|
||
room.guest_user_id = None
|
||
room.status = self.STATUS_WAITING
|
||
await self._add_system_message(room.id, "对方已离开房间", self.MSG_TYPE_LEAVE, user_id)
|
||
else:
|
||
return False
|
||
|
||
await self.db.commit()
|
||
logger.info(f"用户离开房间: room_code={room_code}, user_id={user_id}")
|
||
return True
|
||
|
||
async def start_practice(
|
||
self,
|
||
room_code: str,
|
||
user_id: int
|
||
) -> PracticeRoom:
|
||
"""
|
||
开始对练(仅房主可操作)
|
||
|
||
Args:
|
||
room_code: 房间码
|
||
user_id: 用户ID(必须是房主)
|
||
|
||
Returns:
|
||
PracticeRoom: 房间对象
|
||
"""
|
||
room = await self.get_room_by_code(room_code)
|
||
if not room:
|
||
raise ValueError("房间不存在")
|
||
|
||
if room.host_user_id != user_id:
|
||
raise ValueError("只有房主可以开始对练")
|
||
|
||
if room.status != self.STATUS_READY:
|
||
raise ValueError("房间未就绪,请等待对方加入")
|
||
|
||
room.status = self.STATUS_PRACTICING
|
||
room.started_at = datetime.now()
|
||
|
||
await self.db.commit()
|
||
await self.db.refresh(room)
|
||
|
||
# 发送开始消息
|
||
await self._add_system_message(room.id, "对练开始!", self.MSG_TYPE_START)
|
||
|
||
logger.info(f"对练开始: room_code={room_code}")
|
||
return room
|
||
|
||
async def end_practice(
|
||
self,
|
||
room_code: str,
|
||
user_id: int
|
||
) -> PracticeRoom:
|
||
"""
|
||
结束对练
|
||
|
||
Args:
|
||
room_code: 房间码
|
||
user_id: 用户ID
|
||
|
||
Returns:
|
||
PracticeRoom: 房间对象
|
||
"""
|
||
room = await self.get_room_by_code(room_code)
|
||
if not room:
|
||
raise ValueError("房间不存在")
|
||
|
||
if room.status != self.STATUS_PRACTICING:
|
||
raise ValueError("对练未在进行中")
|
||
|
||
# 计算时长
|
||
if room.started_at:
|
||
duration = (datetime.now() - room.started_at).total_seconds()
|
||
room.duration_seconds = int(duration)
|
||
|
||
room.status = self.STATUS_COMPLETED
|
||
room.ended_at = datetime.now()
|
||
|
||
await self.db.commit()
|
||
await self.db.refresh(room)
|
||
|
||
# 发送结束消息
|
||
await self._add_system_message(room.id, "对练结束!", self.MSG_TYPE_END)
|
||
|
||
logger.info(f"对练结束: room_code={room_code}, duration={room.duration_seconds}s")
|
||
return room
|
||
|
||
# ==================== 消息管理 ====================
|
||
|
||
async def send_message(
|
||
self,
|
||
room_id: int,
|
||
user_id: int,
|
||
content: Optional[str],
|
||
role_name: Optional[str] = None,
|
||
message_type: Optional[str] = None,
|
||
extra_data: Optional[dict] = None
|
||
) -> PracticeRoomMessage:
|
||
"""
|
||
发送聊天消息或信令消息
|
||
|
||
Args:
|
||
room_id: 房间ID
|
||
user_id: 发送者ID
|
||
content: 消息内容
|
||
role_name: 角色名称
|
||
message_type: 消息类型(默认为 chat)
|
||
extra_data: 额外数据(用于 WebRTC 信令等)
|
||
|
||
Returns:
|
||
PracticeRoomMessage: 消息对象
|
||
"""
|
||
import json
|
||
|
||
# 获取当前消息序号
|
||
sequence = await self._get_next_sequence(room_id)
|
||
|
||
# 如果是信令消息,将 extra_data 序列化到 content 中
|
||
actual_content = content
|
||
if extra_data and not content:
|
||
actual_content = json.dumps(extra_data)
|
||
|
||
message = PracticeRoomMessage(
|
||
room_id=room_id,
|
||
user_id=user_id,
|
||
message_type=message_type or self.MSG_TYPE_CHAT,
|
||
content=actual_content,
|
||
role_name=role_name,
|
||
sequence=sequence
|
||
)
|
||
|
||
self.db.add(message)
|
||
|
||
# 只有聊天消息才更新房间统计
|
||
if (message_type or self.MSG_TYPE_CHAT) == self.MSG_TYPE_CHAT:
|
||
room = await self.get_room_by_id(room_id)
|
||
if room:
|
||
room.total_turns += 1
|
||
user_role = room.get_user_role(user_id)
|
||
if user_role == "A":
|
||
room.role_a_turns += 1
|
||
elif user_role == "B":
|
||
room.role_b_turns += 1
|
||
|
||
await self.db.commit()
|
||
await self.db.refresh(message)
|
||
|
||
return message
|
||
|
||
async def get_messages(
|
||
self,
|
||
room_id: int,
|
||
since_sequence: int = 0,
|
||
limit: int = 100
|
||
) -> List[PracticeRoomMessage]:
|
||
"""
|
||
获取房间消息(用于SSE轮询)
|
||
|
||
Args:
|
||
room_id: 房间ID
|
||
since_sequence: 从该序号之后开始获取
|
||
limit: 最大数量
|
||
|
||
Returns:
|
||
List[PracticeRoomMessage]: 消息列表
|
||
"""
|
||
result = await self.db.execute(
|
||
select(PracticeRoomMessage)
|
||
.where(
|
||
and_(
|
||
PracticeRoomMessage.room_id == room_id,
|
||
PracticeRoomMessage.sequence > since_sequence
|
||
)
|
||
)
|
||
.order_by(PracticeRoomMessage.sequence)
|
||
.limit(limit)
|
||
)
|
||
return list(result.scalars().all())
|
||
|
||
async def get_all_messages(self, room_id: int) -> List[PracticeRoomMessage]:
|
||
"""
|
||
获取房间所有消息
|
||
|
||
Args:
|
||
room_id: 房间ID
|
||
|
||
Returns:
|
||
List[PracticeRoomMessage]: 消息列表
|
||
"""
|
||
result = await self.db.execute(
|
||
select(PracticeRoomMessage)
|
||
.where(PracticeRoomMessage.room_id == room_id)
|
||
.order_by(PracticeRoomMessage.sequence)
|
||
)
|
||
return list(result.scalars().all())
|
||
|
||
# ==================== 查询方法 ====================
|
||
|
||
async def get_room_by_code(self, room_code: str) -> Optional[PracticeRoom]:
|
||
"""根据房间码获取房间"""
|
||
result = await self.db.execute(
|
||
select(PracticeRoom).where(
|
||
and_(
|
||
PracticeRoom.room_code == room_code,
|
||
PracticeRoom.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def get_room_by_id(self, room_id: int) -> Optional[PracticeRoom]:
|
||
"""根据ID获取房间"""
|
||
result = await self.db.execute(
|
||
select(PracticeRoom).where(
|
||
and_(
|
||
PracticeRoom.id == room_id,
|
||
PracticeRoom.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def get_user_rooms(
|
||
self,
|
||
user_id: int,
|
||
status: Optional[str] = None,
|
||
limit: int = 20
|
||
) -> List[PracticeRoom]:
|
||
"""获取用户的房间列表"""
|
||
query = select(PracticeRoom).where(
|
||
and_(
|
||
(PracticeRoom.host_user_id == user_id) | (PracticeRoom.guest_user_id == user_id),
|
||
PracticeRoom.is_deleted == False
|
||
)
|
||
)
|
||
|
||
if status:
|
||
query = query.where(PracticeRoom.status == status)
|
||
|
||
query = query.order_by(PracticeRoom.created_at.desc()).limit(limit)
|
||
|
||
result = await self.db.execute(query)
|
||
return list(result.scalars().all())
|
||
|
||
async def get_room_with_users(self, room_code: str) -> Optional[Dict[str, Any]]:
|
||
"""获取房间详情(包含用户信息)"""
|
||
room = await self.get_room_by_code(room_code)
|
||
if not room:
|
||
return None
|
||
|
||
# 获取用户信息
|
||
host_user = None
|
||
guest_user = None
|
||
|
||
if room.host_user_id:
|
||
result = await self.db.execute(
|
||
select(User).where(User.id == room.host_user_id)
|
||
)
|
||
host_user = result.scalar_one_or_none()
|
||
|
||
if room.guest_user_id:
|
||
result = await self.db.execute(
|
||
select(User).where(User.id == room.guest_user_id)
|
||
)
|
||
guest_user = result.scalar_one_or_none()
|
||
|
||
return {
|
||
"room": room,
|
||
"host_user": host_user,
|
||
"guest_user": guest_user,
|
||
"host_role_name": room.get_role_name(room.host_role),
|
||
"guest_role_name": room.get_role_name("B" if room.host_role == "A" else "A") if guest_user else None
|
||
}
|
||
|
||
# ==================== 辅助方法 ====================
|
||
|
||
async def _generate_unique_room_code(self) -> str:
|
||
"""生成唯一的6位房间码"""
|
||
for _ in range(10): # 最多尝试10次
|
||
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
||
# 排除容易混淆的字符
|
||
code = code.replace('0', 'X').replace('O', 'Y').replace('I', 'Z').replace('1', 'W')
|
||
|
||
# 检查是否已存在
|
||
existing = await self.get_room_by_code(code)
|
||
if not existing:
|
||
return code
|
||
|
||
raise ValueError("无法生成唯一房间码,请稍后重试")
|
||
|
||
async def _get_next_sequence(self, room_id: int) -> int:
|
||
"""获取下一个消息序号"""
|
||
result = await self.db.execute(
|
||
select(PracticeRoomMessage.sequence)
|
||
.where(PracticeRoomMessage.room_id == room_id)
|
||
.order_by(PracticeRoomMessage.sequence.desc())
|
||
.limit(1)
|
||
)
|
||
last_seq = result.scalar_one_or_none()
|
||
return (last_seq or 0) + 1
|
||
|
||
async def _add_system_message(
|
||
self,
|
||
room_id: int,
|
||
content: str,
|
||
msg_type: str,
|
||
user_id: Optional[int] = None
|
||
) -> PracticeRoomMessage:
|
||
"""添加系统消息"""
|
||
sequence = await self._get_next_sequence(room_id)
|
||
|
||
message = PracticeRoomMessage(
|
||
room_id=room_id,
|
||
user_id=user_id,
|
||
message_type=msg_type,
|
||
content=content,
|
||
sequence=sequence
|
||
)
|
||
|
||
self.db.add(message)
|
||
await self.db.commit()
|
||
await self.db.refresh(message)
|
||
|
||
return message
|
||
|
||
# ==================== 报告生成 ====================
|
||
|
||
async def generate_report(self, room_id: int) -> Dict[str, Any]:
|
||
"""
|
||
生成对练报告
|
||
|
||
Args:
|
||
room_id: 房间ID
|
||
|
||
Returns:
|
||
包含房间信息、对话分析、表现评估的完整报告
|
||
"""
|
||
# 获取房间信息
|
||
room = await self.get_room(room_id)
|
||
if not room:
|
||
return None
|
||
|
||
# 获取房间消息
|
||
messages = await self.get_messages(room_id)
|
||
chat_messages = [m for m in messages if m.message_type == self.MSG_TYPE_CHAT]
|
||
|
||
# 获取用户信息
|
||
host_user = await self._get_user(room.host_user_id)
|
||
guest_user = await self._get_user(room.guest_user_id) if room.guest_user_id else None
|
||
|
||
# 分析对话
|
||
analysis = self._analyze_conversation(room, chat_messages)
|
||
|
||
# 构建报告
|
||
report = {
|
||
"room": {
|
||
"id": room.id,
|
||
"room_code": room.room_code,
|
||
"scene_name": room.scene_name or "自由对练",
|
||
"scene_type": room.scene_type,
|
||
"scene_background": room.scene_background,
|
||
"role_a_name": room.role_a_name,
|
||
"role_b_name": room.role_b_name,
|
||
"status": room.status,
|
||
"duration_seconds": room.duration_seconds or 0,
|
||
"total_turns": room.total_turns or 0,
|
||
"started_at": room.started_at.isoformat() if room.started_at else None,
|
||
"ended_at": room.ended_at.isoformat() if room.ended_at else None,
|
||
},
|
||
"participants": {
|
||
"host": {
|
||
"user_id": room.host_user_id,
|
||
"username": host_user.username if host_user else "未知用户",
|
||
"role": room.host_role,
|
||
"role_name": room.role_a_name if room.host_role == "A" else room.role_b_name,
|
||
},
|
||
"guest": {
|
||
"user_id": room.guest_user_id,
|
||
"username": guest_user.username if guest_user else "未加入",
|
||
"role": "B" if room.host_role == "A" else "A",
|
||
"role_name": room.role_b_name if room.host_role == "A" else room.role_a_name,
|
||
} if room.guest_user_id else None,
|
||
},
|
||
"analysis": analysis,
|
||
"messages": [
|
||
{
|
||
"id": m.id,
|
||
"user_id": m.user_id,
|
||
"content": m.content,
|
||
"role_name": m.role_name,
|
||
"sequence": m.sequence,
|
||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
||
}
|
||
for m in chat_messages
|
||
],
|
||
}
|
||
|
||
return report
|
||
|
||
def _analyze_conversation(
|
||
self,
|
||
room: PracticeRoom,
|
||
messages: List[PracticeRoomMessage]
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
分析对话内容
|
||
|
||
返回对话分析结果,包括:
|
||
- 对话统计
|
||
- 参与度分析
|
||
- 对话质量评估
|
||
- 改进建议
|
||
"""
|
||
if not messages:
|
||
return {
|
||
"summary": "暂无对话记录",
|
||
"statistics": {
|
||
"total_messages": 0,
|
||
"role_a_messages": 0,
|
||
"role_b_messages": 0,
|
||
"avg_message_length": 0,
|
||
"conversation_duration": room.duration_seconds or 0,
|
||
},
|
||
"participation": {
|
||
"role_a_ratio": 0,
|
||
"role_b_ratio": 0,
|
||
"balance_score": 0,
|
||
},
|
||
"quality": {
|
||
"overall_score": 0,
|
||
"engagement_score": 0,
|
||
"response_quality": 0,
|
||
},
|
||
"suggestions": ["尚无足够的对话数据进行分析"],
|
||
}
|
||
|
||
# 统计消息
|
||
role_a_messages = [m for m in messages if m.role_name == room.role_a_name]
|
||
role_b_messages = [m for m in messages if m.role_name == room.role_b_name]
|
||
|
||
total_messages = len(messages)
|
||
role_a_count = len(role_a_messages)
|
||
role_b_count = len(role_b_messages)
|
||
|
||
# 计算平均消息长度
|
||
total_length = sum(len(m.content or "") for m in messages)
|
||
avg_length = round(total_length / total_messages) if total_messages > 0 else 0
|
||
|
||
# 计算参与度
|
||
role_a_ratio = round(role_a_count / total_messages * 100, 1) if total_messages > 0 else 0
|
||
role_b_ratio = round(role_b_count / total_messages * 100, 1) if total_messages > 0 else 0
|
||
|
||
# 平衡度评分(越接近50:50越高)
|
||
balance_score = round(100 - abs(role_a_ratio - 50) * 2, 1)
|
||
balance_score = max(0, min(100, balance_score))
|
||
|
||
# 质量评估(基于简单规则)
|
||
engagement_score = min(100, total_messages * 5) # 每条消息5分,最高100
|
||
|
||
# 响应质量(基于平均消息长度)
|
||
response_quality = min(100, avg_length * 2) # 每字2分,最高100
|
||
|
||
# 综合评分
|
||
overall_score = round((balance_score + engagement_score + response_quality) / 3, 1)
|
||
|
||
# 生成建议
|
||
suggestions = []
|
||
if balance_score < 70:
|
||
suggestions.append(f"对话参与度不均衡,建议{room.role_a_name if role_a_ratio < 50 else room.role_b_name}增加互动")
|
||
if avg_length < 20:
|
||
suggestions.append("平均消息较短,建议增加更详细的表达")
|
||
if total_messages < 10:
|
||
suggestions.append("对话轮次较少,建议增加更多交流")
|
||
if overall_score >= 80:
|
||
suggestions.append("对话质量良好,继续保持!")
|
||
elif overall_score < 60:
|
||
suggestions.append("建议增加对话深度和互动频率")
|
||
|
||
if not suggestions:
|
||
suggestions.append("表现正常,可以尝试更复杂的场景练习")
|
||
|
||
return {
|
||
"summary": f"本次对练共进行 {total_messages} 轮对话,时长 {room.duration_seconds or 0} 秒",
|
||
"statistics": {
|
||
"total_messages": total_messages,
|
||
"role_a_messages": role_a_count,
|
||
"role_b_messages": role_b_count,
|
||
"avg_message_length": avg_length,
|
||
"conversation_duration": room.duration_seconds or 0,
|
||
},
|
||
"participation": {
|
||
"role_a_ratio": role_a_ratio,
|
||
"role_b_ratio": role_b_ratio,
|
||
"balance_score": balance_score,
|
||
},
|
||
"quality": {
|
||
"overall_score": overall_score,
|
||
"engagement_score": engagement_score,
|
||
"response_quality": response_quality,
|
||
},
|
||
"suggestions": suggestions,
|
||
}
|
||
|
||
async def _get_user(self, user_id: Optional[int]) -> Optional[User]:
|
||
"""获取用户信息"""
|
||
if not user_id:
|
||
return None
|
||
result = await self.db.execute(
|
||
select(User).where(User.id == user_id)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
|
||
# ==================== 便捷函数 ====================
|
||
|
||
async def create_practice_room(
|
||
db: AsyncSession,
|
||
host_user_id: int,
|
||
**kwargs
|
||
) -> PracticeRoom:
|
||
"""便捷函数:创建房间"""
|
||
service = PracticeRoomService(db)
|
||
return await service.create_room(host_user_id, **kwargs)
|
||
|
||
|
||
async def join_practice_room(
|
||
db: AsyncSession,
|
||
room_code: str,
|
||
user_id: int
|
||
) -> PracticeRoom:
|
||
"""便捷函数:加入房间"""
|
||
service = PracticeRoomService(db)
|
||
return await service.join_room(room_code, user_id)
|