package web import ( "fmt" "html" "net/http" "net/url" "strconv" "strings" "git.iioio.com/freefire/jiang13-forum/models" "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) // PostPageData 帖详情 type PostPageData struct { PageChrome PostID uint PostPath string PostTitle string AuthorName string AuthorID uint BoardID uint BoardName string Pinned bool // 展示用:全局或版内置顶 GlobalPinned bool BoardPinned bool Featured bool EditLocked bool PostTypeLabel string CreatedLabel string ViewCount int LikeCount int Liked bool Favorited bool BodyHTML string CommentCount int Comments []CommentView CommentsLocked bool CanEdit bool CanReportPost bool IsAdmin bool } // CommentView 评论 type CommentView struct { ID uint Floor int AuthorName string AuthorID uint CreatedLabel string Content string ContentHidden bool ReplyToID uint ReplyToFloor int ReplyToAuthor string LikeCount int Liked bool IsPrivate bool CanReport bool } // PostView GET /post/:id func (d Deps) PostView(c *gin.Context) { ctx := d.ctx(c) idStr := stripIDParam(c.Param("id"), d.Settings.Permalink().Ext) id, err := strconv.ParseUint(idStr, 10, 64) if err != nil || id == 0 { d.render404(ctx) return } post, err := d.Post.FindByID(uint(id)) if err != nil || !services.CanViewPost(post, ctx.UserID(), ctx.IsAdmin()) { d.render404(ctx) return } if post.Status == models.ContentStatusPublished { d.Post.RecordView(uint(id)) } hasReplied := ctx.UserID() > 0 && d.Comment.HasUserReplied(uint(id), ctx.UserID()) body := services.ApplyPostContentGates(post.Content, post, ctx.UserID(), ctx.IsAdmin(), hasReplied) comments, _ := d.Comment.ListByPost(uint(id), ctx.UserID(), ctx.IsAdmin(), post.UserID, nil) cv := make([]CommentView, 0, len(comments)) for _, cm := range comments { an := strings.TrimSpace(cm.User.Nickname) if an == "" { an = cm.User.Username } view := CommentView{ ID: cm.ID, Floor: cm.Floor, AuthorName: an, AuthorID: cm.UserID, CreatedLabel: formatTime(cm.CreatedAt), Content: cm.Content, ContentHidden: cm.ContentHidden, LikeCount: cm.LikeCount, Liked: cm.Liked, IsPrivate: cm.IsPrivate, CanReport: ctx.IsSigned() && (cm.UserID == 0 || cm.UserID != ctx.UserID()), } if cm.ReplyTarget != nil { view.ReplyToID = cm.ReplyTarget.ID view.ReplyToFloor = cm.ReplyTarget.Floor rn := strings.TrimSpace(cm.ReplyTarget.User.Nickname) if rn == "" { rn = cm.ReplyTarget.User.Username } if rn == "" { rn = cm.ReplyTarget.GuestNick } view.ReplyToAuthor = rn } else if cm.ReplyTo != nil { view.ReplyToID = *cm.ReplyTo } cv = append(cv, view) } author := strings.TrimSpace(post.User.Nickname) if author == "" { author = post.User.Username } boardName := "" if post.Board.ID > 0 { boardName = post.Board.Name } chrome := d.chrome(ctx, post.Title+" · "+d.Settings.SiteBranding().Name, "", "post/body") chrome.ActiveBoard = post.BoardID ctx.HTML(http.StatusOK, "post", PostPageData{ PageChrome: chrome, PostID: post.ID, PostPath: url.QueryEscape(fmt.Sprintf("/post/%d", post.ID)), PostTitle: post.Title, AuthorName: author, AuthorID: post.UserID, BoardID: post.BoardID, BoardName: boardName, Pinned: post.Pinned || post.BoardPinned, GlobalPinned: post.Pinned, BoardPinned: post.BoardPinned, Featured: post.Featured, EditLocked: post.EditLocked, PostTypeLabel: postTypeLabel(post.PostType), CreatedLabel: formatTime(post.CreatedAt), ViewCount: post.ViewCount, LikeCount: post.LikeCount, Liked: d.Post.IsLiked(ctx.UserID(), post.ID), Favorited: d.Post.IsFavorited(ctx.UserID(), post.ID), BodyHTML: body, CommentCount: len(cv), Comments: cv, CommentsLocked: post.CommentsLocked, CanEdit: d.Post.CanUserEdit(post, ctx.UserID(), ctx.IsAdmin()), CanReportPost: ctx.IsSigned() && post.UserID != ctx.UserID(), IsAdmin: ctx.IsAdmin(), }) } func postTypeLabel(t string) string { switch t { case models.PostTypeQuestion: return "问答" case models.PostTypePoll: return "投票" case models.PostTypeBounty: return "悬赏" case models.PostTypeLottery: return "抽奖" default: return "" } } // PostComment POST 评论 func (d Deps) PostComment(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求,请重试") ctx.Redirect("/") return } id, err := parsePostID(c, d) if err != nil { d.render404(ctx) return } content := strings.TrimSpace(c.PostForm("content")) if content == "" { ctx.SetFlash("评论不能为空") ctx.Redirect(fmt.Sprintf("/post/%d#comments", id)) return } var replyTo *uint if raw := strings.TrimSpace(c.PostForm("reply_to")); raw != "" { rid, err := strconv.ParseUint(raw, 10, 64) if err == nil && rid > 0 { v := uint(rid) replyTo = &v } } isPrivate := c.PostForm("is_private") == "1" || c.PostForm("is_private") == "on" safe := "

