feat(admin): 用户管理新增分组列、分组筛选与专属分组一键替换

- 新增分组列:展示用户的专属/公开分组,支持 hover 查看详情
- 新增分组筛选:下拉选择或模糊搜索分组名过滤用户
- 专属分组替换:点击专属分组弹出操作菜单,选择目标分组后
  自动授予新分组权限、迁移绑定的 Key、移除旧分组权限
- 后端新增 POST /admin/users/:id/replace-group 端点,事务内
  完成分组替换并失效认证缓存
This commit is contained in:
QTom
2026-03-18 23:28:11 +08:00
parent 0236b97d49
commit ba7d2aecbb
29 changed files with 594 additions and 9 deletions

View File

@@ -445,5 +445,9 @@ func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *ser
return ""
}
func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*service.ReplaceUserGroupResult, error) {
return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil
}
// Ensure stub implements interface.
var _ service.AdminService = (*stubAdminService)(nil)

View File

@@ -75,6 +75,7 @@ type UpdateBalanceRequest struct {
// - role: filter by user role
// - search: search in email, username
// - attr[{id}]: filter by custom attribute value, e.g. attr[1]=company
// - group_name: fuzzy filter by allowed group name
func (h *UserHandler) List(c *gin.Context) {
page, pageSize := response.ParsePagination(c)
@@ -89,6 +90,7 @@ func (h *UserHandler) List(c *gin.Context) {
Status: c.Query("status"),
Role: c.Query("role"),
Search: search,
GroupName: strings.TrimSpace(c.Query("group_name")),
Attributes: parseAttributeFilters(c),
}
if raw, ok := c.GetQuery("include_subscriptions"); ok {
@@ -366,3 +368,35 @@ func (h *UserHandler) GetBalanceHistory(c *gin.Context) {
"total_recharged": totalRecharged,
})
}
// ReplaceGroupRequest represents the request to replace a user's exclusive group
type ReplaceGroupRequest struct {
OldGroupID int64 `json:"old_group_id" binding:"required,gt=0"`
NewGroupID int64 `json:"new_group_id" binding:"required,gt=0"`
}
// ReplaceGroup handles replacing a user's exclusive group
// POST /api/v1/admin/users/:id/replace-group
func (h *UserHandler) ReplaceGroup(c *gin.Context) {
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid user ID")
return
}
var req ReplaceGroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
result, err := h.adminService.ReplaceUserGroup(c.Request.Context(), userID, req.OldGroupID, req.NewGroupID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{
"migrated_keys": result.MigratedKeys,
})
}