audit-amount-sum-20261009.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * 只读:把 202608 全区明细按镇/村加总,并和批头、展板、统计表对照。
  3. * node scripts/audit-amount-sum-20261009.js
  4. */
  5. const http = require('http')
  6. const fs = require('fs')
  7. const path = require('path')
  8. function req(method, apiPath, body) {
  9. return new Promise((resolve) => {
  10. const data = body == null ? null : JSON.stringify(body)
  11. const headers = { Accept: 'application/json' }
  12. if (data) {
  13. headers['Content-Type'] = 'application/json'
  14. headers['Content-Length'] = Buffer.byteLength(data)
  15. }
  16. const r = http.request(
  17. { hostname: '121.43.55.7', port: 11091, path: '/sjnmtybt' + apiPath, method, headers, timeout: 90000 },
  18. (res) => {
  19. let raw = ''
  20. res.on('data', (c) => (raw += c))
  21. res.on('end', () => {
  22. try {
  23. resolve(JSON.parse(raw))
  24. } catch (e) {
  25. resolve({ code: res.statusCode, parseError: true, raw: String(raw).slice(0, 160) })
  26. }
  27. })
  28. },
  29. )
  30. r.on('error', (e) => resolve({ code: 0, error: e.message }))
  31. r.on('timeout', () => {
  32. r.destroy()
  33. resolve({ code: 0, error: 'timeout' })
  34. })
  35. if (data) r.write(data)
  36. r.end()
  37. })
  38. }
  39. async function reqRetry(method, apiPath, body) {
  40. let last
  41. for (let i = 0; i < 4; i++) {
  42. last = await req(method, apiPath, body)
  43. if (last.code === 200) return last
  44. await new Promise((r) => setTimeout(r, 800 * (i + 1)))
  45. }
  46. return last
  47. }
  48. const n = (v) => {
  49. const x = Number(v || 0)
  50. return Number.isFinite(x) ? x : 0
  51. }
  52. const r2 = (v) => Math.round(n(v) * 100) / 100
  53. ;(async () => {
  54. const towns = (await req('GET', '/api/regions?level=TOWN&parentId=D_SJ')).data || []
  55. const townName = Object.fromEntries(towns.map((t) => [t.id, t.name]))
  56. const share = Object.fromEntries(towns.map((t) => [t.id, t]))
  57. const villageName = {}
  58. for (const t of towns) {
  59. const vs = (await req('GET', '/api/regions?level=VILLAGE&parentId=' + encodeURIComponent(t.id))).data || []
  60. for (const v of vs) villageName[v.id] = { name: v.name, town: t.name, townId: t.id }
  61. }
  62. const batches = (await reqRetry('POST', '/api/batches/page', { pageNum: 1, pageSize: 50, payMonth: '202608' })).data?.list || []
  63. const district = batches.find((b) => b.batchLevel === 'DISTRICT')
  64. const townBatches = batches.filter((b) => b.batchLevel === 'TOWN')
  65. if (!district) throw new Error('no district batch')
  66. const std = await req('GET', '/api/amount-standards/current?townId=T_DISTRICT&standardType=MONTHLY')
  67. const monthlyStd = n(std.data?.amount)
  68. const pageSize = 200
  69. let pageNum = 1
  70. let total = 0
  71. let loaded = 0
  72. let formulaBad = 0
  73. let standardBad = 0
  74. let shareBad = 0
  75. let dup = 0
  76. const seen = new Set()
  77. const byTown = {}
  78. const byVillage = {}
  79. const monthlyHist = {}
  80. const examples = []
  81. let sumPay = 0
  82. let sumMonthly = 0
  83. let sumSup = 0
  84. let sumFun = 0
  85. let sumDed = 0
  86. let sumOne = 0
  87. let sumActual = 0
  88. let fail = 0
  89. let actualNull = 0
  90. const pageErrors = []
  91. for (;;) {
  92. const res = await reqRetry('POST', '/api/payments/details/page', {
  93. batchId: district.id,
  94. pageNum,
  95. pageSize,
  96. })
  97. if (res.code !== 200) {
  98. pageErrors.push({ pageNum, code: res.code, message: res.message || res.error || res.raw })
  99. break
  100. }
  101. const list = res.data?.list || []
  102. total = Number(res.data?.total || 0)
  103. if (!list.length) break
  104. for (const d of list) {
  105. if (!d || d.changeFlag === 'REMOVE') continue
  106. loaded += 1
  107. const pid = d.personnelId || d.id
  108. if (pid && seen.has(pid)) dup += 1
  109. if (pid) seen.add(pid)
  110. const expect = n(d.monthlyAmount) + n(d.oneTimeAmount) + n(d.supplementAmount) + n(d.funeralAmount) - n(d.deductAmount)
  111. const got = n(d.totalAmount)
  112. if (Math.abs(expect - got) > 0.05) {
  113. formulaBad += 1
  114. if (examples.length < 8) examples.push({ kind: 'formula', name: d.name, town: townName[d.townId], expect: r2(expect), got: r2(got) })
  115. }
  116. const m = r2(d.monthlyAmount)
  117. monthlyHist[String(m)] = (monthlyHist[String(m)] || 0) + 1
  118. if (monthlyStd > 0 && m > 0 && Math.abs(m - monthlyStd) > 0.05 && n(d.funeralAmount) === 0 && n(d.supplementAmount) === 0) {
  119. standardBad += 1
  120. if (examples.length < 12) examples.push({ kind: 'standard', name: d.name, town: townName[d.townId], monthly: m })
  121. }
  122. const sh = share[d.townId]
  123. if (sh && got > 0 && d.districtAmount != null) {
  124. const expectD = got * n(sh.districtSharePercent) / 100
  125. if (Math.abs(n(d.districtAmount) + n(d.townAmount) - got) > 0.2 || Math.abs(n(d.districtAmount) - expectD) > 0.5) {
  126. shareBad += 1
  127. }
  128. }
  129. const tid = d.townId || '?'
  130. const vid = d.villageId || '?'
  131. if (!byTown[tid]) byTown[tid] = { count: 0, payable: 0, actual: 0, fail: 0 }
  132. if (!byVillage[vid]) byVillage[vid] = { townId: tid, count: 0, payable: 0 }
  133. byTown[tid].count += 1
  134. byTown[tid].payable += got
  135. byVillage[vid].count += 1
  136. byVillage[vid].payable += got
  137. sumPay += got
  138. sumMonthly += n(d.monthlyAmount)
  139. sumSup += n(d.supplementAmount)
  140. sumFun += n(d.funeralAmount)
  141. sumDed += n(d.deductAmount)
  142. sumOne += n(d.oneTimeAmount)
  143. if (d.actualAmount == null) actualNull += 1
  144. else sumActual += n(d.actualAmount)
  145. if (d.returnSuccess === false) {
  146. fail += 1
  147. byTown[tid].fail += 1
  148. }
  149. }
  150. process.stdout.write('\rpage ' + pageNum + ' loaded ' + loaded + '/' + total)
  151. if (loaded >= total || pageNum > 250) break
  152. pageNum += 1
  153. }
  154. console.log('')
  155. const dash = await req('GET', '/api/dashboard/summary?payMonth=202608')
  156. const stats = await reqRetry('POST', '/api/batch-stats/page', { pageNum: 1, pageSize: 200, payMonth: '202608' })
  157. const townCompare = townBatches.map((tb) => {
  158. const s = byTown[tb.townId] || { count: 0, payable: 0, fail: 0 }
  159. return {
  160. town: townName[tb.townId] || tb.townId,
  161. status: tb.status,
  162. headerPeople: n(tb.peopleCount),
  163. headerAmount: r2(tb.totalAmount),
  164. slicePeople: s.count,
  165. sliceAmount: r2(s.payable),
  166. peopleGap: n(tb.peopleCount) - s.count,
  167. amountGap: r2(n(tb.totalAmount) - s.payable),
  168. parentBatchId: tb.parentBatchId,
  169. parentOk: tb.parentBatchId === district.id,
  170. fail: s.fail,
  171. }
  172. })
  173. const emptyVillages = Object.entries(villageName)
  174. .filter(([id]) => !byVillage[id] || !byVillage[id].count)
  175. .map(([, v]) => v.town + '/' + v.name)
  176. const out = {
  177. district: {
  178. id: district.id,
  179. batchNo: district.batchNo,
  180. status: district.status,
  181. peopleCount: district.peopleCount,
  182. totalAmount: r2(district.totalAmount),
  183. normalAmount: r2(district.normalAmount),
  184. supplementAmount: r2(district.supplementAmount),
  185. funeralAmount: r2(district.funeralAmount),
  186. deductAmount: r2(district.deductAmount),
  187. exportStatus: district.exportStatus,
  188. returnStatus: district.returnStatus,
  189. },
  190. standard: { monthly: monthlyStd, funeral: n(std.data?.funeralAmount) },
  191. scan: {
  192. apiTotal: total,
  193. loaded,
  194. unique: seen.size,
  195. dup,
  196. formulaBad,
  197. standardBad,
  198. shareBad,
  199. actualNull,
  200. fail,
  201. sumPay: r2(sumPay),
  202. sumMonthly: r2(sumMonthly),
  203. sumSup: r2(sumSup),
  204. sumFun: r2(sumFun),
  205. sumDed: r2(sumDed),
  206. sumOne: r2(sumOne),
  207. sumActual: r2(sumActual),
  208. pages: pageNum,
  209. pageErrors,
  210. monthlyHist,
  211. examples,
  212. complete: loaded >= total && !pageErrors.length,
  213. },
  214. townCompare,
  215. villageWithPay: Object.values(byVillage).filter((v) => v.count).length,
  216. villageMaster: Object.keys(villageName).length,
  217. emptyVillageCount: emptyVillages.length,
  218. emptyVillageSample: emptyVillages.slice(0, 20),
  219. dashboard: dash.data,
  220. statCount: (stats.data?.list || []).length,
  221. statTotal: stats.data?.total,
  222. statSample: (stats.data?.list || []).slice(0, 5).map((s) => ({
  223. grain: s.grain || s.level || s.statLevel,
  224. townId: s.townId,
  225. villageId: s.villageId,
  226. shouldPayCount: s.shouldPayCount,
  227. shouldPayAmount: s.shouldPayAmount,
  228. actualPayAmount: s.actualPayAmount,
  229. failCount: s.failCount,
  230. keys: Object.keys(s),
  231. })),
  232. }
  233. const file = path.join(__dirname, '..', '..', 'docs', '金额核对-202608.json')
  234. fs.writeFileSync(file, JSON.stringify(out, null, 2))
  235. console.log('WROTE', file, 'complete', out.scan.complete, 'loaded', loaded, '/', total)
  236. })().catch((e) => {
  237. console.error(e)
  238. process.exit(1)
  239. })