Files
crmeb-mp/docs/分享图片获取逻辑.md
Karlcarlo b45a190a59 chore: 入库生产基线(2026-10-26 工作树快照)
- 含首页壳/容器页/授权页/分享图/首页配置等此前未入库的在途改动
- 新增未跟踪的 pages/index/mall.vue(第 5 个 tabBar 页,pages.json 已引用)
- 排除本地备份文件(*.bak* / *.origin-bak)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 17:07:49 +08:00

16 KiB
Raw Blame History

分享图片获取逻辑(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 无法用代码拉起分享面板,卡片只能由承载 <web-view> 的宿主小程序页产出。
  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 节。