hub-gif 7d219feb3c feat(pipeline): 策略页独立 strategy_config 与仅规则稿选项
新增 PipelineJob.strategy_config(默认 use_llm_default);GET /api/strategy-config-defaults/;PATCH 任务可保存策略偏好。策略生成页:本次仅规则稿复选框(默认关)、保存策略偏好、任务下拉去掉目录;第九章对齐仍由服务端默认处理。迁移 0021。

Made-with: Cursor
2026-04-17 16:41:20 +08:00

407 lines
16 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.

from django.db import models
class JobStatus(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
CANCELLED = "cancelled", "已终止"
PAUSED = "paused", "已暂停(待换 Cookie 续跑)"
class PipelineJob(models.Model):
platform = models.CharField(max_length=32, default="jd", db_index=True)
keyword = models.CharField(max_length=256)
max_skus = models.PositiveIntegerField(null=True, blank=True)
page_start = models.PositiveIntegerField(null=True, blank=True)
page_to = models.PositiveIntegerField(null=True, blank=True)
# 相对 data/JD 的子路径,或绝对路径(须在 Low GI/data/JD 下空则时间戳_关键词
pipeline_run_dir = models.TextField(blank=True, default="")
# Cookie二选一优先 cookie_text任务内写入临时文件再跑流水线
cookie_file_path = models.TextField(blank=True, default="")
cookie_text = models.TextField(blank=True, default="")
pvid = models.CharField(max_length=128, blank=True, default="")
request_delay = models.CharField(
max_length=64,
blank=True,
default="",
help_text='"30-60";空则沿用副本默认',
)
list_pages = models.CharField(
max_length=64,
blank=True,
default="",
help_text='评论分页,如 "1-2";空则沿用副本默认',
)
scenario_filter_enabled = models.BooleanField(null=True, blank=True)
# 竞品报告 / competitor-brief关注词、场景词组、外部市场表等JSON空对象=用爬虫脚本默认)
report_config = models.JSONField(default=dict, blank=True)
# 策略生成页独立配置(与 report_config 无关),如默认是否使用大模型润色等
strategy_config = models.JSONField(default=dict, blank=True)
status = models.CharField(
max_length=16,
choices=JobStatus.choices,
default=JobStatus.PENDING,
db_index=True,
)
cancellation_requested = models.BooleanField(default=False, db_index=True)
resume_from_checkpoint = models.BooleanField(default=False, db_index=True)
run_dir = models.TextField(blank=True, default="")
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"[{self.platform}] {self.keyword} ({self.status})"
class PipelineJobCheckpoint(models.Model):
"""Cookie 暂停续跑等场景的断点元数据(与任务一对一)。"""
job = models.OneToOneField(
PipelineJob,
on_delete=models.CASCADE,
related_name="checkpoint_row",
)
phase = models.CharField(max_length=32, db_index=True)
payload = models.JSONField(default=dict, blank=True)
hint_zh = models.TextField(blank=True, default="")
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-updated_at"]
def __str__(self) -> str:
return f"checkpoint job={self.job_id} phase={self.phase}"
class JdProduct(models.Model):
"""京东 SKU 主档:同一 ``platform`` + ``sku_id`` 唯一,多次抓取时覆盖为最新一行合并表数据。"""
platform = models.CharField(max_length=16, default="jd", db_index=True)
sku_id = models.CharField(max_length=64, db_index=True)
ware_id = models.CharField(max_length=64, blank=True, default="")
title = models.TextField(blank=True, default="")
detail_brand = models.CharField(max_length=512, blank=True, default="")
detail_price_final = models.CharField(max_length=128, blank=True, default="")
detail_category_path = models.TextField(blank=True, default="")
current_payload = models.JSONField(default=dict, blank=True)
last_job = models.ForeignKey(
PipelineJob,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="touched_products",
)
last_captured_at = models.DateTimeField(null=True, blank=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-updated_at"]
constraints = [
models.UniqueConstraint(
fields=["platform", "sku_id"],
name="uniq_pipeline_jdproduct_platform_sku",
),
]
def __str__(self) -> str:
return f"{self.platform}:{self.sku_id}"
class JdProductSnapshot(models.Model):
"""某次流水线任务下该 SKU 的整行快照,用于历史对比与回放。"""
product = models.ForeignKey(
JdProduct,
on_delete=models.CASCADE,
related_name="snapshots",
)
job = models.ForeignKey(
PipelineJob,
on_delete=models.CASCADE,
related_name="product_snapshots",
)
run_dir = models.TextField(blank=True, default="")
captured_at = models.DateTimeField(db_index=True)
payload = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ["-captured_at"]
constraints = [
models.UniqueConstraint(
fields=["product", "job"],
name="uniq_pipeline_jdproductsnapshot_product_job",
),
]
def __str__(self) -> str:
return f"{self.product_id} @ job {self.job_id}"
class JdLeafCategoryNorm(models.Model):
"""叶类目归一:与导出 ``leaf_category`` 原文一致(去空格),用于任务内类目筛选索引。"""
label = models.CharField(max_length=512, unique=True, db_index=True)
class Meta:
ordering = ["label"]
def __str__(self) -> str:
return self.label[:80]
class JdJobSearchRow(models.Model):
"""单次任务下 PC 搜索导出表一行,字段与 ``pc_search_export.csv`` 列一一对应(内部英文属性名)。"""
job = models.ForeignKey(
PipelineJob,
on_delete=models.CASCADE,
related_name="job_search_rows",
)
row_index = models.PositiveIntegerField()
item_id = models.TextField(blank=True, default="")
sku_id = models.TextField(blank=True, default="", db_index=True)
title = models.TextField(blank=True, default="")
price = models.TextField(blank=True, default="")
coupon_price = models.TextField(blank=True, default="")
original_price = models.TextField(blank=True, default="")
selling_point = models.TextField(blank=True, default="")
comment_sales_floor = models.TextField(blank=True, default="")
total_sales = models.TextField(blank=True, default="")
hot_list_rank = models.TextField(blank=True, default="")
comment_count = models.TextField(blank=True, default="")
shop_name = models.TextField(blank=True, default="")
shop_url = models.TextField(blank=True, default="")
shop_info_url = models.TextField(blank=True, default="")
location = models.TextField(blank=True, default="")
detail_url = models.TextField(blank=True, default="")
image = models.TextField(blank=True, default="")
seckill_info = models.TextField(blank=True, default="")
attributes = models.TextField(blank=True, default="")
leaf_category = models.TextField(blank=True, default="")
leaf_category_norm = models.ForeignKey(
JdLeafCategoryNorm,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="search_rows",
)
matrix_group_label = models.CharField(
max_length=80,
blank=True,
default="",
db_index=True,
verbose_name="类目",
help_text="与 §5 矩阵同源:由合并表商详路径解析;可按 SKU 从合并表回填",
)
price_value = models.FloatField(null=True, blank=True, db_index=True)
sales_sort_value = models.BigIntegerField(
null=True,
blank=True,
db_index=True,
help_text="从 total_sales / 销量楼层解析,供排序",
)
comment_count_sort_value = models.BigIntegerField(
null=True,
blank=True,
db_index=True,
help_text="从评价量文案解析,供排序",
)
platform = models.TextField(blank=True, default="")
keyword = models.TextField(blank=True, default="")
page = models.TextField(blank=True, default="")
class Meta:
ordering = ["row_index"]
constraints = [
models.UniqueConstraint(
fields=["job", "row_index"],
name="uniq_pipeline_jdjobsearchrow_job_idx",
),
]
indexes = [
models.Index(fields=["job", "sku_id"]),
models.Index(fields=["job", "leaf_category_norm"]),
models.Index(fields=["job", "matrix_group_label"]),
models.Index(fields=["job", "price_value"]),
models.Index(fields=["job", "sales_sort_value"]),
models.Index(fields=["job", "comment_count_sort_value"]),
]
def __str__(self) -> str:
return f"job{self.job_id} search#{self.row_index}"
class JdJobDetailRow(models.Model):
"""单次任务下 ``detail_ware_export.csv`` 一行leanskuId + 与合并表一致的商详子集)。"""
job = models.ForeignKey(
PipelineJob,
on_delete=models.CASCADE,
related_name="job_detail_rows",
)
row_index = models.PositiveIntegerField()
sku_id = models.TextField(blank=True, default="", db_index=True)
detail_brand = models.TextField(blank=True, default="")
detail_price_final = models.TextField(blank=True, default="")
detail_shop_name = models.TextField(blank=True, default="")
detail_category_path = models.TextField(blank=True, default="")
detail_product_attributes = models.TextField(blank=True, default="")
detail_body_ingredients = models.TextField(blank=True, default="")
buyer_ranking_line = models.TextField(blank=True, default="")
buyer_promo_text = models.TextField(blank=True, default="")
detail_price_value = models.FloatField(null=True, blank=True, db_index=True)
matrix_group_label = models.CharField(
max_length=80,
blank=True,
default="",
db_index=True,
verbose_name="类目",
help_text="与 §5 矩阵同源:由 detail_category_path 解析",
)
class Meta:
ordering = ["row_index"]
constraints = [
models.UniqueConstraint(
fields=["job", "row_index"],
name="uniq_pipeline_jdjobdetailrow_job_idx",
),
]
indexes = [
models.Index(fields=["job", "sku_id"]),
models.Index(fields=["job", "detail_price_value"]),
models.Index(fields=["job", "matrix_group_label"]),
]
def __str__(self) -> str:
return f"job{self.job_id} detail#{self.row_index}"
class JdJobCommentRow(models.Model):
"""单次任务下 ``comments_flat.csv`` 一行。"""
job = models.ForeignKey(
PipelineJob,
on_delete=models.CASCADE,
related_name="job_comment_rows",
)
row_index = models.PositiveIntegerField()
sku_id = models.TextField(blank=True, default="", db_index=True)
comment_id = models.TextField(blank=True, default="")
user_nick_name = models.TextField(blank=True, default="")
tag_comment_content = models.TextField(blank=True, default="")
comment_date = models.TextField(blank=True, default="")
buy_count_text = models.TextField(blank=True, default="")
large_pic_urls = models.TextField(blank=True, default="")
comment_score = models.TextField(blank=True, default="")
class Meta:
ordering = ["row_index"]
constraints = [
models.UniqueConstraint(
fields=["job", "row_index"],
name="uniq_pipeline_jdjobcommentrow_job_idx",
),
]
indexes = [
models.Index(fields=["job", "sku_id"]),
]
def __str__(self) -> str:
return f"job{self.job_id} cmt#{self.row_index}"
class JdJobMergedRow(models.Model):
"""单次任务下合并宽表一行lean搜索列 + 商详子集 + 评论摘要),与 ``keyword_pipeline_merged.csv`` 列一一对应。"""
job = models.ForeignKey(
PipelineJob,
on_delete=models.CASCADE,
related_name="job_merged_rows",
)
row_index = models.PositiveIntegerField()
pipeline_keyword = models.TextField(blank=True, default="")
sku_id = models.TextField(blank=True, default="", db_index=True)
ware_id = models.TextField(blank=True, default="")
title = models.TextField(blank=True, default="")
price = models.TextField(blank=True, default="")
coupon_price = models.TextField(blank=True, default="")
original_price = models.TextField(blank=True, default="")
selling_point = models.TextField(blank=True, default="")
hot_list_rank = models.TextField(blank=True, default="")
comment_fuzzy = models.TextField(blank=True, default="")
comment_sales_floor = models.TextField(blank=True, default="")
total_sales = models.TextField(blank=True, default="")
shop_name = models.TextField(blank=True, default="")
detail_url = models.TextField(blank=True, default="")
image = models.TextField(blank=True, default="")
attributes = models.TextField(blank=True, default="")
leaf_category = models.TextField(blank=True, default="")
leaf_category_norm = models.ForeignKey(
JdLeafCategoryNorm,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="merged_rows",
)
matrix_group_label = models.CharField(
max_length=80,
blank=True,
default="",
db_index=True,
verbose_name="类目",
help_text="与 §5 矩阵同源:由 detail_category_path 解析",
)
price_value = models.FloatField(null=True, blank=True, db_index=True)
sales_sort_value = models.BigIntegerField(
null=True,
blank=True,
db_index=True,
help_text="从 total_sales / 销量楼层解析,供排序",
)
comment_count_sort_value = models.BigIntegerField(
null=True,
blank=True,
db_index=True,
help_text="从 pipeline_comment_count 解析,供排序",
)
keyword = models.TextField(blank=True, default="")
page = models.TextField(blank=True, default="")
detail_brand = models.TextField(blank=True, default="")
detail_price_final = models.TextField(blank=True, default="")
detail_shop_name = models.TextField(blank=True, default="")
detail_category_path = models.TextField(blank=True, default="")
detail_product_attributes = models.TextField(blank=True, default="")
detail_body_ingredients = models.TextField(blank=True, default="")
buyer_ranking_line = models.TextField(blank=True, default="")
buyer_promo_text = models.TextField(blank=True, default="")
pipeline_comment_count = models.TextField(blank=True, default="")
comment_preview = models.TextField(blank=True, default="")
class Meta:
ordering = ["row_index"]
constraints = [
models.UniqueConstraint(
fields=["job", "row_index"],
name="uniq_pipeline_jdjobmergedrow_job_idx",
),
]
indexes = [
models.Index(fields=["job", "sku_id"]),
models.Index(fields=["job", "leaf_category_norm"]),
models.Index(fields=["job", "matrix_group_label"]),
models.Index(fields=["job", "price_value"]),
models.Index(fields=["job", "sales_sort_value"]),
models.Index(fields=["job", "comment_count_sort_value"]),
]
def __str__(self) -> str:
return f"job{self.job_id} merged#{self.row_index}"