辣评投稿管理系统的实现(四)

 

投稿系统概述

投稿管理系统是辣评平台的核心功能,用于管理用户提交的小说作品。系统需要支持投稿的创建、编辑、删除、查询等多种操作。

系统目标

  • 支持灵活的投稿创建和管理
  • 提供高效的投稿查询和展示
  • 实现文件上传和存储管理
  • 支持投稿的版本管理

投稿数据结构设计

核心数据模型

// Submission 投稿模型
type Submission struct {
    ID              uint      `gorm:"primaryKey"`
    UserID          uint      `gorm:"index"`
    CompetitionID   uint      `gorm:"index"`
    Title           string    `gorm:"index"`
    NovelType       string    // 小说类型
    Content         string    `gorm:"type:longtext"`
    FilePath        string    // 文件路径
    FileSize        int64     // 文件大小
    WordCount       int       // 字数统计
    Status          string    // 投稿状态
    AuthorBio       string    `gorm:"type:text"` // 作者自述
    CreatedAt       time.Time
    UpdatedAt       time.Time
    DeletedAt       gorm.DeletedAt `gorm:"index"`
    
    // 关联
    User            *User
    Competition     *Competition
}

// SubmissionFile 投稿文件模型
type SubmissionFile struct {
    ID              uint
    SubmissionID    uint      `gorm:"index"`
    FileName        string
    FilePath        string
    FileSize        int64
    FileType        string
    UploadedAt      time.Time
    CreatedAt       time.Time
}

文件上传与存储

文件上传处理

// UploadSubmissionFile 上传投稿文件
func (s *SubmissionService) UploadSubmissionFile(ctx context.Context, userID uint, file *multipart.FileHeader) (string, error) {
    // 1. 验证文件大小
    if file.Size > 50*1024*1024 { // 50MB 限制
        return "", errors.New("文件过大")
    }
    
    // 2. 验证文件类型
    allowedTypes := map[string]bool{
        "text/plain": true,
        "application/pdf": true,
        "application/msword": true,
    }
    
    if !allowedTypes[file.Header.Get("Content-Type")] {
        return "", errors.New("不支持的文件类型")
    }
    
    // 3. 生成文件路径
    timestamp := time.Now().Unix()
    fileName := fmt.Sprintf("%d_%d_%s", userID, timestamp, file.Filename)
    filePath := filepath.Join("uploads", "submissions", fileName)
    
    // 4. 保存文件
    src, err := file.Open()
    if err != nil {
        return "", err
    }
    defer src.Close()
    
    dst, err := os.Create(filePath)
    if err != nil {
        return "", err
    }
    defer dst.Close()
    
    if _, err := io.Copy(dst, src); err != nil {
        return "", err
    }
    
    return filePath, nil
}

文件存在性检查

// CheckFileExists 检查文件是否存在
func (s *SubmissionService) CheckFileExists(filePath string) bool {
    _, err := os.Stat(filePath)
    return err == nil
}

// ValidateSubmissionFiles 验证投稿文件
func (s *SubmissionService) ValidateSubmissionFiles(ctx context.Context, submissionID uint) error {
    var submission Submission
    if err := s.db.First(&submission, submissionID).Error; err != nil {
        return err
    }
    
    // 检查主文件
    if !s.CheckFileExists(submission.FilePath) {
        return errors.New("投稿文件不存在")
    }
    
    // 检查关联文件
    var files []SubmissionFile
    if err := s.db.Where("submission_id = ?", submissionID).Find(&files).Error; err != nil {
        return err
    }
    
    for _, file := range files {
        if !s.CheckFileExists(file.FilePath) {
            return fmt.Errorf("文件 %s 不存在", file.FileName)
        }
    }
    
    return nil
}

投稿编辑与删除功能

投稿编辑

// EditSubmission 编辑投稿
func (s *SubmissionService) EditSubmission(ctx context.Context, submissionID, userID uint, req *EditSubmissionRequest) error {
    submission := &Submission{}
    
    // 1. 获取投稿
    if err := s.db.First(submission, submissionID).Error; err != nil {
        return err
    }
    
    // 2. 验证权限
    if submission.UserID != userID {
        return errors.New("无权编辑此投稿")
    }
    
    // 3. 检查投稿状态
    if submission.Status == "submitted" {
        return errors.New("已提交的投稿不能编辑")
    }
    
    // 4. 更新投稿信息
    updates := map[string]interface{}{
        "title": req.Title,
        "content": req.Content,
        "novel_type": req.NovelType,
        "author_bio": req.AuthorBio,
        "word_count": len(req.Content),
    }
    
    return s.db.Model(submission).Updates(updates).Error
}

投稿删除

// DeleteSubmission 删除投稿
func (s *SubmissionService) DeleteSubmission(ctx context.Context, submissionID, userID uint) error {
    submission := &Submission{}
    
    // 1. 获取投稿
    if err := s.db.First(submission, submissionID).Error; err != nil {
        return err
    }
    
    // 2. 验证权限
    if submission.UserID != userID {
        return errors.New("无权删除此投稿")
    }
    
    // 3. 删除关联文件
    if err := s.deleteSubmissionFiles(ctx, submissionID); err != nil {
        return err
    }
    
    // 4. 软删除投稿
    return s.db.Delete(submission).Error
}

