headsup(mini): 小程序 onShareAppMessage/Timeline 需链式继承 adminId 才能正确归属定制师 (配合 PR #2977)
这个提交包含在:
父节点
cb7898dc2d
当前提交
208f67b53a
@ -0,0 +1,230 @@
|
||||
# heads-up(小程序分享): onShareAppMessage / onShareTimeline 需链式继承 adminId
|
||||
|
||||
> **类型**: heads-up + 前端待办 (后端无需改动)
|
||||
> **关联 PR/Issue**: 后端 PR #2977 (Closes #2976) 配套
|
||||
> **日期**: 2026-05-24
|
||||
> **影响范围**: 小程序内原生分享渠道(分享给朋友 / 分享到朋友圈)的客人下单定制师归属
|
||||
> **接收方**: mmg (前端 hl-mini 仓库)
|
||||
> **前端**: **需要改造** (本仓后端已就绪,等前端补齐链路)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 背景
|
||||
|
||||
wx 实测发现:**admin (id=1001 SUPER_ADMIN, 非定制师) 在管理端「分享到小程序」生成的 URL Scheme,客人扫码下单后订单的「定制师」被错绑成 admin 自己,不是中间转发的真定制师 wx**。
|
||||
|
||||
根因: URL Scheme 的 t 参数在微信 wxa/generatescheme 时**加密锁死**,中间转发不能改写,query 里的 `adminId=` 永远是"生成人"。
|
||||
|
||||
后端已修 (PR #2977 已合 dev,测试服真测拒绝路径通过):
|
||||
- `POST /admin/wxapp/share/url-link` 和 `/url-scheme` 加 guard
|
||||
- 仅 `status=ACTIVE` + `roleKeys 含 CUSTOMIZER` + `wechatUserid 非空` 的 admin 可生成
|
||||
- 错误码 `200312 SHARE_REQUIRES_BOUND_CUSTOMIZER`, 文案: **"仅持定制师角色且已绑定企业微信的成员可生成分享链接"**
|
||||
|
||||
但这只覆盖了**链路 A** (admin 端管理后台「分享 URL Scheme」按钮)。**链路 B / C 还没覆盖**,需要 mmg 前端配合。
|
||||
|
||||
---
|
||||
|
||||
## 📊 三个分享渠道现状
|
||||
|
||||
| # | 渠道 | 入口 | 当前前端实现 | 客人下单归属 |
|
||||
|---|---|---|---|---|
|
||||
| **A** | admin/定制师 在管理端点「分享 URL Scheme」 | hl-ui MiniappShareModal.vue | URL query 含 `adminId=<生成人id>`, 小程序 onLoad 接到存 storage | ✅ PR #2977 后只有定制师能生成, customizer 正确 |
|
||||
| **B** | 定制师在小程序产品详情页点右上角「分享给朋友」 | `onShareAppMessage()` | **只传 `shareOpenId`, 没传 adminId** | ❌ 客人下单 customizerId 空 → 兜底随机 |
|
||||
| **C** | 定制师在小程序产品详情页点「分享到朋友圈」 | `onShareTimeline()` | **只传 `shareOpenId`, 没传 adminId** | ❌ 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 🔬 代码定位
|
||||
|
||||
仓库 `mmg/hl-mini` main 分支, 文件 `packages/product/core/detail/detail.js`:
|
||||
|
||||
### L191-199 onLoad (接收链路 A 的 adminId, 这部分已经对)
|
||||
|
||||
```js
|
||||
onLoad(options) {
|
||||
const productId = options.id;
|
||||
this._shareOpenId = options.shareOpenId || '';
|
||||
this._adminId = options.adminId || '';
|
||||
// 链路 A 的 adminId 已写入 storage 桥接订单详情页
|
||||
if (this._adminId) {
|
||||
storage.setShareCustomizerId(this._adminId);
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### L1464-1472 onShareAppMessage (BUG: 没拼 adminId)
|
||||
|
||||
```js
|
||||
onShareAppMessage() {
|
||||
const { product, productId, shareCardImage, heroBgImage } = this.data;
|
||||
const openId = storage.get('openId') || '';
|
||||
return {
|
||||
title: product.title || '呼籁旅行 ...',
|
||||
path: `/packages/product/core/detail/detail?id=${productId}&shareOpenId=${openId}`, // ← 缺 adminId
|
||||
imageUrl: shareCardImage || heroBgImage || ''
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### L1474-1482 onShareTimeline (BUG: 同样没拼)
|
||||
|
||||
```js
|
||||
onShareTimeline() {
|
||||
...
|
||||
return {
|
||||
title: ...,
|
||||
query: `id=${productId}&shareOpenId=${openId}`, // ← 缺 adminId
|
||||
imageUrl: ...
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 前端推荐改造 (链式继承 adminId)
|
||||
|
||||
**最简方案 — 不需要调任何后端接口, 沿用 storage 桥接的 adminId**:
|
||||
|
||||
```js
|
||||
onShareAppMessage() {
|
||||
const { product, productId, shareCardImage, heroBgImage } = this.data;
|
||||
const openId = storage.get('openId') || '';
|
||||
// 链式继承: 当前页面是从分享链接进入的话, _adminId 不为空; 否则从 storage 兜底
|
||||
const adminId = this._adminId || storage.get('shareCustomizerId') || '';
|
||||
// path 拼上 adminId, 这样客人扫码进店 onLoad 时 options.adminId 仍是原始定制师
|
||||
const adminPart = adminId ? `&adminId=${adminId}` : '';
|
||||
return {
|
||||
title: product.title || '呼籁旅行 ...',
|
||||
path: `/packages/product/core/detail/detail?id=${productId}&shareOpenId=${openId}${adminPart}`,
|
||||
imageUrl: shareCardImage || heroBgImage || ''
|
||||
};
|
||||
}
|
||||
|
||||
onShareTimeline() {
|
||||
const { product, productId, shareCardImage, heroBgImage } = this.data;
|
||||
const openId = storage.get('openId') || '';
|
||||
const adminId = this._adminId || storage.get('shareCustomizerId') || '';
|
||||
const adminPart = adminId ? `&adminId=${adminId}` : '';
|
||||
return {
|
||||
title: product.title || '呼籁旅行 ...',
|
||||
query: `id=${productId}&shareOpenId=${openId}${adminPart}`,
|
||||
imageUrl: shareCardImage || heroBgImage || ''
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 链式继承的业务效果
|
||||
|
||||
1. wx (定制师) 通过 admin 端生成首个链接 `?adminId=wx_id` 发给客人 A
|
||||
2. A 进店 onLoad → `_adminId = wx_id` → 存 storage
|
||||
3. **A 在小程序里再点「分享给朋友」给客人 B** → onShareAppMessage 沿用 wx_id → path 含 `adminId=wx_id`
|
||||
4. B 扫码进店下单 → customizerId=wx_id → customizer 归 wx ✅
|
||||
5. 第 N 次转发都归原始定制师, **营销链路追溯完整**
|
||||
|
||||
---
|
||||
|
||||
## 🔧 升级方案 (可选, 若定制师自己直接打开小程序产品页要分享)
|
||||
|
||||
如果定制师 wx **没有经过 admin 端链接, 自己直接进入小程序** 想要分享产品 → `_adminId` 为空, storage 也无 → 链式继承拿不到 adminId → 客人下单兜底随机。
|
||||
|
||||
解决方法: 页面 onLoad 调后端 `GET /mp/customizer/me` (PR #2913 已上线):
|
||||
|
||||
```js
|
||||
onLoad(options) {
|
||||
this._shareOpenId = options.shareOpenId || '';
|
||||
this._adminId = options.adminId || '';
|
||||
if (this._adminId) {
|
||||
storage.setShareCustomizerId(this._adminId);
|
||||
}
|
||||
// 升级: 如果当前用户是定制师本人, 把自己 adminId 也写入 storage 用于分享
|
||||
if (!this._adminId) {
|
||||
customizerApi.me().then(res => {
|
||||
if (res?.data?.isCustomizer && res?.data?.adminId) {
|
||||
this._adminId = res.data.adminId;
|
||||
storage.setShareCustomizerId(this._adminId);
|
||||
}
|
||||
});
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`/mp/customizer/me` 响应结构:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"isCustomizer": true,
|
||||
"adminId": "1760000000000099",
|
||||
"customizerName": "王骁",
|
||||
"avatar": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 改造范围 (其他可分享页面)
|
||||
|
||||
类似的分享 hook 在以下页面也需要同步加 adminId (基于 grep 结果):
|
||||
|
||||
- ✅ `packages/product/core/detail/detail.js` (重点, 上述代码)
|
||||
- `packages/product/mengma/detail/detail.js` (小蒙马产品详情)
|
||||
- `packages/scenic/detail/detail.js` (景区详情)
|
||||
- `packages/hotel/detail/detail.js` (酒店详情)
|
||||
- `packages/restaurant/detail/detail.js` (餐厅详情)
|
||||
- `packages/guide/detail/detail.js` (向导详情)
|
||||
- `packages/user/travel-guide/travel-guide.js` (旅行攻略)
|
||||
- `packages/product/compare/compare.js` (对比页)
|
||||
- `pages/index/index.js` (首页)
|
||||
- `pages/trip/trip.js` (行程页)
|
||||
|
||||
建议**至少先改 4 个核心入口**: 核心产品详情 / 小蒙马产品详情 / 主题(产品线)详情 / 首页。其他后续补。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验证方式 (前端改完后)
|
||||
|
||||
1. wx (定制师) 在管理端「分享 URL Scheme」生成 → 给客人 A
|
||||
2. A 进店 → 在小程序内再点产品页右上角「分享给朋友」 → 给客人 B
|
||||
3. B 进店 onLoad → `console.log(this._adminId)` 应等于 wx 的 admin_id
|
||||
4. B 下单 → 后端订单的 `customizer_id` 字段应等于 wx 的 admin_id
|
||||
5. 朋友圈分享: A 点「分享到朋友圈」, 别人通过朋友圈进店, 同样验证 `_adminId` 链式继承
|
||||
|
||||
---
|
||||
|
||||
## 🔗 后端接口清单 (mmg 不需要改, 已就绪)
|
||||
|
||||
| 接口 | 路径 | 用途 |
|
||||
|---|---|---|
|
||||
| 获取当前定制师身份 | `GET /mp/customizer/me` | 当前 C 端用户是定制师? 返 adminId |
|
||||
| 客人下单 | `POST /mp/order/create` | 接受 `customizerId` (字符串透传雪花 ID) + `sharerOpenid` (溯源用) |
|
||||
| Admin 生成 URL Link / Scheme | `POST /admin/wxapp/share/url-link` `/url-scheme` | PR #2977 后限定持 CUSTOMIZER + 绑企微的 admin |
|
||||
|
||||
`customizerId` 在订单创建时由后端 `CustomizerValidator.resolve()` 校验:
|
||||
- `status = ACTIVE`
|
||||
- `roleKeys` 含 `CUSTOMIZER`
|
||||
- 通过 → 订单 customizer = 该 adminId
|
||||
- 不通过 → 订单 customizer 走 `assignRandomCustomizer` 兜底随机
|
||||
|
||||
---
|
||||
|
||||
## 📅 时间线建议
|
||||
|
||||
| # | 任务 | 负责人 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1 | 后端 admin 端 guard PR #2977 | wx | ✅ 已合 dev / 测试服真测拒绝路径通过 |
|
||||
| 2 | 前端 onShareAppMessage / Timeline 链式继承 adminId (核心产品页) | **mmg** | ⏳ |
|
||||
| 3 | 前端其他分享入口同步改 (景区/酒店/...等) | mmg | ⏳ 可分多 PR |
|
||||
| 4 | 前端调 `/mp/customizer/me` 兜底定制师本人首次进入 (可选升级) | mmg | ⏳ 可选 |
|
||||
| 5 | 后端 PR #2977 合 main → 上正式 | wx | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## 联系人
|
||||
|
||||
后端: wx (呼籁旅行)
|
||||
前端: mmg
|
||||
|
||||
如有疑问看后端 PR https://git.1814.love:8443/wx/HL/pulls/2977 + 工单 https://git.1814.love:8443/wx/HL/issues/2976
|
||||
正在加载...
x
在新工单中引用
屏蔽一个用户