Files
zkxw/docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md
T
zlwuandCursor e7677d1661 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 <cursoragent@cursor.com>
2026-10-03 11:25:47 +08:00

22 KiB
Raw Blame History

按样本缴费 + 预签约订单 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

依赖顺序

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(对开发库)
# 在已配置的 MySQL 上执行 docs/sql/biz_per_sample_payment_presign.sql
  • Step 4: Commit
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: 写失败单测(核心规则)

# 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: 运行确认失败
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 至少包含:

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: 再跑单测

pytest tests/test_protocol_sample_payment.py -v

Expected: PASS

  • Step 5: Commit
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

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

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
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 增加

"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

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 与工具函数

// 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 }
}

冒烟:

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

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
// 仅保留 缴费确认 | 开票申请;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

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 与意向金状态

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
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 内,采集协议单附近)

{
  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
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

git commit -m "$(cat <<'EOF'
feat: finish P0 per-sample payment and presign order integration

EOF
)"
  • Step 6:(可选)更新知识图谱
./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 连续执行并设检查点

你更倾向哪一种?