2025-09-01 18:29:59 +08:00
|
|
|
|
package entities
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ArticleStatus 文章状态枚举
|
|
|
|
|
|
type ArticleStatus string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
ArticleStatusDraft ArticleStatus = "draft" // 草稿
|
|
|
|
|
|
ArticleStatusPublished ArticleStatus = "published" // 已发布
|
|
|
|
|
|
ArticleStatusArchived ArticleStatus = "archived" // 已归档
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Article 文章聚合根
|
|
|
|
|
|
// 系统的核心内容实体,提供文章的完整生命周期管理
|
|
|
|
|
|
// 支持草稿、发布、归档状态,实现Entity接口便于统一管理
|
|
|
|
|
|
type Article struct {
|
|
|
|
|
|
// 基础标识
|
|
|
|
|
|
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"文章唯一标识"`
|
|
|
|
|
|
Title string `gorm:"type:varchar(200);not null" json:"title" comment:"文章标题"`
|
|
|
|
|
|
Content string `gorm:"type:text;not null" json:"content" comment:"文章内容"`
|
|
|
|
|
|
Summary string `gorm:"type:varchar(500)" json:"summary" comment:"文章摘要"`
|
|
|
|
|
|
CoverImage string `gorm:"type:varchar(500)" json:"cover_image" comment:"封面图片"`
|
|
|
|
|
|
|
|
|
|
|
|
// 分类
|
|
|
|
|
|
CategoryID string `gorm:"type:varchar(36)" json:"category_id" comment:"分类ID"`
|
|
|
|
|
|
|
|
|
|
|
|
// 状态管理
|
|
|
|
|
|
Status ArticleStatus `gorm:"type:varchar(20);not null;default:'draft'" json:"status" comment:"文章状态"`
|
|
|
|
|
|
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
|
|
|
|
|
|
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
|
|
|
|
|
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
2025-09-02 16:37:28 +08:00
|
|
|
|
TaskID string `gorm:"type:varchar(100)" json:"task_id" comment:"定时任务ID"`
|
2025-09-01 18:29:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 统计信息
|
|
|
|
|
|
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
|
|
|
|
|
|
|
|
|
|
|
|
// 时间戳字段
|
|
|
|
|
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
|
|
|
|
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
|
|
|
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
|
|
|
|
|
|
|
|
|
|
|
// 关联关系
|
|
|
|
|
|
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty" comment:"分类信息"`
|
|
|
|
|
|
Tags []Tag `gorm:"many2many:article_tag_relations;" json:"tags,omitempty" comment:"标签列表"`
|
|
|
|
|
|
|
|
|
|
|
|
// 领域事件 (不持久化)
|
|
|
|
|
|
domainEvents []interface{} `gorm:"-" json:"-"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TableName 指定表名
|
|
|
|
|
|
func (Article) TableName() string {
|
|
|
|
|
|
return "articles"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BeforeCreate GORM钩子:创建前自动生成UUID
|
|
|
|
|
|
func (a *Article) BeforeCreate(tx *gorm.DB) error {
|
|
|
|
|
|
if a.ID == "" {
|
|
|
|
|
|
a.ID = uuid.New().String()
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 实现 Entity 接口 - 提供统一的实体管理接口
|
|
|
|
|
|
// GetID 获取实体唯一标识
|
|
|
|
|
|
func (a *Article) GetID() string {
|
|
|
|
|
|
return a.ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetCreatedAt 获取创建时间
|
|
|
|
|
|
func (a *Article) GetCreatedAt() time.Time {
|
|
|
|
|
|
return a.CreatedAt
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUpdatedAt 获取更新时间
|
|
|
|
|
|
func (a *Article) GetUpdatedAt() time.Time {
|
|
|
|
|
|
return a.UpdatedAt
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate 验证文章信息
|
|
|
|
|
|
// 检查文章必填字段是否完整,确保数据的有效性
|
|
|
|
|
|
func (a *Article) Validate() error {
|
|
|
|
|
|
if a.Title == "" {
|
|
|
|
|
|
return NewValidationError("文章标题不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.Content == "" {
|
|
|
|
|
|
return NewValidationError("文章内容不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证标题长度
|
|
|
|
|
|
if len(a.Title) > 200 {
|
|
|
|
|
|
return NewValidationError("文章标题不能超过200个字符")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证摘要长度
|
|
|
|
|
|
if a.Summary != "" && len(a.Summary) > 500 {
|
|
|
|
|
|
return NewValidationError("文章摘要不能超过500个字符")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Publish 发布文章
|
|
|
|
|
|
func (a *Article) Publish() error {
|
|
|
|
|
|
if a.Status == ArticleStatusPublished {
|
|
|
|
|
|
return NewValidationError("文章已经是发布状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.Status = ArticleStatusPublished
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
a.PublishedAt = &now
|
|
|
|
|
|
a.ScheduledAt = nil // 清除定时发布时间
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SchedulePublish 定时发布文章
|
2025-09-02 16:37:28 +08:00
|
|
|
|
func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error {
|
2025-09-01 18:29:59 +08:00
|
|
|
|
if a.Status == ArticleStatusPublished {
|
|
|
|
|
|
return NewValidationError("文章已经是发布状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if scheduledTime.Before(time.Now()) {
|
|
|
|
|
|
return NewValidationError("定时发布时间不能早于当前时间")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
|
|
|
|
|
|
a.ScheduledAt = &scheduledTime
|
2025-09-02 16:37:28 +08:00
|
|
|
|
a.TaskID = taskID
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateSchedulePublish 更新定时发布时间
|
|
|
|
|
|
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string) error {
|
|
|
|
|
|
if a.Status == ArticleStatusPublished {
|
|
|
|
|
|
return NewValidationError("文章已经是发布状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if scheduledTime.Before(time.Now()) {
|
|
|
|
|
|
return NewValidationError("定时发布时间不能早于当前时间")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.ScheduledAt = &scheduledTime
|
|
|
|
|
|
a.TaskID = taskID
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CancelSchedulePublish 取消定时发布
|
|
|
|
|
|
func (a *Article) CancelSchedulePublish() error {
|
|
|
|
|
|
if a.Status == ArticleStatusPublished {
|
|
|
|
|
|
return NewValidationError("文章已经是发布状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.ScheduledAt = nil
|
|
|
|
|
|
a.TaskID = ""
|
2025-09-01 18:29:59 +08:00
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsScheduled 判断是否已设置定时发布
|
|
|
|
|
|
func (a *Article) IsScheduled() bool {
|
|
|
|
|
|
return a.ScheduledAt != nil && a.Status == ArticleStatusDraft
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetScheduledTime 获取定时发布时间
|
|
|
|
|
|
func (a *Article) GetScheduledTime() *time.Time {
|
|
|
|
|
|
return a.ScheduledAt
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Archive 归档文章
|
|
|
|
|
|
func (a *Article) Archive() error {
|
|
|
|
|
|
if a.Status == ArticleStatusArchived {
|
|
|
|
|
|
return NewValidationError("文章已经是归档状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
a.Status = ArticleStatusArchived
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IncrementViewCount 增加阅读量
|
|
|
|
|
|
func (a *Article) IncrementViewCount() {
|
|
|
|
|
|
a.ViewCount++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetFeatured 设置推荐状态
|
|
|
|
|
|
func (a *Article) SetFeatured(featured bool) {
|
|
|
|
|
|
a.IsFeatured = featured
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsPublished 判断是否已发布
|
|
|
|
|
|
func (a *Article) IsPublished() bool {
|
|
|
|
|
|
return a.Status == ArticleStatusPublished
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsDraft 判断是否为草稿
|
|
|
|
|
|
func (a *Article) IsDraft() bool {
|
|
|
|
|
|
return a.Status == ArticleStatusDraft
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsArchived 判断是否已归档
|
|
|
|
|
|
func (a *Article) IsArchived() bool {
|
|
|
|
|
|
return a.Status == ArticleStatusArchived
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CanEdit 判断是否可以编辑
|
|
|
|
|
|
func (a *Article) CanEdit() bool {
|
|
|
|
|
|
return a.Status == ArticleStatusDraft
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CanPublish 判断是否可以发布
|
|
|
|
|
|
func (a *Article) CanPublish() bool {
|
|
|
|
|
|
return a.Status == ArticleStatusDraft
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CanArchive 判断是否可以归档
|
|
|
|
|
|
func (a *Article) CanArchive() bool {
|
|
|
|
|
|
return a.Status == ArticleStatusPublished
|
|
|
|
|
|
}
|