# 临时质检 · 外部质检对象编号 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_qc_work_order` 增加可空字段 `external_target_code`;手动创建/更新临时工单时用纯函数归一化互斥目标(有产品则清空外部编号,有外部编号则清空产品 ID);列表/详情透出字段;前端弹窗用来源切换(方案 A);卡片标题与详情按来源展示。生产派发链路不改。 **Tech Stack:** FastAPI / SQLAlchemy / Pydantic v2 / Vue 3 / Element Plus;SQL MySQL 5.7。 **Spec:** `docs/superpowers/specs/2026-08-20-temporary-qc-external-target-code-design.md` --- ## File map | File | Responsibility | |------|----------------| | Create: `docs/sql/2026-08-20-qc-work-order-external-target-code.sql` | 增量 ALTER | | Modify: `backend/app/models/models.py` | `BizQcWorkOrder.external_target_code` | | Modify: `backend/app/schemas/qc_work_order.py` | Create/Update Body 增加 `externalTargetCode` | | Modify: `backend/app/crud/qc_work_order.py` | 归一化校验、create/update、`_wo_to_dict`、keyword | | Create: `backend/tests/test_temp_qc_target_resolve.py` | 互斥归一化纯函数单测 | | Modify: `backend/app/api/qc.py` | create/update 传入 `external_target_code` | | Modify: `web-admin/src/api/qc-work-order.ts` | 类型与 create/update 参数 | | Modify: `web-admin/src/views/quality-inspection/qc-work-order/index.vue` | 方案 A 弹窗 | | Modify: `web-admin/src/utils/qc-work-order-card-meta.ts` | 卡片标题 + temporary meta | | Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/inbound-view-sections.ts` | 详情分区 | | Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue` | 任务侧「质检对象」兜底 | | Modify: `web-admin/src/utils/qc-work-order-tabs.ts` | temporary keyword placeholder | --- ### Task 1: SQL 增量字段 **Files:** - Create: `docs/sql/2026-08-20-qc-work-order-external-target-code.sql` - [ ] **Step 1: Write SQL** ```sql -- 临时质检:外部质检对象编号(手写,可空) -- MySQL 5.7 ALTER TABLE `biz_qc_work_order` ADD COLUMN `external_target_code` varchar(64) DEFAULT NULL COMMENT '外部质检对象编号(手写)' AFTER `cell_product_id`; ``` - [ ] **Step 2: Commit** ```bash git add docs/sql/2026-08-20-qc-work-order-external-target-code.sql git commit -m "docs(sql): add external_target_code on biz_qc_work_order" ``` --- ### Task 2: 后端模型 + 互斥归一化纯函数(TDD) **Files:** - Modify: `backend/app/models/models.py`(`BizQcWorkOrder`,约 `cell_product_id` 后) - Modify: `backend/app/crud/qc_work_order.py` - Create: `backend/tests/test_temp_qc_target_resolve.py` - [ ] **Step 1: Write failing tests** ```python """临时质检对象:系统产品 / 外部编号互斥归一化。""" import unittest from app.crud.qc_work_order import resolve_temp_qc_target class TestResolveTempQcTarget(unittest.TestCase): def test_cell_product_only(self): cp, ext = resolve_temp_qc_target( cell_product_id=" pid-1 ", external_target_code=" " ) self.assertEqual(cp, "pid-1") self.assertIsNone(ext) def test_external_only(self): cp, ext = resolve_temp_qc_target( cell_product_id="", external_target_code=" EXT-1 " ) self.assertIsNone(cp) self.assertEqual(ext, "EXT-1") def test_both_raises(self): with self.assertRaises(ValueError) as ctx: resolve_temp_qc_target( cell_product_id="pid", external_target_code="EXT" ) self.assertIn("不能同时", str(ctx.exception)) def test_neither_raises(self): with self.assertRaises(ValueError) as ctx: resolve_temp_qc_target(cell_product_id="", external_target_code="") self.assertIn("质检对象", str(ctx.exception)) def test_external_too_long(self): with self.assertRaises(ValueError) as ctx: resolve_temp_qc_target( cell_product_id="", external_target_code="x" * 65 ) self.assertIn("64", str(ctx.exception)) if __name__ == "__main__": unittest.main() ``` - [ ] **Step 2: Run tests — expect FAIL (import / missing function)** ```bash cd backend && source venv/bin/activate && python -m pytest tests/test_temp_qc_target_resolve.py -v ``` Expected: FAIL(`resolve_temp_qc_target` 未定义) - [ ] **Step 3: Add model column** In `BizQcWorkOrder`, immediately after `cell_product_id`: ```python cell_product_id = Column(String(64), nullable=True, comment="质检对象-细胞产品ID") external_target_code = Column(String(64), nullable=True, comment="外部质检对象编号(手写)") ``` - [ ] **Step 4: Implement `resolve_temp_qc_target` near `_validate_cell_product_id`** ```python def resolve_temp_qc_target( *, cell_product_id: Optional[str] = None, external_target_code: Optional[str] = None, ) -> Tuple[Optional[str], Optional[str]]: """临时质检对象互斥归一化。返回 (cell_product_id|None, external_target_code|None)。""" cp = (cell_product_id or "").strip() or None ext = (external_target_code or "").strip() or None if cp and ext: raise ValueError("质检对象不能同时选择系统产品并填写外部编号") if not cp and not ext: raise ValueError("请选择质检对象或填写外部编号") if ext and len(ext) > 64: raise ValueError("外部编号最长 64 个字符") if ext: return None, ext return cp, None ``` Ensure `Tuple` / `Optional` imports exist at top of `qc_work_order.py`. - [ ] **Step 5: Run tests — expect PASS** ```bash cd backend && source venv/bin/activate && python -m pytest tests/test_temp_qc_target_resolve.py -v ``` Expected: PASS - [ ] **Step 6: Commit** ```bash git add backend/app/models/models.py backend/app/crud/qc_work_order.py backend/tests/test_temp_qc_target_resolve.py git commit -m "feat(backend): resolve temporary QC target cell product vs external code" ``` --- ### Task 3: Schema + create/update + dict + keyword + API **Files:** - Modify: `backend/app/schemas/qc_work_order.py` - Modify: `backend/app/crud/qc_work_order.py`(`create_temp_qc_work_order`、`update_qc_work_order`、`_wo_to_dict`、列表 keyword) - Modify: `backend/app/api/qc.py` - [ ] **Step 1: Extend Pydantic bodies** In `QcWorkOrderCreateBody` add: ```python external_target_code: Optional[str] = Field( None, alias="externalTargetCode", description="外部质检对象编号(手写)" ) ``` In `QcWorkOrderUpdateBody` add the same field. - [ ] **Step 2: Update `create_temp_qc_work_order`** Change signature to accept `external_target_code: str = ""` and replace the old forced `_validate_cell_product_id` block with: ```python cp_id, ext_code = resolve_temp_qc_target( cell_product_id=cell_product_id, external_target_code=external_target_code, ) if cp_id: cp_id = _validate_cell_product_id(db, cp_id, tenant_no) wo = BizQcWorkOrder( ... cell_product_id=cp_id, external_target_code=ext_code, ... ) ``` Keep other fields unchanged. - [ ] **Step 3: Update `update_qc_work_order`** Add param `external_target_code: Optional[str] = None`. For `wo_type == "temporary"`, when **either** `cell_product_id is not None` **or** `external_target_code is not None`(编辑弹窗总会同时提交两者时用显式传参): 约定:编辑临时工单时 API **始终同时传入** `cellProductId` 与 `externalTargetCode`(一侧为空串)。实现: ```python if wo_type == "temporary": if cell_product_id is not None or external_target_code is not None: cp_id, ext_code = resolve_temp_qc_target( cell_product_id=cell_product_id if cell_product_id is not None else (wo.cell_product_id or ""), external_target_code=external_target_code if external_target_code is not None else (getattr(wo, "external_target_code", None) or ""), ) # 前端编辑会同时提交两侧;为避免「只改备注」误走上面分支, # API 层:仅当 body 显式带了 cellProductId 或 externalTargetCode 字段时才更新对象。 if cp_id: cp_id = _validate_cell_product_id(db, cp_id, tenant_no) wo.cell_product_id = cp_id wo.external_target_code = ext_code ``` 更稳妥的约定(推荐在本 Task 落实): - `update` 增加显式参数;API 用 `body.model_fields_set` / `body.model_dump(exclude_unset=True)` 判断是否更新质检对象。 - 若 `cellProductId` 或 `externalTargetCode` **任一在 unset 之外**:用 body 值(缺省当 `""`)调用 `resolve_temp_qc_target`,再写回。 - 若两者都 unset:不改质检对象(备注/状态路径不变)。 Example in API: ```python data = body.model_dump(by_alias=False, exclude_unset=True) touch_target = "cell_product_id" in data or "external_target_code" in data wo = wo_svc.update_qc_work_order( ..., cell_product_id=data.get("cell_product_id") if touch_target else None, external_target_code=data.get("external_target_code") if touch_target else None, # 当 touch_target 时,传入两侧(缺省 ""),crud 内若两者参数均 not None 则 resolve ... ) ``` Simplest crud rule that matches frontend(编辑弹窗总是带两侧): ```python if wo_type == "temporary" and cell_product_id is not None and external_target_code is not None: cp_id, ext_code = resolve_temp_qc_target( cell_product_id=cell_product_id, external_target_code=external_target_code, ) if cp_id: cp_id = _validate_cell_product_id(db, cp_id, tenant_no) wo.cell_product_id = cp_id wo.external_target_code = ext_code ``` 备注-only 更新(`WorkOrderRemarkDialog`)不传这两个字段 → 两者保持 `None` → 不改对象。前端临时编辑弹窗必须 **同时传** `cellProductId` 与 `externalTargetCode`(一侧 `""`)。 - [ ] **Step 4: `_wo_to_dict` 增加字段** Next to `cellProductId`: ```python "cellProductId": getattr(wo, "cell_product_id", None) or "", "externalTargetCode": getattr(wo, "external_target_code", None) or "", ``` - [ ] **Step 5: Keyword — general list** In `list_*` keyword block(约 `keyword_conditions = [...]`),追加: ```python BizQcWorkOrder.external_target_code.like(like), ``` Also in `list_temp_qc_work_orders` if still used: ```python q0 = q0.filter( or_( BizQcWorkOrder.work_order_no.like(like), BizQcWorkOrder.external_target_code.like(like), ) ) ``` (需 `or_` 已导入。) - [ ] **Step 6: Wire API create/update** `create_qc_work_order` temporary branch: ```python wo = wo_svc.create_temp_qc_work_order( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), cell_product_id=body.cell_product_id or "", external_target_code=body.external_target_code or "", remark=body.remark or "", ) ``` `update_qc_work_order` API: pass `external_target_code=body.external_target_code`(与 `cell_product_id` 同样,可为 None)。 - [ ] **Step 7: Smoke import** ```bash cd backend && source venv/bin/activate && python -c "from app.crud.qc_work_order import resolve_temp_qc_target, create_temp_qc_work_order; from app.schemas.qc_work_order import QcWorkOrderCreateBody; print('OK')" ``` Expected: `OK` - [ ] **Step 8: Commit** ```bash git add backend/app/schemas/qc_work_order.py backend/app/crud/qc_work_order.py backend/app/api/qc.py git commit -m "feat(backend): persist external target code on temporary QC work orders" ``` --- ### Task 4: 前端 API 类型 **Files:** - Modify: `web-admin/src/api/qc-work-order.ts` - [ ] **Step 1: Extend `QcWorkOrder`** After `cellProductId?: string` add: ```typescript externalTargetCode?: string ``` - [ ] **Step 2: Extend create/update payloads** ```typescript export function fetchCreateQcWorkOrder(data: { workOrderType?: string protocolNo?: string sampleCode?: string receptionSource?: string qcBizType?: string cellProductId?: string externalTargetCode?: string remark?: string }) { ... } export function fetchUpdateQcWorkOrder( id: string, data: { cellProductId?: string externalTargetCode?: string workOrderStatus?: string qcConclusion?: string remark?: string } ) { ... } ``` - [ ] **Step 3: Commit** ```bash git add web-admin/src/api/qc-work-order.ts git commit -m "feat(web-admin): type externalTargetCode on QC work order API" ``` --- ### Task 5: 新增/编辑弹窗(方案 A) **Files:** - Modify: `web-admin/src/views/quality-inspection/qc-work-order/index.vue` - [ ] **Step 1: Template — temporary form block** Replace the temporary branch of the add dialog form so it includes来源切换. Structure: ```vue