历史说明
本文主要记录作者自述功能在早期阶段的独立模型设计(如 AuthorBio 方案)。
后续主线实现调整为将自述字段内聚到投稿模型(Submission.Statement、StatementCount、StatementUpdateAt),并通过投稿相关接口维护。
因此,本文中的独立模型与接口示例应理解为阶段性设计,用于保留演进过程。
作者自述功能概述
作者自述功能允许投稿者为自己的作品添加创作背景、灵感来源等信息,增强作品的可读性和吸引力。
功能目标
- 支持作者自述内容编辑
- 实现字数统计功能
- 提供自述内容展示
- 支持自述内容查看和编辑
功能需求分析
核心需求
- 编辑功能 - 作者可以编辑自述内容
- 字数限制 - 设置合理的字数限制
- 展示功能 - 在投稿详情页展示自述内容
- 版本管理 - 记录自述内容的修改历史
用户场景
- 作者在投稿时添加自述
- 作者修改已有的自述
- 读者查看作者自述
- 管理员审核自述内容
数据模型设计
数据结构
// 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 - 恢复版本
总结
作者自述功能为投稿者提供了展示创作背景和灵感的平台,增强了作品的可读性和吸引力。
关键特性
- 灵活的编辑功能
- 准确的字数统计
- 版本管理
- 模板支持
- 内容验证
这个功能提升了平台的专业度和用户体验。