diff --git a/public/js/app.js b/public/js/app.js index 315e21f..04ed447 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -64,7 +64,6 @@ async function openTakeRanking() { const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() }); if (!res.ok) return; const data = await res.json(); - console.log('take-ranking data:', data); const rows = data.rows || []; // 显示统计卡片 diff --git a/public/js/stats.js b/public/js/stats.js index 85dfee9..338df18 100644 --- a/public/js/stats.js +++ b/public/js/stats.js @@ -265,24 +265,36 @@ async function loadBossRankingPreview() { } catch (e) { console.error(e); } } -function renderBossRankingPreview(rows) { - const container = document.getElementById('rankingPreviewList'); +// 渲染横向柱状图排行榜的通用函数 +function renderHBarRanking(rows, { containerId, getValue, getLabel, onClick, formatValue }) { + const container = document.getElementById(containerId); if (!rows || rows.length === 0) { container.innerHTML = '
暂无数据
'; return; } - const max = Math.max(...rows.map(r => r.total_income), 1); + const max = Math.max(...rows.map(r => getValue(r)), 1); container.innerHTML = rows.map((r, i) => { - const pct = Math.max((r.total_income / max) * 100, 2); - return `
+ const pct = Math.max((getValue(r) / max) * 100, 2); + const label = getLabel(r).replace(/'/g, "\\'"); + const value = formatValue ? formatValue(getValue(r)) : (getValue(r).toFixed ? getValue(r).toFixed(2) : getValue(r)); + return `
${i + 1}
-
${r.boss}
+
${label}
-
${r.total_income.toFixed(2)}
+
${value}
`; }).join(''); } +function renderBossRankingPreview(rows) { + renderHBarRanking(rows, { + containerId: 'rankingPreviewList', + getValue: r => r.total_income, + getLabel: r => r.boss, + onClick: 'openBossDetail' + }); +} + let currentBossName = ''; let bossDetailFrom = 'homePage'; let bossDetailYear = 0; @@ -498,21 +510,13 @@ async function loadPartnerRankingPreview() { } function renderPartnerRankingPreview(rows) { - const container = document.getElementById('partnerRankingPreviewList'); - if (!rows || rows.length === 0) { - container.innerHTML = '
暂无数据
'; - return; - } - const max = Math.max(...rows.map(r => r.dispatch_count), 1); - container.innerHTML = rows.map((r, i) => { - const pct = Math.max((r.dispatch_count / max) * 100, 2); - return `
-
${i + 1}
-
${r.partner}
-
-
${r.dispatch_count}单
-
`; - }).join(''); + renderHBarRanking(rows, { + containerId: 'partnerRankingPreviewList', + getValue: r => r.dispatch_count, + getLabel: r => r.partner, + onClick: 'openPartnerDetail', + formatValue: v => v + '单' + }); } function openPartnerRankingPage() { @@ -537,20 +541,15 @@ async function loadPartnerRankingDetail() { params.push('endDate=' + formatLocalDate(d)); } if (params.length) url += '?' + params.join('&'); - console.log('partner-ranking url:', url); try { const res = await fetch(url, { headers: headers() }); - console.log('partner-ranking res.ok:', res.ok); if (!res.ok) return; const data = await res.json(); - console.log('partner-ranking data:', data); const rows = data.rows || []; - console.log('rows:', rows); const summary = data.summary || { total_partner_income: 0, total_rebate_income: 0, total_milkTea_income: 0 }; // 计算派单总数(从列表) const totalDispatch = rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0); - console.log('totalDispatch:', totalDispatch); // 更新统计卡片 document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch; diff --git a/server/db.js b/server/db.js index 8965e48..ee0caa7 100644 --- a/server/db.js +++ b/server/db.js @@ -32,6 +32,14 @@ db.exec(` created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); + + CREATE INDEX IF NOT EXISTS idx_records_user_id ON records(user_id); + CREATE INDEX IF NOT EXISTS idx_records_category ON records(category); + CREATE INDEX IF NOT EXISTS idx_records_created_at ON records(created_at); + CREATE INDEX IF NOT EXISTS idx_records_type ON records(type); + CREATE INDEX IF NOT EXISTS idx_records_boss ON records(boss); + CREATE INDEX IF NOT EXISTS idx_records_partner ON records(partner); + CREATE INDEX IF NOT EXISTS idx_records_source ON records(source); `); // 迁移:去掉 category 的 CHECK 约束 diff --git a/server/routes/stats.js b/server/routes/stats.js index 96ce653..96b3aa4 100644 --- a/server/routes/stats.js +++ b/server/routes/stats.js @@ -40,23 +40,19 @@ router.get('/monthly-records', (req, res) => { const { startDate, endDate } = getMonthRange(req.query.year, req.query.month); const offset = (Number(page) - 1) * Number(limit); - const summary = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total + // 合并 summary 和 total 为一次查询 + const stats = db.prepare(` + SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as count FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? `).get(req.user.id, type, startDate, endDate); - const total = db.prepare(` - SELECT COUNT(*) as count FROM records - WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? - `).get(req.user.id, type, startDate, endDate).count; - const records = db.prepare(` SELECT * FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ? `).all(req.user.id, type, startDate, endDate, Number(limit), offset); - res.json({ total: summary.total, records, page: Number(page), limit: Number(limit), totalCount: total }); + res.json({ total: stats.total, records, page: Number(page), limit: Number(limit), totalCount: stats.count }); }); // 年度净收入按年统计 @@ -256,40 +252,32 @@ router.get('/boss-records', (req, res) => { dateParams = [startDate, endDate]; } - const myIncomeSummary = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total - FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income'${dateFilter} - `).get(req.user.id, boss, ...dateParams); - - const expenseSummary = db.prepare(` + // 合并 summary 查询为一次 + const summary = db.prepare(` SELECT + COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as my_income, COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as dispatch_expense, COALESCE(SUM(CASE WHEN category = '接单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as order_expense, COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_expense, COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count, - COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count + COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count, + COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} `).get(req.user.id, boss, ...dateParams); - const boss_total_expense = expenseSummary.dispatch_expense + expenseSummary.order_expense + expenseSummary.redEnvelope_expense; - - const total = db.prepare(` - SELECT COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} - `).get(req.user.id, boss, ...dateParams).count; - const records = db.prepare(` SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ? `).all(req.user.id, boss, ...dateParams, Number(limit), offset); res.json({ - my_income: myIncomeSummary.total, - boss_total_expense: boss_total_expense, - take_count: expenseSummary.take_count, - dispatch_count: expenseSummary.dispatch_count, + my_income: summary.my_income, + boss_total_expense: summary.dispatch_expense + summary.order_expense + summary.redEnvelope_expense, + take_count: summary.take_count, + dispatch_count: summary.dispatch_count, records, page: Number(page), limit: Number(limit), - totalCount: total + totalCount: summary.count }); }); @@ -310,26 +298,19 @@ router.get('/partner-records', (req, res) => { dateParams = [startDate, endDate]; } - const dispatchStats = db.prepare(` + // 合并 summary 查询为一次 + const summary = db.prepare(` SELECT - COALESCE(SUM(quantity), 0) as dispatch_count, - COALESCE(SUM(quantity * (COALESCE(unit_price, 0) - COALESCE(rebate, amount * 1.0 / CASE WHEN quantity > 0 THEN quantity ELSE 1 END))), 0) as spread_income, - COALESCE(SUM(amount), 0) as rebate_income - FROM records WHERE user_id = ? AND category = '派单' AND partner = ?${dateFilter} - `).get(req.user.id, partner, ...dateParams); - - const otherIncome = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total - FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ?${dateFilter} - `).get(req.user.id, partner, ...dateParams); - - const total = db.prepare(` - SELECT COUNT(*) as count FROM records - WHERE user_id = ? AND ( + COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count, + COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * (COALESCE(unit_price, 0) - COALESCE(rebate, amount * 1.0 / CASE WHEN quantity > 0 THEN quantity ELSE 1 END)) ELSE 0 END), 0) as spread_income, + COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as rebate_income, + COALESCE(SUM(CASE WHEN type = 'income' AND category != '派单' AND category != '接单' AND source = ? THEN amount ELSE 0 END), 0) as other_income, + COUNT(*) as count + FROM records WHERE user_id = ? AND ( (category = '派单' AND partner = ?) OR (type = 'income' AND category != '派单' AND category != '接单' AND source = ?) )${dateFilter} - `).get(req.user.id, partner, partner, ...dateParams).count; + `).get(partner, req.user.id, partner, ...dateParams); const records = db.prepare(` SELECT * FROM records @@ -341,13 +322,14 @@ router.get('/partner-records', (req, res) => { res.json({ dispatch_count: dispatchStats.dispatch_count, - spread_income: dispatchStats.spread_income, - rebate_income: dispatchStats.rebate_income, - other_income: otherIncome.total, + dispatch_count: summary.dispatch_count, + spread_income: summary.spread_income, + rebate_income: summary.rebate_income, + other_income: summary.other_income, records, page: Number(page), limit: Number(limit), - totalCount: total + totalCount: summary.count }); });