Cline 4 dias atrás
pai
commit
d65e62a3c3

+ 446 - 0
API_TEST_GUIDE.md

@@ -0,0 +1,446 @@
+# 历史记录接口测试指南
+
+## 概述
+
+本指南提供了测试历史记录获取功能的完整步骤,包括两个服务的接口:
+- **shudao-chat-py** (端口 22001): 主聊天服务
+- **shudao-aichat** (端口 8000): AI报告生成服务
+
+---
+
+## 前置条件
+
+### 1. 获取认证 Token
+
+所有接口都需要在请求头中携带 Authorization token:
+
+```bash
+Authorization: Bearer {your_token}
+```
+
+### 2. 确认服务运行状态
+
+```bash
+# 检查 shudao-chat-py 服务
+curl http://localhost:22001/docs
+
+# 检查 shudao-aichat 服务
+curl http://localhost:8000/docs
+```
+
+---
+
+## 一、shudao-chat-py 服务接口测试
+
+### 1.1 获取历史对话列表
+
+**接口地址:** `GET /apiv1/get_history_record`
+
+**请求参数:**
+- `ai_conversation_id` (可选): 指定对话ID,0或不传表示获取所有
+- `business_type` (可选): 业务类型过滤
+  - 0: AI问答
+  - 1: PPT大纲
+  - 2: AI写作
+  - 3: 考试工坊
+
+**测试用例 1:获取所有历史记录**
+
+```bash
+curl -X GET "http://localhost:22001/apiv1/get_history_record?ai_conversation_id=0" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE"
+```
+
+**测试用例 2:按业务类型过滤(AI问答)**
+
+```bash
+curl -X GET "http://localhost:22001/apiv1/get_history_record?business_type=0" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE"
+```
+
+**测试用例 3:获取指定对话**
+
+```bash
+curl -X GET "http://localhost:22001/apiv1/get_history_record?ai_conversation_id=123" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE"
+```
+
+**预期响应格式:**
+
+```json
+{
+  "statusCode": 200,
+  "msg": "success",
+  "data": [
+    {
+      "id": 123,
+      "content": "用户问题内容摘要...",
+      "business_type": 0,
+      "exam_name": "",
+      "created_at": 1712345678
+    }
+  ]
+}
+```
+
+### 1.2 发送消息并创建对话
+
+**接口地址:** `POST /apiv1/stream/chat-with-db`
+
+**请求示例:**
+
+```bash
+curl -X POST "http://localhost:22001/apiv1/stream/chat-with-db" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "message": "什么是桥梁施工安全规范?",
+    "ai_conversation_id": 0,
+    "business_type": 0
+  }'
+```
+
+**说明:**
+- `ai_conversation_id=0` 会创建新对话
+- 返回的 SSE 流中会包含 `ai_conversation_id` 和 `ai_message_id`
+
+### 1.3 删除历史记录
+
+**接口地址:** `POST /apiv1/delete_history_record`
+
+**请求示例:**
+
+```bash
+curl -X POST "http://localhost:22001/apiv1/delete_history_record" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "ai_conversation_id": 123
+  }'
+```
+
+---
+
+## 二、shudao-aichat 服务接口测试
+
+### 2.1 完整报告生成流程(会创建对话记录)
+
+**接口地址:** `POST /api/v1/report/complete-flow`
+
+**请求示例:**
+
+```bash
+curl -X POST "http://localhost:8000/api/v1/report/complete-flow" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "user_question": "请介绍隧道施工安全管理规范",
+    "ai_conversation_id": 0,
+    "window_size": 3,
+    "n_results": 10,
+    "is_network_search_enabled": true,
+    "enable_online_model": false
+  }'
+```
+
+**参数说明:**
+- `user_question`: 用户问题(必填)
+- `user_id`: 用户ID(可选,不填会从token解析)
+- `ai_conversation_id`: 对话ID,0表示创建新对话
+- `window_size`: 滑动窗口大小,默认3
+- `n_results`: 检索结果数量,默认10
+- `is_network_search_enabled`: 是否启用网络搜索,默认true
+- `enable_online_model`: 是否启用在线模型,默认false
+
+**SSE 事件流说明:**
+
+```
+data: {"type": "initial", "user_id": "N7114089", "ai_conversation_id": 456, "ai_message_id": 789}
+
+data: {"type": "status", "message": "正在分析问题意图..."}
+
+data: {"type": "intent", "is_professional_question": true, "keywords": ["隧道", "施工安全"]}
+
+data: {"type": "documents", "total": 5}
+
+data: {"type": "report_start", "file_index": 1, "source_file": "隧道施工规范.pdf"}
+
+data: {"type": "report_chunk", "file_index": 1, "chunk": "报告内容..."}
+
+data: {"type": "completed", "message": "报告生成完成"}
+```
+
+### 2.2 更新 AI 消息内容
+
+**接口地址:** `POST /api/v1/report/update-ai-message`
+
+**请求示例:**
+
+```bash
+curl -X POST "http://localhost:8000/api/v1/report/update-ai-message" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "ai_message_id": 789,
+    "content": "完整的AI回复内容..."
+  }'
+```
+
+---
+
+## 三、完整测试流程
+
+### 步骤 1:创建新对话(shudao-aichat)
+
+```bash
+# 发起报告生成请求
+curl -X POST "http://localhost:8000/api/v1/report/complete-flow" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "user_question": "桥梁施工有哪些安全要求?",
+    "ai_conversation_id": 0
+  }'
+```
+
+**记录返回的:**
+- `ai_conversation_id`: 新创建的对话ID
+- `ai_message_id`: AI消息ID
+
+### 步骤 2:验证对话已创建(shudao-chat-py)
+
+```bash
+# 获取历史记录列表
+curl -X GET "http://localhost:22001/apiv1/get_history_record?business_type=0" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE"
+```
+
+**验证点:**
+- 返回的列表中应包含刚创建的对话
+- `user_id` 应为当前用户的 userCode(如 "N7114089")
+- 不应出现其他用户的对话记录
+
+### 步骤 3:继续对话
+
+```bash
+# 使用已有的 ai_conversation_id 继续对话
+curl -X POST "http://localhost:8000/api/v1/report/complete-flow" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "user_question": "具体有哪些规范文件?",
+    "ai_conversation_id": 456
+  }'
+```
+
+### 步骤 4:再次验证历史记录
+
+```bash
+# 获取指定对话的详情
+curl -X GET "http://localhost:22001/apiv1/get_history_record?ai_conversation_id=456" \
+  -H "Authorization: Bearer YOUR_TOKEN_HERE"
+```
+
+---
+
+## 四、常见问题排查
+
+### 问题 1:返回 401 未授权
+
+**原因:** Token 无效或过期
+
+**解决:**
+```bash
+# 检查 token 格式
+# 正确格式:Authorization: Bearer {token}
+# 或直接:Authorization: {token}
+```
+
+### 问题 2:历史记录为空
+
+**排查步骤:**
+
+1. 检查数据库中的 user_id 字段:
+```sql
+-- 查看 ai_conversation 表
+SELECT id, user_id, content, created_at 
+FROM ai_conversation 
+WHERE is_deleted = 0 
+ORDER BY created_at DESC 
+LIMIT 10;
+
+-- 查看 ai_message 表
+SELECT id, user_id, type, content, ai_conversation_id 
+FROM ai_message 
+WHERE is_deleted = 0 
+ORDER BY created_at DESC 
+LIMIT 10;
+```
+
+2. 验证 token 解析的 user_id:
+```bash
+# 查看服务日志
+tail -f shudao-aichat/logs/app.log | grep "从token解析user_id"
+```
+
+### 问题 3:看到其他用户的历史记录
+
+**原因:** user_id 被写死或解析错误(已修复)
+
+**验证修复:**
+```bash
+# 检查日志中的 user_id
+tail -f shudao-aichat/logs/app.log | grep "Complete Flow"
+```
+
+应该看到类似:
+```
+[Complete Flow] 从token解析user_id: N7114089
+[对话记录] 创建新conversation: id=456, user_id=N7114089
+```
+
+### 问题 4:前端 Vite 代理错误
+
+**错误信息:**
+```
+[vite] http proxy error: /apiv1/get_history_record
+Error: connect ECONNREFUSED 127.0.0.1:22001
+```
+
+**解决:**
+1. 确认 shudao-chat-py 服务正在运行:
+```bash
+curl http://localhost:22001/docs
+```
+
+2. 检查 vite.config.js 代理配置:
+```javascript
+proxy: {
+  '/apiv1': {
+    target: 'http://localhost:22001',
+    changeOrigin: true
+  }
+}
+```
+
+---
+
+## 五、数据库验证
+
+### 查询用户的所有对话
+
+```sql
+SELECT 
+    c.id,
+    c.user_id,
+    c.content,
+    c.business_type,
+    c.created_at,
+    COUNT(m.id) as message_count
+FROM ai_conversation c
+LEFT JOIN ai_message m ON m.ai_conversation_id = c.id AND m.is_deleted = 0
+WHERE c.user_id = 'N7114089' AND c.is_deleted = 0
+GROUP BY c.id
+ORDER BY c.created_at DESC;
+```
+
+### 查询对话的所有消息
+
+```sql
+SELECT 
+    id,
+    user_id,
+    type,
+    LEFT(content, 100) as content_preview,
+    created_at
+FROM ai_message
+WHERE ai_conversation_id = 456 AND is_deleted = 0
+ORDER BY created_at ASC;
+```
+
+---
+
+## 六、Postman 测试集合
+
+### 环境变量设置
+
+```json
+{
+  "base_url_chat": "http://localhost:22001",
+  "base_url_aichat": "http://localhost:8000",
+  "auth_token": "YOUR_TOKEN_HERE"
+}
+```
+
+### 测试用例集合
+
+1. **获取历史记录**
+   - Method: GET
+   - URL: `{{base_url_chat}}/apiv1/get_history_record?business_type=0`
+   - Headers: `Authorization: Bearer {{auth_token}}`
+
+2. **创建新对话**
+   - Method: POST
+   - URL: `{{base_url_aichat}}/api/v1/report/complete-flow`
+   - Headers: `Authorization: Bearer {{auth_token}}`
+   - Body: 见上文示例
+
+3. **删除对话**
+   - Method: POST
+   - URL: `{{base_url_chat}}/apiv1/delete_history_record`
+   - Headers: `Authorization: Bearer {{auth_token}}`
+   - Body: `{"ai_conversation_id": 123}`
+
+---
+
+## 七、性能测试建议
+
+### 并发测试
+
+```bash
+# 使用 Apache Bench 测试
+ab -n 100 -c 10 \
+  -H "Authorization: Bearer YOUR_TOKEN" \
+  http://localhost:22001/apiv1/get_history_record?business_type=0
+```
+
+### 负载测试
+
+```bash
+# 使用 wrk 测试
+wrk -t4 -c100 -d30s \
+  -H "Authorization: Bearer YOUR_TOKEN" \
+  http://localhost:22001/apiv1/get_history_record
+```
+
+---
+
+## 八、监控和日志
+
+### 查看实时日志
+
+```bash
+# shudao-chat-py 日志
+tail -f shudao-main/shudao-chat-py/logs/app.log
+
+# shudao-aichat 日志
+tail -f shudao-aichat/logs/app.log
+```
+
+### 关键日志关键词
+
+- `[Complete Flow]`: 报告生成流程
+- `[对话记录]`: 对话创建/更新
+- `从token解析user_id`: user_id 解析
+- `get_history_record`: 历史记录查询
+
+---
+
+## 总结
+
+修复后的系统应该满足:
+1. ✅ 每个用户只能看到自己的历史记录
+2. ✅ user_id 正确从 token 的 userCode 字段解析
+3. ✅ 对话记录正确关联到用户
+4. ✅ 前端可以正常获取历史消息列表
+
+如有问题,请检查日志并参考本指南的排查步骤。

