Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
This commit is contained in:
296
internal/repository/comment_repo.go
Normal file
296
internal/repository/comment_repo.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CommentRepository 评论仓储
|
||||
type CommentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCommentRepository 创建评论仓储
|
||||
func NewCommentRepository(db *gorm.DB) *CommentRepository {
|
||||
return &CommentRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建评论
|
||||
func (r *CommentRepository) Create(comment *model.Comment) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 创建评论
|
||||
err := tx.Create(comment).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 增加帖子的评论数并同步热度分
|
||||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||||
Updates(map[string]interface{}{
|
||||
"comments_count": gorm.Expr("comments_count + 1"),
|
||||
"hot_score": gorm.Expr("likes_count * 2 + (comments_count + 1) * 3 + views_count * 0.1"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是回复,增加父评论的回复数
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||||
UpdateColumn("replies_count", gorm.Expr("replies_count + 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取评论
|
||||
func (r *CommentRepository) GetByID(id string) (*model.Comment, error) {
|
||||
var comment model.Comment
|
||||
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
// Update 更新评论
|
||||
func (r *CommentRepository) Update(comment *model.Comment) error {
|
||||
return r.db.Save(comment).Error
|
||||
}
|
||||
|
||||
// UpdateModerationStatus 更新评论审核状态
|
||||
func (r *CommentRepository) UpdateModerationStatus(commentID string, status model.CommentStatus) error {
|
||||
return r.db.Model(&model.Comment{}).
|
||||
Where("id = ?", commentID).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
// Delete 删除评论(软删除,同时清理关联数据)
|
||||
func (r *CommentRepository) Delete(id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 先查询评论获取post_id和parent_id
|
||||
var comment model.Comment
|
||||
if err := tx.First(&comment, "id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除评论点赞记录
|
||||
if err := tx.Where("comment_id = ?", id).Delete(&model.CommentLike{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除评论(软删除)
|
||||
if err := tx.Delete(&model.Comment{}, "id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 减少帖子的评论数并同步热度分
|
||||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||||
Updates(map[string]interface{}{
|
||||
"comments_count": gorm.Expr("comments_count - 1"),
|
||||
"hot_score": gorm.Expr("likes_count * 2 + (comments_count - 1) * 3 + views_count * 0.1"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是回复,减少父评论的回复数
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||||
UpdateColumn("replies_count", gorm.Expr("replies_count - 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetByPostID 获取帖子评论
|
||||
func (r *CommentRepository) GetByPostID(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
||||
var comments []*model.Comment
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.Comment{}).Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Order("created_at ASC").
|
||||
Find(&comments).Error
|
||||
|
||||
return comments, total, err
|
||||
}
|
||||
|
||||
// GetByPostIDWithReplies 获取帖子评论(包含回复,扁平化结构)
|
||||
// 所有层级的回复都扁平化展示在顶级评论的 replies 中
|
||||
func (r *CommentRepository) GetByPostIDWithReplies(postID string, page, pageSize, replyLimit int) ([]*model.Comment, int64, error) {
|
||||
var comments []*model.Comment
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.Comment{}).Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Order("created_at ASC").
|
||||
Find(&comments).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if len(comments) == 0 {
|
||||
return comments, total, nil
|
||||
}
|
||||
|
||||
rootIDs := make([]string, 0, len(comments))
|
||||
commentsByID := make(map[string]*model.Comment, len(comments))
|
||||
for _, comment := range comments {
|
||||
rootIDs = append(rootIDs, comment.ID)
|
||||
commentsByID[comment.ID] = comment
|
||||
}
|
||||
|
||||
// 批量加载所有回复,内存中按 root_id 分组并裁剪每个根评论的返回条数
|
||||
var allReplies []*model.Comment
|
||||
if err := r.db.Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Order("created_at ASC").
|
||||
Find(&allReplies).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
repliesByRoot := make(map[string][]*model.Comment, len(rootIDs))
|
||||
for _, reply := range allReplies {
|
||||
if reply.RootID == nil {
|
||||
continue
|
||||
}
|
||||
rootID := *reply.RootID
|
||||
if replyLimit <= 0 || len(repliesByRoot[rootID]) < replyLimit {
|
||||
repliesByRoot[rootID] = append(repliesByRoot[rootID], reply)
|
||||
}
|
||||
}
|
||||
|
||||
type replyCountRow struct {
|
||||
RootID string
|
||||
Total int64
|
||||
}
|
||||
var replyCountRows []replyCountRow
|
||||
if err := r.db.Model(&model.Comment{}).
|
||||
Select("root_id, COUNT(*) AS total").
|
||||
Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||||
Group("root_id").
|
||||
Scan(&replyCountRows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
replyCountMap := make(map[string]int64, len(replyCountRows))
|
||||
for _, row := range replyCountRows {
|
||||
replyCountMap[row.RootID] = row.Total
|
||||
}
|
||||
|
||||
for _, rootID := range rootIDs {
|
||||
comment := commentsByID[rootID]
|
||||
comment.Replies = repliesByRoot[rootID]
|
||||
comment.RepliesCount = int(replyCountMap[rootID])
|
||||
}
|
||||
|
||||
return comments, total, nil
|
||||
}
|
||||
|
||||
// loadFlatReplies 加载评论的所有回复(扁平化,所有层级都在同一层)
|
||||
func (r *CommentRepository) loadFlatReplies(rootComment *model.Comment, limit int) {
|
||||
var allReplies []*model.Comment
|
||||
|
||||
// 查询所有以该评论为根评论的回复(不包括顶级评论本身)
|
||||
r.db.Where("root_id = ? AND status = ?", rootComment.ID, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Order("created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&allReplies)
|
||||
|
||||
rootComment.Replies = allReplies
|
||||
}
|
||||
|
||||
// GetRepliesByRootID 根据根评论ID分页获取回复(扁平化)
|
||||
func (r *CommentRepository) GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
||||
var replies []*model.Comment
|
||||
var total int64
|
||||
|
||||
// 统计总数
|
||||
r.db.Model(&model.Comment{}).Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished).Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Order("created_at ASC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&replies).Error
|
||||
|
||||
return replies, total, err
|
||||
}
|
||||
|
||||
// GetReplies 获取回复
|
||||
func (r *CommentRepository) GetReplies(parentID string) ([]*model.Comment, error) {
|
||||
var comments []*model.Comment
|
||||
err := r.db.Where("parent_id = ? AND status = ?", parentID, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Order("created_at ASC").
|
||||
Find(&comments).Error
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// Like 点赞评论
|
||||
func (r *CommentRepository) Like(commentID, userID string) error {
|
||||
// 检查是否已经点赞
|
||||
var existing model.CommentLike
|
||||
err := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error
|
||||
if err == nil {
|
||||
// 已经点赞
|
||||
return nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建点赞记录
|
||||
like := &model.CommentLike{
|
||||
CommentID: commentID,
|
||||
UserID: userID,
|
||||
}
|
||||
err = r.db.Create(like).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 增加评论点赞数
|
||||
return r.db.Model(&model.Comment{}).Where("id = ?", commentID).
|
||||
UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error
|
||||
}
|
||||
|
||||
// Unlike 取消点赞评论
|
||||
func (r *CommentRepository) Unlike(commentID, userID string) error {
|
||||
result := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
// 减少评论点赞数
|
||||
return r.db.Model(&model.Comment{}).Where("id = ?", commentID).
|
||||
UpdateColumn("likes_count", gorm.Expr("likes_count - 1")).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLiked 检查是否已点赞
|
||||
func (r *CommentRepository) IsLiked(commentID, userID string) bool {
|
||||
var count int64
|
||||
r.db.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
166
internal/repository/device_token_repo.go
Normal file
166
internal/repository/device_token_repo.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DeviceTokenRepository 设备Token仓储
|
||||
type DeviceTokenRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewDeviceTokenRepository 创建设备Token仓储
|
||||
func NewDeviceTokenRepository(db *gorm.DB) *DeviceTokenRepository {
|
||||
return &DeviceTokenRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建设备Token
|
||||
func (r *DeviceTokenRepository) Create(token *model.DeviceToken) error {
|
||||
return r.db.Create(token).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取设备Token
|
||||
func (r *DeviceTokenRepository) GetByID(id int64) (*model.DeviceToken, error) {
|
||||
var token model.DeviceToken
|
||||
err := r.db.First(&token, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// Update 更新设备Token
|
||||
func (r *DeviceTokenRepository) Update(token *model.DeviceToken) error {
|
||||
return r.db.Save(token).Error
|
||||
}
|
||||
|
||||
// Delete 删除设备Token(软删除)
|
||||
func (r *DeviceTokenRepository) Delete(id int64) error {
|
||||
return r.db.Delete(&model.DeviceToken{}, id).Error
|
||||
}
|
||||
|
||||
// GetByUserID 获取用户所有设备
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *DeviceTokenRepository) GetByUserID(userID string) ([]*model.DeviceToken, error) {
|
||||
var tokens []*model.DeviceToken
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Find(&tokens).Error
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
// GetActiveByUserID 获取用户活跃设备
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *DeviceTokenRepository) GetActiveByUserID(userID string) ([]*model.DeviceToken, error) {
|
||||
var tokens []*model.DeviceToken
|
||||
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).
|
||||
Order("last_used_at DESC").
|
||||
Find(&tokens).Error
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
// GetByDeviceID 根据设备ID获取设备Token
|
||||
func (r *DeviceTokenRepository) GetByDeviceID(deviceID string) (*model.DeviceToken, error) {
|
||||
var token model.DeviceToken
|
||||
err := r.db.Where("device_id = ?", deviceID).First(&token).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// GetByPushToken 根据推送Token获取设备信息
|
||||
func (r *DeviceTokenRepository) GetByPushToken(pushToken string) (*model.DeviceToken, error) {
|
||||
var token model.DeviceToken
|
||||
err := r.db.Where("push_token = ?", pushToken).First(&token).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// DeactivateAllExcept 登出其他设备(停用除指定设备外的所有设备)
|
||||
func (r *DeviceTokenRepository) DeactivateAllExcept(userID int64, deviceID string) error {
|
||||
return r.db.Model(&model.DeviceToken{}).
|
||||
Where("user_id = ? AND device_id != ?", userID, deviceID).
|
||||
Update("is_active", false).Error
|
||||
}
|
||||
|
||||
// Upsert 创建或更新设备Token
|
||||
// 如果设备ID已存在,则更新Token和激活状态;否则创建新记录
|
||||
func (r *DeviceTokenRepository) Upsert(token *model.DeviceToken) error {
|
||||
var existing model.DeviceToken
|
||||
err := r.db.Where("device_id = ?", token.DeviceID).First(&existing).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
return r.db.Create(token).Error
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新现有记录
|
||||
return r.db.Model(&existing).Updates(map[string]interface{}{
|
||||
"push_token": token.PushToken,
|
||||
"is_active": true,
|
||||
"device_name": token.DeviceName,
|
||||
"last_used_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateLastUsed 更新最后使用时间
|
||||
func (r *DeviceTokenRepository) UpdateLastUsed(deviceID string) error {
|
||||
return r.db.Model(&model.DeviceToken{}).
|
||||
Where("device_id = ?", deviceID).
|
||||
Update("last_used_at", time.Now()).Error
|
||||
}
|
||||
|
||||
// Deactivate 停用设备
|
||||
func (r *DeviceTokenRepository) Deactivate(deviceID string) error {
|
||||
return r.db.Model(&model.DeviceToken{}).
|
||||
Where("device_id = ?", deviceID).
|
||||
Update("is_active", false).Error
|
||||
}
|
||||
|
||||
// Activate 激活设备
|
||||
func (r *DeviceTokenRepository) Activate(deviceID string) error {
|
||||
return r.db.Model(&model.DeviceToken{}).
|
||||
Where("device_id = ?", deviceID).
|
||||
Updates(map[string]interface{}{
|
||||
"is_active": true,
|
||||
"last_used_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DeleteByUserID 删除用户所有设备Token
|
||||
func (r *DeviceTokenRepository) DeleteByUserID(userID int64) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&model.DeviceToken{}).Error
|
||||
}
|
||||
|
||||
// GetDeviceCountByUserID 获取用户设备数量
|
||||
func (r *DeviceTokenRepository) GetDeviceCountByUserID(userID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.DeviceToken{}).
|
||||
Where("user_id = ?", userID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetActiveDeviceCountByUserID 获取用户活跃设备数量
|
||||
func (r *DeviceTokenRepository) GetActiveDeviceCountByUserID(userID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.DeviceToken{}).
|
||||
Where("user_id = ? AND is_active = ?", userID, true).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// DeleteInactiveDevices 删除长时间未使用的设备
|
||||
func (r *DeviceTokenRepository) DeleteInactiveDevices(before time.Time) error {
|
||||
return r.db.Where("is_active = ? AND last_used_at < ?", false, before).
|
||||
Delete(&model.DeviceToken{}).Error
|
||||
}
|
||||
50
internal/repository/group_join_request_repo.go
Normal file
50
internal/repository/group_join_request_repo.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GroupJoinRequestRepository interface {
|
||||
Create(req *model.GroupJoinRequest) error
|
||||
GetByFlag(flag string) (*model.GroupJoinRequest, error)
|
||||
Update(req *model.GroupJoinRequest) error
|
||||
GetPendingByGroupAndTarget(groupID, targetUserID string, reqType model.GroupJoinRequestType) (*model.GroupJoinRequest, error)
|
||||
}
|
||||
|
||||
type groupJoinRequestRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewGroupJoinRequestRepository(db *gorm.DB) GroupJoinRequestRepository {
|
||||
return &groupJoinRequestRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *groupJoinRequestRepository) Create(req *model.GroupJoinRequest) error {
|
||||
return r.db.Create(req).Error
|
||||
}
|
||||
|
||||
func (r *groupJoinRequestRepository) GetByFlag(flag string) (*model.GroupJoinRequest, error) {
|
||||
var req model.GroupJoinRequest
|
||||
if err := r.db.Where("flag = ?", flag).First(&req).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func (r *groupJoinRequestRepository) Update(req *model.GroupJoinRequest) error {
|
||||
return r.db.Save(req).Error
|
||||
}
|
||||
|
||||
func (r *groupJoinRequestRepository) GetPendingByGroupAndTarget(groupID, targetUserID string, reqType model.GroupJoinRequestType) (*model.GroupJoinRequest, error) {
|
||||
var req model.GroupJoinRequest
|
||||
err := r.db.Where("group_id = ? AND target_user_id = ? AND request_type = ? AND status = ?",
|
||||
groupID, targetUserID, reqType, model.GroupJoinRequestStatusPending).
|
||||
Order("created_at DESC").
|
||||
First(&req).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
242
internal/repository/group_repo.go
Normal file
242
internal/repository/group_repo.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GroupRepository 群组仓库接口
|
||||
type GroupRepository interface {
|
||||
// 群组操作
|
||||
Create(group *model.Group) error
|
||||
GetByID(id string) (*model.Group, error)
|
||||
Update(group *model.Group) error
|
||||
Delete(id string) error
|
||||
GetByOwnerID(ownerID string, page, pageSize int) ([]model.Group, int64, error)
|
||||
|
||||
// 群成员操作
|
||||
AddMember(member *model.GroupMember) error
|
||||
GetMember(groupID string, userID string) (*model.GroupMember, error)
|
||||
GetMembers(groupID string, page, pageSize int) ([]model.GroupMember, int64, error)
|
||||
UpdateMember(member *model.GroupMember) error
|
||||
RemoveMember(groupID string, userID string) error
|
||||
GetMemberCount(groupID string) (int64, error)
|
||||
IsMember(groupID string, userID string) (bool, error)
|
||||
GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error)
|
||||
|
||||
// 角色相关
|
||||
GetMemberRole(groupID string, userID string) (string, error)
|
||||
SetMemberRole(groupID string, userID string, role string) error
|
||||
GetAdmins(groupID string) ([]model.GroupMember, error)
|
||||
|
||||
// 群公告操作
|
||||
CreateAnnouncement(announcement *model.GroupAnnouncement) error
|
||||
GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error)
|
||||
GetAnnouncementByID(id string) (*model.GroupAnnouncement, error)
|
||||
DeleteAnnouncement(id string) error
|
||||
}
|
||||
|
||||
// groupRepository 群组仓库实现
|
||||
type groupRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGroupRepository 创建群组仓库
|
||||
func NewGroupRepository(db *gorm.DB) GroupRepository {
|
||||
return &groupRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建群组
|
||||
func (r *groupRepository) Create(group *model.Group) error {
|
||||
return r.db.Create(group).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取群组
|
||||
func (r *groupRepository) GetByID(id string) (*model.Group, error) {
|
||||
var group model.Group
|
||||
err := r.db.First(&group, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// Update 更新群组
|
||||
func (r *groupRepository) Update(group *model.Group) error {
|
||||
return r.db.Save(group).Error
|
||||
}
|
||||
|
||||
// Delete 删除群组
|
||||
func (r *groupRepository) Delete(id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 删除群成员
|
||||
if err := tx.Where("group_id = ?", id).Delete(&model.GroupMember{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除群公告
|
||||
if err := tx.Where("group_id = ?", id).Delete(&model.GroupAnnouncement{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除群组
|
||||
if err := tx.Delete(&model.Group{}, "id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetByOwnerID 根据群主ID获取群组列表
|
||||
func (r *groupRepository) GetByOwnerID(ownerID string, page, pageSize int) ([]model.Group, int64, error) {
|
||||
var groups []model.Group
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Group{}).Where("owner_id = ?", ownerID)
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&groups).Error
|
||||
return groups, total, err
|
||||
}
|
||||
|
||||
// AddMember 添加群成员
|
||||
func (r *groupRepository) AddMember(member *model.GroupMember) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 更新群组成员数量
|
||||
return tx.Model(&model.Group{}).Where("id = ?", member.GroupID).
|
||||
Update("member_count", gorm.Expr("member_count + ?", 1)).Error
|
||||
})
|
||||
}
|
||||
|
||||
// GetMember 获取群成员
|
||||
func (r *groupRepository) GetMember(groupID string, userID string) (*model.GroupMember, error) {
|
||||
var member model.GroupMember
|
||||
err := r.db.First(&member, "group_id = ? AND user_id = ?", groupID, userID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &member, nil
|
||||
}
|
||||
|
||||
// GetMembers 获取群成员列表
|
||||
func (r *groupRepository) GetMembers(groupID string, page, pageSize int) ([]model.GroupMember, int64, error) {
|
||||
var members []model.GroupMember
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.GroupMember{}).Where("group_id = ?", groupID)
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at ASC").Find(&members).Error
|
||||
return members, total, err
|
||||
}
|
||||
|
||||
// UpdateMember 更新群成员
|
||||
func (r *groupRepository) UpdateMember(member *model.GroupMember) error {
|
||||
return r.db.Save(member).Error
|
||||
}
|
||||
|
||||
// RemoveMember 移除群成员
|
||||
func (r *groupRepository) RemoveMember(groupID string, userID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 删除成员
|
||||
if err := tx.Where("group_id = ? AND user_id = ?", groupID, userID).Delete(&model.GroupMember{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 更新群组成员数量
|
||||
return tx.Model(&model.Group{}).Where("id = ?", groupID).
|
||||
Update("member_count", gorm.Expr("member_count - ?", 1)).Error
|
||||
})
|
||||
}
|
||||
|
||||
// GetMemberCount 获取群成员数量
|
||||
func (r *groupRepository) GetMemberCount(groupID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.GroupMember{}).Where("group_id = ?", groupID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// IsMember 检查是否是群成员
|
||||
func (r *groupRepository) IsMember(groupID string, userID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.GroupMember{}).Where("group_id = ? AND user_id = ?", groupID, userID).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// GetUserGroups 获取用户加入的群组列表
|
||||
func (r *groupRepository) GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error) {
|
||||
var groups []model.Group
|
||||
var total int64
|
||||
|
||||
// 通过群成员表查询用户加入的群组
|
||||
subQuery := r.db.Model(&model.GroupMember{}).
|
||||
Select("group_id").
|
||||
Where("user_id = ?", userID)
|
||||
|
||||
query := r.db.Model(&model.Group{}).Where("id IN (?)", subQuery)
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&groups).Error
|
||||
return groups, total, err
|
||||
}
|
||||
|
||||
// GetMemberRole 获取成员角色
|
||||
func (r *groupRepository) GetMemberRole(groupID string, userID string) (string, error) {
|
||||
member, err := r.GetMember(groupID, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return member.Role, nil
|
||||
}
|
||||
|
||||
// SetMemberRole 设置成员角色
|
||||
func (r *groupRepository) SetMemberRole(groupID string, userID string, role string) error {
|
||||
return r.db.Model(&model.GroupMember{}).
|
||||
Where("group_id = ? AND user_id = ?", groupID, userID).
|
||||
Update("role", role).Error
|
||||
}
|
||||
|
||||
// GetAdmins 获取群管理员列表
|
||||
func (r *groupRepository) GetAdmins(groupID string) ([]model.GroupMember, error) {
|
||||
var admins []model.GroupMember
|
||||
err := r.db.Where("group_id = ? AND role = ?", groupID, model.GroupRoleAdmin).Find(&admins).Error
|
||||
return admins, err
|
||||
}
|
||||
|
||||
// CreateAnnouncement 创建群公告
|
||||
func (r *groupRepository) CreateAnnouncement(announcement *model.GroupAnnouncement) error {
|
||||
return r.db.Create(announcement).Error
|
||||
}
|
||||
|
||||
// GetAnnouncements 获取群公告列表
|
||||
func (r *groupRepository) GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error) {
|
||||
var announcements []model.GroupAnnouncement
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.GroupAnnouncement{}).Where("group_id = ?", groupID)
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
// 置顶的排在前面,然后按时间倒序
|
||||
err := query.Offset(offset).Limit(pageSize).Order("is_pinned DESC, created_at DESC").Find(&announcements).Error
|
||||
return announcements, total, err
|
||||
}
|
||||
|
||||
// GetAnnouncementByID 根据ID获取群公告
|
||||
func (r *groupRepository) GetAnnouncementByID(id string) (*model.GroupAnnouncement, error) {
|
||||
var announcement model.GroupAnnouncement
|
||||
err := r.db.First(&announcement, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &announcement, nil
|
||||
}
|
||||
|
||||
// DeleteAnnouncement 删除群公告
|
||||
func (r *groupRepository) DeleteAnnouncement(id string) error {
|
||||
return r.db.Delete(&model.GroupAnnouncement{}, "id = ?", id).Error
|
||||
}
|
||||
543
internal/repository/message_repo.go
Normal file
543
internal/repository/message_repo.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// MessageRepository 消息仓储
|
||||
type MessageRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewMessageRepository 创建消息仓储
|
||||
func NewMessageRepository(db *gorm.DB) *MessageRepository {
|
||||
return &MessageRepository{db: db}
|
||||
}
|
||||
|
||||
// CreateMessage 创建消息
|
||||
func (r *MessageRepository) CreateMessage(msg *model.Message) error {
|
||||
return r.db.Create(msg).Error
|
||||
}
|
||||
|
||||
// GetConversation 获取会话
|
||||
func (r *MessageRepository) GetConversation(id string) (*model.Conversation, error) {
|
||||
var conv model.Conversation
|
||||
err := r.db.Preload("Group").First(&conv, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
// GetOrCreatePrivateConversation 获取或创建私聊会话
|
||||
// 使用参与者关系表来管理会话
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *MessageRepository) GetOrCreatePrivateConversation(user1ID, user2ID string) (*model.Conversation, error) {
|
||||
var conv model.Conversation
|
||||
|
||||
fmt.Printf("[DEBUG] GetOrCreatePrivateConversation: user1ID=%s, user2ID=%s\n", user1ID, user2ID)
|
||||
|
||||
// 查找两个用户共同参与的私聊会话
|
||||
err := r.db.Table("conversations c").
|
||||
Joins("INNER JOIN conversation_participants cp1 ON c.id = cp1.conversation_id AND cp1.user_id = ?", user1ID).
|
||||
Joins("INNER JOIN conversation_participants cp2 ON c.id = cp2.conversation_id AND cp2.user_id = ?", user2ID).
|
||||
Where("c.type = ?", model.ConversationTypePrivate).
|
||||
First(&conv).Error
|
||||
|
||||
if err == nil {
|
||||
_ = r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id IN ?", conv.ID, []string{user1ID, user2ID}).
|
||||
Update("hidden_at", nil).Error
|
||||
fmt.Printf("[DEBUG] GetOrCreatePrivateConversation: found existing conversation, ID=%s\n", conv.ID)
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 没找到会话,创建新会话
|
||||
fmt.Printf("[DEBUG] GetOrCreatePrivateConversation: no existing conversation found, creating new one\n")
|
||||
conv = model.Conversation{
|
||||
Type: model.ConversationTypePrivate,
|
||||
}
|
||||
|
||||
// 使用事务创建会话和参与者
|
||||
err = r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&conv).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建参与者记录 - UserID 存储为 string (UUID)
|
||||
participants := []model.ConversationParticipant{
|
||||
{ConversationID: conv.ID, UserID: user1ID},
|
||||
{ConversationID: conv.ID, UserID: user2ID},
|
||||
}
|
||||
if err := tx.Create(&participants).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
fmt.Printf("[DEBUG] GetOrCreatePrivateConversation: created new conversation, ID=%s\n", conv.ID)
|
||||
}
|
||||
|
||||
return &conv, err
|
||||
}
|
||||
|
||||
// GetConversations 获取用户会话列表
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *MessageRepository) GetConversations(userID string, page, pageSize int) ([]*model.Conversation, int64, error) {
|
||||
var convs []*model.Conversation
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("user_id = ? AND hidden_at IS NULL", userID).
|
||||
Count(&total)
|
||||
|
||||
if total == 0 {
|
||||
return convs, total, nil
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
// 查询会话列表并预加载关联数据:
|
||||
// 当前用户维度先按置顶排序,再按更新时间排序
|
||||
err := r.db.Model(&model.Conversation{}).
|
||||
Joins("INNER JOIN conversation_participants cp ON conversations.id = cp.conversation_id").
|
||||
Where("cp.user_id = ? AND cp.hidden_at IS NULL", userID).
|
||||
Preload("Group").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Order("cp.is_pinned DESC").
|
||||
Order("conversations.updated_at DESC").
|
||||
Find(&convs).Error
|
||||
|
||||
return convs, total, err
|
||||
}
|
||||
|
||||
// GetMessages 获取会话消息
|
||||
func (r *MessageRepository) GetMessages(conversationID string, page, pageSize int) ([]*model.Message, int64, error) {
|
||||
var messages []*model.Message
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.Message{}).Where("conversation_id = ?", conversationID).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("conversation_id = ?", conversationID).
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Order("seq DESC").
|
||||
Find(&messages).Error
|
||||
|
||||
return messages, total, err
|
||||
}
|
||||
|
||||
// GetMessagesAfterSeq 获取指定seq之后的消息(用于增量同步)
|
||||
func (r *MessageRepository) GetMessagesAfterSeq(conversationID string, afterSeq int64, limit int) ([]*model.Message, error) {
|
||||
var messages []*model.Message
|
||||
err := r.db.Where("conversation_id = ? AND seq > ?", conversationID, afterSeq).
|
||||
Order("seq ASC").
|
||||
Limit(limit).
|
||||
Find(&messages).Error
|
||||
return messages, err
|
||||
}
|
||||
|
||||
// GetMessagesBeforeSeq 获取指定seq之前的历史消息(用于下拉加载更多)
|
||||
func (r *MessageRepository) GetMessagesBeforeSeq(conversationID string, beforeSeq int64, limit int) ([]*model.Message, error) {
|
||||
var messages []*model.Message
|
||||
fmt.Printf("[DEBUG] GetMessagesBeforeSeq: conversationID=%s, beforeSeq=%d, limit=%d\n", conversationID, beforeSeq, limit)
|
||||
err := r.db.Where("conversation_id = ? AND seq < ?", conversationID, beforeSeq).
|
||||
Order("seq DESC"). // 降序获取最新消息在前
|
||||
Limit(limit).
|
||||
Find(&messages).Error
|
||||
fmt.Printf("[DEBUG] GetMessagesBeforeSeq: found %d messages, seq range: ", len(messages))
|
||||
for i, m := range messages {
|
||||
if i < 5 || i >= len(messages)-2 {
|
||||
fmt.Printf("%d ", m.Seq)
|
||||
} else if i == 5 {
|
||||
fmt.Printf("... ")
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
// 反转回正序
|
||||
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
|
||||
messages[i], messages[j] = messages[j], messages[i]
|
||||
}
|
||||
return messages, err
|
||||
}
|
||||
|
||||
// GetConversationParticipants 获取会话参与者
|
||||
func (r *MessageRepository) GetConversationParticipants(conversationID string) ([]*model.ConversationParticipant, error) {
|
||||
var participants []*model.ConversationParticipant
|
||||
err := r.db.Where("conversation_id = ?", conversationID).Find(&participants).Error
|
||||
return participants, err
|
||||
}
|
||||
|
||||
// GetParticipant 获取用户在会话中的参与者信息
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *MessageRepository) GetParticipant(conversationID string, userID string) (*model.ConversationParticipant, error) {
|
||||
var participant model.ConversationParticipant
|
||||
err := r.db.Where("conversation_id = ? AND user_id = ?", conversationID, userID).First(&participant).Error
|
||||
if err != nil {
|
||||
// 如果找不到参与者,尝试添加(修复没有参与者记录的问题)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 检查会话是否存在
|
||||
var conv model.Conversation
|
||||
if err := r.db.First(&conv, conversationID).Error; err == nil {
|
||||
// 会话存在,添加参与者
|
||||
participant = model.ConversationParticipant{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
}
|
||||
if err := r.db.Create(&participant).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &participant, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &participant, nil
|
||||
}
|
||||
|
||||
// UpdateLastReadSeq 更新已读位置
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *MessageRepository) UpdateLastReadSeq(conversationID string, userID string, lastReadSeq int64) error {
|
||||
result := r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Update("last_read_seq", lastReadSeq)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 如果没有更新任何记录,说明参与者记录不存在,需要插入
|
||||
if result.RowsAffected == 0 {
|
||||
// 尝试插入新记录(跨数据库 upsert)
|
||||
err := r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "conversation_id"},
|
||||
{Name: "user_id"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"last_read_seq": lastReadSeq,
|
||||
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}),
|
||||
}).Create(&model.ConversationParticipant{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
LastReadSeq: lastReadSeq,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePinned 更新会话置顶状态(用户维度)
|
||||
func (r *MessageRepository) UpdatePinned(conversationID string, userID string, isPinned bool) error {
|
||||
result := r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Update("is_pinned", isPinned)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "conversation_id"},
|
||||
{Name: "user_id"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"is_pinned": isPinned,
|
||||
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}),
|
||||
}).Create(&model.ConversationParticipant{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
IsPinned: isPinned,
|
||||
}).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnreadCount 获取未读消息数
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *MessageRepository) GetUnreadCount(conversationID string, userID string) (int64, error) {
|
||||
var participant model.ConversationParticipant
|
||||
err := r.db.Where("conversation_id = ? AND user_id = ?", conversationID, userID).First(&participant).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = r.db.Model(&model.Message{}).
|
||||
Where("conversation_id = ? AND sender_id != ? AND seq > ?", conversationID, userID, participant.LastReadSeq).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// UpdateConversationLastSeq 更新会话的最后消息seq和时间
|
||||
func (r *MessageRepository) UpdateConversationLastSeq(conversationID string, seq int64) error {
|
||||
return r.db.Model(&model.Conversation{}).
|
||||
Where("id = ?", conversationID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_seq": seq,
|
||||
"last_msg_time": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetNextSeq 获取会话的下一个seq值
|
||||
func (r *MessageRepository) GetNextSeq(conversationID string) (int64, error) {
|
||||
var conv model.Conversation
|
||||
err := r.db.Select("last_seq").First(&conv, conversationID).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return conv.LastSeq + 1, nil
|
||||
}
|
||||
|
||||
// CreateMessageWithSeq 创建消息并更新seq(事务操作)
|
||||
func (r *MessageRepository) CreateMessageWithSeq(msg *model.Message) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 获取当前seq并+1
|
||||
var conv model.Conversation
|
||||
if err := tx.Select("last_seq").First(&conv, msg.ConversationID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Seq = conv.LastSeq + 1
|
||||
|
||||
// 创建消息
|
||||
if err := tx.Create(msg).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新会话的last_seq
|
||||
if err := tx.Model(&model.Conversation{}).
|
||||
Where("id = ?", msg.ConversationID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_seq": msg.Seq,
|
||||
"last_msg_time": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 新消息到达后,自动恢复被“仅自己删除”的会话
|
||||
if err := tx.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ?", msg.ConversationID).
|
||||
Update("hidden_at", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllUnreadCount 获取用户所有会话的未读消息总数
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *MessageRepository) GetAllUnreadCount(userID string) (int64, error) {
|
||||
var totalUnread int64
|
||||
err := r.db.Table("conversation_participants AS cp").
|
||||
Joins("LEFT JOIN messages AS m ON m.conversation_id = cp.conversation_id AND m.sender_id <> ? AND m.seq > cp.last_read_seq AND m.deleted_at IS NULL", userID).
|
||||
Where("cp.user_id = ?", userID).
|
||||
Select("COALESCE(COUNT(m.id), 0)").
|
||||
Scan(&totalUnread).Error
|
||||
return totalUnread, err
|
||||
}
|
||||
|
||||
// GetMessageByID 根据ID获取消息
|
||||
func (r *MessageRepository) GetMessageByID(messageID string) (*model.Message, error) {
|
||||
var message model.Message
|
||||
err := r.db.First(&message, "id = ?", messageID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// CountMessagesBySenderInConversation 统计会话中某用户已发送消息数
|
||||
func (r *MessageRepository) CountMessagesBySenderInConversation(conversationID, senderID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Message{}).
|
||||
Where("conversation_id = ? AND sender_id = ?", conversationID, senderID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// UpdateMessageStatus 更新消息状态
|
||||
func (r *MessageRepository) UpdateMessageStatus(messageID int64, status model.MessageStatus) error {
|
||||
return r.db.Model(&model.Message{}).
|
||||
Where("id = ?", messageID).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
// GetOrCreateSystemParticipant 获取或创建用户在系统会话中的参与者记录
|
||||
// 系统会话是虚拟会话,但需要参与者记录来跟踪已读状态
|
||||
func (r *MessageRepository) GetOrCreateSystemParticipant(userID string) (*model.ConversationParticipant, error) {
|
||||
var participant model.ConversationParticipant
|
||||
err := r.db.Where("conversation_id = ? AND user_id = ?",
|
||||
model.SystemConversationID, userID).First(&participant).Error
|
||||
|
||||
if err == nil {
|
||||
return &participant, nil
|
||||
}
|
||||
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 自动创建参与者记录
|
||||
participant = model.ConversationParticipant{
|
||||
ConversationID: model.SystemConversationID,
|
||||
UserID: userID,
|
||||
LastReadSeq: 0,
|
||||
}
|
||||
|
||||
if err := r.db.Create(&participant).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &participant, nil
|
||||
}
|
||||
|
||||
// GetSystemMessagesUnreadCount 获取系统消息未读数
|
||||
func (r *MessageRepository) GetSystemMessagesUnreadCount(userID string) (int64, error) {
|
||||
// 获取或创建参与者记录
|
||||
participant, err := r.GetOrCreateSystemParticipant(userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 计算未读数:查询 seq > last_read_seq 的消息
|
||||
var count int64
|
||||
err = r.db.Model(&model.Message{}).
|
||||
Where("conversation_id = ? AND seq > ?",
|
||||
model.SystemConversationID, participant.LastReadSeq).
|
||||
Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// MarkAllSystemMessagesAsRead 标记所有系统消息已读
|
||||
func (r *MessageRepository) MarkAllSystemMessagesAsRead(userID string) error {
|
||||
// 获取系统会话的最新 seq
|
||||
var maxSeq int64
|
||||
err := r.db.Model(&model.Message{}).
|
||||
Where("conversation_id = ?", model.SystemConversationID).
|
||||
Select("COALESCE(MAX(seq), 0)").
|
||||
Scan(&maxSeq).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用跨数据库 upsert 方式更新或创建参与者记录
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "conversation_id"},
|
||||
{Name: "user_id"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"last_read_seq": maxSeq,
|
||||
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}),
|
||||
}).Create(&model.ConversationParticipant{
|
||||
ConversationID: model.SystemConversationID,
|
||||
UserID: userID,
|
||||
LastReadSeq: maxSeq,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetConversationByGroupID 通过群组ID获取会话
|
||||
func (r *MessageRepository) GetConversationByGroupID(groupID string) (*model.Conversation, error) {
|
||||
var conv model.Conversation
|
||||
err := r.db.Where("group_id = ?", groupID).First(&conv).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
// RemoveParticipant 移除会话参与者
|
||||
// 当用户退出群聊时,需要同时移除其在对应会话中的参与者记录
|
||||
func (r *MessageRepository) RemoveParticipant(conversationID string, userID string) error {
|
||||
return r.db.Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Delete(&model.ConversationParticipant{}).Error
|
||||
}
|
||||
|
||||
// AddParticipant 添加会话参与者
|
||||
// 当用户加入群聊时,需要同时将其添加到对应会话的参与者记录
|
||||
func (r *MessageRepository) AddParticipant(conversationID string, userID string) error {
|
||||
// 先检查是否已经是参与者
|
||||
var count int64
|
||||
err := r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果已经是参与者,直接返回
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 添加参与者
|
||||
participant := model.ConversationParticipant{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
LastReadSeq: 0,
|
||||
}
|
||||
return r.db.Create(&participant).Error
|
||||
}
|
||||
|
||||
// DeleteConversationByGroupID 删除群组对应的会话及其参与者
|
||||
// 当解散群组时调用
|
||||
func (r *MessageRepository) DeleteConversationByGroupID(groupID string) error {
|
||||
// 获取群组对应的会话
|
||||
conv, err := r.GetConversationByGroupID(groupID)
|
||||
if err != nil {
|
||||
// 如果会话不存在,直接返回
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 删除会话参与者
|
||||
if err := tx.Where("conversation_id = ?", conv.ID).Delete(&model.ConversationParticipant{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除会话中的消息
|
||||
if err := tx.Where("conversation_id = ?", conv.ID).Delete(&model.Message{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除会话
|
||||
if err := tx.Delete(&model.Conversation{}, "id = ?", conv.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// HideConversationForUser 仅对当前用户隐藏会话(私聊删除)
|
||||
func (r *MessageRepository) HideConversationForUser(conversationID, userID string) error {
|
||||
now := time.Now()
|
||||
return r.db.Model(&model.ConversationParticipant{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Update("hidden_at", &now).Error
|
||||
}
|
||||
78
internal/repository/notification_repo.go
Normal file
78
internal/repository/notification_repo.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// NotificationRepository 通知仓储
|
||||
type NotificationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewNotificationRepository 创建通知仓储
|
||||
func NewNotificationRepository(db *gorm.DB) *NotificationRepository {
|
||||
return &NotificationRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建通知
|
||||
func (r *NotificationRepository) Create(notification *model.Notification) error {
|
||||
return r.db.Create(notification).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取通知
|
||||
func (r *NotificationRepository) GetByID(id string) (*model.Notification, error) {
|
||||
var notification model.Notification
|
||||
err := r.db.First(¬ification, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬ification, nil
|
||||
}
|
||||
|
||||
// GetByUserID 获取用户通知
|
||||
func (r *NotificationRepository) GetByUserID(userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error) {
|
||||
var notifications []*model.Notification
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Notification{}).Where("user_id = ?", userID)
|
||||
|
||||
if unreadOnly {
|
||||
query = query.Where("is_read = ?", false)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(¬ifications).Error
|
||||
|
||||
return notifications, total, err
|
||||
}
|
||||
|
||||
// MarkAsRead 标记为已读
|
||||
func (r *NotificationRepository) MarkAsRead(id string) error {
|
||||
return r.db.Model(&model.Notification{}).Where("id = ?", id).Update("is_read", true).Error
|
||||
}
|
||||
|
||||
// MarkAllAsRead 标记所有为已读
|
||||
func (r *NotificationRepository) MarkAllAsRead(userID string) error {
|
||||
return r.db.Model(&model.Notification{}).Where("user_id = ?", userID).Update("is_read", true).Error
|
||||
}
|
||||
|
||||
// Delete 删除通知
|
||||
func (r *NotificationRepository) Delete(id string) error {
|
||||
return r.db.Delete(&model.Notification{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// GetUnreadCount 获取未读数量
|
||||
func (r *NotificationRepository) GetUnreadCount(userID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Notification{}).Where("user_id = ? AND is_read = ?", userID, false).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// DeleteAllByUserID 删除用户所有通知
|
||||
func (r *NotificationRepository) DeleteAllByUserID(userID string) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&model.Notification{}).Error
|
||||
}
|
||||
360
internal/repository/post_repo.go
Normal file
360
internal/repository/post_repo.go
Normal file
@@ -0,0 +1,360 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PostRepository 帖子仓储
|
||||
type PostRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewPostRepository 创建帖子仓储
|
||||
func NewPostRepository(db *gorm.DB) *PostRepository {
|
||||
return &PostRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建帖子
|
||||
func (r *PostRepository) Create(post *model.Post, images []string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 创建帖子
|
||||
if err := tx.Create(post).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建图片记录
|
||||
for i, url := range images {
|
||||
image := &model.PostImage{
|
||||
PostID: post.ID,
|
||||
URL: url,
|
||||
SortOrder: i,
|
||||
}
|
||||
if err := tx.Create(image).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取帖子
|
||||
func (r *PostRepository) GetByID(id string) (*model.Post, error) {
|
||||
var post model.Post
|
||||
err := r.db.Preload("User").Preload("Images").First(&post, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
// Update 更新帖子
|
||||
func (r *PostRepository) Update(post *model.Post) error {
|
||||
return r.db.Save(post).Error
|
||||
}
|
||||
|
||||
// UpdateModerationStatus 更新帖子审核状态
|
||||
func (r *PostRepository) UpdateModerationStatus(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
"reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
"reviewed_by": reviewedBy,
|
||||
"reject_reason": rejectReason,
|
||||
}
|
||||
return r.db.Model(&model.Post{}).Where("id = ?", postID).Updates(updates).Error
|
||||
}
|
||||
|
||||
// Delete 删除帖子(软删除,同时清理关联数据)
|
||||
func (r *PostRepository) Delete(id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 删除帖子图片
|
||||
if err := tx.Where("post_id = ?", id).Delete(&model.PostImage{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除帖子点赞记录
|
||||
if err := tx.Where("post_id = ?", id).Delete(&model.PostLike{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除帖子收藏记录
|
||||
if err := tx.Where("post_id = ?", id).Delete(&model.Favorite{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除评论点赞记录(子查询获取该帖子所有评论ID)
|
||||
if err := tx.Where("comment_id IN (SELECT id FROM comments WHERE post_id = ?)", id).Delete(&model.CommentLike{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除帖子评论
|
||||
if err := tx.Where("post_id = ?", id).Delete(&model.Comment{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 最后删除帖子本身(软删除)
|
||||
return tx.Delete(&model.Post{}, "id = ?", id).Error
|
||||
})
|
||||
}
|
||||
|
||||
// List 分页获取帖子列表
|
||||
func (r *PostRepository) List(page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished)
|
||||
|
||||
if userID != "" {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Preload("User").Preload("Images").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&posts).Error
|
||||
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// GetUserPosts 获取用户帖子
|
||||
func (r *PostRepository) GetUserPosts(userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, model.PostStatusPublished).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("user_id = ? AND status = ?", userID, model.PostStatusPublished).Preload("User").Preload("Images").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&posts).Error
|
||||
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// GetFavorites 获取用户收藏
|
||||
func (r *PostRepository) GetFavorites(userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
subQuery := r.db.Model(&model.Favorite{}).Where("user_id = ?", userID).Select("post_id")
|
||||
r.db.Model(&model.Post{}).Where("id IN (?) AND status = ?", subQuery, model.PostStatusPublished).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("id IN (?) AND status = ?", subQuery, model.PostStatusPublished).Preload("User").Preload("Images").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&posts).Error
|
||||
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// Like 点赞帖子
|
||||
func (r *PostRepository) Like(postID, userID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查是否已经点赞
|
||||
var existing model.PostLike
|
||||
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&existing).Error
|
||||
if err == nil {
|
||||
// 已经点赞,直接返回
|
||||
return nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建点赞记录
|
||||
if err := tx.Create(&model.PostLike{
|
||||
PostID: postID,
|
||||
UserID: userID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 增加帖子点赞数并同步热度分
|
||||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||||
Updates(map[string]interface{}{
|
||||
"likes_count": gorm.Expr("likes_count + 1"),
|
||||
"hot_score": gorm.Expr("(likes_count + 1) * 2 + comments_count * 3 + views_count * 0.1"),
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// Unlike 取消点赞
|
||||
func (r *PostRepository) Unlike(postID, userID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Where("post_id = ? AND user_id = ?", postID, userID).Delete(&model.PostLike{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
// 减少帖子点赞数并同步热度分
|
||||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||||
Updates(map[string]interface{}{
|
||||
"likes_count": gorm.Expr("likes_count - 1"),
|
||||
"hot_score": gorm.Expr("(likes_count - 1) * 2 + comments_count * 3 + views_count * 0.1"),
|
||||
}).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// IsLiked 检查是否点赞
|
||||
func (r *PostRepository) IsLiked(postID, userID string) bool {
|
||||
var count int64
|
||||
r.db.Model(&model.PostLike{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// Favorite 收藏帖子
|
||||
func (r *PostRepository) Favorite(postID, userID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查是否已经收藏
|
||||
var existing model.Favorite
|
||||
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&existing).Error
|
||||
if err == nil {
|
||||
// 已经收藏,直接返回
|
||||
return nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建收藏记录
|
||||
if err := tx.Create(&model.Favorite{
|
||||
PostID: postID,
|
||||
UserID: userID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 增加帖子收藏数
|
||||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||||
UpdateColumn("favorites_count", gorm.Expr("favorites_count + 1")).Error
|
||||
})
|
||||
}
|
||||
|
||||
// Unfavorite 取消收藏
|
||||
func (r *PostRepository) Unfavorite(postID, userID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Where("post_id = ? AND user_id = ?", postID, userID).Delete(&model.Favorite{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
// 减少帖子收藏数
|
||||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||||
UpdateColumn("favorites_count", gorm.Expr("favorites_count - 1")).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// IsFavorited 检查是否收藏
|
||||
func (r *PostRepository) IsFavorited(postID, userID string) bool {
|
||||
var count int64
|
||||
r.db.Model(&model.Favorite{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// IncrementViews 增加帖子观看量
|
||||
func (r *PostRepository) IncrementViews(postID string) error {
|
||||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||||
Updates(map[string]interface{}{
|
||||
"views_count": gorm.Expr("views_count + 1"),
|
||||
"hot_score": gorm.Expr("likes_count * 2 + comments_count * 3 + (views_count + 1) * 0.1"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// Search 搜索帖子
|
||||
func (r *PostRepository) Search(keyword string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished)
|
||||
|
||||
// 搜索标题和内容
|
||||
if keyword != "" {
|
||||
if r.db.Dialector.Name() == "postgres" {
|
||||
// PostgreSQL 使用全文检索表达式,为 pg_trgm/GIN 索引升级预留路径
|
||||
query = query.Where(
|
||||
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
|
||||
keyword,
|
||||
)
|
||||
} else {
|
||||
searchPattern := "%" + keyword + "%"
|
||||
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
|
||||
}
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Preload("User").Preload("Images").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&posts).Error
|
||||
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// GetFollowingPosts 获取关注用户的帖子
|
||||
func (r *PostRepository) GetFollowingPosts(userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
// 子查询:获取当前用户关注的所有用户ID
|
||||
subQuery := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
|
||||
|
||||
// 统计总数
|
||||
r.db.Model(&model.Post{}).Where("user_id IN (?) AND status = ?", subQuery, model.PostStatusPublished).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("user_id IN (?) AND status = ?", subQuery, model.PostStatusPublished).
|
||||
Preload("User").Preload("Images").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
Find(&posts).Error
|
||||
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// GetHotPosts 获取热门帖子(按点赞数和评论数排序)
|
||||
func (r *PostRepository) GetHotPosts(page, pageSize int) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
// 热门排序使用预计算热度分,避免每次请求进行表达式排序计算
|
||||
err := r.db.Where("status = ?", model.PostStatusPublished).Preload("User").Preload("Images").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Order("hot_score DESC, created_at DESC").
|
||||
Find(&posts).Error
|
||||
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// GetByIDs 根据ID列表获取帖子(保持传入顺序)
|
||||
func (r *PostRepository) GetByIDs(ids []string) ([]*model.Post, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*model.Post{}, nil
|
||||
}
|
||||
|
||||
var posts []*model.Post
|
||||
err := r.db.Preload("User").Preload("Images").
|
||||
Where("id IN ? AND status = ?", ids, model.PostStatusPublished).
|
||||
Find(&posts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 按传入ID顺序排序
|
||||
postMap := make(map[string]*model.Post)
|
||||
for _, post := range posts {
|
||||
postMap[post.ID] = post
|
||||
}
|
||||
|
||||
ordered := make([]*model.Post, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if post, ok := postMap[id]; ok {
|
||||
ordered = append(ordered, post)
|
||||
}
|
||||
}
|
||||
|
||||
return ordered, nil
|
||||
}
|
||||
172
internal/repository/push_repo.go
Normal file
172
internal/repository/push_repo.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PushRecordRepository 推送记录仓储
|
||||
type PushRecordRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewPushRecordRepository 创建推送记录仓储
|
||||
func NewPushRecordRepository(db *gorm.DB) *PushRecordRepository {
|
||||
return &PushRecordRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建推送记录
|
||||
func (r *PushRecordRepository) Create(record *model.PushRecord) error {
|
||||
return r.db.Create(record).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取推送记录
|
||||
func (r *PushRecordRepository) GetByID(id int64) (*model.PushRecord, error) {
|
||||
var record model.PushRecord
|
||||
err := r.db.First(&record, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// Update 更新推送记录
|
||||
func (r *PushRecordRepository) Update(record *model.PushRecord) error {
|
||||
return r.db.Save(record).Error
|
||||
}
|
||||
|
||||
// GetPendingPushes 获取待推送记录
|
||||
func (r *PushRecordRepository) GetPendingPushes(limit int) ([]*model.PushRecord, error) {
|
||||
var records []*model.PushRecord
|
||||
err := r.db.Where("push_status = ?", model.PushStatusPending).
|
||||
Where("expired_at IS NULL OR expired_at > ?", time.Now()).
|
||||
Order("created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&records).Error
|
||||
return records, err
|
||||
}
|
||||
|
||||
// GetByUserID 根据用户ID获取推送记录
|
||||
// userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致
|
||||
func (r *PushRecordRepository) GetByUserID(userID string, limit, offset int) ([]*model.PushRecord, error) {
|
||||
var records []*model.PushRecord
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Find(&records).Error
|
||||
return records, err
|
||||
}
|
||||
|
||||
// GetByMessageID 根据消息ID获取推送记录
|
||||
func (r *PushRecordRepository) GetByMessageID(messageID int64) ([]*model.PushRecord, error) {
|
||||
var records []*model.PushRecord
|
||||
err := r.db.Where("message_id = ?", messageID).
|
||||
Order("created_at DESC").
|
||||
Find(&records).Error
|
||||
return records, err
|
||||
}
|
||||
|
||||
// GetFailedPushesForRetry 获取失败待重试的推送
|
||||
func (r *PushRecordRepository) GetFailedPushesForRetry(limit int) ([]*model.PushRecord, error) {
|
||||
var records []*model.PushRecord
|
||||
err := r.db.Where("push_status = ?", model.PushStatusFailed).
|
||||
Where("retry_count < max_retry").
|
||||
Where("expired_at IS NULL OR expired_at > ?", time.Now()).
|
||||
Order("created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&records).Error
|
||||
return records, err
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建推送记录
|
||||
func (r *PushRecordRepository) BatchCreate(records []*model.PushRecord) error {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Create(&records).Error
|
||||
}
|
||||
|
||||
// BatchUpdateStatus 批量更新推送状态
|
||||
func (r *PushRecordRepository) BatchUpdateStatus(ids []int64, status model.PushStatus) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"push_status": status,
|
||||
}
|
||||
if status == model.PushStatusPushed {
|
||||
updates["pushed_at"] = time.Now()
|
||||
}
|
||||
return r.db.Model(&model.PushRecord{}).
|
||||
Where("id IN ?", ids).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
// UpdateStatus 更新单条记录状态
|
||||
func (r *PushRecordRepository) UpdateStatus(id int64, status model.PushStatus) error {
|
||||
updates := map[string]interface{}{
|
||||
"push_status": status,
|
||||
}
|
||||
if status == model.PushStatusPushed {
|
||||
updates["pushed_at"] = time.Now()
|
||||
}
|
||||
return r.db.Model(&model.PushRecord{}).
|
||||
Where("id = ?", id).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
// MarkAsFailed 标记为失败
|
||||
func (r *PushRecordRepository) MarkAsFailed(id int64, errMsg string) error {
|
||||
return r.db.Model(&model.PushRecord{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"push_status": model.PushStatusFailed,
|
||||
"error_message": errMsg,
|
||||
"retry_count": gorm.Expr("retry_count + 1"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// MarkAsDelivered 标记为已送达
|
||||
func (r *PushRecordRepository) MarkAsDelivered(id int64) error {
|
||||
return r.db.Model(&model.PushRecord{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"push_status": model.PushStatusDelivered,
|
||||
"delivered_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DeleteExpiredRecords 删除过期的推送记录(软删除)
|
||||
func (r *PushRecordRepository) DeleteExpiredRecords() error {
|
||||
return r.db.Where("expired_at IS NOT NULL AND expired_at < ?", time.Now()).
|
||||
Delete(&model.PushRecord{}).Error
|
||||
}
|
||||
|
||||
// GetStatsByUserID 获取用户推送统计
|
||||
func (r *PushRecordRepository) GetStatsByUserID(userID int64) (map[model.PushStatus]int64, error) {
|
||||
type statusCount struct {
|
||||
Status model.PushStatus
|
||||
Count int64
|
||||
}
|
||||
var results []statusCount
|
||||
|
||||
err := r.db.Model(&model.PushRecord{}).
|
||||
Select("push_status as status, count(*) as count").
|
||||
Where("user_id = ?", userID).
|
||||
Group("push_status").
|
||||
Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := make(map[model.PushStatus]int64)
|
||||
for _, r := range results {
|
||||
stats[r.Status] = r.Count
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
112
internal/repository/sticker_repo.go
Normal file
112
internal/repository/sticker_repo.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StickerRepository 自定义表情仓库接口
|
||||
type StickerRepository interface {
|
||||
// 获取用户的所有表情
|
||||
GetByUserID(userID string) ([]model.UserSticker, error)
|
||||
// 根据ID获取表情
|
||||
GetByID(id string) (*model.UserSticker, error)
|
||||
// 创建表情
|
||||
Create(sticker *model.UserSticker) error
|
||||
// 删除表情
|
||||
Delete(id string) error
|
||||
// 删除用户的所有表情
|
||||
DeleteByUserID(userID string) error
|
||||
// 检查表情是否存在
|
||||
Exists(userID string, url string) (bool, error)
|
||||
// 更新排序
|
||||
UpdateSortOrder(id string, sortOrder int) error
|
||||
// 批量更新排序
|
||||
BatchUpdateSortOrder(userID string, orders map[string]int) error
|
||||
// 获取用户表情数量
|
||||
CountByUserID(userID string) (int64, error)
|
||||
}
|
||||
|
||||
// stickerRepository 自定义表情仓库实现
|
||||
type stickerRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewStickerRepository 创建自定义表情仓库
|
||||
func NewStickerRepository(db *gorm.DB) StickerRepository {
|
||||
return &stickerRepository{db: db}
|
||||
}
|
||||
|
||||
// GetByUserID 获取用户的所有表情
|
||||
func (r *stickerRepository) GetByUserID(userID string) ([]model.UserSticker, error) {
|
||||
var stickers []model.UserSticker
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Order("sort_order ASC, created_at DESC").
|
||||
Find(&stickers).Error
|
||||
return stickers, err
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取表情
|
||||
func (r *stickerRepository) GetByID(id string) (*model.UserSticker, error) {
|
||||
var sticker model.UserSticker
|
||||
err := r.db.Where("id = ?", id).First(&sticker).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sticker, nil
|
||||
}
|
||||
|
||||
// Create 创建表情
|
||||
func (r *stickerRepository) Create(sticker *model.UserSticker) error {
|
||||
return r.db.Create(sticker).Error
|
||||
}
|
||||
|
||||
// Delete 删除表情
|
||||
func (r *stickerRepository) Delete(id string) error {
|
||||
return r.db.Where("id = ?", id).Delete(&model.UserSticker{}).Error
|
||||
}
|
||||
|
||||
// DeleteByUserID 删除用户的所有表情
|
||||
func (r *stickerRepository) DeleteByUserID(userID string) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&model.UserSticker{}).Error
|
||||
}
|
||||
|
||||
// Exists 检查表情是否存在
|
||||
func (r *stickerRepository) Exists(userID string, url string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserSticker{}).
|
||||
Where("user_id = ? AND url = ?", userID, url).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// UpdateSortOrder 更新排序
|
||||
func (r *stickerRepository) UpdateSortOrder(id string, sortOrder int) error {
|
||||
return r.db.Model(&model.UserSticker{}).
|
||||
Where("id = ?", id).
|
||||
Update("sort_order", sortOrder).Error
|
||||
}
|
||||
|
||||
// BatchUpdateSortOrder 批量更新排序
|
||||
func (r *stickerRepository) BatchUpdateSortOrder(userID string, orders map[string]int) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
for id, sortOrder := range orders {
|
||||
if err := tx.Model(&model.UserSticker{}).
|
||||
Where("id = ? AND user_id = ?", id, userID).
|
||||
Update("sort_order", sortOrder).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CountByUserID 获取用户表情数量
|
||||
func (r *stickerRepository) CountByUserID(userID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserSticker{}).
|
||||
Where("user_id = ?", userID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
114
internal/repository/system_notification_repo.go
Normal file
114
internal/repository/system_notification_repo.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SystemNotificationRepository 系统通知仓储
|
||||
type SystemNotificationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSystemNotificationRepository 创建系统通知仓储
|
||||
func NewSystemNotificationRepository(db *gorm.DB) *SystemNotificationRepository {
|
||||
return &SystemNotificationRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建系统通知
|
||||
func (r *SystemNotificationRepository) Create(notification *model.SystemNotification) error {
|
||||
return r.db.Create(notification).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取通知
|
||||
func (r *SystemNotificationRepository) GetByID(id int64) (*model.SystemNotification, error) {
|
||||
var notification model.SystemNotification
|
||||
err := r.db.First(¬ification, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬ification, nil
|
||||
}
|
||||
|
||||
// GetByReceiverID 获取用户的通知列表
|
||||
func (r *SystemNotificationRepository) GetByReceiverID(receiverID string, page, pageSize int) ([]*model.SystemNotification, int64, error) {
|
||||
var notifications []*model.SystemNotification
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.SystemNotification{}).Where("receiver_id = ?", receiverID)
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).
|
||||
Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
Find(¬ifications).Error
|
||||
|
||||
return notifications, total, err
|
||||
}
|
||||
|
||||
// GetUnreadByReceiverID 获取用户的未读通知列表
|
||||
func (r *SystemNotificationRepository) GetUnreadByReceiverID(receiverID string, limit int) ([]*model.SystemNotification, error) {
|
||||
var notifications []*model.SystemNotification
|
||||
err := r.db.Where("receiver_id = ? AND is_read = ?", receiverID, false).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(¬ifications).Error
|
||||
return notifications, err
|
||||
}
|
||||
|
||||
// GetUnreadCount 获取用户未读通知数量
|
||||
func (r *SystemNotificationRepository) GetUnreadCount(receiverID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.SystemNotification{}).
|
||||
Where("receiver_id = ? AND is_read = ?", receiverID, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// MarkAsRead 标记单条通知为已读
|
||||
func (r *SystemNotificationRepository) MarkAsRead(id int64, receiverID string) error {
|
||||
now := model.SystemNotification{}.UpdatedAt
|
||||
return r.db.Model(&model.SystemNotification{}).
|
||||
Where("id = ? AND receiver_id = ?", id, receiverID).
|
||||
Updates(map[string]interface{}{
|
||||
"is_read": true,
|
||||
"read_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// MarkAllAsRead 标记用户所有通知为已读
|
||||
func (r *SystemNotificationRepository) MarkAllAsRead(receiverID string) error {
|
||||
now := model.SystemNotification{}.UpdatedAt
|
||||
return r.db.Model(&model.SystemNotification{}).
|
||||
Where("receiver_id = ? AND is_read = ?", receiverID, false).
|
||||
Updates(map[string]interface{}{
|
||||
"is_read": true,
|
||||
"read_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// Delete 删除通知(软删除)
|
||||
func (r *SystemNotificationRepository) Delete(id int64, receiverID string) error {
|
||||
return r.db.Where("id = ? AND receiver_id = ?", id, receiverID).
|
||||
Delete(&model.SystemNotification{}).Error
|
||||
}
|
||||
|
||||
// GetByType 获取用户指定类型的通知
|
||||
func (r *SystemNotificationRepository) GetByType(receiverID string, notifyType model.SystemNotificationType, page, pageSize int) ([]*model.SystemNotification, int64, error) {
|
||||
var notifications []*model.SystemNotification
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.SystemNotification{}).
|
||||
Where("receiver_id = ? AND type = ?", receiverID, notifyType)
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).
|
||||
Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
Find(¬ifications).Error
|
||||
|
||||
return notifications, total, err
|
||||
}
|
||||
404
internal/repository/user_repo.go
Normal file
404
internal/repository/user_repo.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓储
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository 创建用户仓储
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *UserRepository) Create(user *model.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (r *UserRepository) GetByID(id string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.First(&user, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByUsername 根据用户名获取用户
|
||||
func (r *UserRepository) GetByUsername(username string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.First(&user, "username = ?", username).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmail 根据邮箱获取用户
|
||||
func (r *UserRepository) GetByEmail(email string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.First(&user, "email = ?", email).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByPhone 根据手机号获取用户
|
||||
func (r *UserRepository) GetByPhone(phone string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.First(&user, "phone = ?", phone).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *UserRepository) Update(user *model.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *UserRepository) Delete(id string) error {
|
||||
return r.db.Delete(&model.User{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// List 分页获取用户列表
|
||||
func (r *UserRepository) List(page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.User{}).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// GetFollowers 获取用户粉丝
|
||||
func (r *UserRepository) GetFollowers(userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
subQuery := r.db.Model(&model.Follow{}).Where("following_id = ?", userID).Select("follower_id")
|
||||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("id IN (?)", subQuery).
|
||||
Order("created_at DESC, id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// GetFollowing 获取用户关注
|
||||
func (r *UserRepository) GetFollowing(userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
subQuery := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
|
||||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("id IN (?)", subQuery).
|
||||
Order("created_at DESC, id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// CreateFollow 创建关注关系
|
||||
func (r *UserRepository) CreateFollow(follow *model.Follow) error {
|
||||
return r.db.Create(follow).Error
|
||||
}
|
||||
|
||||
// DeleteFollow 删除关注关系
|
||||
func (r *UserRepository) DeleteFollow(followerID, followingID string) error {
|
||||
return r.db.Where("follower_id = ? AND following_id = ?", followerID, followingID).Delete(&model.Follow{}).Error
|
||||
}
|
||||
|
||||
// IsFollowing 检查是否关注了某用户
|
||||
func (r *UserRepository) IsFollowing(followerID, followingID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Follow{}).Where("follower_id = ? AND following_id = ?", followerID, followingID).Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// IncrementFollowersCount 增加用户粉丝数
|
||||
func (r *UserRepository) IncrementFollowersCount(userID string) error {
|
||||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("followers_count", gorm.Expr("followers_count + 1")).Error
|
||||
}
|
||||
|
||||
// DecrementFollowersCount 减少用户粉丝数
|
||||
func (r *UserRepository) DecrementFollowersCount(userID string) error {
|
||||
return r.db.Model(&model.User{}).Where("id = ? AND followers_count > 0", userID).
|
||||
UpdateColumn("followers_count", gorm.Expr("followers_count - 1")).Error
|
||||
}
|
||||
|
||||
// IncrementFollowingCount 增加用户关注数
|
||||
func (r *UserRepository) IncrementFollowingCount(userID string) error {
|
||||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("following_count", gorm.Expr("following_count + 1")).Error
|
||||
}
|
||||
|
||||
// DecrementFollowingCount 减少用户关注数
|
||||
func (r *UserRepository) DecrementFollowingCount(userID string) error {
|
||||
return r.db.Model(&model.User{}).Where("id = ? AND following_count > 0", userID).
|
||||
UpdateColumn("following_count", gorm.Expr("following_count - 1")).Error
|
||||
}
|
||||
|
||||
// RefreshFollowersCount 刷新用户粉丝数(通过实际计数)
|
||||
func (r *UserRepository) RefreshFollowersCount(userID string) error {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Follow{}).Where("following_id = ?", userID).Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("followers_count", count).Error
|
||||
}
|
||||
|
||||
// GetPostsCount 获取用户帖子数(实时计算)
|
||||
func (r *UserRepository) GetPostsCount(userID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Post{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetPostsCountBatch 批量获取用户帖子数(实时计算)
|
||||
// 返回 map[userID]postsCount
|
||||
func (r *UserRepository) GetPostsCountBatch(userIDs []string) (map[string]int64, error) {
|
||||
result := make(map[string]int64)
|
||||
if len(userIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 初始化所有用户ID的计数为0
|
||||
for _, userID := range userIDs {
|
||||
result[userID] = 0
|
||||
}
|
||||
|
||||
// 使用 GROUP BY 一次性查询所有用户的帖子数
|
||||
type CountResult struct {
|
||||
UserID string
|
||||
Count int64
|
||||
}
|
||||
var counts []CountResult
|
||||
err := r.db.Model(&model.Post{}).
|
||||
Select("user_id, count(*) as count").
|
||||
Where("user_id IN ?", userIDs).
|
||||
Group("user_id").
|
||||
Scan(&counts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新查询结果
|
||||
for _, c := range counts {
|
||||
result[c.UserID] = c.Count
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RefreshFollowingCount 刷新用户关注数(通过实际计数)
|
||||
func (r *UserRepository) RefreshFollowingCount(userID string) error {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("following_count", count).Error
|
||||
}
|
||||
|
||||
// IsBlocked 检查拉黑关系是否存在(blocker -> blocked)
|
||||
func (r *UserRepository) IsBlocked(blockerID, blockedID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserBlock{}).
|
||||
Where("blocker_id = ? AND blocked_id = ?", blockerID, blockedID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// IsBlockedEitherDirection 检查是否任一方向存在拉黑
|
||||
func (r *UserRepository) IsBlockedEitherDirection(userA, userB string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserBlock{}).
|
||||
Where("(blocker_id = ? AND blocked_id = ?) OR (blocker_id = ? AND blocked_id = ?)",
|
||||
userA, userB, userB, userA).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// BlockUserAndCleanupRelations 拉黑用户并清理双向关注关系(事务)
|
||||
func (r *UserRepository) BlockUserAndCleanupRelations(blockerID, blockedID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
block := &model.UserBlock{
|
||||
BlockerID: blockerID,
|
||||
BlockedID: blockedID,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "blocker_id"}, {Name: "blocked_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(block).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Where("follower_id = ? AND following_id = ?", blockerID, blockedID).
|
||||
Delete(&model.Follow{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("follower_id = ? AND following_id = ?", blockedID, blockerID).
|
||||
Delete(&model.Follow{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, uid := range []string{blockerID, blockedID} {
|
||||
var followersCount int64
|
||||
if err := tx.Model(&model.Follow{}).Where("following_id = ?", uid).Count(&followersCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", uid).
|
||||
UpdateColumn("followers_count", followersCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var followingCount int64
|
||||
if err := tx.Model(&model.Follow{}).Where("follower_id = ?", uid).Count(&followingCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", uid).
|
||||
UpdateColumn("following_count", followingCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UnblockUser 取消拉黑
|
||||
func (r *UserRepository) UnblockUser(blockerID, blockedID string) error {
|
||||
return r.db.Where("blocker_id = ? AND blocked_id = ?", blockerID, blockedID).
|
||||
Delete(&model.UserBlock{}).Error
|
||||
}
|
||||
|
||||
// GetBlockedUsers 获取用户黑名单列表
|
||||
func (r *UserRepository) GetBlockedUsers(blockerID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
subQuery := r.db.Model(&model.UserBlock{}).Where("blocker_id = ?", blockerID).Select("blocked_id")
|
||||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("id IN (?)", subQuery).
|
||||
Order("created_at DESC, id DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// Search 搜索用户
|
||||
func (r *UserRepository) Search(keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.User{})
|
||||
|
||||
// 搜索用户名、昵称、简介
|
||||
if keyword != "" {
|
||||
if r.db.Dialector.Name() == "postgres" {
|
||||
query = query.Where(
|
||||
"to_tsvector('simple', COALESCE(username, '') || ' ' || COALESCE(nickname, '') || ' ' || COALESCE(bio, '')) @@ plainto_tsquery('simple', ?)",
|
||||
keyword,
|
||||
)
|
||||
} else {
|
||||
searchPattern := "%" + keyword + "%"
|
||||
query = query.Where("username LIKE ? OR nickname LIKE ? OR bio LIKE ?", searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// GetMutualFollowStatus 批量获取双向关注状态
|
||||
// 返回 map[userID][isFollowing, isFollowingMe]
|
||||
func (r *UserRepository) GetMutualFollowStatus(currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
|
||||
result := make(map[string][2]bool)
|
||||
|
||||
if len(targetUserIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] GetMutualFollowStatus: currentUserID=%s, targetUserIDs=%v\n", currentUserID, targetUserIDs)
|
||||
|
||||
// 初始化所有目标用户为未关注状态
|
||||
for _, userID := range targetUserIDs {
|
||||
result[userID] = [2]bool{false, false}
|
||||
}
|
||||
|
||||
// 查询当前用户关注了哪些目标用户 (isFollowing)
|
||||
var followingIDs []string
|
||||
err := r.db.Model(&model.Follow{}).
|
||||
Where("follower_id = ? AND following_id IN ?", currentUserID, targetUserIDs).
|
||||
Pluck("following_id", &followingIDs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("[DEBUG] GetMutualFollowStatus: currentUser follows these targets: %v\n", followingIDs)
|
||||
for _, id := range followingIDs {
|
||||
status := result[id]
|
||||
status[0] = true
|
||||
result[id] = status
|
||||
}
|
||||
|
||||
// 查询哪些目标用户关注了当前用户 (isFollowingMe)
|
||||
var followerIDs []string
|
||||
err = r.db.Model(&model.Follow{}).
|
||||
Where("follower_id IN ? AND following_id = ?", targetUserIDs, currentUserID).
|
||||
Pluck("follower_id", &followerIDs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("[DEBUG] GetMutualFollowStatus: these targets follow currentUser: %v\n", followerIDs)
|
||||
for _, id := range followerIDs {
|
||||
status := result[id]
|
||||
status[1] = true
|
||||
result[id] = status
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] GetMutualFollowStatus: final result=%v\n", result)
|
||||
return result, nil
|
||||
}
|
||||
141
internal/repository/vote_repo.go
Normal file
141
internal/repository/vote_repo.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// VoteRepository 投票仓储
|
||||
type VoteRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewVoteRepository 创建投票仓储
|
||||
func NewVoteRepository(db *gorm.DB) *VoteRepository {
|
||||
return &VoteRepository{db: db}
|
||||
}
|
||||
|
||||
// CreateOptions 批量创建投票选项
|
||||
func (r *VoteRepository) CreateOptions(postID string, options []string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
for i, content := range options {
|
||||
option := &model.VoteOption{
|
||||
PostID: postID,
|
||||
Content: content,
|
||||
SortOrder: i,
|
||||
}
|
||||
if err := tx.Create(option).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetOptionsByPostID 获取帖子的所有投票选项
|
||||
func (r *VoteRepository) GetOptionsByPostID(postID string) ([]model.VoteOption, error) {
|
||||
var options []model.VoteOption
|
||||
err := r.db.Where("post_id = ?", postID).Order("sort_order ASC").Find(&options).Error
|
||||
return options, err
|
||||
}
|
||||
|
||||
// Vote 用户投票
|
||||
func (r *VoteRepository) Vote(postID, userID, optionID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查用户是否已投票
|
||||
var existing model.UserVote
|
||||
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&existing).Error
|
||||
if err == nil {
|
||||
// 已经投票,返回错误
|
||||
return errors.New("user already voted")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证选项是否属于该帖子
|
||||
var option model.VoteOption
|
||||
if err := tx.Where("id = ? AND post_id = ?", optionID, postID).First(&option).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New("invalid option")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建投票记录
|
||||
if err := tx.Create(&model.UserVote{
|
||||
PostID: postID,
|
||||
UserID: userID,
|
||||
OptionID: optionID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 原子增加选项投票数
|
||||
return tx.Model(&model.VoteOption{}).Where("id = ?", optionID).
|
||||
UpdateColumn("votes_count", gorm.Expr("votes_count + 1")).Error
|
||||
})
|
||||
}
|
||||
|
||||
// Unvote 取消投票
|
||||
func (r *VoteRepository) Unvote(postID, userID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 获取用户的投票记录
|
||||
var userVote model.UserVote
|
||||
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil // 没有投票记录,直接返回
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除投票记录
|
||||
result := tx.Where("post_id = ? AND user_id = ?", postID, userID).Delete(&model.UserVote{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
// 原子减少选项投票数
|
||||
return tx.Model(&model.VoteOption{}).Where("id = ?", userVote.OptionID).
|
||||
UpdateColumn("votes_count", gorm.Expr("votes_count - 1")).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserVote 获取用户在指定帖子的投票
|
||||
func (r *VoteRepository) GetUserVote(postID, userID string) (*model.UserVote, error) {
|
||||
var userVote model.UserVote
|
||||
err := r.db.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &userVote, nil
|
||||
}
|
||||
|
||||
// UpdateOption 更新选项内容
|
||||
func (r *VoteRepository) UpdateOption(optionID, content string) error {
|
||||
return r.db.Model(&model.VoteOption{}).Where("id = ?", optionID).
|
||||
Update("content", content).Error
|
||||
}
|
||||
|
||||
// DeleteOptionsByPostID 删除帖子的所有投票选项
|
||||
func (r *VoteRepository) DeleteOptionsByPostID(postID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 删除关联的用户投票记录
|
||||
if err := tx.Where("post_id = ?", postID).Delete(&model.UserVote{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除投票选项
|
||||
return tx.Where("post_id = ?", postID).Delete(&model.VoteOption{}).Error
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user