From 5ea55233b19e777bfb9d0d09d16991a01c9654c8 Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Thu, 16 Apr 2026 10:04:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(dataset):=20=E7=B1=BB=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E6=A1=88=E7=BB=9F=E4=B8=80=E5=B9=B6=E6=94=AF=E6=8C=81=E6=8C=89?= =?UTF-8?q?=E5=BA=97=E9=93=BA=E5=90=8D=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 摘要 category_options;列/导出「类目」;查询参数 shop_q 模糊匹配店铺(搜索 shop_name、商详 detail_shop_name、宽表双列)。 Made-with: Cursor --- backend/pipeline/dataset_api.py | 22 +++++++++++++-- backend/pipeline/dataset_nonempty.py | 2 +- backend/pipeline/ingest.py | 2 +- backend/pipeline/matrix_group_label.py | 2 +- .../0019_matrix_label_verbose_category.py | 28 +++++++++++++++++++ backend/pipeline/models.py | 6 ++-- backend/pipeline/views.py | 11 ++++++-- frontend/src/components/JobDatasetModal.vue | 25 +++++++++++++---- frontend/src/composables/useJobs.js | 3 ++ 9 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 backend/pipeline/migrations/0019_matrix_label_verbose_category.py diff --git a/backend/pipeline/dataset_api.py b/backend/pipeline/dataset_api.py index 511d36b..e41211c 100644 --- a/backend/pipeline/dataset_api.py +++ b/backend/pipeline/dataset_api.py @@ -1,4 +1,4 @@ -"""库内数据浏览 API:排序、价格与报告细类筛选(查询参数解析与 QuerySet 变换)。""" +"""库内数据浏览 API:排序、价格、类目(§5 矩阵)与店铺名筛选。""" from __future__ import annotations from typing import Any @@ -57,10 +57,15 @@ def price_bounds_from_request(request: Request) -> tuple[float | None, float | N def report_group_from_request(request: Request) -> str: - """与 §5 矩阵一致的细类名(如「饼干」「米」);对应查询参数 ``report_group``。""" + """与 §5 矩阵一致的类目名(如饼干、米);查询参数 ``report_group``。""" return (request.query_params.get("report_group") or "").strip() +def shop_q_from_request(request: Request) -> str: + """店铺名模糊匹配;查询参数 ``shop_q``。""" + return (request.query_params.get("shop_q") or "").strip() + + def detail_category_q_from_request(request: Request) -> str: return (request.query_params.get("detail_category_q") or "").strip() @@ -68,6 +73,7 @@ def detail_category_q_from_request(request: Request) -> str: def filter_echo( *, report_group: str, + shop_q: str, price_min: float | None, price_max: float | None, detail_category_q: str, @@ -76,6 +82,7 @@ def filter_echo( ) -> dict[str, Any]: return { "report_group": report_group or None, + "shop_q": shop_q or None, "price_min": price_min, "price_max": price_max, "detail_category_q": detail_category_q or None, @@ -88,6 +95,9 @@ def apply_search_filters(qs: QuerySet, request: Request) -> QuerySet: rg = report_group_from_request(request) if rg: qs = qs.filter(Q(matrix_group_label=rg) | Q(leaf_category=rg)) + sq = shop_q_from_request(request) + if sq: + qs = qs.filter(shop_name__icontains=sq) pmin, pmax = price_bounds_from_request(request) if pmin is not None: qs = qs.filter(price_value__gte=pmin) @@ -121,6 +131,9 @@ def apply_detail_filters(qs: QuerySet, request: Request) -> QuerySet: rg = report_group_from_request(request) if rg: qs = qs.filter(matrix_group_label=rg) + sq = shop_q_from_request(request) + if sq: + qs = qs.filter(detail_shop_name__icontains=sq) q = detail_category_q_from_request(request) if q: qs = qs.filter(detail_category_path__icontains=q) @@ -157,6 +170,11 @@ def apply_merged_filters(qs: QuerySet, request: Request) -> QuerySet: rg = report_group_from_request(request) if rg: qs = qs.filter(matrix_group_label=rg) + sq = shop_q_from_request(request) + if sq: + qs = qs.filter( + Q(shop_name__icontains=sq) | Q(detail_shop_name__icontains=sq) + ) q = detail_category_q_from_request(request) if q: qs = qs.filter(detail_category_path__icontains=q) diff --git a/backend/pipeline/dataset_nonempty.py b/backend/pipeline/dataset_nonempty.py index f4405a0..cebbca4 100644 --- a/backend/pipeline/dataset_nonempty.py +++ b/backend/pipeline/dataset_nonempty.py @@ -14,7 +14,7 @@ from .csv_schema import ( from .models import JdJobCommentRow, JdJobDetailRow, JdJobMergedRow, JdJobSearchRow, PipelineJob from .row_serialize import COMMENT_FIELDS_ORDER, DETAIL_FIELDS_ORDER -MATRIX_GROUP_COLUMN = {"key": "matrix_group_label", "label": "报告细类(§5矩阵)"} +MATRIX_GROUP_COLUMN = {"key": "matrix_group_label", "label": "类目"} def _is_nonempty(val) -> bool: diff --git a/backend/pipeline/ingest.py b/backend/pipeline/ingest.py index f3a8e46..8bdf8e6 100644 --- a/backend/pipeline/ingest.py +++ b/backend/pipeline/ingest.py @@ -122,7 +122,7 @@ def _bulk_create_in_chunks(model, objects: list[Any]) -> None: def _sync_search_rows_matrix_labels( job: PipelineJob, merged_kw_list: list[tuple[int, dict[str, str]]] ) -> None: - """按 SKU 将合并表解析出的报告细类回填到搜索行(与 §5 矩阵口径一致)。""" + """按 SKU 将合并表解析出的类目回填到搜索行(与 §5 矩阵口径一致)。""" sku_to_mg: dict[str, str] = {} for _, kw in merged_kw_list: sk = (kw.get("sku_id") or "").strip() diff --git a/backend/pipeline/matrix_group_label.py b/backend/pipeline/matrix_group_label.py index 6034715..182ea68 100644 --- a/backend/pipeline/matrix_group_label.py +++ b/backend/pipeline/matrix_group_label.py @@ -1,6 +1,6 @@ """ 与 ``jd_competitor_report._matrix_group_label_from_path`` 同源: -从商详 ``detail_category_path`` 解析 §5 竞品矩阵用的细类展示名(如「饼干」「米」)。 +从商详 ``detail_category_path`` 解析 §5 竞品矩阵用的类目展示名(如饼干、米)。 """ from __future__ import annotations diff --git a/backend/pipeline/migrations/0019_matrix_label_verbose_category.py b/backend/pipeline/migrations/0019_matrix_label_verbose_category.py new file mode 100644 index 0000000..9471991 --- /dev/null +++ b/backend/pipeline/migrations/0019_matrix_label_verbose_category.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.1 on 2026-04-16 02:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pipeline', '0018_report_group_matrix_label'), + ] + + operations = [ + migrations.AlterField( + 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.AlterField( + 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.AlterField( + 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='类目'), + ), + ] diff --git a/backend/pipeline/models.py b/backend/pipeline/models.py index 793f3b8..28e33ed 100644 --- a/backend/pipeline/models.py +++ b/backend/pipeline/models.py @@ -195,7 +195,7 @@ class JdJobSearchRow(models.Model): blank=True, default="", db_index=True, - verbose_name="报告细类", + verbose_name="类目", help_text="与 §5 矩阵同源:由合并表商详路径解析;可按 SKU 从合并表回填", ) price_value = models.FloatField(null=True, blank=True, db_index=True) @@ -246,7 +246,7 @@ class JdJobDetailRow(models.Model): blank=True, default="", db_index=True, - verbose_name="报告细类", + verbose_name="类目", help_text="与 §5 矩阵同源:由 detail_category_path 解析", ) @@ -340,7 +340,7 @@ class JdJobMergedRow(models.Model): blank=True, default="", db_index=True, - verbose_name="报告细类", + verbose_name="类目", help_text="与 §5 矩阵同源:由 detail_category_path 解析", ) price_value = models.FloatField(null=True, blank=True, db_index=True) diff --git a/backend/pipeline/views.py b/backend/pipeline/views.py index 21c48f1..cebe406 100644 --- a/backend/pipeline/views.py +++ b/backend/pipeline/views.py @@ -32,6 +32,7 @@ from .dataset_api import ( parse_sort_meta, price_bounds_from_request, report_group_from_request, + shop_q_from_request, ) from .dataset_nonempty import ( comment_columns_for_api, @@ -756,7 +757,7 @@ def _read_page_params(request) -> tuple[int, int]: def _report_group_options_for_job(job: PipelineJob) -> list[str]: - """与 §5 矩阵一致的细类名列表(来自合并表 ``detail_category_path`` 解析)。""" + """类目选项:与 §5 矩阵一致,来自合并表 ``detail_category_path`` 解析。""" qs = ( JdJobMergedRow.objects.filter(job=job) .exclude(matrix_group_label="") @@ -794,7 +795,7 @@ class JobDatasetSummaryView(APIView): "detail_columns": detail_columns_for_api(job), "comment_columns": comment_columns_for_api(job), "merged_columns": merged_columns_for_api(job), - "report_group_options": _report_group_options_for_job(job), + "category_options": _report_group_options_for_job(job), "detail_category_path_options": _detail_category_path_options(job), "dataset_sort_help": { "search": sorted(SEARCH_SORT_FIELDS), @@ -813,6 +814,7 @@ class JobDatasetSearchView(APIView): sort, desc = parse_sort_meta(request) sort_eff = sort if sort in SEARCH_SORT_FIELDS else "row_index" rg = report_group_from_request(request) + sq = shop_q_from_request(request) pmin, pmax = price_bounds_from_request(request) dcq = detail_category_q_from_request(request) qs = JdJobSearchRow.objects.filter(job=job) @@ -828,6 +830,7 @@ class JobDatasetSearchView(APIView): "page_size": page_size, "filters": filter_echo( report_group=rg, + shop_q=sq, price_min=pmin, price_max=pmax, detail_category_q=dcq, @@ -846,6 +849,7 @@ class JobDatasetDetailView(APIView): sort, desc = parse_sort_meta(request) sort_eff = sort if sort in DETAIL_SORT_FIELDS else "row_index" rg = report_group_from_request(request) + sq = shop_q_from_request(request) pmin, pmax = price_bounds_from_request(request) dcq = detail_category_q_from_request(request) qs = JdJobDetailRow.objects.filter(job=job) @@ -861,6 +865,7 @@ class JobDatasetDetailView(APIView): "page_size": page_size, "filters": filter_echo( report_group=rg, + shop_q=sq, price_min=pmin, price_max=pmax, detail_category_q=dcq, @@ -901,6 +906,7 @@ class JobDatasetMergedView(APIView): sort, desc = parse_sort_meta(request) sort_eff = sort if sort in MERGED_SORT_FIELDS else "row_index" rg = report_group_from_request(request) + sq = shop_q_from_request(request) pmin, pmax = price_bounds_from_request(request) dcq = detail_category_q_from_request(request) qs = JdJobMergedRow.objects.filter(job=job) @@ -916,6 +922,7 @@ class JobDatasetMergedView(APIView): "page_size": page_size, "filters": filter_echo( report_group=rg, + shop_q=sq, price_min=pmin, price_max=pmax, detail_category_q=dcq, diff --git a/frontend/src/components/JobDatasetModal.vue b/frontend/src/components/JobDatasetModal.vue index d20edc0..47ffaaf 100644 --- a/frontend/src/components/JobDatasetModal.vue +++ b/frontend/src/components/JobDatasetModal.vue @@ -24,7 +24,7 @@ const SORT_LABELS = { sku_id: 'SKU', title: '标题', leaf_category: '叶类目', - matrix_group_label: '报告细类', + matrix_group_label: '类目', detail_category_path: '类目路径', detail_brand: '品牌', } @@ -40,8 +40,9 @@ const err = ref('') const commentSkuFilter = ref('') const sortField = ref('row_index') const sortOrder = ref('asc') -/** 与 §5 矩阵一致的细类名(如饼干、米),对应接口参数 report_group */ +/** 类目(§5 矩阵),对应接口参数 report_group */ const reportGroup = ref('') +const shopQ = ref('') const priceMin = ref('') const priceMax = ref('') const detailCategoryQ = ref('') @@ -70,7 +71,7 @@ const sortOptions = computed(() => { return keys.map((k) => ({ value: k, label: SORT_LABELS[k] || k })) }) -const reportGroupOptions = computed(() => summary.value?.report_group_options || []) +const categoryOptions = computed(() => summary.value?.category_options || []) const displayColumns = computed(() => { const s = summary.value @@ -162,6 +163,7 @@ async function refreshList() { sort: sortField.value, order: sortOrder.value, reportGroup: reportGroup.value.trim(), + shopQ: shopQ.value.trim(), priceMin: priceMin.value, priceMax: priceMax.value, detailCategoryQ: detailCategoryQ.value.trim(), @@ -196,6 +198,7 @@ watch( sortField.value = 'row_index' sortOrder.value = 'asc' reportGroup.value = '' + shopQ.value = '' priceMin.value = '' priceMax.value = '' detailCategoryQ.value = '' @@ -214,6 +217,7 @@ watch(tab, () => { sortField.value = 'row_index' sortOrder.value = 'asc' reportGroup.value = '' + shopQ.value = '' priceMin.value = '' priceMax.value = '' detailCategoryQ.value = '' @@ -224,6 +228,7 @@ watch( sortField, sortOrder, reportGroup, + shopQ, priceMin, priceMax, detailCategoryQ, @@ -243,6 +248,7 @@ watch( sortField, sortOrder, reportGroup, + shopQ, priceMin, priceMax, detailCategoryQ, @@ -428,12 +434,21 @@ async function runExport(format) { +