/** * sjnmtybt-server 真实接口实现(对齐新前端页面流程与既有函数签名) * 页面仍调用 @/api/biz;useMock=false 时走到本模块。 */ import service, { clearGetCache, del, get, post, put } from '@/utils/request' import { getAppConfig } from '@/utils/appConfig' import { FLOW_STAGES, filterTodosByRole, getBarChartTitle, getDashboardScopeLabel, LEADER_WORKBENCH_TIPS, SOCIAL_WORKBENCH_TIPS, TOWN_REVIEW_TIPS, VILLAGE_WORKBENCH_TIPS, } from '@/utils/buildDashboard' import { batchStatusLabel, processActionLabelOf, roleLabelOf } from '@/utils/statusLabels' import type { AccountItem, AlertItem, Approval, BatchMemberPerson, DashboardData, DeathRecord, DiskFailPerson, DiskRecord, HandleAlertPayload, MonthlyBatch, OpLog, Person, SpecialBiz, SubsidyStandard, VillageBatch, VillageMonthlyBatch, AdjustReissuePerson, BatchFlowOverview, ProcessLogItem, } from '@/types/biz' import { ensureTowns, ensureAllVillages, ensureVillages, invalidateRegionCache, regionName, resolveScopeIds, townIdOf, type RegionRow, } from './regionStore' import { approvalIdBatch, approvalIdSpecial, approvalIdQualify, flowStageLabel, mapAlert, mapDisk, mapPerson, mapProcessLog, mapReturnFail, mapSpecial, mapStandard, mapVillageBatch, mapVillageMonthly, monthToPayMonth, parseApprovalId, personIdStr, sanitizePersonnelRemark, specialTypeToCode, isPayMonthLike, } from './mappers' type PageResult = { list?: T[]; total?: number; pageNum?: number; pageSize?: number } async function pageAll( fetcher: (pageNum: number, pageSize: number) => Promise | null | undefined>, pageSize = 100, ): Promise { const all: T[] = [] for (let pageNum = 1; ; pageNum++) { const res = await fetcher(pageNum, pageSize) const list = res?.list || [] all.push(...list) const total = Number(res?.total || 0) if (list.length < pageSize || (total > 0 && all.length >= total)) break } return all } function currentPayMonth() { return monthToPayMonth(getAppConfig().currentMonth) } function operatorOf() { const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) return role?.label || cfg.currentRole || '联调' } type MonthStatRow = { payMonth?: string peopleCount?: number totalAmount?: number | string actualAmount?: number | string failCount?: number batchCount?: number } type RegionDashRow = { payMonth?: string townId?: string villageId?: string grain?: string personnelCount?: number currentMonth?: MonthStatRow series?: MonthStatRow[] } type BatchStatRow = { townId?: string villageId?: string shouldPayCount?: number shouldPayAmount?: number | string actualPayAmount?: number | string } function formatPayMonthLabel(ym?: string) { const s = String(ym || '') if (s.length === 6) return `${s.slice(0, 4)}-${s.slice(4)}` return s || '-' } function seriesToHalfYearCharts(series: MonthStatRow[] | undefined, months = 6) { const list = Array.isArray(series) ? series.slice(-months) : [] return { people: list.map((m) => ({ name: formatPayMonthLabel(m.payMonth), value: Number(m.peopleCount || 0), })), amount: list.map((m) => ({ name: formatPayMonthLabel(m.payMonth), value: Number(m.totalAmount || 0), })), } } /** 本批次各村发放人数/金额(镇级工作台) */ async function buildTownVillageChartsFromApi(townId: string, payMonth: string) { await ensureVillages(townId) const batches = await pageAll>((pageNum, pageSize) => post>>('/batches/page', { pageNum, pageSize, townId, payMonth, batchLevel: 'TOWN', lite: true, }), ) const peopleMap = new Map() const amountMap = new Map() for (const b of batches) { if (String(b.status || '') === 'ARCHIVED') continue const vName = regionName(String(b.villageId || '')) || String(b.villageId || '未知村') peopleMap.set(vName, (peopleMap.get(vName) || 0) + Number(b.peopleCount || 0)) amountMap.set(vName, (amountMap.get(vName) || 0) + Number(b.totalAmount || 0)) } const names = Array.from(new Set([...peopleMap.keys(), ...amountMap.keys()])).sort((a, b) => a.localeCompare(b, 'zh-CN'), ) return { people: names.map((name) => ({ name, value: peopleMap.get(name) || 0 })), amount: names.map((name) => ({ name, value: amountMap.get(name) || 0 })), } } function emptyDashboard(role: ReturnType['roles'][0], payMonth: string): DashboardData { const isDistrict = Boolean(role.allData) return { stats: { normal: 0, pending: 0, alert: 0, amount: 0, monthIncrease: 0, monthDecrease: 0 }, batchInfo: { batchNo: payMonth, scope: getDashboardScopeLabel(role), peopleCount: 0, amount: 0, todoCount: 0, alertCount: 0, }, barChartTitle: getBarChartTitle(role), workflow: [...FLOW_STAGES], workflowActive: 0, townAmountChart: [], batchCompareChart: [], statusPieMonth: [], statusPieYear: [], todos: [], recentAlerts: [], villageTips: role.village ? VILLAGE_WORKBENCH_TIPS : undefined, districtOverview: isDistrict ? { focus: role.key === 'agri' ? 'qualify' : 'amount', townCount: 0, villageCount: 0, peopleCount: 0, amount: 0, withLandCount: 0, withoutLandCount: 0, qualifiedCount: 0, townPeopleChart: [], townAmountChart: [], townWithLandChart: [], townWithoutLandChart: [], qualifyPie: [], halfYearPeopleChart: [], halfYearAmountChart: [], townSummary: [], } : undefined, } } type WorkbenchApi = { summary?: Record & { totalPeople?: number totalAmount?: number | string pendingAlertCount?: number townStats?: BatchStatRow[] } region?: RegionDashRow personnelCount?: number withLandCount?: number withoutLandCount?: number qualifiedCount?: number pendingTownApproveCount?: number pendingLeaderApproveCount?: number monthBatchCount?: number monthPeopleCount?: number monthAmount?: number | string monthIncreaseCount?: number monthDecreaseCount?: number townQualify?: Array<{ townId?: string peopleCount?: number villageCount?: number withLandCount?: number withoutLandCount?: number qualifiedCount?: number amount?: number | string }> villageCharts?: Array<{ villageId?: string; peopleCount?: number; amount?: number | string }> batchStatusCounts?: Record recentAlerts?: Record[] monthBatches?: Record[] todos?: Array<{ id?: string type?: string title?: string node?: string path?: string bizId?: string read?: boolean }> } /** 线上旧包无 /workbench 时,用已有 summary + region-stats 拼装 */ async function fetchWorkbenchLegacy( payMonth: string, townId?: string, villageId?: string, months = 6, ): Promise { const [summary, region] = await Promise.all([ get( '/dashboard/summary', { payMonth, townId: townId || undefined }, { silent: true }, ).catch(() => null), get( '/dashboard/region-stats', { payMonth, townId: townId || undefined, villageId: villageId || undefined, months, }, { silent: true }, ).catch(() => null), ]) if (!summary && !region) return null const monthBatches = await pageAll((pageNum, pageSize) => post>>( '/batches/page', { pageNum, pageSize, payMonth, townId: townId || undefined, villageId: villageId || undefined, batchLevel: townId ? 'TOWN' : 'DISTRICT', lite: true, }, { silent: true }, ).catch(() => null), ) const batchStatusCounts: Record = {} for (const b of monthBatches) { const st = String(b.status || '') if (!st) continue batchStatusCounts[st] = (batchStatusCounts[st] || 0) + 1 } const alertList = await pageAll((pageNum, pageSize) => post>>( '/alerts/page', { pageNum, pageSize, handleStatus: 'PENDING', townId: townId || undefined, villageId: villageId || undefined, }, { silent: true }, ).catch(() => null), ) let villageCharts: WorkbenchApi['villageCharts'] if (townId && !villageId) { const charts = await buildTownVillageChartsFromApi(townId, payMonth).catch(() => null) if (charts) { villageCharts = charts.people.map((p, idx) => ({ villageId: p.name, peopleCount: p.value, amount: charts.amount[idx]?.value || 0, })) } } return { summary: summary || undefined, region: region || undefined, personnelCount: Number(region?.personnelCount || 0), monthPeopleCount: Number(region?.currentMonth?.peopleCount || 0), monthAmount: region?.currentMonth?.totalAmount, monthBatchCount: monthBatches.length, pendingTownApproveCount: Number(batchStatusCounts.PENDING_TOWN_APPROVE || 0), pendingLeaderApproveCount: Number(batchStatusCounts.PENDING_LEADER_APPROVE || 0), batchStatusCounts, monthBatches, recentAlerts: alertList, villageCharts, } } async function loadWorkbenchApi( payMonth: string, townId?: string, villageId?: string, months = 6, roleKey?: string, ): Promise { const wb = await get( '/dashboard/workbench', { payMonth, townId: townId || undefined, villageId: villageId || undefined, months, operator: operatorOf(), roleKey: roleKey || getAppConfig().currentRole, }, { silent: true, timeout: 180_000 }, ).catch(() => null) if (wb) return wb return fetchWorkbenchLegacy(payMonth, townId, villageId, months) } /** 报表/工作台共用:一次拉人员合计、街镇资格、本月金额,禁止为此再 pageAll 人员 */ export async function fetchWorkbenchStats(params?: { payMonth?: string town?: string village?: string months?: number roleKey?: string }): Promise { await ensureTowns() const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === (params?.roleKey || cfg.currentRole)) const { townId, villageId } = await resolveScopeIds( params?.town || role?.town, params?.village || role?.village, ) const payMonth = params?.payMonth || currentPayMonth() return loadWorkbenchApi( payMonth, townId || undefined, villageId || undefined, params?.months ?? 1, params?.roleKey || cfg.currentRole, ) } /** 标记工作台待办已读 */ export async function markWorkbenchTodoRead(todoKey: string) { if (!todoKey) return await post('/dashboard/workbench/todos/read', { todoKey, operator: operatorOf(), }).catch(() => undefined) } /** —— 工作台:优先单接口聚合;旧后台无 workbench 时自动降级 —— */ export async function fetchDashboard(params: { roleKey: string currentMonth: string }): Promise { await ensureTowns() const role = getAppConfig().roles.find((r) => r.key === params.roleKey) || getAppConfig().roles[0] const { townId, villageId } = await resolveScopeIds(role.town, role.village) if (townId) await ensureVillages(townId) const payMonth = monthToPayMonth(params.currentMonth) const base = emptyDashboard(role, payMonth) base.batchInfo.scope = role.village && role.town ? `${role.town}/${role.village}` : role.town ? role.town : role.allData ? townId ? regionName(townId) || role.town || '全区' : '全区' : base.batchInfo.scope const wb = await loadWorkbenchApi( payMonth, townId || undefined, villageId || undefined, 6, params.roleKey, ) if (!wb) return base const summary = wb.summary const region = wb.region const halfYear = seriesToHalfYearCharts(region?.series, 6) let regionPeople = Number(region?.currentMonth?.peopleCount || 0) let regionAmount = Number(region?.currentMonth?.totalAmount || 0) if (regionPeople <= 0) regionPeople = Number(wb.monthPeopleCount || 0) if (regionAmount <= 0) regionAmount = Number(wb.monthAmount || 0) // 发放人数/金额禁止回退到在册人数或镇级汇总,避免村级卡片 1367 人配上被截断的 53 万 if (halfYear.people.length) { const lastIdx = halfYear.people.length - 1 if (regionPeople > 0) halfYear.people[lastIdx].value = regionPeople if (regionAmount > 0) halfYear.amount[lastIdx].value = regionAmount } const pendingAlert = Number(summary?.pendingAlertCount || 0) const pendingTown = Number(wb.pendingTownApproveCount || 0) const pendingLeader = Number(wb.pendingLeaderApproveCount || 0) const pending = role.key === 'town' ? pendingTown : role.key === 'leader' ? pendingLeader : pendingTown + pendingLeader base.stats.amount = regionAmount base.stats.alert = pendingAlert base.stats.normal = Number(region?.personnelCount ?? wb.personnelCount ?? regionPeople) base.stats.pending = pending base.stats.monthIncrease = Number(wb.monthIncreaseCount || 0) base.stats.monthDecrease = Number(wb.monthDecreaseCount || 0) base.batchInfo.peopleCount = regionPeople base.batchInfo.amount = regionAmount base.batchInfo.alertCount = pendingAlert const monthBatches = (wb.monthBatches || []).map(mapVillageBatch) const statusCounts = wb.batchStatusCounts || {} base.statusPieMonth = Object.entries(statusCounts) .map(([code, value]) => ({ name: batchStatusLabel(code), value: Number(value) || 0, })) .filter((i) => i.value > 0) base.statusPieYear = base.statusPieMonth base.recentAlerts = (wb.recentAlerts || []).map(mapAlert) const apiTodos = (wb.todos || []).map((t) => ({ id: String(t.id || ''), type: String(t.type || '待办'), title: String(t.title || ''), node: String(t.node || ''), path: t.path ? String(t.path) : undefined, bizId: t.bizId ? String(t.bizId) : undefined, read: !!t.read, })) const allowedPaths = getAppConfig().rolePermissions[role.key] || [] const rawTodos = apiTodos.length > 0 ? apiTodos : role.allData ? [] : monthBatches.map((b) => ({ id: `BATCH|${b.id}`, type: '批次流转', title: `${b.batchNo} ${b.town}${b.village}月度批次`, node: b.stage, path: '/approvals', bizId: b.id, read: false, })) base.todos = filterTodosByRole(rawTodos, role.key, allowedPaths) base.batchInfo.todoCount = base.todos.filter((t) => !t.read).length let workflowActive = FLOW_STAGES.length - 1 for (let i = 0; i < FLOW_STAGES.length; i++) { if (monthBatches.some((b) => b.stage.includes(FLOW_STAGES[i]) || (i === 0 && b.stage === '退回修改'))) { workflowActive = i break } } base.workflowActive = workflowActive if (role.village || role.key === 'village') { base.villageHalfYearPeopleChart = halfYear.people base.villageHalfYearAmountChart = halfYear.amount base.townAmountChart = halfYear.amount base.batchCompareChart = halfYear.amount.map((i, idx) => ({ ...i, count: halfYear.people[idx]?.value, })) } if (townId && !villageId && (role.key === 'town' || role.key === 'social' || role.key === 'leader')) { const villageCharts = { people: (wb.villageCharts || []).map((v) => ({ name: regionName(String(v.villageId || '')) || String(v.villageId || '未知村'), value: Number(v.peopleCount || 0), })), amount: (wb.villageCharts || []).map((v) => ({ name: regionName(String(v.villageId || '')) || String(v.villageId || '未知村'), value: Number(v.amount || 0), })), } base.townVillagePeopleChart = villageCharts.people base.townVillageAmountChart = villageCharts.amount base.townAmountChart = villageCharts.amount if (halfYear.people.length) { base.batchCompareChart = halfYear.amount.map((i, idx) => ({ ...i, count: halfYear.people[idx]?.value, })) } } const withLand = Number(wb.withLandCount || 0) const withoutLand = Number(wb.withoutLandCount || 0) if (role.key === 'town') { base.townReview = { pendingCount: pendingTown, withLandCount: withLand, withoutLandCount: withoutLand, pendingVillageCount: new Set(monthBatches.filter((b) => b.statusCode === 'PENDING_TOWN_APPROVE').map((b) => b.village)).size, qualifyPie: [ { name: '有土地退养', value: withLand }, { name: '无土地有资格', value: withoutLand }, ].filter((i) => i.value > 0), villageReviews: (wb.villageCharts || []).map((v) => ({ village: regionName(String(v.villageId || '')) || String(v.villageId || '未知村'), total: Number(v.peopleCount || 0), pending: 0, withLand: 0, withoutLand: 0, })), pendingPeople: [], tips: TOWN_REVIEW_TIPS, } } if (role.key === 'social') { base.socialWorkbench = { matchPendingCount: Number(statusCounts.PENDING_INSURANCE_MATCH || statusCounts.INSURANCE_MATCHING || 0), compareReadyCount: Number(statusCounts.READY_EXPORT || 0), diskPendingCount: Number(statusCounts.READY_EXPORT || 0) + Number(statusCounts.EXPORTED || 0), alertCount: pendingAlert, tips: SOCIAL_WORKBENCH_TIPS, } } if (role.key === 'leader') { base.leaderWorkbench = { pendingApprovalCount: pendingLeader, specialPendingCount: 0, issueReadyCount: Number(statusCounts.READY_EXPORT || 0), alertCount: pendingAlert, tips: LEADER_WORKBENCH_TIPS, } } if (role.allData && base.districtOverview) { const townStats = (summary?.townStats as BatchStatRow[] | undefined) || [] const byTownPay = new Map() for (const s of townStats) { const name = regionName(String(s.townId || '')) || String(s.townId || '未知') const curT = byTownPay.get(name) || { people: 0, amount: 0 } curT.people += Number(s.shouldPayCount || 0) curT.amount += Number(s.shouldPayAmount || 0) byTownPay.set(name, curT) } const qRows = wb.townQualify || [] const names = Array.from( new Set([ ...byTownPay.keys(), ...qRows.map((r) => regionName(String(r.townId || '')) || String(r.townId || '未知')), ]), ).sort((a, b) => a.localeCompare(b, 'zh-CN')) const qByName = new Map( qRows.map((r) => [ regionName(String(r.townId || '')) || String(r.townId || '未知'), r, ]), ) base.districtOverview.halfYearPeopleChart = halfYear.people base.districtOverview.halfYearAmountChart = halfYear.amount base.districtOverview.peopleCount = regionPeople base.districtOverview.amount = regionAmount base.districtOverview.withLandCount = withLand base.districtOverview.withoutLandCount = withoutLand base.districtOverview.qualifiedCount = Number(wb.qualifiedCount || 0) base.districtOverview.townCount = names.length base.districtOverview.villageCount = qRows.reduce((s, r) => s + Number(r.villageCount || 0), 0) base.districtOverview.townPeopleChart = names.map((n) => ({ name: n, value: Number(qByName.get(n)?.peopleCount || 0) || byTownPay.get(n)?.people || 0, })) base.districtOverview.townAmountChart = names.map((n) => ({ name: n, value: Number(qByName.get(n)?.amount || 0) || byTownPay.get(n)?.amount || 0, })) base.districtOverview.townWithLandChart = names.map((n) => ({ name: n, value: Number(qByName.get(n)?.withLandCount || 0), })) base.districtOverview.townWithoutLandChart = names.map((n) => ({ name: n, value: Number(qByName.get(n)?.withoutLandCount || 0), })) base.districtOverview.qualifyPie = [ { name: '有土地退养', value: withLand }, { name: '无土地有资格', value: withoutLand }, { name: '未标注资格', value: Math.max(0, Number(wb.personnelCount || 0) - withLand - withoutLand), }, ].filter((i) => i.value > 0) base.districtOverview.townSummary = names.map((n) => { const q = qByName.get(n) const pay = byTownPay.get(n) return { town: n, villageCount: Number(q?.villageCount || 0), peopleCount: Number(q?.peopleCount || 0) || Number(pay?.people || 0), amount: Number(q?.amount || 0) || Number(pay?.amount || 0), withLandCount: Number(q?.withLandCount || 0), withoutLandCount: Number(q?.withoutLandCount || 0), qualifiedCount: Number(q?.qualifiedCount || 0), } }) base.townAmountChart = base.districtOverview.townAmountChart } return base } /** —— 人员 / 村批 —— */ export async function fetchVillageBatches(): Promise { await ensureTowns() const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const { townId } = await resolveScopeIds(role?.town, role?.village) if (townId) await ensureVillages(townId) const districtPage = await post>>('/batches/page', { pageNum: 1, pageSize: 50, batchLevel: 'DISTRICT', lite: true, }) let list = districtPage?.list || [] if (!list.length) { const townPage = await post>>('/batches/page', { pageNum: 1, pageSize: 50, batchLevel: 'TOWN', lite: true, }) const towns = townPage?.list || [] const seen = new Set() const synthesized: Record[] = [] for (const b of towns) { const parent = String(b.parentBatchId || '') const id = parent || String(b.id || '') if (!id || seen.has(id)) continue seen.add(id) synthesized.push({ ...b, id, batchLevel: 'DISTRICT', townId: '', villageId: '', batchNo: String(b.payMonth || b.batchNo || ''), }) } list = synthesized } const mapped = list.map(mapVillageBatch) return mapped.sort((a, b) => { const ak = isPayMonthLike(a.batchNo) ? a.batchNo : `0${a.batchNo}` const bk = isPayMonthLike(b.batchNo) ? b.batchNo : `0${b.batchNo}` if (ak !== bk) return bk.localeCompare(ak) return String(b.id).localeCompare(String(a.id)) }) } /** 已停用:村合作社不再创建村批 */ export async function ensureVillageMonthBatch(_town?: string, _village?: string, _payMonth?: string) { return } export type PeoplePageParams = { batchNo?: string town?: string village?: string name?: string idCard?: string phone?: string /** 业务状态码,如 NORMAL / PAUSED */ bizStatus?: string /** 居保匹配码 SUCCESS / FAIL / PENDING… */ insuranceMatchResult?: string /** 退养资格 WITH_LAND / WITHOUT_LAND / INELIGIBLE / UNSET */ retireQualifyType?: string source?: string /** 排除业务状态,逗号分隔 */ bizStatusNotIn?: string /** 仅包含业务状态,逗号分隔 */ bizStatusIn?: string /** 非本村视角隐藏他村 DRAFT */ hideDraftUnlessVillage?: boolean pageNum?: number pageSize?: number } export async function fetchPeoplePage( params?: PeoplePageParams, ): Promise<{ list: Person[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const town = params?.town || role?.town const village = params?.village || role?.village const { townId, villageId } = await resolveScopeIds(town, village) if (townId) await ensureVillages(town || townId) else await ensureAllVillages() const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const res = await post>>('/personnel/page', { pageNum, pageSize, townId: townId || undefined, villageId: villageId || undefined, name: params?.name || undefined, idNumber: params?.idCard || undefined, phoneNumber: params?.phone || undefined, bizStatus: params?.bizStatus || undefined, insuranceMatchResult: params?.insuranceMatchResult || undefined, retireQualifyType: params?.retireQualifyType && params.retireQualifyType !== 'UNSET' ? params.retireQualifyType : undefined, retireQualifyUnset: params?.retireQualifyType === 'UNSET' ? true : undefined, source: params?.source || undefined, bizStatusNotIn: params?.bizStatusNotIn || undefined, bizStatusIn: params?.bizStatusIn || undefined, hideDraftUnlessVillage: params?.hideDraftUnlessVillage ? true : undefined, payMonth: params?.batchNo || undefined, }, { timeout: 90_000 }) const list = (res?.list || []).map((p) => mapPerson(p, params?.batchNo || currentPayMonth())) return { list, total: Number(res?.total || 0), pageNum: Number(res?.pageNum || pageNum), pageSize: Number(res?.pageSize || pageSize), } } export async function fetchPeople(params?: { batchNo?: string town?: string village?: string }): Promise { const page = await fetchPeoplePage({ ...params, pageNum: 1, pageSize: 500, }) // 兼容旧调用:尽量拉全本村(单页上限 500;超大村可再扩) if (page.total <= page.list.length) return page.list const all = [...page.list] const pages = Math.max(1, Math.ceil(page.total / 500)) for (let n = 2; n <= pages; n++) { const more = await fetchPeoplePage({ ...params, pageNum: n, pageSize: 500 }) all.push(...more.list) if (more.list.length < 500) break } return all } export async function downloadPersonnelImportTemplate(town?: string, village?: string) { const { townId, villageId } = await resolveScopeIds(town, village) const res = await service.get('/personnel/import-template', { params: { townId: townId || undefined, villageId: villageId || undefined, }, responseType: 'blob', }) const blob = res.data as Blob const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = '人员导入模板.xlsx' a.click() URL.revokeObjectURL(url) } export type PersonnelImportFailRow = { rowNum?: number error?: string cells?: string[] name?: string idNumber?: string phoneNumber?: string town?: string village?: string bankCardNumber?: string [key: string]: unknown } export type PersonnelImportResult = { fileName?: string successCount?: number failCount?: number errors?: string[] failRows?: PersonnelImportFailRow[] message?: string } export type PersonnelImportHistoryItem = { id?: string operator?: string operateTime?: number fileName?: string successCount?: number failCount?: number errorSummary?: string townId?: string villageId?: string result?: string } export async function importPersonnelExcel(file: File, town?: string, village?: string) { const { townId, villageId } = await resolveScopeIds(town, village) if (!townId || !villageId) throw new Error('请先选择街镇与村居') const form = new FormData() form.append('file', file) return post('/personnel/import', form as unknown as Record, { params: { townId, villageId }, timeout: 600000, }) } export async function exportPersonnelImportFailRows(failRows: PersonnelImportFailRow[]) { if (!failRows?.length) throw new Error('没有可导出的失败行') const res = await service.post('/personnel/import/fail-export', { failRows }, { responseType: 'blob', timeout: 120000, }) const blob = res.data as Blob if (blob && blob.type && blob.type.includes('application/json')) { const text = await blob.text() let msg = '导出失败' try { const j = JSON.parse(text) as { message?: string; msg?: string } msg = j.message || j.msg || msg } catch { /* ignore */ } throw new Error(msg) } const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = '人员导入失败行.xlsx' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } export async function fetchPersonnelImportHistory(limit = 50): Promise { const list = await get('/personnel/import/history', { limit }) return Array.isArray(list) ? list : [] } export async function fetchAlertDetail(alertId: string): Promise { if (!alertId) return null await ensureTowns() try { const raw = await get>(`/alerts/${encodeURIComponent(alertId)}`) return raw ? mapAlert(raw) : null } catch { return null } } export async function fetchPersonDetail(personnelId: string): Promise { if (!personnelId) return null await ensureTowns() try { const raw = await get>( `/personnel/${encodeURIComponent(personnelId)}`, ) return raw ? mapPerson(raw) : null } catch { return null } } export async function savePerson(payload: Partial) { const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const town = payload.town || role?.town const village = payload.village || role?.village const { townId, villageId } = await resolveScopeIds(town, village) if (!townId || !villageId) throw new Error('请先选择街镇与村居') const isEdit = Boolean(payload.id) const blank = (v: unknown) => (v == null || v === '' ? undefined : String(v).trim()) const numOrU = (v: unknown) => v == null || v === '' || Number.isNaN(Number(v)) ? undefined : Number(v) const raw = await post>( '/personnel', { id: isEdit ? String(payload.id) : undefined, name: payload.name, idNumber: payload.idCard, townId, villageId, bankCardNumber: payload.bankCard, bankName: blank(payload.bankName), bankType: blank(payload.bankType), bankBranchCode: blank(payload.bankBranchCode), socialInsuranceCode: blank(payload.socialInsuranceCode), phoneNumber: blank(payload.phoneNumber), address: blank(payload.address), householdType: blank(payload.householdType), villageCode: blank(payload.villageCode), entrustPlanCode: blank(payload.entrustPlanCode), entrustPlanName: blank(payload.entrustPlanName), monthlyStandard: numOrU(payload.standard), baseAmount: numOrU(payload.baseAmount), oneTimeAmount: numOrU(payload.oneTimeAmount), landSubsidyIncrease: numOrU(payload.landSubsidyIncrease), funeralDiffAmount: numOrU(payload.funeralDiffAmount), enjoyStartMonth: monthToPayMonth(payload.startMonth) || (isEdit ? undefined : currentPayMonth()), enjoyEndMonth: monthToPayMonth(payload.enjoyEndMonth) || undefined, supplementStartMonth: monthToPayMonth(payload.supplementStartMonth) || undefined, nextPayStartMonth: monthToPayMonth(payload.nextPayStartMonth) || undefined, payeeName: blank(payload.payeeName) || blank(payload.name), deathDate: isEdit ? String(payload.deathDate ?? '').trim() : blank(payload.deathDate), heirName: isEdit ? String(payload.heirName ?? '').trim() : blank(payload.heirName), heirRelation: isEdit ? String(payload.heirRelation ?? '').trim() : blank(payload.heirRelation), heirIdNumber: isEdit ? String(payload.heirIdNumber ?? '').trim() : blank(payload.heirIdNumber), heirBankCard: isEdit ? String(payload.heirBankCard ?? '').trim() : blank(payload.heirBankCard), heirPhone: isEdit ? String(payload.heirPhone ?? '').trim() : blank(payload.heirPhone), source: payload.source || (isEdit ? '管理员修改' : '村级录入'), remarks: (() => { const edited = payload.remark const raw = payload.remarkRaw if (raw != null && sanitizePersonnelRemark(raw) === String(edited || '').trim()) { return raw } return edited })(), operatorRoleKey: cfg.currentRole, expectedUpdateTime: isEdit ? payload.updateTimeMs : undefined, }, { skipCache: true }, ) clearGetCache() return raw ? mapPerson(raw) : null } export async function submitRetireQualify( personnelId: string, payload?: { opinion?: string; fileUrls?: string }, config?: { silent?: boolean }, ) { const raw = await post>( `/personnel/${encodeURIComponent(personnelId)}/qualify/submit`, { opinion: payload?.opinion, fileUrls: payload?.fileUrls, operator: operatorOf(), }, config, ) return raw ? mapPerson(raw) : null } export async function submitRetireQualifyBatch(payload: { personnelIds: string[] opinion?: string fileUrls?: string }) { const res = await post>( '/personnel/qualify/submit-batch', { personnelIds: payload.personnelIds, opinion: payload.opinion, fileUrls: payload.fileUrls, operator: operatorOf(), }, { timeout: 180_000 }, ) return { ok: res?.ok !== false, message: String(res?.message || ''), successCount: Number(res?.successCount || 0), pendingCount: Number(res?.pendingCount || 0), failCount: Number(res?.failCount || 0), } } function qualifyTypeCode( retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格', ) { return retireQualifyType === '无资格' ? 'INELIGIBLE' : retireQualifyType === '无土地有资格' ? 'WITHOUT_LAND' : retireQualifyType === '有土地退养' ? 'WITH_LAND' : undefined } export async function reviewRetireQualify(payload: { personnelId: string pass: boolean retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格' opinion?: string fileUrls?: string }) { await post(`/personnel/${encodeURIComponent(payload.personnelId)}/qualify/review`, { pass: payload.pass, retireQualifyType: qualifyTypeCode(payload.retireQualifyType), opinion: payload.opinion, fileUrls: payload.fileUrls, operator: operatorOf(), }) return { ok: true } } export async function reviewRetireQualifyBatch(payload: { personnelIds: string[] pass: boolean retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格' opinion?: string fileUrls?: string }) { const res = await post>('/personnel/qualify/review-batch', { personnelIds: payload.personnelIds, pass: payload.pass, retireQualifyType: qualifyTypeCode(payload.retireQualifyType), opinion: payload.opinion, fileUrls: payload.fileUrls, operator: operatorOf(), }, { timeout: 180_000 }) return { ok: res?.ok !== false, message: String(res?.message || ''), successCount: Number(res?.successCount || 0), failCount: Number(res?.failCount || 0), } } export async function generateDistrictMonthBatch( payMonth?: string, matchInsurance = true, simulateFirstDay = false, ) { const res = await post>('/batches/generate-district-month', undefined, { params: { payMonth: (payMonth || currentPayMonth()).replace(/-/g, ''), matchInsurance, simulateFirstDay: simulateFirstDay || undefined, }, timeout: 1_200_000, }) clearGetCache() return res } export async function fetchPendingSpecialCount(town?: string): Promise { await ensureTowns() const { townId } = await resolveScopeIds(town) const specials = await get>>('/special-biz/page', { pageNum: 1, pageSize: 200, approveStatus: '待分管领导审批', }) const rows = specials?.list || [] if (!townId) return rows.length return rows.filter((s) => String(s.townId || '') === townId).length } export async function submitVillageBatch(batchId: string) { await post(`/batches/${encodeURIComponent(batchId)}/submit`) return { ok: true } } /** 更新批次纳入人员(草稿/退回可改) */ export async function updateBatchMembers(batchId: string, personnelIds: string[]) { await put( `/batches/${encodeURIComponent(batchId)}/members`, { personnelIds, operator: operatorOf(), }, { timeout: 60000 }, ) return { ok: true } } /** 批次已纳入人员 ID(发放明细) */ export async function fetchBatchMemberIds(batchId: string): Promise { if (!batchId) return [] const list = await pageAll>((pageNum, pageSize) => post>>('/payments/details/page', { pageNum, pageSize, batchId, }), ) return list.map((d) => String(d.personnelId || '')).filter(Boolean) } function numOr0(v: unknown) { const n = Number(v) return Number.isFinite(n) ? n : 0 } function attachPaymentAmounts( base: Person, d: Record, batchNo: string, ): BatchMemberPerson { const monthlyAmount = numOr0(d.monthlyAmount) const oneTimeAmount = numOr0(d.oneTimeAmount) const supplementAmount = numOr0(d.supplementAmount) const funeralAmount = numOr0(d.funeralAmount) const deductAmount = numOr0(d.deductAmount) const payableRaw = d.totalAmount const payableAmount = payableRaw == null || payableRaw === '' ? monthlyAmount + oneTimeAmount + supplementAmount + funeralAmount - deductAmount : numOr0(payableRaw) return { ...base, batchNo, // 列表「月补标准」只用发放明细,禁止回退人员底数陈旧月标(同批不一致的根因之一) standard: monthlyAmount, bankCard: String(d.bankCardNumber || base.bankCard || ''), remark: String(d.remarks || base.remark || ''), paymentDetailId: String(d.id || ''), monthlyAmount, oneTimeAmount, supplementAmount, funeralAmount, deductAmount, payableAmount, districtAmount: d.districtAmount == null || d.districtAmount === '' ? undefined : numOr0(d.districtAmount), townAmount: d.townAmount == null || d.townAmount === '' ? undefined : numOr0(d.townAmount), actualAmount: d.actualAmount == null || d.actualAmount === '' ? undefined : numOr0(d.actualAmount), returnResult: d.returnResult ? String(d.returnResult) : undefined, changeFlag: d.changeFlag === 'NEW' || d.changeFlag === 'KEEP' || d.changeFlag === 'REMOVE' ? (d.changeFlag as 'NEW' | 'KEEP' | 'REMOVE') : undefined, } } /** * 本村发放历史:按发放明细行展示(月补/补发/应发等),不用人员底数里陈旧的 monthlyStandard。 */ /** 指定人员的历史发放明细(各批次) */ export async function fetchPersonnelPaymentHistory( personnelId: string, ): Promise { if (!personnelId) return [] // 区划已缓存则不再请求;人员详情与明细分页并行 const townsPromise = ensureTowns().catch(() => undefined) const [page1, personRaw] = await Promise.all([ post>>('/payments/details/page', { pageNum: 1, pageSize: 200, personnelId, }), get>(`/personnel/${encodeURIComponent(personnelId)}`, undefined, { silent: true, }).catch(() => null), ]) await townsPromise const details = [...(page1?.list || [])] const total = Number(page1?.total || details.length) if (total > details.length) { for (let pageNum = 2; pageNum <= 5 && details.length < total; pageNum++) { const more = await post>>('/payments/details/page', { pageNum, pageSize: 200, personnelId, }) const rows = more?.list || [] if (!rows.length) break details.push(...rows) if (rows.length < 200) break } } if (!details.length) return [] const basePerson = personRaw ? mapPerson(personRaw) : null const result: BatchMemberPerson[] = [] for (const d of details) { const batchNo = String(d.batchNo || d.payMonth || '') const base = basePerson || mapPerson( { id: personnelId, name: d.name, idNumber: d.idNumber, townId: d.townId, villageId: d.villageId, bankCardNumber: d.bankCardNumber, bankName: d.payBank, monthlyStandard: d.monthlyAmount, remarks: d.remarks, bizStatus: 'NORMAL', }, batchNo, ) result.push(attachPaymentAmounts(base, d, batchNo)) } return result.sort((a, b) => String(b.batchNo || '').localeCompare(String(a.batchNo || ''))) } /** 指定人员的告警/处理记录 */ export async function fetchPersonnelAlerts(params: { personnelId?: string personName?: string town?: string village?: string }): Promise { await ensureTowns() const { townId, villageId } = await resolveScopeIds(params.town, params.village) const list = await pageAll>((pageNum, pageSize) => post>>('/alerts/page', { pageNum, pageSize, personnelId: params.personnelId || undefined, personnelName: params.personName || undefined, townId: townId || undefined, villageId: villageId || undefined, }), ) let mapped = list.map(mapAlert) // 后端未带 personnelId 过滤时,前端再按姓名兜底 if (params.personnelId) { const byId = mapped.filter((a) => a.personnelId === params.personnelId) if (byId.length) mapped = byId else if (params.personName) { mapped = mapped.filter((a) => a.personName === params.personName) } } return mapped.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || ''))) } export async function fetchVillagePaymentHistory(params?: { town?: string village?: string }): Promise { await ensureTowns() const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const town = params?.town || role?.town const village = params?.village || role?.village const { townId, villageId } = await resolveScopeIds(town, village) if (townId) await ensureVillages(townId) const details = await pageAll>((pageNum, pageSize) => post>>('/payments/details/page', { pageNum, pageSize, townId: townId || undefined, villageId: villageId || undefined, }), ) if (!details.length) return [] const roster = await fetchPeople({ town, village }).catch(() => [] as Person[]) const byId = new Map(roster.map((p) => [String(p.id), p])) const result: BatchMemberPerson[] = [] for (const d of details) { const pid = String(d.personnelId || '') const batchNo = String(d.batchNo || d.payMonth || '') const base = (pid && byId.get(pid)) || mapPerson( { id: pid || String(d.id || ''), name: d.name, idNumber: d.idNumber, townId: d.townId, villageId: d.villageId, bankCardNumber: d.bankCardNumber, bankName: d.payBank, monthlyStandard: d.monthlyAmount, remarks: d.remarks, bizStatus: 'NORMAL', }, batchNo, ) result.push(attachPaymentAmounts(base, d, batchNo)) } return result.sort((a, b) => { const bn = String(b.batchNo || '').localeCompare(String(a.batchNo || '')) if (bn !== 0) return bn return String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN') }) } /** * 本批次人员明细:只返回发放明细中的人(不是全村人员库)。 * 草稿/退回批会先走 ensure-current-month,剔除暂停/停止/死亡等默认不应在册人员。 * 附带本批次金额拆分(月补/补发/扣减/应发合计)。 */ export async function fetchBatchMemberPeoplePage( batchId: string, opts?: { batchNo?: string town?: string village?: string pruneDefaults?: boolean parentBatchId?: string pageNum?: number pageSize?: number name?: string idCard?: string sortBy?: string sortOrder?: string onlyFuneral?: boolean onlySupplement?: boolean }, ): Promise<{ list: BatchMemberPerson[] total: number pageNum: number pageSize: number summary?: Record }> { const pageNum = opts?.pageNum || 1 const pageSize = opts?.pageSize || 10 if (!batchId) return { list: [], total: 0, pageNum, pageSize } await ensureTowns() const rawTown = opts?.town const town = rawTown && rawTown !== '全区' && rawTown !== '本镇' ? rawTown : undefined const village = opts?.village const villageKey = village === '本镇' || village === '全区' ? undefined : village const { townId, villageId } = await resolveScopeIds(town, villageKey) const queryBatchId = opts?.parentBatchId || batchId const detailsRes = await post>>('/payments/details/page', { pageNum, pageSize, batchId: queryBatchId, townId: townId || undefined, villageId: villageId || undefined, name: opts?.name?.trim() || undefined, idNumber: opts?.idCard?.trim() || undefined, sortBy: opts?.sortBy || undefined, sortOrder: opts?.sortOrder || undefined, onlyFuneral: opts?.onlyFuneral || undefined, onlySupplement: opts?.onlySupplement || undefined, }) const details = detailsRes?.list || [] const batchNo = opts?.batchNo || currentPayMonth() const result: BatchMemberPerson[] = [] for (const d of details) { const pid = String(d.personnelId || '') if (!pid) continue const base = mapPerson( { id: pid, name: d.name, idNumber: d.idNumber, townId: d.townId, villageId: d.villageId, bankCardNumber: d.bankCardNumber, monthlyStandard: d.monthlyAmount, remarks: d.remarks, bizStatus: d.bizStatus || 'NORMAL', retireQualifyType: d.retireQualifyType, }, batchNo, ) result.push(attachPaymentAmounts(base, d, batchNo)) } return { list: result, total: Number(detailsRes?.total || result.length), pageNum, pageSize, summary: detailsRes?.summary, } } export async function fetchBatchMemberPeople( batchId: string, opts?: { batchNo?: string town?: string village?: string pruneDefaults?: boolean parentBatchId?: string pageNum?: number pageSize?: number }, ): Promise { const page = await fetchBatchMemberPeoplePage(batchId, { ...opts, pageNum: opts?.pageNum || 1, pageSize: opts?.pageSize || 10, }) return page.list } /** 批次详情(原始 BatchVO → VillageBatch) */ export async function fetchBatchDetail(batchId: string): Promise { if (!batchId) return null await ensureTowns() const raw = await get>(`/batches/${encodeURIComponent(batchId)}`) if (!raw) return null if (raw.townId) await ensureVillages(String(raw.townId)) return mapVillageBatch(raw) } /** 批次流程总览(可执行动作 + 阶段说明) */ export async function fetchBatchOverview(batchId: string): Promise { if (!batchId) return null try { const raw = await get>( `/workflow/batches/${encodeURIComponent(batchId)}/overview`, ) if (!raw) return null const logs = Array.isArray(raw.recentLogs) ? (raw.recentLogs as Record[]).map(mapProcessLog) : [] return { batchId: String(raw.batchId || batchId), batchNo: String(raw.batchNo || ''), currentStage: flowStageLabel(String(raw.currentStage || '')), status: String(raw.status || ''), stageDescription: raw.stageDescription ? String(raw.stageDescription) : undefined, availableActions: Array.isArray(raw.availableActions) ? (raw.availableActions as string[]) : [], recentLogs: logs, } } catch { return null } } /** 批次流程时间线 */ export async function fetchBatchTimeline(batchId: string): Promise { if (!batchId) return [] try { const res = await get> | Record[]>( `/process-logs/batches/${encodeURIComponent(batchId)}/timeline`, ) const list = Array.isArray(res) ? res : res?.list || [] return list .map(mapProcessLog) .sort((a, b) => String(b.operateTime || '').localeCompare(String(a.operateTime || ''))) } catch { return [] } } /** 人员状态/特殊业务时间线(统一走 POST /process-logs/page,避免未重启后端时 GET timeline 404) */ export async function fetchPersonnelTimeline(personnelId: string): Promise { if (!personnelId) return [] try { const page = await post>>('/process-logs/page', { personnelId, pageNum: 1, pageSize: 100, }) return (page?.list || []) .map(mapProcessLog) .sort((a, b) => String(b.operateTime || '').localeCompare(String(a.operateTime || ''))) } catch { return [] } } /** 补充流程备注 */ export async function addBatchProcessNote(payload: { batchId: string opinion: string fileUrls?: string }) { await post('/process-logs/note', { batchId: payload.batchId, opinion: payload.opinion, fileUrls: payload.fileUrls, operator: operatorOf(), }) return { ok: true } } /** —— 居保 —— */ /** 人工确认需要 batchId:记录最近一次打开的居保批次 */ let lastInsuranceBatchId = '' const STAGE_TO_BATCH_STATUS: Record = { 人员新增录入: 'DRAFT', 镇经发中心初审: 'PENDING_TOWN_APPROVE', 居保状态匹配: 'PENDING_INSURANCE_MATCH', 比对结果已生成: 'PENDING_STATUS_MAINTAIN', 分管领导审批: 'PENDING_LEADER_APPROVE', 待线下发放: 'READY_EXPORT', 回盘信息确认: 'EXPORTED', 已回盘确认: 'RETURNED', 已归档: 'ARCHIVED', 退回修改: 'REJECTED', } export async function fetchInsuranceBatchesPage(params?: { tab?: 'todo' | 'history' batchNo?: string stage?: string townId?: string pageNum?: number pageSize?: number }): Promise<{ list: VillageBatch[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const statusIn = params?.stage ? STAGE_TO_BATCH_STATUS[params.stage] : params?.tab === 'history' ? 'PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED' : 'PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN' const townId = params?.townId || undefined const batchLevel = townId ? 'TOWN' : 'DISTRICT' const res = await post>>('/batches/page', { pageNum, pageSize, batchLevel, townId, batchNo: params?.batchNo || undefined, statusIn: statusIn || undefined, lite: true, withTownTotals: true, }) let list = (res?.list || []).map(mapVillageBatch) if (!list.length && !params?.batchNo && !townId) { const fallback = await post>>('/batches/page', { pageNum, pageSize, batchLevel: 'TOWN', batchNo: params?.batchNo || undefined, statusIn: statusIn || undefined, lite: true, withTownTotals: true, }) list = (fallback?.list || []).map(mapVillageBatch) return { list, total: Number(fallback?.total || list.length), pageNum, pageSize, } } return { list, total: Number(res?.total || list.length), pageNum, pageSize } } export async function fetchInsuranceBatches(params?: { tab?: 'todo' | 'history' }): Promise { const page = await fetchInsuranceBatchesPage({ ...params, pageNum: 1, pageSize: 10 }) return page.list } function mapInsuranceResultRow(r: Record, batchNo?: string): Person { const match = String(r.matchResult || r.insuranceMatchResult || r.result || '') let biz = String(r.bizStatus || '') if (!biz) { if (match === 'SUCCESS' || match === 'MANUAL_PASS') biz = 'NORMAL' else if (match === 'FAIL' || match === 'LOSE_ELIGIBILITY') biz = 'ABNORMAL' else biz = 'PENDING_INSURANCE_MATCH' } const rawMsg = String(r.message || r.remarks || r.failReason || '') let remark = rawMsg if (match === 'MANUAL_PASS') { if (!remark || remark === '匹配成功' || remark === '居保比对通过') { remark = '人工确认匹配成功' } } else if (match === 'SUCCESS') { if (!remark || remark === '匹配成功' || remark === '居保比对通过') { remark = '系统自动匹配成功' } } return mapPerson( { id: r.personnelId || r.id, name: r.name || r.personnelName, idNumber: r.idNumber, townId: r.townId, villageId: r.villageId, insuranceMatchResult: match, bizStatus: biz, retireQualifyType: r.retireQualifyType, remarks: remark, }, batchNo, ) } export async function fetchInsuranceResultsPage(params?: { batchId?: string batchNo?: string town?: string village?: string pageNum?: number pageSize?: number }): Promise<{ list: Person[]; total: number; pageNum: number; pageSize: number }> { const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 let batchId = params?.batchId || lastInsuranceBatchId if (!batchId) { const found = await fetchInsuranceBatchesPage({ batchNo: params?.batchNo, pageNum: 1, pageSize: 1, }) batchId = found.list[0]?.id || '' } if (!batchId) return { list: [], total: 0, pageNum, pageSize } lastInsuranceBatchId = batchId try { const townName = params?.town && params.town !== '全区' && params.town !== '本镇' ? params.town : '' const villageName = params?.village && params.village !== '全区' && params.village !== '本镇' ? params.village : '' const { townId } = await resolveScopeIds(townName || undefined, villageName || undefined) const raw = await get> | Record[]>( `/insurance-match/batches/${encodeURIComponent(batchId)}/results`, { townId: townId || undefined, pageNum, pageSize, }, { timeout: 120_000 }, ) const rows = Array.isArray(raw) ? raw : raw?.list || [] const total = Array.isArray(raw) ? raw.length : Number(raw?.total || rows.length) let mapped = rows.map((r) => mapInsuranceResultRow(r, params?.batchNo)) if (townName) mapped = mapped.filter((r) => r.town === townName) return { list: mapped, total, pageNum, pageSize } } catch { return { list: [], total: 0, pageNum, pageSize } } } export async function fetchInsuranceResults(params?: { batchId?: string batchNo?: string town?: string village?: string pageNum?: number pageSize?: number }): Promise { const page = await fetchInsuranceResultsPage({ ...params, pageNum: params?.pageNum || 1, pageSize: params?.pageSize || 10, }) return page.list } export async function triggerInsuranceMatch( batchIds?: string | string[], personnelIds?: string | string[], ) { const ids = batchIds == null ? [] : Array.isArray(batchIds) ? batchIds : [batchIds] const pids = personnelIds == null ? [] : Array.isArray(personnelIds) ? personnelIds.map(String) : [String(personnelIds)] let count = 0 let lastSummary: Record | null = null for (const batchId of ids) { lastInsuranceBatchId = batchId const res = await post<{ nextStatus?: string failCount?: number successCount?: number manualCount?: number items?: Array> }>('/insurance-match/trigger', { batchId, personnelIds: pids.length ? pids : undefined, operator: operatorOf(), }) lastSummary = { batchId, ...(res || {}), failItems: (res?.items || []).filter((i) => { const r = String(i.matchResult || i.match_result || '') return r === 'FAIL' || r === 'LOSE_ELIGIBILITY' || r === 'PENDING' }), } // 全员成功时后端已进领导审;仍补齐明细。有 FAIL 时不要假装已生成可提交清单 if (!res?.failCount) { await post(`/payments/batches/${encodeURIComponent(batchId)}/generate`).catch(() => undefined) } count += 1 } return { ok: true, count, lastSummary } } /** 居保匹配任务执行日志 */ export async function fetchInsuranceTaskLogs(batchId: string, limit = 30) { if (!batchId) return [] as Record[] const list = await get[]>( `/insurance-match/batches/${encodeURIComponent(batchId)}/task-logs`, { limit }, ) return Array.isArray(list) ? list : [] } export async function confirmInsuranceManual( personId: number | string, opts?: { batchId?: string; pass?: boolean; remark?: string }, ) { const bid = opts?.batchId || lastInsuranceBatchId if (!bid) { throw new Error('请先打开比对结果后再人工确认') } lastInsuranceBatchId = bid const pass = opts?.pass !== false await post('/insurance-match/manual-confirm', { personnelId: personIdStr(personId), batchId: bid, pass, remark: opts?.remark || (pass ? '人工确认匹配成功' : '人工确认暂停发放'), operator: operatorOf(), }) return { ok: true } } /** 居保完成后提交分管领导(推进批次状态,非仅生成明细) */ export async function submitToLeader(batchId: string) { await post(`/batches/${encodeURIComponent(batchId)}/submit-leader`) return { ok: true } } /** 批次内居保未闭环人数(未匹配/失败等;提交领导前校验) */ export async function fetchUnresolvedInsuranceCount(batchId: string): Promise { if (!batchId) return 0 const res = await get<{ count?: number }>( `/batches/${encodeURIComponent(batchId)}/unresolved-insurance-count`, undefined, { silent: true }, ) return Number(res?.count || 0) } /** 批量查询居保未闭环人数(月补贴清单列表用) */ export async function fetchUnresolvedInsuranceCounts( batchIds: string[], ): Promise> { const ids = (batchIds || []).filter(Boolean) if (!ids.length) return {} const res = await post>( '/batches/unresolved-insurance-counts', { batchIds: ids }, { silent: true }, ) return res || {} } export async function exportCompareList(_batchId?: string) { return { ok: false as const, message: '比对清单文件导出接口尚未对接,请在「查看比对结果」中核对名单', fileName: '', } } export async function returnBatchToTown(batchId: string, reason?: string) { await post('/batches/town-approve', { bizId: batchId, action: 'REJECT', opinion: reason || '居保匹配异常,退回镇经发中心', operator: operatorOf(), }) return { ok: true } } /** —— 审批 —— */ const TOWN_HISTORY_SKIP = new Set(['DRAFT', 'PENDING_TOWN_APPROVE']) const LEADER_HISTORY_STATUSES = new Set(['READY_EXPORT', 'EXPORTED', 'RETURNED', 'ARCHIVED']) function toTownApproval(vb: VillageBatch, status: string): Approval { return { id: approvalIdBatch(vb.id), batchId: vb.id, bizType: '月度批次人员申请', batchNo: vb.batchNo, town: vb.town, village: vb.village, title: `${vb.village}${vb.month}批次人员申请`, applicant: `${vb.village}合作社`, node: '镇经发中心初审', status, createdAt: vb.submittedAt || '', peopleCount: vb.peopleCount, totalAmount: vb.totalAmount, } } function toQualifyApproval(p: Person, status: string): Approval { return { id: approvalIdQualify(String(p.id)), bizType: '退养资格审批', batchNo: '—', town: p.town, village: p.village, title: `${p.name || ''}退养资格审批`, applicant: `${p.village || ''}合作社`, node: '镇经发中心初审', status, createdAt: p.updateTime || p.createTime || '', peopleCount: 1, personName: p.name, personnelId: String(p.id), idCard: p.idCard, } } function toLeaderApproval(vb: VillageBatch, status: string): Approval { return { id: approvalIdBatch(vb.id), batchId: vb.id, bizType: '月补贴清单确认', batchNo: vb.batchNo, town: vb.town, village: vb.village || '本镇', title: `${vb.town}月补贴清单提交分管领导`, applicant: `${vb.town}社区事务受理中心`, node: '分管领导线上审批', status, createdAt: vb.submittedAt || '', peopleCount: vb.peopleCount, totalAmount: vb.totalAmount, } } export async function fetchApprovals(params?: { tab?: string node?: string town?: string village?: string batchNo?: string bizTypeIncludes?: string pageNum?: number pageSize?: number status?: string }): Promise<{ list: Approval[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const { townId, villageId } = await resolveScopeIds(params?.town, params?.village) if (townId) await ensureVillages(townId) const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const tab = params?.tab === 'history' ? 'history' : params?.tab === 'all' ? 'all' : 'pending' const wantTown = !params?.node || params.node.includes('镇经发') const wantLeader = !params?.node || params.node.includes('分管领导') if (wantTown && !wantLeader) { if (tab !== 'history') { const people = await fetchPeoplePage({ town: params?.town, village: params?.village, bizStatus: 'PENDING_TOWN_APPROVE', pageNum, pageSize, }) return { list: people.list.map((p) => toQualifyApproval(p, '待审批')), total: people.total, pageNum, pageSize, } } const status = params?.status const action = status === '已通过' ? 'QUALIFY_PASS' : status === '已驳回' ? 'QUALIFY_REJECT' : 'QUALIFY_PASS,QUALIFY_REJECT' const logs = await post>>('/process-logs/page', { pageNum, pageSize, townId: townId || undefined, villageId: villageId || undefined, bizType: 'RETIRE_QUALIFY', action, }) const rows = logs?.list || [] const list: Approval[] = [] await Promise.all( rows.map(async (log) => { const pid = String(log.personnelId || '') if (!pid) return const raw = await get>(`/personnel/${encodeURIComponent(pid)}`).catch( () => null, ) if (!raw) return const p = mapPerson(raw) const row = toQualifyApproval( p, String(log.action) === 'QUALIFY_PASS' ? '已通过' : '已驳回', ) const mapped = mapProcessLog(log) if (mapped.operateTime) row.createdAt = mapped.operateTime list.push(row) }), ) return { list, total: Number(logs?.total || list.length), pageNum, pageSize } } const list: Approval[] = [] let extraTotal = 0 if (wantLeader && tab !== 'history') { const batchPage = await post>>('/batches/page', { pageNum: 1, pageSize: 20, townId: townId || undefined, batchLevel: 'TOWN', status: 'PENDING_LEADER_APPROVE', batchNo: params?.batchNo || undefined, lite: true, }) const specials = await collectSpecialApprovals({ townId, town: params?.town, approveStatuses: ['待分管领导审批'], pageNum, pageSize, }) if (pageNum === 1) { for (const b of batchPage?.list || []) list.push(toLeaderApproval(mapVillageBatch(b), '待审批')) } list.push(...specials.list) extraTotal = Number(batchPage?.total || 0) + specials.total } else if (wantLeader) { const status = params?.status const specials = await collectSpecialApprovals({ townId, town: params?.town, approveStatuses: status === '已通过' ? ['已通过'] : status === '已驳回' ? ['已驳回'] : ['已通过', '已驳回'], pageNum, pageSize, }) list.push(...specials.list) extraTotal = specials.total } return { list, total: extraTotal || list.length, pageNum, pageSize } } async function pageTownStatRows(opts: { townId?: string status?: string batchNo?: string }) { return pageAll>((pageNum, pageSize) => post>>('/batches/page', { pageNum, pageSize, townId: opts.townId || undefined, batchLevel: 'TOWN', status: opts.status, batchNo: opts.batchNo, lite: true, withTownTotals: true, }), ) } async function pageDistrictBatchRows(opts?: { status?: string; batchNo?: string }) { return pageAll>((pageNum, pageSize) => post>>('/batches/page', { pageNum, pageSize, batchLevel: 'DISTRICT', status: opts?.status, batchNo: opts?.batchNo, lite: true, withDistrictTotals: true, }), ) } async function collectSpecialApprovals(opts: { townId?: string town?: string approveStatuses: string[] pageNum?: number pageSize?: number }): Promise<{ list: Approval[]; total: number }> { const query: Record = { pageNum: opts.pageNum || 1, pageSize: opts.pageSize || 10, } if (opts.approveStatuses.length === 1) { query.approveStatus = opts.approveStatuses[0] } const specials = await get>>('/special-biz/page', query) const specialRows = specials?.list || [] const townIdsNeedVillage = new Set() if (opts.townId) townIdsNeedVillage.add(opts.townId) for (const s of specialRows) { if (s.townId) townIdsNeedVillage.add(String(s.townId)) } await Promise.all([...townIdsNeedVillage].map((tid) => ensureVillages(tid))) const peopleById = new Map>() const missingIds = [ ...new Set( specialRows .filter((s) => s.personnelId && (!s.villageId || !s.name || !s.idNumber)) .map((s) => String(s.personnelId)), ), ] await Promise.all( missingIds.map(async (id) => { const p = await get>(`/personnel/${encodeURIComponent(id)}`).catch( () => null, ) if (p) peopleById.set(id, p) }), ) const list: Approval[] = [] for (const s of specialRows) { const statusRaw = String(s.approveStatus || '') if (opts.approveStatuses.length && !opts.approveStatuses.includes(statusRaw)) continue let sTownId = String(s.townId || '') let sVillageId = String(s.villageId || '') let personName = String(s.name || '') let idCard = s.idNumber ? String(s.idNumber) : '' const personnelId = s.personnelId ? String(s.personnelId) : '' const person = personnelId ? peopleById.get(personnelId) : undefined if (person) { if (!sVillageId && person.villageId) sVillageId = String(person.villageId) if (!sTownId && person.townId) sTownId = String(person.townId) if (!personName && person.name) personName = String(person.name) if (!idCard && person.idNumber) idCard = String(person.idNumber) } else if (personnelId && (!sVillageId || !personName)) { try { const detail = await get>( `/personnel/${encodeURIComponent(personnelId)}`, ) if (detail) { if (!sVillageId && detail.villageId) sVillageId = String(detail.villageId) if (!sTownId && detail.townId) sTownId = String(detail.townId) if (!personName && detail.name) personName = String(detail.name) if (!idCard && detail.idNumber) idCard = String(detail.idNumber) } } catch { /* ignore */ } } if (sTownId) await ensureVillages(sTownId) if (opts.townId && sTownId && sTownId !== opts.townId) continue const townLabel = regionName(sTownId) || opts.town || '' const villageResolved = sVillageId ? regionName(sVillageId) : '' const villageLabel = villageResolved && !villageResolved.startsWith('V_') ? villageResolved : '' if (!opts.townId && opts.town && townLabel && townLabel !== opts.town) continue const deathDate = s.deathDate ? String(s.deathDate) : '' const reason = String(s.reason || '') const bizTypeRaw = String(s.bizType || '') const typeLabel = ({ FUNERAL: '丧葬补贴', STOP: '停止发放', PAUSE: '暂停发放', RESUME: '恢复发放' } as Record< string, string >)[bizTypeRaw] || mapSpecial(s).type const isDeathReg = Boolean(deathDate) || reason.includes('死亡登记') const status = statusRaw === '待分管领导审批' || statusRaw === '待审批' || !statusRaw ? '待审批' : statusRaw list.push({ id: approvalIdSpecial(String(s.id || '')), bizType: isDeathReg ? '死亡登记' : typeLabel, batchNo: s.batchNo ? String(s.batchNo) : '—', town: townLabel, village: villageLabel || '—', title: isDeathReg ? `${personName || ''}死亡登记(待改人员状态)` : `${personName || townLabel}${typeLabel}申请`, applicant: `${townLabel || ''}社区事务受理中心`, node: '分管领导线上审批', status, createdAt: fmtTimeSafe(s.createTime as number), peopleCount: personnelId ? 1 : undefined, totalAmount: s.amount != null ? Number(s.amount) : undefined, personName: personName || undefined, personnelId: personnelId || undefined, reason: reason || undefined, fileUrls: s.fileUrls ? String(s.fileUrls) : undefined, deathDate: deathDate || undefined, amount: s.amount != null ? Number(s.amount) : undefined, opinion: statusRaw === '待分管领导审批' || statusRaw === '待审批' || !statusRaw ? undefined : s.opinion ? String(s.opinion) : undefined, heirName: s.heirName ? String(s.heirName) : undefined, idCard: idCard || undefined, }) } return { list, total: Number(specials?.total || list.length) } } export async function reviewPersonQualify(payload: { personId: number | string retireQualifyType: '有土地退养' | '无土地有资格' | '无资格' }) { return reviewRetireQualify({ personnelId: personIdStr(payload.personId), pass: payload.retireQualifyType !== '无资格', retireQualifyType: payload.retireQualifyType, }) } export async function decideApprovals(payload: { ids: string[] pass: boolean opinion?: string fileUrls?: string retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格' }) { const qualifyIds: string[] = [] const others: string[] = [] for (const id of payload.ids || []) { const parsed = parseApprovalId(id) if (!parsed) continue if (parsed.kind === 'qualify') qualifyIds.push(parsed.bizId) else others.push(id) } if (qualifyIds.length) { const r = await reviewRetireQualifyBatch({ personnelIds: qualifyIds, pass: payload.pass, retireQualifyType: payload.retireQualifyType, opinion: payload.opinion, fileUrls: payload.fileUrls, }) if (!r.ok) { throw new Error(r.message || '批量资格审批失败') } } for (const id of others) { await decideApproval({ id, pass: payload.pass, opinion: payload.opinion, fileUrls: payload.fileUrls, retireQualifyType: payload.retireQualifyType, }) } return { ok: true } } export async function decideApproval(payload: { id: string pass: boolean opinion?: string fileUrls?: string retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格' }) { const parsed = parseApprovalId(payload.id) if (!parsed) return { ok: false } if (parsed.kind === 'qualify') { await reviewRetireQualify({ personnelId: parsed.bizId, pass: payload.pass, retireQualifyType: payload.retireQualifyType, opinion: payload.opinion, fileUrls: payload.fileUrls, }) return { ok: true } } const body = { bizId: parsed.bizId, action: payload.pass ? 'PASS' : 'REJECT', opinion: payload.opinion || (payload.pass ? '同意' : '退回修改'), operator: operatorOf(), fileUrls: payload.fileUrls || undefined, } if (parsed.kind === 'batch') { await post('/batches/leader-approve', body) } else { await post('/special-biz/leader-approve', body) } clearGetCache() return { ok: true } } /** —— 状态维护 / 标准 —— */ export async function fetchStatusBatches(): Promise { const all = await fetchVillageBatches() return all.filter((b) => ['分管领导审批', '待线下发放', '回盘信息确认'].includes(b.stage), ) } const standardIdMap = new Map() export async function fetchStandards(town?: string): Promise { await ensureTowns() const townId = town ? townIdOf(town) : undefined // 后端返回 PageResult,不是裸数组 const page = await get>>('/amount-standards', { townId: townId || undefined, pageNum: 1, pageSize: 200, }) const rows = Array.isArray(page) ? page : page?.list || [] const list = rows.map((r) => { const m = mapStandard(r) standardIdMap.set(m.id, String(r.id)) return m }) return list.sort((a, b) => (b.year !== a.year ? b.year - a.year : b.date.localeCompare(a.date))) } /** 本镇当前有效丧葬补贴标准(元);优先 /amount-standards/current,失败则回退标准列表 */ export async function fetchCurrentFuneralAmount(town: string): Promise { if (!town) return null await ensureTowns() const townId = townIdOf(town) if (!townId) return null try { const raw = await get>( '/amount-standards/current', { townId, standardType: 'FUNERAL' }, { silent: true }, ) const amt = Number(raw?.funeralAmount ?? raw?.amount ?? 0) if (Number.isFinite(amt) && amt > 0) return amt } catch { /* 回退列表 */ } try { const list = await fetchStandards(town) const cur = list.find((s) => s.current && s.funeral > 0) || list.find((s) => s.funeral > 0) return cur && cur.funeral > 0 ? cur.funeral : null } catch { return null } } function dateToEpochMs(date: string): number { const s = String(date || '').trim() if (/^\d{13}$/.test(s)) return Number(s) if (/^\d{10}$/.test(s)) return Number(s) * 1000 const m = s.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/) if (m) { const t = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime() if (!Number.isNaN(t)) return t } const n = Number(s) if (Number.isFinite(n) && n > 1_000_000_000) return n < 20_000_000_000 ? n * 1000 : n throw new Error('生效日期格式无效') } export async function createStandard(payload: { town: string year: number monthly: number funeral: number date: string reason: string }) { const townId = townIdOf(payload.town) if (!townId) return { ok: false as const, message: '未识别街镇' } let effectiveTime: number try { effectiveTime = dateToEpochMs(payload.date) } catch { return { ok: false as const, message: '请选择有效的生效日期' } } // 新增标准只落库,不走 yearly-adjust(那会生成调标补发批) const saved = await post>('/amount-standards', { townId, standardType: 'YEARLY_ADJUST', amount: payload.monthly, funeralAmount: payload.funeral, effectiveTime, adjustReason: payload.reason, operator: operatorOf(), allowDirect: getAppConfig().currentRole === 'admin', }) const pendingTip = String(saved?.remarks || '') if (pendingTip.includes('待分管领导审批')) { return { ok: true as const, pendingTip: '标准变更已提交,待分管领导审批后生效' } } return { ok: true as const } } export async function updateStandard(payload: { id: number monthly: number funeral: number date: string reason: string rawId?: string town?: string }) { const realId = payload.rawId || standardIdMap.get(payload.id) let effectiveTime: number try { effectiveTime = dateToEpochMs(payload.date) } catch { return { ok: false as const, message: '请选择有效的生效日期' } } const townId = payload.town ? townIdOf(payload.town) : '' if (!townId) return { ok: false as const, message: '未识别街镇' } const saved = await post>('/amount-standards', { id: realId, townId, standardType: 'YEARLY_ADJUST', amount: payload.monthly, funeralAmount: payload.funeral, effectiveTime, adjustReason: payload.reason, operator: operatorOf(), allowDirect: getAppConfig().currentRole === 'admin', }) const pendingTip = String(saved?.remarks || '') if (pendingTip.includes('待分管领导审批')) { return { ok: true as const, pendingTip: '标准变更已提交,待分管领导审批后生效' } } return { ok: true as const } } /** 管理员删除补助标准(软删) */ export async function deleteStandard(payload: { id: number; rawId?: string }) { const realId = payload.rawId || standardIdMap.get(payload.id) if (!realId) return { ok: false as const, message: '未找到标准ID' } const res = await del<{ ok?: boolean; message?: string; promotedId?: string }>( `/amount-standards/${encodeURIComponent(realId)}`, ) return { ok: true as const, message: res?.message, promotedId: res?.promotedId, } } export async function updateDeathStatus(deathId: string) { // deathId 可能是人员 ID 或告警 ID;死亡闭环走特殊业务审批,此处仅兼容旧入口 try { await put('/personnel/status', { personnelId: deathId, bizStatus: 'STOPPED', payThisMonth: false, remarks: '已完成死亡状态更新,停止发放', allowDirect: true, operator: operatorOf(), }) } catch { /* ignore */ } return { ok: true } } export async function applyPersonStatusAction(payload: { personId: number | string action: string amount?: number reason?: string fileUrls?: string }) { const personnelId = personIdStr(payload.personId) const map: Record = { 停止: 'STOP', '暂停/停止': 'STOP', 暂停: 'PAUSE', 暂停发放: 'PAUSE', 停止发放: 'STOP', 恢复: 'RESUME', 恢复发放: 'RESUME', 丧葬: 'FUNERAL', 死亡状态更新: 'FUNERAL', 补发: 'SUPPLEMENT', 调标补发: 'STANDARD_ADJUST', } const bizType = map[payload.action] // 暂停/停止/恢复一律走特殊业务 → 分管领导审批,禁止直改状态 if (bizType === 'STOP' || bizType === 'PAUSE' || bizType === 'RESUME' || bizType === 'FUNERAL' || bizType === 'SUPPLEMENT') { await post('/special-biz', { personnelId, bizType, reason: payload.reason || payload.action, amount: payload.amount, fileUrls: payload.fileUrls, operator: operatorOf(), }) } return { ok: true } } /** —— 特殊业务 —— */ export async function fetchSpecialsPage(params?: { town?: string bizType?: string approveStatus?: string pageNum?: number pageSize?: number }): Promise<{ list: SpecialBiz[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const { townId } = await resolveScopeIds(params?.town, undefined) const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const res = await get>>('/special-biz/page', { pageNum, pageSize, townId: townId || undefined, bizType: params?.bizType || undefined, approveStatus: params?.approveStatus || undefined, }) return { list: (res?.list || []).map(mapSpecial), total: Number(res?.total || 0), pageNum, pageSize, } } export async function fetchSpecials(params?: { town?: string }): Promise { const page = await fetchSpecialsPage({ ...params, pageNum: 1, pageSize: 10 }) return page.list } /** 审批详情:按人员 ID 拉底数(特殊业务单关联 1 人) */ export async function fetchSpecialBizPerson(personnelId: string): Promise { if (!personnelId) return null await ensureTowns() try { const detail = await get>( `/personnel/${encodeURIComponent(personnelId)}`, ) if (!detail) return null return mapPerson(detail) } catch { return null } } export async function fetchDeaths(params?: { town?: string pageNum?: number pageSize?: number }): Promise { await ensureTowns() const { townId } = await resolveScopeIds(params?.town, undefined) const townName = params?.town || (townId ? regionName(townId) : '') if (townId) await ensureVillages(townId) // 1) 死亡登记/丧葬特殊业务单(新流程) const specialRes = await get>>('/special-biz/page', { bizType: 'FUNERAL', pageNum: params?.pageNum || 1, pageSize: params?.pageSize || 10, townId: townId || undefined, }) // 2) 人员底数已是 FUNERAL 的历史记录(旧直改流程)——只取当前页,禁止循环拉全量 const funeralPeopleRes = await post>>('/personnel/page', { pageNum: params?.pageNum || 1, pageSize: params?.pageSize || 10, townId: townId || undefined, bizStatus: 'FUNERAL', }) const funeralPeople = funeralPeopleRes?.list || [] const byKey = new Map() const preferPut = (key: string, rec: DeathRecord, preferSpecial: boolean) => { const prev = byKey.get(key) if (!prev) { byKey.set(key, rec) return } const prevSpecial = !String(prev.id).startsWith('P:') if (preferSpecial && !prevSpecial) { byKey.set(key, rec) return } if (prevSpecial === preferSpecial) { if (String(rec.createdAt || '') > String(prev.createdAt || '')) byKey.set(key, rec) } } for (const s of specialRes?.list || []) { const tid = String(s.townId || '') // 有镇 ID 且不匹配则跳过;镇 ID 缺失时先保留,后面用名称再滤 if (townId && tid && tid !== townId) continue const reason = String(s.reason || '') const deathDate = String(s.deathDate || '') const isDeathReg = Boolean(deathDate) || reason.includes('死亡登记') || reason.includes('死亡日期') const isFuneralOnly = !isDeathReg && (reason.includes('申请丧葬补贴') || reason.includes('丧葬补贴')) // 纯丧葬补贴申请归「丧葬补贴」页;死亡登记/无法区分的历史单留在本页 if (isFuneralOnly) continue let town = regionName(tid) || townName let village = regionName(String(s.villageId || '')) if (townName && town && town !== townName && !tid) continue if (townName && !town) town = townName const personnelId = s.personnelId ? String(s.personnelId) : '' const approveStatus = String(s.approveStatus || '待分管领导审批') // 列表状态:驳回/待审原样;通过后展示「死亡丧葬」(与人员落地态一致) let listStatus = approveStatus if (approveStatus === '已通过' || approveStatus.includes('通过')) { listStatus = '死亡丧葬' } else if (approveStatus.includes('驳回')) { listStatus = '已驳回' } const rec: DeathRecord = { id: String(s.id || ''), personnelId: personnelId || undefined, personName: String(s.name || ''), idCard: String(s.idNumber || ''), town, village, deathDate, heirName: s.heirName ? String(s.heirName) : '', heirIdCard: s.heirIdNumber ? String(s.heirIdNumber) : '', heirBankCard: s.heirBankCard ? String(s.heirBankCard) : '', heirContact: s.heirPhone ? String(s.heirPhone) : '', source: '社区事务受理中心手工录入', status: listStatus, createdAt: fmtTimeSafe(s.createTime as number), opinion: s.opinion ? String(s.opinion) : undefined, reason: reason || undefined, fileUrls: s.fileUrls ? String(s.fileUrls) : undefined, } preferPut(personnelId || rec.id, rec, true) } for (const p of funeralPeople) { const pid = String(p.id || '') if (!pid) continue // 已有特殊业务死亡登记单时,不再用底数 FUNERAL 覆盖(避免驳回后仍显示「死亡丧葬」) if (byKey.has(pid)) continue if (townId && p.townId && String(p.townId) !== townId) continue const town = regionName(String(p.townId || '')) || townName if (townName && town && town !== townName) continue const deathDate = String(p.deathDate || '') const remarks = String(p.remarks || '') const rec: DeathRecord = { id: `P:${pid}`, personnelId: pid, personName: String(p.name || ''), idCard: String(p.idNumber || ''), town: town || townName, village: regionName(String(p.villageId || '')), deathDate, heirName: p.heirName ? String(p.heirName) : '', heirIdCard: p.heirIdNumber ? String(p.heirIdNumber) : '', heirBankCard: p.heirBankCard ? String(p.heirBankCard) : '', heirContact: p.heirPhone ? String(p.heirPhone) : '', source: '历史死亡状态', status: '死亡丧葬(历史)', createdAt: deathDate ? `${deathDate} 00:00:00` : fmtTimeSafe((p.updateTime as number) || (p.createTime as number)), reason: remarks || '人员状态已为死亡丧葬(历史直改,无审批留痕)', fileUrls: undefined, } preferPut(pid, rec, false) } const list = [...byKey.values()].sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')), ) ;(list as DeathRecord[] & { __total?: number }).__total = Number( specialRes?.total || funeralPeopleRes?.total || list.length, ) return list } export async function fetchDeathsPage(params?: { town?: string pageNum?: number pageSize?: number }): Promise<{ list: DeathRecord[]; total: number; pageNum: number; pageSize: number }> { const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const list = await fetchDeaths({ ...params, pageNum, pageSize }) const total = Number((list as DeathRecord[] & { __total?: number }).__total || list.length) return { list, total, pageNum, pageSize } } function fmtTimeSafe(ts?: number | string | null): string { if (ts == null || ts === '') return '' const n = Number(ts) if (!Number.isFinite(n) || n <= 0) return String(ts) const d = new Date(n) if (Number.isNaN(d.getTime())) return '' const p = (x: number) => String(x).padStart(2, '0') return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` } /** 上传死亡证明等附件,返回相对 url */ export async function uploadBizFile(file: File): Promise<{ url: string; name: string }> { const form = new FormData() form.append('file', file) const data = await post<{ url?: string; name?: string }>( '/files/upload', form as unknown as Record, ) const url = String(data?.url || '') if (!url) throw new Error('附件上传失败') return { url, name: String(data?.name || file.name) } } export async function createDeathRecord(payload: { personId?: string | number personName: string idCard: string town: string village: string deathDate: string heirName?: string heirRelation?: string heirIdCard?: string heirBankCard?: string heirContact?: string fileUrls?: string }) { let personnelId = payload.personId ? personIdStr(payload.personId) : '' const { townId } = await resolveScopeIds(payload.town, payload.village) if (!personnelId) { const found = await post>>('/personnel/page', { pageNum: 1, pageSize: 5, idNumber: payload.idCard, townId: townId || undefined, }) personnelId = found?.list?.[0]?.id ? String(found.list[0].id) : '' } if (!personnelId) { return { ok: false as const, message: '未找到本镇在册人员,请从列表中选择' } } const detail = await get>(`/personnel/${encodeURIComponent(personnelId)}`) const st = String(detail?.bizStatus || '') if (st === 'FUNERAL' || st === 'STOPPED') { return { ok: false as const, message: '该人员已是死亡/停止状态,不可重复录入' } } // 提交特殊业务 FUNERAL → 待分管领导审批;通过后才改人员状态 try { const reasonParts = [`死亡登记;死亡日期 ${payload.deathDate}`] if (payload.heirRelation) reasonParts.push(`与逝者关系:${payload.heirRelation}`) // 先落继承人信息(含关系),审批通过后再改死亡状态 // 必须带回逝者银行卡:后端编辑时 null 会触发 putKeep 保留,但缺字段时部分路径仍可能清空 if (detail?.id) { const ut = Number(detail.updateTime) await post('/personnel', { id: personnelId, name: detail.name || payload.personName, idNumber: detail.idNumber || payload.idCard, townId: detail.townId || townId, villageId: detail.villageId, bankCardNumber: detail.bankCardNumber || undefined, bankName: detail.bankName || undefined, bankType: detail.bankType || undefined, bankBranchCode: detail.bankBranchCode || undefined, heirName: payload.heirName || undefined, heirRelation: payload.heirRelation || undefined, heirIdNumber: payload.heirIdCard || undefined, heirBankCard: payload.heirBankCard || undefined, heirPhone: payload.heirContact || undefined, source: '死亡登记', expectedUpdateTime: Number.isFinite(ut) && ut > 0 ? ut : undefined, }).catch(() => undefined) } const created = await post>('/special-biz', { personnelId, bizType: 'FUNERAL', deathDate: payload.deathDate, reason: reasonParts.join(';'), heirName: payload.heirName, heirIdNumber: payload.heirIdCard, heirBankCard: payload.heirBankCard, heirPhone: payload.heirContact, fileUrls: payload.fileUrls, townId: detail?.townId || townId, operator: operatorOf(), }) return { ok: true as const, id: String(created?.id || '') } } catch (e: unknown) { const msg = e instanceof Error ? e.message : '提交失败' return { ok: false as const, message: msg } } } export async function createSpecialBiz(payload: { type: string personId?: string | number personName: string town: string village: string reason: string amount: number heirName?: string fileUrls?: string /** 多发退款方案:DEDUCT_NEXT | RETURN_FISCAL */ refundMethod?: 'DEDUCT_NEXT' | 'RETURN_FISCAL' }) { const { townId, villageId } = await resolveScopeIds(payload.town, payload.village) const bizType = specialTypeToCode(payload.type) let personnelId = payload.personId ? personIdStr(payload.personId) : '' if (bizType !== 'STANDARD_ADJUST' && !personnelId) { const people = await post>>('/personnel/page', { pageNum: 1, pageSize: 20, name: payload.personName, townId: townId || undefined, villageId: villageId || undefined, }) personnelId = people?.list?.[0]?.id ? String(people.list[0].id) : '' if (!personnelId) throw new Error(`未找到本镇人员:${payload.personName}`) } if (bizType !== 'STANDARD_ADJUST' && !personnelId) { throw new Error('请选择本镇在册人员') } // 校验所选人确属本镇 if (personnelId && townId) { const detail = await get>( `/personnel/${encodeURIComponent(personnelId)}`, ) if (detail?.townId && String(detail.townId) !== townId) { throw new Error('只能为本镇在册人员发起特殊业务') } } const created = await post>('/special-biz', { personnelId: personnelId || undefined, bizType, reason: payload.reason, amount: payload.amount, townId: townId || undefined, heirName: payload.heirName, fileUrls: payload.fileUrls, refundMethod: bizType === 'REFUND' ? payload.refundMethod || 'DEDUCT_NEXT' : undefined, operator: operatorOf(), operatorRoleKey: getAppConfig().currentRole, }) return { ok: true, id: String(created?.id || '') } } export async function exportSpecialSummary(_type?: string) { return { ok: false as const, fileName: '', message: '特殊业务汇总导出未对接后端接口' } } export async function triggerAdjustReissue(town?: string) { const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const townName = town || role?.town if (!townName) { return { ok: false, message: '请指定街镇', batchNo: currentPayMonth(), count: 0, villageCount: 0 } } const { townId } = await resolveScopeIds(townName) if (!townId) return { ok: false, message: '未识别街镇', batchNo: currentPayMonth(), count: 0, villageCount: 0 } const created = await post>('/special-biz', { bizType: 'STANDARD_ADJUST', townId, reason: '年度调标差额补发', operator: operatorOf(), }) const related = String(created?.relatedBatchIds || '') const villageCount = related ? related.split(/[,,]/).filter(Boolean).length : 0 return { ok: true, batchNo: String(created?.batchNo || currentPayMonth()), villageCount, count: Number(created?.amount ? 0 : 0) || villageCount, message: undefined as string | undefined, } } /** 从备注解析旧/新月标(批次或明细备注:旧月标1280|新月标2113) */ function parseOldNewMonthly(text?: string): { oldMonthly: number; newMonthly: number } | null { const s = String(text || '') const oldM = s.match(/旧月标\s*([0-9]+(?:\.[0-9]+)?)/) const newM = s.match(/新月标\s*([0-9]+(?:\.[0-9]+)?)/) if (!oldM || !newM) return null const oldMonthly = Number(oldM[1]) const newMonthly = Number(newM[1]) if (!(newMonthly > 0) || !(oldMonthly >= 0)) return null return { oldMonthly, newMonthly } } /** 从明细备注解析应发月数与单月差额 */ function parseAdjustMonthsDiff(text?: string): { months: number; diff: number } | null { const s = String(text || '') const m = s.match(/应发\s*(\d+)\s*月\s*[×xX*]\s*差额\s*([0-9]+(?:\.[0-9]+)?)/) if (!m) return null return { months: Number(m[1]), diff: Number(m[2]) } } /** 从金额标准表取当前新标 + 上一档旧标 */ async function loadTownAdjustStandards(townId: string): Promise<{ oldMonthly: number newMonthly: number } | null> { try { const page = await get>>('/amount-standards', { townId, pageNum: 1, pageSize: 100, }) const list = (page?.list || []).filter((r) => { const t = String(r.standardType || '') return (t === 'YEARLY_ADJUST' || t === 'MONTHLY') && Number(r.amount || 0) > 0 }) if (!list.length) return null list.sort((a, b) => Number(b.effectiveTime || 0) - Number(a.effectiveTime || 0)) const cur = list.find((r) => r.current === true || r.current === 'true') || list[0] const curAmt = Number(cur.amount || 0) const curEff = Number(cur.effectiveTime || 0) const prev = list.find((r) => { if (String(r.id) === String(cur.id)) return false const amt = Number(r.amount || 0) if (!(amt > 0)) return false const eff = Number(r.effectiveTime || 0) return !curEff || !eff || eff < curEff }) if (!prev) return null const oldMonthly = Number(prev.amount || 0) if (!(curAmt > oldMonthly)) return null return { oldMonthly, newMonthly: curAmt } } catch { return null } } export async function fetchAdjustReissueRoster(params?: { town?: string batchNo?: string village?: string }): Promise { await ensureTowns() const { townId, villageId } = await resolveScopeIds(params?.town, params?.village) const townStandards = townId ? await loadTownAdjustStandards(townId) : null // 调标村批一般为 YYYY99 或备注含调标;按发放明细补发额组装(单页,禁止循环拉全量) const batchRes = await post>>('/batches/page', { pageNum: 1, pageSize: 20, batchLevel: 'DISTRICT', batchNo: params?.batchNo, lite: true, }) const batches = batchRes?.list || [] const adjustBatches = batches.filter((b) => { const no = String(b.batchNo || b.payMonth || '') const remarks = String(b.remarks || '') const id = String(b.id || '') return ( no.endsWith('99') || id.includes('99') || remarks.includes('YEARLY_ADJUST') || remarks.includes('调标') || Number(b.supplementAmount || 0) > 0 ) }) const roster: AdjustReissuePerson[] = [] for (const b of adjustBatches) { if (params?.village && regionName(String(b.villageId || '')) !== params.village) continue const batchBasis = parseOldNewMonthly(String(b.remarks || '')) || townStandards const detailsRes = await post>>('/payments/details/page', { pageNum: 1, pageSize: 10, batchId: b.id, townId: townId || undefined, villageId: villageId || undefined, }) const details = detailsRes?.list || [] for (const d of details) { const amt = Number(d.supplementAmount ?? d.totalAmount ?? 0) if (!amt) continue const detailBasis = Number(d.oldMonthlyAmount) > 0 && Number(d.newMonthlyAmount) > 0 ? { oldMonthly: Number(d.oldMonthlyAmount), newMonthly: Number(d.newMonthlyAmount) } : parseOldNewMonthly(String(d.remarks || '')) const monthsDiff = Number(d.adjustMonths) > 0 ? { months: Number(d.adjustMonths), diff: Number(d.supplementAmount || 0) / Number(d.adjustMonths) } : parseAdjustMonthsDiff(String(d.remarks || '')) let newStandard = 0 let oldStandard = 0 if (detailBasis) { oldStandard = detailBasis.oldMonthly newStandard = detailBasis.newMonthly } else if (batchBasis) { oldStandard = batchBasis.oldMonthly newStandard = batchBasis.newMonthly } else if (monthsDiff && monthsDiff.diff > 0) { // 仅有差额时:明细月标若为新标则反推旧标;否则用差额反推 const monthly = Number(d.monthlyAmount || 0) if (monthly > monthsDiff.diff) { newStandard = monthly oldStandard = Math.max(0, monthly - monthsDiff.diff) } else { newStandard = monthsDiff.diff oldStandard = 0 } } else { const monthly = Number(d.monthlyAmount || 0) if (monthly > 0) { // 历史脏数据曾把月标写成 0;若已有新标则用差额反推 newStandard = monthly const months = monthsDiff?.months || 0 const unitDiff = months > 0 ? amt / months : 0 oldStandard = Math.max(0, monthly - (unitDiff || 0)) } } // 仍缺新标时:用「补发额 / 月数 + 旧标」补全 if (newStandard <= 0 && oldStandard > 0 && monthsDiff?.months) { newStandard = oldStandard + amt / monthsDiff.months } if (oldStandard <= 0 && newStandard > 0 && monthsDiff?.months) { oldStandard = Math.max(0, newStandard - amt / monthsDiff.months) } if (newStandard <= 0 && monthsDiff?.diff) { newStandard = (oldStandard || 0) + monthsDiff.diff } if (oldStandard <= 0 && newStandard > 0 && monthsDiff?.diff) { oldStandard = Math.max(0, newStandard - monthsDiff.diff) } roster.push({ id: String(d.id || ''), batchNo: String(b.batchNo || b.payMonth || params?.batchNo || ''), personId: String(d.personnelId || '') as unknown as number, name: String(d.name || ''), idCard: String(d.idNumber || ''), town: regionName(String(d.townId || b.townId || '')), village: regionName(String(d.villageId || b.villageId || '')), oldStandard: Number(oldStandard.toFixed(2)), newStandard: Number(newStandard.toFixed(2)), amount: amt, status: '待提交', remark: '年度调标差额补发', }) } } return roster } /** —— 月补贴 —— */ export async function fetchVillageMonthlyBatchesPage(params?: { town?: string pageNum?: number pageSize?: number /** 默认 true;列表首屏可传 false 先出表,再异步补全镇人数/金额 */ withTownTotals?: boolean }): Promise<{ list: VillageMonthlyBatch[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const { townId } = await resolveScopeIds(params?.town || role?.town) if (townId) await ensureVillages(townId) const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const withTownTotals = params?.withTownTotals !== false const res = await post>>('/batches/page', { pageNum, pageSize, townId: townId || undefined, batchLevel: 'TOWN', statusIn: 'PENDING_STATUS_MAINTAIN,PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED', lite: true, withTownTotals, }) const list = (res?.list || []).map(mapVillageMonthly) return { list, total: Number(res?.total || list.length), pageNum, pageSize } } export async function fetchVillageMonthlyBatches(town?: string): Promise { const page = await fetchVillageMonthlyBatchesPage({ town, pageNum: 1, pageSize: 10 }) return page.list } export async function fetchMonthlyBatches(): Promise { const villages = await fetchVillageMonthlyBatches() const map = new Map() for (const v of villages) { const key = `${v.batchNo}|${v.town}` const cur = map.get(key) if (!cur) { map.set(key, { id: `MB${v.batchNo}${townIdOf(v.town)}`, batchNo: v.batchNo, month: v.month, town: v.town, peopleCount: v.peopleCount, normalAmount: v.normalAmount, extraAmount: v.extraAmount, deductAmount: v.deductAmount, totalAmount: v.totalAmount, status: v.status, }) } else { cur.peopleCount += v.peopleCount cur.normalAmount += v.normalAmount cur.extraAmount += v.extraAmount cur.deductAmount += v.deductAmount cur.totalAmount += v.totalAmount } } return [...map.values()] } export async function calculateMonthly() { await generateDistrictMonthBatch(currentPayMonth(), true).catch(() => undefined) return { ok: true } } export async function confirmMonthly(id: string) { await post('/payments/export-disk', { batchId: id, bypassDeadline: true }).catch(() => undefined) return { ok: true } } /** 导出居保匹配结果 xlsx(按 batchId 或 townId) */ export async function exportInsuranceMatch(params: { batchId?: string town?: string townId?: string }) { await ensureTowns() let townId = params.townId || '' if (!townId && params.town) { const scope = await resolveScopeIds(params.town, undefined) townId = scope.townId || '' } if (!params.batchId && !townId) { return { ok: false as const, fileName: '', message: '请指定批次或街镇' } } try { const res = await service.get('/insurance-match/export-xlsx', { params: { batchId: params.batchId || undefined, townId: townId || undefined, }, responseType: 'blob', timeout: 120_000, }) const blob = res.data as Blob if (blob && blob.type && blob.type.includes('application/json')) { const text = await blob.text() let msg = '导出失败' try { const j = JSON.parse(text) as { message?: string; msg?: string } msg = j.message || j.msg || msg } catch { /* ignore */ } return { ok: false as const, fileName: '', message: msg } } let fileName = '居保匹配结果.xlsx' const cd = String(res.headers?.['content-disposition'] || '') const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd) if (m) { fileName = decodeURIComponent((m[1] || m[2] || '').trim()) } const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) return { ok: true as const, fileName, message: '' } } catch (e) { return { ok: false as const, fileName: '', message: e instanceof Error ? e.message : '导出失败', } } } export async function exportMonthlyFile(id?: string) { if (!id) { return { ok: false as const, message: '缺少批次ID', fileName: '' } } try { const res = await service.get(`/payments/batches/${encodeURIComponent(id)}/export-xlsx`, { responseType: 'blob', timeout: 120_000, }) const blob = res.data as Blob if (blob && blob.type && blob.type.includes('application/json')) { const text = await blob.text() let msg = '导出失败' try { const j = JSON.parse(text) as { message?: string; msg?: string } msg = j.message || j.msg || msg } catch { /* ignore */ } return { ok: false as const, fileName: '', message: msg } } let fileName = `月补贴发放清单_${currentPayMonth()}.xlsx` const cd = String(res.headers?.['content-disposition'] || '') const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd) if (m) { fileName = decodeURIComponent((m[1] || m[2] || '').trim()) } const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) return { ok: true as const, fileName, message: '' } } catch (e) { return { ok: false as const, fileName: '', message: e instanceof Error ? e.message : '导出失败', } } } export async function issueVillageMonthlyDisk(batchId: string) { const res = await post>('/payments/export-disk', { batchId, bypassDeadline: true, }) return { ok: true as const, fileName: String(res?.fileName || res?.exportDiskNo || `${currentPayMonth()}-月补贴发放文件`), fileUrl: res?.fileUrl ? String(res.fileUrl) : undefined, message: String(res?.offlineTip || ''), } } /** 下载后端存储的相对路径文件(出盘/附件等;DMS /static/ 经 resolve 代理) */ export async function downloadStoredFile(fileUrl: string, fileName?: string) { if (!fileUrl) throw new Error('缺少文件地址') const { resolveBizFileHref } = await import('@/utils/bizFiles') let href = fileUrl.trim() if (!href.startsWith('http')) { href = resolveBizFileHref(href) if (href.includes('/files/resolve') && !href.includes('download=')) { href += (href.includes('?') ? '&' : '?') + 'download=true' } } // axios baseURL=/api:去掉前缀避免 /api/api/... const reqPath = href.startsWith('http') ? href : href.startsWith('/api/') ? href.slice('/api'.length) : href const res = await service.get(reqPath, { baseURL: href.startsWith('http') ? '' : undefined, responseType: 'blob', timeout: 120_000, }) const blob = res.data as Blob const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName || 'download' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } /** 快照测算轮次 */ export async function snapshotCalcRound(batchId: string) { const res = await post>( `/payments/batches/${encodeURIComponent(batchId)}/calc-rounds`, undefined, { params: { operator: operatorOf() } }, ) return { ok: true as const, ...(res || {}) } } /** 查询测算轮次列表 */ export async function listCalcRounds(batchId: string) { const list = await get[]>( `/payments/batches/${encodeURIComponent(batchId)}/calc-rounds`, ) return Array.isArray(list) ? list : [] } /** 锁定/解锁测算 */ export async function lockCalc(batchId: string, locked: boolean) { await post( `/payments/batches/${encodeURIComponent(batchId)}/calc-lock`, undefined, { params: { locked, operator: operatorOf() } }, ) return { ok: true as const, locked } } /** 推进退款进度 */ export async function advanceRefundProgress(id: string, stage: string) { await post(`/special-biz/${encodeURIComponent(id)}/refund-progress`, { stage }) return { ok: true as const } } /** 退款进度统计 */ export async function fetchRefundStats() { const res = await get>('/special-biz/refund-stats') return res || { total: 0, byStage: {} as Record } } export type ReportTopicType = 'pay-abnormal' | 'personnel-change' | 'special-biz' /** 专题报表 topics(分页) */ export async function fetchReportTopicPage( type: ReportTopicType, params?: { townId?: string payMonth?: string bizType?: string pageNum?: number pageSize?: number }, ): Promise<{ list: Record[]; total: number }> { const path = type === 'pay-abnormal' ? '/reports/topics/pay-abnormal' : type === 'personnel-change' ? '/reports/topics/personnel-change' : '/reports/topics/special-biz' const res = await get>>(path, { townId: params?.townId || undefined, payMonth: params?.payMonth || undefined, bizType: params?.bizType || undefined, pageNum: params?.pageNum || 1, pageSize: params?.pageSize || 20, }) return { list: Array.isArray(res?.list) ? res.list : [], total: Number(res?.total || 0), } } /** @deprecated use fetchReportTopicPage */ export async function fetchReportTopic( type: ReportTopicType, params?: { townId?: string; payMonth?: string; bizType?: string }, ) { const page = await fetchReportTopicPage(type, { ...params, pageNum: 1, pageSize: 20 }) return page.list } /** 报表页首屏:summary + region-stats,避免拉完整 workbench 扫全库人员 */ export async function fetchReportPageBootstrap(params?: { payMonth?: string town?: string village?: string }): Promise<{ personnelCount: number townQualify: WorkbenchApi['townQualify'] villageCharts: WorkbenchApi['villageCharts'] } | null> { await ensureTowns() const cfg = getAppConfig() const role = cfg.roles.find((r) => r.key === cfg.currentRole) const { townId, villageId } = await resolveScopeIds( params?.town || role?.town, params?.village || role?.village, ) const payMonth = params?.payMonth || currentPayMonth() const [summary, region] = await Promise.all([ get('/dashboard/summary', { payMonth, townId: townId || undefined, }).catch(() => null), get('/dashboard/region-stats', { payMonth, townId: townId || undefined, villageId: villageId || undefined, months: 1, }).catch(() => null), ]) if (!summary && !region) return null const townQualify = (summary?.townStats || []).map((s) => ({ townId: String(s.townId || ''), peopleCount: Number(s.shouldPayCount || 0), villageCount: 0, withLandCount: 0, withoutLandCount: 0, qualifiedCount: 0, amount: s.shouldPayAmount, })) let villageCharts: WorkbenchApi['villageCharts'] if (townId && !villageId) { const charts = await buildTownVillageChartsFromApi(townId, payMonth).catch(() => null) if (charts) { villageCharts = charts.people.map((p, idx) => ({ villageId: p.name, peopleCount: p.value, amount: charts.amount[idx]?.value || 0, })) } } return { personnelCount: Number(region?.personnelCount || summary?.totalPeople || 0), townQualify, villageCharts, } } /** 月度汇总导出 csv */ export async function exportReportSummary(payMonth?: string) { const res = await service.get('/reports/export/summary.xlsx', { params: { payMonth: payMonth || undefined }, responseType: 'blob', timeout: 120_000, }) const blob = res.data as Blob if (blob.size <= 80) { throw new Error('所选月份暂无批次/发放明细,无法导出月度汇总') } let fileName = `monthly-summary-${payMonth || 'all'}.csv` const cd = String(res.headers?.['content-disposition'] || '') const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd) if (m) fileName = decodeURIComponent((m[1] || m[2] || '').trim()) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) return { ok: true as const, fileName } } /** 同比(人数/金额 vs 上月) */ export async function fetchYoy(payMonth?: string) { return get>('/reports/yoy', { payMonth: payMonth || currentPayMonth(), }) } function parseSysUserRemarks(remarks?: string) { const out: Record = {} const s = String(remarks || '') if (!s.startsWith('SYS_USER|')) return out const parts = s.split('|') for (let i = 1; i + 1 < parts.length; i += 2) { out[parts[i]] = parts[i + 1] } return out } function mapAdminUser(raw: Record, idx: number): AccountItem { const parsed = parseSysUserRemarks(String(raw.remarks || '')) const username = parsed.username || String(raw.username || raw.content || '') .replace(/^SYS_USER\|/, '') .trim() const status = String(raw.status || raw.result || '启用') const opTime = raw.operateTime let lastLogin = '' if (typeof opTime === 'number' && opTime > 0) { lastLogin = fmtTimeSafe(opTime) } return { id: String(raw.id || idx + 1), username, name: parsed.displayName || username, roleKey: parsed.role || 'OPERATOR', org: parsed.org || '—', status: status.includes('禁') ? '禁用' : '启用', lastLogin, } } /** 系统用户列表 */ export async function fetchAccounts(): Promise { const list = await get[]>('/admin/users') if (!Array.isArray(list)) return [] return list.map((r, idx) => mapAdminUser(r, idx)) } export async function saveAccount(payload: { username: string password?: string displayName?: string role?: string }) { const res = await post>('/admin/users', { username: payload.username, password: payload.password || '123456', displayName: payload.displayName || payload.username, role: payload.role || 'OPERATOR', }) return { ok: true as const, id: String(res?.id || '') } } export async function setAccountStatus(id: string, enabled: boolean) { await post(`/admin/users/${encodeURIComponent(id)}/status`, undefined, { params: { enabled }, }) return { ok: true as const } } export async function resetAccountPassword(id: string, password?: string) { await post(`/admin/users/${encodeURIComponent(id)}/reset-password`, { password: password || '123456', }) return { ok: true as const } } /** 角色切换/登录审计写入 */ export async function recordLoginAudit(payload: { username?: string roleKey?: string roleLabel?: string ip?: string result?: string }) { await post('/admin/login-logs', { username: payload.username || operatorOf(), roleKey: payload.roleKey || getAppConfig().currentRole, roleLabel: payload.roleLabel || operatorOf(), ip: payload.ip || '', result: payload.result || '成功', }).catch(() => undefined) return { ok: true as const } } export async function exportProcessTimeline(params: { batchId?: string; personnelId?: string }) { const res = await service.get('/process-logs/export', { params, responseType: 'blob', timeout: 120_000, }) const blob = res.data as Blob const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'process-timeline.csv' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) return { ok: true as const } } export async function fetchBatchProgress(batchId: string) { const list = await get>>( `/process-logs/batches/${encodeURIComponent(batchId)}/progress`, ) return Array.isArray(list) ? list : [] } const ADMIN_OP_LOG_EXCLUDE = 'SYS_USER,SYS_CONFIG,LOGIN_LOG' function mapAdminOpLog(raw: Record, idx: number): OpLog { const mapped = mapProcessLog(raw) const role = roleLabelOf(mapped.operatorRole) || mapped.operatorRole || mapped.operatorOrg || '' return { id: mapped.id || String(idx + 1), rawId: mapped.id, time: mapped.operateTime || fmtTimeSafe(raw.operateTime as number), user: mapped.operator || '', role, module: processActionLabelOf(mapped.bizType) || mapped.bizType || '流程', action: mapped.actionLabel || processActionLabelOf(mapped.action), result: mapped.result || '成功', bizType: mapped.bizType, opinion: mapped.opinion, batchNo: mapped.batchNo, personnelId: mapped.personnelId, remarks: mapped.remarks, fileUrls: mapped.fileUrls, operatorOrg: mapped.operatorOrg, stage: mapped.stage, } } function parseLoginRemark(remarks?: string) { if (!remarks) return '' const parts = remarks.split('|') const ipIdx = parts.indexOf('IP') const ip = ipIdx >= 0 ? String(parts[ipIdx + 1] || '').trim() : '' const roleLabel = String(parts[2] || '').trim() return [roleLabel, ip ? `IP ${ip}` : ''].filter(Boolean).join(' · ') } export async function fetchLoginLogsPage(params?: { pageNum?: number; pageSize?: number }) { const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const res = await post>>('/process-logs/page', { pageNum, pageSize, bizType: 'LOGIN_LOG', }) const list = (res?.list || []).map((r, idx) => { const item = mapAdminOpLog(r, idx) const extra = parseLoginRemark(item.remarks) || item.opinion || item.operatorOrg || '' return { ...item, module: '登录', role: extra || item.role, } }) return { list, total: Number(res?.total || 0), pageNum, pageSize } } export async function fetchLoginLogs(limit = 100) { const page = await fetchLoginLogsPage({ pageNum: 1, pageSize: limit }) return page.list } export async function exportOpLogs() { const res = await service.get('/admin/op-logs/export', { responseType: 'blob', timeout: 120_000, }) const blob = res.data as Blob const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'op-logs.csv' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) return { ok: true as const } } export async function cleanupOpLogs(days = 90) { const res = await post>('/admin/op-logs/cleanup', undefined, { params: { days }, }) return { ok: true as const, removed: Number(res?.removed || 0), days } } export async function fetchValidateRules() { return get>('/admin/validate-rules') } export async function saveValidateRules(rules: Record) { await post('/admin/validate-rules', rules) return { ok: true as const } } /** 待审批的标准变更提示(如有) */ export async function fetchStandardPendingTip(town?: string): Promise { const res = await get>>('/special-biz/page', { pageNum: 1, pageSize: 20, bizType: 'STANDARD_CHANGE', approveStatus: '待分管领导审批', }) const rows = res?.list || [] if (!rows.length) return '' if (town) { await ensureTowns() const townId = townIdOf(town) const hit = rows.find((r) => !townId || String(r.townId || '') === townId) if (!hit) return '' return `存在待领导审批的标准变更(${town}),审批通过后新标准才生效` } return `存在 ${rows.length} 条待领导审批的标准变更,审批通过后新标准才生效` } export async function confirmVillageMonthlyReturn(payload: { batchId: string failCount?: number }) { await post('/return-disk/confirm', { batchId: payload.batchId, manualAllSuccess: !payload.failCount, operator: operatorOf(), }) return { ok: true } } /** —— 回盘 —— */ export async function fetchDisksPage(params?: { town?: string pageNum?: number pageSize?: number }): Promise<{ list: DiskRecord[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const { townId } = await resolveScopeIds(params?.town) if (townId) await ensureVillages(townId) const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const res = await post>>('/batches/page', { pageNum, pageSize, townId: townId || undefined, batchLevel: 'TOWN', statusIn: 'EXPORTED,RETURNED,ARCHIVED', lite: true, withTownTotals: true, }) const filtered = res?.list || [] if (!townId) { const townIds = [...new Set(filtered.map((b) => String(b.townId || '')).filter(Boolean))] await Promise.all(townIds.map((id) => ensureVillages(id))) } const list = filtered.map(mapDisk) return { list, total: Number(res?.total || list.length), pageNum, pageSize } } export async function fetchDisks(town?: string): Promise { const page = await fetchDisksPage({ town, pageNum: 1, pageSize: 10 }) return page.list } export async function exportDisk() { return { ok: true } } export async function confirmReturnDisk(payload?: { id?: string; failCount?: number }) { if (!payload?.id) return { ok: false } await post('/return-disk/confirm', { batchId: payload.id, manualAllSuccess: !payload.failCount, operator: operatorOf(), }) return { ok: true } } export async function uploadReturnDiskFile(payload: { id: string; file: File }) { const form = new FormData() form.append('file', payload.file) const uploaded = await post>( `/return-disk/upload?batchId=${encodeURIComponent(payload.id)}`, form as unknown as Record, ) // 上传解析后确认回盘(对齐旧前端:导入+确认) const confirmed = await post>('/return-disk/confirm', { batchId: payload.id, fileName: payload.file.name, operator: operatorOf(), }).catch(() => uploaded) return { ok: true as const, failCount: Number(confirmed?.failCount ?? uploaded?.failCount ?? 0), alertCount: Number(confirmed?.alertCount ?? 0), message: String(confirmed?.message || uploaded?.message || '回盘文件已处理'), } } export async function fetchDiskFailPeoplePage( diskId: string, params?: { pageNum?: number; pageSize?: number }, ): Promise<{ list: DiskFailPerson[]; total: number; pageNum: number; pageSize: number }> { const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 200 const res = await post>>('/return-disk/details/page', { pageNum, pageSize, batchId: diskId, }) const failed = (res?.list || []).filter((d) => d.success === false) const list = failed.map((d) => mapReturnFail(d, diskId)) return { list, total: failed.length, pageNum, pageSize } } export async function fetchDiskFailPeople(diskId: string): Promise { const page = await fetchDiskFailPeoplePage(diskId, { pageNum: 1, pageSize: 10 }) return page.list } export async function confirmDiskFailPaid(failPersonId: string) { // 纳入待补发/已确认:走待补发台账 reissue 或仅标记 try { await post('/return-arrears/reissue', { arrearsIds: [failPersonId], operator: operatorOf(), }) } catch { /* 明细 ID 可能不是台账 ID,忽略 */ } return { ok: true as const } } export async function archiveToNextMonth(id?: string) { if (id) await post(`/dashboard/archive/${encodeURIComponent(id)}`) return { ok: true } } /** —— 告警 —— */ export async function fetchAlertsPage(params?: { town?: string village?: string pageNum?: number pageSize?: number }): Promise<{ list: AlertItem[]; total: number; pageNum: number; pageSize: number }> { await ensureTowns() const { townId, villageId } = await resolveScopeIds(params?.town, params?.village) const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const res = await post>>('/alerts/page', { pageNum, pageSize, townId: townId || undefined, villageId: villageId || undefined, }) return { list: (res?.list || []).map(mapAlert), total: Number(res?.total || 0), pageNum, pageSize, } } export async function fetchAlerts(): Promise { const page = await fetchAlertsPage({ pageNum: 1, pageSize: 100 }) return page.list } export async function handleAlert(payload: HandleAlertPayload | string, result?: string) { const data: HandleAlertPayload = typeof payload === 'string' ? { id: payload, personName: '', idCard: '', town: '', village: '', bankCard: '', bankName: '', socialInsuranceCode: '', heirName: '', heirIdCard: '', heirBankCard: '', heirContact: '', result, } : payload // 人员字段由页面 savePerson 一次落库;此处只关告警,避免二次无锁覆盖 await post('/alerts/handle', { alertId: data.id, handleStatus: 'CLOSED', handleResult: data.result || '已核实并处理', remarks: data.remarks || undefined, fileUrls: data.fileUrls || undefined, handler: operatorOf(), }) return { ok: true } } /** —— 报表 / 系统 —— */ export async function fetchReportBatches(): Promise { return fetchVillageBatches() } /** —— 系统管理员运维 —— */ export async function listRegionsAdmin(params?: { level?: string parentId?: string }): Promise { const data = await get( '/regions', { level: params?.level, parentId: params?.parentId, }, { skipCache: true }, ) if (Array.isArray(data)) return data if (data && typeof data === 'object' && Array.isArray(data.list)) return data.list return [] } export async function saveRegionAdmin(payload: { id?: string code: string name: string level: string parentId?: string entrustPlanCode?: string defaultBank?: string enabled?: boolean remarks?: string districtSharePercent?: number townSharePercent?: number }) { await post('/regions', payload) invalidateRegionCache() return { ok: true as const } } export async function deleteRegionAdmin(id: string) { await del(`/regions/${encodeURIComponent(id)}`) invalidateRegionCache() return { ok: true as const } } export async function pageAdminBatches(params?: { payMonth?: string townId?: string villageId?: string batchLevel?: string status?: string pageNum?: number pageSize?: number /** 默认 false;列表首屏先出表,再异步 true 补镇人数/金额 */ withTownTotals?: boolean }): Promise<{ list: Array> total: number pageNum: number pageSize: number }> { await ensureTowns() const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const withTownTotals = params?.withTownTotals === true const res = await post>>('/batches/page', { pageNum, pageSize, payMonth: params?.payMonth || undefined, townId: params?.townId || undefined, villageId: params?.villageId || undefined, status: params?.status || undefined, batchLevel: params?.batchLevel || undefined, lite: true, withTownTotals, withDistrictTotals: withTownTotals, }) const list = (res?.list || []) .filter((b) => String(b.batchLevel || '') !== 'VILLAGE') .map((b) => ({ id: String(b.id || ''), batchNo: String(b.batchNo || b.payMonth || ''), payMonth: String(b.payMonth || ''), batchLevel: String(b.batchLevel || ''), townId: String(b.townId || ''), villageId: String(b.villageId || ''), town: regionName(String(b.townId || '')), village: String(b.batchLevel || '') === 'DISTRICT' ? '全区' : String(b.batchLevel || '') === 'TOWN' ? '本镇' : regionName(String(b.villageId || '')), peopleCount: Number(b.peopleCount || 0), totalAmount: Number(b.totalAmount || 0), status: String(b.status || ''), currentStage: String(b.currentStage || ''), })) return { list, total: Number(res?.total || list.length), pageNum, pageSize } } export async function purgeBatchAdmin(batchId: string) { await post(`/batches/${encodeURIComponent(batchId)}/purge`) return { ok: true as const } } export async function deletePersonnelAdmin(personnelId: string) { await del(`/personnel/${encodeURIComponent(personnelId)}`) return { ok: true as const } } export async function deletePersonnelBatchAdmin(ids: string[]) { const res = await post>('/personnel/batch-delete', { ids }, { timeout: 180000 }) return res } export async function clearAllPersonnelAdmin() { const res = await post>( '/personnel/clear-all', { confirm: 'CLEAR_ALL_PERSONNEL' }, { timeout: 600000 }, ) return res } export async function updatePersonnelStatusAdmin(payload: { personnelId: string bizStatus: string payThisMonth?: boolean remarks?: string }) { const remarks = payload.remarks await put('/personnel/status', { ...payload, remarks, allowDirect: true }) return { ok: true as const } } export async function initPersonnelStatusAdmin(payload: { bizStatus: string insuranceMatchResult: string retireQualifyType?: string payThisMonth?: boolean townId?: string villageId?: string }) { const res = await post>('/personnel/init-status', { confirm: 'INIT_STATUS_CONFIRM', operator: operatorOf(), bizStatus: payload.bizStatus, insuranceMatchResult: payload.insuranceMatchResult, retireQualifyType: payload.retireQualifyType || 'UNSET', payThisMonth: payload.payThisMonth, townId: payload.townId || undefined, villageId: payload.villageId || undefined, }, { timeout: 1_200_000 }) return { ok: true as const, updatedCount: Number(res?.updatedCount || 0), skippedArchived: Number(res?.skippedArchived || 0), skippedSame: Number(res?.skippedSame || 0), failCount: Number(res?.failCount || 0), message: String(res?.message || '已初始化'), } } /** 管理员:恢复初始化(清空批次/明细/流程审批/告警等;人员底数保留,资格与状态打回待提交) */ export async function factoryResetBusinessData() { const res = await post>('/admin/factory-reset', { confirm: 'RESET_CONFIRM', operator: operatorOf(), }, { timeout: 180000 }) return { ok: true as const, destroyedTotal: Number(res?.destroyedTotal || 0), detail: res || {}, message: String(res?.message || '已恢复初始化'), } } export async function fetchOpLogsPage(params?: { pageNum?: number pageSize?: number operator?: string operatorRole?: string bizType?: string action?: string result?: string operateTimeFrom?: number operateTimeTo?: number }) { const pageNum = params?.pageNum || 1 const pageSize = params?.pageSize || 10 const res = await post>>('/process-logs/page', { pageNum, pageSize, excludeBizType: ADMIN_OP_LOG_EXCLUDE, operator: params?.operator || undefined, operatorRole: params?.operatorRole || undefined, bizType: params?.bizType || undefined, action: params?.action || undefined, result: params?.result || undefined, operateTimeFrom: params?.operateTimeFrom || undefined, operateTimeTo: params?.operateTimeTo || undefined, }) return { list: (res?.list || []).map((r, idx) => mapAdminOpLog(r, idx)), total: Number(res?.total || 0), pageNum, pageSize, } } export async function fetchOpLogs(): Promise { const page = await fetchOpLogsPage({ pageNum: 1, pageSize: 10 }) return page.list }