- 新增收藏夹系统 (Folder/FolderItem 模型、CRUD API、前端管理页) - 新增文章详情页收藏按钮 (书签图标、ToggleFavorite 一键收藏) - 新增 Studio 趋势图 (赋能值/评论/点赞/收藏 4 线图, 7d/30d/90d) - 新增通知偏好设置页 (评论/回复/点赞/关注 4 类开关) - 后端通知列表支持按偏好过滤 (排除已关闭的通知类型) - 下载 Chart.js 到本地静态资源 (static/js/chart.umd.min.js) - 补充收藏夹卡片、开关滑块、趋势图网格等 CSS 样式
305 lines
9.0 KiB
Go
305 lines
9.0 KiB
Go
package repository
|
||
|
||
import (
|
||
"metazone.cc/metalab/internal/model"
|
||
"metazone.cc/metalab/internal/service"
|
||
"strconv"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// CommentRepo 评论数据访问
|
||
type CommentRepo struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewCommentRepo 构造函数
|
||
func NewCommentRepo(db *gorm.DB) *CommentRepo {
|
||
return &CommentRepo{db: db}
|
||
}
|
||
|
||
// Create 创建评论
|
||
func (r *CommentRepo) Create(comment *model.Comment) error {
|
||
return r.db.Create(comment).Error
|
||
}
|
||
|
||
// FindByID 按 ID 查找评论(含作者信息)
|
||
func (r *CommentRepo) FindByID(id uint) (*model.Comment, error) {
|
||
var c model.Comment
|
||
err := r.db.Table("comments").
|
||
Select("comments.*, users.username as author_name, users.avatar as author_avatar").
|
||
Joins("LEFT JOIN users ON users.uid = comments.user_id").
|
||
Where("comments.id = ?", id).
|
||
First(&c).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &c, nil
|
||
}
|
||
|
||
// FindRootComments 分页查询文章的顶级评论(parent_id IS NULL,未删除)
|
||
func (r *CommentRepo) FindRootComments(postID uint, offset, limit int) ([]model.Comment, int64, error) {
|
||
var total int64
|
||
base := r.db.Table("comments").
|
||
Where("post_id = ? AND parent_id IS NULL AND is_deleted = false", postID)
|
||
|
||
if err := base.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
var list []model.Comment
|
||
err := base.
|
||
Select("comments.*, users.username as author_name, users.avatar as author_avatar").
|
||
Joins("LEFT JOIN users ON users.uid = comments.user_id").
|
||
Order("comments.created_at DESC").
|
||
Offset(offset).Limit(limit).
|
||
Find(&list).Error
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 为每条顶级评论填充回复数
|
||
if len(list) > 0 {
|
||
ids := make([]uint, len(list))
|
||
for i, c := range list {
|
||
ids[i] = c.ID
|
||
}
|
||
countMap := r.batchCountReplies(ids)
|
||
for i := range list {
|
||
list[i].RepliesCount = countMap[list[i].ID]
|
||
}
|
||
}
|
||
|
||
return list, total, nil
|
||
}
|
||
|
||
// FindReplies 查询某条顶级评论的所有回复(不分页,全量返回,未删除)
|
||
func (r *CommentRepo) FindReplies(rootID uint) ([]model.Comment, error) {
|
||
var list []model.Comment
|
||
err := r.db.Table("comments").
|
||
Select("comments.*, users.username as author_name, users.avatar as author_avatar").
|
||
Joins("LEFT JOIN users ON users.uid = comments.user_id").
|
||
Where("comments.root_id = ? AND comments.parent_id IS NOT NULL AND comments.is_deleted = false", rootID).
|
||
Order("comments.created_at ASC").
|
||
Find(&list).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 填充 reply_to_name
|
||
r.fillReplyToNames(list)
|
||
|
||
return list, nil
|
||
}
|
||
|
||
// fillReplyToNames 通过 JOIN users 获取被回复者当前显示名
|
||
func (r *CommentRepo) fillReplyToNames(list []model.Comment) {
|
||
if len(list) == 0 {
|
||
return
|
||
}
|
||
type nameRow struct {
|
||
UID string
|
||
Username string
|
||
}
|
||
var rows []nameRow
|
||
r.db.Table("users").
|
||
Select("uid, username").
|
||
Where("uid IN (SELECT reply_to_uid FROM comments WHERE root_id IN (?) AND parent_id IS NOT NULL)", r.rootIDsFromList(list)).
|
||
Find(&rows)
|
||
|
||
m := make(map[string]string)
|
||
for _, row := range rows {
|
||
m[row.UID] = row.Username
|
||
}
|
||
for i := range list {
|
||
if list[i].ReplyToUID != "" {
|
||
list[i].ReplyToName = m[list[i].ReplyToUID]
|
||
}
|
||
}
|
||
}
|
||
|
||
func (r *CommentRepo) rootIDsFromList(list []model.Comment) []uint {
|
||
ids := make([]uint, len(list))
|
||
for i, c := range list {
|
||
ids[i] = c.RootID
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// batchCountReplies 批量统计回复数
|
||
func (r *CommentRepo) batchCountReplies(rootIDs []uint) map[uint]int {
|
||
type countRow struct {
|
||
RootID uint
|
||
Count int
|
||
}
|
||
var rows []countRow
|
||
r.db.Table("comments").
|
||
Select("root_id, COUNT(*) as count").
|
||
Where("root_id IN ? AND parent_id IS NOT NULL AND is_deleted = false", rootIDs).
|
||
Group("root_id").
|
||
Find(&rows)
|
||
|
||
m := make(map[uint]int)
|
||
for _, row := range rows {
|
||
m[row.RootID] = row.Count
|
||
}
|
||
return m
|
||
}
|
||
|
||
// SoftDelete 软删除评论(设置 is_deleted=true)
|
||
func (r *CommentRepo) SoftDelete(id uint) error {
|
||
return r.db.Model(&model.Comment{}).Where("id = ?", id).Update("is_deleted", true).Error
|
||
}
|
||
|
||
// IncrCommentsCount 文章评论数+1
|
||
func (r *CommentRepo) IncrCommentsCount(postID uint) error {
|
||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumn("comments_count", gorm.Expr("comments_count + 1")).Error
|
||
}
|
||
|
||
// DecrCommentsCount 文章评论数-1
|
||
func (r *CommentRepo) DecrCommentsCount(postID uint) error {
|
||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumn("comments_count", gorm.Expr("GREATEST(comments_count - 1, 0)")).Error
|
||
}
|
||
|
||
// CountRepliesByRootByStatus 统计某条顶级评论的回复数(可选是否统计已删除)
|
||
func (r *CommentRepo) CountRepliesByRoot(rootID uint, includeDeleted bool) (int, error) {
|
||
var count int64
|
||
q := r.db.Model(&model.Comment{}).Where("root_id = ? AND parent_id IS NOT NULL", rootID)
|
||
if !includeDeleted {
|
||
q = q.Where("is_deleted = false")
|
||
}
|
||
err := q.Count(&count).Error
|
||
return int(count), err
|
||
}
|
||
|
||
// CreateMention 创建@提及记录
|
||
func (r *CommentRepo) CreateMention(mention *model.CommentMention) error {
|
||
return r.db.Create(mention).Error
|
||
}
|
||
|
||
// FindMentionsByComment 查询某条评论的@提及列表
|
||
func (r *CommentRepo) FindMentionsByComment(commentID uint) ([]model.CommentMention, error) {
|
||
var list []model.CommentMention
|
||
err := r.db.Where("comment_id = ?", commentID).Find(&list).Error
|
||
return list, err
|
||
}
|
||
|
||
// SearchUsersByLevel 搜索指定等级及以上用户(用于@艾特),返回 uid/username/exp
|
||
func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int) ([]struct {
|
||
UID string
|
||
Username string
|
||
Exp int
|
||
}, error) {
|
||
type result struct {
|
||
UID uint
|
||
Username string
|
||
Exp int
|
||
}
|
||
var list []result
|
||
|
||
threshold := model.LevelThresholds[minLevel]
|
||
|
||
err := r.db.Table("users").
|
||
Select("uid, username, exp").
|
||
Where("username LIKE ? AND exp >= ? AND deleted_at IS NULL", "%"+keyword+"%", threshold).
|
||
Order("exp DESC").
|
||
Limit(limit).
|
||
Find(&list).Error
|
||
|
||
var res []struct {
|
||
UID string
|
||
Username string
|
||
Exp int
|
||
}
|
||
for _, u := range list {
|
||
res = append(res, struct {
|
||
UID string
|
||
Username string
|
||
Exp int
|
||
}{UID: strconv.FormatUint(uint64(u.UID), 10), Username: u.Username, Exp: u.Exp})
|
||
}
|
||
return res, err
|
||
}
|
||
|
||
// GetUserUIDByID 通过 uint ID 获取 user 的 UID 字符串
|
||
func (r *CommentRepo) GetUserUIDByID(userID uint) (string, error) {
|
||
var uid uint
|
||
err := r.db.Table("users").Select("uid").Where("uid = ?", userID).Scan(&uid).Error
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return strconv.FormatUint(uint64(uid), 10), nil
|
||
}
|
||
|
||
// GetUsernameByID 通过 userID 获取用户名(用于通知内容)
|
||
func (r *CommentRepo) GetUsernameByID(userID uint) (string, error) {
|
||
var username string
|
||
err := r.db.Table("users").Select("username").Where("uid = ?", userID).Scan(&username).Error
|
||
return username, err
|
||
}
|
||
|
||
// FindPostIDByComment 查询评论所属的文章ID
|
||
func (r *CommentRepo) FindPostIDByComment(commentID uint) (uint, error) {
|
||
var postID uint
|
||
err := r.db.Table("comments").Select("post_id").Where("id = ?", commentID).Scan(&postID).Error
|
||
return postID, err
|
||
}
|
||
|
||
// UpdateRootID 更新评论的 root_id 字段(顶级评论创建后回填)
|
||
func (r *CommentRepo) UpdateRootID(id, rootID uint) error {
|
||
return r.db.Model(&model.Comment{}).Where("id = ?", id).Update("root_id", rootID).Error
|
||
}
|
||
|
||
// FindPostAuthorID 查询文章的作者 user_id
|
||
func (r *CommentRepo) FindPostAuthorID(postID uint) (uint, error) {
|
||
var authorID uint
|
||
err := r.db.Table("posts").Select("user_id").Where("id = ?", postID).Scan(&authorID).Error
|
||
return authorID, err
|
||
}
|
||
|
||
// AggregateCommentsByAuthor 按日期聚合评论量(作者所有文章在时间段内的评论)
|
||
func (r *CommentRepo) AggregateCommentsByAuthor(userID uint, since string) ([]service.TrendPoint, error) {
|
||
var results []service.TrendPoint
|
||
err := r.db.Table("comments").
|
||
Select("TO_CHAR(comments.created_at, 'YYYY-MM-DD') AS date, COUNT(*) AS value").
|
||
Joins("JOIN posts ON posts.id = comments.post_id").
|
||
Where("posts.user_id = ? AND comments.created_at >= ?", userID, since).
|
||
Group("TO_CHAR(comments.created_at, 'YYYY-MM-DD')").
|
||
Order("date ASC").
|
||
Scan(&results).Error
|
||
if results == nil {
|
||
results = []service.TrendPoint{}
|
||
}
|
||
return results, err
|
||
}
|
||
|
||
// ListAllComments 后台评论列表(支持关键词搜索、分页)
|
||
func (r *CommentRepo) ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error) {
|
||
var total int64
|
||
q := r.db.Table("comments").
|
||
Joins("LEFT JOIN users ON users.uid = comments.user_id").
|
||
Joins("LEFT JOIN posts ON posts.id = comments.post_id")
|
||
|
||
if !showDeleted {
|
||
q = q.Where("comments.is_deleted = false")
|
||
}
|
||
if keyword != "" {
|
||
like := "%" + keyword + "%"
|
||
q = q.Where("comments.body LIKE ? OR users.username LIKE ? OR posts.title LIKE ?", like, like, like)
|
||
}
|
||
|
||
if err := q.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
var rows []model.AdminCommentRow
|
||
err := q.
|
||
Select("comments.id, comments.post_id, posts.title as post_title, comments.body, users.username as author_name, comments.is_deleted, comments.created_at, comments.updated_at").
|
||
Order("comments.created_at DESC").
|
||
Offset(offset).Limit(limit).
|
||
Scan(&rows).Error
|
||
return rows, total, err
|
||
}
|