fix: 完成 Gitea 目录改组收尾(import、构建与 LICENSE)
同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/middleware"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/auth"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// APIMe 当前登录用户
|
||||
@@ -25,7 +25,7 @@ func (h *Handlers) APIMe(c *gin.Context) {
|
||||
user, err := h.User.GetByID(uid)
|
||||
if err != nil {
|
||||
// 账号已删或不存在:清掉失效 cookie,与未登录态一致
|
||||
c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true)
|
||||
c.SetCookie(auth.CookieName, "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"user": nil})
|
||||
return
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func (h *Handlers) APIMe(c *gin.Context) {
|
||||
view := user.ToSelf()
|
||||
if h.Badge != nil {
|
||||
if badges, bErr := h.Badge.ListUserBadges(user.ID); bErr == nil {
|
||||
view.Badges = service.BadgeViews(badges, 0)
|
||||
view.Badges = services.BadgeViews(badges, 0)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -51,14 +51,14 @@ func (h *Handlers) APIBoards(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if boards == nil {
|
||||
boards = []service.BoardWithStats{}
|
||||
boards = []services.BoardWithStats{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"boards": boards})
|
||||
}
|
||||
|
||||
// APIHealth 健康检查(容器探活 / 负载均衡)
|
||||
func (h *Handlers) APIHealth(c *gin.Context) {
|
||||
if err := model.PingDB(); err != nil {
|
||||
if err := models.PingDB(); err != nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"status": "unavailable",
|
||||
"error": err.Error(),
|
||||
@@ -71,10 +71,10 @@ func (h *Handlers) APIHealth(c *gin.Context) {
|
||||
// APIStats 论坛概览统计
|
||||
func (h *Handlers) APIStats(c *gin.Context) {
|
||||
var userCount, postCount, boardCount, commentCount int64
|
||||
model.DB.Model(&model.User{}).Count(&userCount)
|
||||
model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished).Count(&postCount)
|
||||
model.DB.Model(&model.Board{}).Count(&boardCount)
|
||||
model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&commentCount)
|
||||
models.DB.Model(&models.User{}).Count(&userCount)
|
||||
models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPublished).Count(&postCount)
|
||||
models.DB.Model(&models.Board{}).Count(&boardCount)
|
||||
models.DB.Model(&models.Comment{}).Where("status = ?", models.ContentStatusPublished).Count(&commentCount)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": userCount,
|
||||
"posts": postCount,
|
||||
@@ -144,19 +144,19 @@ func (h *Handlers) APIAdminDeleteBoard(c *gin.Context) {
|
||||
// APIAdminDashboard 管理后台概览
|
||||
func (h *Handlers) APIAdminDashboard(c *gin.Context) {
|
||||
var userCount, postCount, boardCount, commentCount int64
|
||||
model.DB.Model(&model.User{}).Count(&userCount)
|
||||
model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished).Count(&postCount)
|
||||
model.DB.Model(&model.Board{}).Count(&boardCount)
|
||||
model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&commentCount)
|
||||
models.DB.Model(&models.User{}).Count(&userCount)
|
||||
models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPublished).Count(&postCount)
|
||||
models.DB.Model(&models.Board{}).Count(&boardCount)
|
||||
models.DB.Model(&models.Comment{}).Where("status = ?", models.ContentStatusPublished).Count(&commentCount)
|
||||
pendingPosts, _ := h.Post.PendingPostCount()
|
||||
pendingComments, _ := h.Comment.PendingCommentCount()
|
||||
pendingReports, _ := h.Report.PendingCount()
|
||||
pendingFriendLinks, _ := h.FriendLinkApply.PendingCount()
|
||||
recentPosts, _, _ := h.Post.List(service.PostListQuery{
|
||||
recentPosts, _, _ := h.Post.List(services.PostListQuery{
|
||||
Page: 1, Size: 8, ViewerIsAdmin: true, Status: "all",
|
||||
})
|
||||
if recentPosts == nil {
|
||||
recentPosts = []model.Post{}
|
||||
recentPosts = []models.Post{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": userCount, "posts": postCount, "boards": boardCount,
|
||||
@@ -175,7 +175,7 @@ func (h *Handlers) APIAdminPosts(c *gin.Context) {
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
status := strings.TrimSpace(c.DefaultQuery("status", "all"))
|
||||
posts, total, err := h.Post.ListItems(service.PostListQuery{
|
||||
posts, total, err := h.Post.ListItems(services.PostListQuery{
|
||||
Page: page, Size: size, Keyword: keyword,
|
||||
ViewerIsAdmin: true, Status: status,
|
||||
})
|
||||
@@ -184,7 +184,7 @@ func (h *Handlers) APIAdminPosts(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if posts == nil {
|
||||
posts = []service.PostListItem{}
|
||||
posts = []services.PostListItem{}
|
||||
}
|
||||
pending, _ := h.Post.PendingPostCount()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -321,7 +321,7 @@ func (h *Handlers) APIAdminTrashPosts(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if posts == nil {
|
||||
posts = []service.TrashPostItem{}
|
||||
posts = []services.TrashPostItem{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"posts": posts, "total": total, "page": page,
|
||||
@@ -360,7 +360,7 @@ func (h *Handlers) APIAdminComments(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if comments == nil {
|
||||
comments = []model.Comment{}
|
||||
comments = []models.Comment{}
|
||||
}
|
||||
pending, _ := h.Comment.PendingCommentCount()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -374,18 +374,18 @@ func (h *Handlers) APIAdminComments(c *gin.Context) {
|
||||
// APIAdminApproveComment 通过评论审核
|
||||
func (h *Handlers) APIAdminApproveComment(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Comment.SetStatus(uint(id), model.ContentStatusPublished); err != nil {
|
||||
if err := h.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if h.Notify != nil {
|
||||
if comment, err := h.Comment.GetByID(uint(id)); err == nil {
|
||||
comment.Status = model.ContentStatusPublished
|
||||
comment.Status = models.ContentStatusPublished
|
||||
h.Notify.AsyncNotifyCommentPublished(comment)
|
||||
h.Notify.AsyncNotifyCommentMentions(comment)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "评论已通过审核", "status": model.ContentStatusPublished})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "评论已通过审核", "status": models.ContentStatusPublished})
|
||||
}
|
||||
|
||||
// APIAdminRejectComment 拒绝评论并私信通知
|
||||
@@ -404,7 +404,7 @@ func (h *Handlers) APIAdminRejectComment(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.Comment.SetStatus(uint(id), model.ContentStatusRejected); err != nil {
|
||||
if err := h.Comment.SetStatus(uint(id), models.ContentStatusRejected); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -417,13 +417,13 @@ func (h *Handlers) APIAdminRejectComment(c *gin.Context) {
|
||||
_, _ = h.Message.SendSystem(
|
||||
comment.UserID,
|
||||
"评论未通过审核",
|
||||
service.FormatCommentRejectContent(title, comment.PostID, comment.Floor, reason),
|
||||
model.MessageKindReject,
|
||||
services.FormatCommentRejectContent(title, comment.PostID, comment.Floor, reason),
|
||||
models.MessageKindReject,
|
||||
&pid,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已拒绝该评论并通知作者", "status": model.ContentStatusRejected})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已拒绝该评论并通知作者", "status": models.ContentStatusRejected})
|
||||
}
|
||||
|
||||
// APIAdminDeleteComment 管理员软删除评论(进入回收站)
|
||||
@@ -447,7 +447,7 @@ func (h *Handlers) APIAdminTrashComments(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if comments == nil {
|
||||
comments = []service.TrashCommentItem{}
|
||||
comments = []services.TrashCommentItem{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"comments": comments, "total": total, "page": page,
|
||||
@@ -492,7 +492,7 @@ func (h *Handlers) APIAdminUsers(c *gin.Context) {
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
filter := strings.TrimSpace(c.DefaultQuery("filter", "all"))
|
||||
users, total, err := h.User.ListUsers(service.UserListQuery{
|
||||
users, total, err := h.User.ListUsers(services.UserListQuery{
|
||||
Page: page, Size: size, Keyword: keyword, Filter: filter,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -500,10 +500,10 @@ func (h *Handlers) APIAdminUsers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if users == nil {
|
||||
users = []model.User{}
|
||||
users = []models.User{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": model.UsersToAdmin(users), "total": total, "page": page,
|
||||
"users": models.UsersToAdmin(users), "total": total, "page": page,
|
||||
"total_pages": calcTotalPages(total, size),
|
||||
"keyword": keyword,
|
||||
"filter": filter,
|
||||
@@ -560,7 +560,7 @@ func (h *Handlers) APIAdminDownloadBackup(c *gin.Context) {
|
||||
// APIAdminSettings 系统设置信息
|
||||
func (h *Handlers) APIAdminSettings(c *gin.Context) {
|
||||
limits := h.Settings.Limits()
|
||||
filterContent, _ := service.ReadFilterWordsFile(h.Cfg.FilterWordsPath())
|
||||
filterContent, _ := services.ReadFilterWordsFile(h.Cfg.FilterWordsPath())
|
||||
clients, _ := h.Settings.ListOAuthClients()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"filter_path": h.Cfg.FilterWordsPath(),
|
||||
@@ -575,7 +575,7 @@ func (h *Handlers) APIAdminSettings(c *gin.Context) {
|
||||
"storage": h.Settings.StorageConfigPublic(),
|
||||
"branding": h.Settings.SiteBranding(),
|
||||
"filter_words": filterContent,
|
||||
"filter_word_count": service.CountFilterWords(filterContent),
|
||||
"filter_word_count": services.CountFilterWords(filterContent),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ func (h *Handlers) APISiteBranding(c *gin.Context) {
|
||||
|
||||
// APIAdminUpdateBranding 更新站点品牌文案
|
||||
func (h *Handlers) APIAdminUpdateBranding(c *gin.Context) {
|
||||
var req service.SiteBranding
|
||||
var req services.SiteBranding
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -620,7 +620,7 @@ func (h *Handlers) APIAdminUploadBrandingAsset(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "图片不能超过 2MB"})
|
||||
return
|
||||
}
|
||||
url, err := service.SaveUploadedImage(h.Store, file, service.UploadCategorySite, kind)
|
||||
url, err := services.SaveUploadedImage(h.Store, file, services.UploadCategorySite, kind)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -677,7 +677,7 @@ func (h *Handlers) APIAdminClearBrandingAsset(c *gin.Context) {
|
||||
|
||||
// APIAdminUpdateForumSettings 更新论坛设置
|
||||
func (h *Handlers) APIAdminUpdateForumSettings(c *gin.Context) {
|
||||
var req service.ForumLimits
|
||||
var req services.ForumLimits
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -694,7 +694,7 @@ func (h *Handlers) APIAdminUpdateForumSettings(c *gin.Context) {
|
||||
|
||||
// APIAdminUpdateMailSettings 更新邮件 SMTP 配置
|
||||
func (h *Handlers) APIAdminUpdateMailSettings(c *gin.Context) {
|
||||
var req service.MailConfig
|
||||
var req services.MailConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -711,7 +711,7 @@ func (h *Handlers) APIAdminUpdateMailSettings(c *gin.Context) {
|
||||
|
||||
// APIAdminUpdateOIDCSettings 更新 OIDC Provider 全局配置
|
||||
func (h *Handlers) APIAdminUpdateOIDCSettings(c *gin.Context) {
|
||||
var req service.OIDCConfig
|
||||
var req services.OIDCConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -749,7 +749,7 @@ func (h *Handlers) APIProjects(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取失败"})
|
||||
return
|
||||
}
|
||||
list = service.AttachGiteaOwners(list, h.Badge)
|
||||
list = services.AttachGiteaOwners(list, h.Badge)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"projects": list,
|
||||
"total": total,
|
||||
@@ -760,7 +760,7 @@ func (h *Handlers) APIProjects(c *gin.Context) {
|
||||
|
||||
// APIAdminUpdateGiteaSettings 更新 Gitea 同步配置
|
||||
func (h *Handlers) APIAdminUpdateGiteaSettings(c *gin.Context) {
|
||||
var req service.GiteaSyncConfig
|
||||
var req services.GiteaSyncConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -777,7 +777,7 @@ func (h *Handlers) APIAdminUpdateGiteaSettings(c *gin.Context) {
|
||||
|
||||
// APIAdminUpdateStorageSettings 更新上传存储(本地 / S3 兼容),保存后立即热切换
|
||||
func (h *Handlers) APIAdminUpdateStorageSettings(c *gin.Context) {
|
||||
var req service.StorageConfig
|
||||
var req services.StorageConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -799,7 +799,7 @@ func (h *Handlers) APIAdminUpdateStorageSettings(c *gin.Context) {
|
||||
// APIAdminSyncGitea 立即同步 Gitea 公开仓库
|
||||
func (h *Handlers) APIAdminSyncGitea(c *gin.Context) {
|
||||
if h.Gitea == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrGiteaNotConfigured.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrGiteaNotConfigured.Error()})
|
||||
return
|
||||
}
|
||||
n, err := h.Gitea.SyncRepos()
|
||||
@@ -826,7 +826,7 @@ func (h *Handlers) APIAdminListOAuthClients(c *gin.Context) {
|
||||
|
||||
// APIAdminCreateOAuthClient 创建 OAuth 应用
|
||||
func (h *Handlers) APIAdminCreateOAuthClient(c *gin.Context) {
|
||||
var req service.OAuthClientInput
|
||||
var req services.OAuthClientInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -850,7 +850,7 @@ func (h *Handlers) APIAdminUpdateOAuthClient(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效 ID"})
|
||||
return
|
||||
}
|
||||
var req service.OAuthClientInput
|
||||
var req services.OAuthClientInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
@@ -897,16 +897,16 @@ func (h *Handlers) APIAdminTestMail(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写收件邮箱"})
|
||||
return
|
||||
}
|
||||
if err := service.ValidateEmail(req.To); err != nil {
|
||||
if err := services.ValidateEmail(req.To); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !h.Settings.MailReady() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()})
|
||||
return
|
||||
}
|
||||
siteName := h.Settings.SiteBranding().Name
|
||||
err := h.Mail.Send(service.NormalizeEmail(req.To), "邮件配置测试",
|
||||
err := h.Mail.Send(services.NormalizeEmail(req.To), "邮件配置测试",
|
||||
fmt.Sprintf("这是一封来自%s的测试邮件,说明 SMTP 配置正常。", siteName))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -917,14 +917,14 @@ func (h *Handlers) APIAdminTestMail(c *gin.Context) {
|
||||
|
||||
// APIAdminFilterWords 读取敏感词配置
|
||||
func (h *Handlers) APIAdminFilterWords(c *gin.Context) {
|
||||
content, err := service.ReadFilterWordsFile(h.Cfg.FilterWordsPath())
|
||||
content, err := services.ReadFilterWordsFile(h.Cfg.FilterWordsPath())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取敏感词配置失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"content": content,
|
||||
"word_count": service.CountFilterWords(content),
|
||||
"word_count": services.CountFilterWords(content),
|
||||
"path": h.Cfg.FilterWordsPath(),
|
||||
})
|
||||
}
|
||||
@@ -938,13 +938,13 @@ func (h *Handlers) APIAdminUpdateFilterWords(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := service.WriteFilterWordsFile(h.Cfg.FilterWordsPath(), req.Content, h.Filter); err != nil {
|
||||
if err := services.WriteFilterWordsFile(h.Cfg.FilterWordsPath(), req.Content, h.Filter); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存敏感词配置失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "敏感词已保存并生效",
|
||||
"word_count": service.CountFilterWords(req.Content),
|
||||
"word_count": services.CountFilterWords(req.Content),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -959,7 +959,7 @@ func (h *Handlers) APIPosts(c *gin.Context) {
|
||||
author := strings.TrimSpace(c.Query("author"))
|
||||
titleOnly := c.Query("title_only") == "1" || strings.EqualFold(c.Query("title_only"), "true")
|
||||
|
||||
q := service.PostListQuery{
|
||||
q := services.PostListQuery{
|
||||
BoardID: uint(boardID),
|
||||
UserID: uint(userID),
|
||||
Page: page,
|
||||
@@ -982,10 +982,10 @@ func (h *Handlers) APIPosts(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if items == nil {
|
||||
items = []service.PostListItem{}
|
||||
items = []services.PostListItem{}
|
||||
}
|
||||
if h.Badge != nil {
|
||||
users := make([]*model.User, 0, len(items))
|
||||
users := make([]*models.User, 0, len(items))
|
||||
for i := range items {
|
||||
if items[i].User.ID > 0 {
|
||||
users = append(users, &items[i].User)
|
||||
@@ -1012,34 +1012,34 @@ func (h *Handlers) APIPostDetail(c *gin.Context) {
|
||||
}
|
||||
uid := h.currentUserID(c)
|
||||
isAdmin := h.isAdmin(c)
|
||||
if !service.CanViewPost(post, uid, isAdmin) {
|
||||
if !services.CanViewPost(post, uid, isAdmin) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
if c.Query("skip_view") != "1" && post.Status == model.ContentStatusPublished {
|
||||
if c.Query("skip_view") != "1" && post.Status == models.ContentStatusPublished {
|
||||
h.Post.RecordView(uint(id))
|
||||
}
|
||||
// 出口再消毒:兼容库内历史脏 HTML(如 <style>),避免旧帖污染整页
|
||||
post.Content = service.SanitizePostHTML(post.Content)
|
||||
post.Content = services.SanitizePostHTML(post.Content)
|
||||
hasReplied := uid > 0 && h.Comment.HasUserReplied(uint(id), uid)
|
||||
if uid == 0 {
|
||||
post.Content = service.RedactMembersOnlyHTML(post.Content)
|
||||
post.Content = service.RedactReplyOnlyHTML(post.Content)
|
||||
post.Content = services.RedactMembersOnlyHTML(post.Content)
|
||||
post.Content = services.RedactReplyOnlyHTML(post.Content)
|
||||
} else if !isAdmin && post.UserID != uid && !hasReplied {
|
||||
// 作者与管理员始终可见;其他用户需已回复
|
||||
post.Content = service.RedactReplyOnlyHTML(post.Content)
|
||||
post.Content = services.RedactReplyOnlyHTML(post.Content)
|
||||
}
|
||||
// 积分解锁块:作者/站长全文;其他人按解锁记录 redact
|
||||
if isAdmin || post.UserID == uid {
|
||||
post.Content = service.RevealAllPointsOnly(post.Content)
|
||||
post.Content = services.RevealAllPointsOnly(post.Content)
|
||||
} else {
|
||||
unlocked, _ := service.ListUnlockedKeys(uid, uint(id))
|
||||
post.Content = service.RedactPointsOnlyHTML(post.Content, unlocked)
|
||||
unlocked, _ := services.ListUnlockedKeys(uid, uint(id))
|
||||
post.Content = services.RedactPointsOnlyHTML(post.Content, unlocked)
|
||||
}
|
||||
comments, _ := h.Comment.ListByPost(uint(id), uid, isAdmin, post.UserID, h.parseGuestCommentIDs(c))
|
||||
if h.Badge != nil {
|
||||
if post.User.ID > 0 {
|
||||
h.Badge.AttachBadgeSummaries([]*model.User{&post.User}, 3)
|
||||
h.Badge.AttachBadgeSummaries([]*models.User{&post.User}, 3)
|
||||
}
|
||||
h.Badge.AttachBadgeSummariesOnComments(comments, 2)
|
||||
}
|
||||
@@ -1060,21 +1060,21 @@ func (h *Handlers) APIPostDetail(c *gin.Context) {
|
||||
"is_edited": isEdited,
|
||||
"post_edit_window_hours": h.Settings.PostEditWindowHours(),
|
||||
}
|
||||
if post.PostType == model.PostTypePoll {
|
||||
if poll, err := service.GetPollView(uint(id), uid); err == nil {
|
||||
if post.PostType == models.PostTypePoll {
|
||||
if poll, err := services.GetPollView(uint(id), uid); err == nil {
|
||||
resp["poll"] = poll
|
||||
}
|
||||
}
|
||||
if post.PostType == model.PostTypeLottery {
|
||||
if lottery, err := service.GetPostLotteryView(post); err == nil && lottery != nil {
|
||||
if post.PostType == models.PostTypeLottery {
|
||||
if lottery, err := services.GetPostLotteryView(post); err == nil && lottery != nil {
|
||||
resp["lottery"] = lottery
|
||||
}
|
||||
}
|
||||
if post.PostType == model.PostTypeBounty && post.BountyStatus == model.BountyStatusOpen && post.BountyPoints > 0 {
|
||||
canRefund, blockReason := service.CanRefundBounty(post, isAdmin)
|
||||
if post.PostType == models.PostTypeBounty && post.BountyStatus == models.BountyStatusOpen && post.BountyPoints > 0 {
|
||||
canRefund, blockReason := services.CanRefundBounty(post, isAdmin)
|
||||
resp["bounty_can_refund"] = canRefund
|
||||
resp["bounty_refund_block_reason"] = blockReason
|
||||
if n, err := service.CountEligibleBountyReplies(model.DB, post.ID, post.UserID); err == nil {
|
||||
if n, err := services.CountEligibleBountyReplies(models.DB, post.ID, post.UserID); err == nil {
|
||||
resp["bounty_eligible_reply_count"] = n
|
||||
}
|
||||
}
|
||||
@@ -1091,7 +1091,7 @@ func (h *Handlers) APIPostComments(c *gin.Context) {
|
||||
}
|
||||
uid := h.currentUserID(c)
|
||||
isAdmin := h.isAdmin(c)
|
||||
if !service.CanViewPost(post, uid, isAdmin) {
|
||||
if !services.CanViewPost(post, uid, isAdmin) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
@@ -1101,7 +1101,7 @@ func (h *Handlers) APIPostComments(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if comments == nil {
|
||||
comments = []model.Comment{}
|
||||
comments = []models.Comment{}
|
||||
}
|
||||
if h.Badge != nil {
|
||||
h.Badge.AttachBadgeSummariesOnComments(comments, 2)
|
||||
@@ -1138,7 +1138,7 @@ func (h *Handlers) APIRecentComments(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []service.RecentCommentItem{}
|
||||
list = []services.RecentCommentItem{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"comments": list})
|
||||
}
|
||||
@@ -1151,7 +1151,7 @@ func (h *Handlers) APIRecentUsers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []service.RecentUserItem{}
|
||||
list = []services.RecentUserItem{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"users": list})
|
||||
}
|
||||
@@ -1166,7 +1166,7 @@ func (h *Handlers) APIFavorites(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if favs == nil {
|
||||
favs = []model.PostFavorite{}
|
||||
favs = []models.PostFavorite{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"favorites": favs, "total": total, "page": page})
|
||||
}
|
||||
@@ -1217,6 +1217,6 @@ func (h *Handlers) APIPostRevisionDetail(c *gin.Context) {
|
||||
}
|
||||
|
||||
func isClientLimitError(err error) bool {
|
||||
return errors.Is(err, service.ErrSearchKeywordTooShort) ||
|
||||
errors.Is(err, service.ErrSearchKeywordTooLong)
|
||||
return errors.Is(err, services.ErrSearchKeywordTooShort) ||
|
||||
errors.Is(err, services.ErrSearchKeywordTooLong)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// APIMePoints 余额与流水
|
||||
@@ -53,7 +53,7 @@ func (h *Handlers) APIMeCheckInGet(c *gin.Context) {
|
||||
func (h *Handlers) APIMeCheckIn(c *gin.Context) {
|
||||
st, err := h.Points.CheckIn(h.currentUserID(c))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrAlreadyCheckedIn) {
|
||||
if errors.Is(err, services.ErrAlreadyCheckedIn) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func (h *Handlers) APIUnlockPostBlock(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 block_key"})
|
||||
return
|
||||
}
|
||||
res, err := service.UnlockPointsBlock(h.currentUserID(c), uint(id), req.BlockKey)
|
||||
res, err := services.UnlockPointsBlock(h.currentUserID(c), uint(id), req.BlockKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -120,7 +120,7 @@ func (h *Handlers) APIAdminVerifyUser(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := service.SetVerified(uint(id), req.Verified); err != nil {
|
||||
if err := services.SetVerified(uint(id), req.Verified); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -141,11 +141,11 @@ func (h *Handlers) APIAdminSetUserLevel(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := service.SetUserLevel(uint(id), req.Level); err != nil {
|
||||
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": model.ExpForLevel(req.Level)})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "等级已更新", "level": req.Level, "exp": models.ExpForLevel(req.Level)})
|
||||
}
|
||||
|
||||
// APIAdminAdjustPoints 调积分
|
||||
@@ -179,7 +179,7 @@ func (h *Handlers) APIAdminListBadges(c *gin.Context) {
|
||||
|
||||
// APIAdminUpsertBadge 创建/更新徽章定义
|
||||
func (h *Handlers) APIAdminUpsertBadge(c *gin.Context) {
|
||||
var def model.BadgeDef
|
||||
var def models.BadgeDef
|
||||
if err := c.ShouldBindJSON(&def); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ func (h *Handlers) APIApplyFriendLink(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
result, err := h.FriendLinkApply.Create(service.FriendLinkApplyInput{
|
||||
result, err := h.FriendLinkApply.Create(services.FriendLinkApplyInput{
|
||||
UserID: uid,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
@@ -65,10 +65,10 @@ func (h *Handlers) APIUploadFriendLinkLogo(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "图片文件过大"})
|
||||
return
|
||||
}
|
||||
url, err := service.SaveUploadedImage(
|
||||
url, err := services.SaveUploadedImage(
|
||||
h.Store,
|
||||
file,
|
||||
service.UploadCategorySite,
|
||||
services.UploadCategorySite,
|
||||
fmt.Sprintf("fl_%d", uid),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -83,7 +83,7 @@ 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(service.FriendLinkApplyListQuery{
|
||||
list, total, err := h.FriendLinkApply.ListAdmin(services.FriendLinkApplyListQuery{
|
||||
Page: page, Size: size, Status: status,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -236,7 +236,7 @@ func (h *Handlers) APIUpdateFriendLinkApply(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
result, err := h.FriendLinkApply.Update(uid, uint(id), service.FriendLinkApplyInput{
|
||||
result, err := h.FriendLinkApply.Update(uid, uint(id), services.FriendLinkApplyInput{
|
||||
UserID: uid,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -10,58 +10,58 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/auth"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// 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
|
||||
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
|
||||
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, token string) {
|
||||
c.SetCookie(middleware.CookieName, token, int(service.TokenExpire.Seconds()), "/", "", false, true)
|
||||
c.SetCookie(auth.CookieName, token, int(services.TokenExpire.Seconds()), "/", "", false, true)
|
||||
}
|
||||
|
||||
func (h *Handlers) currentUserID(c *gin.Context) uint {
|
||||
if v, ok := c.Get(middleware.CtxUserID); ok {
|
||||
if v, ok := c.Get(auth.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
|
||||
if v, ok := c.Get(auth.CtxRole); ok {
|
||||
return v == models.RoleAdmin
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadCurrentUser 加载当前登录用户完整资料(含认证/积分)
|
||||
func (h *Handlers) loadCurrentUser(c *gin.Context) (*model.User, error) {
|
||||
func (h *Handlers) loadCurrentUser(c *gin.Context) (*models.User, error) {
|
||||
uid := h.currentUserID(c)
|
||||
if uid == 0 {
|
||||
return nil, errors.New("未登录")
|
||||
@@ -133,7 +133,7 @@ func (h *Handlers) APIRegisterConfig(c *gin.Context) {
|
||||
"mail_ready": mailReady,
|
||||
"require_email_code": mailReady,
|
||||
"register_open": userCount == 0 || mailReady,
|
||||
"email_code_len": service.EmailCodeLen,
|
||||
"email_code_len": services.EmailCodeLen,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ func (h *Handlers) APISendRegisterEmailCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !h.Settings.MailReady() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.EmailCode.SendRegisterCode(req.Email); err != nil {
|
||||
@@ -167,7 +167,7 @@ func (h *Handlers) APISendResetEmailCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !h.Settings.MailReady() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.EmailCode.SendResetCode(req.Email); err != nil {
|
||||
@@ -189,11 +189,11 @@ func (h *Handlers) APIResetPassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !h.Settings.MailReady() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()})
|
||||
return
|
||||
}
|
||||
if !h.EmailCode.VerifyPurpose(service.EmailCodePurposeReset, req.Email, req.EmailCode) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrEmailCodeInvalid.Error()})
|
||||
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 {
|
||||
@@ -244,12 +244,12 @@ func (h *Handlers) APIRegister(c *gin.Context) {
|
||||
userCount := h.Auth.UserCount()
|
||||
mailReady := h.Settings.MailReady()
|
||||
if userCount > 0 && !mailReady {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrRegisterClosed.Error()})
|
||||
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": service.ErrEmailCodeInvalid.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrEmailCodeInvalid.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func (h *Handlers) APILogin(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handlers) APILogout(c *gin.Context) {
|
||||
c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true)
|
||||
c.SetCookie(auth.CookieName, "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已退出"})
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ func (h *Handlers) APIUserPublic(c *gin.Context) {
|
||||
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)
|
||||
view.Badges = services.BadgeViews(badges, 0)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -396,10 +396,10 @@ func (h *Handlers) APIUploadPostImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
uid := h.currentUserID(c)
|
||||
url, err := service.SaveUploadedImage(
|
||||
url, err := services.SaveUploadedImage(
|
||||
h.Store,
|
||||
file,
|
||||
service.UploadCategoryPosts,
|
||||
services.UploadCategoryPosts,
|
||||
fmt.Sprintf("%d", uid),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -421,20 +421,20 @@ func (h *Handlers) APICreatePost(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
extras := service.ParsePostExtrasFromForm(
|
||||
extras := services.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 {
|
||||
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 == model.ContentStatusPending {
|
||||
if post.Status == models.ContentStatusPending {
|
||||
msg = "已提交审核,通过后将公开显示"
|
||||
if h.Notify != nil {
|
||||
h.Notify.AsyncNotifyPendingPost(post)
|
||||
@@ -479,8 +479,8 @@ func (h *Handlers) APIToggleLike(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var post model.Post
|
||||
model.DB.First(&post, id)
|
||||
var post models.Post
|
||||
models.DB.First(&post, id)
|
||||
c.JSON(http.StatusOK, gin.H{"liked": liked, "like_count": post.LikeCount})
|
||||
}
|
||||
|
||||
@@ -535,7 +535,7 @@ func (h *Handlers) APICreateComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
in := service.CommentCreateInput{
|
||||
in := services.CommentCreateInput{
|
||||
UserID: uid,
|
||||
PostID: uint(postID),
|
||||
Content: content,
|
||||
@@ -551,14 +551,14 @@ func (h *Handlers) APICreateComment(c *gin.Context) {
|
||||
msg := "评论成功"
|
||||
if h.Notify != nil {
|
||||
switch comment.Status {
|
||||
case model.ContentStatusPublished:
|
||||
case models.ContentStatusPublished:
|
||||
h.Notify.AsyncNotifyCommentPublished(comment)
|
||||
h.Notify.AsyncNotifyCommentMentions(comment)
|
||||
case model.ContentStatusPending:
|
||||
case models.ContentStatusPending:
|
||||
msg = "评论已提交,审核通过后公开显示"
|
||||
h.Notify.AsyncNotifyPendingComment(comment)
|
||||
}
|
||||
} else if comment.Status == model.ContentStatusPending {
|
||||
} else if comment.Status == models.ContentStatusPending {
|
||||
msg = "评论已提交,审核通过后公开显示"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": msg, "floor": comment.Floor, "id": comment.ID, "status": comment.Status})
|
||||
@@ -586,7 +586,7 @@ func (h *Handlers) APIUpdateComment(c *gin.Context) {
|
||||
status := ""
|
||||
if comment, e := h.Comment.GetByID(uint(id)); e == nil {
|
||||
status = comment.Status
|
||||
if status == model.ContentStatusPending && !h.isAdmin(c) {
|
||||
if status == models.ContentStatusPending && !h.isAdmin(c) {
|
||||
msg = "评论已更新,审核通过后公开显示"
|
||||
}
|
||||
if enteredPending && h.Notify != nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"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(service.ConversationListQuery{
|
||||
list, total, err := h.Message.ListConversations(services.ConversationListQuery{
|
||||
UserID: h.currentUserID(c),
|
||||
Page: page,
|
||||
Size: size,
|
||||
@@ -41,7 +41,7 @@ func (h *Handlers) APIConversationMessages(c *gin.Context) {
|
||||
before, _ := strconv.ParseUint(c.DefaultQuery("before", "0"), 10, 64)
|
||||
uid := h.currentUserID(c)
|
||||
|
||||
list, total, err := h.Message.ListConversationMessages(service.ConversationMessagesQuery{
|
||||
list, total, err := h.Message.ListConversationMessages(services.ConversationMessagesQuery{
|
||||
UserID: uid,
|
||||
PeerID: uint(peerID),
|
||||
Page: page,
|
||||
@@ -63,10 +63,10 @@ func (h *Handlers) APIConversationMessages(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var peer *model.User
|
||||
var peer *models.User
|
||||
if peerID > 0 {
|
||||
var u model.User
|
||||
if err := model.DB.First(&u, uint(peerID)).Error; err == nil {
|
||||
var u models.User
|
||||
if err := models.DB.First(&u, uint(peerID)).Error; err == nil {
|
||||
peer = &u
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func (h *Handlers) APISendMessage(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
msg, err := h.Message.Send(service.MessageSendInput{
|
||||
msg, err := h.Message.Send(services.MessageSendInput{
|
||||
FromUserID: h.currentUserID(c),
|
||||
ToUserID: req.ToUserID,
|
||||
Subject: req.Subject,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/middleware"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/auth"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// OIDCDiscovery OpenID Provider 元数据
|
||||
@@ -47,7 +47,7 @@ func (h *Handlers) OIDCAuthorize(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
req := service.AuthorizeRequest{
|
||||
req := services.AuthorizeRequest{
|
||||
ClientID: c.Query("client_id"),
|
||||
RedirectURI: c.Query("redirect_uri"),
|
||||
ResponseType: c.Query("response_type"),
|
||||
@@ -60,7 +60,7 @@ func (h *Handlers) OIDCAuthorize(c *gin.Context) {
|
||||
|
||||
if err := h.OIDC.ValidateAuthorize(req); err != nil {
|
||||
// redirect_uri 未通过校验时不能重定向,避免开放重定向
|
||||
if errors.Is(err, service.ErrOIDCInvalidRedirect) || errors.Is(err, service.ErrOIDCInvalidClient) {
|
||||
if errors.Is(err, services.ErrOIDCInvalidRedirect) || errors.Is(err, services.ErrOIDCInvalidClient) {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (h *Handlers) OIDCAuthorize(c *gin.Context) {
|
||||
|
||||
callback, err := h.OIDC.IssueAuthCode(uid, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOIDCUserBanned) {
|
||||
if errors.Is(err, services.ErrOIDCUserBanned) {
|
||||
h.oidcErrorRedirect(c, req.RedirectURI, req.State, "access_denied", "账号已被禁言")
|
||||
return
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (h *Handlers) OIDCToken(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := h.OIDC.ExchangeCode(service.TokenRequest{
|
||||
resp, err := h.OIDC.ExchangeCode(services.TokenRequest{
|
||||
GrantType: c.PostForm("grant_type"),
|
||||
Code: c.PostForm("code"),
|
||||
RedirectURI: c.PostForm("redirect_uri"),
|
||||
@@ -133,12 +133,12 @@ func (h *Handlers) OIDCToken(c *gin.Context) {
|
||||
status := http.StatusBadRequest
|
||||
code := "invalid_grant"
|
||||
switch {
|
||||
case errors.Is(err, service.ErrOIDCInvalidClient):
|
||||
case errors.Is(err, services.ErrOIDCInvalidClient):
|
||||
status = http.StatusUnauthorized
|
||||
code = "invalid_client"
|
||||
case errors.Is(err, service.ErrOIDCInvalidRequest):
|
||||
case errors.Is(err, services.ErrOIDCInvalidRequest):
|
||||
code = "invalid_request"
|
||||
case errors.Is(err, service.ErrOIDCPKCEFailed):
|
||||
case errors.Is(err, services.ErrOIDCPKCEFailed):
|
||||
code = "invalid_grant"
|
||||
}
|
||||
c.JSON(status, gin.H{"error": code, "error_description": err.Error()})
|
||||
@@ -179,7 +179,7 @@ func (h *Handlers) OIDCLogout(c *gin.Context) {
|
||||
state = c.PostForm("state")
|
||||
}
|
||||
|
||||
c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true)
|
||||
c.SetCookie(auth.CookieName, "", -1, "/", "", false, true)
|
||||
|
||||
if h.OIDC == nil {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// APICreatePostReport 举报帖子
|
||||
@@ -53,7 +53,7 @@ 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(service.ReportListQuery{
|
||||
list, total, err := h.Report.ListAdmin(services.ReportListQuery{
|
||||
Status: status,
|
||||
Page: page,
|
||||
Size: size,
|
||||
@@ -84,7 +84,7 @@ func (h *Handlers) APIAdminHandleReport(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
rep, err := h.Report.Handle(service.HandleReportInput{
|
||||
rep, err := h.Report.Handle(services.HandleReportInput{
|
||||
ReportID: uint(id),
|
||||
HandlerID: h.currentUserID(c),
|
||||
Action: req.Action,
|
||||
@@ -101,11 +101,11 @@ func (h *Handlers) APIAdminHandleReport(c *gin.Context) {
|
||||
// APIAdminApprovePost 通过帖子审核
|
||||
func (h *Handlers) APIAdminApprovePost(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Post.SetStatus(uint(id), model.ContentStatusPublished); err != nil {
|
||||
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": model.ContentStatusPublished})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "帖子已通过审核", "status": models.ContentStatusPublished})
|
||||
}
|
||||
|
||||
// APIAdminRejectPost 拒绝帖子并私信通知作者(标记为 rejected,不进回收站)
|
||||
@@ -133,7 +133,7 @@ func (h *Handlers) APIAdminRejectPost(c *gin.Context) {
|
||||
title := post.Title
|
||||
postID := post.ID
|
||||
|
||||
if err := h.Post.SetStatus(postID, model.ContentStatusRejected); err != nil {
|
||||
if err := h.Post.SetStatus(postID, models.ContentStatusRejected); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -142,8 +142,8 @@ func (h *Handlers) APIAdminRejectPost(c *gin.Context) {
|
||||
_, msgErr := h.Message.SendSystem(
|
||||
authorID,
|
||||
"帖子《"+title+"》未通过审核",
|
||||
service.FormatRejectContent(title, postID, reason),
|
||||
model.MessageKindReject,
|
||||
services.FormatRejectContent(title, postID, reason),
|
||||
models.MessageKindReject,
|
||||
&pid,
|
||||
nil,
|
||||
)
|
||||
@@ -151,13 +151,13 @@ func (h *Handlers) APIAdminRejectPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "帖子已拒绝,但私信通知失败:" + msgErr.Error(),
|
||||
"notified": false,
|
||||
"status": model.ContentStatusRejected,
|
||||
"status": models.ContentStatusRejected,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "已拒绝该帖并私信通知作者",
|
||||
"notified": true,
|
||||
"status": model.ContentStatusRejected,
|
||||
"status": models.ContentStatusRejected,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/embed_static"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/seo"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -60,7 +60,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) {
|
||||
|
||||
now := time.Now().UTC()
|
||||
permalink := h.Settings.Permalink()
|
||||
urls := []service.SitemapURL{
|
||||
urls := []services.SitemapURL{
|
||||
{Loc: base + "/", LastMod: now, ChangeFreq: "hourly", Priority: "1.0"},
|
||||
{Loc: base + "/projects", LastMod: now, ChangeFreq: "daily", Priority: "0.6"},
|
||||
{Loc: base + "/links", LastMod: now, ChangeFreq: "weekly", Priority: "0.6"},
|
||||
@@ -68,8 +68,8 @@ func (h *Handlers) SitemapXML(c *gin.Context) {
|
||||
|
||||
if boards, err := h.Board.List(); err == nil {
|
||||
for _, board := range boards {
|
||||
urls = append(urls, service.SitemapURL{
|
||||
Loc: base + service.QueryBoardHome(board.ID, permalink),
|
||||
urls = append(urls, services.SitemapURL{
|
||||
Loc: base + services.QueryBoardHome(board.ID, permalink),
|
||||
LastMod: board.UpdatedAt.UTC(),
|
||||
ChangeFreq: "daily",
|
||||
Priority: "0.7",
|
||||
@@ -83,7 +83,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) {
|
||||
if lm.IsZero() {
|
||||
lm = p.CreatedAt
|
||||
}
|
||||
urls = append(urls, service.SitemapURL{
|
||||
urls = append(urls, services.SitemapURL{
|
||||
Loc: base + permalink.PostPath(p.ID),
|
||||
LastMod: lm.UTC(),
|
||||
ChangeFreq: "weekly",
|
||||
@@ -94,7 +94,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) {
|
||||
|
||||
if users, e2 := h.User.ListSitemap(seoSitemapLimit); e2 == nil {
|
||||
for _, u := range users {
|
||||
urls = append(urls, service.SitemapURL{
|
||||
urls = append(urls, services.SitemapURL{
|
||||
Loc: base + permalink.UserPath(u.ID),
|
||||
LastMod: u.UpdatedAt.UTC(),
|
||||
ChangeFreq: "weekly",
|
||||
@@ -109,7 +109,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) {
|
||||
if lm.IsZero() {
|
||||
lm = p.CreatedAt
|
||||
}
|
||||
urls = append(urls, service.SitemapURL{
|
||||
urls = append(urls, services.SitemapURL{
|
||||
Loc: base + permalink.PagePath(p.Slug),
|
||||
LastMod: lm.UTC(),
|
||||
ChangeFreq: "monthly",
|
||||
@@ -160,14 +160,14 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
if siteName == "" {
|
||||
siteName = "姜十三论坛"
|
||||
}
|
||||
defaultImage := service.AbsoluteURL(base, brand.DefaultShareImage())
|
||||
defaultImage := services.AbsoluteURL(base, brand.DefaultShareImage())
|
||||
siteKeywords := brand.MetaKeywords()
|
||||
permalink := h.Settings.Permalink()
|
||||
|
||||
// 旧版 /?board=id → 规范板块路径
|
||||
if path == "/" || path == "" {
|
||||
if boardID, err := strconv.ParseUint(c.Query("board"), 10, 64); err == nil && boardID > 0 {
|
||||
target := service.QueryBoardHome(uint(boardID), permalink)
|
||||
target := services.QueryBoardHome(uint(boardID), permalink)
|
||||
if q := c.Request.URL.RawQuery; q != "" {
|
||||
// 保留 sort/keyword 等 query,去掉 board
|
||||
vals := c.Request.URL.Query()
|
||||
@@ -181,7 +181,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
isBot := service.IsSEOCrawler(c.Request.UserAgent())
|
||||
isBot := services.IsSEOCrawler(c.Request.UserAgent())
|
||||
if isBot {
|
||||
c.Header("Vary", "User-Agent")
|
||||
}
|
||||
@@ -201,11 +201,11 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
if desc == "" {
|
||||
desc = brand.MetaDescription()
|
||||
}
|
||||
meta := attachSiteSEO(&embed_static.SPAPageMeta{
|
||||
meta := attachSiteSEO(&seo.PageMeta{
|
||||
Title: pageTitle(board.Name, siteName),
|
||||
Description: service.TruncateRunes(desc, seoDescMax),
|
||||
Keywords: service.JoinSEOKeywords(board.Name, siteKeywords),
|
||||
Canonical: service.AbsoluteURL(base, bm.Canonical),
|
||||
Description: services.TruncateRunes(desc, seoDescMax),
|
||||
Keywords: services.JoinSEOKeywords(board.Name, siteKeywords),
|
||||
Canonical: services.AbsoluteURL(base, bm.Canonical),
|
||||
OGType: "website",
|
||||
OGImage: defaultImage,
|
||||
}, siteName, siteKeywords)
|
||||
@@ -213,7 +213,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botBoardHTML(meta, *board)))
|
||||
return
|
||||
}
|
||||
embed_static.ServeSPAWithMeta(c, meta)
|
||||
servePendingSSR(c, meta.Title, `<p>板块页 SSR 迁移中,请先从 <a href="/">首页</a> 浏览。</p>`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -224,16 +224,16 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
post, err := h.Post.FindByID(pm.ID)
|
||||
if err != nil || !service.CanViewPost(post, h.currentUserID(c), h.isAdmin(c)) {
|
||||
if err != nil || !services.CanViewPost(post, h.currentUserID(c), h.isAdmin(c)) {
|
||||
h.serveNotFound(c, base, siteName, siteKeywords, path, isBot)
|
||||
return
|
||||
}
|
||||
postKeywords := service.JoinSEOKeywords(post.Board.Name, siteKeywords)
|
||||
postKeywords := services.JoinSEOKeywords(post.Board.Name, siteKeywords)
|
||||
if isBot {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botPostHTML(base, siteName, defaultImage, postKeywords, post)))
|
||||
return
|
||||
}
|
||||
embed_static.ServeSPAWithMeta(c, attachSiteSEO(h.postPageMeta(base, siteName, defaultImage, post), siteName, postKeywords))
|
||||
servePendingSSR(c, pageTitle(post.Title, siteName), `<p>帖子详情 SSR 迁移中,请先从 <a href="/">首页</a> 返回。</p>`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botUserHTML(base, siteName, defaultImage, siteKeywords, user)))
|
||||
return
|
||||
}
|
||||
embed_static.ServeSPAWithMeta(c, attachSiteSEO(h.userPageMeta(base, siteName, defaultImage, user), siteName, siteKeywords))
|
||||
servePendingSSR(c, pageTitle(user.Nickname, siteName), `<p>用户主页 SSR 迁移中,请先从 <a href="/">首页</a> 返回。</p>`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -267,12 +267,12 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
h.serveNotFound(c, base, siteName, siteKeywords, path, isBot)
|
||||
return
|
||||
}
|
||||
desc := service.ExcerptFromHTML(page.Content, seoDescMax)
|
||||
meta := attachSiteSEO(&embed_static.SPAPageMeta{
|
||||
desc := services.ExcerptFromHTML(page.Content, seoDescMax)
|
||||
meta := attachSiteSEO(&seo.PageMeta{
|
||||
Title: pageTitle(page.Title, siteName),
|
||||
Description: desc,
|
||||
Keywords: service.JoinSEOKeywords(page.Title, siteKeywords),
|
||||
Canonical: service.AbsoluteURL(base, pg.Canonical),
|
||||
Keywords: services.JoinSEOKeywords(page.Title, siteKeywords),
|
||||
Canonical: services.AbsoluteURL(base, pg.Canonical),
|
||||
OGType: "article",
|
||||
OGImage: defaultImage,
|
||||
}, siteName, siteKeywords)
|
||||
@@ -281,7 +281,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(renderBotHTML(meta, body)))
|
||||
return
|
||||
}
|
||||
embed_static.ServeSPAWithMeta(c, meta)
|
||||
servePendingSSR(c, meta.Title, page.Content)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,13 +291,23 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 其余已知路由:SPA + head meta;首页对爬虫额外返回可读正文
|
||||
// 其余已知路由:爬虫可读首页;用户走占位页(首页本身已由 routers/web SSR)
|
||||
meta := h.buildSPAPageMeta(c, path, brand, base, siteName, defaultImage)
|
||||
if isBot && (path == "/" || path == "") {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botHomeHTML(meta, brand)))
|
||||
return
|
||||
}
|
||||
embed_static.ServeSPAWithMeta(c, meta)
|
||||
servePendingSSR(c, meta.Title, `<p>该页面 SSR 迁移中。<a href="/">返回首页</a></p>`)
|
||||
}
|
||||
|
||||
func servePendingSSR(c *gin.Context, title, bodyHTML string) {
|
||||
if strings.TrimSpace(title) == "" {
|
||||
title = "姜十三论坛"
|
||||
}
|
||||
page := fmt.Sprintf(`<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>%s</title><link rel="stylesheet" href="/ssr-assets/site.css"/></head><body class="j13-body"><main class="j13-main" style="max-width:800px;margin:2rem auto;padding:1rem">%s</main></body></html>`,
|
||||
html.EscapeString(title), bodyHTML)
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(page))
|
||||
}
|
||||
|
||||
func (h *Handlers) serveNotFound(c *gin.Context, base, siteName, keywords, path string, isBot bool) {
|
||||
@@ -306,14 +316,17 @@ func (h *Handlers) serveNotFound(c *gin.Context, base, siteName, keywords, path
|
||||
c.Data(http.StatusNotFound, "text/html; charset=utf-8", []byte(botNotFoundHTML(base, siteName, keywords, path)))
|
||||
return
|
||||
}
|
||||
embed_static.ServeSPAWithMeta(c, notFoundPageMeta(base, siteName, keywords, path))
|
||||
page := fmt.Sprintf(`<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"/><title>%s</title></head><body><h1>404</h1><p>页面不存在。</p><p><a href="/">返回首页</a></p></body></html>`,
|
||||
html.EscapeString(pageTitle("页面不存在", siteName)))
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Data(http.StatusNotFound, "text/html; charset=utf-8", []byte(page))
|
||||
}
|
||||
|
||||
func notFoundPageMeta(base, siteName, keywords, path string) *embed_static.SPAPageMeta {
|
||||
return attachSiteSEO(&embed_static.SPAPageMeta{
|
||||
func notFoundPageMeta(base, siteName, keywords, path string) *seo.PageMeta {
|
||||
return attachSiteSEO(&seo.PageMeta{
|
||||
Title: pageTitle("页面不存在", siteName),
|
||||
Description: "您访问的页面不存在或已删除",
|
||||
Canonical: service.AbsoluteURL(base, path),
|
||||
Canonical: services.AbsoluteURL(base, path),
|
||||
OGType: "website",
|
||||
Robots: "noindex,follow",
|
||||
Status: http.StatusNotFound,
|
||||
@@ -321,7 +334,7 @@ func notFoundPageMeta(base, siteName, keywords, path string) *embed_static.SPAPa
|
||||
}
|
||||
|
||||
// attachSiteSEO 填充站点级 keywords / og:site_name / og:locale
|
||||
func attachSiteSEO(meta *embed_static.SPAPageMeta, siteName, keywords string) *embed_static.SPAPageMeta {
|
||||
func attachSiteSEO(meta *seo.PageMeta, siteName, keywords string) *seo.PageMeta {
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -341,7 +354,7 @@ func isKnownPublicPath(path string) bool {
|
||||
if seoPostEditRe.MatchString(path) {
|
||||
return true
|
||||
}
|
||||
permalink := service.PermalinkConfig{}
|
||||
permalink := services.PermalinkConfig{}
|
||||
if permalink.MatchBoardPath(path).OK {
|
||||
return true
|
||||
}
|
||||
@@ -351,15 +364,15 @@ func isKnownPublicPath(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand service.SiteBranding, base, siteName, defaultImage string) *embed_static.SPAPageMeta {
|
||||
func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand services.SiteBranding, base, siteName, defaultImage string) *seo.PageMeta {
|
||||
siteTitle := brand.DocumentTitle()
|
||||
homeDesc := service.TruncateRunes(brand.MetaDescription(), seoDescMax)
|
||||
homeDesc := services.TruncateRunes(brand.MetaDescription(), seoDescMax)
|
||||
siteKeywords := brand.MetaKeywords()
|
||||
meta := attachSiteSEO(&embed_static.SPAPageMeta{
|
||||
meta := attachSiteSEO(&seo.PageMeta{
|
||||
Title: siteTitle,
|
||||
Description: homeDesc,
|
||||
Keywords: siteKeywords,
|
||||
Canonical: service.AbsoluteURL(base, pathWithQuery(c)),
|
||||
Canonical: services.AbsoluteURL(base, pathWithQuery(c)),
|
||||
OGType: "website",
|
||||
OGImage: defaultImage,
|
||||
}, siteName, siteKeywords)
|
||||
@@ -379,9 +392,9 @@ func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand service.S
|
||||
desc = brand.MetaDescription()
|
||||
}
|
||||
meta.Title = pageTitle(board.Name, siteName)
|
||||
meta.Description = service.TruncateRunes(desc, seoDescMax)
|
||||
meta.Canonical = service.AbsoluteURL(base, service.QueryBoardHome(board.ID, h.Settings.Permalink()))
|
||||
meta.Keywords = service.JoinSEOKeywords(board.Name, siteKeywords)
|
||||
meta.Description = services.TruncateRunes(desc, seoDescMax)
|
||||
meta.Canonical = services.AbsoluteURL(base, services.QueryBoardHome(board.ID, h.Settings.Permalink()))
|
||||
meta.Keywords = services.JoinSEOKeywords(board.Name, siteKeywords)
|
||||
return meta
|
||||
}
|
||||
// 无效板块 id:仍显示首页,但可标记 noindex
|
||||
@@ -393,38 +406,38 @@ func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand service.S
|
||||
"@type": "WebSite",
|
||||
"name": siteName,
|
||||
"description": meta.Description,
|
||||
"url": service.AbsoluteURL(base, "/"),
|
||||
"url": services.AbsoluteURL(base, "/"),
|
||||
})
|
||||
}
|
||||
|
||||
if path == "/projects" {
|
||||
meta.Title = pageTitle("项目", siteName)
|
||||
meta.Description = service.TruncateRunes(siteName+" 的公开项目列表", seoDescMax)
|
||||
meta.Keywords = service.JoinSEOKeywords("项目", siteKeywords)
|
||||
meta.Description = services.TruncateRunes(siteName+" 的公开项目列表", seoDescMax)
|
||||
meta.Keywords = services.JoinSEOKeywords("项目", siteKeywords)
|
||||
}
|
||||
|
||||
if path == "/links" {
|
||||
meta.Title = pageTitle("友情链接", siteName)
|
||||
meta.Description = service.TruncateRunes(siteName+" 的友情链接与申请入口", seoDescMax)
|
||||
meta.Keywords = service.JoinSEOKeywords("友情链接", siteKeywords)
|
||||
meta.Description = services.TruncateRunes(siteName+" 的友情链接与申请入口", seoDescMax)
|
||||
meta.Keywords = services.JoinSEOKeywords("友情链接", siteKeywords)
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model.Post) *embed_static.SPAPageMeta {
|
||||
func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *models.Post) *seo.PageMeta {
|
||||
permalink := h.Settings.Permalink()
|
||||
content := service.RedactGatedPostHTML(post.Content)
|
||||
content := services.RedactGatedPostHTML(post.Content)
|
||||
plain := post.ContentPlain
|
||||
if plain == "" {
|
||||
plain = service.StripHTMLForSearch(content)
|
||||
plain = services.StripHTMLForSearch(content)
|
||||
}
|
||||
desc := service.TruncateRunes(plain, seoDescMax)
|
||||
author := service.DisplayName(&post.User)
|
||||
canonical := service.AbsoluteURL(base, permalink.PostPath(post.ID))
|
||||
ogImage := service.AbsoluteURL(base, service.FirstImageURL(content))
|
||||
desc := services.TruncateRunes(plain, seoDescMax)
|
||||
author := services.DisplayName(&post.User)
|
||||
canonical := services.AbsoluteURL(base, permalink.PostPath(post.ID))
|
||||
ogImage := services.AbsoluteURL(base, services.FirstImageURL(content))
|
||||
if ogImage == "" {
|
||||
ogImage = service.AbsoluteURL(base, post.User.Avatar)
|
||||
ogImage = services.AbsoluteURL(base, post.User.Avatar)
|
||||
}
|
||||
if ogImage == "" {
|
||||
ogImage = defaultImage
|
||||
@@ -442,7 +455,7 @@ func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model
|
||||
"author": map[string]any{
|
||||
"@type": "Person",
|
||||
"name": author,
|
||||
"url": service.AbsoluteURL(base, permalink.UserPath(post.UserID)),
|
||||
"url": services.AbsoluteURL(base, permalink.UserPath(post.UserID)),
|
||||
},
|
||||
"interactionStatistic": map[string]any{
|
||||
"@type": "InteractionCounter",
|
||||
@@ -456,12 +469,12 @@ func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model
|
||||
if ogImage != "" {
|
||||
jsonld["image"] = []string{ogImage}
|
||||
}
|
||||
body := service.TruncateRunes(plain, seoPrerenderMax)
|
||||
body := services.TruncateRunes(plain, seoPrerenderMax)
|
||||
if body != "" {
|
||||
jsonld["articleBody"] = body
|
||||
}
|
||||
|
||||
return &embed_static.SPAPageMeta{
|
||||
return &seo.PageMeta{
|
||||
Title: pageTitle(post.Title, siteName),
|
||||
Description: desc,
|
||||
Canonical: canonical,
|
||||
@@ -471,16 +484,16 @@ func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *model.User) *embed_static.SPAPageMeta {
|
||||
func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *models.User) *seo.PageMeta {
|
||||
permalink := h.Settings.Permalink()
|
||||
name := service.DisplayName(user)
|
||||
name := services.DisplayName(user)
|
||||
desc := strings.TrimSpace(user.Signature)
|
||||
if desc == "" {
|
||||
desc = name + " 的主页"
|
||||
}
|
||||
desc = service.TruncateRunes(desc, seoDescMax)
|
||||
canonical := service.AbsoluteURL(base, permalink.UserPath(user.ID))
|
||||
ogImage := service.AbsoluteURL(base, user.Avatar)
|
||||
desc = services.TruncateRunes(desc, seoDescMax)
|
||||
canonical := services.AbsoluteURL(base, permalink.UserPath(user.ID))
|
||||
ogImage := services.AbsoluteURL(base, user.Avatar)
|
||||
if ogImage == "" {
|
||||
ogImage = defaultImage
|
||||
}
|
||||
@@ -500,7 +513,7 @@ func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *model
|
||||
jsonld["mainEntity"].(map[string]any)["image"] = ogImage
|
||||
}
|
||||
|
||||
return &embed_static.SPAPageMeta{
|
||||
return &seo.PageMeta{
|
||||
Title: pageTitle(name+" 的主页", siteName),
|
||||
Description: desc,
|
||||
Canonical: canonical,
|
||||
@@ -538,13 +551,13 @@ func pathWithQuery(c *gin.Context) string {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
permalink := service.PermalinkConfig{} // 由调用方在 buildSPAPageMeta 中单独处理 board
|
||||
permalink := services.PermalinkConfig{} // 由调用方在 buildSPAPageMeta 中单独处理 board
|
||||
if q := c.Request.URL.RawQuery; q != "" {
|
||||
if path == "/" {
|
||||
board := c.Query("board")
|
||||
if board != "" {
|
||||
_ = permalink
|
||||
return service.LegacyQueryBoardHome(uint(parseUintOrZero(board)))
|
||||
return services.LegacyQueryBoardHome(uint(parseUintOrZero(board)))
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,16 +6,16 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/embed_static"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/seo"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// 爬虫专用伪静态 HTML(无 SPA;仅 User-Agent 命中爬虫时返回,避免用户刷新闪屏)
|
||||
|
||||
func renderBotHTML(meta *embed_static.SPAPageMeta, bodyInner string) string {
|
||||
func renderBotHTML(meta *seo.PageMeta, bodyInner string) string {
|
||||
if meta == nil {
|
||||
meta = &embed_static.SPAPageMeta{}
|
||||
meta = &seo.PageMeta{}
|
||||
}
|
||||
ogType := strings.TrimSpace(meta.OGType)
|
||||
if ogType == "" {
|
||||
@@ -87,7 +87,7 @@ func writeEscapedMeta(b *strings.Builder, attr, key, content string) {
|
||||
b.WriteString("<meta " + attr + "=\"" + html.EscapeString(key) + "\" content=\"" + html.EscapeString(content) + "\"/>")
|
||||
}
|
||||
|
||||
func (h *Handlers) botBoardHTML(meta *embed_static.SPAPageMeta, board model.Board) string {
|
||||
func (h *Handlers) botBoardHTML(meta *seo.PageMeta, board models.Board) string {
|
||||
desc := strings.TrimSpace(board.Description)
|
||||
if desc == "" {
|
||||
desc = meta.Description
|
||||
@@ -99,7 +99,7 @@ func (h *Handlers) botBoardHTML(meta *embed_static.SPAPageMeta, board model.Boar
|
||||
return renderBotHTML(meta, body)
|
||||
}
|
||||
|
||||
func (h *Handlers) botHomeHTML(meta *embed_static.SPAPageMeta, brand service.SiteBranding) string {
|
||||
func (h *Handlers) botHomeHTML(meta *seo.PageMeta, brand services.SiteBranding) string {
|
||||
name := strings.TrimSpace(brand.Name)
|
||||
if name == "" {
|
||||
name = "姜十三论坛"
|
||||
@@ -117,10 +117,10 @@ func (h *Handlers) botHomeHTML(meta *embed_static.SPAPageMeta, brand service.Sit
|
||||
return renderBotHTML(meta, body.String())
|
||||
}
|
||||
|
||||
func (h *Handlers) botPostHTML(base, siteName, defaultImage, keywords string, post *model.Post) string {
|
||||
func (h *Handlers) botPostHTML(base, siteName, defaultImage, keywords string, post *models.Post) string {
|
||||
meta := attachSiteSEO(h.postPageMeta(base, siteName, defaultImage, post), siteName, keywords)
|
||||
content := service.SanitizePostHTML(service.RedactGatedPostHTML(post.Content))
|
||||
author := service.DisplayName(&post.User)
|
||||
content := services.SanitizePostHTML(services.RedactGatedPostHTML(post.Content))
|
||||
author := services.DisplayName(&post.User)
|
||||
var body strings.Builder
|
||||
body.WriteString("<article>")
|
||||
body.WriteString("<h1>" + html.EscapeString(post.Title) + "</h1>")
|
||||
@@ -138,9 +138,9 @@ func (h *Handlers) botPostHTML(base, siteName, defaultImage, keywords string, po
|
||||
return renderBotHTML(meta, body.String())
|
||||
}
|
||||
|
||||
func (h *Handlers) botUserHTML(base, siteName, defaultImage, keywords string, user *model.User) string {
|
||||
func (h *Handlers) botUserHTML(base, siteName, defaultImage, keywords string, user *models.User) string {
|
||||
meta := attachSiteSEO(h.userPageMeta(base, siteName, defaultImage, user), siteName, keywords)
|
||||
name := service.DisplayName(user)
|
||||
name := services.DisplayName(user)
|
||||
sig := strings.TrimSpace(user.Signature)
|
||||
var body strings.Builder
|
||||
body.WriteString("<h1>" + html.EscapeString(name) + " 的主页</h1>")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// APIPages 已发布单页摘要列表
|
||||
@@ -17,7 +17,7 @@ func (h *Handlers) APIPages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if pages == nil {
|
||||
pages = []service.SitePageSummary{}
|
||||
pages = []services.SitePageSummary{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"pages": pages})
|
||||
}
|
||||
@@ -57,14 +57,14 @@ func (h *Handlers) APIAdminPages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if pages == nil {
|
||||
pages = []model.SitePage{}
|
||||
pages = []models.SitePage{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"pages": pages})
|
||||
}
|
||||
|
||||
// APIAdminCreatePage 创建单页
|
||||
func (h *Handlers) APIAdminCreatePage(c *gin.Context) {
|
||||
var in service.SitePageInput
|
||||
var in services.SitePageInput
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
|
||||
return
|
||||
@@ -80,7 +80,7 @@ func (h *Handlers) APIAdminCreatePage(c *gin.Context) {
|
||||
// APIAdminUpdatePage 更新单页
|
||||
func (h *Handlers) APIAdminUpdatePage(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
var in service.SitePageInput
|
||||
var in services.SitePageInput
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
|
||||
return
|
||||
@@ -137,11 +137,11 @@ func (h *Handlers) APIPollVote(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"})
|
||||
return
|
||||
}
|
||||
if err := service.VotePoll(uint(id), h.currentUserID(c), body.OptionIDs); err != nil {
|
||||
if err := services.VotePoll(uint(id), h.currentUserID(c), body.OptionIDs); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
poll, _ := service.GetPollView(uint(id), h.currentUserID(c))
|
||||
poll, _ := services.GetPollView(uint(id), h.currentUserID(c))
|
||||
c.JSON(http.StatusOK, gin.H{"message": "投票成功", "poll": poll})
|
||||
}
|
||||
|
||||
@@ -153,11 +153,11 @@ func (h *Handlers) APIPollClose(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
if err := service.ClosePoll(uint(id), h.currentUserID(c), h.isAdmin(c), post.UserID); err != nil {
|
||||
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, _ := service.GetPollView(uint(id), h.currentUserID(c))
|
||||
poll, _ := services.GetPollView(uint(id), h.currentUserID(c))
|
||||
c.JSON(http.StatusOK, gin.H{"message": "投票已结束", "poll": poll})
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (h *Handlers) APIBountyAward(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择评论"})
|
||||
return
|
||||
}
|
||||
if err := service.AwardBounty(uint(id), h.currentUserID(c), h.isAdmin(c), uint(commentID)); err != nil {
|
||||
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
|
||||
}
|
||||
@@ -179,7 +179,7 @@ func (h *Handlers) APIBountyAward(c *gin.Context) {
|
||||
// APIBountyRefund 退回悬赏
|
||||
func (h *Handlers) APIBountyRefund(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := service.RefundBounty(uint(id), h.currentUserID(c), h.isAdmin(c)); err != nil {
|
||||
if err := services.RefundBounty(uint(id), h.currentUserID(c), h.isAdmin(c)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func (h *Handlers) APIBountyRefund(c *gin.Context) {
|
||||
// APILotteryDraw 帖内抽奖开奖
|
||||
func (h *Handlers) APILotteryDraw(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
view, err := service.DrawPostLottery(uint(id), h.currentUserID(c), h.isAdmin(c))
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
)
|
||||
|
||||
// ServeImageThumb 帖子图片缩略图(按需生成并缓存)
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func (h *Handlers) ServeImageThumb(c *gin.Context) {
|
||||
rel := strings.TrimPrefix(c.Param("filepath"), "/")
|
||||
uploadsRoot := filepath.Join(h.Cfg.DataDir, "uploads")
|
||||
thumbPath, err := service.EnsureUploadThumb(uploadsRoot, rel)
|
||||
thumbPath, err := services.EnsureUploadThumb(uploadsRoot, rel)
|
||||
if err != nil {
|
||||
// 生成失败时回退原图,避免正文裂图
|
||||
orig := filepath.Join(uploadsRoot, filepath.FromSlash(rel))
|
||||
|
||||
159
routers/setup.go
159
routers/setup.go
@@ -1,7 +1,6 @@
|
||||
package router
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -9,12 +8,11 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/config"
|
||||
"git.iioio.com/freefire/jiang13-forum/embed_static"
|
||||
"git.iioio.com/freefire/jiang13-forum/handler"
|
||||
"git.iioio.com/freefire/jiang13-forum/middleware"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/auth"
|
||||
webpublic "git.iioio.com/freefire/jiang13-forum/public"
|
||||
"git.iioio.com/freefire/jiang13-forum/routers/api"
|
||||
webpages "git.iioio.com/freefire/jiang13-forum/routers/web"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -24,7 +22,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(gin.Logger())
|
||||
|
||||
// SSR 静态资源(独立前缀,避免与 SPA /assets/* 冲突)
|
||||
// SSR 静态资源
|
||||
if sub, err := fs.Sub(webpublic.Assets, "assets"); err == nil {
|
||||
ssrFiles := http.StripPrefix("/ssr-assets", http.FileServer(http.FS(sub)))
|
||||
r.GET("/ssr-assets/*filepath", func(c *gin.Context) {
|
||||
@@ -32,59 +30,42 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
ssrFiles.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// 生产:内嵌 React SPA(未迁移路径仍走 SPA);dev:Vite 可对照旧前台
|
||||
if !cfg.DevMode {
|
||||
if err := embed_static.SetupEmbed(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[dev] SSR 页面请访问 http://localhost:3000 ;旧 SPA 对照可用 Vite :5173\n")
|
||||
if cfg.DevMode {
|
||||
fmt.Fprintf(os.Stderr, "[dev] SSR 请访问 http://localhost:%d (对照 SPA 请 checkout main)\n", cfg.Port)
|
||||
}
|
||||
|
||||
filter := service.NewSensitiveFilter()
|
||||
_ = service.WriteDefaultFilterWords(cfg.FilterWordsPath())
|
||||
filter := services.NewSensitiveFilter()
|
||||
_ = services.WriteDefaultFilterWords(cfg.FilterWordsPath())
|
||||
filter.LoadFromFile(cfg.FilterWordsPath())
|
||||
|
||||
settingsSvc := service.NewForumSettingsService()
|
||||
// SPA 入口 HTML 注入标题与品牌 JSON,避免刷新时先闪默认文案
|
||||
embed_static.SetSPADocumentTitle(func() string {
|
||||
return settingsSvc.SiteBranding().DocumentTitle()
|
||||
})
|
||||
embed_static.SetSPABrandingJSON(func() []byte {
|
||||
b, err := json.Marshal(settingsSvc.SiteBranding())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
})
|
||||
authSvc := service.NewAuthService(cfg.JWTSecret, filter, settingsSvc)
|
||||
userSvc := service.NewUserService(filter, settingsSvc)
|
||||
boardSvc := service.NewBoardService()
|
||||
settingsSvc := services.NewForumSettingsService()
|
||||
authSvc := services.NewAuthService(cfg.JWTSecret, filter, settingsSvc)
|
||||
userSvc := services.NewUserService(filter, settingsSvc)
|
||||
boardSvc := services.NewBoardService()
|
||||
boardSvc.EnsureDefaultBoard()
|
||||
postSvc := service.NewPostService(filter, settingsSvc)
|
||||
commentSvc := service.NewCommentService(filter, settingsSvc)
|
||||
messageSvc := service.NewMessageService(filter, settingsSvc)
|
||||
reportSvc := service.NewReportService(filter, settingsSvc, messageSvc, postSvc, commentSvc)
|
||||
backupSvc := service.NewBackupService(cfg.DBPath(), cfg.DataDir)
|
||||
limiter := service.NewRateLimiter(settingsSvc)
|
||||
captchaSvc := service.NewCaptchaService()
|
||||
mailSvc := service.NewMailService(settingsSvc)
|
||||
emailCodeSvc := service.NewEmailCodeService(mailSvc)
|
||||
notifySvc := service.NewNotifyService(messageSvc, mailSvc, settingsSvc)
|
||||
friendLinkApplySvc := service.NewFriendLinkApplyService(settingsSvc, messageSvc)
|
||||
oidcSvc, err := service.NewOIDCService(cfg, settingsSvc)
|
||||
postSvc := services.NewPostService(filter, settingsSvc)
|
||||
commentSvc := services.NewCommentService(filter, settingsSvc)
|
||||
messageSvc := services.NewMessageService(filter, settingsSvc)
|
||||
reportSvc := services.NewReportService(filter, settingsSvc, messageSvc, postSvc, commentSvc)
|
||||
backupSvc := services.NewBackupService(cfg.DBPath(), cfg.DataDir)
|
||||
limiter := services.NewRateLimiter(settingsSvc)
|
||||
captchaSvc := services.NewCaptchaService()
|
||||
mailSvc := services.NewMailService(settingsSvc)
|
||||
emailCodeSvc := services.NewEmailCodeService(mailSvc)
|
||||
notifySvc := services.NewNotifyService(messageSvc, mailSvc, settingsSvc)
|
||||
friendLinkApplySvc := services.NewFriendLinkApplyService(settingsSvc, messageSvc)
|
||||
oidcSvc, err := services.NewOIDCService(cfg, settingsSvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
giteaSvc := service.NewGiteaService(settingsSvc)
|
||||
giteaSvc := services.NewGiteaService(settingsSvc)
|
||||
giteaSvc.StartBackgroundSync()
|
||||
|
||||
uploadStore := service.NewUploadStore(cfg.DataDir, settingsSvc)
|
||||
uploadStore := services.NewUploadStore(cfg.DataDir, settingsSvc)
|
||||
if err := uploadStore.ReloadFromSettings(settingsSvc); err != nil {
|
||||
// 配置不完整时保持本地磁盘,避免进程无法启动;管理员可在后台修正后热切换
|
||||
fmt.Fprintf(os.Stderr, "警告: 对象存储初始化失败,暂用本地磁盘: %v\n", err)
|
||||
_ = uploadStore.Apply(service.StorageConfig{Type: "local"})
|
||||
_ = uploadStore.Apply(services.StorageConfig{Type: "local"})
|
||||
}
|
||||
// 后台同步存量文件到媒体索引,避免列表依赖实时扫盘
|
||||
go func() {
|
||||
@@ -95,18 +76,18 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
}
|
||||
}()
|
||||
|
||||
h := &handler.Handlers{
|
||||
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: service.NewPointsService(), Badge: service.NewBadgeService(),
|
||||
SitePage: service.NewSitePageService(filter),
|
||||
Points: services.NewPointsService(), Badge: services.NewBadgeService(),
|
||||
SitePage: services.NewSitePageService(filter),
|
||||
FriendLinkApply: friendLinkApplySvc,
|
||||
}
|
||||
authMW := middleware.NewAuthMiddleware(authSvc)
|
||||
authMW := auth.NewAuthMiddleware(authSvc)
|
||||
|
||||
// Gitea 式 SSR 公开页(优先于 SPA)
|
||||
webpages.Register(r, webpages.Deps{
|
||||
@@ -148,9 +129,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
pubAPI.GET("/pages/:slug", h.APIPageDetail)
|
||||
pubAPI.GET("/captcha", h.APICaptcha)
|
||||
pubAPI.GET("/register/config", h.APIRegisterConfig)
|
||||
pubAPI.POST("/register/email-code", middleware.RateLimitMiddleware(limiter, "register"), h.APISendRegisterEmailCode)
|
||||
pubAPI.POST("/password-reset/email-code", middleware.RateLimitMiddleware(limiter, "register"), h.APISendResetEmailCode)
|
||||
pubAPI.POST("/password-reset", middleware.RateLimitMiddleware(limiter, "login"), h.APIResetPassword)
|
||||
pubAPI.POST("/register/email-code", auth.RateLimitMiddleware(limiter, "register"), h.APISendRegisterEmailCode)
|
||||
pubAPI.POST("/password-reset/email-code", auth.RateLimitMiddleware(limiter, "register"), h.APISendResetEmailCode)
|
||||
pubAPI.POST("/password-reset", auth.RateLimitMiddleware(limiter, "login"), h.APIResetPassword)
|
||||
pubAPI.GET("/posts", h.APIPosts)
|
||||
pubAPI.GET("/posts/hot", h.APIHotPosts)
|
||||
pubAPI.GET("/tags", h.APITags)
|
||||
@@ -161,10 +142,10 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
pubAPI.GET("/users/:id", h.APIUserPublic)
|
||||
pubAPI.GET("/posts/:id", h.APIPostDetail)
|
||||
pubAPI.GET("/posts/:id/comments", h.APIPostComments)
|
||||
pubAPI.POST("/posts/:id/comments", middleware.RateLimitMiddleware(limiter, "comment"), h.APICreateComment)
|
||||
pubAPI.POST("/posts/:id/comments", auth.RateLimitMiddleware(limiter, "comment"), h.APICreateComment)
|
||||
pubAPI.GET("/projects", h.APIProjects)
|
||||
pubAPI.POST("/register", middleware.RateLimitMiddleware(limiter, "register"), h.APIRegister)
|
||||
pubAPI.POST("/login", middleware.RateLimitMiddleware(limiter, "login"), h.APILogin)
|
||||
pubAPI.POST("/register", auth.RateLimitMiddleware(limiter, "register"), h.APIRegister)
|
||||
pubAPI.POST("/login", auth.RateLimitMiddleware(limiter, "login"), h.APILogin)
|
||||
}
|
||||
|
||||
// 需登录 API
|
||||
@@ -178,7 +159,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
api.POST("/profile/password", h.APIUpdatePassword)
|
||||
api.POST("/profile/avatar", h.APIUploadAvatar)
|
||||
api.POST("/uploads/image", h.APIUploadPostImage)
|
||||
api.POST("/posts", middleware.RateLimitMiddleware(limiter, "post"), h.APICreatePost)
|
||||
api.POST("/posts", auth.RateLimitMiddleware(limiter, "post"), h.APICreatePost)
|
||||
api.PUT("/posts/:id", h.APIUpdatePost)
|
||||
api.DELETE("/posts/:id", h.APIDeletePost)
|
||||
api.GET("/posts/:id/revisions", h.APIPostRevisions)
|
||||
@@ -191,17 +172,17 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
api.POST("/posts/:id/bounty/award", h.APIBountyAward)
|
||||
api.POST("/posts/:id/bounty/refund", h.APIBountyRefund)
|
||||
api.POST("/posts/:id/lottery/draw", h.APILotteryDraw)
|
||||
api.POST("/posts/:id/report", middleware.RateLimitMiddleware(limiter, "report"), h.APICreatePostReport)
|
||||
api.POST("/posts/:id/report", auth.RateLimitMiddleware(limiter, "report"), h.APICreatePostReport)
|
||||
api.GET("/messages/unread-count", h.APIMessageUnreadCount)
|
||||
api.GET("/messages/notifications", h.APIMessageNotifications)
|
||||
api.POST("/messages/notifications/read", h.APIMarkNotificationsRead)
|
||||
api.GET("/messages/conversations", h.APIMessageConversations)
|
||||
api.GET("/messages/conversations/:peerId", h.APIConversationMessages)
|
||||
api.POST("/messages/conversations/:peerId/read", h.APIMarkConversationRead)
|
||||
api.POST("/messages", middleware.RateLimitMiddleware(limiter, "message"), h.APISendMessage)
|
||||
api.POST("/messages", auth.RateLimitMiddleware(limiter, "message"), h.APISendMessage)
|
||||
api.POST("/messages/read-all", h.APIMarkAllMessagesRead)
|
||||
api.POST("/comments/:id/like", h.APIToggleCommentLike)
|
||||
api.POST("/comments/:id/report", middleware.RateLimitMiddleware(limiter, "report"), h.APICreateCommentReport)
|
||||
api.POST("/comments/:id/report", auth.RateLimitMiddleware(limiter, "report"), h.APICreateCommentReport)
|
||||
api.DELETE("/comments/:id", h.APIDeleteComment)
|
||||
api.PUT("/comments/:id", h.APIUpdateComment)
|
||||
api.GET("/me/points", h.APIMePoints)
|
||||
@@ -209,11 +190,11 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
api.POST("/me/check-in", h.APIMeCheckIn)
|
||||
api.GET("/me/lottery", h.APIMeLotteryGet)
|
||||
api.POST("/me/lottery", h.APIMeLotteryDraw)
|
||||
api.POST("/posts/:id/unlock", middleware.RateLimitMiddleware(limiter, "post"), h.APIUnlockPostBlock)
|
||||
api.POST("/friend-links/apply", middleware.RateLimitMiddleware(limiter, "friend_link"), h.APIApplyFriendLink)
|
||||
api.POST("/friend-links/logo", middleware.RateLimitMiddleware(limiter, "post"), h.APIUploadFriendLinkLogo)
|
||||
api.POST("/posts/:id/unlock", auth.RateLimitMiddleware(limiter, "post"), h.APIUnlockPostBlock)
|
||||
api.POST("/friend-links/apply", auth.RateLimitMiddleware(limiter, "friend_link"), h.APIApplyFriendLink)
|
||||
api.POST("/friend-links/logo", auth.RateLimitMiddleware(limiter, "post"), h.APIUploadFriendLinkLogo)
|
||||
api.GET("/friend-links/my-applies", h.APIMyFriendLinkApplies)
|
||||
api.PUT("/friend-links/applies/:id", middleware.RateLimitMiddleware(limiter, "friend_link"), h.APIUpdateFriendLinkApply)
|
||||
api.PUT("/friend-links/applies/:id", auth.RateLimitMiddleware(limiter, "friend_link"), h.APIUpdateFriendLinkApply)
|
||||
api.DELETE("/friend-links/applies/:id", h.APICancelFriendLinkApply)
|
||||
}
|
||||
|
||||
@@ -288,41 +269,27 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
adminAPI.GET("/backup/download/:name", h.APIAdminDownloadBackup)
|
||||
}
|
||||
|
||||
// 后台管理页面由 React SPA 渲染(JSON API 见上方 /api/admin)
|
||||
// dev 模式下前端由 Vite 提供,后台页面路由不在此注册
|
||||
if !cfg.DevMode {
|
||||
admin := r.Group("/admin")
|
||||
// 管理后台 HTML:SSR 尚未迁移;勿用 /*filepath(与 /admin/login 冲突)
|
||||
adminPendingHTML := `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"/><title>管理后台</title></head><body><h1>管理后台 SSR 迁移中</h1><p>API 仍可用;UI 请暂时对照 <code>main</code> 分支 SPA,或等待后续模板页。</p><p><a href="/">返回首页</a></p></body></html>`
|
||||
adminPending := func(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, adminPendingHTML)
|
||||
}
|
||||
admin := r.Group("/admin")
|
||||
{
|
||||
admin.GET("/login", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
})
|
||||
adminAuth := admin.Group("/", authMW.RequireAuth(), authMW.RequireAdmin())
|
||||
{
|
||||
admin.GET("/login", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
})
|
||||
|
||||
adminAuth := admin.Group("/", authMW.RequireAuth(), authMW.RequireAdmin())
|
||||
{
|
||||
adminAuth.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") })
|
||||
for _, page := range []string{"dashboard", "boards", "pages", "links", "posts", "comments", "reports", "users", "badges", "media", "settings"} {
|
||||
adminAuth.GET("/"+page, embed_static.ServeSPANoIndex)
|
||||
}
|
||||
}
|
||||
adminAuth.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") })
|
||||
adminAuth.GET("/dashboard", adminPending)
|
||||
adminAuth.GET("/:page", adminPending)
|
||||
}
|
||||
}
|
||||
|
||||
// 未迁移路径:生产仍回落 React SPA;/ 与 /board/:id 已由 routers/web 接管
|
||||
if cfg.DevMode {
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "页面未找到。SSR 首页请打开 / ;旧 SPA 请用 Vite http://localhost:5173",
|
||||
})
|
||||
})
|
||||
} else {
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if embed_static.IsSPARoute(c.Request.URL.Path) {
|
||||
h.ServePublicSPA(c)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
})
|
||||
}
|
||||
// 未迁移公开路径:爬虫可读 HTML / 用户占位(首页与板块已由 routers/web 接管)
|
||||
r.NoRoute(h.ServePublicSPA)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package web
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -6,18 +6,18 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/middleware"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/auth"
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/webrender"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Deps 页面路由依赖(复用现有 service,避免 Phase 1 大搬家)
|
||||
type Deps struct {
|
||||
Settings *service.ForumSettingsService
|
||||
Board *service.BoardService
|
||||
Post *service.PostService
|
||||
Settings *services.ForumSettingsService
|
||||
Board *services.BoardService
|
||||
Post *services.PostService
|
||||
}
|
||||
|
||||
// BoardView 侧栏板块
|
||||
@@ -61,7 +61,7 @@ type HomePageData struct {
|
||||
}
|
||||
|
||||
// Register 注册已迁移的 SSR 页面(优先于 SPA)
|
||||
func Register(r *gin.Engine, deps Deps, authMW *middleware.AuthMiddleware) {
|
||||
func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
g := r.Group("/", authMW.OptionalAuth())
|
||||
g.GET("/", deps.Home)
|
||||
g.GET("/board/:id", deps.Home)
|
||||
@@ -99,21 +99,21 @@ func (d Deps) Home(c *gin.Context) {
|
||||
}
|
||||
|
||||
var uid uint
|
||||
if v, ok := c.Get(middleware.CtxUserID); ok {
|
||||
if v, ok := c.Get(auth.CtxUserID); ok {
|
||||
uid, _ = v.(uint)
|
||||
}
|
||||
isAdmin := false
|
||||
if v, ok := c.Get(middleware.CtxRole); ok {
|
||||
if v, ok := c.Get(auth.CtxRole); ok {
|
||||
switch r := v.(type) {
|
||||
case model.Role:
|
||||
isAdmin = r == model.RoleAdmin
|
||||
case models.Role:
|
||||
isAdmin = r == models.RoleAdmin
|
||||
case string:
|
||||
isAdmin = r == string(model.RoleAdmin)
|
||||
isAdmin = r == string(models.RoleAdmin)
|
||||
}
|
||||
}
|
||||
username, _ := c.Get(middleware.CtxUsername)
|
||||
username, _ := c.Get(auth.CtxUsername)
|
||||
|
||||
q := service.PostListQuery{
|
||||
q := services.PostListQuery{
|
||||
BoardID: boardID,
|
||||
Page: page,
|
||||
Size: size,
|
||||
|
||||
Reference in New Issue
Block a user