Files
zkxw/docs/superpowers/plans/2026-10-10-transfer-remark-and-withdraw.md
T

18 KiB
Raw Blame History

转账业务备注 + 缴费确认中撤回 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 意向金与正式协议转账每笔支持业务备注录入/展示,并展示财务备注;jfqrz 支持业务撤回为 dfj 后修改再提交;yjf 不可撤回。

Architecture: 复用表字段 remark / finance_remark,无 DDL。状态规则集中在 protocol_transfer_status.can_withdraw_receipt;预签与正式各增 withdraw_receipt CRUD + POST .../withdraw API。前端转账表统一列:备注(dfj 可编)+ 财务备注(只读)+ jfqrz 操作列「撤回」。

Tech Stack: FastAPI / SQLAlchemy / unittest;Vue 3 + Element Plus;现有 presign-order / protocol-sample-payment API 模块。

Spec: docs/superpowers/specs/2026-10-10-transfer-remark-and-withdraw-design.md


File map

File Responsibility
backend/app/services/protocol_transfer_status.py can_withdraw_receipt
backend/tests/test_presign_order.py(或同目录新测) 撤回规则 + 预签 withdraw 行为单测
backend/tests/test_protocol_sample_payment.py 正式转账 withdraw 行为单测(可用纯函数+mock 或既有风格)
backend/app/crud/presign_order.py withdraw_receipt
backend/app/crud/protocol_sample_payment.py withdraw_receipt(正式)
backend/app/api/presign_order.py POST /presign-transfers/{id}/withdraw
backend/app/api/protocol_sample_payment.py POST /payment-transfers/{id}/withdraw
web-admin/src/api/presign-order.ts fetchWithdrawPresignTransfer
web-admin/src/api/protocol-sample-payment.ts fetchWithdrawPaymentTransfer
web-admin/.../PresignOrderFormDialog.vue 备注列 + 撤回
web-admin/.../SamplePaymentPanel.vue 备注列 + 撤回
web-admin/.../ProtocolFormDialog.vue 意向金只读表补备注列(财务备注若已有则对齐)
web-admin/.../ProtocolViewDialog.vue 意向金 + 样本转账只读补列
web-admin/.../payment-confirm/index.vue 列表补业务「备注」列
web-admin/.../payment-info/PaymentExcelList.vue 期次转账明细卡补备注/财务备注

SQL: 无需迁移。


Task 1: 状态规则 can_withdraw_receipt(TDD)

Files:

  • Modify: backend/app/services/protocol_transfer_status.py

  • Modify: backend/tests/test_presign_order.py

  • Step 1: 写失败用例

在 TestPresignTransferStatus(或新建 TestWithdrawReceipt)增加:

def test_can_withdraw_only_jfqrz(self):
    from app.services.protocol_transfer_status import can_withdraw_receipt

    self.assertTrue(can_withdraw_receipt("jfqrz"))
    self.assertFalse(can_withdraw_receipt("dfj"))
    self.assertFalse(can_withdraw_receipt("yjf"))
    self.assertFalse(can_withdraw_receipt(None))
    self.assertFalse(can_withdraw_receipt(""))
  • Step 2: 跑测确认失败
cd backend && source venv/bin/activate
python -m pytest tests/test_presign_order.py::TestPresignTransferStatus::test_can_withdraw_only_jfqrz -q

Expected: ImportError 或 AttributeError(函数不存在)。

  • Step 3: 最小实现

在 protocol_transfer_status.py 增加:

def can_withdraw_receipt(pay_status: Optional[str]) -> bool:
    """仅缴费确认中可撤回为待缴费。"""
    return (pay_status or "").strip() == "jfqrz"
  • Step 4: 跑测确认通过
python -m pytest tests/test_presign_order.py::TestPresignTransferStatus::test_can_withdraw_only_jfqrz -q

Expected: PASS

  • Step 5: Commit
git add backend/app/services/protocol_transfer_status.py backend/tests/test_presign_order.py
git commit -m "$(cat <<'EOF'
feat: add can_withdraw_receipt for jfqrz transfer withdraw.

EOF
)"

Task 2: 预签 withdraw_receipt CRUD + API(TDD)

Files:

  • Modify: backend/app/crud/presign_order.py

  • Modify: backend/app/api/presign_order.py

  • Modify: backend/tests/test_presign_order.py

  • Step 1: 写失败用例(纯逻辑,用 SimpleNamespace 模拟行)

