1. AGENTS.md 更新 - water-docs: 新增 specs/ 与 docs/design/ 生命周期规则章节 - water-backend: 更新协作引用(建设期/建成后、evidence 模块化) 2. specs/ 重复合并 - 006-reminder-event-design 合并入 003-rev006-reminder-event-design - 001-rev004-accounting 删除冗余 data-model.md + contracts/ - 002-rev005-invoice-flow 删除冗余 data-model.md + contracts/ 3. evidence 按模块归档 - 35 个 REV-004 文件归入 evidence/rev004-accounting/ - 7 个通用 bugfix 文件归入 evidence/bugfix/ 和 bugfix/frontend/ - 新建 rev005-invoice/、rev006-reminder/、rev007-statistics/ 目录 4. guides/ 清理 - 14 个 REV004_*.md 移入 evidence/rev004-accounting/ 5. 遗留文件处理 - docs/research/ 归档到 Archive/06_Migration_Plans/ - backend-check detached worktrees 清理 6. 交叉引用修复 - 006-reminder-event-design → 003-rev006-reminder-event-design - docs/guides/REV004_ → docs/evidence/rev004-accounting/REV004_ 7. DB 设计文档修正(01_Database_Design.md) - biz_invoice 明确为开票配置表,非发票记录表 - 新增 biz_invoice_record 为发票申请/结果主表 - 新增 biz_charge_invoice_rel 账单-发票关联说明 - REV-005 承接口径表名全部修正 8. 发票审计证据 - 新增 evidence/rev005-invoice/2026-06-16-invoice-document-audit.md
18 KiB
催缴登记 (Arrearage Reminder) 前端对接 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: 将两个前端页面(欠费催缴池 + 催缴记录)从 mock 数据切换到真实后端 API,包括 API 层补齐、催缴表单对接、汇总统计、导出功能。
Architecture: 后端 9 个接口均已就绪 (POST /create, GET /page, GET /pending-page, GET /get, GET /detail-list, POST /batch-create, GET /pending-summary, GET /pending-export, GET /pending-detail-export)。前端按 "API 层 → 催缴表单 → 欠费池页面 → 催缴记录页面" 的顺序逐层对接,每层完成后可独立验证。
Tech Stack: Vue 3, TypeScript, Element Plus, Axios (@/config/axios), pnpm
File Structure
API layer
- Modify:
../water-frontend/src/api/collectionManage/arrears/index.ts- 新增所有请求/响应 VO 类型 + 7 个缺失的 API 方法
Components
- Modify:
../water-frontend/src/views/collectionManage/arrears/components/RemindForm.vue- 对接
batchCreateAPI,接收父组件传入的选中行数据
- 对接
Views
- Modify:
../water-frontend/src/views/collectionManage/arrears/index.vue- 汇总统计条对接
getPendingSummary、导出对接getPendingExportList、催缴按钮传递选中数据给 RemindForm
- 汇总统计条对接
- Modify:
../water-frontend/src/views/collectionManage/collectionRecord/index.vue- 列表对接
getPage、详情展开对接getDetailList、导出对接后端(如有对应接口) - 查询表单字段对齐
ArrearageReminderPageReqVO
- 列表对接
Task 1: 补齐 API 层 — 类型定义与全部接口方法
Files:
-
Modify:
../water-frontend/src/api/collectionManage/arrears/index.ts -
Step 1: 添加所有 VO 类型定义和 API 方法
import request from '@/config/axios'
import download from '@/utils/download'
// ========== 请求 VO ==========
export interface ArrearagePendingPageReqVO {
pageNo: number
pageSize: number
deptId?: number
code?: string
name?: string
address?: string
mobile?: string
custStates?: number[]
custType?: number
billMonthStart?: number
billMonthEnd?: number
meterReaderId?: number
bookCode?: string
arrearsCountMin?: number
arrearsCountMax?: number
arrearsAmountMin?: number
arrearsAmountMax?: number
remindFilter?: string
}
export interface ArrearageReminderPageReqVO {
pageNo: number
pageSize: number
custId?: number
reminderUser?: string
reminderType?: number
reminderReason?: number
reminderResult?: number
}
export interface ArrearageReminderCreateReqVO {
custId: number
chargeIds: number[]
reminderType: number
reminderReason: number
reminderResult: number
reminderUser: string
reminderTemplate?: number
remark?: string
}
export interface ArrearageReminderBatchCreateReqVO {
customers: {
custId: number
chargeIds: number[]
}[]
reminderType: number
reminderReason: number
reminderResult: number
reminderUser: string
reminderTemplate?: number
remark?: string
}
// ========== 响应 VO ==========
export interface ArrearagePendingPageRespVO {
custId: number
custCode: string
custName: string
custAddress: string
meterCaliber: string
waterNature: string
arrearsCount: number
waterVolume: number
totalAmount: number
penaltyAmount: number
receivableAmount: number
billAmount: number
accountMonthRange: string
custStatus: string
prestoreAmount: number
mobile: string
isRemindedThisMonth: boolean
agreementNo: string
contractNo: string
details: ArrearagePendingChargeDetailRespVO[]
}
export interface ArrearagePendingChargeDetailRespVO {
chargeId: number
billMonth: string
lastReading: number
currentReading: number
waterVolume: number
billAmount: number
penaltyAmount: number
readingDate: string
}
export interface ArrearagePendingSummaryRespVO {
custCount: number
arrearsCount: number
waterVolume: number
billAmount: number
penaltyAmount: number
totalAmount: number
receivableAmount: number
}
export interface ArrearageReminderPageRespVO {
id: number
custId: number
custCode: string
custName: string
reminderType: number
reminderReason: number
reminderUser: string
remark: string
reminderTemplate: number
completeTime: string
pushState: number
pushResults: string
reminderResult: number
batchStamp: string
totalBillWater: number
totalExtendedAmount: number
totalLateFee: number
deposit: number
mobile: string
createTime: string
updateTime: string
}
export interface ArrearageReminderRespVO extends ArrearageReminderPageRespVO {}
export interface ArrearageReminderDetailRespVO {
id: number
arrearageReminderId: number
chargeId: number
lateFee: number
createTime: string
updateTime: string
}
export interface ArrearageReminderCreateRespVO {
id: number
detailCount: number
}
export interface ArrearageReminderBatchCreateRespVO {
successCount: number
failCount: number
successItems: { custId: number; reminderId: number; detailCount: number }[]
failItems: { custId: number; reason: string }[]
}
// ========== API 方法 ==========
export const ArrearsApi = {
// 待催欠费池
getPendingPage: async (params: ArrearagePendingPageReqVO) => {
return await request.get<{ list: ArrearagePendingPageRespVO[]; total: number }>({
url: '/business/arrearage-reminder/pending-page',
params
})
},
getPendingSummary: async (params: ArrearagePendingPageReqVO) => {
return await request.get<ArrearagePendingSummaryRespVO>({
url: '/business/arrearage-reminder/pending-summary',
params
})
},
// 催缴登记 CRUD
create: async (data: ArrearageReminderCreateReqVO) => {
return await request.post<ArrearageReminderCreateRespVO>({
url: '/business/arrearage-reminder/create',
data
})
},
batchCreate: async (data: ArrearageReminderBatchCreateReqVO) => {
return await request.post<ArrearageReminderBatchCreateRespVO>({
url: '/business/arrearage-reminder/batch-create',
data
})
},
getPage: async (params: ArrearageReminderPageReqVO) => {
return await request.get<{ list: ArrearageReminderPageRespVO[]; total: number }>({
url: '/business/arrearage-reminder/page',
params
})
},
get: async (id: number) => {
return await request.get<ArrearageReminderRespVO>({
url: '/business/arrearage-reminder/get',
params: { id }
})
},
getDetailList: async (reminderId: number) => {
return await request.get<ArrearageReminderDetailRespVO[]>({
url: '/business/arrearage-reminder/detail-list',
params: { reminderId }
})
},
// 导出
exportPending: async (params: ArrearagePendingPageReqVO) => {
const res = await request.download({
url: '/business/arrearage-reminder/pending-export',
params
})
download.response(res, '待催欠费客户.xlsx')
},
exportPendingDetail: async (params: ArrearagePendingPageReqVO) => {
const res = await request.download({
url: '/business/arrearage-reminder/pending-detail-export',
params
})
download.response(res, '待催欠费明细.xlsx')
}
}
- Step 2: 验证 API 层编译
Run: cd ../water-frontend && npx vue-tsc --noEmit src/api/collectionManage/arrears/index.ts 2>&1 | head -20
Expected: No type errors
Task 2: RemindForm 对接 batchCreate API
Files:
-
Modify:
../water-frontend/src/views/collectionManage/arrears/components/RemindForm.vue -
Step 1: 重写 RemindForm 脚本,接收选中行 props 并调 batchCreate
将 <script setup lang="ts"> 段替换为:
import { ArrearsApi } from '@/api/collectionManage/arrears'
const message = useMessage()
const dialogVisible = ref(false)
const formRef = ref()
const props = defineProps<{
selectedRows: any[]
}>()
// reminderType 映射: '催缴单'->1, '短信'->2, '电话'->3, '微信'->4, '其它'->5, '热线'->6
const methodMap: Record<string, number> = {
'催缴单': 1, '短信': 2, '电话': 3, '微信': 4, '其它': 5, '热线': 6
}
const formData = ref({
method: '催缴单',
operator: '',
reason: '',
remark: ''
})
const rules = {
method: [{ required: true, message: '请选择催缴方式', trigger: 'change' }],
operator: [{ required: true, message: '请选择催缴人员', trigger: 'change' }],
reason: [{ required: true, message: '请选择催缴原因', trigger: 'change' }]
}
const open = () => {
dialogVisible.value = true
formData.value = {
method: '催缴单',
operator: '',
reason: '',
remark: ''
}
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
try {
const customers = props.selectedRows.map((row: any) => ({
custId: row.custId,
chargeIds: row.details?.map((d: any) => d.chargeId) || []
}))
const res = await ArrearsApi.batchCreate({
customers,
reminderType: methodMap[formData.value.method] || 1,
reminderReason: Number(formData.value.reason),
reminderResult: 0,
reminderUser: formData.value.operator,
remark: formData.value.remark
})
if (res.failCount > 0) {
const failMsgs = res.failItems.map((f: any) => `客户${f.custId}: ${f.reason}`).join('; ')
message.warning(`成功 ${res.successCount} 条,失败 ${res.failCount} 条。${failMsgs}`)
} else {
message.success(`成功创建 ${res.successCount} 条催缴记录`)
}
dialogVisible.value = false
emit('success')
} catch (e: any) {
message.error(e?.message || '催缴失败')
}
})
}
const emit = defineEmits(['success'])
defineExpose({ open })
- Step 2: 修改父组件传参 — 更新 arrears/index.vue 中 RemindForm 的使用
在 ../water-frontend/src/views/collectionManage/arrears/index.vue 底部模板中:
<!-- 修改前 -->
<RemindForm ref="remindFormRef" @success="handleSuccess" />
<!-- 修改后 -->
<RemindForm ref="remindFormRef" :selected-rows="selectedRows" @success="handleSuccess" />
- Step 3: 验证编译
Run: cd ../water-frontend && npx vue-tsc --noEmit 2>&1 | tail -20
Expected: No type errors (may have pre-existing errors in other files, check only arrearage-related)
Task 3: 欠费催缴池页面 — 汇总统计 + 导出 + 全选催缴
Files:
-
Modify:
../water-frontend/src/views/collectionManage/arrears/index.vue -
Step 1: 添加汇总统计数据获取
在 getList 调用后追加 summary 请求。找到 script setup 中的 getList 函数(约 line 629),在其后添加:
const summary = ref<{
custCount: number
arrearsCount: number
waterVolume: number
billAmount: number
penaltyAmount: number
totalAmount: number
receivableAmount: number
}>({
custCount: 0, arrearsCount: 0, waterVolume: 0,
billAmount: 0, penaltyAmount: 0, totalAmount: 0, receivableAmount: 0
})
const fetchSummary = async () => {
try {
summary.value = await ArrearsApi.getPendingSummary(buildPageParams())
} catch {
// summary stays at zero
}
}
修改 handleQuery 和 onMounted 中的 getList() 调用为并行:
const handleQuery = () => {
queryParams.pageNo = 1
Promise.all([getList(), fetchSummary()])
}
并在 onMounted 中:
onMounted(async () => {
await Promise.resolve(getSiteTree())
await Promise.all([getList(), fetchSummary()])
})
- Step 2: 替换模板中的硬编码统计值为真实数据
找到 <el-descriptions> 区域(约 line 211-226),替换其中各 el-descriptions-item 的值为 summary 响应式数据:
<el-descriptions :column="5" class="mt-15px mb-8px" border label-width="120">
<el-descriptions-item label="客户数">
<span class="text-orange-500 font-bold">{{ summary.custCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="欠费笔数">
<span class="text-orange-500 font-bold">{{ summary.arrearsCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="用水量">
<span class="text-orange-500 font-bold">{{ summary.waterVolume }}</span>
</el-descriptions-item>
<el-descriptions-item label="合计金额">
<span class="text-orange-500 font-bold">¥{{ summary.totalAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="账单金额">
<span class="text-orange-500 font-bold">¥{{ summary.billAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="违约金">
<span class="text-orange-500 font-bold">¥{{ summary.penaltyAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="应缴金额">
<span class="text-orange-500 font-bold">¥{{ summary.receivableAmount }}</span>
</el-descriptions-item>
</el-descriptions>
- Step 3: 导出对接真实 API
找到 handleExport 函数(约 line 663),替换为:
const handleExport = async () => {
try {
message.loading('正在导出...')
await ArrearsApi.exportPending(buildPageParams())
message.success('导出成功')
} catch {
message.error('导出失败')
}
}
- Step 4: 全部催缴传参
找到 handleRemindAll(约 line 657),当前直接 remindFormRef.value.open(),需要传入当前列表中所有行的数据。修改为:
const handleRemindAll = () => {
selectedRows.value = list.value
remindFormRef.value.open()
}
- Step 5: 从 API 导入汇总相关方法
在 arrears/index.vue 的 import 中,ArrearsApi 的导入行保持不变(当前已导入),新增 summary ref 定义的位置放到 selectedRows 附近。
- Step 6: 验证编译
Run: cd ../water-frontend && npx vue-tsc --noEmit 2>&1 | tail -20
Expected: No new type errors from arrears files
Task 4: 催缴记录页面 — 列表与详情对接
Files:
-
Modify:
../water-frontend/src/views/collectionManage/collectionRecord/index.vue -
Step 1: 添加 API 导入
在 script setup 顶部 import 中加入:
import { ArrearsApi } from '@/api/collectionManage/arrears'
- Step 2: 添加详情数据 ref 和获取方法
在 list ref 附近(约 line 294)追加:
const detailMap = ref<Record<number, any[]>>({})
const fetchDetailList = async (reminderId: number) => {
if (detailMap.value[reminderId]) return
try {
const details = await ArrearsApi.getDetailList(reminderId)
detailMap.value[reminderId] = details
} catch {
detailMap.value[reminderId] = []
}
}
- Step 3: 替换 getList 为真实 API 调用
找到现有的 getList 函数(当前为 mock 数据),替换为:
const getList = async () => {
loading.value = true
try {
const data = await ArrearsApi.getPage({
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
custId: undefined, // 后续可扩展
reminderUser: queryParams.remindUser || undefined,
reminderType: undefined,
reminderReason: queryParams.remindReason ? Number(queryParams.remindReason) : undefined,
reminderResult: queryParams.remindResult ? Number(queryParams.remindResult) : undefined
})
list.value = (data?.list || []).map((item: any) => ({
...item,
id: item.id,
deptName: item.deptName || '-',
custCode: item.custCode,
custName: item.custName,
custAddress: item.custAddress || '-',
remindUser: item.reminderUser,
remindMethod: item.reminderType,
remindReason: item.reminderReason,
mobile: item.mobile,
arrearsCount: 0, // reminder records don't carry count — fetched via details
arrearsWaterVolume: item.totalBillWater,
arrearsAmount: item.totalExtendedAmount,
penaltyAmount: item.totalLateFee,
prestoreBalance: item.deposit,
completeDate: item.completeTime,
registerDate: item.createTime,
remark: item.remark,
expanded: false,
pushStatus: item.pushState,
details: []
}))
total.value = data?.total || 0
} finally {
loading.value = false
}
}
- Step 4: 展开行对接详情
找到模板中的 expand slot(当前显示 mock 数据 detailsMock1),替换为绑定 detailMap:
<template #expand="scope">
<div class="p-20px bg-gray-50">
<el-table
:data="detailMap[scope.row.id] || []"
border
style="width: 100%"
v-loading="!detailMap[scope.row.id]"
>
<el-table-column prop="chargeId" label="账单ID" width="120" />
<el-table-column prop="lateFee" label="违约金" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
</el-table>
</div>
</template>
- Step 5: 展开事件触发详情加载
在 handleExpandChange 中调用 fetchDetailList:
const handleExpandChange = (row: any, expandedRows: any[]) => {
row.expanded = expandedRows.includes(row)
if (row.expanded) {
fetchDetailList(row.id)
}
}
- Step 6: 删除 mock 数据
删除文件中的 detailsMock1 等 mock 数据声明。
- Step 7: 验证编译
Run: cd ../water-frontend && npx vue-tsc --noEmit 2>&1 | tail -20
Expected: No new type errors
Verification Checklist
完成所有 4 个 Task 后,执行端到端 smoke:
npx vue-tsc --noEmit编译通过- 欠费催缴池页面:汇总统计数字不再是硬编码
- 欠费催缴池页面:点击"导出"触发
GET /pending-export下载 xlsx - 欠费催缴池页面:勾选客户 → 点击"选中催缴" → 弹窗填写 → 提交 → 调
POST /batch-create - 欠费催缴池页面:点击"全部催缴"将当前列表全部行传入 RemindForm
- 催缴记录页面:列表从
GET /page加载,查询条件生效 - 催缴记录页面:展开行从
GET /detail-list加载账单明细