spark-server/internal/module/admin/logic/im.go
DESKTOP-L8OUL9F\gfl0034 292ac26ea6 fix(popadmin): IM消息展示可读时间并支持时段筛选
后端 GET /im/messages 增加 start/end(最长12个月),前端格式化 ts 并提供日/周/月/年/总/自定义筛选。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 15:05:54 +08:00

130 lines
3.6 KiB
Go
Raw Permalink 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 logic
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"spark-server/internal/pkg/errs"
)
const maxMessageRangeMonths = 12
func (s *Service) ImStats(ctx context.Context) (*ImStats, error) {
if !s.ready() {
return nil, ErrNotReady
}
return s.repo.ImStats(ctx)
}
func (s *Service) ListMessages(ctx context.Context, q string, limit int, start, end string) ([]ImMessageView, error) {
if !s.ready() {
return nil, ErrNotReady
}
if limit <= 0 || limit > 500 {
limit = 100
}
from, to, err := parseMessageTimeRange(start, end)
if err != nil {
return nil, err
}
q = strings.TrimSpace(q)
rows, err := s.repo.ListRecentMessages(ctx, limit*3, from, to)
if err != nil {
return nil, err
}
out := make([]ImMessageView, 0, limit)
for i := range rows {
v := messageView(&rows[i])
if q != "" && !messageMatch(&v, q) {
continue
}
out = append(out, v)
if len(out) >= limit {
break
}
}
return out, nil
}
// parseMessageTimeRange accepts YYYY-MM-DD start/end (Asia/Shanghai, inclusive calendar days).
// Empty both means no filter; one-sided or span > 12 months is invalid.
func parseMessageTimeRange(start, end string) (from, to *time.Time, err error) {
start = strings.TrimSpace(start)
end = strings.TrimSpace(end)
if start == "" && end == "" {
return nil, nil, nil
}
if start == "" || end == "" {
return nil, nil, errs.ErrInvalidArgument.WithMsg("start 与 end 需同时提供(YYYY-MM-DD)")
}
loc, locErr := time.LoadLocation("Asia/Shanghai")
if locErr != nil {
loc = time.FixedZone("CST", 8*3600)
}
fromDay, err := time.ParseInLocation("2006-01-02", start, loc)
if err != nil {
return nil, nil, errs.ErrInvalidArgument.WithMsg("start 日期格式应为 YYYY-MM-DD")
}
endDay, err := time.ParseInLocation("2006-01-02", end, loc)
if err != nil {
return nil, nil, errs.ErrInvalidArgument.WithMsg("end 日期格式应为 YYYY-MM-DD")
}
if endDay.Before(fromDay) {
return nil, nil, errs.ErrInvalidArgument.WithMsg("end 不能早于 start")
}
// Inclusive end day → exclusive upper bound at next midnight.
toExclusive := endDay.AddDate(0, 0, 1)
if toExclusive.After(fromDay.AddDate(0, maxMessageRangeMonths, 0)) {
return nil, nil, errs.ErrInvalidArgument.WithMsg(fmt.Sprintf("查询跨度最长 %d 个月", maxMessageRangeMonths))
}
return &fromDay, &toExclusive, nil
}
func (s *Service) ListConversations(ctx context.Context, userID int64) (convs []ImConversationView, groups []GroupView, stats *ImStats, err error) {
if !s.ready() {
return nil, nil, nil, ErrNotReady
}
if userID <= 0 {
stats, err = s.ImStats(ctx)
return nil, nil, stats, err
}
rows, err := s.repo.ListUserConversations(ctx, userID)
if err != nil {
return nil, nil, nil, err
}
convs = make([]ImConversationView, 0, len(rows))
groups = make([]GroupView, 0)
seenGroup := map[int64]struct{}{}
for i := range rows {
chatType := "private"
r := &rows[i]
if r.Type == 2 {
chatType = "group"
}
key := fmt.Sprintf("c:%d", r.ID)
if chatType == "private" {
key = fmt.Sprintf("p2p:%d_%d", r.UserSmall, r.UserBig)
} else if r.GroupID > 0 {
key = fmt.Sprintf("group:%d", r.GroupID)
}
convs = append(convs, ImConversationView{
Key: key, ChatType: chatType,
UserA: r.UserSmall, UserB: r.UserBig, GroupID: r.GroupID,
LastMsgID: r.LastMsgID, UpdatedAt: r.LastMsgAt.Format(time.RFC3339),
})
if r.GroupID > 0 {
if _, ok := seenGroup[r.GroupID]; !ok {
seenGroup[r.GroupID] = struct{}{}
gv, gerr := s.GroupDetail(ctx, strconv.FormatInt(r.GroupID, 10))
if gerr == nil && gv != nil {
groups = append(groups, *gv)
}
}
}
}
return convs, groups, nil, nil
}