class TestPresignWithdrawReceipt(unittest.TestCase):
    def test_withdraw_sets_dfj_keeps_finance_remark(self):
        from types import SimpleNamespace
        from unittest.mock import MagicMock
        from app.crud import presign_order as po

        t = SimpleNamespace(
            id="t1",
            pay_status="jfqrz",
            finance_remark="keep-me",
            finance_confirm_attachments="http://x",
            update_by=None,
            update_time=None,
        )
        db = MagicMock()
        # patch _get_transfer_or_raise
        orig = po._get_transfer_or_raise
        po._get_transfer_or_raise = lambda *a, **k: t
        try:
            out = po.withdraw_receipt(
                db, transfer_id="t1", tenant_no="zkxw", user_id="u1"
            )
            self.assertEqual(out.pay_status, "dfj")
            self.assertEqual(out.finance_remark, "keep-me")
            self.assertEqual(out.finance_confirm_attachments, "http://x")
            db.commit.assert_called()
        finally:
            po._get_transfer_or_raise = orig

    def test_withdraw_rejects_non_jfqrz(self):
        from types import SimpleNamespace
        from unittest.mock import MagicMock
        from app.crud import presign_order as po

        t = SimpleNamespace(id="t1", pay_status="yjf", finance_remark="x")
        db = MagicMock()
        orig = po._get_transfer_or_raise
        po._get_transfer_or_raise = lambda *a, **k: t
        try:
            with self.assertRaises(ValueError) as ctx:
                po.withdraw_receipt(
                    db, transfer_id="t1", tenant_no="zkxw", user_id="u1"
                )
            self.assertIn("撤回", str(ctx.exception))
        finally:
            po._get_transfer_or_raise = orig
  • Step 2: 跑测确认失败
python -m pytest tests/test_presign_order.py::TestPresignWithdrawReceipt -q

Expected: FAIL(withdraw_receipt 不存在)。

  • Step 3: 实现 CRUD

在 presign_order.py 中 submit_receipt_finance 附近增加:

from app.services.protocol_transfer_status import (
    can_confirm_receipt,
    can_submit_receipt,
    can_withdraw_receipt,
)

def withdraw_receipt(
    db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
) -> BizPresignTransfer:
    t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
    if not can_withdraw_receipt(t.pay_status):
        raise ValueError("仅缴费确认中可撤回")
    t.pay_status = "dfj"
    # 故意不改 finance_remark / finance_confirm_attachments
    t.update_by = user_id
    t.update_time = _now()
    db.add(t)
    db.commit()
    db.refresh(t)
    return t

API(对齐 submit 路径)在 presign_order.py router:

@router.post("/presign-transfers/{transfer_id}/withdraw")
async def withdraw_presign_transfer(
    transfer_id: str,
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    try:
        tenant_no = _get_tenant_no(db, user)
        user_id = _get_current_user_id(user)
        obj = po_svc.withdraw_receipt(
            db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
        )
        return success_response(
            po_svc._transfer_to_dict(db, obj, tenant_no), message="已撤回"
        )
    except PermissionError as e:
        return error_response(403, str(e))
    except ValueError as e:
        return error_response(400, str(e))
  • Step 4: 跑测通过
python -m pytest tests/test_presign_order.py::TestPresignWithdrawReceipt tests/test_presign_order.py::TestPresignTransferStatus -q

Expected: PASS

  • Step 5: Commit
git add backend/app/crud/presign_order.py backend/app/api/presign_order.py backend/tests/test_presign_order.py
git commit -m "$(cat <<'EOF'
feat: add presign transfer withdraw API for jfqrz.

EOF
)"

Task 3: 正式协议转账 withdraw_receipt + API(TDD)

Files:

  • Modify: backend/app/crud/protocol_sample_payment.py

  • Modify: backend/app/api/protocol_sample_payment.py

  • Modify: backend/tests/test_protocol_sample_payment.py(或新建同类用例)

  • Step 1: 写失败用例

仿 Task 2,对 protocol_sample_payment.withdraw_receipt 写:

  • jfqrz → dfj,finance_remark 不变
  • dfj / yjf → ValueError

获取转账行用该模块现有 _get_transfer_or_raise(若无私有 getter,则用与 submit_receipt_finance 相同的查找逻辑抽出或 mock)。

