2026-03-26 10:39:58 +08:00
|
|
|
/**
|
|
|
|
|
* 应用错误类
|
|
|
|
|
*/
|
|
|
|
|
class AppError extends Error {
|
|
|
|
|
constructor(message, status = 400) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.status = status;
|
|
|
|
|
this.expose = true; // 允许向客户端暴露错误信息
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 日期范围辅助函数
|
|
|
|
|
*/
|
2026-03-12 11:23:10 +08:00
|
|
|
function getMonthRange(qYear, qMonth) {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const y = Number(qYear) || now.getFullYear();
|
|
|
|
|
const m = Number(qMonth) || (now.getMonth() + 1);
|
|
|
|
|
const startDate = `${y}-${String(m).padStart(2, '0')}-01`;
|
|
|
|
|
const nm = m + 1 > 12 ? 1 : m + 1;
|
|
|
|
|
const ny = m + 1 > 12 ? y + 1 : y;
|
|
|
|
|
const endDate = `${ny}-${String(nm).padStart(2, '0')}-01`;
|
|
|
|
|
return { startDate, endDate };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
/**
|
|
|
|
|
* CSV行解析(处理引号内的逗号)
|
|
|
|
|
*/
|
2026-03-12 11:23:10 +08:00
|
|
|
function parseCSVLine(line) {
|
|
|
|
|
const result = [];
|
|
|
|
|
let current = '';
|
|
|
|
|
let inQuotes = false;
|
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
|
|
|
const ch = line[i];
|
|
|
|
|
if (inQuotes) {
|
|
|
|
|
if (ch === '"' && line[i + 1] === '"') { current += '"'; i++; }
|
|
|
|
|
else if (ch === '"') { inQuotes = false; }
|
|
|
|
|
else { current += ch; }
|
|
|
|
|
} else {
|
|
|
|
|
if (ch === '"') { inQuotes = true; }
|
|
|
|
|
else if (ch === ',') { result.push(current); current = ''; }
|
|
|
|
|
else { current += ch; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.push(current);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
/**
|
|
|
|
|
* 输入验证函数
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2026-07-01 16:13:47 +08:00
|
|
|
if (!Number.isFinite(num)) {
|
2026-03-26 10:39:58 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 16:13:47 +08:00
|
|
|
function optionalString(value, field, maxLength) {
|
|
|
|
|
if (value === undefined || value === null || value === '') return null;
|
|
|
|
|
return validateString(String(value), field, maxLength);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
function validateInList(value, field, list) {
|
|
|
|
|
if (!list.includes(value)) {
|
|
|
|
|
throw new AppError(`${field} 值无效`, 400);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 16:13:47 +08:00
|
|
|
function validatePage(value, defaultValue = 1) {
|
|
|
|
|
return Math.max(1, Number.parseInt(value, 10) || defaultValue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateLimit(value, defaultValue, maxLimit) {
|
|
|
|
|
const limit = Number.parseInt(value, 10) || defaultValue;
|
|
|
|
|
return Math.min(Math.max(1, limit), maxLimit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateIsoDate(value, field) {
|
|
|
|
|
if (value === undefined || value === null || value === '') return new Date().toISOString();
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
|
|
|
throw new AppError(`${field} 格式无效`, 400);
|
|
|
|
|
}
|
|
|
|
|
return date.toISOString();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
module.exports = {
|
|
|
|
|
AppError,
|
|
|
|
|
getMonthRange,
|
|
|
|
|
parseCSVLine,
|
|
|
|
|
validateRequired,
|
|
|
|
|
validateNumber,
|
|
|
|
|
validateString,
|
2026-07-01 16:13:47 +08:00
|
|
|
optionalString,
|
|
|
|
|
validateInList,
|
|
|
|
|
validatePage,
|
|
|
|
|
validateLimit,
|
|
|
|
|
validateIsoDate
|
2026-03-26 10:39:58 +08:00
|
|
|
};
|