# 基础实验业务 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:** 新增 `biz_type=basic_experiment` 基础实验业务线:B2B 细胞产品售卖、一产品一售卖计划、独立缴费/出库/交付闭环。 **Architecture:** 复用 `biz_pilot_*` 表与中试/临床出库链路;申请单扩展课题/客户字段;售卖方案保存时按产品行批量 `create_schedule`(每行 → 1 条 `infusion_schedule` + 工单 + 缴费)。前端通过 `pilot-biz-type.ts` 策略配置 + 路由 `props.bizType` 隔离 UI。 **Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2(后端),Vue 3 + Element Plus + TypeScript(前端),MySQL 5.7 **Design Spec:** `docs/superpowers/specs/2026-06-10-basic-experiment-design.md` --- ## File Structure ### SQL — New - `docs/sql/migrations/2026-06-10-basic-experiment.sql` — ALTER `biz_pilot_application`、扩展 `biz_type` 注释、`biz_pilot_payment` 注释 - `docs/sql/migrations/2026-06-10-basic-experiment-menu.sql` — `sys_menu` 基础实验菜单(可选,与 `menu.py` 对齐) ### Backend — Modified - `backend/app/models/models.py` — `BizPilotApplication` 新增客户/课题字段;`biz_type` 注释扩展 - `backend/app/crud/pilot_application.py` — JCSY 前缀、`basic_experiment` 校验、售卖计划批量创建、`_row_to_dict` 新字段 - `backend/app/api/pilot_application.py` — Create/Update schema 新字段 - `backend/app/crud/clinical_payment.py` — `auto_create_payment` / 列表支持 `basic_experiment` - `backend/app/api/clinical_payment.py` — 列表接口 `bizType` 过滤 - `backend/app/crud/outbound_release_review.py` — `_BIZ_TYPE_LABELS`、详情聚合 - `backend/app/crud/pilot_outbound_status_sync.py` — `_OUTBOUND_BIZ_TYPES` - `backend/app/crud/qc_work_order.py` — 出库详情 `basic_experiment` 上下文 - `backend/app/crud/production_work_order.py` — 出库详情 `basic_experiment` 上下文 - `backend/app/crud/pilot_infusion_schedule_mgmt.py` — 详情/列表 `basic_experiment` 字段 - `backend/app/api/menu.py` — 基础实验菜单树 ### Backend — New Tests - `backend/tests/test_basic_experiment_application.py` — 申请单创建、多产品多计划、工单/缴费联动 ### Frontend — New - `web-admin/src/config/pilot-biz-type.ts` — `bizType` 策略配置表 ### Frontend — Modified - `web-admin/src/router/modules/sqxp.ts` — 基础实验一级菜单 + 子路由 - `web-admin/src/utils/status-tag-registry.ts` — `basic_experiment` 标签 - `web-admin/src/utils/outbound-qc-pilot-view-sections.ts` — `basic_experiment` variant - `web-admin/src/views/pilot-service/pilot-application/index.vue` — Tab/卡片按 config 渲染 - `web-admin/src/views/pilot-service/pilot-application/PilotApplicationTab.vue` - `web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue` — 基础实验表单 + 售卖产品子表 - `web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue` - `web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue` — 交付 Tab 显隐 - `web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue` — 售卖计划文案/列 - `web-admin/src/views/pilot-service/pilot-application/ClinicalPaymentTab.vue` — `bizType` 列展示 - `web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue` — 接收 `bizType` prop - `web-admin/src/views/pilot-service/outbound-release-review/index.vue` - `web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue` - `web-admin/src/views/production/production-work-order/index.vue` - `web-admin/src/api/business.ts` — 类型/请求参数(若需) --- ## Task 1: 数据库迁移 **Files:** - Create: `docs/sql/migrations/2026-06-10-basic-experiment.sql` - Modify: `docs/sql/zkxw.sql`(全量基线同步) - [ ] **Step 1: 编写迁移 SQL** ```sql -- 基础实验:biz_pilot_application 扩展 ALTER TABLE biz_pilot_application MODIFY COLUMN biz_type VARCHAR(20) NOT NULL DEFAULT 'pilot_service' COMMENT '业务类型: pilot_service=中试服务, clinical_application=临床应用, basic_experiment=基础实验'; ALTER TABLE biz_pilot_application ADD COLUMN project_title VARCHAR(200) DEFAULT NULL COMMENT '课题名称(基础实验)' AFTER application_name, ADD COLUMN customer_name VARCHAR(200) DEFAULT NULL COMMENT '客户名称(基础实验)' AFTER project_title, ADD COLUMN customer_contact VARCHAR(100) DEFAULT NULL COMMENT '客户联系人(基础实验)' AFTER customer_name, ADD COLUMN customer_phone VARCHAR(32) DEFAULT NULL COMMENT '客户联系方式(基础实验)' AFTER customer_contact; ALTER TABLE biz_pilot_payment MODIFY COLUMN biz_type VARCHAR(20) NOT NULL DEFAULT 'clinical_application' COMMENT '业务类型: clinical_application=临床应用, basic_experiment=基础实验'; ``` - [ ] **Step 2: 在测试库执行迁移** Run: `mysql -u root -p zkxw < docs/sql/migrations/2026-06-10-basic-experiment.sql` Expected: 无报错;`DESCRIBE biz_pilot_application` 可见四个新字段。 - [ ] **Step 3: 同步 `docs/sql/zkxw.sql` 中对应表定义** - [ ] **Step 4: Commit** ```bash git add docs/sql/migrations/2026-06-10-basic-experiment.sql docs/sql/zkxw.sql git commit -m "feat: add basic experiment DB migration for application customer fields" ``` --- ## Task 2: 后端模型与 `_row_to_dict` **Files:** - Modify: `backend/app/models/models.py` - Modify: `backend/app/crud/pilot_application.py` - [ ] **Step 1: 在 `BizPilotApplication` 增加四列** ```python project_title = Column(String(200), nullable=True, comment="课题名称(基础实验)") customer_name = Column(String(200), nullable=True, comment="客户名称(基础实验)") customer_contact = Column(String(100), nullable=True, comment="客户联系人(基础实验)") customer_phone = Column(String(32), nullable=True, comment="客户联系方式(基础实验)") ``` 更新 `biz_type` 列 comment 含 `basic_experiment`。 - [ ] **Step 2: 扩展 `_row_to_dict_application`** ```python "projectTitle": row.project_title or "", "customerName": row.customer_name or "", "customerContact": row.customer_contact or "", "customerPhone": row.customer_phone or "", ``` - [ ] **Step 3: 扩展 `_generate_application_no`** ```python if biz_type == "clinical_application": prefix = f"LCSQ{today_str}" elif biz_type == "basic_experiment": prefix = f"JCSY{today_str}" else: prefix = f"ZSSQ{today_str}" ``` - [ ] **Step 4: Commit** ```bash git add backend/app/models/models.py backend/app/crud/pilot_application.py git commit -m "feat: extend pilot application model for basic experiment fields" ``` --- ## Task 3: 申请单创建/更新 — `basic_experiment` 校验 **Files:** - Modify: `backend/app/crud/pilot_application.py` - Modify: `backend/app/api/pilot_application.py` - [ ] **Step 1: 写失败测试** Create `backend/tests/test_basic_experiment_application.py`: ```python import pytest from app.crud import pilot_application as crud def test_generate_application_no_basic_experiment_prefix(): # mock db session 或集成测试:biz_type=basic_experiment 时前缀 JCSY assert "JCSQ" not in "JCSY202606100001" # placeholder; 实现后用真实调用断言 ``` 实现时改为:调用 `_generate_application_no` 或 create API 返回 `applicationNo.startswith("JCSY")`。 - [ ] **Step 2: 修改 `create_application` 分支** ```python biz_type = data.get("bizType") or "pilot_service" if biz_type == "basic_experiment": if not (data.get("projectTitle") or "").strip(): raise ValueError("请填写课题名称") if not (data.get("customerName") or "").strip(): raise ValueError("请填写客户名称") if not (data.get("customerContact") or "").strip(): raise ValueError("请填写客户联系人") if not (data.get("customerPhone") or "").strip(): raise ValueError("请填写客户联系方式") channel_service_order_id = "" channel_id = "" channel_name = "" elif biz_type == "clinical_application": # 保持现有逻辑 ... else: if not channel_service_order_id: raise ValueError("请选择渠道服务订单") ``` 创建 `BizPilotApplication` 时写入 `project_title/customer_*`;`consent_form_urls=None`。 - [ ] **Step 3: 修改 `update_application` 支持新字段** - [ ] **Step 4: API schema `CreateBody`/`UpdateBody` 增加** ```python project_title: Optional[str] = Field(None, alias="projectTitle") customer_name: Optional[str] = Field(None, alias="customerName") customer_contact: Optional[str] = Field(None, alias="customerContact") customer_phone: Optional[str] = Field(None, alias="customerPhone") ``` - [ ] **Step 5: 运行测试** Run: `cd backend && source venv/bin/activate && pytest tests/test_basic_experiment_application.py -v` - [ ] **Step 6: Commit** --- ## Task 4: 售卖计划批量创建(一产品一计划) **Files:** - Modify: `backend/app/crud/pilot_application.py` - Modify: `backend/app/api/pilot_application.py` - [ ] **Step 1: 新增 `create_basic_experiment_sale_schedules`** 在 `pilot_application.py` 中实现(或扩展现有 save infusion API): ```python def create_basic_experiment_sale_schedules( db: Session, application_id: str, product_lines: list, tenant_no: str, user_id: str, dept_id: str, ) -> list: """每个产品行调用一次 create_schedule,cellProducts 仅含一行。""" plan = _get_or_create_infusion_plan(db, application_id, tenant_no, user_id, dept_id) results = [] for idx, line in enumerate(product_lines): schedule_data = { "cellType": line.get("cellTypeName") or "", "specText": line.get("specText") or "", "infusionTime": line.get("pickupTime"), # 提货时间 → infusion_time "amount": line.get("amount"), "sortNo": idx + 1, "cellProducts": [{ "priceId": line.get("priceId"), "projectCode": line.get("projectCode"), "projectName": line.get("projectName"), "cellTypeName": line.get("cellTypeName"), "specText": line.get("specText"), "agreementPrice": line.get("agreementPrice"), "actualPrice": line.get("actualPrice"), "quantity": line.get("quantity") or 1, }], } results.append( create_schedule(db, plan.id, schedule_data, tenant_no, user_id, dept_id) ) return results ``` - [ ] **Step 2: 扩展 `create_schedule` 缴费自动创建** 将条件从仅 `clinical_application` 扩展为: ```python if application and application.biz_type in ("clinical_application", "basic_experiment") and data.get("amount"): auto_create_payment(..., biz_type=application.biz_type) ``` - [ ] **Step 3: API 端点** 新增或扩展 `POST /pilot-application/{id}/sale-schedules`(body: `productLines: [...]`),仅 `biz_type=basic_experiment` 可用。 - [ ] **Step 4: 测试 — 2 行产品生成 2 条 schedule + 2 组工单** ```python def test_basic_experiment_two_products_create_two_schedules(db_session, ...): # 创建申请单后提交 2 行产品 # 断言 schedule 数量 == 2 # 断言 production_work_order / qc_work_order 各 2 条 # 断言 biz_pilot_payment 2 条 ``` - [ ] **Step 5: Commit** --- ## Task 5: 缴费模块扩展 **Files:** - Modify: `backend/app/crud/clinical_payment.py` - Modify: `backend/app/api/clinical_payment.py` - [ ] **Step 1: `auto_create_payment` 增加 `biz_type` 参数** 默认 `clinical_application`;基础实验传 `basic_experiment`。 - [ ] **Step 2: 列表查询支持 `bizType` 过滤** 所有 `list_*` 函数增加 `biz_type: Optional[str]`,过滤 `BizPilotPayment.biz_type`。 - [ ] **Step 3: 列表 `_row_to_dict` 关联申请单输出 `projectTitle/customerName`** - [ ] **Step 4: Commit** --- ## Task 6: 出库链路扩展 **Files:** - Modify: `backend/app/crud/pilot_outbound_status_sync.py` - Modify: `backend/app/crud/outbound_release_review.py` - Modify: `backend/app/crud/qc_work_order.py` - Modify: `backend/app/crud/production_work_order.py` - Modify: `backend/app/crud/pilot_infusion_schedule_mgmt.py` - [ ] **Step 1: 元组/字典加入 `basic_experiment`** ```python _OUTBOUND_BIZ_TYPES = ("pilot_service", "clinical_application", "basic_experiment") _BIZ_TYPE_LABELS = {..., "basic_experiment": "基础实验"} ``` - [ ] **Step 2: 出库详情接口 — `patient` 对 `basic_experiment` 返回 null** 详情 `application` 含 `projectTitle/customerName/customerContact/customerPhone`。 - [ ] **Step 3: `pilot_infusion_schedule_mgmt` 完成逻辑 — 跳过渠道扣费** 与 `clinical_application` 相同:`biz_type == basic_experiment` 时不扣服务订单。 - [ ] **Step 4: Commit** --- ## Task 7: 菜单与路由 **Files:** - Modify: `web-admin/src/router/modules/sqxp.ts` - Modify: `backend/app/api/menu.py` - Create: `docs/sql/migrations/2026-06-10-basic-experiment-menu.sql`(可选) - [ ] **Step 1: `sqxp.ts` 新增一级菜单「基础实验」** ```ts { id: 'BEX00000000000000000000000000001', name: 'BasicExperiment', path: '/basic-experiment', component: '/index/index', meta: { title: '基础实验', icon: 'ri:flask-line', keepAlive: false, roles: ['admin'] }, children: [ { id: 'BEX00000000000000000000000000002', name: 'BasicExperimentApplication', path: 'application', component: '/pilot-service/pilot-application/index', meta: { title: '基础实验申请单', keepAlive: true, menuCode: 'basic-experiment:application:list' }, props: { bizType: 'basic_experiment' } }, { id: 'BEX00000000000000000000000000003', name: 'BasicExperimentPaymentConfirm', path: 'payment-confirm', component: '/pilot-service/clinical-payment-confirm/index', meta: { title: '缴退费财务确认', keepAlive: true, menuCode: 'basic-experiment:payment-confirm:list' }, props: { bizType: 'basic_experiment' } }, { id: 'BEX00000000000000000000000000004', name: 'BasicExperimentOutboundRelease', path: 'outbound-release-review', component: '/pilot-service/outbound-release-review/index', meta: { title: '出库放行审核', keepAlive: true, menuCode: 'basic-experiment:outbound-release-review:list' }, props: { bizType: 'basic_experiment' } } ] } ``` - [ ] **Step 2: 同步 `backend/app/api/menu.py`** - [ ] **Step 3: Commit** --- ## Task 8: 前端 `pilot-biz-type.ts` 策略配置 **Files:** - Create: `web-admin/src/config/pilot-biz-type.ts` - [ ] **Step 1: 定义配置表** ```ts export type PilotBizType = 'pilot_service' | 'clinical_application' | 'basic_experiment' export const PILOT_BIZ_TYPE_CONFIG: Record = { pilot_service: { /* 现有默认 */ showStandardAttachments: false, ... }, clinical_application: { showStandardAttachments: false, ... }, basic_experiment: { label: '基础实验', applicationNoPrefix: 'JCSY', showChannel: false, showPatient: false, showConsent: false, showOtherAttachments: false, showStandardAttachments: true, showCheckup: false, showInfusionConfirm: false, showDelivery: true, showPayment: true, planLabel: '售卖方案', scheduleLabel: '售卖计划', pickupTimeLabel: '提货时间', } } export function getPilotBizConfig(bizType?: string) { return PILOT_BIZ_TYPE_CONFIG[(bizType as PilotBizType) || 'pilot_service'] } ``` - [ ] **Step 2: `status-tag-registry.ts` 增加 `basic_experiment`** - [ ] **Step 3: Commit** --- ## Task 9: 申请单表单与列表 **Files:** - Modify: `web-admin/src/views/pilot-service/pilot-application/index.vue` - Modify: `web-admin/src/views/pilot-service/pilot-application/PilotApplicationTab.vue` - Modify: `web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue` - Modify: `web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue` - [ ] **Step 1: `index.vue` 用 `getPilotBizConfig` 控制 Tab 卡片** 基础实验:申请单 | 售卖计划 | 缴费信息(无体检)。 - [ ] **Step 2: `ApplicationFormDialog.vue` — 基础实验区块** - 显示:课题名称、客户名称/联系人/联系方式、标准附件(单上传区,图片/PDF,卡片布局) - 隐藏:渠道、服务订单、知情同意书、其他附件、患者 - 售卖产品子表:细胞产品(`biz_cell_product_price`)、参考价、实际价(可改)、份数、提货时间、金额 - 保存:先 create/update application,再调 `sale-schedules` API 批量创建计划 - [ ] **Step 3: `PilotApplicationTab` 列表列** 基础实验列表展示:申请单号、课题名称、客户名称、状态(隐藏渠道列)。 - [ ] **Step 4: `ApplicationViewDialog` 只读态对齐** - [ ] **Step 5: 手动验证** 1. 打开 `/#/basic-experiment/application` 2. 新增申请单,添加 2 行产品不同提货时间 3. 保存后在售卖计划 Tab 看到 2 条计划 - [ ] **Step 6: Commit** --- ## Task 10: 售卖计划 Tab + 缴费 **Files:** - Modify: `web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue` - Modify: `web-admin/src/views/pilot-service/pilot-application/ClinicalPaymentTab.vue` - Modify: `web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue` - [ ] **Step 1: `InfusionScheduleTab` 文案与列** - `scheduleLabel` / `pickupTimeLabel` 从 config 读取 - 隐藏输注地点/方式/输注状态(`basic_experiment`) - 展示:细胞产品、金额、提货时间、缴费状态 - [ ] **Step 2: `ClinicalPaymentTab` + 缴退费页** - 请求带 `bizType: 'basic_experiment'` - 列:课题名称、客户名称(替代患者/渠道) - [ ] **Step 3: Commit** --- ## Task 11: 出库查看弹窗 + 放行审核 **Files:** - Modify: `web-admin/src/utils/outbound-qc-pilot-view-sections.ts` - Modify: `web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue` - Modify: `web-admin/src/views/production/production-work-order/index.vue` - Modify: `web-admin/src/views/pilot-service/outbound-release-review/index.vue` - [ ] **Step 1: 扩展 `OutboundViewContextVariant`** ```ts basic_experiment: { application: '基础实验申请信息', schedule: '售卖计划信息' } ``` 新增 section builder:申请信息含课题/客户;无患者;计划含提货时间/金额;产品明细表。 - [ ] **Step 2: QC/生产查看传入 `variant='basic_experiment'`** 当 `detail.application.bizType === 'basic_experiment'`。 - [ ] **Step 3: 放行审核列表 `bizType` 过滤 + 查看弹窗** - [ ] **Step 4: Commit** --- ## Task 12: 交付确认 **Files:** - Modify: `web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue` - [ ] **Step 1: `getPilotBizConfig(bizType).showDelivery` 控制交付 Tab** - [ ] **Step 2: 隐藏输注确认 Tab(`showInfusionConfirm: false`)** - [ ] **Step 3: 按 schedule 粒度操作交付(复用现有 `pilot_delivery` API)** - [ ] **Step 4: Commit** --- ## Task 13: 字段一致性检查与收尾 **Files:** - Run: `backend/scripts/check_field_consistency.py` - Run: `./scripts/update-graph.sh frontend` + `backend` - [ ] **Step 1: 运行字段一致性脚本并修复遗漏** Run: `cd backend && python scripts/check_field_consistency.py` - [ ] **Step 2: 更新知识图谱** Run: `./scripts/update-graph.sh all` - [ ] **Step 3: 验收清单(对照 design spec §8)** | # | 验收项 | 通过 | |---|--------|------| | 1 | 侧栏「基础实验」三菜单可见 | | | 2 | JCSY 申请单 + 课题/客户/标准附件 | | | 3 | 2 行产品 → 2 条售卖计划 | | | 4 | 每计划自动工单 + 待缴费 | | | 5 | QC/生产查看弹窗无患者、有基础实验信息 | | | 6 | 放行/交接/交付可用 | | | 7 | 缴退费流程可用且数据隔离 | | | 8 | 中试/临床列表无 JCSY | | - [ ] **Step 4: Commit** ```bash git commit -m "feat: complete basic experiment business line" ``` --- ## Spec Coverage Self-Review | Spec 章节 | 对应 Task | |-----------|-----------| | §2 业务流程 / 一产品一计划 | Task 4 | | §3 数据库 | Task 1–2 | | §4.1 申请单 API | Task 3–4 | | §4.2 缴费 | Task 5 | | §4.3–4.6 出库/交付 | Task 6, 11–12 | | §5 前端菜单/表单/查看 | Task 7–12 | | §6 权限 menuCode | Task 7 | | §8 验收标准 | Task 13 | 无 TBD/占位;类型命名统一 camelCase API / snake_case DB。 --- ## Execution Handoff Plan complete and saved to `docs/superpowers/plans/2026-06-10-basic-experiment.md`. Two execution options: **1. Subagent-Driven (recommended)** — 每个 Task 派发独立 subagent,任务间做代码审查,迭代快 **2. Inline Execution** — 本会话按 Task 顺序直接实现,每 2–3 个 Task 设检查点 Which approach?