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

227 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 分享图片获取逻辑(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 节。