mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
⚡ add like functionality with Like model, implement like/unlike methods, and update Post and Comment models to track like counts
This commit is contained in:
8
internal/dto/label.go
Normal file
8
internal/dto/label.go
Normal file
@ -0,0 +1,8 @@
|
||||
package dto
|
||||
|
||||
type LabelDto struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Color string `json:"color"`
|
||||
TailwindClassName string `json:"tailwind_class_name"`
|
||||
}
|
16
internal/dto/post.go
Normal file
16
internal/dto/post.go
Normal file
@ -0,0 +1,16 @@
|
||||
package dto
|
||||
|
||||
type PostDto struct {
|
||||
UserID uint `json:"user_id"` // 发布者的用户ID
|
||||
Title string `json:"title"` // 帖子标题
|
||||
Content string `json:"content"`
|
||||
Labels []LabelDto `json:"labels"` // 关联的标签
|
||||
IsPrivate bool `json:"is_private"` // 是否为私密帖子
|
||||
}
|
||||
|
||||
type CreatePostReq struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Labels []LabelDto `json:"labels"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
}
|
@ -11,11 +11,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func UseAuth() app.HandlerFunc {
|
||||
func UseAuth(block bool) app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
// For cookie
|
||||
token := string(c.Cookie("token"))
|
||||
refreshToken := string(c.Cookie("refresh_token"))
|
||||
|
||||
// 尝试用普通 token 认证
|
||||
tokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(token)
|
||||
if err == nil && tokenClaims != nil {
|
||||
ctx = context.WithValue(ctx, "user_id", tokenClaims.UserID)
|
||||
@ -23,27 +25,41 @@ func UseAuth() app.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// token 失效 使用refresh token重新签发和鉴权
|
||||
// token 失效 使用 refresh token 重新签发和鉴权
|
||||
refreshTokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(refreshToken)
|
||||
if err == nil && refreshTokenClaims != nil {
|
||||
ok, err := isStatefulJwtValid(refreshTokenClaims)
|
||||
if err == nil && ok {
|
||||
ctx = context.WithValue(ctx, "user_id", refreshTokenClaims.UserID) // 修改这里,使用refreshTokenClaims
|
||||
c.Next(ctx)
|
||||
newTokenClaims := utils.Jwt.NewClaims(refreshTokenClaims.UserID, refreshTokenClaims.SessionKey, refreshTokenClaims.Stateful, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, 30)*int(time.Hour)))
|
||||
ctx = context.WithValue(ctx, "user_id", refreshTokenClaims.UserID)
|
||||
|
||||
// 生成新 token
|
||||
newTokenClaims := utils.Jwt.NewClaims(
|
||||
refreshTokenClaims.UserID,
|
||||
refreshTokenClaims.SessionKey,
|
||||
refreshTokenClaims.Stateful,
|
||||
time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, 30)*int(time.Hour)),
|
||||
)
|
||||
newToken, err := newTokenClaims.ToString()
|
||||
if err == nil {
|
||||
ctxutils.SetTokenCookie(c, newToken)
|
||||
} else {
|
||||
resps.InternalServerError(c, resps.ErrInternalServerError)
|
||||
}
|
||||
|
||||
c.Next(ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 所有认证方式都失败,返回未授权错误
|
||||
resps.UnAuthorized(c, resps.ErrUnauthorized)
|
||||
c.Abort()
|
||||
// 所有认证方式都失败
|
||||
if block {
|
||||
// 若需要阻断,返回未授权错误并中止请求
|
||||
resps.UnAuthorized(c, resps.ErrUnauthorized)
|
||||
c.Abort()
|
||||
} else {
|
||||
// 若不需要阻断,继续请求但不设置用户ID
|
||||
c.Next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,13 @@ import "gorm.io/gorm"
|
||||
|
||||
type Comment struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"index"` // 评论的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
TargetID uint `gorm:"index"` // 目标ID
|
||||
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
|
||||
ReplyID uint `gorm:"index"` // 回复的评论ID
|
||||
Content string `gorm:"type:text"` // 评论内容
|
||||
Depth int `gorm:"default:0"` // 评论的层级深度
|
||||
UserID uint `gorm:"index"` // 评论的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
TargetID uint `gorm:"index"` // 目标ID
|
||||
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
|
||||
ReplyID uint `gorm:"index"` // 回复的评论ID
|
||||
Content string `gorm:"type:text"` // 评论内容
|
||||
Depth int `gorm:"default:0"` // 评论的层级深度
|
||||
LikeCount uint64
|
||||
CommentCount uint64
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import "gorm.io/gorm"
|
||||
|
||||
type Label struct {
|
||||
gorm.Model
|
||||
Key string `gorm:"uniqueIndex"` // 标签键,唯一标识
|
||||
Value string `gorm:"type:text"` // 标签值,描述标签的内容
|
||||
Color string `gorm:"type:text"` // 前端可用颜色代码
|
||||
Key string `gorm:"uniqueIndex"` // 标签键,唯一标识
|
||||
Value string `gorm:"type:text"` // 标签值,描述标签的内容
|
||||
Color string `gorm:"type:text"` // 前端可用颜色代码
|
||||
TailwindClassName string `gorm:"type:text"` // Tailwind CSS 的类名,用于前端样式
|
||||
}
|
||||
|
42
internal/model/like.go
Normal file
42
internal/model/like.go
Normal file
@ -0,0 +1,42 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Like struct {
|
||||
gorm.Model
|
||||
TargetType string
|
||||
TargetID uint
|
||||
UserID uint
|
||||
}
|
||||
|
||||
// AfterCreate 点赞后更新被点赞对象的计数
|
||||
func (l *Like) AfterCreate(tx *gorm.DB) (err error) {
|
||||
switch l.TargetType {
|
||||
case constant.TargetTypePost:
|
||||
return tx.Model(&Post{}).Where("id = ?", l.TargetID).
|
||||
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1)).Error
|
||||
case constant.TargetTypeComment:
|
||||
return tx.Model(&Comment{}).Where("id = ?", l.TargetID).
|
||||
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1)).Error
|
||||
default:
|
||||
return fmt.Errorf("不支持的目标类型: %s", l.TargetType)
|
||||
}
|
||||
}
|
||||
|
||||
// AfterDelete 取消点赞后更新被点赞对象的计数
|
||||
func (l *Like) AfterDelete(tx *gorm.DB) (err error) {
|
||||
switch l.TargetType {
|
||||
case constant.TargetTypePost:
|
||||
return tx.Model(&Post{}).Where("id = ?", l.TargetID).
|
||||
UpdateColumn("like_count", gorm.Expr("GREATEST(like_count - ?, 0)", 1)).Error
|
||||
case constant.TargetTypeComment:
|
||||
return tx.Model(&Comment{}).Where("id = ?", l.TargetID).
|
||||
UpdateColumn("like_count", gorm.Expr("GREATEST(like_count - ?, 0)", 1)).Error
|
||||
default:
|
||||
return fmt.Errorf("不支持的目标类型: %s", l.TargetType)
|
||||
}
|
||||
}
|
@ -4,9 +4,13 @@ import "gorm.io/gorm"
|
||||
|
||||
type Post struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"index"` // 发布者的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
Title string `gorm:"type:text;not null"` // 帖子标题
|
||||
Content string `gorm:"type:text;not null"` // 帖子内容
|
||||
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
||||
UserID uint `gorm:"index"` // 发布者的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
Title string `gorm:"type:text;not null"` // 帖子标题
|
||||
Content string `gorm:"type:text;not null"` // 帖子内容
|
||||
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
||||
IsPrivate bool `gorm:"default:false"` // 是否为私密帖子
|
||||
LikeCount uint64
|
||||
CommentCount uint64
|
||||
VisitorCount uint64
|
||||
}
|
||||
|
@ -126,8 +126,10 @@ func migrate() error {
|
||||
return GetDB().AutoMigrate(
|
||||
&model.Comment{},
|
||||
&model.Label{},
|
||||
&model.Like{},
|
||||
&model.OidcConfig{},
|
||||
&model.Post{},
|
||||
&model.Session{},
|
||||
&model.User{})
|
||||
&model.User{},
|
||||
&model.UserOpenID{})
|
||||
}
|
||||
|
86
internal/repo/like.go
Normal file
86
internal/repo/like.go
Normal file
@ -0,0 +1,86 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type likeRepo struct{}
|
||||
|
||||
var Like = &likeRepo{}
|
||||
|
||||
// Like 用户点赞,幂等
|
||||
func (l *likeRepo) Like(userID, targetID uint, targetType string) error {
|
||||
err := l.checkTargetType(targetType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var existingLike model.Like
|
||||
err = GetDB().Where("target_type = ? AND target_id = ? AND user_id = ?", targetType, targetID, userID).First(&existingLike).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
like := &model.Like{
|
||||
TargetType: targetType,
|
||||
TargetID: targetID,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
return GetDB().Create(like).Error
|
||||
}
|
||||
|
||||
// UnLike 取消点赞
|
||||
func (l *likeRepo) UnLike(userID, targetID uint, targetType string) error {
|
||||
err := l.checkTargetType(targetType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return GetDB().Where("target_type = ? AND target_id = ? AND user_id = ?",
|
||||
targetType, targetID, userID).Delete(&model.Like{}).Error
|
||||
}
|
||||
|
||||
// IsLiked 检查是否点赞
|
||||
func (l *likeRepo) IsLiked(userID, targetID uint, targetType string) (bool, error) {
|
||||
err := l.checkTargetType(targetType)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var like model.Like
|
||||
err = GetDB().Where("target_type = ? AND target_id = ? AND user_id = ?", targetType, targetID, userID).First(&like).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Count 点赞计数
|
||||
func (l *likeRepo) Count(targetID uint, targetType string) (int64, error) {
|
||||
err := l.checkTargetType(targetType)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var count int64
|
||||
err = GetDB().Model(&model.Like{}).Where("target_type = ? AND target_id = ?", targetType, targetID).Count(&count).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (l *likeRepo) checkTargetType(targetType string) error {
|
||||
switch targetType {
|
||||
case constant.TargetTypePost, constant.TargetTypeComment:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid target type")
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func registerLabelRoutes(group *route.RouterGroup) {
|
||||
labelGroup := group.Group("/label").Use(middleware.UseAuth())
|
||||
labelGroupWithoutAuth := group.Group("/label")
|
||||
labelGroup := group.Group("/label").Use(middleware.UseAuth(true))
|
||||
labelGroupWithoutAuth := group.Group("/label").Use(middleware.UseAuth(false))
|
||||
{
|
||||
labelGroupWithoutAuth.GET("/l/:id", v1.Label.Get)
|
||||
labelGroupWithoutAuth.GET("/list", v1.Label.List)
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
// page 页面API路由
|
||||
|
||||
func registerPageRoutes(group *route.RouterGroup) {
|
||||
postGroup := group.Group("/page").Use(middleware.UseAuth())
|
||||
postGroupWithoutAuth := group.Group("/page")
|
||||
postGroup := group.Group("/page").Use(middleware.UseAuth(true))
|
||||
postGroupWithoutAuth := group.Group("/page").Use(middleware.UseAuth(false))
|
||||
{
|
||||
postGroupWithoutAuth.GET("/p/:id", v1.Page.Get)
|
||||
postGroupWithoutAuth.GET("/list", v1.Page.List)
|
||||
|
@ -9,12 +9,11 @@ import (
|
||||
// post 文章API路由
|
||||
|
||||
func registerPostRoutes(group *route.RouterGroup) {
|
||||
postGroup := group.Group("/post").Use(middleware.UseAuth())
|
||||
postGroupWithoutAuth := group.Group("/post")
|
||||
postGroup := group.Group("/post").Use(middleware.UseAuth(true))
|
||||
postGroupWithoutAuth := group.Group("/post").Use(middleware.UseAuth(false))
|
||||
{
|
||||
postGroupWithoutAuth.GET("/p/:id", v1.Post.Get)
|
||||
postGroupWithoutAuth.GET("/list", v1.Post.List)
|
||||
|
||||
postGroup.POST("/p", v1.Post.Create)
|
||||
postGroup.PUT("/p", v1.Post.Update)
|
||||
postGroup.DELETE("/p", v1.Post.Delete)
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func registerUserRoutes(group *route.RouterGroup) {
|
||||
userGroup := group.Group("/user").Use(middleware.UseAuth())
|
||||
userGroup := group.Group("/user").Use(middleware.UseAuth(true))
|
||||
userGroupWithoutAuth := group.Group("/user")
|
||||
userGroupWithoutAuthNeedsCaptcha := userGroupWithoutAuth.Use(middleware.UseCaptcha())
|
||||
{
|
||||
|
Reference in New Issue
Block a user