feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑

- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作
- 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转
- 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录
- 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交
- 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
This commit is contained in:
hujun
2026-10-07 10:01:05 +08:00
parent 1723363b7e
commit 71b3c25695
37 changed files with 1389 additions and 323 deletions
+24 -10
View File
@@ -4,6 +4,7 @@ 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)
@@ -17,7 +18,7 @@ export function requireAccount(): UTSJSONObject {
}
export function sameAccount(session: UTSJSONObject): boolean {
const current = getSession()
return current != null && draftText(current, 'token') == draftText(session, 'token') && draftText(current, 'phone') == draftText(session, 'phone')
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()
@@ -37,19 +38,26 @@ async function accountRequest(path: string, method: RequestMethod, payload: UTSJ
requireAccount()
return await requestJson(path, method, payload)
}
function cacheProfile(result: UTSJSONObject, phone: string): AccountProfile {
const profile: AccountProfile = { phone, nickname: draftText(result, 'nickname'), avatar: draftText(result, 'avatarUrl') }
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('登录状态已变化,请重新进入页面')
return cacheProfile(result, draftText(session, 'phone'))
// 保存完成前发出的旧读取不能回写昵称或头像缓存。
if (version != profileWriteVersion) throw new Error('账户资料已更新,请重新读取')
return cacheProfile(result, session)
}
export async function saveAccountProfile(nickname: string, avatarId: string): Promise<void> {
const session = requireAccount(); const phone = draftText(session, 'phone')
const session = requireAccount()
const name = nickname.trim()
if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符')
const payload: UTSJSONObject = { nickname: name }
@@ -57,12 +65,16 @@ export async function saveAccountProfile(nickname: string, avatarId: string): Pr
else if (avatarId.length > 0) payload['avatarId'] = avatarId
const result = await accountRequest('/account/profile', 'PUT', payload)
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
cacheProfile(result, phone)
profileWriteVersion++
cacheProfile(result, session)
}
export async function uploadAccountAvatar(path: string): Promise<UTSJSONObject> {
requireAccount()
return await uploadJson('/account/avatar', path)
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('验证码用途无效')
@@ -75,12 +87,14 @@ export async function changeAccountPassword(mode: string, oldPassword: string, c
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 })
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 位数字验证码')
await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
const result = await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
if (result['reLoginRequired'] != true) throw new Error('手机号修改响应不完整,请重试')
}