import { createRequestError } from './request-client.js' import { assertSmsCode } from '@/utils/auth/verification.js' import { assertPlainPayload, normalizeOptionalText, normalizeOssIdString } from './request-normalizers.js' import { normalizeOptionalResponseText } from './response-normalizers.js' import { normalizeBusinessFileAccess } from './business-file-contract.js' export const PROFILE_SEX_OPTIONS = Object.freeze([ Object.freeze({ value: '0', label: '男' }), Object.freeze({ value: '1', label: '女' }), Object.freeze({ value: '2', label: '未知' }) ]) const profileSexValues = new Set(PROFILE_SEX_OPTIONS.map(({ value }) => value)) const isCalendarDate = (value) => { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false const [year, month, day] = value.split('-').map(Number) const date = new Date(Date.UTC(year, month - 1, day)) return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day } const normalizeProfileResponseText = (value, field) => { if (value === undefined || value === null) return '' if (typeof value !== 'string') { throw createRequestError(`个人资料字段 ${field} 无效`, 'PROFILE_RESPONSE_INVALID') } return value.trim() } export const normalizePhoneChangePayload = (payload) => { assertPlainPayload(payload, new Set(['phone', 'smsCode', 'currentPasswordHash']), '换绑手机号请求') if (typeof payload.phone !== 'string' || !/^1\d{10}$/.test(payload.phone.trim())) { throw new TypeError('新手机号格式无效') } if (typeof payload.currentPasswordHash !== 'string' || !/^[a-f0-9]{32}$/i.test(payload.currentPasswordHash)) { throw new TypeError('当前密码摘要无效') } return { phone: payload.phone.trim(), smsCode: assertSmsCode(payload.smsCode), currentPassword: payload.currentPasswordHash.toLowerCase() } } const normalizeProfileUserId = (value) => { if (value === undefined || value === null || value === '') return '' if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value) throw createRequestError('个人资料字段 userId 无效', 'PROFILE_RESPONSE_INVALID') } export const normalizeAppProfile = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw createRequestError('个人资料响应格式无效', 'PROFILE_RESPONSE_INVALID') } const birthday = normalizeProfileResponseText(payload.birthday, 'birthday') if (birthday && !isCalendarDate(birthday.slice(0, 10))) { throw createRequestError('个人资料字段 birthday 无效', 'PROFILE_RESPONSE_INVALID') } return { userId: normalizeProfileUserId(payload.userId), tenantId: normalizeProfileResponseText(payload.tenantId, 'tenantId'), userNo: normalizeProfileResponseText(payload.userNo, 'userNo'), phone: normalizeProfileResponseText(payload.phone, 'phone'), nickName: normalizeProfileResponseText(payload.nickName, 'nickName'), realName: normalizeProfileResponseText(payload.realName, 'realName'), avatarFile: normalizeBusinessFileAccess( payload.avatarFile, '个人头像', 'PROFILE_RESPONSE_INVALID' ), sex: normalizeProfileResponseText(payload.sex, 'sex'), birthday, email: normalizeProfileResponseText(payload.email, 'email'), registerSource: normalizeProfileResponseText(payload.registerSource, 'registerSource'), status: normalizeProfileResponseText(payload.status, 'status') } } export const normalizeProfileUpdatePayload = (payload) => { const allowedFields = new Set(['nickName', 'realName', 'avatar', 'sex', 'birthday', 'email']) assertPlainPayload(payload, allowedFields, '个人资料请求') const normalizedPayload = {} for (const [field, limit] of [['nickName', 30], ['realName', 30]]) { if (!Object.prototype.hasOwnProperty.call(payload, field)) continue const normalizedText = normalizeOptionalText(payload[field], field) if (!normalizedText) continue if (normalizedText.length > limit) throw new TypeError(`${field} 超出长度限制`) normalizedPayload[field] = normalizedText } if (Object.prototype.hasOwnProperty.call(payload, 'sex')) { const sex = normalizeOptionalText(payload.sex, 'sex') if (sex) { if (!profileSexValues.has(sex)) throw new TypeError('sex 必须为 0、1 或 2') normalizedPayload.sex = sex } } if (Object.prototype.hasOwnProperty.call(payload, 'birthday')) { const birthday = normalizeOptionalText(payload.birthday, 'birthday') if (birthday) { if (!isCalendarDate(birthday)) { throw new TypeError('birthday 必须是 yyyy-MM-dd 日期') } normalizedPayload.birthday = birthday } } if (Object.prototype.hasOwnProperty.call(payload, 'email')) { const email = normalizeOptionalText(payload.email, 'email') if (email) { if (email.length > 100 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw new TypeError('email 格式无效') } normalizedPayload.email = email } } if (Object.prototype.hasOwnProperty.call(payload, 'avatar')) { const avatarOssId = payload.avatar if (avatarOssId !== undefined && avatarOssId !== null && avatarOssId !== '') { normalizedPayload.avatar = normalizeOssIdString(avatarOssId, 'avatar') } } if (Object.keys(normalizedPayload).length === 0) { throw new TypeError('个人资料至少需要一个可更新字段') } return normalizedPayload } export const normalizeRecommendationPreference = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('个性化推荐偏好响应无效', 'RECOMMENDATION_PREFERENCE_RESPONSE_INVALID') } if (typeof value.enabled !== 'boolean') { throw createRequestError('个性化推荐偏好缺少 enabled', 'RECOMMENDATION_PREFERENCE_RESPONSE_INVALID') } if (!Number.isSafeInteger(value.version) || value.version < 0) { throw createRequestError('个性化推荐偏好 version 无效', 'RECOMMENDATION_PREFERENCE_RESPONSE_INVALID') } return { enabled: value.enabled, version: value.version, updatedAt: normalizeOptionalResponseText(value.updatedAt, 'updatedAt', '个性化推荐偏好响应', 'RECOMMENDATION_PREFERENCE_RESPONSE_INVALID') } }