feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Pydantic模式包"""
|
||||
50
backend/app/schemas/ability.py
Normal file
50
backend/app/schemas/ability.py
Normal 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
|
||||
|
||||
35
backend/app/schemas/auth.py
Normal file
35
backend/app/schemas/auth.py
Normal 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
|
||||
73
backend/app/schemas/base.py
Normal file
73
backend/app/schemas/base.py
Normal 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
|
||||
)
|
||||
364
backend/app/schemas/course.py
Normal file
364
backend/app/schemas/course.py
Normal 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
316
backend/app/schemas/exam.py
Normal 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="是否为最终轮次(如果是,则同时更新总分和状态)")
|
||||
102
backend/app/schemas/notification.py
Normal file
102
backend/app/schemas/notification.py
Normal 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列表,为空则标记全部已读")
|
||||
|
||||
318
backend/app/schemas/practice.py
Normal file
318
backend/app/schemas/practice.py
Normal 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
128
backend/app/schemas/scrm.py
Normal 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
|
||||
|
||||
59
backend/app/schemas/system_log.py
Normal file
59
backend/app/schemas/system_log.py
Normal 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
|
||||
|
||||
|
||||
|
||||
67
backend/app/schemas/task.py
Normal file
67
backend/app/schemas/task.py
Normal 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="平均完成率")
|
||||
|
||||
|
||||
|
||||
260
backend/app/schemas/training.py
Normal file
260
backend/app/schemas/training.py
Normal 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
154
backend/app/schemas/user.py
Normal 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 # 搜索用户名、邮箱、姓名
|
||||
61
backend/app/schemas/yanji.py
Normal file
61
backend/app/schemas/yanji.py
Normal 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="总数量")
|
||||
|
||||
Reference in New Issue
Block a user