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 }