feat: 添加双人对练功能
Some checks failed
continuous-integration/drone/push Build is failing

- 新增数据库迁移脚本 (practice_rooms, practice_room_messages)
- 新增后端 API: 房间创建/加入/消息同步/报告生成
- 新增前端页面: 入口页/对练房间/报告页
- 新增 AI 双人评估服务和提示词
This commit is contained in:
yuliang_guo
2026-01-28 15:20:03 +08:00
parent fc299ed7b7
commit b6aea2e23d
14 changed files with 4195 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
"""
双人对练房间模型
功能:
- 房间管理(创建、加入、状态)
- 参与者管理
- 实时消息同步
"""
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.models.base import Base
class PracticeRoom(Base):
"""双人对练房间模型"""
__tablename__ = "practice_rooms"
id = Column(Integer, primary_key=True, index=True, comment="房间ID")
room_code = Column(String(10), unique=True, nullable=False, index=True, comment="6位邀请码")
room_name = Column(String(200), comment="房间名称")
# 场景信息
scene_id = Column(Integer, ForeignKey("practice_scenes.id", ondelete="SET NULL"), comment="关联场景ID")
scene_name = Column(String(200), comment="场景名称")
scene_type = Column(String(50), comment="场景类型")
scene_background = Column(Text, comment="场景背景")
# 角色设置
role_a_name = Column(String(50), default="角色A", comment="角色A名称如销售顾问")
role_b_name = Column(String(50), default="角色B", comment="角色B名称如顾客")
role_a_description = Column(Text, comment="角色A描述")
role_b_description = Column(Text, comment="角色B描述")
# 参与者信息
host_user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="房主用户ID")
guest_user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), comment="加入者用户ID")
host_role = Column(String(10), default="A", comment="房主选择的角色(A/B)")
max_participants = Column(Integer, default=2, comment="最大参与人数")
# 状态和时间
status = Column(String(20), default="waiting", index=True, comment="状态: waiting/ready/practicing/completed/canceled")
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
started_at = Column(DateTime, comment="开始时间")
ended_at = Column(DateTime, comment="结束时间")
duration_seconds = Column(Integer, default=0, comment="对练时长(秒)")
# 对话统计
total_turns = Column(Integer, default=0, comment="总对话轮次")
role_a_turns = Column(Integer, default=0, comment="角色A发言次数")
role_b_turns = Column(Integer, default=0, comment="角色B发言次数")
# 软删除
is_deleted = Column(Boolean, default=False, comment="是否删除")
deleted_at = Column(DateTime, comment="删除时间")
def __repr__(self):
return f"<PracticeRoom(code='{self.room_code}', status='{self.status}')>"
@property
def is_full(self) -> bool:
"""房间是否已满"""
return self.guest_user_id is not None
@property
def participant_count(self) -> int:
"""当前参与人数"""
count = 1 # 房主
if self.guest_user_id:
count += 1
return count
def get_user_role(self, user_id: int) -> str:
"""获取用户在房间中的角色"""
if user_id == self.host_user_id:
return self.host_role
elif user_id == self.guest_user_id:
return "B" if self.host_role == "A" else "A"
return None
def get_role_name(self, role: str) -> str:
"""获取角色名称"""
if role == "A":
return self.role_a_name
elif role == "B":
return self.role_b_name
return None
def get_user_role_name(self, user_id: int) -> str:
"""获取用户的角色名称"""
role = self.get_user_role(user_id)
return self.get_role_name(role) if role else None
class PracticeRoomMessage(Base):
"""房间实时消息模型"""
__tablename__ = "practice_room_messages"
id = Column(Integer, primary_key=True, index=True, comment="消息ID")
room_id = Column(Integer, ForeignKey("practice_rooms.id", ondelete="CASCADE"), nullable=False, index=True, comment="房间ID")
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), comment="发送者用户ID")
message_type = Column(String(20), nullable=False, comment="消息类型: chat/system/join/leave/start/end")
content = Column(Text, comment="消息内容")
role_name = Column(String(50), comment="角色名称")
sequence = Column(Integer, nullable=False, comment="消息序号")
created_at = Column(DateTime(3), server_default=func.now(3), comment="创建时间")
def __repr__(self):
return f"<PracticeRoomMessage(room_id={self.room_id}, type='{self.message_type}', seq={self.sequence})>"
def to_dict(self) -> dict:
"""转换为字典用于SSE推送"""
return {
"id": self.id,
"room_id": self.room_id,
"user_id": self.user_id,
"message_type": self.message_type,
"content": self.content,
"role_name": self.role_name,
"sequence": self.sequence,
"created_at": self.created_at.isoformat() if self.created_at else None
}