// deleteSubmissionFiles 删除投稿文件
func (s *SubmissionService) deleteSubmissionFiles(ctx context.Context, submissionID uint) error {
    var files []SubmissionFile
    if err := s.db.Where("submission_id = ?", submissionID).Find(&files).Error; err != nil {
        return err
    }
    
    for _, file := range files {
        if err := os.Remove(file.FilePath); err != nil {
            // 记录错误但继续处理
            log.Printf("删除文件失败: %v", err)
        }
    }
    
    return s.db.Where("submission_id = ?", submissionID).Delete(&SubmissionFile{}).Error
}

投稿筛选与查询优化

高级筛选

// SubmissionFilter 投稿筛选条件
type SubmissionFilter struct {
    UserID        uint
    CompetitionID uint
    NovelType     string
    Status        string
    Keyword       string
    Page          int
    PageSize      int
}

// QuerySubmissions 查询投稿
func (s *SubmissionService) QuerySubmissions(ctx context.Context, filter *SubmissionFilter) ([]Submission, int64, error) {
    var submissions []Submission
    var total int64
    
    query := s.db
    
    // 应用筛选条件
    if filter.UserID > 0 {
        query = query.Where("user_id = ?", filter.UserID)
    }
    if filter.CompetitionID > 0 {
        query = query.Where("competition_id = ?", filter.CompetitionID)
    }
    if filter.NovelType != "" {
        query = query.Where("novel_type = ?", filter.NovelType)
    }
    if filter.Status != "" {
        query = query.Where("status = ?", filter.Status)
    }
    if filter.Keyword != "" {
        query = query.Where("title LIKE ? OR content LIKE ?", "%"+filter.Keyword+"%", "%"+filter.Keyword+"%")
    }
    
    // 获取总数
    if err := query.Model(&Submission{}).Count(&total).Error; err != nil {
        return nil, 0, err
    }
    
    // 分页查询
    offset := (filter.Page - 1) * filter.PageSize
    if err := query.Offset(offset).Limit(filter.PageSize).
        Order("created_at DESC").
        Find(&submissions).Error; err != nil {
        return nil, 0, err
    }
    
    return submissions, total, nil
}

查询优化

// 使用预加载和索引优化查询
func (s *SubmissionService) GetSubmissionDetail(ctx context.Context, submissionID uint) (*Submission, error) {
    submission := &Submission{}
    
    err := s.db.Preload("User").
        Preload("Competition").
        Where("id = ?", submissionID).
        First(submission).Error
    
    return submission, err
}

字数统计与内容分析

字数统计

// CountWords 统计字数
func (s *SubmissionService) CountWords(content string) int {
    // 移除空白字符
    content = strings.TrimSpace(content)
    
    // 统计字数(支持中文和英文)
    count := 0
    for _, r := range content {
        if unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsPunct(r) {
            count++
        }
    }
    
    return count
}

// UpdateWordCount 更新字数统计
func (s *SubmissionService) UpdateWordCount(ctx context.Context, submissionID uint) error {
    submission := &Submission{}
    if err := s.db.First(submission, submissionID).Error; err != nil {
        return err
    }
    
    wordCount := s.CountWords(submission.Content)
    return s.db.Model(submission).Update("word_count", wordCount).Error
}

投稿状态管理

状态流转

// SubmissionStatus 投稿状态常量
const (
    StatusDraft     = "draft"      // 草稿
    StatusSubmitted = "submitted"  // 已提交
    StatusReviewing = "reviewing"  // 审核中
    StatusApproved  = "approved"   // 已通过
    StatusRejected  = "rejected"   // 已拒绝
)

// SubmitSubmission 提交投稿
func (s *SubmissionService) SubmitSubmission(ctx context.Context, submissionID, userID uint) error {
    submission := &Submission{}
    
    if err := s.db.First(submission, submissionID).Error; err != nil {
        return err
    }
    
    // 验证权限和状态
    if submission.UserID != userID {
        return errors.New("无权提交此投稿")
    }
    if submission.Status != StatusDraft {
        return errors.New("只能提交草稿状态的投稿")
    }
    
    // 验证投稿内容
    if err := s.validateSubmissionContent(submission); err != nil {
        return err
    }
    
    // 更新状态
    return s.db.Model(submission).Update("status", StatusSubmitted).Error
}

// validateSubmissionContent 验证投稿内容
func (s *SubmissionService) validateSubmissionContent(submission *Submission) error {
    if submission.Title == "" {
        return errors.New("标题不能为空")
    }
    if len(submission.Content) < 100 {
        return errors.New("内容过短")
    }
    if submission.NovelType == "" {
        return errors.New("小说类型不能为空")
    }
    return nil
}

API 接口设计

创建投稿

POST /api/submissions
{
    "title": "投稿标题",
    "content": "投稿内容",
    "novel_type": "科幻",
    "author_bio": "作者自述"
}

获取投稿列表

GET /api/submissions?competition_id=1&page=1&page_size=10

编辑投稿

PUT /api/submissions/{id}
{
    "title": "新标题",
    "content": "新内容"
}

提交投稿

POST /api/submissions/{id}/submit

删除投稿

DELETE /api/submissions/{id}

总结

投稿管理系统的实现涉及多个关键方面:

核心功能

  • 完整的投稿生命周期管理
  • 灵活的文件上传和存储
  • 高效的查询和筛选
  • 准确的字数统计

关键设计原则

  • 数据安全性和一致性
  • 文件管理的可靠性
  • 查询性能优化
  • 用户权限控制

投稿管理系统为辣评平台提供了完整的内容管理能力。

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International