fix: 修复帖子提交审核权限缺失 + 防重复提交 + 分页逻辑复用
- 修复 Submit API 缺少 uid 权限检查,任意登录用户可提交他人帖子审核 - 前端 handleSubmit 添加 submitting 锁和按钮禁用/恢复,防止重复提交 - 分页计算 TotalPages/PrevPage/NextPage 提取为 Pagination 方法,消除三处重复代码
This commit is contained in:
@ -23,5 +23,31 @@ func (p *Pagination) Offset() int {
|
|||||||
return (p.Page - 1) * p.PageSize
|
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
|
const TimeFormat = time.RFC3339
|
||||||
|
|||||||
@ -43,11 +43,7 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) {
|
|||||||
|
|
||||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||||
p.DefaultPagination()
|
p.DefaultPagination()
|
||||||
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
|
totalPages := p.TotalPages(total)
|
||||||
prevPage := p.Page - 1
|
|
||||||
if prevPage < 1 { prevPage = 1 }
|
|
||||||
nextPage := p.Page + 1
|
|
||||||
if nextPage > totalPages { nextPage = totalPages }
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "admin/posts/index.html", common.BuildAdminPageData(c, gin.H{
|
c.HTML(http.StatusOK, "admin/posts/index.html", common.BuildAdminPageData(c, gin.H{
|
||||||
"Title": "内容管理",
|
"Title": "内容管理",
|
||||||
@ -55,8 +51,8 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) {
|
|||||||
"Total": total,
|
"Total": total,
|
||||||
"Page": p.Page,
|
"Page": p.Page,
|
||||||
"TotalPages": totalPages,
|
"TotalPages": totalPages,
|
||||||
"PrevPage": prevPage,
|
"PrevPage": p.PrevPage(),
|
||||||
"NextPage": nextPage,
|
"NextPage": p.NextPage(totalPages),
|
||||||
"Keyword": keyword,
|
"Keyword": keyword,
|
||||||
"Status": status,
|
"Status": status,
|
||||||
"StatusNames": model.PostStatusDisplayNames,
|
"StatusNames": model.PostStatusDisplayNames,
|
||||||
|
|||||||
@ -49,11 +49,7 @@ func (ctrl *PostController) ListPage(c *gin.Context) {
|
|||||||
|
|
||||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||||
p.DefaultPagination()
|
p.DefaultPagination()
|
||||||
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
|
totalPages := p.TotalPages(total)
|
||||||
prevPage := p.Page - 1
|
|
||||||
if prevPage < 1 { prevPage = 1 }
|
|
||||||
nextPage := p.Page + 1
|
|
||||||
if nextPage > totalPages { nextPage = totalPages }
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "posts/index.html", common.BuildPageData(c, gin.H{
|
c.HTML(http.StatusOK, "posts/index.html", common.BuildPageData(c, gin.H{
|
||||||
"Title": "社区帖子",
|
"Title": "社区帖子",
|
||||||
@ -61,8 +57,8 @@ func (ctrl *PostController) ListPage(c *gin.Context) {
|
|||||||
"Total": total,
|
"Total": total,
|
||||||
"Page": p.Page,
|
"Page": p.Page,
|
||||||
"TotalPages": totalPages,
|
"TotalPages": totalPages,
|
||||||
"PrevPage": prevPage,
|
"PrevPage": p.PrevPage(),
|
||||||
"NextPage": nextPage,
|
"NextPage": p.NextPage(totalPages),
|
||||||
"Keyword": keyword,
|
"Keyword": keyword,
|
||||||
"ExtraCSS": "/static/css/posts.css",
|
"ExtraCSS": "/static/css/posts.css",
|
||||||
}))
|
}))
|
||||||
@ -276,12 +272,30 @@ func (ctrl *PostController) Delete(c *gin.Context) {
|
|||||||
|
|
||||||
// Submit API 提交审核
|
// Submit API 提交审核
|
||||||
func (ctrl *PostController) Submit(c *gin.Context) {
|
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)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Error(c, http.StatusBadRequest, "无效的帖子 ID")
|
common.Error(c, http.StatusBadRequest, "无效的帖子 ID")
|
||||||
return
|
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 err := ctrl.postService.SubmitForAudit(uint(id)); err != nil {
|
||||||
if errors.Is(err, common.ErrPostCannotSubmit) || errors.Is(err, common.ErrPostNotFound) {
|
if errors.Is(err, common.ErrPostCannotSubmit) || errors.Is(err, common.ErrPostNotFound) {
|
||||||
common.Error(c, http.StatusBadRequest, err.Error())
|
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 := common.Pagination{Page: page, PageSize: pageSize}
|
||||||
p.DefaultPagination()
|
p.DefaultPagination()
|
||||||
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
|
|
||||||
|
|
||||||
common.Ok(c, model.PostListResult{
|
common.Ok(c, model.PostListResult{
|
||||||
Items: posts,
|
Items: posts,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: p.Page,
|
Page: p.Page,
|
||||||
TotalPages: totalPages,
|
TotalPages: p.TotalPages(total),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { common, createLowlight } from 'lowlight'
|
|||||||
let editor = null
|
let editor = null
|
||||||
let currentHTML = ''
|
let currentHTML = ''
|
||||||
let draftTimer = null
|
let draftTimer = null
|
||||||
|
let submitting = false
|
||||||
|
|
||||||
// ---- 初始化 ----
|
// ---- 初始化 ----
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@ -337,25 +338,34 @@ function handleKeyboard(e) {
|
|||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const postId = document.getElementById('postId')?.value
|
if (submitting) return
|
||||||
const title = document.getElementById('postTitle')?.value.trim()
|
submitting = true
|
||||||
|
|
||||||
if (!title) { alert('请输入标题'); return }
|
const submitBtn = document.querySelector('.btn-submit')
|
||||||
|
if (submitBtn) {
|
||||||
// 获取编辑器 HTML 内容
|
submitBtn.disabled = true
|
||||||
const body = editor ? editor.getHTML() : ''
|
submitBtn.textContent = '提交中...'
|
||||||
// 忽略空段落
|
}
|
||||||
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') : ''
|
|
||||||
|
|
||||||
try {
|
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, {
|
const resp = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
@ -380,5 +390,12 @@ async function handleSubmit(e) {
|
|||||||
} else {
|
} else {
|
||||||
alert('请求失败,请重试')
|
alert('请求失败,请重试')
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
submitting = false
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false
|
||||||
|
submitBtn.textContent = submitBtn.textContent === '提交中...' ?
|
||||||
|
(document.getElementById('postId')?.value ? '保存修改' : '发布帖子') : submitBtn.textContent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user