From e7677d1661b1e52ce3025df65e70a439ec87708d Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sat, 3 Oct 2026 11:25:47 +0800 Subject: [PATCH] feat: finish P0 per-sample payment and presign order integration Wire finance transfer list, protocol view v2 readonly payment, and mark design/plan as P0 implemented. Co-authored-by: Cursor --- backend/app/api/protocol_sample_payment.py | 32 + .../2026-10-03-per-sample-payment-presign.md | 653 +++++++++++++ ...torage-protocol-sample-payment-analysis.md | 487 ++++++++++ ...10-03-per-sample-payment-presign-design.md | 481 +++++++++ web-admin/src/api/protocol-sample-payment.ts | 14 + .../protocol-audit/PaymentInfoTab.vue | 314 ++---- .../protocol/ProtocolViewDialog.vue | 910 +++++++++++------- 7 files changed, 2279 insertions(+), 612 deletions(-) create mode 100644 docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md create mode 100644 docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md create mode 100644 docs/superpowers/specs/2026-10-03-per-sample-payment-presign-design.md diff --git a/backend/app/api/protocol_sample_payment.py b/backend/app/api/protocol_sample_payment.py index 6d739938..ab1b1356 100644 --- a/backend/app/api/protocol_sample_payment.py +++ b/backend/app/api/protocol_sample_payment.py @@ -331,6 +331,38 @@ async def submit_payment_transfer_finance( return error_response(400, str(e)) +@router.get("/payment-transfers") +async def list_payment_transfers( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100, alias="pageSize"), + payStatus: str = Query("dfj", alias="payStatus"), + keyword: Optional[str] = None, + installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"), + installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"), + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + """按转账笔状态分页(缴费信息看板 / 财务共用)。""" + try: + tenant_no = _get_tenant_no(db, user) + items, total = psp_svc.list_transfers_for_finance( + db, + pay_status=payStatus, + tenant_no=tenant_no, + keyword=keyword, + page=page, + page_size=page_size, + user_info=user, + due_date_start=installmentDueDateStart, + due_date_end=installmentDueDateEnd, + ) + return success_response( + {"list": items, "total": total, "page": page, "pageSize": page_size} + ) + except PermissionError as e: + return error_response(403, str(e)) + + @router.get("/payment-transfers/receipt-confirm") async def list_receipt_confirm( page: int = Query(1, ge=1), diff --git a/docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md b/docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md new file mode 100644 index 00000000..84f8d3cb --- /dev/null +++ b/docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md @@ -0,0 +1,653 @@ +# 按样本缴费 + 预签约订单 Implementation Plan(P0) + +> **状态**:P0 Tasks 1–10 已落地(2026-10-03)。开票改挂 transfer / 旧单迁移属 P1。 +> **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:** 正式协议按样本建立缴费方案(应缴批次 + 多笔转账、财务按笔确认);协议手填总金额与退费登记;新增预签约订单(意向金按笔确认、转正式仅提示);财务页下线退费确认 Tab。 + +**Architecture:** 新表三层:`biz_protocol_sample_payment` → `biz_protocol_payment_schedule` → `biz_protocol_payment_transfer`;预签约独立主表 + `biz_presign_transfer`;协议 `payment_model_version` 区分新旧;旧 `biz_protocol_payment`/`detail` 仅服务 version=1。财务待办合并查询正式转账笔与预签约意向金笔。 + +**Tech Stack:** MySQL 5.7 · FastAPI · SQLAlchemy · Vue 3 · Element Plus · pnpm · pytest + +**Design spec:** `docs/superpowers/specs/2026-10-03-per-sample-payment-presign-design.md` +**现状分析:** `docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md` + +**本计划范围 = 设计文档 P0-a + P0-b + P0-c。** 开票改挂 transfer、旧单迁移工具属 P1,不在本计划强制交付。 + +--- + +## File Map + +| 文件 | 职责 | +|------|------| +| `docs/sql/biz_per_sample_payment_presign.sql` | **新建** 全量表结构 DDL | +| `backend/app/models/models.py` | ORM:新表 + protocol 增量字段 | +| `backend/app/crud/protocol_sample_payment.py` | **新建** 样本方案/批次/转账 CRUD + 财务确认 | +| `backend/app/crud/protocol_refund_record.py` | **新建** 退费登记 | +| `backend/app/crud/presign_order.py` | **新建** 预签约 + 意向金转账 + convert | +| `backend/app/api/protocol_sample_payment.py` | **新建** 正式缴费/转账财务 API | +| `backend/app/api/presign_order.py` | **新建** 预签约 API | +| `backend/app/api/__init__.py` / `app/main.py` | 注册路由 | +| `backend/app/api/menu.py` | 菜单:预签约 + 财务文案/隐藏退费 | +| `backend/app/api/collection_protocol.py` | 详情 bundle 带 version=2 缴费;总金额 | +| `backend/tests/test_protocol_sample_payment.py` | **新建** 核心单测 | +| `backend/tests/test_presign_order.py` | **新建** | +| `web-admin/src/api/protocol-sample-payment.ts` | **新建** | +| `web-admin/src/api/presign-order.ts` | **新建** | +| `web-admin/src/router/modules/sqxp.ts` | 预签约路由;财务菜单 title | +| `web-admin/src/views/cell-storage/presign-order/**` | **新建** 预签约列表/表单 | +| `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` | 按样本缴费 UI、总金额、意向金提示 | +| `web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordPanel.vue` | **新建** 退费登记块 | +| `web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue` | 按转账笔看板(version=2) | +| `web-admin/src/views/cell-storage/payment-confirm/index.vue` | 合并正式+预签约待办;隐藏退费 Tab | +| `web-admin/src/utils/protocol-sample-payment.ts` | **新建** 批次已缴/欠缴计算、总金额提示 | +| `web-admin/src/utils/status-tag-registry.ts` | 预签约状态等 Tag | + +--- + +## 依赖顺序 + +```text +Task 1 DDL + ORM + → Task 2 样本缴费 CRUD + 转账财务状态机(单测) + → Task 3 正式缴费 API + 注册路由 + → Task 4 退费登记 CRUD/API + → Task 5 协议详情/保存接入 version=2 + 总金额 + → Task 6 前端协议表单按样本缴费 + 退费登记 UI + → Task 7 财务确认页改造(按笔、去退费 Tab) + → Task 8 预签约后端(含 convert + 意向金财务) + → Task 9 预签约前端 + 菜单路由 + → Task 10 缴费信息 Tab / 查看弹窗适配 + 冒烟清单 +``` + +--- + +### Task 1: DDL 与 ORM + +**Files:** +- Create: `docs/sql/biz_per_sample_payment_presign.sql` +- Modify: `backend/app/models/models.py` + +- [ ] **Step 1: 编写 DDL** + +在 `docs/sql/biz_per_sample_payment_presign.sql` 中创建(utf8mb4,含公共字段:`tenant_no, order_num, remark, create_by, create_dept_id, create_time, update_by, update_time, del_flag`): + +1. `biz_collection_protocol` 增加: + - `protocol_total_amount` DECIMAL(18,2) NULL + - `source_presign_order_id` VARCHAR(64) NULL + - `payment_model_version` SMALLINT NOT NULL DEFAULT 2 + (已有行若需兼容旧逻辑,上线脚本可将存量更新为 `1`) + +2. `biz_protocol_sample_payment` +3. `biz_protocol_payment_schedule` +4. `biz_protocol_payment_transfer`(`pay_status` 默认 `dfj`;无退费字段) +5. `biz_protocol_refund_record` +6. `biz_presign_order` +7. `biz_presign_order_sample_type` +8. `biz_presign_transfer` + +索引建议: + +- `(tenant_no, collection_protocol_id, del_flag)` on sample_payment / schedule / transfer / refund +- `(tenant_no, sample_code, del_flag)` on sample_payment / transfer +- `(tenant_no, pay_status, del_flag)` on transfer / presign_transfer +- `(tenant_no, protocol_no, del_flag)` on presign_order + +字段定义以设计文档 §5 为准。 + +- [ ] **Step 2: 增加 ORM 类** + +在 `models.py` 中新增对应 `class`,命名: + +- `BizProtocolSamplePayment` +- `BizProtocolPaymentSchedule` +- `BizProtocolPaymentTransfer` +- `BizProtocolRefundRecord` +- `BizPresignOrder` +- `BizPresignOrderSampleType` +- `BizPresignTransfer` + +并在 `BizCollectionProtocol` 上增加三个新 Column。 + +- [ ] **Step 3: 本地执行 DDL**(对开发库) + +```bash +# 在已配置的 MySQL 上执行 docs/sql/biz_per_sample_payment_presign.sql +``` + +- [ ] **Step 4: Commit** + +```bash +git add docs/sql/biz_per_sample_payment_presign.sql backend/app/models/models.py +git commit -m "$(cat <<'EOF' +feat: add per-sample payment and presign order schema + +EOF +)" +``` + +--- + +### Task 2: 样本缴费 CRUD 与转账状态机(TDD) + +**Files:** +- Create: `backend/app/crud/protocol_sample_payment.py` +- Create: `backend/tests/test_protocol_sample_payment.py` + +- [ ] **Step 1: 写失败单测(核心规则)** + +```python +# backend/tests/test_protocol_sample_payment.py +"""纯函数/服务规则单测;DB 集成可按项目惯例用 fixture。""" +from decimal import Decimal + +# 若逻辑放在 services,先测服务层;示例: +from app.services.protocol_transfer_status import ( + can_submit_receipt, + can_confirm_receipt, + schedule_paid_and_arrears, +) + +def test_submit_only_dfj(): + assert can_submit_receipt("dfj") is True + assert can_submit_receipt("jfqrz") is False + assert can_submit_receipt("yjf") is False + +def test_confirm_only_jfqrz(): + assert can_confirm_receipt("jfqrz") is True + assert can_confirm_receipt("dfj") is False + +def test_schedule_arrears(): + paid, arrears = schedule_paid_and_arrears( + due=Decimal("1000"), + confirmed_transfers=[Decimal("300"), Decimal("200")], + ) + assert paid == Decimal("500") + assert arrears == Decimal("500") +``` + +- [ ] **Step 2: 运行确认失败** + +```bash +cd backend && source venv/bin/activate +pytest tests/test_protocol_sample_payment.py -v +``` + +Expected: FAIL(模块不存在) + +- [ ] **Step 3: 实现 `app/services/protocol_transfer_status.py` + CRUD 骨架** + +`protocol_sample_payment.py` 至少包含: + +```python +def upsert_sample_payment(...) -> BizProtocolSamplePayment: ... +def replace_schedules(db, sample_payment_id, schedules: list) -> None: ... +def add_transfer(...) -> BizProtocolPaymentTransfer: ... +def update_transfer_draft(...) -> ... # 仅 dfj +def submit_receipt_finance(db, transfer_id, tenant_no, user_id) -> ... +def confirm_receipt(...) -> ... # jfqrz → yjf +def reject_receipt(...) -> ... # jfqrz → dfj +def list_transfers_for_finance(db, *, pay_status, tenant_no, keyword, page, page_size, user_info): ... +def get_sample_payments_bundle(db, collection_protocol_id, tenant_no) -> list[dict]: ... +``` + +规则: + +- 提交:`pay_status==dfj` 且 `transfer_amount>0` 且 `transfer_type` 非空 → `jfqrz` +- 存在任一批次下转账为 `jfqrz` 时,`replace_schedules` / 改 `pay_mode` 抛 `ValueError` +- 关联键写入 `sample_code`;`protocol_sample_id` 可选 + +- [ ] **Step 4: 再跑单测** + +```bash +pytest tests/test_protocol_sample_payment.py -v +``` + +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/services/protocol_transfer_status.py \ + backend/app/crud/protocol_sample_payment.py \ + backend/tests/test_protocol_sample_payment.py +git commit -m "$(cat <<'EOF' +feat: add sample payment schedule and transfer status rules + +EOF +)" +``` + +--- + +### Task 3: 正式缴费 / 财务 API + +**Files:** +- Create: `backend/app/api/protocol_sample_payment.py` +- Modify: `backend/app/api/__init__.py` +- Modify: `backend/app/main.py` + +- [ ] **Step 1: 实现路由(前缀 `/web/admin`)** + +| 方法 | 路径 | +|------|------| +| GET | `/collection-protocols/{id}/sample-payments` | +| PUT | `/collection-protocols/{id}/sample-payments`(body: 方案+批次+可选草稿转账) | +| POST | `/payment-transfers`(在某 schedule 下新增转账) | +| PUT | `/payment-transfers/{id}`(草稿) | +| POST | `/payment-transfers/{id}/submit-receipt-finance` | +| GET | `/payment-transfers/receipt-confirm` | +| GET | `/payment-transfers/receipt-confirmed` | +| POST | `/payment-transfers/{id}/receipt-confirm` | +| POST | `/payment-transfers/{id}/receipt-reject` | + +鉴权:`require_admin_or_super_admin`;列表 `apply_data_scope`。 + +- [ ] **Step 2: 注册 router**(与现有 `collection_protocol` 相同方式 include) + +- [ ] **Step 3: 手工用 Swagger `/docs` 冒烟一条 dfj→jfqrz→yjf** + +- [ ] **Step 4: Commit** + +```bash +git add backend/app/api/protocol_sample_payment.py backend/app/api/__init__.py backend/app/main.py +git commit -m "$(cat <<'EOF' +feat: expose per-sample payment and transfer finance APIs + +EOF +)" +``` + +--- + +### Task 4: 协议退费登记 + +**Files:** +- Create: `backend/app/crud/protocol_refund_record.py` +- Create: `backend/app/api/protocol_refund_record.py`(或并入 collection_protocol) +- Test: `backend/tests/test_protocol_refund_record.py` + +- [ ] **Step 1: 单测 — 创建登记必填金额>0** + +```python +def test_refund_amount_must_be_positive(): + with pytest.raises(ValueError): + validate_refund_amount(0) + with pytest.raises(ValueError): + validate_refund_amount(-1) + validate_refund_amount(0.01) +``` + +- [ ] **Step 2: 实现 CRUD + API** + +``` +GET/POST /collection-protocols/{id}/refund-records +PUT/DELETE /protocol-refund-records/{id} +``` + +不修改任何 transfer 的 `pay_status`。 + +- [ ] **Step 3: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat: add protocol-level refund registration API + +EOF +)" +``` + +--- + +### Task 5: 协议保存 / 详情接入 version=2 + +**Files:** +- Modify: `backend/app/api/collection_protocol.py` +- Modify: `backend/app/crud/collection_protocol.py`(`get_protocol_detail_bundle`、`_protocol_to_dict`) + +- [ ] **Step 1: `_protocol_to_dict` / bundle 增加** + +```python +"protocolTotalAmount": float(...) if ... else None, +"sourcePresignOrderId": ..., +"paymentModelVersion": int(getattr(obj, "payment_model_version", 1) or 1), +"samplePayments": [...], # version==2 +"refundRecords": [...], +"presignDepositHint": {...}, # 若有来源预签约:已确认意向金合计 +``` + +- [ ] **Step 2: 创建/更新** + +- 新协议默认 `payment_model_version=2` +- body 可收 `protocol_total_amount`、`sample_payments` +- **version=2 时不再要求旧 `payment`/`payment_details` 必填**;旧字段写入仅当 version=1 +- 保存顺序建议:当事人 → 采集 → **样本** → **sample_payments(校验后)**(因方案挂 sample_code,样本须先落库占码) + 注意:与旧「缴费先于样本」不同;新模型转账未占号池,样本编码仍在 `save_samples`,缴费方案在样本之后保存即可。 + +- [ ] **Step 3: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat: wire protocol bundle to per-sample payment model v2 + +EOF +)" +``` + +--- + +### Task 6: 前端协议表单 — 按样本缴费 + 退费登记 + +**Files:** +- Create: `web-admin/src/api/protocol-sample-payment.ts` +- Create: `web-admin/src/utils/protocol-sample-payment.ts` +- Create: `web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue` +- Create: `web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordBlock.vue` +- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` +- Modify: `web-admin/src/utils/status-tag-registry.ts`(如需) + +- [ ] **Step 1: API 与工具函数** + +```typescript +// protocol-sample-payment.ts 工具 +export function sumSampleContractAmounts(payments: { contractAmount?: number }[]) { + return payments.reduce((s, p) => s + (Number(p.contractAmount) || 0), 0) +} + +export function protocolTotalMismatchHint( + total: number | null | undefined, + sampleSum: number +): string | null { + if (total == null || Number.isNaN(Number(total))) return null + if (Math.abs(Number(total) - sampleSum) > 0.015) { + return `协议总金额 ${Number(total).toFixed(2)} 与样本合同合计 ${sampleSum.toFixed(2)} 不一致,请核对` + } + return null +} + +export function schedulePaidArrears( + dueAmount: number, + transfers: { payStatus?: string; transferAmount?: number }[] +) { + const paid = transfers + .filter((t) => t.payStatus === 'yjf') + .reduce((s, t) => s + (Number(t.transferAmount) || 0), 0) + return { paid, arrears: Math.round((dueAmount - paid) * 100) / 100 } +} +``` + +冒烟: + +```bash +cd web-admin && pnpm exec tsx -e " +import { protocolTotalMismatchHint, schedulePaidArrears } from './src/utils/protocol-sample-payment.ts' +console.log(protocolTotalMismatchHint(100, 90)) +console.log(schedulePaidArrears(1000, [{payStatus:'yjf',transferAmount:400}])) +" +``` + +- [ ] **Step 2: `SamplePaymentPanel.vue`** + +- 按 `formData.samples` 渲染每个样本一张卡片(标题用样本类型名 + sampleCode) +- 字段:payMode、contractAmount、分期参数 +- 批次表 + 展开转账笔;转账可「提交财务确认」 +- 分改趸按钮:调用与旧 `applyPayModeSwitch` 同类逻辑,但作用在单样本(可新建 `protocol-sample-pay-mode-switch.ts`) + +- [ ] **Step 3: 改造 `ProtocolFormDialog` 缴费 Tab** + +- 顶部:来源预签约意向金提示(`presignDepositHint`) +- 协议总金额手填;旁路显示样本合计;保存前 `protocolTotalMismatchHint` → `ElMessage.warning` 不阻断 +- `payment_model_version===2`(新建默认)走 `SamplePaymentPanel`;不再使用整单一套旧 paymentDetails 为主路径 +- 隐藏或移除依赖「协议级一套 djfq」的强制校验;样本数与单样本价可用新 preset(单样本价目) + +- [ ] **Step 4: `ProtocolRefundRecordBlock`** + +- 列表 + 新增弹窗:金额、时间、备注、附件(MinIO 卡片上传,folder 如 `protocol-refund-record`) +- 调用 refund-records API + +- [ ] **Step 5: 浏览器手工验收**(两样本分别分期/趸交、3 笔转账提交) + +- [ ] **Step 6: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): per-sample payment UI and refund registration on protocol form + +EOF +)" +``` + +--- + +### Task 7: 财务确认页改造 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/payment-confirm/index.vue` +- Modify: `web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue`(停用) +- Modify: `web-admin/src/api/protocol-sample-payment.ts`(财务 list) +- Modify: `web-admin/src/api/presign-order.ts`(可先留空,Task 8/9 接意向金) +- Modify: `web-admin/src/router/modules/sqxp.ts`(title 可选) +- Modify: `backend/app/api/menu.py`(title + 保持一致) + +- [ ] **Step 1: 缴费确认 Tab 改调 `/payment-transfers/receipt-confirm`** + +卡片字段:协议号、母亲、样本编码、期次、转账金额/时间/方式、来源=`formal`。 + +- [ ] **Step 2: 隐藏退费确认 Tab** + +```typescript +// 仅保留 缴费确认 | 开票申请;RefundConfirmTab 不再挂载 +const tabs = [ + { key: 'receipt', label: '缴费确认' }, + { key: 'invoice', label: '开票申请' }, +] +``` + +- [ ] **Step 3: 菜单文案改为「缴费财务确认及开票」**(`sqxp.ts` + `menu.py`) + +- [ ] **Step 4: 开票暂保持旧 detail API(P1 再改);在 Tab 上加简短注释或 TODO 指向设计 P1——**仅注释,不阻塞 P0** + +- [ ] **Step 5: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): finance confirm by transfer and remove refund confirm tab + +EOF +)" +``` + +--- + +### Task 8: 预签约后端 + +**Files:** +- Create: `backend/app/crud/presign_order.py` +- Create: `backend/app/api/presign_order.py` +- Create: `backend/tests/test_presign_order.py` +- Modify: `backend/app/main.py` / `api/__init__.py` + +- [ ] **Step 1: 单测 convert 与意向金状态** + +```python +def test_presign_transfer_same_status_machine(): + assert can_submit_receipt("dfj") + # convert 不自动创建 sample payment 金额 +``` + +- [ ] **Step 2: CRUD** + +- 预签约主表 + sample_types 多行 + transfers +- `convert_to_protocol`: + - 创建 `biz_collection_protocol`(`source_presign_order_id`、母亲等到 party、样本类型生成 sample 行但**价格/缴费手填空或默认空方案**) + - 预签约 `order_status=converted`,`converted_protocol_id=...` + - **不**把意向金拆到 transfer + - 正式协议 bundle 的 `presignDepositHint.confirmedAmount` = Σ 预签约 yjf 转账 + +- [ ] **Step 3: API** + +``` +CRUD /presign-orders +POST /presign-orders/{id}/convert +意向金转账:submit/confirm/reject(可复用同一状态函数) +GET 财务列表可在 payment-transfers 合并接口中用 source 区分,或 +GET /presign-transfers/receipt-confirm +``` + +推荐:**财务列表单一聚合接口** `GET /finance-receipt-pending` 返回 formal+presign,避免前端两次合并。若求快,前端两次请求 merge 亦可(在 Task 7/9 注明)。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat: add presign order CRUD, deposit transfers, and convert API + +EOF +)" +``` + +--- + +### Task 9: 预签约前端 + 菜单 + +**Files:** +- Create: `web-admin/src/api/presign-order.ts` +- Create: `web-admin/src/views/cell-storage/presign-order/index.vue` +- Create: `web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue` +- Modify: `web-admin/src/router/modules/sqxp.ts` +- Modify: `backend/app/api/menu.py` + +- [ ] **Step 1: 路由**(细胞存储 children 内,采集协议单附近) + +```typescript +{ + id: '50000000000000000000000000000020', + name: 'CellStoragePresignOrder', + path: 'presign-order', + component: '/cell-storage/presign-order/index', + meta: { + title: '预签约订单', + keepAlive: true, + menuCode: 'cell-storage:presign-order:list' + } +} +``` + +`menu.py` 同步一项。 + +- [ ] **Step 2: 列表页** + +对照 `unit/index.vue`:筛选(母亲姓名、协议编号、日期)+ 表格列对齐设计字段;到账金额用已确认合计。 + +- [ ] **Step 3: 表单弹窗** + +- 年/月/日可用一个 `orderDate` 日期控件 +- 样本类型 `el-select` multiple +- 整单 contractAmount、payMode +- 意向金转账笔可增删;提交财务 +- 「转正式协议」按钮 → convert → 跳转或打开正式协议编辑并带意向金提示 + +- [ ] **Step 4: 财务页合并预签约待办**(补完 Task 7) + +卡片 `source` 徽章:预签约 / 正式 + +- [ ] **Step 5: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): add presign order menu and deposit finance flow + +EOF +)" +``` + +--- + +### Task 10: 适配收尾与验收 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue` +- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue`(只读展示 samplePayments + refundRecords + 意向金提示) +- Modify: `web-admin/src/views/cell-storage/protocol-audit/index.vue`(若需入口) + +- [ ] **Step 1: PaymentInfoTab** + +version=2:按转账笔分列(dfj/jfqrz/yjf);去掉提交退费入口。 +version=1:保留旧逻辑分支。 + +- [ ] **Step 2: ViewDialog** + +只读展示各样本方案、批次已缴/欠缴、转账状态 Tag、退费登记列表。 + +- [ ] **Step 3: 验收清单(手工)** + +| # | 场景 | 期望 | +|---|------|------| +| 1 | 新建两样本协议,样本A分期6期、样本B趸交 | 两套方案独立保存 | +| 2 | 趸交批次下录3笔并逐笔财务确认 | 3条待办;已缴累加正确 | +| 3 | 总金额≠样本合计 | 可保存,有 warning | +| 4 | 登记2条退费 | 财务页无退费 Tab;协议可见历史 | +| 5 | 预签约多选类型+意向金确认+转正式 | 正式单见提示;样本金额手填 | +| 6 | 旧 version=1 协议(若有) | 旧缴费 UI/API 仍可用 | + +- [ ] **Step 4: 更新设计文档状态为「实现中/已实现(P0)」** + +- [ ] **Step 5: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat: finish P0 per-sample payment and presign order integration + +EOF +)" +``` + +- [ ] **Step 6:(可选)更新知识图谱** + +```bash +./scripts/update-graph.sh all +``` + +--- + +## P1 后续(本计划不实施,仅登记) + +- 开票 `payment_transfer_id` 改造(设计 §5.9) +- 旧协议迁移工具 +- `paymentSummary` / 质检费用摘要全面切换 +- OA 退费实例 id 回写 `workflow_instance_id` + +--- + +## Spec coverage 自检 + +| 设计章节 | 对应 Task | +|----------|-----------| +| §3 菜单 | Task 7、9 | +| §5 表结构 | Task 1 | +| §6.1 转账状态机 | Task 2、3、8 | +| §6.2 分改趸样本级 | Task 6 | +| §6.4 退费登记 | Task 4、6 | +| §7.1 预签约 UI | Task 9 | +| §7.2 协议缴费 UI | Task 6 | +| §7.3 财务页 | Task 7、9 | +| §8 API | Task 3、4、8 | +| §9 总金额提示 | Task 6 | +| §10 新旧版本 | Task 5、10 | +| §11 P0-a/b/c | Task 1–10 | +| 开票改挂 / 迁移 | P1,未纳入 | + +**Placeholder scan:** 无 TBD 实现步骤;开票 P1 已明确延期。 + +--- + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md`. + +**两种执行方式:** + +1. **Subagent-Driven(推荐)** — 每任务派生子代理,任务间复审,迭代快 +2. **Inline Execution** — 本会话按 executing-plans 连续执行并设检查点 + +你更倾向哪一种? diff --git a/docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md b/docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md new file mode 100644 index 00000000..14b8dce1 --- /dev/null +++ b/docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md @@ -0,0 +1,487 @@ +# 细胞存储三模块现状分析(改码参考) + +**日期**:2026-10-03 +**状态**:现状梳理(非改造设计) +**用途**:后续按新思路改代码前的权威对照基线;描述「当前代码实际如何工作」,而非「应当如何」。 + +**覆盖模块**: + +| 菜单 | Hash 路由 | menuCode | +|------|-----------|----------| +| 采集协议单 | `/#/cell-storage/protocol-audit` | `biz:cellStorageProtocolAudit:list` | +| 样本信息管理 | `/#/cell-storage/sample-info` | `cell-storage:sample-info:list` | +| 缴退费财务确认及开票 | `/#/cell-storage/payment-confirm` | `biz:cellStoragePaymentConfirm:list` | + +**需求权威**:`docs/中科兴皖需求V0.3.md` §2.2.6~2.2.9 + +**相关设计文档**(增量能力): + +- `docs/superpowers/specs/2026-05-13-cell-storage-protocol-modules-design.md`(早期模块设计) +- `docs/superpowers/specs/2026-05-19-protocol-donation-convert-design.md`(转科研) +- `docs/superpowers/specs/2026-06-04-protocol-pay-mode-switch-design.md`(缴费模式切换,若存在) +- `docs/superpowers/specs/2026-06-17-protocol-invoice-request-design.md`(开票) +- `docs/superpowers/specs/2026-06-01-sample-qc-sms-notification-design.md` +- `docs/superpowers/specs/2025-05-23-storage-entry-sms-notification-design.md` +- OpenSpec:`openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md`(细胞存储样本跨表关联键) + +--- + +## 1. 总览:双并行业务架构 + +采集协议单是源头。创建后并行两条线: + +``` +┌──────────────────── 细胞存储 ────────────────────┐ +│ 【采集协议单】 合同 + 客户 + 样本源头 + 缴费配置 │ +│ │ │ +│ ├──────────────┬─────────────────────────┤ +│ ▼ ▼ │ +│ 【样本信息管理】 【缴退费财务确认及开票】 │ +│ 只读运营台+短信 财务终审+开票完成 │ +│ │ ▲ │ +│ ▼ │ 业务侧发起 │ +│ 接收→质检→生产→入库 缴费信息 Tab / 满月待办 │ +└──────────────────────────────────────────────────┘ +``` + +| 模块 | 角色 | 本质 | +|------|------|------| +| 采集协议单 | 市场/采集专员 | 主单据 CRUD、状态变更、缴费发起、开票申请入口 | +| 样本信息管理 | 运营 | **只读查询台** + 线下短信提取/标记(不登记接收、不做质检入库操作) | +| 缴退费财务确认及开票 | 财务 | 收款/退费确认 + 开票完成 | + +**菜单演进**:样本相关 Tab、缴费确认、开票已从早期「单一协议管理页」拆出;隐藏路由仍保留 `protocol`(录入表格版)、`payment-info` 等兼容入口。 + +--- + +## 2. 数据模型(以协议为中心) + +``` +biz_collection_protocol (1) +├── biz_protocol_party_customer (1) 当事人/客户 +├── biz_protocol_collection_info (1) 采集信息 +├── biz_protocol_sample (N) +│ └── biz_protocol_sample_transport +├── biz_protocol_payment (1) +│ └── biz_protocol_payment_detail (N) +├── biz_protocol_audit (N) 审核记录(不改协议状态) +├── biz_protocol_storage_cert +├── biz_protocol_fullmoon_todo +├── biz_protocol_invoice_request (N) +│ └── biz_protocol_invoice_request_item (N) +└── biz_protocol_donation_convert (0..1) 与科研捐赠互转 +``` + +### 2.1 跨表关联硬规则(OpenSpec) + +``` +关联键 = tenant_no + sample_code +禁止用 collection_protocol_id / protocol_sample_id 做业务匹配 +``` + +原因:协议编辑可能重建样本行 `id`;协议可转科研;样本编码不变。接收防重、详情回显、质检/入库 JOIN 均按此规则。 + +### 2.2 协议主表关键字段 + +| 字段 | 含义 | +|------|------| +| `protocol_no` / `protocol_no_id` | 协议号(可自动分配号池) | +| `protocol_status` | `dsx` 待生效 / `sxz` 生效中 / `ygq` 已过期 / `yzf` 已作废 / `sdfq` 属地废弃 / `zky` 转科研 | +| `protocol_attachments` / `consent_form_urls` / `health_survey_form_urls` | 附件 URL 逗号分隔 | +| `void_type` | 作废类型(字典);作废时必填 | +| `collector_user_id` | 采集专员 | +| `effective_*` / `expire_*` / `void_*` / `to_research_*` | 各状态变更时间与描述 | + +无效态 `yzf` / `sdfq`:下游接收、生产列表会过滤。 + +--- + +## 3. 【采集协议单】模块 + +### 3.1 前端结构 + +``` +web-admin/src/views/cell-storage/ +├── protocol/ # 隐藏录入列表 +│ ├── index.vue +│ ├── ProtocolFormDialog.vue # 新增/编辑大表单(权威录入) +│ ├── ProtocolViewDialog.vue +│ ├── ProtocolAuditDialog.vue +│ └── ProtocolStatusChangeDialog.vue +└── protocol-audit/ # 主管理页 + ├── index.vue # Pill Tab + ├── ProtocolAuditTab.vue + ├── PaymentInfoTab.vue # 业务侧缴费看板 + ├── StorageCertTab.vue + ├── FullmoonTodoTab.vue # 满月保险 + 开票申请入口 + └── Sample*InfoTab.vue # 被 sample-info 复用 +``` + +API:`web-admin/src/api/collection-protocol.ts` + +### 3.2 协议状态机 + +``` +创建 → [dsx] 待生效 + │ 协议生效 + ▼ + [sxz] 生效中 ──┬── 过期 → [ygq] + ├── 作废 → [yzf](须 voidType) + ├── 属地废弃 → [sdfq] + └── 转科研 → [zky](convert API,建捐赠单) +``` + +- **审核独立**:只写 `biz_protocol_audit`(`shtg`/`shth`),不驱动 `protocol_status`。 +- **协议号**:空则 `allocate_next_biz_no`;已存在不可改。 +- **删除**:软删主单 + 样本 + 运输,释放样本编码占用。 + +### 3.3 表单保存顺序(API 硬约束) + +`_persist_protocol_sub_resources`(`backend/app/api/collection_protocol.py`): + +``` +1. party_customer(可 upsert biz_customer) +2. collection_info +3. payment + payment_details ← 必须在样本之前 +4. samples(含运输) +5. storage_cert +``` + +原因:缴费校验失败时不得已占用样本编码。创建失败补偿 `soft_delete_collection_protocol`。 + +### 3.4 ProtocolFormDialog 字段分区 + +| Tab | 主要字段 | payload 路径 | +|-----|----------|--------------| +| 基本信息 | 协议状态*、登记日、采集专员、备注、三类附件 | 主表字段 + attachments | +| 采集信息 | 地区、医院、医护、销售、样本/母血采集时间 | `collectionInfo` | +| 样本及运输 | 类型、编码、价格、年限、状态、特殊标记、运输 | `samples[]` + `transport` | +| 客户信息 | 关联客户、母亲/父亲/新生儿全套 | `partyCustomer` | +| 缴费信息 | 模式、合同额、分期参数、明细行 | `payment` + `paymentDetails` | + +- `formRules` **仅强制** `protocolStatus`。 +- 组件:`sample-transport-form-section`(样本+运输权威 UI)。 +- 附件:卡片化立即上传(OpenSpec UI 附件约定以本弹窗为样例)。 + +**提交前前端校验顺序**: + +1. el-form validate +2. 缴费模式 ↔ 有效样本数(`djfq` 仅 2 样本) +3. 新建时样本编码 `usedInProtocol` 占用拦截 +4. 趸交应缴合计 ≠ 合同额 → **仅 warning,不阻断** +5. `syncFqPaymentDetailRowCount` → POST/PUT + +### 3.5 缴费默认定价(样本数 × 模式) + +`web-admin/src/utils/protocol-payment-presets.ts`: + +| 有效样本数(≠ yzf) | 可选模式 | 合同金额示例 | +|---------------------|----------|-------------| +| 1 | `dj` / `fq` | 趸交 22880;分期 25880(6 期) | +| 2 | `dj` / `djfq` / `fq` | 趸交 37880;趸交+分期 41000;分期 44000 | + +### 3.6 缴费模式切换矩阵 + +前端:`protocol-pay-mode-switch.ts` +后端:`protocol_pay_mode_switch.py`(`LOCKED_STATUSES` / `BLOCKING_STATUSES`) + +| 从 → 到 | 条件 | 结果 | +|---------|------|------| +| 任意 | 存在进行中确认(前端还拦 `ytf`) | 阻断 | +| `dj` → `fq`/`djfq` | 已有锁定明细 | 阻断 | +| `dj` → `fq`/`djfq` | 全待缴 | 确认后按期数重建 | +| `fq`/`djfq` → `dj` | 已全部缴清 | 阻断 | +| `fq`/`djfq` → `dj` | 有已缴+未缴 | 保留 yjf,作废 dfj,新增剩余趸交 | +| `fq` ↔ `djfq` | 已有锁定 | 阻断;否则重建 | + +后端保存 P0:`jfqrz`/`tfqrz` 禁止保存;分期条数=期数;比例合计 100%;期数不少于已缴最大期次。 +**注意**:前端 `BLOCKING` 含 `ytf`,后端不含(已退费允许随协议保存只读明细)。 + +### 3.7 后端核心文件 + +| 路径 | 角色 | +|------|------| +| `backend/app/api/collection_protocol.py` | 协议/缴费/储存证/满月/开票 API | +| `backend/app/crud/collection_protocol.py` | 核心 CRUD(约 3900+ 行) | +| `backend/app/services/protocol_pay_mode_switch.py` | 缴费校验 | +| `backend/app/crud/collection_protocol_no.py` | 协议号池 | +| `backend/app/models/models.py` | ORM | + +--- + +## 4. 【样本信息管理】模块 + +### 4.1 定位 + +**不是**样本录入页,**不是**接收/质检/入库操作台。 + +是协议样本的 **全链路查询 + 客户短信触达台**(组件复用自 `protocol-audit/`): + +| Tab | 职责 | 短信 | +|-----|------|------| +| 样本采集信息 | 采集进度 | 无 | +| 样本接收信息 | 接收结论 | 有 | +| 样本质检信息 | 入库质检结论 | 有 | +| 样本入库信息 | 入库/拒入库 | 有 | + +深链:`?tab=sample-collection|sample-reception|sample-qc|storage-entry` +旧路由 `sample-reception-info` / `storage-entry-info` 重定向到本页。 + +### 4.2 样本状态 `sample_status` + +``` +(空) 未采集 → ycj 已采集 → jcz 检测中 → yrk 已入库 +任意 → yzf 已作废 +``` + +- 协议表单录入默认常为 `ycj`。 +- **接收登记不改** `sample_status`。 +- **入库**:`store_sample` 按 `sample_code` 回写 `yrk` + `storage_in_date`。 +- **取消入库**:若原为 `yrk` → 回退 `jcz`,清空 `storage_in_date`。 + +### 4.3 真实写库链路(本页只读展示) + +接收结论 = `js` 时(`create_sample_reception`): + +``` +写 biz_sample_reception + 检查项 + → create_work_order_from_reception # 入库质检工单 dpz + → create_production_work_order_from_reception + → try_auto_dispatch_builtin_tasks… # 内置任务派发 + → create_review_from_reception # 入库放行审核 drk +``` + +结论 = `bj`:只写接收,不创建工单/审核。 + +防重:`tenant_no + reception_source + sample_code`。 +实验室入库编码:结论为 `js` 时必填且租户唯一。 + +### 4.4 质检结论派生 + +`backend/app/services/qc_work_order_conclusion.py`: + +``` +任一任务 conclusion == btg → 工单 btg +全部任务 conclusion == tg → 工单 tg +否则 → ""(尚无结论) +``` + +工单状态:`dpz` → `ypz` →(全部任务 ywc)`ywc` 并落库 `wo.conclusion`。 +`try_reopen_work_order`:回退 `ypz`,清空 conclusion。 + +质检 Tab 列表派生(`_qc_status_label`): + +| 条件 | 码 | +|------|-----| +| 无工单等 | `dzj` 待质检 | +| `ypz` | `zjz` 质检中 | +| `ywc` + tg/btg | `tg` / `btg` | +| `ywc` 结论未齐 | `zjz` | +| 工单 `yzf` | `yzf` | + +协议详情回显:`get_qc_info_map_for_samples` 细胞存储侧必须按 **sample_code** 关联(`trigger_source=reception`)。 + +### 4.5 入库双审 + +`submit_review`(production / qc 两侧): + +- 任一方 `btg` → 聚合 `btg` +- 双方 `tg` → 聚合 `tg` → `storage_transition` = `store` → `store_sample` +- 已入库后结论变 `btg` → `unstore` + +前置:生产与质检均通过且 `storage_status=drk` 才能入库。 + +### 4.6 短信规则 + +系统**不自动发送**。运营:提取文案 → 复制 → 线下发 → 标记已通知。 + +| Tab | 模板要点 | +|-----|----------| +| 接收 | 已于{日期}完成接收,结论:{接收/拒收} | +| 质检 | 合格 / 未通过;未完成返回「暂无短信内容…」 | +| 入库 | 已入库日期 / 审核未通过;待确认暂无内容 | + +### 4.7 与缴退费关系 + +- **无直接 FK**;缴费挂协议不挂样本行。 +- 间接:有效样本数 → 默认定价 / 可选缴费模式。 +- 本页无缴费操作;查看协议弹窗内可见缴费。 + +### 4.8 代码锚点 + +| 路径 | 角色 | +|------|------| +| `web-admin/.../sample-info/index.vue` | 壳页 | +| `web-admin/.../protocol-audit/Sample*InfoTab.vue` | 四 Tab | +| `backend/app/api/protocol_sample_collection.py` | 采集列表 | +| `backend/app/api/sample_sms_notification.py` 等 | 三短信 API | +| `backend/app/crud/sample_reception.py` | 接收写库 | +| `backend/app/crud/qc_work_order.py` | 质检工单 | +| `backend/app/crud/storage_release_review.py` | 入库放行 | + +--- + +## 5. 【缴退费财务确认及开票】模块 + +### 5.1 三角色协作 + +``` +业务(缴费信息 / 满月待办) 财务(本模块) + 提交缴费确认 dfj→jfqrz → 确认收款 → yjf(或退回→dfj) + 提交退费确认 yjf→tfqrz → 确认退费 → ytf(或退回→yjf) + 发起开票申请 → dkp → 完成开票 → ykp(上传发票) +``` + +前端: + +- `payment-confirm/index.vue`(缴费确认) +- `RefundConfirmTab.vue` +- `InvoiceRequestTab.vue` +- 上游:`PaymentInfoTab.vue`、`FullmoonTodoTab.vue` + +### 5.2 缴费明细状态机(`pay_status`) + +``` +dfj → jfqrz → yjf → tfqrz → ytf + ↑退回 ↑退回 +``` + +| 状态 | 业务可做 | 财务可做 | 关键写入 | +|------|----------|----------|----------| +| `dfj` | 编辑草稿、提交确认 | — | 转账方式/金额/时间/凭证 | +| `jfqrz` | — | 确认/退回 | finance_* → yjf 或 → dfj | +| `yjf` | 发起退费 | — | refund_* → tfqrz | +| `tfqrz` | — | 确认/退回 | refund_finance_* → ytf 或 → yjf | +| `ytf` | — | — | 终态 | + +提交收款强制:转账方式非空 + `transfer_amount > 0`。 +提交退费强制:仅 `yjf` + `refund_amount > 0`。 + +**注意**:批量提交收款不跑单条草稿完整性校验(假定已是合格 `dfj`)。 + +### 5.3 开票(独立状态机,不改 `pay_status`) + +``` +可开票 = pay_status=yjf AND 明细未在有效申请子表(dkp|ykp) +金额 = Σ transfer_amount(快照 line_amount / invoice_amount) +完成 = 仅 dkp + 发票附件必填 → ykp +不做 = 驳回、冲红、改缴费状态、结构化发票号/日期 +``` + +- 多期可合并一张发票(1 主表 + N 子表)。 +- 已占用期不可再选;剩余期可另起申请。 +- 抬头六字段申请时 **选填**。 + +函数:`create_invoice_request` / `complete_invoice_request` / `list_invoice_eligible_payment_details`。 + +### 5.4 平行模块(勿混淆) + +| 模块 | 差异 | +|------|------| +| 临床缴退费财务确认 | `biz_pilot_payment`;无开票 Tab | +| 渠道服务订单财务确认 | 另一套流水状态机 | + +--- + +## 6. 协议转科研(`zky`)边界 + +入口:`research_donation.convert_from_storage`。 + +### 6.1 前置 + +- 源协议 `sxz` +- 无有效 `biz_protocol_donation_convert` +- 协议号非空 + +### 6.2 事务内拷贝 + +| 拷贝 | 不拷贝 | +|------|--------| +| 捐赠主单(同号 `donation_no`) | **缴费主表/明细** | +| party / collection | 审核 / 开票 / 储存证 / 满月 | +| samples(同 sample_code)+ transports | 接收/质检/入库审核记录 | + +源协议 → `zky` + `to_research_*`;写关联表 `convert_type=storage_to_research`。 + +### 6.3 同码共存 + +配对后两边可暂共用 `sample_code`;保存唯一性对配对 ID **豁免**(`get_partner_sample_codes`)。 +之后两边可各自改号,**不自动同步**;关联表 `biz_no` 只留转移快照。 + +产品含义:转科研是合同形态切换,**不是财务结算迁移**;退费/开票仍对原协议缴费明细。 + +--- + +## 7. 端到端写库总图 + +``` +协议保存 + 客户 → 采集 →【缴费校验+明细】→ 样本编码占用 → 储存证 + │ │ + ▼ ▼ + 缴费明细状态机 sample_code + dfj→jfqrz→yjf→… │ + │ ▼ + │ 接收(js) → QC + 生产 + 入库审核(drk) + │ │ + ▼ ▼ 双审 tg + 财务确认 / 开票(dkp→ykp) store → sample_status=yrk + +转科研(sxz→zky):新捐赠同号同码;缴费不跟走 +``` + +--- + +## 8. 实现时注意点(改码易踩坑) + +1. **跨表只认 `tenant_no + sample_code`**,勿用样本行 id 做业务匹配。 +2. **缴费先于样本**保存;创建失败要补偿软删。 +3. **审核不改协议状态**;财务确认不改协议状态;开票不改 `pay_status`。 +4. **前端/后端缴费阻断集合不完全一致**(`ytf`)。 +5. **批量提交收款**校验弱于单条。 +6. **转科研不迁缴费**;样本下游历史仍按编码可查。 +7. **管理端色值**用 Fresh Mint(`#10b981`),勿混小程序三墙绿。 +8. 平行「缴退费财务确认」菜单(临床/渠道)表模型不同。 + +--- + +## 9. 关键代码地图(速查) + +### 前端 + +| 模块 | 路径 | +|------|------| +| 协议主页 | `web-admin/src/views/cell-storage/protocol-audit/` | +| 协议表单 | `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` | +| 样本信息 | `web-admin/src/views/cell-storage/sample-info/index.vue` | +| 财务确认 | `web-admin/src/views/cell-storage/payment-confirm/` | +| API | `web-admin/src/api/collection-protocol.ts` 等 | +| 缴费工具 | `web-admin/src/utils/protocol-payment-*.ts`、`protocol-pay-mode-switch.ts` | +| 路由 | `web-admin/src/router/modules/sqxp.ts` | + +### 后端 + +| 模块 | 路径 | +|------|------| +| 协议 API/CRUD | `backend/app/api/collection_protocol.py`、`crud/collection_protocol.py` | +| 样本采集列表 | `api/protocol_sample_collection.py`、`crud/protocol_sample_collection.py` | +| 接收 | `crud/sample_reception.py` | +| 质检 | `crud/qc_work_order.py`、`services/qc_work_order_conclusion.py` | +| 入库 | `crud/storage_release_review.py` | +| 短信 | `crud/sample_sms_notification.py`、`qc_sms_notification.py`、`storage_entry_sms_notification.py` | +| 转科研 | `crud/research_donation.py`(`convert_from_storage`)、`crud/protocol_donation_convert.py` | +| 缴费模式 | `services/protocol_pay_mode_switch.py` | + +--- + +## 10. 后续改码建议用法 + +1. 先在本文确认「现状行为」与「要改的边界」。 +2. 若行为变更跨状态机/表结构,另开 OpenSpec change 或 `docs/superpowers/specs/YYYY-MM-DD-*-design.md`。 +3. 改样本下游关联时,对照 §2.1 与 OpenSpec api-conventions。 +4. 改缴费/开票时,对照 §5 状态矩阵与「明确不做」列表,避免无意引入驳回/冲红等本期未实现能力。 +5. 改协议表单字段时,同步检查:Create/Update Body、`_row_to_dict`、前端 `formData`(见 `docs/dev-field-consistency.md`)。 + +--- + +**文档维护**:若实现偏离本文描述,请同步更新本节或标注「已失效段落」,避免后续改码基于过时基线。 diff --git a/docs/superpowers/specs/2026-10-03-per-sample-payment-presign-design.md b/docs/superpowers/specs/2026-10-03-per-sample-payment-presign-design.md new file mode 100644 index 00000000..28260e86 --- /dev/null +++ b/docs/superpowers/specs/2026-10-03-per-sample-payment-presign-design.md @@ -0,0 +1,481 @@ +# 按样本缴费 + 预签约订单 + 意向金 — 设计说明(方案 2) + +**日期**:2026-10-03 +**状态**:已实现(P0)· 实现计划见 `docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md` +**方案**:方案 2 — 三层缴费(样本方案 → 应缴批次 → 转账笔)+ 预签约独立模块 + +**关联现状分析**:`docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md` + +**关联既有设计**(将被本设计部分取代或调整): + +- `2026-06-04-protocol-pay-mode-switch-design.md`(模式切换下沉到样本级) +- `2026-06-17-protocol-invoice-request-design.md`(开票对象改为转账笔) +- 采集协议单财务退费确认流程(**本期取消**,改为协议级登记 + 工作流审批) + +--- + +## 1. 背景与目标 + +### 1.1 问题 + +现状:1 协议 → 1 套缴费主表 → N 条明细(期次与转账揉在一行);多样本时按**金额之和**做趸交/分期。 +实际业务:每个样本各自趸交/分期;一期/趸交计划下可有**多笔转账**;财务按**笔**确认;退费走工作流,协议侧只需登记。 + +### 1.2 目标 + +1. 正式协议:**每样本一套缴费方案**(合同金额、缴费方式、分期参数)。 +2. 缴费分层:**应缴批次** + **转账笔**;财务确认最小单位 = 转账笔。 +3. 协议级保留**可手填总金额**;与样本合同金额合计不一致时仅提示。 +4. 新增**预签约订单**:录入意向金;按笔财务确认;转正式协议时仅提示意向金金额。 +5. **退费**:协议级登记(金额/备注/时间/附件),审批在工作流;下线协议模块内退费财务确认。 + +### 1.3 非目标(本期不做) + +- 转正式单时自动冲抵/拆分意向金到样本。 +- 强制协议总金额 = 样本合同金额之和。 +- 开票驳回、冲红。 +- 历史协议自动拆分迁移(见 §10;默认新单新模型,旧单兼容策略另定)。 +- 新建独立「退费工作流」菜单(假设使用既有 OA;本模块只登记)。 + +--- + +## 2. 已确认产品决策 + +| # | 议题 | 决策 | +|---|------|------| +| D1 | 财务确认粒度 | **每一笔转账**(转 3 次确认 3 次) | +| D2 | 意向金入口 | **预签约订单**;字段含到账金额、刷卡时间等 | +| D3 | 预签约样本 | **多样本类型多选** | +| D4 | 预签约金额/方式 | **整单一套**合同总额 + 缴费方式 | +| D5 | 意向金财务 | 与正式缴费相同:登记 → 提交财务 → **按笔确认** | +| D6 | 转正式协议 | 缴费区上方**提示意向金数量**;正式金额**全手填**,不自动冲抵 | +| D7 | 协议总金额 | **可手填**;与样本合计不一致仅提示 | +| D8 | 退费 | **不走**采集协议单财务退费确认;工作流审批;协议侧**协议级登记**(可多条历史) | +| D9 | 菜单 | 新增 1 个「预签约订单」;改造「采集协议单」「缴退费财务确认及开票」 | + +--- + +## 3. 菜单与模块范围 + +### 3.1 新增菜单(1) + +| 菜单 | 建议 path | 归属 | +|------|-----------|------| +| 预签约订单 | `/#/cell-storage/presign-order` | 细胞存储,与采集协议单同级 | + +### 3.2 改造菜单(2) + +| 菜单 | 改造要点 | +|------|----------| +| 采集协议单 | 按样本缴费 UI;协议总金额;退费登记;转单意向金提示;缴费 Tab 适配 | +| 缴退费财务确认及开票 | 按转账笔待办(正式 + 预签约意向金);**移除/隐藏退费确认 Tab**;开票挂转账笔;菜单文案建议改为「缴费财务确认及开票」 | + +### 3.3 隐藏/废弃 + +| 项 | 处理 | +|----|------| +| 缴费信息(已隐藏) | 随新模型改造,仍可隐藏 | +| 退费确认(财务)(已隐藏) | 废弃,不再使用 | +| 支付明细上的 `tfqrz`/`ytf` 退费状态机 | 新模型不再使用;旧数据只读兼容 | + +### 3.4 连带 + +| 模块 | 说明 | +|------|------| +| 样本信息管理 | 查看协议弹窗缴费展示适配 | +| OA 工作流 | 退费审批配置(若已有则对接登记;本设计不规定流程节点) | +| 质检/生产费用摘要 | `paymentSummary` 改为按样本/转账笔汇总 | + +--- + +## 4. 领域模型 + +### 4.1 总览 + +``` +biz_presign_order(预签约订单) + ├── 样本类型多选(子表或逗号/JSON) + └── biz_presign_transfer(意向金转账笔)→ 财务按笔确认 + │ + │ 转正式协议(复制客户/样本类型等;缴费手填) + ▼ +biz_collection_protocol + ├── protocol_total_amount(手填总金额) + ├── source_presign_order_id(可空) + ├── biz_protocol_sample (N) + │ └── biz_protocol_sample_payment(每样本 1 套方案) + │ ├── pay_mode / contract_amount / 分期参数 + │ └── biz_protocol_payment_schedule(应缴批次) + │ └── biz_protocol_payment_transfer(转账笔)→ 财务按笔确认 + ├── biz_protocol_refund_record(协议级退费登记,N) + └── biz_protocol_invoice_request + └── item → transfer_id(改挂转账笔) +``` + +### 4.2 与旧模型关系 + +| 旧表 | 新模型 | +|------|--------| +| `biz_protocol_payment`(1/协议) | 改为 **每样本** `biz_protocol_sample_payment`(或扩展旧表加 `sample_code` 且 1 协议 N 行) | +| `biz_protocol_payment_detail`(期+转账合一) | 拆为 **schedule(批次)** + **transfer(转账笔)** | +| detail 上退费字段/状态 | 废弃;改 `biz_protocol_refund_record` | + +**实现选型(推荐)**: + +- **新建** `biz_protocol_sample_payment` / `biz_protocol_payment_schedule` / `biz_protocol_payment_transfer` / `biz_presign_order` / `biz_presign_transfer` / `biz_protocol_refund_record`。 +- 旧 `biz_protocol_payment` + `detail`:**保留只读**服务历史协议(`payment_model_version=1`);新协议 `payment_model_version=2`。 + +(若产品要求「旧单也改成按样本」,另开迁移专项;见 §10。) + +### 4.3 样本关联键 + +缴费方案、批次、转账笔关联样本时: + +- **持久化**:同时存 `protocol_sample_id`(创建时)+ **`sample_code`(业务关联主键)**。 +- 列表/财务/开票 JOIN:**优先 `tenant_no + sample_code`**(与 OpenSpec 一致)。 +- 协议编辑重建样本行 id 后,按 `sample_code` 回绑,避免断链。 + +--- + +## 5. 表结构设计 + +### 5.1 预签约主表 `biz_presign_order` + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | VARCHAR(64) | PK | +| order_date | CHAR(8) | 业务日期 YYYYMMDD(对应年/月/日) | +| protocol_no | VARCHAR(64) | 协议编号(预占或转单后回写,规则实现时定) | +| mother_name | VARCHAR(64) | 母亲姓名 | +| expected_delivery_date | CHAR(8) | 预产期 | +| region_id / hospital_id | VARCHAR(64) | 采集地区/医院 | +| sample_collect_time | DATETIME | 采集时间 | +| pay_mode | VARCHAR(8) | 整单初步缴费方式 `dj`/`fq`/… | +| contract_amount | DECIMAL(18,2) | 整单合同金额 | +| sales_user_id | VARCHAR(64) | 市场专员 | +| region_manager_user_id | VARCHAR(64) | 地区经理 | +| remark | VARCHAR(512) | 备注 | +| order_status | VARCHAR(16) | 如 `draft`/`active`/`converted`/`void`(最终码表实现定) | +| converted_protocol_id | VARCHAR(64) | 转成的正式协议 id,可空 | +| 公共字段 | | tenant_no, create_*, update_*, del_flag, … | + +列表「到账金额」= Σ 已财务确认的意向金转账笔;「刷卡时间」可取最近一笔确认时间或业务填写的代表时间(实现时定展示规则)。 + +### 5.2 预签约样本类型 `biz_presign_order_sample_type` + +| 字段 | 说明 | +|------|------| +| id | PK | +| presign_order_id | 预签约 id | +| sample_type_id | 样本类型 | +| order_num | 排序 | + +(多选几个类型就几行。) + +### 5.3 预签约转账笔 `biz_presign_transfer` + +| 字段 | 说明 | +|------|------| +| id | PK | +| presign_order_id | | +| transfer_amount | 缴费金额 | +| transfer_time | 刷卡/转账时间 | +| transfer_type | 缴费方式(微信/银行等字典) | +| transfer_attachments | 附件 | +| remark | | +| pay_status | 同正式转账笔:`dfj`/`jfqrz`/`yjf`(**无退费态**) | +| finance_confirm_attachments / finance_remark | 财务确认 | +| 公共字段 | | + +### 5.4 协议主表增量 `biz_collection_protocol` + +| 字段 | 说明 | +|------|------| +| protocol_total_amount | 手填协议总金额 | +| source_presign_order_id | 来源预签约,可空 | +| payment_model_version | `1` 旧 / `2` 新;默认新单 = 2 | + +### 5.5 样本缴费方案 `biz_protocol_sample_payment` + +| 字段 | 说明 | +|------|------| +| id | PK | +| collection_protocol_id | | +| protocol_sample_id | 可空兜底 | +| sample_code | **业务关联键** | +| pay_mode | `dj` 趸交 / `fq` 分期 /(分改趸为操作结果,不必新枚举或增加 `fgd` 仅作标记) | +| contract_amount | 该样本合同金额 | +| installment_count / interval / start_date / advance_days / ratios 或 amounts | 分期参数 | +| lump_sum_deadline | 趸交截止 | +| 公共字段 | | + +约束:同一协议下同一 `sample_code` 至多 1 条有效方案(`del_flag=0`)。 + +### 5.6 应缴批次 `biz_protocol_payment_schedule` + +| 字段 | 说明 | +|------|------| +| id | PK | +| sample_payment_id | 方案 id | +| collection_protocol_id | 冗余便于列表 | +| sample_code | 冗余 | +| schedule_no | 期次:趸交可为 1 或 null;分期 1..N | +| due_amount | 应缴金额 | +| due_date | 应缴日期 YYYYMMDD | +| schedule_status | 可选:`open`/`cleared`/`void`(或纯计算) | +| remark | | +| 公共字段 | | + +**已缴金额** = Σ 下属转账笔中 `pay_status=yjf` 的 `transfer_amount` +**欠缴金额** = `due_amount − 已缴`(展示用,可不落库) + +### 5.7 转账笔 `biz_protocol_payment_transfer` + +| 字段 | 说明 | +|------|------| +| id | PK | +| schedule_id | 所属批次 | +| sample_payment_id / collection_protocol_id / sample_code | 冗余 | +| transfer_amount | | +| transfer_time | | +| transfer_type | | +| transfer_attachments | | +| remark | | +| pay_status | `dfj` → `jfqrz` → `yjf` | +| finance_confirm_attachments / finance_remark | | +| 公共字段 | | + +**说明**:退费相关字段**不放**本表。 + +### 5.8 协议退费登记 `biz_protocol_refund_record` + +| 字段 | 说明 | +|------|------| +| id | PK | +| collection_protocol_id | | +| refund_amount | | +| refund_time | | +| refund_remark | | +| refund_attachments | | +| workflow_instance_id | 可选,关联 OA 实例 | +| 公共字段 | | + +不绑样本、不绑转账笔;一协议可多条。 + +### 5.9 开票子表调整 + +`biz_protocol_invoice_request_item`: + +- 新字段 `payment_transfer_id`(正式转账笔)。 +- 可开票条件:转账笔 `pay_status=yjf` 且未被有效开票占用。 +- 金额快照 = 转账笔 `transfer_amount`。 +- 旧 `payment_detail_id`:仅服务 `payment_model_version=1` 历史单。 + +--- + +## 6. 状态机 + +### 6.1 转账笔(正式 + 预签约意向金) + +``` +dfj(待缴费/待提交) + │ 业务提交财务确认 + ▼ +jfqrz(缴费确认中) + │ 财务确认 → yjf + │ 财务退回 → dfj + ▼ +yjf(已缴费) +``` + +- **无** `tfqrz` / `ytf`(新模型)。 +- 仅 `dfj` 可编辑转账草稿;仅 `dfj` 可提交;仅 `jfqrz` 可确认/退回。 + +### 6.2 缴费方式(样本级) + +| 模式 | 批次 | 转账 | +|------|------|------| +| 趸交 `dj` | 通常 1 个应缴批次(应缴=样本合同额) | 批次下可多笔 | +| 分期 `fq` | N 个批次(常 6 期) | 每批次可多笔 | +| 分改趸 | 操作:保留已产生 yjf 的批次;作废未清批次;新建 1 个剩余应缴批次 | 同趸交 | + +规则对齐旧「模式切换」设计,但作用域改为**单样本方案**。存在 `jfqrz` 时禁止切换。 + +### 6.3 预签约订单状态(建议) + +| 状态 | 含义 | +|------|------| +| draft/active | 可编辑、可录意向金 | +| converted | 已转正式协议 | +| void | 作废 | + +转正式:生成/关联 `biz_collection_protocol`,写入 `source_presign_order_id`;**不**自动生成样本缴费金额与转账。 + +### 6.4 退费 + +- 工作流:系统外/OA 完成审批。 +- 协议:CRUD `biz_protocol_refund_record` 登记结果。 +- **不改变**转账笔 `pay_status`。 + +--- + +## 7. 前端页面设计 + +### 7.1 预签约订单(新) + +- 模式:标准列表(对照 `unit` / 复杂列表按需)+ 新增/编辑弹窗。 +- 列表列:对齐用户提供表头(序号、年/月/日、协议编号、母亲姓名、样本类型汇总、地区医院、缴费方式、采集时间、合同金额、到账金额、刷卡时间、预产期、市场专员、地区经理、备注)。 +- 表单:样本类型多选;整单合同额/缴费方式;意向金转账笔可编辑列表(多笔)。 +- 操作:提交财务确认、查看财务状态、**转正式协议**。 + +### 7.2 采集协议单 — 缴费区(改造) + +``` +[提示条] 来源预签约意向金已确认合计:¥xxx(若有) +[协议总金额] 手填 [样本合同合计] 只读汇总 不一致时 warning + +Tab 或折叠:样本 A | 样本 B | … + - 缴费方式、合同金额、分期参数 + - 应缴批次表(应缴、应缴日、已缴、欠缴) + - 展开批次 → 转账笔列表(提交财务) +``` + +- 分改趸:样本方案工具按钮,确认后重建未清批次。 +- **退费登记**:独立区块/弹窗,列表展示历史登记;新增金额/时间/备注/附件。 + +### 7.3 缴费财务确认及开票(改造) + +| Tab | 内容 | +|-----|------| +| 缴费确认 | 待办 = 所有 `jfqrz` 转账笔(正式 + 预签约);已办 = `yjf` | +| ~~退费确认~~ | **删除或隐藏** | +| 开票申请 | 可选 `yjf` 转账笔;逻辑同前,对象改 transfer | + +卡片需展示:协议号、母亲、样本编码(正式)、来源(正式/预签约)、金额、时间。 + +### 7.4 UI 规范 + +- 遵循 Fresh Mint / `docs/web-admin-ui-spec-fresh-mint.md`。 +- 弹窗 `dialog-form-layout`、`label-position=top`。 +- 按钮带图标;状态用 `el-tag`。 +- 模式索引:预签约列表偏 P01;协议缴费改造对照既有 protocol 页。 + +--- + +## 8. 后端 API 草案 + +前缀:`/web/admin`;租户隔离 + 数据权限。 + +### 8.1 预签约 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/presign-orders` | 列表/创建 | +| GET/PUT/DELETE | `/presign-orders/{id}` | 详情/更新/软删 | +| POST | `/presign-orders/{id}/convert` | 转正式协议 | +| POST | `/presign-transfers/{id}/submit-receipt-finance` | 提交确认 | +| POST | `/presign-transfers/{id}/receipt-confirm` / `receipt-reject` | 财务确认/退回 | + +### 8.2 正式样本缴费 + +| 方法 | 路径 | 说明 | +|------|------|------| +| PUT | `/collection-protocols/{id}/sample-payments` | 批量保存各样本方案+批次(或分资源) | +| POST | `/payment-transfers/{id}/submit-receipt-finance` | | +| GET | `/payment-transfers/receipt-confirm` 等 | 财务列表(可与预签约合并查询层) | +| POST | `/payment-transfers/{id}/receipt-confirm` / `reject` | | + +### 8.3 退费登记 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/collection-protocols/{id}/refund-records` | | +| PUT/DELETE | `/protocol-refund-records/{id}` | | + +### 8.4 开票 + +- eligible 改为返回可开票 **transfer** 列表。 +- create item 写 `payment_transfer_id`。 + +### 8.5 废弃 API(新单不再调用) + +- `submit-refund-finance` / `refund-confirm` / `refund-reject`(可保留供旧版协议 `payment_model_version=1`)。 + +--- + +## 9. 定价与校验 + +### 9.1 默认定价 + +- 废弃「1/2 样本共享一套 `djfq` 总价」作为唯一方案。 +- 改为:**单样本类型价目**(或沿用原单样本价)写入该样本 `contract_amount`;双样本 = 两套方案。 +- 协议 `protocol_total_amount` 默认可带出样本合计,允许用户改。 + +### 9.2 保存校验 + +| 规则 | 级别 | +|------|------| +| 样本方案存在 `jfqrz` 时禁止改模式/重生成批次 | 阻断 | +| 分期批次数 = 期数 | 阻断 | +| 批次应缴合计与样本合同金额 | 建议提示,是否强制实现时定(默认提示) | +| 协议总金额 ≠ Σ 样本合同金额 | **仅提示**(D7) | +| 转账提交:金额>0、方式必填 | 阻断 | + +--- + +## 10. 历史数据策略 + +**推荐默认(P0)**: + +- 新创建协议 / 预签约转正:`payment_model_version=2`。 +- 旧协议 `version=1`:继续走旧表与旧财务接口;前端按版本切换 UI。 +- 不做自动「总账拆到样本」。 + +**可选 P1**:人工/脚本迁移工具(按规则挂到第一样本或均分)— 需单独评审,不阻塞 P0。 + +--- + +## 11. 实施分期建议 + +| 阶段 | 内容 | +|------|------| +| P0-a | 表结构 + 样本缴费方案/批次/转账笔 CRUD + 协议总金额 + 按笔财务确认(正式) | +| P0-b | 采集协议单缴费 UI 改造;财务确认页改造;下线退费确认 Tab;退费登记 | +| P0-c | 预签约订单模块 + 意向金按笔确认 + 转正式 + 意向金提示 | +| P1 | 开票改挂 transfer;旧单迁移工具;菜单文案「缴费财务确认及开票」 | +| P2 | 费用摘要/查看弹窗等全量适配;文档与 OpenSpec 归档 | + +--- + +## 12. 风险与依赖 + +1. **协议编辑重建样本 id**:必须靠 `sample_code` 回绑缴费方案。 +2. **财务待办量增加**:按笔后卡片变多,需分页与筛选(协议号/母亲/样本编码/来源)。 +3. **开票**:与旧 detail 开票并存一期,注意双写/双读分支。 +4. **OA 退费**:本模块只登记;若工作流未就绪,登记仍可先做。 +5. **权限**:预签约、转账确认、退费登记需独立或复用 menuCode。 + +--- + +## 13. 验收要点(摘要) + +1. 两样本协议可分别设趸交/分期,互不影响。 +2. 同一批次下登记 3 笔转账 → 财务出现 3 条待确认 → 确认后批次已缴累加正确。 +3. 协议总金额与样本合计不符时能保存且有提示。 +4. 预签约多选样本类型;意向金多笔确认;转正式后缴费区显示意向金提示且金额不自动写入样本。 +5. 协议可登记多条退费;财务页无退费确认待办(新单)。 +6. 旧版协议(若保留)仍可按旧流程处理收款。 + +--- + +## 14. 文档自检 + +- [x] 无「TBD」占位关键决策(实施码表细节已标明「实现时定」) +- [x] 与 D1–D9 无矛盾 +- [x] 退费与按笔收款边界清晰 +- [x] 菜单范围与先前确认一致(+1 预签约,改 2 个既有) +- [x] 未纳入自动冲抵意向金、强制总金额相等 + +--- + +**请审阅本文**。确认后可进入 `writing-plans` 拆实现任务;若需修改决策,直接标注章节号即可。 diff --git a/web-admin/src/api/protocol-sample-payment.ts b/web-admin/src/api/protocol-sample-payment.ts index 50543e1f..0f28ffa6 100644 --- a/web-admin/src/api/protocol-sample-payment.ts +++ b/web-admin/src/api/protocol-sample-payment.ts @@ -48,6 +48,20 @@ export function fetchSubmitPaymentTransferFinance(transferId: string) { }) } +export function fetchPaymentTransferList(params?: { + page?: number + pageSize?: number + payStatus?: string + keyword?: string + installmentDueDateStart?: string + installmentDueDateEnd?: string +}) { + return request.get>>({ + url: '/web/admin/payment-transfers', + params + }) +} + export function fetchPaymentTransferReceiptConfirmList(params?: { page?: number pageSize?: number diff --git a/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue b/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue index 8a6fa90d..49ff4eb4 100644 --- a/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue +++ b/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue @@ -87,7 +87,12 @@ size="small" class="payment-card__status" > - {{ protocolStatusDisplayLabel(String(row.protocolStatus || ''), String(row.voidType || '')) }} + {{ + protocolStatusDisplayLabel( + String(row.protocolStatus || ''), + String(row.voidType || '') + ) + }} @@ -158,13 +163,7 @@ > 缴费确认 - - 退费确认 - + @@ -260,12 +259,7 @@ /> - +
@@ -342,108 +332,6 @@ - - -
- - - - - - - -
-
- -
- - 上传附件 -
-
-
-
- - -
- -
-
-

- 支持拖拽或点击上传图片、文档等,多个附件将一并提交,单文件不超过 20MB -

-
-
-
-
- -
- >({ dfj: createColumnState(), jfqrz: createColumnState(), - yjf: createColumnState(), - tfqrz: createColumnState(), - ytf: createColumnState() + yjf: createColumnState() }) const expandedCardIds = ref>(new Set()) const viewVisible = ref(false) @@ -580,6 +468,7 @@ const receiptSaveLoading = ref(false) const receiptSubmitLoading = ref(false) const receiptTargetId = ref('') + const receiptIsTransfer = ref(false) const receiptPreviewRow = ref | null>(null) const receiptFormRef = ref() const receiptForm = reactive({ @@ -594,24 +483,6 @@ transferAmount: [{ required: true, message: '请填写转账金额', trigger: 'blur' }] } - const refundDialogVisible = ref(false) - const refundSubmitLoading = ref(false) - const refundTargetIds = ref([]) - const refundFormRef = ref() - const refundForm = reactive({ - refundAmount: null as number | null, - refundRemark: '', - attachments: [] as Array<{ - id: string - fileName: string - fileUrl: string - _uploading?: boolean - }> - }) - const refundFormRules: FormRules = { - refundAmount: [{ required: true, message: '请填写退费金额', trigger: 'blur' }] - } - let loadRequestId = 0 const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '') @@ -625,18 +496,40 @@ } } + function isTransferRow(row: Record) { + return row.source === 'formal' || Boolean(row.sampleCode && row.scheduleId) + } + async function loadColumn(key: PayStatusKey) { const col = columnStates[key] col.loading = true try { - const res = await fetchPaymentDetailList({ - page: col.page, - pageSize: col.pageSize, - payStatus: key, - ...listQueryParams() - }) - col.list = res.list || [] - col.total = res.total ?? 0 + const q = listQueryParams() + const [legacy, transfer] = await Promise.all([ + fetchPaymentDetailList({ + page: col.page, + pageSize: col.pageSize, + payStatus: key, + ...q + }), + fetchPaymentTransferList({ + page: col.page, + pageSize: col.pageSize, + payStatus: key, + keyword: q.keyword, + installmentDueDateStart: q.installmentDueDateStart, + installmentDueDateEnd: q.installmentDueDateEnd + }) + ]) + let transferList = transfer.list || [] + if (q.transferType) { + transferList = transferList.filter( + (r) => String(r.transferType || '') === String(q.transferType) + ) + } + // 新转账笔优先展示,其后拼接旧 detail(version=1) + col.list = [...transferList, ...(legacy.list || [])] + col.total = (transfer.total ?? 0) + (legacy.total ?? 0) } finally { col.loading = false } @@ -742,12 +635,11 @@ const id = String(row.id || '') if (!id) return receiptTargetId.value = id + receiptIsTransfer.value = isTransferRow(row) receiptPreviewRow.value = row receiptForm.transferType = String(row.transferType || '') receiptForm.transferAmount = - row.transferAmount != null && row.transferAmount !== '' - ? Number(row.transferAmount) - : null + row.transferAmount != null && row.transferAmount !== '' ? Number(row.transferAmount) : null receiptForm.transferTime = row.transferTime ? String(row.transferTime) : '' receiptForm.remark = String(row.remark || '') receiptForm.attachments = mapRowAttachments(row.attachments, row.transferAttachments) @@ -843,7 +735,12 @@ } receiptSaveLoading.value = true try { - await fetchUpdatePaymentDetailDraft(receiptTargetId.value, buildReceiptPayload()) + const payload = buildReceiptPayload() + if (receiptIsTransfer.value) { + await fetchUpdatePaymentTransfer(receiptTargetId.value, payload) + } else { + await fetchUpdatePaymentDetailDraft(receiptTargetId.value, payload) + } receiptDialogVisible.value = false loadAllColumns() } finally { @@ -874,7 +771,13 @@ } receiptSubmitLoading.value = true try { - await fetchSubmitReceiptFinance(receiptTargetId.value, buildReceiptPayload()) + const payload = buildReceiptPayload() + if (receiptIsTransfer.value) { + await fetchUpdatePaymentTransfer(receiptTargetId.value, payload) + await fetchSubmitPaymentTransferFinance(receiptTargetId.value) + } else { + await fetchSubmitReceiptFinance(receiptTargetId.value, payload) + } receiptDialogVisible.value = false loadAllColumns() } finally { @@ -882,31 +785,6 @@ } } - function resetRefundForm(defaultAmount?: number) { - refundForm.refundAmount = defaultAmount ?? null - refundForm.refundRemark = '' - refundForm.attachments = [] - refundFormRef.value?.clearValidate() - } - - function openRefundFinanceDialog(row: Record) { - const id = String(row.id || '') - if (!id) return - refundTargetIds.value = [id] - const defaultAmount = row.transferAmount != null ? Number(row.transferAmount) : undefined - resetRefundForm( - defaultAmount != null && !Number.isNaN(defaultAmount) ? defaultAmount : undefined - ) - refundDialogVisible.value = true - } - - function refundAttachmentsToUrls() { - return refundForm.attachments - .filter((f) => f.fileUrl && !f._uploading) - .map((f) => f.fileUrl) - .join(',') - } - function fullFileUrl(url: string) { if (!url) return '' if (/^https?:\/\//i.test(url)) return url @@ -917,70 +795,6 @@ return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url) } - async function handleRefundAttachmentChange(uploadFile: { raw?: File }) { - const file = uploadFile.raw - if (!file) return - const tempId = `temp_${Date.now()}` - refundForm.attachments.push({ - id: tempId, - fileName: file.name, - fileUrl: '', - _uploading: true - }) - const idx = refundForm.attachments.length - 1 - try { - const fd = new FormData() - fd.append('folder', 'protocol-payment-refund') - fd.append('file', file) - const res = await fetchUploadAttachment(fd) - refundForm.attachments[idx] = { - id: res.id || tempId, - fileName: file.name || res.fileName, - fileUrl: res.fileUrl, - _uploading: false - } - } catch { - ElMessage.error('文件上传失败') - refundForm.attachments.splice(idx, 1) - } - } - - function removeRefundAttachment(index: number) { - refundForm.attachments.splice(index, 1) - } - - const { isDragover: isRefundAttachDragover, dropZoneHandlers: refundAttachDropHandlers } = - useAttachmentDropZone({ - onFiles: (files) => { - for (const file of files) { - void handleRefundAttachmentChange({ raw: file }) - } - } - }) - - async function submitRefundFinance() { - if (!refundTargetIds.value.length) return - const valid = await refundFormRef.value?.validate().catch(() => false) - if (!valid) return - if (refundForm.attachments.some((f) => f._uploading)) { - ElMessage.warning('附件上传中,请稍候') - return - } - const payload = { - refundAmount: Number(refundForm.refundAmount), - refundRemark: refundForm.refundRemark?.trim() || undefined, - refundAttachments: refundAttachmentsToUrls() || undefined - } - refundSubmitLoading.value = true - try { - await fetchSubmitRefundFinance(refundTargetIds.value[0], payload) - refundDialogVisible.value = false - loadAllColumns() - } finally { - refundSubmitLoading.value = false - } - } - function handleSearch() { loadAllColumns(true) } diff --git a/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue b/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue index a910bb96..678eb8ab 100644 --- a/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue +++ b/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue @@ -43,7 +43,12 @@ >{{ protocolStatusDisplayLabel(String(protocol?.protocolStatus || ''), String(protocol?.voidType || '')) }}{{ + protocolStatusDisplayLabel( + String(protocol?.protocolStatus || ''), + String(protocol?.voidType || '') + ) + }}
@@ -91,7 +96,9 @@