chore: 主题改媒体查询并清理未挂载 API

去掉 head 防闪脚本;删除论坛 JSON CRUD 与 crawler,仅保留机器入口;用户向文档对齐 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 05:10:23 +08:00
parent 4a21d362d0
commit 58b94df50c
27 changed files with 277 additions and 3182 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,218 +0,0 @@
package api
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/services"
)
// APIMePoints 余额与流水
func (h *Handlers) APIMePoints(c *gin.Context) {
uid := h.currentUserID(c)
user, err := h.User.GetByID(uid)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
rows, total, err := h.Points.ListLedger(uid, page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
checkIn, _ := h.Points.GetCheckInStatus(uid)
lottery, _ := h.Points.GetLotteryStatus(uid)
c.JSON(http.StatusOK, gin.H{
"points": user.Points,
"creator_income_total": user.CreatorIncomeTotal,
"ledger": rows,
"total": total,
"page": page,
"total_pages": calcTotalPages(total, size),
"check_in": checkIn,
"lottery": lottery,
})
}
// APIMeCheckInGet 今日签到状态
func (h *Handlers) APIMeCheckInGet(c *gin.Context) {
st, err := h.Points.GetCheckInStatus(h.currentUserID(c))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"check_in": st})
}
// APIMeCheckIn 每日签到
func (h *Handlers) APIMeCheckIn(c *gin.Context) {
st, err := h.Points.CheckIn(h.currentUserID(c))
if err != nil {
if errors.Is(err, services.ErrAlreadyCheckedIn) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, _ := h.User.GetByID(h.currentUserID(c))
pts := 0
if user != nil {
pts = user.Points
}
c.JSON(http.StatusOK, gin.H{"message": "签到成功", "check_in": st, "points": pts})
}
// APIMeLottery GET 状态 / POST 抽奖
func (h *Handlers) APIMeLotteryGet(c *gin.Context) {
st, err := h.Points.GetLotteryStatus(h.currentUserID(c))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"lottery": st})
}
func (h *Handlers) APIMeLotteryDraw(c *gin.Context) {
st, err := h.Points.DrawLottery(h.currentUserID(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, _ := h.User.GetByID(h.currentUserID(c))
pts := 0
if user != nil {
pts = user.Points
}
c.JSON(http.StatusOK, gin.H{"message": "抽奖完成", "lottery": st, "points": pts})
}
// APIUnlockPostBlock 积分解锁付费块
func (h *Handlers) APIUnlockPostBlock(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
BlockKey string `json:"block_key"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.BlockKey == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 block_key"})
return
}
res, err := services.UnlockPointsBlock(h.currentUserID(c), uint(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})
}
// APIAdminVerifyUser 认证开关
func (h *Handlers) APIAdminVerifyUser(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Verified bool `json:"verified"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if err := services.SetVerified(uint(id), req.Verified); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
msg := "已取消认证"
if req.Verified {
msg = "已认证"
}
c.JSON(http.StatusOK, gin.H{"message": msg, "verified": req.Verified})
}
// APIAdminSetUserLevel 设等级
func (h *Handlers) APIAdminSetUserLevel(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Level int `json:"level"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if err := services.SetUserLevel(uint(id), req.Level); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "等级已更新", "level": req.Level, "exp": models.ExpForLevel(req.Level)})
}
// APIAdminAdjustPoints 调积分
func (h *Handlers) APIAdminAdjustPoints(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Delta int `json:"delta"`
Note string `json:"note"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
bal, err := h.Points.AdminAdjust(uint(id), req.Delta, req.Note)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "积分已调整", "points": bal})
}
// APIAdminListBadges 徽章定义列表
func (h *Handlers) APIAdminListBadges(c *gin.Context) {
rows, err := h.Badge.ListDefs(true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"badges": rows})
}
// APIAdminUpsertBadge 创建/更新徽章定义
func (h *Handlers) APIAdminUpsertBadge(c *gin.Context) {
var def models.BadgeDef
if err := c.ShouldBindJSON(&def); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if err := h.Badge.UpsertDef(&def); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已保存", "badge": def})
}
// APIAdminAwardBadge 颁发/收回限定徽章
func (h *Handlers) APIAdminAwardBadge(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
BadgeID uint `json:"badge_id"`
Revoke bool `json:"revoke"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.BadgeID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if req.Revoke {
if err := h.Badge.Revoke(uint(id), req.BadgeID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已收回徽章"})
return
}
if err := h.Badge.AwardLimited(uint(id), req.BadgeID, h.currentUserID(c)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已颁发徽章"})
}

View File

@@ -1,281 +0,0 @@
package api
import (
"fmt"
"net/http"
"strconv"
"strings"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
// APIApplyFriendLink 提交友情链接申请
func (h *Handlers) APIApplyFriendLink(c *gin.Context) {
uid := h.currentUserID(c)
if uid == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
var req struct {
Name string `json:"name"`
URL string `json:"url"`
Logo string `json:"logo"`
LinkOnHomepage bool `json:"link_on_homepage"`
ReciprocalPageURL string `json:"reciprocal_page_url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
result, err := h.FriendLinkApply.Create(services.FriendLinkApplyInput{
UserID: uid,
Name: req.Name,
URL: req.URL,
Logo: req.Logo,
LinkOnHomepage: req.LinkOnHomepage,
ReciprocalPageURL: req.ReciprocalPageURL,
OurSiteURL: h.publicBaseURL(c),
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp := gin.H{
"message": friendLinkApplySubmittedMessage(h.Settings.FriendLinkReciprocalCheckEnabled(), false),
"apply": result.Apply,
}
c.JSON(http.StatusOK, resp)
}
// APIUploadFriendLinkLogo 上传友链申请 LOGO
func (h *Handlers) APIUploadFriendLinkLogo(c *gin.Context) {
uid := h.currentUserID(c)
if uid == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
file, err := c.FormFile("logo")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择 LOGO 图片"})
return
}
maxBytes := int64(h.Settings.AvatarMaxMB()) * 1024 * 1024
if file.Size > maxBytes {
c.JSON(http.StatusBadRequest, gin.H{"error": "图片文件过大"})
return
}
url, err := services.SaveUploadedImage(
h.Store,
file,
services.UploadCategorySite,
fmt.Sprintf("fl_%d", uid),
)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "LOGO 已上传", "url": url})
}
// APIAdminFriendLinkApplies 管理员友链申请列表
func (h *Handlers) APIAdminFriendLinkApplies(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
status := strings.TrimSpace(c.DefaultQuery("status", "pending"))
list, total, err := h.FriendLinkApply.ListAdmin(services.FriendLinkApplyListQuery{
Page: page, Size: size, Status: status,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
pending, _ := h.FriendLinkApply.PendingCount()
c.JSON(http.StatusOK, gin.H{
"applies": list,
"total": total,
"page": page,
"pending_count": pending,
"reciprocal_check_enabled": h.Settings.FriendLinkReciprocalCheckEnabled(),
})
}
// APIAdminUpdateFriendLinkSettings 更新友链相关开关(回链检测 / 入口展示)
func (h *Handlers) APIAdminUpdateFriendLinkSettings(c *gin.Context) {
var req struct {
ReciprocalCheckEnabled *bool `json:"reciprocal_check_enabled"`
NavShowFriendLinks *bool `json:"nav_show_friend_links"`
FooterShowFriendLinks *bool `json:"footer_show_friend_links"`
AsideShowFriendLinks *bool `json:"aside_show_friend_links"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if req.ReciprocalCheckEnabled == nil && req.NavShowFriendLinks == nil &&
req.FooterShowFriendLinks == nil && req.AsideShowFriendLinks == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if req.ReciprocalCheckEnabled != nil {
if err := h.Settings.SetFriendLinkReciprocalCheckEnabled(*req.ReciprocalCheckEnabled); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if req.NavShowFriendLinks != nil {
if err := h.Settings.SetNavShowFriendLinks(*req.NavShowFriendLinks); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if req.FooterShowFriendLinks != nil {
if err := h.Settings.SetFooterShowFriendLinks(*req.FooterShowFriendLinks); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if req.AsideShowFriendLinks != nil {
if err := h.Settings.SetAsideFriendLinksEnabled(*req.AsideShowFriendLinks); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
msg := "设置已保存"
if req.ReciprocalCheckEnabled != nil && req.NavShowFriendLinks == nil &&
req.FooterShowFriendLinks == nil && req.AsideShowFriendLinks == nil {
if *req.ReciprocalCheckEnabled {
msg = "已开启回链检测"
} else {
msg = "已关闭回链检测"
}
}
c.JSON(http.StatusOK, gin.H{
"message": msg,
"reciprocal_check_enabled": h.Settings.FriendLinkReciprocalCheckEnabled(),
"nav_show_friend_links": h.Settings.NavShowFriendLinks(),
"footer_show_friend_links": h.Settings.FooterShowFriendLinks(),
"aside_show_friend_links": h.Settings.AsideShowFriendLinks(),
})
}
// APIAdminApproveFriendLinkApply 通过友链申请
func (h *Handlers) APIAdminApproveFriendLinkApply(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
apply, err := h.FriendLinkApply.Approve(uint(id))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已通过并加入友情链接", "apply": apply})
}
// APIAdminRejectFriendLinkApply 拒绝友链申请
func (h *Handlers) APIAdminRejectFriendLinkApply(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Note string `json:"note"`
}
_ = c.ShouldBindJSON(&req)
apply, err := h.FriendLinkApply.Reject(uint(id), req.Note)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已拒绝申请", "apply": apply})
}
// APIMyFriendLinkApplies 当前用户的友链申请列表
func (h *Handlers) APIMyFriendLinkApplies(c *gin.Context) {
uid := h.currentUserID(c)
if uid == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
list, err := h.FriendLinkApply.ListMine(uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"applies": list})
}
// APICancelFriendLinkApply 撤销待审友链申请
func (h *Handlers) APICancelFriendLinkApply(c *gin.Context) {
uid := h.currentUserID(c)
if uid == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.FriendLinkApply.Cancel(uid, uint(id)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已撤销申请"})
}
// APIUpdateFriendLinkApply 修改并重新提交友链申请
func (h *Handlers) APIUpdateFriendLinkApply(c *gin.Context) {
uid := h.currentUserID(c)
if uid == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Name string `json:"name"`
URL string `json:"url"`
Logo string `json:"logo"`
LinkOnHomepage bool `json:"link_on_homepage"`
ReciprocalPageURL string `json:"reciprocal_page_url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
result, err := h.FriendLinkApply.Update(uid, uint(id), services.FriendLinkApplyInput{
UserID: uid,
Name: req.Name,
URL: req.URL,
Logo: req.Logo,
LinkOnHomepage: req.LinkOnHomepage,
ReciprocalPageURL: req.ReciprocalPageURL,
OurSiteURL: h.publicBaseURL(c),
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp := gin.H{
"message": friendLinkApplySubmittedMessage(h.Settings.FriendLinkReciprocalCheckEnabled(), true),
"apply": result.Apply,
}
c.JSON(http.StatusOK, resp)
}
// APIAdminRecheckFriendLinkApply 管理员重新检测回链
func (h *Handlers) APIAdminRecheckFriendLinkApply(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
apply, err := h.FriendLinkApply.RecheckReciprocal(uint(id), h.publicBaseURL(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已开始重新检测回链", "apply": apply})
}
func friendLinkApplySubmittedMessage(checkEnabled, isUpdate bool) string {
if isUpdate {
if checkEnabled {
return "申请已更新,回链检测将在后台进行"
}
return "申请已更新"
}
if checkEnabled {
return "申请已提交,回链检测将在后台进行"
}
return "申请已提交"
}

View File

@@ -1,52 +1,22 @@
package api
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/config"
"git.iioio.com/freefire/jiang13-forum/modules/auth"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
// Handlers 聚合所有 HTTP 处理器
// Handlers 本分支仅挂载机器入口health / SEO / thumb / OIDC
// 论坛 CRUD JSON 已删除,对照见 main 与 docs/rebuild-spec/04-api.md。
type Handlers struct {
Cfg *config.Config
Store *services.UploadStore
Auth *services.AuthService
User *services.UserService
Board *services.BoardService
Post *services.PostService
Comment *services.CommentService
Message *services.MessageService
Notify *services.NotifyService
Report *services.ReportService
Backup *services.BackupService
Filter *services.SensitiveFilter
Limiter *services.RateLimiter
Settings *services.ForumSettingsService
Captcha *services.CaptchaService
Mail *services.MailService
EmailCode *services.EmailCodeService
OIDC *services.OIDCService
Gitea *services.GiteaService
Points *services.PointsService
Badge *services.BadgeService
SitePage *services.SitePageService
FriendLinkApply *services.FriendLinkApplyService
}
func (h *Handlers) setAuthCookie(c *gin.Context, sessionID string) {
if sessionID == "" {
return
}
auth.SetSessionCookie(c, sessionID)
Cfg *config.Config
Settings *services.ForumSettingsService
Board *services.BoardService
Post *services.PostService
OIDC *services.OIDCService
}
func (h *Handlers) currentUserID(c *gin.Context) uint {
@@ -56,550 +26,7 @@ func (h *Handlers) currentUserID(c *gin.Context) uint {
return 0
}
func (h *Handlers) isAdmin(c *gin.Context) bool {
if v, ok := c.Get(auth.CtxRole); ok {
return v == models.RoleAdmin
}
return false
}
// loadCurrentUser 加载当前登录用户完整资料(含认证/积分)
func (h *Handlers) loadCurrentUser(c *gin.Context) (*models.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) {
mailReady := h.Settings.MailReady()
c.JSON(http.StatusOK, gin.H{
"is_first_user": false, // 已废弃:管理员仅由 /install 创建
"mail_ready": mailReady,
"require_email_code": mailReady,
"register_open": mailReady,
"email_code_len": services.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": services.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": services.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": services.ErrMailNotConfigured.Error()})
return
}
if !h.EmailCode.VerifyPurpose(services.EmailCodePurposeReset, req.Email, req.EmailCode) {
c.JSON(http.StatusBadRequest, gin.H{"error": services.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"`
CaptchaID string `json:"captcha_id" form:"captcha_id"`
Captcha string `json:"captcha" form:"captcha"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if h.Captcha == nil || !h.Captcha.Verify(req.CaptchaID, req.Captcha) {
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": services.ErrRegisterClosed.Error()})
return
}
if mailReady {
if !h.EmailCode.Verify(req.Email, req.EmailCode) {
c.JSON(http.StatusBadRequest, gin.H{"error": services.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(), c.Request.UserAgent())
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(), c.Request.UserAgent())
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(auth.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 = services.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 := services.SaveUploadedImage(
h.Store,
file,
services.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 := services.ParsePostExtrasFromForm(
c.PostForm("poll_options"),
c.PostForm("bounty_points"),
c.PostForm("lottery_winner_count"),
)
if post.PostType == models.PostTypePoll || post.PostType == models.PostTypeBounty || post.PostType == models.PostTypeLottery {
if err := services.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 == models.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 models.Post
models.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 := services.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 models.ContentStatusPublished:
h.Notify.AsyncNotifyCommentPublished(comment)
h.Notify.AsyncNotifyCommentMentions(comment)
case models.ContentStatusPending:
msg = "评论已提交,审核通过后公开显示"
h.Notify.AsyncNotifyPendingComment(comment)
}
} else if comment.Status == models.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 == models.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})
// APIHealth 探活
func (h *Handlers) APIHealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

View File

@@ -1,70 +0,0 @@
package api
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
// APIAdminMedia 列出媒体资源
func (h *Handlers) APIAdminMedia(c *gin.Context) {
if h.Store == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "上传存储未初始化"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "24"))
category := c.DefaultQuery("category", "all")
query := c.Query("q")
result, err := h.Store.ListMedia(category, query, page, size)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// APIAdminDeleteMedia 批量删除媒体
func (h *Handlers) APIAdminDeleteMedia(c *gin.Context) {
if h.Store == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "上传存储未初始化"})
return
}
var req struct {
URLs []string `json:"urls"`
URL string `json:"url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
urls := make([]string, 0, len(req.URLs)+1)
for _, u := range req.URLs {
u = strings.TrimSpace(u)
if u != "" {
urls = append(urls, u)
}
}
if u := strings.TrimSpace(req.URL); u != "" {
urls = append(urls, u)
}
if len(urls) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要删除的文件"})
return
}
if len(urls) > 100 {
c.JSON(http.StatusBadRequest, gin.H{"error": "单次最多删除 100 个文件"})
return
}
n, err := h.Store.DeleteMedia(urls)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "已删除 " + strconv.Itoa(n) + " 项媒体",
"deleted": n,
})
}

View File

@@ -1,170 +0,0 @@
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/services"
)
// APIMessageConversations 会话列表(按对方聚合)
func (h *Handlers) APIMessageConversations(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "30"))
list, total, err := h.Message.ListConversations(services.ConversationListQuery{
UserID: h.currentUserID(c),
Page: page,
Size: size,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"conversations": list,
"total": total,
"page": page,
})
}
// APIConversationMessages 某会话内消息
func (h *Handlers) APIConversationMessages(c *gin.Context) {
peerID, err := strconv.ParseUint(c.Param("peerId"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的会话"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "50"))
before, _ := strconv.ParseUint(c.DefaultQuery("before", "0"), 10, 64)
uid := h.currentUserID(c)
list, total, err := h.Message.ListConversationMessages(services.ConversationMessagesQuery{
UserID: uid,
PeerID: uint(peerID),
Page: page,
Size: size,
Before: uint(before),
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 首次打开(非向上翻页)时标已读
if before == 0 {
_ = h.Message.MarkConversationRead(uid, uint(peerID))
for i := range list {
if list[i].ToUserID == uid {
list[i].IsRead = true
}
}
}
var peer *models.User
if peerID > 0 {
var u models.User
if err := models.DB.First(&u, uint(peerID)).Error; err == nil {
peer = &u
}
}
c.JSON(http.StatusOK, gin.H{
"messages": list,
"total": total,
"peer_user_id": uint(peerID),
"peer_user": peer,
"is_system": peerID == 0,
})
}
// APIMarkConversationRead 将会话标为已读
func (h *Handlers) APIMarkConversationRead(c *gin.Context) {
peerID, err := strconv.ParseUint(c.Param("peerId"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的会话"})
return
}
if err := h.Message.MarkConversationRead(h.currentUserID(c), uint(peerID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已标为已读"})
}
// APIMessageUnreadCount 未读私信数(含私信/通知分项)
func (h *Handlers) APIMessageUnreadCount(c *gin.Context) {
total, dm, notify, err := h.Message.UnreadCounts(h.currentUserID(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"count": total,
"dm_count": dm,
"notify_count": notify,
})
}
// APIMessageNotifications 系统通知列表
func (h *Handlers) APIMessageNotifications(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "30"))
kind := c.Query("kind")
uid := h.currentUserID(c)
list, total, err := h.Message.ListNotifications(uid, page, size, kind)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"notifications": list,
"total": total,
"page": page,
"kind": kind,
})
}
// APIMarkNotificationsRead 系统通知全部已读
func (h *Handlers) APIMarkNotificationsRead(c *gin.Context) {
if err := h.Message.MarkNotificationsRead(h.currentUserID(c)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "通知已全部标为已读"})
}
// APISendMessage 发送私信
func (h *Handlers) APISendMessage(c *gin.Context) {
var req struct {
ToUserID uint `json:"to_user_id"`
Subject string `json:"subject"`
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
msg, err := h.Message.Send(services.MessageSendInput{
FromUserID: h.currentUserID(c),
ToUserID: req.ToUserID,
Subject: req.Subject,
Content: req.Content,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": msg})
}
// APIMarkAllMessagesRead 全部已读
func (h *Handlers) APIMarkAllMessagesRead(c *gin.Context) {
if err := h.Message.MarkAllRead(h.currentUserID(c)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已全部标为已读"})
}

View File

@@ -1,163 +0,0 @@
package api
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/services"
)
// APICreatePostReport 举报帖子
func (h *Handlers) APICreatePostReport(c *gin.Context) {
postID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Reason string `json:"reason"`
Detail string `json:"detail"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
rep, err := h.Report.Create(h.currentUserID(c), uint(postID), req.Reason, req.Detail)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "举报已提交,感谢反馈", "report": rep})
}
// APICreateCommentReport 举报评论
func (h *Handlers) APICreateCommentReport(c *gin.Context) {
commentID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Reason string `json:"reason"`
Detail string `json:"detail"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
rep, err := h.Report.CreateCommentReport(h.currentUserID(c), uint(commentID), req.Reason, req.Detail)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "举报已提交,感谢反馈", "report": rep})
}
// APIAdminReports 举报列表
func (h *Handlers) APIAdminReports(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
status := c.DefaultQuery("status", "pending")
list, total, err := h.Report.ListAdmin(services.ReportListQuery{
Status: status,
Page: page,
Size: size,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pending, _ := h.Report.PendingCount()
c.JSON(http.StatusOK, gin.H{
"reports": list,
"total": total,
"page": page,
"pending_count": pending,
"status": status,
})
}
// APIAdminHandleReport 处理举报
func (h *Handlers) APIAdminHandleReport(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Action string `json:"action"`
HandleNote string `json:"handle_note"`
RejectReason string `json:"reject_reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
rep, err := h.Report.Handle(services.HandleReportInput{
ReportID: uint(id),
HandlerID: h.currentUserID(c),
Action: req.Action,
HandleNote: req.HandleNote,
RejectReason: req.RejectReason,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "处理完成", "report": rep})
}
// APIAdminApprovePost 通过帖子审核
func (h *Handlers) APIAdminApprovePost(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.Post.SetStatus(uint(id), models.ContentStatusPublished); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "帖子已通过审核", "status": models.ContentStatusPublished})
}
// APIAdminRejectPost 拒绝帖子并私信通知作者(标记为 rejected不进回收站
func (h *Handlers) APIAdminRejectPost(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
reason := strings.TrimSpace(req.Reason)
if reason == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写拒绝原因"})
return
}
post, err := h.Post.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
authorID := post.UserID
title := post.Title
postID := post.ID
if err := h.Post.SetStatus(postID, models.ContentStatusRejected); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pid := postID
_, msgErr := h.Message.SendSystem(
authorID,
"帖子《"+title+"》未通过审核",
services.FormatRejectContent(title, postID, reason),
models.MessageKindReject,
&pid,
nil,
)
if msgErr != nil {
c.JSON(http.StatusOK, gin.H{
"message": "帖子已拒绝,但私信通知失败:" + msgErr.Error(),
"notified": false,
"status": models.ContentStatusRejected,
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "已拒绝该帖并私信通知作者",
"notified": true,
"status": models.ContentStatusRejected,
})
}

View File

@@ -1,198 +0,0 @@
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/services"
)
// APIPages 已发布单页摘要列表
func (h *Handlers) APIPages(c *gin.Context) {
pages, err := h.SitePage.ListPublished()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if pages == nil {
pages = []services.SitePageSummary{}
}
c.JSON(http.StatusOK, gin.H{"pages": pages})
}
// APIPageDetail 单页详情
func (h *Handlers) APIPageDetail(c *gin.Context) {
slug := c.Param("slug")
allowDraft := h.isAdmin(c)
page, err := h.SitePage.GetBySlug(slug, allowDraft)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"page": page})
}
// APIAdminGetPage 管理端单页详情
func (h *Handlers) APIAdminGetPage(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的单页 ID"})
return
}
page, err := h.SitePage.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "单页不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"page": page})
}
// APIAdminPages 管理端单页列表
func (h *Handlers) APIAdminPages(c *gin.Context) {
pages, err := h.SitePage.ListAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if pages == nil {
pages = []models.SitePage{}
}
c.JSON(http.StatusOK, gin.H{"pages": pages})
}
// APIAdminCreatePage 创建单页
func (h *Handlers) APIAdminCreatePage(c *gin.Context) {
var in services.SitePageInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
return
}
page, err := h.SitePage.Create(in)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "单页已创建", "page": page})
}
// APIAdminUpdatePage 更新单页
func (h *Handlers) APIAdminUpdatePage(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var in services.SitePageInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
return
}
if err := h.SitePage.Update(uint(id), in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "单页已更新"})
}
// APIAdminDeletePage 删除单页
func (h *Handlers) APIAdminDeletePage(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.SitePage.Delete(uint(id)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "单页已删除"})
}
// APIAdminSetPagePublished 切换单页发布状态
func (h *Handlers) APIAdminSetPagePublished(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的单页 ID"})
return
}
var body struct {
Published bool `json:"published"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
return
}
if err := h.SitePage.SetPublished(uint(id), body.Published); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
msg := "已取消发布"
if body.Published {
msg = "已发布"
}
c.JSON(http.StatusOK, gin.H{"message": msg, "published": body.Published})
}
// APIPollVote 投票
func (h *Handlers) APIPollVote(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
OptionIDs []uint `json:"option_ids"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
return
}
if err := services.VotePoll(uint(id), h.currentUserID(c), body.OptionIDs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
poll, _ := services.GetPollView(uint(id), h.currentUserID(c))
c.JSON(http.StatusOK, gin.H{"message": "投票成功", "poll": poll})
}
// APIPollClose 结束投票
func (h *Handlers) APIPollClose(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
post, err := h.Post.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
return
}
if err := services.ClosePoll(uint(id), h.currentUserID(c), h.isAdmin(c), post.UserID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
poll, _ := services.GetPollView(uint(id), h.currentUserID(c))
c.JSON(http.StatusOK, gin.H{"message": "投票已结束", "poll": poll})
}
// APIBountyAward 采纳悬赏
func (h *Handlers) APIBountyAward(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
commentID, _ := strconv.ParseUint(c.PostForm("comment_id"), 10, 64)
if commentID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择评论"})
return
}
if err := services.AwardBounty(uint(id), h.currentUserID(c), h.isAdmin(c), uint(commentID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "悬赏已发放"})
}
// APIBountyRefund 退回悬赏
func (h *Handlers) APIBountyRefund(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := services.RefundBounty(uint(id), h.currentUserID(c), h.isAdmin(c)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "悬赏已退回"})
}
// APILotteryDraw 帖内抽奖开奖
func (h *Handlers) APILotteryDraw(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
view, err := services.DrawPostLottery(uint(id), h.currentUserID(c), h.isAdmin(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "开奖完成", "lottery": view})
}

View File

@@ -60,8 +60,6 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
if err != nil {
return nil, err
}
// Gitea 仓库同步后置:本阶段不启动后台同步,亦不挂管理入口
giteaSvc := services.NewGiteaService(settingsSvc)
uploadStore := services.NewUploadStore(cfg.DataDir, settingsSvc)
if err := uploadStore.ReloadFromSettings(settingsSvc); err != nil {
@@ -79,15 +77,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
sitePageSvc := services.NewSitePageService(filter)
h := &api.Handlers{
Cfg: cfg, Store: uploadStore, Auth: authSvc, User: userSvc, Board: boardSvc,
Post: postSvc, Comment: commentSvc, Message: messageSvc, Notify: notifySvc, Report: reportSvc,
Backup: backupSvc,
Filter: filter, Limiter: limiter, Settings: settingsSvc,
Captcha: captchaSvc, Mail: mailSvc, EmailCode: emailCodeSvc,
OIDC: oidcSvc, Gitea: giteaSvc,
Points: services.NewPointsService(), Badge: services.NewBadgeService(),
SitePage: sitePageSvc,
FriendLinkApply: friendLinkApplySvc,
Cfg: cfg, Settings: settingsSvc,
Board: boardSvc, Post: postSvc,
OIDC: oidcSvc,
}
authMW := auth.NewAuthMiddleware(authSvc)
@@ -118,9 +110,6 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
r.GET("/health", h.APIHealth)
r.GET("/robots.txt", h.RobotsTxt)
r.GET("/sitemap.xml", h.SitemapXML)
// 机器注册辅助(与 SSR 注册共用 CaptchaService
r.GET("/api/captcha", h.APICaptcha)
r.POST("/api/register", h.APIRegister)
// OIDC Provider外部机器 / Gitea SSO
r.GET("/.well-known/openid-configuration", h.OIDCDiscovery)
@@ -132,7 +121,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
r.GET("/oauth/logout", h.OIDCLogout)
r.POST("/oauth/logout", h.OIDCLogout)
// 精简机器 API健康检查已注册保留只读探测与 OIDC论坛 UI 不再走 /api
// 机器入口:健康检查 / SEO / OIDC / 缩略图;论坛 UI 仅走 routers/web
r.NoRoute(webpages.Deps{
DataDir: cfg.DataDir, JWTSecret: cfg.JWTSecret,
Settings: settingsSvc, Auth: authSvc,