refactor: 系统优化 - 错误处理、输入验证、无限滚动

主要改进:
- 新增配置管理 (server/config.js)
- 新增日志工具 (server/utils/logger.js)
- 添加全局错误处理中间件,统一错误响应格式
- 添加输入验证和 AppError 错误类
- 重构 db.js 迁移逻辑,代码更清晰
- 前端列表改为无限滚动加载,每页 20 条
- 封装 AppState 状态管理模块

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lizhilun
2026-03-26 10:39:58 +08:00
co-authored by Claude Opus 4.6
parent 19e5a48f07
commit d551841035
12 changed files with 789 additions and 350 deletions
+178 -141
View File
@@ -1,13 +1,15 @@
const express = require('express');
const db = require('../db');
const { getMonthRange } = require('../utils/helpers');
const config = require('../config');
const logger = require('../utils/logger');
const { getMonthRange, AppError, validateRequired, validateNumber, validateInList } = require('../utils/helpers');
const router = express.Router();
// 内存缓存(限制大小,基于LRU)
const statsCache = new Map();
const CACHE_TTL = 2 * 60 * 1000;
const MAX_CACHE_SIZE = 100;
const CACHE_TTL = config.cache.ttl;
const MAX_CACHE_SIZE = config.cache.maxSize;
function getCached(key, fetchFn) {
const cached = statsCache.get(key);
@@ -24,9 +26,10 @@ function getCached(key, fetchFn) {
}
return Promise.resolve(fetchFn()).then(data => {
statsCache.set(key, { data, ts: Date.now() });
logger.debug(`缓存更新: ${key}`);
return data;
}).catch(err => {
console.error('Cache fetch error:', err.message);
logger.error('缓存获取失败:', err.message);
throw err;
});
}
@@ -54,48 +57,60 @@ function formatLocalDate(date) {
}
// 本月统计
router.get('/monthly', (req, res) => {
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
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));
router.get('/monthly', (req, res, next) => {
try {
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
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)).catch(next);
} catch (err) {
next(err);
}
});
// 月收入/支出记录列表
router.get('/monthly-records', (req, res) => {
const { type, page = 1, limit = 20 } = req.query;
if (!type || !['income', 'expense'].includes(type)) return res.status(400).json({ error: '缺少类型参数' });
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
const offset = (Number(page) - 1) * Number(limit);
router.get('/monthly-records', (req, res, next) => {
try {
const { type, page = 1, limit = 20 } = req.query;
if (!type || !['income', 'expense'].includes(type)) {
throw new AppError('缺少类型参数或类型无效', 400);
}
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
const validatedPage = Math.max(1, Number(page) || 1);
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
const offset = (validatedPage - 1) * validatedLimit;
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 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 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);
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, validatedLimit, offset);
res.json({ total: stats.total, records, page: Number(page), limit: Number(limit), totalCount: stats.count });
res.json({ total: stats.total, records, page: validatedPage, limit: validatedLimit, totalCount: stats.count });
} catch (err) {
next(err);
}
});
// 年度净收入按年统计
@@ -277,98 +292,112 @@ router.get('/total', (req, res) => {
});
// 老板交易记录
router.get('/boss-records', (req, res) => {
const boss = req.query.boss;
const { page = 1, limit = 20, year, month } = req.query;
if (!boss) return res.status(400).json({ error: '缺少老板参数' });
const offset = (Number(page) - 1) * Number(limit);
router.get('/boss-records', (req, res, next) => {
try {
const boss = req.query.boss;
const { page = 1, limit = 20, year, month } = req.query;
if (!boss) throw new AppError('缺少老板参数', 400);
// 日期过滤条件
let dateFilter = '';
let dateParams = [];
if (year && month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
dateParams = [startDate, endDate];
const validatedPage = Math.max(1, Number(page) || 1);
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
const offset = (validatedPage - 1) * validatedLimit;
// 日期过滤条件
let dateFilter = '';
let dateParams = [];
if (year && month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
dateParams = [startDate, endDate];
}
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,
COUNT(*) as count
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
`).get(req.user.id, boss, ...dateParams);
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, validatedLimit, offset);
res.json({
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: validatedPage,
limit: validatedLimit,
totalCount: summary.count
});
} catch (err) {
next(err);
}
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,
COUNT(*) as count
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
`).get(req.user.id, boss, ...dateParams);
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: 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: summary.count
});
});
// 陪玩交易记录
router.get('/partner-records', (req, res) => {
const partner = req.query.partner;
const { page = 1, limit = 20, year, month } = req.query;
if (!partner) return res.status(400).json({ error: '缺少陪玩参数' });
const offset = (Number(page) - 1) * Number(limit);
router.get('/partner-records', (req, res, next) => {
try {
const partner = req.query.partner;
const { page = 1, limit = 20, year, month } = req.query;
if (!partner) throw new AppError('缺少陪玩参数', 400);
// 日期过滤条件
let dateFilter = '';
let dateParams = [];
if (year && month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
dateParams = [startDate, endDate];
const validatedPage = Math.max(1, Number(page) || 1);
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
const offset = (validatedPage - 1) * validatedLimit;
// 日期过滤条件
let dateFilter = '';
let dateParams = [];
if (year && month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
dateParams = [startDate, endDate];
}
const summary = db.prepare(`
SELECT
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(partner, req.user.id, partner, partner, ...dateParams);
const records = db.prepare(`
SELECT * FROM records
WHERE user_id = ? AND (
(category = '派单' AND partner = ?) OR
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
)${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
`).all(req.user.id, partner, partner, ...dateParams, validatedLimit, offset);
res.json({
dispatch_count: summary.dispatch_count,
spread_income: summary.spread_income,
rebate_income: summary.rebate_income,
other_income: summary.other_income,
records,
page: validatedPage,
limit: validatedLimit,
totalCount: summary.count
});
} catch (err) {
next(err);
}
const summary = db.prepare(`
SELECT
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(partner, req.user.id, partner, partner, ...dateParams);
const records = db.prepare(`
SELECT * FROM records
WHERE user_id = ? AND (
(category = '派单' AND partner = ?) OR
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
)${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
`).all(req.user.id, partner, partner, ...dateParams, Number(limit), offset);
res.json({
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: summary.count
});
});
// 最近7天每日收入
@@ -399,25 +428,33 @@ router.get('/daily-income', (req, res) => {
});
// 某天收入记录详情
router.get('/daily-detail', (req, res) => {
const { date, page = 1, limit = 20 } = req.query;
if (!date) return res.status(400).json({ error: '缺少日期参数' });
const offset = (Number(page) - 1) * Number(limit);
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
const endDate = formatLocalDate(nextDay);
router.get('/daily-detail', (req, res, next) => {
try {
const { date, page = 1, limit = 20 } = req.query;
if (!date) throw new AppError('缺少日期参数', 400);
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 validatedPage = Math.max(1, Number(page) || 1);
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
const offset = (validatedPage - 1) * validatedLimit;
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);
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
const endDate = formatLocalDate(nextDay);
res.json({ total_income: stats.total_income, records, page: Number(page), limit: Number(limit), totalCount: stats.count });
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 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, validatedLimit, offset);
res.json({ total_income: stats.total_income, records, page: validatedPage, limit: validatedLimit, totalCount: stats.count });
} catch (err) {
next(err);
}
});
// 老板收入排行 - 首页预览