perf: 优化数据库查询和前端代码质量

- 添加 records 表常用字段索引提升查询性能
- 合并月度/老板/陪玩记录查询减少数据库往返
- 提取重复的横向柱状图渲染逻辑为通用函数
- 移除调试用的 console.log 语句

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lizhilun
2026-03-19 10:22:15 +08:00
co-authored by Claude Opus 4.6
parent 14927f74aa
commit 0b450a65ed
4 changed files with 62 additions and 74 deletions
-1
View File
@@ -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 || [];
// 显示统计卡片
+26 -27
View File
@@ -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 = '<div class="ranking-empty">暂无数据</div>';
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 `<div class="hbar-row" onclick="openBossDetail('${r.boss.replace(/'/g, "\\'")}', 'homePage')">
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 `<div class="hbar-row" onclick="${onClick}(${JSON.stringify(label).replace(/"/g, '&quot;')}, 'homePage')">
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
<div class="hbar-label" title="${r.boss}">${r.boss}</div>
<div class="hbar-label" title="${label}">${label}</div>
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
<div class="hbar-value">${r.total_income.toFixed(2)}</div>
<div class="hbar-value">${value}</div>
</div>`;
}).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 = '<div class="ranking-empty">暂无数据</div>';
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 `<div class="hbar-row" onclick="openPartnerDetail('${r.partner.replace(/'/g, "\\'")}', 'homePage')">
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
<div class="hbar-label" title="${r.partner}">${r.partner}</div>
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
<div class="hbar-value">${r.dispatch_count}单</div>
</div>`;
}).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;
+8
View File
@@ -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 约束
+28 -46
View File
@@ -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
});
});