Ver Fonte

feat: 存量空标签文章一次性回填(admin backfill-tags 端点)

Xiaogang Liao há 6 dias atrás
pai
commit
4d48ac3968

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -269,6 +269,12 @@ public class AdminArticleController {
         return Result.success("操作成功");
     }
 
+    @PostMapping("/backfill-tags")
+    public Result<Integer> backfillTags() {
+        int n = articleService.backfillEmptyPublishedTags();
+        return Result.success(n);
+    }
+
     @PostMapping("/toggle-featured")
     public Result<String> toggleFeatured(@RequestBody Map<String, Object> body) {
         Long id = ParamUtils.getLong(body.get("id"));

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java

@@ -180,6 +180,21 @@ public class ArticleService {
         return visible.size() > size ? visible.subList(0, size) : visible;
     }
 
+    /**
+     * 存量回填:为所有「已发布且 tags 为空」的文章异步触发自动打标(幂等,可重复调用)。
+     * @return 触发的文章数
+     */
+    public int backfillEmptyPublishedTags() {
+        List<Article> list = articleMapper.selectList(new LambdaQueryWrapper<Article>()
+                .eq(Article::getStatus, "published")
+                .and(w -> w.isNull(Article::getTags).or().eq(Article::getTags, "")));
+        for (Article a : list) {
+            ensureAutoTagsIfEmpty(a.getId());
+        }
+        log.info("存量空标签文章回填触发: {} 篇", list.size());
+        return list.size();
+    }
+
     /**
      * 相关文章:按标签匹配(FIND_IN_SET 精确匹配整标签),共享标签数降序 → 发布时间降序,
      * 排除自身,仅取已发布,经可见性过滤后截取前 size 篇。无标签/无匹配返回空。

+ 75 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/ArticleBackfillTest.java

@@ -0,0 +1,75 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.service.ArticleService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * 存量空标签文章一次性回填单元测试
+ */
+class ArticleBackfillTest {
+
+    private ArticleService service;
+    private ArticleMapper articleMapper;
+    private ThreadPoolTaskExecutor taskExecutor;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        service = new ArticleService();
+        articleMapper = mock(ArticleMapper.class);
+        taskExecutor = mock(ThreadPoolTaskExecutor.class);
+        inject("articleMapper", articleMapper);
+        inject("taskExecutor", taskExecutor);
+    }
+
+    private void inject(String field, Object value) throws Exception {
+        Field f = ArticleService.class.getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(service, value);
+    }
+
+    private Article published(Long id, String tags) {
+        Article a = new Article();
+        a.setId(id);
+        a.setStatus("published");
+        a.setTags(tags);
+        a.setTitle("t" + id);
+        a.setContent("<p>正文</p>");
+        return a;
+    }
+
+    @Test
+    void backfillEmptyPublishedTags_onlyEmptyPublishedScheduled() {
+        Article empty1 = published(1L, "");
+        Article empty2 = published(2L, null);
+        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(empty1, empty2));
+        when(articleMapper.selectById(1L)).thenReturn(empty1);
+        when(articleMapper.selectById(2L)).thenReturn(empty2);
+
+        int n = service.backfillEmptyPublishedTags();
+
+        assertEquals(2, n);
+        verify(taskExecutor, times(2)).execute(any(Runnable.class));
+    }
+
+    @Test
+    void backfillEmptyPublishedTags_noEmptyArticles_returnsZero() {
+        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(java.util.Collections.emptyList());
+
+        int n = service.backfillEmptyPublishedTags();
+
+        assertEquals(0, n);
+        verify(taskExecutor, never()).execute(any(Runnable.class));
+    }
+}