# 入库质检列表按实验室入库编码排序 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:** 【质检工单】与【我的质检任务】的「细胞存储」「科研捐赠」Tab,按实验室入库编码倒序、同编码按创建时间倒序分页与展示,跨页一致。 **Architecture:** 后端在入库两 Tab 的列表查询上用相关子查询解析 `lab_storage_code`(优先 `reception_id`,否则按样本编码),`ORDER BY COALESCE(code,'') DESC, create_time DESC`;前端入库卡片与我的任务聚合排序对齐同一比较规则;出库组内排序不动。 **Tech Stack:** FastAPI / SQLAlchemy 2.0 / Vue 3 / TypeScript;设计依据 `docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md`。 **Spec:** `docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md` --- ## File map | File | Responsibility | |------|----------------| | Create: `backend/app/crud/qc_inbound_lab_storage_sort.py` | 判断是否启用编码排序、相关子查询表达式、`order_by` 元组 | | Modify: `backend/app/crud/qc_work_order.py` | `list_work_orders` 入库两 Tab 使用新 `order_by` | | Modify: `backend/app/crud/qc_task.py` | `list_mine_tasks` 入库两 Tab 使用新 `order_by` | | Create: `backend/tests/test_qc_inbound_lab_storage_sort.py` | 纯函数:来源判断 + 排序键比较语义 | | Create: `web-admin/src/utils/qc-inbound-lab-storage-sort.ts` | 前端统一比较:编码 DESC → 时间 DESC | | Modify: `web-admin/src/utils/qc-work-order-groups.ts` | 仅入库组内改为时间倒序 | | Modify: `web-admin/src/utils/qc-work-order-inbound-sample-cards.ts` | 卡片列表主序改为编码/时间 | | Modify: `web-admin/src/utils/qc-mine-task-groups.ts` | 导出按编码排序的工单条目 | | Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts` | mine 模式分页前调用排序 | --- ### Task 1: 后端纯函数 — 来源判断与比较语义 **Files:** - Create: `backend/app/crud/qc_inbound_lab_storage_sort.py` - Create: `backend/tests/test_qc_inbound_lab_storage_sort.py` - [ ] **Step 1: Write the failing test** ```python # backend/tests/test_qc_inbound_lab_storage_sort.py """入库质检列表:实验室入库编码排序语义。""" import unittest from app.crud.qc_inbound_lab_storage_sort import ( compare_lab_storage_then_create_time_desc, uses_inbound_lab_storage_list_sort, ) class TestUsesInboundLabStorageListSort(unittest.TestCase): def test_cell_storage_and_research_donation(self): self.assertTrue(uses_inbound_lab_storage_list_sort("cell_storage")) self.assertTrue(uses_inbound_lab_storage_list_sort("research_donation")) def test_other_sources_false(self): self.assertFalse(uses_inbound_lab_storage_list_sort("")) self.assertFalse(uses_inbound_lab_storage_list_sort(None)) self.assertFalse(uses_inbound_lab_storage_list_sort("temporary")) class TestCompareLabStorageThenCreateTimeDesc(unittest.TestCase): def test_code_desc_empty_as_empty_string(self): # '' < 'B0000001' lexicographically → DESC 时有编码在前 self.assertEqual( compare_lab_storage_then_create_time_desc("B0000001", "2026-01-01", "", "2026-12-01"), -1, ) self.assertEqual( compare_lab_storage_then_create_time_desc("", "2026-12-01", "B0000001", "2026-01-01"), 1, ) def test_same_code_newer_create_time_first(self): self.assertEqual( compare_lab_storage_then_create_time_desc( "B1", "2026-08-02 10:00:00", "B1", "2026-08-01 10:00:00" ), -1, ) def test_higher_code_first(self): self.assertEqual( compare_lab_storage_then_create_time_desc("B2", "t", "B1", "t"), -1, ) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd backend && source venv/bin/activate && python -m pytest tests/test_qc_inbound_lab_storage_sort.py -v ``` Expected: FAIL(模块不存在或导入失败) - [ ] **Step 3: Write minimal implementation** ```python # backend/app/crud/qc_inbound_lab_storage_sort.py """入库质检列表:按实验室入库编码排序的辅助逻辑。""" from __future__ import annotations from typing import Optional from sqlalchemy import and_, func, select from sqlalchemy.sql import ColumnElement from app.models.models import BizQcWorkOrder, BizSampleReception _INBOUND_LAB_SORT_SOURCES = frozenset({"cell_storage", "research_donation"}) def uses_inbound_lab_storage_list_sort(reception_source: Optional[str]) -> bool: return (reception_source or "").strip() in _INBOUND_LAB_SORT_SOURCES def compare_lab_storage_then_create_time_desc( code_a: Optional[str], time_a: Optional[str], code_b: Optional[str], time_b: Optional[str], ) -> int: """列表序比较:编码 DESC,同编码时间 DESC。返回负值表示 a 应排在 b 前。""" ca = (code_a or "").strip() cb = (code_b or "").strip() if ca != cb: # DESC: 较大字符串在前 return -1 if ca > cb else 1 ta = (time_a or "").strip() tb = (time_b or "").strip() if ta != tb: return -1 if ta > tb else 1 return 0 def lab_storage_code_expr_for_work_order() -> ColumnElement: """相关子查询:优先 reception_id,否则同租户 sample_code 最新一条接收记录的编码。""" by_id = ( select(BizSampleReception.lab_storage_code) .where( and_( BizSampleReception.id == BizQcWorkOrder.reception_id, BizSampleReception.del_flag == 0, ) ) .correlate(BizQcWorkOrder) .scalar_subquery() ) by_sample = ( select(BizSampleReception.lab_storage_code) .where( and_( BizSampleReception.sample_code == BizQcWorkOrder.sample_code, BizSampleReception.tenant_no == BizQcWorkOrder.tenant_no, BizSampleReception.del_flag == 0, BizQcWorkOrder.sample_code.isnot(None), BizQcWorkOrder.sample_code != "", ) ) .order_by(BizSampleReception.create_time.desc()) .limit(1) .correlate(BizQcWorkOrder) .scalar_subquery() ) return func.coalesce(by_id, by_sample, "") def inbound_lab_storage_order_by_clauses(): """供 Query.order_by(*clauses) 使用。""" code = lab_storage_code_expr_for_work_order() return ( code.desc(), BizQcWorkOrder.create_time.desc(), ) ``` - [ ] **Step 4: Run tests to verify they pass** ```bash cd backend && source venv/bin/activate && python -m pytest tests/test_qc_inbound_lab_storage_sort.py -v ``` Expected: PASS - [ ] **Step 5: Commit**(仅当用户明确要求提交时执行) ```bash git add backend/app/crud/qc_inbound_lab_storage_sort.py backend/tests/test_qc_inbound_lab_storage_sort.py git commit -m "$(cat <<'EOF' feat: add inbound QC list sort helpers by lab storage code EOF )" ``` --- ### Task 2: `list_work_orders` 应用入库排序 **Files:** - Modify: `backend/app/crud/qc_work_order.py`(`list_work_orders` 中约 1706–1715 行的 `order_by`) - [ ] **Step 1: Wire order_by** 在 `list_work_orders` 文件顶部附近增加导入: ```python from app.crud.qc_inbound_lab_storage_sort import ( inbound_lab_storage_order_by_clauses, uses_inbound_lab_storage_list_sort, ) ``` 将: ```python rows = ( _filtered() .order_by(BizQcWorkOrder.create_time.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) ``` 替换为: ```python q_page = _filtered() if uses_inbound_lab_storage_list_sort(reception_source): q_page = q_page.order_by(*inbound_lab_storage_order_by_clauses()) else: q_page = q_page.order_by(BizQcWorkOrder.create_time.desc()) rows = ( q_page .offset((page - 1) * page_size) .limit(page_size) .all() ) ``` - [ ] **Step 2: Sanity import check** ```bash cd backend && source venv/bin/activate && python -c "from app.crud.qc_work_order import list_work_orders; print('ok')" ``` Expected: `ok` - [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) ```bash git add backend/app/crud/qc_work_order.py git commit -m "$(cat <<'EOF' feat: order inbound QC work orders by lab storage code then create time EOF )" ``` --- ### Task 3: `list_mine_tasks` 应用入库排序 **Files:** - Modify: `backend/app/crud/qc_task.py`(`list_mine_tasks` 的 `order_by`,约 637–644 行) - [ ] **Step 1: Wire order_by** 增加导入: ```python from app.crud.qc_inbound_lab_storage_sort import ( inbound_lab_storage_order_by_clauses, uses_inbound_lab_storage_list_sort, ) ``` 将 `list_mine_tasks` 内: ```python rows = ( _filtered() .order_by( BizQcTask.create_time.desc(), BizQcWorkOrder.work_order_no.desc(), BizQcTask.task_no.asc(), ) .offset((page - 1) * page_size) .limit(page_size) .all() ) ``` 替换为: ```python q_page = _filtered() if uses_inbound_lab_storage_list_sort(reception_source): q_page = q_page.order_by( *inbound_lab_storage_order_by_clauses(), BizQcWorkOrder.work_order_no.desc(), BizQcTask.task_no.asc(), ) else: q_page = q_page.order_by( BizQcTask.create_time.desc(), BizQcWorkOrder.work_order_no.desc(), BizQcTask.task_no.asc(), ) rows = ( q_page .offset((page - 1) * page_size) .limit(page_size) .all() ) ``` 注意:确认 `list_mine_tasks` 形参名确为 `reception_source`(与现有签名一致)。 - [ ] **Step 2: Sanity import check** ```bash cd backend && source venv/bin/activate && python -c "from app.crud.qc_task import list_mine_tasks; print('ok')" ``` Expected: `ok` - [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) ```bash git add backend/app/crud/qc_task.py git commit -m "$(cat <<'EOF' feat: order mine QC tasks by lab storage code on inbound tabs EOF )" ``` --- ### Task 4: 前端统一比较工具 **Files:** - Create: `web-admin/src/utils/qc-inbound-lab-storage-sort.ts` web-admin 无单测 runner;用纯函数 + 节点内联断言冒烟(或手动对照设计文档)。 - [ ] **Step 1: Add compare helpers** ```typescript // web-admin/src/utils/qc-inbound-lab-storage-sort.ts /** 编码 DESC → 时间 DESC;负值表示 a 排在 b 前(与后端一致) */ export function compareLabStorageThenCreateTimeDesc( codeA: string | null | undefined, timeA: string | null | undefined, codeB: string | null | undefined, timeB: string | null | undefined ): number { const ca = String(codeA || '').trim() const cb = String(codeB || '').trim() if (ca !== cb) { return ca > cb ? -1 : 1 } const ta = String(timeA || '').trim() const tb = String(timeB || '').trim() if (ta !== tb) { return ta > tb ? -1 : 1 } return 0 } export function resolveGroupLabStorageCode( rows: { labStorageCode?: string | null }[] ): string { for (const row of rows) { const code = String(row.labStorageCode || '').trim() if (code) return code } return '' } export function resolveGroupMaxCreateTime( rows: { createTime?: string | null }[] ): string { let max = '' for (const row of rows) { const t = String(row.createTime || '') if (t > max) max = t } return max } ``` - [ ] **Step 2: Smoke-check in Node(可选)** 在仓库根用短脚本确认比较方向(复制函数体或通过构建后的 bundle 非必须;优先在实现后浏览器验)。 - [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) ```bash git add web-admin/src/utils/qc-inbound-lab-storage-sort.ts git commit -m "$(cat <<'EOF' feat: add frontend inbound lab storage sort compare helpers EOF )" ``` --- ### Task 5: 入库组内与样本卡片排序 **Files:** - Modify: `web-admin/src/utils/qc-work-order-groups.ts` - Modify: `web-admin/src/utils/qc-work-order-inbound-sample-cards.ts` - [ ] **Step 1: Inbound-only within-group sort** 在 `qc-work-order-groups.ts`: 保留现有 `sortWorkOrdersInGroup`(出库继续用)。新增: ```typescript function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) { return [...rows].sort((a, b) => compareLabStorageThenCreateTimeDesc( '', // 同组编码相同,仅比时间;传空避免干扰 a.createTime, '', b.createTime ) ) } ``` 更直接: ```typescript function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) { return [...rows].sort((a, b) => String(b.createTime || '').localeCompare(String(a.createTime || '')) ) } ``` 将 `groupInboundQcWorkOrders` 内 `sortWorkOrdersInGroup(workOrders)` 改为 `sortWorkOrdersInInboundGroup(workOrders)`。 `groupOutboundQcWorkOrders` 仍调用 `sortWorkOrdersInGroup`。 顶部增加(若用 compare 工具则 import;若仅 localeCompare 可省略): ```typescript // 无需改 outbound ``` - [ ] **Step 2: Card list order in `buildInboundSampleListItems`** 在 `qc-work-order-inbound-sample-cards.ts` 增加 import: ```typescript import { compareLabStorageThenCreateTimeDesc, resolveGroupLabStorageCode, resolveGroupMaxCreateTime } from '@/utils/qc-inbound-lab-storage-sort' ``` 将末尾: ```typescript const sortedGroups = [...groups].sort((a, b) => { const headA = a.workOrders[0] const headB = b.workOrders[0] if (!headA || !headB) return 0 const protocolA = resolveInboundProtocolGroupKey(headA) const protocolB = resolveInboundProtocolGroupKey(headB) if (protocolA !== protocolB) { return protocolSortTime(b).localeCompare(protocolSortTime(a)) } return (a.sampleCode || '').localeCompare(b.sampleCode || '') }) ``` 替换为: ```typescript const sortedGroups = [...groups].sort((a, b) => compareLabStorageThenCreateTimeDesc( resolveGroupLabStorageCode(a.workOrders), resolveGroupMaxCreateTime(a.workOrders), resolveGroupLabStorageCode(b.workOrders), resolveGroupMaxCreateTime(b.workOrders) ) ) ``` 若 `protocolSortTime` 不再被本文件使用,删除该死函数。 同协议胶囊内按 `sampleCode` 的排序(`buildLinksFromVisibleGroups` 等)**保持不变**。 - [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) ```bash git add web-admin/src/utils/qc-work-order-groups.ts web-admin/src/utils/qc-work-order-inbound-sample-cards.ts git commit -m "$(cat <<'EOF' feat: sort inbound QC sample cards by lab storage code then create time EOF )" ``` --- ### Task 6: 我的质检任务分页前排序 **Files:** - Modify: `web-admin/src/utils/qc-mine-task-groups.ts` - Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts` - [ ] **Step 1: Export sorter** 在 `qc-mine-task-groups.ts` 增加: ```typescript import { compareLabStorageThenCreateTimeDesc } from '@/utils/qc-inbound-lab-storage-sort' import { isInboundSampleQcTab } from '@/utils/qc-work-order-tabs' export function sortMineEntriesForInboundLabStorage( entries: MineWorkOrderEntry[] ): MineWorkOrderEntry[] { return [...entries].sort((a, b) => compareLabStorageThenCreateTimeDesc( a.workOrder.labStorageCode, a.workOrder.createTime, b.workOrder.labStorageCode, b.workOrder.createTime ) ) } ``` 确认 `isInboundSampleQcTab` 已从 tabs 导出;若已有同类 import,合并到现有 import 块。 - [ ] **Step 2: Call before paginate in mine `loadData`** 在 `useQcWorkOrderTabPage.ts` 的 mine 分支,在 `filterMineEntriesByWorkOrderSearch` 之后、`paginateMineEntries` 之前: ```typescript let entries = groupMineTasksByWorkOrder(tasks) entries = filterMineEntriesByWorkOrderSearch(entries, buildMineWorkOrderSearchFilters()) if (isInboundSampleQcTab(tabKey.value)) { entries = sortMineEntriesForInboundLabStorage(entries) } const pageResult = paginateMineEntries(entries, pagination.page, pagination.pageSize) ``` 补充 import:`sortMineEntriesForInboundLabStorage`、`isInboundSampleQcTab`(后者可能已导入)。 - [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) ```bash git add web-admin/src/utils/qc-mine-task-groups.ts web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts git commit -m "$(cat <<'EOF' feat: sort mine inbound QC entries by lab storage code before paging EOF )" ``` --- ### Task 7: 图谱更新与手工验收 **Files:** - 无业务代码;跑 `scripts/update-graph.sh` - [ ] **Step 1: Update knowledge graph** ```bash ./scripts/update-graph.sh all ``` - [ ] **Step 2: Manual QA checklist** 1. 打开 `/#/quality-group/quality-manage/qc-work-order?tab=inbound_cell_storage`:卡片标题(实验室入库编码)整体倒序;同编码时创建时间新的在上;翻页后仍符合该序。 2. 切换 `inbound_research_donation`:同上。 3. `/#/quality-group/quality-manage/my-qc-task` 对应两 Tab:同上。 4. 无编码卡片按空串参与倒序(相对有编码的位置与字符串 DESC 一致)。 5. 「全部 / 出库 / 临时」Tab 顺序与交互无明显回归;同协议样本胶囊顺序仍按样本编码。 - [ ] **Step 3: Mark design status(可选)** 将 `docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md` 状态改为 `已实现`。 --- ## Spec coverage (self-review) | Spec 项 | Task | |---------|------| | 仅两页两 Tab | Task 2/3 条件 `uses_inbound_*`;Task 5/6 仅 inbound | | 编码 DESC + 时间 DESC | Task 1–6 | | 空串参与倒序 | Task 1 测试 + COALESCE/COALESCE | | 后端分页 | Task 2 | | 我的任务 | Task 3 + Task 6 | | 组内时间倒序、出库不变 | Task 5 | | 互链不改 | Task 5 明确保留 | | 无 DB 迁移 | 全程 JOIN/子查询 | 无 TBD 占位;前后端比较函数命名与语义一致(负值 = a 在前)。