diff --git a/pom.xml b/pom.xml index 10b03ac..4311ad1 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ io.qdrant client - 1.15.0 + 1.16.1 guava diff --git a/src/main/java/com/yolo/keyborad/common/ErrorCode.java b/src/main/java/com/yolo/keyborad/common/ErrorCode.java index 739b176..fbedb56 100644 --- a/src/main/java/com/yolo/keyborad/common/ErrorCode.java +++ b/src/main/java/com/yolo/keyborad/common/ErrorCode.java @@ -37,7 +37,9 @@ public enum ErrorCode { USER_CHARACTER_DEL_ERROR(50007, "删除用户人设失败"), VERIFY_CODE_ERROR(50008, "验证码错误"), REPEATEDLY_ADDING_CHARACTER(50009, "重复添加键盘人设"), - MAIL_SEND_BUSY(50010,"邮件发送频繁,1分钟后再试" ); + MAIL_SEND_BUSY(50010,"邮件发送频繁,1分钟后再试" ), + PASSWORD_CAN_NOT_NULL(50011, "密码不能为空" ), + USER_HAS_EXISTED(50012, "用户已存在" ); /** * 状态码 */ diff --git a/src/main/java/com/yolo/keyborad/config/LLMConfig.java b/src/main/java/com/yolo/keyborad/config/LLMConfig.java index 70afeab..5d1a721 100644 --- a/src/main/java/com/yolo/keyborad/config/LLMConfig.java +++ b/src/main/java/com/yolo/keyborad/config/LLMConfig.java @@ -54,7 +54,7 @@ public class LLMConfig { MetadataMode.EMBED, OpenAiEmbeddingOptions.builder() .model("qwen/qwen3-embedding-8b") - .dimensions(2048) + .dimensions(1536) .user("user-6") .build(), RetryUtils.DEFAULT_RETRY_TEMPLATE); diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java index 4077171..5a12d72 100644 --- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java +++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java @@ -81,7 +81,10 @@ public class SaTokenConfigure implements WebMvcConfigurer { "/character/listByTagWithNotLogin", "/character/detailWithNotLogin", "/character/addUserCharacter", - "/api/apple/validate-receipt" + "/api/apple/validate-receipt", + "/character/list", + "/user/resetPassWord", + "/chat/talk" }; } @Bean diff --git a/src/main/java/com/yolo/keyborad/controller/ChatController.java b/src/main/java/com/yolo/keyborad/controller/ChatController.java new file mode 100644 index 0000000..11c2094 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/controller/ChatController.java @@ -0,0 +1,104 @@ +package com.yolo.keyborad.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.yolo.keyborad.model.dto.chat.ChatReq; +import com.yolo.keyborad.model.dto.chat.ChatStreamMessage; +import com.yolo.keyborad.model.entity.KeyboardCharacter; +import com.yolo.keyborad.service.KeyboardCharacterService; +import com.yolo.keyborad.service.impl.QdrantVectorService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/* + * @author: ziin + * @date: 2025/12/8 15:05 + */ +@RestController +@RequestMapping("/chat") +@Slf4j +@CrossOrigin +@Tag(name = "聊天", description = "聊天接口") +public class ChatController { + + @Resource + private ChatClient client; + + @Resource + private OpenAiEmbeddingModel embeddingModel; + + @Resource + private QdrantVectorService qdrantVectorService; + + @Resource + private KeyboardCharacterService keyboardCharacterService; + + + @PostMapping("/talk") + @Operation(summary = "聊天润色接口", description = "聊天润色接口") + @Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something") + public Flux> testTalk(@RequestBody ChatReq chatReq){ + + KeyboardCharacter character = keyboardCharacterService.getById(chatReq.getCharacterId()); + + // 1. LLM 流式输出 + Flux llmFlux = client + .prompt(character.getPrompt() + + "\nUser message: %s".formatted(chatReq.getMessage())) + .system(""" + Format rules: + - Return EXACTLY 3 replies. + - Use "" as the separator. + - reply1reply2reply3 + """) + .user(chatReq.getMessage()) + .options(OpenAiChatOptions.builder() + .user(StpUtil.getLoginIdAsString()) + .build()) + .stream() + .content() + .map(chunk -> new ChatStreamMessage("llm_chunk", chunk)); + + // 2. 向量搜索Flux(一次性发送搜索结果) + Flux searchFlux = Mono + .fromCallable(() -> qdrantVectorService.searchText(chatReq.getMessage())) + .subscribeOn(Schedulers.boundedElastic()) // 避免阻塞 event-loop + .map(list -> new ChatStreamMessage("search_result", list)) + .flux(); + // 3. 结束标记 + Flux doneFlux = + Flux.just(new ChatStreamMessage("done", null)); + + // 4. 合并所有Flux + Flux merged = + Flux.merge(llmFlux, searchFlux) + .concatWith(doneFlux); + + // 5. SSE 包装 + return merged.map(msg -> + ServerSentEvent.builder(msg) + .event(msg.getType()) + .build() + ); + } + + + @PostMapping("/save_embed") + @Operation(summary = "保存润色后的句子", description = "保存润色后的句子") + @Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something") + public Flux testTalkWithVector(@RequestBody ChatReq chatReq) { + + return null; + } +} diff --git a/src/main/java/com/yolo/keyborad/controller/DemoController.java b/src/main/java/com/yolo/keyborad/controller/DemoController.java index 605453b..fa55543 100644 --- a/src/main/java/com/yolo/keyborad/controller/DemoController.java +++ b/src/main/java/com/yolo/keyborad/controller/DemoController.java @@ -62,8 +62,6 @@ public class DemoController { @Operation(summary = "测试聊天接口", description = "测试接口") @Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something") public Flux testTalk(@DefaultValue("you are so cute!") String userInput){ - String delimiter = "/t"; - return client .prompt(""" You're a 25-year-old guy—witty and laid-back, always replying in English. @@ -83,7 +81,7 @@ public class DemoController { """) .user(userInput) .options(OpenAiChatOptions.builder() - .user(StpUtil.getLoginIdAsString()) // ✅ 这里每次请求都会重新取当前登录用户 + .user(StpUtil.getLoginIdAsString())// ✅ 这里每次请求都会重新取当前登录用户 .build()) .stream() .content(); @@ -99,12 +97,6 @@ public class DemoController { } - @Operation(summary = "IOS内购凭证校验", description = "IOS内购凭证校验") - public BaseResponse iosPay(@RequestBody IosPayVerifyReq req) { - - return null; - } - @PostMapping("/testSaveEmbed") @Operation(summary = "测试存储向量接口", description = "测试存储向量接口") @Parameter(name = "userInput",required = true,description = "测试存储向量接口") diff --git a/src/main/java/com/yolo/keyborad/model/dto/chat/ChatReq.java b/src/main/java/com/yolo/keyborad/model/dto/chat/ChatReq.java new file mode 100644 index 0000000..e905e77 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/dto/chat/ChatReq.java @@ -0,0 +1,20 @@ +package com.yolo.keyborad.model.dto.chat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/* + * @author: ziin + * @date: 2025/12/8 15:12 + */ +@Data +public class ChatReq { + + @Schema(description="键盘人设 id") + private Long characterId; + + @Schema(description="用户输入") + private String message; + + +} diff --git a/src/main/java/com/yolo/keyborad/model/dto/chat/ChatStreamMessage.java b/src/main/java/com/yolo/keyborad/model/dto/chat/ChatStreamMessage.java new file mode 100644 index 0000000..6aa5557 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/dto/chat/ChatStreamMessage.java @@ -0,0 +1,13 @@ +package com.yolo.keyborad.model.dto.chat; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatStreamMessage { + private String type; // "llm_chunk" / "search_result" / "done" + private Object data; +} diff --git a/src/main/java/com/yolo/keyborad/service/ChatService.java b/src/main/java/com/yolo/keyborad/service/ChatService.java new file mode 100644 index 0000000..8c589ca --- /dev/null +++ b/src/main/java/com/yolo/keyborad/service/ChatService.java @@ -0,0 +1,8 @@ +package com.yolo.keyborad.service; + +/* + * @author: ziin + * @date: 2025/12/8 15:16 + */ +public interface ChatService { +} diff --git a/src/main/java/com/yolo/keyborad/service/impl/ChatServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/ChatServiceImpl.java new file mode 100644 index 0000000..a4b64f1 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/service/impl/ChatServiceImpl.java @@ -0,0 +1,12 @@ +package com.yolo.keyborad.service.impl; + +import com.yolo.keyborad.service.ChatService; +import org.springframework.stereotype.Service; + +/* + * @author: ziin + * @date: 2025/12/8 15:17 + */ +@Service +public class ChatServiceImpl implements ChatService { +} diff --git a/src/main/java/com/yolo/keyborad/service/impl/KeyboardCharacterServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/KeyboardCharacterServiceImpl.java index e6b5fd0..63dc152 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/KeyboardCharacterServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/KeyboardCharacterServiceImpl.java @@ -106,9 +106,10 @@ public class KeyboardCharacterServiceImpl extends ServiceImpl() + KeyboardUserCharacter alreadyExistsCharacter = keyboardUserCharacterMapper.selectOne( + new LambdaQueryWrapper() .eq(KeyboardUserCharacter::getCharacterId, addDTO.getCharacterId()) - .eq(KeyboardUserCharacter::getDeleted, false)); + .eq(KeyboardUserCharacter::getUserId, userId)); if (alreadyExistsCharacter != null){ throw new BusinessException(ErrorCode.REPEATEDLY_ADDING_CHARACTER); } diff --git a/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java b/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java index d8e9984..1bc85c5 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java +++ b/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java @@ -31,7 +31,7 @@ public class QdrantVectorService { @Resource private QdrantClient qdrantClient; - private static final String COLLECTION_NAME = "test_document"; + private static final String COLLECTION_NAME = "chat_resources"; @Resource private EmbeddingModel embeddingModel; diff --git a/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java index 6307cc8..f2e536c 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java @@ -105,9 +105,20 @@ public class UserServiceImpl extends ServiceImpl() + .eq(KeyboardUser::getEmail, userRegisterDTO.getMailAddress())); + + if (userMail != null) { + throw new BusinessException(ErrorCode.USER_HAS_EXISTED); + } + + if (userRegisterDTO.getPassword() == null) { + throw new BusinessException(ErrorCode.PASSWORD_CAN_NOT_NULL); + } if (!userRegisterDTO.getPassword().equals(userRegisterDTO.getPasswordConfirm())) { throw new BusinessException(ErrorCode.CONFIRM_PASSWORD_NOT_MATCH); } + KeyboardUser keyboardUser = new KeyboardUser(); keyboardUser.setUid(IdUtil.getSnowflake().nextId()); keyboardUser.setNickName("User_" + RandomUtil.randomString(6)); @@ -115,11 +126,12 @@ public class UserServiceImpl extends ServiceImpl 0; } @@ -171,7 +183,7 @@ public class UserServiceImpl extends ServiceImpl