Wire finance transfer list, protocol view v2 readonly payment, and mark design/plan as P0 implemented. Co-authored-by: Cursor <cursoragent@cursor.com>
22 KiB
按样本缴费 + 预签约订单 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):
-
biz_collection_protocol增加:protocol_total_amountDECIMAL(18,2) NULLsource_presign_order_idVARCHAR(64) NULLpayment_model_versionSMALLINT NOT NULL DEFAULT 2
(已有行若需兼容旧逻辑,上线脚本可将存量更新为1)
-
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 为准。
- Step 2: 增加 ORM 类
在 models.py 中新增对应 class,命名:
BizProtocolSamplePaymentBizProtocolPaymentScheduleBizProtocolPaymentTransferBizProtocolRefundRecordBizPresignOrderBizPresignOrderSampleTypeBizPresignTransfer
并在 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-selectmultiple -
整单 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.
两种执行方式:
- Subagent-Driven(推荐) — 每任务派生子代理,任务间复审,迭代快
- Inline Execution — 本会话按 executing-plans 连续执行并设检查点
你更倾向哪一种?