|
|
@@ -0,0 +1,1164 @@
|
|
|
+# 入库放行审核双闸门 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:** 入库放行审核改为生产主管与质检主管两侧都通过后才自动入库;细胞存储入口只读总览;已入库后任一侧改为不通过则直接回退出库。
|
|
|
+
|
|
|
+**Architecture:** 仍用一张 `biz_storage_release_review`。新增 `prod_review_*` / `qc_review_*` 两套字段;原 `review_conclusion` 改为两侧汇总。提交本侧后重算汇总并调用 `store` / `unstore`。三个菜单共用同一 Vue 页,用路由 `props.reviewRole` 区分能力。纯函数(汇总、Tab 语义、入库动作)先单测再接线。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2(backend)、Vue 3 + Element Plus(web-admin)、MySQL 5.7、pytest
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-08-19-storage-release-dual-gate-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File Map
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `docs/sql/2026-08-19-storage-release-dual-gate.sql` | ALTER 新列 + 历史数据拷到质检侧 + 重算汇总 |
|
|
|
+| `docs/sql/zkxw.sql` | 仅更新 `biz_storage_release_review` 的 `CREATE TABLE` |
|
|
|
+| `backend/app/models/models.py` | ORM 增加两侧字段 |
|
|
|
+| `backend/app/schemas/storage_release_review.py` | `reviewSide`、两侧筛选 |
|
|
|
+| `backend/app/crud/storage_release_review.py` | 纯函数 + 提交/入库/回退 + 列表 Tab |
|
|
|
+| `backend/app/api/storage_release_review.py` | 传 `reviewSide`;写接口校验对应菜单 |
|
|
|
+| `backend/tests/test_storage_release_dual_gate.py` | **新建** 双闸门纯函数单测 |
|
|
|
+| `backend/app/api/menu.py` | 生产、质检菜单增加入口 |
|
|
|
+| `web-admin/src/router/modules/sqxp.ts` | 三入口路由与 `reviewRole` |
|
|
|
+| `web-admin/src/api/storage-release-review.ts` | 类型与请求参数 |
|
|
|
+| `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | 按角色显隐按钮、两列结论 |
|
|
|
+| `StorageReleaseReviewDialog.vue` | 提交本侧,对侧只读 |
|
|
|
+| `StorageReleaseViewDialog.vue` | 两节放行 + 入库 |
|
|
|
+| `web-admin/src/components/business/sample-storage-release-info/index.vue` | 协议卡展示两侧 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 双闸门纯函数与单测
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/tests/test_storage_release_dual_gate.py`
|
|
|
+- Modify: `backend/app/crud/storage_release_review.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败单测**
|
|
|
+
|
|
|
+创建 `backend/tests/test_storage_release_dual_gate.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.crud.storage_release_review import (
|
|
|
+ aggregate_review_conclusion,
|
|
|
+ conclusion_blank,
|
|
|
+ matches_dual_review_phase,
|
|
|
+ normalize_review_side,
|
|
|
+ review_side_required_menu,
|
|
|
+ should_copy_legacy_to_qc,
|
|
|
+ storage_transition,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def test_conclusion_blank():
|
|
|
+ assert conclusion_blank(None) is True
|
|
|
+ assert conclusion_blank("") is True
|
|
|
+ assert conclusion_blank(" ") is True
|
|
|
+ assert conclusion_blank("tg") is False
|
|
|
+ assert conclusion_blank("btg") is False
|
|
|
+
|
|
|
+
|
|
|
+def test_aggregate_both_tg():
|
|
|
+ assert aggregate_review_conclusion("tg", "tg") == "tg"
|
|
|
+
|
|
|
+
|
|
|
+def test_aggregate_any_btg():
|
|
|
+ assert aggregate_review_conclusion("btg", "tg") == "btg"
|
|
|
+ assert aggregate_review_conclusion("tg", "btg") == "btg"
|
|
|
+ assert aggregate_review_conclusion("btg", None) == "btg"
|
|
|
+ assert aggregate_review_conclusion("", "btg") == "btg"
|
|
|
+
|
|
|
+
|
|
|
+def test_aggregate_pending_when_not_aligned():
|
|
|
+ assert aggregate_review_conclusion("tg", None) is None
|
|
|
+ assert aggregate_review_conclusion(None, "tg") is None
|
|
|
+ assert aggregate_review_conclusion(None, None) is None
|
|
|
+ assert aggregate_review_conclusion("", "") is None
|
|
|
+
|
|
|
+
|
|
|
+def test_storage_transition_store_when_both_tg_and_pending():
|
|
|
+ assert storage_transition("tg", "tg", "drk") == "store"
|
|
|
+
|
|
|
+
|
|
|
+def test_storage_transition_unstore_when_btg_and_stored():
|
|
|
+ assert storage_transition("btg", "tg", "yrk") == "unstore"
|
|
|
+ assert storage_transition("tg", "btg", "yrk") == "unstore"
|
|
|
+
|
|
|
+
|
|
|
+def test_storage_transition_noop_otherwise():
|
|
|
+ assert storage_transition("tg", "tg", "yrk") == "noop"
|
|
|
+ assert storage_transition("tg", None, "drk") == "noop"
|
|
|
+ assert storage_transition("btg", "tg", "drk") == "noop"
|
|
|
+ assert storage_transition(None, None, "yrk") == "noop"
|
|
|
+
|
|
|
+
|
|
|
+def test_matches_dual_review_phase_production():
|
|
|
+ assert matches_dual_review_phase("production", "pending", None, "tg") is True
|
|
|
+ assert matches_dual_review_phase("production", "pending", "tg", "tg") is False
|
|
|
+ assert matches_dual_review_phase("production", "reviewed", "btg", None) is True
|
|
|
+ assert matches_dual_review_phase("production", "reviewed", None, "tg") is False
|
|
|
+
|
|
|
+
|
|
|
+def test_matches_dual_review_phase_qc():
|
|
|
+ assert matches_dual_review_phase("qc", "pending", "tg", None) is True
|
|
|
+ assert matches_dual_review_phase("qc", "reviewed", "tg", "tg") is True
|
|
|
+ assert matches_dual_review_phase("qc", "reviewed", "tg", None) is False
|
|
|
+
|
|
|
+
|
|
|
+def test_matches_dual_review_phase_overview():
|
|
|
+ assert matches_dual_review_phase("overview", "pending", None, "tg") is True
|
|
|
+ assert matches_dual_review_phase("overview", "pending", "tg", "tg") is False
|
|
|
+ assert matches_dual_review_phase("overview", "reviewed", "tg", "btg") is True
|
|
|
+ assert matches_dual_review_phase("overview", "reviewed", "tg", None) is False
|
|
|
+
|
|
|
+
|
|
|
+def test_should_copy_legacy_to_qc():
|
|
|
+ assert should_copy_legacy_to_qc(None, "tg") is True
|
|
|
+ assert should_copy_legacy_to_qc("", "btg") is True
|
|
|
+ assert should_copy_legacy_to_qc("tg", "tg") is False
|
|
|
+ assert should_copy_legacy_to_qc(None, None) is False
|
|
|
+ assert should_copy_legacy_to_qc(None, "") is False
|
|
|
+
|
|
|
+
|
|
|
+def test_normalize_review_side():
|
|
|
+ assert normalize_review_side("production") == "production"
|
|
|
+ assert normalize_review_side(" QC ") == "qc"
|
|
|
+ assert normalize_review_side("overview") == "overview"
|
|
|
+ assert normalize_review_side("all") is None
|
|
|
+ assert normalize_review_side(None) is None
|
|
|
+
|
|
|
+
|
|
|
+def test_review_side_required_menu():
|
|
|
+ assert review_side_required_menu("production") == "production:storageReleaseReview:list"
|
|
|
+ assert review_side_required_menu("qc") == "quality:storageReleaseReview:list"
|
|
|
+ assert review_side_required_menu("overview") is None
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑单测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_dual_gate.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(无法导入新函数)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现纯函数**
|
|
|
+
|
|
|
+在 `backend/app/crud/storage_release_review.py` 的 `normalize_review_phase` 之后插入:
|
|
|
+
|
|
|
+```python
|
|
|
+def conclusion_blank(value: Optional[str]) -> bool:
|
|
|
+ return not (value or "").strip()
|
|
|
+
|
|
|
+
|
|
|
+def aggregate_review_conclusion(
|
|
|
+ prod: Optional[str], qc: Optional[str]
|
|
|
+) -> Optional[str]:
|
|
|
+ p = (prod or "").strip()
|
|
|
+ q = (qc or "").strip()
|
|
|
+ if p == "btg" or q == "btg":
|
|
|
+ return "btg"
|
|
|
+ if p == "tg" and q == "tg":
|
|
|
+ return "tg"
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def storage_transition(
|
|
|
+ prod: Optional[str], qc: Optional[str], storage_status: Optional[str]
|
|
|
+) -> str:
|
|
|
+ """Return 'store' | 'unstore' | 'noop'."""
|
|
|
+ agg = aggregate_review_conclusion(prod, qc)
|
|
|
+ st = (storage_status or "drk").strip() or "drk"
|
|
|
+ if agg == "tg" and st == "drk":
|
|
|
+ return "store"
|
|
|
+ if agg == "btg" and st == "yrk":
|
|
|
+ return "unstore"
|
|
|
+ return "noop"
|
|
|
+
|
|
|
+
|
|
|
+def normalize_review_side(raw: Optional[str]) -> Optional[str]:
|
|
|
+ s = (raw or "").strip().lower()
|
|
|
+ if s in ("overview", "production", "qc"):
|
|
|
+ return s
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def review_side_required_menu(review_side: Optional[str]) -> Optional[str]:
|
|
|
+ if review_side == "production":
|
|
|
+ return "production:storageReleaseReview:list"
|
|
|
+ if review_side == "qc":
|
|
|
+ return "quality:storageReleaseReview:list"
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def matches_dual_review_phase(
|
|
|
+ review_side: Optional[str],
|
|
|
+ phase: Optional[str],
|
|
|
+ prod: Optional[str],
|
|
|
+ qc: Optional[str],
|
|
|
+) -> bool:
|
|
|
+ """Pure predicate for Tab semantics; SQL filter must match this."""
|
|
|
+ side = normalize_review_side(review_side) or "overview"
|
|
|
+ ph = normalize_review_phase(phase)
|
|
|
+ if not ph:
|
|
|
+ return True
|
|
|
+ prod_blank = conclusion_blank(prod)
|
|
|
+ qc_blank = conclusion_blank(qc)
|
|
|
+ if side == "production":
|
|
|
+ return prod_blank if ph == "pending" else (not prod_blank)
|
|
|
+ if side == "qc":
|
|
|
+ return qc_blank if ph == "pending" else (not qc_blank)
|
|
|
+ both_filled = (not prod_blank) and (not qc_blank)
|
|
|
+ return (not both_filled) if ph == "pending" else both_filled
|
|
|
+
|
|
|
+
|
|
|
+def should_copy_legacy_to_qc(
|
|
|
+ qc_review_conclusion: Optional[str],
|
|
|
+ legacy_review_conclusion: Optional[str],
|
|
|
+) -> bool:
|
|
|
+ return conclusion_blank(qc_review_conclusion) and not conclusion_blank(
|
|
|
+ legacy_review_conclusion
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑单测确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_dual_gate.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS(全部 green)
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/tests/test_storage_release_dual_gate.py backend/app/crud/storage_release_review.py
|
|
|
+git commit -m "feat: add dual-gate storage release pure functions and tests."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: SQL 加列与历史迁移脚本
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `docs/sql/2026-08-19-storage-release-dual-gate.sql`
|
|
|
+- Modify: `docs/sql/zkxw.sql`(仅 `CREATE TABLE \`biz_storage_release_review\`` 字段列表,不要改 INSERT 行)
|
|
|
+
|
|
|
+- [ ] **Step 1: 写变更 SQL**
|
|
|
+
|
|
|
+创建 `docs/sql/2026-08-19-storage-release-dual-gate.sql`:
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 入库放行审核:生产/质检双闸门
|
|
|
+-- MySQL 5.7;历史拷贝条件见下方 UPDATE 的 WHERE(可重复执行)
|
|
|
+
|
|
|
+ALTER TABLE `biz_storage_release_review`
|
|
|
+ ADD COLUMN `qc_review_conclusion` VARCHAR(16) DEFAULT NULL COMMENT '质检侧结论 tg/btg/NULL待审' AFTER `review_time`,
|
|
|
+ ADD COLUMN `qc_review_remark` VARCHAR(512) DEFAULT NULL COMMENT '质检侧审核备注' AFTER `qc_review_conclusion`,
|
|
|
+ ADD COLUMN `qc_review_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '质检侧审核附件(逗号分隔URL)' AFTER `qc_review_remark`,
|
|
|
+ ADD COLUMN `qc_review_by` VARCHAR(64) DEFAULT NULL COMMENT '质检侧审核人ID' AFTER `qc_review_attachments`,
|
|
|
+ ADD COLUMN `qc_review_by_name` VARCHAR(64) DEFAULT NULL COMMENT '质检侧审核人姓名' AFTER `qc_review_by`,
|
|
|
+ ADD COLUMN `qc_review_time` DATETIME DEFAULT NULL COMMENT '质检侧审核时间' AFTER `qc_review_by_name`,
|
|
|
+ ADD COLUMN `prod_review_conclusion` VARCHAR(16) DEFAULT NULL COMMENT '生产侧结论 tg/btg/NULL待审' AFTER `qc_review_time`,
|
|
|
+ ADD COLUMN `prod_review_remark` VARCHAR(512) DEFAULT NULL COMMENT '生产侧审核备注' AFTER `prod_review_conclusion`,
|
|
|
+ ADD COLUMN `prod_review_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '生产侧审核附件(逗号分隔URL)' AFTER `prod_review_remark`,
|
|
|
+ ADD COLUMN `prod_review_by` VARCHAR(64) DEFAULT NULL COMMENT '生产侧审核人ID' AFTER `prod_review_attachments`,
|
|
|
+ ADD COLUMN `prod_review_by_name` VARCHAR(64) DEFAULT NULL COMMENT '生产侧审核人姓名' AFTER `prod_review_by`,
|
|
|
+ ADD COLUMN `prod_review_time` DATETIME DEFAULT NULL COMMENT '生产侧审核时间' AFTER `prod_review_by_name`;
|
|
|
+
|
|
|
+ALTER TABLE `biz_storage_release_review`
|
|
|
+ ADD KEY `idx_srr_dual_conclusion` (`tenant_no`, `qc_review_conclusion`, `prod_review_conclusion`, `del_flag`);
|
|
|
+
|
|
|
+-- 旧单人结论只落到质检侧;已入库状态不改
|
|
|
+UPDATE `biz_storage_release_review`
|
|
|
+SET
|
|
|
+ `qc_review_conclusion` = `review_conclusion`,
|
|
|
+ `qc_review_remark` = `review_remark`,
|
|
|
+ `qc_review_attachments` = `review_attachments`,
|
|
|
+ `qc_review_by` = `review_by`,
|
|
|
+ `qc_review_by_name` = `review_by_name`,
|
|
|
+ `qc_review_time` = `review_time`
|
|
|
+WHERE `del_flag` = 0
|
|
|
+ AND (`qc_review_conclusion` IS NULL OR `qc_review_conclusion` = '')
|
|
|
+ AND `review_conclusion` IS NOT NULL
|
|
|
+ AND `review_conclusion` <> '';
|
|
|
+
|
|
|
+-- 按双闸门重算汇总(旧「仅质检通过」汇总变为 NULL)
|
|
|
+UPDATE `biz_storage_release_review`
|
|
|
+SET `review_conclusion` = CASE
|
|
|
+ WHEN `prod_review_conclusion` = 'tg' AND `qc_review_conclusion` = 'tg' THEN 'tg'
|
|
|
+ WHEN `prod_review_conclusion` = 'btg' OR `qc_review_conclusion` = 'btg' THEN 'btg'
|
|
|
+ ELSE NULL
|
|
|
+END
|
|
|
+WHERE `del_flag` = 0;
|
|
|
+```
|
|
|
+
|
|
|
+若目标库已加过列,跳过 ALTER,只跑 UPDATE。加列失败时用 `SHOW COLUMNS FROM biz_storage_release_review LIKE 'qc_review_conclusion'` 判断。
|
|
|
+
|
|
|
+- [ ] **Step 2: 同步 `docs/sql/zkxw.sql` 的 CREATE TABLE**
|
|
|
+
|
|
|
+定位 `CREATE TABLE \`biz_storage_release_review\``,在 `review_time` 与 `storage_status` 之间插入与 Step 1 相同的 12 个新列,并在 KEY 列表增加 `idx_srr_dual_conclusion`。不要改该表的 INSERT 语句(未列出的新列走 DEFAULT NULL)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 在开发库执行脚本**
|
|
|
+
|
|
|
+对当前开发库执行 `docs/sql/2026-08-19-storage-release-dual-gate.sql`(按项目惯例用已有 MySQL 客户端 / 运维方式,不要把密码写进仓库)。
|
|
|
+
|
|
|
+抽查:
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT review_conclusion, qc_review_conclusion, prod_review_conclusion, storage_status
|
|
|
+FROM biz_storage_release_review
|
|
|
+WHERE del_flag = 0
|
|
|
+LIMIT 20;
|
|
|
+```
|
|
|
+
|
|
|
+Expected:旧 `tg` 且已入库 → `qc=tg`、`prod` 空、`storage_status=yrk`、汇总 `review_conclusion` 为空;旧未审 → 两侧都空。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/sql/2026-08-19-storage-release-dual-gate.sql docs/sql/zkxw.sql
|
|
|
+git commit -m "docs: add storage release dual-gate schema and data migration SQL."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: ORM 增加两侧字段
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/models/models.py`(`BizStorageReleaseReview`,约 1057 行 `review_time` 之后)
|
|
|
+
|
|
|
+- [ ] **Step 1: 在模型中增加字段**
|
|
|
+
|
|
|
+在 `review_time = Column(...)` 之后、`storage_status = Column(...)` 之前插入:
|
|
|
+
|
|
|
+```python
|
|
|
+ qc_review_conclusion = Column(String(16), nullable=True, comment="质检侧结论 tg通过 btg不通过")
|
|
|
+ qc_review_remark = Column(String(512), nullable=True, comment="质检侧审核备注")
|
|
|
+ qc_review_attachments = Column(String(4000), nullable=True, comment="质检侧审核附件(逗号分隔URL)")
|
|
|
+ qc_review_by = Column(String(64), nullable=True, comment="质检侧审核人ID")
|
|
|
+ qc_review_by_name = Column(String(64), nullable=True, comment="质检侧审核人姓名")
|
|
|
+ qc_review_time = Column(DateTime, nullable=True, comment="质检侧审核时间")
|
|
|
+ prod_review_conclusion = Column(String(16), nullable=True, comment="生产侧结论 tg通过 btg不通过")
|
|
|
+ prod_review_remark = Column(String(512), nullable=True, comment="生产侧审核备注")
|
|
|
+ prod_review_attachments = Column(String(4000), nullable=True, comment="生产侧审核附件(逗号分隔URL)")
|
|
|
+ prod_review_by = Column(String(64), nullable=True, comment="生产侧审核人ID")
|
|
|
+ prod_review_by_name = Column(String(64), nullable=True, comment="生产侧审核人姓名")
|
|
|
+ prod_review_time = Column(DateTime, nullable=True, comment="生产侧审核时间")
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 确认模型可导入**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -c "from app.models.models import BizStorageReleaseReview; print(BizStorageReleaseReview.qc_review_conclusion.name, BizStorageReleaseReview.prod_review_conclusion.name)"
|
|
|
+```
|
|
|
+
|
|
|
+Expected: `qc_review_conclusion prod_review_conclusion`
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/models/models.py
|
|
|
+git commit -m "feat: add production and QC review columns on storage release model."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: `_row_to_dict` 与 Pydantic schema
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/crud/storage_release_review.py`(`_row_to_dict`)
|
|
|
+- Modify: `backend/app/schemas/storage_release_review.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展 `_row_to_dict`**
|
|
|
+
|
|
|
+在现有 `"reviewTime": _fmt_dt(r.review_time),` 之后插入两侧字段(汇总 `review_*` 保留):
|
|
|
+
|
|
|
+```python
|
|
|
+ "qcReviewConclusion": r.qc_review_conclusion or "",
|
|
|
+ "qcReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(r.qc_review_conclusion or "", r.qc_review_conclusion or ""),
|
|
|
+ "qcReviewRemark": r.qc_review_remark or "",
|
|
|
+ "qcReviewAttachments": r.qc_review_attachments or "",
|
|
|
+ "qcReviewBy": r.qc_review_by or "",
|
|
|
+ "qcReviewByName": r.qc_review_by_name or "",
|
|
|
+ "qcReviewTime": _fmt_dt(r.qc_review_time),
|
|
|
+ "prodReviewConclusion": r.prod_review_conclusion or "",
|
|
|
+ "prodReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(r.prod_review_conclusion or "", r.prod_review_conclusion or ""),
|
|
|
+ "prodReviewRemark": r.prod_review_remark or "",
|
|
|
+ "prodReviewAttachments": r.prod_review_attachments or "",
|
|
|
+ "prodReviewBy": r.prod_review_by or "",
|
|
|
+ "prodReviewByName": r.prod_review_by_name or "",
|
|
|
+ "prodReviewTime": _fmt_dt(r.prod_review_time),
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 更新 schema**
|
|
|
+
|
|
|
+`StorageReleaseReviewListQuery` 增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ reviewSide: Optional[str] = None
|
|
|
+ prodReviewConclusion: Optional[str] = None
|
|
|
+ qcReviewConclusion: Optional[str] = None
|
|
|
+```
|
|
|
+
|
|
|
+`ReviewSubmitBody` 与 `BatchReviewBody` 增加必填:
|
|
|
+
|
|
|
+```python
|
|
|
+ reviewSide: str = Field(..., description="production/qc")
|
|
|
+```
|
|
|
+
|
|
|
+`StorageReleaseReviewListItem` 增加与 `_row_to_dict` 对应的 `qcReview*` / `prodReview*` 可选字段。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/crud/storage_release_review.py backend/app/schemas/storage_release_review.py
|
|
|
+git commit -m "feat: expose dual-side storage release fields in row dict and schemas."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 列表 Tab 过滤 + 提交后入库/回退
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/crud/storage_release_review.py`(`list_reviews`、`submit_review`、`batch_submit_review`、`store_sample`、新增 `unstore_sample`、`get_storage_release_map_for_samples`)
|
|
|
+
|
|
|
+- [ ] **Step 1: 改 `list_reviews` 的 phase / 侧筛选**
|
|
|
+
|
|
|
+签名增加(放在 `review_phase` 之后):
|
|
|
+
|
|
|
+```python
|
|
|
+ review_side: Optional[str] = None,
|
|
|
+ prod_review_conclusion: Optional[str] = None,
|
|
|
+ qc_review_conclusion: Optional[str] = None,
|
|
|
+```
|
|
|
+
|
|
|
+**删除**(或替换)现有按汇总 `review_conclusion` 是否为空做 `reviewPhase` 的那段,改为:
|
|
|
+
|
|
|
+```python
|
|
|
+ phase = normalize_review_phase(review_phase)
|
|
|
+ side = normalize_review_side(review_side) or "overview"
|
|
|
+ if phase:
|
|
|
+ prod_blank = or_(
|
|
|
+ BizStorageReleaseReview.prod_review_conclusion.is_(None),
|
|
|
+ BizStorageReleaseReview.prod_review_conclusion == "",
|
|
|
+ )
|
|
|
+ qc_blank = or_(
|
|
|
+ BizStorageReleaseReview.qc_review_conclusion.is_(None),
|
|
|
+ BizStorageReleaseReview.qc_review_conclusion == "",
|
|
|
+ )
|
|
|
+ if side == "production":
|
|
|
+ q0 = q0.filter(prod_blank if phase == "pending" else ~prod_blank)
|
|
|
+ elif side == "qc":
|
|
|
+ q0 = q0.filter(qc_blank if phase == "pending" else ~qc_blank)
|
|
|
+ else:
|
|
|
+ both_filled = and_(~prod_blank, ~qc_blank)
|
|
|
+ q0 = q0.filter(~both_filled if phase == "pending" else both_filled)
|
|
|
+ prc = (prod_review_conclusion or "").strip()
|
|
|
+ if prc:
|
|
|
+ q0 = q0.filter(BizStorageReleaseReview.prod_review_conclusion == prc)
|
|
|
+ qrc = (qc_review_conclusion or "").strip()
|
|
|
+ if qrc:
|
|
|
+ q0 = q0.filter(BizStorageReleaseReview.qc_review_conclusion == qrc)
|
|
|
+```
|
|
|
+
|
|
|
+保留原 `review_conclusion` 汇总过滤、`ready_for_review_only`、作废隐藏,不要改那些函数。
|
|
|
+
|
|
|
+- [ ] **Step 2: 改 `store_sample` 门槛,并实现 `unstore_sample`**
|
|
|
+
|
|
|
+`store_sample` 中把 `if rev.review_conclusion != "tg":` 换成:
|
|
|
+
|
|
|
+```python
|
|
|
+ if aggregate_review_conclusion(rev.prod_review_conclusion, rev.qc_review_conclusion) != "tg":
|
|
|
+ raise ValueError("生产与质检均通过后才能入库")
|
|
|
+```
|
|
|
+
|
|
|
+在 `store_sample` 之后新增:
|
|
|
+
|
|
|
+```python
|
|
|
+def unstore_sample(
|
|
|
+ db: Session,
|
|
|
+ review_id: str,
|
|
|
+ *,
|
|
|
+ tenant_no: str,
|
|
|
+ user_id: str,
|
|
|
+) -> BizStorageReleaseReview:
|
|
|
+ rev = (
|
|
|
+ db.query(BizStorageReleaseReview)
|
|
|
+ .filter(
|
|
|
+ BizStorageReleaseReview.id == review_id,
|
|
|
+ BizStorageReleaseReview.tenant_no == tenant_no,
|
|
|
+ BizStorageReleaseReview.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not rev:
|
|
|
+ raise ValueError("审核记录不存在")
|
|
|
+ if (rev.storage_status or "") != "yrk":
|
|
|
+ return rev
|
|
|
+ rev.storage_status = "drk"
|
|
|
+ rev.storage_by = None
|
|
|
+ rev.storage_by_name = None
|
|
|
+ rev.storage_time = None
|
|
|
+ rev.update_by = user_id
|
|
|
+ rev.update_time = datetime.now()
|
|
|
+ db.add(rev)
|
|
|
+ if (rev.reception_source or "cell_storage") == "cell_storage":
|
|
|
+ sample_code = (rev.sample_code or "").strip()
|
|
|
+ if sample_code:
|
|
|
+ ps = (
|
|
|
+ db.query(BizProtocolSample)
|
|
|
+ .filter(
|
|
|
+ BizProtocolSample.sample_code == sample_code,
|
|
|
+ BizProtocolSample.tenant_no == tenant_no,
|
|
|
+ BizProtocolSample.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if ps and (ps.sample_status or "") == "yrk":
|
|
|
+ ps.sample_status = "jcz"
|
|
|
+ ps.storage_in_date = None
|
|
|
+ ps.update_time = datetime.now()
|
|
|
+ db.add(ps)
|
|
|
+ db.flush()
|
|
|
+ return rev
|
|
|
+```
|
|
|
+
|
|
|
+不要改科研捐赠样本状态(与现网 `store_sample` 对称)。不要碰细胞产品/库位。
|
|
|
+
|
|
|
+- [ ] **Step 3: 重写 `submit_review`**
|
|
|
+
|
|
|
+签名改为:
|
|
|
+
|
|
|
+```python
|
|
|
+def submit_review(
|
|
|
+ db: Session,
|
|
|
+ review_id: str,
|
|
|
+ *,
|
|
|
+ review_side: str,
|
|
|
+ review_conclusion: str,
|
|
|
+ review_remark: Optional[str] = None,
|
|
|
+ review_attachments: Optional[str] = None,
|
|
|
+ tenant_no: str,
|
|
|
+ user_id: str,
|
|
|
+) -> BizStorageReleaseReview:
|
|
|
+```
|
|
|
+
|
|
|
+查到 `rev` 后:
|
|
|
+
|
|
|
+```python
|
|
|
+ side = normalize_review_side(review_side)
|
|
|
+ if side not in ("production", "qc"):
|
|
|
+ raise ValueError("审核侧无效")
|
|
|
+ conclusion = (review_conclusion or "").strip()
|
|
|
+ if conclusion not in ("tg", "btg"):
|
|
|
+ raise ValueError("审核结论无效")
|
|
|
+ user_name = _get_user_name(db, user_id)
|
|
|
+ now = datetime.now()
|
|
|
+ remark = (review_remark or "").strip() or None
|
|
|
+ attachments = (review_attachments or "").strip() or None
|
|
|
+ if side == "production":
|
|
|
+ rev.prod_review_conclusion = conclusion
|
|
|
+ rev.prod_review_remark = remark
|
|
|
+ rev.prod_review_attachments = attachments
|
|
|
+ rev.prod_review_by = user_id
|
|
|
+ rev.prod_review_by_name = user_name
|
|
|
+ rev.prod_review_time = now
|
|
|
+ else:
|
|
|
+ rev.qc_review_conclusion = conclusion
|
|
|
+ rev.qc_review_remark = remark
|
|
|
+ rev.qc_review_attachments = attachments
|
|
|
+ rev.qc_review_by = user_id
|
|
|
+ rev.qc_review_by_name = user_name
|
|
|
+ rev.qc_review_time = now
|
|
|
+ agg = aggregate_review_conclusion(rev.prod_review_conclusion, rev.qc_review_conclusion)
|
|
|
+ rev.review_conclusion = agg
|
|
|
+ rev.review_remark = remark
|
|
|
+ rev.review_attachments = attachments
|
|
|
+ rev.review_by = user_id
|
|
|
+ rev.review_by_name = user_name
|
|
|
+ rev.review_time = now
|
|
|
+ rev.update_by = user_id
|
|
|
+ rev.update_time = now
|
|
|
+ db.add(rev)
|
|
|
+ db.flush()
|
|
|
+ action = storage_transition(
|
|
|
+ rev.prod_review_conclusion, rev.qc_review_conclusion, rev.storage_status
|
|
|
+ )
|
|
|
+ if action == "store":
|
|
|
+ store_sample(db, review_id, tenant_no=tenant_no, user_id=user_id)
|
|
|
+ db.refresh(rev)
|
|
|
+ elif action == "unstore":
|
|
|
+ unstore_sample(db, review_id, tenant_no=tenant_no, user_id=user_id)
|
|
|
+ db.refresh(rev)
|
|
|
+ return rev
|
|
|
+```
|
|
|
+
|
|
|
+`batch_submit_review` 增加必填 `review_side`,并传给 `submit_review`。
|
|
|
+
|
|
|
+`store_sample` 在 `submit_review` 之后调用时,记录已 flush;`store_sample` 会再查一次。注意 `submit_review` 里先 flush 再 `store_sample`,避免读到旧 `storage_status`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 更新 `get_storage_release_map_for_samples`**
|
|
|
+
|
|
|
+不要再用「汇总结论为空则跳过」。有任一侧结论或已入库都应返回。`out[sid]` 增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ "qcReviewConclusion": rev.qc_review_conclusion or "",
|
|
|
+ "qcReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(rev.qc_review_conclusion or "", rev.qc_review_conclusion or ""),
|
|
|
+ "qcReviewByName": rev.qc_review_by_name or "",
|
|
|
+ "qcReviewTime": _fmt_dt(rev.qc_review_time),
|
|
|
+ "qcReviewRemark": rev.qc_review_remark or "",
|
|
|
+ "qcReviewAttachments": rev.qc_review_attachments or "",
|
|
|
+ "prodReviewConclusion": rev.prod_review_conclusion or "",
|
|
|
+ "prodReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(rev.prod_review_conclusion or "", rev.prod_review_conclusion or ""),
|
|
|
+ "prodReviewByName": rev.prod_review_by_name or "",
|
|
|
+ "prodReviewTime": _fmt_dt(rev.prod_review_time),
|
|
|
+ "prodReviewRemark": rev.prod_review_remark or "",
|
|
|
+ "prodReviewAttachments": rev.prod_review_attachments or "",
|
|
|
+ "storageStatus": rev.storage_status or "drk",
|
|
|
+ "storageStatusLabel": _STORAGE_STATUS_LABELS.get(rev.storage_status or "drk", rev.storage_status or "drk"),
|
|
|
+```
|
|
|
+
|
|
|
+保留原汇总 `reviewConclusion*` 字段。跳过条件改为:记录不存在则 continue;存在则始终输出。
|
|
|
+
|
|
|
+- [ ] **Step 5: 再跑纯函数单测**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_dual_gate.py tests/test_storage_release_review_phase.py tests/test_storage_release_review_ready_filter.py tests/test_storage_release_review_hide_voided.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS。`normalize_review_phase` 测试仍成立。
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/crud/storage_release_review.py
|
|
|
+git commit -m "feat: apply dual-gate storage release submit, auto store, and unstore."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: API 接入 `reviewSide` 与写权限
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/storage_release_review.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 增加菜单常量与写权限校验**
|
|
|
+
|
|
|
+文件顶部增加:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.core.menu_guard import (
|
|
|
+ require_admin_or_super_admin,
|
|
|
+ require_any_menu_code,
|
|
|
+ user_has_menu_perm,
|
|
|
+)
|
|
|
+
|
|
|
+STORAGE_RELEASE_READ_MENU_CODES = (
|
|
|
+ "biz:storageReleaseReview:list",
|
|
|
+ "production:storageReleaseReview:list",
|
|
|
+ "quality:storageReleaseReview:list",
|
|
|
+)
|
|
|
+STORAGE_RELEASE_WRITE_MENU_CODES = (
|
|
|
+ "production:storageReleaseReview:list",
|
|
|
+ "quality:storageReleaseReview:list",
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def _ensure_review_side_write(db: Session, user: dict, review_side: str) -> str:
|
|
|
+ side = srr_svc.normalize_review_side(review_side)
|
|
|
+ if side not in ("production", "qc"):
|
|
|
+ raise ValueError("审核侧无效")
|
|
|
+ menu = srr_svc.review_side_required_menu(side)
|
|
|
+ if not menu or not user_has_menu_perm(db, user, menu):
|
|
|
+ raise PermissionError("无权提交该侧审核")
|
|
|
+ return side
|
|
|
+```
|
|
|
+
|
|
|
+列表 Depends 改为 `require_any_menu_code(*STORAGE_RELEASE_READ_MENU_CODES)`(行为仍等同运营端登录,但标明三入口)。
|
|
|
+
|
|
|
+列表函数增加 Query:`reviewSide`、`prodReviewConclusion`、`qcReviewConclusion`,传入 `list_reviews` 的 `review_side` / `prod_review_conclusion` / `qc_review_conclusion`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 提交/批量/入库走写权限**
|
|
|
+
|
|
|
+`submit_single_review`:
|
|
|
+
|
|
|
+```python
|
|
|
+ side = _ensure_review_side_write(db, user, body.reviewSide)
|
|
|
+ rev = srr_svc.submit_review(
|
|
|
+ db,
|
|
|
+ review_id,
|
|
|
+ review_side=side,
|
|
|
+ review_conclusion=body.reviewConclusion,
|
|
|
+ review_remark=body.reviewRemark,
|
|
|
+ review_attachments=body.reviewAttachments,
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ user_id=user_id,
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+`batch_review` 同样先 `_ensure_review_side_write`,再 `batch_submit_review(..., review_side=side, ...)`。
|
|
|
+
|
|
|
+`store_single_sample` / `batch_store_samples`:用户必须拥有 `STORAGE_RELEASE_WRITE_MENU_CODES` 中任一(`user_has_menu_perm` 循环)。细胞存储菜单不够。无权限 `raise PermissionError`。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/api/storage_release_review.py
|
|
|
+git commit -m "feat: require reviewSide and write menus on storage release APIs."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 菜单与路由三入口
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/router/modules/sqxp.ts`
|
|
|
+- Modify: `backend/app/api/menu.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 生产管理增加菜单**
|
|
|
+
|
|
|
+在 `sqxp.ts` 的 `ProductionWorkOrder`(生产工单)之后插入:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ {
|
|
|
+ id: '95400000000000000000000000000001',
|
|
|
+ name: 'ProductionStorageReleaseReview',
|
|
|
+ path: 'storage-release-review',
|
|
|
+ component: '/quality-inspection/storage-release-review/index',
|
|
|
+ meta: {
|
|
|
+ title: '入库放行审核',
|
|
|
+ keepAlive: true,
|
|
|
+ menuCode: 'production:storageReleaseReview:list'
|
|
|
+ },
|
|
|
+ props: { reviewRole: 'production' }
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+`menu.py` 在 `ProductionWorkOrder` 对象之后插入对应 JSON(`id` / `name` / `path` / `component` / `meta` 与上面一致,`props` 若静态树不支持可只放 meta;前端以 `sqxp.ts` 的 `props` 为准)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 质检管理改为真实页**
|
|
|
+
|
|
|
+替换 `sqxp.ts` 里 `QualityInspectionStorageReleaseReviewRedirect` 整段为:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ {
|
|
|
+ id: '70000000000000000000000000000014',
|
|
|
+ name: 'QualityInspectionStorageReleaseReview',
|
|
|
+ path: 'storage-release-review',
|
|
|
+ component: '/quality-inspection/storage-release-review/index',
|
|
|
+ meta: {
|
|
|
+ title: '入库放行审核',
|
|
|
+ keepAlive: true,
|
|
|
+ menuCode: 'quality:storageReleaseReview:list'
|
|
|
+ },
|
|
|
+ props: { reviewRole: 'qc' }
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+去掉 `isHide` 与 `redirect`。`menu.py` 的质检管理 `children` 在「质检工单」之后增加同样一项。
|
|
|
+
|
|
|
+细胞存储现有路由保持不变,**不要**加 `reviewRole`(页面默认 `overview`)。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/router/modules/sqxp.ts backend/app/api/menu.py
|
|
|
+git commit -m "feat: add production and QC menus for storage release review."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: 前端 API 类型
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/storage-release-review.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展类型与请求**
|
|
|
+
|
|
|
+`StorageReleaseReview` 增加:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ qcReviewConclusion?: string
|
|
|
+ qcReviewConclusionLabel?: string
|
|
|
+ qcReviewRemark?: string
|
|
|
+ qcReviewAttachments?: string
|
|
|
+ qcReviewBy?: string
|
|
|
+ qcReviewByName?: string
|
|
|
+ qcReviewTime?: string
|
|
|
+ prodReviewConclusion?: string
|
|
|
+ prodReviewConclusionLabel?: string
|
|
|
+ prodReviewRemark?: string
|
|
|
+ prodReviewAttachments?: string
|
|
|
+ prodReviewBy?: string
|
|
|
+ prodReviewByName?: string
|
|
|
+ prodReviewTime?: string
|
|
|
+```
|
|
|
+
|
|
|
+`fetchStorageReleaseReviewList` params 增加:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ reviewSide?: 'overview' | 'production' | 'qc'
|
|
|
+ prodReviewConclusion?: string
|
|
|
+ qcReviewConclusion?: string
|
|
|
+```
|
|
|
+
|
|
|
+`submitStorageReleaseReview` / `batchReviewStorageRelease` 的 data 增加必填 `reviewSide: 'production' | 'qc'`。
|
|
|
+
|
|
|
+可删除前端对 `storeSample(id)` 的页面调用(Task 9),但 API 函数可保留。
|
|
|
+
|
|
|
+- [ ] **Step 2: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/storage-release-review.ts
|
|
|
+git commit -m "feat: add dual-side fields to storage release frontend API."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: 列表页按入口显隐并拆结论列
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/quality-inspection/storage-release-review/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 增加 `reviewRole` 并驱动请求**
|
|
|
+
|
|
|
+在 `defineOptions` 后:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const props = defineProps<{
|
|
|
+ reviewRole?: 'overview' | 'production' | 'qc'
|
|
|
+ }>()
|
|
|
+
|
|
|
+ const reviewRole = computed<'overview' | 'production' | 'qc'>(() => {
|
|
|
+ const r = props.reviewRole
|
|
|
+ if (r === 'production' || r === 'qc' || r === 'overview') return r
|
|
|
+ return 'overview'
|
|
|
+ })
|
|
|
+
|
|
|
+ const canWrite = computed(() => reviewRole.value !== 'overview')
|
|
|
+```
|
|
|
+
|
|
|
+`loadData` 的请求增加 `reviewSide: reviewRole.value`,筛选改为:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ prodReviewConclusion: searchForm.prodReviewConclusion || undefined,
|
|
|
+ qcReviewConclusion: searchForm.qcReviewConclusion || undefined,
|
|
|
+```
|
|
|
+
|
|
|
+去掉 `reviewConclusion` 入参。
|
|
|
+
|
|
|
+`searchForm` 把 `reviewConclusion` 换成 `prodReviewConclusion: ''`、`qcReviewConclusion: ''`。`handleReset` 同步清空这两个字段。
|
|
|
+
|
|
|
+`watch(reviewRole, () => { pagination.page = 1; selectedRows.value = []; loadData() })`
|
|
|
+
|
|
|
+`onActivated(() => loadData())`(三路由共用 SFC,避免 keep-alive 脏数据)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 筛选、工具栏、列、操作**
|
|
|
+
|
|
|
+筛选区原「审核结论」换成两个 `el-form-item`:
|
|
|
+
|
|
|
+- label `生产结论`,`v-model="searchForm.prodReviewConclusion"`,options 仍用 `reviewConclusionOptions`
|
|
|
+- label `质检结论`,`v-model="searchForm.qcReviewConclusion"`
|
|
|
+
|
|
|
+工具栏:
|
|
|
+
|
|
|
+- `v-if="canWrite"` 包住「批量审核」
|
|
|
+- `v-if="canWrite"` 包住「批量入库」
|
|
|
+- `canBatchReview` 保持所选非空
|
|
|
+- `canBatchStore` 改为:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const canBatchStore = computed(
|
|
|
+ () =>
|
|
|
+ selectedRows.value.length > 0 &&
|
|
|
+ selectedRows.value.every(
|
|
|
+ (r) =>
|
|
|
+ r.prodReviewConclusion === 'tg' &&
|
|
|
+ r.qcReviewConclusion === 'tg' &&
|
|
|
+ r.storageStatus === 'drk'
|
|
|
+ )
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+表格:用两列替换原「审核结论」列;**删除**「审核人」「审核时间」列。
|
|
|
+
|
|
|
+生产结论列模板(质检列同理,字段换 `qcReviewConclusion`):
|
|
|
+
|
|
|
+```vue
|
|
|
+ <el-table-column
|
|
|
+ prop="prodReviewConclusion"
|
|
|
+ label="生产结论"
|
|
|
+ min-width="110"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag
|
|
|
+ v-if="row.prodReviewConclusion"
|
|
|
+ :type="storageReleaseConclusionTagType(row.prodReviewConclusion)"
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ {{ storageReleaseConclusionLabel(row.prodReviewConclusion) }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag v-else type="info" size="small">待审核</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+```
|
|
|
+
|
|
|
+操作列:
|
|
|
+
|
|
|
+- 「查看」始终显示
|
|
|
+- 「审核」`v-if="canWrite"`
|
|
|
+- **删除**「样本入库」按钮及 `handleStore`、对 `storeSample` 的 import 使用(批量仍用 `batchStoreSample`)
|
|
|
+
|
|
|
+`StorageReleaseReviewDialog` 增加 `:review-side="reviewRole === 'overview' ? 'qc' : reviewRole"`(overview 不会打开审核弹窗)。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/quality-inspection/storage-release-review/index.vue
|
|
|
+git commit -m "feat: split storage release list by review role and dual conclusions."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 10: 审核弹窗只提交本侧
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 增加 `reviewSide` prop**
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const props = defineProps<{
|
|
|
+ modelValue: boolean
|
|
|
+ reviewIds: string[]
|
|
|
+ reviewItems?: StorageReleaseReviewQueueItem[]
|
|
|
+ reviewSide: 'production' | 'qc'
|
|
|
+ }>()
|
|
|
+```
|
|
|
+
|
|
|
+`dialogTitle`:非批量时 `props.reviewSide === 'production' ? '生产入库放行审核' : '质检入库放行审核'`。
|
|
|
+
|
|
|
+在证据区 verdict chips 之后、本侧表单之前,增加对侧只读(用 `activeDetail.review`):
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div v-if="peerSideLabel" class="review-peer-readonly">
|
|
|
+ <span>{{ peerSideLabel }}:</span>
|
|
|
+ <el-tag :type="peerTagType" size="small">{{ peerConclusionText }}</el-tag>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const peerSideLabel = computed(() =>
|
|
|
+ props.reviewSide === 'production' ? '质检结论' : '生产结论'
|
|
|
+ )
|
|
|
+ const peerConclusion = computed(() => {
|
|
|
+ const rev = activeDetail.value?.review
|
|
|
+ return props.reviewSide === 'production'
|
|
|
+ ? rev?.qcReviewConclusion || ''
|
|
|
+ : rev?.prodReviewConclusion || ''
|
|
|
+ })
|
|
|
+ const peerConclusionText = computed(() =>
|
|
|
+ peerConclusion.value
|
|
|
+ ? storageReleaseConclusionLabel(peerConclusion.value)
|
|
|
+ : '待审核'
|
|
|
+ )
|
|
|
+ const peerTagType = computed(() =>
|
|
|
+ peerConclusion.value
|
|
|
+ ? storageReleaseConclusionTagType(peerConclusion.value)
|
|
|
+ : 'info'
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+若尚未 import `storageReleaseConclusionLabel` / `storageReleaseConclusionTagType`,从 `@/utils/status-tag-registry` 引入。
|
|
|
+
|
|
|
+- [ ] **Step 2: payload 带 `reviewSide`**
|
|
|
+
|
|
|
+`buildPayload`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ return {
|
|
|
+ reviewSide: props.reviewSide,
|
|
|
+ reviewConclusion: form.reviewConclusion,
|
|
|
+ reviewRemark: form.reviewRemark.trim() || undefined,
|
|
|
+ reviewAttachments: attachmentUrls || undefined
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+打开弹窗加载详情后,本侧表单可预填已有结论:`reviewSide === 'production' ? review.prodReviewConclusion : review.qcReviewConclusion`,空则默认 `tg`。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue
|
|
|
+git commit -m "feat: submit storage release reviews for one side only."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 11: 查看弹窗拆质检/生产/入库
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 替换原「放行审核」一节**
|
|
|
+
|
|
|
+增加本地函数:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ function sideConclusionDisplay(code?: string) {
|
|
|
+ const v = code || ''
|
|
|
+ return v ? storageReleaseConclusionLabel(v) : '待审核'
|
|
|
+ }
|
|
|
+ function sideConclusionTag(code?: string) {
|
|
|
+ const v = code || ''
|
|
|
+ return v ? storageReleaseConclusionTagType(v) : 'info'
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+用三节替换原来的 `title: '放行审核'`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ {
|
|
|
+ title: '质检放行',
|
|
|
+ slot: 'qc-review-attachments',
|
|
|
+ fields: [
|
|
|
+ {
|
|
|
+ label: '审核结论',
|
|
|
+ value: sideConclusionDisplay(rev.qcReviewConclusion),
|
|
|
+ tagType: sideConclusionTag(rev.qcReviewConclusion)
|
|
|
+ },
|
|
|
+ { label: '审核人', value: rev.qcReviewByName || '—' },
|
|
|
+ { label: '审核时间', value: rev.qcReviewTime || '—' },
|
|
|
+ { label: '审核备注', value: rev.qcReviewRemark || '—', span: true }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '生产放行',
|
|
|
+ slot: 'prod-review-attachments',
|
|
|
+ fields: [
|
|
|
+ {
|
|
|
+ label: '审核结论',
|
|
|
+ value: sideConclusionDisplay(rev.prodReviewConclusion),
|
|
|
+ tagType: sideConclusionTag(rev.prodReviewConclusion)
|
|
|
+ },
|
|
|
+ { label: '审核人', value: rev.prodReviewByName || '—' },
|
|
|
+ { label: '审核时间', value: rev.prodReviewTime || '—' },
|
|
|
+ { label: '审核备注', value: rev.prodReviewRemark || '—', span: true }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '入库',
|
|
|
+ fields: [
|
|
|
+ {
|
|
|
+ label: '入库状态',
|
|
|
+ value: storageStatusLabel(rev.storageStatus || 'drk'),
|
|
|
+ tagType: storageStatusTagType(rev.storageStatus || 'drk')
|
|
|
+ },
|
|
|
+ { label: '入库人', value: rev.storageByName || '—' },
|
|
|
+ { label: '入库时间', value: rev.storageTime || '—' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+从 `@/utils/status-tag-registry` 增加 `storageStatusLabel`、`storageStatusTagType`(若尚未导入)。
|
|
|
+
|
|
|
+附件 slot:按 `qcReviewAttachments` / `prodReviewAttachments` 各做只读列表,逻辑克隆现有 `reviewAttachments` 拆 URL 代码。删除对汇总 `reviewAttachments` 的唯一 slot(或保留但不作为主展示)。
|
|
|
+
|
|
|
+- [ ] **Step 2: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue
|
|
|
+git commit -m "feat: show production and QC verdicts in storage release view dialog."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 12: 协议查看卡展示两侧结论
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/business/sample-storage-release-info/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展类型与模板**
|
|
|
+
|
|
|
+`StorageReleaseInfo` 增加 `qcReviewConclusion`、`qcReviewConclusionLabel`、`prodReviewConclusion`、`prodReviewConclusionLabel`、`storageStatus`、`storageStatusLabel`(以及两侧人/时间可选)。
|
|
|
+
|
|
|
+网格改为四行 Tag:生产结论、质检结论、入库状态;人/时间可各侧一行。空结论显示「待审核」。组件内用与现网相同的 `el-tag` 语义(`tg` success、`btg` danger、空 info)。
|
|
|
+
|
|
|
+无记录仍显示「暂无入库放行审核信息」。有记录但两侧都待审也应展示(配合 CRUD map 不再因汇总结论为空而省略)。
|
|
|
+
|
|
|
+- [ ] **Step 2: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/business/sample-storage-release-info/index.vue
|
|
|
+git commit -m "feat: show both release verdicts on protocol sample cards."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 13: 字段一致性、图谱、手工验收
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- 按需:`backend/scripts/check_field_consistency.py` 若已注册该实体则修 diff
|
|
|
+- 图谱:`./scripts/update-graph.sh all`
|
|
|
+
|
|
|
+- [ ] **Step 1: 字段一致性**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python scripts/check_field_consistency.py
|
|
|
+```
|
|
|
+
|
|
|
+`submit` 的可写字段是 `reviewSide` + 本侧结论,不是把 `prodReview*` 全部放进 formData。若脚本误报,只对已注册实体修真实遗漏,不要为通过脚本把只读两侧字段塞进 CreateBody。
|
|
|
+
|
|
|
+- [ ] **Step 2: 更新图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh all
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 手工清单(实现者勾选)**
|
|
|
+
|
|
|
+1. 质检入口审通过、生产入口再通过 → 自动已入库;采集样本 `yrk`。
|
|
|
+2. 对调顺序同样自动入库。
|
|
|
+3. 已入库后任一侧改不通过 → 放行 `drk`,样本 `jcz`,库位产品仍在。
|
|
|
+4. 两侧再都通过 → 再次 `yrk`。
|
|
|
+5. 细胞存储入口无审核/批量入库;生产入口改不了质检字段。
|
|
|
+6. 批量入库只对双 `tg` 且 `drk` 生效。
|
|
|
+7. 历史已入库行:总览待审核 Tab 可见「已入库 + 生产待审」。
|
|
|
+8. 待审核就绪过滤、作废隐藏仍有效。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit 图谱(若有 diff)**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/graphify-out web-admin/graphify-out
|
|
|
+git commit -m "chore: update graphify after storage release dual-gate."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage(自检)
|
|
|
+
|
|
|
+| 规格章节 | 任务 |
|
|
|
+|----------|------|
|
|
|
+| §3 三入口与 menuCode | Task 7 |
|
|
|
+| §3 写接口校验 reviewSide | Task 6 |
|
|
|
+| §4 汇总与状态机 | Task 1、5 |
|
|
|
+| §5 表结构 | Task 2、3、4 |
|
|
|
+| §6 自动入库 / 回退 / 批量入库 | Task 5、6、9 |
|
|
|
+| §7 API | Task 4–6 |
|
|
|
+| §8.1–8.2 列表 | Task 9 |
|
|
|
+| §8.3 审核弹窗 | Task 10 |
|
|
|
+| §8.4 查看弹窗 | Task 11 |
|
|
|
+| §8.5 协议卡 | Task 5 map + Task 12 |
|
|
|
+| §9 历史迁移 | Task 2 |
|
|
|
+| §10 短信仍用汇总 | 不改短信模块;汇总由 Task 5 维护 |
|
|
|
+| §11 测试要点 | Task 1 单测 + Task 13 手工 |
|
|
|
+| 去掉单条入库按钮 | Task 9 |
|
|
|
+| 就绪过滤 / 作废隐藏不变 | Task 5 明确不改那些函数 |
|