package adminhandler import ( "archive/zip" "bytes" "encoding/csv" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" adminlogic "spark-server/internal/module/admin/logic" "spark-server/internal/pkg/errs" ) type Handler struct { svc *adminlogic.Service } func New(svc *adminlogic.Service) *Handler { return &Handler{svc: svc} } func (h *Handler) ready(c *gin.Context) bool { if h == nil || h.svc == nil { Fail(c, http.StatusServiceUnavailable, "admin 服务未就绪") return false } return true } func (h *Handler) ImStats(c *gin.Context) { if !h.ready(c) { return } out, err := h.svc.ImStats(c.Request.Context()) if err != nil { Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, out) } func (h *Handler) ImMessages(c *gin.Context) { if !h.ready(c) { return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) list, err := h.svc.ListMessages(c.Request.Context(), c.Query("q"), limit, c.Query("start"), c.Query("end")) if err != nil { if errors.Is(err, errs.ErrInvalidArgument) { msg := "start/end 无效" if ae, ok := errs.AsAppError(err); ok && ae.CustomMsg() { msg = ae.Msg } Fail(c, http.StatusBadRequest, msg) return } Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, gin.H{"messages": list, "count": len(list)}) } func (h *Handler) ImConversations(c *gin.Context) { if !h.ready(c) { return } userID, _ := strconv.ParseInt(c.DefaultQuery("userId", "0"), 10, 64) convs, groups, stats, err := h.svc.ListConversations(c.Request.Context(), userID) if err != nil { Fail(c, http.StatusInternalServerError, err.Error()) return } if userID <= 0 { OK(c, gin.H{"stats": stats, "note": "传 userId 可查看该用户会话;否则仅返回统计"}) return } OK(c, gin.H{"conversations": convs, "groups": groups}) } func (h *Handler) ListGroups(c *gin.Context) { if !h.ready(c) { return } list, err := h.svc.ListGroups(c.Request.Context(), c.Query("q")) if err != nil { Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, list) } func (h *Handler) GroupDetail(c *gin.Context) { if !h.ready(c) { return } g, err := h.svc.GroupDetail(c.Request.Context(), c.Param("groupId")) if err != nil { if errors.Is(err, errs.ErrNotFound) { Fail(c, http.StatusNotFound, "群不存在") return } Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, g) } func (h *Handler) UpsertGroup(c *gin.Context) { Fail(c, http.StatusBadRequest, "V3 群由客户端创建;请使用 DELETE 解散已有群") } func (h *Handler) DeleteGroup(c *gin.Context) { if !h.ready(c) { return } if err := h.svc.DissolveGroup(c.Request.Context(), c.Param("groupId")); err != nil { if errors.Is(err, errs.ErrNotFound) { Fail(c, http.StatusNotFound, "群不存在") return } Fail(c, http.StatusInternalServerError, err.Error()) return } OKMsg(c, "已解散", nil) } func (h *Handler) ExportGroupZip(c *gin.Context) { if !h.ready(c) { return } g, err := h.svc.GroupDetail(c.Request.Context(), c.Param("groupId")) if err != nil { c.Status(http.StatusNotFound) return } buf := &bytes.Buffer{} zw := zip.NewWriter(buf) csvBuf := &bytes.Buffer{} w := csv.NewWriter(csvBuf) _ = w.Write([]string{"userId", "email", "nickname", "role"}) for i := range g.Members { m := &g.Members[i] _ = w.Write([]string{ strconv.FormatInt(m.UserID, 10), m.Email, m.Nickname, m.Role, }) } w.Flush() f1, _ := zw.Create("members.csv") _, _ = f1.Write(csvBuf.Bytes()) msgJSON, _ := json.Marshal(g.Messages) f2, _ := zw.Create("messages.json") _, _ = f2.Write(msgJSON) _ = zw.Close() c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="group-%s.zip"`, g.GroupID)) c.Data(http.StatusOK, "application/zip", buf.Bytes()) } func (h *Handler) ListComplaints(c *gin.Context) { if !h.ready(c) { return } list, err := h.svc.ListComplaints(c.Request.Context()) if err != nil { Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, list) } func (h *Handler) AddComplaint(c *gin.Context) { if !h.ready(c) { return } var body map[string]any _ = c.ShouldBindJSON(&body) out, err := h.svc.AddComplaint(c.Request.Context(), str(body, "email"), str(body, "target"), str(body, "reason"), str(body, "detail")) if err != nil { if errors.Is(err, errs.ErrInvalidArgument) { Fail(c, http.StatusBadRequest, "email/target 无效") return } Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, out) } func (h *Handler) SameIPRank(c *gin.Context) { if !h.ready(c) { return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) items, err := h.svc.SameIPRank(c.Request.Context(), limit) if err != nil { Fail(c, http.StatusInternalServerError, err.Error()) return } OK(c, gin.H{"items": items, "appCode": "spark"}) } func (h *Handler) Ranking(c *gin.Context) { OK(c, gin.H{ "type": c.Param("type"), "date": c.Query("date"), "items": []any{}, "note": "日排行依赖客户端上报;V3 暂未聚合", }) } func (h *Handler) OpsCatalog(c *gin.Context) { OK(c, gin.H{ "module": "spark-server-admin", "im": []string{"/im/stats", "/im/messages", "/im/conversations"}, "groups": []string{"/groups", "/groups/{id}", "/groups/{id}/export-zip"}, "users": []string{"/users/by-nickname", "/users/by-device", "/users/by-ip"}, }) } func (h *Handler) APIDoc(c *gin.Context) { OK(c, gin.H{ "title": "Spark IM V3 Admin API", "paths": []gin.H{ {"method": "GET", "path": "/api/admin/spark/im/stats", "desc": "IM 统计"}, {"method": "GET", "path": "/api/admin/spark/im/messages", "desc": "消息检索"}, {"method": "GET", "path": "/api/admin/spark/im/conversations", "desc": "用户会话"}, {"method": "GET", "path": "/api/admin/spark/groups", "desc": "群列表"}, {"method": "GET", "path": "/api/admin/spark/groups/{id}", "desc": "群详情"}, {"method": "DELETE", "path": "/api/admin/spark/groups/{id}", "desc": "解散群"}, {"method": "GET", "path": "/api/admin/users", "desc": "用户列表(V3)"}, }, "aliases": []string{"/api/admin/spark ≡ /api/admin/pop"}, "auth": "Bearer admin JWT (type=admin),与 Java popadmin 登录同密钥", }) } func str(body map[string]any, key string) string { if body == nil { return "" } v, ok := body[key] if !ok || v == nil { return "" } return strings.TrimSpace(fmt.Sprint(v)) }