feat: 完整内容 Limits 与伪静态 Admin
设置页可配字数/编辑窗/分页与伪静态后缀;公开帖/板/用户链接走规范路径并 301。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -429,6 +429,7 @@ type adminSettingsData struct {
|
||||
RateReg int
|
||||
RateLogin int
|
||||
RateWindow int
|
||||
Limits services.ForumLimits
|
||||
FilterWords string
|
||||
FilterCount int
|
||||
Mail services.MailConfig
|
||||
@@ -465,6 +466,7 @@ func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
||||
RateReg: lim.RateLimitRegister,
|
||||
RateLogin: lim.RateLimitLogin,
|
||||
RateWindow: lim.RateLimitWindowSec,
|
||||
Limits: lim,
|
||||
FilterWords: words,
|
||||
FilterCount: services.CountFilterWords(words),
|
||||
Mail: mail,
|
||||
@@ -624,6 +626,51 @@ func (d Deps) AdminSettingsLimitsPost(c *gin.Context) {
|
||||
ctx.Redirect("/admin/settings")
|
||||
}
|
||||
|
||||
// AdminSettingsContentLimitsPost 字数与编辑窗等
|
||||
func (d Deps) AdminSettingsContentLimitsPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminSettings(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
atoi := func(name string) int {
|
||||
n, _ := strconv.Atoi(strings.TrimSpace(c.PostForm(name)))
|
||||
return n
|
||||
}
|
||||
err := d.Settings.UpdateContentLimits(
|
||||
atoi("post_edit_window_hours"), atoi("comment_edit_window_minutes"),
|
||||
atoi("post_title_max"), atoi("post_tags_max"), atoi("post_content_max"), atoi("comment_max"),
|
||||
atoi("search_keyword_min"), atoi("search_keyword_max"), atoi("page_size_default"),
|
||||
atoi("password_min_len"), atoi("avatar_max_mb"), atoi("signature_max"),
|
||||
c.PostForm("open_posts_in_new_tab") == "1" || c.PostForm("open_posts_in_new_tab") == "on",
|
||||
c.PostForm("open_content_links_in_new_tab") == "1" || c.PostForm("open_content_links_in_new_tab") == "on",
|
||||
strings.TrimSpace(c.PostForm("feed_list_style")),
|
||||
)
|
||||
if err != nil {
|
||||
d.renderAdminSettings(ctx, "内容限制无效,请检查数值范围")
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("内容限制已保存")
|
||||
ctx.Redirect("/admin/settings")
|
||||
}
|
||||
|
||||
// AdminSettingsPermalinkPost 伪静态
|
||||
func (d Deps) AdminSettingsPermalinkPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminSettings(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
enabled := c.PostForm("permalink_enabled") == "1" || c.PostForm("permalink_enabled") == "on"
|
||||
ext := strings.TrimSpace(c.PostForm("permalink_ext"))
|
||||
if err := d.Settings.UpdatePermalinkSettings(enabled, ext); err != nil {
|
||||
d.renderAdminSettings(ctx, "伪静态后缀无效(1–16 位字母数字)")
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("伪静态设置已保存")
|
||||
ctx.Redirect("/admin/settings")
|
||||
}
|
||||
|
||||
// AdminSettingsFilterWordsPost 敏感词
|
||||
func (d Deps) AdminSettingsFilterWordsPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type boardsItemView struct {
|
||||
ID uint
|
||||
Name string
|
||||
Href string
|
||||
Description string
|
||||
Icon string
|
||||
ColorIndex int
|
||||
@@ -32,6 +33,7 @@ func (d Deps) renderBoards(ctx *webctx.Context) {
|
||||
chrome := d.chrome(ctx, "板块 · "+brand.Name, "", "")
|
||||
list, _ := d.Board.ListWithStats()
|
||||
items := make([]boardsItemView, 0, len(list))
|
||||
pl := d.permalink()
|
||||
for _, b := range list {
|
||||
color := b.ColorIndex
|
||||
if color < 0 {
|
||||
@@ -40,6 +42,7 @@ func (d Deps) renderBoards(ctx *webctx.Context) {
|
||||
items = append(items, boardsItemView{
|
||||
ID: b.ID,
|
||||
Name: b.Name,
|
||||
Href: pl.BoardPath(b.ID),
|
||||
Description: b.Description,
|
||||
Icon: b.Icon,
|
||||
ColorIndex: color,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||
@@ -44,12 +45,14 @@ type SitePageLink struct {
|
||||
type BoardView struct {
|
||||
ID uint
|
||||
Name string
|
||||
Href string
|
||||
}
|
||||
|
||||
// RightAsideHotPost 右栏热门帖
|
||||
type RightAsideHotPost struct {
|
||||
ID uint
|
||||
Title string
|
||||
Href string
|
||||
}
|
||||
|
||||
// RightAsideTag 右栏标签
|
||||
@@ -61,6 +64,7 @@ type RightAsideTag struct {
|
||||
// RightAsideComment 右栏最新评论
|
||||
type RightAsideComment struct {
|
||||
PostID uint
|
||||
PostHref string
|
||||
Author string
|
||||
Excerpt string
|
||||
PostTitle string
|
||||
@@ -70,6 +74,7 @@ type RightAsideComment struct {
|
||||
type RightAsideUser struct {
|
||||
ID uint
|
||||
Nickname string
|
||||
Href string
|
||||
}
|
||||
|
||||
// RightAsideFriendLink 右栏友链
|
||||
@@ -111,6 +116,8 @@ type PageChrome struct {
|
||||
LogoURL string
|
||||
FaviconURL string
|
||||
OGImageURL string
|
||||
PermalinkSuffix string // 如 ".html";未启用为空
|
||||
OpenPostsInNewTab bool
|
||||
LoggedIn bool
|
||||
IsAdmin bool
|
||||
ViewerName string
|
||||
@@ -151,8 +158,9 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
||||
}
|
||||
boards, _ := d.Board.List()
|
||||
bv := make([]BoardView, 0, len(boards))
|
||||
pl := d.permalink()
|
||||
for _, b := range boards {
|
||||
bv = append(bv, BoardView{ID: b.ID, Name: b.Name})
|
||||
bv = append(bv, BoardView{ID: b.ID, Name: b.Name, Href: pl.BoardPath(b.ID)})
|
||||
}
|
||||
var unread int64
|
||||
if ctx.IsSigned() && d.Message != nil {
|
||||
@@ -176,6 +184,8 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
||||
LogoURL: strings.TrimSpace(brand.Logo),
|
||||
FaviconURL: strings.TrimSpace(brand.Favicon),
|
||||
OGImageURL: strings.TrimSpace(brand.DefaultShareImage()),
|
||||
PermalinkSuffix: d.Settings.Permalink().Suffix(),
|
||||
OpenPostsInNewTab: d.Settings.OpenPostsInNewTab(),
|
||||
LoggedIn: ctx.IsSigned(),
|
||||
IsAdmin: ctx.IsAdmin(),
|
||||
ViewerName: name,
|
||||
@@ -242,8 +252,9 @@ func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) R
|
||||
}
|
||||
if d.Post != nil {
|
||||
if items, err := d.Post.HotPosts(rightAsideHotLimit); err == nil {
|
||||
pl := d.permalink()
|
||||
for _, it := range items {
|
||||
out.HotPosts = append(out.HotPosts, RightAsideHotPost{ID: it.ID, Title: it.Title})
|
||||
out.HotPosts = append(out.HotPosts, RightAsideHotPost{ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID)})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,9 +275,10 @@ func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) R
|
||||
case services.AsideWidgetRecentComments:
|
||||
if d.Comment != nil {
|
||||
if list, err := d.Comment.ListRecentPublic(rightAsideCommentLimit); err == nil {
|
||||
pl := d.permalink()
|
||||
for _, c := range list {
|
||||
block.Comments = append(block.Comments, RightAsideComment{
|
||||
PostID: c.PostID, Author: c.Author, Excerpt: c.Excerpt, PostTitle: c.PostTitle,
|
||||
PostID: c.PostID, PostHref: pl.PostPath(c.PostID), Author: c.Author, Excerpt: c.Excerpt, PostTitle: c.PostTitle,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -274,8 +286,9 @@ func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) R
|
||||
case services.AsideWidgetRecentUsers:
|
||||
if d.User != nil {
|
||||
if list, err := d.User.ListRecentRegistered(rightAsideUserLimit); err == nil {
|
||||
pl := d.permalink()
|
||||
for _, u := range list {
|
||||
block.Users = append(block.Users, RightAsideUser{ID: u.ID, Nickname: u.Nickname})
|
||||
block.Users = append(block.Users, RightAsideUser{ID: u.ID, Nickname: u.Nickname, Href: pl.UserPath(u.ID)})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,3 +324,39 @@ func stripIDParam(raw, permalinkExt string) string {
|
||||
raw = strings.TrimSuffix(raw, ".htm")
|
||||
return raw
|
||||
}
|
||||
|
||||
func (d Deps) permalink() services.PermalinkConfig {
|
||||
if d.Settings == nil {
|
||||
return services.PermalinkConfig{}
|
||||
}
|
||||
return d.Settings.Permalink()
|
||||
}
|
||||
|
||||
func (d Deps) postHref(id uint) string {
|
||||
return d.permalink().PostPath(id)
|
||||
}
|
||||
|
||||
func (d Deps) boardHref(id uint) string {
|
||||
return d.permalink().BoardPath(id)
|
||||
}
|
||||
|
||||
func (d Deps) userHref(id uint) string {
|
||||
return d.permalink().UserPath(id)
|
||||
}
|
||||
|
||||
// redirectIfNotCanonical 伪静态开启时把无后缀/错误后缀 301 到规范路径
|
||||
func (d Deps) redirectIfNotCanonical(c *gin.Context, match services.PermalinkMatch) bool {
|
||||
if !match.OK || !d.permalink().Enabled {
|
||||
return false
|
||||
}
|
||||
path := c.Request.URL.Path
|
||||
if !match.NeedsCanonicalRedirect(path) {
|
||||
return false
|
||||
}
|
||||
loc := match.Canonical
|
||||
if q := c.Request.URL.RawQuery; q != "" {
|
||||
loc = loc + "?" + q
|
||||
}
|
||||
c.Redirect(http.StatusMovedPermanently, loc)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func (d Deps) ComposePost(c *gin.Context) {
|
||||
} else {
|
||||
ctx.SetFlash("发帖成功")
|
||||
}
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", post.ID))
|
||||
ctx.Redirect(d.postHref(post.ID))
|
||||
}
|
||||
|
||||
// PostEditGet 编辑帖
|
||||
@@ -152,7 +152,7 @@ func (d Deps) PostEditPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("已保存")
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", post.ID))
|
||||
ctx.Redirect(d.postHref(post.ID))
|
||||
}
|
||||
|
||||
// ComposeUpload 帖图上传(JSON,供 compose 页 fetch)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type favItem struct {
|
||||
PostID uint
|
||||
Title string
|
||||
Href string
|
||||
AuthorName string
|
||||
BoardName string
|
||||
CreatedLabel string
|
||||
@@ -43,6 +44,7 @@ func (d Deps) FavoritesGet(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
items := make([]favItem, 0, len(favs))
|
||||
pl := d.permalink()
|
||||
for _, f := range favs {
|
||||
title := f.Post.Title
|
||||
author := strings.TrimSpace(f.Post.User.Nickname)
|
||||
@@ -54,7 +56,7 @@ func (d Deps) FavoritesGet(c *gin.Context) {
|
||||
board = f.Post.Board.Name
|
||||
}
|
||||
items = append(items, favItem{
|
||||
PostID: f.PostID, Title: title, AuthorName: author, BoardName: board,
|
||||
PostID: f.PostID, Title: title, Href: pl.PostPath(f.PostID), AuthorName: author, BoardName: board,
|
||||
CreatedLabel: formatTime(f.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
admin.POST("/settings/brand/upload", deps.AdminSettingsBrandUploadPost)
|
||||
admin.POST("/settings/brand/clear", deps.AdminSettingsBrandClearPost)
|
||||
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
|
||||
admin.POST("/settings/content-limits", deps.AdminSettingsContentLimitsPost)
|
||||
admin.POST("/settings/permalink", deps.AdminSettingsPermalinkPost)
|
||||
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
|
||||
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
|
||||
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
|
||||
@@ -165,6 +167,7 @@ type HomePageData struct {
|
||||
type PostListItem struct {
|
||||
ID uint
|
||||
Title string
|
||||
Href string
|
||||
AuthorName string
|
||||
BoardName string
|
||||
Pinned bool
|
||||
@@ -176,22 +179,22 @@ type PostListItem struct {
|
||||
// FormAction 搜索表单提交路径(当前 Feed)
|
||||
func (d HomePageData) FormAction() string {
|
||||
if d.ActiveBoard > 0 {
|
||||
return fmt.Sprintf("/board/%d", d.ActiveBoard)
|
||||
return fmt.Sprintf("/board/%d%s", d.ActiveBoard, d.PermalinkSuffix)
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
// SortHref 排序链接,保留搜索参数
|
||||
func (d HomePageData) SortHref(sort string) string {
|
||||
return buildFeedURL(d.ActiveBoard, sort, 0, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
||||
return buildFeedURL(d.ActiveBoard, d.PermalinkSuffix, sort, 0, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
||||
}
|
||||
|
||||
// PageHref 分页链接,保留搜索与排序
|
||||
func (d HomePageData) PageHref(page int) string {
|
||||
return buildFeedURL(d.ActiveBoard, d.Sort, page, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
||||
return buildFeedURL(d.ActiveBoard, d.PermalinkSuffix, d.Sort, page, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
||||
}
|
||||
|
||||
func buildFeedURL(boardID uint, sort string, page int, keyword, tag, author string, titleOnly bool) string {
|
||||
func buildFeedURL(boardID uint, permalinkSuffix, sort string, page int, keyword, tag, author string, titleOnly bool) string {
|
||||
q := url.Values{}
|
||||
if sort != "" && sort != "latest" {
|
||||
q.Set("sort", sort)
|
||||
@@ -213,7 +216,7 @@ func buildFeedURL(boardID uint, sort string, page int, keyword, tag, author stri
|
||||
}
|
||||
path := "/"
|
||||
if boardID > 0 {
|
||||
path = fmt.Sprintf("/board/%d", boardID)
|
||||
path = fmt.Sprintf("/board/%d%s", boardID, permalinkSuffix)
|
||||
}
|
||||
if enc := q.Encode(); enc != "" {
|
||||
return path + "?" + enc
|
||||
@@ -243,6 +246,12 @@ func (d Deps) Home(c *gin.Context) {
|
||||
if n, err := strconv.ParseUint(idStr, 10, 64); err == nil {
|
||||
boardID = uint(n)
|
||||
}
|
||||
if boardID > 0 {
|
||||
match := d.permalink().MatchBoardPath(c.Request.URL.Path)
|
||||
if d.redirectIfNotCanonical(c, match) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chrome := d.chrome(ctx, "", "", "home/feed")
|
||||
@@ -309,6 +318,7 @@ func (d Deps) Home(c *gin.Context) {
|
||||
}
|
||||
|
||||
posts := make([]PostListItem, 0, len(items))
|
||||
pl := d.permalink()
|
||||
for _, it := range items {
|
||||
authorName := strings.TrimSpace(it.User.Nickname)
|
||||
if authorName == "" {
|
||||
@@ -319,7 +329,7 @@ func (d Deps) Home(c *gin.Context) {
|
||||
bname = it.Board.Name
|
||||
}
|
||||
posts = append(posts, PostListItem{
|
||||
ID: it.ID, Title: it.Title, AuthorName: authorName, BoardName: bname,
|
||||
ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID), AuthorName: authorName, BoardName: bname,
|
||||
Pinned: it.Pinned, Featured: it.Featured, CommentCount: it.CommentCount,
|
||||
CreatedLabel: it.CreatedAt.Local().Format("2006-01-02 15:04"),
|
||||
})
|
||||
|
||||
@@ -18,11 +18,14 @@ import (
|
||||
type PostPageData struct {
|
||||
PageChrome
|
||||
PostID uint
|
||||
PostHref string
|
||||
PostPath string
|
||||
PostTitle string
|
||||
AuthorName string
|
||||
AuthorID uint
|
||||
AuthorHref string
|
||||
BoardID uint
|
||||
BoardHref string
|
||||
BoardName string
|
||||
Pinned bool // 展示用:全局或版内置顶
|
||||
GlobalPinned bool
|
||||
@@ -132,6 +135,10 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
d.render404(ctx)
|
||||
return
|
||||
}
|
||||
match := d.permalink().MatchPostPath(c.Request.URL.Path)
|
||||
if d.redirectIfNotCanonical(c, match) {
|
||||
return
|
||||
}
|
||||
post, err := d.Post.FindByID(uint(id))
|
||||
if err != nil || !services.CanViewPost(post, ctx.UserID(), ctx.IsAdmin()) {
|
||||
d.render404(ctx)
|
||||
@@ -260,9 +267,11 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
|
||||
ctx.HTML(http.StatusOK, "post", PostPageData{
|
||||
PageChrome: chrome, PostID: post.ID,
|
||||
PostPath: url.QueryEscape(fmt.Sprintf("/post/%d", post.ID)),
|
||||
PostHref: d.postHref(post.ID),
|
||||
PostPath: url.QueryEscape(d.postHref(post.ID)),
|
||||
PostTitle: post.Title, AuthorName: author, AuthorID: post.UserID,
|
||||
BoardID: post.BoardID, BoardName: boardName,
|
||||
AuthorHref: d.userHref(post.UserID),
|
||||
BoardID: post.BoardID, BoardHref: d.boardHref(post.BoardID), BoardName: boardName,
|
||||
Pinned: post.Pinned || post.BoardPinned, GlobalPinned: post.Pinned, BoardPinned: post.BoardPinned,
|
||||
Featured: post.Featured, EditLocked: post.EditLocked,
|
||||
PostTypeLabel: postTypeLabel(post.PostType), CreatedLabel: formatTime(post.CreatedAt),
|
||||
|
||||
@@ -42,7 +42,8 @@ type userPublicData struct {
|
||||
// UserPublic 公开用户主页(不含邮箱)
|
||||
func (d Deps) UserPublic(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
idStr := stripIDParam(c.Param("id"), d.Settings.Permalink().Ext)
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
d.render404(ctx)
|
||||
return
|
||||
@@ -52,6 +53,10 @@ func (d Deps) UserPublic(c *gin.Context) {
|
||||
d.render404(ctx)
|
||||
return
|
||||
}
|
||||
match := d.permalink().MatchUserPath(c.Request.URL.Path)
|
||||
if d.redirectIfNotCanonical(c, match) {
|
||||
return
|
||||
}
|
||||
st, _ := d.User.ActivityStats(user.ID)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
@@ -67,13 +72,14 @@ func (d Deps) UserPublic(c *gin.Context) {
|
||||
ViewerIsAdmin: ctx.IsAdmin(),
|
||||
})
|
||||
items := make([]PostListItem, 0, len(posts))
|
||||
pl := d.permalink()
|
||||
for _, p := range posts {
|
||||
board := ""
|
||||
if p.Board.ID > 0 {
|
||||
board = p.Board.Name
|
||||
}
|
||||
items = append(items, PostListItem{
|
||||
ID: p.ID, Title: p.Title, AuthorName: displayName(user),
|
||||
ID: p.ID, Title: p.Title, Href: pl.PostPath(p.ID), AuthorName: displayName(user),
|
||||
BoardName: board, Pinned: p.Pinned || p.BoardPinned, Featured: p.Featured,
|
||||
CreatedLabel: formatTime(p.CreatedAt),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user