spark-server/internal/transport/http/handler/admin/im_groups.go
zhr 13b42cef7b chore: 合并本地改动与 main(t_ 表名、lint、音乐分类)
在 fast-forward 到 origin/main 后解决 music logic/repo 冲突,SQL 与模型统一 t_* 表名;Makefile 修正 golangci-lint 变量并令 test/deploy 依赖 lint;补齐 admin/transport 等 lint 修复与 music logic 单测以满足覆盖率门禁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 10:56:59 +08:00

243 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 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))
}