+ 33 - 21
shudao-chat-py/routers/chat.py

@@ -108,7 +108,7 @@ async def send_deepseek_message(
         # 创建或获取对话
         if not data.conversation_id:
             conversation = AIConversation(
-                user_id=user.userCode,
+                user_id=user.user_id,
                 content=message[:100],
                 business_type=data.business_type,
                 exam_name=data.exam_name if data.business_type == 3 else "",
@@ -241,7 +241,7 @@ async def send_deepseek_message(
             "data": {
                 "conversation_id": conv_id,
                 "response": response_text,
-                "user_id": user.userCode,
+                "user_id": user.user_id,
                 "business_type": data.business_type,
             },
         }
@@ -251,21 +251,33 @@ async def send_deepseek_message(
 
 
 @router.get("/get_history_record")
-async def get_history_record(request: Request, db: Session = Depends(get_db)):
+async def get_history_record(
+    request: Request,
+    ai_conversation_id: int = 0,
+    business_type: int = None,
+    db: Session = Depends(get_db)
+):
     """获取对话历史记录列表"""
     user = request.state.user
     if not user:
         return {"statusCode": 401, "msg": "未授权"}
-    conversations = (
-        db.query(AIConversation)
-        .filter(
-            AIConversation.user_id == user.userCode,
-            AIConversation.is_deleted == 0,
-        )
-        .order_by(AIConversation.created_at.desc())
-        .limit(50)
-        .all()
+    
+    # 构建查询
+    query = db.query(AIConversation).filter(
+        AIConversation.user_id == user.user_id,
+        AIConversation.is_deleted == 0,
     )
+    
+    # 如果指定了 business_type,则按类型过滤
+    if business_type is not None:
+        query = query.filter(AIConversation.business_type == business_type)
+    
+    # 如果指定了 ai_conversation_id,则只返回该对话
+    if ai_conversation_id > 0:
+        query = query.filter(AIConversation.id == ai_conversation_id)
+    
+    conversations = query.order_by(AIConversation.created_at.desc()).limit(50).all()
+    
     return {
         "statusCode": 200,
         "msg": "success",
@@ -301,7 +313,7 @@ async def delete_conversation(
 
     db.query(AIConversation).filter(
         AIConversation.id == data.ai_conversation_id,
-        AIConversation.user_id == user.userCode,
+        AIConversation.user_id == user.user_id,
     ).update({"is_deleted": 1, "updated_at": int(time.time())})
 
     db.query(AIMessage).filter(
@@ -326,7 +338,7 @@ async def delete_history_record(
         return {"statusCode": 401, "msg": "未授权"}
     db.query(AIConversation).filter(
         AIConversation.id == data.ai_conversation_id,
-        AIConversation.user_id == user.userCode,
+        AIConversation.user_id == user.user_id,
     ).update({"is_deleted": 1, "updated_at": int(time.time())})
     db.commit()
     return {"statusCode": 200, "msg": "删除成功"}
@@ -426,7 +438,7 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
             # 1. 创建或获取对话
             if data.ai_conversation_id == 0:
                 conversation = AIConversation(
-                    user_id=user.userCode,
+                    user_id=user.user_id,
                     content=message[:100],
                     business_type=data.business_type,
                     exam_name=data.exam_name,
@@ -444,7 +456,7 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
             # 2. 插入用户消息
             user_msg = AIMessage(
                 ai_conversation_id=conv_id,
-                user_id=user.userCode,
+                user_id=user.user_id,
                 type="user",
                 content=message,
                 created_at=int(time.time()),
@@ -458,7 +470,7 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
             # 3. 插入 AI 占位消息
             ai_msg = AIMessage(
                 ai_conversation_id=conv_id,
-                user_id=user.userCode,
+                user_id=user.user_id,
                 type="ai",
                 content="",
                 prev_user_id=user_msg.id,
@@ -661,7 +673,7 @@ async def online_search(question: str, request: Request, db: Session = Depends(g
                 "max_text_len": 4000  # 最大文本长度
             },
             "response_mode": "blocking",
-            "user": getattr(user, "account", str(user.userCode)),
+            "user": getattr(user, "account", str(user.user_id)),
         }
 
         async with httpx.AsyncClient(timeout=30.0) as client:
@@ -744,7 +756,7 @@ async def intent_recognition(
         if data.save_to_db and intent_type in ("greeting", "问候", "faq", "常见问题"):
             if data.ai_conversation_id == 0:
                 conversation = AIConversation(
-                    user_id=user.userCode,
+                    user_id=user.user_id,
                     content=data.message[:100],
                     business_type=0,
                     created_at=int(time.time()),
@@ -760,7 +772,7 @@ async def intent_recognition(
 
             user_msg = AIMessage(
                 ai_conversation_id=conv_id,
-                user_id=user.userCode,
+                user_id=user.user_id,
                 type="user",
                 content=data.message,
                 created_at=int(time.time()),
@@ -772,7 +784,7 @@ async def intent_recognition(
 
             ai_msg = AIMessage(
                 ai_conversation_id=conv_id,
-                user_id=user.userCode,
+                user_id=user.user_id,
                 type="ai",
                 content=response_text,
                 prev_user_id=user_msg.id,

+ 3 - 3
shudao-chat-py/routers/hazard.py

@@ -86,12 +86,12 @@ async def hazard(
         )
         
         # 5. 上传结果图片到 OSS
-        result_filename = f"hazard_detection/{user.userCode}/{int(time.time())}.jpg"
+        result_filename = f"hazard_detection/{user.user_id}/{int(time.time())}.jpg"
         result_url = await oss_service.upload_bytes(result_image_bytes, result_filename)
         
         # 6. 插入 RecognitionRecord
         record = RecognitionRecord(
-            user_id=user.userCode,
+            user_id=user.user_id,
             scene_type=data.scene_type,
             original_image_url=data.image_url,
             recognition_image_url=result_url,
@@ -145,7 +145,7 @@ async def save_step(
         # 更新步骤
         affected = db.query(RecognitionRecord).filter(
             RecognitionRecord.id == data.record_id,
-            RecognitionRecord.user_id == user.userCode
+            RecognitionRecord.user_id == user.user_id
         ).update({
             "current_step": data.current_step,
             "updated_at": int(time.time())

+ 5 - 5
shudao-chat-py/routers/scene.py

@@ -102,13 +102,13 @@ async def get_history_recognition_record(request: Request, db: Session = Depends
     
     # 获取所有记录(不限制数量)
     records = db.query(RecognitionRecord).filter(
-        RecognitionRecord.user_id == user.userCode,
+        RecognitionRecord.user_id == user.user_id,
         RecognitionRecord.is_deleted == 0
     ).order_by(RecognitionRecord.updated_at.desc()).all()
     
     # 计算总数
     total = db.query(RecognitionRecord).filter(
-        RecognitionRecord.user_id == user.userCode,
+        RecognitionRecord.user_id == user.user_id,
         RecognitionRecord.is_deleted == 0
     ).count()
     
@@ -178,7 +178,7 @@ async def delete_recognition_record(data: DeleteRecognitionRequest, request: Req
     
     db.query(RecognitionRecord).filter(
         RecognitionRecord.id == data.recognition_id,
-        RecognitionRecord.user_id == user.userCode
+        RecognitionRecord.user_id == user.user_id
     ).update({
         "is_deleted": 1,
         "deleted_at": int(time.time())
@@ -231,7 +231,7 @@ async def get_latest_recognition_record(request: Request, db: Session = Depends(
         return {"statusCode": 401, "msg": "未授权"}
     
     record = db.query(RecognitionRecord).filter(
-        RecognitionRecord.user_id == user.userCode,
+        RecognitionRecord.user_id == user.user_id,
         RecognitionRecord.is_deleted == 0
     ).order_by(RecognitionRecord.created_at.desc()).first()
     
@@ -357,7 +357,7 @@ async def get_recognition_records(
     
     # 构建查询条件
     query = db.query(RecognitionRecord).filter(
-        RecognitionRecord.user_id == user.userCode,
+        RecognitionRecord.user_id == user.user_id,
         RecognitionRecord.is_deleted == 0
     )
     

+ 2 - 2
shudao-chat-py/routers/tracking.py

@@ -30,7 +30,7 @@ async def record_tracking(
     
     # 创建埋点记录
     record = TrackingRecord(
-        user_id=user.userCode,
+        user_id=user.user_id,
         api_path=data.api_path,
         api_name=data.api_name,
         method=request.method,
@@ -55,7 +55,7 @@ async def get_tracking_records(
     user = request.state.user
     
     records = db.query(TrackingRecord).filter(
-        TrackingRecord.user_id == user.userCode
+        TrackingRecord.user_id == user.user_id
     ).order_by(TrackingRecord.created_at.desc()).limit(limit).all()
     
     return {

+ 16 - 4
shudao-chat-py/utils/token.py

@@ -45,10 +45,22 @@ def verify_local_token(token: str) -> Optional[dict]:
                 decoded['username'] = username
             if 'account' not in decoded:
                 decoded['account'] = username
-            if 'id' not in decoded and 'user_id' in decoded:
-                decoded['id'] = decoded['user_id']
-            elif 'id' not in decoded and 'sub' in decoded:
-                decoded['id'] = decoded['sub']
+            
+            # 统一 user_id 字段(兼容 userCode)
+            if 'userCode' in decoded and 'user_id' not in decoded:
+                decoded['user_id'] = decoded['userCode']
+            elif 'user_id' in decoded and 'userCode' not in decoded:
+                decoded['userCode'] = decoded['user_id']
+            elif 'id' in decoded:
+                if 'user_id' not in decoded:
+                    decoded['user_id'] = decoded['id']
+                if 'userCode' not in decoded:
+                    decoded['userCode'] = decoded['id']
+            elif 'sub' in decoded:
+                if 'user_id' not in decoded:
+                    decoded['user_id'] = decoded['sub']
+                if 'userCode' not in decoded:
+                    decoded['userCode'] = decoded['sub']
 
             logger.info(f"[Token验证] 识别为有效 token: {username}")
             return decoded