" + html.EscapeString(content) + "

" cm, err := d.Comment.Create(services.CommentCreateInput{ PostID: id, UserID: ctx.UserID(), Content: safe, ReplyTo: replyTo, IsPrivate: isPrivate, }) if err != nil { ctx.SetFlash(err.Error()) ctx.Redirect(fmt.Sprintf("/post/%d#comments", id)) return } anchor := "#comments" if cm != nil && cm.Floor > 0 { anchor = fmt.Sprintf("#floor-%d", cm.Floor) } ctx.Redirect(fmt.Sprintf("/post/%d%s", id, anchor)) } // PostCommentLike 评论点赞切换(PRG) func (d Deps) PostCommentLike(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求") ctx.Redirect("/") return } postID, err := parsePostID(c, d) if err != nil { d.render404(ctx) return } cid, err := strconv.ParseUint(c.Param("cid"), 10, 64) if err != nil || cid == 0 { d.render404(ctx) return } cm, err := d.Comment.GetByID(uint(cid)) if err != nil || cm.PostID != postID { d.render404(ctx) return } _, _, _ = d.Comment.ToggleLike(ctx.UserID(), uint(cid)) ctx.Redirect(fmt.Sprintf("/post/%d#floor-%d", postID, cm.Floor)) } // PostUnlock POST /post/:id/unlock — 积分解锁(JSON,供详情页渐进增强) func (d Deps) PostUnlock(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { c.JSON(http.StatusBadRequest, gin.H{"error": "无效请求,请重试"}) return } id, err := parsePostID(c, d) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"}) return } var req struct { BlockKey string `json:"block_key" form:"block_key"` } _ = c.ShouldBindJSON(&req) if req.BlockKey == "" { req.BlockKey = strings.TrimSpace(c.PostForm("block_key")) } if req.BlockKey == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 block_key"}) return } res, err := services.UnlockPointsBlock(ctx.UserID(), id, req.BlockKey) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": "解锁成功", "unlock": res, }) } // PostLike 赞 func (d Deps) PostLike(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求") ctx.Redirect("/") return } id, err := parsePostID(c, d) if err != nil { d.render404(ctx) return } _, _ = d.Post.ToggleLike(ctx.UserID(), id) ctx.Redirect(fmt.Sprintf("/post/%d", id)) } // PostFavorite 收藏 func (d Deps) PostFavorite(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求") ctx.Redirect("/") return } id, err := parsePostID(c, d) if err != nil { d.render404(ctx) return } _, _ = d.Post.ToggleFavorite(ctx.UserID(), id) ctx.Redirect(fmt.Sprintf("/post/%d", id)) } func parsePostID(c *gin.Context, d Deps) (uint, error) { idStr := stripIDParam(c.Param("id"), d.Settings.Permalink().Ext) id, err := strconv.ParseUint(idStr, 10, 64) return uint(id), err }