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 格式
This commit is contained in:
@@ -32,6 +32,11 @@ from app.models.certificate import (
|
||||
UserCertificate,
|
||||
CertificateType,
|
||||
)
|
||||
from app.models.user_course_progress import (
|
||||
UserCourseProgress,
|
||||
UserMaterialProgress,
|
||||
ProgressStatus,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
@@ -72,4 +77,7 @@ __all__ = [
|
||||
"CertificateTemplate",
|
||||
"UserCertificate",
|
||||
"CertificateType",
|
||||
"UserCourseProgress",
|
||||
"UserMaterialProgress",
|
||||
"ProgressStatus",
|
||||
]
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
"""
|
||||
证书系统数据模型
|
||||
|
||||
定义证书模板和用户证书的数据结构
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, Enum as SQLEnum, DECIMAL, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class CertificateType(str, Enum):
|
||||
"""证书类型枚举"""
|
||||
COURSE = "course" # 课程结业证书
|
||||
EXAM = "exam" # 考试合格证书
|
||||
ACHIEVEMENT = "achievement" # 成就证书
|
||||
|
||||
|
||||
class CertificateTemplate(Base):
|
||||
"""证书模板表"""
|
||||
__tablename__ = "certificate_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(100), nullable=False, comment="模板名称")
|
||||
type = Column(SQLEnum(CertificateType), nullable=False, comment="证书类型")
|
||||
background_url = Column(String(500), comment="证书背景图URL")
|
||||
template_html = Column(Text, comment="HTML模板内容")
|
||||
template_style = Column(Text, comment="CSS样式")
|
||||
is_active = Column(Boolean, default=True, comment="是否启用")
|
||||
sort_order = Column(Integer, default=0, comment="排序顺序")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
certificates = relationship("UserCertificate", back_populates="template")
|
||||
|
||||
|
||||
class UserCertificate(Base):
|
||||
"""用户证书表"""
|
||||
__tablename__ = "user_certificates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID")
|
||||
template_id = Column(Integer, ForeignKey("certificate_templates.id"), nullable=False, comment="模板ID")
|
||||
certificate_no = Column(String(50), unique=True, nullable=False, comment="证书编号")
|
||||
title = Column(String(200), nullable=False, comment="证书标题")
|
||||
description = Column(Text, comment="证书描述")
|
||||
issued_at = Column(DateTime, default=datetime.now, comment="颁发时间")
|
||||
valid_until = Column(DateTime, comment="有效期至")
|
||||
|
||||
# 关联信息
|
||||
course_id = Column(Integer, comment="关联课程ID")
|
||||
exam_id = Column(Integer, comment="关联考试ID")
|
||||
badge_id = Column(Integer, comment="关联奖章ID")
|
||||
|
||||
# 成绩信息
|
||||
score = Column(DECIMAL(5, 2), comment="考试分数")
|
||||
completion_rate = Column(DECIMAL(5, 2), comment="完成率")
|
||||
|
||||
# 生成的文件
|
||||
pdf_url = Column(String(500), comment="PDF文件URL")
|
||||
image_url = Column(String(500), comment="分享图片URL")
|
||||
|
||||
# 元数据
|
||||
meta_data = Column(JSON, comment="扩展元数据")
|
||||
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
template = relationship("CertificateTemplate", back_populates="certificates")
|
||||
user = relationship("User", backref="certificates")
|
||||
"""
|
||||
证书系统数据模型
|
||||
|
||||
定义证书模板和用户证书的数据结构
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, Enum as SQLEnum, DECIMAL, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class CertificateType(str, Enum):
|
||||
"""证书类型枚举"""
|
||||
COURSE = "course" # 课程结业证书
|
||||
EXAM = "exam" # 考试合格证书
|
||||
ACHIEVEMENT = "achievement" # 成就证书
|
||||
|
||||
|
||||
class CertificateTemplate(Base):
|
||||
"""证书模板表"""
|
||||
__tablename__ = "certificate_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(100), nullable=False, comment="模板名称")
|
||||
type = Column(SQLEnum(CertificateType), nullable=False, comment="证书类型")
|
||||
background_url = Column(String(500), comment="证书背景图URL")
|
||||
template_html = Column(Text, comment="HTML模板内容")
|
||||
template_style = Column(Text, comment="CSS样式")
|
||||
is_active = Column(Boolean, default=True, comment="是否启用")
|
||||
sort_order = Column(Integer, default=0, comment="排序顺序")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
certificates = relationship("UserCertificate", back_populates="template")
|
||||
|
||||
|
||||
class UserCertificate(Base):
|
||||
"""用户证书表"""
|
||||
__tablename__ = "user_certificates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID")
|
||||
template_id = Column(Integer, ForeignKey("certificate_templates.id"), nullable=False, comment="模板ID")
|
||||
certificate_no = Column(String(50), unique=True, nullable=False, comment="证书编号")
|
||||
title = Column(String(200), nullable=False, comment="证书标题")
|
||||
description = Column(Text, comment="证书描述")
|
||||
issued_at = Column(DateTime, default=datetime.now, comment="颁发时间")
|
||||
valid_until = Column(DateTime, comment="有效期至")
|
||||
|
||||
# 关联信息
|
||||
course_id = Column(Integer, comment="关联课程ID")
|
||||
exam_id = Column(Integer, comment="关联考试ID")
|
||||
badge_id = Column(Integer, comment="关联奖章ID")
|
||||
|
||||
# 成绩信息
|
||||
score = Column(DECIMAL(5, 2), comment="考试分数")
|
||||
completion_rate = Column(DECIMAL(5, 2), comment="完成率")
|
||||
|
||||
# 生成的文件
|
||||
pdf_url = Column(String(500), comment="PDF文件URL")
|
||||
image_url = Column(String(500), comment="分享图片URL")
|
||||
|
||||
# 元数据
|
||||
meta_data = Column(JSON, comment="扩展元数据")
|
||||
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
template = relationship("CertificateTemplate", back_populates="certificates")
|
||||
user = relationship("User", backref="certificates")
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
"""
|
||||
等级与奖章系统模型
|
||||
|
||||
包含:
|
||||
- UserLevel: 用户等级信息
|
||||
- ExpHistory: 经验值变化历史
|
||||
- BadgeDefinition: 奖章定义
|
||||
- UserBadge: 用户已获得的奖章
|
||||
- LevelConfig: 等级配置
|
||||
"""
|
||||
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Date, Boolean, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import Base, BaseModel
|
||||
|
||||
|
||||
class UserLevel(Base):
|
||||
"""用户等级表"""
|
||||
__tablename__ = "user_levels"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
|
||||
level = Column(Integer, nullable=False, default=1, comment="当前等级")
|
||||
exp = Column(Integer, nullable=False, default=0, comment="当前经验值")
|
||||
total_exp = Column(Integer, nullable=False, default=0, comment="累计获得经验值")
|
||||
login_streak = Column(Integer, nullable=False, default=0, comment="连续登录天数")
|
||||
max_login_streak = Column(Integer, nullable=False, default=0, comment="历史最长连续登录天数")
|
||||
last_login_date = Column(Date, nullable=True, comment="最后登录日期")
|
||||
last_checkin_at = Column(DateTime, nullable=True, comment="最后签到时间")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="user_level", uselist=False)
|
||||
|
||||
|
||||
class ExpHistory(Base):
|
||||
"""经验值历史表"""
|
||||
__tablename__ = "exp_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
exp_change = Column(Integer, nullable=False, comment="经验值变化")
|
||||
exp_type = Column(String(50), nullable=False, index=True, comment="类型:exam/practice/training/task/login/badge/other")
|
||||
source_id = Column(Integer, nullable=True, comment="来源记录ID")
|
||||
description = Column(String(255), nullable=False, comment="描述")
|
||||
level_before = Column(Integer, nullable=True, comment="变化前等级")
|
||||
level_after = Column(Integer, nullable=True, comment="变化后等级")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="exp_histories")
|
||||
|
||||
|
||||
class BadgeDefinition(Base):
|
||||
"""奖章定义表"""
|
||||
__tablename__ = "badge_definitions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
code = Column(String(50), nullable=False, unique=True, comment="奖章编码")
|
||||
name = Column(String(100), nullable=False, comment="奖章名称")
|
||||
description = Column(String(255), nullable=False, comment="奖章描述")
|
||||
icon = Column(String(100), nullable=False, default="Medal", comment="图标名称")
|
||||
category = Column(String(50), nullable=False, index=True, comment="分类")
|
||||
condition_type = Column(String(50), nullable=False, comment="条件类型")
|
||||
condition_field = Column(String(100), nullable=True, comment="条件字段")
|
||||
condition_value = Column(Integer, nullable=False, default=1, comment="条件数值")
|
||||
exp_reward = Column(Integer, nullable=False, default=0, comment="奖励经验值")
|
||||
sort_order = Column(Integer, nullable=False, default=0, comment="排序")
|
||||
is_active = Column(Boolean, nullable=False, default=True, comment="是否启用")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
user_badges = relationship("UserBadge", back_populates="badge")
|
||||
|
||||
|
||||
class UserBadge(Base):
|
||||
"""用户奖章表"""
|
||||
__tablename__ = "user_badges"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
badge_id = Column(Integer, ForeignKey("badge_definitions.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
unlocked_at = Column(DateTime, nullable=False, default=datetime.now, comment="解锁时间")
|
||||
is_notified = Column(Boolean, nullable=False, default=False, comment="是否已通知")
|
||||
notified_at = Column(DateTime, nullable=True, comment="通知时间")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="badges")
|
||||
badge = relationship("BadgeDefinition", back_populates="user_badges")
|
||||
|
||||
|
||||
class LevelConfig(Base):
|
||||
"""等级配置表"""
|
||||
__tablename__ = "level_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
level = Column(Integer, nullable=False, unique=True, comment="等级")
|
||||
exp_required = Column(Integer, nullable=False, comment="升到此级所需经验值")
|
||||
total_exp_required = Column(Integer, nullable=False, comment="累计所需经验值")
|
||||
title = Column(String(50), nullable=False, comment="等级称号")
|
||||
color = Column(String(20), nullable=True, comment="等级颜色")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
|
||||
|
||||
# 经验值类型枚举
|
||||
class ExpType:
|
||||
"""经验值类型"""
|
||||
EXAM = "exam" # 考试
|
||||
PRACTICE = "practice" # 练习
|
||||
TRAINING = "training" # 陪练
|
||||
TASK = "task" # 任务
|
||||
LOGIN = "login" # 登录/签到
|
||||
BADGE = "badge" # 奖章奖励
|
||||
OTHER = "other" # 其他
|
||||
|
||||
|
||||
# 奖章分类枚举
|
||||
class BadgeCategory:
|
||||
"""奖章分类"""
|
||||
LEARNING = "learning" # 学习进度
|
||||
EXAM = "exam" # 考试成绩
|
||||
PRACTICE = "practice" # 练习时长
|
||||
STREAK = "streak" # 连续打卡
|
||||
SPECIAL = "special" # 特殊成就
|
||||
|
||||
|
||||
# 条件类型枚举
|
||||
class ConditionType:
|
||||
"""解锁条件类型"""
|
||||
COUNT = "count" # 次数
|
||||
SCORE = "score" # 分数
|
||||
STREAK = "streak" # 连续天数
|
||||
LEVEL = "level" # 等级
|
||||
DURATION = "duration" # 时长
|
||||
"""
|
||||
等级与奖章系统模型
|
||||
|
||||
包含:
|
||||
- UserLevel: 用户等级信息
|
||||
- ExpHistory: 经验值变化历史
|
||||
- BadgeDefinition: 奖章定义
|
||||
- UserBadge: 用户已获得的奖章
|
||||
- LevelConfig: 等级配置
|
||||
"""
|
||||
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Date, Boolean, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import Base, BaseModel
|
||||
|
||||
|
||||
class UserLevel(Base):
|
||||
"""用户等级表"""
|
||||
__tablename__ = "user_levels"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
|
||||
level = Column(Integer, nullable=False, default=1, comment="当前等级")
|
||||
exp = Column(Integer, nullable=False, default=0, comment="当前经验值")
|
||||
total_exp = Column(Integer, nullable=False, default=0, comment="累计获得经验值")
|
||||
login_streak = Column(Integer, nullable=False, default=0, comment="连续登录天数")
|
||||
max_login_streak = Column(Integer, nullable=False, default=0, comment="历史最长连续登录天数")
|
||||
last_login_date = Column(Date, nullable=True, comment="最后登录日期")
|
||||
last_checkin_at = Column(DateTime, nullable=True, comment="最后签到时间")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="user_level", uselist=False)
|
||||
|
||||
|
||||
class ExpHistory(Base):
|
||||
"""经验值历史表"""
|
||||
__tablename__ = "exp_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
exp_change = Column(Integer, nullable=False, comment="经验值变化")
|
||||
exp_type = Column(String(50), nullable=False, index=True, comment="类型:exam/practice/training/task/login/badge/other")
|
||||
source_id = Column(Integer, nullable=True, comment="来源记录ID")
|
||||
description = Column(String(255), nullable=False, comment="描述")
|
||||
level_before = Column(Integer, nullable=True, comment="变化前等级")
|
||||
level_after = Column(Integer, nullable=True, comment="变化后等级")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="exp_histories")
|
||||
|
||||
|
||||
class BadgeDefinition(Base):
|
||||
"""奖章定义表"""
|
||||
__tablename__ = "badge_definitions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
code = Column(String(50), nullable=False, unique=True, comment="奖章编码")
|
||||
name = Column(String(100), nullable=False, comment="奖章名称")
|
||||
description = Column(String(255), nullable=False, comment="奖章描述")
|
||||
icon = Column(String(100), nullable=False, default="Medal", comment="图标名称")
|
||||
category = Column(String(50), nullable=False, index=True, comment="分类")
|
||||
condition_type = Column(String(50), nullable=False, comment="条件类型")
|
||||
condition_field = Column(String(100), nullable=True, comment="条件字段")
|
||||
condition_value = Column(Integer, nullable=False, default=1, comment="条件数值")
|
||||
exp_reward = Column(Integer, nullable=False, default=0, comment="奖励经验值")
|
||||
sort_order = Column(Integer, nullable=False, default=0, comment="排序")
|
||||
is_active = Column(Boolean, nullable=False, default=True, comment="是否启用")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 关联
|
||||
user_badges = relationship("UserBadge", back_populates="badge")
|
||||
|
||||
|
||||
class UserBadge(Base):
|
||||
"""用户奖章表"""
|
||||
__tablename__ = "user_badges"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
badge_id = Column(Integer, ForeignKey("badge_definitions.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
unlocked_at = Column(DateTime, nullable=False, default=datetime.now, comment="解锁时间")
|
||||
is_notified = Column(Boolean, nullable=False, default=False, comment="是否已通知")
|
||||
notified_at = Column(DateTime, nullable=True, comment="通知时间")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="badges")
|
||||
badge = relationship("BadgeDefinition", back_populates="user_badges")
|
||||
|
||||
|
||||
class LevelConfig(Base):
|
||||
"""等级配置表"""
|
||||
__tablename__ = "level_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
level = Column(Integer, nullable=False, unique=True, comment="等级")
|
||||
exp_required = Column(Integer, nullable=False, comment="升到此级所需经验值")
|
||||
total_exp_required = Column(Integer, nullable=False, comment="累计所需经验值")
|
||||
title = Column(String(50), nullable=False, comment="等级称号")
|
||||
color = Column(String(20), nullable=True, comment="等级颜色")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.now)
|
||||
|
||||
|
||||
# 经验值类型枚举
|
||||
class ExpType:
|
||||
"""经验值类型"""
|
||||
EXAM = "exam" # 考试
|
||||
PRACTICE = "practice" # 练习
|
||||
TRAINING = "training" # 陪练
|
||||
TASK = "task" # 任务
|
||||
LOGIN = "login" # 登录/签到
|
||||
BADGE = "badge" # 奖章奖励
|
||||
OTHER = "other" # 其他
|
||||
|
||||
|
||||
# 奖章分类枚举
|
||||
class BadgeCategory:
|
||||
"""奖章分类"""
|
||||
LEARNING = "learning" # 学习进度
|
||||
EXAM = "exam" # 考试成绩
|
||||
PRACTICE = "practice" # 练习时长
|
||||
STREAK = "streak" # 连续打卡
|
||||
SPECIAL = "special" # 特殊成就
|
||||
|
||||
|
||||
# 条件类型枚举
|
||||
class ConditionType:
|
||||
"""解锁条件类型"""
|
||||
COUNT = "count" # 次数
|
||||
SCORE = "score" # 分数
|
||||
STREAK = "streak" # 连续天数
|
||||
LEVEL = "level" # 等级
|
||||
DURATION = "duration" # 时长
|
||||
|
||||
@@ -1,122 +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
|
||||
}
|
||||
"""
|
||||
双人对练房间模型
|
||||
|
||||
功能:
|
||||
- 房间管理(创建、加入、状态)
|
||||
- 参与者管理
|
||||
- 实时消息同步
|
||||
"""
|
||||
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
|
||||
}
|
||||
|
||||
201
backend/app/models/user_course_progress.py
Normal file
201
backend/app/models/user_course_progress.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
用户课程学习进度数据库模型
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
Integer,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
Float,
|
||||
DateTime,
|
||||
UniqueConstraint,
|
||||
Index,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class ProgressStatus(str, Enum):
|
||||
"""学习进度状态枚举"""
|
||||
NOT_STARTED = "not_started" # 未开始
|
||||
IN_PROGRESS = "in_progress" # 学习中
|
||||
COMPLETED = "completed" # 已完成
|
||||
|
||||
|
||||
class UserCourseProgress(BaseModel):
|
||||
"""
|
||||
用户课程进度表
|
||||
记录用户对每门课程的整体学习进度
|
||||
"""
|
||||
|
||||
__tablename__ = "user_course_progress"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "course_id", name="uq_user_course"),
|
||||
Index("idx_user_course_progress_user", "user_id"),
|
||||
Index("idx_user_course_progress_course", "course_id"),
|
||||
Index("idx_user_course_progress_status", "status"),
|
||||
)
|
||||
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="用户ID",
|
||||
)
|
||||
course_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("courses.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="课程ID",
|
||||
)
|
||||
|
||||
# 进度信息
|
||||
status: Mapped[ProgressStatus] = mapped_column(
|
||||
String(20),
|
||||
default=ProgressStatus.NOT_STARTED.value,
|
||||
nullable=False,
|
||||
comment="学习状态:not_started/in_progress/completed",
|
||||
)
|
||||
progress_percent: Mapped[float] = mapped_column(
|
||||
Float,
|
||||
default=0.0,
|
||||
nullable=False,
|
||||
comment="完成百分比(0-100)",
|
||||
)
|
||||
completed_materials: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="已完成资料数",
|
||||
)
|
||||
total_materials: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="总资料数",
|
||||
)
|
||||
|
||||
# 学习时长统计
|
||||
total_study_time: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="总学习时长(秒)",
|
||||
)
|
||||
|
||||
# 时间记录
|
||||
first_accessed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="首次访问时间",
|
||||
)
|
||||
last_accessed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="最后访问时间",
|
||||
)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="完成时间",
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
user = relationship("User", backref="course_progress")
|
||||
course = relationship("Course", backref="user_progress")
|
||||
|
||||
|
||||
class UserMaterialProgress(BaseModel):
|
||||
"""
|
||||
用户资料进度表
|
||||
记录用户对每个课程资料的学习进度
|
||||
"""
|
||||
|
||||
__tablename__ = "user_material_progress"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "material_id", name="uq_user_material"),
|
||||
Index("idx_user_material_progress_user", "user_id"),
|
||||
Index("idx_user_material_progress_material", "material_id"),
|
||||
)
|
||||
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="用户ID",
|
||||
)
|
||||
material_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("course_materials.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="资料ID",
|
||||
)
|
||||
course_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("courses.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="课程ID(冗余字段,便于查询)",
|
||||
)
|
||||
|
||||
# 进度信息
|
||||
is_completed: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
default=False,
|
||||
nullable=False,
|
||||
comment="是否已完成",
|
||||
)
|
||||
progress_percent: Mapped[float] = mapped_column(
|
||||
Float,
|
||||
default=0.0,
|
||||
nullable=False,
|
||||
comment="阅读/播放进度百分比(0-100)",
|
||||
)
|
||||
|
||||
# 视频/音频特有字段
|
||||
last_position: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="上次播放位置(秒)",
|
||||
)
|
||||
total_duration: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="媒体总时长(秒)",
|
||||
)
|
||||
|
||||
# 学习时长
|
||||
study_time: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="学习时长(秒)",
|
||||
)
|
||||
|
||||
# 时间记录
|
||||
first_accessed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="首次访问时间",
|
||||
)
|
||||
last_accessed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="最后访问时间",
|
||||
)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="完成时间",
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
user = relationship("User", backref="material_progress")
|
||||
material = relationship("CourseMaterial", backref="user_progress")
|
||||
course = relationship("Course", backref="material_user_progress")
|
||||
Reference in New Issue
Block a user