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
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 schemasbackend/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 — 挂载新路由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 — 退费确认页面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 — 接收 bizType prop,条件渲染web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue — 临床应用模式隐藏渠道订单web-admin/src/views/pilot-service/infusion-schedule-management/index.vue — 临床应用显示金额列docs/sql/clinical-application-migration.sql — ALTER + CREATE TABLEFiles:
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='临床应用缴费记录';
Run: mysql -u root -p zkxw < docs/sql/clinical-application-migration.sql
git add docs/sql/clinical-application-migration.sql
git commit -m "feat: add clinical application DB migration (biz_type + biz_pilot_payment)"
Files:
backend/app/models/models.py — BizPilotApplication 和 BizPilotInfusionScheduleCreate: 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=临床应用")
BizPilotInfusionSchedule 类中添加 amount 字段在 backend/app/models/models.py 中 BizPilotInfusionSchedule 类的 qc_status 字段后面添加:
amount = Column(Numeric(18, 2), nullable=True, comment="临床应用-手动填写的输注金额(中试为NULL)")
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="删除标记")
models.py 末尾导入新模型在 backend/app/models/models.py 末尾已有的 import 行后面添加:
from app.models.clinical_payment import BizPilotPayment # noqa: E402,F401
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
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"
Files:
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) 的模式。
创建 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)
Run: cd backend && source venv/bin/activate && python -c "from app.crud.clinical_payment import list_payments; print('OK')"
Expected: OK
git add backend/app/crud/clinical_payment.py
git commit -m "feat: add clinical payment CRUD layer"
Files:
backend/app/api/clinical_payment.pybackend/app/api/__init__.pyModify: 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') |
__init__.py 注册路由在 backend/app/api/__init__.py 添加:
from app.api.clinical_payment import router as clinical_payment_router
在 routers 列表中添加 "clinical_payment_router"。
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"])
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(或对应数量)
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)"
Files:
backend/app/crud/pilot_application.py关键改动点(参照已读取的代码):
_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}"
_row_to_dict_application 添加 bizType 字段在 line 96-110 的 dict 中添加:
"bizType": row.biz_type or "pilot_service",
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)
create_application 支持 biz_type=clinical_application在 line 385 的 create_application 函数中:
biz_type == 'clinical_application' 时,channel_service_order_id 不必填,直接从 data 中获取 channelId_generate_application_no 时传入 biz_typedef 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,
)
create_schedule 支持临床应用 amount + 自动创建缴费记录在 line 1106 的 create_schedule 函数中:
amount 字段到 BizPilotInfusionSchedulebiz_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,
)
_row_to_dict_infusion_schedule 添加 amount 字段在输注计划序列化函数中添加:
"amount": float(row.amount) if row.amount else None,
Run: cd backend && source venv/bin/activate && python -c "from app.crud.pilot_application import create_application; print('OK')"
Expected: OK
git add backend/app/crud/pilot_application.py
git commit -m "feat: pilot_application CRUD supports biz_type for clinical application"
Files:
Modify: backend/app/api/pilot_application.py
[ ] Step 1: list 路由添加 bizType 过滤
在申请单列表路由中,从 Query 参数获取 bizType 并传给 list_applications。
在创建申请单路由中,从前端传入的 data 中透传 bizType 字段。
在创建输注计划路由中,透传 amount 字段。
Run: cd backend && source venv/bin/activate && python -c "from app.api.pilot_application import router; print('OK')"
Expected: OK
git add backend/app/api/pilot_application.py
git commit -m "feat: pilot_application API supports bizType filter and amount field"
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' }
}
]
}
Run: cd web-admin && npx tsc --noEmit src/router/modules/sqxp.ts 2>&1 | head -5
git add web-admin/src/router/modules/sqxp.ts
git commit -m "feat: add clinical application menu and routes"
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)
}
git add web-admin/src/api/business.ts
git commit -m "feat: add clinical payment API functions"
Files:
web-admin/src/views/pilot-service/clinical-payment-info/index.vue参照 web-admin/src/views/cell-storage/payment-info/index.vue 的结构。
核心结构:
dfj 待缴费 / yjf 已缴费payStatusLabel + payStatusTagType)、操作复用 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"
Files:
web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue参照 web-admin/src/views/cell-storage/payment-confirm/index.vue。
核心结构:
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)"
Files:
web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue参照 web-admin/src/views/cell-storage/refund-confirm/index.vue。
核心结构:
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)"
Files:
web-admin/src/views/pilot-service/pilot-application/index.vueModify: 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"
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 字段)。
git add web-admin/src/views/pilot-service/infusion-schedule-management/
git commit -m "feat: infusion schedule page shows payment info for clinical application"
Run: cd backend && source venv/bin/activate && python -c "from app.main import app; print('App OK')"
Run: cd web-admin && pnpm build 2>&1 | tail -5
Expected: 构建成功
对以下文件运行 lsp_diagnostics:
backend/app/models/models.pybackend/app/crud/pilot_application.pybackend/app/crud/clinical_payment.pyweb-admin/src/router/modules/sqxp.tsExpected: 无新增 error
git add -A
git commit -m "feat: complete clinical application business with payment flow"