658 lines
21 KiB
Python
658 lines
21 KiB
Python
"""
|
||
深度分析 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),
|
||
}
|