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
Files:
docs/sql/pilot-application-migration.sqlModify: backend/app/models/models.py
[ ] Step 1: 创建 SQL 迁移文件
创建 docs/sql/pilot-application-migration.sql,内容为 ALTER biz_pilot_application 增加 4 个字段 + 唯一索引:
-- 中试服务申请单 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);
遵循项目已有 Model 模式(参考 BizChannelServiceOrder 在 L1761 的写法):
__tablename__ 和中文注释新增的 4 个 Model:
BizPilotApplication → biz_pilot_application
BizPilotPatient → biz_pilot_patient
BizPilotInfusionPlan → biz_pilot_infusion_plan
BizPilotInfusionSchedule → biz_pilot_infusion_schedule
[ ] 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
Run lsp_diagnostics on backend/app/models/models.py — expect 0 errors.
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 部分:
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 个):
_row_to_dict_application(row, db=None, tenant_no=None):
_row_to_dict_patient(row):
_row_to_dict_infusion_plan(row, db=None, tenant_no=None):
_row_to_dict_infusion_schedule(row):
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→jxzcomplete_application(db, app_id, tenant_no, user_id) — jxz→ywcvoid_application(db, app_id, tenant_no, user_id) — dks或jxz→yzfPatient(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 ASCcreate_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) — 级联软删除其下所有 scheduleInfusionSchedule(6 个):
list_schedules_by_plan(db, plan_id, tenant_no) — 按 order_num ASCauto_generate_schedules(db, plan_id, tenant_no, user_id, dept_id) — 读取 plan.total_course_count,批量插入 N 条 schedulecreate_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 生成逻辑:
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}'
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
Run lsp_diagnostics on backend/app/crud/pilot_application.py — expect 0 errors.
Files:
backend/app/api/pilot_application.pybackend/app/api/__init__.pyModify: backend/app/main.py
[ ] Step 1: 创建 API 路由文件
创建 backend/app/api/pilot_application.py,遵循 api/pilot_service.py 的模式(Pydantic schemas 内联 + 路由函数)。
import 部分:
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 个):
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:
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} |
每个路由函数结构:
@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}
在 backend/app/api/__init__.py 中追加:
from app.api.pilot_application import router as pilot_application_router
并在 routers 列表中追加 pilot_application_router。
在 backend/app/main.py 中追加路由 include(参考已有的 pilot_service_router 注册方式)。
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
Run lsp_diagnostics on backend/app/api/pilot_application.py, backend/app/api/__init__.py, backend/app/main.py — expect 0 errors on all.
Files:
web-admin/src/api/admin.tsweb-admin/src/utils/status-tag-registry.tsModify: web-admin/src/router/modules/sqxp.ts
[ ] Step 1: 在 admin.ts 末尾追加 19 个 API 函数
遵循文件中已有函数模式(参考 fetchPilotApplicationList 风格),追加以下函数:
// ==================== 中试服务申请单 ====================
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 })
}
/** 中试申请单状态 */
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'
}
在 PilotService 的 children 数组末尾追加:
{
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 后递增。
Run lsp_diagnostics on all 3 modified files — expect 0 errors.
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(复杂列表页)
页面结构:
关键交互:
数据加载: onMounted 调用 fetchPilotApplicationList,筛选变更时重新加载。
样式: 遵循项目 Fresh Mint 色系(--primary #10b981),使用已有的 SCSS 变量和 mixin。
Run lsp_diagnostics on web-admin/src/views/pilot-service/pilot-application/index.vue — expect 0 errors.
Files:
Create: web-admin/src/views/pilot-service/pilot-application/ApplicationDetail.vue
[ ] Step 1: 创建详情页
参考模板页: src/views/system/product/index.vue(含详情展示的复杂页面)
页面结构:
基本信息 Tab:
输注方案 Tab:
新增/编辑输注方案弹窗(el-dialog):
数据加载: onMounted 根据 route.query.id 调用 fetchPilotApplicationDetail 获取详情(含患者+方案),同时 fetchPilotInfusionPlanList 获取方案列表,每个方案再 fetchPilotInfusionScheduleList 获取计划。
样式约定:
label-position="top", .dialog-form-layout, .form-grid-2backdrop-filter: none.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.
Files: 无新文件
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
Run: cd web-admin && pnpm build
Expected: Build succeeds with exit code 0
Run lsp_diagnostics on directory web-admin/src/views/pilot-service/pilot-application/ — expect 0 errors.
Run: ./scripts/update-graph.sh backend frontend
Expected: 知识图谱更新完成
| 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 |
无 TBD/TODO/占位符。所有步骤含具体代码或明确指令。