const API = require('../../lib/API') const db = require('../../plugin/DataBase/db') const AccessControl = require('../../lib/AccessControl') const { BaseStdResponse } = require('../../BaseStdResponse') const { checkQuota } = require('../../lib/AIChat/QuotaService') const { getUserVipInfo } = require('../../lib/VipService') const WeixinBindingService = require('../../lib/AIChat/WeixinBindingService') function getSemesterRange() { const now = new Date() const year = now.getFullYear() const feb1 = new Date(year, 1, 1, 0, 0, 0, 0) const aug31 = new Date(year, 7, 31, 0, 0, 0, 0) const start = now >= feb1 && now < aug31 ? feb1 : new Date(now < feb1 ? year - 1 : year, 7, 31, 0, 0, 0, 0) const end = new Date(now.getTime() + 24 * 60 * 60 * 1000) return { start: start.getTime(), end: end.getTime() } } function compactQuota(quota) { const configured = Boolean(quota?.configured) const limit = Number(quota?.limit || 0) const used = Number(quota?.used || 0) const remaining = Number(quota?.remaining || Math.max(0, limit - used)) return { configured, limit, used, remaining, percent: configured && limit > 0 ? Math.max(0, Math.min(100, Math.round((remaining / limit) * 100))) : 0 } } function dateKey(date) { const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${month}-${day}` } function fillTrend(rows) { const rowMap = new Map((rows || []).map(row => [row.day_label, row])) const today = new Date() const list = [] for (let i = 13; i >= 0; i--) { const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i) const key = dateKey(date) const row = rowMap.get(key) || {} list.push({ date: key, delta: Number(row.delta || 0), income: Number(row.income || 0), expense: Math.abs(Number(row.expense || 0)) }) } return list } class AssistantDashboard extends API { constructor() { super() this.setPath('/AIChat/Assistant/Dashboard') this.setMethod('GET') } async onRequest(req, res) { const { uuid, session } = req.query if ([uuid, session].some(value => value === '' || value === null || value === undefined)) { return res.json({ ...BaseStdResponse.MISSING_PARAMETER }) } if (!await AccessControl.checkSession(uuid, session)) { return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED }) } const semester = getSemesterRange() const [userRows, accountRows, ledgerRows, webQuota, wechatQuota, vipInfo, binding] = await Promise.all([ db.query('SELECT ic_count, lepao_count, vip, vip_expire_time FROM users WHERE uuid = ? LIMIT 1', [uuid]), db.query( `SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END), 0) AS normal, COALESCE(SUM(CASE WHEN state = 1 THEN 0 ELSE 1 END), 0) AS need_login, COALESCE(SUM(CASE WHEN auto_run = 1 THEN 1 ELSE 0 END), 0) AS auto_run FROM lepao_account WHERE create_user = ? AND ((update_time >= ? AND update_time < ?) OR (create_time >= ? AND create_time < ?))`, [uuid, semester.start, semester.end, semester.start, semester.end] ), db.query( `SELECT DATE_FORMAT(created_at, '%m-%d') AS day_label, COALESCE(SUM(delta), 0) AS delta, COALESCE(SUM(CASE WHEN delta > 0 THEN delta ELSE 0 END), 0) AS income, COALESCE(SUM(CASE WHEN delta < 0 THEN delta ELSE 0 END), 0) AS expense FROM lepao_count_ledger WHERE user_uuid = ? AND created_at >= DATE_SUB(CURDATE(), INTERVAL 13 DAY) GROUP BY DATE_FORMAT(created_at, '%m-%d') ORDER BY MIN(created_at) ASC`, [uuid] ), checkQuota({ uuid, channel: 'web' }), checkQuota({ uuid, channel: 'wechat' }), getUserVipInfo(uuid), WeixinBindingService.getBindingByUser(uuid) ]) if (!userRows || !accountRows || !ledgerRows) { return res.json({ ...BaseStdResponse.DATABASE_ERR }) } const user = userRows[0] || {} const account = accountRows[0] || {} const total = Number(account.total || 0) const normal = Number(account.normal || 0) const needLogin = Number(account.need_login || 0) const autoRun = Number(account.auto_run || 0) const compactBinding = binding ? { state: Number(binding.state), bound: Number(binding.state) === 1, last_user_message_time: Number(binding.last_user_message_time || 0), bind_time: Number(binding.bind_time || 0), last_error: binding.last_error || '' } : null return res.json({ ...BaseStdResponse.OK, data: { user: { lepao_count: Number(user.lepao_count || 0), ic_count: Number(user.ic_count || 0), vip: Boolean(vipInfo.vip), vip_expire_time: Number(vipInfo.vip_expire_time || 0) }, semester: { start: semester.start, end: semester.end }, account: { total, normal, need_login: needLogin, auto_run: autoRun, manual: Math.max(0, total - autoRun), normal_percent: total > 0 ? Math.round((normal / total) * 100) : 0, auto_percent: total > 0 ? Math.round((autoRun / total) * 100) : 0 }, quota: { vip: Boolean(vipInfo.vip), vip_expire_time: Number(vipInfo.vip_expire_time || 0), web: compactQuota(webQuota), wechat: compactQuota(wechatQuota) }, weixin: compactBinding, ledger_trend: fillTrend(ledgerRows) } }) } } module.exports.AssistantDashboard = AssistantDashboard