package prettyid import ( "strconv" "strings" "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" "github.com/gin-gonic/gin" ) type Handler struct { user userclient.Client audit shared.OperationLogger } func New(user userclient.Client, audit shared.OperationLogger) *Handler { return &Handler{user: user, audit: audit} } type poolRequest struct { Name string `json:"name"` LevelTrack string `json:"level_track"` MinLevel int32 `json:"min_level"` MaxLevel int32 `json:"max_level"` RuleType string `json:"rule_type"` RuleConfigJSON string `json:"rule_config_json"` Status string `json:"status"` SortOrder int32 `json:"sort_order"` } type generateRequest struct { RuleType string `json:"rule_type"` RuleConfigJSON string `json:"rule_config_json"` Count int32 `json:"count"` } type grantRequest struct { TargetUserID any `json:"target_user_id"` DisplayUserID string `json:"display_user_id"` DurationMs int64 `json:"duration_ms"` Reason string `json:"reason"` } type statusRequest struct { Status string `json:"status"` Reason string `json:"reason"` } func (h *Handler) ListPools(c *gin.Context) { page, pageSize, ok := pageQuery(c) if !ok { response.BadRequest(c, "分页参数不正确") return } // admin-server 只解析 HTTP 查询参数和当前管理员身份,池状态、等级轨道合法性由 user-service 统一校验。 items, total, err := h.user.ListPrettyDisplayIDPools(c.Request.Context(), userclient.ListPrettyDisplayIDPoolsRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", Status: strings.TrimSpace(c.Query("status")), LevelTrack: strings.TrimSpace(c.Query("level_track")), Page: page, PageSize: pageSize, }) if err != nil { response.BadRequest(c, err.Error()) return } response.OK(c, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize}) } func (h *Handler) CreatePool(c *gin.Context) { var req poolRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "靓号池参数不正确") return } // 创建池时操作者从后台登录态读取,不能信任请求体里的管理员 ID。 pool, err := h.user.CreatePrettyDisplayIDPool(c.Request.Context(), userclient.PrettyDisplayIDPoolRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", Name: req.Name, LevelTrack: req.LevelTrack, MinLevel: req.MinLevel, MaxLevel: req.MaxLevel, RuleType: req.RuleType, RuleConfigJSON: req.RuleConfigJSON, Status: req.Status, SortOrder: req.SortOrder, OperatorAdminID: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "create-pretty-id-pool", "pretty_display_id_pools", pool.PoolID, "success", "name="+pool.Name) response.Created(c, pool) } func (h *Handler) UpdatePool(c *gin.Context) { var req poolRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "靓号池参数不正确") return } // pool_id 来自路径,body 只承载可编辑字段,避免前端误传 ID 导致更新目标不一致。 poolID := strings.TrimSpace(c.Param("pool_id")) pool, err := h.user.UpdatePrettyDisplayIDPool(c.Request.Context(), userclient.PrettyDisplayIDPoolRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", PoolID: poolID, Name: req.Name, LevelTrack: req.LevelTrack, MinLevel: req.MinLevel, MaxLevel: req.MaxLevel, RuleType: req.RuleType, RuleConfigJSON: req.RuleConfigJSON, Status: req.Status, SortOrder: req.SortOrder, OperatorAdminID: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "update-pretty-id-pool", "pretty_display_id_pools", pool.PoolID, "success", "status="+pool.Status) response.OK(c, pool) } func (h *Handler) Generate(c *gin.Context) { var req generateRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "生成参数不正确") return } // 生成只从路径读取 pool_id,请求体里的规则和数量会继续交给 user-service 校验并写批次审计。 poolID := strings.TrimSpace(c.Param("pool_id")) batch, err := h.user.GeneratePrettyDisplayIDs(c.Request.Context(), userclient.GeneratePrettyDisplayIDsRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", PoolID: poolID, RuleType: req.RuleType, RuleConfigJSON: req.RuleConfigJSON, Count: req.Count, OperatorAdminID: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "generate-pretty-ids", "pretty_display_id_generation_batches", batch.BatchID, "success", "pool_id="+poolID) response.Created(c, batch) } func (h *Handler) ListIDs(c *gin.Context) { page, pageSize, ok := pageQuery(c) if !ok { response.BadRequest(c, "分页参数不正确") return } assignedUserID, ok := optionalInt64Query(c, "assigned_user_id") if !ok { response.BadRequest(c, "assigned_user_id 参数不正确") return } // 列表筛选保持轻量透传,空字符串表示不过滤,assigned_user_id 只在正整数时作为精确条件。 items, total, err := h.user.ListPrettyDisplayIDs(c.Request.Context(), userclient.ListPrettyDisplayIDsRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", PoolID: strings.TrimSpace(c.Query("pool_id")), Source: strings.TrimSpace(c.Query("source")), Status: strings.TrimSpace(c.Query("status")), Keyword: strings.TrimSpace(c.Query("keyword")), AssignedUserID: assignedUserID, Page: page, PageSize: pageSize, }) if err != nil { response.BadRequest(c, err.Error()) return } response.OK(c, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize}) } func (h *Handler) Grant(c *gin.Context) { var req grantRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "发放参数不正确") return } targetUserID, ok := parseFlexibleInt64(req.TargetUserID) if !ok { response.BadRequest(c, "target_user_id 参数不正确") return } // 后台发放允许 target_user_id 由 JS 以数字或字符串提交,这里转成 int64 后把业务校验交给 user-service。 result, err := h.user.AdminGrantPrettyDisplayID(c.Request.Context(), userclient.AdminGrantPrettyDisplayIDRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", TargetUserID: targetUserID, DisplayUserID: req.DisplayUserID, DurationMs: req.DurationMs, Reason: req.Reason, OperatorAdminID: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "grant-pretty-id", "pretty_display_ids", result.PrettyID, "success", "target_user_id="+strconv.FormatInt(targetUserID, 10)) response.Created(c, result) } func (h *Handler) SetStatus(c *gin.Context) { var req statusRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "状态参数不正确") return } // 状态修改以路径 pretty_id 为准,请求体只允许目标状态和原因,防止改错号码。 prettyID := strings.TrimSpace(c.Param("pretty_id")) item, err := h.user.SetPrettyDisplayIDStatus(c.Request.Context(), userclient.SetPrettyDisplayIDStatusRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", PrettyID: prettyID, Status: req.Status, Reason: req.Reason, OperatorAdminID: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "set-pretty-id-status", "pretty_display_ids", prettyID, "success", "status="+item.Status) response.OK(c, item) } func pageQuery(c *gin.Context) (int32, int32, bool) { // 分页参数在 admin-server 先收敛到 1..100,避免后台页面传错值造成 user-service 大分页查询。 page := int32(1) pageSize := int32(20) if raw := strings.TrimSpace(c.Query("page")); raw != "" { value, err := strconv.ParseInt(raw, 10, 32) if err != nil || value <= 0 { return 0, 0, false } page = int32(value) } if raw := strings.TrimSpace(c.Query("page_size")); raw != "" { value, err := strconv.ParseInt(raw, 10, 32) if err != nil || value <= 0 { return 0, 0, false } pageSize = int32(value) } if pageSize > 100 { pageSize = 100 } return page, pageSize, true } func optionalInt64Query(c *gin.Context, name string) (int64, bool) { // 空查询表示不筛选,非空必须是正整数,避免把 0 当成真实用户 ID 传给列表接口。 raw := strings.TrimSpace(c.Query(name)) if raw == "" { return 0, true } value, err := strconv.ParseInt(raw, 10, 64) return value, err == nil && value > 0 } func parseFlexibleInt64(raw any) (int64, bool) { // JSON 大整数在前端经常以字符串提交,数字提交时也必须是精确整数,不能接受小数或负数。 switch value := raw.(type) { case float64: if value <= 0 || value != float64(int64(value)) { return 0, false } return int64(value), true case string: parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) return parsed, err == nil && parsed > 0 default: return 0, false } }