先读 submit_receipt_finance(约 792 行起)确认取行方式,withdraw 与之对称。

  • Step 2: 跑测确认失败
python -m pytest tests/test_protocol_sample_payment.py -k withdraw -q

Expected: FAIL

  • Step 3: 实现
def withdraw_receipt(
    db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
) -> BizProtocolPaymentTransfer:
    t = ...  # 与 submit 相同取行
    if not can_withdraw_receipt(t.pay_status):
        raise ValueError("仅缴费确认中可撤回")
    t.pay_status = "dfj"
    t.update_by = user_id
    t.update_time = _now()
    db.add(t)
    db.commit()
    db.refresh(t)
    return t

API:

@router.post("/payment-transfers/{transfer_id}/withdraw")
async def withdraw_payment_transfer(...):
    # 同 submit_payment_transfer_finance:取租户、调 withdraw、_protocol_accessible、返回 _transfer_to_dict
  • Step 4: 跑测通过 + 确认草稿更新仍可写 remark(既有 _apply_transfer_editable_fields 已含 remark,无需改 schema)
python -m pytest tests/test_protocol_sample_payment.py -k withdraw -q
python -m pytest tests/test_presign_order.py -q

Expected: PASS

  • Step 5: Commit
git add backend/app/crud/protocol_sample_payment.py backend/app/api/protocol_sample_payment.py backend/tests/test_protocol_sample_payment.py
git commit -m "$(cat <<'EOF'
feat: add protocol payment transfer withdraw API for jfqrz.

EOF
)"

Task 4: 前端 API 封装

Files:

  • Modify: web-admin/src/api/presign-order.ts

  • Modify: web-admin/src/api/protocol-sample-payment.ts

  • Step 1: 增加函数

presign-order.ts(紧挨 fetchSubmitPresignTransferFinance):

export function fetchWithdrawPresignTransfer(transferId: string) {
  return request({
    url: `/web/admin/presign-transfers/${transferId}/withdraw`,
    method: 'post'
  })
}

protocol-sample-payment.ts:

export function fetchWithdrawPaymentTransfer(transferId: string) {
  return request({
    url: `/web/admin/payment-transfers/${transferId}/withdraw`,
    method: 'post'
  })
}
  • Step 2: Commit
git add web-admin/src/api/presign-order.ts web-admin/src/api/protocol-sample-payment.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add withdraw APIs for presign and payment transfers.

EOF
)"

Task 5: 预签表单 — 备注列 + 撤回

Files:

  • Modify: web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue

  • Step 1: 转账表在「转账凭证」与「状态」之间插入两列

<el-table-column label="备注" min-width="160" align="center">
  <template #default="{ row }">
    <el-input
      v-if="!isTransferLocked(row) && !isConverted"
      v-model="row.remark"
      maxlength="512"
      clearable
      placeholder="选填"
    />
    <span v-else>{{ row.remark || '—' }}</span>
  </template>
</el-table-column>
<el-table-column label="财务备注" min-width="140" align="center" show-overflow-tooltip>
  <template #default="{ row }">{{ row.financeRemark || '—' }}</template>
</el-table-column>
  • Step 2: addTransfer 默认带 remark: '';保存 create/update payload 带 remark

在现有保存转账逻辑里确保 remark: tr.remark(create/update 已支持则对齐字段名)。

  • Step 3: 操作列 jfqrz 增加撤回
async function withdrawTransfer(row: Record<string, any>) {
  if (!row.id) return
  await ElMessageBox.confirm(
    '确认撤回该笔意向金的财务确认申请?撤回后可修改并重新提交。',
    '撤回',
    { type: 'warning' }
  )
  await fetchWithdrawPresignTransfer(String(row.id))
  row.payStatus = 'dfj'
  ElMessage.success('已撤回')
}

模板:row.payStatus === 'jfqrz' && !isConverted 时显示「撤回」按钮(带 RefreshLeft 或 Refresh 图标)。

  • Step 4: 手工/自测路径:录入备注 → 提交 → 列表见财务待办 → 撤回 → 改备注 → 再提交。

  • Step 5: Commit

git add web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add presign transfer remark column and withdraw.

EOF
)"

Task 6: SamplePaymentPanel — 备注列 + 撤回

