feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Pydantic模式包"""

View File

@@ -0,0 +1,50 @@
"""
能力评估相关的Pydantic Schema
"""
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
class AbilityDimension(BaseModel):
"""能力维度评分"""
name: str = Field(..., description="能力维度名称")
score: int = Field(..., ge=0, le=100, description="评分(0-100)")
feedback: str = Field(..., description="反馈建议")
class CourseRecommendation(BaseModel):
"""课程推荐"""
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
recommendation_reason: str = Field(..., description="推荐理由")
priority: str = Field(..., description="优先级: high/medium/low")
match_score: int = Field(..., ge=0, le=100, description="匹配度(0-100)")
class AbilityAssessmentResponse(BaseModel):
"""能力评估响应"""
assessment_id: int = Field(..., description="评估记录ID")
total_score: int = Field(..., ge=0, le=100, description="综合评分")
dimensions: List[AbilityDimension] = Field(..., description="能力维度列表")
recommended_courses: List[CourseRecommendation] = Field(..., description="推荐课程列表")
conversation_count: int = Field(..., description="分析的对话数量")
analyzed_at: Optional[datetime] = Field(None, description="分析时间")
class AbilityAssessmentHistory(BaseModel):
"""能力评估历史记录"""
id: int
user_id: int
source_type: str
source_id: Optional[str]
total_score: Optional[int]
ability_dimensions: List[AbilityDimension]
recommended_courses: Optional[List[CourseRecommendation]]
conversation_count: Optional[int]
analyzed_at: datetime
created_at: datetime
class Config:
from_attributes = True

View File

@@ -0,0 +1,35 @@
"""
认证相关 Schema
"""
from pydantic import EmailStr, Field
from .base import BaseSchema
class LoginRequest(BaseSchema):
"""登录请求"""
username: str = Field(..., description="用户名/邮箱/手机号")
password: str = Field(..., min_length=6)
class Token(BaseSchema):
"""令牌响应"""
access_token: str
refresh_token: str
token_type: str = "bearer"
class TokenPayload(BaseSchema):
"""令牌载荷"""
sub: str # 用户ID
type: str # access 或 refresh
exp: int # 过期时间
class RefreshTokenRequest(BaseSchema):
"""刷新令牌请求"""
refresh_token: str

View File

@@ -0,0 +1,73 @@
"""基础响应模式"""
from typing import Generic, TypeVar, Optional, Any, List
from pydantic import BaseModel, Field
from datetime import datetime
DataT = TypeVar("DataT")
class ResponseModel(BaseModel, Generic[DataT]):
"""
统一响应格式模型
"""
code: int = Field(default=200, description="响应状态码")
message: str = Field(default="success", description="响应消息")
data: Optional[DataT] = Field(default=None, description="响应数据")
request_id: Optional[str] = Field(default=None, description="请求ID")
class BaseSchema(BaseModel):
"""基础模式"""
class Config:
from_attributes = True # Pydantic V2
json_encoders = {datetime: lambda v: v.isoformat()}
class TimestampMixin(BaseModel):
"""时间戳混入"""
created_at: datetime
updated_at: datetime
class IDMixin(BaseModel):
"""ID混入"""
id: int
class PaginationParams(BaseModel):
"""分页参数"""
page: int = Field(default=1, ge=1, description="页码")
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
@property
def offset(self) -> int:
"""计算偏移量"""
return (self.page - 1) * self.page_size
@property
def limit(self) -> int:
"""计算限制数量"""
return self.page_size
class PaginatedResponse(BaseModel, Generic[DataT]):
"""分页响应模型"""
items: list[DataT] = Field(default_factory=list, description="数据列表")
total: int = Field(default=0, description="总数量")
page: int = Field(default=1, description="当前页码")
page_size: int = Field(default=20, description="每页数量")
pages: int = Field(default=1, description="总页数")
@classmethod
def create(cls, items: list[DataT], total: int, page: int, page_size: int):
"""创建分页响应"""
pages = (total + page_size - 1) // page_size if page_size > 0 else 1
return cls(
items=items, total=total, page=page, page_size=page_size, pages=pages
)

View File

@@ -0,0 +1,364 @@
"""
课程相关的数据验证模型
"""
from typing import Optional, List
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict, field_validator
from app.models.course import CourseStatus, CourseCategory
class CourseBase(BaseModel):
"""
课程基础模型
"""
name: str = Field(..., min_length=1, max_length=200, description="课程名称")
description: Optional[str] = Field(None, description="课程描述")
category: CourseCategory = Field(default=CourseCategory.GENERAL, description="课程分类")
cover_image: Optional[str] = Field(None, max_length=500, description="封面图片URL")
duration_hours: Optional[float] = Field(None, ge=0, description="课程时长(小时)")
difficulty_level: Optional[int] = Field(None, ge=1, le=5, description="难度等级(1-5)")
tags: Optional[List[str]] = Field(default_factory=list, description="标签列表")
sort_order: int = Field(default=0, description="排序顺序")
is_featured: bool = Field(default=False, description="是否推荐")
allow_download: bool = Field(default=False, description="是否允许下载资料")
@field_validator("category", mode="before")
@classmethod
def normalize_category(cls, v):
"""允许使用枚举的名称或值(忽略大小写)。空字符串使用默认值。"""
if isinstance(v, CourseCategory):
return v
if isinstance(v, str):
s = v.strip()
# 空字符串使用默认值
if not s:
return CourseCategory.GENERAL
# 优先按值匹配technology 等)
try:
return CourseCategory(s.lower())
except Exception:
pass
# 再按名称匹配TECHNOLOGY 等)
try:
return CourseCategory[s.upper()]
except Exception:
pass
return v
class CourseCreate(CourseBase):
"""
创建课程模型
"""
status: CourseStatus = Field(default=CourseStatus.DRAFT, description="课程状态")
class CourseUpdate(BaseModel):
"""
更新课程模型
"""
name: Optional[str] = Field(None, min_length=1, max_length=200, description="课程名称")
description: Optional[str] = Field(None, description="课程描述")
category: Optional[CourseCategory] = Field(None, description="课程分类")
status: Optional[CourseStatus] = Field(None, description="课程状态")
cover_image: Optional[str] = Field(None, max_length=500, description="封面图片URL")
duration_hours: Optional[float] = Field(None, ge=0, description="课程时长(小时)")
difficulty_level: Optional[int] = Field(None, ge=1, le=5, description="难度等级(1-5)")
tags: Optional[List[str]] = Field(None, description="标签列表")
sort_order: Optional[int] = Field(None, description="排序顺序")
is_featured: Optional[bool] = Field(None, description="是否推荐")
allow_download: Optional[bool] = Field(None, description="是否允许下载资料")
@field_validator("category", mode="before")
@classmethod
def normalize_category_update(cls, v):
if v is None:
return v
if isinstance(v, CourseCategory):
return v
if isinstance(v, str):
s = v.strip()
if not s: # 空字符串视为None不更新
return None
try:
return CourseCategory(s.lower())
except Exception:
pass
try:
return CourseCategory[s.upper()]
except Exception:
pass
return v
class CourseInDB(CourseBase):
"""
数据库中的课程模型
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="课程ID")
status: CourseStatus = Field(..., description="课程状态")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
published_at: Optional[datetime] = Field(None, description="发布时间")
publisher_id: Optional[int] = Field(None, description="发布人ID")
created_by: Optional[int] = Field(None, description="创建人ID")
updated_by: Optional[int] = Field(None, description="更新人ID")
# 用户岗位相关的课程类型(必修/选修非数据库字段由API动态计算
course_type: Optional[str] = Field(None, description="课程类型required=必修, optional=选修")
class CourseList(BaseModel):
"""
课程列表查询参数
"""
status: Optional[CourseStatus] = Field(None, description="课程状态")
category: Optional[CourseCategory] = Field(None, description="课程分类")
is_featured: Optional[bool] = Field(None, description="是否推荐")
keyword: Optional[str] = Field(None, description="搜索关键词")
# 课程资料相关模型
class CourseMaterialBase(BaseModel):
"""
课程资料基础模型
"""
name: str = Field(..., min_length=1, max_length=200, description="资料名称")
description: Optional[str] = Field(None, description="资料描述")
sort_order: int = Field(default=0, description="排序顺序")
class CourseMaterialCreate(CourseMaterialBase):
"""
创建课程资料模型
"""
file_url: str = Field(..., max_length=500, description="文件URL")
file_type: str = Field(..., max_length=50, description="文件类型")
file_size: int = Field(..., gt=0, description="文件大小(字节)")
@field_validator("file_type")
def validate_file_type(cls, v):
"""验证文件类型
支持格式TXT、Markdown、MDX、PDF、HTML、Excel、Word、CSV、VTT、Properties
"""
allowed_types = [
"txt", "md", "mdx", "pdf", "html", "htm",
"xlsx", "xls", "docx", "doc", "csv", "vtt", "properties"
]
file_ext = v.lower()
if file_ext not in allowed_types:
raise ValueError(f"不支持的文件类型: {v}。允许的类型: TXT、Markdown、MDX、PDF、HTML、Excel、Word、CSV、VTT、Properties")
return file_ext
class CourseMaterialInDB(CourseMaterialBase):
"""
数据库中的课程资料模型
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="资料ID")
course_id: int = Field(..., description="课程ID")
file_url: str = Field(..., description="文件URL")
file_type: str = Field(..., description="文件类型")
file_size: int = Field(..., description="文件大小(字节)")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
# 知识点相关模型
class KnowledgePointBase(BaseModel):
"""
知识点基础模型
"""
name: str = Field(..., min_length=1, max_length=200, description="知识点名称")
description: Optional[str] = Field(None, description="知识点描述")
type: str = Field(default="理论知识", description="知识点类型")
source: int = Field(default=0, description="来源0=手动1=AI分析")
topic_relation: Optional[str] = Field(None, description="与主题的关系描述")
class KnowledgePointCreate(KnowledgePointBase):
"""
创建知识点模型
"""
material_id: int = Field(..., description="关联资料ID必填")
class KnowledgePointUpdate(BaseModel):
"""
更新知识点模型
"""
name: Optional[str] = Field(None, min_length=1, max_length=200, description="知识点名称")
description: Optional[str] = Field(None, description="知识点描述")
type: Optional[str] = Field(None, description="知识点类型")
source: Optional[int] = Field(None, description="来源0=手动1=AI分析")
topic_relation: Optional[str] = Field(None, description="与主题的关系描述")
material_id: int = Field(..., description="关联资料ID必填")
class KnowledgePointInDB(KnowledgePointBase):
"""
数据库中的知识点模型
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="知识点ID")
course_id: int = Field(..., description="课程ID")
material_id: int = Field(..., description="关联资料ID")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
class KnowledgePointTree(KnowledgePointInDB):
"""
知识点树形结构
"""
children: List["KnowledgePointTree"] = Field(
default_factory=list, description="子知识点"
)
# 成长路径相关模型
class GrowthPathCourse(BaseModel):
"""
成长路径中的课程
"""
course_id: int = Field(..., description="课程ID")
order: int = Field(..., ge=0, description="排序")
is_required: bool = Field(default=True, description="是否必修")
class GrowthPathBase(BaseModel):
"""
成长路径基础模型
"""
name: str = Field(..., min_length=1, max_length=200, description="路径名称")
description: Optional[str] = Field(None, description="路径描述")
target_role: Optional[str] = Field(None, max_length=100, description="目标角色")
courses: List[GrowthPathCourse] = Field(default_factory=list, description="课程列表")
estimated_duration_days: Optional[int] = Field(None, ge=1, description="预计完成天数")
is_active: bool = Field(default=True, description="是否启用")
sort_order: int = Field(default=0, description="排序顺序")
class GrowthPathCreate(GrowthPathBase):
"""
创建成长路径模型
"""
pass
class GrowthPathInDB(GrowthPathBase):
"""
数据库中的成长路径模型
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="路径ID")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
# 课程考试设置相关Schema
class CourseExamSettingsBase(BaseModel):
"""
课程考试设置基础模型
"""
single_choice_count: int = Field(default=4, ge=0, le=50, description="单选题数量")
multiple_choice_count: int = Field(default=2, ge=0, le=30, description="多选题数量")
true_false_count: int = Field(default=1, ge=0, le=20, description="判断题数量")
fill_blank_count: int = Field(default=2, ge=0, le=10, description="填空题数量")
essay_count: int = Field(default=1, ge=0, le=10, description="问答题数量")
duration_minutes: int = Field(default=10, ge=10, le=180, description="考试时长(分钟)")
difficulty_level: int = Field(default=3, ge=1, le=5, description="难度系数(1-5)")
passing_score: int = Field(default=60, ge=0, le=100, description="及格分数")
is_enabled: bool = Field(default=True, description="是否启用")
show_answer_immediately: bool = Field(default=False, description="是否立即显示答案")
allow_retake: bool = Field(default=True, description="是否允许重考")
max_retake_times: Optional[int] = Field(None, ge=1, le=10, description="最大重考次数")
class CourseExamSettingsCreate(CourseExamSettingsBase):
"""
创建课程考试设置模型
"""
pass
class CourseExamSettingsUpdate(BaseModel):
"""
更新课程考试设置模型
"""
single_choice_count: Optional[int] = Field(None, ge=0, le=50, description="单选题数量")
multiple_choice_count: Optional[int] = Field(None, ge=0, le=30, description="多选题数量")
true_false_count: Optional[int] = Field(None, ge=0, le=20, description="判断题数量")
fill_blank_count: Optional[int] = Field(None, ge=0, le=10, description="填空题数量")
essay_count: Optional[int] = Field(None, ge=0, le=10, description="问答题数量")
duration_minutes: Optional[int] = Field(None, ge=10, le=180, description="考试时长(分钟)")
difficulty_level: Optional[int] = Field(None, ge=1, le=5, description="难度系数(1-5)")
passing_score: Optional[int] = Field(None, ge=0, le=100, description="及格分数")
is_enabled: Optional[bool] = Field(None, description="是否启用")
show_answer_immediately: Optional[bool] = Field(None, description="是否立即显示答案")
allow_retake: Optional[bool] = Field(None, description="是否允许重考")
max_retake_times: Optional[int] = Field(None, ge=1, le=10, description="最大重考次数")
class CourseExamSettingsInDB(CourseExamSettingsBase):
"""
数据库中的课程考试设置模型
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="设置ID")
course_id: int = Field(..., description="课程ID")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
# 岗位分配相关Schema
class CoursePositionAssignment(BaseModel):
"""
课程岗位分配模型
"""
position_id: int = Field(..., description="岗位ID")
course_type: str = Field(default="required", pattern="^(required|optional)$", description="课程类型required必修/optional选修")
priority: int = Field(default=0, description="优先级/排序")
class CoursePositionAssignmentInDB(CoursePositionAssignment):
"""
数据库中的课程岗位分配模型
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="分配ID")
course_id: int = Field(..., description="课程ID")
position_name: Optional[str] = Field(None, description="岗位名称")
position_description: Optional[str] = Field(None, description="岗位描述")
member_count: Optional[int] = Field(None, description="岗位成员数")

316
backend/app/schemas/exam.py Normal file
View File

@@ -0,0 +1,316 @@
"""
考试相关的Schema定义
"""
from typing import List, Optional, Dict, Any
from datetime import datetime
from pydantic import BaseModel, Field
class StartExamRequest(BaseModel):
"""开始考试请求"""
course_id: int = Field(..., description="课程ID")
count: int = Field(10, ge=1, le=100, description="题目数量")
class StartExamResponse(BaseModel):
"""开始考试响应"""
exam_id: int = Field(..., description="考试ID")
class ExamAnswer(BaseModel):
"""考试答案"""
question_id: str = Field(..., description="题目ID")
answer: str = Field(..., description="答案")
class SubmitExamRequest(BaseModel):
"""提交考试请求"""
exam_id: int = Field(..., description="考试ID")
answers: List[ExamAnswer] = Field(..., description="答案列表")
class SubmitExamResponse(BaseModel):
"""提交考试响应"""
exam_id: int = Field(..., description="考试ID")
total_score: float = Field(..., description="总分")
pass_score: float = Field(..., description="及格分")
is_passed: bool = Field(..., description="是否通过")
correct_count: int = Field(..., description="正确题数")
total_count: int = Field(..., description="总题数")
accuracy: float = Field(..., description="正确率")
class QuestionInfo(BaseModel):
"""题目信息"""
id: str = Field(..., description="题目ID")
type: str = Field(..., description="题目类型")
title: str = Field(..., description="题目标题")
content: Optional[str] = Field(None, description="题目内容")
options: Optional[Dict[str, Any]] = Field(None, description="选项")
score: float = Field(..., description="分值")
class ExamResultInfo(BaseModel):
"""答题结果信息"""
question_id: int = Field(..., description="题目ID")
user_answer: Optional[str] = Field(None, description="用户答案")
is_correct: bool = Field(..., description="是否正确")
score: float = Field(..., description="得分")
class ExamDetailResponse(BaseModel):
"""考试详情响应"""
id: int = Field(..., description="考试ID")
course_id: int = Field(..., description="课程ID")
exam_name: str = Field(..., description="考试名称")
question_count: int = Field(..., description="题目数量")
total_score: float = Field(..., description="总分")
pass_score: float = Field(..., description="及格分")
start_time: Optional[str] = Field(None, description="开始时间")
end_time: Optional[str] = Field(None, description="结束时间")
duration_minutes: int = Field(..., description="考试时长(分钟)")
status: str = Field(..., description="考试状态")
score: Optional[float] = Field(None, description="得分")
is_passed: Optional[bool] = Field(None, description="是否通过")
questions: Optional[Dict[str, Any]] = Field(None, description="题目数据")
results: Optional[List[ExamResultInfo]] = Field(None, description="答题结果")
answers: Optional[Dict[str, Any]] = Field(None, description="用户答案")
class ExamRecordInfo(BaseModel):
"""考试记录信息"""
id: int = Field(..., description="考试ID")
course_id: int = Field(..., description="课程ID")
exam_name: str = Field(..., description="考试名称")
question_count: int = Field(..., description="题目数量")
total_score: float = Field(..., description="总分")
score: Optional[float] = Field(None, description="得分")
is_passed: Optional[bool] = Field(None, description="是否通过")
status: str = Field(..., description="考试状态")
start_time: Optional[str] = Field(None, description="开始时间")
end_time: Optional[str] = Field(None, description="结束时间")
created_at: str = Field(..., description="创建时间")
# 新增统计字段
accuracy: Optional[float] = Field(None, description="正确率(%)")
correct_count: Optional[int] = Field(None, description="正确题数")
wrong_count: Optional[int] = Field(None, description="错题数")
duration_seconds: Optional[int] = Field(None, description="考试用时(秒)")
course_name: Optional[str] = Field(None, description="课程名称")
question_type_stats: Optional[List[Dict[str, Any]]] = Field(None, description="分题型统计")
class ExamRecordResponse(BaseModel):
"""考试记录列表响应"""
items: List[ExamRecordInfo] = Field(..., description="考试记录列表")
total: int = Field(..., description="总数")
page: int = Field(..., description="当前页")
size: int = Field(..., description="每页数量")
pages: int = Field(..., description="总页数")
# ==================== AI服务响应Schema ====================
class MistakeRecord(BaseModel):
"""错题记录详情"""
question_id: Optional[int] = Field(None, description="题目ID")
knowledge_point_id: Optional[int] = Field(None, description="知识点ID")
question_content: str = Field(..., description="题目内容")
correct_answer: str = Field(..., description="正确答案")
user_answer: str = Field(..., description="用户答案")
class GenerateExamRequest(BaseModel):
"""生成考试试题请求"""
course_id: int = Field(..., description="课程ID")
position_id: Optional[int] = Field(None, description="岗位ID,如果不提供则从用户信息中自动获取")
current_round: int = Field(1, ge=1, le=3, description="当前轮次(1/2/3)")
exam_id: Optional[int] = Field(None, description="已存在的exam_id(第2、3轮传入)")
mistake_records: Optional[str] = Field(None, description="错题记录JSON字符串,第一轮不传此参数,第二三轮传入上一轮错题的JSON字符串")
single_choice_count: int = Field(4, ge=0, le=50, description="单选题数量")
multiple_choice_count: int = Field(2, ge=0, le=30, description="多选题数量")
true_false_count: int = Field(1, ge=0, le=20, description="判断题数量")
fill_blank_count: int = Field(2, ge=0, le=10, description="填空题数量")
essay_count: int = Field(1, ge=0, le=10, description="问答题数量")
difficulty_level: int = Field(3, ge=1, le=5, description="难度系数(1-5)")
class GenerateExamResponse(BaseModel):
"""生成考试试题响应"""
result: str = Field(..., description="试题JSON数组(字符串格式)")
workflow_run_id: Optional[str] = Field(None, description="AI服务调用ID")
task_id: Optional[str] = Field(None, description="任务ID")
exam_id: int = Field(..., description="考试ID真实的数据库ID")
class JudgeAnswerRequest(BaseModel):
"""判断主观题答案请求"""
question: str = Field(..., description="题目内容")
correct_answer: str = Field(..., description="标准答案")
user_answer: str = Field(..., description="用户提交的答案")
analysis: str = Field(..., description="正确答案的解析(来源于试题生成器)")
class JudgeAnswerResponse(BaseModel):
"""判断主观题答案响应"""
is_correct: bool = Field(..., description="是否正确")
correct_answer: str = Field(..., description="标准答案")
feedback: Optional[str] = Field(None, description="判断反馈信息")
class RecordMistakeRequest(BaseModel):
"""记录错题请求"""
exam_id: int = Field(..., description="考试ID")
question_id: Optional[int] = Field(None, description="题目ID(AI生成的题目可能为空)")
knowledge_point_id: Optional[int] = Field(None, description="知识点ID")
question_content: str = Field(..., description="题目内容")
correct_answer: str = Field(..., description="正确答案")
user_answer: str = Field(..., description="用户答案")
question_type: Optional[str] = Field(None, description="题型(single/multiple/judge/blank/essay)")
class RecordMistakeResponse(BaseModel):
"""记录错题响应"""
id: int = Field(..., description="错题记录ID")
created_at: datetime = Field(..., description="创建时间")
class MistakeRecordItem(BaseModel):
"""错题记录项"""
id: int = Field(..., description="错题记录ID")
question_id: Optional[int] = Field(None, description="题目ID")
knowledge_point_id: Optional[int] = Field(None, description="知识点ID")
question_content: str = Field(..., description="题目内容")
correct_answer: str = Field(..., description="正确答案")
user_answer: str = Field(..., description="用户答案")
created_at: datetime = Field(..., description="创建时间")
class GetMistakesResponse(BaseModel):
"""获取错题记录响应"""
mistakes: List[MistakeRecordItem] = Field(..., description="错题列表")
# ==================== 成绩报告和错题本相关Schema ====================
class RoundScores(BaseModel):
"""三轮得分"""
round1: Optional[float] = Field(None, description="第一轮得分")
round2: Optional[float] = Field(None, description="第二轮得分")
round3: Optional[float] = Field(None, description="第三轮得分")
class ExamReportOverview(BaseModel):
"""成绩报告概览"""
avg_score: float = Field(..., description="平均成绩(基于round1_score)")
total_exams: int = Field(..., description="考试总数")
pass_rate: float = Field(..., description="及格率")
total_questions: int = Field(..., description="答题总数")
class ExamTrendItem(BaseModel):
"""成绩趋势项"""
date: str = Field(..., description="日期(YYYY-MM-DD)")
avg_score: float = Field(..., description="平均分")
class SubjectStatItem(BaseModel):
"""科目统计项"""
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
avg_score: float = Field(..., description="平均分")
exam_count: int = Field(..., description="考试次数")
max_score: float = Field(..., description="最高分")
min_score: float = Field(..., description="最低分")
pass_rate: float = Field(..., description="及格率")
class RecentExamItem(BaseModel):
"""最近考试记录项"""
id: int = Field(..., description="考试ID")
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
score: Optional[float] = Field(None, description="最终得分")
total_score: float = Field(..., description="总分")
is_passed: Optional[bool] = Field(None, description="是否通过")
duration_seconds: Optional[int] = Field(None, description="考试用时(秒)")
start_time: str = Field(..., description="开始时间")
end_time: Optional[str] = Field(None, description="结束时间")
round_scores: RoundScores = Field(..., description="三轮得分")
class ExamReportResponse(BaseModel):
"""成绩报告响应"""
overview: ExamReportOverview = Field(..., description="概览数据")
trends: List[ExamTrendItem] = Field(..., description="趋势数据")
subjects: List[SubjectStatItem] = Field(..., description="科目分析")
recent_exams: List[RecentExamItem] = Field(..., description="最近考试记录")
class MistakeListItem(BaseModel):
"""错题列表项"""
id: int = Field(..., description="错题记录ID")
exam_id: int = Field(..., description="考试ID")
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
question_content: str = Field(..., description="题目内容")
correct_answer: str = Field(..., description="正确答案")
user_answer: str = Field(..., description="用户答案")
question_type: Optional[str] = Field(None, description="题型")
knowledge_point_id: Optional[int] = Field(None, description="知识点ID")
knowledge_point_name: Optional[str] = Field(None, description="知识点名称")
created_at: datetime = Field(..., description="创建时间")
class MistakeListResponse(BaseModel):
"""错题列表响应"""
items: List[MistakeListItem] = Field(..., description="错题列表")
total: int = Field(..., description="总数")
page: int = Field(..., description="当前页")
size: int = Field(..., description="每页数量")
pages: int = Field(..., description="总页数")
class MistakeByCourse(BaseModel):
"""按课程统计错题"""
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
count: int = Field(..., description="错题数量")
class MistakeByType(BaseModel):
"""按题型统计错题"""
type: str = Field(..., description="题型代码")
type_name: str = Field(..., description="题型名称")
count: int = Field(..., description="错题数量")
class MistakeByTime(BaseModel):
"""按时间统计错题"""
week: int = Field(..., description="最近一周")
month: int = Field(..., description="最近一月")
quarter: int = Field(..., description="最近三月")
class MistakesStatisticsResponse(BaseModel):
"""错题统计响应"""
total: int = Field(..., description="错题总数")
by_course: List[MistakeByCourse] = Field(..., description="按课程统计")
by_type: List[MistakeByType] = Field(..., description="按题型统计")
by_time: MistakeByTime = Field(..., description="按时间统计")
class UpdateRoundScoreRequest(BaseModel):
"""更新轮次得分请求"""
round: int = Field(..., ge=1, le=3, description="轮次(1/2/3)")
score: float = Field(..., ge=0, le=100, description="得分")
is_final: bool = Field(False, description="是否为最终轮次(如果是,则同时更新总分和状态)")

View File

@@ -0,0 +1,102 @@
"""
站内消息通知相关的数据验证模型
"""
from typing import Optional, List
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict
class NotificationType(str, Enum):
"""通知类型枚举"""
POSITION_ASSIGN = "position_assign" # 岗位分配
COURSE_ASSIGN = "course_assign" # 课程分配
EXAM_REMIND = "exam_remind" # 考试提醒
TASK_ASSIGN = "task_assign" # 任务分配
SYSTEM = "system" # 系统通知
class NotificationBase(BaseModel):
"""
通知基础模型
"""
title: str = Field(..., min_length=1, max_length=200, description="通知标题")
content: Optional[str] = Field(None, description="通知内容")
type: NotificationType = Field(default=NotificationType.SYSTEM, description="通知类型")
related_id: Optional[int] = Field(None, description="关联数据ID")
related_type: Optional[str] = Field(None, max_length=50, description="关联数据类型")
class NotificationCreate(NotificationBase):
"""
创建通知模型
"""
user_id: int = Field(..., description="接收用户ID")
sender_id: Optional[int] = Field(None, description="发送者用户ID")
class NotificationBatchCreate(BaseModel):
"""
批量创建通知模型(发送给多个用户)
"""
user_ids: List[int] = Field(..., min_length=1, description="接收用户ID列表")
title: str = Field(..., min_length=1, max_length=200, description="通知标题")
content: Optional[str] = Field(None, description="通知内容")
type: NotificationType = Field(default=NotificationType.SYSTEM, description="通知类型")
related_id: Optional[int] = Field(None, description="关联数据ID")
related_type: Optional[str] = Field(None, max_length=50, description="关联数据类型")
sender_id: Optional[int] = Field(None, description="发送者用户ID")
class NotificationUpdate(BaseModel):
"""
更新通知模型
"""
is_read: Optional[bool] = Field(None, description="是否已读")
class NotificationInDB(NotificationBase):
"""
数据库中的通知模型
"""
model_config = ConfigDict(from_attributes=True)
id: int
user_id: int
is_read: bool
sender_id: Optional[int] = None
created_at: datetime
updated_at: datetime
class NotificationResponse(NotificationInDB):
"""
通知响应模型(可扩展发送者信息)
"""
sender_name: Optional[str] = Field(None, description="发送者姓名")
class NotificationListResponse(BaseModel):
"""
通知列表响应模型
"""
items: List[NotificationResponse]
total: int
unread_count: int
class NotificationCountResponse(BaseModel):
"""
未读通知数量响应模型
"""
unread_count: int
total: int
class MarkReadRequest(BaseModel):
"""
标记已读请求模型
"""
notification_ids: Optional[List[int]] = Field(None, description="通知ID列表为空则标记全部已读")

View File

@@ -0,0 +1,318 @@
"""
陪练功能相关Schema定义
"""
from typing import Optional, List
from datetime import datetime
from pydantic import BaseModel, Field, field_validator
# ==================== 枚举类型 ====================
class SceneType:
"""场景类型枚举"""
PHONE = "phone" # 电话销售
FACE = "face" # 面对面销售
COMPLAINT = "complaint" # 客户投诉
AFTER_SALES = "after-sales" # 售后服务
PRODUCT_INTRO = "product-intro" # 产品介绍
class Difficulty:
"""难度等级枚举"""
BEGINNER = "beginner" # 入门
JUNIOR = "junior" # 初级
INTERMEDIATE = "intermediate" # 中级
SENIOR = "senior" # 高级
EXPERT = "expert" # 专家
class SceneStatus:
"""场景状态枚举"""
ACTIVE = "active" # 启用
INACTIVE = "inactive" # 禁用
# ==================== 场景Schema ====================
class PracticeSceneBase(BaseModel):
"""陪练场景基础Schema"""
name: str = Field(..., max_length=200, description="场景名称")
description: Optional[str] = Field(None, description="场景描述")
type: str = Field(..., description="场景类型: phone/face/complaint/after-sales/product-intro")
difficulty: str = Field(..., description="难度等级: beginner/junior/intermediate/senior/expert")
status: str = Field(default="active", description="状态: active/inactive")
background: str = Field(..., description="场景背景设定")
ai_role: str = Field(..., description="AI角色描述")
objectives: List[str] = Field(..., description="练习目标数组")
keywords: Optional[List[str]] = Field(default=None, description="关键词数组")
duration: int = Field(default=10, ge=1, le=120, description="预计时长(分钟)")
@field_validator('type')
@classmethod
def validate_type(cls, v):
"""验证场景类型"""
valid_types = ['phone', 'face', 'complaint', 'after-sales', 'product-intro']
if v not in valid_types:
raise ValueError(f"场景类型必须是: {', '.join(valid_types)}")
return v
@field_validator('difficulty')
@classmethod
def validate_difficulty(cls, v):
"""验证难度等级"""
valid_difficulties = ['beginner', 'junior', 'intermediate', 'senior', 'expert']
if v not in valid_difficulties:
raise ValueError(f"难度等级必须是: {', '.join(valid_difficulties)}")
return v
@field_validator('status')
@classmethod
def validate_status(cls, v):
"""验证状态"""
valid_statuses = ['active', 'inactive']
if v not in valid_statuses:
raise ValueError(f"状态必须是: {', '.join(valid_statuses)}")
return v
@field_validator('objectives')
@classmethod
def validate_objectives(cls, v):
"""验证练习目标"""
if not v or len(v) < 1:
raise ValueError("至少需要1个练习目标")
if len(v) > 10:
raise ValueError("练习目标不能超过10个")
return v
class PracticeSceneCreate(PracticeSceneBase):
"""创建陪练场景Schema"""
pass
class PracticeSceneUpdate(BaseModel):
"""更新陪练场景Schema所有字段可选"""
name: Optional[str] = Field(None, max_length=200, description="场景名称")
description: Optional[str] = Field(None, description="场景描述")
type: Optional[str] = Field(None, description="场景类型")
difficulty: Optional[str] = Field(None, description="难度等级")
status: Optional[str] = Field(None, description="状态")
background: Optional[str] = Field(None, description="场景背景设定")
ai_role: Optional[str] = Field(None, description="AI角色描述")
objectives: Optional[List[str]] = Field(None, description="练习目标数组")
keywords: Optional[List[str]] = Field(None, description="关键词数组")
duration: Optional[int] = Field(None, ge=1, le=120, description="预计时长(分钟)")
class PracticeSceneResponse(PracticeSceneBase):
"""陪练场景响应Schema"""
id: int
usage_count: int
rating: float
created_by: Optional[int] = None
updated_by: Optional[int] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# ==================== 对话Schema ====================
class StartPracticeRequest(BaseModel):
"""开始陪练对话请求Schema"""
# 场景信息(首次消息必填,后续消息可选)
scene_id: Optional[int] = Field(None, description="场景ID可选")
scene_name: Optional[str] = Field(None, description="场景名称")
scene_description: Optional[str] = Field(None, description="场景描述")
scene_background: Optional[str] = Field(None, description="场景背景")
scene_ai_role: Optional[str] = Field(None, description="AI角色")
scene_objectives: Optional[List[str]] = Field(None, description="练习目标")
scene_keywords: Optional[List[str]] = Field(None, description="关键词")
# 对话信息
user_message: str = Field(..., description="用户消息")
conversation_id: Optional[str] = Field(None, description="对话ID续接对话时必填")
is_first: bool = Field(..., description="是否首次消息")
@field_validator('scene_name')
@classmethod
def validate_scene_name_for_first(cls, v, info):
"""首次消息时场景名称必填"""
if info.data.get('is_first') and not v:
raise ValueError("首次消息时场景名称必填")
return v
@field_validator('scene_background')
@classmethod
def validate_scene_background_for_first(cls, v, info):
"""首次消息时场景背景必填"""
if info.data.get('is_first') and not v:
raise ValueError("首次消息时场景背景必填")
return v
@field_validator('scene_ai_role')
@classmethod
def validate_scene_ai_role_for_first(cls, v, info):
"""首次消息时AI角色必填"""
if info.data.get('is_first') and not v:
raise ValueError("首次消息时AI角色必填")
return v
@field_validator('scene_objectives')
@classmethod
def validate_scene_objectives_for_first(cls, v, info):
"""首次消息时练习目标必填"""
if info.data.get('is_first') and (not v or len(v) == 0):
raise ValueError("首次消息时练习目标必填")
return v
class InterruptPracticeRequest(BaseModel):
"""中断对话请求Schema"""
conversation_id: str = Field(..., description="对话ID")
chat_id: str = Field(..., description="聊天ID")
class ConversationInfo(BaseModel):
"""对话信息Schema"""
id: str = Field(..., description="对话ID")
name: str = Field(..., description="对话名称")
created_at: int = Field(..., description="创建时间(时间戳)")
class ConversationsResponse(BaseModel):
"""对话列表响应Schema"""
items: List[ConversationInfo]
has_more: bool
page: int
size: int
# ==================== 场景提取Schema ====================
class ExtractSceneRequest(BaseModel):
"""提取场景请求Schema"""
course_id: int = Field(..., description="课程ID")
class ExtractedSceneData(BaseModel):
"""提取的场景数据Schema"""
name: str = Field(..., description="场景名称")
description: str = Field(..., description="场景描述")
type: str = Field(..., description="场景类型")
difficulty: str = Field(..., description="难度等级")
background: str = Field(..., description="场景背景")
ai_role: str = Field(..., description="AI角色描述")
objectives: List[str] = Field(..., description="练习目标数组")
keywords: Optional[List[str]] = Field(default=[], description="关键词数组")
class ExtractSceneResponse(BaseModel):
"""提取场景响应Schema"""
scene: ExtractedSceneData = Field(..., description="场景数据")
workflow_run_id: str = Field(..., description="工作流运行ID")
task_id: str = Field(..., description="任务ID")
# ==================== 陪练会话Schema ====================
class PracticeSessionCreate(BaseModel):
"""创建陪练会话请求Schema"""
scene_id: Optional[int] = Field(None, description="场景ID")
scene_name: str = Field(..., description="场景名称")
scene_type: Optional[str] = Field(None, description="场景类型")
conversation_id: Optional[str] = Field(None, description="Coze对话ID")
class PracticeSessionResponse(BaseModel):
"""陪练会话响应Schema"""
id: int
session_id: str
user_id: int
scene_id: Optional[int]
scene_name: str
scene_type: Optional[str]
conversation_id: Optional[str]
start_time: datetime
end_time: Optional[datetime]
duration_seconds: int
turns: int
status: str
created_at: datetime
class Config:
from_attributes = True
class SaveDialogueRequest(BaseModel):
"""保存对话记录请求Schema"""
session_id: str = Field(..., description="会话ID")
speaker: str = Field(..., description="说话人: user/ai")
content: str = Field(..., description="对话内容")
sequence: int = Field(..., ge=1, description="顺序号从1开始")
class PracticeDialogueResponse(BaseModel):
"""对话记录响应Schema"""
id: int
session_id: str
speaker: str
content: str
timestamp: datetime
sequence: int
class Config:
from_attributes = True
# ==================== 分析报告Schema ====================
class ScoreBreakdownItem(BaseModel):
"""分数细分项"""
name: str
score: int = Field(..., ge=0, le=100)
description: str
class AbilityDimensionItem(BaseModel):
"""能力维度项"""
name: str
score: int = Field(..., ge=0, le=100)
feedback: str
class DialogueReviewItem(BaseModel):
"""对话复盘项"""
speaker: str
time: str
content: str
tags: List[str] = Field(default_factory=list)
comment: str = Field(default="")
class SuggestionItem(BaseModel):
"""改进建议项"""
title: str
content: str
example: Optional[str] = None
class PracticeAnalysisResult(BaseModel):
"""陪练分析结果Schema"""
total_score: int = Field(..., ge=0, le=100, description="综合得分")
score_breakdown: List[ScoreBreakdownItem] = Field(..., description="分数细分")
ability_dimensions: List[AbilityDimensionItem] = Field(..., description="能力维度")
dialogue_review: List[DialogueReviewItem] = Field(..., description="对话复盘")
suggestions: List[SuggestionItem] = Field(..., description="改进建议")
class PracticeReportResponse(BaseModel):
"""陪练报告响应Schema"""
session_info: PracticeSessionResponse
analysis: PracticeAnalysisResult
class Config:
from_attributes = True

128
backend/app/schemas/scrm.py Normal file
View File

@@ -0,0 +1,128 @@
"""
SCRM 系统对接 API Schema 定义
用于 SCRM 系统调用考陪练系统的数据查询接口
"""
from typing import List, Optional
from pydantic import BaseModel, Field
from datetime import datetime
# ==================== 通用响应 ====================
class SCRMBaseResponse(BaseModel):
"""SCRM API 通用响应基类"""
code: int = Field(default=0, description="响应码0=成功")
message: str = Field(default="success", description="响应消息")
# ==================== 1. 获取员工岗位 ====================
class PositionInfo(BaseModel):
"""岗位信息"""
position_id: int = Field(..., description="岗位ID")
position_name: str = Field(..., description="岗位名称")
is_primary: bool = Field(default=True, description="是否主岗位")
joined_at: Optional[str] = Field(None, description="加入时间")
class EmployeePositionData(BaseModel):
"""员工岗位数据"""
employee_id: int = Field(..., description="员工ID")
userid: Optional[str] = Field(None, description="企微员工userid可能为空")
name: str = Field(..., description="员工姓名")
positions: List[PositionInfo] = Field(default=[], description="岗位列表")
class EmployeePositionResponse(SCRMBaseResponse):
"""获取员工岗位响应"""
data: Optional[EmployeePositionData] = None
# ==================== 2. 获取岗位课程 ====================
class CourseInfo(BaseModel):
"""课程信息"""
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
course_type: str = Field(..., description="课程类型required/optional")
priority: int = Field(default=0, description="优先级")
knowledge_point_count: int = Field(default=0, description="知识点数量")
class PositionCoursesData(BaseModel):
"""岗位课程数据"""
position_id: int = Field(..., description="岗位ID")
position_name: str = Field(..., description="岗位名称")
courses: List[CourseInfo] = Field(default=[], description="课程列表")
class PositionCoursesResponse(SCRMBaseResponse):
"""获取岗位课程响应"""
data: Optional[PositionCoursesData] = None
# ==================== 3. 搜索知识点 ====================
class KnowledgePointSearchRequest(BaseModel):
"""搜索知识点请求"""
keywords: List[str] = Field(..., min_length=1, description="搜索关键词列表")
position_id: Optional[int] = Field(None, description="岗位ID用于优先排序")
course_ids: Optional[List[int]] = Field(None, description="限定课程范围")
knowledge_type: Optional[str] = Field(None, description="知识点类型筛选")
limit: int = Field(default=10, ge=1, le=100, description="返回数量")
class KnowledgePointBrief(BaseModel):
"""知识点简要信息"""
knowledge_point_id: int = Field(..., description="知识点ID")
name: str = Field(..., description="知识点名称")
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
type: str = Field(..., description="知识点类型")
relevance_score: float = Field(default=1.0, description="相关度分数")
class KnowledgePointSearchData(BaseModel):
"""知识点搜索结果数据"""
total: int = Field(..., description="匹配总数")
items: List[KnowledgePointBrief] = Field(default=[], description="知识点列表")
class KnowledgePointSearchResponse(SCRMBaseResponse):
"""搜索知识点响应"""
data: Optional[KnowledgePointSearchData] = None
# ==================== 4. 获取知识点详情 ====================
class KnowledgePointDetailData(BaseModel):
"""知识点详情数据"""
knowledge_point_id: int = Field(..., description="知识点ID")
name: str = Field(..., description="知识点名称")
course_id: int = Field(..., description="课程ID")
course_name: str = Field(..., description="课程名称")
type: str = Field(..., description="知识点类型")
content: str = Field(..., description="知识点完整内容description")
material_id: Optional[int] = Field(None, description="关联的课程资料ID")
material_type: Optional[str] = Field(None, description="资料文件类型")
material_url: Optional[str] = Field(None, description="资料文件URL")
topic_relation: Optional[str] = Field(None, description="与主题的关系描述")
source: int = Field(default=0, description="来源0=手动创建1=AI分析生成")
created_at: Optional[str] = Field(None, description="创建时间")
class KnowledgePointDetailResponse(SCRMBaseResponse):
"""获取知识点详情响应"""
data: Optional[KnowledgePointDetailData] = None
# ==================== 错误响应 ====================
class SCRMErrorResponse(SCRMBaseResponse):
"""错误响应"""
code: int = Field(..., description="错误码")
message: str = Field(..., description="错误消息")
data: None = None

View File

@@ -0,0 +1,59 @@
"""
系统日志 Schema
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class SystemLogBase(BaseModel):
"""系统日志基础Schema"""
level: str = Field(..., description="日志级别: debug, info, warning, error")
type: str = Field(..., description="日志类型: system, user, api, error, security")
user: Optional[str] = Field(None, description="操作用户")
user_id: Optional[int] = Field(None, description="用户ID")
ip: Optional[str] = Field(None, description="IP地址")
message: str = Field(..., description="日志消息")
user_agent: Optional[str] = Field(None, description="User Agent")
path: Optional[str] = Field(None, description="请求路径")
method: Optional[str] = Field(None, description="请求方法")
extra_data: Optional[str] = Field(None, description="额外数据JSON格式")
class SystemLogCreate(SystemLogBase):
"""创建系统日志Schema"""
pass
class SystemLogResponse(SystemLogBase):
"""系统日志响应Schema"""
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class SystemLogQuery(BaseModel):
"""系统日志查询参数"""
level: Optional[str] = Field(None, description="日志级别筛选")
type: Optional[str] = Field(None, description="日志类型筛选")
user: Optional[str] = Field(None, description="用户筛选")
keyword: Optional[str] = Field(None, description="关键词搜索搜索message字段")
start_date: Optional[datetime] = Field(None, description="开始日期")
end_date: Optional[datetime] = Field(None, description="结束日期")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量")
class SystemLogListResponse(BaseModel):
"""系统日志列表响应"""
items: list[SystemLogResponse]
total: int
page: int
page_size: int
total_pages: int

