stats.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. /**
  2. * JKES 跑步记录汇总(mylist 分页)
  3. */
  4. const { jkesRequest } = require('./request.js')
  5. function normStatusValue(v) {
  6. if (v == null) return ''
  7. if (typeof v === 'object' && v.value != null) return String(v.value).toUpperCase()
  8. return String(v).toUpperCase()
  9. }
  10. function isEndRecord(r) {
  11. const s = normStatusValue(r.status)
  12. return s === 'END'
  13. }
  14. function isIncampusRecord(r) {
  15. const s = normStatusValue(r.dataStatus)
  16. return s === 'INCAMPUS'
  17. }
  18. function parseOneTime(v) {
  19. if (v == null) return NaN
  20. if (typeof v === 'number' && v > 1e12) return v
  21. if (typeof v === 'number' && v > 1e9 && v < 1e12) return v * 1000
  22. const str = String(v).trim()
  23. if (/^\d+$/.test(str)) {
  24. const n = parseInt(str, 10)
  25. if (n > 1e12) return n
  26. if (n > 1e9) return n * 1000
  27. }
  28. const t = Date.parse(str.replace(/-/g, '/'))
  29. return Number.isFinite(t) ? t : NaN
  30. }
  31. /** 归属月份:优先结束时间,否则开始时间 */
  32. function parseRecordMonthTime(r) {
  33. const tEnd = parseOneTime(r.endTime ?? r.end_time ?? r.finishTime)
  34. if (Number.isFinite(tEnd)) return tEnd
  35. const tBegin = parseOneTime(r.beginTime ?? r.startTime ?? r.createTime ?? r.deviceTime)
  36. return Number.isFinite(tBegin) ? tBegin : NaN
  37. }
  38. /**
  39. * 单条记录距离 → 千米(真机 mylist / end 返回的 distance 为米,如 6324.01≈6.324km)。
  40. * END+校内且 distance 为 0:官方尚未写入里程,返回 0(不计入汇总,待列表更新后再算)。
  41. */
  42. function recordDistanceKm(r) {
  43. const raw =
  44. r.distance ??
  45. r.info?.distance ??
  46. r.runDistance ??
  47. r.mileage ??
  48. r.totalDistance ??
  49. r.length
  50. let d = typeof raw === 'string' ? parseFloat(raw.trim()) : Number(raw)
  51. if (!Number.isFinite(d) || d <= 0) return 0
  52. const km = d / 1000
  53. if (!Number.isFinite(km) || km <= 0) return 0
  54. return Math.round(km * 1000) / 1000
  55. }
  56. function extractPage(apiData) {
  57. if (!apiData || apiData.code !== 0 || !apiData.data) return null
  58. const d = apiData.data
  59. if (d.page && (Array.isArray(d.page.records) || d.page.records === undefined)) {
  60. return {
  61. records: Array.isArray(d.page.records) ? d.page.records : [],
  62. pages: Number(d.page.pages) || 1,
  63. current: Number(d.page.current) || 1
  64. }
  65. }
  66. if (Array.isArray(d.records)) {
  67. return {
  68. records: d.records,
  69. pages: Number(d.pages) || 1,
  70. current: Number(d.current) || 1
  71. }
  72. }
  73. if (Array.isArray(d.list)) {
  74. return { records: d.list, pages: Number(d.pages) || 1, current: Number(d.page) || 1 }
  75. }
  76. return null
  77. }
  78. function parseRecordSpeed(r) {
  79. const raw = r.speed ?? r.info?.speed
  80. const n = Number(raw)
  81. return Number.isFinite(n) ? n : 0
  82. }
  83. /**
  84. * 统计本月「结束且校内」的跑步距离之和(千米)
  85. */
  86. async function fetchJkesMonthKm(token, year, month1to12) {
  87. const monthStart = new Date(year, month1to12 - 1, 1).getTime()
  88. const monthEnd = new Date(year, month1to12, 0, 23, 59, 59, 999).getTime()
  89. let page = 1
  90. let totalKm = 0
  91. const pageSize = 50
  92. let pages = 1
  93. while (page <= pages) {
  94. const url = `/health/runRecord/mylist?pageSize=${pageSize}&page=${page}`
  95. const data = await jkesRequest(url, {}, token)
  96. const pg = extractPage(data)
  97. if (!pg) break
  98. pages = Number(pg.pages) || 1
  99. const records = pg.records || []
  100. for (const r of records) {
  101. if (!isEndRecord(r)) continue
  102. if (!isIncampusRecord(r)) continue
  103. const t = parseRecordMonthTime(r)
  104. if (!Number.isFinite(t) || t < monthStart || t > monthEnd) continue
  105. totalKm += recordDistanceKm(r)
  106. }
  107. page += 1
  108. if (records.length === 0) break
  109. }
  110. console.log(`[JKES] 本月跑步距离: ${totalKm} km`)
  111. return Math.round(totalKm * 1000) / 1000
  112. }
  113. /**
  114. * 全量有效记录里程(千米),用于 total_num 近似同步
  115. */
  116. async function fetchJkesTotalKm(token) {
  117. let page = 1
  118. let totalKm = 0
  119. const pageSize = 50
  120. let pages = 1
  121. while (page <= pages) {
  122. const url = `/health/runRecord/mylist?pageSize=${pageSize}&page=${page}`
  123. const data = await jkesRequest(url, {}, token)
  124. const pg = extractPage(data)
  125. if (!pg) break
  126. pages = Number(pg.pages) || 1
  127. const records = pg.records || []
  128. for (const r of records) {
  129. if (!isEndRecord(r)) continue
  130. if (!isIncampusRecord(r)) continue
  131. totalKm += recordDistanceKm(r)
  132. }
  133. page += 1
  134. if (records.length === 0) break
  135. }
  136. return Math.round(totalKm * 1000) / 1000
  137. }
  138. /** end 接口返回的 info 与列表记录字段兼容 */
  139. function distanceKmFromEndInfo(info) {
  140. return recordDistanceKm(info || {})
  141. }
  142. /**
  143. * 按记录 ID 在 mylist 分页中查找。
  144. * 刚结束时可能出现 END+INCAMPUS 但 distance/speed 尚未更新。
  145. */
  146. async function fetchJkesRecordById(token, recordId, maxPages = 8) {
  147. const id = String(recordId || '').trim()
  148. if (!id) return null
  149. let page = 1
  150. const pageSize = 50
  151. let pages = 1
  152. while (page <= pages && page <= maxPages) {
  153. const url = `/health/runRecord/mylist?pageSize=${pageSize}&page=${page}`
  154. const data = await jkesRequest(url, {}, token)
  155. const pg = extractPage(data)
  156. if (!pg) return null
  157. pages = Number(pg.pages) || 1
  158. const hit = (pg.records || []).find((x) => String(x?.id) === id)
  159. if (hit) return hit
  160. page += 1
  161. }
  162. return null
  163. }
  164. function isJkesRecordValidInCampus(r) {
  165. return !!r && isEndRecord(r) && isIncampusRecord(r)
  166. }
  167. /** 完整同步:校内有效 + 距离>0 + 配速/速度>0 */
  168. function isJkesRecordFullySynced(r) {
  169. if (!isJkesRecordValidInCampus(r)) return false
  170. return recordDistanceKm(r) > 0 && parseRecordSpeed(r) > 0
  171. }
  172. module.exports = {
  173. fetchJkesMonthKm,
  174. fetchJkesTotalKm,
  175. recordDistanceKm,
  176. distanceKmFromEndInfo,
  177. extractPage,
  178. fetchJkesRecordById,
  179. isJkesRecordValidInCampus,
  180. isJkesRecordFullySynced
  181. }