فهرست منبع

feat(admin): 文章编辑新增朗读文本(audio_text)输入框,TTS 优先朗读此文本;修改后自动作废已生成音频

- AdminArticleController.create/update:接收并持久化 audioText;变更时置空 audioUrl 触发下次重新合成;带微信内容安全检测
- ArticleEdit.vue:摘要下方新增「朗读文本」textarea;loadDetail/loadForCopy 回填;buildSubmitData 带上该字段
Xiaogang Liao 4 روز پیش
والد
کامیت
a3dadf5943

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

@@ -18,6 +18,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import com.etotem.cfc.util.SortUtil;
@@ -94,10 +95,17 @@ public class AdminArticleController {
         if (!checkTextSafe(content)) {
             return Result.error("文章内容包含敏感信息,发布失败");
         }
+        if (!checkTextSafe((String) body.get("audioText"))) {
+            return Result.error("朗读文本包含敏感信息,发布失败");
+        }
 
         Article article = new Article();
         article.setTitle((String) body.get("title"));
         article.setContent((String) body.get("content"));
+        // 朗读文本(TTS 专用;留空则自动从正文生成)
+        if (body.get("audioText") != null) {
+            article.setAudioText(body.get("audioText").toString());
+        }
         if (body.get("categoryId") != null && !body.get("categoryId").toString().trim().isEmpty()) {
             article.setCategoryId(ParamUtils.getLong(body.get("categoryId")));
         }
@@ -169,10 +177,14 @@ public class AdminArticleController {
         if (!checkTextSafe(content)) {
             return Result.error("文章内容包含敏感信息,更新失败");
         }
+        if (!checkTextSafe((String) body.get("audioText"))) {
+            return Result.error("朗读文本包含敏感信息,更新失败");
+        }
 
         Long articleId = ParamUtils.getLong(body.get("id"));
+        Article existing = null;
         if (articleId != null) {
-            Article existing = articleService.getById(articleId);
+            existing = articleService.getById(articleId);
             if (existing != null && !"draft".equals(existing.getStatus())
                     && !"published".equals(existing.getStatus())
                     && !"withdrawn".equals(existing.getStatus())) {
@@ -226,6 +238,13 @@ public class AdminArticleController {
         } else if (articleTypeVal instanceof String) {
             article.setArticleType((String) articleTypeVal);
         }
+        // 朗读文本(TTS 专用;留空则从正文生成)。变更时作废已生成音频,下次「听文章」重新合成
+        if (body.get("audioText") != null) {
+            article.setAudioText(body.get("audioText").toString());
+            if (existing != null && !Objects.equals(existing.getAudioText(), article.getAudioText())) {
+                article.setAudioUrl("");
+            }
+        }
         articleService.update(article);
 
         return Result.success("更新成功");

+ 9 - 0
cfc-web/src/views/admin/ArticleEdit.vue

@@ -67,6 +67,11 @@
           <el-input v-model="form.summary" type="textarea" :rows="3" placeholder="请输入文章摘要" maxlength="500" show-word-limit :disabled="readonly" />
         </el-form-item>
 
+        <el-form-item label="朗读文本">
+          <el-input v-model="form.audioText" type="textarea" :rows="3" placeholder="AI 朗读(TTS)专用文本;留空则自动从正文生成" :disabled="readonly" />
+          <span class="form-hint">「听文章」时优先朗读此文本;修改后已生成的旧音频会自动作废并重新合成</span>
+        </el-form-item>
+
         <el-form-item label="封面图">
           <template v-if="!readonly">
             <el-upload
@@ -187,6 +192,7 @@ export default {
         categoryId: '',
         title: '',
         summary: '',
+        audioText: '',
         coverImage: '',
         content: '',
         tags: '',
@@ -613,6 +619,7 @@ onError: (file, err, res) => {
             categoryId: d.categoryId,
             title: d.title || '',
             summary: d.summary || '',
+            audioText: d.audioText || '',
             coverImage: d.coverImage || '',
             content: d.content || '',
             tags: d.tags || '',
@@ -652,6 +659,7 @@ onError: (file, err, res) => {
             categoryId: d.categoryId,
             title: (d.title || '') + '(副本)',
             summary: d.summary || '',
+            audioText: d.audioText || '',
             coverImage: d.coverImage || '',
             content: d.content || '',
             tags: d.tags || '',
@@ -711,6 +719,7 @@ onError: (file, err, res) => {
         categoryId: this.form.categoryId,
         title: this.form.title,
         summary: this.form.summary,
+        audioText: this.form.audioText,
         coverImage: this.form.coverImage,
         content: this.form.content,
         tags: this.form.tags,