- 编辑器投稿页布局优化:标题与编辑器高度动态适配,工具栏与内容区视觉对齐 - 稿件展示页布局优化:MD 渲染区与评论区视觉分离,代码块主题微调 - CSRF 中间件:图像上传端点豁免,解决 Vditor 拖拽/粘贴上传 403 - Post 状态映射、GetGinUser、SaveUploadedFile 提取至 common 包,遵循审计建议 - 新增个人空间功能:/space(需登录)重定向到 /space/:uid,/space/:uid 公开访问 - 空间页模板:参考 B 站布局,展示头像、用户名、个性签名、UID、加入时间及稿件列表 - 导航栏:用户名链接指向 /space,新增「设置」入口 - 分层实现:SpaceController → SpaceService → PostRepo.FindByUserID,严格 ISP 接口隔离
232 lines
8.7 KiB
Markdown
232 lines
8.7 KiB
Markdown
# 帖子系统代码审计报告
|
||
|
||
> 审计日期: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.go`,8 个方法,无冗余 |
|
||
| ISP 接口隔离 | ✅ | 前台 `postUseCase` 与后台 `adminPostUseCase` 分离 |
|
||
| DIP 依赖倒置 | ✅ | Controller→接口, Service→接口 |
|
||
| 依赖注入 | ✅ | 全部构造函数注入,无硬编码 |
|
||
| KISS | ✅ | 无过度设计 |
|
||
| YAGNI | ✅ | 无超前功能 |
|
||
| LoD | ✅ | 无跨层直接依赖 |
|
||
|
||
---
|
||
|
||
## 二、问题清单
|
||
|
||
### 🔴 中等问题(4 个)
|
||
|
||
#### 问题 1:Repository 方法代码重复(~70%)
|
||
|
||
| 项 | 详情 |
|
||
|----|------|
|
||
| **文件** | `internal/repository/post_repo.go` |
|
||
| **位置** | `FindPageable`(行 49-73)vs `FindAdminPageable`(行 76-102) |
|
||
| **违反原则** | DRY |
|
||
| **描述** | 两个方法的核心查询逻辑(Table + Select + Joins + keyword LIKE + Count + Offset/Limit + ORDER BY)几乎完全一致,唯一区别是 `FindPageable` 固定过滤 `status = 'approved'`,`FindAdminPageable` 支持可选的 status 参数。两段代码约 70% 重复。 |
|
||
|
||
**现状:**
|
||
```go
|
||
// 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 条件。
|
||
|
||
---
|
||
|
||
#### 问题 2:Service 方法重复
|
||
|
||
| 项 | 详情 |
|
||
|----|------|
|
||
| **文件** | `internal/service/post_service.go` |
|
||
| **位置** | `List`(行 148-156)vs `ListAdmin`(行 159-167) |
|
||
| **违反原则** | DRY |
|
||
| **描述** | 两个方法的 nil 检查 + 分页调用模式完全一致,唯一区别是调用的 repo 方法不同。 |
|
||
|
||
**现状:**
|
||
```go
|
||
// 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 检查逻辑。
|
||
|
||
---
|
||
|
||
#### 问题 3:Controller 权限检查重复 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 个方法中逐字重复: |
|
||
|
||
```go
|
||
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)`。
|
||
|
||
---
|
||
|
||
#### 问题 4:Model 层职责过重
|
||
|
||
| 项 | 详情 |
|
||
|----|------|
|
||
| **文件** | `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.go`,`PostStatusDisplayNames` 移到 common 或 controller,`IsPostAccessible` 移到 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/` 包。
|
||
|
||
---
|
||
|
||
#### 问题 6:Controller 层分页重复初始化
|
||
|
||
| 项 | 详情 |
|
||
|----|------|
|
||
| **文件** | `internal/controller/post_controller.go` |
|
||
| **位置** | `ListPage`(行 47-49)和 `ListAPI`(行 307-308) |
|
||
| **违反原则** | DRY |
|
||
| **描述** | Controller 层为模板渲染再次创建 `Pagination` 对象并调用 `DefaultPagination()`,而 Service 层 `List()` / `ListAdmin()` 内部已经做过一次分页参数校验。Controller 层可以信任 Service 返回的 total 直接计算。 |
|
||
|
||
```go
|
||
// controller 层重复的:
|
||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||
p.DefaultPagination()
|
||
// ... 然后调用 service.List(),service 里又做了一遍
|
||
```
|
||
|
||
**建议修复:** Controller 层直接用 `common.Pagination.PageCount(total, pageSize)` 计算总页数,不再重复调用 `DefaultPagination()`。
|
||
|
||
---
|
||
|
||
#### 问题 7:Admin Restore 错误分类缺失
|
||
|
||
| 项 | 详情 |
|
||
|----|------|
|
||
| **文件** | `internal/controller/admin/admin_post_controller.go` |
|
||
| **位置** | `Restore`(行 153-155) |
|
||
| **违反原则** | 一致性 |
|
||
| **描述** | 同文件内其他方法(`Approve`/`Reject`/`Unlock`/`Lock`)都对 `ErrPostNotFound` 做 `errors.Is` 精确匹配返回 400,但 `Restore` 没有,所有错误统一返回 500。 |
|
||
|
||
```go
|
||
// 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 | 轻微优化项 |
|