frontend notice: admin 产品分享下拉换接口为 /admin/user/customizers (PR #1919 后端已上线)
ShareModal.vue 当前调 getUserPage({status:1}) 被 admin_user.status 英文常量白名单
拦截直接返空,导致目标用户下拉永远空、分享功能不可用。
PR #1919 已新增专用接口 /admin/user/customizers (无 status / 企微过滤),
前端切到该接口即可。详见 changelog 内契约 + 验收点。
这个提交包含在:
父节点
3973320606
当前提交
49d18a8f9f
@ -0,0 +1,233 @@
|
|||||||
|
# 前端 BUG 通知 — admin 产品分享「目标用户」下拉换接口为 `/admin/user/customizers`
|
||||||
|
|
||||||
|
**日期**: 2026-05-09
|
||||||
|
**类型**: 前端 BUG (admin 后台 hl-ui)
|
||||||
|
**模块**: 产品管理 → 产品列表 → **分享产品**(ShareModal,**不是** MiniappShareModal)
|
||||||
|
**通知**: @mmg
|
||||||
|
**后端是否需改动**: 否(PR #1919 已上线专用接口,前端只需切接口)
|
||||||
|
**严重性**: 高 — 「目标用户」下拉永远空,**整个分享功能不可用**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 关键变化
|
||||||
|
|
||||||
|
PR #1919 (Closes #1918) 后端为产品分享场景**新增了独立接口** `GET /admin/user/customizers`(返回所有持 `CUSTOMIZER` 角色的 admin,**无 status 过滤、无企微绑定过滤**)。
|
||||||
|
|
||||||
|
但当前前端 `ShareModal.vue` 没接这个新接口,line 87-102 仍在调 `getUserPage({status: 1})`(老的 admin 分页接口)。后端 `AdminUserController.listAdmins` line 167 看到 `status="1"` 不在英文常量白名单(`ACTIVE/LOCKED/DISABLED/DELETED`)内,**直接返回空 PageResult**(Issue #1829 防 jsqlparser 崩的硬白名单),所以下拉永远是空。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 现象
|
||||||
|
|
||||||
|
入口:admin 后台 → 产品列表 → 列表行「**分享**」按钮 (`hl-ui/src/views/product/list/components/ShareModal.vue`)
|
||||||
|
|
||||||
|
弹窗"分享产品"对话框 →「目标用户」下拉框点开 → **空列表 + 提示"请选择目标用户"** → 无法选择任何人 → 无法点确定 → 整个分享流程跑不通。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 根因(代码反推)
|
||||||
|
|
||||||
|
### 前端调错接口
|
||||||
|
|
||||||
|
`hl-ui/src/views/product/list/components/ShareModal.vue:87-102`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function loadUsers() {
|
||||||
|
userLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getUserPage({ page: 1, pageSize: 100, status: 1 }) // ❌ 接口选错 + 参数选错
|
||||||
|
userOptions.value = (res?.records || res?.list || [])
|
||||||
|
.filter((u) => String(u.id) !== String(currentUserId.value))
|
||||||
|
.map((u) => ({
|
||||||
|
label: `${u.realName || u.username}${u.mobile ? ` (${u.mobile})` : ''}`,
|
||||||
|
value: u.id,
|
||||||
|
}))
|
||||||
|
} catch (error) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`getUserPage` (`hl-ui/src/api/user.js:128-130`) 实际调 `GET /admin/user`,即 admin 用户分页接口。
|
||||||
|
|
||||||
|
### 后端返空逻辑
|
||||||
|
|
||||||
|
`hl-user-service/src/main/java/com/hulalv/user/controller/AdminUserController.java:167-169`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// status 字典白名单校验:非字典值(如 "1"/"0"/"true"/"ABCXYZ")直接返空页,
|
||||||
|
// 不进 mapper 以规避 MP PaginationInnerInterceptor 的 jsqlparser fallback BUG (Issue #1829)
|
||||||
|
if (status != null && !status.isEmpty() && !VALID_ADMIN_STATUS.contains(status)) {
|
||||||
|
return Result.success(PageResult.of(Collections.emptyList(), 0L, page, pageSize));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`VALID_ADMIN_STATUS` 是英文常量集合 `{ACTIVE, LOCKED, DISABLED, DELETED}`,前端传 `status: 1`(数字 1) → 不在白名单 → 接口直接返 `{records: [], total: 0}` → 下拉永远空。
|
||||||
|
|
||||||
|
### 后端已有专用接口(PR #1919)
|
||||||
|
|
||||||
|
PR #1919 (commit `a18329b7`) 已专门为分享产品下拉新增接口,前端**未接**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 期望前端做的事
|
||||||
|
|
||||||
|
修改文件: `hl-ui/src/views/product/list/components/ShareModal.vue`
|
||||||
|
|
||||||
|
**优先级 1**: 把 `loadUsers` 里的 `getUserPage(...)` 改为调新接口 `GET /admin/user/customizers`(无入参)。
|
||||||
|
|
||||||
|
**优先级 2**: 字段 mapping 改为新接口返回的轻量 VO 字段(见下方契约)。
|
||||||
|
|
||||||
|
**优先级 3**: `currentUserId` 排除自己的逻辑改用 `adminId` 比较(新接口字段是 `adminId`,不是 `id`)。
|
||||||
|
|
||||||
|
### 建议改后伪代码
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { listShareCustomizers } from '@/api/user' // 新增导出函数
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
userLoading.value = true
|
||||||
|
try {
|
||||||
|
const list = await listShareCustomizers() // GET /admin/user/customizers
|
||||||
|
userOptions.value = (list || [])
|
||||||
|
.filter((u) => String(u.adminId) !== String(currentUserId.value))
|
||||||
|
.map((u) => ({
|
||||||
|
label: u.username, // 轻量 VO 没 realName / mobile,只用 username
|
||||||
|
value: u.adminId, // 注意是 adminId,不是 id
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载定制师列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
userLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`hl-ui/src/api/user.js` 建议在 `getDesignerList` 旁边加:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* 分享产品场景的目标定制师下拉(不分页,无 status/企微过滤)
|
||||||
|
* @returns {Promise<Array>} Array<CustomizerSimpleVO>
|
||||||
|
*/
|
||||||
|
export function listShareCustomizers() {
|
||||||
|
return http.get('/user/customizers')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后端接口契约
|
||||||
|
|
||||||
|
### `GET /admin/user/customizers`
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
- `Authorization: Bearer <admin token>` (网关校验)
|
||||||
|
|
||||||
|
**Query / Body**: 无任何参数。
|
||||||
|
|
||||||
|
**Response** `Result<List<CustomizerSimpleVO>>`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"adminId": 1001,
|
||||||
|
"username": "wx-01",
|
||||||
|
"avatar": "https://cdn.hulalv.com/avatar/xxx.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"adminId": 1002,
|
||||||
|
"username": "test_admin",
|
||||||
|
"avatar": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必返 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `data[].adminId` | Long | ✅ | admin 主键,前端 v-model 绑这个 |
|
||||||
|
| `data[].username` | String | ✅ | admin 登录名,作为下拉显示文案 |
|
||||||
|
| `data[].avatar` | String | ❌ | admin_user 自身头像列,可能为 null |
|
||||||
|
|
||||||
|
**重要**:
|
||||||
|
- 返回**所有持 `CUSTOMIZER` 角色的 admin**(`status=ACTIVE/LOCKED/DISABLED` 全部包含,只排除 `status=DELETED`)。
|
||||||
|
- **不要求企微绑定**,与 `/admin/user/designers` 的区别见 Swagger notes。
|
||||||
|
- 按 `adminId` 升序,稳定排序,前端可缓存。
|
||||||
|
- **不**返回 `realName` / `mobile` / `phone` / 富信息(分享场景不需要,后端故意精简)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么不能继续用 `/admin/user`(老分页接口)
|
||||||
|
|
||||||
|
1. **status=1 永远返空**:后端老 admin_user.status 是英文常量,前端传整数 `1` 必返空(白名单逻辑见上)。
|
||||||
|
2. **status=ACTIVE 仍不全**:即使前端改成 `status: 'ACTIVE'`,也漏掉 `LOCKED/DISABLED` 的定制师 — 业务上分享给 LOCKED admin 也是合理的(后端不限制)。
|
||||||
|
3. **`/admin/user` 不限角色**:会带回 `SUPER_ADMIN/ADMIN/FINANCE` 等非定制师,前端还得二次过滤,徒增复杂度。
|
||||||
|
4. **`/admin/user/designers` 也不行**:它是 C 端订单展示用,过滤了 `status=ACTIVE` + `企微非空`,与分享场景需求不一致。
|
||||||
|
|
||||||
|
→ **必须用 `/admin/user/customizers`**,这是后端 PR #1919 专门为本场景设计的接口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 复现取证(mmg 在 DevTools 验证)
|
||||||
|
|
||||||
|
1. **Network**:点产品列表「分享」按钮 → 抓「目标用户」下拉触发的请求,确认 URL 是 `GET /admin/user`(老接口)还是 `GET /admin/user/customizers`(新接口)。
|
||||||
|
2. **Response**:看老接口响应是否就是 `{ records: [], total: 0 }`。
|
||||||
|
3. **DevTools Sources**:搜 `ShareModal.vue` line 90,确认调用是 `getUserPage({status: 1})`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
mmg 修完前端后,验收点:
|
||||||
|
1. admin 产品列表点「分享」→ 弹窗里「目标用户」下拉**有数据**,列出当前所有 `CUSTOMIZER` 角色 admin(不含自己)。
|
||||||
|
2. 选一个目标 → 点「确定」→ 后端 `POST /admin/product/item/{id}/share` 200 → 提示"分享成功"。
|
||||||
|
3. 下拉里 `status=LOCKED/DISABLED` 的定制师**也应能选**(后端不限,业务允许)。
|
||||||
|
4. **不要**再传 `status: 1` 这种数字状态值给任何 `/admin/user/*` 接口 — 后端 status 字段一律是英文常量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
**前端(mmg 修改)**:
|
||||||
|
- `hl-ui/src/views/product/list/components/ShareModal.vue` (line 87-102 的 `loadUsers` 整段改)
|
||||||
|
- `hl-ui/src/api/user.js` (新增 `listShareCustomizers` 导出)
|
||||||
|
|
||||||
|
**后端(零改动,仅供前端复核)**:
|
||||||
|
- `hl-user-service/src/main/java/com/hulalv/user/controller/AdminUserController.java:182-195` (`/customizers` 端点)
|
||||||
|
- `hl-user-service/src/main/java/com/hulalv/user/service/DesignerService.java:60-130` (`listAllCustomizersForShare` + `getAllCustomizerAdminsRaw`)
|
||||||
|
- `hl-user-service/src/main/java/com/hulalv/user/vo/CustomizerSimpleVO.java` (轻量 VO)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- **仅影响**:管理后台「分享产品」对话框的「目标用户」下拉。
|
||||||
|
- **零影响**:
|
||||||
|
- C 端订单定制师卡片(继续用 `/admin/user/designers`,过滤逻辑不变)
|
||||||
|
- admin 用户管理列表(`/admin/user` 分页接口本身没变,前端别处该传 `status='ACTIVE'` 的地方继续按英文常量传)
|
||||||
|
- 产品复制底层逻辑(`ProductCopyService` 已支持 `targetAdminId` 参数,后端打通)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关历史 PR
|
||||||
|
|
||||||
|
| PR | Issue | 说明 | 是否仍有效 |
|
||||||
|
|----|-------|------|------------|
|
||||||
|
| #1488 | - | admin URL Scheme/Link 接口(`MiniappShareModal` 走这个) | ✅ 有效,与本通知无关 |
|
||||||
|
| **#1919** | **#1918** | **本通知关联**:实现 admin 分享产品功能(后端打通+新增 `/customizers` 接口) | ✅ 最新 |
|
||||||
|
|
||||||
|
历史撤回的通知 `09_frontend_notice_admin_product-share-remove-target-user-step.md` 是针对 **MiniappShareModal**(URL scheme 短链场景),与本通知针对的 **ShareModal**(产品复制给定制师)是两个不同弹窗,不要混。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- 关联 Issue: [wx/HL#1918](https://git.1814.love:8443/wx/HL/issues/1918)
|
||||||
|
- 关联 PR: [wx/HL#1919](https://git.1814.love:8443/wx/HL/pulls/1919)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**联系人**: wx
|
||||||
正在加载...
x
在新工单中引用
屏蔽一个用户