| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- /**
- * 只读:把 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)
- })
|