diff --git a/backend/app.pid b/backend/app.pid
index 2c710dc58..901b3e35f 100644
--- a/backend/app.pid
+++ b/backend/app.pid
@@ -1 +1 @@
-60905
+73984
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
index 1687079ea..2a4b17a89 100644
--- a/backend/app/api/__init__.py
+++ b/backend/app/api/__init__.py
@@ -24,6 +24,7 @@ from app.api.pilot_delivery import router as pilot_delivery_router
from app.api.pilot_infusion_confirm import router as pilot_infusion_confirm_router
from app.api.pilot_infusion_schedule_mgmt import router as pilot_infusion_schedule_mgmt_router
from app.api.outbound_release_review import router as outbound_release_review_router
+from app.api.clinical_payment import router as clinical_payment_router
__all__ = [
"public_router",
@@ -51,4 +52,5 @@ __all__ = [
"pilot_infusion_confirm_router",
"pilot_infusion_schedule_mgmt_router",
"outbound_release_review_router",
+ "clinical_payment_router",
]
diff --git a/backend/app/api/clinical_payment.py b/backend/app/api/clinical_payment.py
new file mode 100644
index 000000000..935c4adc3
--- /dev/null
+++ b/backend/app/api/clinical_payment.py
@@ -0,0 +1,337 @@
+from typing import List, Optional
+
+from fastapi import APIRouter, Depends, Query
+from pydantic import BaseModel, ConfigDict, Field
+from sqlalchemy.orm import Session
+
+from app.core.database import get_db
+from app.core.menu_guard import require_admin_or_super_admin
+from app.core.response import error_response, success_response
+from app.crud import clinical_payment
+from app.models.models import SysUser
+
+router = APIRouter(prefix="/web/admin", tags=["临床应用缴费管理"])
+
+
+def _get_tenant_no(db: Session, user: dict) -> str:
+ user_id = user.get("user_id")
+ if not user_id:
+ raise PermissionError("用户身份信息缺失,无法确定租户")
+ sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
+ if not sys_user or not sys_user.tenant_no:
+ raise PermissionError("用户租户信息缺失")
+ return sys_user.tenant_no
+
+
+class PaymentIdBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True)
+
+ id: str
+
+
+class BatchPaymentIdsBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True)
+
+ payment_ids: List[str] = Field(..., alias="paymentIds")
+
+
+class ConfirmReceiptBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True)
+
+ id: str
+ finance_confirm_attachments: Optional[str] = Field(None, alias="financeConfirmAttachments")
+ finance_remark: Optional[str] = Field(None, alias="financeRemark")
+
+
+class SubmitRefundBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True)
+
+ id: str
+ refund_amount: float = Field(..., alias="refundAmount")
+ refund_remark: Optional[str] = Field(None, alias="refundRemark")
+ refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
+
+
+class BatchSubmitRefundBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True)
+
+ payment_ids: List[str] = Field(..., alias="paymentIds")
+ refund_amount: float = Field(..., alias="refundAmount")
+ refund_remark: Optional[str] = Field(None, alias="refundRemark")
+ refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
+
+
+class ConfirmRefundBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True)
+
+ id: str
+ refund_finance_confirm_attachments: Optional[str] = Field(
+ None, alias="refundFinanceConfirmAttachments"
+ )
+ refund_finance_remark: Optional[str] = Field(None, alias="refundFinanceRemark")
+
+
+@router.get("/clinical-payments")
+async def api_list_payments(
+ page: int = Query(1, ge=1),
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
+ payStatus: Optional[str] = Query(None, alias="payStatus"),
+ keyword: Optional[str] = Query(None, alias="keyword"),
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ rows, total = clinical_payment.list_payments(
+ db,
+ page=page,
+ page_size=pageSize,
+ pay_status=payStatus,
+ keyword=keyword,
+ tenant_no=tenant_no,
+ user_info=user,
+ )
+ return success_response({"rows": rows, "total": total})
+ except Exception as e:
+ return error_response(500, str(e))
+
+
+@router.get("/clinical-payments/{payment_id}")
+async def api_get_payment_detail(
+ payment_id: str,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ result = clinical_payment.get_payment_detail(
+ db, payment_id=payment_id, tenant_no=tenant_no
+ )
+ if result is None:
+ return error_response(404, "缴费记录不存在")
+ return success_response(result)
+ except ValueError as e:
+ return error_response(404, str(e))
+
+
+@router.post("/clinical-payments/{payment_id}/submit-receipt")
+async def api_submit_receipt(
+ payment_id: str,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ clinical_payment.submit_receipt(
+ db,
+ payment_id=payment_id,
+ tenant_no=tenant_no,
+ user_id=user.get("user_id"),
+ )
+ return success_response(None, message="提交缴费确认成功")
+ except ValueError as e:
+ return error_response(400, str(e))
+
+
+@router.post("/clinical-payments/submit-receipt-batch")
+async def api_batch_submit_receipt(
+ body: BatchPaymentIdsBody,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ count = clinical_payment.batch_submit_receipt(
+ db,
+ payment_ids=body.payment_ids,
+ tenant_no=tenant_no,
+ user_id=user.get("user_id"),
+ )
+ return success_response({"count": count}, message=f"批量提交成功,共{count}条")
+ except ValueError as e:
+ return error_response(400, str(e))
+
+
+@router.post("/clinical-payments/{payment_id}/confirm-receipt")
+async def api_confirm_receipt(
+ payment_id: str,
+ body: ConfirmReceiptBody,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ clinical_payment.confirm_receipt(
+ db,
+ payment_id=payment_id,
+ tenant_no=tenant_no,
+ user_id=user.get("user_id"),
+ finance_confirm_attachments=body.finance_confirm_attachments,
+ finance_remark=body.finance_remark,
+ )
+ return success_response(None, message="确认收款成功")
+ except ValueError as e:
+ return error_response(400, str(e))
+
+
+@router.post("/clinical-payments/{payment_id}/submit-refund")
+async def api_submit_refund(
+ payment_id: str,
+ body: SubmitRefundBody,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ clinical_payment.submit_refund(
+ db,
+ payment_id=payment_id,
+ tenant_no=tenant_no,
+ user_id=user.get("user_id"),
+ refund_amount=body.refund_amount,
+ refund_remark=body.refund_remark,
+ refund_attachments=body.refund_attachments,
+ )
+ return success_response(None, message="提交退费申请成功")
+ except ValueError as e:
+ return error_response(400, str(e))
+
+
+@router.post("/clinical-payments/submit-refund-batch")
+async def api_batch_submit_refund(
+ body: BatchSubmitRefundBody,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ count = clinical_payment.batch_submit_refund(
+ db,
+ payment_ids=body.payment_ids,
+ tenant_no=tenant_no,
+ user_id=user.get("user_id"),
+ refund_amount=body.refund_amount,
+ refund_remark=body.refund_remark,
+ refund_attachments=body.refund_attachments,
+ )
+ return success_response({"count": count}, message=f"批量提交退费成功,共{count}条")
+ except ValueError as e:
+ return error_response(400, str(e))
+
+
+@router.post("/clinical-payments/{payment_id}/confirm-refund")
+async def api_confirm_refund(
+ payment_id: str,
+ body: ConfirmRefundBody,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ clinical_payment.confirm_refund(
+ db,
+ payment_id=payment_id,
+ tenant_no=tenant_no,
+ user_id=user.get("user_id"),
+ refund_finance_confirm_attachments=body.refund_finance_confirm_attachments,
+ refund_finance_remark=body.refund_finance_remark,
+ )
+ return success_response(None, message="确认退费成功")
+ except ValueError as e:
+ return error_response(400, str(e))
+
+
+@router.get("/clinical-payments/receipt-pending")
+async def api_receipt_pending(
+ page: int = Query(1, ge=1),
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
+ keyword: Optional[str] = Query(None, alias="keyword"),
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ rows, total = clinical_payment.list_payments(
+ db,
+ page=page,
+ page_size=pageSize,
+ pay_status="jfqrz",
+ keyword=keyword,
+ tenant_no=tenant_no,
+ user_info=user,
+ )
+ return success_response({"rows": rows, "total": total})
+ except Exception as e:
+ return error_response(500, str(e))
+
+
+@router.get("/clinical-payments/receipt-confirmed")
+async def api_receipt_confirmed(
+ page: int = Query(1, ge=1),
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
+ keyword: Optional[str] = Query(None, alias="keyword"),
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ rows, total = clinical_payment.list_payments(
+ db,
+ page=page,
+ page_size=pageSize,
+ pay_status="yjf",
+ keyword=keyword,
+ tenant_no=tenant_no,
+ user_info=user,
+ )
+ return success_response({"rows": rows, "total": total})
+ except Exception as e:
+ return error_response(500, str(e))
+
+
+@router.get("/clinical-payments/refund-pending")
+async def api_refund_pending(
+ page: int = Query(1, ge=1),
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
+ keyword: Optional[str] = Query(None, alias="keyword"),
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ rows, total = clinical_payment.list_payments(
+ db,
+ page=page,
+ page_size=pageSize,
+ pay_status="tfqrz",
+ keyword=keyword,
+ tenant_no=tenant_no,
+ user_info=user,
+ )
+ return success_response({"rows": rows, "total": total})
+ except Exception as e:
+ return error_response(500, str(e))
+
+
+@router.get("/clinical-payments/refund-confirmed")
+async def api_refund_confirmed(
+ page: int = Query(1, ge=1),
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
+ keyword: Optional[str] = Query(None, alias="keyword"),
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ tenant_no = _get_tenant_no(db, user)
+ try:
+ rows, total = clinical_payment.list_payments(
+ db,
+ page=page,
+ page_size=pageSize,
+ pay_status="ytf",
+ keyword=keyword,
+ tenant_no=tenant_no,
+ user_info=user,
+ )
+ return success_response({"rows": rows, "total": total})
+ except Exception as e:
+ return error_response(500, str(e))
diff --git a/backend/app/api/menu.py b/backend/app/api/menu.py
index 27b88c242..6d6dc5e57 100644
--- a/backend/app/api/menu.py
+++ b/backend/app/api/menu.py
@@ -1,4 +1,4 @@
-"""运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)"""
+"""运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,13 个一级菜单)"""
from fastapi import APIRouter, Depends
from app.core.menu_guard import require_admin_or_super_admin
@@ -6,11 +6,559 @@ from app.core.response import success_response
router = APIRouter(prefix="/web/menu", tags=["菜单管理"])
-# 与 web-admin/src/router/modules/sqxp.ts 一致:系统管理、基础信息管理 等
+# 与 web-admin/src/router/modules/sqxp.ts 一致:13 个一级菜单
STATIC_MENU_TREE = [
+ # ────────────────────────────────────────────
+ # 1. 细胞存储
+ # ────────────────────────────────────────────
{
- "id": "100",
- "name": "System",
+ "id": "CEL00000000000000000000000000001",
+ "name": "CellStorage",
+ "path": "/cell-storage",
+ "component": "/index/index",
+ "meta": {
+ "title": "细胞存储",
+ "icon": "ri:test-tube-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "50000000000000000000000000000002",
+ "name": "CellStorageProtocol",
+ "path": "protocol",
+ "component": "/cell-storage/protocol/index",
+ "meta": {"title": "采集协议单录入", "keepAlive": True, "menuCode": "biz:cellStorageProtocol:list"},
+ },
+ {
+ "id": "50000000000000000000000000000003",
+ "name": "CellStorageProtocolAudit",
+ "path": "protocol-audit",
+ "component": "/cell-storage/protocol-audit/index",
+ "meta": {"title": "采集协议单管理", "keepAlive": True, "menuCode": "biz:cellStorageProtocolAudit:list"},
+ },
+ {
+ "id": "50000000000000000000000000000004",
+ "name": "CellStoragePaymentConfirm",
+ "path": "payment-confirm",
+ "component": "/cell-storage/payment-confirm/index",
+ "meta": {"title": "缴退费财务确认", "keepAlive": True, "menuCode": "biz:cellStoragePaymentConfirm:list"},
+ },
+ {
+ "id": "70000000000000000000000000000011",
+ "name": "CellStorageStorageReleaseReview",
+ "path": "storage-release-review",
+ "component": "/quality-inspection/storage-release-review/index",
+ "meta": {"title": "入库放行审核", "keepAlive": True, "menuCode": "biz:storageReleaseReview:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 2. 科研捐赠
+ # ────────────────────────────────────────────
+ {
+ "id": "RSC00000000000000000000000000001",
+ "name": "ResearchDonation",
+ "path": "/research-donation",
+ "component": "/index/index",
+ "meta": {
+ "title": "科研捐赠",
+ "icon": "ri:hand-heart-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "60000000000000000000000000000002",
+ "name": "ResearchDonationList",
+ "path": "list",
+ "component": "/research-donation/list/index",
+ "meta": {"title": "捐赠协议单录入", "keepAlive": True, "menuCode": "biz:researchDonation:list"},
+ },
+ {
+ "id": "60000000000000000000000000000003",
+ "name": "ResearchDonationAudit",
+ "path": "audit",
+ "component": "/research-donation/audit/index",
+ "meta": {"title": "捐赠协议单管理", "keepAlive": True, "menuCode": "biz:researchDonationAudit:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 3. 中试服务
+ # ────────────────────────────────────────────
+ {
+ "id": "PIL00000000000000000000000000001",
+ "name": "PilotService",
+ "path": "/pilot-service",
+ "component": "/index/index",
+ "meta": {
+ "title": "中试服务",
+ "icon": "ri:flask-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "80000000000000000000000000000010",
+ "name": "PilotApplication",
+ "path": "pilot-application",
+ "component": "/pilot-service/pilot-application/index",
+ "meta": {"title": "中试服务申请单", "keepAlive": True, "menuCode": "pilot-service:pilot-application:list"},
+ },
+ {
+ "id": "PIL00000000000000000000000000004",
+ "name": "PilotOutboundRelease",
+ "path": "outbound-release-review",
+ "component": "/pilot-service/outbound-release-review/index",
+ "meta": {"title": "出库放行审核", "keepAlive": True, "menuCode": "pilot-service:outbound-release-review:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 4. 临床应用
+ # ────────────────────────────────────────────
+ {
+ "id": "CLI00000000000000000000000000001",
+ "name": "ClinicalApplication",
+ "path": "/clinical-application",
+ "component": "/index/index",
+ "meta": {
+ "title": "临床应用",
+ "icon": "ri:heart-pulse-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "CLI00000000000000000000000000002",
+ "name": "ClinicalApplicationList",
+ "path": "clinical-application",
+ "component": "/pilot-service/pilot-application/index",
+ "meta": {"title": "临床申请单", "keepAlive": True, "menuCode": "clinical-application:application:list"},
+ },
+ {
+ "id": "CLI00000000000000000000000000003",
+ "name": "ClinicalCheckupManagement",
+ "path": "clinical-checkup-management",
+ "component": "/pilot-service/checkup-management/index",
+ "meta": {"title": "体检管理", "keepAlive": True, "menuCode": "clinical-application:checkup-management:list"},
+ },
+ {
+ "id": "CLI00000000000000000000000000004",
+ "name": "ClinicalInfusionSchedule",
+ "path": "clinical-infusion-schedule-management",
+ "component": "/pilot-service/infusion-schedule-management/index",
+ "meta": {"title": "输注计划管理", "keepAlive": True, "menuCode": "clinical-application:infusion-schedule-management:list"},
+ },
+ {
+ "id": "CLI00000000000000000000000000005",
+ "name": "ClinicalPaymentInfo",
+ "path": "clinical-payment-info",
+ "component": "/pilot-service/clinical-payment-info/index",
+ "meta": {"title": "缴费信息", "keepAlive": True, "menuCode": "clinical-application:payment-info:list"},
+ },
+ {
+ "id": "CLI00000000000000000000000000006",
+ "name": "ClinicalPaymentConfirm",
+ "path": "clinical-payment-confirm",
+ "component": "/pilot-service/clinical-payment-confirm/index",
+ "meta": {"title": "缴费确认(财务)", "keepAlive": True, "menuCode": "clinical-application:payment-confirm:list"},
+ },
+ {
+ "id": "CLI00000000000000000000000000007",
+ "name": "ClinicalRefundConfirm",
+ "path": "clinical-refund-confirm",
+ "component": "/pilot-service/clinical-refund-confirm/index",
+ "meta": {"title": "退费确认(财务)", "keepAlive": True, "menuCode": "clinical-application:refund-confirm:list"},
+ },
+ {
+ "id": "CLI00000000000000000000000000008",
+ "name": "ClinicalOutboundRelease",
+ "path": "clinical-outbound-release-review",
+ "component": "/pilot-service/outbound-release-review/index",
+ "meta": {"title": "出库放行审核", "keepAlive": True, "menuCode": "clinical-application:outbound-release-review:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 5. 质检管理
+ # ────────────────────────────────────────────
+ {
+ "id": "QCM00000000000000000000000000001",
+ "name": "QualityManage",
+ "path": "/quality-manage",
+ "component": "/index/index",
+ "meta": {
+ "title": "质检管理",
+ "icon": "ri:shield-check-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "70000000000000000000000000000002",
+ "name": "QualityInspectionSampleReception",
+ "path": "sample-reception",
+ "component": "/quality-inspection/sample-reception/index",
+ "meta": {"title": "样本接收", "keepAlive": True, "menuCode": "biz:sampleReception:list"},
+ },
+ {
+ "id": "70000000000000000000000000000007",
+ "name": "QualityInspectionQcWorkOrder",
+ "path": "qc-work-order",
+ "component": "/quality-inspection/qc-work-order/index",
+ "meta": {"title": "质检管理", "keepAlive": True, "menuCode": "biz:qcWorkOrder:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 6. 生产管理
+ # ────────────────────────────────────────────
+ {
+ "id": "PRM00000000000000000000000000001",
+ "name": "ProductionManage",
+ "path": "/production",
+ "component": "/index/index",
+ "meta": {
+ "title": "生产管理",
+ "icon": "grid",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "61000000000000000000000000000003",
+ "name": "ProductionStorageFacility",
+ "path": "storage-facility",
+ "component": "/operations/storage-facility/index",
+ "meta": {"title": "库位管理", "keepAlive": True, "menuCode": "operations:storage-facility:list"},
+ },
+ {
+ "id": "90000000000000000000000000000013",
+ "name": "CellProduct",
+ "path": "cell-product",
+ "component": "/production/cell-product/index",
+ "meta": {"title": "细胞产品库", "keepAlive": True, "menuCode": "production:cell-product:list"},
+ },
+ {
+ "id": "90000000000000000000000000000011",
+ "name": "ProductionWorkOrder",
+ "path": "production-work-order",
+ "component": "/production/production-work-order/index",
+ "meta": {"title": "生产管理", "keepAlive": True, "menuCode": "production:work-order:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 7. 运营支撑
+ # ────────────────────────────────────────────
+ {
+ "id": "OPS00000000000000000000000000001",
+ "name": "Operations",
+ "path": "/operations",
+ "component": "/index/index",
+ "meta": {
+ "title": "运营支撑",
+ "icon": "operation",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "61000000000000000000000000000002",
+ "name": "CleanRoom",
+ "path": "clean-room",
+ "component": "/operations/clean-room/index",
+ "meta": {"title": "洁净室管理", "keepAlive": True, "menuCode": "operations:clean-room:list"},
+ },
+ {
+ "id": "61000000000000000000000000000004",
+ "name": "ExhibitionAppointment",
+ "path": "exhibition-appointment",
+ "component": "/operations/exhibition-appointment/index",
+ "meta": {"title": "展馆预约", "keepAlive": True, "menuCode": "operations:exhibition-appointment:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 8. 市场基础信息
+ # ────────────────────────────────────────────
+ {
+ "id": "MKT00000000000000000000000000001",
+ "name": "MarketInfo",
+ "path": "/market-info",
+ "component": "/index/index",
+ "meta": {
+ "title": "市场基础信息",
+ "icon": "ri:price-tag-3-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "122",
+ "name": "BizCellSampleType",
+ "path": "cell-sample-type",
+ "component": "/basic-info/cell-sample-type/index",
+ "meta": {"title": "细胞样本基础信息维护", "keepAlive": True, "menuCode": "biz:cellSampleType:list"},
+ },
+ {
+ "id": "123",
+ "name": "BizCollectionRegion",
+ "path": "collection-region",
+ "component": "/basic-info/collection-region/index",
+ "meta": {"title": "细胞采集基础信息维护", "keepAlive": True, "menuCode": "biz:collectionRegion:list"},
+ },
+ {
+ "id": "80000000000000000000000000000002",
+ "name": "CellProductType",
+ "path": "cell-product-type",
+ "component": "/pilot-service/cell-product-type/index",
+ "meta": {"title": "细胞产品基础信息维护", "keepAlive": True, "menuCode": "pilot-service:cell-product-type:list"},
+ },
+ {
+ "id": "40000000000000000000000000000007",
+ "name": "BasicInfoInsuranceCompany",
+ "path": "insurance-company",
+ "component": "/basic-info/insurance-company/index",
+ "meta": {"title": "保险公司及险种价格", "keepAlive": True, "menuCode": "basic-info:insurance-company:list"},
+ },
+ {
+ "id": "80000000000000000000000000000007",
+ "name": "CheckupInstitution",
+ "path": "checkup-institution",
+ "component": "/pilot-service/checkup-institution/index",
+ "meta": {"title": "体检机构及项目价格", "keepAlive": True, "menuCode": "pilot-service:checkup-institution:list"},
+ },
+ {
+ "id": "50000000000000000000000000000009",
+ "name": "MarketMaterial",
+ "path": "material",
+ "component": "/cell-storage/material/index",
+ "meta": {"title": "市场物料库", "keepAlive": True, "menuCode": "biz:material:list"},
+ },
+ {
+ "id": "121",
+ "name": "BizCustomer",
+ "path": "customer",
+ "component": "/basic-info/customer/index",
+ "meta": {"title": "客户管理", "keepAlive": True, "menuCode": "biz:customer:list"},
+ },
+ {
+ "id": "80000000000000000000000000000006",
+ "name": "Channel",
+ "path": "channel",
+ "component": "/pilot-service/channel/index",
+ "meta": {"title": "渠道信息管理", "keepAlive": True, "menuCode": "pilot-service:channel:list"},
+ },
+ {
+ "id": "80000000000000000000000000000009",
+ "name": "ChannelServiceOrderFinance",
+ "path": "channel-service-order-finance",
+ "component": "/pilot-service/channel-service-order-finance/index",
+ "meta": {"title": "渠道服务订单(财务确认)", "keepAlive": True, "menuCode": "pilot-service:channel-service-order-finance:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 9. 质检基础信息
+ # ────────────────────────────────────────────
+ {
+ "id": "QCB00000000000000000000000000001",
+ "name": "QualityBase",
+ "path": "/quality-base",
+ "component": "/index/index",
+ "meta": {
+ "title": "质检基础信息",
+ "icon": "ri:clipboard-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "70000000000000000000000000000003",
+ "name": "QualityInspectionSampleReceptionCheckItem",
+ "path": "sample-reception-check-item",
+ "component": "/quality-inspection/sample-reception-check-item/index",
+ "meta": {"title": "接收检查项配置", "keepAlive": True, "menuCode": "biz:sampleReceptionCheckItem:list"},
+ },
+ {
+ "id": "70000000000000000000000000000006",
+ "name": "QualityInspectionLabTestItem",
+ "path": "lab-test-item",
+ "component": "/quality-inspection/lab-test-item/index",
+ "meta": {"title": "质检项目管理", "keepAlive": True, "menuCode": "biz:qcLabTestItem:list"},
+ },
+ {
+ "id": "70000000000000000000000000000009",
+ "name": "QualityInspectionQcMaterial",
+ "path": "qc-material",
+ "component": "/quality-inspection/qc-material/index",
+ "meta": {"title": "质检物料库", "keepAlive": True, "menuCode": "biz:qcMaterial:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 10. 生产基础信息
+ # ────────────────────────────────────────────
+ {
+ "id": "PRB00000000000000000000000000001",
+ "name": "ProductionBase",
+ "path": "/production-base",
+ "component": "/index/index",
+ "meta": {
+ "title": "生产基础信息",
+ "icon": "ri:tools-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "90000000000000000000000000000002",
+ "name": "ProductionConsumable",
+ "path": "consumable",
+ "component": "/production/consumable/index",
+ "meta": {"title": "耗材管理", "keepAlive": True, "menuCode": "production:consumable:list"},
+ },
+ {
+ "id": "90000000000000000000000000000003",
+ "name": "ProductionReagent",
+ "path": "reagent",
+ "component": "/production/reagent/index",
+ "meta": {"title": "试剂管理", "keepAlive": True, "menuCode": "production:reagent:list"},
+ },
+ {
+ "id": "90000000000000000000000000000004",
+ "name": "ProductionSop",
+ "path": "sop",
+ "component": "/production/sop/index",
+ "meta": {"title": "SOP文档", "keepAlive": True, "menuCode": "production:sop-document:list"},
+ },
+ {
+ "id": "90000000000000000000000000000005",
+ "name": "ProductionProcessCategory",
+ "path": "process-category",
+ "component": "/production/process-category/index",
+ "meta": {"title": "工艺基础信息维护", "keepAlive": True, "menuCode": "production:process-category:list"},
+ },
+ {
+ "id": "90000000000000000000000000000008",
+ "name": "ProductionProcess",
+ "path": "process",
+ "component": "/production/process/index",
+ "meta": {"title": "工艺库管理", "keepAlive": True, "menuCode": "production:process:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 11. 系统基础信息
+ # ────────────────────────────────────────────
+ {
+ "id": "SYSB000000000000000000000000000001",
+ "name": "SystemBase",
+ "path": "/system-base",
+ "component": "/index/index",
+ "meta": {
+ "title": "系统基础信息",
+ "icon": "ri:archive-line",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "107",
+ "name": "Site",
+ "path": "site",
+ "component": "/system/site/index",
+ "meta": {"title": "站点管理", "keepAlive": True, "menuCode": "system:site:list"},
+ },
+ {
+ "id": "109",
+ "name": "Unit",
+ "path": "unit",
+ "component": "/system/unit/index",
+ "meta": {"title": "物品单位管理", "keepAlive": True, "menuCode": "system:unit:list"},
+ },
+ {
+ "id": "110",
+ "name": "Express",
+ "path": "express",
+ "component": "/system/express/index",
+ "meta": {"title": "快递公司", "keepAlive": True, "menuCode": "system:express:list"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 12. 工作流管理
+ # ────────────────────────────────────────────
+ {
+ "id": "WKF00000000000000000000000000001",
+ "name": "Workflow",
+ "path": "/workflow",
+ "component": "/index/index",
+ "meta": {
+ "title": "工作流管理",
+ "icon": "workflow",
+ "keepAlive": False,
+ "roles": ["admin"],
+ },
+ "children": [
+ {
+ "id": "10000000000000000000000000000wf01",
+ "name": "WorkflowProcess",
+ "path": "process",
+ "component": "/system/workflow/process/index",
+ "meta": {"title": "流程配置", "keepAlive": True, "menuCode": "workflow:process"},
+ },
+ {
+ "id": "10000000000000000000000000000wf02",
+ "name": "WorkflowPending",
+ "path": "pending",
+ "component": "/system/workflow/task-pending/index",
+ "meta": {"title": "待办中心", "keepAlive": True, "menuCode": "workflow:pending"},
+ },
+ {
+ "id": "10000000000000000000000000000wf03",
+ "name": "WorkflowCompleted",
+ "path": "completed",
+ "component": "/system/workflow/task-completed/index",
+ "meta": {"title": "已办中心", "keepAlive": True, "menuCode": "workflow:completed"},
+ },
+ {
+ "id": "10000000000000000000000000000wf05",
+ "name": "WorkflowMyApplications",
+ "path": "my-applications",
+ "component": "/system/workflow/my-applications/index",
+ "meta": {"title": "我的申请", "keepAlive": True, "menuCode": "workflow:myApplications"},
+ },
+ {
+ "id": "10000000000000000000000000000wf04",
+ "name": "LeaveRequest",
+ "path": "leave",
+ "component": "/system/leave/index",
+ "meta": {"title": "请假申请", "keepAlive": True, "menuCode": "workflow:leave"},
+ },
+ ],
+ },
+
+ # ────────────────────────────────────────────
+ # 13. 系统管理
+ # ────────────────────────────────────────────
+ {
+ "id": "SYSM000000000000000000000000000001",
+ "name": "SystemManage",
"path": "/system",
"component": "/index/index",
"meta": {
@@ -34,6 +582,13 @@ STATIC_MENU_TREE = [
"component": "/system/dict/index",
"meta": {"title": "字典管理", "keepAlive": True, "menuCode": "system:dict:list"},
},
+ {
+ "id": "106",
+ "name": "Menu",
+ "path": "menu",
+ "component": "/system/menu/index",
+ "meta": {"title": "菜单管理", "keepAlive": True, "menuCode": "system:menu:list"},
+ },
{
"id": "103",
"name": "Dept",
@@ -41,20 +596,6 @@ STATIC_MENU_TREE = [
"component": "/system/dept/index",
"meta": {"title": "组织管理", "keepAlive": True, "menuCode": "system:dept:list"},
},
- {
- "id": "104",
- "name": "User",
- "path": "user",
- "component": "/system/user/index",
- "meta": {"title": "用户管理", "keepAlive": True, "menuCode": "system:user:list"},
- },
- {
- "id": "10000000000000000000000000000008",
- "name": "UserDetail",
- "path": "user/detail",
- "component": "/system/user/detail",
- "meta": {"title": "用户详情", "isHide": True, "keepAlive": True},
- },
{
"id": "105",
"name": "Role",
@@ -63,18 +604,11 @@ STATIC_MENU_TREE = [
"meta": {"title": "角色管理", "keepAlive": True, "menuCode": "system:role:list"},
},
{
- "id": "106",
- "name": "Menu",
- "path": "menu",
- "component": "/system/menu/index",
- "meta": {"title": "菜单管理", "keepAlive": True, "menuCode": "system:menu:list"},
- },
- {
- "id": "107",
- "name": "Site",
- "path": "site",
- "component": "/system/site/index",
- "meta": {"title": "站点管理", "keepAlive": True, "menuCode": "system:site:list"},
+ "id": "104",
+ "name": "User",
+ "path": "user",
+ "component": "/system/user/index",
+ "meta": {"title": "用户管理", "keepAlive": True, "menuCode": "system:user:list"},
},
{
"id": "108",
@@ -84,175 +618,15 @@ STATIC_MENU_TREE = [
"meta": {"title": "职位管理", "keepAlive": True, "menuCode": "system:post:list"},
},
{
- "id": "109",
- "name": "Unit",
- "path": "unit",
- "component": "/system/unit/index",
- "meta": {"title": "物品单位管理", "keepAlive": True, "menuCode": "system:unit:list"},
- },
- {
- "id": "110",
- "name": "Express",
- "path": "express",
- "component": "/system/express/index",
- "meta": {"title": "快递公司", "keepAlive": True, "menuCode": "system:express:list"},
+ "id": "10000000000000000000000000000008",
+ "name": "UserDetail",
+ "path": "user/detail",
+ "component": "/system/user/detail",
+ "meta": {"title": "用户详情", "isHide": True, "keepAlive": True},
},
],
- },
- {
- "id": "40000000000000000000000000000001",
- "name": "BasicInfo",
- "path": "/basic-info",
- "component": "/index/index",
- "meta": {
- "title": "基础信息管理",
- "icon": "ri:folder-user-line",
- "keepAlive": False,
- "roles": ["admin"],
- },
- "children": [
- {
- "id": "40000000000000000000000000000002",
- "name": "BizCustomer",
- "path": "customer",
- "component": "/basic-info/customer/index",
- "meta": {"title": "客户管理", "keepAlive": True, "menuCode": "biz:customer:list"},
- },
- {
- "id": "40000000000000000000000000000003",
- "name": "BizCellSampleType",
- "path": "cell-sample-type",
- "component": "/basic-info/cell-sample-type/index",
- "meta": {"title": "细胞样本类型管理", "keepAlive": True, "menuCode": "biz:cellSampleType:list"},
- },
- {
- "id": "40000000000000000000000000000004",
- "name": "BizCollectionRegion",
- "path": "collection-region",
- "component": "/basic-info/collection-region/index",
- "meta": {"title": "采集地区及医院管理", "keepAlive": True, "menuCode": "biz:collectionRegion:list"},
- },
- {
- "id": "40000000000000000000000000000005",
- "name": "BizCollectionProtocolNo",
- "path": "collection-protocol-no",
- "component": "/basic-info/collection-protocol-no/index",
- "meta": {"title": "采集协议号管理", "keepAlive": True, "menuCode": "biz:collectionProtocolNo:list"},
- },
- {
- "id": "40000000000000000000000000000006",
- "name": "BizSampleCode",
- "path": "sample-code",
- "component": "/basic-info/sample-code/index",
- "meta": {"title": "样本编码管理", "keepAlive": True, "menuCode": "biz:sampleCode:list"},
- },
- ],
- },
- {
- "id": "50000000000000000000000000000001",
- "name": "CellStorage",
- "path": "/cell-storage",
- "component": "/index/index",
- "meta": {
- "title": "细胞存储管理",
- "icon": "ri:test-tube-line",
- "keepAlive": False,
- "roles": ["admin"],
- },
- "children": [
- {
- "id": "50000000000000000000000000000002",
- "name": "CellStorageProtocol",
- "path": "protocol",
- "component": "/cell-storage/protocol/index",
- "meta": {"title": "采集协议单录入", "keepAlive": True},
- },
- {
- "id": "50000000000000000000000000000003",
- "name": "CellStorageProtocolAudit",
- "path": "protocol-audit",
- "component": "/cell-storage/protocol-audit/index",
- "meta": {"title": "采集协议单管理", "keepAlive": True},
- },
- {
- "id": "50000000000000000000000000000007",
- "name": "CellStoragePaymentInfo",
- "path": "payment-info",
- "component": "/cell-storage/payment-info/index",
- "meta": {"title": "缴费信息", "keepAlive": True},
- },
- {
- "id": "50000000000000000000000000000004",
- "name": "CellStoragePaymentConfirm",
- "path": "payment-confirm",
- "component": "/cell-storage/payment-confirm/index",
- "meta": {"title": "缴费确认(财务)", "keepAlive": True},
- },
- {
- "id": "50000000000000000000000000000005",
- "name": "CellStorageRefundConfirm",
- "path": "refund-confirm",
- "component": "/cell-storage/refund-confirm/index",
- "meta": {"title": "退费确认(财务)", "keepAlive": True},
- },
- ],
- },
- {
- "id": "70000000000000000000000000000001",
- "name": "QualityInspection",
- "path": "/quality-inspection",
- "component": "/index/index",
- "meta": {
- "title": "质量检测",
- "icon": "ri:shield-check-line",
- "keepAlive": False,
- "roles": ["admin"],
- },
- "children": [
- {
- "id": "70000000000000000000000000000002",
- "name": "QualityInspectionSampleReception",
- "path": "sample-reception",
- "component": "/quality-inspection/sample-reception/index",
- "meta": {"title": "样本接收", "keepAlive": True},
- },
- {
- "id": "70000000000000000000000000000003",
- "name": "QualityInspectionSampleReceptionCheckItem",
- "path": "sample-reception-check-item",
- "component": "/quality-inspection/sample-reception-check-item/index",
- "meta": {"title": "接收检查项配置", "keepAlive": True},
- },
- {
- "id": "70000000000000000000000000000004",
- "name": "QualityInspectionThirdPartyCompany",
- "path": "third-party-company",
- "component": "/quality-inspection/third-party-company/index",
- "meta": {"title": "第三方检测公司及项目", "keepAlive": True, "menuCode": "biz:qcThirdPartyCompany:list"},
- },
- {
- "id": "70000000000000000000000000000006",
- "name": "QualityInspectionLabTestItem",
- "path": "lab-test-item",
- "component": "/quality-inspection/lab-test-item/index",
- "meta": {"title": "实验室自检项目", "keepAlive": True, "menuCode": "biz:qcLabTestItem:list"},
- },
- {
- "id": "70000000000000000000000000000007",
- "name": "QualityInspectionQcWorkOrder",
- "path": "qc-work-order",
- "component": "/quality-inspection/qc-work-order/index",
- "meta": {"title": "入库质检工单", "keepAlive": True, "menuCode": "biz:qcWorkOrder:list"},
- },
- {
- "id": "70000000000000000000000000000008",
- "name": "QualityInspectionQcTask",
- "path": "qc-task",
- "component": "/quality-inspection/qc-task/index",
- "meta": {"title": "质检任务", "keepAlive": True, "menuCode": "biz:qcTask:list"},
- },
- ],
- },
+ }
+
]
diff --git a/backend/app/api/pilot_application.py b/backend/app/api/pilot_application.py
index c42a4269d..fa5cc1530 100644
--- a/backend/app/api/pilot_application.py
+++ b/backend/app/api/pilot_application.py
@@ -44,8 +44,9 @@ class PlanIdBody(BaseModel):
class ApplicationCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
- channel_service_order_id: str = Field(..., alias="channelServiceOrderId")
+ channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
remark: Optional[str] = None
+ biz_type: Optional[str] = Field(None, alias="bizType")
class ApplicationUpdateBody(BaseModel):
@@ -111,6 +112,7 @@ class ScheduleCreateBody(BaseModel):
plan_id: str = Field(..., alias="infusionPlanId")
infusion_time: Optional[str] = Field(None, alias="infusionTime")
remark: Optional[str] = None
+ amount: Optional[float] = None
# --- 复合保存 Schema(参考采集协议单一次性保存模式)---
@@ -202,6 +204,7 @@ async def api_list_applications(
channelId: Optional[str] = Query(None, alias="channelId"),
applicationStatus: Optional[str] = Query(None, alias="applicationStatus"),
channelServiceOrderId: Optional[str] = Query(None, alias="channelServiceOrderId"),
+ bizType: Optional[str] = Query(None, alias="bizType"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
@@ -215,6 +218,7 @@ async def api_list_applications(
application_status=applicationStatus,
channel_service_order_id=channelServiceOrderId,
keyword=keyword,
+ biz_type=bizType,
)
return success_response(result)
@@ -241,8 +245,9 @@ async def api_create_application(
try:
tenant_no = _get_tenant_no(db, user)
data = {
- "channelServiceOrderId": body.channel_service_order_id,
+ "channelServiceOrderId": body.channel_service_order_id or "",
"remark": body.remark,
+ "bizType": body.biz_type or "pilot_service",
}
result = pilot_application.create_application(
db,
@@ -638,6 +643,7 @@ async def api_create_schedule(
data = {
"infusionTime": body.infusion_time,
"remark": body.remark,
+ "amount": body.amount,
}
result = pilot_application.create_schedule(
db,
diff --git a/backend/app/crud/clinical_payment.py b/backend/app/crud/clinical_payment.py
new file mode 100644
index 000000000..ed1bc6fdf
--- /dev/null
+++ b/backend/app/crud/clinical_payment.py
@@ -0,0 +1,383 @@
+"""临床应用缴费 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)
diff --git a/backend/app/crud/pilot_application.py b/backend/app/crud/pilot_application.py
index d821268f1..661b3f42f 100644
--- a/backend/app/crud/pilot_application.py
+++ b/backend/app/crud/pilot_application.py
@@ -71,9 +71,9 @@ def _release_application_no_for_soft_delete(application_no: str, row_id: str) ->
return f"{base[:max_base_len]}{suffix}"
-def _generate_application_no(db: Session, tenant_no: str) -> str:
+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"ZSSQ{today_str}"
+ prefix = f"LCSQ{today_str}" if biz_type == "clinical_application" else f"ZSSQ{today_str}"
existing = (
db.query(BizPilotApplication.application_no)
.filter(
@@ -99,6 +99,7 @@ def _row_to_dict_application(row, db=None, tenant_no=None) -> Dict[str, Any]:
"applicationNo": row.application_no or "",
"applicationName": row.application_name or "",
"applicationStatus": row.application_status or "",
+ "bizType": row.biz_type or "pilot_service",
"channelServiceOrderId": row.channel_service_order_id or "",
"channelId": row.channel_id or "",
"channelName": row.channel_name or "",
@@ -268,6 +269,7 @@ def _row_to_dict_infusion_schedule(row, cell_products: Optional[List[Dict[str, A
"qcStatus": row.qc_status or "dzc",
"cellProducts": cell_products if cell_products is not None else [],
"attachments": row.attachments or "",
+ "amount": float(row.amount) if row.amount else None,
"tenantNo": row.tenant_no or "",
"remark": row.remark or "",
"createBy": row.create_by or "",
@@ -290,6 +292,7 @@ def list_applications(
application_status: Optional[str] = None,
channel_service_order_id: Optional[str] = None,
keyword: Optional[str] = None,
+ biz_type: Optional[str] = None,
):
q = db.query(BizPilotApplication).filter(
BizPilotApplication.tenant_no == tenant_no,
@@ -303,6 +306,8 @@ def list_applications(
q = q.filter(BizPilotApplication.application_status.in_(statuses))
if channel_service_order_id:
q = q.filter(BizPilotApplication.channel_service_order_id == channel_service_order_id)
+ if biz_type:
+ q = q.filter(BizPilotApplication.biz_type == biz_type)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
@@ -390,25 +395,28 @@ def create_application(
dept_id: str,
) -> Dict[str, Any]:
channel_service_order_id = data.get("channelServiceOrderId") or ""
- if not channel_service_order_id:
+ biz_type = data.get("bizType") or "pilot_service"
+ if not channel_service_order_id and biz_type != "clinical_application":
raise ValueError("请选择渠道服务订单")
# Validate service order exists and is active
- svc_order = (
- db.query(BizChannelServiceOrder)
- .filter(
- BizChannelServiceOrder.id == channel_service_order_id,
- BizChannelServiceOrder.del_flag == 0,
+ svc_order = None
+ if channel_service_order_id:
+ svc_order = (
+ db.query(BizChannelServiceOrder)
+ .filter(
+ BizChannelServiceOrder.id == channel_service_order_id,
+ BizChannelServiceOrder.del_flag == 0,
+ )
+ .first()
)
- .first()
- )
- if not svc_order:
- raise ValueError("渠道服务订单不存在")
- if svc_order.order_status != "sxz":
- raise ValueError("渠道服务订单未生效,无法创建中试申请")
+ if not svc_order:
+ raise ValueError("渠道服务订单不存在")
+ if svc_order.order_status != "sxz":
+ raise ValueError("渠道服务订单未生效,无法创建中试申请")
now = datetime.now()
- application_no = _generate_application_no(db, tenant_no)
+ application_no = _generate_application_no(db, tenant_no, biz_type=biz_type)
row = BizPilotApplication(
id=str(uuid.uuid4()),
@@ -416,8 +424,8 @@ def create_application(
application_name=data.get("applicationName") or "",
application_status="dks",
channel_service_order_id=channel_service_order_id,
- channel_id=svc_order.channel_id or "",
- channel_name=svc_order.channel_name or "",
+ channel_id=svc_order.channel_id or "" if svc_order else "",
+ channel_name=svc_order.channel_name or "" if svc_order else "",
tenant_no=tenant_no,
order_num=0,
remark=data.get("remark") or "",
@@ -427,6 +435,7 @@ def create_application(
update_by=user_id,
update_time=now,
del_flag=0,
+ biz_type=biz_type,
)
db.add(row)
db.commit()
@@ -1129,6 +1138,7 @@ def create_schedule(
infusion_status=data.get("infusionStatus") or "dtz",
qc_status=data.get("qcStatus") or "dzc",
attachments=data.get("attachments") or "",
+ amount=data.get("amount") if data.get("amount") else None,
tenant_no=tenant_no,
order_num=data.get("sortNo") or data.get("orderNum") or 0,
remark=data.get("remark") or "",
@@ -1164,6 +1174,33 @@ def create_schedule(
user_id=user_id,
create_dept_id=dept_id,
)
+ # 临床应用自动创建缴费记录
+ plan_row = (
+ db.query(BizPilotInfusionPlan)
+ .filter(BizPilotInfusionPlan.id == plan_id, BizPilotInfusionPlan.del_flag == 0)
+ .first()
+ )
+ if plan_row:
+ application = (
+ db.query(BizPilotApplication)
+ .filter(
+ BizPilotApplication.id == plan_row.pilot_application_id,
+ BizPilotApplication.del_flag == 0,
+ )
+ .first()
+ )
+ if application and 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,
+ )
if commit:
db.commit()
db.refresh(row)
diff --git a/backend/app/crud/production_work_order.py b/backend/app/crud/production_work_order.py
index 77cbb7c14..2f3734110 100644
--- a/backend/app/crud/production_work_order.py
+++ b/backend/app/crud/production_work_order.py
@@ -179,6 +179,49 @@ def _enrich_outbound_items(db: Session, items: List[Dict[str, Any]]) -> List[Dic
return items
+def _outbound_work_order_ids_by_keyword(db: Session, tenant_no: str, keyword: str) -> List[str]:
+ """出库工单关键词:按关联申请单号、申请单名称匹配。"""
+ from app.models.models import (
+ BizPilotApplication,
+ BizPilotInfusionPlan,
+ BizPilotInfusionSchedule,
+ )
+
+ kw = (keyword or "").strip()
+ if not kw:
+ return []
+ like = f"%{kw}%"
+ rows = (
+ db.query(BizProductionWorkOrder.id)
+ .join(
+ BizPilotInfusionSchedule,
+ BizProductionWorkOrder.infusion_schedule_id == BizPilotInfusionSchedule.id,
+ )
+ .join(
+ BizPilotInfusionPlan,
+ BizPilotInfusionSchedule.infusion_plan_id == BizPilotInfusionPlan.id,
+ )
+ .join(
+ BizPilotApplication,
+ BizPilotInfusionPlan.pilot_application_id == BizPilotApplication.id,
+ )
+ .filter(
+ BizProductionWorkOrder.del_flag == 0,
+ BizProductionWorkOrder.tenant_no == tenant_no,
+ BizProductionWorkOrder.work_order_type == "outbound",
+ BizPilotInfusionSchedule.del_flag == 0,
+ BizPilotInfusionPlan.del_flag == 0,
+ BizPilotApplication.del_flag == 0,
+ or_(
+ BizPilotApplication.application_no.like(like),
+ BizPilotApplication.application_name.like(like),
+ ),
+ )
+ .all()
+ )
+ return [r[0] for r in rows]
+
+
def _reception_source_label(source: Optional[str]) -> str:
key = (source or "").strip() or "cell_storage"
return _RECEPTION_SOURCE_LABELS.get(key, key)
@@ -410,13 +453,15 @@ def list_production_work_orders(
kw = (query.keyword or "").strip()
if kw:
like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionWorkOrder.work_order_no.like(like),
- BizProductionWorkOrder.protocol_no.like(like),
- BizProductionWorkOrder.sample_code.like(like),
- )
- )
+ keyword_conditions = [
+ BizProductionWorkOrder.work_order_no.like(like),
+ BizProductionWorkOrder.protocol_no.like(like),
+ BizProductionWorkOrder.sample_code.like(like),
+ ]
+ outbound_ids = _outbound_work_order_ids_by_keyword(db, tenant_no, kw)
+ if outbound_ids:
+ keyword_conditions.append(BizProductionWorkOrder.id.in_(outbound_ids))
+ q0 = q0.filter(or_(*keyword_conditions))
elif query.work_order_no and query.work_order_no.strip():
like = f"%{query.work_order_no.strip()}%"
q0 = q0.filter(BizProductionWorkOrder.work_order_no.like(like))
diff --git a/backend/app/crud/qc_work_order.py b/backend/app/crud/qc_work_order.py
index 8af173133..ec4209821 100644
--- a/backend/app/crud/qc_work_order.py
+++ b/backend/app/crud/qc_work_order.py
@@ -195,6 +195,45 @@ def _enrich_outbound_work_orders(
return items
+def _outbound_qc_work_order_ids_by_keyword(
+ db: Session, tenant_no: str, keyword: str
+) -> List[str]:
+ """出库质检工单关键词:按关联申请单号、申请单名称匹配。"""
+ kw = (keyword or "").strip()
+ if not kw:
+ return []
+ like = f"%{kw}%"
+ rows = (
+ db.query(BizQcWorkOrder.id)
+ .join(
+ BizPilotInfusionSchedule,
+ BizQcWorkOrder.infusion_schedule_id == BizPilotInfusionSchedule.id,
+ )
+ .join(
+ BizPilotInfusionPlan,
+ BizPilotInfusionSchedule.infusion_plan_id == BizPilotInfusionPlan.id,
+ )
+ .join(
+ BizPilotApplication,
+ BizPilotInfusionPlan.pilot_application_id == BizPilotApplication.id,
+ )
+ .filter(
+ BizQcWorkOrder.del_flag == 0,
+ BizQcWorkOrder.tenant_no == tenant_no,
+ BizQcWorkOrder.work_order_type == "outbound",
+ BizPilotInfusionSchedule.del_flag == 0,
+ BizPilotInfusionPlan.del_flag == 0,
+ BizPilotApplication.del_flag == 0,
+ or_(
+ BizPilotApplication.application_no.like(like),
+ BizPilotApplication.application_name.like(like),
+ ),
+ )
+ .all()
+ )
+ return [r[0] for r in rows]
+
+
def _accept_conclusion_label(v: Optional[str]) -> str:
key = (v or "").strip()
return _ACCEPT_CONCLUSION_LABELS.get(key, key)
@@ -565,13 +604,16 @@ def list_work_orders(
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizQcWorkOrder.work_order_no.like(like),
- BizQcWorkOrder.protocol_no.like(like),
- BizQcWorkOrder.sample_code.like(like),
- )
- )
+ keyword_conditions = [
+ BizQcWorkOrder.work_order_no.like(like),
+ BizQcWorkOrder.protocol_no.like(like),
+ BizQcWorkOrder.sample_code.like(like),
+ ]
+ if tenant_no:
+ outbound_ids = _outbound_qc_work_order_ids_by_keyword(db, tenant_no, kw)
+ if outbound_ids:
+ keyword_conditions.append(BizQcWorkOrder.id.in_(outbound_ids))
+ q0 = q0.filter(or_(*keyword_conditions))
st = (work_order_status or "").strip()
if st:
q0 = q0.filter(BizQcWorkOrder.work_order_status == st)
diff --git a/backend/app/main.py b/backend/app/main.py
index 1569a72ea..97b1d734e 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
from app.core.api_crypto_middleware import ApiCryptoMiddleware
from app.models import models # noqa: F401
-from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router, ethnic_router, sample_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router
+from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router, ethnic_router, sample_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router
@asynccontextmanager
@@ -123,6 +123,7 @@ app.include_router(pilot_delivery_router, prefix=settings.API_V1_PREFIX)
app.include_router(pilot_infusion_confirm_router, prefix=settings.API_V1_PREFIX)
app.include_router(pilot_infusion_schedule_mgmt_router, prefix=settings.API_V1_PREFIX)
app.include_router(outbound_release_review_router, prefix=settings.API_V1_PREFIX)
+app.include_router(clinical_payment_router, prefix=settings.API_V1_PREFIX)
def _mask_secret(value: str) -> str:
diff --git a/backend/app/models/clinical_payment.py b/backend/app/models/clinical_payment.py
new file mode 100644
index 000000000..7c8dc78ef
--- /dev/null
+++ b/backend/app/models/clinical_payment.py
@@ -0,0 +1,41 @@
+"""临床应用缴费记录模型"""
+from sqlalchemy import Column, String, Text, SmallInteger, Numeric, DateTime
+from sqlalchemy.sql import func
+from app.models.base import Base
+
+
+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="删除标记")
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index 4b6963e06..4b6511268 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -2826,6 +2826,7 @@ class BizPilotApplication(Base):
channel_service_order_id = Column(String(64), nullable=False, comment="渠道服务购买订单ID")
channel_id = Column(String(64), nullable=True, comment="渠道ID(缓存自服务订单)")
channel_name = Column(String(200), nullable=True, comment="渠道名称(缓存)")
+ biz_type = Column(String(20), nullable=False, default="pilot_service", comment="业务类型: pilot_service=中试服务, clinical_application=临床应用")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
@@ -2907,6 +2908,7 @@ class BizPilotInfusionSchedule(Base):
infusion_status = Column(String(16), nullable=False, default="dtz", comment="dtz待输注 yrqr已用户确认 yqx已取消 ywc已完成")
cancel_reason = Column(String(512), nullable=True, default=None, comment="取消原因")
qc_status = Column(String(16), nullable=False, default="dzc", comment="dzc待质检 yzj已质检 yqx已取消")
+ amount = Column(Numeric(18, 2), nullable=True, comment="临床应用-手动填写的输注金额(中试为NULL)")
attachments = Column(String(4000), nullable=True, comment="附件逗号分隔")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
@@ -3251,3 +3253,4 @@ class BizOutboundReleaseReview(Base):
from app.models.qc_param import BizQcItemParam, BizQcTaskResult # noqa: E402,F401
from app.models.qc_material import BizQcMaterial, BizQcMaterialUsage # noqa: E402,F401
from app.models.material import BizMaterial, BizMaterialStockLog # noqa: E402,F401
+from app.models.clinical_payment import BizPilotPayment # noqa: E402,F401
diff --git a/backend/scripts/reorder_web_menus.py b/backend/scripts/reorder_web_menus.py
new file mode 100644
index 000000000..ccd5be5f7
--- /dev/null
+++ b/backend/scripts/reorder_web_menus.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""重排 Web 端一级菜单顺序,并生成 sys_menu 初始化 SQL。"""
+from __future__ import annotations
+
+import ast
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+MENU_PY = ROOT / "backend/app/api/menu.py"
+SQXP_TS = ROOT / "web-admin/src/router/modules/sqxp.ts"
+SQL_OUT = ROOT / "docs/sql/sys_menu_web_init.sql"
+
+BLOCK_ORDER = [2, 3, 4, 5, 7, 9, 10, 1, 6, 8, 11, 12, 13]
+
+BLOCK_TITLES = {
+ 1: "市场基础信息",
+ 2: "细胞存储",
+ 3: "科研捐赠",
+ 4: "中试服务",
+ 5: "临床应用",
+ 6: "质检基础信息",
+ 7: "质检管理",
+ 8: "生产基础信息",
+ 9: "生产管理",
+ 10: "运营支撑",
+ 11: "系统基础信息",
+ 12: "工作流管理",
+ 13: "系统管理",
+}
+
+SQXP_HEADER = """import type { AppRouteRecord } from '@/types/router'
+
+/**
+ * 前端路由(13 个一级菜单:细胞存储、科研捐赠、中试服务、临床应用、
+ * 质检管理、生产管理、运营支撑、市场基础信息、质检基础信息、生产基础信息、
+ * 系统基础信息、工作流管理、系统管理)
+ * 与后端菜单对齐时请同步 `backend/app/api/menu.py`。
+ */
+export const sqxpAdminRoutes: AppRouteRecord[] = [
+"""
+
+MENU_TAIL = (
+ "\n]\n\n\n"
+ '@router.get("/list")\n'
+ "async def get_menu_list(_user: dict = Depends(require_admin_or_super_admin)):\n"
+ ' """获取菜单列表(与当前前端路由一致)"""\n'
+ ' return success_response({"menuList": STATIC_MENU_TREE})\n'
+)
+
+BLOCK_RE_TS = re.compile(
+ r" // ──+\n // (\d+)\. ([^\n]+)\n // ──+\n",
+ re.MULTILINE,
+)
+BLOCK_RE_PY = re.compile(
+ r" # ──+\n # (\d+)\. ([^\n]+)\n # ──+\n",
+ re.MULTILINE,
+)
+
+
+def split_menu_blocks(
+ content: str, array_start_marker: str, block_re: re.Pattern
+) -> tuple[str, dict[int, str]]:
+ start = content.index(array_start_marker) + len(array_start_marker)
+ depth = 1
+ i = start
+ while i < len(content) and depth > 0:
+ if content[i] == "[":
+ depth += 1
+ elif content[i] == "]":
+ depth -= 1
+ i += 1
+ array_body = content[start : i - 1]
+ prefix = content[:start]
+
+ parts = block_re.split(array_body)
+ blocks: dict[int, str] = {}
+ idx = 1
+ while idx < len(parts):
+ num = int(parts[idx])
+ block = parts[idx + 2].rstrip()
+ if block.endswith(","):
+ block = block[:-1]
+ blocks[num] = block.rstrip()
+ idx += 3
+ return prefix, blocks
+
+
+def reorder_blocks(blocks: dict[int, str]) -> list[tuple[int, str, str]]:
+ ordered = []
+ for new_idx, old_num in enumerate(BLOCK_ORDER, start=1):
+ ordered.append((new_idx, BLOCK_TITLES[old_num], blocks[old_num]))
+ return ordered
+
+
+def assemble_blocks(ordered: list[tuple[int, str, str]], *, ts: bool) -> str:
+ chunks = []
+ for num, title, body in ordered:
+ if ts:
+ sep = " // " + "─" * 46
+ chunks.append(f"{sep}\n // {num}. {title}\n{sep}\n{body}")
+ else:
+ sep = " # " + "─" * 44
+ chunks.append(f"{sep}\n # {num}. {title}\n{sep}\n{body}")
+ return ",\n\n".join(chunks) + "\n"
+
+
+def load_static_menu_tree() -> list:
+ content = MENU_PY.read_text(encoding="utf-8")
+ marker = "STATIC_MENU_TREE = "
+ start = content.index(marker) + len(marker)
+ depth = 0
+ i = start
+ while i < len(content):
+ ch = content[i]
+ if ch == "[":
+ depth += 1
+ elif ch == "]":
+ depth -= 1
+ if depth == 0:
+ i += 1
+ break
+ i += 1
+ literal = content[start:i]
+ return ast.literal_eval(literal)
+
+
+def sql_escape(val: str | None) -> str:
+ if val is None:
+ return "NULL"
+ return "'" + str(val).replace("'", "''") + "'"
+
+
+def walk_routes(nodes: list, parent_id: str, order_counter: dict, rows: list) -> None:
+ for node in nodes:
+ node_id = node["id"]
+ meta = node.get("meta") or {}
+ title = meta.get("title") or node.get("name") or ""
+ path = node.get("path") or ""
+ component = node.get("component") or ""
+ children = node.get("children") or []
+ is_hide = meta.get("isHide") is True
+ menu_code = meta.get("menuCode")
+ perms = menu_code if menu_code else None
+
+ if parent_id == "2":
+ order_counter["web"] += 1
+ order_num = order_counter["web"]
+ else:
+ order_counter.setdefault(parent_id, 0)
+ order_counter[parent_id] += 1
+ order_num = order_counter[parent_id]
+
+ menu_type = "M" if children else "C"
+ visible = "0" if is_hide else "1"
+
+ rows.append(
+ (
+ node_id,
+ title,
+ parent_id,
+ menu_type,
+ path,
+ component or None,
+ perms,
+ visible,
+ order_num,
+ )
+ )
+
+ if children:
+ walk_routes(children, node_id, order_counter, rows)
+
+
+def generate_sql(tree: list) -> str:
+ rows: list = []
+ order_counter: dict = {"web": 0}
+ walk_routes(tree, "2", order_counter, rows)
+
+ lines = [
+ "-- Web 端 sys_menu 初始化(根节点 + 13 个一级业务菜单及子菜单)",
+ "-- 生成:backend/scripts/reorder_web_menus.py",
+ "-- 一级顺序:细胞存储 → 科研捐赠 → 中试服务 → 临床应用 → 质检管理 → 生产管理",
+ "-- → 运营支撑 → 市场基础信息 → 质检基础信息 → 生产基础信息",
+ "-- → 系统基础信息 → 工作流管理 → 系统管理",
+ "",
+ "SET NAMES utf8mb4;",
+ "",
+ "INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES",
+ "('1', '移动端菜单', '0', 'M', '/mini', NULL, NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),",
+ "('2', 'Web端菜单', '0', 'M', '/web', NULL, NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);",
+ "",
+ "INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES",
+ ]
+
+ value_lines = []
+ for row in rows:
+ (
+ node_id,
+ title,
+ parent_id,
+ menu_type,
+ path,
+ component,
+ perms,
+ visible,
+ order_num,
+ ) = row
+ value_lines.append(
+ f"({sql_escape(node_id)}, {sql_escape(title)}, {sql_escape(parent_id)}, "
+ f"{sql_escape(menu_type)}, {sql_escape(path)}, {sql_escape(component)}, "
+ f"{sql_escape(perms)}, 1, {sql_escape(visible)}, '1', 'zkxw', {order_num}, "
+ f"NULL, NULL, '', NOW(), NULL, NOW(), 0)"
+ )
+
+ lines.append(",\n".join(value_lines) + ";")
+ lines.append("")
+ return "\n".join(lines)
+
+
+def main() -> None:
+ menu_content = MENU_PY.read_text(encoding="utf-8")
+ pre, blocks = split_menu_blocks(menu_content, "STATIC_MENU_TREE = [\n", BLOCK_RE_PY)
+ body = assemble_blocks(reorder_blocks(blocks), ts=False)
+ MENU_PY.write_text(pre + body + MENU_TAIL, encoding="utf-8")
+ print(f"updated {MENU_PY.relative_to(ROOT)}")
+
+ ts_content = SQXP_TS.read_text(encoding="utf-8")
+ _, blocks_ts = split_menu_blocks(
+ ts_content, "export const sqxpAdminRoutes: AppRouteRecord[] = [\n", BLOCK_RE_TS
+ )
+ body_ts = assemble_blocks(reorder_blocks(blocks_ts), ts=True)
+ SQXP_TS.write_text(SQXP_HEADER + body_ts + "]\n", encoding="utf-8")
+ print(f"updated {SQXP_TS.relative_to(ROOT)}")
+
+ tree = load_static_menu_tree()
+ sql = generate_sql(tree)
+ SQL_OUT.write_text(sql, encoding="utf-8")
+ row_count = sql.count("('") - 2 # minus root inserts
+ print(f"wrote {SQL_OUT.relative_to(ROOT)} (~{row_count} menu rows, {len(tree)} top-level menus)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/demos/consumable-merge-plan-b-demo.html b/docs/demos/consumable-merge-plan-b-demo.html
new file mode 100644
index 000000000..d0eeeac6b
--- /dev/null
+++ b/docs/demos/consumable-merge-plan-b-demo.html
@@ -0,0 +1,953 @@
+
+
+
+
+
+ 方案 B · 耗材管理融合页 UI Demo
+
+
+
+
+
+
+
+
+
+
+
+ 生产管理 / 耗材管理
+ 方案 B 静态 Demo · 非真实数据
+
+
+
+
+ 方案 B:两级 Tab — 一级区分「耗材 / 耗材包」;仅在「耗材」下出现二级 Tab「耗材列表 /
+ 变动记录」。侧栏合并后仅保留一个菜单「耗材管理」(灰线划掉的为将移除的菜单项示意)。
+ 用浏览器直接打开本文件即可预览,路径:docs/demos/consumable-merge-plan-b-demo.html
+
+
+
+
+
耗材管理
+ 融合页 · 方案 B
+
+
+
+
+
+
+
+
+
+
+
耗材视图
+
+
+
+
+
+
+
+ 当前结构:一级「耗材」 → 二级「耗材列表」(筛选 + 工具栏 + 表格 + 分页)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
按编码、名称模糊查询,可按状态筛选。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
按耗材编码/名称、变动类型和时间范围查询变动记录。
+
+
+
+
+
+
+
+
+
+
+
+ | 序号 |
+ 耗材编码 |
+ 耗材名称 |
+ 变动类型 |
+ 变动数量 |
+ 变动后库存 |
+ 操作人 |
+ 变动时间 |
+
+
+
+
+ | 1 |
+ HC-2024-001 |
+ 细胞培养瓶 25cm² |
+ 入库 |
+ +50 |
+ 128 |
+ 张三 |
+ 2026-05-26 14:32 |
+
+
+ | 2 |
+ HC-2024-018 |
+ 无菌离心管 15mL |
+ 出库 |
+ -20 |
+ 12 |
+ 李四 |
+ 2026-05-25 09:15 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
按编号、名称模糊查询。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/sql/sys_menu_web_init.sql b/docs/sql/sys_menu_web_init.sql
new file mode 100644
index 000000000..07aa9813b
--- /dev/null
+++ b/docs/sql/sys_menu_web_init.sql
@@ -0,0 +1,112 @@
+-- Web 端 sys_menu 初始化(根节点 + 13 个一级业务菜单及子菜单)
+-- 生成:backend/scripts/reorder_web_menus.py
+-- 一级顺序:细胞存储 → 科研捐赠 → 中试服务 → 临床应用 → 质检管理 → 生产管理
+-- → 运营支撑 → 市场基础信息 → 质检基础信息 → 生产基础信息
+-- → 系统基础信息 → 工作流管理 → 系统管理
+
+SET NAMES utf8mb4;
+
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
+('1', '移动端菜单', '0', 'M', '/mini', NULL, NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('2', 'Web端菜单', '0', 'M', '/web', NULL, NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);
+
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
+('CEL00000000000000000000000000001', '细胞存储', '2', 'M', '/cell-storage', '/index/index', NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('50000000000000000000000000000002', '采集协议单录入', 'CEL00000000000000000000000000001', 'C', 'protocol', '/cell-storage/protocol/index', 'biz:cellStorageProtocol:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('50000000000000000000000000000003', '采集协议单管理', 'CEL00000000000000000000000000001', 'C', 'protocol-audit', '/cell-storage/protocol-audit/index', 'biz:cellStorageProtocolAudit:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 缴费信息/样本接收/样本入库/储存证/满月待办已并入采集协议单管理 Tab;已有库可执行:
+-- UPDATE sys_menu SET del_flag=1 WHERE id IN ('904b5173519411f1944300163e3bdd96','50000000000000000000000000000011','50000000000000000000000000000012','50000000000000000000000000000006','50000000000000000000000000000008');
+('50000000000000000000000000000004', '缴退费财务确认', 'CEL00000000000000000000000000001', 'C', 'payment-confirm', '/cell-storage/payment-confirm/index', 'biz:cellStoragePaymentConfirm:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('70000000000000000000000000000011', '入库放行审核', 'CEL00000000000000000000000000001', 'C', 'storage-release-review', '/quality-inspection/storage-release-review/index', 'biz:storageReleaseReview:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 退费确认已并入缴退费财务确认 Tab;已有库可执行:UPDATE sys_menu SET menu_name='缴退费财务确认' WHERE id='50000000000000000000000000000004'; UPDATE sys_menu SET del_flag=1 WHERE id='50000000000000000000000000000005';
+-- 入库放行审核已移至细胞存储;已有库可执行:UPDATE sys_menu SET parent_id='CEL00000000000000000000000000001', order_num=5 WHERE id='70000000000000000000000000000011';
+('RSC00000000000000000000000000001', '科研捐赠', '2', 'M', '/research-donation', '/index/index', NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('60000000000000000000000000000002', '捐赠协议单录入', 'RSC00000000000000000000000000001', 'C', 'list', '/research-donation/list/index', 'biz:researchDonation:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('60000000000000000000000000000003', '捐赠协议单管理', 'RSC00000000000000000000000000001', 'C', 'audit', '/research-donation/audit/index', 'biz:researchDonationAudit:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('PIL00000000000000000000000000001', '中试服务', '2', 'M', '/pilot-service', '/index/index', NULL, 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('80000000000000000000000000000010', '中试服务申请单', 'PIL00000000000000000000000000001', 'C', 'pilot-application', '/pilot-service/pilot-application/index', 'pilot-service:pilot-application:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 体检管理/输注计划已并入中试服务申请单 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id IN ('PIL00000000000000000000000000002','PIL00000000000000000000000000003');
+('PIL00000000000000000000000000004', '出库放行审核', 'PIL00000000000000000000000000001', 'C', 'outbound-release-review', '/pilot-service/outbound-release-review/index', 'pilot-service:outbound-release-review:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000001', '临床应用', '2', 'M', '/clinical-application', '/index/index', NULL, 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000002', '临床申请单', 'CLI00000000000000000000000000001', 'C', 'clinical-application', '/pilot-service/pilot-application/index', 'clinical-application:application:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000003', '体检管理', 'CLI00000000000000000000000000001', 'C', 'clinical-checkup-management', '/pilot-service/checkup-management/index', 'clinical-application:checkup-management:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000004', '输注计划管理', 'CLI00000000000000000000000000001', 'C', 'clinical-infusion-schedule-management', '/pilot-service/infusion-schedule-management/index', 'clinical-application:infusion-schedule-management:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000005', '缴费信息', 'CLI00000000000000000000000000001', 'C', 'clinical-payment-info', '/pilot-service/clinical-payment-info/index', 'clinical-application:payment-info:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000006', '缴费确认(财务)', 'CLI00000000000000000000000000001', 'C', 'clinical-payment-confirm', '/pilot-service/clinical-payment-confirm/index', 'clinical-application:payment-confirm:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000007', '退费确认(财务)', 'CLI00000000000000000000000000001', 'C', 'clinical-refund-confirm', '/pilot-service/clinical-refund-confirm/index', 'clinical-application:refund-confirm:list', 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('CLI00000000000000000000000000008', '出库放行审核', 'CLI00000000000000000000000000001', 'C', 'clinical-outbound-release-review', '/pilot-service/outbound-release-review/index', 'clinical-application:outbound-release-review:list', 1, '1', '1', 'zkxw', 7, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('QCM00000000000000000000000000001', '质检管理', '2', 'M', '/quality-manage', '/index/index', NULL, 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('70000000000000000000000000000002', '样本接收', 'QCM00000000000000000000000000001', 'C', 'sample-reception', '/quality-inspection/sample-reception/index', 'biz:sampleReception:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('70000000000000000000000000000007', '质检管理', 'QCM00000000000000000000000000001', 'C', 'qc-work-order', '/quality-inspection/qc-work-order/index', 'biz:qcWorkOrder:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 质检任务已并入质检管理 Tab;已有库可执行:
+-- UPDATE sys_menu SET menu_name='质检管理' WHERE id='70000000000000000000000000000007';
+-- UPDATE sys_menu SET del_flag=1 WHERE id='70000000000000000000000000000008';
+('PRM00000000000000000000000000001', '生产管理', '2', 'M', '/production', '/index/index', NULL, 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('61000000000000000000000000000003', '库位管理', 'PRM00000000000000000000000000001', 'C', 'storage-facility', '/operations/storage-facility/index', 'operations:storage-facility:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('90000000000000000000000000000013', '细胞产品库', 'PRM00000000000000000000000000001', 'C', 'cell-product', '/production/cell-product/index', 'production:cell-product:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('90000000000000000000000000000011', '生产管理', 'PRM00000000000000000000000000001', 'C', 'production-work-order', '/production/production-work-order/index', 'production:work-order:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 生产任务已并入生产管理 Tab;已有库可执行:
+-- UPDATE sys_menu SET menu_name='生产管理' WHERE id='90000000000000000000000000000011';
+-- UPDATE sys_menu SET del_flag=1 WHERE id='90000000000000000000000000000012';
+('OPS00000000000000000000000000001', '运营支撑', '2', 'M', '/operations', '/index/index', NULL, 1, '1', '1', 'zkxw', 7, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('61000000000000000000000000000002', '洁净室管理', 'OPS00000000000000000000000000001', 'C', 'clean-room', '/operations/clean-room/index', 'operations:clean-room:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('61000000000000000000000000000004', '展馆预约', 'OPS00000000000000000000000000001', 'C', 'exhibition-appointment', '/operations/exhibition-appointment/index', 'operations:exhibition-appointment:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('MKT00000000000000000000000000001', '市场基础信息', '2', 'M', '/market-info', '/index/index', NULL, 1, '1', '1', 'zkxw', 8, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('122', '细胞样本基础信息维护', 'MKT00000000000000000000000000001', 'C', 'cell-sample-type', '/basic-info/cell-sample-type/index', 'biz:cellSampleType:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('123', '细胞采集基础信息维护', 'MKT00000000000000000000000000001', 'C', 'collection-region', '/basic-info/collection-region/index', 'biz:collectionRegion:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('80000000000000000000000000000002', '细胞产品基础信息维护', 'MKT00000000000000000000000000001', 'C', 'cell-product-type', '/pilot-service/cell-product-type/index', 'pilot-service:cell-product-type:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('40000000000000000000000000000007', '保险公司及险种价格', 'MKT00000000000000000000000000001', 'C', 'insurance-company', '/basic-info/insurance-company/index', 'basic-info:insurance-company:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('80000000000000000000000000000007', '体检机构及项目价格', 'MKT00000000000000000000000000001', 'C', 'checkup-institution', '/pilot-service/checkup-institution/index', 'pilot-service:checkup-institution:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('50000000000000000000000000000009', '市场物料库', 'MKT00000000000000000000000000001', 'C', 'material', '/cell-storage/material/index', 'biz:material:list', 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('121', '客户管理', 'MKT00000000000000000000000000001', 'C', 'customer', '/basic-info/customer/index', 'biz:customer:list', 1, '1', '1', 'zkxw', 7, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('80000000000000000000000000000006', '渠道信息管理', 'MKT00000000000000000000000000001', 'C', 'channel', '/pilot-service/channel/index', 'pilot-service:channel:list', 1, '1', '1', 'zkxw', 8, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('80000000000000000000000000000009', '渠道服务订单(财务确认)', 'MKT00000000000000000000000000001', 'C', 'channel-service-order-finance', '/pilot-service/channel-service-order-finance/index', 'pilot-service:channel-service-order-finance:list', 1, '1', '1', 'zkxw', 9, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 采集协议号已并入细胞采集基础信息维护 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='124';
+-- 样本编码已并入细胞样本基础信息维护 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='125';
+-- 变动记录已并入市场物料库 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='50000000000000000000000000000010';
+-- 规格/价格体系/项目价格已并入细胞产品基础信息维护 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id IN ('80000000000000000000000000000003','80000000000000000000000000000004','80000000000000000000000000000005');
+-- 服务购买订单已并入渠道信息管理 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='80000000000000000000000000000008';
+-- 市场基础信息二级菜单排序与更名(已有库可执行):
+-- UPDATE sys_menu SET order_num=1 WHERE id='122';
+-- UPDATE sys_menu SET order_num=2 WHERE id='123';
+-- UPDATE sys_menu SET order_num=3 WHERE id='80000000000000000000000000000002';
+-- UPDATE sys_menu SET order_num=4 WHERE id='40000000000000000000000000000007';
+-- UPDATE sys_menu SET order_num=5 WHERE id='80000000000000000000000000000007';
+-- UPDATE sys_menu SET order_num=6 WHERE id='50000000000000000000000000000009';
+-- UPDATE sys_menu SET order_num=7 WHERE id='121';
+-- UPDATE sys_menu SET order_num=8 WHERE id='80000000000000000000000000000006';
+-- UPDATE sys_menu SET order_num=9, menu_name='渠道服务订单(财务确认)' WHERE id='80000000000000000000000000000009';
+('QCB00000000000000000000000000001', '质检基础信息', '2', 'M', '/quality-base', '/index/index', NULL, 1, '1', '1', 'zkxw', 9, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('70000000000000000000000000000003', '接收检查项配置', 'QCB00000000000000000000000000001', 'C', 'sample-reception-check-item', '/quality-inspection/sample-reception-check-item/index', 'biz:sampleReceptionCheckItem:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('70000000000000000000000000000006', '质检项目管理', 'QCB00000000000000000000000000001', 'C', 'lab-test-item', '/quality-inspection/lab-test-item/index', 'biz:qcLabTestItem:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 第三方检测已并入质检项目管理 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='70000000000000000000000000000004'; UPDATE sys_menu SET menu_name='质检项目管理' WHERE id='70000000000000000000000000000006';
+('70000000000000000000000000000009', '质检物料库', 'QCB00000000000000000000000000001', 'C', 'qc-material', '/quality-inspection/qc-material/index', 'biz:qcMaterial:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 变动记录已并入质检物料库 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='70000000000000000000000000000010';
+('PRB00000000000000000000000000001', '生产基础信息', '2', 'M', '/production-base', '/index/index', NULL, 1, '1', '1', 'zkxw', 10, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('90000000000000000000000000000002', '耗材管理', 'PRB00000000000000000000000000001', 'C', 'consumable', '/production/consumable/index', 'production:consumable:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 耗材包已并入耗材管理 Tab,不再单独挂菜单;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='90000000000000000000000000000007';
+('90000000000000000000000000000003', '试剂管理', 'PRB00000000000000000000000000001', 'C', 'reagent', '/production/reagent/index', 'production:reagent:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('90000000000000000000000000000004', 'SOP文档', 'PRB00000000000000000000000000001', 'C', 'sop', '/production/sop/index', 'production:sop-document:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('90000000000000000000000000000005', '工艺基础信息维护', 'PRB00000000000000000000000000001', 'C', 'process-category', '/production/process-category/index', 'production:process-category:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 产出物类型已并入工艺基础信息维护 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='90000000000000000000000000000006';
+('90000000000000000000000000000008', '工艺库管理', 'PRB00000000000000000000000000001', 'C', 'process', '/production/process/index', 'production:process:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+-- 工艺路线已并入工艺库管理 Tab;已有库可执行:UPDATE sys_menu SET del_flag=1 WHERE id='90000000000000000000000000000010';
+('SYSB000000000000000000000000000001', '系统基础信息', '2', 'M', '/system-base', '/index/index', NULL, 1, '1', '1', 'zkxw', 11, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('107', '站点管理', 'SYSB000000000000000000000000000001', 'C', 'site', '/system/site/index', 'system:site:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('109', '物品单位管理', 'SYSB000000000000000000000000000001', 'C', 'unit', '/system/unit/index', 'system:unit:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('110', '快递公司', 'SYSB000000000000000000000000000001', 'C', 'express', '/system/express/index', 'system:express:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('WKF00000000000000000000000000001', '工作流管理', '2', 'M', '/workflow', '/index/index', NULL, 1, '1', '1', 'zkxw', 12, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('10000000000000000000000000000wf01', '流程配置', 'WKF00000000000000000000000000001', 'C', 'process', '/system/workflow/process/index', 'workflow:process', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('10000000000000000000000000000wf02', '待办中心', 'WKF00000000000000000000000000001', 'C', 'pending', '/system/workflow/task-pending/index', 'workflow:pending', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('10000000000000000000000000000wf03', '已办中心', 'WKF00000000000000000000000000001', 'C', 'completed', '/system/workflow/task-completed/index', 'workflow:completed', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('10000000000000000000000000000wf05', '我的申请', 'WKF00000000000000000000000000001', 'C', 'my-applications', '/system/workflow/my-applications/index', 'workflow:myApplications', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('10000000000000000000000000000wf04', '请假申请', 'WKF00000000000000000000000000001', 'C', 'leave', '/system/leave/index', 'workflow:leave', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('SYSM000000000000000000000000000001', '系统管理', '2', 'M', '/system', '/index/index', NULL, 1, '1', '1', 'zkxw', 13, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('101', '租户管理', 'SYSM000000000000000000000000000001', 'C', 'tenant', '/system/tenant/index', 'system:tenant:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('102', '字典管理', 'SYSM000000000000000000000000000001', 'C', 'dict', '/system/dict/index', 'system:dict:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('106', '菜单管理', 'SYSM000000000000000000000000000001', 'C', 'menu', '/system/menu/index', 'system:menu:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('103', '组织管理', 'SYSM000000000000000000000000000001', 'C', 'dept', '/system/dept/index', 'system:dept:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('105', '角色管理', 'SYSM000000000000000000000000000001', 'C', 'role', '/system/role/index', 'system:role:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('104', '用户管理', 'SYSM000000000000000000000000000001', 'C', 'user', '/system/user/index', 'system:user:list', 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('108', '职位管理', 'SYSM000000000000000000000000000001', 'C', 'post', '/system/post/index', 'system:post:list', 1, '1', '1', 'zkxw', 7, NULL, NULL, '', NOW(), NULL, NOW(), 0),
+('10000000000000000000000000000008', '用户详情', 'SYSM000000000000000000000000000001', 'C', 'user/detail', '/system/user/detail', NULL, 1, '0', '1', 'zkxw', 8, NULL, NULL, '', NOW(), NULL, NOW(), 0);
diff --git a/docs/superpowers/plans/2026-05-27-clinical-application.md b/docs/superpowers/plans/2026-05-27-clinical-application.md
new file mode 100644
index 000000000..9afb308e2
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-27-clinical-application.md
@@ -0,0 +1,1252 @@
+# 临床应用业务 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` 加 `amount`
+- `backend/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_type`
+- `backend/app/api/__init__.py` — 注册 `clinical_payment_router`
+- `backend/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` — 接收 `bizType` prop,条件渲染
+- `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**
+
+```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**
+
+```bash
+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` 字段后面添加:
+
+```python
+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` 字段后面添加:
+
+```python
+amount = Column(Numeric(18, 2), nullable=True, comment="临床应用-手动填写的输注金额(中试为NULL)")
+```
+
+- [ ] **Step 3: 创建 `backend/app/models/clinical_payment.py`**
+
+```python
+"""临床应用缴费记录模型"""
+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 行后面添加:
+
+```python
+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**
+
+```bash
+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`,包含以下函数:
+
+```python
+"""临床应用缴费 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**
+
+```bash
+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`:
+
+```python
+"""临床应用缴费 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` 添加:
+
+```python
+from app.api.clinical_payment import router as clinical_payment_router
+```
+
+在 `routers` 列表中添加 `"clinical_payment_router"`。
+
+- [ ] **Step 3: 在 `main.py` 挂载路由**
+
+在 `backend/app/main.py` 中现有路由挂载位置添加:
+
+```python
+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**
+
+```bash
+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` 动态选择:
+
+```python
+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 中添加:
+
+```python
+"bizType": row.biz_type or "pilot_service",
+```
+
+- [ ] **Step 3: `list_applications` 支持 biz_type 过滤**
+
+在 line 284 的 `list_applications` 函数签名中添加 `biz_type` 参数,并在查询中添加过滤:
+
+```python
+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`
+
+```python
+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`
+
+```python
+# 在 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 字段**
+
+在输注计划序列化函数中添加:
+
+```python
+"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**
+
+```bash
+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**
+
+```bash
+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 末尾(`]` 之前)添加临床应用管理一级菜单**
+
+```typescript
+ {
+ 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**
+
+```bash
+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 函数**
+
+```typescript
+// ─── 临床应用缴费 ─────────────────────────────────────
+
+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**
+
+```bash
+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**
+
+```bash
+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**
+
+```bash
+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**
+
+```bash
+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**
+
+在 `
+
+
diff --git a/web-admin/src/views/basic-info/cell-sample-type/index.vue b/web-admin/src/views/basic-info/cell-sample-type/index.vue
index a7ce98a6e..302dfba0e 100644
--- a/web-admin/src/views/basic-info/cell-sample-type/index.vue
+++ b/web-admin/src/views/basic-info/cell-sample-type/index.vue
@@ -2,16 +2,13 @@
-
- 细胞样本类型
- 维护样本类型编码、名称、存储年限与存储价格。
-
-
-
细胞样本类型列表
+ 细胞样本基础信息维护
+
+
@@ -113,17 +110,32 @@
/>
- 查看
- 编辑
- 删除
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -209,6 +221,12 @@
code-label="类型编码"
:sections="viewSections"
/>
+
+
+
+
+
+
@@ -223,7 +241,10 @@
View,
Collection,
RefreshLeft,
- Search
+ Search,
+ More,
+ Edit,
+ Delete
} from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
@@ -233,9 +254,12 @@
fetchDeleteCellSampleType
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
+ import SampleCodeTab from './SampleCodeTab.vue'
defineOptions({ name: 'BizCellSampleType' })
+ const route = useRoute()
+ const activeTab = ref('type')
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref([])
const selectedRows = ref([])
@@ -382,6 +406,13 @@
}
}
+ function handleDropdownCommand(command: string, row: any) {
+ switch (command) {
+ case 'edit': handleEdit(row); break
+ case 'delete': handleDelete(row); break
+ }
+ }
+
async function handleDelete(row: Api.BizCellSampleType.CellSampleTypeItem) {
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
await fetchDeleteCellSampleType(row.id)
@@ -413,6 +444,10 @@
)
onMounted(() => {
+ const tab = route.query.tab
+ if (tab === 'sample-code') {
+ activeTab.value = 'sample-code'
+ }
loadData()
})
diff --git a/web-admin/src/views/basic-info/collection-protocol-no/index.vue b/web-admin/src/views/basic-info/collection-protocol-no/index.vue
index 81b515cb7..d9499aee7 100644
--- a/web-admin/src/views/basic-info/collection-protocol-no/index.vue
+++ b/web-admin/src/views/basic-info/collection-protocol-no/index.vue
@@ -1,781 +1,18 @@
-
-
+
-
-
- 采集协议号
- 维护采集协议号池及是否下发标记。
-
-
-
-
-
采集协议号列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ row.issued === 1 ? '是' : '否' }}
-
-
-
- {{ resolveIssuedUserName(row) }}
-
-
- {{ row.issuedTime || '-' }}
-
-
- {{ row.remark || '-' }}
-
-
-
-
- 查看
- 编辑
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 序号位数补零后的总位数
-
-
-
- {{ batchGenPreview }}
-
-
-
-
-
-
- 取消
-
-
-
- 生成
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 已选 {{ selectedRows.length }} 条,下发后"是否下发"将变为"是"
-
-
-
-
-
-
- 取消
-
-
-
- 确定下发
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formData.issuedTime }}
-
-
-
-
-
-
-
-
-
- 取消
-
-
-
- 确定
-
-
-
-
-
-
-
+
-
-
diff --git a/web-admin/src/views/basic-info/collection-region/CollectionProtocolNoTab.vue b/web-admin/src/views/basic-info/collection-region/CollectionProtocolNoTab.vue
new file mode 100644
index 000000000..91907b981
--- /dev/null
+++ b/web-admin/src/views/basic-info/collection-region/CollectionProtocolNoTab.vue
@@ -0,0 +1,795 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.issued === 1 ? '是' : '否' }}
+
+
+
+ {{ resolveIssuedUserName(row) }}
+
+
+ {{ row.issuedTime || '-' }}
+
+
+ {{ row.remark || '-' }}
+
+
+
+
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 序号位数补零后的总位数
+
+
+
+ {{ batchGenPreview }}
+
+
+
+
+
+
+ 取消
+
+
+
+ 生成
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已选 {{ selectedRows.length }} 条,下发后"是否下发"将变为"是"
+
+
+
+
+
+
+ 取消
+
+
+
+ 确定下发
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formData.issuedTime }}
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/basic-info/collection-region/index.vue b/web-admin/src/views/basic-info/collection-region/index.vue
index 46b46eaaf..776fd3d35 100644
--- a/web-admin/src/views/basic-info/collection-region/index.vue
+++ b/web-admin/src/views/basic-info/collection-region/index.vue
@@ -2,16 +2,13 @@
-
- 采集地区及医院
- 维护采集地区;医院在子表中维护,可通过「地区医院配置」管理。
-
-
-
采集地区列表
+ 细胞采集基础信息维护
+
+
@@ -117,20 +114,35 @@
/>
- 查看
- 地区医院配置
- 编辑
- 删除
+
+
+ 地区医院配置
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -354,6 +366,12 @@
:meta="viewRow?.createTime ? '创建时间:' + viewRow.createTime : ''"
:sections="viewSections"
/>
+
+
+
+
+
+
@@ -361,7 +379,7 @@
diff --git a/web-admin/src/views/basic-info/customer/index.vue b/web-admin/src/views/basic-info/customer/index.vue
index 3db72e371..562d36ffe 100644
--- a/web-admin/src/views/basic-info/customer/index.vue
+++ b/web-admin/src/views/basic-info/customer/index.vue
@@ -2,11 +2,6 @@
-
- 客户管理
- 维护客户主数据;同一租户下身份证号不可重复。
-
-
客户列表
@@ -139,17 +134,32 @@
/>
- 查看
- 编辑
- 删除
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -249,7 +259,7 @@
-
-
diff --git a/web-admin/src/views/cell-storage/fullmoon-todo/index.vue b/web-admin/src/views/cell-storage/fullmoon-todo/index.vue
index 2b413aa79..a465c1226 100644
--- a/web-admin/src/views/cell-storage/fullmoon-todo/index.vue
+++ b/web-admin/src/views/cell-storage/fullmoon-todo/index.vue
@@ -1,1289 +1,18 @@
-
-
+
-
-
- 满月待办管理
-
- 展示全部已生效采集协议;已满月且未办保险的行以红色背景标识,其余为正常底色。
-
-
-
-
-
-
待办列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持按协议号、姓名模糊检索;待办状态默认未办保险,满月日期默认近 30 天。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
-
-
-
-
-
-
-
-
- {{ row.todoStatus == 1 ? '已办保险' : '未办保险' }}
-
-
-
-
-
-
- {{ row.insuranceSubscribed == 1 ? '是' : '否' }}
-
-
-
-
-
-
-
- 查看
-
- 办理保险
-
-
-
-
-
-
-
-
-
-
-
-
-
- 将为已选
-
{{ batchEditingRows.length }}
- 条记录统一填写保险信息(适用于团体险等场景)。
-
{{
- batchProtocolSummary
- }}
-
-
-
-
-
-
- 取消
-
-
- 确定
-
-
-
-
-
-
-
-
-
-
+
-
-
diff --git a/web-admin/src/views/cell-storage/material-stock-log/index.vue b/web-admin/src/views/cell-storage/material-stock-log/index.vue
index adaf4bb22..f864f1013 100644
--- a/web-admin/src/views/cell-storage/material-stock-log/index.vue
+++ b/web-admin/src/views/cell-storage/material-stock-log/index.vue
@@ -1,303 +1,18 @@
-
-
+
-
-
- 市场物料变动记录
- 查看市场物料的入库、出库历史变动记录
-
-
-
-
-
变动记录列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
按物料名称/编码查询,可按变动类型和日期范围筛选。
-
- 重置
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ row.changeType === 'rk' ? '入库' : '出库' }}
-
-
-
-
-
-
- {{ formatChangeQty(row).text }}
-
-
-
-
-
-
-
- {{ row.changeType === 'ck' ? row.receiver || '-' : '-' }}
-
-
-
-
- {{ row.changeType === 'ck' ? row.purpose || '-' : '-' }}
-
-
-
-
- {{ row.createByName || row.createBy || '-' }}
-
-
-
-
- {{ row.remark || '-' }}
-
-
-
-
-
-
- 查看
-
-
-
-
-
-
-
-
-
-
-
+
-
-
diff --git a/web-admin/src/views/cell-storage/material/MaterialStockLogTab.vue b/web-admin/src/views/cell-storage/material/MaterialStockLogTab.vue
new file mode 100644
index 000000000..2cc2be925
--- /dev/null
+++ b/web-admin/src/views/cell-storage/material/MaterialStockLogTab.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
按物料名称/编码查询,可按变动类型和日期范围筛选。
+
+ 重置
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.changeType === 'rk' ? '入库' : '出库' }}
+
+
+
+
+
+
+ {{ formatChangeQty(row).text }}
+
+
+
+
+
+
+
+ {{ row.changeType === 'ck' ? row.receiver || '-' : '-' }}
+
+
+
+
+ {{ row.changeType === 'ck' ? row.purpose || '-' : '-' }}
+
+
+
+
+ {{ row.createByName || row.createBy || '-' }}
+
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+
+
+
+ 查看
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/material/index.vue b/web-admin/src/views/cell-storage/material/index.vue
index 01f29a7c6..791e3d8bc 100644
--- a/web-admin/src/views/cell-storage/material/index.vue
+++ b/web-admin/src/views/cell-storage/material/index.vue
@@ -2,16 +2,13 @@
-
- 市场物料库
- 管理市场部门的物料信息,支持入库、出库操作
-
-
-
物料列表
+ 市场物料库
+
+
@@ -160,27 +157,38 @@
/>
- 入库
- 出库
- 查看
- 编辑
- 删除
+
+
+ 入库
+
+
+ 出库
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -807,6 +815,12 @@
:meta="viewRow?.createTime || ''"
:sections="viewSections"
/>
+
+
+
+
+
+
@@ -825,7 +839,8 @@
Upload,
Download,
Edit,
- Delete
+ Delete,
+ More
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { fetchDictDataOptions, fetchItemUnitList, fetchUserList } from '@/api/admin'
@@ -840,9 +855,12 @@
fetchBatchStockOutMaterial
} from '@/api/material'
import type { Material } from '@/api/material'
+ import MaterialStockLogTab from './MaterialStockLogTab.vue'
defineOptions({ name: 'CellStorageMaterial' })
+ const route = useRoute()
+ const activeTab = ref('list')
const searchForm = reactive({ keyword: '', category: '', status: '' })
const tableRef = ref()
const tableData = ref([])
@@ -1216,6 +1234,14 @@
viewVisible.value = true
}
+ function handleDropdownCommand(command: string, row: any) {
+ switch (command) {
+ case 'view': handleView(row); break
+ case 'edit': handleEdit(row); break
+ case 'delete': handleDelete(row); break
+ }
+ }
+
async function handleDelete(row: Material) {
await ElMessageBox.confirm('确定要删除该物料吗?', '提示', { type: 'warning' })
await fetchDeleteMaterial(row.id)
@@ -1520,6 +1546,10 @@
)
onMounted(() => {
+ const tab = route.query.tab
+ if (tab === 'stock-log') {
+ activeTab.value = 'stock-log'
+ }
loadDictOptions()
loadData()
})
diff --git a/web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue b/web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue
new file mode 100644
index 000000000..be4038b8e
--- /dev/null
+++ b/web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue
@@ -0,0 +1,632 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30
+ 天内应缴费的明细。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.motherName || '-' }}
+
+
+ {{ maskIdCard(row.motherIdCard) }}
+
+
+ {{ row.newbornName || '-' }}
+
+
+ {{ maskIdCard(row.newbornIdCard) }}
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+ {{ payModeLabel(String(row.payMode || '')) }}
+
+
+ {{ row.installmentNo ?? '-' }}
+
+
+ {{
+ formatPaymentDueDate(row.installmentDueDate)
+ }}
+
+
+ {{ formatPaymentAmount(row.dueAmount) }}
+
+
+ {{ formatPaymentAmount(row.transferAmount) }}
+
+
+ {{
+ transferTypeLabel(String(row.transferType || ''))
+ }}
+
+
+ {{ row.transferTime || '-' }}
+
+
+ {{ formatPaymentAmount(row.refundAmount) }}
+
+
+ {{ row.refundRemark || '-' }}
+
+
+
+
+ {{ activeTab === 'pending' ? '退费确认中' : '已退费' }}
+
+
+
+
+
+ {{ row.refundFinanceRemark || '-' }}
+
+
+
+
+
+ 确认退费
+
+ 查看确认信息
+
+
+
+
+
+
+
+
+
+ 财务确认
+
+
+
+
+
+
+ 取消
+
+
+
+ 确认
+
+
+
+
+
+
+
+ 财务确认信息
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/payment-confirm/index.vue b/web-admin/src/views/cell-storage/payment-confirm/index.vue
index fca003841..bb096c6f0 100644
--- a/web-admin/src/views/cell-storage/payment-confirm/index.vue
+++ b/web-admin/src/views/cell-storage/payment-confirm/index.vue
@@ -1,35 +1,49 @@
-
+
-
- 缴费确认
-
- 待确认:处理「缴费确认中」明细;已确认:查看财务确认收款记录及当时填写的备注与附件。
-
-
-
-
-
-
-
- {{ tab.label }}
-
-
-
-
-
-
-
缴费确认列表
+
+
+
+
+
+
+
+
+
@@ -369,6 +383,9 @@
+
+
+
@@ -376,7 +393,7 @@
diff --git a/web-admin/src/views/cell-storage/protocol-audit/FullmoonTodoTab.vue b/web-admin/src/views/cell-storage/protocol-audit/FullmoonTodoTab.vue
new file mode 100644
index 000000000..af28ace64
--- /dev/null
+++ b/web-admin/src/views/cell-storage/protocol-audit/FullmoonTodoTab.vue
@@ -0,0 +1,1276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持按协议号、姓名模糊检索;待办状态默认未办保险,满月日期默认近 30 天。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+
+
+
+
+
+ {{ row.todoStatus == 1 ? '已办保险' : '未办保险' }}
+
+
+
+
+
+
+ {{ row.insuranceSubscribed == 1 ? '是' : '否' }}
+
+
+
+
+
+
+
+ 查看
+
+ 办理保险
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 将为已选
+
{{ batchEditingRows.length }}
+ 条记录统一填写保险信息(适用于团体险等场景)。
+
{{
+ batchProtocolSummary
+ }}
+
+
+
+
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue b/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue
new file mode 100644
index 000000000..512c24a1d
--- /dev/null
+++ b/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue
@@ -0,0 +1,852 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持按协议号模糊检索;默认展示近 30 天内应缴费的明细。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.motherName || '-' }}
+
+
+
+
+ {{ maskIdCard(row.motherIdCard) }}
+
+
+
+
+ {{ row.newbornName || '-' }}
+
+
+
+
+ {{ maskIdCard(row.newbornIdCard) }}
+
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+
+ {{ payModeLabel(String(row.payMode || '')) }}
+
+
+
+
+
+ {{ formatDate(String(row.installmentDueDate || '')) }}
+
+
+
+
+ {{ formatAmount(row.dueAmount) }}
+
+
+
+
+ {{ formatAmount(row.transferAmount) }}
+
+
+
+
+ {{ transferTypeLabel(String(row.transferType || '')) }}
+
+
+
+
+ {{ row.transferTime || '-' }}
+
+
+
+
+
+ {{ payStatusLabel(String(row.payStatus || '')) }}
+
+
+
+
+
+
+
+ 查看
+
+
+ 缴费确认
+
+
+ 退费确认
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确认提交
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue b/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue
new file mode 100644
index 000000000..1fa8c0117
--- /dev/null
+++ b/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue
@@ -0,0 +1,582 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。
+
+ 重置
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+
+
+
+
+
+ {{
+ newbornSexLabel(String(row.newbornSex || ''))
+ }}
+
+
+ {{
+ deliveryModeLabel(String(row.deliveryMode || ''))
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 审核
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol-audit/SampleReceptionInfoTab.vue b/web-admin/src/views/cell-storage/protocol-audit/SampleReceptionInfoTab.vue
new file mode 100644
index 000000000..48f7c7488
--- /dev/null
+++ b/web-admin/src/views/cell-storage/protocol-audit/SampleReceptionInfoTab.vue
@@ -0,0 +1,352 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内接收的样本。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+
+
+
+
+
+
+
+ {{
+ row.acceptConclusionLabel ||
+ (row.acceptConclusion === 'js'
+ ? '接收'
+ : row.acceptConclusion === 'bj'
+ ? '拒收'
+ : '待接收确认')
+ }}
+
+
+
+
+
+
+ {{ row.smsNotified == 1 ? '已通知' : '未通知' }}
+
+
+
+
+
+
+ 查看
+
+
+ 提取短信
+
+
+ {{ row.smsNotified == 1 ? '标记未通知' : '标记已通知' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol-audit/StorageCertTab.vue b/web-admin/src/views/cell-storage/protocol-audit/StorageCertTab.vue
new file mode 100644
index 000000000..d826410b5
--- /dev/null
+++ b/web-admin/src/views/cell-storage/protocol-audit/StorageCertTab.vue
@@ -0,0 +1,879 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持按协议号、姓名模糊检索;制作状态默认未制作,入库日期默认近 30 天。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+
+ {{ row.motherPhone || '—' }}
+
+
+ {{ maskIdCard(row.motherIdCard) }}
+
+
+
+
+ {{ formatStorageInDate(row.storageInDate) }}
+
+
+
+
+ {{ row.makeStatus == '1' ? '已制作' : '未制作' }}
+
+
+
+
+ {{ formatStorageInDate(row.makeDate) }}
+
+
+
+
+ 上传储存证
+
+
+ 查看
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持图片、视频、PDF、Word、Excel等文件
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+
![预览]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol-audit/StorageEntryInfoTab.vue b/web-admin/src/views/cell-storage/protocol-audit/StorageEntryInfoTab.vue
new file mode 100644
index 000000000..44a6da135
--- /dev/null
+++ b/web-admin/src/views/cell-storage/protocol-audit/StorageEntryInfoTab.vue
@@ -0,0 +1,345 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内入库的样本。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
+
+
+
+
+
+
+
+
+
+
+ {{ row.storageStatusLabel }}
+
+
+
+
+
+
+ {{ row.smsNotified == 1 ? '已通知' : '未通知' }}
+
+
+
+
+
+
+ 查看
+
+
+ 提取短信
+
+
+ {{ row.smsNotified == 1 ? '标记未通知' : '标记已通知' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol-audit/index.vue b/web-admin/src/views/cell-storage/protocol-audit/index.vue
index d18b73df6..634ca1ed2 100644
--- a/web-admin/src/views/cell-storage/protocol-audit/index.vue
+++ b/web-admin/src/views/cell-storage/protocol-audit/index.vue
@@ -1,573 +1,225 @@
-
-
+
-
- 采集协议单管理
- 查看与管理采集协议单及审核数据。
-
-
-
-
协议单列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。
-
- 重置
- 搜索
-
-
-
-
-
-
-
-
-
+
-
+
+
+ {{ item.title }}
+ {{ item.desc }}
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/cell-storage/protocol/index.vue b/web-admin/src/views/cell-storage/protocol/index.vue
index 54a9f3c0f..ca8ca4e55 100644
--- a/web-admin/src/views/cell-storage/protocol/index.vue
+++ b/web-admin/src/views/cell-storage/protocol/index.vue
@@ -2,11 +2,6 @@
-
- 采集协议单录入
- 录入与管理采集协议单主数据。
-
-
协议单列表
@@ -241,21 +236,32 @@
/>
- 查看
- 编辑
- 删除
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -295,7 +301,7 @@
-
-
diff --git a/web-admin/src/views/cell-storage/sample-reception-info/index.vue b/web-admin/src/views/cell-storage/sample-reception-info/index.vue
index e9ede4904..7e0001efd 100644
--- a/web-admin/src/views/cell-storage/sample-reception-info/index.vue
+++ b/web-admin/src/views/cell-storage/sample-reception-info/index.vue
@@ -1,363 +1,18 @@
-
+
-
-
- 样本接收信息
- 查看样本接收信息,提取短信通知内容并管理通知状态。
-
-
-
-
-
样本接收列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内接收的样本。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
- {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
-
-
-
-
-
-
-
-
-
-
- {{
- row.acceptConclusionLabel ||
- (row.acceptConclusion === 'js'
- ? '接收'
- : row.acceptConclusion === 'bj'
- ? '拒收'
- : '待接收确认')
- }}
-
-
-
-
-
-
- {{ row.smsNotified == 1 ? '已通知' : '未通知' }}
-
-
-
-
-
-
- 查看
-
-
- 提取短信
-
-
- {{ row.smsNotified == 1 ? '标记未通知' : '标记已通知' }}
-
-
-
-
-
-
-
-
-
-
+
diff --git a/web-admin/src/views/cell-storage/storage-cert/index.vue b/web-admin/src/views/cell-storage/storage-cert/index.vue
index 0ea508298..6a9d9744d 100644
--- a/web-admin/src/views/cell-storage/storage-cert/index.vue
+++ b/web-admin/src/views/cell-storage/storage-cert/index.vue
@@ -1,890 +1,18 @@
-
-
+
-
-
- 储存证管理
- 管理细胞储存协议的储存证制作与查看。
-
-
-
-
-
储存证列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持按协议号、姓名模糊检索;制作状态默认未制作,入库日期默认近 30 天。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
- {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
-
-
-
-
- {{ row.motherPhone || '—' }}
-
-
- {{ maskIdCard(row.motherIdCard) }}
-
-
-
-
- {{ formatStorageInDate(row.storageInDate) }}
-
-
-
-
- {{ row.makeStatus == '1' ? '已制作' : '未制作' }}
-
-
-
-
- {{ formatStorageInDate(row.makeDate) }}
-
-
-
-
- 上传储存证
-
-
- 查看
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持图片、视频、PDF、Word、Excel等文件
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
-
-
- 确定
-
-
-
-
-
-
-
![预览]()
-
-
-
-
-
-
-
-
-
-
+
-
-
diff --git a/web-admin/src/views/cell-storage/storage-entry-info/index.vue b/web-admin/src/views/cell-storage/storage-entry-info/index.vue
index d653171b9..f6b3d5e67 100644
--- a/web-admin/src/views/cell-storage/storage-entry-info/index.vue
+++ b/web-admin/src/views/cell-storage/storage-entry-info/index.vue
@@ -1,356 +1,18 @@
-
+
-
-
- 样本入库信息
- 查看样本入库信息,提取短信通知内容并管理通知状态。
-
-
-
-
-
样本入库列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内入库的样本。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
- {{ protocolStatusLabel(String(row.protocolStatus || '')) }}
-
-
-
-
-
-
-
-
-
-
- {{ row.storageStatusLabel }}
-
-
-
-
-
-
- {{ row.smsNotified == 1 ? '已通知' : '未通知' }}
-
-
-
-
-
-
- 查看
-
-
- 提取短信
-
-
- {{ row.smsNotified == 1 ? '标记未通知' : '标记已通知' }}
-
-
-
-
-
-
-
-
-
-
+
diff --git a/web-admin/src/views/dashboard/console/index.vue b/web-admin/src/views/dashboard/console/index.vue
index c0575130f..a6bccbc51 100644
--- a/web-admin/src/views/dashboard/console/index.vue
+++ b/web-admin/src/views/dashboard/console/index.vue
@@ -1,11 +1,6 @@
-
-
工作台
-
以下为固定参考页,新增管理端界面请对照布局与类名;视觉以文档为准。
-
-
快速入口
diff --git a/web-admin/src/views/operations/clean-room/index.vue b/web-admin/src/views/operations/clean-room/index.vue
index 7ff4ec7dd..fa8301300 100644
--- a/web-admin/src/views/operations/clean-room/index.vue
+++ b/web-admin/src/views/operations/clean-room/index.vue
@@ -2,11 +2,6 @@
-
- 洁净室管理
- 维护洁净室信息及人员进出记录。
-
-
洁净室列表
@@ -113,22 +108,33 @@
/>
- 查看
-
- 编辑
-
- 人员进出
-
- 删除
-
+
+ 人员进出
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -477,6 +483,7 @@
View,
Edit,
Delete,
+ More,
User,
OfficeBuilding,
RefreshLeft,
@@ -656,6 +663,14 @@
viewVisible.value = true
}
+ function handleDropdownCommand(command: string, row: any) {
+ switch (command) {
+ case 'view': handleView(row); break
+ case 'edit': handleEdit(row); break
+ case 'delete': handleDelete(row); break
+ }
+ }
+
async function handleDelete(row: any) {
await ElMessageBox.confirm('确定要删除该洁净室吗?', '提示', { type: 'warning' })
await fetchDeleteCleanRoom(row.id)
diff --git a/web-admin/src/views/operations/demo/index.vue b/web-admin/src/views/operations/demo/index.vue
index 777a156f4..921e9d828 100644
--- a/web-admin/src/views/operations/demo/index.vue
+++ b/web-admin/src/views/operations/demo/index.vue
@@ -1,11 +1,6 @@
-
-
选择项
diff --git a/web-admin/src/views/operations/exhibition-appointment/index.vue b/web-admin/src/views/operations/exhibition-appointment/index.vue
index bba11239a..4beb71fee 100644
--- a/web-admin/src/views/operations/exhibition-appointment/index.vue
+++ b/web-admin/src/views/operations/exhibition-appointment/index.vue
@@ -2,11 +2,6 @@
-
- 展馆预约
- 管理展馆来访预约信息,包括来访单位、联系人及到访日期等。
-
-
预约列表
@@ -133,17 +128,32 @@
/>
- 查看
- 编辑
- 删除
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -239,7 +249,7 @@
-
-
diff --git a/web-admin/src/views/pilot-service/cell-product-spec/index.vue b/web-admin/src/views/pilot-service/cell-product-spec/index.vue
index e22a29934..c0428847e 100644
--- a/web-admin/src/views/pilot-service/cell-product-spec/index.vue
+++ b/web-admin/src/views/pilot-service/cell-product-spec/index.vue
@@ -1,389 +1,18 @@
-
-
+
-
-
- 细胞产品规格管理
- 维护细胞产品的规格编码与名称。
-
-
-
-
-
规格列表
-
-
-
-
-
-
-
-
-
-
-
按编码、名称模糊查询。
-
- 重置
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 查看
- 编辑
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
-
-
-
- 确定
-
-
-
-
-
-
-
+
-
-
diff --git a/web-admin/src/views/pilot-service/cell-product-type/CellProductPriceTab.vue b/web-admin/src/views/pilot-service/cell-product-type/CellProductPriceTab.vue
new file mode 100644
index 000000000..5b529ab37
--- /dev/null
+++ b/web-admin/src/views/pilot-service/cell-product-type/CellProductPriceTab.vue
@@ -0,0 +1,547 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
按项目名称、编码模糊查询。
+
+ 重置
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '-' }}
+
+
+
+
+
+
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)"
+ >
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/cell-product-type/CellProductSpecTab.vue b/web-admin/src/views/pilot-service/cell-product-type/CellProductSpecTab.vue
new file mode 100644
index 000000000..b684decf8
--- /dev/null
+++ b/web-admin/src/views/pilot-service/cell-product-type/CellProductSpecTab.vue
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
按编码、名称模糊查询。
+
+ 重置
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/cell-product-type/ProjectPriceSystemTab.vue b/web-admin/src/views/pilot-service/cell-product-type/ProjectPriceSystemTab.vue
new file mode 100644
index 000000000..e3ede0035
--- /dev/null
+++ b/web-admin/src/views/pilot-service/cell-product-type/ProjectPriceSystemTab.vue
@@ -0,0 +1,807 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
按体系名称、编码模糊查询。
+
+ 重置
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ String(row.status) === '1' ? '正常' : '停用' }}
+
+
+
+
+
+
+
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)">
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '-' }}
+
+
+
+
+
+ 取消
+
+ 确认添加({{ priceSelectorSelected.length }})
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/cell-product-type/index.vue b/web-admin/src/views/pilot-service/cell-product-type/index.vue
index e0ff9fdb9..9bd25e0de 100644
--- a/web-admin/src/views/pilot-service/cell-product-type/index.vue
+++ b/web-admin/src/views/pilot-service/cell-product-type/index.vue
@@ -2,16 +2,13 @@
-
- 细胞产品类型管理
- 维护细胞产品的类型编码与名称。
-
-
-
类型列表
+ 细胞产品基础信息维护
+
+
@@ -102,21 +99,35 @@
/>
- 查看
- 编辑
- 删除
+
+
+ 查看
+
+ handleDropdownCommand(cmd, row)"
+ >
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -183,6 +194,20 @@
code-label="类型编码"
:sections="viewSections"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -197,6 +222,7 @@
View,
Edit,
Delete,
+ More,
ScaleToOriginal,
RefreshLeft,
Search
@@ -209,9 +235,14 @@
fetchUpdateCellProductType,
fetchDeleteCellProductType
} from '@/api/admin'
+ import CellProductSpecTab from './CellProductSpecTab.vue'
+ import CellProductPriceTab from './CellProductPriceTab.vue'
+ import ProjectPriceSystemTab from './ProjectPriceSystemTab.vue'
defineOptions({ name: 'PilotCellProductType' })
+ const route = useRoute()
+ const activeTab = ref('type')
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref([])
const selectedRows = ref([])
@@ -337,6 +368,11 @@
viewVisible.value = true
}
+ function handleDropdownCommand(cmd: string, row: Api.CellProductType.CellProductTypeItem) {
+ if (cmd === 'edit') handleEdit(row)
+ else if (cmd === 'delete') handleDelete(row)
+ }
+
async function handleDelete(row: Api.CellProductType.CellProductTypeItem) {
try {
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
@@ -376,6 +412,10 @@
)
onMounted(() => {
+ const tab = route.query.tab
+ if (tab === 'spec' || tab === 'price' || tab === 'project-price') {
+ activeTab.value = tab as string
+ }
loadData()
})
diff --git a/web-admin/src/views/pilot-service/channel-service-order-finance/index.vue b/web-admin/src/views/pilot-service/channel-service-order-finance/index.vue
index a65885549..f605a6f1a 100644
--- a/web-admin/src/views/pilot-service/channel-service-order-finance/index.vue
+++ b/web-admin/src/views/pilot-service/channel-service-order-finance/index.vue
@@ -1,11 +1,6 @@
-
- 财务确认
- 集中处理渠道服务订单的操作流水确认。
-
-
财务确认
diff --git a/web-admin/src/views/pilot-service/channel-service-order/index.vue b/web-admin/src/views/pilot-service/channel-service-order/index.vue
index 89b7cc203..fc010eace 100644
--- a/web-admin/src/views/pilot-service/channel-service-order/index.vue
+++ b/web-admin/src/views/pilot-service/channel-service-order/index.vue
@@ -1,2346 +1,18 @@
-
+
-
-
- 渠道服务订单
- 管理渠道预付款充值与产品购买订单,支持发起财务确认、退款、作废等操作。
-
-
-
-
-
订单列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
默认展示近 30 天内创建的订单;渠道名称输入后自动检索。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ channelServiceOrderTypeLabel(row.orderType) }}
-
-
-
-
-
-
- ¥{{ formatAmount(row.currentBalance) }}
-
- -
-
-
-
-
-
- ¥{{ formatAmount(row.purchaseTotalAmount) }}
-
- -
-
-
-
-
-
- {{ channelServiceOrderStatusLabel(row.orderStatus) }}
-
-
-
-
-
-
- {{ displayValue(row.createByName || row.createBy) }}
-
-
-
-
-
-
-
- 查看
-
-
-
操作流水
-
-
- 编辑
-
-
-
-
- 删除
-
-
-
-
-
- 发起充值
-
-
-
- 申请退款
-
-
-
- 购买产品
-
-
-
- 退订产品
-
-
-
- 作废
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ¥{{ formatAmount(row.agreementUnitPrice) }}
-
-
-
- {{ formatProductQty(calcProductCumulativeUsedQty(row)) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
-
-
- 确定
-
-
-
-
-
-
- {{
- formatOrderContext(currentOrderForTxn)
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持图片、视频、PDF、Word、Excel 等,可上传多个
-
-
-
-
-
- 取消
-
-
- 确认
-
-
-
-
-
-
- {{
- formatOrderContext(currentOrderForTxn)
- }}
-
-
- 退款金额(元)当前余额:¥{{ formatAmount(currentOrderForTxn?.currentBalance) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持图片、视频、PDF、Word、Excel 等,可上传多个
-
-
-
-
-
- 取消
-
-
- 确认
-
-
-
-
-
-
- {{
- formatOrderContext(currentOrderForTxn)
- }}
-
-
-
-
-
-
- handlePurchasePriceChange(v, $index)"
- >
-
-
-
-
-
- ¥{{ formatAmount(row.unitPrice) }}
-
-
-
-
-
-
-
- ¥{{ formatAmount(row.amount) }}
-
-
-
-
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
-
-
- 确认
-
-
-
-
-
-
- {{
- formatOrderContext(currentOrderForTxn)
- }}
-
-
-
-
-
-
-
- ¥{{ formatAmount(row.agreementUnitPrice) }}
-
-
-
-
-
-
-
-
- ¥{{ formatAmount(row.refundAmount) }}
-
-
-
-
- 退订合计:¥{{ formatAmount(unsubscribeTotalAmount) }}(元)
-
-
-
-
-
-
-
-
- 取消
-
-
- 确认退订
-
-
-
-
-
-
-
-
![预览]()
-
-
-
-
-
-
+
-
-
diff --git a/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue b/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue
new file mode 100644
index 000000000..fd5609e26
--- /dev/null
+++ b/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue
@@ -0,0 +1,2352 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
默认展示近 30 天内创建的订单;渠道名称输入后自动检索。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ channelServiceOrderTypeLabel(row.orderType) }}
+
+
+
+
+
+
+ ¥{{ formatAmount(row.currentBalance) }}
+
+ -
+
+
+
+
+
+ ¥{{ formatAmount(row.purchaseTotalAmount) }}
+
+ -
+
+
+
+
+
+ {{ channelServiceOrderStatusLabel(row.orderStatus) }}
+
+
+
+
+
+
+ {{ displayValue(row.createByName || row.createBy) }}
+
+
+
+
+
+
+
+
操作流水
+
+
+ 发起充值
+
+
+ 申请退款
+
+
+ 购买产品
+
+
+ 退订产品
+
+
+ 作废
+
+ handleDropdownCommand(cmd, row)"
+ >
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ¥{{ formatAmount(row.agreementUnitPrice) }}
+
+
+
+ {{ formatProductQty(calcProductCumulativeUsedQty(row)) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+ {{
+ formatOrderContext(currentOrderForTxn)
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持图片、视频、PDF、Word、Excel 等,可上传多个
+
+
+
+
+
+ 取消
+
+
+ 确认
+
+
+
+
+
+
+ {{
+ formatOrderContext(currentOrderForTxn)
+ }}
+
+
+ 退款金额(元)当前余额:¥{{ formatAmount(currentOrderForTxn?.currentBalance) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持图片、视频、PDF、Word、Excel 等,可上传多个
+
+
+
+
+
+ 取消
+
+
+ 确认
+
+
+
+
+
+
+ {{
+ formatOrderContext(currentOrderForTxn)
+ }}
+
+
+
+
+
+
+ handlePurchasePriceChange(v, $index)"
+ >
+
+
+
+
+
+ ¥{{ formatAmount(row.unitPrice) }}
+
+
+
+
+
+
+
+ ¥{{ formatAmount(row.amount) }}
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确认
+
+
+
+
+
+
+ {{
+ formatOrderContext(currentOrderForTxn)
+ }}
+
+
+
+
+
+
+
+ ¥{{ formatAmount(row.agreementUnitPrice) }}
+
+
+
+
+
+
+
+
+ ¥{{ formatAmount(row.refundAmount) }}
+
+
+
+
+ 退订合计:¥{{ formatAmount(unsubscribeTotalAmount) }}(元)
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确认退订
+
+
+
+
+
+
+
+
![预览]()
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/channel/index.vue b/web-admin/src/views/pilot-service/channel/index.vue
index e0f2c1967..97054f854 100644
--- a/web-admin/src/views/pilot-service/channel/index.vue
+++ b/web-admin/src/views/pilot-service/channel/index.vue
@@ -2,16 +2,13 @@
-
- 渠道信息管理
- 维护渠道基础信息、开票信息、邮寄地址及协议管理。
-
-
-
渠道列表
+ 渠道信息管理
+
+
@@ -122,24 +119,42 @@
/>
-
- 查看
-
-
- 编辑
-
-
- 渠道协议
-
-
- 删除
-
+
+
+ 渠道协议
+
+ handleDropdownCommand(cmd, row)"
+ >
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -1109,6 +1124,12 @@
/>
+
+
+
+
+
+
@@ -1126,6 +1147,7 @@
Edit,
Delete,
Document,
+ More,
VideoPlay,
Loading,
Download,
@@ -1177,9 +1199,13 @@
fetchProjectSystemList,
fetchProjectSystemItems
} from '@/api/admin'
+ import ChannelServiceOrderTab from './ChannelServiceOrderTab.vue'
defineOptions({ name: 'PilotChannel' })
+ const route = useRoute()
+ const mainTab = ref('channel')
+
// ---------------------------------------------------------------------------
// 常量映射
// ---------------------------------------------------------------------------
@@ -1686,6 +1712,20 @@
// ---------------------------------------------------------------------------
// 查看弹窗
// ---------------------------------------------------------------------------
+ function handleDropdownCommand(command: string, row: any) {
+ switch (command) {
+ case 'view':
+ handleView(row)
+ break
+ case 'edit':
+ handleEdit(row)
+ break
+ case 'delete':
+ handleDelete(row)
+ break
+ }
+ }
+
function handleView(row: any) {
viewChannelId.value = String(row.id)
viewRow.value = row
@@ -2410,6 +2450,10 @@
{ debounce: 300, deep: true }
)
onMounted(() => {
+ const tab = route.query.tab
+ if (tab === 'service-order') {
+ mainTab.value = 'service-order'
+ }
loadData()
})
diff --git a/web-admin/src/views/pilot-service/checkup-institution/index.vue b/web-admin/src/views/pilot-service/checkup-institution/index.vue
index 4634817bb..d6be7bcae 100644
--- a/web-admin/src/views/pilot-service/checkup-institution/index.vue
+++ b/web-admin/src/views/pilot-service/checkup-institution/index.vue
@@ -2,11 +2,6 @@
-
- 体检机构及项目价格
- 维护体检机构基础信息、开票信息及项目价格管理。
-
-
机构列表
@@ -111,24 +106,42 @@
- 查看
- 项目价格配置
- 编辑
- 删除
+
+
+ 项目价格配置
+
+ handleDropdownCommand(cmd, row)"
+ >
+
+
+
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
@@ -429,6 +442,7 @@
Setting,
Edit,
Delete,
+ More,
OfficeBuilding
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
@@ -702,6 +716,20 @@
// ---------------------------------------------------------------------------
// 查看弹窗
// ---------------------------------------------------------------------------
+ function handleDropdownCommand(command: string, row: any) {
+ switch (command) {
+ case 'view':
+ handleView(row)
+ break
+ case 'edit':
+ handleEdit(row)
+ break
+ case 'delete':
+ handleDelete(row)
+ break
+ }
+ }
+
async function handleView(row: any) {
viewRow.value = row
const sections: ViewSection[] = [
diff --git a/web-admin/src/views/pilot-service/checkup-management/index.vue b/web-admin/src/views/pilot-service/checkup-management/index.vue
index 5b28c9891..e97cca447 100644
--- a/web-admin/src/views/pilot-service/checkup-management/index.vue
+++ b/web-admin/src/views/pilot-service/checkup-management/index.vue
@@ -1,433 +1,30 @@
-
+
-
-
- 体检管理
- 管理中试服务患者体检信息填写、报告上传与费用记录。
-
-
+
+
体检记录
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持申请单号、患者姓名、渠道名称模糊查询,输入即检索。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ sexLabel(row.sex) || '—' }}
-
-
- {{ row.age ?? '—' }}
-
-
- {{ row.channelName || '—' }}
-
-
- {{ row.institutionName || '—' }}
-
-
- {{ row.reserveTime || '—' }}
-
-
-
-
- {{ checkupStatusLabel(row.checkupStatus) }}
-
-
-
-
- {{
- row.actualCheckupFee != null ? '¥' + formatMoneyDisplay(row.actualCheckupFee) : '—'
- }}
-
-
-
-
- 填写体检信息
-
-
-
- 查看
-
-
- 编辑
-
-
-
- 标记已体检
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue b/web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue
new file mode 100644
index 000000000..8d6e6127d
--- /dev/null
+++ b/web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue
@@ -0,0 +1,726 @@
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
缴费确认列表
+
+
+
+
+
+
+
+
+
+
+
支持按申请单号、患者姓名模糊检索。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.patientName || '-' }}
+
+
+ {{ row.channelName || '-' }}
+
+
+ {{ row.cellTypeSpec || '-' }}
+
+
+ {{ row.infusionTime || '-' }}
+
+
+
+ {{ formatAmount(row.paymentAmount) }}
+
+
+
+
+
+ {{ activeTab === 'jfqrz' ? '缴费确认中' : '已缴费' }}
+
+
+
+
+ {{ row.financeRemark || '-' }}
+
+
+
+
+
+ 确认收款
+
+
+ 查看确认信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确认
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/clinical-payment-info/index.vue b/web-admin/src/views/pilot-service/clinical-payment-info/index.vue
new file mode 100644
index 000000000..379514826
--- /dev/null
+++ b/web-admin/src/views/pilot-service/clinical-payment-info/index.vue
@@ -0,0 +1,707 @@
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
缴费信息列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
支持按申请单号模糊检索。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.patientName || '-' }}
+
+
+ {{ row.channelName || '-' }}
+
+
+ {{ row.cellTypeSpec || '-' }}
+
+
+ {{ row.infusionTime || '-' }}
+
+
+
+ {{ formatAmount(row.paymentAmount) }}
+
+
+
+
+
+ {{ payStatusLabel(String(row.payStatus || '')) }}
+
+
+
+
+
+
+ 缴费确认
+
+
+ 退费确认
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+ 确认提交
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue b/web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue
new file mode 100644
index 000000000..dfb721b7d
--- /dev/null
+++ b/web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue
@@ -0,0 +1,804 @@
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
退费确认列表
+
+
+
+
+
+
+
+
+
+
+
支持按申请单号、患者姓名模糊检索。
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.patientName || '-' }}
+
+
+ {{ row.channelName || '-' }}
+
+
+ {{ row.cellTypeSpec || '-' }}
+
+
+ {{ row.infusionTime || '-' }}
+
+
+
+ {{ formatAmount(row.paymentAmount) }}
+
+
+
+
+ {{ formatAmount(row.refundAmount) }}
+
+
+
+
+
+ {{ activeTab === 'tfqrz' ? '退费确认中' : '已退费' }}
+
+
+
+
+ {{ row.refundRemark || '-' }}
+
+
+ {{ row.refundFinanceRemark || '-' }}
+
+
+
+
+
+ 确认退费
+
+
+ 查看确认信息
+
+
+
+
+
+
+
+
+
+
+
退费申请信息
+
+
+
+ {{ confirmRow?.applicationNo || '-' }}
+
+
+ {{ confirmRow?.patientName || '-' }}
+
+
+ {{ formatAmount(confirmRow?.paymentAmount) }}
+
+
+ {{ formatAmount(confirmRow?.refundAmount) }}
+
+
+ {{
+ confirmRow?.refundRemark || '—'
+ }}
+
+
+
+
+
+ 财务确认
+
+
+
+
+
+
+ 取消
+
+
+
+ 确认
+
+
+
+
+
+
+
+
退费申请信息
+
+
+
+ {{ viewRow?.applicationNo || '-' }}
+
+
+ {{ viewRow?.patientName || '-' }}
+
+
+ {{ formatAmount(viewRow?.paymentAmount) }}
+
+
+ {{ formatAmount(viewRow?.refundAmount) }}
+
+
+ {{
+ viewRow?.refundRemark || '—'
+ }}
+
+
+
+
+
+ 财务确认信息
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue b/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue
index 522a00895..1923c4040 100644
--- a/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue
+++ b/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue
@@ -1,551 +1,32 @@
-
+
-
-
- 输注计划管理
- 管理中试服务患者输注计划的确认、完成与取消。
-
-
+
+
输注计划
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持申请单号、患者姓名、渠道名称模糊查询,输入即检索。
-
-
- 重置
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ sexLabel(row.patientSex ?? row.sex) || '—' }}
-
-
- {{ row.patientAge ?? row.age ?? '—' }}
-
-
- {{ row.channelName || '—' }}
-
-
- {{ formatScheduleSortNo(row) }}
-
-
- {{ formatScheduleTotalCount(row) }}
-
-
- {{ row.infusionTime || '—' }}
-
-
- {{ row.infusionLocation || '—' }}
-
-
-
-
- {{ scheduleStatusLabel(row.scheduleStatus) }}
-
-
-
-
-
-
- {{ infusionStatusLabel(row.infusionStatus) }}
-
-
-
-
-
- {{
- qcStatusLabel(row.qcStatus)
- }}
-
-
-
-
-
- 编辑
-
-
- 用户已确认
-
-
- 完成
-
-
- 取消
-
-
- 出库交接
-
-
- 查看
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web-admin/src/views/pilot-service/outbound-release-review/index.vue b/web-admin/src/views/pilot-service/outbound-release-review/index.vue
index f833a2941..c783750bf 100644
--- a/web-admin/src/views/pilot-service/outbound-release-review/index.vue
+++ b/web-admin/src/views/pilot-service/outbound-release-review/index.vue
@@ -2,11 +2,6 @@
-
- 出库放行审核
- 审核出库放行记录,管理中试服务出库流程。
-
-
审核列表
diff --git a/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue b/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue
index f89242037..7ed51e1b1 100644
--- a/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue
+++ b/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue
@@ -1928,14 +1928,12 @@
savePilotDeliveryMethod,
createOutboundProductionWorkOrder,
createOutboundQcWorkOrder,
- fetchReleaseReviewList,
- updateReleaseReview,
- deleteReleaseReview,
fetchInfusionConfirmList,
confirmInfusion,
updateInfusionConfirm,
deleteInfusionConfirm
} from '@/api/admin'
+ import { fetchReleaseReviewList, updateReleaseReview, deleteReleaseReview } from '@/api/outbound-release-review'
import { fetchUploadAttachment } from '@/api/business'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import {
diff --git a/web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue b/web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue
index 414afdbe5..2a1bc5be7 100644
--- a/web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue
+++ b/web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue
@@ -60,6 +60,7 @@
输注方案
-
+
请先在「申请单信息」中选择渠道与服务订单,系统将按订单类型加载协议与可选细胞产品。
+
+ 请先在「申请单信息」中选择渠道,系统将加载可选细胞产品。
+
-