perf: 系统性能优化 - 数据库索引、缓存、GZIP压缩和查询合并
- 添加3个复合索引提升查询性能 (user_date, user_category, destination) - 添加GZIP压缩和静态资源缓存 (maxAge: 1h) - 添加API响应no-cache头 - 添加stats端点2分钟内存缓存 (per-user key, LRU淘汰) - 合并多次数据库查询为单次查询 - 添加滚动防抖改善移动端体验 - 修复res.once内存泄漏问题 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3a4048a29e
commit
df3a9daa65
Generated
+64
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3"
|
"jsonwebtoken": "^9.0.3"
|
||||||
@@ -190,6 +191,60 @@
|
|||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.1.0",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
@@ -905,6 +960,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3"
|
"jsonwebtoken": "^9.0.3"
|
||||||
|
|||||||
+14
-3
@@ -1,3 +1,12 @@
|
|||||||
|
// 防抖工具函数
|
||||||
|
function debounce(fn, delay) {
|
||||||
|
let timeoutId;
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => fn.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 全局状态变量(供 app.js 引用)
|
// 全局状态变量(供 app.js 引用)
|
||||||
var orderPage = 1, orderHasMore = true, orderLoading = false;
|
var orderPage = 1, orderHasMore = true, orderLoading = false;
|
||||||
var otherIncomePage = 1, otherIncomeHasMore = true, otherIncomeLoading = false;
|
var otherIncomePage = 1, otherIncomeHasMore = true, otherIncomeLoading = false;
|
||||||
@@ -97,8 +106,8 @@ function renderMilkTeaItem(r) {
|
|||||||
'<div class="order-item-bottom"><span>' + (r.source || '') + '</span><span>' + time + '</span></div></div>';
|
'<div class="order-item-bottom"><span>' + (r.source || '') + '</span><span>' + time + '</span></div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动加载
|
// 滚动加载(带防抖)
|
||||||
window.addEventListener('scroll', () => {
|
const handleListScroll = debounce(() => {
|
||||||
const pageListMap = [
|
const pageListMap = [
|
||||||
{ page: 'orderListPage', list: 'orderList' },
|
{ page: 'orderListPage', list: 'orderList' },
|
||||||
{ page: 'dispatchListPage', list: 'dispatchList' },
|
{ page: 'dispatchListPage', list: 'dispatchList' },
|
||||||
@@ -118,7 +127,9 @@ window.addEventListener('scroll', () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, 100);
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleListScroll);
|
||||||
|
|
||||||
// 兼容旧接口
|
// 兼容旧接口
|
||||||
async function loadOrders(append) { return loadList('orderList', append); }
|
async function loadOrders(append) { return loadList('orderList', append); }
|
||||||
|
|||||||
+11
-1
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const compression = require('compression');
|
||||||
|
|
||||||
const db = require('./db');
|
const db = require('./db');
|
||||||
const { router: authRouter, auth } = require('./routes/auth');
|
const { router: authRouter, auth } = require('./routes/auth');
|
||||||
@@ -12,9 +13,18 @@ const app = express();
|
|||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
app.use(compression());
|
||||||
|
app.use(express.static(path.join(__dirname, '..', 'public'), {
|
||||||
|
maxAge: '1h',
|
||||||
|
etag: true
|
||||||
|
}));
|
||||||
|
|
||||||
// 联系人自动补全
|
// 联系人自动补全
|
||||||
|
app.use('/api', (req, res, next) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/contacts', auth, (req, res) => {
|
app.get('/api/contacts', auth, (req, res) => {
|
||||||
const field = req.query.field;
|
const field = req.query.field;
|
||||||
const allowed = ['boss', 'partner', 'source', 'destination'];
|
const allowed = ['boss', 'partner', 'source', 'destination'];
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ db.exec(`
|
|||||||
CREATE INDEX IF NOT EXISTS idx_records_boss ON records(boss);
|
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_partner ON records(partner);
|
||||||
CREATE INDEX IF NOT EXISTS idx_records_source ON records(source);
|
CREATE INDEX IF NOT EXISTS idx_records_source ON records(source);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_user_date ON records(user_id, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_user_category ON records(user_id, category, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_destination ON records(destination);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 迁移:去掉 category 的 CHECK 约束
|
// 迁移:去掉 category 的 CHECK 约束
|
||||||
|
|||||||
+118
-79
@@ -4,6 +4,47 @@ const { getMonthRange } = require('../utils/helpers');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 内存缓存(限制大小,基于LRU)
|
||||||
|
const statsCache = new Map();
|
||||||
|
const CACHE_TTL = 2 * 60 * 1000;
|
||||||
|
const MAX_CACHE_SIZE = 100;
|
||||||
|
|
||||||
|
function getCached(key, fetchFn) {
|
||||||
|
const cached = statsCache.get(key);
|
||||||
|
if (cached && Date.now() - cached.ts < CACHE_TTL) {
|
||||||
|
// LRU: move to end
|
||||||
|
statsCache.delete(key);
|
||||||
|
statsCache.set(key, cached);
|
||||||
|
return Promise.resolve(cached.data);
|
||||||
|
}
|
||||||
|
if (statsCache.size >= MAX_CACHE_SIZE) {
|
||||||
|
// Delete oldest entry
|
||||||
|
const firstKey = statsCache.keys().next().value;
|
||||||
|
statsCache.delete(firstKey);
|
||||||
|
}
|
||||||
|
return fetchFn().then(data => {
|
||||||
|
statsCache.set(key, { data, ts: Date.now() });
|
||||||
|
return data;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Cache fetch error:', err.message);
|
||||||
|
return fetchFn(); // fallback to direct fetch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST/DELETE/PUT/PATCH 后清除当前用户的缓存
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
res.once('finish', () => {
|
||||||
|
if (['POST', 'DELETE', 'PUT', 'PATCH'].includes(req.method) && req.user?.id) {
|
||||||
|
for (const key of statsCache.keys()) {
|
||||||
|
if (key.includes(`:${req.user.id}:`)) {
|
||||||
|
statsCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// 格式化本地日期为 YYYY-MM-DD
|
// 格式化本地日期为 YYYY-MM-DD
|
||||||
function formatLocalDate(date) {
|
function formatLocalDate(date) {
|
||||||
const y = date.getFullYear();
|
const y = date.getFullYear();
|
||||||
@@ -15,6 +56,8 @@ function formatLocalDate(date) {
|
|||||||
// 本月统计
|
// 本月统计
|
||||||
router.get('/monthly', (req, res) => {
|
router.get('/monthly', (req, res) => {
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||||
|
const cacheKey = `monthly:${req.user.id}:${startDate}:${endDate}`;
|
||||||
|
getCached(cacheKey, () => {
|
||||||
const stats = db.prepare(`
|
const stats = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_order_count,
|
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_order_count,
|
||||||
@@ -30,7 +73,8 @@ router.get('/monthly', (req, res) => {
|
|||||||
FROM records
|
FROM records
|
||||||
WHERE user_id = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
WHERE user_id = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
`).get(req.user.id, startDate, endDate);
|
`).get(req.user.id, startDate, endDate);
|
||||||
res.json(stats);
|
return stats;
|
||||||
|
}).then(stats => res.json(stats));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 月收入/支出记录列表
|
// 月收入/支出记录列表
|
||||||
@@ -40,7 +84,6 @@ 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);
|
||||||
|
|
||||||
// 合并 summary 和 total 为一次查询
|
|
||||||
const stats = db.prepare(`
|
const stats = db.prepare(`
|
||||||
SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as count
|
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') < ?
|
||||||
@@ -57,19 +100,7 @@ router.get('/monthly-records', (req, res) => {
|
|||||||
|
|
||||||
// 年度净收入按年统计
|
// 年度净收入按年统计
|
||||||
router.get('/yearly-net-income', (req, res) => {
|
router.get('/yearly-net-income', (req, res) => {
|
||||||
// 获取所有年份数据
|
const combined = db.prepare(`
|
||||||
const yearlyStats = db.prepare(`
|
|
||||||
SELECT
|
|
||||||
strftime('%Y', created_at) as year,
|
|
||||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
|
||||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
|
||||||
FROM records
|
|
||||||
WHERE user_id = ?
|
|
||||||
GROUP BY year ORDER BY year DESC
|
|
||||||
`).all(req.user.id);
|
|
||||||
|
|
||||||
// 获取所有年份的每月数据
|
|
||||||
const monthlyByYear = db.prepare(`
|
|
||||||
SELECT
|
SELECT
|
||||||
strftime('%Y', created_at) as year,
|
strftime('%Y', created_at) as year,
|
||||||
strftime('%m', created_at) as month,
|
strftime('%m', created_at) as month,
|
||||||
@@ -77,33 +108,50 @@ router.get('/yearly-net-income', (req, res) => {
|
|||||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||||
FROM records
|
FROM records
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
GROUP BY year, month ORDER BY year DESC, month
|
GROUP BY year, month
|
||||||
`).all(req.user.id);
|
UNION ALL
|
||||||
|
|
||||||
// 按年份分组每月数据
|
|
||||||
const monthlyData = {};
|
|
||||||
monthlyByYear.forEach(m => {
|
|
||||||
if (!monthlyData[m.year]) monthlyData[m.year] = [];
|
|
||||||
monthlyData[m.year].push({
|
|
||||||
month: m.month,
|
|
||||||
total_income: m.total_income,
|
|
||||||
total_expense: m.total_expense
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const grandTotal = db.prepare(`
|
|
||||||
SELECT
|
SELECT
|
||||||
|
strftime('%Y', created_at) as year,
|
||||||
|
'00' as month,
|
||||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
||||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||||
FROM records WHERE user_id = ?
|
FROM records
|
||||||
`).get(req.user.id);
|
WHERE user_id = ?
|
||||||
|
GROUP BY year
|
||||||
|
`).all(req.user.id, req.user.id);
|
||||||
|
|
||||||
|
const yearlyStats = [];
|
||||||
|
const monthlyData = {};
|
||||||
|
let grandTotal = { total_income: 0, total_expense: 0 };
|
||||||
|
|
||||||
|
combined.forEach(row => {
|
||||||
|
grandTotal.total_income += row.total_income;
|
||||||
|
grandTotal.total_expense += row.total_expense;
|
||||||
|
if (row.month === '00') {
|
||||||
|
yearlyStats.push({
|
||||||
|
year: row.year,
|
||||||
|
total_income: row.total_income,
|
||||||
|
total_expense: row.total_expense
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!monthlyData[row.year]) monthlyData[row.year] = [];
|
||||||
|
monthlyData[row.year].push({
|
||||||
|
month: row.month,
|
||||||
|
total_income: row.total_income,
|
||||||
|
total_expense: row.total_expense
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yearlyStats.sort((a, b) => b.year - a.year);
|
||||||
res.json({ yearlyStats, monthlyData, grandTotal });
|
res.json({ yearlyStats, monthlyData, grandTotal });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 陪玩派单榜预览
|
// 陪玩派单榜预览
|
||||||
router.get('/partner-ranking/preview', (req, res) => {
|
router.get('/partner-ranking/preview', (req, res) => {
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||||
|
const cacheKey = `partner-ranking-preview:${req.user.id}:${startDate}:${endDate}`;
|
||||||
|
getCached(cacheKey, () => {
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT partner, COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(amount), 0) as dispatch_income
|
SELECT partner, COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(amount), 0) as dispatch_income
|
||||||
FROM records
|
FROM records
|
||||||
@@ -111,7 +159,8 @@ router.get('/partner-ranking/preview', (req, res) => {
|
|||||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
GROUP BY partner ORDER BY dispatch_count DESC LIMIT 10
|
GROUP BY partner ORDER BY dispatch_count DESC LIMIT 10
|
||||||
`).all(req.user.id, startDate, endDate);
|
`).all(req.user.id, startDate, endDate);
|
||||||
res.json(rows);
|
return rows;
|
||||||
|
}).then(rows => res.json(rows));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 陪玩派单榜详情
|
// 陪玩派单榜详情
|
||||||
@@ -141,24 +190,23 @@ router.get('/partner-ranking', (req, res) => {
|
|||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0);
|
row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0);
|
||||||
});
|
});
|
||||||
const summary = db.prepare(`
|
const summaryStats = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(quantity), 0) as total_dispatch_count,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as total_dispatch_count,
|
||||||
COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) as total_dispatch_income,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as total_dispatch_income,
|
||||||
COALESCE(SUM(amount), 0) as total_rebate_income,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as total_rebate_income,
|
||||||
COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) - COALESCE(SUM(amount), 0) as total_partner_income
|
COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as total_milkTea_income
|
||||||
FROM records
|
FROM records
|
||||||
WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != ''
|
WHERE user_id = ? AND type = 'income' AND category IN ('派单', '奶茶')
|
||||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
`).get(req.user.id, start, end);
|
`).get(req.user.id, start, end);
|
||||||
// 获取奶茶收入
|
const summary = {
|
||||||
const milkTeaStats = db.prepare(`
|
total_dispatch_count: summaryStats.total_dispatch_count,
|
||||||
SELECT COALESCE(SUM(amount), 0) as total_milkTea_income
|
total_dispatch_income: summaryStats.total_dispatch_income,
|
||||||
FROM records
|
total_rebate_income: summaryStats.total_rebate_income,
|
||||||
WHERE user_id = ? AND type = 'income' AND category = '奶茶'
|
total_partner_income: summaryStats.total_dispatch_income - summaryStats.total_rebate_income,
|
||||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
total_milkTea_income: summaryStats.total_milkTea_income
|
||||||
`).get(req.user.id, start, end);
|
};
|
||||||
summary.total_milkTea_income = milkTeaStats.total_milkTea_income;
|
|
||||||
res.json({ rows, summary });
|
res.json({ rows, summary });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,22 +217,18 @@ router.get('/take-ranking', (req, res) => {
|
|||||||
FROM records WHERE user_id = ? AND category = '接单' AND boss IS NOT NULL AND boss != ''
|
FROM records WHERE user_id = ? AND category = '接单' AND boss IS NOT NULL AND boss != ''
|
||||||
GROUP BY boss ORDER BY count DESC
|
GROUP BY boss ORDER BY count DESC
|
||||||
`).all(req.user.id);
|
`).all(req.user.id);
|
||||||
// 获取接单总收入和红包收入
|
|
||||||
const stats = db.prepare(`
|
const stats = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(quantity), 0) as total_take_count,
|
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as total_take_count,
|
||||||
COALESCE(SUM(amount), 0) as total_take_income
|
COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as total_take_income,
|
||||||
FROM records WHERE user_id = ? AND category = '接单'
|
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as total_redEnvelope_income
|
||||||
`).get(req.user.id);
|
FROM records WHERE user_id = ? AND type = 'income' AND category IN ('接单', '红包收入')
|
||||||
const redEnvelopeStats = db.prepare(`
|
|
||||||
SELECT COALESCE(SUM(amount), 0) as total_redEnvelope_income
|
|
||||||
FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入'
|
|
||||||
`).get(req.user.id);
|
`).get(req.user.id);
|
||||||
const result = {
|
const result = {
|
||||||
rows: rows,
|
rows: rows,
|
||||||
total_take_count: stats.total_take_count,
|
total_take_count: stats.total_take_count,
|
||||||
total_take_income: stats.total_take_income,
|
total_take_income: stats.total_take_income,
|
||||||
total_redEnvelope_income: redEnvelopeStats.total_redEnvelope_income
|
total_redEnvelope_income: stats.total_redEnvelope_income
|
||||||
};
|
};
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
@@ -204,19 +248,16 @@ router.get('/dispatch-ranking', (req, res) => {
|
|||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0);
|
row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0);
|
||||||
});
|
});
|
||||||
// 获取派单总数和奶茶收入
|
const stats = db.prepare(`
|
||||||
const dispatchTotal = db.prepare(`
|
SELECT
|
||||||
SELECT COALESCE(SUM(quantity), 0) as total_dispatch_count
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as total_dispatch_count,
|
||||||
FROM records WHERE user_id = ? AND category = '派单'
|
COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as total_milkTea_income
|
||||||
`).get(req.user.id);
|
FROM records WHERE user_id = ? AND type = 'income' AND category IN ('派单', '奶茶')
|
||||||
const milkTeaStats = db.prepare(`
|
|
||||||
SELECT COALESCE(SUM(amount), 0) as total_milkTea_income
|
|
||||||
FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶'
|
|
||||||
`).get(req.user.id);
|
`).get(req.user.id);
|
||||||
const result = {
|
const result = {
|
||||||
rows: rows,
|
rows: rows,
|
||||||
total_dispatch_count: dispatchTotal.total_dispatch_count,
|
total_dispatch_count: stats.total_dispatch_count,
|
||||||
total_milkTea_income: milkTeaStats.total_milkTea_income
|
total_milkTea_income: stats.total_milkTea_income
|
||||||
};
|
};
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
@@ -252,7 +293,6 @@ router.get('/boss-records', (req, res) => {
|
|||||||
dateParams = [startDate, endDate];
|
dateParams = [startDate, endDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并 summary 查询为一次
|
|
||||||
const summary = db.prepare(`
|
const summary = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as my_income,
|
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as my_income,
|
||||||
@@ -298,8 +338,6 @@ router.get('/partner-records', (req, res) => {
|
|||||||
dateParams = [startDate, endDate];
|
dateParams = [startDate, endDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并 summary 查询为一次
|
|
||||||
// 注意: source 在 CASE 和 WHERE 中各出现一次,都需要 partner 值
|
|
||||||
const summary = db.prepare(`
|
const summary = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
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,
|
||||||
@@ -347,15 +385,17 @@ router.get('/daily-income', (req, res) => {
|
|||||||
endDay.setDate(endDay.getDate() + 1);
|
endDay.setDate(endDay.getDate() + 1);
|
||||||
const endDate = formatLocalDate(endDay);
|
const endDate = formatLocalDate(endDay);
|
||||||
|
|
||||||
|
const cacheKey = `daily-income:${req.user.id}:${startDate}`;
|
||||||
|
getCached(cacheKey, () => {
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT DATE(created_at, 'localtime') as date, COALESCE(SUM(amount), 0) as income
|
SELECT DATE(created_at, 'localtime') as date, COALESCE(SUM(amount), 0) as income
|
||||||
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
GROUP BY DATE(created_at, 'localtime')
|
GROUP BY DATE(created_at, 'localtime')
|
||||||
`).all(req.user.id, startDate, endDate);
|
`).all(req.user.id, startDate, endDate);
|
||||||
|
|
||||||
const map = {};
|
const map = {};
|
||||||
rows.forEach(r => { map[r.date] = r.income; });
|
rows.forEach(r => { map[r.date] = r.income; });
|
||||||
res.json(days.map(d => ({ date: d, income: map[d] || 0 })));
|
return days.map(d => ({ date: d, income: map[d] || 0 }));
|
||||||
|
}).then(data => res.json(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 某天收入记录详情
|
// 某天收入记录详情
|
||||||
@@ -367,26 +407,24 @@ router.get('/daily-detail', (req, res) => {
|
|||||||
nextDay.setDate(nextDay.getDate() + 1);
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
const endDate = formatLocalDate(nextDay);
|
const endDate = formatLocalDate(nextDay);
|
||||||
|
|
||||||
const summary = db.prepare(`
|
const stats = db.prepare(`
|
||||||
SELECT COALESCE(SUM(amount), 0) as total_income
|
SELECT COALESCE(SUM(amount), 0) as total_income, COUNT(*) as count
|
||||||
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
`).get(req.user.id, date, endDate);
|
`).get(req.user.id, date, endDate);
|
||||||
|
|
||||||
const total = db.prepare(`
|
|
||||||
SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
|
||||||
`).get(req.user.id, date, endDate).count;
|
|
||||||
|
|
||||||
const records = db.prepare(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
SELECT * FROM records WHERE user_id = ? AND type = 'income' 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, date, endDate, Number(limit), offset);
|
`).all(req.user.id, date, endDate, Number(limit), offset);
|
||||||
|
|
||||||
res.json({ total_income: summary.total_income, records, page: Number(page), limit: Number(limit), totalCount: total });
|
res.json({ total_income: stats.total_income, records, page: Number(page), limit: Number(limit), totalCount: stats.count });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 老板收入排行 - 首页预览
|
// 老板收入排行 - 首页预览
|
||||||
router.get('/boss-ranking/preview', (req, res) => {
|
router.get('/boss-ranking/preview', (req, res) => {
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||||
|
const cacheKey = `boss-ranking-preview:${req.user.id}:${startDate}:${endDate}`;
|
||||||
|
getCached(cacheKey, () => {
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT COALESCE(boss, source) as boss, SUM(amount) as total_income,
|
SELECT COALESCE(boss, source) as boss, SUM(amount) as total_income,
|
||||||
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,
|
||||||
@@ -397,7 +435,8 @@ router.get('/boss-ranking/preview', (req, res) => {
|
|||||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
GROUP BY COALESCE(boss, source) ORDER BY total_income DESC LIMIT 10
|
GROUP BY COALESCE(boss, source) ORDER BY total_income DESC LIMIT 10
|
||||||
`).all(req.user.id, startDate, endDate);
|
`).all(req.user.id, startDate, endDate);
|
||||||
res.json(rows);
|
return rows;
|
||||||
|
}).then(rows => res.json(rows));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 老板收入排行 - 详情页(支持筛选)
|
// 老板收入排行 - 详情页(支持筛选)
|
||||||
|
|||||||
Reference in New Issue
Block a user