Files
zkxw/docs/superpowers/plans/2026-06-10-basic-experiment.md
T
2026-06-11 15:42:58 +08:00

606 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 基础实验业务 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<PilotBizType, {
label: string
applicationNoPrefix: string
showChannel: boolean
showPatient: boolean
showConsent: boolean
showOtherAttachments: boolean
showStandardAttachments: boolean
showCheckup: boolean
showInfusionConfirm: boolean
showDelivery: boolean
showPayment: boolean
planLabel: string
scheduleLabel: string
pickupTimeLabel: string
}> = {
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?