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

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

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

View File

@@ -0,0 +1,274 @@
"""
Coze 服务层单元测试
"""
import asyncio
import pytest
from datetime import datetime
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from cozepy import ChatEventType
from app.services.ai.coze.service import CozeService, get_coze_service
from app.services.ai.coze.models import (
SessionType, MessageRole, ContentType, StreamEventType,
CreateSessionRequest, SendMessageRequest, EndSessionRequest,
CozeSession, CozeMessage, StreamEvent
)
from app.services.ai.coze.exceptions import CozeAPIError
@pytest.fixture
def coze_service():
"""创建测试用的服务实例"""
with patch("app.services.ai.coze.service.get_coze_client"):
service = CozeService()
service.bot_config = {
"course_chat": "chat-bot-id",
"training": "training-bot-id",
"exam": "exam-bot-id"
}
service.workspace_id = "test-workspace"
return service
@pytest.mark.asyncio
class TestCozeService:
"""测试 Coze 服务"""
async def test_create_course_chat_session(self, coze_service):
"""测试创建课程对话会话"""
# Mock Coze client
mock_conversation = Mock(id="conv-123")
coze_service.client.conversations.create = Mock(return_value=mock_conversation)
request = CreateSessionRequest(
session_type=SessionType.COURSE_CHAT,
user_id="user-123",
course_id="course-456"
)
response = await coze_service.create_session(request)
# 验证结果
assert response.conversation_id == "conv-123"
assert response.bot_id == "chat-bot-id"
assert isinstance(response.session_id, str)
assert isinstance(response.created_at, datetime)
# 验证会话已保存
session = coze_service._sessions[response.session_id]
assert session.session_type == SessionType.COURSE_CHAT
assert session.user_id == "user-123"
assert session.metadata["course_id"] == "course-456"
async def test_create_training_session(self, coze_service):
"""测试创建陪练会话"""
mock_conversation = Mock(id="conv-456")
coze_service.client.conversations.create = Mock(return_value=mock_conversation)
request = CreateSessionRequest(
session_type=SessionType.TRAINING,
user_id="user-789",
training_topic="客诉处理"
)
response = await coze_service.create_session(request)
assert response.conversation_id == "conv-456"
assert response.bot_id == "training-bot-id"
session = coze_service._sessions[response.session_id]
assert session.session_type == SessionType.TRAINING
assert session.metadata["training_topic"] == "客诉处理"
async def test_send_message_with_stream(self, coze_service):
"""测试发送消息(流式响应)"""
# 创建测试会话
session = CozeSession(
session_id="test-session",
conversation_id="conv-123",
session_type=SessionType.COURSE_CHAT,
user_id="user-123",
bot_id="chat-bot-id"
)
coze_service._sessions["test-session"] = session
coze_service._messages["test-session"] = []
# Mock 流式响应
mock_events = [
Mock(
event=ChatEventType.CONVERSATION_MESSAGE_DELTA,
conversation_id="conv-123",
message=Mock(content="Hello ")
),
Mock(
event=ChatEventType.CONVERSATION_MESSAGE_DELTA,
conversation_id="conv-123",
message=Mock(content="world!")
),
Mock(
event=ChatEventType.CONVERSATION_MESSAGE_COMPLETED,
conversation_id="conv-123",
message=Mock(content="Hello world!"),
usage={"tokens": 10}
)
]
coze_service.client.chat.stream = Mock(return_value=iter(mock_events))
request = SendMessageRequest(
session_id="test-session",
content="Hi there",
stream=True
)
# 收集事件
events = []
async for event in coze_service.send_message(request):
events.append(event)
# 验证事件
assert len(events) == 4 # 2 delta + 1 completed + 1 done
assert events[0].event == StreamEventType.MESSAGE_DELTA
assert events[0].content == "Hello "
assert events[1].event == StreamEventType.MESSAGE_DELTA
assert events[1].content == "world!"
assert events[2].event == StreamEventType.MESSAGE_COMPLETED
assert events[2].content == "Hello world!"
assert events[3].event == StreamEventType.DONE
# 验证消息已保存
messages = coze_service._messages["test-session"]
assert len(messages) == 2 # 用户消息 + 助手消息
assert messages[0].role == MessageRole.USER
assert messages[0].content == "Hi there"
assert messages[1].role == MessageRole.ASSISTANT
assert messages[1].content == "Hello world!"
async def test_send_message_error_handling(self, coze_service):
"""测试发送消息错误处理"""
# 不存在的会话
request = SendMessageRequest(
session_id="nonexistent",
content="Test"
)
with pytest.raises(CozeAPIError, match="会话不存在"):
async for _ in coze_service.send_message(request):
pass
async def test_end_session(self, coze_service):
"""测试结束会话"""
# 创建测试会话和消息
created_at = datetime.now()
session = CozeSession(
session_id="test-session",
conversation_id="conv-123",
session_type=SessionType.TRAINING,
user_id="user-123",
bot_id="training-bot-id",
created_at=created_at
)
coze_service._sessions["test-session"] = session
coze_service._messages["test-session"] = [
Mock(), Mock(), Mock() # 3条消息
]
request = EndSessionRequest(
reason="用户主动结束",
feedback={"rating": 5, "comment": "很有帮助"}
)
response = await coze_service.end_session("test-session", request)
# 验证响应
assert response.session_id == "test-session"
assert isinstance(response.ended_at, datetime)
assert response.message_count == 3
assert response.duration_seconds > 0
# 验证会话元数据
assert session.metadata["end_reason"] == "用户主动结束"
assert session.metadata["feedback"]["rating"] == 5
async def test_end_nonexistent_session(self, coze_service):
"""测试结束不存在的会话"""
request = EndSessionRequest()
with pytest.raises(CozeAPIError, match="会话不存在"):
await coze_service.end_session("nonexistent", request)
async def test_get_session_messages(self, coze_service):
"""测试获取会话消息历史"""
# 创建测试消息
messages = [
CozeMessage(
message_id=f"msg-{i}",
session_id="test-session",
role=MessageRole.USER if i % 2 == 0 else MessageRole.ASSISTANT,
content=f"Message {i}"
)
for i in range(10)
]
coze_service._messages["test-session"] = messages
# 测试分页
result = await coze_service.get_session_messages("test-session", limit=5, offset=2)
assert len(result) == 5
assert result[0].content == "Message 2"
assert result[4].content == "Message 6"
async def test_stream_with_card_content(self, coze_service):
"""测试流式响应中的卡片内容"""
# 创建测试会话
session = CozeSession(
session_id="test-session",
conversation_id="conv-123",
session_type=SessionType.EXAM,
user_id="user-123",
bot_id="exam-bot-id"
)
coze_service._sessions["test-session"] = session
coze_service._messages["test-session"] = []
# Mock 包含卡片的流式响应
mock_events = [
Mock(
event=ChatEventType.CONVERSATION_MESSAGE_DELTA,
conversation_id="conv-123",
message=Mock(content='{"question": "测试题目"}', content_type="card")
),
Mock(
event=ChatEventType.CONVERSATION_MESSAGE_COMPLETED,
conversation_id="conv-123",
message=Mock(content='{"question": "测试题目"}', content_type="card")
)
]
coze_service.client.chat.stream = Mock(return_value=iter(mock_events))
request = SendMessageRequest(
session_id="test-session",
content="生成一道考题"
)
events = []
async for event in coze_service.send_message(request):
events.append(event)
# 验证卡片类型被正确识别
assert events[0].content_type == ContentType.CARD
assert events[1].content_type == ContentType.CARD
# 验证消息保存时的内容类型
messages = coze_service._messages["test-session"]
assert messages[1].content_type == ContentType.CARD
def test_get_coze_service_singleton():
"""测试服务单例"""
service1 = get_coze_service()
service2 = get_coze_service()
assert service1 is service2