- 含首页壳/容器页/授权页/分享图/首页配置等此前未入库的在途改动 - 新增未跟踪的 pages/index/mall.vue(第 5 个 tabBar 页,pages.json 已引用) - 排除本地备份文件(*.bak* / *.origin-bak) Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
16 KiB
Markdown
227 lines
16 KiB
Markdown
# 分享图片获取逻辑(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 节。
|