diff --git a/docs/superpowers/plans/2026-07-16-storage-release-review-qc-completed-filter.md b/docs/superpowers/plans/2026-07-16-storage-release-review-qc-completed-filter.md new file mode 100644 index 00000000..88132d34 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-storage-release-review-qc-completed-filter.md @@ -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>({ + url: '/web/admin/storage-release-reviews', + params + }) +} +``` + +- [ ] **Step 2: 筛选区增加「质检工单是否完成」(仅待审核)** + +在 `index.vue` 筛选栅格中,于「入库状态」表单项之后增加(仅 `activeTab === 'pending'` 时渲染): + +```vue + + + + + +``` + +在 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 选项问题 diff --git a/docs/superpowers/specs/2026-07-16-storage-release-review-qc-completed-filter-design.md b/docs/superpowers/specs/2026-07-16-storage-release-review-qc-completed-filter-design.md new file mode 100644 index 00000000..ac53f511 --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-storage-release-review-qc-completed-filter-design.md @@ -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. 审核表冗余完成标志 | 拒绝(写路径多、一致性成本高) |