Files
zkxw/docs/superpowers/plans/2026-05-24-pilot-application-phase1.md
T
2026-05-25 15:14:06 +08:00

586 lines
25 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.
# 中试服务申请单 Phase 1 实施计划
> **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:** 实现中试服务申请单的完整 CRUD、患者信息管理、输注方案/计划管理,含后端 API + 前端列表页+详情Tab页。
**Architecture:** 独立后端模块方案(方案A)。新建 `crud/pilot_application.py` 和 `api/pilot_application.py`,在 `models.py` 中新增 4 个 Model。前端新增列表页+详情页,路由追加到 `sqxp.ts`。
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2 / Vue 3 / TypeScript / Element Plus / Tailwind CSS 4
**Spec:** `docs/superpowers/specs/2026-05-24-pilot-application-phase1-design.md`
---
## Task 1: SQL 迁移文件 + Backend Models
**Files:**
- Create: `docs/sql/pilot-application-migration.sql`
- Modify: `backend/app/models/models.py`
- [ ] **Step 1: 创建 SQL 迁移文件**
创建 `docs/sql/pilot-application-migration.sql`,内容为 ALTER `biz_pilot_application` 增加 4 个字段 + 唯一索引:
```sql
-- 中试服务申请单 Phase 1 迁移
-- 为 biz_pilot_application 增加申请单号、状态、渠道缓存字段
ALTER TABLE biz_pilot_application
ADD COLUMN application_no VARCHAR(64) DEFAULT NULL
COMMENT '申请单号(自动生成,前缀ZSSQ)' AFTER id,
ADD COLUMN application_status VARCHAR(16) NOT NULL DEFAULT 'dks'
COMMENT 'dks待开始 jxz进行中 ywc已完成 yzf已作废' AFTER application_no,
ADD COLUMN channel_id VARCHAR(64) DEFAULT NULL
COMMENT '渠道ID(缓存自服务订单)' AFTER channel_service_order_id,
ADD COLUMN channel_name VARCHAR(200) DEFAULT NULL
COMMENT '渠道名称(缓存)' AFTER channel_id;
ALTER TABLE biz_pilot_application
ADD UNIQUE KEY uk_application_no (application_no, del_flag);
```
- [ ] **Step 2: 在 models.py 末尾(BizChannelServiceOrderTxn 之后)新增 4 个 Model**
遵循项目已有 Model 模式(参考 `BizChannelServiceOrder` 在 L1761 的写法):
- 字段顺序:id → 业务字段 → public 字段(tenant_no / order_num / remark / create_by / create_dept_id / create_time / update_by / update_time / del_flag)
- 类型映射:VARCHAR→String, INT→Integer, TINYINT→SmallInteger, DATETIME→DateTime, DECIMAL→Numeric
- 添加 `__tablename__` 和中文注释
新增的 4 个 Model:
**BizPilotApplication** → `biz_pilot_application`
- id, application_no, application_status(default='dks'), channel_service_order_id, channel_id, channel_name
- + public fields
**BizPilotPatient** → `biz_pilot_patient`
- id, pilot_application_id, customer_id, patient_name, sex, age, id_card_no, phone_no, emergency_contact, emergency_phone, clinical_diagnosis, diagnosis_attachments, need_checkup(default=1)
- + public fields
**BizPilotInfusionPlan** → `biz_pilot_infusion_plan`
- id, pilot_application_id, cell_type, spec_text, total_course_count, course_interval, infusion_route, infusion_start_date
- + public fields
**BizPilotInfusionSchedule** → `biz_pilot_infusion_schedule`
- id, infusion_plan_id, cell_type, spec_text, infusion_time, schedule_status(default=''), attachments
- + public fields
- [ ] **Step 3: 验证**
Run: `cd backend && source venv/bin/activate && python -c "from app.models.models import BizPilotApplication, BizPilotPatient, BizPilotInfusionPlan, BizPilotInfusionSchedule; print('Models imported OK')"`
Expected: `Models imported OK`
- [ ] **Step 4: 运行 LSP 诊断**
Run `lsp_diagnostics` on `backend/app/models/models.py` — expect 0 errors.
---
## Task 2: Backend CRUD
**Files:**
- Create: `backend/app/crud/pilot_application.py`
- [ ] **Step 1: 创建 CRUD 文件**
创建 `backend/app/crud/pilot_application.py`,遵循 `crud/pilot_service.py` 的模式(`_row_to_dict_*` + 分页列表 + CRUD 函数)。
**import 部分:**
```python
import uuid
import json
from datetime import datetime, date
from typing import Dict, List, Optional, Any
from sqlalchemy.orm import Session
from sqlalchemy import and_
from app.models.models import (
BizPilotApplication, BizPilotPatient, BizPilotInfusionPlan, BizPilotInfusionSchedule,
BizChannelServiceOrder, BizChannelServiceOrderProduct, BizCustomer
)
from app.core.database import get_db
```
**_row_to_dict 函数(4 个):**
1. `_row_to_dict_application(row, db=None, tenant_no=None)`:
- 基础字段:id, applicationNo, applicationStatus, channelServiceOrderId, channelId, channelName, remark, createTime, updateTime
- 扩展字段(需 db 查询):
- patientName: 从 BizPilotPatient 查询(WHERE pilot_application_id=row.id AND del_flag=0)
- infusionPlanCount: 从 BizPilotInfusionPlan COUNT(WHERE pilot_application_id=row.id AND del_flag=0)
- serviceOrderNo: 从 BizChannelServiceOrder 查询(WHERE id=row.channel_service_order_id)取其关联的订单信息
- orderType: 同上,服务订单的 order_type
2. `_row_to_dict_patient(row)`:
- id, pilotApplicationId, customerId, patientName, sex, age, idCardNo, phoneNo, emergencyContact, emergencyPhone, clinicalDiagnosis, diagnosisAttachments, needCheckup, remark, createTime, updateTime
3. `_row_to_dict_infusion_plan(row, db=None, tenant_no=None)`:
- id, pilotApplicationId, cellType, specText, totalCourseCount, courseInterval, infusionRoute, infusionStartDate, remark, createTime, updateTime
- 扩展:scheduleCount (COUNT schedules), completedCount (COUNT WHERE schedule_status='ywc')
4. `_row_to_dict_infusion_schedule(row)`:
- id, infusionPlanId, cellType, specText, infusionTime, scheduleStatus, attachments, remark, createTime, updateTime
**CRUD 函数(16 个):**
**Application(8 个):**
- `list_applications(db, tenant_no, page, page_size, channel_id=None, application_status=None, channel_service_order_id=None)` — 分页列表,按 create_time DESC 排序
- `get_application(db, app_id, tenant_no)` — 获取详情
- `create_application(db, data, tenant_no, user_id, dept_id)` — 校验服务订单存在且 status='sxz',从服务订单缓存 channel_id/channel_name,生成 application_no(ZSSQ+YYYYMMDD+4位序列)
- `update_application(db, app_id, data, tenant_no)` — 仅 dks 状态可编辑
- `soft_delete_application(db, app_id, tenant_no)` — 仅 dks 状态
- `start_application(db, app_id, tenant_no, user_id)` — dks→jxz
- `complete_application(db, app_id, tenant_no, user_id)` — jxz→ywc
- `void_application(db, app_id, tenant_no, user_id)` — dks或jxz→yzf
**Patient(2 个):**
- `get_patient_by_application(db, app_id, tenant_no)`
- `save_patient(db, app_id, data, tenant_no, user_id, dept_id)` — 如果有 customer_id 则更新 biz_customer;如果无则新建 biz_customer 并回填 customer_id。然后用 upsert 逻辑保存 biz_pilot_patient(先查再更新或新建)。
**InfusionPlan(4 个):**
- `list_infusion_plans(db, app_id, tenant_no)` — 按 order_num / create_time ASC
- `create_infusion_plan(db, app_id, data, tenant_no, user_id, dept_id)`
- `update_infusion_plan(db, plan_id, data, tenant_no)`
- `soft_delete_infusion_plan(db, plan_id, tenant_no)` — 级联软删除其下所有 schedule
**InfusionSchedule(6 个):**
- `list_schedules_by_plan(db, plan_id, tenant_no)` — 按 order_num ASC
- `auto_generate_schedules(db, plan_id, tenant_no, user_id, dept_id)` — 读取 plan.total_course_count,批量插入 N 条 schedule
- `create_schedule(db, plan_id, data, tenant_no, user_id, dept_id)` — 手动添加单条
- `update_schedule(db, schedule_id, data, tenant_no)`
- `soft_delete_schedule(db, schedule_id, tenant_no)`
- `mark_schedule_complete(db, schedule_id, tenant_no, user_id)` — schedule_status='ywc'
**application_no 生成逻辑:**
```python
def _generate_application_no(db, tenant_no):
today_str = datetime.now().strftime('%Y%m%d')
prefix = f'ZSSQ{today_str}'
# 查今天已有的最大序号
existing = db.query(BizPilotApplication.application_no).filter(
BizPilotApplication.tenant_no == tenant_no,
BizPilotApplication.application_no.like(f'{prefix}%'),
BizPilotApplication.del_flag == 0
).order_by(BizPilotApplication.application_no.desc()).first()
seq = 1
if existing and existing[0]:
seq = int(existing[0][-4:]) + 1
return f'{prefix}{seq:04d}'
```
- [ ] **Step 2: 验证导入**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud.pilot_application import list_applications; print('CRUD imported OK')"`
Expected: `CRUD imported OK`
- [ ] **Step 3: 运行 LSP 诊断**
Run `lsp_diagnostics` on `backend/app/crud/pilot_application.py` — expect 0 errors.
---
## Task 3: Backend API Routes + Router Registration
**Files:**
- Create: `backend/app/api/pilot_application.py`
- Modify: `backend/app/api/__init__.py`
- Modify: `backend/app/main.py`
- [ ] **Step 1: 创建 API 路由文件**
创建 `backend/app/api/pilot_application.py`,遵循 `api/pilot_service.py` 的模式(Pydantic schemas 内联 + 路由函数)。
**import 部分:**
```python
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import Optional, List
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.crud import pilot_application
```
**Pydantic Schemas(6 个):**
```python
class ApplicationCreateBody(BaseModel):
channel_service_order_id: str
remark: Optional[str] = None
class ApplicationUpdateBody(BaseModel):
remark: Optional[str] = None
class PatientSaveBody(BaseModel):
customer_id: Optional[str] = None
patient_name: str
sex: Optional[str] = None
age: Optional[int] = None
id_card_no: Optional[str] = None
phone_no: Optional[str] = None
emergency_contact: Optional[str] = None
emergency_phone: Optional[str] = None
clinical_diagnosis: Optional[str] = None
diagnosis_attachments: Optional[str] = None
need_checkup: int = 1
class InfusionPlanCreateBody(BaseModel):
pilot_application_id: str
cell_type: str
spec_text: Optional[str] = None
total_course_count: int
course_interval: Optional[str] = None
infusion_route: Optional[str] = None
infusion_start_date: Optional[str] = None
class InfusionPlanUpdateBody(BaseModel):
cell_type: Optional[str] = None
spec_text: Optional[str] = None
total_course_count: Optional[int] = None
course_interval: Optional[str] = None
infusion_route: Optional[str] = None
infusion_start_date: Optional[str] = None
class InfusionScheduleUpdateBody(BaseModel):
infusion_time: Optional[str] = None
remark: Optional[str] = None
attachments: Optional[str] = None
```
**Router:**
```python
router = APIRouter(prefix="/web/admin", tags=["中试服务申请单管理"])
```
**API 路由(19 个):**
| 方法 | 路径 | 函数名 | 依赖 | 说明 |
|------|------|--------|------|------|
| GET | `/pilot-application/list` | api_list_applications | require_admin | 分页列表,query params: page/page_size/channel_id/application_status/channel_service_order_id |
| GET | `/pilot-application/detail` | api_get_application | require_admin | query param: id |
| POST | `/pilot-application/create` | api_create_application | require_admin | body: ApplicationCreateBody |
| PUT | `/pilot-application/update` | api_update_application | require_admin | body: ApplicationUpdateBody + id |
| DELETE | `/pilot-application/delete` | api_delete_application | require_admin | body: {id} |
| POST | `/pilot-application/start` | api_start_application | require_admin | body: {id} |
| POST | `/pilot-application/complete` | api_complete_application | require_admin | body: {id} |
| POST | `/pilot-application/void` | api_void_application | require_admin | body: {id} |
| GET | `/pilot-patient/detail` | api_get_patient | require_admin | query param: application_id |
| POST | `/pilot-patient/save` | api_save_patient | require_admin | body: PatientSaveBody + application_id |
| GET | `/pilot-infusion-plan/list` | api_list_plans | require_admin | query param: application_id |
| POST | `/pilot-infusion-plan/create` | api_create_plan | require_admin | body: InfusionPlanCreateBody |
| PUT | `/pilot-infusion-plan/update` | api_update_plan | require_admin | body: InfusionPlanUpdateBody + id |
| DELETE | `/pilot-infusion-plan/delete` | api_delete_plan | require_admin | body: {id} |
| POST | `/pilot-infusion-schedule/auto-generate` | api_auto_generate | require_admin | body: {plan_id} |
| POST | `/pilot-infusion-schedule/create` | api_create_schedule | require_admin | body: {plan_id, infusion_time?, remark?} |
| PUT | `/pilot-infusion-schedule/update` | api_update_schedule | require_admin | body: InfusionScheduleUpdateBody + id |
| DELETE | `/pilot-infusion-schedule/delete` | api_delete_schedule | require_admin | body: {id} |
| POST | `/pilot-infusion-schedule/mark-complete` | api_mark_complete | require_admin | body: {id} |
每个路由函数结构:
```python
@router.get("/pilot-application/list")
def api_list_applications(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
channel_id: Optional[str] = Query(None),
application_status: Optional[str] = Query(None),
channel_service_order_id: Optional[str] = Query(None),
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _user["tenant_no"]
result = pilot_application.list_applications(db, tenant_no, page, page_size, channel_id, application_status, channel_service_order_id)
return {"code": 0, "data": result}
```
- [ ] **Step 2: 注册路由**
在 `backend/app/api/__init__.py` 中追加:
```python
from app.api.pilot_application import router as pilot_application_router
```
并在 `routers` 列表中追加 `pilot_application_router`。
在 `backend/app/main.py` 中追加路由 include(参考已有的 `pilot_service_router` 注册方式)。
- [ ] **Step 3: 验证**
Run: `cd backend && source venv/bin/activate && python -c "from app.api.pilot_application import router; print(f'Routes: {len(router.routes)}')"`
Expected: `Routes: 19`
- [ ] **Step 4: 运行 LSP 诊断**
Run `lsp_diagnostics` on `backend/app/api/pilot_application.py`, `backend/app/api/__init__.py`, `backend/app/main.py` — expect 0 errors on all.
---
## Task 4: Frontend API 函数 + 状态标签 + 路由
**Files:**
- Modify: `web-admin/src/api/admin.ts`
- Modify: `web-admin/src/utils/status-tag-registry.ts`
- Modify: `web-admin/src/router/modules/sqxp.ts`
- [ ] **Step 1: 在 admin.ts 末尾追加 19 个 API 函数**
遵循文件中已有函数模式(参考 `fetchPilotApplicationList` 风格),追加以下函数:
```typescript
// ==================== 中试服务申请单 ====================
export function fetchPilotApplicationList(params: any) {
return request({ url: '/pilot-application/list', method: 'get', params })
}
export function fetchPilotApplicationDetail(params: any) {
return request({ url: '/pilot-application/detail', method: 'get', params })
}
export function createPilotApplication(data: any) {
return request({ url: '/pilot-application/create', method: 'post', data })
}
export function updatePilotApplication(data: any) {
return request({ url: '/pilot-application/update', method: 'put', data })
}
export function deletePilotApplication(data: any) {
return request({ url: '/pilot-application/delete', method: 'delete', data })
}
export function startPilotApplication(data: any) {
return request({ url: '/pilot-application/start', method: 'post', data })
}
export function completePilotApplication(data: any) {
return request({ url: '/pilot-application/complete', method: 'post', data })
}
export function voidPilotApplication(data: any) {
return request({ url: '/pilot-application/void', method: 'post', data })
}
// 患者信息
export function fetchPilotPatient(params: any) {
return request({ url: '/pilot-patient/detail', method: 'get', params })
}
export function savePilotPatient(data: any) {
return request({ url: '/pilot-patient/save', method: 'post', data })
}
// 输注方案
export function fetchPilotInfusionPlanList(params: any) {
return request({ url: '/pilot-infusion-plan/list', method: 'get', params })
}
export function createPilotInfusionPlan(data: any) {
return request({ url: '/pilot-infusion-plan/create', method: 'post', data })
}
export function updatePilotInfusionPlan(data: any) {
return request({ url: '/pilot-infusion-plan/update', method: 'put', data })
}
export function deletePilotInfusionPlan(data: any) {
return request({ url: '/pilot-infusion-plan/delete', method: 'delete', data })
}
// 输注计划
export function fetchPilotInfusionScheduleList(params: any) {
return request({ url: '/pilot-infusion-schedule/list', method: 'get', params })
}
export function autoGenerateSchedules(data: any) {
return request({ url: '/pilot-infusion-schedule/auto-generate', method: 'post', data })
}
export function updatePilotInfusionSchedule(data: any) {
return request({ url: '/pilot-infusion-schedule/update', method: 'put', data })
}
export function deletePilotInfusionSchedule(data: any) {
return request({ url: '/pilot-infusion-schedule/delete', method: 'delete', data })
}
export function markScheduleComplete(data: any) {
return request({ url: '/pilot-infusion-schedule/mark-complete', method: 'post', data })
}
```
- [ ] **Step 2: 在 status-tag-registry.ts 追加申请单状态标签函数**
```typescript
/** 中试申请单状态 */
export function applicationStatusLabel(status: string): string {
const map: Record<string, string> = { dks: '待开始', jxz: '进行中', ywc: '已完成', yzf: '已作废' }
return map[status] || status || '-'
}
export function applicationStatusTagType(status: string): '' | 'success' | 'warning' | 'danger' | 'info' {
const map: Record<string, '' | 'success' | 'warning' | 'danger' | 'info'> = {
dks: 'info', jxz: '', ywc: 'success', yzf: 'danger'
}
return map[status] || 'info'
}
```
- [ ] **Step 3: 在 sqxp.ts 追加 2 个路由**
在 PilotService 的 children 数组末尾追加:
```typescript
{
path: 'pilot-application',
name: 'PilotApplication',
component: () => import('@views/pilot-service/pilot-application/index.vue'),
meta: { title: '中试服务申请单', icon: 'i-ep:document' }
},
{
path: 'pilot-application/detail',
name: 'PilotApplicationDetail',
component: () => import('@views/pilot-service/pilot-application/ApplicationDetail.vue'),
meta: { title: '申请单详情', hidden: true }
},
```
注意:路由 ID 需要根据现有 PilotService 的 ID 段确定,查看 sqxp.ts 中已有的路由 ID 模式。中试服务模块 ID 段为 8000...,检查当前已使用的最大 ID 后递增。
- [ ] **Step 4: 运行 LSP 诊断**
Run `lsp_diagnostics` on all 3 modified files — expect 0 errors.
---
## Task 5: 前端列表页
**Files:**
- Create: `web-admin/src/views/pilot-service/pilot-application/index.vue`
- [ ] **Step 1: 创建列表页**
创建目录 `web-admin/src/views/pilot-service/pilot-application/` 和 `index.vue`。
**参考模板页:** `src/views/system/order/index.vue`(复杂列表页)
**页面结构:**
- 筛选区:申请单号(el-input)、渠道(el-select,数据源 fetchChannelList)、服务订单(el-select,联动渠道)、状态(el-radio-group 或 tag 按钮)
- 操作栏:新增按钮(el-button type="primary")
- 表格(el-table):列如 spec 中定义
- 分页(el-pagination)
**关键交互:**
- 新增:弹窗(el-dialog),选择渠道 → 联动加载该渠道的服务订单(调用已有的 fetchServiceOrderList,传入 channel_id 筛选)→ 选择服务订单 → 填写备注 → 提交 → 成功后 router.push 到详情页
- 状态操作:根据 applicationStatus 动态显示按钮
- dks:显示"开始"(primary) + "作废"(danger)
- jxz:显示"完成"(primary) + "作废"(danger)
- ywc/yzf:无按钮
- 编辑/删除:仅 dks 状态可操作
- 行点击:router.push({ name: 'PilotApplicationDetail', query: { id: row.id } })
- 状态列用 el-tag + applicationStatusLabel/applicationStatusTagType
**数据加载:** onMounted 调用 fetchPilotApplicationList,筛选变更时重新加载。
**样式:** 遵循项目 Fresh Mint 色系(--primary #10b981),使用已有的 SCSS 变量和 mixin。
- [ ] **Step 2: 运行 LSP 诊断**
Run `lsp_diagnostics` on `web-admin/src/views/pilot-service/pilot-application/index.vue` — expect 0 errors.
---
## Task 6: 前端详情页 — 基本信息 Tab + 输注方案 Tab
**Files:**
- Create: `web-admin/src/views/pilot-service/pilot-application/ApplicationDetail.vue`
- [ ] **Step 1: 创建详情页**
**参考模板页:** `src/views/system/product/index.vue`(含详情展示的复杂页面)
**页面结构:**
- 顶部:返回按钮 + 申请单号 + 状态标签 + 操作按钮(开始/完成/作废,根据状态显示)
- el-tabs:4 个 tab(基本信息 / 输注方案 / 体检信息-locked / 交付信息-locked)
**基本信息 Tab:**
- 上半区:el-descriptions 展示申请单信息(只读)
- 申请单号、渠道名称、关联服务订单、状态、创建时间、备注
- 下半区:患者信息面板(el-card)
- "选择客户"按钮 → el-dialog 弹窗展示客户列表(调用已有 API 获取 biz_customer 列表,支持姓名/手机搜索)
- 选中后自动填充表单字段
- 表单字段:patientName, sex, age, idCardNo, phoneNo, emergencyContact, emergencyPhone, clinicalDiagnosis, diagnosisAttachments(附件上传), needCheckup(el-switch)
- "保存患者信息"按钮
**输注方案 Tab:**
- 工具栏:"新增输注方案"按钮
- 方案列表用 el-table,每行显示方案的完整信息
- 最后一列加 expand 展开行,展开后显示该方案的输注计划列表
- 展开行内的计划列表(el-table):
- 列:序号、细胞类型、规格、输注时间(可编辑)、状态、操作
- 操作:编辑(弹窗修改时间/备注)、删除、标记完成
- "手动添加计划"按钮在展开行内
**新增/编辑输注方案弹窗(el-dialog):**
- 表单字段:cell_type(el-select,数据源从细胞产品类型 API)、spec_text、total_course_count(el-input-number)、course_interval、infusion_route、infusion_start_date(el-date-picker)
- label-position="top",遵循 .form-grid-2 两列布局
- 保存成功后 ElMessageBox.confirm("是否立即生成 N 条输注计划?"),确认则调用 autoGenerateSchedules
**数据加载:** onMounted 根据 route.query.id 调用 fetchPilotApplicationDetail 获取详情(含患者+方案),同时 fetchPilotInfusionPlanList 获取方案列表,每个方案再 fetchPilotInfusionScheduleList 获取计划。
**样式约定:**
- Dialog forms: `label-position="top"`, `.dialog-form-layout`, `.form-grid-2`
- 无 backdrop blur: `backdrop-filter: none`
- Panel heading: `.title-bar-heading` + `::before` 竖线
- 状态 Tag: el-tag + semantic colors
- [ ] **Step 2: 运行 LSP 诊断**
Run `lsp_diagnostics` on `web-admin/src/views/pilot-service/pilot-application/ApplicationDetail.vue` — expect 0 errors.
---
## Task 7: 集成验证
**Files:** 无新文件
- [ ] **Step 1: 后端启动验证**
Run: `cd backend && source venv/bin/activate && python -c "
from app.main import app
routes = [r.path for r in app.routes if hasattr(r, 'path')]
pilot_routes = [r for r in routes if 'pilot-application' in r or 'pilot-patient' in r or 'pilot-infusion' in r]
print(f'Pilot application routes: {len(pilot_routes)}')
for r in sorted(pilot_routes): print(f' {r}')
"`
Expected: 19 routes listed
- [ ] **Step 2: 前端构建验证**
Run: `cd web-admin && pnpm build`
Expected: Build succeeds with exit code 0
- [ ] **Step 3: 前端 LSP 全量诊断**
Run `lsp_diagnostics` on directory `web-admin/src/views/pilot-service/pilot-application/` — expect 0 errors.
- [ ] **Step 4: 更新知识图谱**
Run: `./scripts/update-graph.sh backend frontend`
Expected: 知识图谱更新完成
---
## 自审检查
### Spec Coverage
| Spec 需求 | 对应 Task |
|-----------|----------|
| ALTER biz_pilot_application | Task 1 |
| 4 个 Model 类 | Task 1 |
| Application CRUD 8 个函数 | Task 2 |
| Patient save/get 2 个函数 | Task 2 |
| InfusionPlan CRUD 4 个函数 | Task 2 |
| InfusionSchedule 6 个函数 | Task 2 |
| API routes 19 个 | Task 3 |
| Router registration | Task 3 |
| 19 个前端 API 函数 | Task 4 |
| 状态标签函数 | Task 4 |
| 路由追加 | Task 4 |
| 列表页 | Task 5 |
| 详情页(基本信息+输注方案) | Task 6 |
| 集成验证 | Task 7 |
### Placeholder 扫描
无 TBD/TODO/占位符。所有步骤含具体代码或明确指令。
### Type 一致性
- Model 字段名与 CRUD _row_to_dict 输出的 camelCase key 一致
- API 路径与前端 request URL 一致
- Pydantic schema 字段与 CRUD 函数参数一致