Files
shanxia2.0/docs/superpowers/plans/2026-09-08-db-date-column-sort.md
T

11 KiB
Raw Blame History

地磅域日期列头双三角排序 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 地磅出入库 date、地磅单 inventoryDate 列头双三角排序;列表与导出一致;地磅计划本批跳过。

Architecture: 复用已有 ColumnHeaderFilter inject + useColumnSort。两页现网为拉全量 + 前端切片:列表改客户端排序;_fetch_* 接白名单 order_by 保证导出序。inventoryDate 经 outerjoin 出入库表的 date。

Tech Stack: Vue 3、useColumnSort、FastAPI、SQLAlchemy、resolve_datetime_order_by

Spec: docs/superpowers/specs/2026-09-08-db-date-column-sort-design.md


文件结构

操作 文件 职责
Create backend/app/services/db/list_sort.py 出入库/地磅单白名单 + parse
Create backend/tests/test_db_list_sort.py 单测
Modify backend/app/api/db/inventory.py _fetch_inventory 排序;ExportBody 显式 sort 字段
Modify backend/app/api/db/weighing.py _fetch_weighing join+排序;ExportBody
Modify web-admin/src/views/db/inventory/index.vue provide + applySortAndPage
Modify web-admin/src/views/db/weighing/index.vue provide + loadData 排序

不做: plans、dashboard;毛/皮重时间列。


Task 1: list_sort helper + 测试

Files:

  • Create: backend/app/services/db/list_sort.py

  • Create: backend/tests/test_db_list_sort.py

  • Step 1: 写失败测试

# backend/tests/test_db_list_sort.py
from app.models.db_models import DbInboundOutbound, DbWeighingSlip
from app.services.db.list_sort import (
    order_inventory,
    order_weighing,
    parse_sort_params,
    weighing_needs_inventory_join,
)


def test_inventory_date_asc():
    expr = order_inventory("date", "asc")
    assert "date" in str(expr).lower()
    assert "asc" in str(expr).lower()


def test_inventory_invalid_falls_back():
    default = DbInboundOutbound.date.desc()
    assert order_inventory("hack", "asc", default=default) is default


def test_weighing_inventory_date_desc():
    expr = order_weighing("inventoryDate", "desc")
    assert "date" in str(expr).lower()
    assert "desc" in str(expr).lower()


def test_weighing_needs_join():
    assert weighing_needs_inventory_join("inventoryDate", "asc") is True
    assert weighing_needs_inventory_join("inventoryDate", "nope") is False
    assert weighing_needs_inventory_join(None, "asc") is False


def test_parse_sort_params_camel():
    sf, so = parse_sort_params({"sortField": "date", "sortOrder": "asc"})
    assert sf == "date"
    assert so == "asc"
  • Step 2: 跑测应 FAIL
cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_db_list_sort.py -v
  • Step 3: 实现 helper
# backend/app/services/db/list_sort.py
from __future__ import annotations
from typing import Optional

from app.models.db_models import DbInboundOutbound, DbWeighingSlip
from app.services.crm.list_sort import resolve_datetime_order_by

INVENTORY_WHITELIST = {"date": DbInboundOutbound.date}
WEIGHING_WHITELIST = {"inventoryDate": DbInboundOutbound.date}


def order_inventory(sort_field: Optional[str], sort_order: Optional[str], default=None):
    if default is None:
        default = DbInboundOutbound.date.desc()
    return resolve_datetime_order_by(sort_field, sort_order, INVENTORY_WHITELIST, default)


def order_weighing(sort_field: Optional[str], sort_order: Optional[str], default=None):
    if default is None:
        default = DbWeighingSlip.gross_weight_time.desc()
    return resolve_datetime_order_by(sort_field, sort_order, WEIGHING_WHITELIST, default)


def weighing_needs_inventory_join(sort_field: Optional[str], sort_order: Optional[str]) -> bool:
    field = (sort_field or "").strip()
    order = (sort_order or "").strip().lower()
    return field == "inventoryDate" and order in ("asc", "desc")


def parse_sort_params(params: dict) -> tuple:
    sf = params.get("sortField") or params.get("sort_field")
    so = params.get("sortOrder") or params.get("sort_order")
    return (str(sf).strip() if sf else None), (str(so).strip() if so else None)
  • Step 4: 测通并 Commit
PYTHONPATH=. pytest tests/test_db_list_sort.py -q
git add backend/app/services/db/list_sort.py backend/tests/test_db_list_sort.py
git commit -m "feat(db): add datetime list sort helpers for inventory and weighing."

Task 2: 出入库 _fetch_inventory 接排序

Files:

  • Modify: backend/app/api/db/inventory.py

  • Step 1: Import + ExportBody 字段

from app.services.db.list_sort import order_inventory, parse_sort_params

在 InventoryExportBody 增加:

sort_field: Optional[str] = Field(default=None, alias="sortField")
sort_order: Optional[str] = Field(default=None, alias="sortOrder")
  • Step 2: 改 _fetch_inventory 排序时机

去掉 _base_query(...).order_by(date.desc(), id.desc()) 的链式 order_by。在 apply_db_scope 之后、query.all() 之前:

sf, so = parse_sort_params(params)
default = (DbInboundOutbound.date.desc(), DbInboundOutbound.id.desc())
primary = order_inventory(sf, so, default=DbInboundOutbound.date.desc())
if sf and so and (sf or "").strip() == "date" and (so or "").strip().lower() in ("asc", "desc"):
    query = query.order_by(primary, DbInboundOutbound.id.desc())
else:
    query = query.order_by(*default)

(若 order_inventory 对非法已回退 default 单列,可用:query.order_by(order_inventory(sf, so), DbInboundOutbound.id.desc()),默认即 date.desc + id.desc。)

