Przeglądaj źródła

feat: 发布时若文章标签为空则异步自动生成并写回

Xiaogang Liao 6 dni temu
rodzic
commit
2e2f2649c2

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

@@ -611,6 +611,10 @@ public class ArticleService {
             article.setPublishedAt(new Date());
         }
         articleMapper.updateById(article);
+        // 发布时若标签为空则异步自动生成
+        if ("published".equals(status)) {
+            ensureAutoTagsIfEmpty(id);
+        }
     }
 
     @Transactional
@@ -943,6 +947,8 @@ public class ArticleService {
         article.setCreatedAt(new Date());
         article.setUpdatedAt(new Date());
         articleMapper.insert(article);
+        // 家庭可见直接发布:发布时若标签为空则异步自动生成(公开文章待审核,内部守卫跳过)
+        ensureAutoTagsIfEmpty(article.getId());
         return article;
     }
 
@@ -1018,6 +1024,36 @@ public class ArticleService {
         }
         article.setUpdatedAt(new Date());
         articleMapper.updateById(article);
+        // 审核通过发布:若标签为空则异步自动生成
+        if ("approved".equals(auditStatus)) {
+            ensureAutoTagsIfEmpty(articleId);
+        }
+    }
+
+    /**
+     * 发布路径自动打标入口:仅当文章已发布且 tags 为空时,异步调 LLM 生成并写回。
+     * 失败仅记录 warn,不阻塞发布流程。
+     */
+    private void ensureAutoTagsIfEmpty(Long articleId) {
+        Article article = articleMapper.selectById(articleId);
+        if (article == null || !"published".equals(article.getStatus())) {
+            return;
+        }
+        if (article.getTags() != null && !article.getTags().trim().isEmpty()) {
+            return;
+        }
+        final Long id = article.getId();
+        final String content = article.getContent();
+        taskExecutor.execute(() -> {
+            try {
+                List<String> tagNames = autoGenerateTags(content);
+                if (!tagNames.isEmpty()) {
+                    saveAutoTags(id, tagNames);
+                }
+            } catch (Exception e) {
+                log.warn("AI 自动标签生成失败(不影响文章): articleId={}, error={}", id, e.getMessage());
+            }
+        });
     }
 
     /**

+ 151 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/ArticleAutoTagServiceTest.java

@@ -0,0 +1,151 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.mapper.UserMapper;
+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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * 文章发布路径自动打标签单元测试(纯逻辑,mock Mapper / 线程池,无需 DB)
+ *
+ * 覆盖:发布/审核通过/家庭发布且 tags 为空时异步调度生成;
+ *       已有标签、未发布、文章不存在时不调度。
+ */
+class ArticleAutoTagServiceTest {
+
+    private ArticleService service;
+    private ArticleMapper articleMapper;
+    private UserMapper userMapper;
+    private ThreadPoolTaskExecutor taskExecutor;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        service = new ArticleService();
+        articleMapper = mock(ArticleMapper.class);
+        userMapper = mock(UserMapper.class);
+        taskExecutor = mock(ThreadPoolTaskExecutor.class);
+        inject("articleMapper", articleMapper);
+        inject("userMapper", userMapper);
+        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 article(Long id, String status, String tags) {
+        Article a = new Article();
+        a.setId(id);
+        a.setStatus(status);
+        a.setTags(tags);
+        a.setTitle("测试文章");
+        a.setContent("<p>正文</p>");
+        return a;
+    }
+
+    @Test
+    void toggleStatus_发布且标签为空_调度自动打标() {
+        when(articleMapper.selectById(1L)).thenReturn(article(1L, "draft", ""));
+
+        service.toggleStatus(1L, "published");
+
+        verify(taskExecutor, times(1)).execute(any(Runnable.class));
+    }
+
+    @Test
+    void toggleStatus_标签非空_不调度自动打标() {
+        when(articleMapper.selectById(1L)).thenReturn(article(1L, "draft", "健康,饮水"));
+
+        service.toggleStatus(1L, "published");
+
+        verify(taskExecutor, never()).execute(any(Runnable.class));
+    }
+
+    @Test
+    void toggleStatus_置为草稿_不调度自动打标() {
+        when(articleMapper.selectById(1L)).thenReturn(article(1L, "published", null));
+
+        service.toggleStatus(1L, "draft");
+
+        verify(taskExecutor, never()).execute(any(Runnable.class));
+    }
+
+    @Test
+    void toggleStatus_文章不存在_不调度自动打标() {
+        when(articleMapper.selectById(1L)).thenReturn(null);
+
+        service.toggleStatus(1L, "published");
+
+        verify(taskExecutor, never()).execute(any(Runnable.class));
+    }
+
+    @Test
+    void auditArticle_审核通过且标签为空_调度自动打标() {
+        Article a = article(2L, "draft", null);
+        a.setAuditStatus("pending");
+        when(articleMapper.selectById(2L)).thenReturn(a);
+
+        service.auditArticle(2L, "approved", null, 99L);
+
+        verify(taskExecutor, times(1)).execute(any(Runnable.class));
+    }
+
+    @Test
+    void auditArticle_审核拒绝_不调度自动打标() {
+        Article a = article(2L, "draft", "");
+        a.setAuditStatus("pending");
+        when(articleMapper.selectById(2L)).thenReturn(a);
+
+        service.auditArticle(2L, "rejected", "内容不符", 99L);
+
+        verify(taskExecutor, never()).execute(any(Runnable.class));
+    }
+
+    @Test
+    void publishByUser_家庭可见直接发布且标签为空_调度自动打标() {
+        User user = new User();
+        user.setId(10L);
+        user.setFamilyId(114L);
+        when(userMapper.selectById(10L)).thenReturn(user);
+        when(articleMapper.insert(any(Article.class))).thenAnswer(inv -> {
+            Article a = inv.getArgument(0);
+            a.setId(200L);
+            return 1;
+        });
+        // ensureAutoTagsIfEmpty 内部会按 id 重新查询已持久化的文章(published + 空标签)
+        Article persisted = new Article();
+        persisted.setId(200L);
+        persisted.setStatus("published");
+        persisted.setTags(null);
+        when(articleMapper.selectById(200L)).thenReturn(persisted);
+
+        Article created = service.publishByUser("标题", "<p>内容</p>", null, "private", "mind", 10L, "user");
+
+        assertEquals("published", created.getStatus());
+        verify(taskExecutor, times(1)).execute(any(Runnable.class));
+    }
+
+    @Test
+    void publishByUser_公开文章待审核草稿_不立即打标() {
+        when(articleMapper.insert(any(Article.class))).thenReturn(1);
+
+        Article created = service.publishByUser("标题", "<p>内容</p>", null, "public", null, 10L, "user");
+
+        assertEquals("draft", created.getStatus());
+        verify(taskExecutor, never()).execute(any(Runnable.class));
+    }
+
+}