150 lines
6.3 KiB
JavaScript
150 lines
6.3 KiB
JavaScript
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')
|
|
}
|
|
}
|