From 7579c36fc661920532d321bb59dfb228daee1e08 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Tue, 8 Sep 2026 22:34:33 +0800 Subject: [PATCH] docs: add DB weighbridge date column sort implementation plan. Co-authored-by: Cursor --- .../plans/2026-09-08-db-date-column-sort.md | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-08-db-date-column-sort.md diff --git a/docs/superpowers/plans/2026-09-08-db-date-column-sort.md b/docs/superpowers/plans/2026-09-08-db-date-column-sort.md new file mode 100644 index 000000000..6facb146e --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-db-date-column-sort.md @@ -0,0 +1,376 @@ +# 地磅域日期列头双三角排序 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: 写失败测试** + +```python +# 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** + +```bash +cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_db_list_sort.py -v +``` + +- [ ] **Step 3: 实现 helper** + +```python +# 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** + +```bash +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 字段** + +```python +from app.services.db.list_sort import order_inventory, parse_sort_params +``` + +在 `InventoryExportBody` 增加: + +```python +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()` 之前: + +```python +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。) + +推荐简化: + +```python +sf, so = parse_sort_params(params) +query = query.order_by( + order_inventory(sf, so, default=DbInboundOutbound.date.desc()), + DbInboundOutbound.id.desc(), +) +``` + +- [ ] **Step 3: Commit** + +```bash +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** + +```python +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()` 之前: + +```python +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** + +```bash +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** + +```ts +import { useColumnSort } from '@/composables/useColumnSort' +// vue import 增加 provide +``` + +在 `useServerColumnFilters` 之后: + +```ts +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`** + +```ts +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 合并** + +```ts +const params: Record = { ...toQueryParams(), ...toSortQueryParams() } +``` + +导出 `getFilterParams` 若单独组装,同样合并 `toSortQueryParams()`。 + +- [ ] **Step 4: Commit** + +```bash +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` 内排序** + +现网: + +```ts +allData.value = records.slice().sort((a, b) => getRecordDate(b).localeCompare(getRecordDate(a))) +``` + +改为按 `sortField`/`sortOrder`(缺省 `inventoryDate` 语义用 `getRecordDate`,`desc`): + +```ts +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** + +```bash +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` 后提交图谱: + +```bash +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。