import { type BudgetLevel, type Duration, type Interest, type Pace, type Theme, type Transport, TRAVEL_BUDGET_LEVELS, TRAVEL_DURATIONS, TRAVEL_INTERESTS, TRAVEL_PACES, TRAVEL_THEMES, TRAVEL_TRANSPORTS, type TravelPreferences, } from './types' const DESTINATION = '深圳市光明区' as const export class TravelValidationError extends Error { constructor(message: string) { super(message) this.name = 'TravelValidationError' } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function isEnumValue(value: unknown, allowed: readonly T[]): value is T { return typeof value === 'string' && allowed.includes(value as T) } function normalizeEnumArray( value: unknown, allowed: readonly T[], fieldName: string, ): T[] { if (!Array.isArray(value) || value.some(item => !isEnumValue(item, allowed))) throw new TravelValidationError(`${fieldName}包含无效选项`) const selected = new Set(value as T[]) return allowed.filter(item => selected.has(item)) } function normalizeInteger(value: unknown, fieldName: string, min: number, max: number): number { if (!Number.isInteger(value) || (value as number) < min || (value as number) > max) throw new TravelValidationError(`${fieldName}必须是 ${min} 至 ${max} 的整数`) return value as number } export function normalizeTravelPreferences(value: unknown): TravelPreferences { if (!isRecord(value)) throw new TravelValidationError('出行偏好格式无效') if (value.destination !== DESTINATION) throw new TravelValidationError('当前仅支持深圳市光明区') const themes = normalizeEnumArray(value.themes, TRAVEL_THEMES, '出行主题') const interests = normalizeEnumArray(value.interests, TRAVEL_INTERESTS, '特色偏好') if (themes.length === 0 && interests.length === 0) throw new TravelValidationError('主题或特色偏好至少选择一项') if (!isEnumValue(value.duration, TRAVEL_DURATIONS)) throw new TravelValidationError('游玩时长无效') if (!isEnumValue(value.pace, TRAVEL_PACES)) throw new TravelValidationError('行程节奏无效') if (!isEnumValue(value.transport, TRAVEL_TRANSPORTS)) throw new TravelValidationError('交通方式无效') if (!isEnumValue(value.budgetLevel, TRAVEL_BUDGET_LEVELS)) throw new TravelValidationError('预算档位无效') const adults = normalizeInteger(value.adults, '成人数量', 0, 20) const children = normalizeInteger(value.children, '儿童数量', 0, 20) if (adults + children < 1) throw new TravelValidationError('出行总人数至少为 1') if (!Array.isArray(value.childAges) || value.childAges.length !== children) throw new TravelValidationError('儿童年龄数量必须与儿童人数一致') const childAges = value.childAges.map(age => normalizeInteger(age, '儿童年龄', 0, 17)) if (typeof value.extraRequirements !== 'string') throw new TravelValidationError('补充要求格式无效') const extraRequirements = value.extraRequirements.trim() if (extraRequirements.length > 500) throw new TravelValidationError('补充要求不能超过 500 字') return { destination: DESTINATION, themes, duration: value.duration, pace: value.pace, interests, adults, children, childAges, transport: value.transport, budgetLevel: value.budgetLevel, extraRequirements, } } export function isTravelPreferences(value: unknown): value is TravelPreferences { try { normalizeTravelPreferences(value) return true } catch { return false } } export function cloneTravelPreferences(preferences: TravelPreferences): TravelPreferences { const normalized = normalizeTravelPreferences(preferences) return { ...normalized, themes: [...normalized.themes], interests: [...normalized.interests], childAges: [...normalized.childAges], } }