From c118302d4d855fe5a51a53be60dabb4660fd75d2 Mon Sep 17 00:00:00 2001 From: lzx_win <2602107437@qq.com> Date: Fri, 10 Oct 2025 17:22:06 +0800 Subject: [PATCH] 1 --- .../applet/controller/AppletApiTTSController.java | 47 +++ .../modules/applet/service/AppletApiTTService.java | 26 ++ .../service/impl/AppletApiTTServiceImpl.java | 412 ++++++++++++++++++ .../demo/appletTtsCache/entity/AppletTtsCache.java | 8 + .../src/main/resources/application-dev.yml | 8 +- .../applet/course-page/AppletCoursePageList.vue | 208 +-------- .../course-page/components/ContentEditor.vue | 463 +++++++++++++++++++++ .../course-page/components/MobilePreview.vue | 62 ++- 8 files changed, 1018 insertions(+), 216 deletions(-) create mode 100644 jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletApiTTSController.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletApiTTSController.java index 4eaa550..e8f515d 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletApiTTSController.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletApiTTSController.java @@ -18,6 +18,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import java.util.List; +import java.util.Map; @Slf4j @Tag(name = "文字转语音", description = "文字转语音") @@ -95,4 +96,50 @@ public class AppletApiTTSController { return Result.ok(audioData); } +// @Operation(summary = "创建长文本TTS任务", description = "创建异步长文本语音合成任务。如已有相同文本在生成,则返回已有任务ID") +// @PostMapping(value = "/long/create") +// @IgnoreAuth +// public Result createLongTextTask( +// @Parameter(description = "要转换的文本内容", required = true) @RequestParam("text") String text, +// @Parameter(description = "语速,范围:[-2,6],默认为0") @RequestParam(value = "speed", required = false) Float speed, +// @Parameter(description = "音色ID,默认为0") @RequestParam(value = "voiceType", required = false) Integer voiceType, +// @Parameter(description = "音量大小,范围[-10,10],默认为0") @RequestParam(value = "volume", required = false) Float volume, +// @Parameter(description = "返回音频格式,可取值:wav(默认),mp3,pcm") @RequestParam(value = "codec", required = false) String codec, +// @Parameter(description = "回调地址(可选)") @RequestParam(value = "callbackUrl", required = false) String callbackUrl +// ) { +// String taskId = appletApiTTService.createLongTextTtsTask(text, speed, voiceType, volume, codec, callbackUrl); +// return Result.OK(taskId); +// } + +// @Operation(summary = "查询长文本TTS任务状态", description = "根据taskId查询任务状态,成功时返回缓存对象信息") +// @GetMapping(value = "/long/status") +// @IgnoreAuth +// public Result queryStatus( +// @Parameter(description = "任务ID", required = true) @RequestParam("taskId") String taskId +// ) { +// org.jeecg.modules.demo.appletTtsCache.entity.AppletTtsCache cache = appletApiTTService.queryLongTextTtsTaskStatus(taskId); +// return Result.OK(cache); +// } + + @Operation(summary = "接收长文本TTS回调", description = "接收腾讯云回调并保存音频与状态") + @PostMapping(value = "/long/callback") + @IgnoreAuth + public Result ttsCallback(@RequestBody Map payload) { + try { + log.info("回调内容: {}", payload); + String taskId = (String) payload.get("TaskId"); + Boolean success = payload.get("Success") instanceof Boolean ? (Boolean) payload.get("Success") : null; + String audioBase64 = (String) payload.get("Audio"); + Integer sampleRate = payload.get("SampleRate") instanceof Number ? ((Number) payload.get("SampleRate")).intValue() : null; + String codec = (String) payload.get("Codec"); + String message = (String) payload.get("Message"); + + boolean ok = appletApiTTService.handleTtsCallback(taskId, audioBase64, sampleRate, codec, success != null && success, message); + return ok ? Result.OK(true) : Result.error("回调处理失败"); + } catch (Exception e) { + log.error("回调解析失败: {}", e.getMessage(), e); + return Result.error("回调解析失败: " + e.getMessage()); + } + } + } diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java index cd71587..28d8fe1 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiTTService.java @@ -3,6 +3,7 @@ package org.jeecg.modules.applet.service; import com.tencentcloudapi.tts.v20190823.models.TextToVoiceResponse; import org.jeecg.modules.applet.entity.TtsVo; import org.jeecg.modules.demo.appletTtsTimbre.entity.AppletTtsTimbre; +import org.jeecg.modules.demo.appletTtsCache.entity.AppletTtsCache; import java.util.List; @@ -20,4 +21,29 @@ public interface AppletApiTTService { List list(); + + /** + * 创建长文本TTS任务(异步),如已在生成中则直接返回任务信息 + */ + String createLongTextTtsTask(String text, Float speed, Integer voiceType, Float volume, String codec, String callbackUrl); + + /** + * 按页面创建长文本TTS任务(异步),仅接收页面ID与音色ID + */ + String createLongTextTtsTaskByPage(String pageId, Integer voiceType); + + /** + * 根据页面ID提取页面文本内容,提供给长文本方法使用 + */ + String extractTextByPageId(String pageId); + + /** + * 查询长文本TTS任务状态,并在成功时保存到缓存并更新状态 + */ + AppletTtsCache queryLongTextTtsTaskStatus(String taskId); + + /** + * 接收腾讯云TTS异步回调并处理结果,保存到缓存并更新状态 + */ + boolean handleTtsCallback(String taskId, String audioBase64, Integer sampleRate, String codec, boolean success, String message); } diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java index 481e244..1297b2f 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiTTServiceImpl.java @@ -1,6 +1,9 @@ package org.jeecg.modules.applet.service.impl; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.profile.ClientProfile; @@ -8,6 +11,10 @@ import com.tencentcloudapi.common.profile.HttpProfile; import com.tencentcloudapi.tts.v20190823.TtsClient; import com.tencentcloudapi.tts.v20190823.models.TextToVoiceRequest; import com.tencentcloudapi.tts.v20190823.models.TextToVoiceResponse; +import com.tencentcloudapi.tts.v20190823.models.CreateTtsTaskRequest; +import com.tencentcloudapi.tts.v20190823.models.CreateTtsTaskResponse; +import com.tencentcloudapi.tts.v20190823.models.DescribeTtsTaskStatusRequest; +import com.tencentcloudapi.tts.v20190823.models.DescribeTtsTaskStatusResponse; import lombok.extern.log4j.Log4j2; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.system.util.AppletUserUtil; @@ -15,6 +22,10 @@ import org.jeecg.common.util.oss.OssBootUtil; import org.jeecg.modules.applet.entity.TtsVo; import org.jeecg.modules.applet.service.AppletApiTTService; import org.jeecg.modules.common.IdUtils; +import org.jeecg.modules.demo.appletCoursePage.entity.AppletCoursePage; +import org.jeecg.modules.demo.appletCoursePage.service.IAppletCoursePageService; +import org.jeecg.modules.demo.appletCoursePageWord.entity.AppletCoursePageWord; +import org.jeecg.modules.demo.appletCoursePageWord.service.IAppletCoursePageWordService; import org.jeecg.modules.demo.appletTtsPlayLog.service.IAppletTtsPlayLogService; import org.jeecg.modules.demo.appletTtsTimbre.entity.AppletTtsTimbre; import org.jeecg.modules.demo.appletTtsTimbre.service.IAppletTtsTimbreService; @@ -28,6 +39,10 @@ import org.springframework.stereotype.Service; import java.util.List; import java.util.UUID; +import java.util.Base64; +import java.net.URL; +import java.io.InputStream; +import java.io.ByteArrayOutputStream; @Log4j2 @Service @@ -37,6 +52,8 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { private String secretId; @Value("${tencent.secretKey}") private String secretKey; + @Value("${tencent.TtscallbackUrl}") + private String TtscallbackUrl; @Autowired private IAppletTtsTimbreService appletTtsTimbreService; @@ -44,6 +61,10 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { private IAppletTtsPlayLogService appletTtsPlayLogService; @Autowired private IAppletTtsCacheService appletTtsCacheService; + @Autowired + private IAppletCoursePageService appletCoursePageService; + @Autowired + private IAppletCoursePageWordService appletCoursePageWordService; public TtsVo textToVoice(String text, Float speed, Integer voiceType, Float volume, String codec) { @@ -208,6 +229,397 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { return null; } } + + @Override + public String createLongTextTtsTask(String text, Float speed, Integer voiceType, Float volume, String codec, String callbackUrl) { + try { + // 先检查是否存在生成中的任务(相同文本与参数) + LambdaQueryWrapper generatingWrapper = new LambdaQueryWrapper<>(); + generatingWrapper.eq(AppletTtsCache::getText, text) + .eq(AppletTtsCache::getVoiceType, voiceType) + .eq(volume != null, AppletTtsCache::getVolume, volume != null ? volume.doubleValue() : null) + .eq(speed != null, AppletTtsCache::getSpeed, speed != null ? speed.doubleValue() : null) + .eq(AppletTtsCache::getState, 0); + + AppletTtsCache generating = appletTtsCacheService.getOne(generatingWrapper); + if (generating != null && generating.getTaskId() != null) { + log.info("已有长文本TTS任务正在生成中,直接返回任务ID: {}", generating.getTaskId()); + return generating.getTaskId(); + } + + // 再检查是否已有成功缓存 + LambdaQueryWrapper successWrapper = new LambdaQueryWrapper<>(); + successWrapper.eq(AppletTtsCache::getText, text) + .eq(AppletTtsCache::getVoiceType, voiceType) + .eq(volume != null, AppletTtsCache::getVolume, volume != null ? volume.doubleValue() : null) + .eq(speed != null, AppletTtsCache::getSpeed, speed != null ? speed.doubleValue() : null) + .eq(AppletTtsCache::getSuccess, "Y") + .eq(AppletTtsCache::getState, 1); + AppletTtsCache successCache = appletTtsCacheService.getOne(successWrapper); + if (successCache != null && successCache.getTaskId() != null) { + log.info("长文本TTS已完成,返回任务ID: {}", successCache.getTaskId()); + return successCache.getTaskId(); + } + + // 调用腾讯云异步创建任务 + Credential cred = new Credential(secretId, secretKey); + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("tts.tencentcloudapi.com"); + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + TtsClient client = new TtsClient(cred, "", clientProfile); + + CreateTtsTaskRequest req = new CreateTtsTaskRequest(); + req.setText(text); + if (speed != null) req.setSpeed(speed); + if (voiceType != null) req.setVoiceType(voiceType.longValue()); + if (volume != null) req.setVolume(volume); + if (codec != null && !codec.isEmpty()) { + req.setCodec(codec); + } else { + req.setCodec("wav"); + } + req.setModelType(1L); + req.setPrimaryLanguage(2L); + req.setSampleRate(16000L); + // 启用时间戳字幕 + try { + req.getClass().getMethod("setEnableSubtitle", Boolean.class).invoke(req, Boolean.TRUE); + } catch (Exception ignore) {} + if (callbackUrl != null && !callbackUrl.isEmpty()) { + req.setCallbackUrl(callbackUrl); + } + + CreateTtsTaskResponse resp = client.CreateTtsTask(req); + String taskId = resp.getData() != null ? resp.getData().getTaskId() : null; + if (taskId == null || taskId.isEmpty()) { + throw new JeecgBootException("创建长文本TTS任务失败,未返回任务ID"); + } + + // 写入缓存,标记生成中 + AppletTtsCache cache = new AppletTtsCache(); + cache.setTaskId(taskId); + cache.setText(text); + cache.setVoiceType(voiceType); + cache.setVolume(volume != null ? volume.doubleValue() : null); + cache.setSpeed(speed != null ? speed.doubleValue() : null); + cache.setSuccess("N"); + cache.setState(0); + cache.setCreateTime(new java.util.Date()); + appletTtsCacheService.save(cache); + + log.info("长文本TTS任务创建成功,taskId: {}", taskId); + return taskId; + } catch (Exception e) { + log.error("创建长文本TTS任务失败: {}", e.getMessage(), e); + throw new JeecgBootException("创建长文本TTS任务失败: " + e.getMessage()); + } + } + + @Override + public AppletTtsCache queryLongTextTtsTaskStatus(String taskId) { + try { + Credential cred = new Credential(secretId, secretKey); + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("tts.tencentcloudapi.com"); + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + TtsClient client = new TtsClient(cred, "", clientProfile); + + DescribeTtsTaskStatusRequest req = new DescribeTtsTaskStatusRequest(); + req.setTaskId(taskId); + DescribeTtsTaskStatusResponse resp = client.DescribeTtsTaskStatus(req); + + Long status = resp.getData() != null ? resp.getData().getStatus() : null; // 0等待 1处理中 2成功 3失败 + String audioUrl = resp.getData() != null ? resp.getData().getResultUrl() : null; + + AppletTtsCache cache = appletTtsCacheService + .lambdaQuery() + .eq(AppletTtsCache::getTaskId, taskId) + .one(); + if (cache == null) { + cache = new AppletTtsCache(); + cache.setTaskId(taskId); + cache.setCreateTime(new java.util.Date()); + cache.setState(0); + cache.setSuccess("N"); + appletTtsCacheService.save(cache); + } + + if (status == null || status == 0 || status == 1) { + cache.setState(0); + cache.setUpdateTime(new java.util.Date()); + appletTtsCacheService.updateById(cache); + return cache; + } else if (status == 2) { + // 成功,处理音频 + byte[] audioData = null; + if (audioUrl != null && !audioUrl.isEmpty()) { + audioData = downloadBytes(audioUrl); + } + + if (audioData != null) { + String fileName = IdUtils.generateNo("TTS_") + System.currentTimeMillis() + ".wav"; + ByteArrayInputStream inputStream = new ByteArrayInputStream(audioData); + String uploadedUrl = OssBootUtil.upload(inputStream, "tts/" + fileName); + cache.setAudioId(uploadedUrl != null ? uploadedUrl : audioUrl); + + Double realDuration = AudioDurationUtil.calculateDuration(audioData); + if (realDuration != null) { + cache.setDuration(realDuration); + } + } else { + cache.setAudioId(audioUrl); + } + + // 解析时间戳 + String timestampsJson = null; + try { + timestampsJson = JSON.toJSONString(resp.getData().getSubtitles()); + cache.setTimestamps(timestampsJson); + } catch (Exception ignore) {} + + cache.setSuccess("Y"); + cache.setState(1); + cache.setUpdateTime(new java.util.Date()); + appletTtsCacheService.updateById(cache); + return cache; + } else { + cache.setSuccess("N"); + cache.setState(0); + cache.setUpdateTime(new java.util.Date()); + appletTtsCacheService.updateById(cache); + return cache; + } + } catch (Exception e) { + log.error("查询长文本TTS任务状态失败: {}", e.getMessage(), e); + throw new JeecgBootException("查询长文本TTS任务状态失败: " + e.getMessage()); + } + } + + /** + * 按页面创建长文本TTS任务(异步),仅接收页面ID与音色ID + */ + @Override + public String createLongTextTtsTaskByPage(String pageId, Integer voiceType) { + String text = extractTextByPageId(pageId); + if (text == null || text.trim().isEmpty()) { + throw new JeecgBootException("页面内容为空,无法创建TTS任务"); + } + + try { + // 检查是否存在相同页面与音色的生成中任务 + LambdaQueryWrapper generatingWrapper = new LambdaQueryWrapper<>(); + generatingWrapper.eq(AppletTtsCache::getPageId, pageId) + .eq(AppletTtsCache::getVoiceType, voiceType) + .eq(AppletTtsCache::getState, 0); + AppletTtsCache generating = appletTtsCacheService.getOne(generatingWrapper); + if (generating != null && generating.getTaskId() != null) { + log.info("页面长文本TTS任务生成中,返回任务ID: {}", generating.getTaskId()); + return generating.getTaskId(); + } + + // 检查是否已有成功缓存 + LambdaQueryWrapper successWrapper = new LambdaQueryWrapper<>(); + successWrapper.eq(AppletTtsCache::getPageId, pageId) + .eq(AppletTtsCache::getVoiceType, voiceType) + .eq(AppletTtsCache::getSuccess, "Y") + .eq(AppletTtsCache::getState, 1); + AppletTtsCache successCache = appletTtsCacheService.getOne(successWrapper); + if (successCache != null && successCache.getTaskId() != null) { + log.info("页面长文本TTS已完成,返回任务ID: {}", successCache.getTaskId()); + return successCache.getTaskId(); + } + + // 创建腾讯云任务 + Credential cred = new Credential(secretId, secretKey); + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("tts.tencentcloudapi.com"); + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + TtsClient client = new TtsClient(cred, "", clientProfile); + + CreateTtsTaskRequest req = new CreateTtsTaskRequest(); + req.setText(text); + if (voiceType != null) req.setVoiceType(voiceType.longValue()); + req.setCodec("wav"); + req.setModelType(1L); + req.setPrimaryLanguage(2L); + req.setSampleRate(16000L); + req.setEnableSubtitle(true); + req.setCallbackUrl(TtscallbackUrl); + + CreateTtsTaskResponse resp = client.CreateTtsTask(req); + String taskId = resp.getData() != null ? resp.getData().getTaskId() : null; + if (taskId == null || taskId.isEmpty()) { + throw new JeecgBootException("创建页面长文本TTS任务失败,未返回任务ID"); + } + + // 写入缓存,标记生成中 + AppletTtsCache cache = new AppletTtsCache(); + cache.setTaskId(taskId); + cache.setText(text); + cache.setVoiceType(voiceType); + cache.setPageId(pageId); + cache.setSuccess("N"); + cache.setState(0); + cache.setCreateTime(new java.util.Date()); + appletTtsCacheService.save(cache); + + log.info("页面长文本TTS任务创建成功,taskId: {}", taskId); + return taskId; + } catch (Exception e) { + log.error("创建页面长文本TTS任务失败: {}", e.getMessage(), e); + throw new JeecgBootException("创建页面长文本TTS任务失败: " + e.getMessage()); + } + } + + /** + * 根据页面ID提取页面文本内容 + */ + @Override + public String extractTextByPageId(String pageId) { + // 旧实现(按标题、content纯文本及重点单词拼接)已注释: + // AppletCoursePage page = appletCoursePageService.getById(pageId); + // if (page == null) { + // throw new JeecgBootException("未找到页面: " + pageId); + // } + // StringBuilder oldSb = new StringBuilder(); + // if (page.getTitle() != null) { + // oldSb.append(page.getTitle()).append("\n"); + // } + // if (page.getContent() != null) { + // oldSb.append(stripHtml(page.getContent())).append("\n"); + // } + // List oldWords = appletCoursePageWordService + // .lambdaQuery() + // .eq(AppletCoursePageWord::getPageId, pageId) + // .list(); + // if (oldWords != null && !oldWords.isEmpty()) { + // for (AppletCoursePageWord w : oldWords) { + // if (w.getWord() != null && !w.getWord().isEmpty()) { + // oldSb.append(w.getWord()).append(". "); + // } + // } + // } + // return oldSb.toString().trim(); + + // 新实现:按页面编辑所定义的 JSON 结构解析页面内容,仅提取文本组件 + AppletCoursePage page = appletCoursePageService.getById(pageId); + if (page == null) { + throw new JeecgBootException("未找到页面: " + pageId); + } + String content = page.getContent(); + if (content == null || content.trim().isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + try { + // content 期望是数组:[{ type: 'text'|'image'|'video', content?: string, language?: 'zh'|'en', ... }] + JSONArray arr = JSON.parseArray(content); + for (int i = 0; i < arr.size(); i++) { + JSONObject obj = arr.getJSONObject(i); + if (obj == null) continue; + String type = obj.getString("type"); + if ("text".equalsIgnoreCase(type)) { + String text = obj.getString("content"); + if (text != null && !text.trim().isEmpty()) { + sb.append(text.trim()).append("\n"); + } + } + } + return sb.toString().trim(); + } catch (Exception e) { + // 若不是数组,尝试作为单个对象或直接文本 + try { + JSONObject obj = JSON.parseObject(content); + String type = obj.getString("type"); + if ("text".equalsIgnoreCase(type)) { + String text = obj.getString("content"); + return text != null ? text : ""; + } + } catch (Exception ignore) {} + // 作为纯文本返回 + return content; + } + } + + private String stripHtml(String html) { + try { + String noTag = html.replaceAll("<[^>]*>", " "); + noTag = noTag.replace(" ", " "); + return noTag.replaceAll("\\s+", " ").trim(); + } catch (Exception e) { + return html; + } + } + + + @Override + public boolean handleTtsCallback(String taskId, String audioBase64, Integer sampleRate, String codec, boolean success, String message) { + try { + AppletTtsCache cache = appletTtsCacheService + .lambdaQuery() + .eq(AppletTtsCache::getTaskId, taskId) + .one(); + if (cache == null) { + cache = new AppletTtsCache(); + cache.setTaskId(taskId); + cache.setCreateTime(new java.util.Date()); + } + + if (success) { + byte[] audioData = null; + if (audioBase64 != null && !audioBase64.isEmpty()) { + audioData = Base64.getDecoder().decode(audioBase64); + } + + String fileSuffix = (codec != null && !codec.isEmpty()) ? codec : "wav"; + String fileName = IdUtils.generateNo("TTS_") + System.currentTimeMillis() + "." + fileSuffix; + String uploadedUrl = null; + if (audioData != null) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(audioData); + uploadedUrl = OssBootUtil.upload(inputStream, "tts/" + fileName); + Double realDuration = AudioDurationUtil.calculateDuration(audioData); + if (realDuration != null) { + cache.setDuration(realDuration); + } + } + + cache.setAudioId(uploadedUrl); + cache.setSuccess("Y"); + cache.setState(1); + } else { + cache.setSuccess("N"); + cache.setState(0); + } + cache.setUpdateTime(new java.util.Date()); + if (cache.getId() == null) { + appletTtsCacheService.save(cache); + } else { + appletTtsCacheService.updateById(cache); + } + log.info("TTS回调处理完成,taskId: {}, success: {}, message: {}", taskId, success, message); + return true; + } catch (Exception e) { + log.error("处理TTS回调失败: {}", e.getMessage(), e); + return false; + } + } + + private byte[] downloadBytes(String url) { + try (InputStream in = new URL(url).openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) != -1) { + baos.write(buf, 0, len); + } + return baos.toByteArray(); + } catch (Exception e) { + log.warn("下载音频失败: {}", e.getMessage()); + return null; + } + } /** * 保存TTS播放日志 diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsCache/entity/AppletTtsCache.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsCache/entity/AppletTtsCache.java index 037c2f2..cb54901 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsCache/entity/AppletTtsCache.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsCache/entity/AppletTtsCache.java @@ -17,6 +17,7 @@ import org.jeecgframework.poi.excel.annotation.Excel; import org.jeecg.common.aspect.annotation.Dict; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; +import com.baomidou.mybatisplus.annotation.TableField; import lombok.experimental.Accessors; /** @@ -60,6 +61,10 @@ public class AppletTtsCache implements Serializable { @Excel(name = "音频文件ID/URL", width = 15) @Schema(description = "音频文件ID/URL") private java.lang.String audioId; + /**任务ID*/ + @Excel(name = "任务ID", width = 15) + @Schema(description = "长文本TTS任务ID") + private java.lang.String taskId; /**音频时长(秒)*/ @Excel(name = "音频时长(秒)", width = 15) @Schema(description = "音频时长(秒)") @@ -92,4 +97,7 @@ public class AppletTtsCache implements Serializable { @Excel(name = "状态", width = 15) @Schema(description = "状态 0生成中 1已完成") private java.lang.Integer state; + + @Schema(description = "时间戳") + private java.lang.String timestamps; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml index d6e92ab..9b3bfd9 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml @@ -1,5 +1,5 @@ server: - port: 8003 + port: 8002 undertow: # max-http-post-size: 10MB # 平替 tomcat server.tomcat.max-swallow-siz, undertow该值默认为-1 worker-threads: 16 # 4核CPU标准配置 @@ -344,10 +344,12 @@ wechat: merchantSerialNumber: 55B6EF6CCD7CDC41504138E6B0B7E3EE23209A65 # 商户API证书序列号 apiV3Key: 0fdb77429ffdf206c151af76a663041c # 商户APIV3密钥 refundNotifyUrl: # 退款通知地址(正式环境) - transferNotify: https://www.yurangongfang.com/massage-admin/massage/cash/cashoutNotify/ # 转账结果通知地址 + transferNotify: https://www.multipleculture.com/englishread-admin/massage/cash/cashoutNotify/ # 转账结果通知地址 testPrice: true tencent: secretId: AKIDDH7j7IFzgCUbUBfaJuJVTDk4jCVbT7Xm - secretKey: 4NURCj281g7RWP4Vj8KJ5dy5zf9PSIuN \ No newline at end of file + secretKey: 4NURCj281g7RWP4Vj8KJ5dy5zf9PSIuN + TtscallbackUrl: http://h5.xzaiyp.top/englishread-admin/appletApi/tts/long/callback +# TtscallbackUrl: https://www.multipleculture.com/englishread-admin/appletApi/tts/long/callback \ No newline at end of file diff --git a/jeecgboot-vue3/src/views/applet/course-page/AppletCoursePageList.vue b/jeecgboot-vue3/src/views/applet/course-page/AppletCoursePageList.vue index 95ea77a..9e582a4 100644 --- a/jeecgboot-vue3/src/views/applet/course-page/AppletCoursePageList.vue +++ b/jeecgboot-vue3/src/views/applet/course-page/AppletCoursePageList.vue @@ -71,129 +71,8 @@
- - -
-
- 页面内容 - - - - 文本 - - - - 图片 - - - - 视频 - - -
- - -
- - - - - -
- -

