后端 GET /im/messages 增加 start/end(最长12个月),前端格式化 ts 并提供日/周/月/年/总/自定义筛选。 Co-authored-by: Cursor <cursoragent@cursor.com>
251 lines
6.3 KiB
Go
251 lines
6.3 KiB
Go
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))
|
||
}
|