/** * 只读:把 202608 全区明细按镇/村加总,并和批头、展板、统计表对照。 * node scripts/audit-amount-sum-20261009.js */ const http = require('http') const fs = require('fs') const path = require('path') function req(method, apiPath, body) { return new Promise((resolve) => { const data = body == null ? null : JSON.stringify(body) const headers = { Accept: 'application/json' } if (data) { headers['Content-Type'] = 'application/json' headers['Content-Length'] = Buffer.byteLength(data) } const r = http.request( { hostname: '121.43.55.7', port: 11091, path: '/sjnmtybt' + apiPath, method, headers, timeout: 90000 }, (res) => { let raw = '' res.on('data', (c) => (raw += c)) res.on('end', () => { try { resolve(JSON.parse(raw)) } catch (e) { resolve({ code: res.statusCode, parseError: true, raw: String(raw).slice(0, 160) }) } }) }, ) r.on('error', (e) => resolve({ code: 0, error: e.message })) r.on('timeout', () => { r.destroy() resolve({ code: 0, error: 'timeout' }) }) if (data) r.write(data) r.end() }) } async function reqRetry(method, apiPath, body) { let last for (let i = 0; i < 4; i++) { last = await req(method, apiPath, body) if (last.code === 200) return last await new Promise((r) => setTimeout(r, 800 * (i + 1))) } return last } const n = (v) => { const x = Number(v || 0) return Number.isFinite(x) ? x : 0 } const r2 = (v) => Math.round(n(v) * 100) / 100 ;(async () => { const towns = (await req('GET', '/api/regions?level=TOWN&parentId=D_SJ')).data || [] const townName = Object.fromEntries(towns.map((t) => [t.id, t.name])) const share = Object.fromEntries(towns.map((t) => [t.id, t])) const villageName = {} for (const t of towns) { const vs = (await req('GET', '/api/regions?level=VILLAGE&parentId=' + encodeURIComponent(t.id))).data || [] for (const v of vs) villageName[v.id] = { name: v.name, town: t.name, townId: t.id } } const batches = (await reqRetry('POST', '/api/batches/page', { pageNum: 1, pageSize: 50, payMonth: '202608' })).data?.list || [] const district = batches.find((b) => b.batchLevel === 'DISTRICT') const townBatches = batches.filter((b) => b.batchLevel === 'TOWN') if (!district) throw new Error('no district batch') const std = await req('GET', '/api/amount-standards/current?townId=T_DISTRICT&standardType=MONTHLY') const monthlyStd = n(std.data?.amount) const pageSize = 200 let pageNum = 1 let total = 0 let loaded = 0 let formulaBad = 0 let standardBad = 0 let shareBad = 0 let dup = 0 const seen = new Set() const byTown = {} const byVillage = {} const monthlyHist = {} const examples = [] let sumPay = 0 let sumMonthly = 0 let sumSup = 0 let sumFun = 0 let sumDed = 0 let sumOne = 0 let sumActual = 0 let fail = 0 let actualNull = 0 const pageErrors = [] for (;;) { const res = await reqRetry('POST', '/api/payments/details/page', { batchId: district.id, pageNum, pageSize, }) if (res.code !== 200) { pageErrors.push({ pageNum, code: res.code, message: res.message || res.error || res.raw }) break } const list = res.data?.list || [] total = Number(res.data?.total || 0) if (!list.length) break for (const d of list) { if (!d || d.changeFlag === 'REMOVE') continue loaded += 1 const pid = d.personnelId || d.id if (pid && seen.has(pid)) dup += 1 if (pid) seen.add(pid) const expect = n(d.monthlyAmount) + n(d.oneTimeAmount) + n(d.supplementAmount) + n(d.funeralAmount) - n(d.deductAmount) const got = n(d.totalAmount) if (Math.abs(expect - got) > 0.05) { formulaBad += 1 if (examples.length < 8) examples.push({ kind: 'formula', name: d.name, town: townName[d.townId], expect: r2(expect), got: r2(got) }) } const m = r2(d.monthlyAmount) monthlyHist[String(m)] = (monthlyHist[String(m)] || 0) + 1 if (monthlyStd > 0 && m > 0 && Math.abs(m - monthlyStd) > 0.05 && n(d.funeralAmount) === 0 && n(d.supplementAmount) === 0) { standardBad += 1 if (examples.length < 12) examples.push({ kind: 'standard', name: d.name, town: townName[d.townId], monthly: m }) } const sh = share[d.townId] if (sh && got > 0 && d.districtAmount != null) { const expectD = got * n(sh.districtSharePercent) / 100 if (Math.abs(n(d.districtAmount) + n(d.townAmount) - got) > 0.2 || Math.abs(n(d.districtAmount) - expectD) > 0.5) { shareBad += 1 } } const tid = d.townId || '?' const vid = d.villageId || '?' if (!byTown[tid]) byTown[tid] = { count: 0, payable: 0, actual: 0, fail: 0 } if (!byVillage[vid]) byVillage[vid] = { townId: tid, count: 0, payable: 0 } byTown[tid].count += 1 byTown[tid].payable += got byVillage[vid].count += 1 byVillage[vid].payable += got sumPay += got sumMonthly += n(d.monthlyAmount) sumSup += n(d.supplementAmount) sumFun += n(d.funeralAmount) sumDed += n(d.deductAmount) sumOne += n(d.oneTimeAmount) if (d.actualAmount == null) actualNull += 1 else sumActual += n(d.actualAmount) if (d.returnSuccess === false) { fail += 1 byTown[tid].fail += 1 } } process.stdout.write('\rpage ' + pageNum + ' loaded ' + loaded + '/' + total) if (loaded >= total || pageNum > 250) break pageNum += 1 } console.log('') const dash = await req('GET', '/api/dashboard/summary?payMonth=202608') const stats = await reqRetry('POST', '/api/batch-stats/page', { pageNum: 1, pageSize: 200, payMonth: '202608' }) const townCompare = townBatches.map((tb) => { const s = byTown[tb.townId] || { count: 0, payable: 0, fail: 0 } return { town: townName[tb.townId] || tb.townId, status: tb.status, headerPeople: n(tb.peopleCount), headerAmount: r2(tb.totalAmount), slicePeople: s.count, sliceAmount: r2(s.payable), peopleGap: n(tb.peopleCount) - s.count, amountGap: r2(n(tb.totalAmount) - s.payable), parentBatchId: tb.parentBatchId, parentOk: tb.parentBatchId === district.id, fail: s.fail, } }) const emptyVillages = Object.entries(villageName) .filter(([id]) => !byVillage[id] || !byVillage[id].count) .map(([, v]) => v.town + '/' + v.name) const out = { district: { id: district.id, batchNo: district.batchNo, status: district.status, peopleCount: district.peopleCount, totalAmount: r2(district.totalAmount), normalAmount: r2(district.normalAmount), supplementAmount: r2(district.supplementAmount), funeralAmount: r2(district.funeralAmount), deductAmount: r2(district.deductAmount), exportStatus: district.exportStatus, returnStatus: district.returnStatus, }, standard: { monthly: monthlyStd, funeral: n(std.data?.funeralAmount) }, scan: { apiTotal: total, loaded, unique: seen.size, dup, formulaBad, standardBad, shareBad, actualNull, fail, sumPay: r2(sumPay), sumMonthly: r2(sumMonthly), sumSup: r2(sumSup), sumFun: r2(sumFun), sumDed: r2(sumDed), sumOne: r2(sumOne), sumActual: r2(sumActual), pages: pageNum, pageErrors, monthlyHist, examples, complete: loaded >= total && !pageErrors.length, }, townCompare, villageWithPay: Object.values(byVillage).filter((v) => v.count).length, villageMaster: Object.keys(villageName).length, emptyVillageCount: emptyVillages.length, emptyVillageSample: emptyVillages.slice(0, 20), dashboard: dash.data, statCount: (stats.data?.list || []).length, statTotal: stats.data?.total, statSample: (stats.data?.list || []).slice(0, 5).map((s) => ({ grain: s.grain || s.level || s.statLevel, townId: s.townId, villageId: s.villageId, shouldPayCount: s.shouldPayCount, shouldPayAmount: s.shouldPayAmount, actualPayAmount: s.actualPayAmount, failCount: s.failCount, keys: Object.keys(s), })), } const file = path.join(__dirname, '..', '..', 'docs', '金额核对-202608.json') fs.writeFileSync(file, JSON.stringify(out, null, 2)) console.log('WROTE', file, 'complete', out.scan.complete, 'loaded', loaded, '/', total) })().catch((e) => { console.error(e) process.exit(1) })