fix: 修复帖子提交审核权限缺失 + 防重复提交 + 分页逻辑复用

- 修复 Submit API 缺少 uid 权限检查,任意登录用户可提交他人帖子审核
- 前端 handleSubmit 添加 submitting 锁和按钮禁用/恢复,防止重复提交
- 分页计算 TotalPages/PrevPage/NextPage 提取为 Pagination 方法,消除三处重复代码
This commit is contained in:
2026-05-28 14:47:14 +08:00
parent e45d66fb77
commit 91f2cbebc1
4 changed files with 84 additions and 32 deletions

View File

@ -23,5 +23,31 @@ func (p *Pagination) Offset() int {
return (p.Page - 1) * p.PageSize
}
// TotalPages 根据 total 计算总页数
func (p *Pagination) TotalPages(total int64) int {
if total == 0 {
return 0
}
return int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
}
// PrevPage 上一页(最小为 1
func (p *Pagination) PrevPage() int {
prev := p.Page - 1
if prev < 1 {
return 1
}
return prev
}
// NextPage 下一页(最大为 totalPages
func (p *Pagination) NextPage(totalPages int) int {
next := p.Page + 1
if next > totalPages {
return totalPages
}
return next
}
// 固定时间格式,前后端统一
const TimeFormat = time.RFC3339

View File

@ -43,11 +43,7 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) {
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
prevPage := p.Page - 1
if prevPage < 1 { prevPage = 1 }
nextPage := p.Page + 1
if nextPage > totalPages { nextPage = totalPages }
totalPages := p.TotalPages(total)
c.HTML(http.StatusOK, "admin/posts/index.html", common.BuildAdminPageData(c, gin.H{
"Title": "内容管理",
@ -55,8 +51,8 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) {
"Total": total,
"Page": p.Page,
"TotalPages": totalPages,
"PrevPage": prevPage,
"NextPage": nextPage,
"PrevPage": p.PrevPage(),
"NextPage": p.NextPage(totalPages),
"Keyword": keyword,
"Status": status,
"StatusNames": model.PostStatusDisplayNames,

View File

@ -49,11 +49,7 @@ func (ctrl *PostController) ListPage(c *gin.Context) {
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
prevPage := p.Page - 1
if prevPage < 1 { prevPage = 1 }
nextPage := p.Page + 1
if nextPage > totalPages { nextPage = totalPages }
totalPages := p.TotalPages(total)
c.HTML(http.StatusOK, "posts/index.html", common.BuildPageData(c, gin.H{
"Title": "社区帖子",
@ -61,8 +57,8 @@ func (ctrl *PostController) ListPage(c *gin.Context) {
"Total": total,
"Page": p.Page,
"TotalPages": totalPages,
"PrevPage": prevPage,
"NextPage": nextPage,
"PrevPage": p.PrevPage(),
"NextPage": p.NextPage(totalPages),
"Keyword": keyword,
"ExtraCSS": "/static/css/posts.css",
}))
@ -276,12 +272,30 @@ func (ctrl *PostController) Delete(c *gin.Context) {
// Submit API 提交审核
func (ctrl *PostController) Submit(c *gin.Context) {
uidObj, _ := c.Get("uid")
uid, ok := uidObj.(uint)
if !ok {
common.Error(c, http.StatusUnauthorized, "请先登录")
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
common.Error(c, http.StatusBadRequest, "无效的帖子 ID")
return
}
// 权限检查:仅帖子作者可提交审核
post, err := ctrl.postService.GetByID(uint(id))
if err != nil {
common.Error(c, http.StatusNotFound, "帖子不存在")
return
}
if post.UserID != uid {
common.Error(c, http.StatusForbidden, "无权操作此帖子")
return
}
if err := ctrl.postService.SubmitForAudit(uint(id)); err != nil {
if errors.Is(err, common.ErrPostCannotSubmit) || errors.Is(err, common.ErrPostNotFound) {
common.Error(c, http.StatusBadRequest, err.Error())
@ -312,13 +326,12 @@ func (ctrl *PostController) ListAPI(c *gin.Context) {
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
common.Ok(c, model.PostListResult{
Items: posts,
Total: total,
Page: p.Page,
TotalPages: totalPages,
TotalPages: p.TotalPages(total),
})
}

View File

@ -17,6 +17,7 @@ import { common, createLowlight } from 'lowlight'
let editor = null
let currentHTML = ''
let draftTimer = null
let submitting = false
// ---- 初始化 ----
document.addEventListener('DOMContentLoaded', () => {
@ -337,25 +338,34 @@ function handleKeyboard(e) {
async function handleSubmit(e) {
e.preventDefault()
const postId = document.getElementById('postId')?.value
const title = document.getElementById('postTitle')?.value.trim()
if (submitting) return
submitting = true
if (!title) { alert('请输入标题'); return }
// 获取编辑器 HTML 内容
const body = editor ? editor.getHTML() : ''
// 忽略空段落
const cleanBody = body === '<p></p>' ? '' : body
if (!cleanBody.trim()) { alert('请输入正文'); return }
const isEdit = !!postId
const url = isEdit ? '/api/posts/' + postId : '/api/posts'
const method = isEdit ? 'PUT' : 'POST'
const csrfMeta = document.querySelector('meta[name="csrf-token"]')
const csrf = csrfMeta ? csrfMeta.getAttribute('content') : ''
const submitBtn = document.querySelector('.btn-submit')
if (submitBtn) {
submitBtn.disabled = true
submitBtn.textContent = '提交中...'
}
try {
const postId = document.getElementById('postId')?.value
const title = document.getElementById('postTitle')?.value.trim()
if (!title) { alert('请输入标题'); return }
// 获取编辑器 HTML 内容
const body = editor ? editor.getHTML() : ''
// 忽略空段落
const cleanBody = body === '<p></p>' ? '' : body
if (!cleanBody.trim()) { alert('请输入正文'); return }
const isEdit = !!postId
const url = isEdit ? '/api/posts/' + postId : '/api/posts'
const method = isEdit ? 'PUT' : 'POST'
const csrfMeta = document.querySelector('meta[name="csrf-token"]')
const csrf = csrfMeta ? csrfMeta.getAttribute('content') : ''
const resp = await fetch(url, {
method,
headers: {
@ -380,5 +390,12 @@ async function handleSubmit(e) {
} else {
alert('请求失败,请重试')
}
} finally {
submitting = false
if (submitBtn) {
submitBtn.disabled = false
submitBtn.textContent = submitBtn.textContent === '提交中...' ?
(document.getElementById('postId')?.value ? '保存修改' : '发布帖子') : submitBtn.textContent
}
}
}