# 产品分享链接带 adminId → 下单/支付锁定为分享人定制师

**类型**: 后端 + 前端协作改动
**前端处理者**: mmg
**日期**: 2026-05-07
**关联**: 工单 #1814 / PR #1825 (已合 dev)
**影响页面**:
- 管理后台「产品列表 → 分享到小程序」弹窗(URL Scheme/URL Link 拼接)
- 小程序「产品详情页」(`packages/product/core/detail/detail`) 接收 adminId 参数
- 小程序「下单」接口 (`POST /mp/order/create`) 新增 customizerId 选填字段
- 小程序「支付预下单」接口 (`POST /mp/payment/prepay`) 新增 customizerId 选填字段

---

## 这是什么

定制师傅分享产品给客户时, 客户点链接进小程序下单, 订单的"定制师"锁定为分享人 adminId, 不再走系统随机分配。

设计参考: 截图里管理后台「分享到小程序」弹窗已能生成 URL Scheme / URL Link, 路径 `packages/product/core/detail/detail?id=<productId>&adminId=<currentAdminId>`。

---

## 仅产品分享

✅ 仅产品详情页支持 (URL `packages/product/core/detail/detail`)
❌ 不支持产品线 / 活动 / 订单 / 其他页面分享(产品线/活动等不带 adminId, 不受影响)

定制师傅在小程序内自己分享 (微信原生 onShareAppMessage) 暂不支持 — 需要先做 mp 用户和 admin 账号关联, 已开 #1815 单独跟进。

---

## 前端必做的 4 件事

### 1. 管理后台 PC「分享到小程序」弹窗 — URL 拼接 adminId

文件: `hl-ui/.../components/ProductShareDialog.vue` (或类似名)

URL Scheme / URL Link 生成时, **小程序页面路径**字段值必须拼接 `&adminId=<当前登录管理员 adminId>`:

```js
const path = `packages/product/core/detail/detail?id=${productId}&adminId=${currentAdminId}`
```

`currentAdminId` 从当前登录态拿(Vuex/Pinia 里的 `userStore.adminId`)。**类型必须是 number, 不要是 string** — 后端 `customizerId: Long` 强类型反序列化, String 会 400。

### 2. 小程序产品详情页 — 解析 URL adminId 存 sessionStorage

`packages/product/core/detail/detail.vue` onLoad / onShow:

```js
onLoad(options) {
  if (options.adminId) {
    const adminId = Number(options.adminId)
    if (Number.isFinite(adminId) && adminId > 0) {
      uni.setStorageSync('share_customizer_id', adminId)
    }
  }
}
```

**多次进入不同分享链接**: 每次都覆盖, 不累加(用户先进 A 师傅链接、再进 B 师傅链接, 最后进的 B 链接覆盖之前的 A)。

### 3. 创建订单接口 — 透传 customizerId

`POST /mp/order/create` 请求 body 加字段:

```js
const customizerId = uni.getStorageSync('share_customizer_id') || null
const body = {
  // ... 原有字段
  customizerId,  // null 或 number, 不要传 string
}
```

字段约定:

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `customizerId` | `number` (Long) | ❌ 选填 | 分享人 adminId; 不传/null/0 走系统随机分配; 后端 `@Min(1)` 校验 |

### 4. 支付预下单接口 — 同样透传 customizerId

`POST /mp/payment/prepay` 请求 body 加字段:

```js
const customizerId = uni.getStorageSync('share_customizer_id') || null
const body = {
  orderId: '...',
  customizerId,  // null 或 number
}
```

**为什么支付路径也要传**? "以用户支付的链接为准" — 用户先进 A 师傅链接创建订单(订单 customizerId=A), 创建后退出去, 又进 B 师傅链接来这个订单页支付, 此时支付透传 customizerId=B → 后端漂移回写订单 customizerId=B。如果支付时不传 customizerId, 订单保持创建时的 customizerId 不变。

---

## 后端兜底语义 (不要在前端做这些校验)

后端 CustomizerValidator 严格校验 4 类失败场景, 失败都**走系统随机分配**(不报错, 兼容老前端):

| 失败原因 | 触发条件 | 前端表现 |
|---------|---------|---------|
| ADMIN_NOT_FOUND | adminId 不存在/已删除 | 兜底随机, 静默 |
| INACTIVE_STATUS | admin status ≠ ACTIVE (禁用/锁定) | 兜底随机, 静默 |
| WRONG_ROLE | admin roleKey ≠ CUSTOMIZER (普通管理员/客服/经理) | 兜底随机, 静默 |
| FEIGN_ERROR | user-service Feign 失败 | 兜底随机, 静默 |
| INVALID_INPUT | customizerId == null / ≤ 0 | 兜底随机, 静默 |

后端在所有失败路径**只打 WARN 日志不返错**, 前端不需要展示"分享师傅不可用"这类提示, 用户体验完全无感。

---

## 验证方式 (前端联调时)

1. 用一个**有效 CUSTOMIZER + ACTIVE** 的 adminId 拼 URL 分享 → 客户下单后看「我的订单」是不是显示这个师傅
2. 创建订单后**不立刻支付**, 退出小程序, 再用另一个**有效 CUSTOMIZER** 师傅的分享链接进入这个订单 → 支付前透传新 customizerId → 订单详情应该显示新师傅
3. 不分享直接下单 (URL 无 adminId 参数) → 系统随机分配, 行为同改造前

---

## 兼容性

- ✅ **零破坏性平滑发布**: 老前端不传 customizerId → 后端 null fallback → 兜底随机 = 改造前行为, 后端先合不影响线上
- ✅ 老订单 customizer_id 不动, 仅新建/支付路径生效
- ❌ 不影响 admin 端创建订单流程(后台代下单仍是 admin 自己当 customizer)

---

## 字段约束硬规则

- 类型必须 `number` (Long) — 不要传 `string`
- URL `?adminId=` 后端拿到是 `string`, 必须前端 `Number(adminId)` 转换后才放进请求 body
- 不传 / 传 `null` / 传 `0` / 传负数 → 全部走兜底随机
- 不需要前端做任何校验 (validity / role 等), 后端全权负责

---

## 不在本期范围 (后续工单跟进)

- ❌ 定制师傅在小程序内分享 (微信原生 onShareAppMessage 拼 URL): 需要先做 mp 用户与 admin 账号绑定 → #1815
- ❌ 分享统计 / 分销提成: 现有 `sharer_openid` 字段独立维护, 不与 customizer 强绑定

---

## 责任人

@mmg 接入前端: 4 步 (PC 弹窗 URL / 小程序 onLoad 解析 / 创建订单透传 / 支付透传)