View File

@@ -0,0 +1,67 @@
"""
任务相关Schema
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
class TaskBase(BaseModel):
"""任务基础Schema"""
title: str = Field(..., description="任务标题")
description: Optional[str] = Field(None, description="任务描述")
priority: str = Field("medium", description="优先级(low/medium/high)")
deadline: Optional[datetime] = Field(None, description="截止时间")
requirements: Optional[dict] = Field(None, description="任务要求配置")
course_ids: List[int] = Field(default_factory=list, description="关联课程ID列表")
user_ids: List[int] = Field(default_factory=list, description="分配用户ID列表")
class TaskCreate(TaskBase):
"""创建任务"""
pass
class TaskUpdate(BaseModel):
"""更新任务"""
title: Optional[str] = None
description: Optional[str] = None
priority: Optional[str] = None
status: Optional[str] = None
deadline: Optional[datetime] = None
requirements: Optional[dict] = None
progress: Optional[int] = None
class TaskResponse(BaseModel):
"""任务响应"""
id: int
title: str
description: Optional[str]
priority: str
status: str
creator_id: int
deadline: Optional[datetime]
requirements: Optional[dict]
progress: int
created_at: datetime
updated_at: datetime
# 扩展字段
courses: List[str] = Field(default_factory=list, description="课程名称列表")
assigned_count: int = Field(0, description="分配人数")
completed_count: int = Field(0, description="完成人数")
class Config:
from_attributes = True
class TaskStatsResponse(BaseModel):
"""任务统计响应"""
total: int = Field(0, description="总任务数")
ongoing: int = Field(0, description="进行中")
completed: int = Field(0, description="已完成")
expired: int = Field(0, description="已过期")
avg_completion_rate: float = Field(0.0, description="平均完成率")

View File

@@ -0,0 +1,260 @@
"""陪练模块Pydantic模式"""
from typing import Optional, List, Dict, Any, Generic, TypeVar
from datetime import datetime
from pydantic import BaseModel, Field, ConfigDict
# 定义泛型类型变量
DataT = TypeVar("DataT")
from app.models.training import (
TrainingSceneStatus,
TrainingSessionStatus,
MessageType,
MessageRole,
)
from app.schemas.base import BaseSchema, TimestampMixin, IDMixin
# ========== 陪练场景相关 ==========
class TrainingSceneBase(BaseSchema):
"""陪练场景基础模式"""
name: str = Field(..., max_length=100, description="场景名称")
description: Optional[str] = Field(None, description="场景描述")
category: str = Field(..., max_length=50, description="场景分类")
ai_config: Optional[Dict[str, Any]] = Field(None, description="AI配置")
prompt_template: Optional[str] = Field(None, description="提示词模板")
evaluation_criteria: Optional[Dict[str, Any]] = Field(None, description="评估标准")
is_public: bool = Field(True, description="是否公开")
required_level: Optional[int] = Field(None, description="所需用户等级")
class TrainingSceneCreate(TrainingSceneBase):
"""创建陪练场景模式"""
status: TrainingSceneStatus = Field(
default=TrainingSceneStatus.DRAFT, description="场景状态"
)
class TrainingSceneUpdate(BaseSchema):
"""更新陪练场景模式"""
name: Optional[str] = Field(None, max_length=100)
description: Optional[str] = None
category: Optional[str] = Field(None, max_length=50)
ai_config: Optional[Dict[str, Any]] = None
prompt_template: Optional[str] = None
evaluation_criteria: Optional[Dict[str, Any]] = None
status: Optional[TrainingSceneStatus] = None
is_public: Optional[bool] = None
required_level: Optional[int] = None
class TrainingSceneInDB(TrainingSceneBase, IDMixin, TimestampMixin):
"""数据库中的陪练场景模式"""
status: TrainingSceneStatus
is_deleted: bool = False
created_by: Optional[int] = None
updated_by: Optional[int] = None
class TrainingSceneResponse(TrainingSceneInDB):
"""陪练场景响应模式"""
pass
# ========== 陪练会话相关 ==========
class TrainingSessionBase(BaseSchema):
"""陪练会话基础模式"""
scene_id: int = Field(..., description="场景ID")
session_config: Optional[Dict[str, Any]] = Field(None, description="会话配置")
class TrainingSessionCreate(TrainingSessionBase):
"""创建陪练会话模式"""
pass
class TrainingSessionUpdate(BaseSchema):
"""更新陪练会话模式"""
status: Optional[TrainingSessionStatus] = None
end_time: Optional[datetime] = None
duration_seconds: Optional[int] = None
total_score: Optional[float] = None
evaluation_result: Optional[Dict[str, Any]] = None
class TrainingSessionInDB(TrainingSessionBase, IDMixin, TimestampMixin):
"""数据库中的陪练会话模式"""
user_id: int
coze_conversation_id: Optional[str] = None
start_time: datetime
end_time: Optional[datetime] = None
duration_seconds: Optional[int] = None
status: TrainingSessionStatus
total_score: Optional[float] = None
evaluation_result: Optional[Dict[str, Any]] = None
created_by: Optional[int] = None
updated_by: Optional[int] = None
class TrainingSessionResponse(TrainingSessionInDB):
"""陪练会话响应模式"""
scene: Optional["TrainingSceneResponse"] = None
message_count: Optional[int] = Field(None, description="消息数量")
# ========== 消息相关 ==========
class TrainingMessageBase(BaseSchema):
"""陪练消息基础模式"""
role: MessageRole = Field(..., description="消息角色")
type: MessageType = Field(..., description="消息类型")
content: str = Field(..., description="消息内容")
voice_url: Optional[str] = Field(None, max_length=500, description="语音文件URL")
voice_duration: Optional[float] = Field(None, description="语音时长(秒)")
metadata: Optional[Dict[str, Any]] = Field(None, description="消息元数据")
class TrainingMessageCreate(TrainingMessageBase):
"""创建陪练消息模式"""
session_id: int = Field(..., description="会话ID")
coze_message_id: Optional[str] = Field(None, max_length=100, description="Coze消息ID")
class TrainingMessageInDB(TrainingMessageBase, IDMixin, TimestampMixin):
"""数据库中的陪练消息模式"""
session_id: int
coze_message_id: Optional[str] = None
class TrainingMessageResponse(TrainingMessageInDB):
"""陪练消息响应模式"""
pass
# ========== 报告相关 ==========
class TrainingReportBase(BaseSchema):
"""陪练报告基础模式"""
overall_score: float = Field(..., ge=0, le=100, description="总体得分")
dimension_scores: Dict[str, float] = Field(..., description="各维度得分")
strengths: List[str] = Field(..., description="优势点")
weaknesses: List[str] = Field(..., description="待改进点")
suggestions: List[str] = Field(..., description="改进建议")
detailed_analysis: Optional[str] = Field(None, description="详细分析")
transcript: Optional[str] = Field(None, description="对话文本记录")
statistics: Optional[Dict[str, Any]] = Field(None, description="统计数据")
class TrainingReportCreate(TrainingReportBase):
"""创建陪练报告模式"""
session_id: int = Field(..., description="会话ID")
user_id: int = Field(..., description="用户ID")
class TrainingReportInDB(TrainingReportBase, IDMixin, TimestampMixin):
"""数据库中的陪练报告模式"""
session_id: int
user_id: int
created_by: Optional[int] = None
updated_by: Optional[int] = None
class TrainingReportResponse(TrainingReportInDB):
"""陪练报告响应模式"""
session: Optional[TrainingSessionResponse] = None
# ========== 会话操作相关 ==========
class StartTrainingRequest(BaseSchema):
"""开始陪练请求"""
scene_id: int = Field(..., description="场景ID")
config: Optional[Dict[str, Any]] = Field(None, description="会话配置")
class StartTrainingResponse(BaseSchema):
"""开始陪练响应"""
session_id: int = Field(..., description="会话ID")
coze_conversation_id: Optional[str] = Field(None, description="Coze会话ID")
scene: TrainingSceneResponse = Field(..., description="场景信息")
websocket_url: Optional[str] = Field(None, description="WebSocket连接URL")
class EndTrainingRequest(BaseSchema):
"""结束陪练请求"""
generate_report: bool = Field(True, description="是否生成报告")
class EndTrainingResponse(BaseSchema):
"""结束陪练响应"""
session: TrainingSessionResponse = Field(..., description="会话信息")
report: Optional[TrainingReportResponse] = Field(None, description="陪练报告")
# ========== 列表查询相关 ==========
class TrainingSceneListQuery(BaseSchema):
"""陪练场景列表查询参数"""
category: Optional[str] = Field(None, description="场景分类")
status: Optional[TrainingSceneStatus] = Field(None, description="场景状态")
is_public: Optional[bool] = Field(None, description="是否公开")
search: Optional[str] = Field(None, description="搜索关键词")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量")
class TrainingSessionListQuery(BaseSchema):
"""陪练会话列表查询参数"""
scene_id: Optional[int] = Field(None, description="场景ID")
status: Optional[TrainingSessionStatus] = Field(None, description="会话状态")
start_date: Optional[datetime] = Field(None, description="开始日期")
end_date: Optional[datetime] = Field(None, description="结束日期")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量")
class PaginatedResponse(BaseModel, Generic[DataT]):
"""分页响应模式"""
items: List[DataT] = Field(..., description="数据列表")
total: int = Field(..., description="总数量")
page: int = Field(..., description="当前页码")
page_size: int = Field(..., description="每页数量")
pages: int = Field(..., description="总页数")
# 更新前向引用
TrainingSessionResponse.model_rebuild()
TrainingReportResponse.model_rebuild()

154
backend/app/schemas/user.py Normal file
View File

@@ -0,0 +1,154 @@
"""
用户相关 Schema
"""
from datetime import datetime
from typing import List, Optional
from pydantic import EmailStr, Field, field_validator
from .base import BaseSchema
class UserBase(BaseSchema):
"""用户基础信息"""
username: str = Field(..., min_length=3, max_length=50)
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, pattern=r"^1[3-9]\d{9}$")
full_name: Optional[str] = Field(None, max_length=100)
avatar_url: Optional[str] = None
bio: Optional[str] = None
role: str = Field(default="trainee", pattern="^(admin|manager|trainee)$")
gender: Optional[str] = Field(None, pattern="^(male|female)$")
school: Optional[str] = Field(None, max_length=100)
major: Optional[str] = Field(None, max_length=100)
class UserCreate(UserBase):
"""创建用户"""
password: str = Field(..., min_length=6, max_length=100)
@field_validator("password")
def validate_password(cls, v):
if len(v) < 6:
raise ValueError("密码长度至少为6位")
return v
class UserUpdate(BaseSchema):
"""更新用户"""
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, pattern=r"^1[3-9]\d{9}$")
full_name: Optional[str] = Field(None, max_length=100)
avatar_url: Optional[str] = None
bio: Optional[str] = None
role: Optional[str] = Field(None, pattern="^(admin|manager|trainee)$")
is_active: Optional[bool] = None
gender: Optional[str] = Field(None, pattern="^(male|female)$")
school: Optional[str] = Field(None, max_length=100)
major: Optional[str] = Field(None, max_length=100)
class UserPasswordUpdate(BaseSchema):
"""更新密码"""
old_password: str
new_password: str = Field(..., min_length=6, max_length=100)
class UserInDBBase(UserBase):
"""数据库中的用户基础信息"""
id: int
is_active: bool
is_verified: bool
created_at: datetime
updated_at: datetime
last_login_at: Optional[datetime] = None
class User(UserInDBBase):
"""用户信息(不含敏感数据)"""
teams: List["TeamBasic"] = []
class UserWithPassword(UserInDBBase):
"""用户信息(含密码)"""
hashed_password: str
# Team Schemas
class TeamBase(BaseSchema):
"""团队基础信息"""
name: str = Field(..., min_length=2, max_length=100)
code: str = Field(..., min_length=2, max_length=50)
description: Optional[str] = None
team_type: str = Field(
default="department", pattern="^(department|project|study_group)$"
)
class TeamCreate(TeamBase):
"""创建团队"""
leader_id: Optional[int] = None
parent_id: Optional[int] = None
class TeamUpdate(BaseSchema):
"""更新团队"""
name: Optional[str] = Field(None, min_length=2, max_length=100)
description: Optional[str] = None
leader_id: Optional[int] = None
is_active: Optional[bool] = None
class TeamBasic(BaseSchema):
"""团队基本信息"""
id: int
name: str
code: str
team_type: str
class Team(TeamBase):
"""团队完整信息"""
id: int
is_active: bool
leader_id: Optional[int] = None
parent_id: Optional[int] = None
created_at: datetime
updated_at: datetime
member_count: Optional[int] = 0
class TeamWithMembers(Team):
"""团队信息(含成员)"""
members: List[User] = []
leader: Optional[User] = None
# 避免循环引用
UserBase.model_rebuild()
User.model_rebuild()
Team.model_rebuild()
# Filter schemas
class UserFilter(BaseSchema):
"""用户筛选条件"""
role: Optional[str] = Field(None, pattern="^(admin|manager|trainee)$")
is_active: Optional[bool] = None
team_id: Optional[int] = None
keyword: Optional[str] = None # 搜索用户名、邮箱、姓名

View File

@@ -0,0 +1,61 @@
"""
言迹智能工牌相关Schema定义
"""
from typing import List, Optional
from pydantic import BaseModel, Field
class ConversationMessage(BaseModel):
"""单条对话消息"""
role: str = Field(..., description="角色consultant=销售人员customer=客户")
text: str = Field(..., description="对话文本内容")
begin_time: Optional[str] = Field(None, description="开始时间偏移量(毫秒)")
end_time: Optional[str] = Field(None, description="结束时间偏移量(毫秒)")
class YanjiConversation(BaseModel):
"""完整的对话记录"""
audio_id: int = Field(..., description="录音ID")
visit_id: str = Field(..., description="来访单ID")
start_time: str = Field(..., description="录音开始时间")
duration: int = Field(..., description="录音时长(毫秒)")
consultant_name: str = Field(..., description="销售人员姓名")
consultant_phone: str = Field(..., description="销售人员手机号")
conversation: List[ConversationMessage] = Field(..., description="对话内容列表")
class GetConversationsByVisitIdsRequest(BaseModel):
"""根据来访单ID获取对话记录请求"""
external_visit_ids: List[str] = Field(
...,
min_length=1,
max_length=10,
description="三方来访单ID列表最多10个",
)
class GetConversationsByVisitIdsResponse(BaseModel):
"""获取对话记录响应"""
conversations: List[YanjiConversation] = Field(..., description="对话记录列表")
total: int = Field(..., description="总数量")
class GetConversationsRequest(BaseModel):
"""获取员工对话记录请求"""
consultant_phone: str = Field(..., description="员工手机号")
limit: int = Field(default=10, ge=1, le=100, description="获取数量")
class GetConversationsResponse(BaseModel):
"""获取员工对话记录响应"""
conversations: List[YanjiConversation] = Field(..., description="对话记录列表")
total: int = Field(..., description="总数量")