topfans/frontend/utils/scanLaunch.js
2026-07-13 11:22:56 +08:00

163 lines
6.1 KiB
JavaScript

/**
* 扫码结果处理(纯函数 + 副作用拆分)
* @param {string} rawUrl uni.scanCode 回调里的 result 字符串
* @returns {{ ok: true, assetId: number } | { ok: false, reason: string }}
*
* ★ 兼容处理(2026-07-10 加):
* 1) 剥首尾空白(扫码 app 有时会带 \n / 空格)
* 2) 缺 scheme 时补 http://(Android 扫码常剥 scheme)
* 3) try/catch + decodeURIComponent 兜底(被二次编码的 URL)
* 4) host 大小写不敏感(toLowerCase 比较)
* 5) path 自动 strip 末尾 /(扫码有时会带 trailing slash)
* 6) JSON 包装 {url: "..."} 拆包
* 7) topfans:// 自定义 scheme(从 H5 唤起 app 时用)
*/
export function parseVerifyUrl(rawUrl) {
// ★ 诊断:在函数入口打印 raw 输入,方便定位扫码 app 返回什么
console.log('[parseVerifyUrl] 📥 扫描原始数据:', JSON.stringify({
type: typeof rawUrl,
length: typeof rawUrl === 'string' ? rawUrl.length : null,
value: typeof rawUrl === 'string' ? rawUrl.slice(0, 200) : rawUrl,
isJSON: typeof rawUrl === 'string' && rawUrl.trim().startsWith('{'),
}))
if (!rawUrl || typeof rawUrl !== 'string') {
console.warn('[parseVerifyUrl] invalid input type:', typeof rawUrl)
return { ok: false, reason: '二维码格式不正确' }
}
// 1) 剥空白
let s = rawUrl.trim()
if (!s) {
console.warn('[parseVerifyUrl] empty after trim')
return { ok: false, reason: '二维码格式不正确' }
}
// 2) JSON 包装拆包(部分扫码 app 返 {url: "..."})
if (s.startsWith('{') && s.endsWith('}')) {
try {
const obj = JSON.parse(s)
const extracted = obj.url || obj.URL || obj.link || obj.data
if (extracted && typeof extracted === 'string') {
s = extracted.trim()
}
} catch { /* 不是合法 JSON,继续 */ }
}
// 3) 解码(如果整段被 URL 编码了,先 decode 一次)
if (s.includes('%3A') || s.includes('%2F')) {
try { s = decodeURIComponent(s) } catch { /* 保持原样 */ }
}
// 4) 补 scheme(只有缺时才补,保留 https/http/topfans)
if (!/^(https?|topfans):\/\//i.test(s)) {
s = 'http://' + s
}
// 5) 解析 — 用正则手动拆解(绕开 uniapp Android 老基座 `new URL` 不可用问题)
// 格式: <proto>://<host>[/<path>][?query][#fragment]
// 例: http://topfans.online/verify/88100001
// 或: topfans://verify/88100001
// 或: topfans.online/verify/88100001 (无 scheme,前面已补 http://)
const urlMatch = s.match(/^((?:https?|topfans):)\/\/([^\/\?#]+)(\/[^?#]*)?(\?[^#]*)?(#.*)?$/i)
if (!urlMatch) {
console.warn('[parseVerifyUrl] URL 解析失败(正则不匹配):', s)
return { ok: false, reason: '二维码格式不正确' }
}
const proto = urlMatch[1].toLowerCase() // 已带 ":"(如 "http:")
const host = urlMatch[2].toLowerCase()
const rawPath = urlMatch[3] || '/'
// 6) 协议 + host 校验(支持 http/https UniversalLink + topfans 自定义 scheme)
const isValid =
((proto === 'http:' || proto === 'https:') && host === 'topfans.online') ||
(proto === 'topfans:' && host === 'verify')
if (!isValid) {
console.warn('[parseVerifyUrl] host check fail:', proto, '//', host)
return { ok: false, reason: '二维码格式不正确' }
}
// 7) path 必须以 /verify/ 开头,strip 末尾 /
let pathname = rawPath
if (proto === 'topfans:' && host) {
pathname = '/' + host + pathname
}
if (pathname.endsWith('/')) pathname = pathname.slice(0, -1)
if (!pathname.startsWith('/verify/') && pathname !== '/verify') {
console.warn('[parseVerifyUrl] path check fail:', pathname)
return { ok: false, reason: '二维码格式不正确' }
}
// 8) 提取 code(/verify/{code})
const segs = pathname.split('/')
const code = segs[2] || ''
if (!code) {
console.warn('[parseVerifyUrl] code empty:', segs)
return { ok: false, reason: '二维码格式不正确' }
}
console.log('[parseVerifyUrl] ✅ code=' + code)
return { ok: true, code, rawUrl }
}
/**
* 入口:解析 + 登录 + 跳转(用户主动扫码,解析失败要 toast 提示)
*/
export async function onScanResult(rawUrl) {
const parsed = parseVerifyUrl(rawUrl)
if (!parsed.ok) {
uni.showToast({ title: parsed.reason, icon: 'none' })
return
}
await navigateToVerify(parsed.code)
}
/**
* Deep link 入口:解析 + 登录 + 跳转(系统唤起,解析失败静默吞掉)
*/
export async function onDeepLinkTo(rawUrl) {
const parsed = parseVerifyUrl(rawUrl)
if (!parsed.ok) {
// 静默:系统唤起常因剪贴板/分享被截获的旧 URL 出现,不应弹 toast
return
}
await navigateToVerify(parsed.code)
}
/**
* 私有:已登录跳验真页,未登录跳 portal(带 redirect)
*
* ★ Plan A 修复(2026-07-10):preloader 在 5 个 auth-required API 失败后会自己 reLaunch
* 到 portal,与本函数的 navigateTo(portal)并发,uni-app 抛"do not operate continuously"
* 警告。修复:
* 1) 未登录时先查 getCurrentPages() 末页是否已在 portal,是则跳过 navigateTo
* 2) 始终把目标 URL 存到 storage(`pending_scan_url`),即使 preloader 先跳 portal
* 抢走了 redirect param,portal 仍能从 storage 读出来(后续 portal 侧需配合读取)
*/
async function navigateToVerify(code) {
const token = uni.getStorageSync('access_token') || ''
const targetUrl = `/pages/scan/verify?code=${encodeURIComponent(String(code))}`
// 1) 始终落盘目标 URL,登录后 portal 可读
uni.setStorageSync('pending_scan_url', targetUrl)
// 2) 已登录直接跳
if (token) {
return uni.navigateTo({ url: targetUrl })
}
// 3) 未登录:检查当前是否已经在 portal(防 preloader 抢跳)
const pages = getCurrentPages()
const lastRoute = pages.length > 0 ? pages[pages.length - 1].route || '' : ''
if (lastRoute.indexOf('pages/login/portal') === 0) {
// preloader 已跳 portal,不再 navigateTo(否则 uni-app 报警"do not operate continuously")
// target URL 已在 storage,portal 登录成功后应读 `pending_scan_url` 跳转
return
}
// 4) 正常跳 portal + redirect(常规路径,preloader 还没动)
return uni.navigateTo({
url: `/pages/login/portal?redirect=${encodeURIComponent(targetUrl)}`
})
}