状态: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,不在本计划强制交付。
| 文件 | 职责 |
|---|---|
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 / 查看弹窗适配 + 冒烟清单
Files:
docs/sql/biz_per_sample_payment_presign.sqlModify: 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):
biz_collection_protocol 增加:
protocol_total_amount DECIMAL(18,2) NULLsource_presign_order_id VARCHAR(64) NULLpayment_model_version SMALLINT NOT NULL DEFAULT 21)biz_protocol_sample_payment
biz_protocol_payment_schedule
biz_protocol_payment_transfer(pay_status 默认 dfj;无退费字段)
biz_protocol_refund_record
biz_presign_order
biz_presign_order_sample_type
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 为准。
在 models.py 中新增对应 class,命名:
BizProtocolSamplePaymentBizProtocolPaymentScheduleBizProtocolPaymentTransferBizProtocolRefundRecordBizPresignOrderBizPresignOrderSampleTypeBizPresignTransfer并在 BizCollectionProtocol 上增加三个新 Column。
# 在已配置的 MySQL 上执行 docs/sql/biz_per_sample_payment_presign.sql
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
)"
Files:
backend/app/crud/protocol_sample_payment.pyCreate: 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")
cd backend && source venv/bin/activate
pytest tests/test_protocol_sample_payment.py -v
Expected: FAIL(模块不存在)
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 非空 → jfqrzjfqrz 时,replace_schedules / 改 pay_mode 抛 ValueError关联键写入 sample_code;protocol_sample_id 可选
[ ] Step 4: 再跑单测
pytest tests/test_protocol_sample_payment.py -v
Expected: PASS
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
)"
Files:
backend/app/api/protocol_sample_payment.pybackend/app/api/__init__.pyModify: 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
)"
Files:
backend/app/crud/protocol_refund_record.pybackend/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。
git commit -m "$(cat <<'EOF'
feat: add protocol-level refund registration API
EOF
)"
Files:
backend/app/api/collection_protocol.pyModify: 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
)"
Files:
web-admin/src/api/protocol-sample-payment.tsweb-admin/src/utils/protocol-sample-payment.tsweb-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vueweb-admin/src/views/cell-storage/protocol/ProtocolRefundRecordBlock.vueweb-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vueModify: 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
)"
Files:
web-admin/src/views/cell-storage/payment-confirm/index.vueweb-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue(停用)web-admin/src/api/protocol-sample-payment.ts(财务 list)web-admin/src/api/presign-order.ts(可先留空,Task 8/9 接意向金)web-admin/src/router/modules/sqxp.ts(title 可选)Modify: backend/app/api/menu.py(title + 保持一致)
[ ] Step 1: 缴费确认 Tab 改调 /payment-transfers/receipt-confirm
卡片字段:协议号、母亲、样本编码、期次、转账金额/时间/方式、来源=formal。
// 仅保留 缴费确认 | 开票申请;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
)"
Files:
backend/app/crud/presign_order.pybackend/app/api/presign_order.pybackend/tests/test_presign_order.pyModify: 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=...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 注明)。
git commit -m "$(cat <<'EOF'
feat: add presign order CRUD, deposit transfers, and convert API
EOF
)"
Files:
web-admin/src/api/presign-order.tsweb-admin/src/views/cell-storage/presign-order/index.vueweb-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vueweb-admin/src/router/modules/sqxp.tsModify: 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 同步一项。
对照 unit/index.vue:筛选(母亲姓名、协议编号、日期)+ 表格列对齐设计字段;到账金额用已确认合计。
[ ] Step 3: 表单弹窗
年/月/日可用一个 orderDate 日期控件
样本类型 el-select multiple
整单 contractAmount、payMode
意向金转账笔可增删;提交财务
「转正式协议」按钮 → convert → 跳转或打开正式协议编辑并带意向金提示
[ ] Step 4: 财务页合并预签约待办(补完 Task 7)
卡片 source 徽章:预签约 / 正式
git commit -m "$(cat <<'EOF'
feat(web-admin): add presign order menu and deposit finance flow
EOF
)"
Files:
web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vueweb-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue(只读展示 samplePayments + refundRecords + 意向金提示)Modify: web-admin/src/views/cell-storage/protocol-audit/index.vue(若需入口)
[x] Step 1: PaymentInfoTab
version=2:按转账笔分列(dfj/jfqrz/yjf);去掉提交退费入口。
version=1:保留旧逻辑分支。
只读展示各样本方案、批次已缴/欠缴、转账状态 Tag、退费登记列表。
| # | 场景 | 期望 |
|---|---|---|
| 1 | 新建两样本协议,样本A分期6期、样本B趸交 | 两套方案独立保存 |
| 2 | 趸交批次下录3笔并逐笔财务确认 | 3条待办;已缴累加正确 |
| 3 | 总金额≠样本合计 | 可保存,有 warning |
| 4 | 登记2条退费 | 财务页无退费 Tab;协议可见历史 |
| 5 | 预签约多选类型+意向金确认+转正式 | 正式单见提示;样本金额手填 |
| 6 | 旧 version=1 协议(若有) | 旧缴费 UI/API 仍可用 |
[x] Step 4: 更新设计文档状态为「实现中/已实现(P0)」
[x] Step 5: Commit — e7677d16
[x] Step 6:(可选)更新知识图谱
./scripts/update-graph.sh all
payment_transfer_id 改造(设计 §5.9)paymentSummary / 质检费用摘要全面切换workflow_instance_id| 设计章节 | 对应 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 已明确延期。
Plan complete and saved to docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md.
两种执行方式:
你更倾向哪一种?