hub-gif 6280e436d8 feat(pipeline): JD 流水线 CSV 纯中文表头与模块目录重构
- 统一 csv_schema 与搜索/合并/评价/商详导出为纯中文列名,入库与视图按中文表头解析

- 竞品分析报告兼容新旧表头;新增 csv_header_rewrite 与 rewrite_pipeline_csv_headers 管理命令

- 调整 pipeline 至 jd、llm、reporting、demos 子包并更新任务与测试引用

- 新增购买者优惠摘要抽取、合并表 regen/ingest 命令、0016 迁移及相关测试

Made-with: Cursor
2026-04-15 16:02:29 +08:00

152 lines
5.1 KiB
Python
Raw 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.

from __future__ import annotations
import os
import subprocess
import sys
import time
import traceback
from pathlib import Path
from django.conf import settings
from django.utils import timezone
from .cookie_paste import normalize_browser_cookie_paste
from .ingest import try_ingest_job_full
from .jd.runner import (
resolve_pipeline_run_directory_for_job,
try_write_competitor_report_if_merged_exists,
)
from .models import JobStatus, PipelineJob
def execute_job(job_id: int) -> None:
job = PipelineJob.objects.filter(pk=job_id).first()
if not job:
return
job.status = JobStatus.RUNNING
job.error_message = ""
job.save(update_fields=["status", "error_message", "updated_at"])
job.refresh_from_db()
if job.cancellation_requested:
job.status = JobStatus.CANCELLED
job.cancellation_requested = False
job.error_message = "已终止(任务开始后立即收到终止请求)。"
job.updated_at = timezone.now()
job.save(
update_fields=[
"status",
"cancellation_requested",
"error_message",
"updated_at",
],
)
PipelineJob.objects.filter(pk=job_id).update(cookie_text="")
return
cookie_temp: Path | None = None
try:
if job.platform != "jd":
raise ValueError(f"暂不支持平台: {job.platform}")
cookie_path_for_pipeline: str | None = None
_cookie_body = normalize_browser_cookie_paste(job.cookie_text or "")
if _cookie_body:
runtime_dir = Path(settings.BASE_DIR) / "runtime_cookies"
runtime_dir.mkdir(parents=True, exist_ok=True)
cookie_temp = (runtime_dir / f"job_{job_id}_cookie.txt").resolve()
cookie_temp.write_text(_cookie_body, encoding="utf-8")
cookie_path_for_pipeline = str(cookie_temp)
elif (job.cookie_file_path or "").strip():
cookie_path_for_pipeline = job.cookie_file_path.strip()
rc_cfg = job.report_config if isinstance(job.report_config, dict) else {}
run_dir_path = resolve_pipeline_run_directory_for_job(job)
run_dir_path.mkdir(parents=True, exist_ok=True)
manage_py = Path(settings.BASE_DIR) / "manage.py"
env = os.environ.copy()
env["PIPELINE_JOB_RUN_DIR"] = str(run_dir_path.resolve())
if cookie_path_for_pipeline:
env["PIPELINE_JOB_COOKIE_PATH"] = str(cookie_path_for_pipeline)
proc = subprocess.Popen(
[sys.executable, str(manage_py), "run_pipeline_job", str(job_id)],
cwd=str(Path(settings.BASE_DIR).resolve()),
env=env,
stdin=subprocess.DEVNULL,
)
user_terminated = False
while True:
if proc.poll() is not None:
break
time.sleep(0.25)
if PipelineJob.objects.filter(
pk=job_id, cancellation_requested=True
).exists():
user_terminated = True
proc.terminate()
break
if proc.poll() is None:
try:
proc.wait(timeout=25)
except subprocess.TimeoutExpired:
proc.kill()
try:
proc.wait(timeout=15)
except subprocess.TimeoutExpired:
pass
else:
proc.wait()
returncode = proc.returncode
if returncode is None:
returncode = -1
if user_terminated:
job.status = JobStatus.CANCELLED
job.run_dir = str(run_dir_path.resolve())
job.cancellation_requested = False
job.error_message = (
"已终止:已结束采集子进程(与在终端对脚本按 Ctrl+C 类似,可能留下部分文件)。"
)
try_write_competitor_report_if_merged_exists(
run_dir_path,
(job.keyword or "").strip(),
report_config=rc_cfg or None,
)
elif returncode == 0:
job.status = JobStatus.SUCCESS
job.run_dir = str(run_dir_path.resolve())
job.error_message = ""
job.cancellation_requested = False
else:
job.status = JobStatus.FAILED
job.run_dir = str(run_dir_path.resolve())
job.cancellation_requested = False
job.error_message = f"流水线子进程异常退出exit {returncode})。"
except Exception as e:
job.status = JobStatus.FAILED
job.cancellation_requested = False
job.error_message = f"{e}\n\n{traceback.format_exc()}"
job.updated_at = timezone.now()
job.save(
update_fields=[
"status",
"run_dir",
"error_message",
"cancellation_requested",
"updated_at",
],
)
if job.status == JobStatus.SUCCESS:
try_ingest_job_full(PipelineJob.objects.get(pk=job_id))
if cookie_temp is not None and cookie_temp.is_file():
try:
cookie_temp.unlink()
except OSError:
pass
PipelineJob.objects.filter(pk=job_id).update(cookie_text="")