19 KiB
预签约保存时自动提交财务确认 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: 保存预签约订单后,对表单内所有仍为「已付款」dfj 的意向金转账笔自动提交财务确认,Web 与移动端行为一致。
Architecture: 前端编排:先落库订单/转账并写回 id,再逐笔调用现有提交 API;单笔失败只汇总提示,不回滚保存。纯函数负责收集待提交 id、汇总失败文案与串行提交。
Tech Stack: Vue 3;web-admin(Element Plus + node:test);mobile-oa-h5(Vant + node:test)
Spec: docs/superpowers/specs/2026-10-14-presign-save-auto-submit-finance-design.md
File map
| File | Responsibility |
|---|---|
mobile-oa-h5/src/market/presign/presign-form-state.js |
纯函数:收集 dfj id、失败文案、串行提交 |
mobile-oa-h5/src/market/presign/presign-form-state.test.js |
上述纯函数单测 |
mobile-oa-h5/src/views/market/presign/PresignOrderForm.vue |
save 落库后调用自动提交 |
mobile-oa-h5/src/market/presign/presign-form-view.test.js |
契约:save 路径引用自动提交 helper |
web-admin/src/utils/presign-save-auto-submit.ts |
与移动端同语义的纯函数(TS) |
web-admin/src/utils/presign-save-auto-submit.test.ts |
Web 纯函数单测 |
web-admin/src/api/presign-order.ts |
提交财务确认支持关闭成功 toast |
web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue |
handleSave 写回 id + 自动提交 |
后端 / mobile-h5 不改。
Task 1: 移动端纯函数 + 单测(TDD)
Files:
-
Modify:
mobile-oa-h5/src/market/presign/presign-form-state.js -
Modify:
mobile-oa-h5/src/market/presign/presign-form-state.test.js -
Step 1: 先写失败用例
在 presign-form-state.test.js 末尾追加:
import {
// …保留既有 import,并追加:
collectDfjTransferIds,
formatAutoSubmitFinanceFailureMessage,
submitDfjTransfersOnSave
} from './presign-form-state.js'
test('collectDfjTransferIds only returns paid transfers with server ids', () => {
assert.deepEqual(
collectDfjTransferIds([
{ id: 't1', payStatus: 'dfj' },
{ id: 't2', payStatus: 'jfqrz' },
{ id: '', payStatus: 'dfj' },
{ payStatus: 'dfj' },
{ id: 't3', payStatus: 'dfj' },
{ id: ' t4 ', payStatus: 'dfj' }
]),
['t1', 't3', 't4']
)
assert.deepEqual(collectDfjTransferIds(null), [])
})
test('formatAutoSubmitFinanceFailureMessage summarizes count and reasons', () => {
assert.equal(formatAutoSubmitFinanceFailureMessage([]), '')
assert.equal(
formatAutoSubmitFinanceFailureMessage([
{ id: 't1', message: '请选择转账方式' },
{ id: 't2', message: '请填写转账金额' }
]),
'订单已保存,但有 2 笔提交财务确认失败:请选择转账方式;请填写转账金额'
)
})
test('submitDfjTransfersOnSave submits only dfj ids and continues after failures', async () => {
const calls = []
const failures = await submitDfjTransfersOnSave(
[
{ id: 'ok', payStatus: 'dfj' },
{ id: 'skip', payStatus: 'yjf' },
{ id: 'bad', payStatus: 'dfj' }
],
async (id) => {
calls.push(id)
if (id === 'bad') throw new Error('请填写转账金额')
}
)
assert.deepEqual(calls, ['ok', 'bad'])
assert.deepEqual(failures, [{ id: 'bad', message: '请填写转账金额' }])
})
- Step 2: 跑测确认失败
Run:
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && node --test src/market/presign/presign-form-state.test.js
Expected: FAIL — collectDfjTransferIds / 相关导出走义不存在。
- Step 3: 实现纯函数
在 presign-form-state.js 末尾追加:
export function collectDfjTransferIds(transfers) {
const rows = Array.isArray(transfers) ? transfers : []
return rows
.filter((row) => {
const id = String(row?.id || '').trim()
const status = String(row?.payStatus || 'dfj').trim() || 'dfj'
return Boolean(id) && status === 'dfj'
})
.map((row) => String(row.id).trim())
}
export function formatAutoSubmitFinanceFailureMessage(failures) {
const list = Array.isArray(failures) ? failures.filter(Boolean) : []
if (!list.length) return ''
const reasons = list
.map((item) => String(item?.message || item || '提交失败').trim())
.filter(Boolean)
return `订单已保存,但有 ${list.length} 笔提交财务确认失败:${reasons.join(';')}`
}
export async function submitDfjTransfersOnSave(transfers, submitOne) {
const ids = collectDfjTransferIds(transfers)
const failures = []
for (const id of ids) {
try {
await submitOne(id)
} catch (error) {
failures.push({
id,
message: String(error?.message || error || '提交失败').trim() || '提交失败'
})
}
}
return failures
}
- Step 4: 跑测确认通过
Run:
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && node --test src/market/presign/presign-form-state.test.js
Expected: PASS
- Step 5: Commit
git add mobile-oa-h5/src/market/presign/presign-form-state.js mobile-oa-h5/src/market/presign/presign-form-state.test.js
git commit -m "$(cat <<'EOF'
feat(mobile-oa-h5): add helpers to auto-submit dfj presign transfers on save
EOF
)"
Task 2: 移动端 save 接入自动提交
Files:
-
Modify:
mobile-oa-h5/src/views/market/presign/PresignOrderForm.vue -
Modify:
mobile-oa-h5/src/market/presign/presign-form-view.test.js -
Step 1: 更新契约测试
在 presign-form-view.test.js 的 transfer workflow 用例中追加断言:
assert.match(source, /submitDfjTransfersOnSave/)
assert.match(source, /formatAutoSubmitFinanceFailureMessage/)
- Step 2: 跑契约确认失败
Run:
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && node --test src/market/presign/presign-form-view.test.js
Expected: FAIL — 源码尚无 submitDfjTransfersOnSave。
- Step 3: 改
PresignOrderForm.vue
- import 追加:
import {
// 既有…
mergePersistedTransfer,
submitDfjTransfersOnSave,
formatAutoSubmitFinanceFailureMessage
} from '../../../market/presign/presign-form-state.js'
- 将
save()中落库后的成功提示段替换为:
const orderId = String(result?.id || id.value)
if (mode.value === 'edit') await persistTransfers(orderId)
const transfersForSubmit =
mode.value === 'edit'
? form.value.transfers
: Array.isArray(result?.transfers)
? result.transfers
: []
const failures = await submitDfjTransfersOnSave(
transfersForSubmit,
(transferId) => submitPresignTransfer(context.value, transferId, { signal: saveController.signal })
)
dirty.value = false
allowLeave = true
if (failures.length) {
showToast(formatAutoSubmitFinanceFailureMessage(failures))
} else {
showSuccessToast('保存成功')
}
await goList()
说明:编辑路径依赖现有 persistTransfers + mergePersistedTransfer 写回新建转账 id;新增路径用创建订单响应里的 transfers。
- Step 4: 跑契约确认通过
Run:
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && node --test src/market/presign/presign-form-view.test.js src/market/presign/presign-form-state.test.js
Expected: PASS
- Step 5: Commit
git add mobile-oa-h5/src/views/market/presign/PresignOrderForm.vue mobile-oa-h5/src/market/presign/presign-form-view.test.js
git commit -m "$(cat <<'EOF'
feat(mobile-oa-h5): auto-submit paid earnest transfers after presign save
EOF
)"
Task 3: Web 纯函数 + API 静默提交选项(TDD)
Files:
-
Create:
web-admin/src/utils/presign-save-auto-submit.ts -
Create:
web-admin/src/utils/presign-save-auto-submit.test.ts -
Modify:
web-admin/src/api/presign-order.ts -
Step 1: 写失败单测
创建 presign-save-auto-submit.test.ts:
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
collectDfjTransferIds,
formatAutoSubmitFinanceFailureMessage,
submitDfjTransfersOnSave
} from './presign-save-auto-submit'
describe('presign-save-auto-submit', () => {
it('collects only dfj transfers with ids', () => {
assert.deepEqual(
collectDfjTransferIds([
{ id: 't1', payStatus: 'dfj' },
{ id: 't2', payStatus: 'jfqrz' },
{ id: '', payStatus: 'dfj' },
{ payStatus: 'dfj' },
{ id: 't3', payStatus: 'dfj' }
]),
['t1', 't3']
)
})
it('formats partial failure message', () => {
assert.equal(
formatAutoSubmitFinanceFailureMessage([
{ id: 't1', message: '请选择转账方式' },
{ id: 't2', message: '请填写转账金额' }
]),
'订单已保存,但有 2 笔提交财务确认失败:请选择转账方式;请填写转账金额'
)
})
it('continues submitting after one failure', async () => {
const calls: string[] = []
const failures = await submitDfjTransfersOnSave(
[
{ id: 'ok', payStatus: 'dfj' },
{ id: 'bad', payStatus: 'dfj' }
],
async (id) => {
calls.push(id)
if (id === 'bad') throw new Error('请填写转账金额')
}
)
assert.deepEqual(calls, ['ok', 'bad'])
assert.deepEqual(failures, [{ id: 'bad', message: '请填写转账金额' }])
})
})
- Step 2: 跑测确认失败
Run:
cd /Users/wuzilong/work/00code/zkxw/web-admin && node --test src/utils/presign-save-auto-submit.test.ts
Expected: FAIL — 模块不存在(若需 tsx/ts-node,改用仓库现有跑法:与其它 *.test.ts 相同,例如 pnpm exec tsx --test src/utils/presign-save-auto-submit.test.ts;以 protocol-payment-payload.test.ts 的实际运行命令为准)。
先确认既有测试如何跑:
cd /Users/wuzilong/work/00code/zkxw/web-admin && node --import tsx --test src/utils/protocol-payment-payload.test.ts
若该命令通过,本任务一律使用:
cd /Users/wuzilong/work/00code/zkxw/web-admin && node --import tsx --test src/utils/presign-save-auto-submit.test.ts
- Step 3: 实现 util
创建 presign-save-auto-submit.ts:
export type PresignTransferLike = {
id?: string | null
payStatus?: string | null
}
export type AutoSubmitFailure = {
id: string
message: string
}
export function collectDfjTransferIds(
transfers: PresignTransferLike[] | null | undefined
): string[] {
const rows = Array.isArray(transfers) ? transfers : []
return rows
.filter((row) => {
const id = String(row?.id || '').trim()
const status = String(row?.payStatus || 'dfj').trim() || 'dfj'
return Boolean(id) && status === 'dfj'
})
.map((row) => String(row.id).trim())
}
export function formatAutoSubmitFinanceFailureMessage(
failures: Array<AutoSubmitFailure | string> | null | undefined
): string {
const list = Array.isArray(failures) ? failures.filter(Boolean) : []
if (!list.length) return ''
const reasons = list
.map((item) =>
typeof item === 'string'
? item.trim()
: String(item?.message || '提交失败').trim()
)
.filter(Boolean)
return `订单已保存,但有 ${list.length} 笔提交财务确认失败:${reasons.join(';')}`
}
export async function submitDfjTransfersOnSave(
transfers: PresignTransferLike[] | null | undefined,
submitOne: (transferId: string) => Promise<unknown>
): Promise<AutoSubmitFailure[]> {
const ids = collectDfjTransferIds(transfers)
const failures: AutoSubmitFailure[] = []
for (const id of ids) {
try {
await submitOne(id)
} catch (error: any) {
failures.push({
id,
message: String(error?.message || error || '提交失败').trim() || '提交失败'
})
}
}
return failures
}
- Step 4: API 支持关闭成功 toast
将 fetchSubmitPresignTransferFinance 改为:
export function fetchSubmitPresignTransferFinance(
transferId: string,
options?: { showSuccessMessage?: boolean }
) {
return request.post({
url: `/web/admin/presign-transfers/${transferId}/submit-receipt-finance`,
showSuccessMessage: options?.showSuccessMessage !== false
})
}
(手动提交不传 options,仍为 true;自动提交传 { showSuccessMessage: false }。)
可选:若保存循环里 fetchCreatePresignTransfer 的成功 toast 过多造成干扰,可同样为 create 增加 showSuccessMessage 选项并在 handleSave 内关闭;非本需求硬性要求,优先只关提交 toast。
- Step 5: 跑测确认通过
Run:
cd /Users/wuzilong/work/00code/zkxw/web-admin && node --import tsx --test src/utils/presign-save-auto-submit.test.ts
Expected: PASS
- Step 6: Commit
git add web-admin/src/utils/presign-save-auto-submit.ts web-admin/src/utils/presign-save-auto-submit.test.ts web-admin/src/api/presign-order.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add presign save auto-submit helpers and silent finance submit option
EOF
)"
Task 4: Web 弹窗 handleSave 写回 id 并自动提交
Files:
-
Modify:
web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue -
Step 1: 增加 import
import {
formatAutoSubmitFinanceFailureMessage,
submitDfjTransfersOnSave
} from '@/utils/presign-save-auto-submit'
- Step 2: 重写
handleSave落库与自动提交段
用下列逻辑替换现有 try 内保存主体(保留母亲姓名 / 样本类型前置校验与 saving):
const payload: Record<string, unknown> = {
orderDate: form.orderDate || undefined,
motherName: form.motherName,
expectedDeliveryDate: form.expectedDeliveryDate || undefined,
regionId: form.regionId || undefined,
hospitalId: form.hospitalId || undefined,
sampleCollectTime: form.sampleCollectTime || undefined,
collectorUserId: form.collectorUserId || undefined,
regionManagerUserId: form.regionManagerUserId || undefined,
payMode: form.payMode || undefined,
contractAmount: form.contractAmount ?? undefined,
orderStatus: form.orderStatus || 'active',
sampleTypeIds: form.sampleTypeIds,
remark: form.remark || undefined
}
let transfersForSubmit: Array<Record<string, any>> = []
if (props.orderId) {
await fetchUpdatePresignOrder(props.orderId, payload)
for (const tr of form.transfers) {
if (!tr.id && tr.payStatus === 'dfj') {
const created: any = await fetchCreatePresignTransfer(props.orderId, {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
remark: tr.remark || undefined
})
if (created?.id) tr.id = String(created.id)
} else if (tr.id && tr.payStatus === 'dfj') {
await fetchUpdatePresignTransfer(String(tr.id), {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
remark: tr.remark || undefined
})
}
}
transfersForSubmit = form.transfers
} else {
payload.transfers = form.transfers
.filter((t) => t.payStatus === 'dfj' || !t.payStatus)
.map((t) => ({
transferType: t.transferType,
transferAmount: t.transferAmount,
transferTime: t.transferTime || undefined,
transferAttachments: attachmentsToUrls(t.attachments) || undefined,
remark: t.remark || undefined
}))
const createdOrder: any = await fetchCreatePresignOrder(payload)
transfersForSubmit = Array.isArray(createdOrder?.transfers)
? createdOrder.transfers
: []
}
const failures = await submitDfjTransfersOnSave(transfersForSubmit, (transferId) =>
fetchSubmitPresignTransferFinance(transferId, { showSuccessMessage: false })
)
if (failures.length) {
ElMessage.warning(formatAutoSubmitFinanceFailureMessage(failures))
}
emit('update:modelValue', false)
emit('saved')
要点:
-
编辑新建转账必须写回
created.id -
自动提交静默成功 toast;失败用
ElMessage.warning,仍关弹窗 -
手动
submitTransfer不改 -
Step 3: 静态核对
确认文件内存在:
submitDfjTransfersOnSaveformatAutoSubmitFinanceFailureMessagefetchSubmitPresignTransferFinance(transferId, { showSuccessMessage: false })if (created?.id) tr.id = String(created.id)
可用:
rg -n "submitDfjTransfersOnSave|showSuccessMessage: false|created\?\.id" web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue
Expected: 均有命中。
- Step 4: Commit
git add web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): auto-submit paid earnest transfers after saving presign order
EOF
)"
Task 5: 回归验证
Files: 无新文件
- Step 1: 跑移动端相关测试
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && node --test \
src/market/presign/presign-form-state.test.js \
src/market/presign/presign-form-view.test.js \
src/market/presign/presign-components-contract.test.js
Expected: PASS
- Step 2: 跑 Web util 测试
cd /Users/wuzilong/work/00code/zkxw/web-admin && node --import tsx --test src/utils/presign-save-auto-submit.test.ts
Expected: PASS
-
Step 3: 手工清单(实现者勾选)
-
Web 新增:带 1 笔完整 dfj 转账保存 → 财务确认列表出现「付款确认中」
-
Web 编辑:新增 1 笔 + 已有 1 笔未提交 dfj → 保存后两笔均为确认中
-
Web:故意空转账方式保存 → 订单保存成功弹窗关闭,并警告部分提交失败
-
移动端同上三条
-
手动「提交财务确认」/「撤回」仍可用
-
Step 4: 若手工未做,在 PR/交接说明中注明待验项;有失败则修后重跑 Step 1–2
Spec coverage self-check
| Spec 要求 | Task |
|---|---|
| 全部 dfj 自动提交 | Task 1–4 |
| 前端编排、不改后端保存语义 | Task 2/4 仅调现有 submit API |
| Web + 移动 | Task 2 + Task 4 |
| 部分失败仍关弹窗/回列表 + 文案 | Task 1 文案函数 + Task 2/4 |
| 新建写回 id | Task 2(mergePersisted)+ Task 4(created.id) |
| 静默自动提交 toast | Task 3 API 选项 + Task 4 |
| 保留手动按钮 | Task 4 不改 submitTransfer;移动既有按钮保留 |
| 单测 / 契约 | Task 1、2、3、5 |