feat(分享图): 反查数据源换 /api/mp/public/share-images(含下架应用)—— mama-box/laixin 可配自定义转发图

- APPS_API 换 box_app 专用反查接口(不再被 isPublished=1 过滤卡住)
- 缓存 key bump share_apps_index → v2(旧缓存只有上架应用,残留 1h)
- shareCard.js 首次入库(此前为未跟踪文件);文档同步数据流与缓存说明
This commit is contained in:
2026-09-23 16:59:55 +08:00
parent dcc84dd7b7
commit 91f1ee849c
2 changed files with 354 additions and 19 deletions

234
mixins/shareCard.js Normal file
View File

@@ -0,0 +1,234 @@
// +----------------------------------------------------------------------
// | 分享卡片:box_app 后台配置的分享图 + H5 postMessage 覆盖
// +----------------------------------------------------------------------
// 背景(详见 crmeb/docs/分享图片获取逻辑.md):
// · web-view 里的 H5 不能自己拉起分享面板,卡片只能由承载 <web-view> 的页面产出;
// · 分享图有两个来源,优先级:
// ① box_app App 表的 shareImageUrl(按当前 H5 的 URL 前缀反查)
// ② H5 postMessage({action:'share', imageUrl})(有值才覆盖 ①)
// · 有两个宿主页都需要这套逻辑:
// pages/annex/web_view/index.vue —— 容器页(太阳码 / 分享落地 / JumpPath 进入)
// pages/index/index.vue —— 首页壳(mama-box 应用宫格用 location.href 换 H5,
// 小程序页面栈不变,所以这里的分享永远发生在首页壳)
// 本 mixin 把「反查 + 缓存 + 朋友圈本地图转换」抽出来共用,避免两边逻辑漂移。
//
// 用法:
// ① import shareCard from '@/mixins/shareCard.js' → mixins: [shareCard]
// ② 页面 onLoad 调一次 this._loadShareApps()(预热全量应用表,分享时可同步命中)
// ③ onShareAppMessage(options) / 分享前:
// const target = (options && options.webViewUrl) || this.shareHref || this.webUrl;
// this.resolveShareCard(target); // 应用切换检测 + 每次按当前地址重解析(同步出图)
// return { ..., imageUrl: this.shareImageUrl || '' };
//
// ⚠️ 首页壳不会随 H5 应用切换重新 onLoad,所以:
// · 必须缓存「全量应用表」,在分享那一刻用 options.webViewUrl 同步匹配;
// · **不能把 shareImageUrl 当长期状态**:每次分享都要重新解析,并检测 target 是否换了应用,
// 否则 B 应用会沿用 A 应用的图(resolveShareCard / syncShareCardTarget 就是干这个的)。
const BOX_API_BASE = 'https://mp.yizhezhe.com';
// 分享图反查表:box_app 专用接口,**含未上架应用**(mama-box 没有上架记录、laixin
// 已下架但仍经底 tab/来信卡进入,它们也要有自定义转发图)。
// 与「宫格展示」用的 /api/mp/apps?isPublished=1 刻意解耦:上架状态只决定宫格显隐,
// 不决定转发图 —— 反查只看 URL 前缀(接口只回 url+shareImageUrl,长前缀优先)。
const APPS_API = BOX_API_BASE + '/api/mp/public/share-images';
const APPS_CACHE_KEY = 'share_apps_index_v2'; // 反查表本地缓存 { t, items }
// (v2:数据源从上架表换成含下架的反查表,bump key 让旧缓存立即失效)
const APP_IMG_CACHE_PREFIX = 'web_view_share_img:'; // 单个应用分享图缓存(按 URL 基){ t, url }
const APPS_CACHE_TTL = 60 * 60 * 1000; // 应用表缓存 1 小时
const APP_IMG_CACHE_TTL = 24 * 60 * 60 * 1000; // 单应用分享图缓存 24 小时(后台改图最迟一天生效)
export default {
data() {
return {
// ---------- 分享(H5 无法自己拉起分享面板,只能由承载 web-view 的页面转发) ----------
shareTitle: '', // H5 postMessage(action:'share') 自定义标题
shareSummary: '', // 分享摘要(部分场景用)
shareHref: '', // H5 指定的分享链接(兜底;深链优先 webViewUrl)
shareImageUrl: '', // 好友转发卡片图(网络 https)
shareTimelineImage: '', // 朋友圈卡片图(必须本地路径,见 _prepareTimelineImage)
shareImageSource: '', // 当前分享图来源:'box'(box_app 反查)| 'h5'(H5 postMessage 覆盖)
shareImageBase: '', // 当前分享图所属的 H5 URL 基(切换应用时用它丢弃上一张)
shareApps: [], // box_app 应用表(url → shareImageUrl),供同步匹配
shareAppsLoaded: false, // 应用表是否已就绪
shareAppsLoading: false,// 应用表请求中(防并发重入)
_shareFetched: false, // 本页是否已做过一次异步兜底反查
};
},
methods: {
/* 当前 H5 URL 去掉 query / hash / 末尾斜杠,作为应用 url 的匹配基 */
_shareUrlBase(url) {
return String(url || '').split('#')[0].split('?')[0].replace(/\/+$/, '');
},
/* 清空分享卡片状态(应用切换时调用,避免张冠李戴) */
_clearShareCard() {
this.shareImageUrl = '';
this.shareTimelineImage = '';
this.shareImageSource = '';
this.shareTitle = '';
this.shareSummary = '';
this.shareHref = '';
},
/**
* 分享前调用:检测"当前要分享的 H5 是否换了一个应用"。
* 首页壳用 location.href 在 H5 内部换应用,小程序页面不重新 onLoad ——
* 若不检测,上一个应用的分享图/标题/href 会一直留着(本次线上问题的根因)。
* @returns {string} 当前 target 的 URL 基
*/
syncShareCardTarget(targetUrl) {
const base = this._shareUrlBase(targetUrl);
if (!base) return '';
if (this.shareImageBase && this.shareImageBase !== base) {
// 换了应用:丢弃上一应用的分享数据
this._clearShareCard();
}
this.shareImageBase = base;
return base;
},
/**
* 分享前的统一解析(两个宿主页的 onShareAppMessage 都调它):
* ① 应用切换检测 → 换应用先清空;
* ② 每次都按"当前 H5 地址"重新反查 box_app 的 shareImageUrl
* (不能因为 shareImageUrl 已有值就跳过 —— 那可能是上一个应用的图);
* ③ 当前图若来自本应用 H5 的 postMessage(source='h5'),保留不覆盖。
*/
resolveShareCard(targetUrl) {
this.syncShareCardTarget(targetUrl);
const fromBox = this._lookupShareImage(targetUrl);
if (fromBox) {
if (this.shareImageSource !== 'h5') this._applyShareImage(fromBox, 'box', targetUrl);
} else if (this.shareImageSource === 'box') {
// 当前应用没有 box_app 配置图,而现有图是上一个应用反查来的 → 丢弃
this.shareImageUrl = '';
this.shareImageSource = '';
this.shareTimelineImage = '';
}
if (!this.shareAppsLoaded) this._loadShareApps();
},
/* 在内存应用表里按 URL 前缀匹配 shareImageUrl(同步,分享那一刻就能出图) */
_matchAppShareImage(targetUrl) {
const base = this._shareUrlBase(targetUrl);
if (!base) return '';
const apps = this.shareApps || [];
for (let i = 0; i < apps.length; i++) {
const app = apps[i];
if (!app || !app.url || !app.shareImageUrl) continue;
const appBase = this._shareUrlBase(app.url);
if (!appBase) continue;
// 如 https://mp.yizhezhe.com/taidong == https://mp.yizhezhe.com/taidong/
if (base === appBase || base.indexOf(appBase + '/') === 0) return app.shareImageUrl;
}
return '';
},
/* 同步查图:优先内存应用表(onLoad 预热,1h 缓存),再退单应用缓存(24h TTL) */
_lookupShareImage(targetUrl) {
const base = this._shareUrlBase(targetUrl);
if (!base) return '';
// 1) 内存应用表:后台改图后最迟 1h 生效
const fromList = this._matchAppShareImage(targetUrl);
if (fromList) return fromList;
// 2) 单应用缓存兜底(应用表请求失败 / 冷启动还没回来时用)
try {
const cached = uni.getStorageSync(APP_IMG_CACHE_PREFIX + base);
// 兼容旧格式(纯 URL 字符串,无 TTL)
const rec = (cached && typeof cached === 'object') ? cached : { t: 0, url: cached };
if (rec && rec.url && String(rec.url).indexOf('http') === 0 &&
(!rec.t || (Date.now() - rec.t) < APP_IMG_CACHE_TTL)) {
return rec.url;
}
} catch (e) {}
return '';
},
/* 拉全量已上架应用(带 1h 本地缓存);cb 可选,拉完(或命中缓存)后回调 */
_loadShareApps(cb) {
// 1) 本地缓存命中:冷启动也能同步出图
try {
const cached = uni.getStorageSync(APPS_CACHE_KEY);
if (cached && cached.items && cached.items.length && cached.t &&
(Date.now() - cached.t) < APPS_CACHE_TTL) {
this.shareApps = cached.items;
this.shareAppsLoaded = true;
if (cb) cb();
return;
}
} catch (e) {}
// 2) 请求中:直接返回,避免并发重复拉(漏掉的 cb 由调用方自身的兜底路径覆盖)
if (this.shareAppsLoading) return;
this.shareAppsLoading = true;
uni.request({
url: APPS_API,
method: 'GET',
success: (res) => {
const items = (res.data && res.data.result && res.data.result.items) || [];
if (items.length) {
this.shareApps = items;
this.shareAppsLoaded = true;
try { uni.setStorageSync(APPS_CACHE_KEY, { t: Date.now(), items: items }); } catch (e) {}
}
if (cb) cb();
},
fail: (err) => {
// 常见原因:mp.yizhezhe.com 未加入小程序后台 request 合法域名白名单
console.warn('[shareCard] 应用表拉取失败(请检查 mp.yizhezhe.com 是否在 request 合法域名):', err);
if (cb) cb();
},
complete: () => { this.shareAppsLoading = false; }
});
},
/* 异步兜底反查(容器页 onLoad 用):同步没命中就拉应用表再匹配一次 */
_fetchShareImage(targetUrl) {
if (!targetUrl) return;
this.syncShareCardTarget(targetUrl); // 建立/校正"当前图属于哪个应用"
if (this.shareImageUrl) return; // H5 postMessage 已给图,不覆盖
if (this._shareFetched) return; // 本页只兜底一次
this._shareFetched = true;
const hit = this._lookupShareImage(targetUrl);
if (hit) { this._applyShareImage(hit, 'box', targetUrl); return; }
this._loadShareApps(() => {
const matched = this._matchAppShareImage(targetUrl);
if (!matched) return;
try { uni.setStorageSync(APP_IMG_CACHE_PREFIX + this._shareUrlBase(targetUrl), { t: Date.now(), url: matched }); } catch (e) {}
this._applyShareImage(matched, 'box', targetUrl);
console.log('[shareCard] 分享图片(兜底): ' + matched);
});
},
/* 统一设置分享图:好友转发用网络图;朋友圈另转本地图(见下)
@param source 'box'(box_app 反查,默认)| 'h5'(H5 postMessage 覆盖)
@param forUrl 该图所属的 H5 地址(用于切换应用时判断是否过期) */
_applyShareImage(url, source, forUrl) {
if (!url) return;
if (forUrl) this.shareImageBase = this._shareUrlBase(forUrl);
if (url === this.shareImageUrl) {
if (source) this.shareImageSource = source;
return; // 同一张图不重复处理(H5 可能重复 postMessage)
}
this.shareImageUrl = url;
this.shareImageSource = source || 'box';
this._prepareTimelineImage(url);
},
/* 朋友圈分享图必须是本地路径/代码包路径(网络图会被微信静默忽略)→ getImageInfo 转本地
注意:getImageInfo 对网络图同样受 downloadFile 合法域名限制,域名未配时朋友圈无自定义图。 */
_prepareTimelineImage(url) {
if (!url) return;
uni.getImageInfo({
src: url,
success: (res) => {
this.shareTimelineImage = res.path || '';
console.log('[shareCard] 朋友圈分享图已转本地:', this.shareTimelineImage);
},
fail: (err) => {
console.warn('[shareCard] 朋友圈分享图转本地失败(网络图/域名问题,回退默认截图):', err);
this.shareTimelineImage = '';
}
});
}
}
};