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

377 lines
11 KiB
Markdown
Raw 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.
# 地磅域日期列头双三角排序 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<string, any> = { ...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。