Files
jiapuapp/services/api/profile-contract.js

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')
}
}