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

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

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

570
tests/test_ai_functions.py Normal file
View File

@@ -0,0 +1,570 @@
#!/usr/bin/env python3
"""
考培练系统 AI 功能完整测试脚本
测试所有 AI 功能是否正常:
1. AI 基础服务连通性4sapi + OpenRouter
2. 试题生成功能V2 引擎)
3. 答案判断功能V2 引擎)
4. 知识点分析功能V2 引擎)
5. 课程对话功能V2 引擎)
6. 陪练场景提取功能V2 引擎)
7. 能力分析功能V2 引擎)
测试方式:通过 HTTP API 调用后端服务
"""
import asyncio
import httpx
import json
import sys
import time
from datetime import datetime
from typing import Optional, Dict, Any, List
# ==================== 配置 ====================
# 测试基础URL演示版后端
BASE_URL = "http://localhost:8000/api/v1"
# 测试账户(从数据库获取有效用户)
TEST_USERNAME = "admin"
TEST_PASSWORD = "admin123"
# 测试超时时间(秒)
TIMEOUT = 120
# 测试结果统计
test_results: List[Dict[str, Any]] = []
# ==================== 工具函数 ====================
def print_header(title: str):
"""打印标题"""
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60)
def print_result(test_name: str, success: bool, message: str, duration_ms: int = 0):
"""打印测试结果"""
status = "✅ 通过" if success else "❌ 失败"
duration_str = f" ({duration_ms}ms)" if duration_ms > 0 else ""
print(f" {status} | {test_name}{duration_str}")
if not success:
print(f" └─ {message}")
# 记录结果
test_results.append({
"name": test_name,
"success": success,
"message": message,
"duration_ms": duration_ms,
"timestamp": datetime.now().isoformat()
})
async def get_auth_token(client: httpx.AsyncClient) -> Optional[str]:
"""获取认证 token"""
try:
response = await client.post(
f"{BASE_URL}/auth/login",
data={"username": TEST_USERNAME, "password": TEST_PASSWORD}
)
if response.status_code == 200:
data = response.json()
return data.get("data", {}).get("access_token")
else:
print(f" ⚠️ 登录失败: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f" ⚠️ 登录异常: {e}")
return None
# ==================== 测试用例 ====================
async def test_1_ai_basic_connectivity():
"""测试1: AI 基础服务连通性"""
print_header("测试1: AI 基础服务连通性")
# 直接测试 4sapi
async with httpx.AsyncClient(timeout=30) as client:
# 4sapi 测试
start = time.time()
try:
response = await client.post(
"https://4sapi.com/v1/chat/completions",
headers={
"Authorization": "Bearer sk-9yMCXjRGANbacz20kJY8doSNy6Rf446aYwmgGIuIXQ7DAyBw",
"Content-Type": "application/json"
},
json={
"model": "gemini-3-flash-preview",
"messages": [{"role": "user", "content": "你好,请回复'测试成功'"}],
"max_tokens": 50
}
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
print_result("4sapi.com 连通性", True, f"响应: {content[:50]}...", duration)
else:
print_result("4sapi.com 连通性", False, f"HTTP {response.status_code}: {response.text[:100]}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("4sapi.com 连通性", False, str(e), duration)
# OpenRouter 测试
start = time.time()
try:
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": "Bearer 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",
"Content-Type": "application/json"
},
json={
"model": "google/gemini-3-flash-preview",
"messages": [{"role": "user", "content": "你好,请回复'测试成功'"}],
"max_tokens": 50
}
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
print_result("OpenRouter 连通性", True, f"响应: {content[:50]}...", duration)
else:
print_result("OpenRouter 连通性", False, f"HTTP {response.status_code}: {response.text[:100]}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("OpenRouter 连通性", False, str(e), duration)
async def test_2_exam_generation():
"""测试2: 试题生成功能"""
print_header("测试2: 试题生成功能 (V2 引擎)")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
# 获取 token
token = await get_auth_token(client)
if not token:
print_result("试题生成", False, "无法获取认证 token")
return
headers = {"Authorization": f"Bearer {token}"}
# 先查询一个有知识点的课程
try:
response = await client.get(f"{BASE_URL}/courses", headers=headers)
if response.status_code != 200:
print_result("试题生成 - 查询课程", False, f"HTTP {response.status_code}")
return
courses = response.json().get("data", {}).get("items", [])
if not courses:
print_result("试题生成", False, "没有可用的课程")
return
course_id = courses[0].get("id")
print(f" 📝 使用课程ID: {course_id}")
except Exception as e:
print_result("试题生成 - 查询课程", False, str(e))
return
# 测试试题生成 API
start = time.time()
try:
response = await client.post(
f"{BASE_URL}/exams/generate?engine=v2",
headers=headers,
json={
"course_id": course_id,
"single_choice_count": 2,
"multiple_choice_count": 1,
"true_false_count": 1,
"fill_blank_count": 0,
"essay_count": 0,
"difficulty_level": 3,
"current_round": 1
}
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
result = data.get("data", {}).get("result", "")
try:
questions = json.loads(result) if result else []
print_result("试题生成 (V2)", True, f"生成了 {len(questions)} 道题目", duration)
except:
print_result("试题生成 (V2)", True, f"返回结果: {result[:100]}...", duration)
else:
print_result("试题生成 (V2)", False, f"HTTP {response.status_code}: {response.text[:200]}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("试题生成 (V2)", False, str(e), duration)
async def test_3_answer_judge():
"""测试3: 答案判断功能"""
print_header("测试3: 答案判断功能 (V2 引擎)")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
# 获取 token
token = await get_auth_token(client)
if not token:
print_result("答案判断", False, "无法获取认证 token")
return
headers = {"Authorization": f"Bearer {token}"}
# 测试正确答案
start = time.time()
try:
response = await client.post(
f"{BASE_URL}/exams/judge-answer?engine=v2",
headers=headers,
json={
"question": "玻尿酸的主要作用是什么?",
"correct_answer": "保湿、填充、塑形",
"user_answer": "保湿和填充",
"analysis": "玻尿酸具有强大的保水能力,可用于皮肤保湿和面部填充"
}
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
is_correct = data.get("data", {}).get("is_correct")
print_result("答案判断 (V2)", True, f"判断结果: {'正确' if is_correct else '错误'}", duration)
else:
print_result("答案判断 (V2)", False, f"HTTP {response.status_code}: {response.text[:200]}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("答案判断 (V2)", False, str(e), duration)
async def test_4_knowledge_analysis():
"""测试4: 知识点分析功能"""
print_header("测试4: 知识点分析功能 (V2 引擎)")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
# 获取 token
token = await get_auth_token(client)
if not token:
print_result("知识点分析", False, "无法获取认证 token")
return
headers = {"Authorization": f"Bearer {token}"}
# 先查询课程和资料
try:
response = await client.get(f"{BASE_URL}/courses", headers=headers)
if response.status_code != 200:
print_result("知识点分析 - 查询课程", False, f"HTTP {response.status_code}")
return
courses = response.json().get("data", {}).get("items", [])
if not courses:
print_result("知识点分析", False, "没有可用的课程")
return
course_id = courses[0].get("id")
# 查询课程资料
response = await client.get(f"{BASE_URL}/courses/{course_id}/materials", headers=headers)
if response.status_code != 200:
print_result("知识点分析 - 查询资料", False, f"HTTP {response.status_code}")
return
materials = response.json().get("data", {}).get("items", [])
if not materials:
print_result("知识点分析", False, "课程没有可用的资料")
return
material_id = materials[0].get("id")
print(f" 📝 使用课程ID: {course_id}, 资料ID: {material_id}")
except Exception as e:
print_result("知识点分析 - 查询资料", False, str(e))
return
# 测试知识点分析 API
start = time.time()
try:
response = await client.post(
f"{BASE_URL}/courses/{course_id}/materials/{material_id}/analyze?engine=v2",
headers=headers
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
kps = data.get("data", {}).get("knowledge_points", [])
print_result("知识点分析 (V2)", True, f"提取了 {len(kps)} 个知识点", duration)
else:
# 可能返回 400 因为没有文件内容
error_detail = response.json().get("detail", response.text[:200])
print_result("知识点分析 (V2)", False, f"HTTP {response.status_code}: {error_detail}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("知识点分析 (V2)", False, str(e), duration)
async def test_5_course_chat():
"""测试5: 课程对话功能"""
print_header("测试5: 课程对话功能 (V2 引擎)")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
# 获取 token
token = await get_auth_token(client)
if not token:
print_result("课程对话", False, "无法获取认证 token")
return
headers = {"Authorization": f"Bearer {token}"}
# 先查询课程
try:
response = await client.get(f"{BASE_URL}/courses", headers=headers)
if response.status_code != 200:
print_result("课程对话 - 查询课程", False, f"HTTP {response.status_code}")
return
courses = response.json().get("data", {}).get("items", [])
if not courses:
print_result("课程对话", False, "没有可用的课程")
return
course_id = courses[0].get("id")
print(f" 📝 使用课程ID: {course_id}")
except Exception as e:
print_result("课程对话 - 查询课程", False, str(e))
return
# 测试课程对话 API流式响应
start = time.time()
try:
# 注意:课程对话是流式响应,需要特殊处理
async with client.stream(
"POST",
f"{BASE_URL}/course/chat?engine=v2",
headers=headers,
json={
"course_id": course_id,
"query": "请简单介绍一下这门课程的主要内容"
}
) as response:
if response.status_code == 200:
content = ""
async for line in response.aiter_lines():
if line.startswith("data:"):
try:
data = json.loads(line[5:].strip())
if data.get("event") == "message_chunk":
content += data.get("content", "")
except:
pass
duration = int((time.time() - start) * 1000)
if content:
print_result("课程对话 (V2)", True, f"响应: {content[:100]}...", duration)
else:
print_result("课程对话 (V2)", True, "收到流式响应", duration)
else:
duration = int((time.time() - start) * 1000)
body = await response.aread()
print_result("课程对话 (V2)", False, f"HTTP {response.status_code}: {body.decode()[:200]}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("课程对话 (V2)", False, str(e), duration)
async def test_6_practice_scene():
"""测试6: 陪练场景提取功能"""
print_header("测试6: 陪练场景提取功能 (V2 引擎)")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
# 获取 token
token = await get_auth_token(client)
if not token:
print_result("陪练场景提取", False, "无法获取认证 token")
return
headers = {"Authorization": f"Bearer {token}"}
# 先查询课程
try:
response = await client.get(f"{BASE_URL}/courses", headers=headers)
if response.status_code != 200:
print_result("陪练场景提取 - 查询课程", False, f"HTTP {response.status_code}")
return
courses = response.json().get("data", {}).get("items", [])
if not courses:
print_result("陪练场景提取", False, "没有可用的课程")
return
course_id = courses[0].get("id")
print(f" 📝 使用课程ID: {course_id}")
except Exception as e:
print_result("陪练场景提取 - 查询课程", False, str(e))
return
# 测试陪练场景提取 API
start = time.time()
try:
response = await client.post(
f"{BASE_URL}/practice/extract-scene?engine=v2",
headers=headers,
json={
"course_id": course_id,
"scene_type": "consultation" # 咨询场景
}
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
scenes = data.get("data", {}).get("scenes", [])
print_result("陪练场景提取 (V2)", True, f"提取了 {len(scenes)} 个场景", duration)
else:
error_detail = response.json().get("detail", response.text[:200])
print_result("陪练场景提取 (V2)", False, f"HTTP {response.status_code}: {error_detail}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("陪练场景提取 (V2)", False, str(e), duration)
async def test_7_ability_analysis():
"""测试7: 能力分析功能"""
print_header("测试7: 能力分析功能 (V2 引擎)")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
# 获取 token
token = await get_auth_token(client)
if not token:
print_result("能力分析", False, "无法获取认证 token")
return
headers = {"Authorization": f"Bearer {token}"}
# 测试能力分析 API需要员工的言迹数据
start = time.time()
try:
# 先查询是否有员工数据
response = await client.get(f"{BASE_URL}/users/me", headers=headers)
if response.status_code != 200:
print_result("能力分析 - 获取用户信息", False, f"HTTP {response.status_code}")
return
user = response.json().get("data", {})
user_id = user.get("id")
print(f" 📝 当前用户ID: {user_id}")
# 测试能力分析 API
response = await client.get(
f"{BASE_URL}/ability/user/{user_id}/analysis?engine=v2",
headers=headers
)
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
data = response.json()
dimensions = data.get("data", {}).get("dimensions", [])
print_result("能力分析 (V2)", True, f"分析了 {len(dimensions)} 个能力维度", duration)
elif response.status_code == 404:
# 可能没有言迹数据
print_result("能力分析 (V2)", True, "接口正常(无言迹数据)", duration)
else:
error_detail = response.json().get("detail", response.text[:200])
print_result("能力分析 (V2)", False, f"HTTP {response.status_code}: {error_detail}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result("能力分析 (V2)", False, str(e), duration)
async def test_backend_health():
"""测试后端健康状态"""
print_header("后端服务健康检查")
backends = [
("演示版 (aiedu)", "http://localhost:8000"),
("瑞小美 (kpl)", "http://localhost:8001"),
("华尔倍丽 (hua)", "http://localhost:8010"),
("杨扬宠物 (yy)", "http://localhost:8011"),
("武汉禾丽 (hl)", "http://localhost:8012"),
("芯颜定制 (xy)", "http://localhost:8013"),
("飞沃 (fw)", "http://localhost:8014"),
("恩喜成都 (ex)", "http://localhost:8015"),
("陪练试用版 (pl)", "http://localhost:8020"),
]
async with httpx.AsyncClient(timeout=10) as client:
for name, url in backends:
start = time.time()
try:
response = await client.get(f"{url}/health")
duration = int((time.time() - start) * 1000)
if response.status_code == 200:
print_result(name, True, "健康", duration)
else:
print_result(name, False, f"HTTP {response.status_code}", duration)
except Exception as e:
duration = int((time.time() - start) * 1000)
print_result(name, False, str(e), duration)
def print_summary():
"""打印测试汇总"""
print_header("测试汇总")
total = len(test_results)
passed = sum(1 for r in test_results if r["success"])
failed = total - passed
print(f"\n 总计: {total} 项测试")
print(f" ✅ 通过: {passed}")
print(f" ❌ 失败: {failed}")
if failed > 0:
print("\n 失败项目:")
for r in test_results:
if not r["success"]:
print(f" - {r['name']}: {r['message']}")
# 计算平均延迟
durations = [r["duration_ms"] for r in test_results if r["duration_ms"] > 0]
if durations:
avg_duration = sum(durations) / len(durations)
print(f"\n 平均响应时间: {avg_duration:.0f}ms")
print("\n" + "=" * 60)
return failed == 0
async def main():
"""主测试入口"""
print("\n" + "=" * 60)
print(" 考培练系统 AI 功能完整测试")
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
# 检查后端健康状态
await test_backend_health()
# 运行所有测试
await test_1_ai_basic_connectivity()
await test_2_exam_generation()
await test_3_answer_judge()
await test_4_knowledge_analysis()
await test_5_course_chat()
await test_6_practice_scene()
await test_7_ability_analysis()
# 打印汇总
success = print_summary()
sys.exit(0 if success else 1)
if __name__ == "__main__":
asyncio.run(main())