package model import ( "context" "errors" "math" "sort" "time" "gorm.io/gorm" "spark-server/internal/module/square/entity" ) // notExpiredSQL keeps user posts and unexpired bot posts. const notExpiredSQL = "(expires_at IS NULL OR expires_at > ?)" func (m *Repo) GetPost(ctx context.Context, id int64) (*entity.Post, error) { db, err := m.session(ctx) if err != nil { return nil, err } var row Post if err := db.Where("id = ?", id).Take(&row).Error; err != nil { return nil, mapDBErr(err) } return row.Entity(), nil } func (m *Repo) ListByAuthors(ctx context.Context, channel entity.Channel, authorIDs []int64, cursor int64, limit int) ([]*entity.Post, error) { if len(authorIDs) == 0 { return []*entity.Post{}, nil } db, err := m.session(ctx) if err != nil { return nil, err } ch := normalizeChannel(channel) q := db.Where("channel = ? AND user_id IN ? AND status = ? AND "+notExpiredSQL, ch, authorIDs, entity.StatusNormal, time.Now()) if cursor > 0 { q = q.Where("id < ?", cursor) } var rows []Post if err := q.Order("id DESC").Limit(limit).Find(&rows).Error; err != nil { return nil, mapDBErr(err) } return collectPosts(rows), nil } func (m *Repo) ListByAuthor(ctx context.Context, channel entity.Channel, authorID, cursor int64, limit int) ([]*entity.Post, error) { return m.ListByAuthors(ctx, channel, []int64{authorID}, cursor, limit) } func (m *Repo) ListPublicFeed(ctx context.Context, channel entity.Channel, q entity.FeedListQuery) ([]*entity.Post, error) { db, err := m.session(ctx) if err != nil { return nil, err } ch := normalizeChannel(channel) base := db.Where( "channel = ? AND audit_status = ? AND status = ? AND visibility IN ? AND "+notExpiredSQL, ch, entity.AuditApproved, entity.StatusNormal, []int16{int16(entity.VisibilityPublic), int16(entity.VisibilityExclude)}, time.Now(), ) if q.Category.IsTag() { base = base.Where("category = ?", string(q.Category)) } switch q.Category { case entity.CategoryHot: return listHotFeed(base, q) case entity.CategoryNearby: return listNearbyFeed(base, q) default: return listLatestFeed(base, q) } } func listLatestFeed(base *gorm.DB, q entity.FeedListQuery) ([]*entity.Post, error) { query := base if q.Cursor > 0 { query = query.Where("id < ?", q.Cursor) } var rows []Post if err := query.Order("id DESC").Limit(q.Limit).Find(&rows).Error; err != nil { return nil, mapDBErr(err) } return collectPosts(rows), nil } func listHotFeed(base *gorm.DB, q entity.FeedListQuery) ([]*entity.Post, error) { query := base if q.Cursor > 0 { var cur Post if err := base.Session(&gorm.Session{}).Where("id = ?", q.Cursor).Take(&cur).Error; err == nil { query = query.Where( "(like_count < ? OR (like_count = ? AND id < ?))", cur.LikeCount, cur.LikeCount, cur.ID, ) } else { query = query.Where("id < ?", q.Cursor) } } var rows []Post if err := query.Order("like_count DESC, id DESC").Limit(q.Limit).Find(&rows).Error; err != nil { return nil, mapDBErr(err) } return collectPosts(rows), nil } func listNearbyFeed(base *gorm.DB, q entity.FeedListQuery) ([]*entity.Post, error) { query := base.Where("(poi_lat <> 0 OR poi_lng <> 0)") if q.Cursor > 0 { query = query.Where("id < ?", q.Cursor) } // Over-fetch then sort by manhattan distance in process (nearby stub). fetch := q.Limit if fetch < 50 { fetch = 50 } var rows []Post if err := query.Order("id DESC").Limit(fetch).Find(&rows).Error; err != nil { return nil, mapDBErr(err) } posts := collectPosts(rows) if q.Lat == 0 && q.Lng == 0 { if q.Limit > 0 && len(posts) > q.Limit { posts = posts[:q.Limit] } return posts, nil } sort.SliceStable(posts, func(i, j int) bool { di := manhattan(q.Lat, q.Lng, posts[i].POI.Lat, posts[i].POI.Lng) dj := manhattan(q.Lat, q.Lng, posts[j].POI.Lat, posts[j].POI.Lng) if di == dj { return posts[i].ID > posts[j].ID } return di < dj }) if q.Limit > 0 && len(posts) > q.Limit { posts = posts[:q.Limit] } return posts, nil } func manhattan(lat1, lng1, lat2, lng2 float64) float64 { return math.Abs(lat1-lat2) + math.Abs(lng1-lng2) } func normalizeChannel(ch entity.Channel) string { if ch == "" { return string(entity.ChannelSquare) } return string(ch) } func (m *Repo) ListAudience(ctx context.Context, postIDs []int64) (map[int64][]int64, error) { out := make(map[int64][]int64) if len(postIDs) == 0 { return out, nil } db, err := m.session(ctx) if err != nil { return nil, err } var rows []Audience if err := db.Where("post_id IN ?", postIDs).Find(&rows).Error; err != nil { return nil, mapDBErr(err) } for _, row := range rows { out[row.PostID] = append(out[row.PostID], row.PeerID) } return out, nil } func (m *Repo) ListLikes(ctx context.Context, postIDs []int64) (map[int64][]*entity.Like, error) { out := make(map[int64][]*entity.Like) if len(postIDs) == 0 { return out, nil } db, err := m.session(ctx) if err != nil { return nil, err } var rows []Like if err := db.Where("post_id IN ?", postIDs).Order("created_at ASC").Find(&rows).Error; err != nil { return nil, mapDBErr(err) } for i := range rows { like := rows[i].Entity() out[like.PostID] = append(out[like.PostID], like) } return out, nil } func (m *Repo) ListComments(ctx context.Context, postIDs []int64) (map[int64][]*entity.Comment, error) { out := make(map[int64][]*entity.Comment) if len(postIDs) == 0 { return out, nil } db, err := m.session(ctx) if err != nil { return nil, err } var rows []Comment if err := db.Where("post_id IN ?", postIDs).Order("id ASC").Find(&rows).Error; err != nil { return nil, mapDBErr(err) } for i := range rows { c := rows[i].Entity() out[c.PostID] = append(out[c.PostID], c) } return out, nil } func (m *Repo) GetComment(ctx context.Context, id int64) (*entity.Comment, error) { db, err := m.session(ctx) if err != nil { return nil, err } var row Comment if err := db.Where("id = ?", id).Take(&row).Error; err != nil { return nil, mapDBErr(err) } return row.Entity(), nil } func (m *Repo) GetNoticeState(ctx context.Context, userID int64, channel entity.Channel) (*entity.NoticeState, error) { db, err := m.session(ctx) if err != nil { return nil, err } ch := normalizeChannel(channel) var row NoticeState if err := db.Where("user_id = ? AND channel = ?", userID, ch).Take(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return entity.DefaultNoticeState(userID, entity.Channel(ch)), nil } return nil, mapDBErr(err) } return row.Entity(), nil } func (m *Repo) LatestNoticeID(ctx context.Context, userID int64, channel entity.Channel) (int64, error) { db, err := m.session(ctx) if err != nil { return 0, err } ch := normalizeChannel(channel) var row Notice if err := db.Where("to_user_id = ? AND channel = ?", userID, ch).Order("id DESC").Limit(1).Take(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, nil } return 0, mapDBErr(err) } return row.ID, nil } func (m *Repo) ListNotices(ctx context.Context, userID int64, channel entity.Channel, cursor int64, limit int) ([]*entity.Notice, error) { db, err := m.session(ctx) if err != nil { return nil, err } ch := normalizeChannel(channel) q := db.Where("to_user_id = ? AND channel = ?", userID, ch) if cursor > 0 { q = q.Where("id < ?", cursor) } var rows []Notice if err := q.Order("id DESC").Limit(limit).Find(&rows).Error; err != nil { return nil, mapDBErr(err) } out := make([]*entity.Notice, 0, len(rows)) for i := range rows { out = append(out, rows[i].Entity()) } return out, nil }