Просмотр исходного кода

feat(api): 文章创建幂等检查 + AI标签生成异步化

- ArticleService.create 增加标题幂等检查,重复标题拒绝创建
- AI 自动标签生成改为 taskExecutor 异步执行,不阻塞 HTTP 响应
- AdminArticleController.create 捕获业务异常返回 Result.error

Ultraworked with [Sisyphus](https://github.com/OhMyOpenCode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 месяц назад
Родитель
Сommit
279ca39588

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -118,7 +118,11 @@ public class AdminArticleController {
         if (body.get("status") != null) {
             article.setStatus((String) body.get("status"));
         }
-        articleService.create(article, adminId);
+        try {
+            articleService.create(article, adminId);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
 
         Map<String, Object> result = new HashMap<>();
         result.put("id", article.getId());

+ 41 - 19
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java

@@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
@@ -76,6 +77,9 @@ public class ArticleService {
     @Resource
     private JdbcTemplate jdbcTemplate;
 
+    @Resource(name = "taskExecutor")
+    private ThreadPoolTaskExecutor taskExecutor;
+
     private final ObjectMapper objectMapper = new ObjectMapper();
 
     public Page<Article> getPublicList(Long categoryId, String keyword, String dimensionCode, int page, int size,
@@ -386,6 +390,16 @@ public class ArticleService {
 
     @Transactional
     public void create(Article article, Long adminId) {
+        // 幂等检查:同一标题不允许重复创建
+        if (article.getTitle() != null && !article.getTitle().trim().isEmpty()) {
+            LambdaQueryWrapper<Article> dupCheck = new LambdaQueryWrapper<Article>()
+                    .eq(Article::getTitle, article.getTitle().trim());
+            Long existingCount = articleMapper.selectCount(dupCheck);
+            if (existingCount != null && existingCount > 0) {
+                throw new RuntimeException("标题「" + article.getTitle().trim() + "」已存在,请勿重复创建");
+            }
+        }
+
         // Auto-calculate wordCount from content
         if (article.getContent() != null) {
             article.setWordCount(calculateWordCount(article.getContent()));
@@ -403,16 +417,20 @@ public class ArticleService {
         }
         articleMapper.insert(article);
 
-        // AI auto-tagging
-        try {
-            List<String> tagNames = autoGenerateTags(article.getContent());
-            if (!tagNames.isEmpty()) {
-                saveAutoTags(article.getId(), tagNames);
+        // AI auto-tagging (异步,不阻塞 HTTP 响应)
+        final Long articleId = article.getId();
+        final String content = article.getContent();
+        taskExecutor.execute(() -> {
+            try {
+                List<String> tagNames = autoGenerateTags(content);
+                if (!tagNames.isEmpty()) {
+                    saveAutoTags(articleId, tagNames);
+                }
+            } catch (Exception e) {
+                log.warn("AI 自动标签生成失败(不影响文章创建): articleId={}, error={}",
+                    articleId, e.getMessage());
             }
-        } catch (Exception e) {
-            log.warn("AI 自动标签生成失败(不影响文章创建): articleId={}, error={}",
-                article.getId(), e.getMessage());
-        }
+        });
     }
 
     @Transactional
@@ -428,18 +446,22 @@ public class ArticleService {
         article.setUpdatedAt(new Date());
         articleMapper.updateById(article);
 
-        // AI auto-tagging only when content changed
+        // AI auto-tagging only when content changed (异步,不阻塞 HTTP 响应)
         if (contentChanged && article.getContent() != null) {
-            try {
-                jdbcTemplate.update("DELETE FROM article_tags WHERE article_id = ?", article.getId());
-                List<String> tagNames = autoGenerateTags(article.getContent());
-                if (!tagNames.isEmpty()) {
-                    saveAutoTags(article.getId(), tagNames);
+            final Long articleId = article.getId();
+            final String content = article.getContent();
+            taskExecutor.execute(() -> {
+                try {
+                    jdbcTemplate.update("DELETE FROM article_tags WHERE article_id = ?", articleId);
+                    List<String> tagNames = autoGenerateTags(content);
+                    if (!tagNames.isEmpty()) {
+                        saveAutoTags(articleId, tagNames);
+                    }
+                } catch (Exception e) {
+                    log.warn("AI 自动标签更新失败(不影响文章更新): articleId={}, error={}",
+                        articleId, e.getMessage());
                 }
-            } catch (Exception e) {
-                log.warn("AI 自动标签更新失败(不影响文章更新): articleId={}, error={}",
-                    article.getId(), e.getMessage());
-            }
+            });
         }
     }