const ALLOWED_EXTENSIONS = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv'] const MAX_FILE_SIZE = 50 * 1024 * 1024 const BACKEND_TIMEZONE_OFFSET_MINUTES = 8 * 60 const SESSION_EXPIRES_AT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})([ T])(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:(Z)|([+-])(\d{2}):?(\d{2}))?$/ function validateEnterpriseWebsite(value) { const normalized = String(value || '').trim() if (!normalized) return '请输入企业官网,无官网请填写“无”' if (normalized === '无') return '' if (!/^https?:\/\/[^/\s?#]+(?:[/?#][^\s]*)?$/i.test(normalized)) return '请输入以http://或https://开头的企业官网' return '' } function validateBackgroundFile(file) { if (!file || !file.name || !file.size) return '请选择非空文件' if (file.name.length > 255) return '文件名不能超过255个字符' const ext = file.name.includes('.') ? file.name.split('.').pop().toLowerCase() : '' if (!ALLOWED_EXTENSIONS.includes(ext)) return '文件类型不支持' if (file.size > MAX_FILE_SIZE) return '单个文件不能超过50MB' return '' } function parseSessionExpiresAt(value) { const matched = SESSION_EXPIRES_AT_PATTERN.exec(String(value || '').trim()) if (!matched) return NaN const year = Number(matched[1]) const month = Number(matched[2]) const day = Number(matched[3]) const separator = matched[4] const hour = Number(matched[5]) const minute = Number(matched[6]) const second = Number(matched[7]) const millisecond = Number((matched[8] || '').padEnd(3, '0')) if (year < 1000 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return NaN const hasExplicitTimezone = Boolean(matched[9] || matched[10]) if (separator === 'T' && !hasExplicitTimezone) return NaN if (separator === ' ' && hasExplicitTimezone) return NaN const localWallClock = Date.UTC(year, month - 1, day, hour, minute, second, millisecond) const normalized = new Date(localWallClock) if (normalized.getUTCFullYear() !== year || normalized.getUTCMonth() !== month - 1 || normalized.getUTCDate() !== day || normalized.getUTCHours() !== hour || normalized.getUTCMinutes() !== minute || normalized.getUTCSeconds() !== second) return NaN let offsetMinutes = BACKEND_TIMEZONE_OFFSET_MINUTES if (matched[9] === 'Z') { offsetMinutes = 0 } else if (matched[10]) { const offsetHour = Number(matched[11]) const offsetMinute = Number(matched[12]) if (offsetHour > 23 || offsetMinute > 59) return NaN offsetMinutes = (offsetHour * 60 + offsetMinute) * (matched[10] === '+' ? 1 : -1) } return localWallClock - offsetMinutes * 60 * 1000 } function getSessionRemainingSeconds(expiresAt, now = Date.now()) { const expiresAtTime = parseSessionExpiresAt(expiresAt) if (!Number.isFinite(expiresAtTime)) return 0 return Math.max(0, Math.ceil((expiresAtTime - now) / 1000)) } function createBackgroundQueueItem(file, id) { const selectedFile = file || {} const validationError = validateBackgroundFile(selectedFile) return { id, name: selectedFile.name || '', path: selectedFile.path || '', size: selectedFile.size || 0, progress: 0, status: validationError ? 'failed' : 'pending', error: validationError, validationError } } function getUploadableBackgroundFiles(queue) { return Array.isArray(queue) ? queue.filter(file => file.status === 'pending' || (file.status === 'failed' && !file.validationError)) : [] } function createTeamSubmitPayload(teamInfo) { const payload = Object.assign({}, teamInfo || {}) payload.enterpriseWebsite = String(payload.enterpriseWebsite || '').trim() delete payload.source delete payload.coachId delete payload.uploaderId return payload } function createEmptyTextPreviewState() { return { visible: false, fileName: '', content: '', truncated: false } } function createTextPreviewState(result, fallbackFileName) { const preview = result || {} return { visible: true, fileName: preview.fileName || fallbackFileName || '文本预览', content: String(preview.content === undefined || preview.content === null ? '' : preview.content), truncated: Boolean(preview.truncated) } } function createTeamCreationState() { return { createdTeamId: '', submitting: false, stage: 'idle', completed: false } } function beginTeamSubmission(state) { if (!state || state.submitting || state.completed) return false state.submitting = true return true } function rememberCreatedTeam(state, teamId) { if (!state) return '' if (!state.createdTeamId && teamId) state.createdTeamId = teamId return state.createdTeamId } function setTeamSubmissionStage(state, stage) { if (state) state.stage = stage || 'idle' } function finishTeamSubmission(state, completed = false) { if (!state) return state.submitting = false state.completed = state.completed || completed if (completed) state.stage = 'complete' } function hasUnresolvedBackgroundFiles(queue) { return Array.isArray(queue) && queue.some(item => item.status === 'pending' || item.status === 'uploading' || (item.status === 'failed' && !item.validationError)) } module.exports = { ALLOWED_EXTENSIONS, MAX_FILE_SIZE, validateEnterpriseWebsite, validateBackgroundFile, parseSessionExpiresAt, getSessionRemainingSeconds, createBackgroundQueueItem, getUploadableBackgroundFiles, createTeamSubmitPayload, createEmptyTextPreviewState, createTextPreviewState, createTeamCreationState, beginTeamSubmission, rememberCreatedTeam, setTeamSubmissionStage, finishTeamSubmission, hasUnresolvedBackgroundFiles }