1253 lines
44 KiB
Markdown
1253 lines
44 KiB
Markdown
# 临床应用业务 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**
|
||
|
||
在 `<script setup>` 中:
|
||
```typescript
|
||
const props = defineProps<{ bizType?: string }>()
|
||
const bizType = computed(() => props.bizType || 'pilot_service')
|
||
```
|
||
|
||
列表 API 调用时传入 `bizType` 参数。
|
||
|
||
- [ ] **Step 2: ApplicationFormDialog.vue 条件渲染**
|
||
|
||
- 临床应用模式(`bizType === 'clinical_application'`):
|
||
- 隐藏"渠道服务订单"选择器
|
||
- 显示"渠道"直接选择器(从渠道列表中选择)
|
||
- 隐藏输注方案中的"协议产品价格"选择
|
||
- 显示输注计划的"金额"手动输入框
|
||
- 中试服务模式:保持现有行为不变
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/pilot-service/pilot-application/
|
||
git commit -m "feat: pilot application page supports bizType prop for clinical application"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 13: 前端改造 — 输注计划管理支持金额和缴费状态
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/pilot-service/infusion-schedule-management/index.vue`
|
||
|
||
- [ ] **Step 1: 接收 bizType prop,条件渲染金额列**
|
||
|
||
```typescript
|
||
const props = defineProps<{ bizType?: string }>()
|
||
const isClinical = computed(() => props.bizType === 'clinical_application')
|
||
```
|
||
|
||
在表格中条件渲染:
|
||
- "缴费金额"列:`v-if="isClinical"`
|
||
- "缴费状态"列:`v-if="isClinical"`,使用 `payStatusLabel` + `payStatusTagType`
|
||
|
||
- [ ] **Step 2: 查询缴费状态**
|
||
|
||
在列表查询时,如果是临床应用,额外查询每条输注计划的缴费状态(后端返回 `paymentStatus` 字段)。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/pilot-service/infusion-schedule-management/
|
||
git commit -m "feat: infusion schedule page shows payment info for clinical application"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14: 集成验证
|
||
|
||
- [ ] **Step 1: 后端启动验证**
|
||
|
||
Run: `cd backend && source venv/bin/activate && python -c "from app.main import app; print('App OK')"`
|
||
|
||
- [ ] **Step 2: 前端构建验证**
|
||
|
||
Run: `cd web-admin && pnpm build 2>&1 | tail -5`
|
||
Expected: 构建成功
|
||
|
||
- [ ] **Step 3: LSP 诊断**
|
||
|
||
对以下文件运行 `lsp_diagnostics`:
|
||
- `backend/app/models/models.py`
|
||
- `backend/app/crud/pilot_application.py`
|
||
- `backend/app/crud/clinical_payment.py`
|
||
- `web-admin/src/router/modules/sqxp.ts`
|
||
|
||
Expected: 无新增 error
|
||
|
||
- [ ] **Step 4: Final commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: complete clinical application business with payment flow"
|
||
```
|