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

View File

@@ -13,11 +13,15 @@
→ 存入 App.shareImageUrl = https://mp.yizhezhe.com/upload/picture/xxx.png
[box_app API]
GET /api/mp/apps?identifier=xxx&isPublished=1
→ 返回 { identifier, url, shareImageUrl, ... }
GET /api/mp/apps?identifier=xxx&isPublished=1 (单个应用,太阳码场景)
GET /api/mp/public/share-images (分享图反查表:所有配了图的应用,
**不过滤上架状态**,2026-09-22 起)
→ 返回 { url, shareImageUrl } 精简结构
[小程序 web_view(pages/annex/web_view/index.vue)]
① 扫码/点击应用 → 获取 shareImageUrl → 本地缓存
[小程序宿主页(两个,见第三节)]
pages/annex/web_view/index.vue 容器页:扫码 / JumpPath / 分享落地
pages/index/index.vue 首页壳:兜爸心选应用宫格 location.href 进入的 H5
① 获取 shareImageUrl(网络图)→ 内存 + 本地缓存(mixins/shareCard.js)
② 用户点 ··· 转发给好友 → 卡片图 = shareImageUrl(网络图,微信支持)
③ 用户点 ··· 分享到朋友圈 → getImageInfo 转本地路径 → 卡片图 = 本地路径
```
@@ -34,15 +38,23 @@
- 支持上传 `/api/upload`、编辑回填、单图(maxCount=1)
- extra 提示:建议 5:4 比例,公网可访问
## 三、小程序端获取逻辑(web_view/index.vue)
## 三、小程序端获取逻辑(两个宿主页共用 `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` 列表,命中写入本地缓存 |
| ② 兜底 | 容器页点击应用(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`)
@@ -58,13 +70,13 @@
```
点应用 → JumpPath 传完整 H5 URL(如 https://mp.yizhezhe.com/app_03/)
→ 1. 查本地缓存 web_view_share_img:{originPath} → 命中直接复用
→ 2. 未命中 → GET /api/mp/apps?isPublished=1&pageSize=100(拉全量已上架应用)
→ 3. URL 前缀匹配:originPath === appBase || originPath.startsWith(appBase + '/')
→ 4. 命中 → this._applyShareImage(shareImageUrl) + 写入缓存
→ 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` 标志保证页面内只请求一次。
页面加载时执行,用户点转发时图已就绪。`_shareFetched` 标志保证页面内只做一次异步兜底。
### ③ H5 postMessage 覆盖(`handleMessage`)
@@ -74,30 +86,119 @@ H5 bridge.share({ title, imageUrl }) → postMessage → handleMessage 'share'
→ 为空则保留 box_app 预取的分享图片
```
## 四、好友转发 vs 朋友圈(微信平台差异)
### ④ 首页壳同步匹配(`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(1h)
点 ··· → onShareAppMessage(options)
→ target = options.webViewUrl || this.shareHref || this.webUrl // 当前 web-view 真实地址
→ this.resolveShareCard(target) // 见下:先做应用切换检测,再按当前地址反查
→ imageUrl = this.shareImageUrl || ''
```
`_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 接口)
- `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` |
| 小程序兜底反查 | 同上 → `_fetchShareImage` / `_applyShareImage` / `_prepareTimelineImage` |
| 小程序分享回调 | 同上 → `onShareAppMessage` / `onShareTimeline` / `handleMessage` |
| H5 传图 | `app_03/frontend/src/app/services/bridge.ts` → `share({imageUrl})` |
| 小程序容器页(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 节。