# 帖子系统代码审计报告

> 审计日期：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 | 轻微优化项 |
