Эх сурвалжийг харах

fix(backend): 修复文章TTS批量生成两个阻塞问题

1. audio_url varchar(500) 超出 → 改为 MEDIUMTEXT(迁移404)
   多段音频JSON数组(每段OSS URL ~94字符)在长文中可超2900字符,
   导致 DataIntegrityViolationException(数据不完整或格式错误)。
   60篇文章中4篇长文因此生成失败。

2. HTML剥离后残留孤立引号导致腾讯云TTS拒绝合成
   splitChunks 后过滤无有效文字(中文/字母/数字)的分片,
   避免纯标点段触发 "hasn\t valid words or has invalid words。
Xiaogang Liao 2 долоо хоног өмнө
parent
commit
794936261f

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3251,6 +3251,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
 
         // 迁移403: withdrawal_requests 增加 reject_reason 列(审核中心统一详情:提现驳回原因与活动报名/服务角色申请口径一致)
         // 迁移403: withdrawal_requests 增加 reject_reason 列(审核中心统一详情:提现驳回原因与活动报名/服务角色申请口径一致)
         addWithdrawalRejectReasonColumn();
         addWithdrawalRejectReasonColumn();
+
+        // 迁移404: articles.audio_url 由 varchar(500) 改为 MEDIUMTEXT(多段音频JSON数组可超500字符)
+        alterArticleAudioUrlToMediumtext();
     }
     }
 
 
     private void migrateHealthDimensionsTable() {
     private void migrateHealthDimensionsTable() {
@@ -13025,4 +13028,16 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             log.warn("迁移403: 添加 withdrawal_requests.reject_reason 列失败: {}", e.getMessage());
             log.warn("迁移403: 添加 withdrawal_requests.reject_reason 列失败: {}", e.getMessage());
         }
         }
     }
     }
+
+    // 迁移404: articles.audio_url 由 varchar(500) 改为 MEDIUMTEXT
+    // 多段音频 JSON 数组(每段 OSS URL ~94 字符)在长文中可超 2900 字符,
+    // 超出 varchar(500) 导致 DataIntegrityViolationException("数据不完整或格式错误")
+    private void alterArticleAudioUrlToMediumtext() {
+        try {
+            jdbcTemplate.execute("ALTER TABLE articles MODIFY COLUMN audio_url MEDIUMTEXT NULL COMMENT '音频URL(单URL或JSON数组,多段)'");
+            log.info("迁移404: articles.audio_url 已改为 MEDIUMTEXT");
+        } catch (Exception e) {
+            log.warn("迁移404: articles.audio_url 修改失败: {}", e.getMessage());
+        }
+    }
 }
 }

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleAudioService.java

@@ -16,6 +16,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 
 /**
 /**
  * 文章 AI 朗读音频服务。
  * 文章 AI 朗读音频服务。
@@ -79,6 +80,15 @@ public class ArticleAudioService {
             throw new IllegalArgumentException("文章没有可朗读的文本内容");
             throw new IllegalArgumentException("文章没有可朗读的文本内容");
         }
         }
 
 
+        // 3.1 过滤无有效文字的分片(纯标点/引号/符号段会导致腾讯云 TTS 返回
+        //     "hasn't valid words or has invalid words",如剥离 HTML 后残留的孤立引号)
+        chunks = chunks.stream()
+                .filter(chunk -> chunk.matches(".*[\\u4e00-\\u9fa5A-Za-z0-9].*"))
+                .collect(Collectors.toList());
+        if (chunks.isEmpty()) {
+            throw new IllegalArgumentException("文章没有可朗读的文本内容");
+        }
+
         // 4. 逐段合成并存储
         // 4. 逐段合成并存储
         List<String> urls = new ArrayList<>(chunks.size());
         List<String> urls = new ArrayList<>(chunks.size());
         int index = 0;
         int index = 0;

+ 37 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ArticleAudioServiceChunkFilterTest.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.service;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 验证 splitChunks 过滤逻辑:纯标点段被剔除,有效文字段保留。
+ * 场景:HTML 剥离后残留孤立引号(如 ")导致的无有效文字段。
+ */
+public class ArticleAudioServiceChunkFilterTest {
+
+    /** 与 ArticleAudioService 中过滤条件完全一致 */
+    private boolean isValidChunk(String chunk) {
+        return chunk.matches(".*[\\u4e00-\\u9fa5A-Za-z0-9].*");
+    }
+
+    @Test
+    public void invalidWhenOnlyPunctuation() {
+        assertFalse(isValidChunk("\""));
+        assertFalse(isValidChunk("\"]"));
+        assertFalse(isValidChunk("!?。;;…"));
+        assertFalse(isValidChunk(""));
+    }
+
+    @Test
+    public void validWhenHasChineseOrAlnum() {
+        assertTrue(isValidChunk("孩子说\"没意思\""));
+        assertTrue(isValidChunk("2026年"));
+        assertTrue(isValidChunk("abc"));
+    }
+
+    @Test
+    public void validWhenMixed() {
+        assertTrue(isValidChunk("\"不想上学\"是行为,"));
+    }
+}