2026-09-22 17:25:52 +08:00
|
|
|
|
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
|
|
|
|
|
import { API_BASE_URL, getSession, requestJson } from '@/services/api.uts'
|
2026-09-21 15:04:59 +08:00
|
|
|
|
import { activeAssessmentType, loadAssessmentDraft, draftText } from '@/services/assessment.uts'
|
|
|
|
|
|
|
|
|
|
|
|
export type AccountProfile = { nickname: string, avatar: string, phone: string }
|
|
|
|
|
|
export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png'
|
|
|
|
|
|
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, '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) {
|
2026-09-22 17:25:52 +08:00
|
|
|
|
const saved = getUserStorage(profileKey(phone))
|
2026-09-21 15:04:59 +08:00
|
|
|
|
if (saved != null && typeof saved == 'object') {
|
|
|
|
|
|
nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const draft = loadAssessmentDraft(activeAssessmentType())
|
|
|
|
|
|
if (draft != null && draftText(draft, 'phone') == phone && draftText(draft, 'name').length > 0) nickname = draftText(draft, 'name')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return { phone, nickname: nickname.length > 0 ? nickname : '融资用户', avatar }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安全接口必须经过服务端认证。未配置服务时不模拟验证或修改成功。
|
2026-09-22 17:25:52 +08:00
|
|
|
|
async function accountRequest(path: string, method: RequestMethod, payload: UTSJSONObject): Promise<UTSJSONObject> {
|
|
|
|
|
|
requireAccount()
|
|
|
|
|
|
return await requestJson(path, method, payload)
|
2026-09-21 15:04:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
export async function saveAccountProfile(nickname: string, avatar: string): Promise<void> {
|
|
|
|
|
|
const session = requireAccount(); const phone = draftText(session, 'phone')
|
|
|
|
|
|
const name = nickname.trim()
|
|
|
|
|
|
if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符')
|
|
|
|
|
|
if (API_BASE_URL.length > 0) await accountRequest('/account/profile', 'PUT', { nickname: name, avatar })
|
|
|
|
|
|
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
2026-09-22 17:25:52 +08:00
|
|
|
|
setUserStorage(profileKey(phone), { nickname: name, avatar })
|
2026-09-21 15:04:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
export async function requestSecurityCode(purpose: string, newPhone: string): Promise<void> {
|
|
|
|
|
|
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<void> {
|
|
|
|
|
|
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 位数字验证码')
|
|
|
|
|
|
await accountRequest('/account/password', 'PUT', { verification: mode, oldPassword: mode == 'password' ? oldPassword : '', code: mode == 'sms' ? code : '', newPassword })
|
|
|
|
|
|
}
|
|
|
|
|
|
export async function changeAccountPhone(newPhone: string, currentCode: string, newCode: string): Promise<void> {
|
|
|
|
|
|
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 位数字验证码')
|
|
|
|
|
|
await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 限制头像体积,避免把临时路径或过大的图片写入账户缓存。
|
|
|
|
|
|
export function readAvatar(path: string): Promise<string> {
|
|
|
|
|
|
return new Promise<string>((resolve, reject) => {
|
|
|
|
|
|
// #ifdef WEB
|
|
|
|
|
|
fetch(path).then((response) => response.blob()).then((blob) => {
|
|
|
|
|
|
if (blob.size > 512000 || !['image/jpeg', 'image/png', 'image/webp'].includes(blob.type)) { reject(new Error('请选择 500KB 以内的 JPG、PNG 或 WebP 图片')); return }
|
|
|
|
|
|
const reader = new FileReader()
|
|
|
|
|
|
reader.onload = () => resolve(reader.result as string)
|
|
|
|
|
|
reader.onerror = () => reject(new Error('图片读取失败,请重新选择'))
|
|
|
|
|
|
reader.readAsDataURL(blob)
|
|
|
|
|
|
}).catch(() => reject(new Error('图片读取失败,请重新选择')))
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
// #ifndef WEB
|
|
|
|
|
|
uni.getFileSystemManager().readFile({ filePath: path, encoding: 'base64', success: (result) => {
|
|
|
|
|
|
const data = result.data as string
|
|
|
|
|
|
if (data.length > 683000) { reject(new Error('请选择 500KB 以内的图片')); return }
|
|
|
|
|
|
const mime = data.startsWith('/9j/') ? 'image/jpeg' : (data.startsWith('iVBOR') ? 'image/png' : (data.startsWith('UklGR') ? 'image/webp' : ''))
|
|
|
|
|
|
if (mime.length == 0) { reject(new Error('请选择 JPG、PNG 或 WebP 图片')); return }
|
|
|
|
|
|
resolve('data:' + mime + ';base64,' + data)
|
|
|
|
|
|
}, fail: () => reject(new Error('图片读取失败,请重新选择')) })
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|