GeneratedArticle.java 8.2 KB
package com.aigeo.article.entity;

import com.aigeo.common.enums.ContentStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;

/**
 * 生成文章实体类
 * 对应数据库表:ai_generated_articles
 * 
 * 存储AI生成的文章内容,包括:
 * - 文章标题、内容和摘要
 * - SEO相关元数据
 * - 文章状态和发布信息
 * - 质量评分和统计数据
 *
 * @author AIGEO Team
 * @since 1.0.0
 */
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ai_generated_articles", indexes = {
    @Index(name = "idx_articles_company_status", columnList = "company_id, status"),
    @Index(name = "idx_articles_task_id", columnList = "task_id"),
    @Index(name = "idx_articles_slug", columnList = "slug")
})
public class GeneratedArticle {
    
    /**
     * 主键ID
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, updatable = false)
    private Integer id;

    /**
     * 公司ID(多租户字段)
     */
    @NotNull(message = "公司ID不能为空")
    @Column(name = "company_id", nullable = false)
    private Integer companyId;

    /**
     * 关联的生成任务ID
     */
    @Column(name = "task_id")
    private Integer taskId;

    /**
     * 文章标题
     */
    @NotBlank(message = "文章标题不能为空")
    @Column(name = "title", nullable = false, length = 500)
    private String title;

    /**
     * URL友好的标题(slug)
     */
    @Column(name = "slug", length = 500)
    private String slug;

    /**
     * 文章摘要/描述
     */
    @Column(name = "excerpt", length = 1000)
    private String excerpt;

    /**
     * 文章正文内容
     */
    @Column(name = "content", columnDefinition = "LONGTEXT")
    private String content;

    /**
     * SEO标题(用于<title>标签)
     */
    @Column(name = "seo_title", length = 200)
    private String seoTitle;

    /**
     * SEO描述(用于meta description)
     */
    @Column(name = "seo_description", length = 500)
    private String seoDescription;

    /**
     * 文章标签(JSON数组或逗号分隔)
     */
    @Column(name = "tags", length = 1000)
    private String tags;

    /**
     * 文章字数统计
     */
    @Column(name = "word_count")
    private Integer wordCount;

    /**
     * 阅读时长估算(分钟)
     */
    @Column(name = "reading_time")
    private Integer readingTime;

    /**
     * 文章状态
     */
    @Enumerated(EnumType.STRING)
    @Column(name = "status")
    @Builder.Default
    private ContentStatus status = ContentStatus.DRAFT;

    /**
     * 质量评分(1-100分)
     */
    @Column(name = "quality_score")
    private Integer qualityScore;

    /**
     * AI置信度评分(0-1之间)
     */
    @Column(name = "ai_confidence")
    private Double aiConfidence;

    /**
     * 原创性评分(0-1之间,1表示完全原创)
     */
    @Column(name = "originality_score")
    private Double originalityScore;

    /**
     * SEO评分(1-100分)
     */
    @Column(name = "seo_score")
    private Integer seoScore;

    /**
     * 文章特色图片URL
     */
    @Column(name = "featured_image_url", length = 500)
    private String featuredImageUrl;

    /**
     * 发布时间(NULL表示未发布)
     */
    @Column(name = "published_at")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime publishedAt;

    /**
     * 最后编辑的用户ID
     */
    @Column(name = "last_edited_by")
    private Integer lastEditedBy;

    /**
     * 浏览次数统计
     */
    @Column(name = "view_count")
    @Builder.Default
    private Integer viewCount = 0;

    /**
     * 喜欢次数统计
     */
    @Column(name = "like_count")
    @Builder.Default
    private Integer likeCount = 0;

    /**
     * 分享次数统计
     */
    @Column(name = "share_count")
    @Builder.Default
    private Integer shareCount = 0;

    /**
     * 结构化数据(Schema.org JSON-LD)
     */
    @Column(name = "structured_data", columnDefinition = "JSON")
    private String structuredData;

    /**
     * 元数据(JSON格式存储额外信息)
     */
    @Column(name = "metadata", columnDefinition = "JSON")
    private String metadata;

    /**
     * 创建时间
     */
    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;

    /**
     * 更新时间
     */
    @UpdateTimestamp
    @Column(name = "updated_at")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updatedAt;

    /**
     * 实体创建前的处理
     */
    @PrePersist
    protected void onCreate() {
        if (status == null) status = ContentStatus.DRAFT;
        if (viewCount == null) viewCount = 0;
        if (likeCount == null) likeCount = 0;
        if (shareCount == null) shareCount = 0;
        
        // 生成slug
        if (slug == null && title != null) {
            this.slug = generateSlugFromTitle(title);
        }
        
        // 如果没有SEO标题,使用文章标题
        if (seoTitle == null && title != null) {
            this.seoTitle = title.length() > 60 ? title.substring(0, 60) + "..." : title;
        }
        
        // 估算阅读时长(按平均阅读速度250字/分钟计算)
        if (readingTime == null && wordCount != null) {
            this.readingTime = Math.max(1, (int) Math.ceil(wordCount / 250.0));
        }
    }

    /**
     * 从标题生成URL友好的slug
     * @param title 文章标题
     * @return URL友好的slug
     */
    private String generateSlugFromTitle(String title) {
        if (title == null) return null;
        
        return title.toLowerCase()
                   .replaceAll("[^a-zA-Z0-9\u4e00-\u9fa5\\s-]", "") // 保留字母、数字、中文、空格和连字符
                   .replaceAll("\\s+", "-") // 空格替换为连字符
                   .replaceAll("-+", "-") // 多个连字符合并为一个
                   .replaceAll("^-|-$", ""); // 移除开头和结尾的连字符
    }

    /**
     * 检查文章是否已发布
     */
    public boolean isPublished() {
        return status == ContentStatus.PUBLISHED && publishedAt != null;
    }

    /**
     * 检查文章是否可以发布
     */
    public boolean canPublish() {
        return status == ContentStatus.APPROVED || 
               status == ContentStatus.GENERATED || 
               status == ContentStatus.COMPLETED;
    }

    /**
     * 获取综合评分(质量+SEO+原创性的平均值)
     */
    public Double getOverallScore() {
        int count = 0;
        double total = 0.0;
        
        if (qualityScore != null) {
            total += qualityScore;
            count++;
        }
        if (seoScore != null) {
            total += seoScore;
            count++;
        }
        if (originalityScore != null) {
            total += originalityScore * 100; // 转换为1-100分制
            count++;
        }
        
        return count > 0 ? total / count : null;
    }

    /**
     * 获取参与度评分(基于浏览、点赞、分享数据)
     */
    public Double getEngagementScore() {
        if (viewCount == 0) return 0.0;
        
        // 简单的参与度计算:(点赞数*2 + 分享数*3) / 浏览数 * 100
        return ((likeCount * 2.0 + shareCount * 3.0) / viewCount) * 100;
    }

    /**
     * 增加浏览次数
     */
    public void incrementViewCount() {
        this.viewCount = (viewCount == null ? 0 : viewCount) + 1;
    }

    /**
     * 增加点赞次数
     */
    public void incrementLikeCount() {
        this.likeCount = (likeCount == null ? 0 : likeCount) + 1;
    }

    /**
     * 增加分享次数
     */
    public void incrementShareCount() {
        this.shareCount = (shareCount == null ? 0 : shareCount) + 1;
    }
}