mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-22 16:11:38 +08:00
- 原单文件 views.py 改为包 pipeline/views/:公共辅助、任务生命周期、报告与导出、数据集、商品快照、入库。 - urls 仍使用 from . import views,对外符号通过 __init__.py 聚合导出,行为不变。 Made-with: Cursor
250 lines
9.0 KiB
Python
250 lines
9.0 KiB
Python
"""任务入库后的数据集浏览、筛选与导出。"""
|
||
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|all,export_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
|