597 lines
19 KiB
Markdown
597 lines
19 KiB
Markdown
# 采集协议单 · 缴费默认定价 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:** 按「样本数 × 缴费模式」在新建/首次选模式时填充默认合同金额与分期模板;支持双样本 `djfq`(趸交+分期);趸交允许多条缴费明细;样本数/样本价变化不再自动改合同金额。
|
||
|
||
**Architecture:** 前端新增 `protocol-payment-presets.ts` 集中定价表与 `applyPaymentPreset()`;`PayMode` 扩展为 `dj | fq | djfq`,分期逻辑通过 `isInstallmentPayMode()` 统一处理 `fq` 与 `djfq`;后端校验放宽趸交多 `dfj`、合计校验,`djfq` 走分期分支;方案 B 用 `paymentDefaultsApplied` 标记避免重复填默认。
|
||
|
||
**Tech Stack:** Vue 3 · TypeScript · Element Plus · FastAPI · Python unittest · `tsx`(前端纯函数冒烟)
|
||
|
||
**Design spec:** `docs/superpowers/specs/2026-07-06-protocol-payment-defaults-design.md`
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `web-admin/src/utils/protocol-payment-presets.ts` | **新建** 定价表、`getActiveSampleCount`、`applyPaymentPreset`、`isInstallmentPayMode` |
|
||
| `web-admin/scripts/test-protocol-payment-presets.ts` | **新建** 定价表冒烟脚本(`tsx` 运行) |
|
||
| `web-admin/src/utils/protocol-pay-mode-switch.ts` | `PayMode` 含 `djfq`;趸交多明细;切换时用 preset 重建 |
|
||
| `web-admin/src/utils/protocol-payment-installment.ts` | `alignFq*` / `syncFq*` 识别 `djfq` |
|
||
| `web-admin/src/utils/status-tag-registry.ts` | `djfq` 标签 |
|
||
| `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` | 模式 UI、首次默认、去掉样本价联动、保存校验 |
|
||
| `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` | `djfq` 只读展示 |
|
||
| `web-admin/src/components/business/protocol-payment-detail-readonly/index.vue` | 分期字段展示含 `djfq` |
|
||
| `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue` | 摘要 `djfq` 按分期展示 |
|
||
| `web-admin/src/utils/protocol-standard-import-parse.ts` | (P1)识别「趸交+分期」 |
|
||
| `backend/app/services/protocol_pay_mode_switch.py` | 趸交多明细合计校验;`djfq`→分期分支 |
|
||
| `backend/app/crud/collection_protocol.py` | `align_fq_*` 等 `djfq` 与 `fq` 同等 |
|
||
| `backend/app/services/qc_work_order_fee_summary.py` | `djfq` 标签与分期摘要 |
|
||
| `backend/tests/test_protocol_pay_mode_switch.py` | 更新/新增校验单测 |
|
||
| `backend/tests/test_protocol_payment_presets.py` | **新建**(可选)后端不依赖 preset,本任务以前端脚本为主 |
|
||
|
||
---
|
||
|
||
## 依赖顺序
|
||
|
||
```text
|
||
Task 1 (presets 纯函数)
|
||
→ Task 2 (后端校验)
|
||
→ Task 3 (pay-mode-switch + installment 工具)
|
||
→ Task 4 (标签/只读组件)
|
||
→ Task 5 (ProtocolFormDialog 主改动)
|
||
→ Task 6 (导入 P1 + 收尾 QA)
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1: 定价表纯函数与冒烟测试
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/utils/protocol-payment-presets.ts`
|
||
- Create: `web-admin/scripts/test-protocol-payment-presets.ts`
|
||
|
||
- [ ] **Step 1: 创建 `protocol-payment-presets.ts`**
|
||
|
||
```typescript
|
||
// web-admin/src/utils/protocol-payment-presets.ts
|
||
export type PayMode = 'dj' | 'fq' | 'djfq'
|
||
|
||
export type PaymentPreset = {
|
||
contractAmount: number
|
||
installmentCount?: number
|
||
installmentAmounts?: number[]
|
||
}
|
||
|
||
const PRESETS: Record<1 | 2, Partial<Record<PayMode, PaymentPreset>>> = {
|
||
1: {
|
||
dj: { contractAmount: 22880 },
|
||
fq: {
|
||
contractAmount: 25880,
|
||
installmentCount: 6,
|
||
installmentAmounts: [6480, 3880, 3880, 3880, 3880, 3880]
|
||
}
|
||
},
|
||
2: {
|
||
dj: { contractAmount: 37880 },
|
||
djfq: {
|
||
contractAmount: 41000,
|
||
installmentCount: 5,
|
||
installmentAmounts: [22880, 6480, 3880, 3880, 3880]
|
||
},
|
||
fq: {
|
||
contractAmount: 44000,
|
||
installmentCount: 5,
|
||
installmentAmounts: [12960, 7760, 7760, 7760, 7760]
|
||
}
|
||
}
|
||
}
|
||
|
||
export function getActiveSampleCount(
|
||
samples: Array<{ sampleStatus?: string }>
|
||
): 1 | 2 {
|
||
const n = samples.filter((s) => String(s.sampleStatus || '') !== 'yzf').length
|
||
return (n >= 2 ? 2 : 1) as 1 | 2
|
||
}
|
||
|
||
export function resolvePaymentPreset(
|
||
sampleCount: 1 | 2,
|
||
payMode: PayMode
|
||
): PaymentPreset | null {
|
||
return PRESETS[sampleCount]?.[payMode] ?? null
|
||
}
|
||
|
||
export function isInstallmentPayMode(payMode: string): boolean {
|
||
return payMode === 'fq' || payMode === 'djfq'
|
||
}
|
||
|
||
export function availablePayModes(sampleCount: 1 | 2): PayMode[] {
|
||
return sampleCount === 2 ? ['dj', 'djfq', 'fq'] : ['dj', 'fq']
|
||
}
|
||
|
||
/** 校验双样本才能用 djfq */
|
||
export function isPayModeValidForSampleCount(
|
||
payMode: PayMode,
|
||
sampleCount: 1 | 2
|
||
): boolean {
|
||
return availablePayModes(sampleCount).includes(payMode)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 创建冒烟脚本**
|
||
|
||
```typescript
|
||
// web-admin/scripts/test-protocol-payment-presets.ts
|
||
import assert from 'node:assert/strict'
|
||
import {
|
||
resolvePaymentPreset,
|
||
getActiveSampleCount,
|
||
isInstallmentPayMode,
|
||
isPayModeValidForSampleCount
|
||
} from '../src/utils/protocol-payment-presets'
|
||
|
||
const fq1 = resolvePaymentPreset(1, 'fq')!
|
||
assert.equal(fq1.contractAmount, 25880)
|
||
assert.deepEqual(fq1.installmentAmounts, [6480, 3880, 3880, 3880, 3880, 3880])
|
||
assert.equal(
|
||
fq1.installmentAmounts!.reduce((s, v) => s + v, 0),
|
||
fq1.contractAmount
|
||
)
|
||
|
||
const djfq2 = resolvePaymentPreset(2, 'djfq')!
|
||
assert.equal(djfq2.contractAmount, 41000)
|
||
assert.equal(djfq2.installmentCount, 5)
|
||
|
||
assert.equal(getActiveSampleCount([{ sampleStatus: 'ycj' }]), 1)
|
||
assert.equal(getActiveSampleCount([{ sampleStatus: 'ycj' }, { sampleStatus: 'yzf' }]), 1)
|
||
assert.equal(
|
||
getActiveSampleCount([{ sampleStatus: 'ycj' }, { sampleStatus: 'ycj' }]),
|
||
2
|
||
)
|
||
|
||
assert.equal(isInstallmentPayMode('djfq'), true)
|
||
assert.equal(isInstallmentPayMode('dj'), false)
|
||
assert.equal(isPayModeValidForSampleCount('djfq', 1), false)
|
||
assert.equal(isPayModeValidForSampleCount('djfq', 2), true)
|
||
|
||
console.log('protocol-payment-presets: OK')
|
||
```
|
||
|
||
- [ ] **Step 3: 运行冒烟**
|
||
|
||
```bash
|
||
cd web-admin && pnpm exec tsx scripts/test-protocol-payment-presets.ts
|
||
```
|
||
|
||
Expected: 输出 `protocol-payment-presets: OK`,exit 0。
|
||
|
||
---
|
||
|
||
### Task 2: 后端保存校验(趸交多明细 + djfq)
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/protocol_pay_mode_switch.py`
|
||
- Modify: `backend/tests/test_protocol_pay_mode_switch.py`
|
||
|
||
- [ ] **Step 1: 修改 `validate_payment_details_for_save` 趸交分支**
|
||
|
||
将 `dj` 分支中「仅 1 条 dfj」改为「全部明细应缴合计 = 合同金额」:
|
||
|
||
```python
|
||
# backend/app/services/protocol_pay_mode_switch.py — pay_mode == "dj" 分支替换为:
|
||
|
||
if pay_mode == "dj":
|
||
total_due = sum(
|
||
_dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
|
||
for r in rows
|
||
)
|
||
if contract and abs(total_due - contract) > 0.015:
|
||
raise ValueError(
|
||
f"趸交各明细应缴金额之和应为 {contract:.2f},当前为 {total_due:.2f}"
|
||
)
|
||
# 保留:存在 yjf 时,dfj 行 due 之和应等于 remaining(可选细化为与 total 一致即可)
|
||
```
|
||
|
||
删除:
|
||
|
||
```python
|
||
if len(dfj_rows) > 1:
|
||
raise ValueError("趸交模式仅允许一条待缴费明细")
|
||
```
|
||
|
||
以及单条 `dfj` 的 `expected` 校验(已由 `total_due` 覆盖)。
|
||
|
||
- [ ] **Step 2: `djfq` 走分期分支**
|
||
|
||
```python
|
||
installment_modes = ("fq", "djfq")
|
||
# ...
|
||
elif pay_mode in installment_modes:
|
||
# 原 fq 逻辑不变
|
||
```
|
||
|
||
- [ ] **Step 3: 更新单测**
|
||
|
||
`test_dj_rejects_multiple_dfj` 改名为 `test_dj_allows_multiple_dfj_when_sum_matches`:
|
||
|
||
```python
|
||
def test_dj_allows_multiple_dfj_when_sum_matches(self):
|
||
validate_payment_details_for_save(
|
||
"dj",
|
||
10000,
|
||
None,
|
||
[
|
||
{"payStatus": "dfj", "dueAmount": 2000},
|
||
{"payStatus": "dfj", "dueAmount": 8000},
|
||
],
|
||
)
|
||
|
||
def test_dj_rejects_multiple_dfj_wrong_sum(self):
|
||
with self.assertRaises(ValueError) as ctx:
|
||
validate_payment_details_for_save(
|
||
"dj",
|
||
10000,
|
||
None,
|
||
[
|
||
{"payStatus": "dfj", "dueAmount": 2000},
|
||
{"payStatus": "dfj", "dueAmount": 7000},
|
||
],
|
||
)
|
||
self.assertIn("之和应为", str(ctx.exception))
|
||
```
|
||
|
||
新增:
|
||
|
||
```python
|
||
def test_djfq_same_as_fq(self):
|
||
validate_payment_details_for_save(
|
||
"djfq",
|
||
41000,
|
||
5,
|
||
[
|
||
{"payStatus": "dfj", "installmentNo": 1, "dueAmount": 22880},
|
||
{"payStatus": "dfj", "installmentNo": 2, "dueAmount": 6480},
|
||
{"payStatus": "dfj", "installmentNo": 3, "dueAmount": 3880},
|
||
{"payStatus": "dfj", "installmentNo": 4, "dueAmount": 3880},
|
||
{"payStatus": "dfj", "installmentNo": 5, "dueAmount": 3880},
|
||
],
|
||
installment_ratios=[22880, 6480, 3880, 3880, 3880],
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 4: 运行后端单测**
|
||
|
||
```bash
|
||
cd backend && source venv/bin/activate && python -m pytest tests/test_protocol_pay_mode_switch.py -v
|
||
```
|
||
|
||
Expected: 全部 PASS。
|
||
|
||
- [ ] **Step 5: 修改 `collection_protocol.py` 分期对齐**
|
||
|
||
约 L2814,将:
|
||
|
||
```python
|
||
if pay.pay_mode != "fq" or not pay.installment_count:
|
||
```
|
||
|
||
改为:
|
||
|
||
```python
|
||
if pay.pay_mode not in ("fq", "djfq") or not pay.installment_count:
|
||
```
|
||
|
||
全文检索 `pay_mode != "fq"` / `pay_mode == "fq"` 在同文件内一并调整。
|
||
|
||
---
|
||
|
||
### Task 3: 前端工具层(pay-mode-switch + installment)
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/utils/protocol-pay-mode-switch.ts`
|
||
- Modify: `web-admin/src/utils/protocol-payment-installment.ts`
|
||
|
||
- [ ] **Step 1: 扩展 `PayMode` 并导入 preset**
|
||
|
||
```typescript
|
||
// protocol-pay-mode-switch.ts 顶部
|
||
import {
|
||
type PayMode,
|
||
isInstallmentPayMode,
|
||
resolvePaymentPreset,
|
||
getActiveSampleCount
|
||
} from '@/utils/protocol-payment-presets'
|
||
export type { PayMode }
|
||
```
|
||
|
||
- [ ] **Step 2: `canAddPaymentDetail` 趸交始终可添加**
|
||
|
||
```typescript
|
||
export function canAddPaymentDetail(payMode: string, _details: DetailLike[]): boolean {
|
||
if (isInstallmentPayMode(payMode)) return true
|
||
return true // dj:允许多条明细
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: `evaluatePayModeSwitch` / `applyPayModeSwitch` 支持 `djfq`**
|
||
|
||
- `dj` → `djfq`:与 `dj` → `fq` 相同(无已缴才允许)
|
||
- `djfq` → `dj`:与 `fq` → `dj`
|
||
- `djfq` ↔ `fq`:按分期规则处理
|
||
- `applyPayModeSwitch` 重建分期明细时,从 `resolvePaymentPreset(sampleCount, toMode)` 取 `installmentAmounts`,无 preset 时回退 `defaultInstallmentAmounts`
|
||
|
||
- [ ] **Step 4: `protocol-payment-installment.ts`**
|
||
|
||
将所有 `payment.payMode !== 'fq'` / `=== 'fq'` 改为使用 `isInstallmentPayMode(payment.payMode as string)`(从 presets 导入)。
|
||
|
||
- [ ] **Step 5: 类型检查**
|
||
|
||
```bash
|
||
cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -40
|
||
```
|
||
|
||
Expected: 无新增 error(允许既有 warning)。
|
||
|
||
---
|
||
|
||
### Task 4: 标签与只读展示
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/utils/status-tag-registry.ts`
|
||
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue`
|
||
- Modify: `web-admin/src/components/business/protocol-payment-detail-readonly/index.vue`
|
||
- Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue`
|
||
- Modify: `backend/app/services/qc_work_order_fee_summary.py`
|
||
|
||
- [ ] **Step 1: `status-tag-registry.ts` 增加**
|
||
|
||
```typescript
|
||
djfq: { label: '趸交+分期', tagType: 'warning' } // 或 primary,与 fq 区分即可
|
||
```
|
||
|
||
- [ ] **Step 2: 只读组件分期条件**
|
||
|
||
`payment?.payMode === 'fq'` → `isInstallmentPayMode(payment?.payMode || '')`(ViewDialog、readonly、QcWorkOrderProtocolPaymentPanel)。
|
||
|
||
- [ ] **Step 3: 后端 `_PAY_MODE_LABELS`**
|
||
|
||
```python
|
||
_PAY_MODE_LABELS = {"dj": "趸交", "fq": "分期", "djfq": "趸交+分期"}
|
||
```
|
||
|
||
`build_protocol_payment_summary` 内 `pay_mode == "fq"` 改为 `pay_mode in ("fq", "djfq")`。
|
||
|
||
---
|
||
|
||
### Task 5: ProtocolFormDialog 主改动
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`
|
||
|
||
- [ ] **Step 1: 缴费模式单选 UI**
|
||
|
||
```vue
|
||
<el-radio-group v-model="formData.payMode" @change="onPayModeChange">
|
||
<el-radio label="dj">趸交</el-radio>
|
||
<el-radio v-if="activeSampleCount === 2" label="djfq">趸交+分期</el-radio>
|
||
<el-radio label="fq">分期</el-radio>
|
||
</el-radio-group>
|
||
```
|
||
|
||
`activeSampleCount` = `computed(() => getActiveSampleCount(formData.samples))`。
|
||
|
||
- [ ] **Step 2: 分期区块条件**
|
||
|
||
`formData.payMode === 'fq'` → `isInstallmentPayMode(formData.payMode)`(模板内所有分期表单项、分期金额行)。
|
||
|
||
- [ ] **Step 3: 新增状态 `paymentDefaultsApplied`**
|
||
|
||
```typescript
|
||
const paymentDefaultsApplied = ref<Record<PayMode, boolean>>({
|
||
dj: false,
|
||
fq: false,
|
||
djfq: false
|
||
})
|
||
|
||
function applyPaymentPresetForMode(payMode: PayMode) {
|
||
const preset = resolvePaymentPreset(getActiveSampleCount(formData.samples), payMode)
|
||
if (!preset) return
|
||
formData.contractAmount = preset.contractAmount
|
||
if (isInstallmentPayMode(payMode) && preset.installmentCount) {
|
||
formData.installmentCount = preset.installmentCount
|
||
formData.installmentIntervalMonths = formData.installmentIntervalMonths ?? 6
|
||
formData.installmentStartDate =
|
||
formData.installmentStartDate || defaultInstallmentStartDateYmd()
|
||
formData.installmentAdvanceDays =
|
||
formData.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||
formData.installmentRatios = [...(preset.installmentAmounts || [])]
|
||
formData.paymentDetails = generateInitialFqDetails(
|
||
getFqParamsFromForm(),
|
||
preset.contractAmount
|
||
)
|
||
initPaymentDetailExpandFlags(formData.paymentDetails)
|
||
markPayDetailLocks(formData.paymentDetails)
|
||
syncLastConfirmedFqParams()
|
||
} else {
|
||
formData.paymentDetails = [
|
||
createPaymentDetailRow({
|
||
dueAmount: preset.contractAmount,
|
||
installmentDueDate: formData.lumpSumDeadline || '',
|
||
expanded: true
|
||
})
|
||
]
|
||
}
|
||
paymentDefaultsApplied.value[payMode] = true
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 改造 `onPayModeChange`**
|
||
|
||
在既有 `evaluatePayModeSwitch` 之前/之后:
|
||
|
||
```typescript
|
||
async function onPayModeChange(newMode: PayMode) {
|
||
const oldMode = lastConfirmedPayMode.value
|
||
// ... 既有 evaluate + apply 逻辑 ...
|
||
|
||
if (!paymentDefaultsApplied.value[newMode]) {
|
||
applyPaymentPresetForMode(newMode)
|
||
}
|
||
lastConfirmedPayMode.value = newMode
|
||
}
|
||
```
|
||
|
||
新建弹窗 `resetForm` / `openCreate` 时重置 `paymentDefaultsApplied` 全 `false`;编辑加载详情后全部标 `true`(不覆盖库内数据)。
|
||
|
||
- [ ] **Step 5: 去掉样本价 → 合同额自动联动**
|
||
|
||
`onSamplePriceChange` 改为空实现或仅刷新弱提示:
|
||
|
||
```typescript
|
||
function onSamplePriceChange() {
|
||
// 方案 B:不调用 syncContractAmount()
|
||
}
|
||
```
|
||
|
||
删除或停用 `syncContractAmount()` 内对 `formData.contractAmount` 的写入;保留函数供「恢复默认」P1 使用时可再启用。
|
||
|
||
可选弱提示(缴费 Tab):
|
||
|
||
```typescript
|
||
const samplePriceContractHint = computed(() => {
|
||
const sampleTotal = formData.samples
|
||
.filter((s) => s.sampleStatus !== 'yzf')
|
||
.reduce((sum, s) => sum + (Number(s.price) || 0), 0)
|
||
if (formData.contractAmount == null) return ''
|
||
if (Math.abs(sampleTotal - Number(formData.contractAmount)) > 0.01) {
|
||
return `样本价格合计 ${fmtMoney(sampleTotal)} 与合同金额不一致,请核对`
|
||
}
|
||
return ''
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 6: 保存前校验 `djfq` 样本数**
|
||
|
||
在 `handleSubmit` 缴费校验段:
|
||
|
||
```typescript
|
||
if (
|
||
formData.payMode === 'djfq' &&
|
||
getActiveSampleCount(formData.samples) !== 2
|
||
) {
|
||
await ElMessageBox.alert('「趸交+分期」仅适用于 2 个样本,请切换缴费模式', '无法保存', {
|
||
type: 'warning'
|
||
})
|
||
return
|
||
}
|
||
if (!isPayModeValidForSampleCount(formData.payMode as PayMode, getActiveSampleCount(formData.samples))) {
|
||
await ElMessageBox.alert('当前缴费模式与样本数量不匹配', '无法保存', { type: 'warning' })
|
||
return
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 7: 标准导入后标记默认已应用**
|
||
|
||
`applyPaymentFromImport` 末尾:
|
||
|
||
```typescript
|
||
paymentDefaultsApplied.value = { dj: true, fq: true, djfq: true }
|
||
```
|
||
|
||
- [ ] **Step 8: 编辑加载 `lastConfirmedPayMode`**
|
||
|
||
```typescript
|
||
lastConfirmedPayMode.value = (['fq', 'djfq'].includes(formData.payMode)
|
||
? formData.payMode
|
||
: 'dj') as PayMode
|
||
```
|
||
|
||
- [ ] **Step 9: 手动验证(开发环境)**
|
||
|
||
1. 新建 → 添加 1 样本 → 首次选「分期」→ 合同 25880、6 期金额正确
|
||
2. 再添加第 2 样本 → 合同/明细**不变**
|
||
3. 新建 2 样本 → 选「趸交+分期」→ 41000、5 期
|
||
4. 趸交 → 添加 2 条明细 2000+20880 → 保存成功
|
||
5. 2 样本 `djfq` 删 1 样本 → 保存拦截
|
||
|
||
---
|
||
|
||
### Task 6: 标准导入(P1)与收尾
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/utils/protocol-standard-import-parse.ts`
|
||
- Run: `./scripts/update-graph.sh frontend`
|
||
|
||
- [ ] **Step 1: 导入识别 `djfq`**
|
||
|
||
`parsePayMode` 中:
|
||
|
||
```typescript
|
||
if (/趸交\s*[++]\s*分期|趸交.*分期/.test(v)) return { payMode: 'djfq' }
|
||
```
|
||
|
||
`ProtocolStandardImportParsed.payMode` 类型改为 `'dj' | 'fq' | 'djfq'`。
|
||
|
||
- [ ] **Step 2: 更新知识图谱**
|
||
|
||
```bash
|
||
./scripts/update-graph.sh frontend
|
||
```
|
||
|
||
- [ ] **Step 3: Lint**
|
||
|
||
```bash
|
||
cd web-admin && pnpm lint
|
||
cd backend && python -m pytest tests/test_protocol_pay_mode_switch.py -v
|
||
```
|
||
|
||
- [ ] **Step 4: 更新 design spec 状态**
|
||
|
||
`docs/superpowers/specs/2026-07-06-protocol-payment-defaults-design.md` 表头 `状态` → `已实现`。
|
||
|
||
---
|
||
|
||
## Spec 覆盖自检
|
||
|
||
| Spec § | Task |
|
||
|--------|------|
|
||
| §3 默认定价表 | Task 1 |
|
||
| §4 方案 B 填充时机 | Task 5 Step 3–4、7 |
|
||
| §5 趸交多明细 | Task 2、3、5 |
|
||
| §5.2 djfq 分期 | Task 2–5 |
|
||
| §6 模式切换修订 | Task 3、5 |
|
||
| §7 pay_mode=djfq | Task 2、4 |
|
||
| §8 UI | Task 4、5 |
|
||
| §8.3 样本价解耦 | Task 5 Step 5 |
|
||
| §9–10 前后端摘要 | Task 2–5 |
|
||
| §11 测试要点 | 各 Task Step 验证 |
|
||
| §4.3 恢复默认 P1 | **范围外**(未列入 Task) |
|
||
|
||
---
|
||
|
||
## 人工回归清单(提测)
|
||
|
||
- [ ] 新建 1 样本首次 `fq`:25880 / 6 期模板
|
||
- [ ] 新建 2 样本首次 `djfq`:41000 / 5 期
|
||
- [ ] 样本数 1→2 后金额不变
|
||
- [ ] 趸交 2+ 条 `dfj` 保存成功
|
||
- [ ] 分期合计 ≠ 合同额:前后端拦截
|
||
- [ ] 已缴锁定 + 模式切换:仍符合 2026-06-04
|
||
- [ ] 查看弹窗 / 质检费用摘要:`djfq` 标签与期次表
|
||
- [ ] 标准导入多笔趸交转账保存成功
|
||
|
||
---
|
||
|
||
## 执行方式
|
||
|
||
**Plan 已保存至** `docs/superpowers/plans/2026-07-06-protocol-payment-defaults.md`。
|
||
|
||
**两种执行方式:**
|
||
|
||
1. **Subagent-Driven(推荐)** — 每 Task 派发独立子代理,Task 间人工/代理复核
|
||
2. **Inline Execution** — 本会话按 Task 1→6 连续实现,每 Task 末 checkpoint
|
||
|
||
告知选用哪种方式即可开始实现。
|