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
+67
-3
@@ -1,4 +1,17 @@
|
||||
// 日期范围辅助函数
|
||||
/**
|
||||
* 应用错误类
|
||||
*/
|
||||
class AppError extends Error {
|
||||
constructor(message, status = 400) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.expose = true; // 允许向客户端暴露错误信息
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期范围辅助函数
|
||||
*/
|
||||
function getMonthRange(qYear, qMonth) {
|
||||
const now = new Date();
|
||||
const y = Number(qYear) || now.getFullYear();
|
||||
@@ -10,7 +23,9 @@ function getMonthRange(qYear, qMonth) {
|
||||
return { startDate, endDate };
|
||||
}
|
||||
|
||||
// CSV行解析(处理引号内的逗号)
|
||||
/**
|
||||
* CSV行解析(处理引号内的逗号)
|
||||
*/
|
||||
function parseCSVLine(line) {
|
||||
const result = [];
|
||||
let current = '';
|
||||
@@ -31,4 +46,53 @@ function parseCSVLine(line) {
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = { getMonthRange, parseCSVLine };
|
||||
/**
|
||||
* 输入验证函数
|
||||
*/
|
||||
function validateRequired(obj, fields) {
|
||||
const missing = fields.filter(f => obj[f] === undefined || obj[f] === null || obj[f] === '');
|
||||
if (missing.length > 0) {
|
||||
throw new AppError(`缺少必填字段: ${missing.join(', ')}`, 400);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumber(value, field, min, max) {
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) {
|
||||
throw new AppError(`${field} 必须是数字`, 400);
|
||||
}
|
||||
if (min !== undefined && num < min) {
|
||||
throw new AppError(`${field} 不能小于 ${min}`, 400);
|
||||
}
|
||||
if (max !== undefined && num > max) {
|
||||
throw new AppError(`${field} 不能大于 ${max}`, 400);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
function validateString(value, field, maxLength) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new AppError(`${field} 必须是字符串`, 400);
|
||||
}
|
||||
if (maxLength && value.length > maxLength) {
|
||||
throw new AppError(`${field} 长度不能超过 ${maxLength}`, 400);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function validateInList(value, field, list) {
|
||||
if (!list.includes(value)) {
|
||||
throw new AppError(`${field} 值无效`, 400);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
getMonthRange,
|
||||
parseCSVLine,
|
||||
validateRequired,
|
||||
validateNumber,
|
||||
validateString,
|
||||
validateInList
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 简单日志工具
|
||||
* 支持日志级别和格式化输出
|
||||
*/
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const LEVELS = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3
|
||||
};
|
||||
|
||||
const COLORS = {
|
||||
debug: '\x1b[36m', // cyan
|
||||
info: '\x1b[32m', // green
|
||||
warn: '\x1b[33m', // yellow
|
||||
error: '\x1b[31m', // red
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
const currentLevel = LEVELS[config.log.level] || LEVELS.info;
|
||||
const colorize = config.log.colorize && process.stdout.isTTY;
|
||||
|
||||
function formatTime() {
|
||||
const d = new Date();
|
||||
return d.toISOString().replace('T', ' ').slice(0, 19);
|
||||
}
|
||||
|
||||
function formatMessage(level, ...args) {
|
||||
const timestamp = formatTime();
|
||||
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
||||
|
||||
if (colorize) {
|
||||
return `${COLORS[level]}${prefix}${COLORS.reset} ${args.join(' ')}`;
|
||||
}
|
||||
return `${prefix} ${args.join(' ')}`;
|
||||
}
|
||||
|
||||
const logger = {
|
||||
debug(...args) {
|
||||
if (currentLevel <= LEVELS.debug) {
|
||||
console.log(formatMessage('debug', ...args));
|
||||
}
|
||||
},
|
||||
|
||||
info(...args) {
|
||||
if (currentLevel <= LEVELS.info) {
|
||||
console.log(formatMessage('info', ...args));
|
||||
}
|
||||
},
|
||||
|
||||
warn(...args) {
|
||||
if (currentLevel <= LEVELS.warn) {
|
||||
console.warn(formatMessage('warn', ...args));
|
||||
}
|
||||
},
|
||||
|
||||
error(...args) {
|
||||
if (currentLevel <= LEVELS.error) {
|
||||
console.error(formatMessage('error', ...args));
|
||||
}
|
||||
},
|
||||
|
||||
// 请求日志中间件
|
||||
requestLogger(req, res, next) {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const status = res.statusCode;
|
||||
const level = status >= 400 ? 'warn' : 'info';
|
||||
logger[level](`${req.method} ${req.path} ${status} ${duration}ms`);
|
||||
});
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
Reference in New Issue
Block a user