推荐简化:

sf, so = parse_sort_params(params)
query = query.order_by(
    order_inventory(sf, so, default=DbInboundOutbound.date.desc()),
    DbInboundOutbound.id.desc(),
)
  • Step 3: Commit
git commit -m "feat(db): support datetime column sort on inventory lists and export."

Task 3: 地磅单 _fetch_weighing join + 排序

Files:

  • Modify: backend/app/api/db/weighing.py

  • Step 1: Import + ExportBody

from sqlalchemy import and_, func, or_
from app.services.db.list_sort import (
    order_weighing,
    parse_sort_params,
    weighing_needs_inventory_join,
)

WeighingExportBody 增加 sortField / sortOrder(同 Task 2)。

  • Step 2: 改 _fetch_weighing

去掉开头 .order_by(DbWeighingSlip.gross_weight_time.desc())。在 scope 之后、.all() 之前:

sf, so = parse_sort_params(params)
if weighing_needs_inventory_join(sf, so):
    query = query.outerjoin(
        DbInboundOutbound,
        and_(
            DbWeighingSlip.inbound_outbound_id == DbInboundOutbound.id,
            DbInboundOutbound.del_flag == 0,
        ),
    )
query = query.order_by(
    order_weighing(sf, so, default=DbWeighingSlip.gross_weight_time.desc()),
    DbWeighingSlip.id.desc(),
)
weighings = query.all()

后续 inventory_date_map 逻辑不变。

  • Step 3: Commit
git commit -m "feat(db): support inventoryDate column sort on weighing lists and export."

Task 4: 接线出入库前端

Files:

  • Modify: web-admin/src/views/db/inventory/index.vue

  • Step 1: import / provide

import { useColumnSort } from '@/composables/useColumnSort'
// vue import 增加 provide

在 useServerColumnFilters 之后:

const {
  sortField,
  sortOrder,
  setSort,
  clearSort,
  isSortActive,
  toQueryParams: toSortQueryParams,
  watchVisibleColumns: watchSortVisibleColumns
} = useColumnSort({
  onChange: () => {
    currentPage.value = 1
    applySortAndPage()
  }
})

provide('crmColumnSort', { setSort, clearSort, isSortActive, sortField, sortOrder })

对已有 visibleFilterProps 调用 watchSortVisibleColumns(visibleFilterProps)。

  • Step 2: 改 applySortAndPage
const applySortAndPage = () => {
  const field = sortField.value || 'date'
  const order = sortOrder.value || 'desc'
  const list = [...allRecords.value].sort((a, b) => {
    const av = String(a[field] || '')
    const bv = String(b[field] || '')
    if (!av && !bv) return 0
    if (!av) return order === 'asc' ? -1 : 1
    if (!bv) return order === 'asc' ? 1 : -1
    const cmp = av.localeCompare(bv, 'zh-CN')
    return order === 'asc' ? cmp : -cmp
  })
  total.value = list.length
  const start = (currentPage.value - 1) * pageSize.value
  const end = start + pageSize.value
  tableData.value = list.slice(start, end)
}
  • Step 3: buildListQueryParams / 导出 params 合并
const params: Record<string, any> = { ...toQueryParams(), ...toSortQueryParams() }

导出 getFilterParams 若单独组装,同样合并 toSortQueryParams()。

  • Step 4: Commit
git commit -m "feat(web-admin): wire date column sort on db inventory page."

Task 5: 接线地磅单前端

Files:

  • Modify: web-admin/src/views/db/weighing/index.vue

  • Step 1: 同 Task 4 接入 useColumnSort + provide + watchSortVisibleColumns;onChange → currentPage=1 后对 allData 重排并刷新切片(可不重新请求,也可 loadData())。

  • Step 2: 改 loadData 内排序

现网:

allData.value = records.slice().sort((a, b) => getRecordDate(b).localeCompare(getRecordDate(a)))

改为按 sortField/sortOrder(缺省 inventoryDate 语义用 getRecordDate,desc):

function sortWeighingRows(rows: WeighingRecord[]) {
  const order = sortOrder.value || 'desc'
  return [...rows].sort((a, b) => {
    const av = getRecordDate(a)
    const bv = getRecordDate(b)
    if (!av && !bv) return 0
    if (!av) return order === 'asc' ? -1 : 1
    if (!bv) return order === 'asc' ? 1 : -1
    const cmp = av.localeCompare(bv)
    return order === 'asc' ? cmp : -cmp
  })
}
// loadData:
allData.value = sortWeighingRows(records)

onChange 若不重新拉数:allData.value = sortWeighingRows(allData.value)。

  • Step 3: buildListQueryParams / 导出合并 toSortQueryParams()。

  • Step 4: Commit

git commit -m "feat(web-admin): wire date column sort on db weighing page."

Task 6: 回归与图谱

  • Step 1: PYTHONPATH=. pytest tests/test_db_list_sort.py tests/test_bx_list_sort.py -q
  • Step 2: pnpm dlx vitest run src/composables/useColumnSort.test.ts(在 web-admin)
  • Step 3: 手工:出入库日期三角;地磅单日期三角;计划无三角;导出序
  • Step 4: ./scripts/update-graph.sh all 后提交图谱:
git commit -m "chore: update graphify after DB date column sort."

Spec 覆盖自检

Spec 项 Task
出入库 date 2, 4
地磅单 inventoryDate JOIN 1, 3, 5
跳过计划 文件结构「不做」
导出一致 2, 3 + 前端 params
ColumnHeaderFilter 已具备 无需 Task
空值规则 4, 5 客户端 + MySQL JOIN

无 TBD。