package handler import ( "encoding/base64" "errors" "fmt" "net/http" "strconv" "strings" "git.iioio.com/freefire/jiang13-forum/config" "git.iioio.com/freefire/jiang13-forum/middleware" "git.iioio.com/freefire/jiang13-forum/model" "git.iioio.com/freefire/jiang13-forum/service" "github.com/gin-gonic/gin" ) // Handlers 聚合所有 HTTP 处理器 type Handlers struct { Cfg *config.Config Store *service.UploadStore Auth *service.AuthService User *service.UserService Board *service.BoardService Post *service.PostService Comment *service.CommentService Message *service.MessageService Notify *service.NotifyService Report *service.ReportService Backup *service.BackupService Community *service.CommunityService Filter *service.SensitiveFilter Limiter *service.RateLimiter Settings *service.ForumSettingsService Captcha *service.CaptchaService Mail *service.MailService EmailCode *service.EmailCodeService OIDC *service.OIDCService Gitea *service.GiteaService Points *service.PointsService Badge *service.BadgeService SitePage *service.SitePageService FriendLinkApply *service.FriendLinkApplyService } func (h *Handlers) setAuthCookie(c *gin.Context, token string) { c.SetCookie(middleware.CookieName, token, int(service.TokenExpire.Seconds()), "/", "", false, true) } func (h *Handlers) currentUserID(c *gin.Context) uint { if v, ok := c.Get(middleware.CtxUserID); ok { return v.(uint) } return 0 } func (h *Handlers) isAdmin(c *gin.Context) bool { if v, ok := c.Get(middleware.CtxRole); ok { return v == model.RoleAdmin } return false } // loadCurrentUser 加载当前登录用户完整资料(含认证/积分) func (h *Handlers) loadCurrentUser(c *gin.Context) (*model.User, error) { uid := h.currentUserID(c) if uid == 0 { return nil, errors.New("未登录") } return h.User.GetByID(uid) } func (h *Handlers) skipsModeration(c *gin.Context) bool { u, err := h.loadCurrentUser(c) if err != nil { return h.isAdmin(c) } return u.SkipsModeration() } func (h *Handlers) parseGuestCommentIDs(c *gin.Context) []uint { raw := c.Query("my_ids") if raw == "" { return nil } var ids []uint for _, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } v, err := strconv.ParseUint(part, 10, 64) if err == nil && v > 0 { ids = append(ids, uint(v)) } } return ids } func calcTotalPages(total int64, size int) int { if total == 0 { return 1 } pages := int(total) / size if int(total)%size > 0 { pages++ } if pages < 1 { return 1 } return pages } // --- API --- func (h *Handlers) APICaptcha(c *gin.Context) { id, svg, err := h.Captcha.Generate() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "验证码生成失败"}) return } c.JSON(http.StatusOK, gin.H{ "id": id, "image": "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg)), }) } // APIRegisterConfig 注册页所需公开配置 func (h *Handlers) APIRegisterConfig(c *gin.Context) { userCount := h.Auth.UserCount() mailReady := h.Settings.MailReady() c.JSON(http.StatusOK, gin.H{ "is_first_user": userCount == 0, "mail_ready": mailReady, "require_email_code": mailReady, "register_open": userCount == 0 || mailReady, "email_code_len": service.EmailCodeLen, }) } // APISendRegisterEmailCode 发送注册邮箱验证码 func (h *Handlers) APISendRegisterEmailCode(c *gin.Context) { var req struct { Email string `json:"email" form:"email" binding:"required"` } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } if !h.Settings.MailReady() { c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) return } if err := h.EmailCode.SendRegisterCode(req.Email); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "验证码已发送"}) } // APISendResetEmailCode 发送重置密码验证码 func (h *Handlers) APISendResetEmailCode(c *gin.Context) { var req struct { Email string `json:"email" form:"email" binding:"required"` } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } if !h.Settings.MailReady() { c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) return } if err := h.EmailCode.SendResetCode(req.Email); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "若该邮箱已注册,验证码将发送到邮箱"}) } // APIResetPassword 邮箱验证码重置密码 func (h *Handlers) APIResetPassword(c *gin.Context) { var req struct { Email string `json:"email" form:"email" binding:"required"` EmailCode string `json:"email_code" form:"email_code" binding:"required"` NewPassword string `json:"new_password" form:"new_password" binding:"required"` } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } if !h.Settings.MailReady() { c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) return } if !h.EmailCode.VerifyPurpose(service.EmailCodePurposeReset, req.Email, req.EmailCode) { c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrEmailCodeInvalid.Error()}) return } if err := h.User.ResetPasswordByEmail(req.Email, req.NewPassword); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "密码已重置,请使用新密码登录"}) } // APISearchUsers 用户搜索(@补全) func (h *Handlers) APISearchUsers(c *gin.Context) { q := strings.TrimSpace(c.Query("q")) if q == "" { c.JSON(http.StatusOK, gin.H{"users": []any{}}) return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "8")) users, err := h.User.SearchUsersBrief(q, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } out := make([]gin.H, 0, len(users)) for _, u := range users { out = append(out, gin.H{ "id": u.ID, "username": u.Username, "nickname": u.Nickname, "avatar": u.Avatar, }) } c.JSON(http.StatusOK, gin.H{"users": out}) } func (h *Handlers) APIRegister(c *gin.Context) { var req struct { Username string `json:"username" form:"username" binding:"required"` Password string `json:"password" form:"password" binding:"required"` Nickname string `json:"nickname" form:"nickname"` Email string `json:"email" form:"email" binding:"required"` EmailCode string `json:"email_code" form:"email_code"` } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } userCount := h.Auth.UserCount() mailReady := h.Settings.MailReady() if userCount > 0 && !mailReady { c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrRegisterClosed.Error()}) return } if mailReady { if !h.EmailCode.Verify(req.Email, req.EmailCode) { c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrEmailCodeInvalid.Error()}) return } } user, err := h.Auth.Register(req.Username, req.Password, req.Nickname, req.Email) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } token, _, _ := h.Auth.Login(req.Username, req.Password, c.ClientIP()) h.setAuthCookie(c, token) c.JSON(http.StatusOK, gin.H{"message": "注册成功", "user_id": user.ID}) } func (h *Handlers) APILogin(c *gin.Context) { var req struct { Username string `json:"username" form:"username" binding:"required"` Password string `json:"password" form:"password" binding:"required"` } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } token, user, err := h.Auth.Login(req.Username, req.Password, c.ClientIP()) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } h.setAuthCookie(c, token) c.JSON(http.StatusOK, gin.H{"message": "登录成功", "user": gin.H{"id": user.ID, "nickname": user.Nickname}}) } func (h *Handlers) APILogout(c *gin.Context) { c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true) c.JSON(http.StatusOK, gin.H{"message": "已退出"}) } // APIProfileStats 当前用户活动统计(发帖 / 评论 / 收藏 / 获赞) func (h *Handlers) APIProfileStats(c *gin.Context) { st, err := h.User.ActivityStats(h.currentUserID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"stats": st}) } // APIUserPublic 公开用户主页(资料 + 公开统计) func (h *Handlers) APIUserPublic(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "无效用户"}) return } user, err := h.User.GetByID(uint(id)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) return } st, err := h.User.ActivityStats(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // 收藏数仅本人可见 viewerID := h.currentUserID(c) if viewerID != user.ID { st.FavoriteCount = 0 } view := user.ToPublic() if h.Badge != nil { _ = h.Badge.EvaluateAuto(user.ID) if badges, bErr := h.Badge.ListUserBadges(user.ID); bErr == nil { view.Badges = service.BadgeViews(badges, 0) } } c.JSON(http.StatusOK, gin.H{ "user": view, "stats": st, }) } func (h *Handlers) APIUpdateProfile(c *gin.Context) { nickname := c.PostForm("nickname") if err := h.User.UpdateNickname(h.currentUserID(c), nickname); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, _ := h.User.GetByID(h.currentUserID(c)) var userView any if user != nil { userView = user.ToSelf() } c.JSON(http.StatusOK, gin.H{"message": "昵称已更新", "user": userView}) } func (h *Handlers) APIUpdateSignature(c *gin.Context) { signature := c.PostForm("signature") if err := h.User.UpdateSignature(h.currentUserID(c), signature); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, _ := h.User.GetByID(h.currentUserID(c)) var userView any if user != nil { userView = user.ToSelf() } c.JSON(http.StatusOK, gin.H{"message": "签名已更新", "user": userView}) } func (h *Handlers) APIUpdatePassword(c *gin.Context) { oldPass := c.PostForm("old_password") newPass := c.PostForm("new_password") if err := h.User.UpdatePassword(h.currentUserID(c), oldPass, newPass); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "密码已修改"}) } func (h *Handlers) APIUploadAvatar(c *gin.Context) { file, err := c.FormFile("avatar") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择头像文件"}) return } maxBytes := int64(h.Settings.AvatarMaxMB()) * 1024 * 1024 if file.Size > maxBytes { c.JSON(http.StatusBadRequest, gin.H{"error": "头像文件过大"}) return } url, err := h.User.UploadAvatar(h.currentUserID(c), file, h.Store) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "头像已更新", "avatar": url}) } func (h *Handlers) APIUploadPostImage(c *gin.Context) { file, err := c.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择图片文件"}) return } uid := h.currentUserID(c) url, err := service.SaveUploadedImage( h.Store, file, service.UploadCategoryPosts, fmt.Sprintf("%d", uid), ) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "图片已上传", "url": url}) } func (h *Handlers) APICreatePost(c *gin.Context) { boardID, _ := strconv.ParseUint(c.PostForm("board_id"), 10, 64) title := c.PostForm("title") content := c.PostForm("content") tags := c.PostForm("tags") postType := c.PostForm("post_type") skip := h.skipsModeration(c) post, err := h.Post.Create(h.currentUserID(c), uint(boardID), title, content, tags, postType, skip) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } extras := service.ParsePostExtrasFromForm( c.PostForm("poll_options"), c.PostForm("bounty_points"), c.PostForm("lottery_winner_count"), ) if post.PostType == model.PostTypePoll || post.PostType == model.PostTypeBounty || post.PostType == model.PostTypeLottery { if err := service.FinalizeSpecialPostCreate(post, h.currentUserID(c), extras); err != nil { _ = h.Post.Delete(h.currentUserID(c), post.ID, true) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } msg := "发帖成功" if post.Status == model.ContentStatusPending { msg = "已提交审核,通过后将公开显示" if h.Notify != nil { h.Notify.AsyncNotifyPendingPost(post) } } c.JSON(http.StatusOK, gin.H{"message": msg, "post_id": post.ID, "status": post.Status}) } func (h *Handlers) APIUpdatePost(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) boardID, _ := strconv.ParseUint(c.PostForm("board_id"), 10, 64) isAdmin := h.isAdmin(c) skip := h.skipsModeration(c) err := h.Post.Update(h.currentUserID(c), uint(id), isAdmin, skip, c.PostForm("title"), c.PostForm("content"), c.PostForm("tags"), c.PostForm("post_type"), uint(boardID)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 非免审用户修改后重新进入审核 if !skip && h.Notify != nil { if post, getErr := h.Post.FindByID(uint(id)); getErr == nil { h.Notify.AsyncNotifyPendingPost(post) } } c.JSON(http.StatusOK, gin.H{"message": "帖子已更新"}) } func (h *Handlers) APIDeletePost(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) if err := h.Post.Delete(h.currentUserID(c), uint(id), h.isAdmin(c)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "帖子已移入回收站"}) } func (h *Handlers) APIToggleLike(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) liked, err := h.Post.ToggleLike(h.currentUserID(c), uint(id)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var post model.Post model.DB.First(&post, id) c.JSON(http.StatusOK, gin.H{"liked": liked, "like_count": post.LikeCount}) } func (h *Handlers) APIToggleCommentLike(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) liked, likeCount, err := h.Comment.ToggleLike(h.currentUserID(c), uint(id)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"liked": liked, "like_count": likeCount}) } func (h *Handlers) APIToggleFavorite(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) faved, err := h.Post.ToggleFavorite(h.currentUserID(c), uint(id)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"favorited": faved}) } // APISetQuestionResolved 标记问答帖已解决 / 未解决 func (h *Handlers) APISetQuestionResolved(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) resolved := c.PostForm("resolved") == "1" || c.PostForm("resolved") == "true" if err := h.Post.SetQuestionResolved(h.currentUserID(c), uint(id), h.isAdmin(c), resolved); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } msg := "已标记为未解决" if resolved { msg = "已标记为已解决" } c.JSON(http.StatusOK, gin.H{"message": msg, "question_resolved": resolved}) } func (h *Handlers) APICreateComment(c *gin.Context) { postID, _ := strconv.ParseUint(c.Param("id"), 10, 64) content := c.PostForm("content") var replyTo *uint if rt := c.PostForm("reply_to"); rt != "" { v, _ := strconv.ParseUint(rt, 10, 64) u := uint(v) replyTo = &u } isPrivate := c.PostForm("is_private") == "1" || c.PostForm("is_private") == "true" uid := h.currentUserID(c) if uid == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后评论"}) return } in := service.CommentCreateInput{ UserID: uid, PostID: uint(postID), Content: content, ReplyTo: replyTo, IsPrivate: isPrivate, } comment, err := h.Comment.Create(in) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } msg := "评论成功" if h.Notify != nil { switch comment.Status { case model.ContentStatusPublished: h.Notify.AsyncNotifyCommentPublished(comment) h.Notify.AsyncNotifyCommentMentions(comment) case model.ContentStatusPending: msg = "评论已提交,审核通过后公开显示" h.Notify.AsyncNotifyPendingComment(comment) } } else if comment.Status == model.ContentStatusPending { msg = "评论已提交,审核通过后公开显示" } c.JSON(http.StatusOK, gin.H{"message": msg, "floor": comment.Floor, "id": comment.ID, "status": comment.Status}) } func (h *Handlers) APIDeleteComment(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) if err := h.Comment.Delete(h.currentUserID(c), uint(id), h.isAdmin(c)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "评论已移入回收站"}) } func (h *Handlers) APIUpdateComment(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) content := c.PostForm("content") skip := h.skipsModeration(c) saved, enteredPending, err := h.Comment.Update(h.currentUserID(c), uint(id), h.isAdmin(c), skip, content) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } msg := "评论已更新" status := "" if comment, e := h.Comment.GetByID(uint(id)); e == nil { status = comment.Status if status == model.ContentStatusPending && !h.isAdmin(c) { msg = "评论已更新,审核通过后公开显示" } if enteredPending && h.Notify != nil { h.Notify.AsyncNotifyPendingComment(comment) } } c.JSON(http.StatusOK, gin.H{"message": msg, "content": saved, "status": status}) }