feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑
- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作 - 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转 - 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录 - 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交 - 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
This commit is contained in:
@@ -3,17 +3,122 @@ import { APP_ENVIRONMENT } from '@/services/config.uts'
|
||||
|
||||
export type ReportDownload = { cancel: () => void, done: Promise<string> }
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
let previewFiles: string[] = []
|
||||
const cleanPreviewFiles = () => {
|
||||
previewFiles = previewFiles.filter((path: string): boolean => {
|
||||
try { uni.getFileSystemManager().unlinkSync(path); return false } catch (_err) { return true }
|
||||
})
|
||||
}
|
||||
// 系统文档预览期间保留临时文件,返回小程序后清理;不调用saveFile持久化报告。
|
||||
uni.onAppShow(() => cleanPreviewFiles())
|
||||
// #endif
|
||||
|
||||
export function downloadReportPdf(id: string, revisionId: string = ''): ReportDownload {
|
||||
// 首版仅开放普通浏览器H5;其他端明确提示,不生成假文件或成功回执。
|
||||
// #ifndef H5
|
||||
// #ifndef H5 || MP-WEIXIN
|
||||
return { cancel: () => {}, done: new Promise<string>((_resolve, reject) => { reject(new Error('当前客户端暂未开放PDF下载,请在普通浏览器打开H5并登录后下载。')) }) }
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
cleanPreviewFiles()
|
||||
let cancelled = false; let opening = false; let settled = false
|
||||
let task: DownloadTask | null = null; let tempPath = ''; let rejectDownload: ((error: Error) => void) | null = null
|
||||
const removeTemp = () => {
|
||||
if (tempPath.length == 0) return
|
||||
try { uni.getFileSystemManager().unlinkSync(tempPath) } catch (_err) { previewFiles.push(tempPath) }
|
||||
tempPath = ''
|
||||
}
|
||||
const fail = (message: string) => {
|
||||
removeTemp()
|
||||
if (settled) return
|
||||
settled = true; rejectDownload?.(new Error(message))
|
||||
}
|
||||
const stop = (message: string) => { fail(message); task?.abort() }
|
||||
const cancel = () => {
|
||||
// openDocument已经接管文件后,原页面可能隐藏/销毁,不能中止或删除正在预览的文件。
|
||||
if (opening || settled) return
|
||||
cancelled = true; stop('下载已取消。')
|
||||
}
|
||||
const done = new Promise<string>((resolve, reject) => {
|
||||
rejectDownload = reject
|
||||
if (!/^[1-9][0-9]{0,9}$/.test(id) || (revisionId.length > 0 && !/^[1-9][0-9]{0,9}$/.test(revisionId))) { fail('报告或版本编号无效,请重新打开本人报告。'); return }
|
||||
const token = getSession()?.getString('token') ?? ''
|
||||
if (token.length == 0) { fail('登录已过期,请重新登录后下载。'); return }
|
||||
if (!API_BASE_URL.startsWith('https://')) { fail('小程序PDF下载需要配置HTTPS服务地址。'); return }
|
||||
const current = (): boolean => {
|
||||
if (settled) return false
|
||||
if (cancelled || getSession()?.getString('token') != token) { fail('页面或登录状态已变化,本次下载已取消。'); return false }
|
||||
return true
|
||||
}
|
||||
let mime = ''; let declaredSize = 0; let encoded = false
|
||||
task = uni.downloadFile({
|
||||
url: API_BASE_URL + '/assessments/' + id + '/report.pdf' + (revisionId.length > 0 ? '?revisionId=' + encodeURIComponent(revisionId) : ''),
|
||||
header: { Authorization: 'Bearer ' + token, 'X-Requested-With': 'XMLHttpRequest' }, timeout: 45000,
|
||||
success: (response) => {
|
||||
tempPath = response.tempFilePath
|
||||
if (!current()) { removeTemp(); return }
|
||||
if (response.statusCode == 401) { clearSession(); fail('登录已过期,请重新登录后下载。'); return }
|
||||
try {
|
||||
const fs = uni.getFileSystemManager()
|
||||
const size = (fs.statSync(tempPath, false) as UniNamespace.Stats).size
|
||||
if (response.statusCode != 200 || mime != 'application/pdf') {
|
||||
let message = 'PDF暂不可下载,请稍后重试;已保存报告不受影响。'
|
||||
if (mime == 'application/json' && size > 0 && size <= 8192) {
|
||||
const body = JSON.parse<UTSJSONObject>(fs.readFileSync(tempPath, 'utf8') as string)
|
||||
const serverMessage = body?.getString('msg') ?? ''
|
||||
if (serverMessage.length > 0 && serverMessage.length < 200) message = serverMessage
|
||||
}
|
||||
fail(message); return
|
||||
}
|
||||
if (size > 20971520) { fail('PDF文件过大,请联系管理员。'); return }
|
||||
if (size < 100 || (!encoded && declaredSize > 0 && declaredSize != size)) { fail('下载内容不是完整PDF,请重试。'); return }
|
||||
// 只读取头尾,避免将整份报告加载到小程序内存。
|
||||
const start = fs.readFileSync(tempPath, 'ascii', 0, 5) as string
|
||||
const tailLength = Math.min(size, 1024)
|
||||
const end = fs.readFileSync(tempPath, 'ascii', size - tailLength, tailLength) as string
|
||||
if (start != '%PDF-' || !end.includes('%%EOF')) { fail('下载内容不是完整PDF,请重试。'); return }
|
||||
if (!current()) return
|
||||
opening = true
|
||||
uni.openDocument({ filePath: tempPath, fileType: 'pdf', showMenu: true,
|
||||
success: () => {
|
||||
if (!current()) return
|
||||
previewFiles.push(tempPath); tempPath = ''; settled = true
|
||||
resolve('PDF已打开,可通过右上角菜单保存或转发。')
|
||||
}, fail: () => { opening = false; fail('PDF已下载但暂时无法打开,请重试或更新微信。') }
|
||||
})
|
||||
} catch (_err) { fail('PDF文件读取失败或内容异常,请重试。') }
|
||||
},
|
||||
fail: (error) => {
|
||||
if (!current()) return
|
||||
const reason = error.errMsg ?? ''
|
||||
if (reason.includes('domain list')) fail('PDF下载域名未配置,请联系管理员。')
|
||||
else if (reason.includes('timeout')) fail('PDF生成超时,请稍后重试。')
|
||||
else fail('PDF下载失败,请检查网络后重试。')
|
||||
}
|
||||
})
|
||||
task!.onHeadersReceived((response) => {
|
||||
if (!current()) { task?.abort(); return }
|
||||
const header = response.header as UTSJSONObject
|
||||
Object.keys(header).forEach((key: string) => {
|
||||
const value = header[key]?.toString() ?? ''
|
||||
if (key.toLowerCase() == 'content-type') mime = value.split(';')[0].trim().toLowerCase()
|
||||
if (key.toLowerCase() == 'content-length') declaredSize = Number(value)
|
||||
if (key.toLowerCase() == 'content-encoding') encoded = value.length > 0 && value.toLowerCase() != 'identity'
|
||||
})
|
||||
if (declaredSize > 20971520) stop('PDF文件过大,请联系管理员。')
|
||||
})
|
||||
task!.onProgressUpdate((progress) => {
|
||||
if (!current()) { task?.abort(); return }
|
||||
if (progress.totalBytesWritten > 20971520 || progress.totalBytesExpectedToWrite > 20971520) stop('PDF文件过大,请联系管理员。')
|
||||
})
|
||||
})
|
||||
return { cancel, done }
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
const controller = new AbortController()
|
||||
let cancelled = false
|
||||
const cancel = () => { cancelled = true; controller.abort() }
|
||||
const done = async (): Promise<string> => {
|
||||
if (!/^[1-9][0-9]{0,9}$/.test(id)) throw new Error('报告编号无效,请重新打开本人报告。')
|
||||
if (!/^[1-9][0-9]{0,9}$/.test(id) || (revisionId.length > 0 && !/^[1-9][0-9]{0,9}$/.test(revisionId))) throw new Error('报告或版本编号无效,请重新打开本人报告。')
|
||||
if (/MicroMessenger/i.test(navigator.userAgent)) throw new Error('微信内浏览器暂不支持此下载。请点右上角菜单,选择“在浏览器打开”,登录后重新点击下载PDF。')
|
||||
const token = getSession()?.getString('token') ?? ''
|
||||
if (token.length == 0) throw new Error('登录已过期,请重新登录后下载。')
|
||||
@@ -28,6 +133,7 @@ export function downloadReportPdf(id: string, revisionId: string = ''): ReportDo
|
||||
cache: 'no-store', credentials: 'omit', redirect: 'error', signal: controller.signal
|
||||
})
|
||||
current()
|
||||
if (response == null) throw new Error('下载服务未返回文件,请重试。')
|
||||
if (response.status == 401) { clearSession(); throw new Error('登录已过期,请重新登录后下载。') }
|
||||
const mime = (response.headers.get('Content-Type') ?? '').split(';')[0].trim().toLowerCase()
|
||||
if (!response.ok || mime != 'application/pdf') {
|
||||
@@ -44,7 +150,8 @@ export function downloadReportPdf(id: string, revisionId: string = ''): ReportDo
|
||||
current()
|
||||
const start = new TextDecoder('ascii').decode(bytes.slice(0, 5))
|
||||
const end = new TextDecoder('ascii').decode(bytes.slice(Math.max(0, bytes.byteLength - 1024)))
|
||||
if (bytes.byteLength < 100 || bytes.byteLength > 20971520 || start != '%PDF-' || !end.includes('%%EOF')) throw new Error('下载内容不是完整PDF,请重试。')
|
||||
const encoded = (response.headers.get('Content-Encoding') ?? '').toLowerCase()
|
||||
if (bytes.byteLength < 100 || bytes.byteLength > 20971520 || ((encoded.length == 0 || encoded == 'identity') && declaredSize > 0 && declaredSize != bytes.byteLength) || start != '%PDF-' || !end.includes('%%EOF')) throw new Error('下载内容不是完整PDF,请重试。')
|
||||
// 验证响应后才创建本机Blob。令牌只放请求头,不写URL、文件名或本机存储。
|
||||
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }))
|
||||
const link = document.createElement('a')
|
||||
|
||||
Reference in New Issue
Block a user