docs: add storage release QC completed filter design and plan

Document qcWorkOrderCompleted list filter semantics and implementation steps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-16 12:11:22 +08:00
1 parent 730d0300d1
commit 747b8f1c06
2 files changed
+596

No files matched your search

@@ -0,0 +1,437 @@
# 入库放行审核 · 待审核质检工单完成度过滤 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:** 列表 API 增加可选布尔参数 `qcWorkOrderCompleted`;CRUD 用 `EXISTS`/`NOT EXISTS` 按 `sample_code` 关联 `biz_qc_work_order` 过滤(完成 = 至少一条有效工单且无 `dpz`/`ypz`)。前端仅在待审核展示该筛选项,默认/重置/从已审核切回均为「是」。
**Tech Stack:** FastAPI + SQLAlchemy(backend)、Vue 3 + Element Plus(web-admin)
**Spec:** `docs/superpowers/specs/2026-07-16-storage-release-review-qc-completed-filter-design.md`
---
## File Map
| 文件 | 职责 |
|------|------|
| `backend/app/crud/storage_release_review.py` | `normalize_qc_work_order_completed` + `list_reviews` EXISTS 过滤 |
| `backend/app/api/storage_release_review.py` | 列表 Query `qcWorkOrderCompleted` |
| `backend/tests/test_storage_release_review_qc_completed.py` | **新建** 参数规范化单测 |
| `web-admin/src/api/storage-release-review.ts` | 列表 params 增加 `qcWorkOrderCompleted?: boolean` |
| `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | 待审核筛选项、默认/重置/切 Tab、请求传参 |
---
### Task 1: Backend — 规范化函数 + 列表 EXISTS 过滤 + API
**Files:**
- Create: `backend/tests/test_storage_release_review_qc_completed.py`
- Modify: `backend/app/crud/storage_release_review.py`
- Modify: `backend/app/api/storage_release_review.py`
- [ ] **Step 1: 写失败单测(规范化函数)**
创建 `backend/tests/test_storage_release_review_qc_completed.py`:
```python
from app.crud.storage_release_review import normalize_qc_work_order_completed
def test_normalize_qc_work_order_completed_accepts_bool_and_strings():
assert normalize_qc_work_order_completed(True) is True
assert normalize_qc_work_order_completed(False) is False
assert normalize_qc_work_order_completed("true") is True
assert normalize_qc_work_order_completed("TRUE") is True
assert normalize_qc_work_order_completed("1") is True
assert normalize_qc_work_order_completed("yes") is True
assert normalize_qc_work_order_completed("false") is False
assert normalize_qc_work_order_completed("0") is False
assert normalize_qc_work_order_completed("no") is False
assert normalize_qc_work_order_completed(" FALSE ") is False
def test_normalize_qc_work_order_completed_rejects_unknown():
assert normalize_qc_work_order_completed(None) is None
assert normalize_qc_work_order_completed("") is None
assert normalize_qc_work_order_completed("all") is None
assert normalize_qc_work_order_completed("maybe") is None
```
- [ ] **Step 2: 跑单测确认失败**
```bash
cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_qc_completed.py -v
```
Expected: FAIL(`normalize_qc_work_order_completed` 未定义或导入失败)
- [ ] **Step 3: 实现 normalize、EXISTS 辅助过滤,并接入 `list_reviews`**
在 `backend/app/crud/storage_release_review.py`:
1) 更新 import:
```python
from sqlalchemy import and_, exists, func, or_
```
(`BizQcWorkOrder` 已在 models 导入中,无需新增。)
2) 在 `normalize_review_phase` 附近增加:
```python
def normalize_qc_work_order_completed(raw) -> Optional[bool]:
"""Return True/False when recognizable; None means no filter."""
if raw is None:
return None
if isinstance(raw, bool):
return raw
s = str(raw).strip().lower()
if s in ("true", "1", "yes"):
return True
if s in ("false", "0", "no"):
return False
return None
def _qc_work_order_exists_for_review_sample(
tenant_no: str, *, unfinished_only: bool = False
):
"""Correlate biz_qc_work_order to BizStorageReleaseReview.sample_code."""
conds = [
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.sample_code.isnot(None),
BizQcWorkOrder.sample_code != "",
BizQcWorkOrder.sample_code == BizStorageReleaseReview.sample_code,
BizStorageReleaseReview.sample_code.isnot(None),
BizStorageReleaseReview.sample_code != "",
]
if unfinished_only:
conds.append(BizQcWorkOrder.work_order_status.in_(("dpz", "ypz")))
return (
exists()
.where(and_(*conds))
.correlate(BizStorageReleaseReview)
)
```
3) 修改 `list_reviews` 签名,在 `review_phase` 与 `user_info` 之间增加:
```python
qc_work_order_completed=None,
```
完整签名片段:
```python
def list_reviews(
db: Session,
*,
tenant_no: str,
page: int = 1,
page_size: int = 10,
reception_source: Optional[str] = None,
review_conclusion: Optional[str] = None,
storage_status: Optional[str] = None,
keyword: Optional[str] = None,
review_phase: Optional[str] = None,
qc_work_order_completed=None,
user_info=None,
) -> Dict[str, Any]:
```
4) 在 keyword 过滤之后、`user_info` / data_scope 之前插入:
```python
qc_done = normalize_qc_work_order_completed(qc_work_order_completed)
if qc_done is True:
q0 = q0.filter(
_qc_work_order_exists_for_review_sample(tenant_no, unfinished_only=False),
~_qc_work_order_exists_for_review_sample(tenant_no, unfinished_only=True),
)
elif qc_done is False:
q0 = q0.filter(
or_(
~_qc_work_order_exists_for_review_sample(
tenant_no, unfinished_only=False
),
_qc_work_order_exists_for_review_sample(
tenant_no, unfinished_only=True
),
)
)
```
语义对照(spec §3):
- `True`:至少一条有效工单,且无 `dpz`/`ypz`(`yzf`/`ywc` 均可)
- `False`:无工单,或存在 `dpz`/`ypz`
- `None`:不过滤
- [ ] **Step 4: API 增加 Query 并传入 CRUD**
修改 `backend/app/api/storage_release_review.py` 的 `list_storage_release_reviews`:
```python
@router.get("/storage-release-reviews")
async def list_storage_release_reviews(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
keyword: Optional[str] = None,
receptionSource: Optional[str] = None,
reviewConclusion: Optional[str] = None,
storageStatus: Optional[str] = None,
reviewPhase: Optional[str] = None,
qcWorkOrderCompleted: Optional[bool] = Query(None),
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:storageReleaseReview:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
result = srr_svc.list_reviews(
db,
tenant_no=tenant_no,
page=page,
page_size=pageSize,
reception_source=receptionSource,
review_conclusion=reviewConclusion,
storage_status=storageStatus,
keyword=keyword,
review_phase=reviewPhase,
qc_work_order_completed=qcWorkOrderCompleted,
user_info=user,
)
return success_response(result)
except PermissionError as e:
return error_response(403, str(e))
except Exception as e:
return error_response(400, str(e))
```
说明:FastAPI 会把查询串 `true`/`false` 解析为 bool;缺省为 `None`(不过滤)。CRUD 内仍走 `normalize_qc_work_order_completed`,与单测一致。
- [ ] **Step 5: 跑单测确认通过**
```bash
cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_qc_completed.py tests/test_storage_release_review_phase.py -v
```
Expected: PASS
- [ ] **Step 6: Commit**
```bash
git add \
backend/tests/test_storage_release_review_qc_completed.py \
backend/app/crud/storage_release_review.py \
backend/app/api/storage_release_review.py
git commit -m "$(cat <<'EOF'
feat(storage-release): filter pending list by QC work order completion
Add qcWorkOrderCompleted query param and EXISTS-based sample_code filter
so pending reviews can default to QC-complete samples only.
EOF
)"
```
---
### Task 2: Frontend — API 类型 + 待审核筛选项与 Tab 行为
**Files:**
- Modify: `web-admin/src/api/storage-release-review.ts`
- Modify: `web-admin/src/views/quality-inspection/storage-release-review/index.vue`
- [ ] **Step 1: API 参数增加 `qcWorkOrderCompleted`**
修改 `web-admin/src/api/storage-release-review.ts` 中 `fetchStorageReleaseReviewList`:
```typescript
export function fetchStorageReleaseReviewList(params?: {
page?: number
pageSize?: number
receptionSource?: string
reviewConclusion?: string
storageStatus?: string
keyword?: string
reviewPhase?: 'pending' | 'reviewed'
qcWorkOrderCompleted?: boolean
}) {
return request.get<PageList<StorageReleaseReview>>({
url: '/web/admin/storage-release-reviews',
params
})
}
```
- [ ] **Step 2: 筛选区增加「质检工单是否完成」(仅待审核)**
在 `index.vue` 筛选栅格中,于「入库状态」表单项之后增加(仅 `activeTab === 'pending'` 时渲染):
```vue
<el-form-item v-if="activeTab === 'pending'" label="质检工单是否完成">
<el-select
v-model="searchForm.qcWorkOrderCompleted"
placeholder="全部"
style="width: 100%"
>
<el-option
v-for="item in qcWorkOrderCompletedOptions"
:key="String(item.value)"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
```
在 script 中增加选项与表单字段(`searchForm` 内):
```typescript
/** '' = 全部;'true'/'false' 避免 el-select 对 boolean 的歧义 */
const qcWorkOrderCompletedOptions = [
{ value: 'true', label: '是' },
{ value: 'false', label: '否' },
{ value: '', label: '全部' }
]
const searchForm = reactive({
receptionSource: '',
reviewConclusion: '',
storageStatus: '',
keyword: '',
qcWorkOrderCompleted: 'true' as '' | 'true' | 'false'
})
```
- [ ] **Step 3: `loadData` 仅待审核且非「全部」时传参**
```typescript
async function loadData() {
loading.value = true
try {
const qcParam =
activeTab.value === 'pending' && searchForm.qcWorkOrderCompleted === 'true'
? true
: activeTab.value === 'pending' && searchForm.qcWorkOrderCompleted === 'false'
? false
: undefined
const res = await fetchStorageReleaseReviewList({
page: pagination.page,
pageSize: pagination.pageSize,
receptionSource: searchForm.receptionSource || undefined,
reviewConclusion: searchForm.reviewConclusion || undefined,
storageStatus: searchForm.storageStatus || undefined,
keyword: searchForm.keyword || undefined,
reviewPhase: activeTab.value,
qcWorkOrderCompleted: qcParam
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
```
- [ ] **Step 4: 重置与切 Tab 行为**
`handleReset`:其它字段清空;质检条件回到 `'true'`:
```typescript
function handleReset() {
searchForm.receptionSource = ''
searchForm.reviewConclusion = ''
searchForm.storageStatus = ''
searchForm.keyword = ''
searchForm.qcWorkOrderCompleted = 'true'
pagination.page = 1
loadData()
}
```
`handleTabChange`:切到待审核时恢复质检默认「是」;切到已审核时可不改字段(控件已隐藏且请求不传):
```typescript
function handleTabChange(name: ReviewTab) {
if (activeTab.value === name) return
activeTab.value = name
pagination.page = 1
selectedRows.value = []
if (name === 'pending') {
searchForm.qcWorkOrderCompleted = 'true'
}
loadData()
}
```
- [ ] **Step 5: 本地冒烟(手动)**
```bash
# web-admin 已在跑则刷新;否则:
cd web-admin && pnpm dev
```
打开 `http://localhost:3006/#/market/cell-storage/storage-release-review`,核对:
1. 进入待审核:筛选项默认「是」,列表不含「无质检工单」待审核行
2. 改为「否」→ 搜索:出现无工单或含 `dpz`/`ypz` 的行
3. 「全部」→ 与改前待审核全集一致
4. 切到已审核:该筛选项消失;切回待审核:条件回到「是」
5. 重置:质检条件回到「是」
- [ ] **Step 6: Commit**
```bash
git add \
web-admin/src/api/storage-release-review.ts \
web-admin/src/views/quality-inspection/storage-release-review/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): default pending storage release to QC-complete samples
Add QC work-order completion filter on the pending tab (default yes) and
pass qcWorkOrderCompleted to the list API.
EOF
)"
```
- [ ] **Step 7: 更新代码知识图谱(backend + frontend)**
```bash
./scripts/update-graph.sh all
```
若图谱文件有变更则一并提交:
```bash
git add backend/graphify-out web-admin/graphify-out
git commit -m "$(cat <<'EOF'
chore: update graphify after storage-release QC filter
EOF
)"
```
---
## Spec Coverage Checklist
| Spec 要求 | Task |
|-----------|------|
| 待审核默认仅质检完成 | Task 2 默认 `'true'` + Task 1 `True` 过滤 |
| 筛选项 是/否/全部 | Task 2 UI |
| 全部质检工单(不限 trigger_source) | Task 1 EXISTS 无 trigger 条件 |
| 无工单 = 未完成 | Task 1 `False` 含 `~EXISTS` |
| `yzf` 视为完成 | Task 1 未完成仅 `dpz`/`ypz` |
| 仅待审核 | Task 2 `v-if` + 已审核不传参 |
| 重置 / 切回待审核 →「是」 | Task 2 `handleReset` / `handleTabChange` |
| 分页 total 正确 | Task 1 过滤在 count 之前 |
| 不改审核入库规则 | 无对应任务(刻意不做) |
## Self-Review Notes
- 无 TBD/TODO 占位
- 参数名前后端一致:`qcWorkOrderCompleted` ↔ `qc_work_order_completed`
- 前端用字符串 `'true'|'false'|''` 绑 select,请求时转 boolean / omit,避免 Element Plus boolean 选项问题
@@ -0,0 +1,159 @@
# 入库放行审核 · 待审核默认仅展示质检工单已完成
> 日期:2026-07-16
> 状态:已确认
> 模块:细胞存储 / 质检 → 入库放行审核
> 关联:`docs/superpowers/specs/2026-07-16-storage-release-review-tabs-design.md`
> 界面模式:P22(审核列表)
> 参考页:`web-admin/src/views/quality-inspection/storage-release-review/index.vue`
## 1. 背景与目标
### 现状
「待审核」Tab(`reviewPhase=pending`)仅按 `review_conclusion` 为空过滤,会列出**全部**待审核记录。入库放行记录在样本接收完成后即生成,质检工单是否完成**不参与**列表过滤。
### 目标
1. 待审核默认只展示:该样本编码下质检工单已判定为「完成」的记录。
2. 筛选区增加「质检工单是否完成」,默认「是」,支持改为「否」或「全部」以查看其它待办。
3. 分页与 `total` 与过滤条件一致。
### 范围外
- 不改审核弹窗、查看弹窗、入库业务规则
- 不在后端强制「质检未完成禁止审核」(仅列表默认隐藏;选「否/全部」后仍可审核)
- 不改「已审核」Tab 的筛选与列表语义
- 不新增菜单/路由;不改表结构
## 2. 需求确认记录
| 维度 | 决定 |
|------|------|
| 「完成」覆盖范围 | 样本下**全部**质检工单(不限 `trigger_source`) |
| 无质检工单 | 算**未完成** |
| 已作废 `yzf` | 视为已完成(与 `ywc` 同等) |
| 未完成状态码 | `dpz`(待配置)、`ypz`(已配置) |
| 作用 Tab | **仅「待审核」**;「已审核」不展示该条件、请求不带该参数 |
| 默认值 | 「是」 |
| 重置 | 回到「是」 |
| 从已审核切回待审核 | 质检条件**恢复默认「是」** |
| 实现路径 | 列表 API 增加查询参数 + 后端 SQL 过滤(方案 1) |
## 3. 「质检工单是否完成」语义
在租户 `tenant_no` 下,关联键为审核行的 `sample_code`,工单条件:`biz_qc_work_order.del_flag = 0`。
| 筛选项 | 判定 |
|--------|------|
| **是** | 该样本**至少存在 1 条**质检工单,且**不存在** `work_order_status IN ('dpz','ypz')`(允许全为 `ywc` / `yzf`) |
| **否** | 该样本**无**质检工单,**或**存在至少一条 `dpz`/`ypz` |
| **全部** | 不按质检完成度过滤 |
说明:
- 仅作废工单(全是 `yzf`、无 `dpz`/`ypz`)→ 「是」
- 零工单 → 「否」
- 软删工单(`del_flag=1`)不参与统计
## 4. UI 与交互
### 4.1 筛选区(仅待审核可见)
在现有筛选(关键词、协议类别、审核结论、入库状态)中增加:
| 字段 | 控件 | 选项 | 默认 |
|------|------|------|------|
| 质检工单是否完成 | `el-select` | 是 / 否 / 全部 | 是 |
「已审核」Tab:**不渲染**该表单项。
### 4.2 默认与重置
| 场景 | 质检条件 |
|------|----------|
| 首次进入页面(默认待审核) | 是 |
| 点击「重置」(待审核) | 是(其它筛选项清空逻辑不变) |
| 从「已审核」切回「待审核」 | **恢复「是」**,页码回 1,清空勾选,重新请求 |
| 待审核内切换其它筛选项 / 搜索 | 保留当前质检条件选择 |
### 4.3 请求行为
- 待审核 + 「是」→ 传 `qcWorkOrderCompleted=true`
- 待审核 + 「否」→ 传 `qcWorkOrderCompleted=false`
- 待审核 + 「全部」→ **不传**该参数(或传空,后端视为不过滤)
- 已审核 → **不传**该参数
## 5. API 与数据流
### 5.1 接口
沿用 `GET /web/admin/storage-release-reviews`,新增可选查询参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| `qcWorkOrderCompleted` | `true` \| `false`(查询字符串布尔) | 缺省或不识别 → 不按质检完成度过滤 |
其余参数不变:`page`、`pageSize`、`keyword`、`receptionSource`、`reviewConclusion`、`storageStatus`、`reviewPhase`。
### 5.2 后端过滤(叠在现有条件之后)
在 `list_reviews` 中,当解析到明确布尔值时:
**`true`(是)** — 样本编码满足:
```text
EXISTS (
同租户、del_flag=0、sample_code = 审核行.sample_code 的质检工单
)
AND NOT EXISTS (
同上,且 work_order_status IN ('dpz', 'ypz')
)
```
**`false`(否)** — 样本编码满足:
```text
NOT EXISTS (任意有效工单)
OR EXISTS (有效工单且 work_order_status IN ('dpz', 'ypz'))
```
空 `sample_code` 的审核行:按「无工单」处理 → 仅「否」或「全部」可见,「是」不可见。
过滤必须在 **COUNT 与分页查询之前**应用,保证 `total` 正确。
### 5.3 实现提示
- 优先用 SQLAlchemy `exists()` 子查询,避免先拉全量再过滤。
- 可抽小函数 `normalize_qc_work_order_completed(raw) -> Optional[bool]`,与 `normalize_review_phase` 风格一致;非法值视为 `None`(不过滤)。
## 6. 改动面
| 层 | 路径 | 变更 |
|----|------|------|
| Backend API | `backend/app/api/storage_release_review.py` | 增加 Query `qcWorkOrderCompleted` |
| Backend CRUD | `backend/app/crud/storage_release_review.py` | `list_reviews` 增加过滤;可选 normalize 辅助函数 |
| Backend 测试 | `backend/tests/` | 覆盖 normalize + 完成/未完成/无工单/仅作废语义(可先测纯函数,再视情况补集成) |
| Frontend API | `web-admin/src/api/storage-release-review.ts` | 列表参数增加 `qcWorkOrderCompleted?: boolean` |
| Frontend 页 | `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | 待审核筛选项、默认/重置/切 Tab 行为、请求传参 |
不改:审核/查看弹窗、菜单路由、表结构、已审核列表逻辑(除切回待审核时重置筛选项)。
## 7. 测试要点
1. 待审核默认:仅出现「质检已完成」样本;无工单样本不出现。
2. 选「否」:出现无工单 + 含 `dpz`/`ypz` 的待审核。
3. 选「全部」:与改前待审核集合一致(仅 `reviewPhase=pending`)。
4. 仅 `yzf`(或 `ywc`+`yzf`、无未完成)→ 「是」可见。
5. 已审核:无该筛选项;结果不受该参数影响。
6. 重置、从已审核切回待审核 → 条件回到「是」。
7. 分页 `total` 与当前过滤一致。
## 8. 方案取舍(记录)
| 方案 | 结论 |
|------|------|
| 1. 列表 API 查询参数 + SQL 过滤 | **采用** |
| 2. 前端拉全量后过滤 | 拒绝(分页/total 错误) |
| 3. 审核表冗余完成标志 | 拒绝(写路径多、一致性成本高) |