Skip to content

Commit dd39cbc

Browse files
committed
v 1.0091 后端添加定时发布功能
1 parent 5e99794 commit dd39cbc

7 files changed

Lines changed: 116 additions & 3 deletions

File tree

docs/openblog_mysql.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS articles (
5959
category_id BIGINT NULL,
6060
status VARCHAR(16) NOT NULL,
6161
published_at TIMESTAMP(6) NULL,
62+
scheduled_at TIMESTAMP(6) NULL,
6263
submitted_at TIMESTAMP(6) NULL,
6364
reviewed_at TIMESTAMP(6) NULL,
6465
rejected_reason VARCHAR(512) NULL,
@@ -71,6 +72,7 @@ CREATE TABLE IF NOT EXISTS articles (
7172
updated_at TIMESTAMP(6) NOT NULL,
7273
KEY idx_articles_author_id (author_id),
7374
KEY idx_articles_status_published_at (status, published_at),
75+
KEY idx_articles_status_scheduled_at (status, scheduled_at),
7476
KEY idx_articles_title (title),
7577
CONSTRAINT fk_articles_author
7678
FOREIGN KEY (author_id) REFERENCES users(id)

src/main/java/com/yqz/openblog/OpenBlogApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import org.springframework.boot.SpringApplication;
88
import org.springframework.boot.autoconfigure.SpringBootApplication;
99
import org.springframework.boot.context.properties.EnableConfigurationProperties;
10+
import org.springframework.scheduling.annotation.EnableScheduling;
1011

1112
@SpringBootApplication
13+
@EnableScheduling
1214
@EnableConfigurationProperties({
1315
SiteProperties.class,
1416
CorsProperties.class,

src/main/java/com/yqz/openblog/article/entity/Article.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ public class Article {
4343
@Column
4444
private Instant publishedAt;
4545

46+
/**
47+
* 定时发布的预约时间(UTC Instant)。当 status=SCHEDULED 时该字段必填。
48+
* 到点后由后台定时任务把文章置为 PUBLISHED,并把 publishedAt=scheduledAt。
49+
*/
50+
@Column
51+
private Instant scheduledAt;
52+
4653
@Column
4754
private Instant submittedAt;
4855

@@ -164,6 +171,14 @@ public void setSubmittedAt(Instant submittedAt) {
164171
this.submittedAt = submittedAt;
165172
}
166173

174+
public Instant getScheduledAt() {
175+
return scheduledAt;
176+
}
177+
178+
public void setScheduledAt(Instant scheduledAt) {
179+
this.scheduledAt = scheduledAt;
180+
}
181+
167182
public Instant getReviewedAt() {
168183
return reviewedAt;
169184
}

src/main/java/com/yqz/openblog/article/entity/ArticleStatus.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
public enum ArticleStatus {
44
DRAFT,
5+
/**
6+
* 已预约(到达 scheduledAt 前不可见),由定时任务自动转为 PUBLISHED。
7+
*/
8+
SCHEDULED,
59
PUBLISHED,
610
DELETED
711
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.yqz.openblog.article.job;
2+
3+
import com.yqz.openblog.article.service.ArticleService;
4+
import org.springframework.scheduling.annotation.Scheduled;
5+
import org.springframework.stereotype.Component;
6+
7+
/**
8+
* 定时发布:把到点的 SCHEDULED 文章自动发布为 PUBLISHED。
9+
*
10+
* 说明:多实例部署时通过“条件更新”保证只会发布一次,无需额外分布式锁。
11+
*/
12+
@Component
13+
public class ArticleScheduledPublishJob {
14+
15+
private final ArticleService articleService;
16+
17+
public ArticleScheduledPublishJob(ArticleService articleService) {
18+
this.articleService = articleService;
19+
}
20+
21+
@Scheduled(fixedDelayString = "${openblog.article-schedule.scan-delay-ms:30000}")
22+
public void run() {
23+
// 每轮最多发布 N 篇,避免长事务/长循环;下一轮会继续扫。
24+
articleService.publishDueScheduled(200);
25+
}
26+
}
27+

src/main/java/com/yqz/openblog/article/repo/ArticleMapper.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,43 @@
55
import com.yqz.openblog.article.entity.ArticleStatus;
66
import org.apache.ibatis.annotations.Mapper;
77
import org.apache.ibatis.annotations.Param;
8+
import org.apache.ibatis.annotations.Select;
89
import org.apache.ibatis.annotations.Update;
910

11+
import java.time.Instant;
12+
import java.util.List;
13+
1014
@Mapper
1115
public interface ArticleMapper extends BaseMapper<Article> {
1216

1317
@Update("update articles set view_count = view_count + 1 where id = #{articleId} and status = #{status}")
1418
int incrementViewCount(@Param("articleId") Long articleId, @Param("status") ArticleStatus status);
19+
20+
@Select("""
21+
select id
22+
from articles
23+
where status = 'SCHEDULED'
24+
and scheduled_at is not null
25+
and scheduled_at <= #{now}
26+
order by scheduled_at asc
27+
limit #{limit}
28+
""")
29+
List<Long> listDueScheduledIds(@Param("now") Instant now, @Param("limit") int limit);
30+
31+
/**
32+
* 原子发布:仅当仍为 SCHEDULED 且已到点时才会成功(多实例下确保只发布一次)。
33+
*/
34+
@Update("""
35+
update articles
36+
set status = 'PUBLISHED',
37+
published_at = scheduled_at,
38+
scheduled_at = null,
39+
updated_at = now(6)
40+
where id = #{articleId}
41+
and status = 'SCHEDULED'
42+
and scheduled_at is not null
43+
and scheduled_at <= #{now}
44+
""")
45+
int publishScheduledIfDue(@Param("articleId") Long articleId, @Param("now") Instant now);
1546
}
1647

src/main/java/com/yqz/openblog/article/service/ArticleService.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,17 @@ public ArticleListItemResponse publish(Long authorId, Long articleId, Instant pu
207207
if (a.getTitle() == null || a.getTitle().trim().isEmpty() || a.getContentMarkdown() == null || a.getContentMarkdown().trim().isEmpty()) {
208208
throw new BizException(4002, "标题和正文不能为空");
209209
}
210-
a.setStatus(ArticleStatus.PUBLISHED);
211-
a.setPublishedAt(publishedAt == null ? Instant.now() : publishedAt);
210+
Instant now = Instant.now();
211+
// 未来时间:按“定时发布”处理(文章在到点前不可见)
212+
if (publishedAt != null && publishedAt.isAfter(now)) {
213+
a.setStatus(ArticleStatus.SCHEDULED);
214+
a.setScheduledAt(publishedAt);
215+
a.setPublishedAt(null);
216+
} else {
217+
a.setStatus(ArticleStatus.PUBLISHED);
218+
a.setPublishedAt(publishedAt == null ? now : publishedAt);
219+
a.setScheduledAt(null);
220+
}
212221
a.setSubmittedAt(null);
213222
a.setReviewedAt(null);
214223
a.setRejectedReason(null);
@@ -217,6 +226,28 @@ public ArticleListItemResponse publish(Long authorId, Long articleId, Instant pu
217226
return mapListItem(a);
218227
}
219228

229+
/**
230+
* 定时任务扫描到点文章并发布。
231+
*
232+
* @return 本轮成功发布数量(可能为 0)
233+
*/
234+
public int publishDueScheduled(int batchSize) {
235+
int limit = Math.max(1, Math.min(batchSize, 500));
236+
Instant now = Instant.now();
237+
List<Long> ids = articleMapper.listDueScheduledIds(now, limit);
238+
if (ids.isEmpty()) return 0;
239+
240+
int published = 0;
241+
for (Long id : ids) {
242+
int updated = articleMapper.publishScheduledIfDue(id, now);
243+
if (updated > 0) {
244+
published++;
245+
publishedContentCache.evict(id);
246+
}
247+
}
248+
return published;
249+
}
250+
220251
public void unpublishOrDelete(Long authorId, Long articleId) {
221252
Article a = articleMapper.selectById(articleId);
222253
if (a == null) {
@@ -229,12 +260,13 @@ public void unpublishOrDelete(Long authorId, Long articleId) {
229260
return;
230261
}
231262
a.setStatus(ArticleStatus.DELETED);
263+
a.setScheduledAt(null);
232264
articleMapper.updateById(a);
233265
publishedContentCache.evict(articleId);
234266
}
235267

236268
public PageResult<ArticleListItemResponse> listMine(Long authorId, int page, int size) {
237-
List<ArticleStatus> statuses = List.of(ArticleStatus.DRAFT, ArticleStatus.PUBLISHED);
269+
List<ArticleStatus> statuses = List.of(ArticleStatus.DRAFT, ArticleStatus.SCHEDULED, ArticleStatus.PUBLISHED);
238270
Page<Article> mpPage = new Page<>(page + 1L, size);
239271
LambdaQueryWrapper<Article> w = Wrappers.lambdaQuery();
240272
w.eq(Article::getAuthorId, authorId)

0 commit comments

Comments
 (0)