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 | 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 模式分页前调用排序 |
Files:
backend/app/crud/qc_inbound_lab_storage_sort.pyCreate: backend/tests/test_qc_inbound_lab_storage_sort.py
[ ] Step 1: Write the failing test
# 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,
)
Run:
cd backend && source venv/bin/activate && python -m pytest tests/test_qc_inbound_lab_storage_sort.py -v
Expected: FAIL(模块不存在或导入失败)
# 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(),
)
cd backend && source venv/bin/activate && python -m pytest tests/test_qc_inbound_lab_storage_sort.py -v
Expected: PASS
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
)"
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 文件顶部附近增加导入:
from app.crud.qc_inbound_lab_storage_sort import (
inbound_lab_storage_order_by_clauses,
uses_inbound_lab_storage_list_sort,
)
将:
rows = (
_filtered()
.order_by(BizQcWorkOrder.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
替换为:
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()
)
cd backend && source venv/bin/activate && python -c "from app.crud.qc_work_order import list_work_orders; print('ok')"
Expected: ok
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
)"
list_mine_tasks 应用入库排序Files:
Modify: backend/app/crud/qc_task.py(list_mine_tasks 的 order_by,约 637–644 行)
[ ] Step 1: Wire order_by
增加导入:
from app.crud.qc_inbound_lab_storage_sort import (
inbound_lab_storage_order_by_clauses,
uses_inbound_lab_storage_list_sort,
)
将 list_mine_tasks 内:
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()
)
替换为:
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(与现有签名一致)。
cd backend && source venv/bin/activate && python -c "from app.crud.qc_task import list_mine_tasks; print('ok')"
Expected: ok
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
)"
Files:
web-admin/src/utils/qc-inbound-lab-storage-sort.tsweb-admin 无单测 runner;用纯函数 + 节点内联断言冒烟(或手动对照设计文档)。
// 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
}
在仓库根用短脚本确认比较方向(复制函数体或通过构建后的 bundle 非必须;优先在实现后浏览器验)。
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
)"
Files:
web-admin/src/utils/qc-work-order-groups.tsModify: web-admin/src/utils/qc-work-order-inbound-sample-cards.ts
[ ] Step 1: Inbound-only within-group sort
在 qc-work-order-groups.ts:
保留现有 sortWorkOrdersInGroup(出库继续用)。新增:
function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) {
return [...rows].sort((a, b) =>
compareLabStorageThenCreateTimeDesc(
'', // 同组编码相同,仅比时间;传空避免干扰
a.createTime,
'',
b.createTime
)
)
}
更直接:
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 可省略):
// 无需改 outbound
buildInboundSampleListItems在 qc-work-order-inbound-sample-cards.ts 增加 import:
import {
compareLabStorageThenCreateTimeDesc,
resolveGroupLabStorageCode,
resolveGroupMaxCreateTime
} from '@/utils/qc-inbound-lab-storage-sort'
将末尾:
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 || '')
})
替换为:
const sortedGroups = [...groups].sort((a, b) =>
compareLabStorageThenCreateTimeDesc(
resolveGroupLabStorageCode(a.workOrders),
resolveGroupMaxCreateTime(a.workOrders),
resolveGroupLabStorageCode(b.workOrders),
resolveGroupMaxCreateTime(b.workOrders)
)
)
若 protocolSortTime 不再被本文件使用,删除该死函数。
同协议胶囊内按 sampleCode 的排序(buildLinksFromVisibleGroups 等)保持不变。
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
)"
Files:
web-admin/src/utils/qc-mine-task-groups.tsModify: web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts
[ ] Step 1: Export sorter
在 qc-mine-task-groups.ts 增加:
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 块。
loadData在 useQcWorkOrderTabPage.ts 的 mine 分支,在 filterMineEntriesByWorkOrderSearch 之后、paginateMineEntries 之前:
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(后者可能已导入)。
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
)"
Files:
无业务代码;跑 scripts/update-graph.sh
[ ] Step 1: Update knowledge graph
./scripts/update-graph.sh all
/#/quality-group/quality-manage/qc-work-order?tab=inbound_cell_storage:卡片标题(实验室入库编码)整体倒序;同编码时创建时间新的在上;翻页后仍符合该序。inbound_research_donation:同上。/#/quality-group/quality-manage/my-qc-task 对应两 Tab:同上。将 docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md 状态改为 已实现。
| 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 在前)。