perf: 优化数据库查询和前端代码质量
- 添加 records 表常用字段索引提升查询性能 - 合并月度/老板/陪玩记录查询减少数据库往返 - 提取重复的横向柱状图渲染逻辑为通用函数 - 移除调试用的 console.log 语句 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
14927f74aa
commit
0b450a65ed
@@ -64,7 +64,6 @@ async function openTakeRanking() {
|
|||||||
const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() });
|
const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log('take-ranking data:', data);
|
|
||||||
const rows = data.rows || [];
|
const rows = data.rows || [];
|
||||||
|
|
||||||
// 显示统计卡片
|
// 显示统计卡片
|
||||||
|
|||||||
+26
-27
@@ -265,24 +265,36 @@ async function loadBossRankingPreview() {
|
|||||||
} catch (e) { console.error(e); }
|
} 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) {
|
if (!rows || rows.length === 0) {
|
||||||
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
||||||
return;
|
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) => {
|
container.innerHTML = rows.map((r, i) => {
|
||||||
const pct = Math.max((r.total_income / max) * 100, 2);
|
const pct = Math.max((getValue(r) / max) * 100, 2);
|
||||||
return `<div class="hbar-row" onclick="openBossDetail('${r.boss.replace(/'/g, "\\'")}', 'homePage')">
|
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, '"')}, 'homePage')">
|
||||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
<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-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>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBossRankingPreview(rows) {
|
||||||
|
renderHBarRanking(rows, {
|
||||||
|
containerId: 'rankingPreviewList',
|
||||||
|
getValue: r => r.total_income,
|
||||||
|
getLabel: r => r.boss,
|
||||||
|
onClick: 'openBossDetail'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let currentBossName = '';
|
let currentBossName = '';
|
||||||
let bossDetailFrom = 'homePage';
|
let bossDetailFrom = 'homePage';
|
||||||
let bossDetailYear = 0;
|
let bossDetailYear = 0;
|
||||||
@@ -498,21 +510,13 @@ async function loadPartnerRankingPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPartnerRankingPreview(rows) {
|
function renderPartnerRankingPreview(rows) {
|
||||||
const container = document.getElementById('partnerRankingPreviewList');
|
renderHBarRanking(rows, {
|
||||||
if (!rows || rows.length === 0) {
|
containerId: 'partnerRankingPreviewList',
|
||||||
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
getValue: r => r.dispatch_count,
|
||||||
return;
|
getLabel: r => r.partner,
|
||||||
}
|
onClick: 'openPartnerDetail',
|
||||||
const max = Math.max(...rows.map(r => r.dispatch_count), 1);
|
formatValue: v => v + '单'
|
||||||
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('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPartnerRankingPage() {
|
function openPartnerRankingPage() {
|
||||||
@@ -537,20 +541,15 @@ async function loadPartnerRankingDetail() {
|
|||||||
params.push('endDate=' + formatLocalDate(d));
|
params.push('endDate=' + formatLocalDate(d));
|
||||||
}
|
}
|
||||||
if (params.length) url += '?' + params.join('&');
|
if (params.length) url += '?' + params.join('&');
|
||||||
console.log('partner-ranking url:', url);
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { headers: headers() });
|
const res = await fetch(url, { headers: headers() });
|
||||||
console.log('partner-ranking res.ok:', res.ok);
|
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log('partner-ranking data:', data);
|
|
||||||
const rows = data.rows || [];
|
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 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);
|
const totalDispatch = rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0);
|
||||||
console.log('totalDispatch:', totalDispatch);
|
|
||||||
|
|
||||||
// 更新统计卡片
|
// 更新统计卡片
|
||||||
document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch;
|
document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch;
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ db.exec(`
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
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 约束
|
// 迁移:去掉 category 的 CHECK 约束
|
||||||
|
|||||||
+28
-46
@@ -40,23 +40,19 @@ router.get('/monthly-records', (req, res) => {
|
|||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||||
const offset = (Number(page) - 1) * Number(limit);
|
const offset = (Number(page) - 1) * Number(limit);
|
||||||
|
|
||||||
const summary = db.prepare(`
|
// 合并 summary 和 total 为一次查询
|
||||||
SELECT COALESCE(SUM(amount), 0) as 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') < ?
|
FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
`).get(req.user.id, type, startDate, endDate);
|
`).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(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records
|
SELECT * FROM records
|
||||||
WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, type, startDate, endDate, Number(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];
|
dateParams = [startDate, endDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
const myIncomeSummary = db.prepare(`
|
// 合并 summary 查询为一次
|
||||||
SELECT COALESCE(SUM(amount), 0) as total
|
const summary = db.prepare(`
|
||||||
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income'${dateFilter}
|
|
||||||
`).get(req.user.id, boss, ...dateParams);
|
|
||||||
|
|
||||||
const expenseSummary = db.prepare(`
|
|
||||||
SELECT
|
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 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 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 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 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}
|
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
|
||||||
`).get(req.user.id, boss, ...dateParams);
|
`).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(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
|
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);
|
`).all(req.user.id, boss, ...dateParams, Number(limit), offset);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
my_income: myIncomeSummary.total,
|
my_income: summary.my_income,
|
||||||
boss_total_expense: boss_total_expense,
|
boss_total_expense: summary.dispatch_expense + summary.order_expense + summary.redEnvelope_expense,
|
||||||
take_count: expenseSummary.take_count,
|
take_count: summary.take_count,
|
||||||
dispatch_count: expenseSummary.dispatch_count,
|
dispatch_count: summary.dispatch_count,
|
||||||
records,
|
records,
|
||||||
page: Number(page),
|
page: Number(page),
|
||||||
limit: Number(limit),
|
limit: Number(limit),
|
||||||
totalCount: total
|
totalCount: summary.count
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,26 +298,19 @@ router.get('/partner-records', (req, res) => {
|
|||||||
dateParams = [startDate, endDate];
|
dateParams = [startDate, endDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchStats = db.prepare(`
|
// 合并 summary 查询为一次
|
||||||
|
const summary = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(quantity), 0) as dispatch_count,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 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(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(amount), 0) as rebate_income
|
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as rebate_income,
|
||||||
FROM records WHERE user_id = ? AND category = '派单' AND partner = ?${dateFilter}
|
COALESCE(SUM(CASE WHEN type = 'income' AND category != '派单' AND category != '接单' AND source = ? THEN amount ELSE 0 END), 0) as other_income,
|
||||||
`).get(req.user.id, partner, ...dateParams);
|
COUNT(*) as count
|
||||||
|
FROM records WHERE user_id = ? AND (
|
||||||
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 (
|
|
||||||
(category = '派单' AND partner = ?) OR
|
(category = '派单' AND partner = ?) OR
|
||||||
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
||||||
)${dateFilter}
|
)${dateFilter}
|
||||||
`).get(req.user.id, partner, partner, ...dateParams).count;
|
`).get(partner, req.user.id, partner, ...dateParams);
|
||||||
|
|
||||||
const records = db.prepare(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records
|
SELECT * FROM records
|
||||||
@@ -341,13 +322,14 @@ router.get('/partner-records', (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
dispatch_count: dispatchStats.dispatch_count,
|
dispatch_count: dispatchStats.dispatch_count,
|
||||||
spread_income: dispatchStats.spread_income,
|
dispatch_count: summary.dispatch_count,
|
||||||
rebate_income: dispatchStats.rebate_income,
|
spread_income: summary.spread_income,
|
||||||
other_income: otherIncome.total,
|
rebate_income: summary.rebate_income,
|
||||||
|
other_income: summary.other_income,
|
||||||
records,
|
records,
|
||||||
page: Number(page),
|
page: Number(page),
|
||||||
limit: Number(limit),
|
limit: Number(limit),
|
||||||
totalCount: total
|
totalCount: summary.count
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user