- 测试服 round-trip 验证通过(20 条数据,字段全对,反例 401) - adminId 同时返 Number(老 ID 1001/1002)和 String(雪花 ID),前端必须统一 String 处理防精度丢失
11 KiB
前端 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:
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:
// 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)。
建议改后伪代码
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 旁边加:
/**
* 分享产品场景的目标定制师下拉(不分页,无 status/企微过滤)
* @returns {Promise<Array>} Array<CustomizerSimpleVO>
*/
export function listShareCustomizers() {
return http.get('/user/customizers')
}
✅ 已经过测试服 round-trip 验证(2026-05-09 19:30)
经网关 https://api.test.1814.love:9443/admin/user/customizers + SUPER_ADMIN token 实测:
- HTTP 200 +
code=200+success=true data是非空List,共 20 条(测试服 admin_user 中所有持 CUSTOMIZER 角色且非 DELETED 的 admin)- 字段命名与契约一致:
adminId/username/avatar - 无 token 反例 → 网关 401
Missing or invalid Authorization header - 与
/admin/user/designers对比:designers 只返 18 条(过滤 active+企微),customizers 返 20 条全集(含 admin/qulili 等无企微的轻量 admin) — 符合"分享下拉不过滤"语义
→ 后端接口工作正常,前端切到本接口即可,无需后端再改任何东西。
⚠️ 前端 JS 精度坑(必看)
实测响应里 adminId 同时有 Number 和 String 两种类型:
- 旧 admin (
adminId=1001 / 1002) → JSON Number - 新 admin (雪花 ID 如
"2021059720172838914") → JSON String
→ 前端 v-model 绑 value: u.adminId 时必须统一用 String 处理,否则 19 位雪花 ID 转 Number 会精度丢失末几位,选完用户提交时 targetUserId 错位,后端按错的 ID 复制产品。
正确做法:
.map((u) => ({
label: u.username,
value: String(u.adminId), // ← 统一 String,防雪花 ID 精度丢失
}))
// 排除自己时也用 String 比较
.filter((u) => String(u.adminId) !== String(currentUserId.value))
提交分享时如果后端 VO 字段是 Long targetAdminId,Spring 反序列化会自动把 String "20210597..." 转 Long,前端只管全程当 String 用即可。
后端接口契约
GET /admin/user/customizers
Headers:
Authorization: Bearer <admin token>(网关校验)
Query / Body: 无任何参数。
Response Result<List<CustomizerSimpleVO>>:
{
"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(老分页接口)
- status=1 永远返空:后端老 admin_user.status 是英文常量,前端传整数
1必返空(白名单逻辑见上)。 - status=ACTIVE 仍不全:即使前端改成
status: 'ACTIVE',也漏掉LOCKED/DISABLED的定制师 — 业务上分享给 LOCKED admin 也是合理的(后端不限制)。 /admin/user不限角色:会带回SUPER_ADMIN/ADMIN/FINANCE等非定制师,前端还得二次过滤,徒增复杂度。/admin/user/designers也不行:它是 C 端订单展示用,过滤了status=ACTIVE+企微非空,与分享场景需求不一致。
→ 必须用 /admin/user/customizers,这是后端 PR #1919 专门为本场景设计的接口。
复现取证(mmg 在 DevTools 验证)
- Network:点产品列表「分享」按钮 → 抓「目标用户」下拉触发的请求,确认 URL 是
GET /admin/user(老接口)还是GET /admin/user/customizers(新接口)。 - Response:看老接口响应是否就是
{ records: [], total: 0 }。 - DevTools Sources:搜
ShareModal.vueline 90,确认调用是getUserPage({status: 1})。
验收
mmg 修完前端后,验收点:
- admin 产品列表点「分享」→ 弹窗里「目标用户」下拉有数据,列出当前所有
CUSTOMIZER角色 admin(不含自己)。 - 选一个目标 → 点「确定」→ 后端
POST /admin/product/item/{id}/share200 → 提示"分享成功"。 - 下拉里
status=LOCKED/DISABLED的定制师也应能选(后端不限,业务允许)。 - 不要再传
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参数,后端打通)
- C 端订单定制师卡片(继续用
相关历史 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
- 关联 PR: wx/HL#1919
联系人: wx