44 KiB
临床应用业务 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 区分新增临床应用业务线,包含独立缴费流程(缴费信息/缴费确认/退费确认三个菜单)。
Architecture: 复用中试服务的 biz_pilot_application 表体系,新增 biz_type 字段区分两条线。缴费参照细胞存储 biz_protocol_payment 模式,但简化为单表趸交(一个输注计划一条缴费记录)。前端复用 pilot-service 目录下的页面组件,通过路由 prop 区分行为。
Tech Stack: FastAPI + SQLAlchemy + Pydantic v2(后端),Vue 3 + Element Plus + TypeScript(前端),MySQL 5.7
Design Spec: docs/superpowers/specs/2026-05-27-clinical-application-design.md
File Structure
Backend — New Files
backend/app/models/clinical_payment.py—BizPilotPayment模型backend/app/crud/clinical_payment.py— 缴费 CRUD(参照collection_protocol.py中缴费函数)backend/app/api/clinical_payment.py— 缴费 API 路由(10 个端点)backend/app/schemas/clinical_payment.py— Pydantic schemas
Backend — Modified Files
backend/app/models/models.py—BizPilotApplication加biz_type,BizPilotInfusionSchedule加amountbackend/app/crud/pilot_application.py—create_application/_row_to_dict_application/_generate_application_no/list_applications加biz_type支持backend/app/api/pilot_application.py— 路由参数加biz_typebackend/app/api/__init__.py— 注册clinical_payment_routerbackend/app/main.py— 挂载新路由
Frontend — New Files
web-admin/src/views/pilot-service/clinical-payment-info/index.vue— 缴费信息页面web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue— 缴费确认页面web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue— 退费确认页面
Frontend — Modified Files
web-admin/src/router/modules/sqxp.ts— 新增临床应用管理一级菜单 + 7 个子路由web-admin/src/api/business.ts— 新增 clinical payment API 函数web-admin/src/views/pilot-service/pilot-application/index.vue— 接收bizTypeprop,条件渲染web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue— 临床应用模式隐藏渠道订单web-admin/src/views/pilot-service/infusion-schedule-management/index.vue— 临床应用显示金额列
SQL
docs/sql/clinical-application-migration.sql— ALTER + CREATE TABLE
Task 1: 数据库迁移 SQL
Files:
-
Create:
docs/sql/clinical-application-migration.sql -
Step 1: 编写迁移 SQL
-- ============================================================
-- 临床应用业务 - 数据库迁移
-- ============================================================
-- 1. biz_pilot_application 新增 biz_type 字段
ALTER TABLE biz_pilot_application
ADD COLUMN biz_type VARCHAR(20) NOT NULL DEFAULT 'pilot_service'
COMMENT '业务类型: pilot_service=中试服务, clinical_application=临床应用'
AFTER channel_name;
-- 2. biz_pilot_infusion_schedule 新增 amount 字段
ALTER TABLE biz_pilot_infusion_schedule
ADD COLUMN amount DECIMAL(18,2) DEFAULT NULL
COMMENT '临床应用-手动填写的输注金额(中试为NULL)'
AFTER qc_status;
-- 3. 新建 biz_pilot_payment 缴费记录表
CREATE TABLE biz_pilot_payment (
id VARCHAR(64) NOT NULL COMMENT '主键ID',
pilot_application_id VARCHAR(64) NOT NULL COMMENT '临床申请单ID',
infusion_schedule_id VARCHAR(64) NOT NULL COMMENT '输注计划ID',
biz_type VARCHAR(20) NOT NULL DEFAULT 'clinical_application' COMMENT '业务类型',
amount DECIMAL(18,2) NOT NULL COMMENT '缴费金额',
transfer_type VARCHAR(16) DEFAULT NULL COMMENT '转账方式: wxzz/zzfbzz/yhkzz/xj',
transfer_amount DECIMAL(18,2) DEFAULT NULL COMMENT '实际转账金额',
transfer_time DATETIME DEFAULT NULL COMMENT '转账时间',
transfer_attachments TEXT DEFAULT NULL COMMENT '转账凭证附件',
pay_status VARCHAR(32) NOT NULL DEFAULT 'dfj' COMMENT '缴费状态: dfj/jfqrz/yjf/tfqrz/ytf',
finance_confirm_attachments TEXT DEFAULT NULL COMMENT '财务确认收款附件',
finance_remark VARCHAR(512) DEFAULT NULL COMMENT '财务确认收款备注',
finance_confirm_time DATETIME DEFAULT NULL COMMENT '财务确认收款时间',
refund_amount DECIMAL(18,2) DEFAULT NULL COMMENT '退费金额',
refund_remark VARCHAR(512) DEFAULT NULL COMMENT '退费备注',
refund_attachments TEXT DEFAULT NULL COMMENT '退费附件',
refund_apply_by VARCHAR(64) DEFAULT NULL COMMENT '退费申请人ID',
refund_apply_time DATETIME DEFAULT NULL COMMENT '退费申请时间',
refund_finance_confirm_attachments TEXT DEFAULT NULL COMMENT '退费财务确认附件',
refund_finance_remark VARCHAR(512) DEFAULT NULL COMMENT '退费财务确认备注',
refund_finance_confirm_time DATETIME DEFAULT NULL COMMENT '退费财务确认时间',
tenant_no VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
order_num INT(11) DEFAULT NULL COMMENT '排序号',
remark VARCHAR(512) DEFAULT NULL COMMENT '备注',
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人id',
create_dept_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人id',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
del_flag TINYINT(4) NOT NULL DEFAULT '0' COMMENT '删除标记: 0-正常 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_infusion_schedule (infusion_schedule_id, del_flag),
KEY idx_pilot_app (pilot_application_id, del_flag),
KEY idx_pay_status (pay_status, del_flag),
KEY idx_tenant (tenant_no, del_flag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='临床应用缴费记录';
- Step 2: 在数据库执行迁移
Run: mysql -u root -p zkxw < docs/sql/clinical-application-migration.sql
- Step 3: Commit
git add docs/sql/clinical-application-migration.sql
git commit -m "feat: add clinical application DB migration (biz_type + biz_pilot_payment)"
Task 2: 后端模型层
Files:
-
Modify:
backend/app/models/models.py—BizPilotApplication和BizPilotInfusionSchedule -
Create:
backend/app/models/clinical_payment.py—BizPilotPayment -
Step 1: 在
BizPilotApplication类中添加biz_type字段
在 backend/app/models/models.py 中 BizPilotApplication 类的 channel_name 字段后面添加:
biz_type = Column(String(20), nullable=False, default="pilot_service", comment="业务类型: pilot_service=中试服务, clinical_application=临床应用")
- Step 2: 在
BizPilotInfusionSchedule类中添加amount字段
在 backend/app/models/models.py 中 BizPilotInfusionSchedule 类的 qc_status 字段后面添加:
amount = Column(Numeric(18, 2), nullable=True, comment="临床应用-手动填写的输注金额(中试为NULL)")
- Step 3: 创建
backend/app/models/clinical_payment.py
"""临床应用缴费记录模型"""
from sqlalchemy import Column, String, Text, SmallInteger, Numeric, DateTime
from sqlalchemy.sql import func
from app.models.base import Base
from decimal import Decimal
class BizPilotPayment(Base):
"""临床应用缴费记录 biz_pilot_payment"""
__tablename__ = "biz_pilot_payment"
id = Column(String(64), primary_key=True, comment="主键ID")
pilot_application_id = Column(String(64), nullable=False, comment="临床申请单ID")
infusion_schedule_id = Column(String(64), nullable=False, comment="输注计划ID")
biz_type = Column(String(20), nullable=False, default="clinical_application", comment="业务类型")
amount = Column(Numeric(18, 2), nullable=False, comment="缴费金额")
transfer_type = Column(String(16), nullable=True, comment="转账方式: wxzz/zzfbzz/yhkzz/xj")
transfer_amount = Column(Numeric(18, 2), nullable=True, comment="实际转账金额")
transfer_time = Column(DateTime, nullable=True, comment="转账时间")
transfer_attachments = Column(Text, nullable=True, comment="转账凭证附件")
pay_status = Column(String(32), nullable=False, default="dfj", comment="缴费状态: dfj/jfqrz/yjf/tfqrz/ytf")
finance_confirm_attachments = Column(Text, nullable=True, comment="财务确认收款附件")
finance_remark = Column(String(512), nullable=True, comment="财务确认收款备注")
finance_confirm_time = Column(DateTime, nullable=True, comment="财务确认收款时间")
refund_amount = Column(Numeric(18, 2), nullable=True, comment="退费金额")
refund_remark = Column(String(512), nullable=True, comment="退费备注")
refund_attachments = Column(Text, nullable=True, comment="退费附件")
refund_apply_by = Column(String(64), nullable=True, comment="退费申请人ID")
refund_apply_time = Column(DateTime, nullable=True, comment="退费申请时间")
refund_finance_confirm_attachments = Column(Text, nullable=True, comment="退费财务确认附件")
refund_finance_remark = Column(String(512), nullable=True, comment="退费财务确认备注")
refund_finance_confirm_time = Column(DateTime, nullable=True, comment="退费财务确认时间")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(SmallInteger, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, server_default=func.now(), comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now(), comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
- Step 4: 在
models.py末尾导入新模型
在 backend/app/models/models.py 末尾已有的 import 行后面添加:
from app.models.clinical_payment import BizPilotPayment # noqa: E402,F401
- Step 5: 验证模型加载
Run: cd backend && source venv/bin/activate && python -c "from app.models.models import BizPilotApplication, BizPilotInfusionSchedule; from app.models.clinical_payment import BizPilotPayment; print('OK')"
Expected: OK
- Step 6: Commit
git add backend/app/models/models.py backend/app/models/clinical_payment.py
git commit -m "feat: add biz_type to pilot tables + BizPilotPayment model"
Task 3: 后端 CRUD — clinical_payment.py
Files:
- Create:
backend/app/crud/clinical_payment.py
参照 backend/app/crud/collection_protocol.py 中 confirm_receipt (line 1390)、confirm_refund (line 1522)、submit_receipt_finance (line 1595)、submit_refund_finance (line 1648) 的模式。
- Step 1: 创建 CRUD 文件
创建 backend/app/crud/clinical_payment.py,包含以下函数:
"""临床应用缴费 CRUD"""
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func
from sqlalchemy.orm import Session
from app.models.clinical_payment import BizPilotPayment
from app.models.models import (
BizPilotApplication,
BizPilotPatient,
BizPilotInfusionPlan,
BizPilotInfusionSchedule,
)
# ─── helpers ──────────────────────────────────────────────
def _row_to_dict(row: BizPilotPayment, *, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
d: Dict[str, Any] = {
"id": row.id,
"pilotApplicationId": row.pilot_application_id or "",
"infusionScheduleId": row.infusion_schedule_id or "",
"bizType": row.biz_type or "clinical_application",
"amount": float(row.amount) if row.amount else 0,
"transferType": row.transfer_type or "",
"transferAmount": float(row.transfer_amount) if row.transfer_amount else None,
"transferTime": row.transfer_time.strftime("%Y-%m-%d %H:%M:%S") if row.transfer_time else "",
"transferAttachments": row.transfer_attachments or "",
"payStatus": row.pay_status or "dfj",
"financeConfirmAttachments": row.finance_confirm_attachments or "",
"financeRemark": row.finance_remark or "",
"financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
"refundAmount": float(row.refund_amount) if row.refund_amount else None,
"refundRemark": row.refund_remark or "",
"refundAttachments": row.refund_attachments or "",
"refundApplyBy": row.refund_apply_by or "",
"refundApplyTime": row.refund_apply_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_apply_time else "",
"refundFinanceConfirmAttachments": row.refund_finance_confirm_attachments or "",
"refundFinanceRemark": row.refund_finance_remark or "",
"refundFinanceConfirmTime": row.refund_finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_finance_confirm_time else "",
"tenantNo": row.tenant_no or "",
"remark": row.remark or "",
"createBy": row.create_by or "",
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
}
if extra:
d.update(extra)
return d
def _get_or_raise(db: Session, *, payment_id: str, tenant_no: str) -> BizPilotPayment:
row = (
db.query(BizPilotPayment)
.filter(
and_(
BizPilotPayment.id == payment_id,
BizPilotPayment.tenant_no == tenant_no,
BizPilotPayment.del_flag == 0,
)
)
.first()
)
if not row:
raise ValueError("缴费记录不存在")
return row
def _enrich_payment(db: Session, row: BizPilotPayment) -> Dict[str, Any]:
"""关联查询申请单、患者、输注计划信息,附加到 dict"""
app_row = (
db.query(BizPilotApplication)
.filter(BizPilotApplication.id == row.pilot_application_id)
.first()
)
patient_row = None
schedule_row = (
db.query(BizPilotInfusionSchedule)
.filter(BizPilotInfusionSchedule.id == row.infusion_schedule_id)
.first()
)
if app_row:
patient_row = (
db.query(BizPilotPatient)
.filter(
BizPilotPatient.pilot_application_id == app_row.id,
BizPilotPatient.del_flag == 0,
)
.first()
)
extra = {
"applicationNo": app_row.application_no if app_row else "",
"applicationName": app_row.application_name if app_row else "",
"channelName": app_row.channel_name if app_row else "",
"patientName": patient_row.patient_name if patient_row else "",
"cellType": schedule_row.cell_type if schedule_row else "",
"specText": schedule_row.spec_text if schedule_row else "",
"infusionTime": schedule_row.infusion_time.strftime("%Y-%m-%d %H:%M:%S") if schedule_row and schedule_row.infusion_time else "",
}
return _row_to_dict(row, extra=extra)
# ─── create (auto) ────────────────────────────────────────
def auto_create_payment(
db: Session,
*,
application_id: str,
schedule_id: str,
amount: Any,
tenant_no: str,
user_id: str,
dept_id: str,
) -> Optional[BizPilotPayment]:
"""输注计划创建时自动调用,生成 dfj 缴费记录"""
if amount is None or Decimal(str(amount)) <= 0:
return None
now = datetime.now()
row = BizPilotPayment(
id=str(uuid.uuid4()),
pilot_application_id=application_id,
infusion_schedule_id=schedule_id,
biz_type="clinical_application",
amount=Decimal(str(amount)),
pay_status="dfj",
tenant_no=tenant_no,
order_num=0,
create_by=user_id,
create_dept_id=dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(row)
return row
# ─── list ─────────────────────────────────────────────────
def list_payments(
db: Session,
*,
page: int = 1,
page_size: int = 10,
pay_status: Optional[str] = None,
keyword: Optional[str] = None,
tenant_no: str = "",
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
q = (
db.query(BizPilotPayment)
.filter(
BizPilotPayment.del_flag == 0,
BizPilotPayment.tenant_no == tenant_no,
)
)
if pay_status and pay_status.strip():
q = q.filter(BizPilotPayment.pay_status == pay_status.strip())
if keyword and keyword.strip():
kw = f"%{keyword.strip()}%"
sub = (
db.query(BizPilotApplication.id)
.filter(
BizPilotApplication.del_flag == 0,
BizPilotApplication.application_no.like(kw),
)
.subquery()
)
q = q.filter(BizPilotPayment.pilot_application_id.in_(sub))
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizPilotPayment, user_info, db, tenant_no)
total = q.order_by(None).with_entities(func.count(BizPilotPayment.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(BizPilotPayment.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
out = [_enrich_payment(db, r) for r in rows]
return out, int(total)
# ─── submit receipt (dfj → jfqrz) ────────────────────────
def submit_receipt(
db: Session,
*,
payment_id: str,
tenant_no: str,
user_id: Optional[str],
) -> BizPilotPayment:
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
if row.pay_status != "dfj":
raise ValueError("仅待缴费状态可提交缴费确认")
row.pay_status = "jfqrz"
row.update_by = user_id
row.update_time = datetime.now()
db.add(row)
db.commit()
db.refresh(row)
return row
def batch_submit_receipt(
db: Session,
*,
payment_ids: List[str],
tenant_no: str,
user_id: Optional[str],
) -> int:
ids = [str(i).strip() for i in (payment_ids or []) if str(i).strip()]
if not ids:
raise ValueError("请选择缴费记录")
for pid in ids:
row = _get_or_raise(db, payment_id=pid, tenant_no=tenant_no)
if row.pay_status != "dfj":
continue
row.pay_status = "jfqrz"
row.update_by = user_id
row.update_time = datetime.now()
db.add(row)
db.commit()
return len(ids)
# ─── confirm receipt (jfqrz → yjf) ──────────────────────
def confirm_receipt(
db: Session,
*,
payment_id: str,
tenant_no: str,
user_id: Optional[str],
finance_confirm_attachments: Optional[str] = None,
finance_remark: Optional[str] = None,
) -> BizPilotPayment:
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
if row.pay_status != "jfqrz":
if row.pay_status == "yjf":
raise ValueError("该记录已确认收款")
raise ValueError("当前状态不可确认收款")
row.pay_status = "yjf"
if finance_confirm_attachments is not None:
row.finance_confirm_attachments = finance_confirm_attachments.strip() or None
if finance_remark is not None:
row.finance_remark = finance_remark.strip() or None
row.finance_confirm_time = datetime.now()
row.update_by = user_id
row.update_time = datetime.now()
db.add(row)
db.commit()
db.refresh(row)
return row
# ─── submit refund (yjf → tfqrz) ─────────────────────────
def submit_refund(
db: Session,
*,
payment_id: str,
tenant_no: str,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> BizPilotPayment:
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
if row.pay_status != "yjf":
raise ValueError("仅已缴费状态可发起退费")
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
raise ValueError("请填写有效的退费金额")
row.pay_status = "tfqrz"
row.refund_amount = Decimal(str(refund_amount))
row.refund_remark = (refund_remark or "").strip() or None
row.refund_attachments = (refund_attachments or "").strip() or None
row.refund_apply_by = user_id
row.refund_apply_time = datetime.now()
row.update_by = user_id
row.update_time = datetime.now()
db.add(row)
db.commit()
db.refresh(row)
return row
def batch_submit_refund(
db: Session,
*,
payment_ids: List[str],
tenant_no: str,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> int:
ids = [str(i).strip() for i in (payment_ids or []) if str(i).strip()]
if not ids:
raise ValueError("请选择缴费记录")
for pid in ids:
row = _get_or_raise(db, payment_id=pid, tenant_no=tenant_no)
if row.pay_status != "yjf":
continue
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
continue
row.pay_status = "tfqrz"
row.refund_amount = Decimal(str(refund_amount))
row.refund_remark = (refund_remark or "").strip() or None
row.refund_attachments = (refund_attachments or "").strip() or None
row.refund_apply_by = user_id
row.refund_apply_time = datetime.now()
row.update_by = user_id
row.update_time = datetime.now()
db.add(row)
db.commit()
return len(ids)
# ─── confirm refund (tfqrz → ytf) ───────────────────────
def confirm_refund(
db: Session,
*,
payment_id: str,
tenant_no: str,
user_id: Optional[str],
refund_finance_confirm_attachments: Optional[str] = None,
refund_finance_remark: Optional[str] = None,
) -> BizPilotPayment:
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
if row.pay_status != "tfqrz":
if row.pay_status == "ytf":
raise ValueError("该记录已确认退费")
raise ValueError("当前状态不可确认退费")
row.pay_status = "ytf"
if refund_finance_confirm_attachments is not None:
row.refund_finance_confirm_attachments = refund_finance_confirm_attachments.strip() or None
if refund_finance_remark is not None:
row.refund_finance_remark = refund_finance_remark.strip() or None
row.refund_finance_confirm_time = datetime.now()
row.update_by = user_id
row.update_time = datetime.now()
db.add(row)
db.commit()
db.refresh(row)
return row
# ─── detail ───────────────────────────────────────────────
def get_payment_detail(db: Session, *, payment_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
return _enrich_payment(db, row)
def get_payment_by_schedule(db: Session, *, schedule_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
row = (
db.query(BizPilotPayment)
.filter(
BizPilotPayment.infusion_schedule_id == schedule_id,
BizPilotPayment.tenant_no == tenant_no,
BizPilotPayment.del_flag == 0,
)
.first()
)
if not row:
return None
return _enrich_payment(db, row)
- Step 2: 验证导入
Run: cd backend && source venv/bin/activate && python -c "from app.crud.clinical_payment import list_payments; print('OK')"
Expected: OK
- Step 3: Commit
git add backend/app/crud/clinical_payment.py
git commit -m "feat: add clinical payment CRUD layer"
Task 4: 后端 API 路由 — clinical_payment.py
Files:
-
Create:
backend/app/api/clinical_payment.py -
Modify:
backend/app/api/__init__.py -
Modify:
backend/app/main.py -
Step 1: 创建 API 路由文件
创建 backend/app/api/clinical_payment.py:
"""临床应用缴费 API"""
from fastapi import APIRouter, Depends, Query, Request
from typing import Optional
router = APIRouter()
def _common_params(
request: Request,
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
keyword: Optional[str] = Query(None),
):
return {
"page": page,
"page_size": pageSize,
"keyword": keyword,
"tenant_no": request.state.tenant_no,
"user_info": getattr(request.state, "user_info", None),
}
# ─── 缴费信息页面 ──────────────────────────────────────
@router.get("/clinical-payments")
def list_clinical_payments(
payStatus: Optional[str] = Query(None),
**kwargs,
):
from app.core.database import get_db
from app.crud.clinical_payment import list_payments
params = _common_params(**kwargs) if kwargs else _common_params(
kwargs.get("__request"), kwargs.get("page", 1), kwargs.get("pageSize", 10), kwargs.get("keyword")
)
# 重新从 request 获取参数
# 注意:FastAPI 会自动注入 Query 参数,这里用更简洁的方式
pass
# 更简洁的写法 — 每个端点独立定义
由于上面的草稿过于冗长,下面用完整的每个端点函数替代。每个端点参照 backend/app/api/pilot_service.py 中现有端点的模式(获取 db session、tenant_no、user_info)。
实际实现时参照已有模式:
request.state.tenant_no获取租户request.state.user_info获取用户信息get_db()获取数据库会话_row_to_dict系列函数序列化返回
10 个端点:
| 端点 | CRUD 函数 |
|---|---|
GET /clinical-payments |
list_payments(pay_status=...) |
GET /clinical-payments/{payment_id} |
get_payment_detail(payment_id=...) |
POST /clinical-payments/{payment_id}/submit-receipt |
submit_receipt(payment_id=...) |
POST /clinical-payments/submit-receipt-batch |
batch_submit_receipt(payment_ids=...) |
POST /clinical-payments/{payment_id}/confirm-receipt |
confirm_receipt(payment_id=..., finance_confirm_attachments=..., finance_remark=...) |
POST /clinical-payments/{payment_id}/submit-refund |
submit_refund(payment_id=..., refund_amount=..., refund_remark=..., refund_attachments=...) |
POST /clinical-payments/submit-refund-batch |
batch_submit_refund(payment_ids=..., refund_amount=..., ...) |
POST /clinical-payments/{payment_id}/confirm-refund |
confirm_refund(payment_id=..., refund_finance_confirm_attachments=..., refund_finance_remark=...) |
GET /clinical-payments/receipt-pending |
list_payments(pay_status='jfqrz') |
GET /clinical-payments/receipt-confirmed |
list_payments(pay_status='yjf') |
GET /clinical-payments/refund-pending |
list_payments(pay_status='tfqrz') |
GET /clinical-payments/refund-confirmed |
list_payments(pay_status='ytf') |
- Step 2: 在
__init__.py注册路由
在 backend/app/api/__init__.py 添加:
from app.api.clinical_payment import router as clinical_payment_router
在 routers 列表中添加 "clinical_payment_router"。
- Step 3: 在
main.py挂载路由
在 backend/app/main.py 中现有路由挂载位置添加:
from app.api import clinical_payment_router
app.include_router(clinical_payment_router, prefix="/web/admin", tags=["clinical-payment"])
- Step 4: 验证路由注册
Run: cd backend && source venv/bin/activate && python -c "from app.api.clinical_payment import router; print(f'Routes: {len(router.routes)}')"
Expected: Routes: 12(或对应数量)
- Step 5: Commit
git add backend/app/api/clinical_payment.py backend/app/api/__init__.py backend/app/main.py
git commit -m "feat: add clinical payment API routes (12 endpoints)"
Task 5: 后端 CRUD 改造 — pilot_application.py 支持 biz_type
Files:
- Modify:
backend/app/crud/pilot_application.py
关键改动点(参照已读取的代码):
- Step 1:
_generate_application_no支持 biz_type 参数
在 line 74,将 prefix 根据 biz_type 动态选择:
def _generate_application_no(db: Session, tenant_no: str, biz_type: str = "pilot_service") -> str:
today_str = datetime.now().strftime("%Y%m%d")
prefix = f"LCSQ{today_str}" if biz_type == "clinical_application" else 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]:
try:
seq = int(existing[0][-4:]) + 1
except ValueError:
seq = 1
return f"{prefix}{seq:04d}"
- Step 2:
_row_to_dict_application添加 bizType 字段
在 line 96-110 的 dict 中添加:
"bizType": row.biz_type or "pilot_service",
- Step 3:
list_applications支持 biz_type 过滤
在 line 284 的 list_applications 函数签名中添加 biz_type 参数,并在查询中添加过滤:
def list_applications(
db: Session,
...
biz_type: Optional[str] = None, # 新增
) -> Tuple[List[Dict[str, Any]], int]:
...
if biz_type:
q = q.filter(BizPilotApplication.biz_type == biz_type)
- Step 4:
create_application支持 biz_type=clinical_application
在 line 385 的 create_application 函数中:
- 当
biz_type == 'clinical_application'时,channel_service_order_id不必填,直接从 data 中获取channelId - 调用
_generate_application_no时传入biz_type
def create_application(db, data, tenant_no, user_id, dept_id):
biz_type = data.get("bizType") or "pilot_service"
if biz_type == "pilot_service":
channel_service_order_id = data.get("channelServiceOrderId") or ""
if not channel_service_order_id:
raise ValueError("请选择渠道服务订单")
# ... 现有逻辑不变
else:
# clinical_application
channel_service_order_id = ""
channel_id = data.get("channelId") or ""
channel_name = data.get("channelName") or ""
application_no = _generate_application_no(db, tenant_no, biz_type)
row = BizPilotApplication(
...
biz_type=biz_type,
channel_service_order_id=channel_service_order_id,
channel_id=channel_id,
channel_name=channel_name,
)
- Step 5:
create_schedule支持临床应用 amount + 自动创建缴费记录
在 line 1106 的 create_schedule 函数中:
- 保存
amount字段到BizPilotInfusionSchedule - 当
biz_type == 'clinical_application'时调用auto_create_payment
# 在 row = BizPilotInfusionSchedule(...) 中添加:
amount=data.get("amount") if data.get("amount") else None,
# 在创建工单之后添加:
if application.biz_type == "clinical_application" and data.get("amount"):
from app.crud.clinical_payment import auto_create_payment
auto_create_payment(
db,
application_id=application.id,
schedule_id=schedule_id,
amount=data["amount"],
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
- Step 6:
_row_to_dict_infusion_schedule添加 amount 字段
在输注计划序列化函数中添加:
"amount": float(row.amount) if row.amount else None,
- Step 7: 验证导入和基本逻辑
Run: cd backend && source venv/bin/activate && python -c "from app.crud.pilot_application import create_application; print('OK')"
Expected: OK
- Step 8: Commit
git add backend/app/crud/pilot_application.py
git commit -m "feat: pilot_application CRUD supports biz_type for clinical application"
Task 6: 后端 API 改造 — pilot_application.py 路由
Files:
-
Modify:
backend/app/api/pilot_application.py -
Step 1: list 路由添加 bizType 过滤
在申请单列表路由中,从 Query 参数获取 bizType 并传给 list_applications。
- Step 2: create 路由传递 bizType
在创建申请单路由中,从前端传入的 data 中透传 bizType 字段。
- Step 3: 输注计划创建路由传递 amount
在创建输注计划路由中,透传 amount 字段。
- Step 4: 验证 API 可访问
Run: cd backend && source venv/bin/activate && python -c "from app.api.pilot_application import router; print('OK')"
Expected: OK
- Step 5: Commit
git add backend/app/api/pilot_application.py
git commit -m "feat: pilot_application API supports bizType filter and amount field"
Task 7: 前端路由 + 菜单
Files:
-
Modify:
web-admin/src/router/modules/sqxp.ts -
Step 1: 在 sqxp.ts 末尾(
]之前)添加临床应用管理一级菜单
{
id: '90000000000000000000000000000001',
name: 'ClinicalApplication',
path: '/clinical-application',
component: '/index/index',
meta: {
title: '临床应用管理',
icon: 'ri:heart-pulse-line',
keepAlive: false,
roles: ['admin']
},
children: [
{
id: '90000000000000000000000000000002',
name: 'ClinicalApplication',
path: 'clinical-application',
component: '/pilot-service/pilot-application/index',
meta: {
title: '临床申请单',
keepAlive: true,
menuCode: 'clinical-application:application:list'
},
props: { bizType: 'clinical_application' }
},
{
id: '90000000000000000000000000000003',
name: 'ClinicalCheckupManagement',
path: 'clinical-checkup-management',
component: '/pilot-service/checkup-management/index',
meta: {
title: '体检管理',
keepAlive: true,
menuCode: 'clinical-application:checkup-management:list'
},
props: { bizType: 'clinical_application' }
},
{
id: '90000000000000000000000000000004',
name: 'ClinicalInfusionScheduleManagement',
path: 'clinical-infusion-schedule-management',
component: '/pilot-service/infusion-schedule-management/index',
meta: {
title: '输注计划管理',
keepAlive: true,
menuCode: 'clinical-application:infusion-schedule-management:list'
},
props: { bizType: 'clinical_application' }
},
{
id: '90000000000000000000000000000005',
name: 'ClinicalPaymentInfo',
path: 'clinical-payment-info',
component: '/pilot-service/clinical-payment-info/index',
meta: {
title: '缴费信息',
keepAlive: true,
menuCode: 'clinical-application:payment-info:list'
}
},
{
id: '90000000000000000000000000000006',
name: 'ClinicalPaymentConfirm',
path: 'clinical-payment-confirm',
component: '/pilot-service/clinical-payment-confirm/index',
meta: {
title: '缴费确认(财务)',
keepAlive: true,
menuCode: 'clinical-application:payment-confirm:list'
}
},
{
id: '90000000000000000000000000000007',
name: 'ClinicalRefundConfirm',
path: 'clinical-refund-confirm',
component: '/pilot-service/clinical-refund-confirm/index',
meta: {
title: '退费确认(财务)',
keepAlive: true,
menuCode: 'clinical-application:refund-confirm:list'
}
},
{
id: '90000000000000000000000000000008',
name: 'ClinicalOutboundReleaseReview',
path: 'clinical-outbound-release-review',
component: '/pilot-service/outbound-release-review/index',
meta: {
title: '出库放行审核',
keepAlive: true,
menuCode: 'clinical-application:outbound-release-review:list'
},
props: { bizType: 'clinical_application' }
}
]
}
- Step 2: 验证路由文件语法
Run: cd web-admin && npx tsc --noEmit src/router/modules/sqxp.ts 2>&1 | head -5
- Step 3: Commit
git add web-admin/src/router/modules/sqxp.ts
git commit -m "feat: add clinical application menu and routes"
Task 8: 前端 API — clinical payment
Files:
-
Modify:
web-admin/src/api/business.ts -
Step 1: 在 business.ts 末尾添加 clinical payment API 函数
// ─── 临床应用缴费 ─────────────────────────────────────
export function fetchClinicalPayments(params: any) {
return http.get('/admin/clinical-payments', params)
}
export function fetchClinicalPaymentDetail(id: string) {
return http.get(`/admin/clinical-payments/${id}`)
}
export function submitClinicalReceipt(id: string) {
return http.post(`/admin/clinical-payments/${id}/submit-receipt`)
}
export function batchSubmitClinicalReceipt(ids: string[]) {
return http.post('/admin/clinical-payments/submit-receipt-batch', { ids })
}
export function confirmClinicalReceipt(id: string, data: any) {
return http.post(`/admin/clinical-payments/${id}/confirm-receipt`, data)
}
export function submitClinicalRefund(id: string, data: any) {
return http.post(`/admin/clinical-payments/${id}/submit-refund`, data)
}
export function batchSubmitClinicalRefund(ids: string[], data: any) {
return http.post('/admin/clinical-payments/submit-refund-batch', { ...data, ids })
}
export function confirmClinicalRefund(id: string, data: any) {
return http.post(`/admin/clinical-payments/${id}/confirm-refund`, data)
}
export function fetchClinicalReceiptPending(params: any) {
return http.get('/admin/clinical-payments/receipt-pending', params)
}
export function fetchClinicalReceiptConfirmed(params: any) {
return http.get('/admin/clinical-payments/receipt-confirmed', params)
}
export function fetchClinicalRefundPending(params: any) {
return http.get('/admin/clinical-payments/refund-pending', params)
}
export function fetchClinicalRefundConfirmed(params: any) {
return http.get('/admin/clinical-payments/refund-confirmed', params)
}
- Step 2: Commit
git add web-admin/src/api/business.ts
git commit -m "feat: add clinical payment API functions"
Task 9: 前端页面 — 缴费信息
Files:
- Create:
web-admin/src/views/pilot-service/clinical-payment-info/index.vue
参照 web-admin/src/views/cell-storage/payment-info/index.vue 的结构。
- Step 1: 创建页面
核心结构:
-
Tab 切换:
dfj待缴费 /yjf已缴费 -
列表表格列:申请单号、患者姓名、渠道名称、细胞类型/规格、输注时间、缴费金额、缴费状态(
payStatusLabel+payStatusTagType)、操作 -
待缴费 Tab 操作:单条缴费确认、批量缴费确认
-
已缴费 Tab 操作:单条退费(弹窗填退费金额+备注+附件)、批量退费
-
复用
payStatusLabel、payStatusTagType、formatMoneyDisplay -
Step 2: Commit
git add web-admin/src/views/pilot-service/clinical-payment-info/
git commit -m "feat: add clinical payment info page"
Task 10: 前端页面 — 缴费确认(财务)
Files:
- Create:
web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue
参照 web-admin/src/views/cell-storage/payment-confirm/index.vue。
- Step 1: 创建页面
核心结构:
-
Tab:待确认(
jfqrz)/ 已确认(yjf) -
确认收款弹窗:复用
ProtocolFinanceConfirmFields组件(备注 + 附件上传) -
表格列同缴费信息页
-
Step 2: Commit
git add web-admin/src/views/pilot-service/clinical-payment-confirm/
git commit -m "feat: add clinical payment confirm page (finance)"
Task 11: 前端页面 — 退费确认(财务)
Files:
- Create:
web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue
参照 web-admin/src/views/cell-storage/refund-confirm/index.vue。
- Step 1: 创建页面
核心结构:
-
Tab:待确认(
tfqrz)/ 已确认(ytf) -
确认退费弹窗:展示退费申请信息(只读)+ 财务确认表单(复用
ProtocolFinanceConfirmFields) -
表格列同缴费信息页
-
Step 2: Commit
git add web-admin/src/views/pilot-service/clinical-refund-confirm/
git commit -m "feat: add clinical refund confirm page (finance)"
Task 12: 前端改造 — 申请单页面支持 bizType
Files:
-
Modify:
web-admin/src/views/pilot-service/pilot-application/index.vue -
Modify:
web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue -
Step 1: index.vue 接收 bizType prop
在 <script setup> 中:
const props = defineProps<{ bizType?: string }>()
const bizType = computed(() => props.bizType || 'pilot_service')
列表 API 调用时传入 bizType 参数。
-
Step 2: ApplicationFormDialog.vue 条件渲染
-
临床应用模式(
bizType === 'clinical_application'):- 隐藏"渠道服务订单"选择器
- 显示"渠道"直接选择器(从渠道列表中选择)
- 隐藏输注方案中的"协议产品价格"选择
- 显示输注计划的"金额"手动输入框
-
中试服务模式:保持现有行为不变
-
Step 3: Commit
git add web-admin/src/views/pilot-service/pilot-application/
git commit -m "feat: pilot application page supports bizType prop for clinical application"
Task 13: 前端改造 — 输注计划管理支持金额和缴费状态
Files:
-
Modify:
web-admin/src/views/pilot-service/infusion-schedule-management/index.vue -
Step 1: 接收 bizType prop,条件渲染金额列
const props = defineProps<{ bizType?: string }>()
const isClinical = computed(() => props.bizType === 'clinical_application')
在表格中条件渲染:
-
"缴费金额"列:
v-if="isClinical" -
"缴费状态"列:
v-if="isClinical",使用payStatusLabel+payStatusTagType -
Step 2: 查询缴费状态
在列表查询时,如果是临床应用,额外查询每条输注计划的缴费状态(后端返回 paymentStatus 字段)。
- Step 3: Commit
git add web-admin/src/views/pilot-service/infusion-schedule-management/
git commit -m "feat: infusion schedule page shows payment info for clinical application"
Task 14: 集成验证
- Step 1: 后端启动验证
Run: cd backend && source venv/bin/activate && python -c "from app.main import app; print('App OK')"
- Step 2: 前端构建验证
Run: cd web-admin && pnpm build 2>&1 | tail -5
Expected: 构建成功
- Step 3: LSP 诊断
对以下文件运行 lsp_diagnostics:
backend/app/models/models.pybackend/app/crud/pilot_application.pybackend/app/crud/clinical_payment.pyweb-admin/src/router/modules/sqxp.ts
Expected: 无新增 error
- Step 4: Final commit
git add -A
git commit -m "feat: complete clinical application business with payment flow"