From d38d33f5782b0919ac419115afc032783e3baa23 Mon Sep 17 00:00:00 2001 From: gitee Date: Mon, 21 Jul 2025 17:51:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BC=B9=E5=B9=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 9 + .../supervision/config/SecurityConfig.java | 2 +- .../supervision/config/WebSocketConfig.java | 22 +++ .../supervision/constant/PromptTemplate.java | 6 +- .../controller/AgentController.java | 6 + .../controller/DigitalHumanController.java | 52 ++++++ .../com/supervision/domain/DigitalHuman.java | 5 + .../domain/LivetalkingChatDTO.java | 42 +++++ .../com/supervision/domain/UserDetail.java | 10 ++ .../com/supervision/dto/DanmakuMessage.java | 92 ++++++++++ .../com/supervision/dto/DigitalHumanDTO.java | 65 +++++++ .../supervision/dto/DigitalHumanVoiceDTO.java | 12 ++ .../DigitalHumanDialogueLogService.java | 4 + .../service/LivetalkingService.java | 15 ++ .../service/danmaku/DanmakuPublisher.java | 119 +++++++++++++ .../danmaku/DanmakuWebSocketHandler.java | 163 ++++++++++++++++++ .../danmaku/DigitalHumanManageService.java | 23 +++ .../danmaku/WebSocketSessionManager.java | 39 +++++ .../DigitalHumanDialogueLogServiceImpl.java | 7 + .../impl/DigitalHumanManageServiceImpl.java | 73 ++++++++ .../service/impl/LivetalkingServiceImpl.java | 62 +++++++ .../service/impl/UserDetailsServiceImpl.java | 4 +- .../java/com/supervision/util/UserUtil.java | 12 ++ src/main/resources/application.yml | 4 + src/main/resources/static/danmaku.js | 146 ++++++++++++++++ src/main/resources/static/index.html | 51 ++++++ .../supervision/PlatformApplicationTest.java | 30 ++++ 27 files changed, 1071 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/supervision/config/WebSocketConfig.java create mode 100644 src/main/java/com/supervision/controller/DigitalHumanController.java create mode 100644 src/main/java/com/supervision/domain/LivetalkingChatDTO.java create mode 100644 src/main/java/com/supervision/dto/DanmakuMessage.java create mode 100644 src/main/java/com/supervision/dto/DigitalHumanDTO.java create mode 100644 src/main/java/com/supervision/dto/DigitalHumanVoiceDTO.java create mode 100644 src/main/java/com/supervision/service/LivetalkingService.java create mode 100644 src/main/java/com/supervision/service/danmaku/DanmakuPublisher.java create mode 100644 src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java create mode 100644 src/main/java/com/supervision/service/danmaku/DigitalHumanManageService.java create mode 100644 src/main/java/com/supervision/service/danmaku/WebSocketSessionManager.java create mode 100644 src/main/java/com/supervision/service/impl/DigitalHumanManageServiceImpl.java create mode 100644 src/main/java/com/supervision/service/impl/LivetalkingServiceImpl.java create mode 100644 src/main/resources/static/danmaku.js create mode 100644 src/main/resources/static/index.html diff --git a/pom.xml b/pom.xml index 913df1f..6370740 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,15 @@ mybatis-plus-boot-starter 3.5.5 + + org.springframework.boot + spring-boot-starter-websocket + + + com.hankcs + hanlp + portable-1.8.6 + org.postgresql postgresql diff --git a/src/main/java/com/supervision/config/SecurityConfig.java b/src/main/java/com/supervision/config/SecurityConfig.java index 0ce1a23..76ca171 100644 --- a/src/main/java/com/supervision/config/SecurityConfig.java +++ b/src/main/java/com/supervision/config/SecurityConfig.java @@ -36,7 +36,7 @@ public class SecurityConfig { http .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/**","/agent/streamChat").permitAll() + .requestMatchers("/auth/**","/agent/streamChat","/livetalking/chatCallBack","/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session diff --git a/src/main/java/com/supervision/config/WebSocketConfig.java b/src/main/java/com/supervision/config/WebSocketConfig.java new file mode 100644 index 0000000..7956662 --- /dev/null +++ b/src/main/java/com/supervision/config/WebSocketConfig.java @@ -0,0 +1,22 @@ +package com.supervision.config; + +import com.supervision.service.danmaku.DanmakuWebSocketHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +@RequiredArgsConstructor +public class WebSocketConfig implements WebSocketConfigurer { + + private final DanmakuWebSocketHandler webSocketHandler; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(webSocketHandler, "/ws/danmaku") + .setAllowedOrigins("*"); + } +} diff --git a/src/main/java/com/supervision/constant/PromptTemplate.java b/src/main/java/com/supervision/constant/PromptTemplate.java index ec5b6b0..a4925c2 100644 --- a/src/main/java/com/supervision/constant/PromptTemplate.java +++ b/src/main/java/com/supervision/constant/PromptTemplate.java @@ -4,12 +4,14 @@ public class PromptTemplate { public static final String GENERAL_INDUSTRY_TEMPLATE = """ - 你是一个通用行业的智能体,擅长处理各种行业的任务。请根据用户的需求,提供专业的建议和解决方案。 + 你是一个由苏胜天科技有限公司制造的通用行业的智能体,擅长处理各种行业的任务。请根据用户的需求,提供专业的建议和解决方案。 + 请以专业的语气回答问题,确保提供的信息准确且有用。 用户需求:{query} """; public static final String HEART_GUIDE_TEMPLATE = """ - 你是一个心灵导师,擅长提供情感支持和心理指导。请根据用户的需求,提供温暖和关怀的建议。 + 你是一个由苏胜天科技有限公司制造的心灵导师智能体,擅长提供情感支持和心理指导。请根据用户的需求,提供温暖和关怀的建议。 + 请以温柔的语气回答问题,确保提供的信息能够帮助用户感到被理解和支持。 用户需求:{query} """; } diff --git a/src/main/java/com/supervision/controller/AgentController.java b/src/main/java/com/supervision/controller/AgentController.java index f03511b..03236af 100644 --- a/src/main/java/com/supervision/controller/AgentController.java +++ b/src/main/java/com/supervision/controller/AgentController.java @@ -34,6 +34,12 @@ public class AgentController { return agentService.streamChat(agentChatReqDTO); } + /** + * 分页查询数字人列表 + * @param page 当前页码 + * @param pageSize 每页大小 + * @return + */ @GetMapping("/pageList") public R> pageList(@RequestParam (name = "page", required = false,defaultValue = "1") Integer page, @RequestParam (name = "pageSize", required = false,defaultValue = "10") Integer pageSize) { diff --git a/src/main/java/com/supervision/controller/DigitalHumanController.java b/src/main/java/com/supervision/controller/DigitalHumanController.java new file mode 100644 index 0000000..0e8e855 --- /dev/null +++ b/src/main/java/com/supervision/controller/DigitalHumanController.java @@ -0,0 +1,52 @@ +package com.supervision.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.supervision.domain.LivetalkingChatDTO; +import com.supervision.dto.DigitalHumanDTO; +import com.supervision.dto.DigitalHumanVoiceDTO; +import com.supervision.dto.R; +import com.supervision.service.danmaku.DigitalHumanManageService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 数字人控制器 + */ +@RestController +@RequestMapping("/digitalHuman") +@RequiredArgsConstructor +public class DigitalHumanController { + + private final DigitalHumanManageService digitalHumanManageService; + + /** + * 分页查询数字人列表 + * @param page 当前页码 + * @param pageSize 每页大小 + * @return + */ + @GetMapping("/pageList") + public R> pageList(@RequestParam(name = "page", required = false,defaultValue = "1") Integer page, + @RequestParam (name = "pageSize", required = false,defaultValue = "10") Integer pageSize) { + + IPage paged = digitalHumanManageService.pageList(page, pageSize); + return R.ok(paged); + } + + /** + * 切换数字人语音 + * @param digitalHumanVoiceDTO 包含语音ID和数字人ID的DTO + * @return + */ + @PostMapping("/switchVoice") + public R setVoice(@RequestBody DigitalHumanVoiceDTO digitalHumanVoiceDTO) { + digitalHumanManageService.setVoice(digitalHumanVoiceDTO); + return R.ok(); + } + + @PostMapping("/livetalking/chatCallBack") + public R chatCallBack(@RequestBody LivetalkingChatDTO digitalHumanVoiceDTO) { + digitalHumanManageService.chatCallBack(digitalHumanVoiceDTO); + return R.ok(); + } +} diff --git a/src/main/java/com/supervision/domain/DigitalHuman.java b/src/main/java/com/supervision/domain/DigitalHuman.java index f61c8a1..9228eda 100644 --- a/src/main/java/com/supervision/domain/DigitalHuman.java +++ b/src/main/java/com/supervision/domain/DigitalHuman.java @@ -55,6 +55,11 @@ public class DigitalHuman implements Serializable { */ private String headPicId; + /** + * 模特性别 0:女 1:男 + */ + private String gender; + /** * 创建时间 */ diff --git a/src/main/java/com/supervision/domain/LivetalkingChatDTO.java b/src/main/java/com/supervision/domain/LivetalkingChatDTO.java new file mode 100644 index 0000000..cb4c0e2 --- /dev/null +++ b/src/main/java/com/supervision/domain/LivetalkingChatDTO.java @@ -0,0 +1,42 @@ +package com.supervision.domain; + +import com.supervision.dto.DanmakuMessage; +import lombok.Data; + +@Data +public class LivetalkingChatDTO { + + /** + * 消息id + */ + private String messageId; + + /** + * 数字人id + */ + private String humanId; + + + /** + * 房间id + */ + private String roomId; + + /** + * 问题 + */ + private String query; + + /** + * 回答 + */ + private String answer; + + public LivetalkingChatDTO() { + } + + public LivetalkingChatDTO(DanmakuMessage danmakuMessage) { + this.roomId = danmakuMessage.getRoomId(); + this.query = danmakuMessage.getContent(); + } +} diff --git a/src/main/java/com/supervision/domain/UserDetail.java b/src/main/java/com/supervision/domain/UserDetail.java index b45aa51..2e2f585 100644 --- a/src/main/java/com/supervision/domain/UserDetail.java +++ b/src/main/java/com/supervision/domain/UserDetail.java @@ -8,6 +8,8 @@ import java.util.Collection; public class UserDetail extends User { private String userId; + private String nickname; + public UserDetail(String userId ,String username, String password, Collection authorities) { super(username, password, authorities); @@ -29,4 +31,12 @@ public class UserDetail extends User { public String getUserId() { return userId; } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } } diff --git a/src/main/java/com/supervision/dto/DanmakuMessage.java b/src/main/java/com/supervision/dto/DanmakuMessage.java new file mode 100644 index 0000000..1a1e6b6 --- /dev/null +++ b/src/main/java/com/supervision/dto/DanmakuMessage.java @@ -0,0 +1,92 @@ +package com.supervision.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.supervision.domain.DigitalHuman; +import com.supervision.domain.DigitalHumanDialogueLog; +import lombok.Data; + +import java.lang.ref.PhantomReference; + +@Data +public class DanmakuMessage { + + /** + * 房间ID + */ + private String roomId; + + /** + * 弹幕内容 + */ + private String content; + + /** + * 用户ID + */ + private String userId; + + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 弹幕颜色 + */ + private String color; + + /** + * 弹幕字体大小 + */ + private Integer size; + + /** + * 时间戳 + */ + private Long timestamp; + + /** + * 弹幕类型 0:用户发送 1:系统回复 + */ + private String type; + + public DanmakuMessage() { + } + + // 简化构造方法 + public DanmakuMessage(String roomId, String content, String userId) { + this.roomId = roomId; + this.content = content; + this.userId = userId; + this.color = "#ffffff"; + this.size = 16; + this.timestamp = System.currentTimeMillis(); + } + + public String toJson() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException("序列化失败", e); + } + } + + public static DanmakuMessage fromJson(String json) { + try { + return new ObjectMapper().readValue(json, DanmakuMessage.class); + } catch (JsonProcessingException e) { + throw new RuntimeException("反序列化失败", e); + } + } + + public DigitalHumanDialogueLog toDialogueLog() { + DigitalHumanDialogueLog log = new DigitalHumanDialogueLog(); + log.setDigitalHumanId(this.roomId); + log.setUserInput(this.content); + log.setUserId(this.userId); + log.setAnswerType("0"); // 默认回答类型为正常回答 + return log; + } +} diff --git a/src/main/java/com/supervision/dto/DigitalHumanDTO.java b/src/main/java/com/supervision/dto/DigitalHumanDTO.java new file mode 100644 index 0000000..1966a97 --- /dev/null +++ b/src/main/java/com/supervision/dto/DigitalHumanDTO.java @@ -0,0 +1,65 @@ +package com.supervision.dto; + +import com.supervision.domain.DigitalHuman; +import lombok.Data; + +@Data +public class DigitalHumanDTO { + + private String id; + + /** + * 模特名 + */ + private String modelName; + + /** + * 所属行业 + */ + private String industry; + + /** + * 擅长 + */ + private String goodAt; + + /** + * 默认音色id + + */ + private String defaultVoiceId; + + /** + * 在线状态 0:离线 1:在线 + */ + private String onlineStatus; + + /** + * 模型类型 0:系统预设 1:自定义 + */ + private String modelType; + + /** + * 头像图片id + */ + private String headPicId; + + /** + * 模特性别 0:女 1:男 + */ + private String gender; + public DigitalHumanDTO() { + } + + public DigitalHumanDTO(DigitalHuman digitalHuman) { + this.id = digitalHuman.getId(); + this.modelName = digitalHuman.getModelName(); + this.industry = digitalHuman.getIndustry(); + this.goodAt = digitalHuman.getGoodAt(); + this.defaultVoiceId = digitalHuman.getDefaultVoiceId(); + this.onlineStatus = digitalHuman.getOnlineStatus(); + this.modelType = digitalHuman.getModelType(); + this.headPicId = digitalHuman.getHeadPicId(); + this.gender = digitalHuman.getGender(); + } +} diff --git a/src/main/java/com/supervision/dto/DigitalHumanVoiceDTO.java b/src/main/java/com/supervision/dto/DigitalHumanVoiceDTO.java new file mode 100644 index 0000000..64ad6f1 --- /dev/null +++ b/src/main/java/com/supervision/dto/DigitalHumanVoiceDTO.java @@ -0,0 +1,12 @@ +package com.supervision.dto; + +import lombok.Data; + +@Data +public class DigitalHumanVoiceDTO { + + private String voiceId; + + private String digitalHumanId; + +} diff --git a/src/main/java/com/supervision/service/DigitalHumanDialogueLogService.java b/src/main/java/com/supervision/service/DigitalHumanDialogueLogService.java index 5af7d38..74c38ca 100644 --- a/src/main/java/com/supervision/service/DigitalHumanDialogueLogService.java +++ b/src/main/java/com/supervision/service/DigitalHumanDialogueLogService.java @@ -2,6 +2,7 @@ package com.supervision.service; import com.supervision.domain.DigitalHumanDialogueLog; import com.baomidou.mybatisplus.extension.service.IService; +import com.supervision.dto.DanmakuMessage; /** * @author Administrator @@ -10,4 +11,7 @@ import com.baomidou.mybatisplus.extension.service.IService; */ public interface DigitalHumanDialogueLogService extends IService { + + + String saveLog(DanmakuMessage danmakuMessage); } diff --git a/src/main/java/com/supervision/service/LivetalkingService.java b/src/main/java/com/supervision/service/LivetalkingService.java new file mode 100644 index 0000000..bdb3790 --- /dev/null +++ b/src/main/java/com/supervision/service/LivetalkingService.java @@ -0,0 +1,15 @@ +package com.supervision.service; + +import com.supervision.domain.LivetalkingChatDTO; + +/** + * 数字人直播管理服务接口 + */ +public interface LivetalkingService { + + + void chat(LivetalkingChatDTO livetalkingChatDTO); + + + void setVoice(String voiceId, String digitalHumanId); +} diff --git a/src/main/java/com/supervision/service/danmaku/DanmakuPublisher.java b/src/main/java/com/supervision/service/danmaku/DanmakuPublisher.java new file mode 100644 index 0000000..03dfe3f --- /dev/null +++ b/src/main/java/com/supervision/service/danmaku/DanmakuPublisher.java @@ -0,0 +1,119 @@ +package com.supervision.service.danmaku; + +import com.supervision.dto.DanmakuMessage; +import java.util.concurrent.*; +import java.util.function.Predicate; + +/** + * 弹幕消息发布中心(基于Flow API) + */ +public class DanmakuPublisher { + // 使用单例模式 + private static final DanmakuPublisher INSTANCE = new DanmakuPublisher(); + + // 使用SubmissionPublisher作为基础实现 + private final SubmissionPublisher publisher; + // 房间过滤器缓存 + private final ConcurrentHashMap> roomFilters; + // 线程池 + private final ExecutorService executor; + + private DanmakuPublisher() { + this.executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + this.publisher = new SubmissionPublisher<>(executor, Flow.defaultBufferSize()); + this.roomFilters = new ConcurrentHashMap<>(); + } + + public static DanmakuPublisher getInstance() { + return INSTANCE; + } + + /** + * 发布新弹幕 + */ + public void publish(DanmakuMessage danmaku) { + publisher.submit(danmaku); + } + + /** + * 订阅指定房间的弹幕 + */ + public Flow.Subscription subscribe(String roomId, Flow.Subscriber subscriber) { + // 创建房间过滤器 + Predicate filter = msg -> roomId.equals(msg.getRoomId()); + roomFilters.putIfAbsent(roomId, filter); + + // 创建过滤处理器 + FilterProcessor processor = new FilterProcessor(filter, executor); + publisher.subscribe(processor); + processor.subscribe(subscriber); + + return new DanmakuSubscription(processor); + } + + /** + * 自定义Subscription用于取消订阅 + */ + private static class DanmakuSubscription implements Flow.Subscription { + private final FilterProcessor processor; + + DanmakuSubscription(FilterProcessor processor) { + this.processor = processor; + } + + @Override + public void request(long n) { + // 不实现背压控制 + } + + @Override + public void cancel() { + processor.cancel(); + } + } + + /** + * 自定义Processor实现房间过滤 + */ + private static class FilterProcessor extends SubmissionPublisher + implements Flow.Processor { + + private final Predicate filter; + private Flow.Subscription subscription; + + FilterProcessor(Predicate filter, ExecutorService executor) { + super(executor, Flow.defaultBufferSize()); + this.filter = filter; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(Long.MAX_VALUE); // 不限制请求数量 + } + + @Override + public void onNext(DanmakuMessage item) { + if (filter.test(item)) { + submit(item); // 转发给订阅者 + } + } + + @Override + public void onError(Throwable throwable) { + closeExceptionally(throwable); + } + + @Override + public void onComplete() { + close(); + } + + public void cancel() { + if (subscription != null) { + subscription.cancel(); + } + close(); + } + } +} diff --git a/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java b/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java new file mode 100644 index 0000000..3028c1f --- /dev/null +++ b/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java @@ -0,0 +1,163 @@ +package com.supervision.service.danmaku; + +import cn.hutool.core.lang.Assert; +import com.hankcs.hanlp.seg.common.Term; +import com.hankcs.hanlp.tokenizer.StandardTokenizer; +import com.supervision.domain.LivetalkingChatDTO; +import com.supervision.domain.UserDetail; +import com.supervision.dto.DanmakuMessage; +import com.supervision.service.DigitalHumanDialogueLogService; +import com.supervision.service.LivetalkingService; +import com.supervision.util.UserUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import java.io.IOException; +import java.util.concurrent.Flow; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DanmakuWebSocketHandler extends TextWebSocketHandler { + + private final WebSocketSessionManager sessionManager; + + private final DigitalHumanDialogueLogService dialogueLogService; + + private final LivetalkingService livetalkingService; + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) { + // 解析客户端消息 + DanmakuMessage danmaku = DanmakuMessage.fromJson(textMessage.getPayload()); + String roomId = getRoomIdFromSession(session); + danmaku.setRoomId(roomId); + + // 验证消息 + validateDanmaku(danmaku); + + // 保存到日志 + String messageId = dialogueLogService.saveLog(danmaku); + + // 判断是否需要回复 + if (isQuestion(danmaku.getContent())) { + log.info("检测到问题: {}", danmaku.getContent()); + LivetalkingChatDTO livetalkingChatDTO = new LivetalkingChatDTO(danmaku); + livetalkingChatDTO.setMessageId(messageId); + livetalkingService.chat(livetalkingChatDTO); + } + // 发布到消息中心 + DanmakuPublisher.getInstance().publish(danmaku); + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + String roomId = getRoomIdFromSession(session); + sessionManager.addSession(roomId, session); + // 为该会话创建订阅者 + Flow.Subscriber subscriber = createSubscriber(session); + Flow.Subscription subscription = DanmakuPublisher.getInstance() + .subscribe(roomId, subscriber); + + // 将会话与订阅关系保存,方便在连接关闭时取消订阅 + session.getAttributes().put("subscription", subscription); + + try { + UserDetail userDetail = UserUtil.currentUser(session); + DanmakuMessage message = new DanmakuMessage(); + message.setUserId(userDetail.getUserId()); + message.setNickname(userDetail.getNickname()); + message.setContent(userDetail.getNickname() + "来了"); + DanmakuPublisher.getInstance().publish(message); + }catch (Exception e){ + log.error("获取用户信息失败: {}", e.getMessage()); + } + + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + String roomId = getRoomIdFromSession(session); + sessionManager.removeSession(roomId, session); + + // 取消订阅 + Flow.Subscription subscription = (Flow.Subscription) + session.getAttributes().get("subscription"); + if (subscription != null) { + subscription.cancel(); + } + } + + private Flow.Subscriber createSubscriber(WebSocketSession session) { + return new Flow.Subscriber<>() { + private Flow.Subscription subscription; + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(Long.MAX_VALUE); // 不限制请求数量 + } + + @Override + public void onNext(DanmakuMessage danmaku) { + try { + if (session.isOpen()) { + session.sendMessage(new TextMessage(danmaku.toJson())); + } + } catch (IOException e) { + subscription.cancel(); + } + } + + @Override + public void onError(Throwable throwable) { + // 处理错误 + log.info("WebSocket错误: {}", throwable.getMessage()); + } + + @Override + public void onComplete() { + // 发布者关闭 + log.info("WebSocket连接已关闭: {}", session.getId()); + } + }; + } + + private String getRoomIdFromSession(WebSocketSession session) { + // 从URI获取roomId,如 ws://localhost:8080/ws/danmaku?roomId=123 + String query = session.getUri().getQuery(); + return query.split("=")[1]; + } + + private void validateDanmaku(DanmakuMessage danmaku) { + // 验证逻辑... + Assert.notEmpty(danmaku.getRoomId(), "房间ID不能为空"); + Assert.notEmpty(danmaku.getContent(), "弹幕内容不能为空"); + Assert.notEmpty(danmaku.getUserId(), "用户ID不能为空"); + } + + private boolean isQuestion(String sentence) { + String[] questionWords = {"什么", "为什么", "如何", "哪", "谁", "多少", "是否", "能否", "是不是"}; + String[] questionEndings = {"吗", "呢", "?", "?"}; + + // 结尾判断 + for (String end : questionEndings) { + if (sentence.trim().endsWith(end)) { + return true; + } + } + // 分词判断 + for (Term term : StandardTokenizer.segment(sentence)) { + for (String qw : questionWords) { + if (term.word.equals(qw)) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/com/supervision/service/danmaku/DigitalHumanManageService.java b/src/main/java/com/supervision/service/danmaku/DigitalHumanManageService.java new file mode 100644 index 0000000..a1a5b4e --- /dev/null +++ b/src/main/java/com/supervision/service/danmaku/DigitalHumanManageService.java @@ -0,0 +1,23 @@ +package com.supervision.service.danmaku; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.supervision.domain.LivetalkingChatDTO; +import com.supervision.dto.DigitalHumanDTO; +import com.supervision.dto.DigitalHumanVoiceDTO; + +public interface DigitalHumanManageService { + + /** + * 分页查询数字人信息 + * @param page 页码 + * @param pageSize 每页大小 + * @return + */ + IPage pageList(Integer page, Integer pageSize); + + + void setVoice(DigitalHumanVoiceDTO digitalHumanVoiceDTO); + + void chatCallBack(LivetalkingChatDTO digitalHumanVoiceDTO); + +} diff --git a/src/main/java/com/supervision/service/danmaku/WebSocketSessionManager.java b/src/main/java/com/supervision/service/danmaku/WebSocketSessionManager.java new file mode 100644 index 0000000..9121d82 --- /dev/null +++ b/src/main/java/com/supervision/service/danmaku/WebSocketSessionManager.java @@ -0,0 +1,39 @@ +package com.supervision.service.danmaku; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * WebSocket会话管理器 + */ +@Service +@RequiredArgsConstructor +public class WebSocketSessionManager { + // 房间ID -> 会话列表 + private final ConcurrentMap> roomSessions = new ConcurrentHashMap<>(); + + public void addSession(String roomId, WebSocketSession session) { + roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()) + .add(session); + } + + public void removeSession(String roomId, WebSocketSession session) { + Set sessions = roomSessions.get(roomId); + if (sessions != null) { + sessions.remove(session); + if (sessions.isEmpty()) { + roomSessions.remove(roomId); + } + } + } + + public Set getSessionsByRoom(String roomId) { + return roomSessions.getOrDefault(roomId, Collections.emptySet()); + } +} diff --git a/src/main/java/com/supervision/service/impl/DigitalHumanDialogueLogServiceImpl.java b/src/main/java/com/supervision/service/impl/DigitalHumanDialogueLogServiceImpl.java index 29131c3..0783c9d 100644 --- a/src/main/java/com/supervision/service/impl/DigitalHumanDialogueLogServiceImpl.java +++ b/src/main/java/com/supervision/service/impl/DigitalHumanDialogueLogServiceImpl.java @@ -2,6 +2,7 @@ package com.supervision.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.supervision.domain.DigitalHumanDialogueLog; +import com.supervision.dto.DanmakuMessage; import com.supervision.service.DigitalHumanDialogueLogService; import com.supervision.mapper.DigitalHumanDialogueLogMapper; import org.springframework.stereotype.Service; @@ -15,6 +16,12 @@ import org.springframework.stereotype.Service; public class DigitalHumanDialogueLogServiceImpl extends ServiceImpl implements DigitalHumanDialogueLogService{ + @Override + public String saveLog(DanmakuMessage danmakuMessage) { + DigitalHumanDialogueLog dialogueLog = danmakuMessage.toDialogueLog(); + super.save(dialogueLog); + return dialogueLog.getId(); + } } diff --git a/src/main/java/com/supervision/service/impl/DigitalHumanManageServiceImpl.java b/src/main/java/com/supervision/service/impl/DigitalHumanManageServiceImpl.java new file mode 100644 index 0000000..1597cf0 --- /dev/null +++ b/src/main/java/com/supervision/service/impl/DigitalHumanManageServiceImpl.java @@ -0,0 +1,73 @@ +package com.supervision.service.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.supervision.domain.LivetalkingChatDTO; +import com.supervision.domain.DigitalHuman; +import com.supervision.domain.DigitalHumanDialogueLog; +import com.supervision.domain.VoiceInfo; +import com.supervision.dto.DanmakuMessage; +import com.supervision.dto.DigitalHumanDTO; +import com.supervision.dto.DigitalHumanVoiceDTO; +import com.supervision.service.DigitalHumanDialogueLogService; +import com.supervision.service.LivetalkingService; +import com.supervision.service.VoiceInfoService; +import com.supervision.service.danmaku.DanmakuPublisher; +import com.supervision.service.danmaku.DigitalHumanManageService; +import com.supervision.service.DigitalHumanService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DigitalHumanManageServiceImpl implements DigitalHumanManageService { + + private final DigitalHumanService digitalHumanService; + + private final LivetalkingService livetalkingService; + + private final VoiceInfoService voiceInfoService; + + private final DigitalHumanDialogueLogService dialogueLogService; + @Override + public IPage pageList(Integer page, Integer pageSize) { + + Page paged = digitalHumanService.page(Page.of(page, pageSize)); + return paged.convert(DigitalHumanDTO::new); + } + + @Override + public void setVoice(DigitalHumanVoiceDTO digitalHumanVoiceDTO) { + + VoiceInfo voiceInfo = voiceInfoService.getById(digitalHumanVoiceDTO.getVoiceId()); + Assert.notNull(voiceInfo, "语音信息不存在"); + livetalkingService.setVoice(voiceInfo.getVoiceCode(), digitalHumanVoiceDTO.getDigitalHumanId()); + } + + @Override + public void chatCallBack(LivetalkingChatDTO digitalHumanVoiceDTO) { + // 这里可以添加处理聊天回调的逻辑 + log.info("Received chat callback: {}", JSONUtil.toJsonStr(digitalHumanVoiceDTO)); + + DigitalHumanDialogueLog dialogueLog = new DigitalHumanDialogueLog(); + dialogueLog.setId(digitalHumanVoiceDTO.getMessageId()); + dialogueLog.setDigitalHumanId(digitalHumanVoiceDTO.getHumanId()); + dialogueLog.setUserInput(digitalHumanVoiceDTO.getQuery()); + dialogueLog.setSystemOut(digitalHumanVoiceDTO.getAnswer()); + dialogueLogService.updateById(dialogueLog); + + // 消息推送到弹幕系统 + DanmakuMessage danmakuMessage = new DanmakuMessage(); + danmakuMessage.setContent(digitalHumanVoiceDTO.getAnswer()); + danmakuMessage.setRoomId(digitalHumanVoiceDTO.getRoomId()); + danmakuMessage.setUserId(digitalHumanVoiceDTO.getHumanId()); + danmakuMessage.setNickname("智能助手"); + danmakuMessage.setType("1"); + DanmakuPublisher.getInstance().publish(danmakuMessage); + } + +} diff --git a/src/main/java/com/supervision/service/impl/LivetalkingServiceImpl.java b/src/main/java/com/supervision/service/impl/LivetalkingServiceImpl.java new file mode 100644 index 0000000..f102ee9 --- /dev/null +++ b/src/main/java/com/supervision/service/impl/LivetalkingServiceImpl.java @@ -0,0 +1,62 @@ +package com.supervision.service.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.supervision.domain.LivetalkingChatDTO; +import com.supervision.service.LivetalkingService; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LivetalkingServiceImpl implements LivetalkingService { + + @Value("${livetalking.url}") + private String livetalkingUrl; + + @Value("${livetalking.session.id:0}") + private int sessionId; + + @Override + public void chat(LivetalkingChatDTO livetalkingChatDTO) { + String url = livetalkingUrl + "/human"; + log.info("Sending chat request to: " + url); + Map param = new HashMap<>(); + param.put("text", livetalkingChatDTO.getQuery()); + param.put("type", "chat"); + param.put("interrupt",false); + param.put("sessionid", sessionId); + + // 添加额外的参数 + livetalkingChatDTO.setQuery(null); + param.put("extra", livetalkingChatDTO); + HttpRequest request = HttpUtil.createPost(url) + .body(JSONUtil.toJsonStr(param)) + .header("Content-Type", "application/json"); + try (HttpResponse response = request.execute()) { + String body = response.body(); + log.info("Chat response: {}", body); + } + } + + @Override + public void setVoice(String voiceId, String digitalHumanId) { + Assert.notEmpty(voiceId, "语音ID不能为空"); + String url = livetalkingUrl + "/set_voice?voiceId=" + voiceId; + HttpRequest request = HttpUtil.createGet(url); + try (HttpResponse execute = request.execute()){ + String body = execute.body(); + log.info("设置语音结果: {}", body); + } + } +} diff --git a/src/main/java/com/supervision/service/impl/UserDetailsServiceImpl.java b/src/main/java/com/supervision/service/impl/UserDetailsServiceImpl.java index 5d3b71c..346c658 100644 --- a/src/main/java/com/supervision/service/impl/UserDetailsServiceImpl.java +++ b/src/main/java/com/supervision/service/impl/UserDetailsServiceImpl.java @@ -33,6 +33,8 @@ public class UserDetailsServiceImpl implements UserDetailsService { if (sysUser == null) { throw new UsernameNotFoundException("用户不存在: " + username); } - return new UserDetail(sysUser.getId(),sysUser.getUserName(), sysUser.getPassword(), authorities); + UserDetail userDetail = new UserDetail(sysUser.getId(), sysUser.getUserName(), sysUser.getPassword(), authorities); + userDetail.setNickname(sysUser.getNickName()); + return userDetail; } } diff --git a/src/main/java/com/supervision/util/UserUtil.java b/src/main/java/com/supervision/util/UserUtil.java index 6fb0591..484a8e9 100644 --- a/src/main/java/com/supervision/util/UserUtil.java +++ b/src/main/java/com/supervision/util/UserUtil.java @@ -5,6 +5,9 @@ import com.supervision.exception.UnauthorizedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.socket.WebSocketSession; + +import java.security.Principal; public class UserUtil { @@ -20,4 +23,13 @@ public class UserUtil { } return (UserDetail) authentication.getPrincipal(); } + + public static UserDetail currentUser(WebSocketSession session){ + Principal principal = session.getPrincipal(); + if (principal instanceof UserDetail userDetail) { + return userDetail; + } else { + throw new UnauthorizedException("未登录或登录已过期,请重新登录"); + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7ed20fd..799e45f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,10 @@ mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +livetalking: + url: http://192.168.10.96:8010 + session: + id: 0 jwt: secret: "DlHaPUePiN6MyvpMpsMq/t6swzMHqtrRFd2YnofKz4k=" # JWT密钥 使用官方推荐方式生成 Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded()); expiration: 86400000 # 1小时:3600000 1天:86400000 \ No newline at end of file diff --git a/src/main/resources/static/danmaku.js b/src/main/resources/static/danmaku.js new file mode 100644 index 0000000..f9e2bca --- /dev/null +++ b/src/main/resources/static/danmaku.js @@ -0,0 +1,146 @@ +class DanmakuClient { + constructor(container) { + this.container = container; + this.socket = null; + this.roomId = null; + this.tracks = []; + this.initTracks(); + } + + initTracks() { + const height = this.container.clientHeight; + const trackHeight = height / 5; + + for (let i = 0; i < 5; i++) { + this.tracks.push({ + top: i * trackHeight, + inUse: false + }); + } + } + + connect(wsUrl, roomId) { + if (this.socket) this.disconnect(); + + this.roomId = roomId; + this.socket = new WebSocket(`${wsUrl}?roomId=${roomId}`); + + this.socket.onopen = () => { + console.log('WebSocket connected'); + document.getElementById('connect').disabled = true; + document.getElementById('disconnect').disabled = false; + }; + + this.socket.onclose = () => { + console.log('WebSocket disconnected'); + document.getElementById('connect').disabled = false; + document.getElementById('disconnect').disabled = true; + }; + + this.socket.onmessage = (event) => { + const danmaku = JSON.parse(event.data); + this.displayDanmaku(danmaku); + }; + + this.socket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + } + + disconnect() { + if (this.socket) { + this.socket.close(); + this.socket = null; + } + } + + sendDanmaku(content, user, style) { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + alert('请先连接到弹幕服务器'); + return; + } + + if (!content || content.trim() === '') { + alert('弹幕内容不能为空'); + return; + } + + const message = { + roomId: this.roomId, + content, + userId: user || 'anonymous', + color: style?.color || '#ffffff', + size: style?.size || 24 + }; + + this.socket.send(JSON.stringify(message)); + } + + displayDanmaku(danmaku) { + const danmakuElement = document.createElement('div'); + danmakuElement.className = 'danmaku'; + danmakuElement.textContent = danmaku.content; + danmakuElement.style.color = danmaku.color || '#ffffff'; + danmakuElement.style.fontSize = `${danmaku.size || 24}px`; + + const track = this.findAvailableTrack(); + if (!track) return; + + danmakuElement.style.top = `${track.top}px`; + this.container.appendChild(danmakuElement); + + const startX = this.container.clientWidth; + const endX = -danmakuElement.clientWidth; + let startTime = null; + + const animate = (timestamp) => { + if (!startTime) startTime = timestamp; + const progress = (timestamp - startTime) / 10000; + + if (progress < 1) { + const x = startX + (endX - startX) * progress; + danmakuElement.style.transform = `translateX(${x}px)`; + requestAnimationFrame(animate); + } else { + this.container.removeChild(danmakuElement); + track.inUse = false; + } + }; + + track.inUse = true; + requestAnimationFrame(animate); + } + + findAvailableTrack() { + return this.tracks.find(track => !track.inUse); + } +} + +// 初始化客户端 +const container = document.getElementById('danmaku-container'); +const client = new DanmakuClient(container); + +// 绑定事件 +document.getElementById('connect').addEventListener('click', () => { + const roomId = document.getElementById('roomId').value; + client.connect('ws://' + window.location.host + '/ai-platform/ws/danmaku', roomId); +}); + +document.getElementById('disconnect').addEventListener('click', () => { + client.disconnect(); +}); + +document.getElementById('send').addEventListener('click', () => { + const content = document.getElementById('message').value; + const color = document.getElementById('color').value; + const size = document.getElementById('size').value; + + client.sendDanmaku(content, null, { color, size }); + document.getElementById('message').value = ''; +}); + +document.getElementById('message').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + document.getElementById('send').click(); + } +}); \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..27b2e6c --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,51 @@ + + + + + 弹幕系统 - Spring Boot + + + +

弹幕系统

+
+ + + +
+ +
+ +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/src/test/java/com/supervision/PlatformApplicationTest.java b/src/test/java/com/supervision/PlatformApplicationTest.java index 410b146..d0d472d 100644 --- a/src/test/java/com/supervision/PlatformApplicationTest.java +++ b/src/test/java/com/supervision/PlatformApplicationTest.java @@ -1,5 +1,7 @@ package com.supervision; +import com.hankcs.hanlp.seg.common.Term; +import com.hankcs.hanlp.tokenizer.StandardTokenizer; import com.supervision.domain.SysByteArray; import com.supervision.service.SysByteArrayService; import lombok.extern.slf4j.Slf4j; @@ -26,4 +28,32 @@ public class PlatformApplicationTest { SysByteArray sysByteArray = sysByteArrayService.getById("1945676008645058562"); log.info("查询结果: {}", sysByteArray); } + + @Test + void isQuestionTest() { + String sentence = "你是谁"; + + boolean isQuestion = isQuestion(sentence); + System.out.println("是否是问句: " + isQuestion); + } + private boolean isQuestion(String sentence) { + String[] questionWords = {"什么", "为什么", "如何", "哪", "谁", "多少", "是否", "能否", "是不是"}; + String[] questionEndings = {"吗", "呢", "?", "?"}; + + // 结尾判断 + for (String end : questionEndings) { + if (sentence.trim().endsWith(end)) { + return true; + } + } + // 分词判断 + for (Term term : StandardTokenizer.segment(sentence)) { + for (String qw : questionWords) { + if (term.word.equals(qw)) { + return true; + } + } + } + return false; + } }