feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
49
backend/app/models/__init__.py
Normal file
49
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""数据库模型包"""
|
||||
from app.models.base import Base, BaseModel
|
||||
from app.models.user import User
|
||||
from app.models.course import Course, CourseMaterial, KnowledgePoint, GrowthPath
|
||||
from app.models.training import (
|
||||
TrainingScene,
|
||||
TrainingSession,
|
||||
TrainingMessage,
|
||||
TrainingReport,
|
||||
)
|
||||
from app.models.exam import Exam, Question, ExamResult
|
||||
from app.models.exam_mistake import ExamMistake
|
||||
from app.models.position import Position
|
||||
from app.models.position_member import PositionMember
|
||||
from app.models.position_course import PositionCourse
|
||||
from app.models.practice import PracticeScene, PracticeSession, PracticeDialogue, PracticeReport
|
||||
from app.models.system_log import SystemLog
|
||||
from app.models.task import Task, TaskCourse, TaskAssignment
|
||||
from app.models.notification import Notification
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
"BaseModel",
|
||||
"User",
|
||||
"Course",
|
||||
"CourseMaterial",
|
||||
"KnowledgePoint",
|
||||
"GrowthPath",
|
||||
"TrainingScene",
|
||||
"TrainingSession",
|
||||
"TrainingMessage",
|
||||
"TrainingReport",
|
||||
"Exam",
|
||||
"Question",
|
||||
"ExamResult",
|
||||
"ExamMistake",
|
||||
"Position",
|
||||
"PositionMember",
|
||||
"PositionCourse",
|
||||
"PracticeScene",
|
||||
"PracticeSession",
|
||||
"PracticeDialogue",
|
||||
"PracticeReport",
|
||||
"SystemLog",
|
||||
"Task",
|
||||
"TaskCourse",
|
||||
"TaskAssignment",
|
||||
"Notification",
|
||||
]
|
||||
64
backend/app/models/ability.py
Normal file
64
backend/app/models/ability.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
能力评估模型
|
||||
用于存储智能工牌数据分析、练习报告等产生的能力评估结果
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, Text
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class AbilityAssessment(Base):
|
||||
"""能力评估历史表"""
|
||||
__tablename__ = "ability_assessments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, comment='主键ID')
|
||||
user_id = Column(
|
||||
Integer,
|
||||
ForeignKey('users.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
comment='用户ID'
|
||||
)
|
||||
source_type = Column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
comment='数据来源: yanji_badge(智能工牌), practice_report(练习报告), manual(手动评估)'
|
||||
)
|
||||
source_id = Column(
|
||||
String(100),
|
||||
comment='来源记录ID(如录音ID列表,逗号分隔)'
|
||||
)
|
||||
total_score = Column(
|
||||
Integer,
|
||||
comment='综合评分(0-100)'
|
||||
)
|
||||
ability_dimensions = Column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
comment='6个能力维度评分JSON数组'
|
||||
)
|
||||
recommended_courses = Column(
|
||||
JSON,
|
||||
comment='推荐课程列表JSON数组'
|
||||
)
|
||||
conversation_count = Column(
|
||||
Integer,
|
||||
comment='分析的对话数量'
|
||||
)
|
||||
analyzed_at = Column(
|
||||
DateTime,
|
||||
server_default=func.now(),
|
||||
comment='分析时间'
|
||||
)
|
||||
created_at = Column(
|
||||
DateTime,
|
||||
server_default=func.now(),
|
||||
comment='创建时间'
|
||||
)
|
||||
|
||||
# 关系
|
||||
# user = relationship("User", back_populates="ability_assessments")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AbilityAssessment(id={self.id}, user_id={self.user_id}, total_score={self.total_score})>"
|
||||
|
||||
47
backend/app/models/base.py
Normal file
47
backend/app/models/base.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""基础模型定义"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, Boolean, func
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
# 创建基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class BaseModel(Base):
|
||||
"""
|
||||
基础模型类,所有模型都应继承此类
|
||||
包含通用字段:id, created_at, updated_at
|
||||
时区:使用北京时间(Asia/Shanghai, UTC+8)
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
__allow_unmapped__ = True # SQLAlchemy 2.0 兼容性
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), nullable=False, comment="创建时间(北京时间)"
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False, comment="更新时间(北京时间)"
|
||||
)
|
||||
|
||||
|
||||
class SoftDeleteMixin:
|
||||
"""软删除混入类"""
|
||||
|
||||
__allow_unmapped__ = True
|
||||
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
|
||||
class AuditMixin:
|
||||
"""审计字段混入类"""
|
||||
|
||||
__allow_unmapped__ = True
|
||||
|
||||
created_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
updated_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
270
backend/app/models/course.py
Normal file
270
backend/app/models/course.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
课程相关数据库模型
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
Text,
|
||||
Integer,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
Enum as SQLEnum,
|
||||
Float,
|
||||
JSON,
|
||||
DateTime,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
|
||||
|
||||
class CourseStatus(str, Enum):
|
||||
"""课程状态枚举"""
|
||||
|
||||
DRAFT = "draft" # 草稿
|
||||
PUBLISHED = "published" # 已发布
|
||||
ARCHIVED = "archived" # 已归档
|
||||
|
||||
|
||||
class CourseCategory(str, Enum):
|
||||
"""课程分类枚举"""
|
||||
|
||||
TECHNOLOGY = "technology" # 技术
|
||||
MANAGEMENT = "management" # 管理
|
||||
BUSINESS = "business" # 业务
|
||||
GENERAL = "general" # 通用
|
||||
|
||||
|
||||
class Course(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""
|
||||
课程表
|
||||
"""
|
||||
|
||||
__tablename__ = "courses"
|
||||
|
||||
# 基本信息
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="课程名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="课程描述"
|
||||
)
|
||||
category: Mapped[CourseCategory] = mapped_column(
|
||||
SQLEnum(
|
||||
CourseCategory,
|
||||
values_callable=lambda enum_cls: [e.value for e in enum_cls],
|
||||
validate_strings=True,
|
||||
),
|
||||
default=CourseCategory.GENERAL,
|
||||
nullable=False,
|
||||
comment="课程分类",
|
||||
)
|
||||
status: Mapped[CourseStatus] = mapped_column(
|
||||
SQLEnum(
|
||||
CourseStatus,
|
||||
values_callable=lambda enum_cls: [e.value for e in enum_cls],
|
||||
validate_strings=True,
|
||||
),
|
||||
default=CourseStatus.DRAFT,
|
||||
nullable=False,
|
||||
comment="课程状态",
|
||||
)
|
||||
|
||||
# 课程详情
|
||||
cover_image: Mapped[Optional[str]] = mapped_column(
|
||||
String(500), nullable=True, comment="封面图片URL"
|
||||
)
|
||||
duration_hours: Mapped[Optional[float]] = mapped_column(
|
||||
Float, nullable=True, comment="课程时长(小时)"
|
||||
)
|
||||
difficulty_level: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True, comment="难度等级(1-5)"
|
||||
)
|
||||
tags: Mapped[Optional[List[str]]] = mapped_column(
|
||||
JSON, nullable=True, comment="标签列表"
|
||||
)
|
||||
|
||||
# 发布信息
|
||||
published_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="发布时间"
|
||||
)
|
||||
publisher_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True, comment="发布人ID"
|
||||
)
|
||||
|
||||
# 播课信息
|
||||
# 播课功能(Coze工作流直接写数据库)
|
||||
broadcast_audio_url: Mapped[Optional[str]] = mapped_column(
|
||||
String(500), nullable=True, comment="播课音频URL"
|
||||
)
|
||||
broadcast_generated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="播课生成时间"
|
||||
)
|
||||
|
||||
# 排序和权重
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False, comment="排序顺序"
|
||||
)
|
||||
is_featured: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False, comment="是否推荐"
|
||||
)
|
||||
|
||||
# 统计信息
|
||||
student_count: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False, comment="学习人数"
|
||||
)
|
||||
is_new: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False, comment="是否新课程"
|
||||
)
|
||||
|
||||
# 资料下载设置
|
||||
allow_download: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False, comment="是否允许下载资料"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
materials: Mapped[List["CourseMaterial"]] = relationship(
|
||||
"CourseMaterial", back_populates="course"
|
||||
)
|
||||
knowledge_points: Mapped[List["KnowledgePoint"]] = relationship(
|
||||
"KnowledgePoint", back_populates="course"
|
||||
)
|
||||
|
||||
# 岗位分配关系(通过关联表)
|
||||
position_assignments = relationship("PositionCourse", back_populates="course", cascade="all, delete-orphan")
|
||||
exams = relationship("Exam", back_populates="course")
|
||||
questions = relationship("Question", back_populates="course")
|
||||
|
||||
|
||||
class CourseMaterial(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""
|
||||
课程资料表
|
||||
"""
|
||||
|
||||
__tablename__ = "course_materials"
|
||||
|
||||
course_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("courses.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="课程ID",
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="资料名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="资料描述"
|
||||
)
|
||||
file_url: Mapped[str] = mapped_column(String(500), nullable=False, comment="文件URL")
|
||||
file_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="文件类型")
|
||||
file_size: Mapped[int] = mapped_column(Integer, nullable=False, comment="文件大小(字节)")
|
||||
|
||||
# 排序
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False, comment="排序顺序"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
course: Mapped["Course"] = relationship("Course", back_populates="materials")
|
||||
# 关联的知识点(直接关联)
|
||||
knowledge_points: Mapped[List["KnowledgePoint"]] = relationship(
|
||||
"KnowledgePoint", back_populates="material"
|
||||
)
|
||||
|
||||
|
||||
class KnowledgePoint(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""
|
||||
知识点表
|
||||
"""
|
||||
|
||||
__tablename__ = "knowledge_points"
|
||||
|
||||
course_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("courses.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="课程ID",
|
||||
)
|
||||
material_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("course_materials.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="关联资料ID",
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="知识点名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="知识点描述"
|
||||
)
|
||||
type: Mapped[str] = mapped_column(
|
||||
String(50), default="概念定义", nullable=False, comment="知识点类型"
|
||||
)
|
||||
source: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False, comment="来源:0=手动,1=AI分析"
|
||||
)
|
||||
topic_relation: Mapped[Optional[str]] = mapped_column(
|
||||
String(200), nullable=True, comment="与主题的关系描述"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
course: Mapped["Course"] = relationship("Course", back_populates="knowledge_points")
|
||||
material: Mapped["CourseMaterial"] = relationship("CourseMaterial")
|
||||
|
||||
|
||||
class GrowthPath(BaseModel, SoftDeleteMixin):
|
||||
"""
|
||||
成长路径表
|
||||
"""
|
||||
|
||||
__tablename__ = "growth_paths"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="路径名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="路径描述"
|
||||
)
|
||||
target_role: Mapped[Optional[str]] = mapped_column(
|
||||
String(100), nullable=True, comment="目标角色"
|
||||
)
|
||||
|
||||
# 路径配置
|
||||
courses: Mapped[Optional[List[dict]]] = mapped_column(
|
||||
JSON, nullable=True, comment="课程列表[{course_id, order, is_required}]"
|
||||
)
|
||||
|
||||
# 预计时长
|
||||
estimated_duration_days: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True, comment="预计完成天数"
|
||||
)
|
||||
|
||||
# 状态
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False, comment="是否启用"
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False, comment="排序顺序"
|
||||
)
|
||||
|
||||
|
||||
class MaterialKnowledgePoint(BaseModel, SoftDeleteMixin):
|
||||
"""
|
||||
资料知识点关联表
|
||||
"""
|
||||
|
||||
__tablename__ = "material_knowledge_points"
|
||||
|
||||
material_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("course_materials.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="资料ID",
|
||||
)
|
||||
knowledge_point_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("knowledge_points.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="知识点ID",
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False, comment="排序顺序"
|
||||
)
|
||||
is_ai_generated: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False, comment="是否AI生成"
|
||||
)
|
||||
34
backend/app/models/course_exam_settings.py
Normal file
34
backend/app/models/course_exam_settings.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
课程考试设置模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
|
||||
|
||||
class CourseExamSettings(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""课程考试设置表"""
|
||||
__tablename__ = "course_exam_settings"
|
||||
|
||||
course_id = Column(Integer, ForeignKey("courses.id"), unique=True, nullable=False, comment="课程ID")
|
||||
|
||||
# 题型数量设置
|
||||
single_choice_count = Column(Integer, default=4, nullable=False, comment="单选题数量")
|
||||
multiple_choice_count = Column(Integer, default=2, nullable=False, comment="多选题数量")
|
||||
true_false_count = Column(Integer, default=1, nullable=False, comment="判断题数量")
|
||||
fill_blank_count = Column(Integer, default=2, nullable=False, comment="填空题数量")
|
||||
essay_count = Column(Integer, default=1, nullable=False, comment="问答题数量")
|
||||
|
||||
# 考试参数设置
|
||||
duration_minutes = Column(Integer, default=10, nullable=False, comment="考试时长(分钟)")
|
||||
difficulty_level = Column(Integer, default=3, nullable=False, comment="难度系数(1-5)")
|
||||
passing_score = Column(Integer, default=60, nullable=False, comment="及格分数")
|
||||
|
||||
# 其他设置
|
||||
is_enabled = Column(Boolean, default=True, nullable=False, comment="是否启用")
|
||||
show_answer_immediately = Column(Boolean, default=False, nullable=False, comment="是否立即显示答案")
|
||||
allow_retake = Column(Boolean, default=True, nullable=False, comment="是否允许重考")
|
||||
max_retake_times = Column(Integer, default=3, nullable=True, comment="最大重考次数")
|
||||
|
||||
# 关系
|
||||
course = relationship("Course", backref="exam_settings", uselist=False)
|
||||
153
backend/app/models/exam.py
Normal file
153
backend/app/models/exam.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
考试相关模型定义
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Float, func
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class Exam(BaseModel):
|
||||
"""考试记录模型"""
|
||||
|
||||
__tablename__ = "exams"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||
)
|
||||
course_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("courses.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# 考试信息
|
||||
exam_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
question_count: Mapped[int] = mapped_column(Integer, default=10)
|
||||
total_score: Mapped[float] = mapped_column(Float, default=100.0)
|
||||
pass_score: Mapped[float] = mapped_column(Float, default=60.0)
|
||||
|
||||
# 考试时间
|
||||
start_time: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), comment="开始时间(北京时间)")
|
||||
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="结束时间(北京时间)")
|
||||
duration_minutes: Mapped[int] = mapped_column(Integer, default=60) # 考试时长(分钟)
|
||||
|
||||
# 考试结果
|
||||
score: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
||||
|
||||
# 三轮考试得分
|
||||
round1_score: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="第一轮得分")
|
||||
round2_score: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="第二轮得分")
|
||||
round3_score: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="第三轮得分")
|
||||
|
||||
is_passed: Mapped[Optional[bool]] = mapped_column(nullable=True)
|
||||
|
||||
# 考试状态: started, submitted, timeout
|
||||
status: Mapped[str] = mapped_column(String(20), default="started", index=True)
|
||||
|
||||
# 考试数据(JSON格式存储题目和答案)
|
||||
questions: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
answers: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# 关系
|
||||
user = relationship("User", back_populates="exams")
|
||||
course = relationship("Course", back_populates="exams")
|
||||
results = relationship("ExamResult", back_populates="exam")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Exam(id={self.id}, user_id={self.user_id}, course_id={self.course_id}, status={self.status})>"
|
||||
|
||||
|
||||
class Question(BaseModel):
|
||||
"""题目模型"""
|
||||
|
||||
__tablename__ = "questions"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
course_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("courses.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# 题目类型: single_choice, multiple_choice, true_false, fill_blank, essay
|
||||
question_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
|
||||
# 题目内容
|
||||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
content: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 选项(JSON格式,适用于选择题)
|
||||
options: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# 答案
|
||||
correct_answer: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
explanation: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 分值
|
||||
score: Mapped[float] = mapped_column(Float, default=10.0)
|
||||
|
||||
# 难度等级: easy, medium, hard
|
||||
difficulty: Mapped[str] = mapped_column(String(10), default="medium", index=True)
|
||||
|
||||
# 标签(JSON格式)
|
||||
tags: Mapped[Optional[list]] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# 使用统计
|
||||
usage_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
correct_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
# 状态
|
||||
is_active: Mapped[bool] = mapped_column(default=True, index=True)
|
||||
|
||||
# 关系
|
||||
course = relationship("Course", back_populates="questions")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Question(id={self.id}, course_id={self.course_id}, type={self.question_type})>"
|
||||
|
||||
|
||||
class ExamResult(BaseModel):
|
||||
"""考试结果详情模型"""
|
||||
|
||||
__tablename__ = "exam_results"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
exam_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("exams.id"), nullable=False, index=True
|
||||
)
|
||||
question_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("questions.id"), nullable=False
|
||||
)
|
||||
|
||||
# 用户答案
|
||||
user_answer: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 是否正确
|
||||
is_correct: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
# 得分
|
||||
score: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
|
||||
# 答题时长(秒)
|
||||
answer_time: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
|
||||
# 关系
|
||||
exam = relationship("Exam", back_populates="results")
|
||||
question = relationship("Question")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ExamResult(id={self.id}, exam_id={self.exam_id}, question_id={self.question_id}, is_correct={self.is_correct})>"
|
||||
|
||||
|
||||
# 在模型文件末尾添加关系定义
|
||||
# 需要在User模型中添加
|
||||
# exams = relationship("Exam", back_populates="user")
|
||||
|
||||
# 需要在Course模型中添加
|
||||
# exams = relationship("Exam", back_populates="course")
|
||||
# questions = relationship("Question", back_populates="course")
|
||||
|
||||
# 需要在Exam模型中添加
|
||||
# results = relationship("ExamResult", back_populates="exam")
|
||||
43
backend/app/models/exam_mistake.py
Normal file
43
backend/app/models/exam_mistake.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
错题记录模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Text, DateTime, func
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class ExamMistake(BaseModel):
|
||||
"""错题记录表"""
|
||||
__tablename__ = "exam_mistakes"
|
||||
|
||||
# 核心关联字段
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="用户ID")
|
||||
exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True, comment="考试ID")
|
||||
question_id = Column(Integer, ForeignKey("questions.id", ondelete="SET NULL"), nullable=True, index=True, comment="题目ID(AI生成的题目可能为空)")
|
||||
knowledge_point_id = Column(Integer, ForeignKey("knowledge_points.id", ondelete="SET NULL"), nullable=True, index=True, comment="关联的知识点ID")
|
||||
|
||||
# 题目核心信息
|
||||
question_content = Column(Text, nullable=False, comment="题目内容")
|
||||
correct_answer = Column(Text, nullable=False, comment="正确答案")
|
||||
user_answer = Column(Text, nullable=True, comment="用户答案")
|
||||
question_type = Column(Text, nullable=True, index=True, comment="题型(single/multiple/judge/blank/essay)")
|
||||
|
||||
# 掌握状态和统计字段
|
||||
mastery_status = Column(Text, nullable=False, default='unmastered', index=True, comment="掌握状态: unmastered-未掌握, mastered-已掌握")
|
||||
difficulty = Column(Text, nullable=False, default='medium', index=True, comment="题目难度: easy-简单, medium-中等, hard-困难")
|
||||
wrong_count = Column(Integer, nullable=False, default=1, comment="错误次数统计")
|
||||
mastered_at = Column(DateTime, nullable=True, comment="标记掌握时间")
|
||||
|
||||
# 审计字段(继承自BaseModel,但这里重写以匹配数据库实际结构)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now(), comment="创建时间(北京时间)")
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now(), comment="更新时间(北京时间)")
|
||||
|
||||
# 关系
|
||||
user = relationship("User", backref="exam_mistakes")
|
||||
exam = relationship("Exam", backref="mistakes")
|
||||
question = relationship("Question", backref="mistake_records")
|
||||
knowledge_point = relationship("KnowledgePoint", backref="mistake_records")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ExamMistake(id={self.id}, user_id={self.user_id}, exam_id={self.exam_id})>"
|
||||
106
backend/app/models/notification.py
Normal file
106
backend/app/models/notification.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
站内消息通知模型
|
||||
用于记录用户的站内消息通知
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Text, Integer, Boolean, Index, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
"""
|
||||
站内消息通知模型
|
||||
|
||||
用于存储发送给用户的各类站内通知消息,如:
|
||||
- 岗位分配通知
|
||||
- 课程分配通知
|
||||
- 考试提醒通知
|
||||
- 系统公告通知
|
||||
"""
|
||||
__tablename__ = "notifications"
|
||||
|
||||
# 接收用户ID(外键关联到users表)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="接收用户ID"
|
||||
)
|
||||
|
||||
# 通知标题
|
||||
title: Mapped[str] = mapped_column(
|
||||
String(200),
|
||||
nullable=False,
|
||||
comment="通知标题"
|
||||
)
|
||||
|
||||
# 通知内容
|
||||
content: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="通知内容"
|
||||
)
|
||||
|
||||
# 通知类型
|
||||
# position_assign: 岗位分配
|
||||
# course_assign: 课程分配
|
||||
# exam_remind: 考试提醒
|
||||
# task_assign: 任务分配
|
||||
# system: 系统通知
|
||||
type: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default="system",
|
||||
index=True,
|
||||
comment="通知类型:position_assign/course_assign/exam_remind/task_assign/system"
|
||||
)
|
||||
|
||||
# 是否已读
|
||||
is_read: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
default=False,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="是否已读"
|
||||
)
|
||||
|
||||
# 关联数据ID(可选,如岗位ID、课程ID等)
|
||||
related_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
nullable=True,
|
||||
comment="关联数据ID(岗位ID/课程ID等)"
|
||||
)
|
||||
|
||||
# 关联数据类型(可选,如position、course等)
|
||||
related_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
nullable=True,
|
||||
comment="关联数据类型"
|
||||
)
|
||||
|
||||
# 发送者ID(可选,系统通知时为空)
|
||||
sender_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="发送者用户ID"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
user = relationship("User", foreign_keys=[user_id], backref="notifications")
|
||||
sender = relationship("User", foreign_keys=[sender_id])
|
||||
|
||||
# 创建索引以优化查询性能
|
||||
__table_args__ = (
|
||||
Index('idx_notifications_user_read', 'user_id', 'is_read'),
|
||||
Index('idx_notifications_user_created', 'user_id', 'created_at'),
|
||||
Index('idx_notifications_type', 'type'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Notification(id={self.id}, user_id={self.user_id}, title={self.title}, is_read={self.is_read})>"
|
||||
|
||||
54
backend/app/models/position.py
Normal file
54
backend/app/models/position.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
岗位(Position)数据模型
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Integer, Text, ForeignKey, Boolean, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from typing import Optional, List
|
||||
|
||||
from .base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
|
||||
|
||||
class Position(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""
|
||||
岗位表
|
||||
|
||||
字段说明:
|
||||
- name: 岗位名称
|
||||
- code: 岗位编码(唯一),用于稳定引用
|
||||
- description: 岗位描述
|
||||
- parent_id: 上级岗位ID,支持树形结构
|
||||
- status: 状态(active/inactive)
|
||||
"""
|
||||
|
||||
__tablename__ = "positions"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, index=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("positions.id", ondelete="SET NULL"))
|
||||
status: Mapped[str] = mapped_column(String(20), default="active", nullable=False)
|
||||
|
||||
# 新增字段
|
||||
skills: Mapped[Optional[List]] = mapped_column(JSON, nullable=True, comment="核心技能")
|
||||
level: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="岗位等级")
|
||||
sort_order: Mapped[Optional[int]] = mapped_column(Integer, default=0, nullable=True, comment="排序")
|
||||
|
||||
# 关系
|
||||
parent: Mapped[Optional["Position"]] = relationship(
|
||||
"Position", remote_side="Position.id", backref="children", lazy="selectin"
|
||||
)
|
||||
|
||||
# 成员关系(通过关联表)
|
||||
members = relationship("PositionMember", back_populates="position", cascade="all, delete-orphan")
|
||||
|
||||
# 课程关系(通过关联表)
|
||||
courses = relationship("PositionCourse", back_populates="position", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Position(id={self.id}, name={self.name}, code={self.code}, status={self.status})>"
|
||||
|
||||
|
||||
28
backend/app/models/position_course.py
Normal file
28
backend/app/models/position_course.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
岗位课程关联模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Enum, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.base import BaseModel, SoftDeleteMixin
|
||||
|
||||
|
||||
class PositionCourse(BaseModel, SoftDeleteMixin):
|
||||
"""岗位课程关联表"""
|
||||
__tablename__ = "position_courses"
|
||||
|
||||
# 添加唯一约束:同一岗位下同一课程只能有一条有效记录
|
||||
__table_args__ = (
|
||||
UniqueConstraint('position_id', 'course_id', 'is_deleted', name='uix_position_course'),
|
||||
)
|
||||
|
||||
position_id = Column(Integer, ForeignKey("positions.id"), nullable=False, comment="岗位ID")
|
||||
course_id = Column(Integer, ForeignKey("courses.id"), nullable=False, comment="课程ID")
|
||||
|
||||
# 课程类型:required(必修)、optional(选修)
|
||||
course_type = Column(String(20), default="required", nullable=False, comment="课程类型")
|
||||
priority = Column(Integer, default=0, comment="优先级/排序")
|
||||
|
||||
# 关系
|
||||
position = relationship("Position", back_populates="courses")
|
||||
course = relationship("Course", back_populates="position_assignments")
|
||||
26
backend/app/models/position_member.py
Normal file
26
backend/app/models/position_member.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
岗位成员关联模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, UniqueConstraint, func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.base import BaseModel, SoftDeleteMixin
|
||||
|
||||
|
||||
class PositionMember(BaseModel, SoftDeleteMixin):
|
||||
"""岗位成员关联表"""
|
||||
__tablename__ = "position_members"
|
||||
|
||||
# 添加唯一约束:同一岗位下同一用户只能有一条有效记录
|
||||
__table_args__ = (
|
||||
UniqueConstraint('position_id', 'user_id', 'is_deleted', name='uix_position_user'),
|
||||
)
|
||||
|
||||
position_id = Column(Integer, ForeignKey("positions.id"), nullable=False, comment="岗位ID")
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, comment="用户ID")
|
||||
role = Column(String(50), comment="成员角色(预留字段)")
|
||||
joined_at = Column(DateTime, server_default=func.now(), comment="加入时间(北京时间)")
|
||||
|
||||
# 关系
|
||||
position = relationship("Position", back_populates="members")
|
||||
user = relationship("User", back_populates="position_memberships")
|
||||
109
backend/app/models/practice.py
Normal file
109
backend/app/models/practice.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
陪练场景模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, JSON, DECIMAL, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class PracticeScene(Base):
|
||||
"""陪练场景模型"""
|
||||
__tablename__ = "practice_scenes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, comment="场景ID")
|
||||
name = Column(String(200), nullable=False, comment="场景名称")
|
||||
description = Column(Text, comment="场景描述")
|
||||
type = Column(String(50), nullable=False, index=True, comment="场景类型: phone/face/complaint/after-sales/product-intro")
|
||||
difficulty = Column(String(50), nullable=False, index=True, comment="难度等级: beginner/junior/intermediate/senior/expert")
|
||||
status = Column(String(20), default="active", index=True, comment="状态: active/inactive")
|
||||
background = Column(Text, comment="场景背景设定")
|
||||
ai_role = Column(Text, comment="AI角色描述")
|
||||
objectives = Column(JSON, comment="练习目标数组")
|
||||
keywords = Column(JSON, comment="关键词数组")
|
||||
duration = Column(Integer, default=10, comment="预计时长(分钟)")
|
||||
usage_count = Column(Integer, default=0, comment="使用次数")
|
||||
rating = Column(DECIMAL(3, 1), default=0.0, comment="评分")
|
||||
|
||||
# 审计字段
|
||||
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), comment="创建人ID")
|
||||
updated_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), comment="更新人ID")
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
# 软删除字段
|
||||
is_deleted = Column(Boolean, default=False, index=True, comment="是否删除")
|
||||
deleted_at = Column(DateTime, comment="删除时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PracticeScene(id={self.id}, name='{self.name}', type='{self.type}', difficulty='{self.difficulty}')>"
|
||||
|
||||
|
||||
class PracticeSession(Base):
|
||||
"""陪练会话模型"""
|
||||
__tablename__ = "practice_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, comment="会话ID")
|
||||
session_id = Column(String(50), unique=True, nullable=False, index=True, comment="会话唯一标识")
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="学员ID")
|
||||
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="场景类型")
|
||||
conversation_id = Column(String(100), comment="Coze对话ID")
|
||||
|
||||
# 会话时间信息
|
||||
start_time = Column(DateTime, nullable=False, index=True, comment="开始时间")
|
||||
end_time = Column(DateTime, comment="结束时间")
|
||||
duration_seconds = Column(Integer, default=0, comment="时长(秒)")
|
||||
turns = Column(Integer, default=0, comment="对话轮次")
|
||||
status = Column(String(20), default="in_progress", index=True, comment="状态: in_progress/completed/canceled")
|
||||
|
||||
# 审计字段
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
is_deleted = Column(Boolean, default=False, comment="是否删除")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PracticeSession(session_id='{self.session_id}', scene='{self.scene_name}', status='{self.status}')>"
|
||||
|
||||
|
||||
class PracticeDialogue(Base):
|
||||
"""陪练对话记录模型"""
|
||||
__tablename__ = "practice_dialogues"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, comment="对话ID")
|
||||
session_id = Column(String(50), nullable=False, index=True, comment="会话ID")
|
||||
speaker = Column(String(20), nullable=False, comment="说话人: user/ai")
|
||||
content = Column(Text, nullable=False, comment="对话内容")
|
||||
timestamp = Column(DateTime, nullable=False, comment="时间戳")
|
||||
sequence = Column(Integer, nullable=False, comment="顺序号")
|
||||
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PracticeDialogue(session_id='{self.session_id}', speaker='{self.speaker}', seq={self.sequence})>"
|
||||
|
||||
|
||||
class PracticeReport(Base):
|
||||
"""陪练分析报告模型"""
|
||||
__tablename__ = "practice_reports"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, comment="报告ID")
|
||||
session_id = Column(String(50), unique=True, nullable=False, index=True, comment="会话ID")
|
||||
|
||||
# AI分析结果
|
||||
total_score = Column(Integer, comment="综合得分(0-100)")
|
||||
score_breakdown = Column(JSON, comment="分数细分")
|
||||
ability_dimensions = Column(JSON, comment="能力维度")
|
||||
dialogue_review = Column(JSON, comment="对话复盘")
|
||||
suggestions = Column(JSON, comment="改进建议")
|
||||
|
||||
# AI分析元数据
|
||||
workflow_run_id = Column(String(100), comment="AI分析运行ID")
|
||||
task_id = Column(String(100), comment="AI分析任务ID")
|
||||
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PracticeReport(session_id='{self.session_id}', total_score={self.total_score})>"
|
||||
|
||||
60
backend/app/models/system_log.py
Normal file
60
backend/app/models/system_log.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
系统日志模型
|
||||
用于记录系统操作、错误、安全事件等日志信息
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Index
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class SystemLog(BaseModel):
|
||||
"""
|
||||
系统日志模型
|
||||
记录系统各类操作日志
|
||||
"""
|
||||
__tablename__ = "system_logs"
|
||||
|
||||
# 日志级别: debug, info, warning, error
|
||||
level: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
|
||||
# 日志类型: system, user, api, error, security
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
|
||||
# 操作用户(可能为空,如系统自动操作)
|
||||
user: Mapped[str] = mapped_column(String(100), nullable=True, index=True)
|
||||
|
||||
# 用户ID(可能为空)
|
||||
user_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True)
|
||||
|
||||
# IP地址
|
||||
ip: Mapped[str] = mapped_column(String(100), nullable=True)
|
||||
|
||||
# 日志消息
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
# User Agent
|
||||
user_agent: Mapped[str] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# 请求路径(API路径)
|
||||
path: Mapped[str] = mapped_column(String(500), nullable=True, index=True)
|
||||
|
||||
# 请求方法
|
||||
method: Mapped[str] = mapped_column(String(10), nullable=True)
|
||||
|
||||
# 额外数据(JSON格式,可存储详细信息)
|
||||
extra_data: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 创建索引以优化查询性能
|
||||
__table_args__ = (
|
||||
Index('idx_system_logs_created_at', 'created_at'),
|
||||
Index('idx_system_logs_level_type', 'level', 'type'),
|
||||
Index('idx_system_logs_user_created', 'user', 'created_at'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SystemLog(id={self.id}, level={self.level}, type={self.type}, user={self.user})>"
|
||||
|
||||
|
||||
|
||||
100
backend/app/models/task.py
Normal file
100
backend/app/models/task.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
任务相关模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Enum as SQLEnum, JSON, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.models.base import BaseModel
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TaskPriority(str, Enum):
|
||||
"""任务优先级"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
"""任务状态"""
|
||||
PENDING = "pending" # 待开始
|
||||
ONGOING = "ongoing" # 进行中
|
||||
COMPLETED = "completed" # 已完成
|
||||
EXPIRED = "expired" # 已过期
|
||||
|
||||
|
||||
class AssignmentStatus(str, Enum):
|
||||
"""分配状态"""
|
||||
NOT_STARTED = "not_started"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
|
||||
|
||||
class Task(BaseModel):
|
||||
"""任务表"""
|
||||
__tablename__ = "tasks"
|
||||
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="任务标题")
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="任务描述")
|
||||
priority: Mapped[TaskPriority] = mapped_column(
|
||||
SQLEnum(TaskPriority, values_callable=lambda x: [e.value for e in x]),
|
||||
default=TaskPriority.MEDIUM,
|
||||
nullable=False,
|
||||
comment="优先级"
|
||||
)
|
||||
status: Mapped[TaskStatus] = mapped_column(
|
||||
SQLEnum(TaskStatus, values_callable=lambda x: [e.value for e in x]),
|
||||
default=TaskStatus.PENDING,
|
||||
nullable=False,
|
||||
comment="任务状态"
|
||||
)
|
||||
creator_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, comment="创建人ID")
|
||||
deadline: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="截止时间")
|
||||
requirements: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="任务要求配置")
|
||||
progress: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="完成进度")
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# 关系
|
||||
creator = relationship("User", backref="created_tasks", foreign_keys=[creator_id])
|
||||
course_links = relationship("TaskCourse", back_populates="task", cascade="all, delete-orphan")
|
||||
assignments = relationship("TaskAssignment", back_populates="task", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class TaskCourse(BaseModel):
|
||||
"""任务课程关联表"""
|
||||
__tablename__ = "task_courses"
|
||||
|
||||
task_id: Mapped[int] = mapped_column(Integer, ForeignKey("tasks.id"), nullable=False, comment="任务ID")
|
||||
course_id: Mapped[int] = mapped_column(Integer, ForeignKey("courses.id"), nullable=False, comment="课程ID")
|
||||
|
||||
# 关系
|
||||
task = relationship("Task", back_populates="course_links")
|
||||
course = relationship("Course")
|
||||
|
||||
|
||||
class TaskAssignment(BaseModel):
|
||||
"""任务分配表"""
|
||||
__tablename__ = "task_assignments"
|
||||
|
||||
task_id: Mapped[int] = mapped_column(Integer, ForeignKey("tasks.id"), nullable=False, comment="任务ID")
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, comment="分配用户ID")
|
||||
team_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="团队ID")
|
||||
status: Mapped[AssignmentStatus] = mapped_column(
|
||||
SQLEnum(AssignmentStatus, values_callable=lambda x: [e.value for e in x]),
|
||||
default=AssignmentStatus.NOT_STARTED,
|
||||
nullable=False,
|
||||
comment="完成状态"
|
||||
)
|
||||
progress: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="个人完成进度")
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="完成时间")
|
||||
|
||||
# 关系
|
||||
task = relationship("Task", back_populates="assignments")
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
__all__ = ["Task", "TaskCourse", "TaskAssignment", "TaskPriority", "TaskStatus", "AssignmentStatus"]
|
||||
|
||||
|
||||
|
||||
263
backend/app/models/training.py
Normal file
263
backend/app/models/training.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""陪练模块数据模型"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
String,
|
||||
Integer,
|
||||
ForeignKey,
|
||||
Text,
|
||||
JSON,
|
||||
Enum as SQLEnum,
|
||||
Float,
|
||||
Boolean,
|
||||
DateTime,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
|
||||
|
||||
class TrainingSceneStatus(str, Enum):
|
||||
"""陪练场景状态枚举"""
|
||||
|
||||
DRAFT = "draft" # 草稿
|
||||
ACTIVE = "active" # 已激活
|
||||
INACTIVE = "inactive" # 已停用
|
||||
|
||||
|
||||
class TrainingSessionStatus(str, Enum):
|
||||
"""陪练会话状态枚举"""
|
||||
|
||||
CREATED = "created" # 已创建
|
||||
IN_PROGRESS = "in_progress" # 进行中
|
||||
COMPLETED = "completed" # 已完成
|
||||
CANCELLED = "cancelled" # 已取消
|
||||
ERROR = "error" # 异常结束
|
||||
|
||||
|
||||
class MessageType(str, Enum):
|
||||
"""消息类型枚举"""
|
||||
|
||||
TEXT = "text" # 文本消息
|
||||
VOICE = "voice" # 语音消息
|
||||
SYSTEM = "system" # 系统消息
|
||||
|
||||
|
||||
class MessageRole(str, Enum):
|
||||
"""消息角色枚举"""
|
||||
|
||||
USER = "user" # 用户
|
||||
ASSISTANT = "assistant" # AI助手
|
||||
SYSTEM = "system" # 系统
|
||||
|
||||
|
||||
class TrainingScene(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""
|
||||
陪练场景模型
|
||||
定义不同的陪练场景,如面试训练、演讲训练等
|
||||
"""
|
||||
|
||||
__tablename__ = "training_scenes"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
# 基础信息
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="场景名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="场景描述"
|
||||
)
|
||||
category: Mapped[str] = mapped_column(String(50), nullable=False, comment="场景分类")
|
||||
|
||||
# 配置信息
|
||||
ai_config: Mapped[Optional[dict]] = mapped_column(
|
||||
JSON, nullable=True, comment="AI配置(如Coze Bot ID等)"
|
||||
)
|
||||
prompt_template: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="提示词模板"
|
||||
)
|
||||
evaluation_criteria: Mapped[Optional[dict]] = mapped_column(
|
||||
JSON, nullable=True, comment="评估标准"
|
||||
)
|
||||
|
||||
# 状态和权限
|
||||
status: Mapped[TrainingSceneStatus] = mapped_column(
|
||||
SQLEnum(TrainingSceneStatus),
|
||||
default=TrainingSceneStatus.DRAFT,
|
||||
nullable=False,
|
||||
comment="场景状态",
|
||||
)
|
||||
is_public: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False, comment="是否公开"
|
||||
)
|
||||
required_level: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True, comment="所需用户等级"
|
||||
)
|
||||
|
||||
# 关联
|
||||
sessions: Mapped[list["TrainingSession"]] = relationship(
|
||||
"TrainingSession", back_populates="scene", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class TrainingSession(BaseModel, AuditMixin):
|
||||
"""
|
||||
陪练会话模型
|
||||
记录每次陪练会话的信息
|
||||
"""
|
||||
|
||||
__tablename__ = "training_sessions"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
# 基础信息
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, index=True, comment="用户ID"
|
||||
)
|
||||
scene_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("training_scenes.id"), nullable=False, comment="场景ID"
|
||||
)
|
||||
|
||||
# 会话信息
|
||||
coze_conversation_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(100), nullable=True, comment="Coze会话ID"
|
||||
)
|
||||
start_time: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), nullable=False, comment="开始时间(北京时间)"
|
||||
)
|
||||
end_time: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime, nullable=True, comment="结束时间(北京时间)"
|
||||
)
|
||||
duration_seconds: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True, comment="持续时长(秒)"
|
||||
)
|
||||
|
||||
# 状态和配置
|
||||
status: Mapped[TrainingSessionStatus] = mapped_column(
|
||||
SQLEnum(TrainingSessionStatus),
|
||||
default=TrainingSessionStatus.CREATED,
|
||||
nullable=False,
|
||||
comment="会话状态",
|
||||
)
|
||||
session_config: Mapped[Optional[dict]] = mapped_column(
|
||||
JSON, nullable=True, comment="会话配置"
|
||||
)
|
||||
|
||||
# 评估信息
|
||||
total_score: Mapped[Optional[float]] = mapped_column(
|
||||
Float, nullable=True, comment="总分"
|
||||
)
|
||||
evaluation_result: Mapped[Optional[dict]] = mapped_column(
|
||||
JSON, nullable=True, comment="评估结果详情"
|
||||
)
|
||||
|
||||
# 关联
|
||||
scene: Mapped["TrainingScene"] = relationship(
|
||||
"TrainingScene", back_populates="sessions"
|
||||
)
|
||||
messages: Mapped[list["TrainingMessage"]] = relationship(
|
||||
"TrainingMessage",
|
||||
back_populates="session",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="TrainingMessage.created_at",
|
||||
)
|
||||
report: Mapped[Optional["TrainingReport"]] = relationship(
|
||||
"TrainingReport", back_populates="session", uselist=False
|
||||
)
|
||||
|
||||
|
||||
class TrainingMessage(BaseModel):
|
||||
"""
|
||||
陪练消息模型
|
||||
记录会话中的每条消息
|
||||
"""
|
||||
|
||||
__tablename__ = "training_messages"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
# 基础信息
|
||||
session_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("training_sessions.id"), nullable=False, comment="会话ID"
|
||||
)
|
||||
|
||||
# 消息内容
|
||||
role: Mapped[MessageRole] = mapped_column(
|
||||
SQLEnum(MessageRole), nullable=False, comment="消息角色"
|
||||
)
|
||||
type: Mapped[MessageType] = mapped_column(
|
||||
SQLEnum(MessageType), nullable=False, comment="消息类型"
|
||||
)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, comment="消息内容")
|
||||
|
||||
# 语音消息相关
|
||||
voice_url: Mapped[Optional[str]] = mapped_column(
|
||||
String(500), nullable=True, comment="语音文件URL"
|
||||
)
|
||||
voice_duration: Mapped[Optional[float]] = mapped_column(
|
||||
Float, nullable=True, comment="语音时长(秒)"
|
||||
)
|
||||
|
||||
# 元数据
|
||||
message_metadata: Mapped[Optional[dict]] = mapped_column(
|
||||
JSON, nullable=True, comment="消息元数据"
|
||||
)
|
||||
coze_message_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(100), nullable=True, comment="Coze消息ID"
|
||||
)
|
||||
|
||||
# 关联
|
||||
session: Mapped["TrainingSession"] = relationship(
|
||||
"TrainingSession", back_populates="messages"
|
||||
)
|
||||
|
||||
|
||||
class TrainingReport(BaseModel, AuditMixin):
|
||||
"""
|
||||
陪练报告模型
|
||||
存储陪练会话的分析报告
|
||||
"""
|
||||
|
||||
__tablename__ = "training_reports"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
# 基础信息
|
||||
session_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("training_sessions.id"),
|
||||
unique=True,
|
||||
nullable=False,
|
||||
comment="会话ID",
|
||||
)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, index=True, comment="用户ID"
|
||||
)
|
||||
|
||||
# 评分信息
|
||||
overall_score: Mapped[float] = mapped_column(Float, nullable=False, comment="总体得分")
|
||||
dimension_scores: Mapped[dict] = mapped_column(
|
||||
JSON, nullable=False, comment="各维度得分"
|
||||
)
|
||||
|
||||
# 分析内容
|
||||
strengths: Mapped[list[str]] = mapped_column(JSON, nullable=False, comment="优势点")
|
||||
weaknesses: Mapped[list[str]] = mapped_column(JSON, nullable=False, comment="待改进点")
|
||||
suggestions: Mapped[list[str]] = mapped_column(JSON, nullable=False, comment="改进建议")
|
||||
|
||||
# 详细内容
|
||||
detailed_analysis: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="详细分析"
|
||||
)
|
||||
transcript: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="对话文本记录"
|
||||
)
|
||||
|
||||
# 统计信息
|
||||
statistics: Mapped[Optional[dict]] = mapped_column(
|
||||
JSON, nullable=True, comment="统计数据"
|
||||
)
|
||||
|
||||
# 关联
|
||||
session: Mapped["TrainingSession"] = relationship(
|
||||
"TrainingSession", back_populates="report"
|
||||
)
|
||||
171
backend/app/models/user.py
Normal file
171
backend/app/models/user.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
用户相关数据模型
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
Table,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseModel, SoftDeleteMixin
|
||||
|
||||
# 用户-团队关联表(用于多对多关系)
|
||||
user_teams = Table(
|
||||
"user_teams",
|
||||
BaseModel.metadata,
|
||||
Column(
|
||||
"user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
|
||||
),
|
||||
Column(
|
||||
"team_id", Integer, ForeignKey("teams.id", ondelete="CASCADE"), primary_key=True
|
||||
),
|
||||
Column("role", String(50), default="member", nullable=False), # member, leader
|
||||
Column("joined_at", DateTime, server_default=func.now(), nullable=False),
|
||||
UniqueConstraint("user_id", "team_id", name="uq_user_team"),
|
||||
)
|
||||
|
||||
|
||||
class UserTeam(Base):
|
||||
"""用户团队关联模型(用于直接查询关联表)"""
|
||||
|
||||
__allow_unmapped__ = True
|
||||
__table__ = user_teams # 重用已定义的表
|
||||
|
||||
# 定义列映射(不需要id,因为使用复合主键)
|
||||
user_id: Mapped[int]
|
||||
team_id: Mapped[int]
|
||||
role: Mapped[str]
|
||||
joined_at: Mapped[datetime]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserTeam(user_id={self.user_id}, team_id={self.team_id}, role={self.role})>"
|
||||
|
||||
|
||||
class User(BaseModel, SoftDeleteMixin):
|
||||
"""用户模型"""
|
||||
|
||||
__allow_unmapped__ = True
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
# 基础信息
|
||||
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
email: Mapped[Optional[str]] = mapped_column(String(100), unique=True, nullable=True)
|
||||
phone: Mapped[Optional[str]] = mapped_column(String(20), unique=True, nullable=True)
|
||||
hashed_password: Mapped[str] = mapped_column(
|
||||
"password_hash", String(200), nullable=False
|
||||
)
|
||||
|
||||
# 个人信息
|
||||
full_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
avatar_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
bio: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
# 性别: male/female(可扩展)
|
||||
gender: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
|
||||
# 学校
|
||||
school: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
# 专业
|
||||
major: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
# 企微员工userid(用于SCRM系统对接)
|
||||
wework_userid: Mapped[Optional[str]] = mapped_column(String(64), unique=True, nullable=True, comment="企微员工userid")
|
||||
|
||||
# 系统角色:admin, manager, trainee
|
||||
role: Mapped[str] = mapped_column(String(20), default="trainee", nullable=False)
|
||||
|
||||
# 账号状态
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# 时间记录
|
||||
last_login_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
password_changed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
teams: Mapped[List["Team"]] = relationship(
|
||||
"Team",
|
||||
secondary=user_teams,
|
||||
back_populates="members",
|
||||
lazy="selectin",
|
||||
)
|
||||
exams = relationship("Exam", back_populates="user")
|
||||
|
||||
# 岗位关系(通过关联表)
|
||||
position_memberships = relationship("PositionMember", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User(id={self.id}, username={self.username}, role={self.role})>"
|
||||
|
||||
|
||||
class Team(BaseModel, SoftDeleteMixin):
|
||||
"""团队模型"""
|
||||
|
||||
__allow_unmapped__ = True
|
||||
|
||||
__tablename__ = "teams"
|
||||
|
||||
# 基础信息
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 团队类型:department, project, study_group
|
||||
team_type: Mapped[str] = mapped_column(
|
||||
String(50), default="department", nullable=False
|
||||
)
|
||||
|
||||
# 状态
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
# 团队负责人
|
||||
leader_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
# 父团队(支持层级结构)
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("teams.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
members: Mapped[List["User"]] = relationship(
|
||||
"User",
|
||||
secondary=user_teams,
|
||||
back_populates="teams",
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
leader: Mapped[Optional["User"]] = relationship(
|
||||
"User",
|
||||
foreign_keys=[leader_id],
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
parent: Mapped[Optional["Team"]] = relationship(
|
||||
"Team",
|
||||
remote_side="Team.id",
|
||||
foreign_keys=[parent_id],
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
children: Mapped[List["Team"]] = relationship(
|
||||
"Team",
|
||||
back_populates="parent",
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Team(id={self.id}, name={self.name}, code={self.code})>"
|
||||
Reference in New Issue
Block a user