辣评作者自述功能开发(十二)

 

历史说明

本文主要记录作者自述功能在早期阶段的独立模型设计(如 AuthorBio 方案)。
后续主线实现调整为将自述字段内聚到投稿模型(Submission.StatementStatementCountStatementUpdateAt),并通过投稿相关接口维护。
因此,本文中的独立模型与接口示例应理解为阶段性设计,用于保留演进过程。


作者自述功能概述

作者自述功能允许投稿者为自己的作品添加创作背景、灵感来源等信息,增强作品的可读性和吸引力。

功能目标

  • 支持作者自述内容编辑
  • 实现字数统计功能
  • 提供自述内容展示
  • 支持自述内容查看和编辑

功能需求分析

核心需求

  1. 编辑功能 - 作者可以编辑自述内容
  2. 字数限制 - 设置合理的字数限制
  3. 展示功能 - 在投稿详情页展示自述内容
  4. 版本管理 - 记录自述内容的修改历史

用户场景

  • 作者在投稿时添加自述
  • 作者修改已有的自述
  • 读者查看作者自述
  • 管理员审核自述内容

数据模型设计

数据结构

// AuthorBio 作者自述模型
type AuthorBio struct {
    ID              uint      `gorm:"primaryKey"`
    SubmissionID    uint      `gorm:"uniqueIndex"`
    UserID          uint      `gorm:"index"`
    Content         string    `gorm:"type:text"`
    WordCount       int       // 字数统计
    IsPublished     bool      // 是否已发布
    PublishedAt     *time.Time
    CreatedAt       time.Time
    UpdatedAt       time.Time
    DeletedAt       gorm.DeletedAt `gorm:"index"`
    
    // 关联
    Submission      *Submission
    User            *User
}

// AuthorBioVersion 作者自述版本历史
type AuthorBioVersion struct {
    ID              uint
    AuthorBioID     uint
    Content         string    `gorm:"type:text"`
    WordCount       int
    VersionNumber   int
    ChangeSummary   string
    CreatedAt       time.Time
}

// AuthorBioTemplate 作者自述模板
type AuthorBioTemplate struct {
    ID              uint
    Title           string
    Description     string
    Content         string    `gorm:"type:text"`
    IsActive        bool
    CreatedAt       time.Time
}

编辑与展示逻辑

创建和编辑自述

// CreateAuthorBio 创建作者自述
func (s *AuthorBioService) CreateAuthorBio(ctx context.Context, submissionID, userID uint, content string) (*AuthorBio, error) {
    // 1. 验证权限
    submission := &Submission{}
    if err := s.db.First(submission, submissionID).Error; err != nil {
        return nil, err
    }
    
    if submission.UserID != userID {
        return nil, errors.New("无权为此投稿添加自述")
    }
    
    // 2. 验证内容
    if err := s.validateBioContent(content); err != nil {
        return nil, err
    }
    
    // 3. 计算字数
    wordCount := s.countWords(content)
    
    // 4. 创建自述
    bio := &AuthorBio{
        SubmissionID: submissionID,
        UserID:       userID,
        Content:      content,
        WordCount:    wordCount,
        IsPublished:  true,
        PublishedAt:  timePtr(time.Now()),
    }
    
    if err := s.db.Create(bio).Error; err != nil {
        return nil, err
    }
    
    // 5. 记录版本
    s.createBioVersion(ctx, bio.ID, content, wordCount, 1, "初始版本")
    
    return bio, nil
}

