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 = '
+ 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
});
});