diff --git a/docs/superpowers/plans/2026-08-19-storage-release-dual-gate.md b/docs/superpowers/plans/2026-08-19-storage-release-dual-gate.md
new file mode 100644
index 00000000..fe6a1c03
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-19-storage-release-dual-gate.md
@@ -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
+