Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

658 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
深度分析 API 路由
"""
import os
import json
import threading
import time
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import desc
from database import get_db, SessionLocal
from models import DeepAnalysis, PotentialCustomer, SystemSetting
from auth import get_current_user
router = APIRouter(prefix="/deep-analysis", tags=["deep-analysis"])
# 报告存储目录
REPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "analysis_reports")
os.makedirs(REPORTS_DIR, exist_ok=True)
# 后台任务锁和队列
task_lock = threading.Lock()
analysis_queue: List[dict] = []
current_task: Optional[dict] = None
stop_flag = False
def restore_pending_tasks():
"""服务启动时从数据库恢复等待中的任务"""
global analysis_queue
try:
db = SessionLocal()
pending_analyses = db.query(DeepAnalysis).filter(
DeepAnalysis.status == "pending"
).order_by(DeepAnalysis.id.asc()).all()
for analysis in pending_analyses:
analysis_queue.append({
"analysis_id": analysis.id,
"customer_id": analysis.customer_id,
})
print(f"[DeepAnalysis] 恢复 {len(analysis_queue)} 个等待中的任务到队列")
db.close()
except Exception as e:
print(f"[DeepAnalysis] 恢复任务失败: {e}")
# 启动时恢复队列
restore_pending_tasks()
# ============ Pydantic Models ============
class CreateAnalysisRequest(BaseModel):
customer_ids: List[int]
class AnalysisResponse(BaseModel):
id: int
customer_id: int
company_name: str
status: str
model_used: Optional[str]
created_at: str
completed_at: Optional[str]
class AnalysisDetailResponse(AnalysisResponse):
report_content: Optional[str]
error_message: Optional[str]
class ModelConfigRequest(BaseModel):
model: str # "kimi-k2.5" | "deepseek-chat"
api_key: str
class ModelConfigResponse(BaseModel):
model: str
api_key_masked: str # 脱敏显示
# ============ Helper Functions ============
def get_model_config(db: Session) -> dict:
"""获取模型配置"""
model = db.query(SystemSetting).filter(SystemSetting.key == "llm_model").first()
api_key = db.query(SystemSetting).filter(SystemSetting.key == "llm_api_key").first()
return {
"model": model.value if model else "deepseek-chat",
"api_key": api_key.value if api_key else "",
}
def mask_api_key(key: str) -> str:
"""脱敏显示API Key"""
if len(key) <= 8:
return "****"
return key[:4] + "****" + key[-4:]
def generate_report_filename(customer_id: int, company_name: str) -> str:
"""生成报告文件名"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_name = "".join(c for c in company_name if c.isalnum() or c in ('-', '_'))[:50]
return f"{customer_id}_{safe_name}_{timestamp}.md"
def build_analysis_prompt(customer: PotentialCustomer) -> str:
"""构建分析Prompt"""
# 整理企业信息
info_parts = []
if customer.name:
info_parts.append(f"公司名称: {customer.name}")
if customer.industry:
info_parts.append(f"行业: {customer.industry}")
if customer.country_cn:
info_parts.append(f"国家/地区: {customer.country_cn}")
if customer.employees:
info_parts.append(f"员工规模: {customer.employees}")
if customer.founded:
info_parts.append(f"成立时间: {customer.founded}")
if customer.address:
info_parts.append(f"地址: {customer.address}")
if customer.description:
info_parts.append(f"企业描述: {customer.description}")
if customer.website:
info_parts.append(f"官网: {customer.website}")
if customer.hs_entities:
products = ", ".join(customer.hs_entities[:10])
info_parts.append(f"主营产品: {products}")
if customer.annual_purchase_amount:
info_parts.append(f"年采购额: {customer.annual_purchase_amount}")
if customer.procurement_frequency:
info_parts.append(f"采购频次: {customer.procurement_frequency}")
company_info = "\n".join(info_parts) if info_parts else "暂无详细信息"
prompt = f"""请基于以下企业信息,使用商业画布(Business Model Canvas)的9个维度进行深度分析,评估该企业的潜在商业价值。
## 企业基本信息
{company_info}
## 分析要求
请从以下9个维度进行分析,每个维度需要提供:
1. 该维度的核心内容分析
2. 该企业的优势/特点
3. 潜在风险或关注点
4. 对该企业的评分(1-10分)
## 商业画布9个维度
### 1. 客户细分 (Customer Segments)
分析该企业面向的主要客户群体是谁?B2B还是B2C?客户特征如何?
### 2. 价值主张 (Value Propositions)
该企业为客户提供什么独特价值?解决什么痛点?
### 3. 渠道通路 (Channels)
通过什么渠道触达客户?销售渠道、营销渠道有哪些?
### 4. 客户关系 (Customer Relationships)
如何维护客户关系?是长期合作还是一次性交易?
### 5. 收入来源 (Revenue Streams)
主要收入来源是什么?盈利模式如何?
### 6. 核心资源 (Key Resources)
企业的核心资产是什么?技术、品牌、渠道还是人才?
### 7. 关键业务 (Key Activities)
企业最重要的业务活动是什么?生产、研发还是服务?
### 8. 重要合作 (Key Partnerships)
重要的供应商、合作伙伴有哪些?供应链稳定性如何?
### 9. 成本结构 (Cost Structure)
主要成本构成是什么?固定成本还是变动成本为主?
## 输出格式
请用Markdown格式输出,每个维度使用二级标题,包含上述4个方面的内容。最后请给出:
- 总体评分(1-100分)
- 合作建议(是否推荐作为潜在客户深度合作)
- 关键关注要点(3-5条)
"""
return prompt
def call_llm_api(prompt: str, model: str, api_key: str) -> str:
"""调用大模型API"""
try:
import openai
if model.startswith("kimi"):
base_url = "https://api.moonshot.cn/v1"
else:
base_url = "https://api.deepseek.com"
client = openai.OpenAI(api_key=api_key, base_url=base_url)
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一位专业的商业分析顾问,擅长基于商业画布模型进行企业价值评估。请用中文输出专业的分析报告。"},
{"role": "user", "content": prompt},
],
temperature=0.7,
max_tokens=8000,
)
return response.choices[0].message.content
except Exception as e:
raise Exception(f"LLM API调用失败: {str(e)}")
def extract_summary(report_content: str) -> dict:
"""从报告中提取摘要信息"""
summary = {
"overall_score": None,
"recommendation": None,
"key_points": [],
}
lines = report_content.split("\n")
for line in lines:
line = line.strip()
# 提取总体评分
if "总体评分" in line or "总分" in line:
import re
scores = re.findall(r'(\d{2,3})\s*分', line)
if scores:
summary["overall_score"] = int(scores[0])
# 提取合作建议
if "合作建议" in line and ":" in line:
summary["recommendation"] = line.split(":", 1)[1].strip()
return summary
# ============ Background Task Worker ============
def analysis_worker():
"""后台分析任务工作线程"""
global current_task, stop_flag
from database import SessionLocal
print("🔄 深度分析工作线程已启动")
while not stop_flag:
task = None
with task_lock:
if analysis_queue:
task = analysis_queue.pop(0)
if not task:
current_task = None
time.sleep(1)
continue
analysis_id = task["analysis_id"]
current_task = task
db = SessionLocal()
customer = None
try:
# 获取分析记录
analysis = db.query(DeepAnalysis).filter(DeepAnalysis.id == analysis_id).first()
if not analysis:
db.close()
continue
# 获取客户信息(必须先获取,才能更新状态)
customer = db.query(PotentialCustomer).filter(
PotentialCustomer.id == analysis.customer_id
).first()
if not customer:
analysis.status = "failed"
analysis.error_message = "客户信息不存在"
db.commit()
db.close()
continue
# 更新状态为处理中
analysis.status = "processing"
# 同步更新客户表状态
customer.analysis_status = "processing"
customer.last_analyzed_at = datetime.now()
db.commit()
# 获取模型配置
config = get_model_config(db)
if not config["api_key"]:
analysis.status = "failed"
analysis.error_message = "未配置API Key"
db.commit()
db.close()
print(f"⚠️ 分析任务 {analysis_id} 失败: 未配置API Key")
continue
print(f"🤖 开始分析客户: {customer.name} (ID: {customer.id})")
# 构建Prompt并调用API
prompt = build_analysis_prompt(customer)
report_content = call_llm_api(prompt, config["model"], config["api_key"])
# 保存报告文件
filename = generate_report_filename(customer.id, customer.name or "未知企业")
filepath = os.path.join(REPORTS_DIR, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(report_content)
# 提取摘要
canvas_summary = extract_summary(report_content)
# 更新分析记录
analysis.status = "completed"
analysis.report_file_path = filepath
analysis.model_used = config["model"]
analysis.canvas_summary = canvas_summary
analysis.completed_at = datetime.now()
# 同步更新客户表的分析状态(反范式设计,避免级联查询)
customer.analysis_status = "completed"
customer.analysis_count = (customer.analysis_count or 0) + 1
customer.last_analyzed_at = datetime.now()
db.commit()
print(f"✅ 客户分析完成: {customer.name}")
except Exception as e:
error_msg = str(e)
print(f"❌ 分析任务 {analysis_id} 失败: {error_msg}")
try:
analysis.status = "failed"
analysis.error_message = error_msg[:500] # 限制错误信息长度
# 同步更新客户表状态为失败
if customer:
customer.analysis_status = "failed"
customer.analysis_count = (customer.analysis_count or 0) + 1
customer.last_analyzed_at = datetime.now()
db.commit()
except Exception as commit_error:
print(f"❌ 保存失败状态时出错: {commit_error}")
finally:
try:
db.close()
except:
pass
# 启动后台线程
worker_thread = threading.Thread(target=analysis_worker, daemon=True)
worker_thread.start()
# ============ API Routes ============
@router.post("/create")
def create_analysis(
request: CreateAnalysisRequest,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""创建深度分析任务"""
created_ids = []
for customer_id in request.customer_ids:
# 检查客户是否存在
customer = db.query(PotentialCustomer).filter(
PotentialCustomer.id == customer_id
).first()
if not customer:
continue
with task_lock:
# 检查是否已在队列中或正在处理(去重)
already_queued = any(
t["customer_id"] == customer_id
for t in analysis_queue
)
already_processing = (
current_task and
current_task.get("customer_id") == customer_id
)
# 检查是否有未完成的分析记录
existing_pending = db.query(DeepAnalysis).filter(
DeepAnalysis.customer_id == customer_id,
DeepAnalysis.status.in_(["pending", "processing"])
).first()
if already_queued or already_processing or existing_pending:
continue # 跳过已在处理中的
# 创建新的分析记录(允许已有completed记录的再次分析)
analysis = DeepAnalysis(
customer_id=customer_id,
company_name=customer.name or "未知企业",
status="pending",
)
db.add(analysis)
db.commit()
db.refresh(analysis)
# 加入队列
analysis_queue.append({
"analysis_id": analysis.id,
"customer_id": customer_id,
})
created_ids.append(analysis.id)
# 同步更新客户表状态为 pending(反范式设计)
customer.analysis_status = "pending"
customer.last_analyzed_at = datetime.now()
db.commit()
return {
"success": True,
"created_count": len(created_ids),
"analysis_ids": created_ids,
"queued_count": len(analysis_queue),
}
@router.get("/list")
def list_analyses(
status: Optional[str] = None,
page: int = 1,
page_size: int = 20,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""获取深度分析列表(按创建时间倒序)"""
query = db.query(DeepAnalysis)
if status:
query = query.filter(DeepAnalysis.status == status)
total = query.count()
analyses = query.order_by(desc(DeepAnalysis.created_at)).offset(
(page - 1) * page_size
).limit(page_size).all()
return {
"total": total,
"page": page,
"page_size": page_size,
"items": [a.to_dict() for a in analyses],
}
@router.get("/detail/{analysis_id}")
def get_analysis_detail(
analysis_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""获取分析详情(包含报告内容)"""
analysis = db.query(DeepAnalysis).filter(DeepAnalysis.id == analysis_id).first()
if not analysis:
raise HTTPException(status_code=404, detail="分析记录不存在")
result = analysis.to_dict()
# 读取报告内容
if analysis.report_file_path and os.path.exists(analysis.report_file_path):
try:
with open(analysis.report_file_path, "r", encoding="utf-8") as f:
result["report_content"] = f.read()
except Exception as e:
result["report_content"] = f"读取报告失败: {str(e)}"
else:
result["report_content"] = None
return result
@router.get("/status/{customer_id}")
def get_customer_analysis_status(
customer_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""获取某个客户的分析状态(用于显示按钮状态)"""
# 查找最新的分析记录
analysis = db.query(DeepAnalysis).filter(
DeepAnalysis.customer_id == customer_id
).order_by(desc(DeepAnalysis.created_at)).first()
if not analysis:
return {"has_analysis": False, "status": None}
# 检查是否在队列中
with task_lock:
in_queue = any(
t["customer_id"] == customer_id
for t in analysis_queue
)
is_processing = (
current_task and
current_task.get("customer_id") == customer_id
)
return {
"has_analysis": analysis.status == "completed",
"status": analysis.status,
"in_queue": in_queue or is_processing,
"analysis_id": analysis.id,
"completed_at": analysis.completed_at.isoformat() if analysis.completed_at else None,
}
@router.post("/batch-status")
def get_batch_analysis_status(
request: dict,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""批量获取多个客户的分析状态(解决N+1查询问题)"""
customer_ids = request.get("customer_ids", [])
if not customer_ids:
return {}
# 一次性查询所有相关分析记录
analyses = db.query(DeepAnalysis).filter(
DeepAnalysis.customer_id.in_(customer_ids)
).order_by(desc(DeepAnalysis.created_at)).all()
# 按 customer_id 分组,取最新的一条
latest_analysis = {}
for a in analyses:
if a.customer_id not in latest_analysis:
latest_analysis[a.customer_id] = a
# 获取队列状态
with task_lock:
queue_customer_ids = {t["customer_id"] for t in analysis_queue}
processing_customer_id = current_task.get("customer_id") if current_task else None
# 构建结果(使用整数key,与前端customer.id类型一致)
result = {}
for cid in customer_ids:
analysis = latest_analysis.get(cid)
if not analysis:
result[cid] = {"has_analysis": False, "status": None}
else:
in_queue = cid in queue_customer_ids or cid == processing_customer_id
result[cid] = {
"has_analysis": analysis.status == "completed",
"status": analysis.status,
"in_queue": in_queue,
"analysis_id": analysis.id,
"completed_at": analysis.completed_at.isoformat() if analysis.completed_at else None,
}
return result
@router.delete("/delete/{analysis_id}")
def delete_analysis(
analysis_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""删除分析记录及报告文件"""
analysis = db.query(DeepAnalysis).filter(DeepAnalysis.id == analysis_id).first()
if not analysis:
raise HTTPException(status_code=404, detail="分析记录不存在")
# 删除报告文件
if analysis.report_file_path and os.path.exists(analysis.report_file_path):
try:
os.remove(analysis.report_file_path)
except Exception:
pass
db.delete(analysis)
db.commit()
return {"success": True}
@router.get("/queue/status")
def get_queue_status(
current_user=Depends(get_current_user),
):
"""获取队列状态"""
with task_lock:
return {
"queued_count": len(analysis_queue),
"is_processing": current_task is not None,
"current_task": current_task,
}
# ============ Model Config APIs ============
@router.get("/config/model")
def get_model_config_api(
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""获取模型配置(脱敏)"""
config = get_model_config(db)
return {
"model": config["model"],
"api_key_masked": mask_api_key(config["api_key"]),
}
@router.post("/config/model")
def set_model_config_api(
request: ModelConfigRequest,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""设置模型配置"""
# 验证模型名称
valid_models = ["kimi-k2.5", "kimi-latest", "deepseek-chat"]
if request.model not in valid_models:
raise HTTPException(status_code=400, detail=f"不支持的模型: {request.model}")
# 保存模型设置
model_setting = db.query(SystemSetting).filter(SystemSetting.key == "llm_model").first()
if not model_setting:
model_setting = SystemSetting(key="llm_model", description="LLM模型名称")
db.add(model_setting)
model_setting.value = request.model
# 保存API Key(这里简单存储,生产环境建议加密)
key_setting = db.query(SystemSetting).filter(SystemSetting.key == "llm_api_key").first()
if not key_setting:
key_setting = SystemSetting(key="llm_api_key", description="LLM API密钥")
db.add(key_setting)
key_setting.value = request.api_key
db.commit()
return {
"success": True,
"model": request.model,
"api_key_masked": mask_api_key(request.api_key),
}