| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- /**
- * 只读核对线上金额与生命周期断点。不调用会回写统计的接口。
- * node scripts/audit-amount-lifecycle-20261009.js
- */
- const http = require('http')
- const fs = require('fs')
- const path = require('path')
- const HOST = '121.43.55.7'
- const PORT = 11091
- const MONTHS = ['202607', '202608', '202609']
- 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: HOST, port: PORT, path: '/sjnmtybt' + apiPath, method, headers, timeout: 120000 },
- (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, 180) })
- }
- })
- },
- )
- 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()
- })
- }
- const n = (v) => {
- const x = Number(v || 0)
- return Number.isFinite(x) ? x : 0
- }
- const round2 = (v) => Math.round(n(v) * 100) / 100
- const near = (a, b, tol = 0.05) => Math.abs(round2(a) - round2(b)) <= tol
- async function pageAll(apiPath, body, pageSize) {
- const all = []
- let total = 0
- let pageNum = 1
- for (;;) {
- const res = await req('POST', apiPath, { ...body, pageNum, pageSize })
- if (res.code !== 200) return { error: res.message || res.error || res.raw, list: all, total }
- const list = res.data?.list || []
- total = Number(res.data?.total || 0)
- all.push(...list)
- if (!list.length || all.length >= total || pageNum > 80) break
- pageNum += 1
- }
- return { list: all, total, pages: pageNum }
- }
- function linePayable(d) {
- return n(d.monthlyAmount) + n(d.oneTimeAmount) + n(d.supplementAmount) + n(d.funeralAmount) - n(d.deductAmount)
- }
- ;(async () => {
- const report = { at: new Date().toISOString(), host: HOST + ':' + PORT, findings: [] }
- const add = (level, title, detail) => {
- report.findings.push({ level, title, detail })
- console.log(level + ' | ' + title + (detail ? ' | ' + JSON.stringify(detail).slice(0, 400) : ''))
- }
- const regions = await req('GET', '/api/regions?level=TOWN&parentId=D_SJ')
- const towns = (regions.data || []).filter((t) => t && t.enabled !== false)
- report.towns = towns.map((t) => ({
- id: t.id,
- name: t.name,
- districtSharePercent: t.districtSharePercent,
- townSharePercent: t.townSharePercent,
- }))
- const townName = Object.fromEntries(towns.map((t) => [t.id, t.name]))
- const shareOf = Object.fromEntries(towns.map((t) => [t.id, t]))
- for (const t of towns) {
- const d = n(t.districtSharePercent)
- const w = n(t.townSharePercent)
- if (!near(d + w, 100, 0.01)) {
- add('BUG', '街镇分担比例合计不是 100', { town: t.name, district: d, townShare: w })
- }
- }
- const villagesByTown = {}
- for (const t of towns) {
- const vs = await req('GET', '/api/regions?level=VILLAGE&parentId=' + encodeURIComponent(t.id))
- villagesByTown[t.id] = (vs.data || []).filter((v) => v && v.enabled !== false)
- }
- report.villageCounts = Object.fromEntries(
- towns.map((t) => [t.name, villagesByTown[t.id].length]),
- )
- const stdDistrict = await req(
- 'GET',
- '/api/amount-standards/current?townId=T_DISTRICT&standardType=MONTHLY',
- )
- const districtMonthly = n(stdDistrict.data?.amount)
- const districtFuneral = n(stdDistrict.data?.funeralAmount)
- report.districtStandard = {
- monthly: districtMonthly,
- funeral: districtFuneral,
- id: stdDistrict.data?.id,
- raw: stdDistrict.code === 200 ? undefined : stdDistrict,
- }
- if (!(districtMonthly > 0)) add('BUG', '全区现行月标不是正数', report.districtStandard)
- for (const t of towns) {
- const s = await req(
- 'GET',
- '/api/amount-standards/current?townId=' + encodeURIComponent(t.id) + '&standardType=MONTHLY',
- )
- const amt = n(s.data?.amount)
- if (amt && !near(amt, districtMonthly, 0.01)) {
- add('BUG', '街镇当前月标与全区标准不一致', { town: t.name, townAmount: amt, district: districtMonthly })
- }
- }
- const statuses = [
- 'DRAFT',
- 'PENDING_TOWN_APPROVE',
- 'NEW',
- 'PENDING_INSURANCE_MATCH',
- 'NORMAL',
- 'PAUSED',
- 'STOPPED',
- 'FUNERAL',
- 'EXITED',
- 'ABNORMAL',
- 'ARCHIVED',
- ]
- report.personnelTotals = {}
- for (const st of statuses) {
- const p = await req('POST', '/api/personnel/page', { pageNum: 1, pageSize: 1, bizStatus: st })
- report.personnelTotals[st] = p.code === 200 ? Number(p.data?.total || 0) : p.message || p.error
- }
- add('INFO', '人员状态底数', report.personnelTotals)
- const pendingSpecial = await req(
- 'GET',
- '/api/special-biz/page?approveStatus=' + encodeURIComponent('待分管领导审批') + '&pageNum=1&pageSize=200',
- )
- const pendingList = pendingSpecial.data?.list || []
- report.pendingSpecial = {
- total: Number(pendingSpecial.data?.total || pendingList.length),
- byTown: {},
- byType: {},
- }
- for (const s of pendingList) {
- const tn = townName[s.townId] || s.townId || '未知'
- report.pendingSpecial.byTown[tn] = (report.pendingSpecial.byTown[tn] || 0) + 1
- const tp = s.bizType || '未知'
- report.pendingSpecial.byType[tp] = (report.pendingSpecial.byType[tp] || 0) + 1
- }
- report.months = {}
- for (const ym of MONTHS) {
- const batchRes = await req('POST', '/api/batches/page', { pageNum: 1, pageSize: 100, payMonth: ym })
- const batches = batchRes.data?.list || []
- const district = batches.filter((b) => b.batchLevel === 'DISTRICT')
- const townBatches = batches.filter((b) => b.batchLevel === 'TOWN')
- const other = batches.filter((b) => b.batchLevel !== 'DISTRICT' && b.batchLevel !== 'TOWN')
- const month = {
- batchQueryTotal: Number(batchRes.data?.total || batches.length),
- loaded: batches.length,
- district: district.map((b) => ({
- id: b.id,
- batchNo: b.batchNo,
- status: b.status,
- peopleCount: b.peopleCount,
- totalAmount: n(b.totalAmount),
- normalAmount: n(b.normalAmount),
- supplementAmount: n(b.supplementAmount),
- funeralAmount: n(b.funeralAmount),
- deductAmount: n(b.deductAmount),
- exportStatus: b.exportStatus,
- returnStatus: b.returnStatus,
- })),
- townBatches: townBatches.map((b) => ({
- id: b.id,
- batchNo: b.batchNo,
- townId: b.townId,
- town: townName[b.townId] || b.townId,
- status: b.status,
- parentBatchId: b.parentBatchId,
- peopleCount: n(b.peopleCount),
- totalAmount: n(b.totalAmount),
- })),
- otherLevels: other.map((b) => ({
- batchNo: b.batchNo,
- level: b.batchLevel,
- townId: b.townId,
- villageId: b.villageId,
- status: b.status,
- payMonth: b.payMonth,
- peopleCount: b.peopleCount,
- totalAmount: n(b.totalAmount),
- })),
- }
- if (!district.length) add('BUG', ym + ' 没有全区月批', { total: month.batchQueryTotal })
- if (district.length > 1) add('BUG', ym + ' 全区月批多于 1 条', { n: district.length, nos: district.map((b) => b.batchNo) })
- const townIds = new Set(townBatches.map((b) => b.townId))
- for (const t of towns) {
- if (district.length && !townIds.has(t.id)) {
- add('BUG', ym + ' 缺少镇统计批', { town: t.name })
- }
- }
- if (district[0]) {
- const db = district[0]
- const details = await pageAll('/api/payments/details/page', { batchId: db.id }, 400)
- month.detailError = details.error
- month.detailTotal = details.total
- month.detailLoaded = details.list.length
- const rows = details.list.filter((d) => d && d.changeFlag !== 'REMOVE')
- const seen = new Map()
- let formulaBad = 0
- let standardBad = 0
- let shareBad = 0
- let neg = 0
- const examples = []
- const byTown = {}
- const byVillage = {}
- let sum = { monthly: 0, oneTime: 0, supplement: 0, funeral: 0, deduct: 0, payable: 0, actual: 0, fail: 0 }
- for (const d of rows) {
- const pid = d.personnelId || d.idNumber || d.id
- if (seen.has(pid)) {
- if (examples.length < 12) examples.push({ kind: '重复人员', name: d.name, town: townName[d.townId] || d.townId })
- } else seen.set(pid, 1)
- const expect = linePayable(d)
- const got = n(d.totalAmount)
- if (!near(expect, got)) {
- formulaBad += 1
- if (examples.length < 12) {
- examples.push({
- kind: '应发不等于分项',
- name: d.name,
- town: townName[d.townId] || d.townId,
- expect: round2(expect),
- got: round2(got),
- })
- }
- }
- if (n(d.monthlyAmount) < 0 || n(d.supplementAmount) < 0 || n(d.funeralAmount) < 0 || n(d.deductAmount) < 0 || got < 0) {
- neg += 1
- }
- const monthly = n(d.monthlyAmount)
- const funeral = n(d.funeralAmount)
- const supplement = n(d.supplementAmount)
- if (districtMonthly > 0 && monthly > 0 && !near(monthly, districtMonthly) && funeral === 0 && supplement === 0) {
- standardBad += 1
- if (examples.length < 12) {
- examples.push({
- kind: '月标偏离全区标准',
- name: d.name,
- town: townName[d.townId] || d.townId,
- monthly: round2(monthly),
- standard: districtMonthly,
- })
- }
- }
- const share = shareOf[d.townId]
- const total = got
- if (share && total > 0 && (d.districtAmount != null || d.townAmount != null)) {
- const ds = n(d.districtAmount)
- const ts = n(d.townAmount)
- if (!near(ds + ts, total)) {
- shareBad += 1
- } else {
- const expectD = total * n(share.districtSharePercent) / 100
- if (!near(ds, expectD, 0.2)) shareBad += 1
- }
- }
- const keyT = d.townId || '未知'
- const keyV = (d.townId || '') + '|' + (d.villageId || '未知')
- if (!byTown[keyT]) byTown[keyT] = { count: 0, payable: 0, monthly: 0, supplement: 0, funeral: 0, deduct: 0, actual: 0, fail: 0 }
- if (!byVillage[keyV]) byVillage[keyV] = { townId: d.townId, villageId: d.villageId, count: 0, payable: 0 }
- byTown[keyT].count += 1
- byTown[keyT].payable += got
- byTown[keyT].monthly += monthly
- byTown[keyT].supplement += supplement
- byTown[keyT].funeral += funeral
- byTown[keyT].deduct += n(d.deductAmount)
- byVillage[keyV].count += 1
- byVillage[keyV].payable += got
- sum.monthly += monthly
- sum.oneTime += n(d.oneTimeAmount)
- sum.supplement += supplement
- sum.funeral += funeral
- sum.deduct += n(d.deductAmount)
- sum.payable += got
- if (d.returnSuccess === false) {
- sum.fail += 1
- byTown[keyT].fail += 1
- }
- if (d.actualAmount != null) {
- sum.actual += n(d.actualAmount)
- byTown[keyT].actual += n(d.actualAmount)
- }
- }
- for (const k of Object.keys(sum)) sum[k] = round2(sum[k])
- month.sum = sum
- month.formulaBad = formulaBad
- month.standardBad = standardBad
- month.shareBad = shareBad
- month.negative = neg
- month.uniquePeople = seen.size
- month.examples = examples
- if (details.total && details.list.length < details.total) {
- add('BUG', ym + ' 发放明细没有拉全', { total: details.total, loaded: details.list.length, error: details.error })
- }
- if (!near(sum.payable, n(db.totalAmount), 1)) {
- add('BUG', ym + ' 全区批头应发与明细合计不一致', {
- header: round2(db.totalAmount),
- detail: sum.payable,
- headerPeople: db.peopleCount,
- detailPeople: rows.length,
- })
- } else {
- add('OK', ym + ' 全区批头应发与明细一致', { amount: sum.payable, people: rows.length })
- }
- if (Number(db.peopleCount || 0) !== rows.length) {
- add('BUG', ym + ' 全区批人数与明细条数不一致', { header: db.peopleCount, detail: rows.length })
- }
- if (formulaBad) add('BUG', ym + ' 有明细应发不等于分项之和', { formulaBad, examples })
- if (standardBad) add('BUG', ym + ' 有人月标偏离全区现行标准', { standardBad, districtMonthly, examples })
- if (shareBad) add('BUG', ym + ' 区镇分担金额与比例不一致', { shareBad })
- if (neg) add('BUG', ym + ' 存在负数金额', { neg })
- month.townCompare = []
- for (const tb of townBatches) {
- const slice = byTown[tb.townId] || { count: 0, payable: 0 }
- const row = {
- town: townName[tb.townId] || tb.townId,
- status: tb.status,
- headerPeople: n(tb.peopleCount),
- headerAmount: round2(tb.totalAmount),
- slicePeople: slice.count,
- sliceAmount: round2(slice.payable),
- parentOk: !tb.parentBatchId || tb.parentBatchId === db.id,
- }
- month.townCompare.push(row)
- if (!near(row.headerAmount, row.sliceAmount, 1) || row.headerPeople !== row.slicePeople) {
- add('BUG', ym + ' 镇统计与全区明细切片不一致', row)
- }
- if (!row.parentOk) add('BUG', ym + ' 镇统计未挂到全区父批', row)
- }
- const emptyVillages = []
- for (const t of towns) {
- for (const v of villagesByTown[t.id]) {
- const hit = byVillage[t.id + '|' + v.id]
- if (!hit || !hit.count) emptyVillages.push(t.name + '/' + v.name)
- }
- }
- month.emptyVillageCount = emptyVillages.length
- month.emptyVillageSample = emptyVillages.slice(0, 15)
- month.villageWithPay = Object.values(byVillage).filter((v) => v.count > 0).length
- const summary = await req('GET', '/api/dashboard/summary?payMonth=' + ym)
- const dashPeople = n(summary.data?.shouldPayCount ?? summary.data?.peopleCount)
- const dashAmount = n(summary.data?.shouldPayAmount ?? summary.data?.totalAmount)
- month.dashboard = {
- code: summary.code,
- keys: summary.data ? Object.keys(summary.data) : [],
- people: dashPeople,
- amount: round2(dashAmount),
- rawPeople: summary.data?.shouldPayCount,
- rawAmount: summary.data?.shouldPayAmount,
- totalAmount: summary.data?.totalAmount,
- }
- if (summary.code === 200 && (dashPeople || dashAmount)) {
- if (!near(dashAmount, sum.payable, 1) || dashPeople !== rows.length) {
- add('BUG', ym + ' 展板汇总额与明细不一致', {
- dashPeople,
- dashAmount: round2(dashAmount),
- detailPeople: rows.length,
- detailAmount: sum.payable,
- })
- }
- }
- }
- report.months[ym] = month
- }
- const memberIds = new Set()
- const aug = report.months['202608']
- if (aug && aug.detailLoaded) {
- // member ids not stored; re-page is expensive. Use unique count only.
- }
- const newPage = await pageAll('/api/personnel/page', { bizStatus: 'NEW' }, 200)
- let newShouldInAug = 0
- let newMissingStart = 0
- const newSamples = []
- for (const p of newPage.list || []) {
- const start = String(p.nextPayStartMonth || p.enjoyStartMonth || '').replace('-', '')
- if (!start) {
- newMissingStart += 1
- continue
- }
- if (start <= '202608') {
- newShouldInAug += 1
- if (newSamples.length < 8) {
- newSamples.push({ name: p.name, town: townName[p.townId] || p.townId, start, qualify: p.retireQualifyType })
- }
- }
- }
- report.newPeople = {
- total: newPage.total,
- loaded: (newPage.list || []).length,
- missingStart: newMissingStart,
- startOnOrBefore202608: newShouldInAug,
- sample: newSamples,
- error: newPage.error,
- }
- if (newShouldInAug && aug && aug.uniquePeople) {
- add('INFO', '资格已通过且起始月不晚于 202608 的新增人员', {
- count: newShouldInAug,
- note: '若 8 月批已生成后才通过,按规则不自动补进当月,需特殊业务加人',
- sample: newSamples,
- })
- }
- const out = path.join(__dirname, '..', '..', 'docs', '金额与生命周期核对-20261009.json')
- fs.writeFileSync(out, JSON.stringify(report, null, 2))
- console.log('WROTE ' + out + ' findings=' + report.findings.length)
- })().catch((e) => {
- console.error(e)
- process.exit(1)
- })
|