spark-server/internal/transport/http/handler/admin/im_groups.go

242 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
if err != nil {
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 _, m := range g.Members {
_ = 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))
}