mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-24 17:21:40 +08:00
refactor(dataset): 类目筛选改为报告矩阵细类名(report_group)
新增 matrix_group_label 与 jd_competitor_report 同源路径解析;API 使用 report_group 字符串;摘要返回 report_group_options;搜索行按 SKU 从合并表回填细类;导出含报告细类列;前端下拉展示饼干/米等名称。 Made-with: Cursor
This commit is contained in:
parent
f915653104
commit
d95189723c
@ -1,18 +1,35 @@
|
|||||||
"""库内数据浏览 API:排序、价格与类目筛选(查询参数解析与 QuerySet 变换)。"""
|
"""库内数据浏览 API:排序、价格与报告细类筛选(查询参数解析与 QuerySet 变换)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db.models import F, QuerySet
|
from django.db.models import F, Q, QuerySet
|
||||||
from django.db.models.expressions import OrderBy
|
from django.db.models.expressions import OrderBy
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
SEARCH_SORT_FIELDS = frozenset({"row_index", "price", "sku_id", "title", "leaf_category"})
|
SEARCH_SORT_FIELDS = frozenset(
|
||||||
|
{"row_index", "price", "sku_id", "title", "leaf_category", "matrix_group_label"}
|
||||||
|
)
|
||||||
DETAIL_SORT_FIELDS = frozenset(
|
DETAIL_SORT_FIELDS = frozenset(
|
||||||
{"row_index", "price", "sku_id", "detail_category_path", "detail_brand"}
|
{
|
||||||
|
"row_index",
|
||||||
|
"price",
|
||||||
|
"sku_id",
|
||||||
|
"detail_category_path",
|
||||||
|
"detail_brand",
|
||||||
|
"matrix_group_label",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
MERGED_SORT_FIELDS = frozenset(
|
MERGED_SORT_FIELDS = frozenset(
|
||||||
{"row_index", "price", "sku_id", "title", "leaf_category", "detail_category_path"}
|
{
|
||||||
|
"row_index",
|
||||||
|
"price",
|
||||||
|
"sku_id",
|
||||||
|
"title",
|
||||||
|
"leaf_category",
|
||||||
|
"detail_category_path",
|
||||||
|
"matrix_group_label",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -39,11 +56,9 @@ def price_bounds_from_request(request: Request) -> tuple[float | None, float | N
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def category_norm_id_from_request(request: Request) -> int | None:
|
def report_group_from_request(request: Request) -> str:
|
||||||
raw = (request.query_params.get("category_norm_id") or "").strip()
|
"""与 §5 矩阵一致的细类名(如「饼干」「米」);对应查询参数 ``report_group``。"""
|
||||||
if raw.isdigit():
|
return (request.query_params.get("report_group") or "").strip()
|
||||||
return int(raw)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def detail_category_q_from_request(request: Request) -> str:
|
def detail_category_q_from_request(request: Request) -> str:
|
||||||
@ -52,7 +67,7 @@ def detail_category_q_from_request(request: Request) -> str:
|
|||||||
|
|
||||||
def filter_echo(
|
def filter_echo(
|
||||||
*,
|
*,
|
||||||
category_norm_id: int | None,
|
report_group: str,
|
||||||
price_min: float | None,
|
price_min: float | None,
|
||||||
price_max: float | None,
|
price_max: float | None,
|
||||||
detail_category_q: str,
|
detail_category_q: str,
|
||||||
@ -60,7 +75,7 @@ def filter_echo(
|
|||||||
desc: bool,
|
desc: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"category_norm_id": category_norm_id,
|
"report_group": report_group or None,
|
||||||
"price_min": price_min,
|
"price_min": price_min,
|
||||||
"price_max": price_max,
|
"price_max": price_max,
|
||||||
"detail_category_q": detail_category_q or None,
|
"detail_category_q": detail_category_q or None,
|
||||||
@ -70,9 +85,9 @@ def filter_echo(
|
|||||||
|
|
||||||
|
|
||||||
def apply_search_filters(qs: QuerySet, request: Request) -> QuerySet:
|
def apply_search_filters(qs: QuerySet, request: Request) -> QuerySet:
|
||||||
cid = category_norm_id_from_request(request)
|
rg = report_group_from_request(request)
|
||||||
if cid is not None:
|
if rg:
|
||||||
qs = qs.filter(leaf_category_norm_id=cid)
|
qs = qs.filter(Q(matrix_group_label=rg) | Q(leaf_category=rg))
|
||||||
pmin, pmax = price_bounds_from_request(request)
|
pmin, pmax = price_bounds_from_request(request)
|
||||||
if pmin is not None:
|
if pmin is not None:
|
||||||
qs = qs.filter(price_value__gte=pmin)
|
qs = qs.filter(price_value__gte=pmin)
|
||||||
@ -94,6 +109,7 @@ def apply_search_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
|
|||||||
"sku_id": "sku_id",
|
"sku_id": "sku_id",
|
||||||
"title": "title",
|
"title": "title",
|
||||||
"leaf_category": "leaf_category",
|
"leaf_category": "leaf_category",
|
||||||
|
"matrix_group_label": "matrix_group_label",
|
||||||
}[sort]
|
}[sort]
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
OrderBy(F(field), descending=desc, nulls_last=True),
|
OrderBy(F(field), descending=desc, nulls_last=True),
|
||||||
@ -102,6 +118,9 @@ def apply_search_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
|
|||||||
|
|
||||||
|
|
||||||
def apply_detail_filters(qs: QuerySet, request: Request) -> QuerySet:
|
def apply_detail_filters(qs: QuerySet, request: Request) -> QuerySet:
|
||||||
|
rg = report_group_from_request(request)
|
||||||
|
if rg:
|
||||||
|
qs = qs.filter(matrix_group_label=rg)
|
||||||
q = detail_category_q_from_request(request)
|
q = detail_category_q_from_request(request)
|
||||||
if q:
|
if q:
|
||||||
qs = qs.filter(detail_category_path__icontains=q)
|
qs = qs.filter(detail_category_path__icontains=q)
|
||||||
@ -126,6 +145,7 @@ def apply_detail_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
|
|||||||
"sku_id": "sku_id",
|
"sku_id": "sku_id",
|
||||||
"detail_category_path": "detail_category_path",
|
"detail_category_path": "detail_category_path",
|
||||||
"detail_brand": "detail_brand",
|
"detail_brand": "detail_brand",
|
||||||
|
"matrix_group_label": "matrix_group_label",
|
||||||
}[sort]
|
}[sort]
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
OrderBy(F(field), descending=desc, nulls_last=True),
|
OrderBy(F(field), descending=desc, nulls_last=True),
|
||||||
@ -134,9 +154,9 @@ def apply_detail_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
|
|||||||
|
|
||||||
|
|
||||||
def apply_merged_filters(qs: QuerySet, request: Request) -> QuerySet:
|
def apply_merged_filters(qs: QuerySet, request: Request) -> QuerySet:
|
||||||
cid = category_norm_id_from_request(request)
|
rg = report_group_from_request(request)
|
||||||
if cid is not None:
|
if rg:
|
||||||
qs = qs.filter(leaf_category_norm_id=cid)
|
qs = qs.filter(matrix_group_label=rg)
|
||||||
q = detail_category_q_from_request(request)
|
q = detail_category_q_from_request(request)
|
||||||
if q:
|
if q:
|
||||||
qs = qs.filter(detail_category_path__icontains=q)
|
qs = qs.filter(detail_category_path__icontains=q)
|
||||||
@ -162,6 +182,7 @@ def apply_merged_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
|
|||||||
"title": "title",
|
"title": "title",
|
||||||
"leaf_category": "leaf_category",
|
"leaf_category": "leaf_category",
|
||||||
"detail_category_path": "detail_category_path",
|
"detail_category_path": "detail_category_path",
|
||||||
|
"matrix_group_label": "matrix_group_label",
|
||||||
}[sort]
|
}[sort]
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
OrderBy(F(field), descending=desc, nulls_last=True),
|
OrderBy(F(field), descending=desc, nulls_last=True),
|
||||||
|
|||||||
@ -14,6 +14,8 @@ from .csv_schema import (
|
|||||||
from .models import JdJobCommentRow, JdJobDetailRow, JdJobMergedRow, JdJobSearchRow, PipelineJob
|
from .models import JdJobCommentRow, JdJobDetailRow, JdJobMergedRow, JdJobSearchRow, PipelineJob
|
||||||
from .row_serialize import COMMENT_FIELDS_ORDER, DETAIL_FIELDS_ORDER
|
from .row_serialize import COMMENT_FIELDS_ORDER, DETAIL_FIELDS_ORDER
|
||||||
|
|
||||||
|
MATRIX_GROUP_COLUMN = {"key": "matrix_group_label", "label": "报告细类(§5矩阵)"}
|
||||||
|
|
||||||
|
|
||||||
def _is_nonempty(val) -> bool:
|
def _is_nonempty(val) -> bool:
|
||||||
if val is None:
|
if val is None:
|
||||||
@ -62,7 +64,13 @@ def nonempty_merged_fields_for_job(job: PipelineJob) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def search_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
def search_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
||||||
return [{"key": k, "label": JD_SEARCH_CSV_HEADERS[k]} for k in nonempty_search_keys_for_job(job)]
|
cols = [
|
||||||
|
{"key": k, "label": JD_SEARCH_CSV_HEADERS[k]}
|
||||||
|
for k in nonempty_search_keys_for_job(job)
|
||||||
|
]
|
||||||
|
if JdJobSearchRow.objects.filter(job=job).exclude(matrix_group_label="").exists():
|
||||||
|
cols.append(dict(MATRIX_GROUP_COLUMN))
|
||||||
|
return cols
|
||||||
|
|
||||||
|
|
||||||
def _detail_field_to_csv_col(field: str) -> str:
|
def _detail_field_to_csv_col(field: str) -> str:
|
||||||
@ -73,10 +81,13 @@ def _detail_field_to_csv_col(field: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def detail_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
def detail_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
||||||
return [
|
cols = [
|
||||||
{"key": f, "label": _detail_field_to_csv_col(f)}
|
{"key": f, "label": _detail_field_to_csv_col(f)}
|
||||||
for f in nonempty_detail_fields_for_job(job)
|
for f in nonempty_detail_fields_for_job(job)
|
||||||
]
|
]
|
||||||
|
if JdJobDetailRow.objects.filter(job=job).exclude(matrix_group_label="").exists():
|
||||||
|
cols.append(dict(MATRIX_GROUP_COLUMN))
|
||||||
|
return cols
|
||||||
|
|
||||||
|
|
||||||
def _comment_field_to_csv_col(field: str) -> str:
|
def _comment_field_to_csv_col(field: str) -> str:
|
||||||
@ -94,21 +105,30 @@ def comment_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def merged_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
def merged_columns_for_api(job: PipelineJob) -> list[dict[str, str]]:
|
||||||
return [
|
cols = [
|
||||||
{"key": k, "label": MERGED_FIELD_TO_CSV_HEADER[k]}
|
{"key": k, "label": MERGED_FIELD_TO_CSV_HEADER[k]}
|
||||||
for k in nonempty_merged_fields_for_job(job)
|
for k in nonempty_merged_fields_for_job(job)
|
||||||
]
|
]
|
||||||
|
if JdJobMergedRow.objects.filter(job=job).exclude(matrix_group_label="").exists():
|
||||||
|
cols.append(dict(MATRIX_GROUP_COLUMN))
|
||||||
|
return cols
|
||||||
|
|
||||||
|
|
||||||
def search_export_headers(job: PipelineJob) -> list[str]:
|
def search_export_headers(job: PipelineJob) -> list[str]:
|
||||||
keys = nonempty_search_keys_for_job(job)
|
keys = nonempty_search_keys_for_job(job)
|
||||||
return ["id", "row_index"] + [JD_SEARCH_CSV_HEADERS[k] for k in keys]
|
h = ["id", "row_index"] + [JD_SEARCH_CSV_HEADERS[k] for k in keys]
|
||||||
|
if JdJobSearchRow.objects.filter(job=job).exclude(matrix_group_label="").exists():
|
||||||
|
h.append(MATRIX_GROUP_COLUMN["label"])
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
def detail_export_headers(job: PipelineJob) -> list[str]:
|
def detail_export_headers(job: PipelineJob) -> list[str]:
|
||||||
fields = set(nonempty_detail_fields_for_job(job))
|
fields = set(nonempty_detail_fields_for_job(job))
|
||||||
cols = [c for c in DETAIL_CSV_COLUMNS if DETAIL_CSV_TO_FIELD[c] in fields]
|
cols = [c for c in DETAIL_CSV_COLUMNS if DETAIL_CSV_TO_FIELD[c] in fields]
|
||||||
return ["id", "row_index"] + cols
|
h = ["id", "row_index"] + cols
|
||||||
|
if JdJobDetailRow.objects.filter(job=job).exclude(matrix_group_label="").exists():
|
||||||
|
h.append(MATRIX_GROUP_COLUMN["label"])
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
def comment_export_headers(job: PipelineJob) -> list[str]:
|
def comment_export_headers(job: PipelineJob) -> list[str]:
|
||||||
@ -119,4 +139,7 @@ def comment_export_headers(job: PipelineJob) -> list[str]:
|
|||||||
|
|
||||||
def merged_export_headers(job: PipelineJob) -> list[str]:
|
def merged_export_headers(job: PipelineJob) -> list[str]:
|
||||||
keys = nonempty_merged_fields_for_job(job)
|
keys = nonempty_merged_fields_for_job(job)
|
||||||
return ["id", "row_index"] + [MERGED_FIELD_TO_CSV_HEADER[k] for k in keys]
|
h = ["id", "row_index"] + [MERGED_FIELD_TO_CSV_HEADER[k] for k in keys]
|
||||||
|
if JdJobMergedRow.objects.filter(job=job).exclude(matrix_group_label="").exists():
|
||||||
|
h.append(MATRIX_GROUP_COLUMN["label"])
|
||||||
|
return h
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from .csv_schema import (
|
|||||||
MERGED_FIELD_TO_CSV_HEADER,
|
MERGED_FIELD_TO_CSV_HEADER,
|
||||||
)
|
)
|
||||||
from .dataset_nonempty import (
|
from .dataset_nonempty import (
|
||||||
|
MATRIX_GROUP_COLUMN,
|
||||||
comment_export_headers,
|
comment_export_headers,
|
||||||
detail_export_headers,
|
detail_export_headers,
|
||||||
merged_export_headers,
|
merged_export_headers,
|
||||||
@ -34,20 +35,30 @@ from .row_serialize import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _search_row_csv_dict(r: JdJobSearchRow, internal_keys: list[str]) -> dict[str, Any]:
|
def _search_row_csv_dict(
|
||||||
|
r: JdJobSearchRow, internal_keys: list[str], headers: list[str]
|
||||||
|
) -> dict[str, Any]:
|
||||||
d = search_row_to_dict(r)
|
d = search_row_to_dict(r)
|
||||||
out: dict[str, Any] = {"id": d["id"], "row_index": d["row_index"]}
|
out: dict[str, Any] = {"id": d["id"], "row_index": d["row_index"]}
|
||||||
for k in internal_keys:
|
for k in internal_keys:
|
||||||
out[JD_SEARCH_CSV_HEADERS[k]] = d.get(k, "")
|
out[JD_SEARCH_CSV_HEADERS[k]] = d.get(k, "")
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
if zh in headers:
|
||||||
|
out[zh] = d.get("matrix_group_label", "")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _detail_row_csv_dict(r: JdJobDetailRow, csv_cols: list[str]) -> dict[str, Any]:
|
def _detail_row_csv_dict(
|
||||||
|
r: JdJobDetailRow, csv_cols: list[str], headers: list[str]
|
||||||
|
) -> dict[str, Any]:
|
||||||
d = detail_row_to_dict(r)
|
d = detail_row_to_dict(r)
|
||||||
out: dict[str, Any] = {"id": d["id"], "row_index": d["row_index"]}
|
out: dict[str, Any] = {"id": d["id"], "row_index": d["row_index"]}
|
||||||
for col in csv_cols:
|
for col in csv_cols:
|
||||||
fn = DETAIL_CSV_TO_FIELD[col]
|
fn = DETAIL_CSV_TO_FIELD[col]
|
||||||
out[col] = d.get(fn, "")
|
out[col] = d.get(fn, "")
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
if zh in headers:
|
||||||
|
out[zh] = d.get("matrix_group_label", "")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@ -60,11 +71,16 @@ def _comment_row_csv_dict(r: JdJobCommentRow, csv_cols: list[str]) -> dict[str,
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _merged_row_csv_dict(r: JdJobMergedRow, internal_keys: list[str]) -> dict[str, Any]:
|
def _merged_row_csv_dict(
|
||||||
|
r: JdJobMergedRow, internal_keys: list[str], headers: list[str]
|
||||||
|
) -> dict[str, Any]:
|
||||||
d = merged_row_to_dict(r)
|
d = merged_row_to_dict(r)
|
||||||
out: dict[str, Any] = {"id": d["id"], "row_index": d["row_index"]}
|
out: dict[str, Any] = {"id": d["id"], "row_index": d["row_index"]}
|
||||||
for k in internal_keys:
|
for k in internal_keys:
|
||||||
out[MERGED_FIELD_TO_CSV_HEADER[k]] = d.get(k, "")
|
out[MERGED_FIELD_TO_CSV_HEADER[k]] = d.get(k, "")
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
if zh in headers:
|
||||||
|
out[zh] = d.get("matrix_group_label", "")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@ -98,20 +114,30 @@ def _prune_merged_dict(d: dict[str, Any], fields: list[str]) -> dict[str, Any]:
|
|||||||
|
|
||||||
def _rows_as_list_search(job: PipelineJob) -> list[dict[str, Any]]:
|
def _rows_as_list_search(job: PipelineJob) -> list[dict[str, Any]]:
|
||||||
keys = nonempty_search_keys_for_job(job)
|
keys = nonempty_search_keys_for_job(job)
|
||||||
|
extra_mg = JdJobSearchRow.objects.filter(job=job).exclude(matrix_group_label="").exists()
|
||||||
qs = JdJobSearchRow.objects.filter(job=job)
|
qs = JdJobSearchRow.objects.filter(job=job)
|
||||||
return [
|
out: list[dict[str, Any]] = []
|
||||||
_prune_search_dict(search_row_to_dict(obj), keys)
|
for obj in qs.order_by("row_index").iterator(chunk_size=400):
|
||||||
for obj in qs.order_by("row_index").iterator(chunk_size=400)
|
d = search_row_to_dict(obj)
|
||||||
]
|
row = _prune_search_dict(d, keys)
|
||||||
|
if extra_mg:
|
||||||
|
row["matrix_group_label"] = d.get("matrix_group_label", "")
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _rows_as_list_detail(job: PipelineJob) -> list[dict[str, Any]]:
|
def _rows_as_list_detail(job: PipelineJob) -> list[dict[str, Any]]:
|
||||||
fields = nonempty_detail_fields_for_job(job)
|
fields = nonempty_detail_fields_for_job(job)
|
||||||
|
extra_mg = JdJobDetailRow.objects.filter(job=job).exclude(matrix_group_label="").exists()
|
||||||
qs = JdJobDetailRow.objects.filter(job=job)
|
qs = JdJobDetailRow.objects.filter(job=job)
|
||||||
return [
|
out: list[dict[str, Any]] = []
|
||||||
_prune_detail_dict(detail_row_to_dict(obj), fields)
|
for obj in qs.order_by("row_index").iterator(chunk_size=400):
|
||||||
for obj in qs.order_by("row_index").iterator(chunk_size=400)
|
d = detail_row_to_dict(obj)
|
||||||
]
|
row = _prune_detail_dict(d, fields)
|
||||||
|
if extra_mg:
|
||||||
|
row["matrix_group_label"] = d.get("matrix_group_label", "")
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _rows_as_list_comment(job: PipelineJob) -> list[dict[str, Any]]:
|
def _rows_as_list_comment(job: PipelineJob) -> list[dict[str, Any]]:
|
||||||
@ -125,11 +151,16 @@ def _rows_as_list_comment(job: PipelineJob) -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
def _rows_as_list_merged(job: PipelineJob) -> list[dict[str, Any]]:
|
def _rows_as_list_merged(job: PipelineJob) -> list[dict[str, Any]]:
|
||||||
fields = nonempty_merged_fields_for_job(job)
|
fields = nonempty_merged_fields_for_job(job)
|
||||||
|
extra_mg = JdJobMergedRow.objects.filter(job=job).exclude(matrix_group_label="").exists()
|
||||||
qs = JdJobMergedRow.objects.filter(job=job)
|
qs = JdJobMergedRow.objects.filter(job=job)
|
||||||
return [
|
out: list[dict[str, Any]] = []
|
||||||
_prune_merged_dict(merged_row_to_dict(obj), fields)
|
for obj in qs.order_by("row_index").iterator(chunk_size=400):
|
||||||
for obj in qs.order_by("row_index").iterator(chunk_size=400)
|
d = merged_row_to_dict(obj)
|
||||||
]
|
row = _prune_merged_dict(d, fields)
|
||||||
|
if extra_mg:
|
||||||
|
row["matrix_group_label"] = d.get("matrix_group_label", "")
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def build_json_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
def build_json_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
||||||
@ -178,18 +209,21 @@ def _write_csv_from_qs(
|
|||||||
def build_csv_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
def build_csv_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
||||||
if kind == "search":
|
if kind == "search":
|
||||||
sk = nonempty_search_keys_for_job(job)
|
sk = nonempty_search_keys_for_job(job)
|
||||||
|
headers = search_export_headers(job)
|
||||||
text = _write_csv_from_qs(
|
text = _write_csv_from_qs(
|
||||||
qs=JdJobSearchRow.objects.filter(job=job),
|
qs=JdJobSearchRow.objects.filter(job=job),
|
||||||
headers=search_export_headers(job),
|
headers=headers,
|
||||||
row_fn=lambda o, _sk=sk: _search_row_csv_dict(o, _sk),
|
row_fn=lambda o, _sk=sk, _h=headers: _search_row_csv_dict(o, _sk, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_search.csv"
|
name = f"job_{job.id}_search.csv"
|
||||||
elif kind == "detail":
|
elif kind == "detail":
|
||||||
dcols = [c for c in detail_export_headers(job) if c not in ("id", "row_index")]
|
headers = detail_export_headers(job)
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
dcols = [c for c in headers if c not in ("id", "row_index", zh)]
|
||||||
text = _write_csv_from_qs(
|
text = _write_csv_from_qs(
|
||||||
qs=JdJobDetailRow.objects.filter(job=job),
|
qs=JdJobDetailRow.objects.filter(job=job),
|
||||||
headers=detail_export_headers(job),
|
headers=headers,
|
||||||
row_fn=lambda o, _dc=dcols: _detail_row_csv_dict(o, _dc),
|
row_fn=lambda o, _dc=dcols, _h=headers: _detail_row_csv_dict(o, _dc, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_detail.csv"
|
name = f"job_{job.id}_detail.csv"
|
||||||
elif kind == "comments":
|
elif kind == "comments":
|
||||||
@ -202,22 +236,28 @@ def build_csv_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
|||||||
name = f"job_{job.id}_comments.csv"
|
name = f"job_{job.id}_comments.csv"
|
||||||
elif kind == "all":
|
elif kind == "all":
|
||||||
sk = nonempty_search_keys_for_job(job)
|
sk = nonempty_search_keys_for_job(job)
|
||||||
dcols = [c for c in detail_export_headers(job) if c not in ("id", "row_index")]
|
sheaders = search_export_headers(job)
|
||||||
|
dheaders = detail_export_headers(job)
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
dcols = [c for c in dheaders if c not in ("id", "row_index", zh)]
|
||||||
ccols = [c for c in comment_export_headers(job) if c not in ("id", "row_index")]
|
ccols = [c for c in comment_export_headers(job) if c not in ("id", "row_index")]
|
||||||
mk = nonempty_merged_fields_for_job(job)
|
mk = nonempty_merged_fields_for_job(job)
|
||||||
|
mheaders = merged_export_headers(job)
|
||||||
parts = [
|
parts = [
|
||||||
"# search",
|
"# search",
|
||||||
_write_csv_from_qs(
|
_write_csv_from_qs(
|
||||||
qs=JdJobSearchRow.objects.filter(job=job),
|
qs=JdJobSearchRow.objects.filter(job=job),
|
||||||
headers=search_export_headers(job),
|
headers=sheaders,
|
||||||
row_fn=lambda o, _sk=sk: _search_row_csv_dict(o, _sk),
|
row_fn=lambda o, _sk=sk, _h=sheaders: _search_row_csv_dict(o, _sk, _h),
|
||||||
),
|
),
|
||||||
"",
|
"",
|
||||||
"# detail",
|
"# detail",
|
||||||
_write_csv_from_qs(
|
_write_csv_from_qs(
|
||||||
qs=JdJobDetailRow.objects.filter(job=job),
|
qs=JdJobDetailRow.objects.filter(job=job),
|
||||||
headers=detail_export_headers(job),
|
headers=dheaders,
|
||||||
row_fn=lambda o, _dc=dcols: _detail_row_csv_dict(o, _dc),
|
row_fn=lambda o, _dc=dcols, _h=dheaders: _detail_row_csv_dict(
|
||||||
|
o, _dc, _h
|
||||||
|
),
|
||||||
),
|
),
|
||||||
"",
|
"",
|
||||||
"# comments",
|
"# comments",
|
||||||
@ -230,18 +270,19 @@ def build_csv_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
|||||||
"# merged",
|
"# merged",
|
||||||
_write_csv_from_qs(
|
_write_csv_from_qs(
|
||||||
qs=JdJobMergedRow.objects.filter(job=job),
|
qs=JdJobMergedRow.objects.filter(job=job),
|
||||||
headers=merged_export_headers(job),
|
headers=mheaders,
|
||||||
row_fn=lambda o, _mk=mk: _merged_row_csv_dict(o, _mk),
|
row_fn=lambda o, _mk=mk, _h=mheaders: _merged_row_csv_dict(o, _mk, _h),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
text = "\n".join(parts)
|
text = "\n".join(parts)
|
||||||
name = f"job_{job.id}_all.csv"
|
name = f"job_{job.id}_all.csv"
|
||||||
elif kind == "merged":
|
elif kind == "merged":
|
||||||
mk = nonempty_merged_fields_for_job(job)
|
mk = nonempty_merged_fields_for_job(job)
|
||||||
|
headers = merged_export_headers(job)
|
||||||
text = _write_csv_from_qs(
|
text = _write_csv_from_qs(
|
||||||
qs=JdJobMergedRow.objects.filter(job=job),
|
qs=JdJobMergedRow.objects.filter(job=job),
|
||||||
headers=merged_export_headers(job),
|
headers=headers,
|
||||||
row_fn=lambda o, _mk=mk: _merged_row_csv_dict(o, _mk),
|
row_fn=lambda o, _mk=mk, _h=headers: _merged_row_csv_dict(o, _mk, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_merged.csv"
|
name = f"job_{job.id}_merged.csv"
|
||||||
else:
|
else:
|
||||||
@ -262,22 +303,25 @@ def build_xlsx_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
|||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "search"[:31]
|
ws.title = "search"[:31]
|
||||||
sk = nonempty_search_keys_for_job(job)
|
sk = nonempty_search_keys_for_job(job)
|
||||||
|
sheaders = search_export_headers(job)
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
ws,
|
ws,
|
||||||
search_export_headers(job),
|
sheaders,
|
||||||
JdJobSearchRow.objects.filter(job=job),
|
JdJobSearchRow.objects.filter(job=job),
|
||||||
lambda o, _sk=sk: _search_row_csv_dict(o, _sk),
|
lambda o, _sk=sk, _h=sheaders: _search_row_csv_dict(o, _sk, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_search.xlsx"
|
name = f"job_{job.id}_search.xlsx"
|
||||||
elif kind == "detail":
|
elif kind == "detail":
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "detail"[:31]
|
ws.title = "detail"[:31]
|
||||||
dcols = [c for c in detail_export_headers(job) if c not in ("id", "row_index")]
|
dheaders = detail_export_headers(job)
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
dcols = [c for c in dheaders if c not in ("id", "row_index", zh)]
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
ws,
|
ws,
|
||||||
detail_export_headers(job),
|
dheaders,
|
||||||
JdJobDetailRow.objects.filter(job=job),
|
JdJobDetailRow.objects.filter(job=job),
|
||||||
lambda o, _dc=dcols: _detail_row_csv_dict(o, _dc),
|
lambda o, _dc=dcols, _h=dheaders: _detail_row_csv_dict(o, _dc, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_detail.xlsx"
|
name = f"job_{job.id}_detail.xlsx"
|
||||||
elif kind == "comments":
|
elif kind == "comments":
|
||||||
@ -293,23 +337,27 @@ def build_xlsx_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
|||||||
name = f"job_{job.id}_comments.xlsx"
|
name = f"job_{job.id}_comments.xlsx"
|
||||||
elif kind == "all":
|
elif kind == "all":
|
||||||
sk = nonempty_search_keys_for_job(job)
|
sk = nonempty_search_keys_for_job(job)
|
||||||
dcols = [c for c in detail_export_headers(job) if c not in ("id", "row_index")]
|
sheaders = search_export_headers(job)
|
||||||
|
dheaders = detail_export_headers(job)
|
||||||
|
zh = MATRIX_GROUP_COLUMN["label"]
|
||||||
|
dcols = [c for c in dheaders if c not in ("id", "row_index", zh)]
|
||||||
ccols = [c for c in comment_export_headers(job) if c not in ("id", "row_index")]
|
ccols = [c for c in comment_export_headers(job) if c not in ("id", "row_index")]
|
||||||
mk = nonempty_merged_fields_for_job(job)
|
mk = nonempty_merged_fields_for_job(job)
|
||||||
|
mheaders = merged_export_headers(job)
|
||||||
ws1 = wb.active
|
ws1 = wb.active
|
||||||
ws1.title = "search"[:31]
|
ws1.title = "search"[:31]
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
ws1,
|
ws1,
|
||||||
search_export_headers(job),
|
sheaders,
|
||||||
JdJobSearchRow.objects.filter(job=job),
|
JdJobSearchRow.objects.filter(job=job),
|
||||||
lambda o, _sk=sk: _search_row_csv_dict(o, _sk),
|
lambda o, _sk=sk, _h=sheaders: _search_row_csv_dict(o, _sk, _h),
|
||||||
)
|
)
|
||||||
ws2 = wb.create_sheet("detail"[:31])
|
ws2 = wb.create_sheet("detail"[:31])
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
ws2,
|
ws2,
|
||||||
detail_export_headers(job),
|
dheaders,
|
||||||
JdJobDetailRow.objects.filter(job=job),
|
JdJobDetailRow.objects.filter(job=job),
|
||||||
lambda o, _dc=dcols: _detail_row_csv_dict(o, _dc),
|
lambda o, _dc=dcols, _h=dheaders: _detail_row_csv_dict(o, _dc, _h),
|
||||||
)
|
)
|
||||||
ws3 = wb.create_sheet("comments"[:31])
|
ws3 = wb.create_sheet("comments"[:31])
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
@ -321,20 +369,21 @@ def build_xlsx_bytes(*, job: PipelineJob, kind: str) -> tuple[bytes, str]:
|
|||||||
ws4 = wb.create_sheet("merged"[:31])
|
ws4 = wb.create_sheet("merged"[:31])
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
ws4,
|
ws4,
|
||||||
merged_export_headers(job),
|
mheaders,
|
||||||
JdJobMergedRow.objects.filter(job=job),
|
JdJobMergedRow.objects.filter(job=job),
|
||||||
lambda o, _mk=mk: _merged_row_csv_dict(o, _mk),
|
lambda o, _mk=mk, _h=mheaders: _merged_row_csv_dict(o, _mk, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_all.xlsx"
|
name = f"job_{job.id}_all.xlsx"
|
||||||
elif kind == "merged":
|
elif kind == "merged":
|
||||||
mk = nonempty_merged_fields_for_job(job)
|
mk = nonempty_merged_fields_for_job(job)
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "merged"[:31]
|
ws.title = "merged"[:31]
|
||||||
|
mheaders = merged_export_headers(job)
|
||||||
_append_sheet(
|
_append_sheet(
|
||||||
ws,
|
ws,
|
||||||
merged_export_headers(job),
|
mheaders,
|
||||||
JdJobMergedRow.objects.filter(job=job),
|
JdJobMergedRow.objects.filter(job=job),
|
||||||
lambda o, _mk=mk: _merged_row_csv_dict(o, _mk),
|
lambda o, _mk=mk, _h=mheaders: _merged_row_csv_dict(o, _mk, _h),
|
||||||
)
|
)
|
||||||
name = f"job_{job.id}_merged.xlsx"
|
name = f"job_{job.id}_merged.xlsx"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -29,12 +29,12 @@ from .csv_schema import (
|
|||||||
search_csv_effective_total_sales,
|
search_csv_effective_total_sales,
|
||||||
strip_buyer_ranking_line_prefix,
|
strip_buyer_ranking_line_prefix,
|
||||||
)
|
)
|
||||||
|
from .matrix_group_label import matrix_group_label_from_detail_path
|
||||||
from .models import (
|
from .models import (
|
||||||
JdJobCommentRow,
|
JdJobCommentRow,
|
||||||
JdJobDetailRow,
|
JdJobDetailRow,
|
||||||
JdJobMergedRow,
|
JdJobMergedRow,
|
||||||
JdJobSearchRow,
|
JdJobSearchRow,
|
||||||
JdLeafCategoryNorm,
|
|
||||||
JdProduct,
|
JdProduct,
|
||||||
JdProductSnapshot,
|
JdProductSnapshot,
|
||||||
PipelineJob,
|
PipelineJob,
|
||||||
@ -119,6 +119,33 @@ def _bulk_create_in_chunks(model, objects: list[Any]) -> None:
|
|||||||
model.objects.bulk_create(objects[i : i + BULK_CHUNK])
|
model.objects.bulk_create(objects[i : i + BULK_CHUNK])
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_search_rows_matrix_labels(
|
||||||
|
job: PipelineJob, merged_kw_list: list[tuple[int, dict[str, str]]]
|
||||||
|
) -> None:
|
||||||
|
"""按 SKU 将合并表解析出的报告细类回填到搜索行(与 §5 矩阵口径一致)。"""
|
||||||
|
sku_to_mg: dict[str, str] = {}
|
||||||
|
for _, kw in merged_kw_list:
|
||||||
|
sk = (kw.get("sku_id") or "").strip()
|
||||||
|
if not sk:
|
||||||
|
continue
|
||||||
|
mg = matrix_group_label_from_detail_path(kw.get("detail_category_path") or "")
|
||||||
|
if mg:
|
||||||
|
sku_to_mg[sk] = mg
|
||||||
|
if not sku_to_mg:
|
||||||
|
return
|
||||||
|
chunk: list[JdJobSearchRow] = []
|
||||||
|
for r in JdJobSearchRow.objects.filter(job=job).iterator(chunk_size=400):
|
||||||
|
sk = (r.sku_id or "").strip()
|
||||||
|
if sk and sk in sku_to_mg:
|
||||||
|
r.matrix_group_label = sku_to_mg[sk]
|
||||||
|
chunk.append(r)
|
||||||
|
if len(chunk) >= 400:
|
||||||
|
JdJobSearchRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
chunk.clear()
|
||||||
|
if chunk:
|
||||||
|
JdJobSearchRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
|
||||||
|
|
||||||
def _run_dir(job: PipelineJob) -> Path:
|
def _run_dir(job: PipelineJob) -> Path:
|
||||||
return Path(job.run_dir or "").expanduser().resolve()
|
return Path(job.run_dir or "").expanduser().resolve()
|
||||||
|
|
||||||
@ -174,32 +201,12 @@ def ingest_job_dataset_rows(job: PipelineJob) -> dict[str, Any]:
|
|||||||
if not search_rows and search_path.is_file() is False:
|
if not search_rows and search_path.is_file() is False:
|
||||||
pass
|
pass
|
||||||
search_kw_list: list[tuple[int, dict[str, str]]] = []
|
search_kw_list: list[tuple[int, dict[str, str]]] = []
|
||||||
leaf_labels: set[str] = set()
|
|
||||||
for i, row in enumerate(search_rows):
|
for i, row in enumerate(search_rows):
|
||||||
_normalize_search_csv_total_sales(row)
|
_normalize_search_csv_total_sales(row)
|
||||||
kw = _search_row_kwargs(row)
|
kw = _search_row_kwargs(row)
|
||||||
search_kw_list.append((i, kw))
|
search_kw_list.append((i, kw))
|
||||||
lc = (kw.get("leaf_category") or "").strip()[:512]
|
|
||||||
if lc:
|
|
||||||
leaf_labels.add(lc)
|
|
||||||
norm_map: dict[str, JdLeafCategoryNorm] = {}
|
|
||||||
if leaf_labels:
|
|
||||||
have = set(
|
|
||||||
JdLeafCategoryNorm.objects.filter(label__in=leaf_labels).values_list(
|
|
||||||
"label", flat=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
missing = [JdLeafCategoryNorm(label=l) for l in leaf_labels if l not in have]
|
|
||||||
if missing:
|
|
||||||
JdLeafCategoryNorm.objects.bulk_create(missing, ignore_conflicts=True)
|
|
||||||
norm_map = {
|
|
||||||
n.label: n
|
|
||||||
for n in JdLeafCategoryNorm.objects.filter(label__in=leaf_labels)
|
|
||||||
}
|
|
||||||
s_objs: list[JdJobSearchRow] = []
|
s_objs: list[JdJobSearchRow] = []
|
||||||
for i, kw in search_kw_list:
|
for i, kw in search_kw_list:
|
||||||
lc = (kw.get("leaf_category") or "").strip()[:512]
|
|
||||||
norm = norm_map.get(lc) if lc else None
|
|
||||||
pv = effective_list_price_value(
|
pv = effective_list_price_value(
|
||||||
kw.get("coupon_price"), kw.get("price"), kw.get("original_price")
|
kw.get("coupon_price"), kw.get("price"), kw.get("original_price")
|
||||||
)
|
)
|
||||||
@ -207,7 +214,7 @@ def ingest_job_dataset_rows(job: PipelineJob) -> dict[str, Any]:
|
|||||||
JdJobSearchRow(
|
JdJobSearchRow(
|
||||||
job=job,
|
job=job,
|
||||||
row_index=i,
|
row_index=i,
|
||||||
leaf_category_norm=norm,
|
matrix_group_label="",
|
||||||
price_value=pv,
|
price_value=pv,
|
||||||
**kw,
|
**kw,
|
||||||
)
|
)
|
||||||
@ -221,9 +228,14 @@ def ingest_job_dataset_rows(job: PipelineJob) -> dict[str, Any]:
|
|||||||
for i, row in enumerate(detail_rows):
|
for i, row in enumerate(detail_rows):
|
||||||
kw = _detail_row_kwargs(row)
|
kw = _detail_row_kwargs(row)
|
||||||
dpv = float_price_from_cell(kw.get("detail_price_final"))
|
dpv = float_price_from_cell(kw.get("detail_price_final"))
|
||||||
|
mg = matrix_group_label_from_detail_path(kw.get("detail_category_path") or "")
|
||||||
d_objs.append(
|
d_objs.append(
|
||||||
JdJobDetailRow(
|
JdJobDetailRow(
|
||||||
job=job, row_index=i, detail_price_value=dpv, **kw
|
job=job,
|
||||||
|
row_index=i,
|
||||||
|
matrix_group_label=mg,
|
||||||
|
detail_price_value=dpv,
|
||||||
|
**kw,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_bulk_create_in_chunks(JdJobDetailRow, d_objs)
|
_bulk_create_in_chunks(JdJobDetailRow, d_objs)
|
||||||
@ -241,34 +253,13 @@ def ingest_job_dataset_rows(job: PipelineJob) -> dict[str, Any]:
|
|||||||
merged_path = run_dir / FILE_MERGED_CSV
|
merged_path = run_dir / FILE_MERGED_CSV
|
||||||
merged_rows = _read_csv_rows(merged_path) if merged_path.is_file() else []
|
merged_rows = _read_csv_rows(merged_path) if merged_path.is_file() else []
|
||||||
merged_kw_list: list[tuple[int, dict[str, str]]] = []
|
merged_kw_list: list[tuple[int, dict[str, str]]] = []
|
||||||
leaf_labels_m: set[str] = set()
|
|
||||||
for i, row in enumerate(merged_rows):
|
for i, row in enumerate(merged_rows):
|
||||||
_normalize_merged_csv_total_sales(row)
|
_normalize_merged_csv_total_sales(row)
|
||||||
kw = _merged_row_kwargs(row)
|
kw = _merged_row_kwargs(row)
|
||||||
merged_kw_list.append((i, kw))
|
merged_kw_list.append((i, kw))
|
||||||
lc = (kw.get("leaf_category") or "").strip()[:512]
|
|
||||||
if lc:
|
|
||||||
leaf_labels_m.add(lc)
|
|
||||||
norm_map_m: dict[str, JdLeafCategoryNorm] = {}
|
|
||||||
if leaf_labels_m:
|
|
||||||
have_m = set(
|
|
||||||
JdLeafCategoryNorm.objects.filter(label__in=leaf_labels_m).values_list(
|
|
||||||
"label", flat=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
missing_m = [
|
|
||||||
JdLeafCategoryNorm(label=l) for l in leaf_labels_m if l not in have_m
|
|
||||||
]
|
|
||||||
if missing_m:
|
|
||||||
JdLeafCategoryNorm.objects.bulk_create(missing_m, ignore_conflicts=True)
|
|
||||||
norm_map_m = {
|
|
||||||
n.label: n
|
|
||||||
for n in JdLeafCategoryNorm.objects.filter(label__in=leaf_labels_m)
|
|
||||||
}
|
|
||||||
m_objs: list[JdJobMergedRow] = []
|
m_objs: list[JdJobMergedRow] = []
|
||||||
for i, kw in merged_kw_list:
|
for i, kw in merged_kw_list:
|
||||||
lc = (kw.get("leaf_category") or "").strip()[:512]
|
mg = matrix_group_label_from_detail_path(kw.get("detail_category_path") or "")
|
||||||
norm = norm_map_m.get(lc) if lc else None
|
|
||||||
pv = effective_list_price_value(
|
pv = effective_list_price_value(
|
||||||
kw.get("coupon_price"), kw.get("price"), kw.get("original_price")
|
kw.get("coupon_price"), kw.get("price"), kw.get("original_price")
|
||||||
)
|
)
|
||||||
@ -276,13 +267,14 @@ def ingest_job_dataset_rows(job: PipelineJob) -> dict[str, Any]:
|
|||||||
JdJobMergedRow(
|
JdJobMergedRow(
|
||||||
job=job,
|
job=job,
|
||||||
row_index=i,
|
row_index=i,
|
||||||
leaf_category_norm=norm,
|
matrix_group_label=mg,
|
||||||
price_value=pv,
|
price_value=pv,
|
||||||
**kw,
|
**kw,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_bulk_create_in_chunks(JdJobMergedRow, m_objs)
|
_bulk_create_in_chunks(JdJobMergedRow, m_objs)
|
||||||
stats["merged_table_rows"] = len(m_objs)
|
stats["merged_table_rows"] = len(m_objs)
|
||||||
|
_sync_search_rows_matrix_labels(job, merged_kw_list)
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|||||||
63
backend/pipeline/matrix_group_label.py
Normal file
63
backend/pipeline/matrix_group_label.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
与 ``jd_competitor_report._matrix_group_label_from_path`` 同源:
|
||||||
|
从商详 ``detail_category_path`` 解析 §5 竞品矩阵用的细类展示名(如「饼干」「米」)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def category_token_meaningless(seg: str) -> bool:
|
||||||
|
"""纯数字类目 ID、空串或疑似内部编码的段,不宜直接作为矩阵分组展示名。"""
|
||||||
|
t = (seg or "").strip()
|
||||||
|
if not t:
|
||||||
|
return True
|
||||||
|
if t.isdigit():
|
||||||
|
return True
|
||||||
|
if len(t) >= 14 and re.fullmatch(r"[A-Za-z0-9_\-]+", t):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def matrix_display_segment_from_parts(parts: list[str]) -> str | None:
|
||||||
|
"""
|
||||||
|
与历史逻辑一致的主选段;若该段无意义则自右向左找第一段可读文本
|
||||||
|
(避免「仅类目码」或中间段为数字 ID 时整组成品名式乱桶)。
|
||||||
|
"""
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
if len(parts) >= 4:
|
||||||
|
preferred = parts[-2]
|
||||||
|
elif len(parts) >= 3:
|
||||||
|
preferred = parts[1]
|
||||||
|
elif len(parts) >= 2:
|
||||||
|
preferred = parts[1]
|
||||||
|
else:
|
||||||
|
preferred = parts[0]
|
||||||
|
order: list[str] = []
|
||||||
|
if preferred:
|
||||||
|
order.append(preferred)
|
||||||
|
if len(parts) >= 2:
|
||||||
|
order.append(parts[-2])
|
||||||
|
order.append(parts[-1])
|
||||||
|
order.extend(reversed(parts))
|
||||||
|
seen: set[str] = set()
|
||||||
|
for cand in order:
|
||||||
|
if not cand or cand in seen:
|
||||||
|
continue
|
||||||
|
seen.add(cand)
|
||||||
|
if not category_token_meaningless(cand):
|
||||||
|
return cand.strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def matrix_group_label_from_detail_path(path: str) -> str:
|
||||||
|
"""由 ``detail_category_path`` 文本解析细类展示名;空或无可读段则返回空串。"""
|
||||||
|
t = (path or "").strip()
|
||||||
|
if not t:
|
||||||
|
return ""
|
||||||
|
parts = [p.strip() for p in t.replace(">", ">").split(">") if p.strip()]
|
||||||
|
if not parts:
|
||||||
|
return ""
|
||||||
|
key = matrix_display_segment_from_parts(parts)
|
||||||
|
return (key[:80] if key else "")
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2026-04-16 01:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_matrix_group_labels(apps, schema_editor):
|
||||||
|
from pipeline.matrix_group_label import matrix_group_label_from_detail_path
|
||||||
|
|
||||||
|
JdJobDetailRow = apps.get_model("pipeline", "JdJobDetailRow")
|
||||||
|
JdJobMergedRow = apps.get_model("pipeline", "JdJobMergedRow")
|
||||||
|
JdJobSearchRow = apps.get_model("pipeline", "JdJobSearchRow")
|
||||||
|
|
||||||
|
chunk: list = []
|
||||||
|
for r in JdJobDetailRow.objects.all().iterator(chunk_size=400):
|
||||||
|
r.matrix_group_label = matrix_group_label_from_detail_path(
|
||||||
|
r.detail_category_path or ""
|
||||||
|
)
|
||||||
|
chunk.append(r)
|
||||||
|
if len(chunk) >= 400:
|
||||||
|
JdJobDetailRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
chunk.clear()
|
||||||
|
if chunk:
|
||||||
|
JdJobDetailRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
|
||||||
|
chunk = []
|
||||||
|
for r in JdJobMergedRow.objects.all().iterator(chunk_size=400):
|
||||||
|
r.matrix_group_label = matrix_group_label_from_detail_path(
|
||||||
|
r.detail_category_path or ""
|
||||||
|
)
|
||||||
|
chunk.append(r)
|
||||||
|
if len(chunk) >= 400:
|
||||||
|
JdJobMergedRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
chunk.clear()
|
||||||
|
if chunk:
|
||||||
|
JdJobMergedRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
|
||||||
|
sku_to_mg: dict[str, str] = {}
|
||||||
|
for r in JdJobMergedRow.objects.exclude(matrix_group_label="").iterator(
|
||||||
|
chunk_size=400
|
||||||
|
):
|
||||||
|
sk = str(r.sku_id or "").strip()
|
||||||
|
if sk:
|
||||||
|
sku_to_mg[sk] = r.matrix_group_label
|
||||||
|
|
||||||
|
chunk = []
|
||||||
|
for r in JdJobSearchRow.objects.all().iterator(chunk_size=400):
|
||||||
|
sk = str(r.sku_id or "").strip()
|
||||||
|
r.matrix_group_label = sku_to_mg.get(sk, "")
|
||||||
|
chunk.append(r)
|
||||||
|
if len(chunk) >= 400:
|
||||||
|
JdJobSearchRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
chunk.clear()
|
||||||
|
if chunk:
|
||||||
|
JdJobSearchRow.objects.bulk_update(chunk, ["matrix_group_label"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pipeline', '0017_dataset_browse_filters'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jdjobdetailrow',
|
||||||
|
name='matrix_group_label',
|
||||||
|
field=models.CharField(blank=True, db_index=True, default='', help_text='与 §5 矩阵同源:由 detail_category_path 解析', max_length=80, verbose_name='报告细类'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jdjobmergedrow',
|
||||||
|
name='matrix_group_label',
|
||||||
|
field=models.CharField(blank=True, db_index=True, default='', help_text='与 §5 矩阵同源:由 detail_category_path 解析', max_length=80, verbose_name='报告细类'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jdjobsearchrow',
|
||||||
|
name='matrix_group_label',
|
||||||
|
field=models.CharField(blank=True, db_index=True, default='', help_text='与 §5 矩阵同源:由合并表商详路径解析;可按 SKU 从合并表回填', max_length=80, verbose_name='报告细类'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='jdjobdetailrow',
|
||||||
|
index=models.Index(fields=['job', 'matrix_group_label'], name='pipeline_jd_job_id_5595d3_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='jdjobmergedrow',
|
||||||
|
index=models.Index(fields=['job', 'matrix_group_label'], name='pipeline_jd_job_id_163e3f_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='jdjobsearchrow',
|
||||||
|
index=models.Index(fields=['job', 'matrix_group_label'], name='pipeline_jd_job_id_38fae5_idx'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(backfill_matrix_group_labels, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -190,6 +190,14 @@ class JdJobSearchRow(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="search_rows",
|
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)
|
price_value = models.FloatField(null=True, blank=True, db_index=True)
|
||||||
platform = models.TextField(blank=True, default="")
|
platform = models.TextField(blank=True, default="")
|
||||||
keyword = models.TextField(blank=True, default="")
|
keyword = models.TextField(blank=True, default="")
|
||||||
@ -206,6 +214,7 @@ class JdJobSearchRow(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["job", "sku_id"]),
|
models.Index(fields=["job", "sku_id"]),
|
||||||
models.Index(fields=["job", "leaf_category_norm"]),
|
models.Index(fields=["job", "leaf_category_norm"]),
|
||||||
|
models.Index(fields=["job", "matrix_group_label"]),
|
||||||
models.Index(fields=["job", "price_value"]),
|
models.Index(fields=["job", "price_value"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -232,6 +241,14 @@ class JdJobDetailRow(models.Model):
|
|||||||
buyer_ranking_line = models.TextField(blank=True, default="")
|
buyer_ranking_line = models.TextField(blank=True, default="")
|
||||||
buyer_promo_text = 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)
|
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:
|
class Meta:
|
||||||
ordering = ["row_index"]
|
ordering = ["row_index"]
|
||||||
@ -244,6 +261,7 @@ class JdJobDetailRow(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["job", "sku_id"]),
|
models.Index(fields=["job", "sku_id"]),
|
||||||
models.Index(fields=["job", "detail_price_value"]),
|
models.Index(fields=["job", "detail_price_value"]),
|
||||||
|
models.Index(fields=["job", "matrix_group_label"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -317,6 +335,14 @@ class JdJobMergedRow(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="merged_rows",
|
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)
|
price_value = models.FloatField(null=True, blank=True, db_index=True)
|
||||||
keyword = models.TextField(blank=True, default="")
|
keyword = models.TextField(blank=True, default="")
|
||||||
page = models.TextField(blank=True, default="")
|
page = models.TextField(blank=True, default="")
|
||||||
@ -342,6 +368,7 @@ class JdJobMergedRow(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["job", "sku_id"]),
|
models.Index(fields=["job", "sku_id"]),
|
||||||
models.Index(fields=["job", "leaf_category_norm"]),
|
models.Index(fields=["job", "leaf_category_norm"]),
|
||||||
|
models.Index(fields=["job", "matrix_group_label"]),
|
||||||
models.Index(fields=["job", "price_value"]),
|
models.Index(fields=["job", "price_value"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ def search_row_to_dict(r: JdJobSearchRow) -> dict[str, Any]:
|
|||||||
out: dict[str, Any] = {"id": r.id, "row_index": r.row_index}
|
out: dict[str, Any] = {"id": r.id, "row_index": r.row_index}
|
||||||
for k in JD_SEARCH_INTERNAL_KEYS:
|
for k in JD_SEARCH_INTERNAL_KEYS:
|
||||||
out[k] = getattr(r, k) or ""
|
out[k] = getattr(r, k) or ""
|
||||||
|
out["matrix_group_label"] = r.matrix_group_label or ""
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ def detail_row_to_dict(r: JdJobDetailRow) -> dict[str, Any]:
|
|||||||
out: dict[str, Any] = {"id": r.id, "row_index": r.row_index}
|
out: dict[str, Any] = {"id": r.id, "row_index": r.row_index}
|
||||||
for k in DETAIL_FIELDS_ORDER:
|
for k in DETAIL_FIELDS_ORDER:
|
||||||
out[k] = getattr(r, k) or ""
|
out[k] = getattr(r, k) or ""
|
||||||
|
out["matrix_group_label"] = r.matrix_group_label or ""
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@ -43,4 +45,5 @@ def merged_row_to_dict(r: JdJobMergedRow) -> dict[str, Any]:
|
|||||||
out: dict[str, Any] = {"id": r.id, "row_index": r.row_index}
|
out: dict[str, Any] = {"id": r.id, "row_index": r.row_index}
|
||||||
for k in MERGED_FIELDS_ORDER:
|
for k in MERGED_FIELDS_ORDER:
|
||||||
out[k] = getattr(r, k) or ""
|
out[k] = getattr(r, k) or ""
|
||||||
|
out["matrix_group_label"] = r.matrix_group_label or ""
|
||||||
return out
|
return out
|
||||||
|
|||||||
@ -27,11 +27,11 @@ from .dataset_api import (
|
|||||||
apply_merged_order,
|
apply_merged_order,
|
||||||
apply_search_filters,
|
apply_search_filters,
|
||||||
apply_search_order,
|
apply_search_order,
|
||||||
category_norm_id_from_request,
|
|
||||||
detail_category_q_from_request,
|
detail_category_q_from_request,
|
||||||
filter_echo,
|
filter_echo,
|
||||||
parse_sort_meta,
|
parse_sort_meta,
|
||||||
price_bounds_from_request,
|
price_bounds_from_request,
|
||||||
|
report_group_from_request,
|
||||||
)
|
)
|
||||||
from .dataset_nonempty import (
|
from .dataset_nonempty import (
|
||||||
comment_columns_for_api,
|
comment_columns_for_api,
|
||||||
@ -66,7 +66,6 @@ from .models import (
|
|||||||
JdJobDetailRow,
|
JdJobDetailRow,
|
||||||
JdJobMergedRow,
|
JdJobMergedRow,
|
||||||
JdJobSearchRow,
|
JdJobSearchRow,
|
||||||
JdLeafCategoryNorm,
|
|
||||||
JdProduct,
|
JdProduct,
|
||||||
JdProductSnapshot,
|
JdProductSnapshot,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
@ -756,17 +755,15 @@ def _read_page_params(request) -> tuple[int, int]:
|
|||||||
return page, page_size
|
return page, page_size
|
||||||
|
|
||||||
|
|
||||||
def _category_norm_options_for_job(job: PipelineJob, RowModel: type) -> list[dict[str, Any]]:
|
def _report_group_options_for_job(job: PipelineJob) -> list[str]:
|
||||||
ids = (
|
"""与 §5 矩阵一致的细类名列表(来自合并表 ``detail_category_path`` 解析)。"""
|
||||||
RowModel.objects.filter(job=job, leaf_category_norm_id__isnull=False)
|
qs = (
|
||||||
.values_list("leaf_category_norm_id", flat=True)
|
JdJobMergedRow.objects.filter(job=job)
|
||||||
|
.exclude(matrix_group_label="")
|
||||||
|
.values_list("matrix_group_label", flat=True)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
return list(
|
return sorted({str(x) for x in qs if x})
|
||||||
JdLeafCategoryNorm.objects.filter(id__in=ids)
|
|
||||||
.order_by("label")
|
|
||||||
.values("id", "label")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _detail_category_path_options(job: PipelineJob) -> list[str]:
|
def _detail_category_path_options(job: PipelineJob) -> list[str]:
|
||||||
@ -797,12 +794,7 @@ class JobDatasetSummaryView(APIView):
|
|||||||
"detail_columns": detail_columns_for_api(job),
|
"detail_columns": detail_columns_for_api(job),
|
||||||
"comment_columns": comment_columns_for_api(job),
|
"comment_columns": comment_columns_for_api(job),
|
||||||
"merged_columns": merged_columns_for_api(job),
|
"merged_columns": merged_columns_for_api(job),
|
||||||
"search_category_options": _category_norm_options_for_job(
|
"report_group_options": _report_group_options_for_job(job),
|
||||||
job, JdJobSearchRow
|
|
||||||
),
|
|
||||||
"merged_category_options": _category_norm_options_for_job(
|
|
||||||
job, JdJobMergedRow
|
|
||||||
),
|
|
||||||
"detail_category_path_options": _detail_category_path_options(job),
|
"detail_category_path_options": _detail_category_path_options(job),
|
||||||
"dataset_sort_help": {
|
"dataset_sort_help": {
|
||||||
"search": sorted(SEARCH_SORT_FIELDS),
|
"search": sorted(SEARCH_SORT_FIELDS),
|
||||||
@ -820,7 +812,7 @@ class JobDatasetSearchView(APIView):
|
|||||||
page, page_size = _read_page_params(request)
|
page, page_size = _read_page_params(request)
|
||||||
sort, desc = parse_sort_meta(request)
|
sort, desc = parse_sort_meta(request)
|
||||||
sort_eff = sort if sort in SEARCH_SORT_FIELDS else "row_index"
|
sort_eff = sort if sort in SEARCH_SORT_FIELDS else "row_index"
|
||||||
cid = category_norm_id_from_request(request)
|
rg = report_group_from_request(request)
|
||||||
pmin, pmax = price_bounds_from_request(request)
|
pmin, pmax = price_bounds_from_request(request)
|
||||||
dcq = detail_category_q_from_request(request)
|
dcq = detail_category_q_from_request(request)
|
||||||
qs = JdJobSearchRow.objects.filter(job=job)
|
qs = JdJobSearchRow.objects.filter(job=job)
|
||||||
@ -835,7 +827,7 @@ class JobDatasetSearchView(APIView):
|
|||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"filters": filter_echo(
|
"filters": filter_echo(
|
||||||
category_norm_id=cid,
|
report_group=rg,
|
||||||
price_min=pmin,
|
price_min=pmin,
|
||||||
price_max=pmax,
|
price_max=pmax,
|
||||||
detail_category_q=dcq,
|
detail_category_q=dcq,
|
||||||
@ -853,7 +845,7 @@ class JobDatasetDetailView(APIView):
|
|||||||
page, page_size = _read_page_params(request)
|
page, page_size = _read_page_params(request)
|
||||||
sort, desc = parse_sort_meta(request)
|
sort, desc = parse_sort_meta(request)
|
||||||
sort_eff = sort if sort in DETAIL_SORT_FIELDS else "row_index"
|
sort_eff = sort if sort in DETAIL_SORT_FIELDS else "row_index"
|
||||||
cid = category_norm_id_from_request(request)
|
rg = report_group_from_request(request)
|
||||||
pmin, pmax = price_bounds_from_request(request)
|
pmin, pmax = price_bounds_from_request(request)
|
||||||
dcq = detail_category_q_from_request(request)
|
dcq = detail_category_q_from_request(request)
|
||||||
qs = JdJobDetailRow.objects.filter(job=job)
|
qs = JdJobDetailRow.objects.filter(job=job)
|
||||||
@ -868,7 +860,7 @@ class JobDatasetDetailView(APIView):
|
|||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"filters": filter_echo(
|
"filters": filter_echo(
|
||||||
category_norm_id=cid,
|
report_group=rg,
|
||||||
price_min=pmin,
|
price_min=pmin,
|
||||||
price_max=pmax,
|
price_max=pmax,
|
||||||
detail_category_q=dcq,
|
detail_category_q=dcq,
|
||||||
@ -908,7 +900,7 @@ class JobDatasetMergedView(APIView):
|
|||||||
page, page_size = _read_page_params(request)
|
page, page_size = _read_page_params(request)
|
||||||
sort, desc = parse_sort_meta(request)
|
sort, desc = parse_sort_meta(request)
|
||||||
sort_eff = sort if sort in MERGED_SORT_FIELDS else "row_index"
|
sort_eff = sort if sort in MERGED_SORT_FIELDS else "row_index"
|
||||||
cid = category_norm_id_from_request(request)
|
rg = report_group_from_request(request)
|
||||||
pmin, pmax = price_bounds_from_request(request)
|
pmin, pmax = price_bounds_from_request(request)
|
||||||
dcq = detail_category_q_from_request(request)
|
dcq = detail_category_q_from_request(request)
|
||||||
qs = JdJobMergedRow.objects.filter(job=job)
|
qs = JdJobMergedRow.objects.filter(job=job)
|
||||||
@ -923,7 +915,7 @@ class JobDatasetMergedView(APIView):
|
|||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"filters": filter_echo(
|
"filters": filter_echo(
|
||||||
category_norm_id=cid,
|
report_group=rg,
|
||||||
price_min=pmin,
|
price_min=pmin,
|
||||||
price_max=pmax,
|
price_max=pmax,
|
||||||
detail_category_q=dcq,
|
detail_category_q=dcq,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const SORT_LABELS = {
|
|||||||
sku_id: 'SKU',
|
sku_id: 'SKU',
|
||||||
title: '标题',
|
title: '标题',
|
||||||
leaf_category: '叶类目',
|
leaf_category: '叶类目',
|
||||||
|
matrix_group_label: '报告细类',
|
||||||
detail_category_path: '类目路径',
|
detail_category_path: '类目路径',
|
||||||
detail_brand: '品牌',
|
detail_brand: '品牌',
|
||||||
}
|
}
|
||||||
@ -39,7 +40,8 @@ const err = ref('')
|
|||||||
const commentSkuFilter = ref('')
|
const commentSkuFilter = ref('')
|
||||||
const sortField = ref('row_index')
|
const sortField = ref('row_index')
|
||||||
const sortOrder = ref('asc')
|
const sortOrder = ref('asc')
|
||||||
const categoryNormId = ref('')
|
/** 与 §5 矩阵一致的细类名(如饼干、米),对应接口参数 report_group */
|
||||||
|
const reportGroup = ref('')
|
||||||
const priceMin = ref('')
|
const priceMin = ref('')
|
||||||
const priceMax = ref('')
|
const priceMax = ref('')
|
||||||
const detailCategoryQ = ref('')
|
const detailCategoryQ = ref('')
|
||||||
@ -68,12 +70,7 @@ const sortOptions = computed(() => {
|
|||||||
return keys.map((k) => ({ value: k, label: SORT_LABELS[k] || k }))
|
return keys.map((k) => ({ value: k, label: SORT_LABELS[k] || k }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const categoryNormOptions = computed(() => {
|
const reportGroupOptions = computed(() => summary.value?.report_group_options || [])
|
||||||
if (!summary.value) return []
|
|
||||||
if (tab.value === 'search') return summary.value.search_category_options || []
|
|
||||||
if (tab.value === 'merged') return summary.value.merged_category_options || []
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayColumns = computed(() => {
|
const displayColumns = computed(() => {
|
||||||
const s = summary.value
|
const s = summary.value
|
||||||
@ -164,7 +161,7 @@ async function refreshList() {
|
|||||||
: {
|
: {
|
||||||
sort: sortField.value,
|
sort: sortField.value,
|
||||||
order: sortOrder.value,
|
order: sortOrder.value,
|
||||||
categoryNormId: categoryNormId.value.trim(),
|
reportGroup: reportGroup.value.trim(),
|
||||||
priceMin: priceMin.value,
|
priceMin: priceMin.value,
|
||||||
priceMax: priceMax.value,
|
priceMax: priceMax.value,
|
||||||
detailCategoryQ: detailCategoryQ.value.trim(),
|
detailCategoryQ: detailCategoryQ.value.trim(),
|
||||||
@ -198,7 +195,7 @@ watch(
|
|||||||
page.value = 1
|
page.value = 1
|
||||||
sortField.value = 'row_index'
|
sortField.value = 'row_index'
|
||||||
sortOrder.value = 'asc'
|
sortOrder.value = 'asc'
|
||||||
categoryNormId.value = ''
|
reportGroup.value = ''
|
||||||
priceMin.value = ''
|
priceMin.value = ''
|
||||||
priceMax.value = ''
|
priceMax.value = ''
|
||||||
detailCategoryQ.value = ''
|
detailCategoryQ.value = ''
|
||||||
@ -216,7 +213,7 @@ watch(tab, () => {
|
|||||||
exportPanelOpen.value = false
|
exportPanelOpen.value = false
|
||||||
sortField.value = 'row_index'
|
sortField.value = 'row_index'
|
||||||
sortOrder.value = 'asc'
|
sortOrder.value = 'asc'
|
||||||
categoryNormId.value = ''
|
reportGroup.value = ''
|
||||||
priceMin.value = ''
|
priceMin.value = ''
|
||||||
priceMax.value = ''
|
priceMax.value = ''
|
||||||
detailCategoryQ.value = ''
|
detailCategoryQ.value = ''
|
||||||
@ -226,7 +223,7 @@ watch(
|
|||||||
[
|
[
|
||||||
sortField,
|
sortField,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
categoryNormId,
|
reportGroup,
|
||||||
priceMin,
|
priceMin,
|
||||||
priceMax,
|
priceMax,
|
||||||
detailCategoryQ,
|
detailCategoryQ,
|
||||||
@ -245,7 +242,7 @@ watch(
|
|||||||
commentSkuFilter,
|
commentSkuFilter,
|
||||||
sortField,
|
sortField,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
categoryNormId,
|
reportGroup,
|
||||||
priceMin,
|
priceMin,
|
||||||
priceMax,
|
priceMax,
|
||||||
detailCategoryQ,
|
detailCategoryQ,
|
||||||
@ -430,17 +427,13 @@ async function runExport(format) {
|
|||||||
<option value="desc">降序</option>
|
<option value="desc">降序</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<template v-if="tab === 'search' || tab === 'merged'">
|
<label class="filter-item">
|
||||||
<label class="filter-item">
|
报告细类(与矩阵一致)
|
||||||
叶类目
|
<select v-model="reportGroup" class="filter-select wide">
|
||||||
<select v-model="categoryNormId" class="filter-select wide">
|
<option value="">全部</option>
|
||||||
<option value="">全部</option>
|
<option v-for="g in reportGroupOptions" :key="g" :value="g">{{ g }}</option>
|
||||||
<option v-for="c in categoryNormOptions" :key="c.id" :value="String(c.id)">
|
</select>
|
||||||
{{ c.label }}
|
</label>
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
<template v-if="tab === 'detail' || tab === 'merged'">
|
<template v-if="tab === 'detail' || tab === 'merged'">
|
||||||
<label class="filter-item">
|
<label class="filter-item">
|
||||||
类目路径包含
|
类目路径包含
|
||||||
|
|||||||
@ -205,9 +205,10 @@ export function jobDatasetPageUrl(jobId, kind, page = 1, pageSize = 50, opts = {
|
|||||||
if (sku) p.set('sku_id', String(sku))
|
if (sku) p.set('sku_id', String(sku))
|
||||||
if (o.sort) p.set('sort', String(o.sort))
|
if (o.sort) p.set('sort', String(o.sort))
|
||||||
if (o.order) p.set('order', String(o.order))
|
if (o.order) p.set('order', String(o.order))
|
||||||
const cn = o.categoryNormId ?? o.category_norm_id
|
const rg =
|
||||||
if (cn !== undefined && cn !== null && String(cn).trim() !== '')
|
o.reportGroup ?? o.report_group ?? o.categoryNormId ?? o.category_norm_id
|
||||||
p.set('category_norm_id', String(cn).trim())
|
if (rg !== undefined && rg !== null && String(rg).trim() !== '')
|
||||||
|
p.set('report_group', String(rg).trim())
|
||||||
const pmin = o.priceMin ?? o.price_min
|
const pmin = o.priceMin ?? o.price_min
|
||||||
if (pmin !== undefined && pmin !== null && String(pmin).trim() !== '')
|
if (pmin !== undefined && pmin !== null && String(pmin).trim() !== '')
|
||||||
p.set('price_min', String(pmin).trim())
|
p.set('price_min', String(pmin).trim())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user