AssistantDashboard.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. const API = require('../../lib/API')
  2. const db = require('../../plugin/DataBase/db')
  3. const AccessControl = require('../../lib/AccessControl')
  4. const { BaseStdResponse } = require('../../BaseStdResponse')
  5. const { checkQuota } = require('../../lib/AIChat/QuotaService')
  6. const { getUserVipInfo } = require('../../lib/VipService')
  7. const WeixinBindingService = require('../../lib/AIChat/WeixinBindingService')
  8. function getSemesterRange() {
  9. const now = new Date()
  10. const year = now.getFullYear()
  11. const feb1 = new Date(year, 1, 1, 0, 0, 0, 0)
  12. const aug31 = new Date(year, 7, 31, 0, 0, 0, 0)
  13. const start = now >= feb1 && now < aug31
  14. ? feb1
  15. : new Date(now < feb1 ? year - 1 : year, 7, 31, 0, 0, 0, 0)
  16. const end = new Date(now.getTime() + 24 * 60 * 60 * 1000)
  17. return { start: start.getTime(), end: end.getTime() }
  18. }
  19. function compactQuota(quota) {
  20. const configured = Boolean(quota?.configured)
  21. const limit = Number(quota?.limit || 0)
  22. const used = Number(quota?.used || 0)
  23. const remaining = Number(quota?.remaining || Math.max(0, limit - used))
  24. return {
  25. configured,
  26. limit,
  27. used,
  28. remaining,
  29. percent: configured && limit > 0 ? Math.max(0, Math.min(100, Math.round((remaining / limit) * 100))) : 0
  30. }
  31. }
  32. function dateKey(date) {
  33. const month = String(date.getMonth() + 1).padStart(2, '0')
  34. const day = String(date.getDate()).padStart(2, '0')
  35. return `${month}-${day}`
  36. }
  37. function fillTrend(rows) {
  38. const rowMap = new Map((rows || []).map(row => [row.day_label, row]))
  39. const today = new Date()
  40. const list = []
  41. for (let i = 13; i >= 0; i--) {
  42. const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i)
  43. const key = dateKey(date)
  44. const row = rowMap.get(key) || {}
  45. list.push({
  46. date: key,
  47. delta: Number(row.delta || 0),
  48. income: Number(row.income || 0),
  49. expense: Math.abs(Number(row.expense || 0))
  50. })
  51. }
  52. return list
  53. }
  54. class AssistantDashboard extends API {
  55. constructor() {
  56. super()
  57. this.setPath('/AIChat/Assistant/Dashboard')
  58. this.setMethod('GET')
  59. }
  60. async onRequest(req, res) {
  61. const { uuid, session } = req.query
  62. if ([uuid, session].some(value => value === '' || value === null || value === undefined)) {
  63. return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
  64. }
  65. if (!await AccessControl.checkSession(uuid, session)) {
  66. return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
  67. }
  68. const semester = getSemesterRange()
  69. const [userRows, accountRows, ledgerRows, webQuota, wechatQuota, vipInfo, binding] = await Promise.all([
  70. db.query('SELECT ic_count, lepao_count, vip, vip_expire_time FROM users WHERE uuid = ? LIMIT 1', [uuid]),
  71. db.query(
  72. `SELECT
  73. COUNT(*) AS total,
  74. COALESCE(SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END), 0) AS normal,
  75. COALESCE(SUM(CASE WHEN state = 1 THEN 0 ELSE 1 END), 0) AS need_login,
  76. COALESCE(SUM(CASE WHEN auto_run = 1 THEN 1 ELSE 0 END), 0) AS auto_run
  77. FROM lepao_account
  78. WHERE create_user = ?
  79. AND ((update_time >= ? AND update_time < ?) OR (create_time >= ? AND create_time < ?))`,
  80. [uuid, semester.start, semester.end, semester.start, semester.end]
  81. ),
  82. db.query(
  83. `SELECT
  84. DATE_FORMAT(created_at, '%m-%d') AS day_label,
  85. COALESCE(SUM(delta), 0) AS delta,
  86. COALESCE(SUM(CASE WHEN delta > 0 THEN delta ELSE 0 END), 0) AS income,
  87. COALESCE(SUM(CASE WHEN delta < 0 THEN delta ELSE 0 END), 0) AS expense
  88. FROM lepao_count_ledger
  89. WHERE user_uuid = ? AND created_at >= DATE_SUB(CURDATE(), INTERVAL 13 DAY)
  90. GROUP BY DATE_FORMAT(created_at, '%m-%d')
  91. ORDER BY MIN(created_at) ASC`,
  92. [uuid]
  93. ),
  94. checkQuota({ uuid, channel: 'web' }),
  95. checkQuota({ uuid, channel: 'wechat' }),
  96. getUserVipInfo(uuid),
  97. WeixinBindingService.getBindingByUser(uuid)
  98. ])
  99. if (!userRows || !accountRows || !ledgerRows) {
  100. return res.json({ ...BaseStdResponse.DATABASE_ERR })
  101. }
  102. const user = userRows[0] || {}
  103. const account = accountRows[0] || {}
  104. const total = Number(account.total || 0)
  105. const normal = Number(account.normal || 0)
  106. const needLogin = Number(account.need_login || 0)
  107. const autoRun = Number(account.auto_run || 0)
  108. const compactBinding = binding ? {
  109. state: Number(binding.state),
  110. bound: Number(binding.state) === 1,
  111. last_user_message_time: Number(binding.last_user_message_time || 0),
  112. bind_time: Number(binding.bind_time || 0),
  113. last_error: binding.last_error || ''
  114. } : null
  115. return res.json({
  116. ...BaseStdResponse.OK,
  117. data: {
  118. user: {
  119. lepao_count: Number(user.lepao_count || 0),
  120. ic_count: Number(user.ic_count || 0),
  121. vip: Boolean(vipInfo.vip),
  122. vip_expire_time: Number(vipInfo.vip_expire_time || 0)
  123. },
  124. semester: {
  125. start: semester.start,
  126. end: semester.end
  127. },
  128. account: {
  129. total,
  130. normal,
  131. need_login: needLogin,
  132. auto_run: autoRun,
  133. manual: Math.max(0, total - autoRun),
  134. normal_percent: total > 0 ? Math.round((normal / total) * 100) : 0,
  135. auto_percent: total > 0 ? Math.round((autoRun / total) * 100) : 0
  136. },
  137. quota: {
  138. vip: Boolean(vipInfo.vip),
  139. vip_expire_time: Number(vipInfo.vip_expire_time || 0),
  140. web: compactQuota(webQuota),
  141. wechat: compactQuota(wechatQuota)
  142. },
  143. weixin: compactBinding,
  144. ledger_trend: fillTrend(ledgerRows)
  145. }
  146. })
  147. }
  148. }
  149. module.exports.AssistantDashboard = AssistantDashboard