diff --git a/admin-frontend/.eslintrc.cjs b/admin-frontend/.eslintrc.cjs new file mode 100644 index 0000000..8bc624d --- /dev/null +++ b/admin-frontend/.eslintrc.cjs @@ -0,0 +1,23 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + extends: [ + 'plugin:vue/vue3-recommended', + 'eslint:recommended', + '@vue/eslint-config-typescript' + ], + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + // Vue 组件命名规范 + 'vue/multi-word-component-names': 'off', + // 允许使用 any 类型(逐步改进) + '@typescript-eslint/no-explicit-any': 'warn', + // 未使用变量警告 + '@typescript-eslint/no-unused-vars': 'warn' + } +} + diff --git a/admin-frontend/public/favicon.svg b/admin-frontend/public/favicon.svg index a89c613..6712d34 100644 --- a/admin-frontend/public/favicon.svg +++ b/admin-frontend/public/favicon.svg @@ -17,3 +17,8 @@ + + + + + diff --git a/backend/Dockerfile.admin b/backend/Dockerfile.admin index 84de7fd..7cafae6 100644 --- a/backend/Dockerfile.admin +++ b/backend/Dockerfile.admin @@ -64,3 +64,8 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload + + + + + diff --git a/backend/app/api/v1/exam.py b/backend/app/api/v1/exam.py index 5cbc1fa..55ecba3 100644 --- a/backend/app/api/v1/exam.py +++ b/backend/app/api/v1/exam.py @@ -141,6 +141,7 @@ async def submit_exam( @router.get("/mistakes", response_model=ResponseModel[GetMistakesResponse]) async def get_mistakes( exam_id: int, + round: int = Query(None, description="获取指定轮次的错题,不传则获取所有轮次"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): @@ -149,18 +150,31 @@ async def get_mistakes( 用于第二、三轮考试时获取上一轮的错题记录 返回的数据可直接序列化为JSON字符串作为mistake_records参数传给考试生成接口 + + 参数说明: + - exam_id: 考试ID + - round: 指定轮次(1/2/3),用于获取特定轮次的错题 + 第2轮考试时传入round=1,获取第1轮的错题 + 第3轮考试时传入round=2,获取第2轮的错题 + 不传则获取该考试的所有错题 """ logger.info(f"📋 GET /mistakes 收到请求") try: - logger.info(f"📋 获取错题记录 - exam_id: {exam_id}, user_id: {current_user.id}") + logger.info(f"📋 获取错题记录 - exam_id: {exam_id}, round: {round}, user_id: {current_user.id}") - # 查询指定考试的错题记录 - result = await db.execute( - select(ExamMistake).where( - ExamMistake.exam_id == exam_id, - ExamMistake.user_id == current_user.id, - ).order_by(ExamMistake.id) + # 构建查询条件 + query = select(ExamMistake).where( + ExamMistake.exam_id == exam_id, + ExamMistake.user_id == current_user.id, ) + + # 如果指定了轮次,只获取该轮次的错题 + if round is not None: + query = query.where(ExamMistake.round == round) + + query = query.order_by(ExamMistake.id) + + result = await db.execute(query) mistakes = result.scalars().all() logger.info(f"✅ 查询到错题记录数量: {len(mistakes)}") @@ -262,7 +276,18 @@ async def generate_exam( - 第一轮考试:mistake_records 传空或不传 - 第二、三轮错题重考:mistake_records 传入上一轮错题记录的JSON字符串 """ + from app.models.course import Course + try: + # 验证课程存在性 + course = await db.get(Course, request.course_id) + if not course or course.is_deleted: + logger.warning(f"课程不存在或已删除: course_id={request.course_id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"无法生成试题:课程ID {request.course_id} 不存在或已被删除" + ) + # 从用户信息中自动获取岗位ID(如果未提供) position_id = request.position_id if not position_id: @@ -486,6 +511,7 @@ async def record_mistake( mistake = ExamMistake( user_id=current_user.id, exam_id=request.exam_id, + round=request.round, # 记录考试轮次 question_id=request.question_id, knowledge_point_id=None, # 暂时设为None,避免外键约束 question_content=request.question_content, @@ -503,7 +529,7 @@ async def record_mistake( logger.info( f"记录错题成功 - user_id: {current_user.id}, exam_id: {request.exam_id}, " - f"mistake_id: {mistake.id}" + f"round: {request.round}, mistake_id: {mistake.id}" ) return ResponseModel( diff --git a/backend/app/models/exam_mistake.py b/backend/app/models/exam_mistake.py index 69a999f..f627654 100644 --- a/backend/app/models/exam_mistake.py +++ b/backend/app/models/exam_mistake.py @@ -14,6 +14,7 @@ class ExamMistake(BaseModel): # 核心关联字段 user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="用户ID") exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True, comment="考试ID") + round = Column(Integer, nullable=True, default=1, comment="考试轮次(1/2/3)") question_id = Column(Integer, ForeignKey("questions.id", ondelete="SET NULL"), nullable=True, index=True, comment="题目ID(AI生成的题目可能为空)") knowledge_point_id = Column(Integer, ForeignKey("knowledge_points.id", ondelete="SET NULL"), nullable=True, index=True, comment="关联的知识点ID") diff --git a/backend/app/schemas/exam.py b/backend/app/schemas/exam.py index aa4a068..92aeb9d 100644 --- a/backend/app/schemas/exam.py +++ b/backend/app/schemas/exam.py @@ -170,6 +170,7 @@ class JudgeAnswerResponse(BaseModel): class RecordMistakeRequest(BaseModel): """记录错题请求""" exam_id: int = Field(..., description="考试ID") + round: int = Field(1, description="考试轮次(1/2/3)") question_id: Optional[int] = Field(None, description="题目ID(AI生成的题目可能为空)") knowledge_point_id: Optional[int] = Field(None, description="知识点ID") question_content: str = Field(..., description="题目内容") diff --git a/backend/app/services/ai/ability_analysis_service.py b/backend/app/services/ai/ability_analysis_service.py index 5d4f234..693aff0 100644 --- a/backend/app/services/ai/ability_analysis_service.py +++ b/backend/app/services/ai/ability_analysis_service.py @@ -477,3 +477,8 @@ ability_analysis_service = AbilityAnalysisService() + + + + + diff --git a/backend/app/services/ai/course_chat_service.py b/backend/app/services/ai/course_chat_service.py index 92a6014..a301f12 100644 --- a/backend/app/services/ai/course_chat_service.py +++ b/backend/app/services/ai/course_chat_service.py @@ -154,7 +154,10 @@ class CourseChatServiceV2: # 6. 更新会话索引 if is_new_conversation: - await self._add_to_conversation_index(user_id, conversation_id, course_id) + await self._add_to_conversation_index( + user_id, conversation_id, course_id, + first_message=query # 使用用户第一条消息作为会话名称 + ) else: await self._update_conversation_index(user_id, conversation_id) @@ -202,7 +205,7 @@ class CourseChatServiceV2: Tuple[str, Optional[str]]: (事件类型, 数据) - ("conversation_started", conversation_id): 会话开始 - ("chunk", text): 文本块 - - ("end", None): 结束 + - ("done", full_answer): 结束,附带完整回答 - ("error", message): 错误 """ full_answer = "" @@ -251,7 +254,7 @@ class CourseChatServiceV2: yield ("chunk", chunk) # 6. 发送结束事件 - yield ("end", None) + yield ("done", full_answer) # 7. 保存对话历史 await self._save_conversation_history( @@ -262,7 +265,10 @@ class CourseChatServiceV2: # 8. 更新会话索引 if is_new_conversation: - await self._add_to_conversation_index(user_id, conversation_id, course_id) + await self._add_to_conversation_index( + user_id, conversation_id, course_id, + first_message=query # 使用用户第一条消息作为会话名称 + ) else: await self._update_conversation_index(user_id, conversation_id) @@ -526,7 +532,8 @@ class CourseChatServiceV2: self, user_id: int, conversation_id: str, - course_id: int + course_id: int, + first_message: str = "" ) -> None: """ 将会话添加到用户索引 @@ -535,6 +542,7 @@ class CourseChatServiceV2: user_id: 用户ID conversation_id: 会话ID course_id: 课程ID + first_message: 用户第一条消息(用于生成会话名称) """ try: from app.core.redis import get_redis_client @@ -547,12 +555,21 @@ class CourseChatServiceV2: await redis.zadd(index_key, {conversation_id: timestamp}) await redis.expire(index_key, CONVERSATION_INDEX_TTL) - # 2. 保存会话元数据 + # 2. 生成会话名称(取用户第一条消息的前30个字符) + conversation_name = "" + if first_message: + # 移除换行符,截取前30个字符 + conversation_name = first_message.replace('\n', ' ').strip()[:30] + if len(first_message) > 30: + conversation_name += "..." + + # 3. 保存会话元数据(包含会话名称) meta_key = f"{CONVERSATION_META_PREFIX}{conversation_id}" meta_data = { "conversation_id": conversation_id, "user_id": user_id, "course_id": course_id, + "name": conversation_name, # 会话名称 "created_at": timestamp, "updated_at": timestamp, } @@ -563,7 +580,8 @@ class CourseChatServiceV2: ) logger.debug( - f"会话已添加到索引 - user_id: {user_id}, conversation_id: {conversation_id}" + f"会话已添加到索引 - user_id: {user_id}, conversation_id: {conversation_id}, " + f"name: {conversation_name}" ) except RuntimeError: @@ -671,8 +689,10 @@ class CourseChatServiceV2: "updated_at": time.time(), } - # 获取最后一条消息作为预览 + # 获取历史消息 history = await self._get_conversation_history(conv_id) + + # 获取最后一条消息作为预览 last_message = "" if history: # 获取最后一条 assistant 消息 @@ -683,8 +703,21 @@ class CourseChatServiceV2: last_message += "..." break + # 获取会话名称 + # 优先使用元数据中保存的名称,如果没有则从历史消息中提取 + conversation_name = meta.get("name", "") + if not conversation_name and history: + # 从第一条用户消息生成名称 + for msg in history: + if msg["role"] == "user": + conversation_name = msg["content"].replace('\n', ' ').strip()[:30] + if len(msg["content"]) > 30: + conversation_name += "..." + break + conversations.append({ "id": conv_id, + "name": conversation_name, # 添加会话名称字段 "course_id": meta.get("course_id"), "created_at": meta.get("created_at"), "updated_at": meta.get("updated_at"), diff --git a/backend/app/services/ai/exam_generator_service.py b/backend/app/services/ai/exam_generator_service.py index 692bce6..4dbbabd 100644 --- a/backend/app/services/ai/exam_generator_service.py +++ b/backend/app/services/ai/exam_generator_service.py @@ -510,3 +510,8 @@ async def generate_exam( + + + + + diff --git a/backend/app/services/ai/knowledge_analysis_v2.py b/backend/app/services/ai/knowledge_analysis_v2.py index 9f4d6c0..eafc93a 100644 --- a/backend/app/services/ai/knowledge_analysis_v2.py +++ b/backend/app/services/ai/knowledge_analysis_v2.py @@ -546,3 +546,8 @@ knowledge_analysis_service_v2 = KnowledgeAnalysisServiceV2() + + + + + diff --git a/backend/app/services/ai/llm_json_parser.py b/backend/app/services/ai/llm_json_parser.py index 24b4264..23c4ae9 100644 --- a/backend/app/services/ai/llm_json_parser.py +++ b/backend/app/services/ai/llm_json_parser.py @@ -256,6 +256,16 @@ def _preprocess_text(text: str) -> str: # 移除零宽字符 text = re.sub(r'[\u200b\u200c\u200d\ufeff]', '', text) + # 【重要】先替换中文标点为英文标点(在找边界之前做,否则中文引号会破坏边界检测) + cn_punctuation = { + ',': ',', '。': '.', ':': ':', ';': ';', + '"': '"', '"': '"', ''': "'", ''': "'", + '【': '[', '】': ']', '(': '(', ')': ')', + '{': '{', '}': '}', + } + for cn, en in cn_punctuation.items(): + text = text.replace(cn, en) + # 提取 Markdown 代码块 patterns = [ r'```json\s*([\s\S]*?)\s*```', @@ -705,3 +715,4 @@ def clean_llm_output(text: str) -> Tuple[str, List[str]]: + diff --git a/backend/app/services/ai/practice_scene_service.py b/backend/app/services/ai/practice_scene_service.py index 86afa70..805ba2e 100644 --- a/backend/app/services/ai/practice_scene_service.py +++ b/backend/app/services/ai/practice_scene_service.py @@ -377,3 +377,8 @@ async def prepare_practice_knowledge( + + + + + diff --git a/backend/app/services/ai/prompts/ability_analysis_prompts.py b/backend/app/services/ai/prompts/ability_analysis_prompts.py index 1bdccf3..5be0d71 100644 --- a/backend/app/services/ai/prompts/ability_analysis_prompts.py +++ b/backend/app/services/ai/prompts/ability_analysis_prompts.py @@ -213,3 +213,8 @@ PRIORITY_LEVELS = ["high", "medium", "low"] + + + + + diff --git a/backend/app/services/ai/prompts/answer_judge_prompts.py b/backend/app/services/ai/prompts/answer_judge_prompts.py index 6580979..4721d01 100644 --- a/backend/app/services/ai/prompts/answer_judge_prompts.py +++ b/backend/app/services/ai/prompts/answer_judge_prompts.py @@ -46,3 +46,8 @@ INCORRECT_KEYWORDS = ["错误", "incorrect", "false", "no", "wrong", "不正确" + + + + + diff --git a/backend/app/services/ai/prompts/course_chat_prompts.py b/backend/app/services/ai/prompts/course_chat_prompts.py index ac5ab4e..4c1d44f 100644 --- a/backend/app/services/ai/prompts/course_chat_prompts.py +++ b/backend/app/services/ai/prompts/course_chat_prompts.py @@ -72,3 +72,8 @@ DEFAULT_TEMPERATURE = 0.7 + + + + + diff --git a/backend/app/services/ai/prompts/exam_generator_prompts.py b/backend/app/services/ai/prompts/exam_generator_prompts.py index e979dfa..6940882 100644 --- a/backend/app/services/ai/prompts/exam_generator_prompts.py +++ b/backend/app/services/ai/prompts/exam_generator_prompts.py @@ -254,8 +254,11 @@ QUESTION_SCHEMA = { "description": "知识点ID" }, "correct": { - "type": "string", - "description": "正确答案" + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ], + "description": "正确答案(单选/判断/填空为字符串,多选为数组)" }, "analysis": { "type": "string", @@ -298,3 +301,8 @@ MAX_DIFFICULTY_LEVEL = 5 + + + + + diff --git a/backend/app/services/ai/prompts/knowledge_analysis_prompts.py b/backend/app/services/ai/prompts/knowledge_analysis_prompts.py index ab918a3..4554e85 100644 --- a/backend/app/services/ai/prompts/knowledge_analysis_prompts.py +++ b/backend/app/services/ai/prompts/knowledge_analysis_prompts.py @@ -146,3 +146,8 @@ DEFAULT_KNOWLEDGE_TYPE = "理论知识" + + + + + diff --git a/backend/app/services/ai/prompts/practice_analysis_prompts.py b/backend/app/services/ai/prompts/practice_analysis_prompts.py index 45f1298..964b69f 100644 --- a/backend/app/services/ai/prompts/practice_analysis_prompts.py +++ b/backend/app/services/ai/prompts/practice_analysis_prompts.py @@ -191,3 +191,8 @@ ANNOTATION_TAGS = [ + + + + + diff --git a/backend/app/services/ai/prompts/practice_scene_prompts.py b/backend/app/services/ai/prompts/practice_scene_prompts.py index df391cd..8a1158d 100644 --- a/backend/app/services/ai/prompts/practice_scene_prompts.py +++ b/backend/app/services/ai/prompts/practice_scene_prompts.py @@ -190,3 +190,8 @@ DEFAULT_DIFFICULTY = "intermediate" + + + + + diff --git a/backend/requirements-admin.txt b/backend/requirements-admin.txt index 6ef4960..03d183e 100644 --- a/backend/requirements-admin.txt +++ b/backend/requirements-admin.txt @@ -18,3 +18,8 @@ watchfiles==0.21.0 + + + + + diff --git a/backend/数据库架构-统一版.md b/backend/数据库架构-统一版.md index 004c9e1..078a3a4 100644 --- a/backend/数据库架构-统一版.md +++ b/backend/数据库架构-统一版.md @@ -336,6 +336,7 @@ CREATE TABLE IF NOT EXISTS exam_mistakes ( -- 核心关联字段(必需) user_id INT NOT NULL COMMENT '用户ID', exam_id INT NOT NULL COMMENT '考试ID', + round INT DEFAULT 1 COMMENT '考试轮次(1/2/3)', -- 2026-01-27新增:标识错题产生于哪一轮 question_id INT COMMENT '题目ID(AI生成的题目可能为空)', knowledge_point_id INT COMMENT '关联的知识点ID', @@ -356,14 +357,15 @@ CREATE TABLE IF NOT EXISTS exam_mistakes ( INDEX idx_user_id (user_id), INDEX idx_exam_id (exam_id), + INDEX idx_exam_round (exam_id, round), -- 2026-01-27新增:支持按考试和轮次查询 INDEX idx_knowledge_point_id (knowledge_point_id), INDEX idx_question_type (question_type) -- 2025-10-12新增 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='错题记录表'; ``` **核心字段说明:** -- 包含8个核心字段:`user_id`、`exam_id`、`question_id`、`knowledge_point_id`、`question_content`、`correct_answer`、`user_answer`、`question_type` -- 简化设计,去除冗余字段(如错误次数、掌握状态等),聚焦核心功能 +- 包含9个核心字段:`user_id`、`exam_id`、`round`、`question_id`、`knowledge_point_id`、`question_content`、`correct_answer`、`user_answer`、`question_type` +- `round` 字段(2026-01-27新增):标识错题产生于哪一轮考试,用于多轮错题重考时只获取上一轮的错题 - `question_id` 可为空:AI动态生成的题目可能不在 questions 表中 - `knowledge_point_id` 可为空:用于关联知识点,支持错题重考功能 - `question_type` 用于记录题型,支持错题按题型筛选和统计(2025-10-12新增) diff --git a/frontend/src/api/exam.ts b/frontend/src/api/exam.ts index 3839a7e..425ebee 100644 --- a/frontend/src/api/exam.ts +++ b/frontend/src/api/exam.ts @@ -98,6 +98,7 @@ export interface JudgeAnswerResponse { */ export interface RecordMistakeRequest { exam_id: number + round: number // 考试轮次(1/2/3) question_id?: number | null knowledge_point_id?: number | null question_content: string @@ -154,9 +155,18 @@ export function recordMistake(data: RecordMistakeRequest) { /** * 获取错题记录 + * @param examId 考试ID + * @param round 指定轮次(可选),用于获取特定轮次的错题 + * 第2轮考试时传入round=1,获取第1轮的错题 + * 第3轮考试时传入round=2,获取第2轮的错题 + * 不传则获取该考试的所有错题 */ -export function getMistakes(examId: number) { - return http.get(`/api/v1/exams/mistakes?exam_id=${examId}`, { +export function getMistakes(examId: number, round?: number) { + let url = `/api/v1/exams/mistakes?exam_id=${examId}` + if (round !== undefined) { + url += `&round=${round}` + } + return http.get(url, { showLoading: false, }) } diff --git a/frontend/src/utils/cozeVoiceClient.ts b/frontend/src/utils/cozeVoiceClient.ts index 2b6c91c..2d53c23 100644 --- a/frontend/src/utils/cozeVoiceClient.ts +++ b/frontend/src/utils/cozeVoiceClient.ts @@ -89,15 +89,16 @@ export class CozeVoiceClient { console.log('[CozeVoice] ✅ 已设置播放音量=1') } - // 6. 如果有场景提示词,发送初始消息 - // 注意:参考代码不发送初始消息,而是等待用户先说话 - // 但我们的场景需要AI先开场白,所以发送场景提示词 + // ⚠️ 重要:不要在连接后发送场景提示词! + // SDK连接时默认设置 need_play_prologue: true,Bot会自动播放开场白 + // 如果手动发送sendTextMessage会干扰开场白播放流程 + // 场景信息应该在Bot的系统提示词中预设 if (config.scenePrompt) { - this.client.sendTextMessage(config.scenePrompt) - console.log('[CozeVoice] ✅ 场景提示词已发送') + console.log('[CozeVoice] ⚠️ 场景提示词将通过Bot开场白播放,不手动发送') + console.log('[CozeVoice] 📝 场景提示词:', config.scenePrompt.substring(0, 100) + '...') } - console.log('[CozeVoice] 🎉 初始化完成,等待对话...') + console.log('[CozeVoice] 🎉 初始化完成,等待Bot开场白和用户对话...') } catch (error) { console.error('[CozeVoice] ❌ 连接失败:', error) @@ -146,6 +147,10 @@ export class CozeVoiceClient { if (!event) return + // 🔍 调试:打印所有收到的事件(详细日志) + console.log('[CozeVoice] 📨 收到事件:', eventName, '-> event_type:', event.event_type) + console.log('[CozeVoice] 📨 事件数据:', JSON.stringify(event.data || {}).substring(0, 200)) + switch (event.event_type) { // 用户开始说话 - 关闭扬声器本地回放(避免用户听到自己的声音) case 'input_audio_buffer.speech_started': { @@ -221,6 +226,43 @@ export class CozeVoiceClient { this.emit('ai_speech_end', {}) break } + + // 🔍 调试:捕获服务器错误事件 + case 'error': { + console.error('[CozeVoice] ❌ 服务器返回错误:', event) + this.emit('error', { error: event.data?.error || '未知错误' }) + break + } + + // 对话创建事件 + case WebsocketsEventType.CONVERSATION_CHAT_CREATED: { + console.log('[CozeVoice] ✅ 对话创建成功') + break + } + + // 对话进行中 + case 'conversation.chat.in_progress': { + console.log('[CozeVoice] 🔄 AI正在思考...') + break + } + + // 对话失败事件 + case 'conversation.chat.failed': { + console.error('[CozeVoice] ❌ 对话失败:', event) + this.emit('error', { error: event.data?.last_error?.msg || '对话失败' }) + break + } + + // 对话完成事件 + case 'conversation.chat.completed': { + console.log('[CozeVoice] ✅ 对话完成') + break + } + + // 默认处理:记录未知事件 + default: { + console.log('[CozeVoice] ⚠️ 未处理的事件类型:', event.event_type) + } } } diff --git a/frontend/src/views/analysis/report.vue b/frontend/src/views/analysis/report.vue index 36ba373..26571ad 100644 --- a/frontend/src/views/analysis/report.vue +++ b/frontend/src/views/analysis/report.vue @@ -256,6 +256,16 @@ const formatDuration = (seconds: number) => { return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟` } +/** + * 格式化日期为 YYYY-MM-DD(使用本地时间,避免时区问题) + */ +const formatLocalDate = (date: Date): string => { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + /** * 加载成绩报告数据 */ @@ -263,10 +273,10 @@ const loadReportData = async () => { try { const params: any = {} - // 添加时间范围参数 + // 添加时间范围参数(使用本地时间格式化,避免UTC时区偏移问题) if (dateRange.value && dateRange.value.length === 2) { - params.start_date = dateRange.value[0].toISOString().split('T')[0] - params.end_date = dateRange.value[1].toISOString().split('T')[0] + params.start_date = formatLocalDate(dateRange.value[0]) + params.end_date = formatLocalDate(dateRange.value[1]) } const response = await getExamReport(params) diff --git a/frontend/src/views/exam/practice.vue b/frontend/src/views/exam/practice.vue index 69579b9..1d57601 100644 --- a/frontend/src/views/exam/practice.vue +++ b/frontend/src/views/exam/practice.vue @@ -341,8 +341,28 @@ const transformDifyQuestions = (difyQuestions: any[]): any[] => { const options = q.topic?.options || {} // 解析正确答案:支持 "A"、"A,B"、"A、B"、"A:xxx" 等多种格式 const correctAnswerStr = String(q.correct || '') - // 提取正确答案中的选项字母(A、B、C、D等) - const correctLetters = correctAnswerStr.match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || [] + + // 更精确地提取正确答案字母(避免误提取选项内容中的字母) + let correctLetters: string[] = [] + + // 情况1:纯选项字母格式(如 "A", "B", "A,B", "A、B", "A,B,C") + const pureLetterMatch = correctAnswerStr.trim().match(/^[A-Da-d]([,、\s]+[A-Da-d])*$/) + if (pureLetterMatch) { + correctLetters = correctAnswerStr.match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || [] + } else { + // 情况2:选项字母+内容格式(如 "A:xxx" 或 "A: xxx") + // 只提取开头的选项字母(可能有多个,如 "A,C:xxx") + const prefixMatch = correctAnswerStr.match(/^([A-Da-d](?:[,、\s]*[A-Da-d])*)[:::\s]/) + if (prefixMatch) { + correctLetters = prefixMatch[1].match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || [] + } else { + // 情况3:只有开头字母(如 "A" 后面直接跟内容) + const firstLetterMatch = correctAnswerStr.match(/^([A-Da-d])/) + if (firstLetterMatch) { + correctLetters = [firstLetterMatch[1].toUpperCase()] + } + } + } console.log(`🔍 解析题目[${index + 1}] - correct: "${q.correct}", correctLetters: [${correctLetters.join(', ')}]`) @@ -481,10 +501,11 @@ const loadQuestions = async () => { // 第一轮考试不传mistake_records参数 // 第二、三轮考试传入上一轮的错题记录 if (currentRound.value > 1) { - // 用同一个exam_id获取错题 - console.log(`📋 获取第${currentRound.value - 1}轮错题记录 - exam_id: ${currentExamId.value}`) + // 用同一个exam_id获取上一轮的错题(传入round参数只获取上一轮的错题) + const previousRound = currentRound.value - 1 + console.log(`📋 获取第${previousRound}轮错题记录 - exam_id: ${currentExamId.value}, round: ${previousRound}`) - const mistakesRes: any = await getMistakes(currentExamId.value) + const mistakesRes: any = await getMistakes(currentExamId.value, previousRound) console.log('getMistakes原始响应:', mistakesRes) console.log('getMistakes响应结构:', { @@ -785,6 +806,7 @@ const recordWrongAnswer = async () => { try { await recordMistake({ exam_id: currentExamId.value, + round: currentRound.value, // 记录当前轮次 question_id: null, // AI生成的题目没有question_id knowledge_point_id: q.knowledge_point_id || null, question_content: q.title, diff --git a/frontend/src/views/trainee/chat-course.vue b/frontend/src/views/trainee/chat-course.vue index d1bb224..4a16317 100644 --- a/frontend/src/views/trainee/chat-course.vue +++ b/frontend/src/views/trainee/chat-course.vue @@ -10,7 +10,7 @@

