Files
hujun 71b3c25695 feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑
- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作
- 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转
- 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录
- 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交
- 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
2026-10-07 10:01:05 +08:00

101 lines
6.5 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<UTSJSONObject> {
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<AccountProfile> {
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<void> {
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<UTSJSONObject> {
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<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 位数字验证码')
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<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 位数字验证码')
const result = await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
if (result['reLoginRequired'] != true) throw new Error('手机号修改响应不完整,请重试')
}