Files:

  • Modify: web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue

  • Step 1: 转账表同样插入「备注」「财务备注」列(锁定规则用现有 isTransferLocked / 等价:jfqrz/yjf)。

  • Step 2: addTransfer 初始化 remark: '';saveTransfer payload 已有 remark: tr.remark 则保持,确保 UI 绑定 row.remark。

  • Step 3: 撤回

import { fetchWithdrawPaymentTransfer } from '@/api/protocol-sample-payment'

async function withdrawTransfer(tr: Record<string, any>) {
  if (!tr.id) return
  await ElMessageBox.confirm(
    '确认撤回该笔转账的财务确认申请?撤回后可修改并重新提交。',
    '撤回',
    { type: 'warning' }
  )
  await fetchWithdrawPaymentTransfer(String(tr.id))
  tr.payStatus = 'dfj'
  ElMessage.success('已撤回')
}

操作列:payStatus === 'jfqrz' 显示撤回。

  • Step 4: Commit
git add web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add sample payment transfer remark and withdraw.

EOF
)"

Task 7: 协议查看 / 编辑只读表 + 财务确认 + 缴费信息

Files:

  • Modify: web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue(意向金关联表)

  • Modify: web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue(意向金 + sch.transfers)

  • Modify: web-admin/src/views/cell-storage/payment-confirm/index.vue

  • Modify: web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue

  • Step 1: ProtocolFormDialog / ProtocolViewDialog

意向金子表与系列转账只读表:在状态列前增加:

  • 备注 → row.remark || '—'
  • 财务备注 → row.financeRemark || '—'(若意向金表已有财务备注列则只补「备注」)

样本转账只读表同理。

  • Step 2: payment-confirm 待确认表 + 已确认表

在「转账时间」与「状态/财务备注」之间增加:

<el-table-column
  prop="remark"
  label="备注"
  min-width="120"
  align="center"
  show-overflow-tooltip
>
  <template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>

确认详情若展示字段列表,同步加业务备注(财务备注已有则不动)。

  • Step 3: PaymentExcelList 期次转账明细卡

在 transfer-card__meta 或凭证下方增加:

<div v-if="tr.remark" class="transfer-card__remark">备注:{{ tr.remark }}</div>
<div v-if="tr.financeRemark" class="transfer-card__remark">财务备注:{{ tr.financeRemark }}</div>
  • Step 4: Commit
git add web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue \
  web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue \
  web-admin/src/views/cell-storage/payment-confirm/index.vue \
  web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): show transfer remark and finance remark in views.

EOF
)"

Task 8: 回归与图谱

  • Step 1: 后端全量相关测
cd backend && source venv/bin/activate
python -m pytest tests/test_presign_order.py tests/test_protocol_sample_payment.py -q

Expected: PASS

  • Step 2: 更新图谱
cd /Users/wuzilong/work/00code/zkxw && ./scripts/update-graph.sh all
  • Step 3: 手工验收清单
  1. 预签:填备注 → 提交 → 财务页见备注 → 撤回 → 财务备注仍在 → 改金额/备注 → 再提交 → 财务确认。
  2. 正式协议分期/趸交:同上。
  3. yjf 无撤回按钮;改备注控件禁用。
  4. 协议查看弹窗双备注可见。
  • Step 4: Commit 图谱(若有 diff)
git add backend/graphify-out web-admin/graphify-out
git commit -m "$(cat <<'EOF'
chore: refresh graphify after transfer remark withdraw.

EOF
)"

Spec coverage checklist

Spec 要求 Task
业务 remark 可写(仅 dfj) 5, 6
财务 finance_remark 只读展示 5–7
列表/查看都展示双备注 7
can_withdraw_receipt 1
预签 withdraw API,保留 finance_remark 2
正式 withdraw API 3
前端撤回按钮 + 再提交 5, 6
yjf 不可撤回 1–3, 5–6
无 SQL 迁移 (全程)
不改催缴/样本/运费 (不纳入 File map)

Self-review notes

  • 无 TBD/占位步骤。
  • 路径与现网一致:/presign-transfers/{id}/withdraw、/payment-transfers/{id}/withdraw。
  • can_edit_transfer 已含 jfqrz,但 UI/草稿更新仍按 dfj 锁定;本计划以撤回后改为准,不放开 jfqrz 直接改字段。
  • 预签 update_transfer_draft 错误文案「仅已缴费状态可编辑」与实际 dfj 校验不一致,属既有问题,本计划不强制改文案(可选顺手改为「仅待缴费可编辑」)。