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 {