From df3a9daa658c2f84a6ba9836442c3300502ff78b Mon Sep 17 00:00:00 2001 From: lizhilun Date: Wed, 25 Mar 2026 10:25:37 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E7=B3=BB=E7=BB=9F=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20-=20=E6=95=B0=E6=8D=AE=E5=BA=93=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=E3=80=81=E7=BC=93=E5=AD=98=E3=80=81GZIP=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E5=92=8C=E6=9F=A5=E8=AF=A2=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加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 --- package-lock.json | 64 ++++++++++ package.json | 1 + public/js/order-list.js | 17 ++- server/app.js | 12 +- server/db.js | 3 + server/routes/stats.js | 275 +++++++++++++++++++++++----------------- 6 files changed, 250 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fa6c14..1a76b68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "better-sqlite3": "^12.6.2", + "compression": "^1.8.1", "cors": "^2.8.6", "express": "^5.2.1", "jsonwebtoken": "^9.0.3" @@ -190,6 +191,60 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -905,6 +960,15 @@ "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": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index ac00d3b..c00dce2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "better-sqlite3": "^12.6.2", + "compression": "^1.8.1", "cors": "^2.8.6", "express": "^5.2.1", "jsonwebtoken": "^9.0.3" diff --git a/public/js/order-list.js b/public/js/order-list.js index f10ee70..21bab66 100644 --- a/public/js/order-list.js +++ b/public/js/order-list.js @@ -1,3 +1,12 @@ +// 防抖工具函数 +function debounce(fn, delay) { + let timeoutId; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), delay); + }; +} + // 全局状态变量(供 app.js 引用) var orderPage = 1, orderHasMore = true, orderLoading = false; var otherIncomePage = 1, otherIncomeHasMore = true, otherIncomeLoading = false; @@ -97,8 +106,8 @@ function renderMilkTeaItem(r) { '
' + (r.source || '') + '' + time + '
'; } -// 滚动加载 -window.addEventListener('scroll', () => { +// 滚动加载(带防抖) +const handleListScroll = debounce(() => { const pageListMap = [ { page: 'orderListPage', list: 'orderList' }, { page: 'dispatchListPage', list: 'dispatchList' }, @@ -118,7 +127,9 @@ window.addEventListener('scroll', () => { break; } } -}); +}, 100); + +window.addEventListener('scroll', handleListScroll); // 兼容旧接口 async function loadOrders(append) { return loadList('orderList', append); } diff --git a/server/app.js b/server/app.js index af4fba6..94d6460 100644 --- a/server/app.js +++ b/server/app.js @@ -1,6 +1,7 @@ const express = require('express'); const cors = require('cors'); const path = require('path'); +const compression = require('compression'); const db = require('./db'); const { router: authRouter, auth } = require('./routes/auth'); @@ -12,9 +13,18 @@ const app = express(); app.use(cors()); 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) => { const field = req.query.field; const allowed = ['boss', 'partner', 'source', 'destination']; diff --git a/server/db.js b/server/db.js index ee0caa7..4052aec 100644 --- a/server/db.js +++ b/server/db.js @@ -40,6 +40,9 @@ db.exec(` 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); + 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 约束 diff --git a/server/routes/stats.js b/server/routes/stats.js index ede6664..3a8b8ab 100644 --- a/server/routes/stats.js +++ b/server/routes/stats.js @@ -4,6 +4,47 @@ const { getMonthRange } = require('../utils/helpers'); 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 function formatLocalDate(date) { const y = date.getFullYear(); @@ -15,22 +56,25 @@ function formatLocalDate(date) { // 本月统计 router.get('/monthly', (req, res) => { const { startDate, endDate } = getMonthRange(req.query.year, req.query.month); - const stats = db.prepare(` - SELECT - COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_order_count, - COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as take_order_income, - COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_order_count, - COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as dispatch_order_income, - COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income, - COALESCE(SUM(CASE WHEN category = '红包收入' THEN 1 ELSE 0 END), 0) as redEnvelope_count, - COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as milkTea_income, - COALESCE(SUM(CASE WHEN category = '奶茶' THEN 1 ELSE 0 END), 0) as milkTea_count, - 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 = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? - `).get(req.user.id, startDate, endDate); - res.json(stats); + const cacheKey = `monthly:${req.user.id}:${startDate}:${endDate}`; + getCached(cacheKey, () => { + const stats = db.prepare(` + SELECT + COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_order_count, + COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as take_order_income, + COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_order_count, + COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as dispatch_order_income, + COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income, + COALESCE(SUM(CASE WHEN category = '红包收入' THEN 1 ELSE 0 END), 0) as redEnvelope_count, + COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as milkTea_income, + COALESCE(SUM(CASE WHEN category = '奶茶' THEN 1 ELSE 0 END), 0) as milkTea_count, + 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 = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? + `).get(req.user.id, startDate, endDate); + 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 offset = (Number(page) - 1) * Number(limit); - // 合并 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') < ? @@ -57,19 +100,7 @@ router.get('/monthly-records', (req, res) => { // 年度净收入按年统计 router.get('/yearly-net-income', (req, res) => { - // 获取所有年份数据 - 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(` + const combined = db.prepare(` SELECT strftime('%Y', created_at) as year, strftime('%m', created_at) as month, @@ -77,41 +108,59 @@ router.get('/yearly-net-income', (req, res) => { COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense FROM records WHERE user_id = ? - GROUP BY year, month ORDER BY year DESC, month - `).all(req.user.id); - - // 按年份分组每月数据 - 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(` + GROUP BY year, month + UNION ALL 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 = 'expense' THEN amount ELSE 0 END), 0) as total_expense - FROM records WHERE user_id = ? - `).get(req.user.id); + FROM records + 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 }); }); // 陪玩派单榜预览 router.get('/partner-ranking/preview', (req, res) => { const { startDate, endDate } = getMonthRange(req.query.year, req.query.month); - const rows = db.prepare(` - SELECT partner, COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(amount), 0) as dispatch_income - FROM records - WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != '' - AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? - GROUP BY partner ORDER BY dispatch_count DESC LIMIT 10 - `).all(req.user.id, startDate, endDate); - res.json(rows); + const cacheKey = `partner-ranking-preview:${req.user.id}:${startDate}:${endDate}`; + getCached(cacheKey, () => { + const rows = db.prepare(` + SELECT partner, COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(amount), 0) as dispatch_income + FROM records + WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != '' + AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? + GROUP BY partner ORDER BY dispatch_count DESC LIMIT 10 + `).all(req.user.id, startDate, endDate); + return rows; + }).then(rows => res.json(rows)); }); // 陪玩派单榜详情 @@ -141,24 +190,23 @@ router.get('/partner-ranking', (req, res) => { rows.forEach(row => { row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0); }); - const summary = db.prepare(` + const summaryStats = db.prepare(` SELECT - COALESCE(SUM(quantity), 0) as total_dispatch_count, - COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) as total_dispatch_income, - COALESCE(SUM(amount), 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 quantity ELSE 0 END), 0) as total_dispatch_count, + COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as total_dispatch_income, + COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as total_rebate_income, + COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as total_milkTea_income 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') < ? `).get(req.user.id, start, end); - // 获取奶茶收入 - const milkTeaStats = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total_milkTea_income - FROM records - WHERE user_id = ? AND type = 'income' AND category = '奶茶' - AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? - `).get(req.user.id, start, end); - summary.total_milkTea_income = milkTeaStats.total_milkTea_income; + const summary = { + total_dispatch_count: summaryStats.total_dispatch_count, + total_dispatch_income: summaryStats.total_dispatch_income, + total_rebate_income: summaryStats.total_rebate_income, + total_partner_income: summaryStats.total_dispatch_income - summaryStats.total_rebate_income, + total_milkTea_income: summaryStats.total_milkTea_income + }; 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 != '' GROUP BY boss ORDER BY count DESC `).all(req.user.id); - // 获取接单总收入和红包收入 const stats = db.prepare(` SELECT - COALESCE(SUM(quantity), 0) as total_take_count, - COALESCE(SUM(amount), 0) as total_take_income - FROM records WHERE user_id = ? AND category = '接单' - `).get(req.user.id); - const redEnvelopeStats = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total_redEnvelope_income - FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入' + COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as total_take_count, + COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as total_take_income, + COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as total_redEnvelope_income + FROM records WHERE user_id = ? AND type = 'income' AND category IN ('接单', '红包收入') `).get(req.user.id); const result = { rows: rows, total_take_count: stats.total_take_count, total_take_income: stats.total_take_income, - total_redEnvelope_income: redEnvelopeStats.total_redEnvelope_income + total_redEnvelope_income: stats.total_redEnvelope_income }; res.json(result); }); @@ -204,19 +248,16 @@ router.get('/dispatch-ranking', (req, res) => { rows.forEach(row => { row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0); }); - // 获取派单总数和奶茶收入 - const dispatchTotal = db.prepare(` - SELECT COALESCE(SUM(quantity), 0) as total_dispatch_count - FROM records WHERE user_id = ? AND category = '派单' - `).get(req.user.id); - const milkTeaStats = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total_milkTea_income - FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶' + const stats = db.prepare(` + SELECT + COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as total_dispatch_count, + COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as total_milkTea_income + FROM records WHERE user_id = ? AND type = 'income' AND category IN ('派单', '奶茶') `).get(req.user.id); const result = { rows: rows, - total_dispatch_count: dispatchTotal.total_dispatch_count, - total_milkTea_income: milkTeaStats.total_milkTea_income + total_dispatch_count: stats.total_dispatch_count, + total_milkTea_income: stats.total_milkTea_income }; res.json(result); }); @@ -252,7 +293,6 @@ router.get('/boss-records', (req, res) => { dateParams = [startDate, endDate]; } - // 合并 summary 查询为一次 const summary = db.prepare(` SELECT 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]; } - // 合并 summary 查询为一次 - // 注意: source 在 CASE 和 WHERE 中各出现一次,都需要 partner 值 const summary = db.prepare(` SELECT 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); const endDate = formatLocalDate(endDay); - const rows = db.prepare(` - 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') < ? - GROUP BY DATE(created_at, 'localtime') - `).all(req.user.id, startDate, endDate); - - const map = {}; - rows.forEach(r => { map[r.date] = r.income; }); - res.json(days.map(d => ({ date: d, income: map[d] || 0 }))); + const cacheKey = `daily-income:${req.user.id}:${startDate}`; + getCached(cacheKey, () => { + const rows = db.prepare(` + 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') < ? + GROUP BY DATE(created_at, 'localtime') + `).all(req.user.id, startDate, endDate); + const map = {}; + rows.forEach(r => { map[r.date] = r.income; }); + return days.map(d => ({ date: d, income: map[d] || 0 })); + }).then(data => res.json(data)); }); // 某天收入记录详情 @@ -367,37 +407,36 @@ router.get('/daily-detail', (req, res) => { nextDay.setDate(nextDay.getDate() + 1); const endDate = formatLocalDate(nextDay); - const summary = db.prepare(` - SELECT COALESCE(SUM(amount), 0) as total_income + const stats = db.prepare(` + 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') < ? `).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(` 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 ? `).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) => { const { startDate, endDate } = getMonthRange(req.query.year, req.query.month); - const rows = db.prepare(` - 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 dispatch_count, - COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income - FROM records - WHERE user_id = ? AND type = 'income' AND (boss IS NOT NULL AND boss != '' OR source IS NOT NULL AND source != '') - AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? - GROUP BY COALESCE(boss, source) ORDER BY total_income DESC LIMIT 10 - `).all(req.user.id, startDate, endDate); - res.json(rows); + const cacheKey = `boss-ranking-preview:${req.user.id}:${startDate}:${endDate}`; + getCached(cacheKey, () => { + const rows = db.prepare(` + 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 dispatch_count, + COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income + FROM records + WHERE user_id = ? AND type = 'income' AND (boss IS NOT NULL AND boss != '' OR source IS NOT NULL AND source != '') + AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? + GROUP BY COALESCE(boss, source) ORDER BY total_income DESC LIMIT 10 + `).all(req.user.id, startDate, endDate); + return rows; + }).then(rows => res.json(rows)); }); // 老板收入排行 - 详情页(支持筛选)