支持 square/squarenovel/squaresteps/squarehealth 实例与 category 筛选;音乐增加免登录 grouped/track;订阅套餐按 package_name 区分;补 migration 0030/0031 与 OpenAPI。 Co-authored-by: Cursor <cursoragent@cursor.com>
280 lines
7.5 KiB
Go
280 lines
7.5 KiB
Go
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
|
|
}
|