feat: 编辑器与帖子展示布局优化 + 新增个人空间
- 编辑器投稿页布局优化:标题与编辑器高度动态适配,工具栏与内容区视觉对齐 - 稿件展示页布局优化:MD 渲染区与评论区视觉分离,代码块主题微调 - CSRF 中间件:图像上传端点豁免,解决 Vditor 拖拽/粘贴上传 403 - Post 状态映射、GetGinUser、SaveUploadedFile 提取至 common 包,遵循审计建议 - 新增个人空间功能:/space(需登录)重定向到 /space/:uid,/space/:uid 公开访问 - 空间页模板:参考 B 站布局,展示头像、用户名、个性签名、UID、加入时间及稿件列表 - 导航栏:用户名链接指向 /space,新增「设置」入口 - 分层实现:SpaceController → SpaceService → PostRepo.FindByUserID,严格 ISP 接口隔离
This commit is contained in:
@ -3,7 +3,6 @@ package controller
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -44,22 +43,24 @@ func (ctrl *PostController) ListPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if posts == nil {
|
||||
posts = []model.Post{}
|
||||
totalPages := common.PageCount(total, pageSize)
|
||||
prevPage := page - 1
|
||||
if prevPage < 1 {
|
||||
prevPage = 1
|
||||
}
|
||||
nextPage := page + 1
|
||||
if nextPage > totalPages {
|
||||
nextPage = totalPages
|
||||
}
|
||||
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
totalPages := p.TotalPages(total)
|
||||
|
||||
c.HTML(http.StatusOK, "posts/index.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "社区帖子",
|
||||
"Posts": posts,
|
||||
"Total": total,
|
||||
"Page": p.Page,
|
||||
"Page": page,
|
||||
"TotalPages": totalPages,
|
||||
"PrevPage": p.PrevPage(),
|
||||
"NextPage": p.NextPage(totalPages),
|
||||
"PrevPage": prevPage,
|
||||
"NextPage": nextPage,
|
||||
"Keyword": keyword,
|
||||
"ExtraCSS": "/static/css/posts.css",
|
||||
}))
|
||||
@ -83,42 +84,39 @@ func (ctrl *PostController) ShowPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
var uid uint
|
||||
var role string
|
||||
if uidObj, exists := c.Get("uid"); exists {
|
||||
uid, _ = uidObj.(uint)
|
||||
}
|
||||
if roleObj, exists := c.Get("role"); exists {
|
||||
role, _ = roleObj.(string)
|
||||
}
|
||||
uid, role, _ := common.GetGinUser(c)
|
||||
|
||||
// 仅 approved 公开可见(作者本人 + moderator+ 可预览其他状态)
|
||||
if post.Status != model.PostStatusApproved {
|
||||
isAuthor := uid == post.UserID
|
||||
isModerator := model.HasMinRole(role, model.RoleModerator)
|
||||
if post.Status != model.PostStatusApproved && !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "未找到",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if !isAuthor && !isModerator {
|
||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "未找到",
|
||||
}))
|
||||
return
|
||||
ctrl.applyShortcode(post)
|
||||
|
||||
// Open Graph 社交分享数据
|
||||
ogImage := common.ExtractFirstImage(post.Body)
|
||||
if ogImage != "" && !strings.HasPrefix(ogImage, "http") {
|
||||
prefix := ""
|
||||
if !strings.HasPrefix(ogImage, "/") {
|
||||
prefix = "/"
|
||||
}
|
||||
ogImage = "https://" + c.Request.Host + prefix + ogImage
|
||||
}
|
||||
|
||||
// 处理 shortcode:[zone:type:params] → HTML 占位符
|
||||
// 占位 div 会被 Vditor.preview() 保留,由前端 shortcode.js 渲染为卡片
|
||||
if ctrl.shortcodeSvc != nil {
|
||||
result := ctrl.shortcodeSvc.Process(post.Body)
|
||||
post.Body = result.ProcessedBody
|
||||
}
|
||||
ogURL := fmt.Sprintf("https://%s/posts/%d", c.Request.Host, id)
|
||||
|
||||
c.HTML(http.StatusOK, "posts/show.html", common.BuildPageData(c, gin.H{
|
||||
"Title": post.Title,
|
||||
"Post": post,
|
||||
"UID": uid,
|
||||
"StatusNames": model.PostStatusDisplayNames,
|
||||
"ExtraCSS": "/static/css/posts.css",
|
||||
"Title": post.Title,
|
||||
"Post": post,
|
||||
"UID": uid,
|
||||
"StatusNames": common.PostStatusDisplayNames,
|
||||
"ExtraCSS": "/static/css/posts.css",
|
||||
"OgTitle": post.Title,
|
||||
"OgDescription": post.Excerpt,
|
||||
"OgImage": ogImage,
|
||||
"OgURL": ogURL,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -148,16 +146,8 @@ func (ctrl *PostController) EditPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 权限检查:仅作者和 moderator+ 可编辑
|
||||
var uid uint
|
||||
var role string
|
||||
if uidObj, exists := c.Get("uid"); exists {
|
||||
uid, _ = uidObj.(uint)
|
||||
}
|
||||
if roleObj, exists := c.Get("role"); exists {
|
||||
role, _ = roleObj.(string)
|
||||
}
|
||||
if post.UserID != uid && !model.HasMinRole(role, model.RoleModerator) {
|
||||
uid, role, _ := common.GetGinUser(c)
|
||||
if !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "未找到",
|
||||
}))
|
||||
@ -173,8 +163,7 @@ func (ctrl *PostController) EditPage(c *gin.Context) {
|
||||
|
||||
// Create API 发帖
|
||||
func (ctrl *PostController) Create(c *gin.Context) {
|
||||
uidObj, _ := c.Get("uid")
|
||||
uid, ok := uidObj.(uint)
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
@ -197,8 +186,7 @@ func (ctrl *PostController) Create(c *gin.Context) {
|
||||
|
||||
// Update API 编辑帖子
|
||||
func (ctrl *PostController) Update(c *gin.Context) {
|
||||
uidObj, _ := c.Get("uid")
|
||||
uid, ok := uidObj.(uint)
|
||||
uid, role, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
@ -216,16 +204,7 @@ func (ctrl *PostController) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 权限检查:取帖子归属
|
||||
post, err := ctrl.postService.GetByID(uint(id))
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusNotFound, "帖子不存在")
|
||||
return
|
||||
}
|
||||
roleObj, _ := c.Get("role")
|
||||
role, _ := roleObj.(string)
|
||||
if post.UserID != uid && !model.HasMinRole(role, model.RoleModerator) {
|
||||
common.Error(c, http.StatusForbidden, "无权编辑此帖子")
|
||||
if _, ok := ctrl.getPostAndCheckAccess(uint(id), c, uid, role); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@ -243,8 +222,7 @@ func (ctrl *PostController) Update(c *gin.Context) {
|
||||
|
||||
// Delete API 删除帖子
|
||||
func (ctrl *PostController) Delete(c *gin.Context) {
|
||||
uidObj, _ := c.Get("uid")
|
||||
uid, ok := uidObj.(uint)
|
||||
uid, role, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
@ -256,16 +234,7 @@ func (ctrl *PostController) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
post, err := ctrl.postService.GetByID(uint(id))
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusNotFound, "帖子不存在")
|
||||
return
|
||||
}
|
||||
|
||||
roleObj, _ := c.Get("role")
|
||||
role, _ := roleObj.(string)
|
||||
if post.UserID != uid && !model.HasMinRole(role, model.RoleModerator) {
|
||||
common.Error(c, http.StatusForbidden, "无权删除此帖子")
|
||||
if _, ok := ctrl.getPostAndCheckAccess(uint(id), c, uid, role); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@ -277,10 +246,24 @@ func (ctrl *PostController) Delete(c *gin.Context) {
|
||||
common.OkMessage(c, "删除成功")
|
||||
}
|
||||
|
||||
// getPostAndCheckAccess 获取帖子并检查当前用户权限
|
||||
// 返回 (*model.Post, true) 表示通过;返回 (nil, false) 表示已写入错误响应
|
||||
func (ctrl *PostController) getPostAndCheckAccess(id uint, c *gin.Context, uid uint, role string) (*model.Post, bool) {
|
||||
post, err := ctrl.postService.GetByID(id)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusNotFound, "帖子不存在")
|
||||
return nil, false
|
||||
}
|
||||
if !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
||||
common.Error(c, http.StatusForbidden, "无权操作此帖子")
|
||||
return nil, false
|
||||
}
|
||||
return post, true
|
||||
}
|
||||
|
||||
// Submit API 提交审核
|
||||
func (ctrl *PostController) Submit(c *gin.Context) {
|
||||
uidObj, _ := c.Get("uid")
|
||||
uid, ok := uidObj.(uint)
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
@ -292,12 +275,12 @@ func (ctrl *PostController) Submit(c *gin.Context) {
|
||||
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
|
||||
@ -327,18 +310,11 @@ func (ctrl *PostController) ListAPI(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if posts == nil {
|
||||
posts = []model.Post{}
|
||||
}
|
||||
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
|
||||
common.Ok(c, model.PostListResult{
|
||||
Items: posts,
|
||||
Total: total,
|
||||
Page: p.Page,
|
||||
TotalPages: p.TotalPages(total),
|
||||
Page: page,
|
||||
TotalPages: common.PageCount(total, pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
@ -358,27 +334,14 @@ func (ctrl *PostController) ShowAPI(c *gin.Context) {
|
||||
|
||||
// 权限控制:非 approved 帖子仅作者和 moderator+ 可见
|
||||
if post.Status != model.PostStatusApproved {
|
||||
var uid uint
|
||||
var role string
|
||||
if uidObj, exists := c.Get("uid"); exists {
|
||||
uid, _ = uidObj.(uint)
|
||||
}
|
||||
if roleObj, exists := c.Get("role"); exists {
|
||||
role, _ = roleObj.(string)
|
||||
}
|
||||
isAuthor := uid == post.UserID
|
||||
isModerator := model.HasMinRole(role, model.RoleModerator)
|
||||
if !isAuthor && !isModerator {
|
||||
uid, role, _ := common.GetGinUser(c)
|
||||
if !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
||||
common.Error(c, http.StatusNotFound, "帖子不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 shortcode:[zone:type:params] → HTML 占位符
|
||||
if ctrl.shortcodeSvc != nil {
|
||||
result := ctrl.shortcodeSvc.Process(post.Body)
|
||||
post.Body = result.ProcessedBody
|
||||
}
|
||||
ctrl.applyShortcode(post)
|
||||
|
||||
common.Ok(c, post)
|
||||
}
|
||||
@ -390,8 +353,7 @@ var allowedImageExt = map[string]bool{
|
||||
|
||||
// UploadImage 上传帖子图片(需登录,multipart/form-data)
|
||||
func (ctrl *PostController) UploadImage(c *gin.Context) {
|
||||
uidObj, _ := c.Get("uid")
|
||||
uid, ok := uidObj.(uint)
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
@ -430,7 +392,7 @@ func (ctrl *PostController) UploadImage(c *gin.Context) {
|
||||
filename := fmt.Sprintf("%d_%d%s", uid, ts, ext)
|
||||
savePath := filepath.Join(storageDir, filename)
|
||||
|
||||
if err := saveUploadedFile(file, savePath); err != nil {
|
||||
if err := common.SaveUploadedFile(file, savePath); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "保存图片失败")
|
||||
return
|
||||
}
|
||||
@ -439,26 +401,11 @@ func (ctrl *PostController) UploadImage(c *gin.Context) {
|
||||
common.VditorUploadOk(c, map[string]string{header.Filename: url})
|
||||
}
|
||||
|
||||
// saveUploadedFile 将 multipart.File 写入目标路径
|
||||
func saveUploadedFile(file multipart.File, dst string) error {
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
// applyShortcode 处理帖子正文中的 shortcode 标记,替换为 HTML 占位符
|
||||
func (ctrl *PostController) applyShortcode(post *model.Post) {
|
||||
if ctrl.shortcodeSvc != nil {
|
||||
result := ctrl.shortcodeSvc.Process(post.Body)
|
||||
post.Body = result.ProcessedBody
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 限制读取 5MB 防止内存放大
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user