// UpdateAuthorBio 更新作者自述
func (s *AuthorBioService) UpdateAuthorBio(ctx context.Context, bioID, userID uint, content string) (*AuthorBio, error) {
    bio := &AuthorBio{}
    
    // 1. 获取自述
    if err := s.db.First(bio, bioID).Error; err != nil {
        return nil, err
    }
    
    // 2. 验证权限
    if bio.UserID != userID {
        return nil, errors.New("无权编辑此自述")
    }
    
    // 3. 验证内容
    if err := s.validateBioContent(content); err != nil {
        return nil, err
    }
    
    // 4. 计算新的字数
    newWordCount := s.countWords(content)
    
    // 5. 记录旧版本
    oldVersion := &AuthorBioVersion{
        AuthorBioID:   bioID,
        Content:       bio.Content,
        WordCount:     bio.WordCount,
        VersionNumber: 1,
        ChangeSummary: "编辑前版本",
    }
    s.db.Create(oldVersion)
    
    // 6. 更新自述
    updates := map[string]interface{}{
        "content":    content,
        "word_count": newWordCount,
    }
    
    if err := s.db.Model(bio).Updates(updates).Error; err != nil {
        return nil, err
    }
    
    // 7. 记录新版本
    s.createBioVersion(ctx, bioID, content, newWordCount, 2, "用户编辑")
    
    return bio, nil
}

// GetAuthorBio 获取作者自述
func (s *AuthorBioService) GetAuthorBio(ctx context.Context, submissionID uint) (*AuthorBio, error) {
    bio := &AuthorBio{}
    
    if err := s.db.Where("submission_id = ?", submissionID).
        First(bio).Error; err != nil {
        return nil, err
    }
    
    return bio, nil
}

内容验证

// validateBioContent 验证自述内容
func (s *AuthorBioService) validateBioContent(content string) error {
    // 1. 检查内容长度
    if len(content) == 0 {
        return errors.New("自述内容不能为空")
    }
    
    if len(content) < 10 {
        return errors.New("自述内容过短,至少需要10个字符")
    }
    
    if len(content) > 5000 {
        return errors.New("自述内容过长,最多5000个字符")
    }
    
    // 2. 检查字数
    wordCount := s.countWords(content)
    if wordCount > 1000 {
        return errors.New("自述字数过多,最多1000字")
    }
    
    // 3. 检查敏感词
    if s.containsSensitiveWords(content) {
        return errors.New("自述内容包含不适当的词汇")
    }
    
    return nil
}

字数统计实现

字数计算

// countWords 统计字数
func (s *AuthorBioService) countWords(content string) int {
    // 移除空白字符
    content = strings.TrimSpace(content)
    
    // 统计字数(支持中文和英文)
    count := 0
    inWord := false
    
    for _, r := range content {
        if unicode.IsLetter(r) || unicode.IsNumber(r) {
            if !inWord {
                count++
                inWord = true
            }
        } else if unicode.IsPunct(r) || unicode.IsSpace(r) {
            inWord = false
        } else if unicode.Is(unicode.Han, r) {
            // 中文字符单独计数
            count++
        }
    }
    
    return count
}

// GetWordCountStats 获取字数统计
func (s *AuthorBioService) GetWordCountStats(ctx context.Context, competitionID uint) (*WordCountStatistics, error) {
    stats := &WordCountStatistics{}
    
    // 统计平均字数
    s.db.Model(&AuthorBio{}).
        Joins("JOIN submissions ON author_bios.submission_id = submissions.id").
        Where("submissions.competition_id = ?", competitionID).
        Select("AVG(word_count) as avg_word_count").
        Row().
        Scan(&stats.AverageWordCount)
    
    // 统计最大字数
    s.db.Model(&AuthorBio{}).
        Joins("JOIN submissions ON author_bios.submission_id = submissions.id").
        Where("submissions.competition_id = ?", competitionID).
        Select("MAX(word_count) as max_word_count").
        Row().
        Scan(&stats.MaxWordCount)
    
    // 统计最小字数
    s.db.Model(&AuthorBio{}).
        Joins("JOIN submissions ON author_bios.submission_id = submissions.id").
        Where("submissions.competition_id = ?", competitionID).
        Select("MIN(word_count) as min_word_count").
        Row().
        Scan(&stats.MinWordCount)
    
    return stats, nil
}

用户体验优化

前端编辑界面

