This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/docs/post-system-audit.md
Victor_Jay bacfd4945d feat: 编辑器与帖子展示布局优化 + 新增个人空间
- 编辑器投稿页布局优化:标题与编辑器高度动态适配,工具栏与内容区视觉对齐
- 稿件展示页布局优化:MD 渲染区与评论区视觉分离,代码块主题微调
- CSRF 中间件:图像上传端点豁免,解决 Vditor 拖拽/粘贴上传 403
- Post 状态映射、GetGinUser、SaveUploadedFile 提取至 common 包,遵循审计建议
- 新增个人空间功能:/space(需登录)重定向到 /space/:uid,/space/:uid 公开访问
- 空间页模板:参考 B 站布局,展示头像、用户名、个性签名、UID、加入时间及稿件列表
- 导航栏:用户名链接指向 /space,新增「设置」入口
- 分层实现:SpaceController → SpaceService → PostRepo.FindByUserID,严格 ISP 接口隔离
2026-05-30 19:06:10 +08:00

8.7 KiB
Raw Permalink Blame History

帖子系统代码审计报告

审计日期2026-05-30
审计范围:internal/ 下所有 Post 相关文件router / controller / service / repository / model
审计标准DRY / KISS / YAGNI / LoD / SOLID + 分层架构规范


一、架构合规性总览

检查项 状态 说明
分层依赖方向 router→controller→service→repo→model严格单向
Controller 接口文件 controller/interfaces.go + admin/interfaces.go
Service Repository 接口 service/repository.go8 个方法,无冗余
ISP 接口隔离 前台 postUseCase 与后台 adminPostUseCase 分离
DIP 依赖倒置 Controller→接口, Service→接口
依赖注入 全部构造函数注入,无硬编码
KISS 无过度设计
YAGNI 无超前功能
LoD 无跨层直接依赖

二、问题清单

🔴 中等问题4 个)

