354 lines
13 KiB
Python
354 lines
13 KiB
Python
"""
|
|
潜在客户导入服务
|
|
后台任务:分页调用 company API,逐个获取详情并存储到本地数据库
|
|
"""
|
|
import time
|
|
import threading
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, Any
|
|
from sqlalchemy.orm import Session
|
|
|
|
from database import SessionLocal
|
|
from models import ImportTask, PotentialCustomer, PotentialContact
|
|
from fumamx_cli import FumamxCLIWrapper
|
|
|
|
|
|
class ImportService:
|
|
"""导入服务单例类"""
|
|
_instance = None
|
|
_lock = threading.Lock()
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
with cls._lock:
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._init()
|
|
return cls._instance
|
|
|
|
def _init(self):
|
|
"""初始化"""
|
|
self.cli = FumamxCLIWrapper()
|
|
self.current_task_id: Optional[int] = None
|
|
self._cancel_event = threading.Event()
|
|
self._thread: Optional[threading.Thread] = None
|
|
|
|
def get_running_task(self) -> Optional[Dict]:
|
|
"""获取当前运行的任务"""
|
|
if self.current_task_id is None:
|
|
return None
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
task = db.query(ImportTask).filter(
|
|
ImportTask.id == self.current_task_id
|
|
).first()
|
|
if task and task.status == "running":
|
|
return task.to_dict()
|
|
else:
|
|
# 任务已完成或不存在
|
|
self.current_task_id = None
|
|
return None
|
|
finally:
|
|
db.close()
|
|
|
|
def get_latest_task(self) -> Optional[Dict]:
|
|
"""获取最后一次任务(无论状态)"""
|
|
db = SessionLocal()
|
|
try:
|
|
task = db.query(ImportTask).order_by(
|
|
ImportTask.created_at.desc()
|
|
).first()
|
|
return task.to_dict() if task else None
|
|
finally:
|
|
db.close()
|
|
|
|
def start_import(self, keyword: str, page_size: int = 10) -> Dict:
|
|
"""启动导入任务"""
|
|
# 检查是否有运行中的任务
|
|
if self.current_task_id is not None:
|
|
running = self.get_running_task()
|
|
if running:
|
|
return {
|
|
"success": False,
|
|
"error": "已有导入任务正在运行",
|
|
"task": running
|
|
}
|
|
|
|
# 创建新任务
|
|
db = SessionLocal()
|
|
try:
|
|
# 先测试搜索,获取总数量
|
|
search_result = self.cli.search_company(keyword, page=1, page_size=page_size)
|
|
if search_result.get("code") != "0":
|
|
return {
|
|
"success": False,
|
|
"error": f"搜索失败: {search_result.get('msg', '未知错误')}"
|
|
}
|
|
|
|
total_count = int(search_result.get("data", {}).get("total", 0))
|
|
if total_count == 0:
|
|
return {
|
|
"success": False,
|
|
"error": "未找到匹配的企业"
|
|
}
|
|
|
|
total_pages = (total_count + page_size - 1) // page_size
|
|
|
|
# 创建任务记录
|
|
task = ImportTask(
|
|
keyword=keyword,
|
|
status="pending",
|
|
total_pages=total_pages,
|
|
page_size=page_size,
|
|
total_count=total_count,
|
|
processed_count=0,
|
|
success_count=0,
|
|
failed_count=0,
|
|
duplicate_count=0
|
|
)
|
|
db.add(task)
|
|
db.commit()
|
|
db.refresh(task)
|
|
|
|
# 启动后台线程
|
|
self.current_task_id = task.id
|
|
self._cancel_event.clear()
|
|
self._thread = threading.Thread(
|
|
target=self._import_worker,
|
|
args=(task.id, keyword, total_pages, page_size),
|
|
daemon=True
|
|
)
|
|
self._thread.start()
|
|
|
|
return {
|
|
"success": True,
|
|
"task": task.to_dict()
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"启动失败: {str(e)}"
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
def cancel_import(self) -> Dict:
|
|
"""取消当前导入任务"""
|
|
if self.current_task_id is None:
|
|
return {
|
|
"success": False,
|
|
"error": "没有正在运行的任务"
|
|
}
|
|
|
|
self._cancel_event.set()
|
|
|
|
# 更新任务状态
|
|
db = SessionLocal()
|
|
try:
|
|
task = db.query(ImportTask).filter(
|
|
ImportTask.id == self.current_task_id
|
|
).first()
|
|
if task:
|
|
task.status = "cancelled"
|
|
task.completed_at = datetime.now()
|
|
db.commit()
|
|
return {
|
|
"success": True,
|
|
"task": task.to_dict()
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
return {"success": True}
|
|
|
|
def _import_worker(self, task_id: int, keyword: str, total_pages: int, page_size: int):
|
|
"""后台导入工作线程"""
|
|
db = SessionLocal()
|
|
error_logs = [] # 收集错误日志
|
|
|
|
try:
|
|
# 更新任务状态为运行中
|
|
task = db.query(ImportTask).filter(ImportTask.id == task_id).first()
|
|
if not task:
|
|
return
|
|
|
|
task.status = "running"
|
|
task.started_at = datetime.now()
|
|
db.commit()
|
|
|
|
# 逐页处理
|
|
for page in range(1, total_pages + 1):
|
|
# 检查取消标志
|
|
if self._cancel_event.is_set():
|
|
task.status = "cancelled"
|
|
task.completed_at = datetime.now()
|
|
task.error_msg = "\n".join(error_logs[-10:]) if error_logs else None # 保留最后10条错误
|
|
db.commit()
|
|
break
|
|
|
|
# 更新当前页码
|
|
task.current_page = page
|
|
db.commit()
|
|
|
|
try:
|
|
# 1. 搜索当前页企业列表
|
|
search_result = self.cli.search_company(keyword, page=page, page_size=page_size)
|
|
if search_result.get("code") != "0":
|
|
error_msg = f"第{page}页搜索失败: {search_result.get('msg', '未知错误')}"
|
|
error_logs.append(error_msg)
|
|
print(error_msg)
|
|
task.failed_count += page_size
|
|
task.processed_count += page_size
|
|
db.commit()
|
|
continue
|
|
|
|
companies = search_result.get("data", {}).get("results", [])
|
|
|
|
# 逐个处理企业
|
|
for company in companies:
|
|
# 检查取消标志
|
|
if self._cancel_event.is_set():
|
|
break
|
|
|
|
domain = company.get('domain', 'unknown')
|
|
try:
|
|
self._process_company(db, task, company, keyword)
|
|
task.success_count += 1
|
|
except Exception as e:
|
|
task.failed_count += 1
|
|
error_detail = f"[{domain}] {type(e).__name__}: {str(e)}"
|
|
error_logs.append(error_detail)
|
|
print(f"处理企业失败: {error_detail}")
|
|
# 实时更新错误信息到数据库(最多保留最近10条)
|
|
task.error_msg = "\n".join(error_logs[-10:])
|
|
|
|
task.processed_count += 1
|
|
db.commit()
|
|
|
|
# API 限速:每家企业处理完后等待 4 秒(1秒搜索 + 3个详情API各1秒)
|
|
time.sleep(4)
|
|
|
|
if self._cancel_event.is_set():
|
|
break
|
|
|
|
except Exception as e:
|
|
page_error = f"第{page}页处理失败: {type(e).__name__}: {str(e)}"
|
|
error_logs.append(page_error)
|
|
print(page_error)
|
|
task.failed_count += page_size # 粗略估算
|
|
task.processed_count += page_size
|
|
task.error_msg = "\n".join(error_logs[-10:])
|
|
db.commit()
|
|
|
|
# 每页之间额外等待
|
|
time.sleep(2)
|
|
|
|
# 任务完成
|
|
if task.status == "running":
|
|
task.status = "completed"
|
|
task.completed_at = datetime.now()
|
|
task.error_msg = "\n".join(error_logs[-10:]) if error_logs else None
|
|
db.commit()
|
|
|
|
except Exception as e:
|
|
task.status = "failed"
|
|
error_logs.append(f"任务异常: {type(e).__name__}: {str(e)}")
|
|
task.error_msg = "\n".join(error_logs[-10:])
|
|
task.completed_at = datetime.now()
|
|
db.commit()
|
|
finally:
|
|
self.current_task_id = None
|
|
db.close()
|
|
|
|
def _process_company(self, db: Session, task: ImportTask, company: Dict, keyword: str):
|
|
"""处理单个企业"""
|
|
external_id = company.get("id")
|
|
domain = company.get("domain")
|
|
|
|
if not external_id or not domain:
|
|
return
|
|
|
|
# 检查是否已存在(去重)
|
|
existing = db.query(PotentialCustomer).filter(
|
|
PotentialCustomer.domain == domain,
|
|
PotentialCustomer.external_id == external_id
|
|
).first()
|
|
|
|
if existing:
|
|
task.duplicate_count += 1
|
|
return
|
|
|
|
# 2. 获取企业详情
|
|
info_result = self.cli.get_company_info(external_id, domain)
|
|
info_data = info_result.get("data", {}) if info_result.get("code") == 0 else {}
|
|
|
|
# 3. 获取电话/图片信息(可选,失败不影响主流程)
|
|
try:
|
|
phone_result = self.cli.get_company_phones(domain)
|
|
except:
|
|
phone_result = {}
|
|
|
|
# 4. 获取联系人列表
|
|
contacts_result = self.cli.get_company_contacts(external_id, domain)
|
|
contacts_data = contacts_result.get("data", {}) if contacts_result.get("code") == 0 else {}
|
|
contacts_list = contacts_data.get("list") or []
|
|
|
|
# 创建企业记录
|
|
customer = PotentialCustomer(
|
|
external_id=external_id,
|
|
domain=domain,
|
|
name=company.get("title") or info_data.get("name"),
|
|
country=company.get("country") or info_data.get("country"),
|
|
country_en=company.get("countryEn") or info_data.get("countryEn"),
|
|
country_cn=company.get("countryCnName") or info_data.get("countryCn"),
|
|
industry=info_data.get("industry"),
|
|
employees=info_data.get("employees"),
|
|
founded=info_data.get("founded"),
|
|
address=info_data.get("address"),
|
|
phone=info_data.get("phone"),
|
|
description=company.get("description") or info_data.get("description"),
|
|
website=company.get("website"),
|
|
linkedin_url=info_data.get("linkedin_url"),
|
|
procurement_frequency=company.get("procurementFrequency"),
|
|
annual_purchase_amount=company.get("annualPurchaseAmount"),
|
|
hs_codes=company.get("hsCode") or [],
|
|
hs_entities=company.get("hsEntity") or [],
|
|
keyword=keyword,
|
|
source_score=company.get("score"),
|
|
source_db=company.get("sourceDb"),
|
|
import_task_id=task.id,
|
|
contact_count=len(contacts_list)
|
|
)
|
|
|
|
db.add(customer)
|
|
db.flush() # 获取 customer.id
|
|
|
|
# 创建联系人记录
|
|
for contact in contacts_list:
|
|
pc = PotentialContact(
|
|
customer_id=customer.id,
|
|
name=contact.get("name"),
|
|
position=contact.get("position"),
|
|
email=contact.get("email"),
|
|
phone=contact.get("phone") or contact.get("realWhatsApp"),
|
|
linkedin=contact.get("linkedin"),
|
|
facebook=contact.get("facebook"),
|
|
twitter=contact.get("twitter"),
|
|
whatsapp=contact.get("whatsapp"),
|
|
score=contact.get("score"),
|
|
is_executive=bool(contact.get("isExecutive")),
|
|
is_kp=bool(contact.get("isKp")),
|
|
is_purchase=bool(contact.get("isPurchase")),
|
|
email_check_status=contact.get("emailCheckStatus"),
|
|
whatsapp_status=contact.get("whatsAppStatus")
|
|
)
|
|
db.add(pc)
|
|
|
|
db.commit()
|
|
|
|
|
|
# 全局导入服务实例
|
|
import_service = ImportService()
|