<template>
  <div class="author-bio-editor">
    <div class="editor-header">
      <h3>作者自述</h3>
      <span class="word-count">/1000 字</span>
    </div>
    
    <div class="editor-toolbar">
      <el-button @click="insertTemplate">插入模板</el-button>
      <el-button @click="clearContent">清空</el-button>
      <el-button @click="previewBio">预览</el-button>
    </div>
    
    <el-input 
      v-model="bioContent" 
      type="textarea"
      :rows="10"
      placeholder="请输入作者自述(最多1000字)"
      @input="handleContentChange"
      maxlength="5000"
    ></el-input>
    
    <div class="editor-footer">
      <el-button type="primary" @click="saveBio">保存</el-button>
      <el-button @click="cancelEdit">取消</el-button>
    </div>
    
    <div v-if="showPreview" class="preview-panel">
      <div class="preview-content" v-html="bioContent"></div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    submissionId: Number,
    initialContent: String
  },
  data() {
    return {
      bioContent: this.initialContent || '',
      currentWordCount: 0,
      showPreview: false,
      templates: []
    }
  },
  methods: {
    handleContentChange() {
      this.currentWordCount = this.countWords(this.bioContent)
    },
    countWords(content) {
      // 简单的字数统计
      return content.length
    },
    insertTemplate() {
      // 插入模板
    },
    clearContent() {
      this.$confirm('确定清空内容吗?').then(() => {
        this.bioContent = ''
        this.currentWordCount = 0
      })
    },
    previewBio() {
      this.showPreview = !this.showPreview
    },
    async saveBio() {
      try {
        await this.$api.updateAuthorBio(this.submissionId, {
          content: this.bioContent
        })
        this.$message.success('保存成功')
        this.$emit('bio-saved')
      } catch (error) {
        this.$message.error('保存失败')
      }
    },
    cancelEdit() {
      this.$emit('cancel')
    }
  },
  mounted() {
    this.handleContentChange()
  }
}
</script>

<style scoped>
.author-bio-editor {
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
}

.editor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.word-count {
  color: #909399;
  font-size: 14px;
}

.editor-toolbar {
  margin-bottom: 15px;
}

.editor-footer {
  margin-top: 15px;
  text-align: right;
}

.preview-panel {
  margin-top: 20px;
  padding: 15px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.preview-content {
  line-height: 1.6;
  color: #606266;
}
</style>

版本管理

版本历史

// GetBioVersions 获取自述版本历史
func (s *AuthorBioService) GetBioVersions(ctx context.Context, bioID uint) ([]AuthorBioVersion, error) {
    var versions []AuthorBioVersion
    
    if err := s.db.Where("author_bio_id = ?", bioID).
        Order("version_number DESC").
        Find(&versions).Error; err != nil {
        return nil, err
    }
    
    return versions, nil
}

// RestoreBioVersion 恢复自述版本
func (s *AuthorBioService) RestoreBioVersion(ctx context.Context, versionID, userID uint) error {
    version := &AuthorBioVersion{}
    if err := s.db.First(version, versionID).Error; err != nil {
        return err
    }
    
    bio := &AuthorBio{}
    if err := s.db.First(bio, version.AuthorBioID).Error; err != nil {
        return err
    }
    
    // 验证权限
    if bio.UserID != userID {
        return errors.New("无权恢复此版本")
    }
    
    // 恢复内容
    return s.db.Model(bio).Updates(map[string]interface{}{
        "content":    version.Content,
        "word_count": version.WordCount,
    }).Error
}

API 接口

说明:以下接口为当期设计稿接口。当前主线实现请以投稿接口中的自述字段读写为准(如 PUT /api/submissions/:id/statement)。

POST /api/submissions/{id}/author-bio - 创建作者自述
GET /api/submissions/{id}/author-bio - 获取作者自述
PUT /api/author-bio/{id} - 更新作者自述
DELETE /api/author-bio/{id} - 删除作者自述
GET /api/author-bio/{id}/versions - 获取版本历史
POST /api/author-bio/versions/{id}/restore - 恢复版本

总结

作者自述功能为投稿者提供了展示创作背景和灵感的平台,增强了作品的可读性和吸引力。

关键特性

  • 灵活的编辑功能
  • 准确的字数统计
  • 版本管理
  • 模板支持
  • 内容验证

这个功能提升了平台的专业度和用户体验。

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