问题 1Repository 方法代码重复(~70%

详情
文件 internal/repository/post_repo.go
位置 FindPageable(行 49-73vs FindAdminPageable(行 76-102
违反原则 DRY
描述 两个方法的核心查询逻辑Table + Select + Joins + keyword LIKE + Count + Offset/Limit + ORDER BY几乎完全一致唯一区别是 FindPageable 固定过滤 status = 'approved'FindAdminPageable 支持可选的 status 参数。两段代码约 70% 重复。

现状:

// FindPageable (行 49-73)
func (r *PostRepo) FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error) {
    query := r.db.Table("posts").
        Select("posts.*, users.username as author_name").
        Joins("LEFT JOIN users ON users.uid = posts.user_id").
        Where("posts.deleted_at IS NULL").
        Where("posts.status = ?", model.PostStatusApproved)  // ← 唯一差异

    if keyword != "" { /* LIKE 过滤 */ }
    // ... Count + Offset/Limit + Find
}

// FindAdminPageable (行 76-102)
func (r *PostRepo) FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error) {
    query := r.db.Table("posts").
        Select("posts.*, users.username as author_name").
        Joins("LEFT JOIN users ON users.uid = posts.user_id").
        Where("posts.deleted_at IS NULL")
        // ← 无硬编码 status由参数控制

    if keyword != "" { /* LIKE 过滤 */ }
    if status != "" { query = query.Where("posts.status = ?", status) }
    // ... Count + Offset/Limit + Find
}

建议修复: 提取私有方法 findPageableCommon,两个公开方法调用它并传入各自的 WHERE 条件。


问题 2Service 方法重复

详情
文件 internal/service/post_service.go
位置 List(行 148-156vs ListAdmin(行 159-167
违反原则 DRY
描述 两个方法的 nil 检查 + 分页调用模式完全一致,唯一区别是调用的 repo 方法不同。

现状:

// List (行 148-156)
func (s *PostService) List(keyword string, page, pageSize int) ([]model.Post, int64, error) {
    p := common.Pagination{Page: page, PageSize: pageSize}
    p.DefaultPagination()
    posts, total, err := s.repo.FindPageable(keyword, p.Offset(), p.PageSize)
    if posts == nil { posts = []model.Post{} }
    return posts, total, err
}

// ListAdmin (行 159-167)
func (s *PostService) ListAdmin(keyword, status string, page, pageSize int) ([]model.Post, int64, error) {
    p := common.Pagination{Page: page, PageSize: pageSize}
    p.DefaultPagination()
    posts, total, err := s.repo.FindAdminPageable(keyword, status, p.Offset(), p.PageSize)
    if posts == nil { posts = []model.Post{} }
    return posts, total, err
}

建议修复: 提取公共的 Pagination 创建 + nil 检查逻辑。


问题 3Controller 权限检查重复 6 处

详情
文件 internal/controller/post_controller.go
位置 ShowPage(行 82-90)、EditPage(行 144-150)、Update(行 202-211)、Delete(行 239-248)、Submit(行 272-281)、ShowAPI(行 333-339)
违反原则 DRY
描述 以下模式在 6 个方法中逐字重复:
post, err := ctrl.postService.GetByID(uint(id))
if err != nil {
    common.Error(c, http.StatusNotFound, "帖子不存在")
    return
}
if !model.IsPostAccessible(uid, role, post.UserID) {
    common.Error(c, http.StatusForbidden, "无权操作此帖子")
    return
}

建议修复: 提取私有方法 getPostAndCheckAccess(id, c) 返回 (*model.Post, bool)


问题 4Model 层职责过重

详情
文件 internal/model/post.go
位置 行 48-83
违反原则 SRP单一职责
描述 一个文件混合了多种职责:
内容 类型 应在位置
Post struct 数据模型 Model 层
PostStatusDraft 等常量 状态常量 Model 层
PostStatusDisplayNames 视图映射 应移到 View/Controller 层
PostListResult DTO 应移到 model/dto.go
PostCreateRequest / PostUpdateRequest 请求 DTO 应移到 model/dto.go
PostRejectRequest 请求 DTO 应移到 model/dto.go
PostListQuery 查询 DTO 应移到 model/dto.go
IsPostAccessible 业务权限逻辑 应移到 Service 层

建议修复: DTO 结构体移到 model/dto.goPostStatusDisplayNames 移到 common 或 controllerIsPostAccessible 移到 service 层或独立权限模块。


🟡 轻微问题3 个)

问题 5工具函数位置不当

详情
文件 internal/controller/post_controller.go
位置 saveUploadedFile(行 410-430
违反原则 SRP / LoD
描述 saveUploadedFile 是通用文件 I/O 工具函数(创建目录 + 32KB buffer 循环写入),与 HTTP 处理无关,不依赖 controller 的任何字段。放在 controller 文件中职责不匹配。

建议修复: 移到 internal/common/ 或新建 internal/util/ 包。


问题 6Controller 层分页重复初始化

详情
文件 internal/controller/post_controller.go
位置 ListPage(行 47-49ListAPI(行 307-308
违反原则 DRY
描述 Controller 层为模板渲染再次创建 Pagination 对象并调用 DefaultPagination(),而 Service 层 List() / ListAdmin() 内部已经做过一次分页参数校验。Controller 层可以信任 Service 返回的 total 直接计算。
// controller 层重复的:
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
// ... 然后调用 service.List()service 里又做了一遍

建议修复: Controller 层直接用 common.Pagination.PageCount(total, pageSize) 计算总页数,不再重复调用 DefaultPagination()


问题 7Admin Restore 错误分类缺失

详情
文件 internal/controller/admin/admin_post_controller.go
位置 Restore(行 153-155
违反原则 一致性
描述 同文件内其他方法(Approve/Reject/Unlock/Lock)都对 ErrPostNotFounderrors.Is 精确匹配返回 400Restore 没有,所有错误统一返回 500。
// Restore — 缺少错误分类
func (ctrl *AdminPostController) Restore(c *gin.Context) {
    // ...
    if err := ctrl.postService.Restore(id); err != nil {
        // ← 此处应匹配 ErrPostNotFound返回 400 而非 500
        common.Error(c, http.StatusInternalServerError, err.Error())
        return
    }
}

建议修复: 添加 errors.Is(err, common.ErrPostNotFound) 判断,返回 400。


三、亮点

  1. 状态机设计清晰Post 状态流转draft → pending → approved/rejected → locked每个转换有明确前置条件检查和专用错误哨兵
  2. 接口设计优秀postUseCase vs adminPostUseCase 的分离体现良好的关注点分离
  3. 错误哨兵统一管理common/errors.go 集中定义所有 Post 相关错误
  4. 审核开关灵活:通过 SiteSettings.IsAuditEnabled() 运行时控制
  5. Shortcode 扩展性好:新增类型只需添加常量和 case 分支
  6. 软删除 + 恢复:完善的软删除和恢复机制

四、优先级建议

优先级 问题编号 原因
P0 无阻塞性问题
P1 1, 3 Repository/Controller 重复影响维护成本
P2 2, 4 Service 重复 + Model 职责拆分
P3 5, 6, 7 轻微优化项