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:
2026-03-09 21:28:58 +08:00
commit 4d8f2ec997
102 changed files with 25022 additions and 0 deletions

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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(&notification, "id = ?", id).Error
if err != nil {
return nil, err
}
return &notification, 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(&notifications).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
}

View 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
}

View 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
}

View 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
}

View 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(&notification, "id = ?", id).Error
if err != nil {
return nil, err
}
return &notification, 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(&notifications).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(&notifications).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(&notifications).Error
return notifications, total, err
}

View 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
}

View 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
})
}