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:
co-authored by
Claude Opus 4.6
parent
19e5a48f07
commit
d551841035
+178
-141
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
// 老板收入排行 - 首页预览
|
||||
|
||||
Reference in New Issue
Block a user