From fee84ce822f5be9892e349435066a7728242eef7 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Sat, 22 Nov 2025 18:30:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ChatMessageRenderer.vue | 135 +++++++++--------- .../src/views/system/StyleSettings.vue | 129 ++++++++--------- .../{app => }/AppUserPromptController.java | 4 +- .../service/UserPromptServiceImpl.java | 28 ++-- .../userprompt/vo/UserPromptSaveReqVO.java | 9 +- .../service/DigitalHumanTaskServiceImpl.java | 50 ++++++- .../vo/AppTikDigitalHumanCreateReqVO.java | 5 +- .../vo/AppTikLatentsyncResultRespVO.java | 5 + 8 files changed, 198 insertions(+), 167 deletions(-) rename yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/{app => }/AppUserPromptController.java (98%) diff --git a/frontend/app/web-gold/src/components/ChatMessageRenderer.vue b/frontend/app/web-gold/src/components/ChatMessageRenderer.vue index 3f4b3fe760..807cc60fb5 100644 --- a/frontend/app/web-gold/src/components/ChatMessageRenderer.vue +++ b/frontend/app/web-gold/src/components/ChatMessageRenderer.vue @@ -3,7 +3,7 @@ + +.prompt-display :deep(code) { + font-family: 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace; +} + \ No newline at end of file diff --git a/frontend/app/web-gold/src/views/system/StyleSettings.vue b/frontend/app/web-gold/src/views/system/StyleSettings.vue index 635bdb51aa..14b9555c43 100644 --- a/frontend/app/web-gold/src/views/system/StyleSettings.vue +++ b/frontend/app/web-gold/src/views/system/StyleSettings.vue @@ -32,7 +32,6 @@ const editForm = reactive({ content: '', category: '', status: 1, - _originalRecord: null, // 保存原始记录,用于更新时获取必需字段 }) const editFormRef = ref(null) @@ -173,12 +172,7 @@ function handleReset() { // 新增 function handleAdd() { - editForm.id = null - editForm.name = '' - editForm.content = '' - editForm.category = '' - editForm.status = 1 - editForm._originalRecord = null + resetEditForm() editModalVisible.value = true } @@ -188,62 +182,72 @@ function handleEdit(record) { editForm.name = record.name || '' editForm.content = record.content || '' editForm.category = record.category || '' - editForm.status = record.status ?? 1 - // 保存原始记录的完整信息,用于更新时传递必需字段 - editForm._originalRecord = record + editForm.status = record.status !== null && record.status !== undefined ? record.status : 1 editModalVisible.value = true } +// 表单重置 +function resetEditForm() { + editForm.id = null + editForm.name = '' + editForm.content = '' + editForm.category = '' + editForm.status = 1 +} + +// 通用API调用 +async function apiCall(apiFunc, param, successMessage, isDelete = false) { + loading.value = true + try { + const response = await apiFunc(param) + if (response && (response.code === 0 || response.code === 200)) { + message.success(successMessage) + if (!isDelete) { + editModalVisible.value = false + } + loadData() + return true + } else { + throw new Error(response?.msg || response?.message || '操作失败') + } + } catch (error) { + console.error('API调用失败:', error) + message.error(error?.message || '操作失败,请稍后重试') + return false + } finally { + loading.value = false + } +} + // 保存(新增/编辑) async function handleSave() { try { await editFormRef.value.validate() } catch (error) { - console.error('保存提示词失败:', error) + console.error('表单验证失败:', error) + return } - loading.value = true - try { - const payload = { - name: editForm.name.trim(), - content: editForm.content.trim(), - category: editForm.category.trim() || null, - status: editForm.status, - } + const payload = { + name: editForm.name.trim(), + content: editForm.content.trim(), + category: editForm.category.trim() || null, + status: editForm.status, + } - if (editForm.id) { - // 更新:只需要传递要修改的字段,后端会自动填充其他字段 - payload.id = editForm.id - // 注意:sort、useCount、isPublic 等字段后端会自动从数据库获取,无需前端传递 - - const response = await UserPromptApi.updateUserPrompt(payload) - if (response && (response.code === 0 || response.code === 200)) { - message.success('更新成功') - editModalVisible.value = false - loadData() - } else { - throw new Error(response?.msg || response?.message || '更新失败') - } - } else { - // 新增:需要包含所有必需字段 - payload.sort = 0 - payload.useCount = 0 - payload.isPublic = false - - const response = await UserPromptApi.createUserPrompt(payload) - if (response && (response.code === 0 || response.code === 200)) { - message.success('创建成功') - editModalVisible.value = false - loadData() - } else { - throw new Error(response?.msg || response?.message || '创建失败') - } - } - } catch (error) { - console.error('保存提示词失败:', error) - message.error(error?.message || '保存失败,请稍后重试') - } finally { - loading.value = false + if (editForm.id) { + payload.id = editForm.id + await apiCall( + (data) => UserPromptApi.updateUserPrompt(data), + payload, + '更新成功' + ) + } else { + await apiCall( + (data) => UserPromptApi.createUserPrompt(data), + payload, + '创建成功' + ) } } @@ -253,21 +257,12 @@ function handleDelete(record) { title: '确认删除', content: `确定要删除提示词"${record.name}"吗?`, onOk: async () => { - loading.value = true - try { - const response = await UserPromptApi.deleteUserPrompt(record.id) - if (response && (response.code === 0 || response.code === 200)) { - message.success('删除成功') - loadData() - } else { - throw new Error(response?.msg || response?.message || '删除失败') - } - } catch (error) { - console.error('删除提示词失败:', error) - message.error(error?.message || '删除失败,请稍后重试') - } finally { - loading.value = false - } + await apiCall( + (id) => UserPromptApi.deleteUserPrompt(id), + record.id, + '删除成功', + true + ) }, }) } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/app/AppUserPromptController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java similarity index 98% rename from yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/app/AppUserPromptController.java rename to yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java index 1b905eceaa..da2836b6c8 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/app/AppUserPromptController.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.tik.userprompt.controller.app; +package cn.iocoder.yudao.module.tik.userprompt.controller; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -23,7 +23,7 @@ import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.USER_PROMPT_N @Tag(name = "用户 App - 用户提示词") @RestController -@RequestMapping("/ai/user-prompt") +@RequestMapping("/api/ai/user-prompt") @Validated public class AppUserPromptController { diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java index 3dd1c14a69..5e4158e0f1 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java @@ -29,17 +29,16 @@ public class UserPromptServiceImpl implements UserPromptService { @Override public Long createUserPrompt(UserPromptSaveReqVO createReqVO) { - // 插入 UserPromptDO userPrompt = BeanUtils.toBean(createReqVO, UserPromptDO.class); + if (userPrompt.getSort() == null) userPrompt.setSort(0); + if (userPrompt.getUseCount() == null) userPrompt.setUseCount(0); + if (userPrompt.getIsPublic() == null) userPrompt.setIsPublic(false); userPromptMapper.insert(userPrompt); - - // 返回 return userPrompt.getId(); } @Override public void updateUserPrompt(UserPromptSaveReqVO updateReqVO) { - // 1. 手动验证前端表单字段(与前端表单对应) if (updateReqVO.getName() == null || updateReqVO.getName().trim().isEmpty()) { throw new IllegalArgumentException("提示词名称不能为空"); } @@ -49,42 +48,32 @@ public class UserPromptServiceImpl implements UserPromptService { if (updateReqVO.getStatus() == null) { throw new IllegalArgumentException("状态不能为空"); } - - // 2. 校验存在并获取现有记录 + UserPromptDO existing = validateUserPromptExists(updateReqVO.getId()); - - // 3. 手动设置要更新的字段(只更新前端表单中的字段) UserPromptDO updateObj = new UserPromptDO(); updateObj.setId(updateReqVO.getId()); updateObj.setName(updateReqVO.getName().trim()); updateObj.setContent(updateReqVO.getContent().trim()); updateObj.setCategory(updateReqVO.getCategory() != null ? updateReqVO.getCategory().trim() : null); updateObj.setStatus(updateReqVO.getStatus()); - - // 4. 自动填充前端表单中没有的字段(从数据库获取) updateObj.setSort(existing.getSort()); updateObj.setUseCount(existing.getUseCount()); updateObj.setIsPublic(existing.getIsPublic()); - updateObj.setUserId(existing.getUserId()); // 保持用户ID不变 - - // 5. 执行更新 + updateObj.setUserId(existing.getUserId()); + userPromptMapper.updateById(updateObj); } @Override public void deleteUserPrompt(Long id) { - // 校验存在 validateUserPromptExists(id); - // 删除 userPromptMapper.deleteById(id); } @Override - public void deleteUserPromptListByIds(List ids) { - // 删除 + public void deleteUserPromptListByIds(List ids) { userPromptMapper.deleteByIds(ids); - } - + } private UserPromptDO validateUserPromptExists(Long id) { UserPromptDO userPrompt = userPromptMapper.selectById(id); @@ -103,5 +92,4 @@ public class UserPromptServiceImpl implements UserPromptService { public PageResult getUserPromptPage(UserPromptPageReqVO pageReqVO) { return userPromptMapper.selectPage(pageReqVO); } - } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptSaveReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptSaveReqVO.java index 8dc319b2f9..2587a206a4 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptSaveReqVO.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptSaveReqVO.java @@ -27,16 +27,13 @@ public class UserPromptSaveReqVO { @Schema(description = "分类/标签") private String category; - @Schema(description = "是否公开(0-私有,1-公开)", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "是否公开(0-私有,1-公开)不能为空") + @Schema(description = "是否公开(0-私有,1-公开)") private Boolean isPublic; - @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "排序不能为空") + @Schema(description = "排序") private Integer sort; - @Schema(description = "使用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "22185") - @NotNull(message = "使用次数不能为空") + @Schema(description = "使用次数") private Integer useCount; @Schema(description = "状态(0-禁用,1-启用)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java index 4b3c40dda6..9a33012e32 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java @@ -16,7 +16,9 @@ import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileDO; import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper; import cn.iocoder.yudao.module.tik.file.service.TikOssInitService; import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikDigitalHumanTaskDO; +import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikUserVoiceDO; import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikDigitalHumanTaskMapper; +import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikUserVoiceMapper; import cn.iocoder.yudao.module.tik.voice.enums.DigitalHumanTaskStatusEnum; import cn.iocoder.yudao.module.tik.voice.enums.DigitalHumanTaskStepEnum; import cn.iocoder.yudao.module.tik.voice.vo.*; @@ -48,6 +50,7 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService { private final TikDigitalHumanTaskMapper taskMapper; private final TikUserFileMapper userFileMapper; + private final TikUserVoiceMapper userVoiceMapper; private final FileMapper fileMapper; private final FileApi fileApi; private final TikUserVoiceService userVoiceService; @@ -224,9 +227,21 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService { throw new IllegalArgumentException("文案不能为空"); } - // 验证音色ID(必填) - if (StrUtil.isBlank(reqVO.getVoiceId())) { - throw new IllegalArgumentException("音色ID不能为空"); + // 验证音色参数(二选一:voiceId用于系统音色,voiceConfigId用于用户音色) + boolean hasVoiceId = StrUtil.isNotBlank(reqVO.getVoiceId()); + boolean hasVoiceConfigId = reqVO.getVoiceConfigId() != null; + + if (!hasVoiceId && !hasVoiceConfigId) { + throw new IllegalArgumentException("必须提供音色ID(voiceId或voiceConfigId)"); + } + + if (hasVoiceId && hasVoiceConfigId) { + throw new IllegalArgumentException("voiceId和voiceConfigId不能同时提供"); + } + + // 如果是用户音色,验证voiceConfigId对应的用户音色是否存在 + if (hasVoiceConfigId) { + validateUserVoice(reqVO.getVoiceConfigId(), userId); } // 验证视频文件(必填) @@ -253,17 +268,44 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService { } } + /** + * 验证用户音色 + */ + private void validateUserVoice(Long voiceConfigId, Long userId) { + TikUserVoiceDO userVoice = userVoiceMapper.selectById(voiceConfigId); + if (userVoice == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_NOT_EXISTS, "用户音色不存在"); + } + + if (!userVoice.getUserId().equals(userId)) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_FORBIDDEN, "无权访问该音色"); + } + + if (StrUtil.isBlank(userVoice.getVoiceId())) { + throw new IllegalArgumentException("该音色配置无效,缺少voiceId"); + } + } + /** * 创建任务记录 */ private TikDigitalHumanTaskDO createTaskRecord(AppTikDigitalHumanCreateReqVO reqVO, Long userId) { + // 如果是用户音色,需要从voiceConfigId获取voiceId + String voiceId = reqVO.getVoiceId(); + if (voiceId == null && reqVO.getVoiceConfigId() != null) { + TikUserVoiceDO userVoice = userVoiceMapper.selectById(reqVO.getVoiceConfigId()); + if (userVoice != null) { + voiceId = userVoice.getVoiceId(); + } + } + return TikDigitalHumanTaskDO.builder() .userId(userId) .taskName(reqVO.getTaskName()) .aiProvider(StrUtil.blankToDefault(reqVO.getAiProvider(), "302ai")) .videoFileId(reqVO.getVideoFileId()) .videoUrl(reqVO.getVideoUrl()) - .voiceId(reqVO.getVoiceId()) + .voiceId(voiceId) .inputText(reqVO.getInputText()) .speechRate(reqVO.getSpeechRate() != null ? reqVO.getSpeechRate() : 1.0f) .volume(reqVO.getVolume() != null ? reqVO.getVolume() : 0f) diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java index b990622123..ffad439223 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java @@ -33,9 +33,12 @@ public class AppTikDigitalHumanCreateReqVO { @Size(max = 1024, message = "视频URL不能超过1024个字符") private String videoUrl; - @Schema(description = "音色ID(CosyVoice voiceId)", example = "cosyvoice-v3-flash-sys-xxx") + @Schema(description = "音色ID(CosyVoice voiceId,系统音色使用)", example = "cosyvoice-v3-flash-sys-xxx") private String voiceId; + @Schema(description = "用户音色配置ID(tik_user_voice.id,用户音色使用)", example = "123") + private Long voiceConfigId; + @Schema(description = "输入文本(用于语音合成,文案必填)", example = "您好,欢迎体验数字人") @NotBlank(message = "文案不能为空") @Size(max = 4000, message = "文本不能超过4000个字符") diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikLatentsyncResultRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikLatentsyncResultRespVO.java index 4ac98eab3a..e300259bc1 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikLatentsyncResultRespVO.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikLatentsyncResultRespVO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.tik.voice.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.eclipse.angus.mail.iap.ByteArray; /** * Latentsync 任务结果响应 VO @@ -21,6 +22,10 @@ public class AppTikLatentsyncResultRespVO { @Schema(description = "视频信息") private VideoInfo video; + @Schema(description = "视频流信息") + private ByteArray value; + + @Schema(description = "视频信息") @Data public static class VideoInfo {