{{ courseInfo.title }}

- 由 Dify 对话流驱动 + 智能对话助手 @@ -512,19 +512,37 @@ const scrollToBottom = async () => { const loadCourseInfo = async () => { try { const courseId = parseInt(courseInfo.value.id) - if (isNaN(courseId)) { + if (isNaN(courseId) || courseId <= 0) { console.warn('无效的课程ID:', courseInfo.value.id) courseInfo.value.title = '未知课程' return } const res: any = await getCourseDetail(courseId) - // API 返回格式是 { code: 200, data: { name: "...", ... } } - if (res.code === 200 && res.data) { - courseInfo.value.title = res.data.title || res.data.name || '未命名课程' + console.log('课程详情API响应:', res) + + // 兼容不同的响应格式 + // 格式1: { code: 200, data: { name: "...", ... } } + // 格式2: 直接返回课程对象 { name: "...", ... } + let courseData = res + if (res.code !== undefined && res.data) { + // 标准响应格式 + if (res.code === 200) { + courseData = res.data + } else { + console.warn('获取课程详情失败:', res.message || '未知错误') + courseInfo.value.title = '未知课程' + return + } + } + + // 从课程数据中提取名称 + const courseName = courseData?.title || courseData?.name + if (courseName) { + courseInfo.value.title = courseName console.log('课程详情已加载:', courseInfo.value.title) } else { - console.warn('获取课程详情失败:', res.message || '未知错误') - courseInfo.value.title = '未知课程' + console.warn('课程数据中缺少名称字段:', courseData) + courseInfo.value.title = '未命名课程' } } catch (error: any) { console.error('加载课程详情失败:', error) diff --git a/frontend/src/views/trainee/course-detail.vue b/frontend/src/views/trainee/course-detail.vue index 4210028..a22a64e 100644 --- a/frontend/src/views/trainee/course-detail.vue +++ b/frontend/src/views/trainee/course-detail.vue @@ -321,8 +321,7 @@ const pdfScaleStyle = computed(() => { const scale = 1 / pdfRenderScale.value return { transform: `scale(${scale})`, - transformOrigin: 'top left', - width: `${100 * pdfRenderScale.value}%` + transformOrigin: 'top center' } }) @@ -911,6 +910,7 @@ onUnmounted(() => { padding: 10px 20px; background: #fff; border-bottom: 1px solid #e4e7ed; + flex-shrink: 0; .page-controls, .zoom-controls { @@ -933,10 +933,12 @@ onUnmounted(() => { padding: 20px; display: flex; justify-content: center; + align-items: flex-start; // 高清渲染缩放容器 .pdf-scale-wrapper { - display: inline-block; + display: flex; + justify-content: center; :deep(.vue-pdf-embed) { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); diff --git a/frontend/src/views/trainee/growth-path.vue b/frontend/src/views/trainee/growth-path.vue index 1dc0196..7681c5f 100644 --- a/frontend/src/views/trainee/growth-path.vue +++ b/frontend/src/views/trainee/growth-path.vue @@ -78,7 +78,7 @@
@@ -405,7 +405,6 @@ const abilityFeedback = ref([]) // AI 分析和推荐相关 const analyzing = ref(false) -const loadingRecommendations = ref(false) const selectedPriority = ref('all') // 计算属性:过滤后的课程 @@ -723,81 +722,11 @@ const analyzeSmartBadgeData = async () => { /** * 刷新AI推荐课程 + * 重新调用AI分析接口,获取最新的课程推荐 */ const refreshRecommendations = async () => { - loadingRecommendations.value = true - - try { - // 模拟AI推荐算法 - await new Promise(resolve => setTimeout(resolve, 1500)) - - // 根据能力评估结果生成推荐 - const weakPoints = abilityData.value - .filter(item => item.value < 85) - .sort((a, b) => a.value - b.value) - .slice(0, 3) - - // 更新推荐课程(基于薄弱环节) - const newRecommendations = [] - - if (weakPoints.some(p => p.name === '沟通技巧')) { - newRecommendations.push({ - id: 1, - name: '美容咨询与沟通技巧', - description: '提升与客户的沟通能力,学习专业的美容咨询技巧', - duration: 8, - difficulty: 'medium', - learnerCount: 156, - priority: 'high', - targetWeakPoints: ['沟通技巧'], - expectedImprovement: 12, - matchScore: 95, - progress: 85, - examPassed: false - }) - } - - if (weakPoints.some(p => p.name === '应变能力')) { - newRecommendations.push({ - id: 2, - name: '皮肤问题识别与处理', - description: '学习识别常见皮肤问题,掌握针对性的护理和治疗方案', - duration: 12, - difficulty: 'hard', - learnerCount: 89, - priority: 'medium', - targetWeakPoints: ['应变能力'], - expectedImprovement: 15, - matchScore: 88, - progress: 95, - examPassed: true - }) - } - - if (weakPoints.some(p => p.name === '安全意识')) { - newRecommendations.push({ - id: 3, - name: '轻医美项目与效果管理', - description: '深入了解各种轻医美项目,掌握效果评估和管理方法', - duration: 16, - difficulty: 'hard', - learnerCount: 234, - priority: 'medium', - targetWeakPoints: ['安全意识'], - expectedImprovement: 10, - matchScore: 82, - progress: 60, - examPassed: false - }) - } - - recommendedCourses.value = newRecommendations - - } catch (error) { - ElMessage.error('获取推荐课程失败') - } finally { - loadingRecommendations.value = false - } + // 直接调用AI分析智能工牌数据的方法,复用已有逻辑 + await analyzeSmartBadgeData() } /**