ソースを参照

个性化语音修改

Zhangbw 2 ヶ月 前
コミット
5d55a8d867

+ 25 - 0
ruoyi-modules/yp-talk/src/main/java/org/dromara/talk/config/WebMvcConfig.java

@@ -0,0 +1,25 @@
+package org.dromara.talk.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.io.File;
+
+/**
+ * Web MVC 配置
+ *
+ * @author Lion Li
+ * @date 2026-01-27
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        // 配置头像上传路径的静态资源映射
+        String uploadPath = "file:" + System.getProperty("user.dir") + File.separator + "uploads" + File.separator;
+        registry.addResourceHandler("/uploads/**")
+            .addResourceLocations(uploadPath);
+    }
+}

+ 145 - 0
ruoyi-modules/yp-talk/src/main/java/org/dromara/talk/controller/admin/TalkAgentController.java

@@ -0,0 +1,145 @@
+package org.dromara.talk.controller.admin;
+
+import java.util.List;
+import java.io.IOException;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.talk.domain.vo.TalkAgentVo;
+import org.dromara.talk.domain.bo.TalkAgentBo;
+import org.dromara.talk.service.ITalkAgentService;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.core.domain.dto.DictDataDTO;
+import org.dromara.common.core.service.DictService;
+
+/**
+ * 客服配置(管理端)
+ *
+ * @author Lion Li
+ * @date 2026-01-27
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/talk/admin/agent")
+public class TalkAgentController extends BaseController {
+
+    private final ITalkAgentService talkAgentService;
+    private final DictService dictService;
+
+    /**
+     * 查询客服配置列表
+     */
+    @SaCheckPermission("talk:agent:list")
+    @GetMapping("/list")
+    public TableDataInfo<TalkAgentVo> list(TalkAgentBo bo, PageQuery pageQuery) {
+        return talkAgentService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出客服配置列表
+     */
+    @SaCheckPermission("talk:agent:export")
+    @Log(title = "客服配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(TalkAgentBo bo, HttpServletResponse response) {
+        List<TalkAgentVo> list = talkAgentService.queryList(bo);
+        ExcelUtil.exportExcel(list, "客服配置", TalkAgentVo.class, response);
+    }
+
+    /**
+     * 获取客服配置详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("talk:agent:query")
+    @GetMapping("/{id}")
+    public R<TalkAgentVo> getInfo(@NotNull(message = "主键不能为空")
+                                     @PathVariable Long id) {
+        return R.ok(talkAgentService.queryById(id));
+    }
+
+    /**
+     * 新增客服配置
+     */
+    @SaCheckPermission("talk:agent:add")
+    @Log(title = "客服配置", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody TalkAgentBo bo) {
+        return toAjax(talkAgentService.insertByBo(bo));
+    }
+
+    /**
+     * 修改客服配置
+     */
+    @SaCheckPermission("talk:agent:edit")
+    @Log(title = "客服配置", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody TalkAgentBo bo) {
+        return toAjax(talkAgentService.updateByBo(bo));
+    }
+
+    /**
+     * 删除客服配置
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("talk:agent:remove")
+    @Log(title = "客服配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] ids) {
+        return toAjax(talkAgentService.deleteWithValidByIds(List.of(ids), true));
+    }
+
+    /**
+     * 上传客服头像
+     *
+     * @param file 上传的文件
+     */
+    @SaCheckPermission("talk:agent:edit")
+    @Log(title = "客服配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/avatar")
+    public R<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
+        try {
+            String avatarUrl = talkAgentService.uploadAvatar(file);
+            return R.ok(avatarUrl);
+        } catch (IOException e) {
+            return R.fail("上传头像失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取TTS语音字典
+     */
+    @SaCheckPermission("talk:agent:list")
+    @GetMapping("/dict/ttsVcn")
+    public R<List<DictDataDTO>> getTtsVcnDict() {
+        return R.ok(dictService.getDictData("tts_vcn"));
+    }
+
+    /**
+     * 获取语言字典
+     */
+    @SaCheckPermission("talk:agent:list")
+    @GetMapping("/dict/language")
+    public R<List<DictDataDTO>> getLanguageDict() {
+        return R.ok(dictService.getDictData("agent_language"));
+    }
+}

+ 105 - 0
ruoyi-modules/yp-talk/src/main/java/org/dromara/talk/controller/admin/TalkSessionController.java

@@ -0,0 +1,105 @@
+package org.dromara.talk.controller.admin;
+
+import java.util.List;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.talk.domain.vo.TalkSessionVo;
+import org.dromara.talk.domain.bo.TalkSessionBo;
+import org.dromara.talk.service.ITalkSessionService;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+/**
+ * 对话会话(管理端)
+ *
+ * @author Lion Li
+ * @date 2026-01-27
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/talk/admin/session")
+public class TalkSessionController extends BaseController {
+
+    private final ITalkSessionService talkSessionService;
+
+    /**
+     * 查询对话会话列表
+     */
+    @SaCheckPermission("talk:session:list")
+    @GetMapping("/list")
+    public TableDataInfo<TalkSessionVo> list(TalkSessionBo bo, PageQuery pageQuery) {
+        return talkSessionService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出对话会话列表
+     */
+    @SaCheckPermission("talk:session:export")
+    @Log(title = "对话会话", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(TalkSessionBo bo, HttpServletResponse response) {
+        List<TalkSessionVo> list = talkSessionService.queryList(bo);
+        ExcelUtil.exportExcel(list, "对话会话", TalkSessionVo.class, response);
+    }
+
+    /**
+     * 获取对话会话详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("talk:session:query")
+    @GetMapping("/{id}")
+    public R<TalkSessionVo> getInfo(@NotNull(message = "主键不能为空")
+                                     @PathVariable Long id) {
+        return R.ok(talkSessionService.queryById(id));
+    }
+
+    /**
+     * 新增对话会话
+     */
+    @SaCheckPermission("talk:session:add")
+    @Log(title = "对话会话", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody TalkSessionBo bo) {
+        return toAjax(talkSessionService.insertByBo(bo));
+    }
+
+    /**
+     * 修改对话会话
+     */
+    @SaCheckPermission("talk:session:edit")
+    @Log(title = "对话会话", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody TalkSessionBo bo) {
+        return toAjax(talkSessionService.updateByBo(bo));
+    }
+
+    /**
+     * 删除对话会话
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("talk:session:remove")
+    @Log(title = "对话会话", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] ids) {
+        return toAjax(talkSessionService.deleteWithValidByIds(List.of(ids), true));
+    }
+}

+ 67 - 0
ruoyi-modules/yp-talk/src/main/java/org/dromara/talk/controller/api/ChatController.java

@@ -0,0 +1,67 @@
+package org.dromara.talk.controller.api;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.dto.DictDataDTO;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.talk.domain.bo.TalkAgentBo;
+import org.dromara.talk.domain.vo.TalkAgentVo;
+import org.dromara.talk.service.IChatService;
+import org.dromara.talk.service.ITalkAgentService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 对话接口(对话前端)
+ *
+ * @author Lion Li
+ * @date 2026-01-27
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/talk")
+@CrossOrigin(origins = "*", maxAge = 3600)
+public class ChatController {
+
+    private final IChatService chatService;
+    private final DictService dictService;
+    private final ITalkAgentService talkAgentService;
+
+    @SaIgnore
+    @PostMapping("/message")
+    public Map<String, Object> handleMessage(@RequestBody Map<String, Object> request) {
+        String userMessage = (String) request.get("message");
+        Long agentId = request.get("agentId") != null ?
+            Long.valueOf(request.get("agentId").toString()) : null;
+
+        log.info("收到用户消息: {}, 客服ID: {}", userMessage, agentId);
+
+        return chatService.processMessage(userMessage, agentId);
+    }
+
+    @SaIgnore
+    @GetMapping("/dict/language")
+    public List<DictDataDTO> getLanguageDict() {
+        return dictService.getDictData("agent_language");
+    }
+
+    @SaIgnore
+    @GetMapping("/agent/list")
+    public TableDataInfo<TalkAgentVo> getAgentList(TalkAgentBo bo) {
+        List<TalkAgentVo> list = talkAgentService.queryList(bo);
+        return TableDataInfo.build(list);
+    }
+
+    @SaIgnore
+    @PutMapping("/agent/{id}")
+    public Map<String, Object> updateAgentConfig(@PathVariable Long id, @RequestBody TalkAgentBo bo) {
+        bo.setId(id);
+        boolean success = talkAgentService.updateByBo(bo);
+        return Map.of("success", success);
+    }
+}

+ 18 - 0
ruoyi-modules/yp-talk/src/main/java/org/dromara/talk/service/IChatService.java

@@ -0,0 +1,18 @@
+package org.dromara.talk.service;
+
+import java.util.Map;
+
+/**
+ * 聊天服务接口
+ */
+public interface IChatService {
+
+    /**
+     * 处理用户消息
+     *
+     * @param userMessage 用户消息
+     * @param agentId 客服ID
+     * @return 响应数据(包含回复文本和音频)
+     */
+    Map<String, Object> processMessage(String userMessage, Long agentId);
+}

+ 97 - 0
ruoyi-modules/yp-talk/src/main/java/org/dromara/talk/service/impl/ChatServiceImpl.java

@@ -0,0 +1,97 @@
+package org.dromara.talk.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.talk.domain.vo.TalkAgentVo;
+import org.dromara.talk.service.IChatService;
+import org.dromara.talk.service.ITalkAgentService;
+import org.dromara.talk.service.ITtsService;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ChatServiceImpl implements IChatService {
+
+    private final ITtsService ttsService;
+    private final ITalkAgentService talkAgentService;
+
+    @Override
+    public Map<String, Object> processMessage(String userMessage, Long agentId) {
+        log.info("处理用户消息: {}, 客服ID: {}", userMessage, agentId);
+
+        // 获取客服配置
+        TalkAgentVo agentConfig = null;
+        if (agentId != null) {
+            agentConfig = talkAgentService.queryById(agentId);
+        }
+
+        // 生成回复
+        String reply = generateReply(userMessage);
+
+        // 合成语音(传递客服配置)
+        String audioBase64 = synthesizeAudio(reply, agentConfig);
+
+        // 构建响应
+        Map<String, Object> response = new HashMap<>();
+        response.put("reply", reply);
+        response.put("audio", audioBase64);
+        response.put("timestamp", System.currentTimeMillis());
+
+        log.info("消息处理完成: reply长度={}, audio长度={}",
+            reply != null ? reply.length() : 0,
+            audioBase64 != null ? audioBase64.length() : 0);
+
+        return response;
+    }
+
+    private String generateReply(String userMessage) {
+        // 默认回复(后期接入AI)
+        return userMessage;
+    }
+
+    private String synthesizeAudio(String text, TalkAgentVo agentConfig) {
+        CompletableFuture<String> audioFuture = new CompletableFuture<>();
+
+        ttsService.synthesize(text, agentConfig, new ITtsService.AudioCallback() {
+            private final java.io.ByteArrayOutputStream audioBytes = new java.io.ByteArrayOutputStream();
+
+            @Override
+            public void onAudio(String base64Audio, int status) {
+                try {
+                    //解码base64音频片段
+                    byte[] decoded = java.util.Base64.getDecoder().decode(base64Audio);
+                    //追加到字节流
+                    audioBytes.write(decoded);
+                } catch (Exception e) {
+                    log.error("解码音频数据失败", e);
+                }
+                if (status == 2) {
+                    // 将完整音频重新编码为 base64
+                    String finalBase64 = java.util.Base64.getEncoder().encodeToString(audioBytes.toByteArray());
+                    //完成异步任务
+                    audioFuture.complete(finalBase64);
+                }
+            }
+
+            @Override
+            public void onError(int code, String message) {
+                log.error("TTS合成失败: {}", message);
+                audioFuture.complete(null);
+            }
+        });
+
+        // 等待音频合成完成(最多10秒)
+        try {
+            return audioFuture.get(10, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            log.error("等待音频合成超时", e);
+            return null;
+        }
+    }
+}