投稿系统概述
投稿管理系统是辣评平台的核心功能,用于管理用户提交的小说作品。系统需要支持投稿的创建、编辑、删除、查询等多种操作。
系统目标
- 支持灵活的投稿创建和管理
- 提供高效的投稿查询和展示
- 实现文件上传和存储管理
- 支持投稿的版本管理
投稿数据结构设计
核心数据模型
// 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}
总结
投稿管理系统的实现涉及多个关键方面:
核心功能
- 完整的投稿生命周期管理
- 灵活的文件上传和存储
- 高效的查询和筛选
- 准确的字数统计
关键设计原则
- 数据安全性和一致性
- 文件管理的可靠性
- 查询性能优化
- 用户权限控制
投稿管理系统为辣评平台提供了完整的内容管理能力。