import { getUserStorage, setUserStorage } from '@/services/storage.uts' import { getSession, requestJson, uploadJson } from '@/services/api.uts' import { draftText } from '@/services/assessment.uts' export type AccountProfile = { nickname: string, avatar: string, phone: string } export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png' let profileWriteVersion = 0 const profileKey = (phone: string): string => 'finance.account.profile:' + phone export const maskPhone = (phone: string): string => phone.length == 11 ? phone.substring(0, 3) + '****' + phone.substring(7) : phone export const validPhone = (phone: string): boolean => /^1[3-9][0-9]{9}$/.test(phone) export const validCode = (code: string): boolean => /^[0-9]{6}$/.test(code) export const validNewPassword = (password: string): boolean => /^[!-~]{8,32}$/.test(password) && /[A-Za-z]/.test(password) && /[0-9]/.test(password) export function requireAccount(): UTSJSONObject { const session = getSession() if (session == null || draftText(session, 'token').length == 0 || !validPhone(draftText(session, 'phone'))) throw new Error('请先登录后再操作') return session } export function sameAccount(session: UTSJSONObject): boolean { const current = getSession() return current != null && draftText(current, 'token') == draftText(session, 'token') && draftText(current, 'userId') == draftText(session, 'userId') && draftText(current, 'phone') == draftText(session, 'phone') } export function getAccountProfile(): AccountProfile { const session = getSession() const phone = session == null ? '' : draftText(session, 'phone') let nickname = '融资用户'; let avatar = '' if (phone.length > 0) { const saved = getUserStorage(profileKey(phone)) if (saved != null && typeof saved == 'object') { nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar') } } return { phone, nickname: nickname.length > 0 ? nickname : '融资用户', avatar } } // 安全接口必须经过服务端认证。未配置服务时不模拟验证或修改成功。 async function accountRequest(path: string, method: RequestMethod, payload: UTSJSONObject): Promise { requireAccount() return await requestJson(path, method, payload) } function cacheProfile(result: UTSJSONObject, session: UTSJSONObject): AccountProfile { const phone = draftText(session, 'phone'); const name = result.getString('nickname') const avatar = result['avatarUrl'] if (draftText(result, 'userId') != draftText(session, 'userId') || name == null || name.trim().length < 2 || name.length > 20 || (avatar != null && typeof avatar != 'string') || draftText(result, 'phoneMasked') != maskPhone(phone)) throw new Error('账户资料响应不完整或归属不符,请重试') const profile: AccountProfile = { phone, nickname: name, avatar: draftText(result, 'avatarUrl') } setUserStorage(profileKey(phone), { nickname: profile.nickname, avatar: profile.avatar }) return profile } export async function fetchAccountProfile(): Promise { const session = requireAccount() const version = profileWriteVersion const result = await accountRequest('/account/profile', 'GET', {}) if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面') // 保存完成前发出的旧读取不能回写昵称或头像缓存。 if (version != profileWriteVersion) throw new Error('账户资料已更新,请重新读取') return cacheProfile(result, session) } export async function saveAccountProfile(nickname: string, avatarId: string): Promise { const session = requireAccount() const name = nickname.trim() if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符') const payload: UTSJSONObject = { nickname: name } if (avatarId == 'default') payload['avatarId'] = null else if (avatarId.length > 0) payload['avatarId'] = avatarId const result = await accountRequest('/account/profile', 'PUT', payload) if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面') profileWriteVersion++ cacheProfile(result, session) } export async function uploadAccountAvatar(path: string): Promise { const session = requireAccount() const result = await uploadJson('/account/avatar', path) if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面') if (!/^[1-9][0-9]{0,9}$/.test(draftText(result, 'attachmentId')) || draftText(result, 'avatarUrl').length == 0) throw new Error('头像上传响应不完整,请重试') return result } export async function requestSecurityCode(purpose: string, newPhone: string): Promise { if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效') if (purpose == 'phone-new' && !validPhone(newPhone)) throw new Error('请输入正确的新手机号') await accountRequest('/account/security/sms-code', 'POST', { purpose, newPhone: purpose == 'phone-new' ? newPhone : '' }) } export async function changeAccountPassword(mode: string, oldPassword: string, code: string, newPassword: string): Promise { if (!validNewPassword(newPassword)) throw new Error('新密码需为 8–32 位,包含字母和数字,可使用符号') if (mode == 'password') { if (oldPassword.length == 0) throw new Error('请输入当前密码') if (oldPassword == newPassword) throw new Error('新密码不能与当前密码相同') } else if (mode != 'sms' || !validCode(code)) throw new Error('请输入 6 位数字验证码') const result = await accountRequest('/account/password', 'PUT', { verification: mode, oldPassword: mode == 'password' ? oldPassword : '', code: mode == 'sms' ? code : '', newPassword }) if (result['reLoginRequired'] != true) throw new Error('密码修改响应不完整,请重试') } export async function changeAccountPhone(newPhone: string, currentCode: string, newCode: string): Promise { const session = requireAccount() if (!validPhone(newPhone)) throw new Error('请输入正确的新手机号') if (newPhone == draftText(session, 'phone')) throw new Error('新手机号不能与当前手机号相同') if (!validCode(currentCode) || !validCode(newCode)) throw new Error('请输入两组 6 位数字验证码') const result = await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode }) if (result['reLoginRequired'] != true) throw new Error('手机号修改响应不完整,请重试') }