From cbf91fa905712db53a1434da1017d9f7c46d3f56 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Fri, 17 Jul 2026 11:37:53 +0800 Subject: [PATCH] feat(util,navigate): add uniacid stale check for wap url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增租户uniacid一致性校验逻辑,修复装修外链携带过期租户ID导致的跳转错误问题: 1. 新增pickCurrentUniacid和hasStaleUniacidInUrl工具函数 2. 在链接处理逻辑中校验wap_url内的uniacid与当前系统租户ID是否一致 3. 脏数据场景下强制走外链模式而非内部跳转 4. 补充完整的单元测试覆盖 --- common/js/util-navigate.js | 72 +++++++++++++++++++++++++++++ common/js/util.js | 45 ++++++++++++++++++ tests/util-navigate.test.js | 92 ++++++++++++++++++++++++++++++++++++- 3 files changed, 207 insertions(+), 2 deletions(-) diff --git a/common/js/util-navigate.js b/common/js/util-navigate.js index 42a982e..e620502 100644 --- a/common/js/util-navigate.js +++ b/common/js/util-navigate.js @@ -476,6 +476,78 @@ export function resolveNavMode(link) { return 'auto'; } +// ============================================================= +// hasStaleUniacidInUrl +// ============================================================= +// pickCurrentUniacid +// ============================================================= + +/** + * 从两个候选 uniacid 源中选出"系统当前 uniacid"。 + * + * 用法(当前 diyRedirectTo 中的实际调用, 必须与 http.js 行 168 保持一致): + * pickCurrentUniacid( + * uni.getStorageSync('uniacid'), // 持久化, 登录/初始化写入 + * Config.uniacid, // 编译期兜底 + * ) + * + * 优先级: 第一个参数非 0 → 选第一个; 否则选第二个非 0 值; 都没有 → 0。 + * 0 视为"无 uniacid": 业务上 uniacid 不会是 0, parseInt 后为 0 也算无效。 + * + * ★ 注意: 故意【不读 store.state.uniacid】。当前代码库里 store.state 上从未设置 + * uniacid 字段(http.js 只用 storage/Config), 若引入 store 分支会出现 + * "守卫用 A 真值, http 请求用 B 真值"的不一致。 + * + * @param {*} primaryValue 第一候选(运行时持久化值) + * @param {*} secondaryValue 第二候选(编译期兜底) + * @returns {number} 选出的 uniacid; 都无效时返回 0 + */ +export function pickCurrentUniacid(primaryValue, secondaryValue) { + const a = parseInt(primaryValue, 10); + if (a) return a; // 非 0 真值 + const b = parseInt(secondaryValue, 10); + if (b) return b; + return 0; +} + +// ============================================================= +// hasStaleUniacidInUrl +// ============================================================= + +/** + * 检测 link.wap_url 中【URL 原生】的 uniacid query 与系统当前 uniacid 是否一致。 + * + * 用途: uniacid 是平台租户 ID, 会随租户迁移 / 平台调整而变化。 + * - 如果数据里的 wap_url 自带的 uniacid 与当前系统不一致, + * 说明这份装修数据可能是历史遗留或别的租户的(脏数据), + * 不应该被自动改写成内部 SPA(否则会跳到错租户的内页 / 拿错数据)。 + * - 应当保守地走外链(整页 / web-view), 让原 URL(及其 uniacid)如实生效。 + * + * 关键: 必须【在 resolveLinkUrl 抽走 query 之前】判断, 否则 URL 里的 uniacid 已经 + * 被搬到 page_params 里, 分不清"URL 原生"还是"外加的"。 + * + * 比较规则: 两侧 parseInt 后比较, 兼容 '2482' vs 2482、URL 编码、空值。 + * + * @param {string} wap_url link.wap_url (resolveLinkUrl 之前的原始值) + * @param {string|number|null} currentUniacid 系统当前的 uniacid + * (0 / null / '' / undefined 一律视为"无当前 uniacid", 返回 false — 让上游决定怎么做) + * @returns {boolean} true = URL 原生 uniacid 与当前不一致(脏数据, 该强制走线上) + */ +export function hasStaleUniacidInUrl(wap_url, currentUniacid) { + if (!wap_url || typeof wap_url !== 'string') return false; + if (currentUniacid == null || currentUniacid === '' || currentUniacid === 0) return false; + const m = wap_url.match(/[?&]uniacid=([^&#]*)/i); + if (!m) return false; + let urlUniacid; + try { urlUniacid = decodeURIComponent(m[1]); } catch (e) { urlUniacid = m[1]; } + const cur = String(currentUniacid); + const a = parseInt(urlUniacid, 10); + const b = parseInt(cur, 10); + // 两侧都能 parse 成数字时按数字比; 否则按字符串比 + if (!Number.isNaN(a) && !Number.isNaN(b)) return a !== b; + return urlUniacid !== cur; +} + // ============================================================= // appendQueryParams // ============================================================= diff --git a/common/js/util.js b/common/js/util.js index 0875518..ad5f068 100644 --- a/common/js/util.js +++ b/common/js/util.js @@ -19,6 +19,8 @@ import { appendQueryParams as _appendQueryParamsBase, extractInternalAppPath as _extractInternalAppPath, resolveNavMode as _resolveNavMode, + hasStaleUniacidInUrl as _hasStaleUniacidInUrl, + pickCurrentUniacid as _pickCurrentUniacid, _shouldEncodeUrl, _safeEncodeForPlatform, _navigateTo, @@ -44,6 +46,36 @@ import { export { decodeEntity, cleanParamUrls, _toParamValueString, adaptSubpackageUrl }; +/** + * 获取系统当前的 uniacid(运行时单点真值)。 + * + * 必须【与 common/js/http.js 发送请求时使用的 uniacid 完全一致】(行 168): + * data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid + * 否则 uniacid 守卫(脏数据判断)和实际网络请求会基于不同的真值, + * 产生不一致行为。 + * + * 真值源: + * 1) uni.getStorageSync('uniacid') — 持久化, 由登录 / 初始化写入 + * 2) Config.uniacid — 编译期兜底(从 .local.config.js / vue.config.js 注入) + * + * 0 视为"无 uniacid": hasStaleUniacidInUrl 收到 0 时直接返回 false (不强行判断)。 + * + * @returns {number} 当前 uniacid; 无法获取时返回 0 + */ +function _getCurrentUniacid() { + let storageVal; + try { + if (typeof uni !== 'undefined' && uni && typeof uni.getStorageSync === 'function') { + storageVal = uni.getStorageSync('uniacid'); + } + } catch (e) {} + let configVal; + try { + if (Config && Config.uniacid) configVal = Config.uniacid; + } catch (e) {} + return _pickCurrentUniacid(storageVal, configVal); +} + /** * 把 URL 上的 page_params 还原成对象。是 _toParamValueString 的逆操作。 * 容错策略:任何解析失败都安全降级到 defaultValue,不抛异常。 @@ -1088,6 +1120,19 @@ export default { link.wap_url = decodeEntity(link.wap_url); } + // 修复: wap_url 自带的 uniacid query 必须与系统当前 uniacid 一致 + // 原因: uniacid 是本平台租户 ID,会随租户迁移 / 平台调整而变化。 + // 装修数据里的 wap_url?uniacid=2482 一旦与系统当前 uniacid(比如 2483)不一致, + // 说明这份数据可能是历史遗留或别的租户的(脏数据), + // 不应该被自动改写成内部 SPA(否则会跳到错租户的内页 / 拿错数据)。 + // 应当保守地走外链(整页 / web-view), 让原 URL(及其 uniacid)如实生效。 + // ★ 必须在 resolveLinkUrl 之前判断 —— 否则 URL 的 query 被抽走就分不清了。 + // ★ open_mode=internal 时不覆盖 —— 用户显式强制内部, 表示"我知道我在做什么"。 + if (_hasStaleUniacidInUrl(link.wap_url, _getCurrentUniacid()) + && _resolveNavMode(link) !== 'internal') { + link.open_mode = 'online'; + } + // 优先用 page_key 解析实际URL(新机制,向后兼容) const resolvedUrl = resolveLinkUrl(link); if (link.page_key && resolvedUrl) { diff --git a/tests/util-navigate.test.js b/tests/util-navigate.test.js index 4ee7179..a2e4e51 100644 --- a/tests/util-navigate.test.js +++ b/tests/util-navigate.test.js @@ -37,7 +37,7 @@ function loadNavInSandbox() { .replace(/^export\s+let\s+/gm, 'let ') .replace(/^export\s+var\s+/gm, 'var ') + '\nmodule.exports = { _toParamValueString, parsePageParams, resolveLinkUrl, ' - + 'cleanParamUrls, normalizeUrl, appendQueryParams, extractInternalAppPath, resolveNavMode, getPlatform, shouldEncodeUrl, ' + + 'cleanParamUrls, normalizeUrl, appendQueryParams, extractInternalAppPath, resolveNavMode, hasStaleUniacidInUrl, pickCurrentUniacid, getPlatform, shouldEncodeUrl, ' + '_safeEncodeForPlatform, _navigateTo, createNavigateApi };\n'; const sandbox = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports }; sandbox.globalThis = sandbox; @@ -54,7 +54,7 @@ test('util-navigate.js: 能被 vm 加载,所有 export 都存在', () => { const expected = [ '_toParamValueString', 'parsePageParams', 'resolveLinkUrl', 'cleanParamUrls', 'normalizeUrl', 'appendQueryParams', - 'extractInternalAppPath', 'resolveNavMode', + 'extractInternalAppPath', 'resolveNavMode', 'hasStaleUniacidInUrl', 'pickCurrentUniacid', 'getPlatform', 'shouldEncodeUrl', '_safeEncodeForPlatform', '_navigateTo', 'createNavigateApi', ]; @@ -520,3 +520,91 @@ test('resolveNavMode: 未设置 / 未知值 → auto', () => { assert.equal(resolveNavMode(null), 'auto'); assert.equal(resolveNavMode(undefined), 'auto'); }); + +// ============================================================= +// hasStaleUniacidInUrl — URL 原生 uniacid vs 系统当前 uniacid +// 修复: 装修数据里硬编码的 uniacid 可能与系统不一致(脏数据 / 过期租户) +// ============================================================= +test('hasStaleUniacidInUrl: URL 无 uniacid → false', () => { + const { hasStaleUniacidInUrl } = loadNavInSandbox(); + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?id=130', 2482), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing', 2482), false); +}); + +test('hasStaleUniacidInUrl: URL uniacid 与当前一致 → false', () => { + const { hasStaleUniacidInUrl } = loadNavInSandbox(); + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482', 2482), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482', '2482'), false); // 字符串数字 + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482&id=130', 2482), false); +}); + +test('hasStaleUniacidInUrl: URL uniacid 与当前不一致 → true(脏数据)', () => { + const { hasStaleUniacidInUrl } = loadNavInSandbox(); + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482', 2483), true); + assert.equal(hasStaleUniacidInUrl('https://partner.com/landing?uniacid=2482&id=130', 9999), true); +}); + +test('hasStaleUniacidInUrl: 兼容 URL 编码 + 大小写 + 多个 query', () => { + const { hasStaleUniacidInUrl } = loadNavInSandbox(); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?UNIACID=2482', 2482), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=%32%34%38%32', 2482), false); // 全 URL 编码 + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482&a=1&b=2', 2482), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?a=1&uniacid=2482&b=2', 2482), false); +}); + +test('hasStaleUniacidInUrl: currentUniacid 无效 → false(无法比较)', () => { + const { hasStaleUniacidInUrl } = loadNavInSandbox(); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', null), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', undefined), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', ''), false); + assert.equal(hasStaleUniacidInUrl('https://partner.com/x?uniacid=2482', 0), false); +}); + +test('hasStaleUniacidInUrl: 空 / 非法输入 → false(安全降级)', () => { + const { hasStaleUniacidInUrl } = loadNavInSandbox(); + assert.equal(hasStaleUniacidInUrl('', 2482), false); + assert.equal(hasStaleUniacidInUrl(null, 2482), false); + assert.equal(hasStaleUniacidInUrl(undefined, 2482), false); +}); + +// ============================================================= +// pickCurrentUniacid — 从两个候选中选 uniacid +// 实际调用: (uni.getStorageSync('uniacid'), Config.uniacid) +// 必须与 http.js 行 168: data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid +// 保持完全一致, 避免守卫/请求用不同真值 +// 优先级: 第一参(运行时) > 第二参(编译期兜底) > 0 +// ============================================================= +test('pickCurrentUniacid: 第一参(运行时 storage)非 0 → 选第一参', () => { + const { pickCurrentUniacid } = loadNavInSandbox(); + assert.equal(pickCurrentUniacid(2483, 2482), 2483); + assert.equal(pickCurrentUniacid('2483', 2482), 2483); // 字符串数字兼容 +}); + +test('pickCurrentUniacid: 第一参无值 → 回退到第二参(Config 兜底)', () => { + const { pickCurrentUniacid } = loadNavInSandbox(); + assert.equal(pickCurrentUniacid(undefined, 2482), 2482); + assert.equal(pickCurrentUniacid(null, 2482), 2482); + assert.equal(pickCurrentUniacid('', 2482), 2482); // '' 视为无效 +}); + +test('pickCurrentUniacid: 第一参为 0 → 回退到第二参(关键边界)', () => { + const { pickCurrentUniacid } = loadNavInSandbox(); + // 关键: storage 返回 0(空值)等价于"未设置", 必须回退到 Config.uniacid + // 不能因为 storage 是 0 而把 0 当成"已设值"返回 + assert.equal(pickCurrentUniacid(0, 2482), 2482); + assert.equal(pickCurrentUniacid(0, '2482'), 2482); +}); + +test('pickCurrentUniacid: 两个参都无值 → 0', () => { + const { pickCurrentUniacid } = loadNavInSandbox(); + assert.equal(pickCurrentUniacid(undefined, undefined), 0); + assert.equal(pickCurrentUniacid(null, null), 0); + assert.equal(pickCurrentUniacid(0, 0), 0); + assert.equal(pickCurrentUniacid('', ''), 0); +}); + +test('pickCurrentUniacid: 字符串数字自动 parseInt', () => { + const { pickCurrentUniacid } = loadNavInSandbox(); + assert.equal(pickCurrentUniacid('2482', undefined), 2482); + assert.equal(pickCurrentUniacid('2482abc', undefined), 2482); // 解析到非数字停止 +});