hub-gif c57014a067 refactor(pipeline): 将 API 视图拆为按领域子模块
- 原单文件 views.py 改为包 pipeline/views/:公共辅助、任务生命周期、报告与导出、数据集、商品快照、入库。

- urls 仍使用 from . import views,对外符号通过 __init__.py 聚合导出,行为不变。

Made-with: Cursor
2026-04-17 11:20:47 +08:00

250 lines
9.0 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 __future__ import annotations
from django.http import HttpResponse
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from ..dataset_api import (
DETAIL_SORT_FIELDS,
MERGED_SORT_FIELDS,
SEARCH_SORT_FIELDS,
apply_detail_filters,
apply_detail_order,
apply_merged_filters,
apply_merged_order,
apply_search_filters,
apply_search_order,
detail_category_q_from_request,
filter_echo,
parse_sort_meta,
price_bounds_from_request,
report_group_from_request,
shop_from_request,
)
from ..dataset_nonempty import (
comment_columns_for_api,
detail_columns_for_api,
merged_columns_for_api,
search_columns_for_api,
)
from ..export_job import build_csv_bytes, build_json_bytes, build_xlsx_bytes
from ..models import (
JdJobCommentRow,
JdJobDetailRow,
JdJobMergedRow,
JdJobSearchRow,
)
from ..row_serialize import (
comment_row_to_dict,
detail_row_to_dict,
merged_row_to_dict,
search_row_to_dict,
)
from .common import (
dataset_job,
detail_category_path_options,
read_page_params,
report_group_options_for_job,
shop_options_for_job,
)
class JobDatasetSummaryView(APIView):
"""任务在库中的搜索/详情/评价行数(入库后可用)。"""
def get(self, request, pk: int):
job = dataset_job(pk)
return Response(
{
"job_id": job.id,
"keyword": job.keyword,
"status": job.status,
"search_rows": JdJobSearchRow.objects.filter(job=job).count(),
"detail_rows": JdJobDetailRow.objects.filter(job=job).count(),
"comment_rows": JdJobCommentRow.objects.filter(job=job).count(),
"merged_rows": JdJobMergedRow.objects.filter(job=job).count(),
"search_columns": search_columns_for_api(job),
"detail_columns": detail_columns_for_api(job),
"comment_columns": comment_columns_for_api(job),
"merged_columns": merged_columns_for_api(job),
"category_options": report_group_options_for_job(job),
"shop_options": shop_options_for_job(job),
"detail_category_path_options": detail_category_path_options(job),
"dataset_sort_help": {
"search": sorted(SEARCH_SORT_FIELDS),
"detail": sorted(DETAIL_SORT_FIELDS),
"merged": sorted(MERGED_SORT_FIELDS),
"comments": ["row_index"],
},
}
)
class JobDatasetSearchView(APIView):
def get(self, request, pk: int):
job = dataset_job(pk)
page, page_size = read_page_params(request)
sort, desc = parse_sort_meta(request)
sort_eff = sort if sort in SEARCH_SORT_FIELDS else "row_index"
rg = report_group_from_request(request)
sp = shop_from_request(request)
pmin, pmax = price_bounds_from_request(request)
dcq = detail_category_q_from_request(request)
qs = JdJobSearchRow.objects.filter(job=job)
qs = apply_search_filters(qs, request)
qs = apply_search_order(qs, sort_eff, desc)
total = qs.count()
start = (page - 1) * page_size
rows = qs[start : start + page_size]
return Response(
{
"total": total,
"page": page,
"page_size": page_size,
"filters": filter_echo(
report_group=rg,
shop=sp,
price_min=pmin,
price_max=pmax,
detail_category_q=dcq,
sort=sort_eff,
desc=desc,
),
"results": [search_row_to_dict(r) for r in rows],
}
)
class JobDatasetDetailView(APIView):
def get(self, request, pk: int):
job = dataset_job(pk)
page, page_size = read_page_params(request)
sort, desc = parse_sort_meta(request)
sort_eff = sort if sort in DETAIL_SORT_FIELDS else "row_index"
rg = report_group_from_request(request)
sp = shop_from_request(request)
pmin, pmax = price_bounds_from_request(request)
dcq = detail_category_q_from_request(request)
qs = JdJobDetailRow.objects.filter(job=job)
qs = apply_detail_filters(qs, request)
qs = apply_detail_order(qs, sort_eff, desc)
total = qs.count()
start = (page - 1) * page_size
rows = qs[start : start + page_size]
return Response(
{
"total": total,
"page": page,
"page_size": page_size,
"filters": filter_echo(
report_group=rg,
shop=sp,
price_min=pmin,
price_max=pmax,
detail_category_q=dcq,
sort=sort_eff,
desc=desc,
),
"results": [detail_row_to_dict(r) for r in rows],
}
)
class JobDatasetCommentsView(APIView):
def get(self, request, pk: int):
job = dataset_job(pk)
page, page_size = read_page_params(request)
sku_id = (request.query_params.get("sku_id") or "").strip()
qs = JdJobCommentRow.objects.filter(job=job)
if sku_id:
qs = qs.filter(sku_id=sku_id)
total = qs.count()
start = (page - 1) * page_size
rows = qs.order_by("row_index")[start : start + page_size]
return Response(
{
"total": total,
"page": page,
"page_size": page_size,
"sku_filter": sku_id or None,
"results": [comment_row_to_dict(r) for r in rows],
}
)
class JobDatasetMergedView(APIView):
def get(self, request, pk: int):
job = dataset_job(pk)
page, page_size = read_page_params(request)
sort, desc = parse_sort_meta(request)
sort_eff = sort if sort in MERGED_SORT_FIELDS else "row_index"
rg = report_group_from_request(request)
sp = shop_from_request(request)
pmin, pmax = price_bounds_from_request(request)
dcq = detail_category_q_from_request(request)
qs = JdJobMergedRow.objects.filter(job=job)
qs = apply_merged_filters(qs, request)
qs = apply_merged_order(qs, sort_eff, desc)
total = qs.count()
start = (page - 1) * page_size
rows = qs[start : start + page_size]
return Response(
{
"total": total,
"page": page,
"page_size": page_size,
"filters": filter_echo(
report_group=rg,
shop=sp,
price_min=pmin,
price_max=pmax,
detail_category_q=dcq,
sort=sort_eff,
desc=desc,
),
"results": [merged_row_to_dict(r) for r in rows],
}
)
class JobDatasetExportView(APIView):
"""下载kind=search|detail|comments|merged|allexport_fmt=json|csv|xlsx。
``merged``:库内合并宽表行(与 lean 合并 CSV 列一致,入库后导出)。
注意:勿使用查询参数名 ``format``DRF 会将其用于内容协商,非 json 时易在进视图前 404。
"""
def get(self, request, pk: int):
job = dataset_job(pk)
kind = (request.query_params.get("kind") or "search").strip().lower()
fmt = (request.query_params.get("export_fmt") or "json").strip().lower()
if kind not in ("search", "detail", "comments", "all", "merged"):
return Response(
{"detail": "kind 须为 search / detail / comments / all / merged"},
status=status.HTTP_400_BAD_REQUEST,
)
if fmt not in ("json", "csv", "xlsx"):
return Response(
{"detail": "export_fmt 须为 json / csv / xlsx"},
status=status.HTTP_400_BAD_REQUEST,
)
try:
if fmt == "json":
data, filename = build_json_bytes(job=job, kind=kind)
resp = HttpResponse(data, content_type="application/json; charset=utf-8")
elif fmt == "csv":
data, filename = build_csv_bytes(job=job, kind=kind)
resp = HttpResponse(data, content_type="text/csv; charset=utf-8")
else:
data, filename = build_xlsx_bytes(job=job, kind=kind)
resp = HttpResponse(
data,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
resp["Content-Disposition"] = f'attachment; filename="{filename}"'
return resp