# 分享图片获取逻辑(box_app → 小程序 web_view) > 适用项目:CRMEB 小程序(crmeb)+ box_app 应用管理平台 > 功能:小程序 web_view 内点"转发给好友"时,分享卡片使用 box_app 后台配置的分享图片 --- ## 一、数据流全景 ``` [box_app 管理后台] 新建/编辑应用 → 上传"分享图片" + 填"分享标题" → 存入 App.shareImageUrl = https://mp.yizhezhe.com/upload/picture/xxx.png → 存入 App.shareTitle(留空 = 用应用名 App.name) [box_app API] GET /api/mp/apps?identifier=xxx&isPublished=1 (单个应用,太阳码场景) GET /api/mp/public/share-images (分享卡片反查表:所有有 url 的应用, **不过滤上架状态**,2026-09-22 起; 2026-10-13 起每行多回 shareTitle) → 返回 { url, shareImageUrl, shareTitle } 精简结构 (shareImageUrl 为空串 = 没配图;shareTitle 是服务端算好的生效值, 后台留空已回退成应用名 —— 客户端不再实现回退链) [小程序宿主页(两个,见第三节)] pages/annex/web_view/index.vue 容器页:扫码 / JumpPath / 分享落地 pages/index/index.vue 首页壳:兜爸心选应用宫格 location.href 进入的 H5 ① 获取 shareImageUrl(网络图)+ shareTitle → 内存 + 本地缓存(mixins/shareCard.js) ② 用户点 ··· 转发给好友 → 卡片图 = shareImageUrl(网络图,微信支持);标题 = shareTitle ③ 用户点 ··· 分享到朋友圈 → getImageInfo 转本地路径 → 卡片图 = 本地路径 ``` ### 分享标题的优先级(2026-10-13) | 优先级 | 来源 | 落点 | | --- | --- | --- | | ① | H5 上报(`location`/`share`/`setTitle` 消息的 `title`) | `webviewHost.handleHostMessage` → `shareTitle` + `shareTitleSource='h5'` | | ② | 后台该应用「分享标题」= `App.shareTitle` | `shareCard.resolveShareCard` → `_matchAppShareTitle` | | ③ | 应用名 `App.name`(后台留空时的服务端回退) | 同上(服务端已回退,客户端拿到的就是生效标题) | | ④ | 首页壳 `'兜爸心选 · 孕育陪伴'` / 容器页 `'分享'` | 两个宿主页的 `onShareAppMessage` 兜底 | ⚠️ 三条纪律(与分享图同款,别拆开改): 1. **`shareTitleSource==='h5'` 不被后台标题覆盖**,否则 H5 上报的页级标题会被应用级标题吃掉; 2. **换应用要清标题**(`syncShareCardTarget` → `_clearShareCard`),否则 B 应用沿用 A 的标题; 3. 反查数据的本地缓存 key 是 `share_apps_index_v3`(v2 的 items 没有 `shareTitle`)—— **加字段必须 bump key**,否则老客户端要等 1h TTL 才生效。 ## 二、box_app 侧配置(已实现) ### 后端(backend/app/models/app.py + services/app_serv.py) - `App` 模型字段:`shareImageUrl: str | None`(微信分享卡片图,公网 https URL)、 `shareTitle: str | None`(分享卡片标题,**留空 = 用应用名**,2026-10-13 加) - `AppCreate` / `AppUpdate` / `AppOut` 均含两字段,查询/写入透传(find / find_all / find_one / create / update 共 5 处) - 上传接口:`POST /api/upload`(返回公网 URL) ### 管理后台前端(frontend/src/pages/ProductManagement.jsx) - 新建/编辑应用表单:**"分享图片 (Share Image)"** 上传项 + **"分享标题 (Share Title)"** 输入项(maxLength 40) - 支持上传 `/api/upload`、编辑回填、单图(maxCount=1) - 分享图 extra 提示:建议 5:4 比例,公网可访问;分享标题 extra 提示:留空 = 用应用名称 ## 三、小程序端获取逻辑(两个宿主页共用 `mixins/shareCard.js`) > 宿主页有两个,**分享卡片由谁产出取决于用户是从哪进来的**: > - `pages/annex/web_view/index.vue` —— 容器页:太阳码扫码 / 分享卡片落地 / JumpPath / 客服 / 登录跳转; > - `pages/index/index.vue` —— 首页壳:兜爸心选首页的应用宫格用 `location.href` 换 H5(见 `mama-box/js/app.js` > `jumpApp`),**小程序页面栈不变**,所以从这里进的应用(数胎动、孕检日历…)分享永远发生在首页壳。 > > 反查 / 缓存 / 朋友圈本地图转换统一抽到 `mixins/shareCard.js`,两页行为一致。 ### 四种获取路径(按优先级) | 优先级 | 场景 | 方法 | 说明 | |--------|------|------|------| | ① 首选 | 太阳码扫码 | `_resolveAppScene` | 调 `mp/apps?identifier=xxx` 时**一次请求同时拿** H5 url + shareImageUrl,identifier 严格匹配 | | ② 兜底 | 容器页点击应用(JumpPath 直传 URL) | `_fetchShareImage` | 按 URL 前缀反查 `mp/apps` 列表,命中写入本地缓存 | | ③ 覆盖 | H5 postMessage 自定义 | `handleMessage` 'share' | H5 `bridge.share({imageUrl})` 传了图才覆盖,为空保留 box_app 预取值 | | ④ 同步 | 首页壳(应用宫格进入任意 H5) | `_loadShareApps` + `resolveShareCard` | 首页壳 onLoad 预热**全量应用表**(内存 + 1h 本地缓存),`onShareAppMessage(options)` 里用 `options.webViewUrl` 同步匹配出图(首次转发就有图);**每次分享都重解析并检测应用切换**,避免沿用上一个应用的图 | ### ① 太阳码扫码(`_resolveAppScene`) ``` 扫码 → scene=app=testapp → GET https://mp.yizhezhe.com/api/mp/apps?identifier=testapp&isPublished=1&pageSize=1 → items[0] = { url, shareImageUrl, ... } → 同一响应取 app.url(加载 H5)+ app.shareImageUrl(分享图) → 严格匹配 items.find(it => it.identifier === identifier),避免张冠李戴 ``` ### ② 点击应用(`_fetchShareImage`,在 `_finalizeUrl` 中异步触发) ``` 点应用 → JumpPath 传完整 H5 URL(如 https://mp.yizhezhe.com/app_03/) → 1. 先同步查:内存应用表(若已预热)→ 单应用缓存 web_view_share_img:{urlBase}(24h TTL) → 2. 未命中 → GET /api/mp/public/share-images(拉分享图反查表,**含未上架应用**,1h 本地缓存) → 3. URL 前缀匹配:urlBase === appBase || urlBase.startsWith(appBase + '/') → 4. 命中 → this._applyShareImage(shareImageUrl) + 写入单应用缓存 ``` 页面加载时执行,用户点转发时图已就绪。`_shareFetched` 标志保证页面内只做一次异步兜底。 ### ③ H5 postMessage 覆盖(`webviewHost.handleHostMessage`) ``` H5 wxAuth.reportLocation({ title, imageUrl }) / bridge.share({ title, imageUrl }) → postMessage → handleHostMessage 'location' / 'share' → if (data.title) { this.shareTitle = data.title; this.shareTitleSource = 'h5'; } // 有值才覆盖 → if (data.imageUrl) this.shareImageUrl = data.imageUrl // 有值才覆盖 → 为空则保留 box_app 预取的分享图片 ``` ### ④ 首页壳同步匹配(`pages/index/index.vue`,2026-09 补) > 症状:从兜爸心选首页的应用宫格进入的 H5(如数胎动),点右上角 ··· 转发**没有**后台配置的分享图。 原因:宫格用 `location.href` 换 H5,小程序页面栈仍是首页壳 `pages/index/index`;而反查逻辑此前 只写在容器页,首页壳的 `shareImageUrl` 只能靠 H5 postMessage,数胎动这类 H5 没有 `bridge.share` → 卡片图为空 → 微信回退默认截图。 ``` onLoad → _loadShareApps() // 预热反查表:内存 shareApps + 本地缓存 share_apps_index_v3(1h) 点 ··· → onShareAppMessage(options) → target = options.webViewUrl || this.shareHref || this.webUrl // 当前 web-view 真实地址 → this.resolveShareCard(target) // 见下:先做应用切换检测,再按当前地址反查图 + 标题 → imageUrl = this.shareImageUrl || '' ;title = this.shareTitle || info.title || '兜爸心选 · 孕育陪伴' ``` `_lookupShareImage` 是**同步**的:先内存应用表,再单应用缓存。因此首次转发就能出图, 不依赖 postMessage 的投递时机。后台改分享图后,最迟 1 小时(列表缓存 TTL)生效。 朋友圈图仍走 `_prepareTimelineImage`(`getImageInfo` 转本地路径)。 > ⚠️ **跨应用残留(2026-09 修)**:症状是「A 应用转发能出图,切到 B 应用转发还是 A 的图」。 > 根因是首页壳**不随 H5 换页重新 onLoad**(宫格用 `location.href`),而当时分享图取到后就一直 > 留在 `shareImageUrl`,并且解析被 `if (!this.shareImageUrl)` 短路掉了 —— 于是上一个应用的图钉住不换。 > > 修法(`resolveShareCard` / `syncShareCardTarget`,两个宿主页共用): > 1. `shareImageBase` 记录当前图属于哪个 H5 应用;分享前发现 `target` 换了应用 → **清空** > `shareImageUrl` / `shareTimelineImage` / `shareTitle` / `shareHref`; > 2. **每次都按当前 `target` 重新反查**,不再因为已有图就跳过; > 3. 当前图若来自本应用 H5 的 postMessage(`shareImageSource === 'h5'`)才保留优先, > 否则 box_app 配置图覆盖; > 4. 当前应用没有配置图 → 宁可留空回退微信默认截图,也不沿用上一个应用的图。 > > 局限与正解:`onShareTimeline` 拿不到 `webViewUrl`,宿主无法自己判断当前应用。 > 正解是 **H5 主动上报当前位置**:换页/路由切换时发 > `wx.miniProgram.postMessage({ data: { action: 'location', href: location.href } })`, > 宿主 `webviewHost.handleHostMessage` 收到后更新 `shareHref` 并调 `resolveShareCard(href)`, > 朋友圈分享即拿到正确的落地页与分享图(共享库内置 `wxAuth.reportLocation()` / `wxAuth.watchLocation()`)。 > H5 侧完整接入说明见 `box_app/docs/h5-auth-standard.md` 第四节。 > 若 H5 未接入该上报,朋友圈分享只能以最后一次好友转发解析出的图为准。 ## 四、分享路径(深链)—— 2026-09 修复 > 症状:在 web-view 的 H5 里点「分享给微信好友」,接收者点开卡片却落到**小程序首页**, > 而不是发送者当时所在的那个 H5 应用/页面。 ### 根因 1. web-view 里的 H5 **无法用代码拉起分享面板**,卡片只能由承载 `` 的**宿主小程序页**产出。 2. H5 应用是从 mama-box(首页 web-view)的应用宫格用 `location.href` 换页进入的 (`mama-box/js/app.js` → `jumpApp`),小程序页面栈**仍是首页** `pages/index/index`。 3. `app_03` 的 `bridge.share()` 曾在 postMessage 后调 `wx.miniProgram.navigateBack({delta:1})` 试图触发投递 —— 这会把 web-view 页弹掉(栈里只有 tabBar 根页时还会失败), 转发随即被**别的页面**接管;首页回调又写死 `path: '/pages/index/index'`, 于是接收者永远进首页。 4. `onShareAppMessage` 忽略了微信回调参数 `options.webViewUrl`(当前 web-view 真实地址, 含 H5 的 SPA 路由),H5 的 postMessage `href` 又因投递延迟不可靠 → 深链丢失。 ### 修复 | 端 | 改动 | |----|------| | 小程序首页 `pages/index/index.vue` | `onShareAppMessage(options)` 用 `options.webViewUrl`(兜底 `shareHref` / `webUrl`)拼 `path: '/pages/annex/web_view/index?url=...'`;`onMessage` 支持 `action:'share'/'setTitle'`;补 `onShareTimeline` + `uni.showShareMenu`;`onLoad` 支持 `?url=` 落地(朋友圈转发经此转交容器) | | 小程序容器 `pages/annex/web_view/index.vue` | `onShareAppMessage(options)` 同样优先 `options.webViewUrl`;`onShareTimeline` 兜底 `shareHref \|\| url` | | 两页 `stripAuthParams` | 按 key 过滤(不再用会吃掉 `#hash` 的正则),剥 query 的 `openid/unionid/t/_v` 与 hash 的 `openid/unionid/phone/nickname/avatar` | | H5 `app_03/frontend/src/app/services/bridge.ts` | **删除 `navigateBack`**;`share()` 只发 postMessage 并返回是否在小程序内,页内提示用户点右上角「···」 | > **接收者身份**:分享链接一律剥掉发送者身份,接收者经 `pages/annex/web_view` 容器 > 按自己的 openid 重新注入(容器 `_applyIdentity`,query 传参)。 ## 五、好友转发 vs 朋友圈(微信平台差异) | 场景 | 图片要求 | 实现 | |------|---------|------| | **转发给好友** `onShareAppMessage` | 支持 https 网络图 | `imageUrl: this.shareImageUrl`(box_app 网络 URL 直接用) | | **分享到朋友圈** `onShareTimeline` | 仅本地路径/代码包路径,网络图静默忽略 | `_prepareTimelineImage` 用 `uni.getImageInfo` 转本地临时路径 `shareTimelineImage` | > ⚠️ `onShareTimeline` **没有回调参数**,拿不到 `webViewUrl`,只能兜底 `shareHref || url`; > 它也不能自定义 `path`(只重开当前页 + query),所以首页用 `?url=` 把目标转交 web_view 容器。 ## 六、关键前置条件(易踩坑) 1. **box_app 已配置分享图**:管理后台 → 应用 → 上传"分享图片" 2. **图片必须公网 https 可访问**(微信服务器要抓取) 3. **mp.yizhezhe.com 必须在微信后台合法域名白名单**: - `request` 合法域名(拉取 mp/apps 接口;首页壳预热应用表也用它) - `downloadFile` 合法域名(朋友圈转本地图 getImageInfo) - ⚠️ 未配置时:好友图可能正常(网络图),但**朋友圈图转换失败回退默认截图**,`_fetchShareImage` fail 分支会提示 4. **分享图有缓存,改图不是立刻生效**:反查表缓存 `share_apps_index_v2` 1h,单应用分享图缓存 `web_view_share_img:*` 24h。 真机验证新图时可先清缓存(开发者工具 → 清除数据缓存 / 重新编译)或等 TTL 过期。 > **反查表与上架状态解耦(2026-09-22)**:旧数据源 `/api/mp/apps?isPublished=1` 只回上架应用, > 导致 mama-box(未建上架记录)和 laixin(下架、经 mama-box 底 tab/来信卡的硬编码兜底 URL 进入) > 拿不到自定义转发图。现改用专用接口 `GET /api/mp/public/share-images`(box_app `mp_router.py`): > 返回**所有配置了分享图**的应用(含下架),只含 `url + shareImageUrl` 两字段,按 url 长度倒序 > (长前缀优先命中,防撞车)。**给「不上宫格但要可进入」的应用配图 = 后台建/编辑记录、传分享图、 > 保持下架**;宫格展示仍由 `/api/mp/apps?isPublished=1` 决定,互不影响。缓存 key 随之 bump 为 > `share_apps_index_v2`(旧缓存里只有上架应用,不 bump 会残留 1h)。 ## 七、相关代码位置 | 位置 | 文件 | |------|------| | box_app 模型字段 | `box_app/backend/app/models/app.py` | | box_app 服务透传 | `box_app/backend/app/services/app_serv.py` | | box_app 表单上传 | `box_app/frontend/src/pages/ProductManagement.jsx` | | **分享图公共逻辑(两个宿主页共用)** | `crmeb/mixins/shareCard.js` → `resolveShareCard` / `syncShareCardTarget` / `_loadShareApps` / `_lookupShareImage` / `_fetchShareImage` / `_applyShareImage` / `_prepareTimelineImage` | | **宿主页其它公共能力**(消息分发 / 授权面板 / 授权回传 / openid+unionid) | `crmeb/mixins/webviewHost.js` → `handleHostMessage` / `consumeAuthResult` / `submitHostAuth` | | 小程序扫码取图 | `crmeb/pages/annex/web_view/index.vue` → `_resolveAppScene` | | 小程序容器页(onLoad 兜底反查 / 转发回调) | 同上 → `_fetchShareImage`(mixin)/ `onShareAppMessage` / `onShareTimeline` / `@message="handleHostMessage"` | | 小程序首页壳(应用宫格进入的 H5) | `crmeb/pages/index/index.vue` → `onLoad` 预热应用表 + `onShareAppMessage` 同步匹配(带 `spid`)/ `onShareTimeline` / `@message="handleHostMessage"` | | 首页分享兜底(mama-box 等无 App 记录时) | 首页壳 `getShare()` → CRMEB 后台「分享设置」的 `title` / `img` | | 分享链接净化 | 两页的 `stripAuthParams` / `dropKeyValuePairs` | | H5 传图 / 触发分享 | `app_03/frontend/src/app/services/bridge.ts` → `share({title,href,imageUrl})` | > 授权/消息这一侧的完整链路(两条:独立授权页 与 宿主页内嵌面板)见 > `box_app/docs/h5-miniprogram-auth.md` 第 6 节。