hub-gif a43e1f9b1d feat(dataset): 搜索/宽表支持按解析后销量与评价量排序
新增 sales_sort_value、comment_count_sort_value 与 volume_parse;入库与迁移回填;sort=total_sales|comment_count;前端排序下拉文案。

Made-with: Cursor
2026-04-16 10:15:24 +08:00

238 lines
7.1 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.

"""库内数据浏览 API排序、价格、类目§5 矩阵)与店铺(精确)筛选。"""
from __future__ import annotations
from typing import Any
from django.db.models import F, Q, QuerySet
from django.db.models.expressions import OrderBy
from rest_framework.request import Request
SEARCH_SORT_FIELDS = frozenset(
{
"row_index",
"price",
"sku_id",
"title",
"leaf_category",
"matrix_group_label",
"total_sales",
"comment_count",
}
)
DETAIL_SORT_FIELDS = frozenset(
{
"row_index",
"price",
"sku_id",
"detail_category_path",
"detail_brand",
"matrix_group_label",
}
)
MERGED_SORT_FIELDS = frozenset(
{
"row_index",
"price",
"sku_id",
"title",
"leaf_category",
"detail_category_path",
"matrix_group_label",
"total_sales",
"comment_count",
}
)
def _parse_opt_float(val: str | None) -> float | None:
if val is None or not str(val).strip():
return None
try:
return float(str(val).strip())
except ValueError:
return None
def parse_sort_meta(request: Request) -> tuple[str, bool]:
sort = (request.query_params.get("sort") or "row_index").strip()
order = (request.query_params.get("order") or "asc").strip().lower()
desc = order == "desc"
return sort, desc
def price_bounds_from_request(request: Request) -> tuple[float | None, float | None]:
return (
_parse_opt_float(request.query_params.get("price_min")),
_parse_opt_float(request.query_params.get("price_max")),
)
def report_group_from_request(request: Request) -> str:
"""与 §5 矩阵一致的类目名(如饼干、米);查询参数 ``report_group``。"""
return (request.query_params.get("report_group") or "").strip()
def shop_from_request(request: Request) -> str:
"""店铺名精确匹配(与摘要 ``shop_options`` 中某项一致);查询参数 ``shop``。"""
return (request.query_params.get("shop") or "").strip()
def detail_category_q_from_request(request: Request) -> str:
return (request.query_params.get("detail_category_q") or "").strip()
def filter_echo(
*,
report_group: str,
shop: str,
price_min: float | None,
price_max: float | None,
detail_category_q: str,
sort: str,
desc: bool,
) -> dict[str, Any]:
return {
"report_group": report_group or None,
"shop": shop or None,
"price_min": price_min,
"price_max": price_max,
"detail_category_q": detail_category_q or None,
"sort": sort,
"order": "desc" if desc else "asc",
}
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))
sp = shop_from_request(request)
if sp:
qs = qs.filter(shop_name=sp)
pmin, pmax = price_bounds_from_request(request)
if pmin is not None:
qs = qs.filter(price_value__gte=pmin)
if pmax is not None:
qs = qs.filter(price_value__lte=pmax)
return qs
def apply_search_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
sort = sort if sort in SEARCH_SORT_FIELDS else "row_index"
if sort == "price":
return qs.order_by(
OrderBy(F("price_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "total_sales":
return qs.order_by(
OrderBy(F("sales_sort_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "comment_count":
return qs.order_by(
OrderBy(F("comment_count_sort_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "row_index":
return qs.order_by(OrderBy(F("row_index"), descending=desc))
field = {
"sku_id": "sku_id",
"title": "title",
"leaf_category": "leaf_category",
"matrix_group_label": "matrix_group_label",
}[sort]
return qs.order_by(
OrderBy(F(field), descending=desc, nulls_last=True),
"row_index",
)
def apply_detail_filters(qs: QuerySet, request: Request) -> QuerySet:
rg = report_group_from_request(request)
if rg:
qs = qs.filter(matrix_group_label=rg)
sp = shop_from_request(request)
if sp:
qs = qs.filter(detail_shop_name=sp)
q = detail_category_q_from_request(request)
if q:
qs = qs.filter(detail_category_path__icontains=q)
pmin, pmax = price_bounds_from_request(request)
if pmin is not None:
qs = qs.filter(detail_price_value__gte=pmin)
if pmax is not None:
qs = qs.filter(detail_price_value__lte=pmax)
return qs
def apply_detail_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
sort = sort if sort in DETAIL_SORT_FIELDS else "row_index"
if sort == "price":
return qs.order_by(
OrderBy(F("detail_price_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "row_index":
return qs.order_by(OrderBy(F("row_index"), descending=desc))
field = {
"sku_id": "sku_id",
"detail_category_path": "detail_category_path",
"detail_brand": "detail_brand",
"matrix_group_label": "matrix_group_label",
}[sort]
return qs.order_by(
OrderBy(F(field), descending=desc, nulls_last=True),
"row_index",
)
def apply_merged_filters(qs: QuerySet, request: Request) -> QuerySet:
rg = report_group_from_request(request)
if rg:
qs = qs.filter(matrix_group_label=rg)
sp = shop_from_request(request)
if sp:
qs = qs.filter(Q(shop_name=sp) | Q(detail_shop_name=sp))
q = detail_category_q_from_request(request)
if q:
qs = qs.filter(detail_category_path__icontains=q)
pmin, pmax = price_bounds_from_request(request)
if pmin is not None:
qs = qs.filter(price_value__gte=pmin)
if pmax is not None:
qs = qs.filter(price_value__lte=pmax)
return qs
def apply_merged_order(qs: QuerySet, sort: str, desc: bool) -> QuerySet:
sort = sort if sort in MERGED_SORT_FIELDS else "row_index"
if sort == "price":
return qs.order_by(
OrderBy(F("price_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "total_sales":
return qs.order_by(
OrderBy(F("sales_sort_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "comment_count":
return qs.order_by(
OrderBy(F("comment_count_sort_value"), descending=desc, nulls_last=True),
"row_index",
)
if sort == "row_index":
return qs.order_by(OrderBy(F("row_index"), descending=desc))
field = {
"sku_id": "sku_id",
"title": "title",
"leaf_category": "leaf_category",
"detail_category_path": "detail_category_path",
"matrix_group_label": "matrix_group_label",
}[sort]
return qs.order_by(
OrderBy(F(field), descending=desc, nulls_last=True),
"row_index",
)