点击上方按钮添加内容组件

-
-
-
+ + @@ -253,7 +132,8 @@ import { ref, reactive, computed, unref, onMounted, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useMessage } from '/@/hooks/web/useMessage'; import { Icon } from '/@/components/Icon'; -import { JImageUpload, JUpload } from '/@/components/Form'; +// 内容编辑组件 +import ContentEditor from './components/ContentEditor.vue'; import { saveOrUpdate, getById, list, deleteOne, add, edit } from './AppletCoursePage.api'; import { initDictOptions } from '/@/utils/dict/JDictSelectUtil'; import draggable from 'vuedraggable'; @@ -655,86 +535,6 @@ function refreshPreview() { } /** - * 添加文本内容 - */ -function addTextContent() { - contentComponents.value.push({ - id: Date.now() + Math.random(), // 生成唯一ID - type: 'text', - content: '', - language: 'zh' // 默认选择中文 - }); -} - -/** - * 添加图片内容 - */ -function addImageContent() { - contentComponents.value.push({ - id: Date.now() + Math.random(), // 生成唯一ID - type: 'image', - imageUrl: '', - alt: '' - }); -} - -/** - * 添加视频内容 - */ -function addVideoContent() { - contentComponents.value.push({ - id: Date.now() + Math.random(), // 生成唯一ID - type: 'video', - url: '', - coverUrl: '' - }); -} - -/** - * 移除组件 - */ -function removeComponent(index: number) { - contentComponents.value.splice(index, 1); -} - -/** - * 图片上传变化处理 - */ -function handleImageChange(value: string, component: any) { - // JImageUpload组件直接返回图片URL字符串 - component.imageUrl = value; -} - -/** - * 视频上传变化处理 - */ -function handleVideoChange(value: string, component: any) { - component.url = value; -} - -/** - * 视频封面上传变化处理 - */ -function handleVideoCoverChange(value: string, component: any) { - component.coverUrl = value; -} - -/** - * 拖拽开始事件 - */ -function onDragStart(evt: any) { - console.log('拖拽开始:', evt); -} - -/** - * 拖拽结束事件 - */ -function onDragEnd(evt: any) { - console.log('拖拽结束:', evt); - // Vue Draggable 会自动更新 v-model 绑定的数组 - // 这里可以添加额外的处理逻辑,比如保存排序状态 - createMessage.success('组件顺序已更新'); -} /** * 获取页面类型图标 diff --git a/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue b/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue new file mode 100644 index 0000000..486cd6f --- /dev/null +++ b/jeecgboot-vue3/src/views/applet/course-page/components/ContentEditor.vue @@ -0,0 +1,463 @@ + + + + + \ No newline at end of file diff --git a/jeecgboot-vue3/src/views/applet/course-page/components/MobilePreview.vue b/jeecgboot-vue3/src/views/applet/course-page/components/MobilePreview.vue index 966b3ab..3931d98 100644 --- a/jeecgboot-vue3/src/views/applet/course-page/components/MobilePreview.vue +++ b/jeecgboot-vue3/src/views/applet/course-page/components/MobilePreview.vue @@ -14,11 +14,11 @@

暂无内容,请在左侧添加内容组件

-
+
-
-
{{ component.language === 'en' ? 'EN' : '中' }}
-
{{ component.content || '文本内容...' }}
+
+ +
{{ component.content || '文本内容...' }}
@@ -50,6 +50,7 @@