GeneratedArticle.java
8.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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;
}
}