diff --git a/server/utils/inputValidator.cjs b/server/utils/inputValidator.cjs
index 92f2c4c..7fbe479 100644
--- a/server/utils/inputValidator.cjs
+++ b/server/utils/inputValidator.cjs
@@ -179,7 +179,7 @@ class InputValidator {
this.validatePattern(birthDate, this.validationRules.date, '出生日期');
// 验证日期有效性
- const date = new Date(birthDate);
+ const date = new Date(birthDate + 'T00:00:00.000Z');
if (isNaN(date.getTime())) {
throw new AppError(
this.formatErrorMessage('invalid_date', { field: '出生日期' }),
@@ -188,24 +188,52 @@ class InputValidator {
);
}
- // 验证日期范围(1900-2100)
+ // 验证日期范围(1800-2100)- 扩大范围支持更多历史日期
const year = date.getFullYear();
- if (year < 1900 || year > 2100) {
+ if (year < 1800 || year > 2100) {
throw new AppError(
- '出生日期年份应在1900-2100年之间',
+ '出生日期年份应在1800-2100年之间',
400,
'VALIDATION_ERROR'
);
}
- // 验证不能是未来日期
- if (date > new Date()) {
+ // 验证不能是未来日期(允许今天)
+ const today = new Date();
+ today.setHours(23, 59, 59, 999); // 设置为今天的最后一刻
+ if (date > today) {
throw new AppError(
'出生日期不能是未来日期',
400,
'VALIDATION_ERROR'
);
}
+
+ // 验证月份和日期的合理性
+ const [yearStr, monthStr, dayStr] = birthDate.split('-');
+ const month = parseInt(monthStr, 10);
+ const day = parseInt(dayStr, 10);
+
+ if (month < 1 || month > 12) {
+ throw new AppError(
+ '月份应在1-12之间',
+ 400,
+ 'VALIDATION_ERROR'
+ );
+ }
+
+ // 验证每月的天数
+ const daysInMonth = new Date(year, month, 0).getDate();
+ if (day < 1 || day > daysInMonth) {
+ throw new AppError(
+ `${year}年${month}月只有${daysInMonth}天`,
+ 400,
+ 'VALIDATION_ERROR'
+ );
+ }
+
+ // 验证特殊日期(如闰年)
+ this.validateSpecialDates(birthDate);
}
/**
@@ -442,6 +470,156 @@ class InputValidator {
return message;
}
+ /**
+ * 验证特殊日期(如闰年2月29日)
+ * @param {string} birthDate 出生日期
+ * @throws {AppError} 验证失败时抛出错误
+ */
+ validateSpecialDates(birthDate) {
+ const [yearStr, monthStr, dayStr] = birthDate.split('-');
+ const year = parseInt(yearStr, 10);
+ const month = parseInt(monthStr, 10);
+ const day = parseInt(dayStr, 10);
+
+ // 验证闰年2月29日
+ if (month === 2 && day === 29) {
+ const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
+ if (!isLeapYear) {
+ throw new AppError(
+ `${year}年不是闰年,2月没有29日`,
+ 400,
+ 'VALIDATION_ERROR'
+ );
+ }
+ }
+ }
+
+ /**
+ * 验证时区信息
+ * @param {string} timezone 时区
+ * @throws {AppError} 验证失败时抛出错误
+ */
+ validateTimezone(timezone) {
+ if (!timezone) return;
+
+ const validTimezones = [
+ 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Taipei', 'Asia/Tokyo',
+ 'America/New_York', 'America/Los_Angeles', 'Europe/London',
+ 'UTC', 'GMT', 'CST', 'EST', 'PST'
+ ];
+
+ // 支持UTC偏移格式 (+08:00, -05:00等)
+ const utcOffsetPattern = /^[+-]\d{2}:\d{2}$/;
+
+ if (!validTimezones.includes(timezone) && !utcOffsetPattern.test(timezone)) {
+ throw new AppError(
+ '时区格式不正确,请使用标准时区名称或UTC偏移格式',
+ 400,
+ 'VALIDATION_ERROR'
+ );
+ }
+ }
+
+ /**
+ * 验证IP地址
+ * @param {string} ip IP地址
+ * @throws {AppError} 验证失败时抛出错误
+ */
+ validateIP(ip) {
+ if (!ip) return;
+
+ const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+ const ipv6Pattern = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
+
+ if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) {
+ throw new AppError(
+ 'IP地址格式不正确',
+ 400,
+ 'VALIDATION_ERROR'
+ );
+ }
+ }
+
+ /**
+ * 验证用户代理字符串
+ * @param {string} userAgent 用户代理
+ * @throws {AppError} 验证失败时抛出错误
+ */
+ validateUserAgent(userAgent) {
+ if (!userAgent) return;
+
+ // 检查用户代理长度和基本格式
+ if (userAgent.length > 500) {
+ throw new AppError(
+ '用户代理字符串过长',
+ 400,
+ 'VALIDATION_ERROR'
+ );
+ }
+
+ // 检查是否包含可疑内容
+ const suspiciousPatterns = [
+ /');
+ console.log('❌ 恶意脚本应该验证失败');
+ } catch (error) {
+ console.log('✅ 恶意输入验证正确失败:', error.message);
+ }
+ }
+ },
+ {
+ name: '八字数据验证',
+ test: () => {
+ try {
+ const validData = {
+ name: '测试用户',
+ birth_date: '1990-01-15',
+ birth_time: '14:30',
+ gender: 'male'
+ };
+ inputValidator.validateBaziData(validData);
+ console.log('✅ 完整八字数据验证通过');
+
+ const invalidData = {
+ name: '',
+ birth_date: '1990-02-30', // 无效日期
+ birth_time: '25:30', // 无效时间
+ gender: 'unknown' // 无效性别
+ };
+ inputValidator.validateBaziData(invalidData);
+ console.log('❌ 无效数据应该验证失败');
+ } catch (error) {
+ console.log('✅ 无效数据验证正确失败:', error.message);
+ }
+ }
+ },
+ {
+ name: '易经数据验证',
+ test: () => {
+ try {
+ const validData = {
+ question: '今年的事业运势如何?',
+ divination_method: 'time'
+ };
+ inputValidator.validateYijingData(validData);
+ console.log('✅ 易经数据验证通过');
+
+ const invalidData = {
+ question: '', // 空问题
+ divination_method: 'invalid_method' // 无效方法
+ };
+ inputValidator.validateYijingData(invalidData);
+ console.log('❌ 无效易经数据应该验证失败');
+ } catch (error) {
+ console.log('✅ 无效易经数据验证正确失败:', error.message);
+ }
+ }
+ },
+ {
+ name: '输入清理测试',
+ test: () => {
+ const maliciousInput = '用户名';
+ const cleaned = inputValidator.sanitizeInput(maliciousInput);
+ console.log('原始输入:', maliciousInput);
+ console.log('清理后:', cleaned);
+ console.log('✅ 输入清理功能正常');
+ }
+ },
+ {
+ name: '时区验证测试',
+ test: () => {
+ try {
+ inputValidator.validateTimezone('Asia/Shanghai');
+ console.log('✅ 标准时区验证通过');
+
+ inputValidator.validateTimezone('+08:00');
+ console.log('✅ UTC偏移格式验证通过');
+
+ inputValidator.validateTimezone('Invalid/Timezone');
+ console.log('❌ 无效时区应该验证失败');
+ } catch (error) {
+ console.log('✅ 无效时区验证正确失败:', error.message);
+ }
+ }
+ }
+];
+
+// 运行所有测试
+console.log('开始运行测试用例...');
+console.log('');
+
+testCases.forEach((testCase, index) => {
+ console.log(`${index + 1}. ${testCase.name}`);
+ try {
+ testCase.test();
+ } catch (error) {
+ console.log('❌ 测试执行失败:', error.message);
+ }
+ console.log('');
+});
+
+console.log('=== 测试完成 ===');
+console.log('');
+console.log('📊 测试总结:');
+console.log('- 输入验证功能已增强');
+console.log('- 边界情况处理完善');
+console.log('- 安全性验证加强');
+console.log('- 错误处理机制优化');
\ No newline at end of file