diff --git a/docs/忘记密码和重置密码.md b/docs/忘记密码和重置密码.md
new file mode 100644
index 0000000..088b3d6
--- /dev/null
+++ b/docs/忘记密码和重置密码.md
@@ -0,0 +1,23 @@
+"忘记密码"(Forgot Password)和"重置密码"(Reset Password)是密码恢复流程的两个不同步骤:
+
+1. 忘记密码(forgotPassword):
+
+- 流程的第一步
+
+- 当用户表示忘记密码时触发
+
+- 系统向用户发送带有重置令牌的邮件
+
+- 不直接更改密码
+
+1. 重置密码(resetPassword):
+
+- 流程的第二步
+
+- 用户点击邮件中的链接或输入令牌后使用
+
+- 验证令牌并允许用户设置新密码
+
+- 实际修改用户密码的步骤
+
+简单来说,忘记密码是发起恢复流程,重置密码是完成密码更改。
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 8065556..5db35c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,9 @@
lombok
provided
+
+
+
diff --git a/src/main/java/com/yundage/chat/common/ApiResponse.java b/src/main/java/com/yundage/chat/common/ApiResponse.java
index 64cdbaa..41fbc22 100644
--- a/src/main/java/com/yundage/chat/common/ApiResponse.java
+++ b/src/main/java/com/yundage/chat/common/ApiResponse.java
@@ -64,6 +64,10 @@ public class ApiResponse {
return new ApiResponse<>(ErrorCode.NOT_FOUND, message);
}
+ public static ApiResponse conflict(String message) {
+ return new ApiResponse<>(ErrorCode.CONFLICT, message);
+ }
+
// Getters and Setters
public int getCode() {
return code;
diff --git a/src/main/java/com/yundage/chat/common/ErrorCode.java b/src/main/java/com/yundage/chat/common/ErrorCode.java
index 3d48440..3bcf618 100644
--- a/src/main/java/com/yundage/chat/common/ErrorCode.java
+++ b/src/main/java/com/yundage/chat/common/ErrorCode.java
@@ -1,14 +1,19 @@
package com.yundage.chat.common;
public class ErrorCode {
-
- // 通用错误码
+ // 成功
public static final int SUCCESS = 200;
+
+ // 客户端错误
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
+ public static final int CONFLICT = 409;
+
+ // 服务端错误
public static final int INTERNAL_ERROR = 500;
+ public static final int SERVICE_UNAVAILABLE = 503;
// 认证相关错误码 (1000-1999)
public static final int VERIFICATION_CODE_INVALID = 1001;
diff --git a/src/main/java/com/yundage/chat/config/OpenApiConfig.java b/src/main/java/com/yundage/chat/config/OpenApiConfig.java
index 39853b3..927f768 100644
--- a/src/main/java/com/yundage/chat/config/OpenApiConfig.java
+++ b/src/main/java/com/yundage/chat/config/OpenApiConfig.java
@@ -17,8 +17,8 @@ public class OpenApiConfig {
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
- .title("云搭公服对话API")
- .description("云搭公服对话系统API文档")
+ .title("云大所智能问答")
+ .description("云大所智能问答系统API文档")
.version("v1.0.0")
.contact(new Contact()
.name("Yundage")
diff --git a/src/main/java/com/yundage/chat/controller/AuthController.java b/src/main/java/com/yundage/chat/controller/AuthController.java
index 4da0fb6..a31fd48 100644
--- a/src/main/java/com/yundage/chat/controller/AuthController.java
+++ b/src/main/java/com/yundage/chat/controller/AuthController.java
@@ -93,6 +93,21 @@ public class AuthController {
}
}
+ @PostMapping("/logout")
+ @Operation(summary = "用户登出", description = "退出用户登录状态")
+ @ApiResponses(value = {
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "登出成功"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "登出失败")
+ })
+ public ApiResponse> logout() {
+ try {
+ userService.logout();
+ return ApiResponse.success("登出成功");
+ } catch (Exception e) {
+ return ApiResponse.error(ErrorCode.INTERNAL_ERROR, "登出失败: " + e.getMessage());
+ }
+ }
+
@PostMapping("/forgot-password")
@Operation(summary = "忘记密码", description = "发送密码重置邮件")
@ApiResponses(value = {
diff --git a/src/main/java/com/yundage/chat/controller/ChatController.java b/src/main/java/com/yundage/chat/controller/ChatController.java
index 36d7713..4da8bd3 100644
--- a/src/main/java/com/yundage/chat/controller/ChatController.java
+++ b/src/main/java/com/yundage/chat/controller/ChatController.java
@@ -87,4 +87,24 @@ public class ChatController {
return ResponseEntity.ok(ApiResponse.error("获取消息历史失败:" + e.getMessage()));
}
}
+
+ @DeleteMapping("/conversations/{conversationId}")
+ @Operation(summary = "删除会话", description = "删除指定ID的会话")
+ public ResponseEntity> deleteConversation(
+ @PathVariable Long conversationId,
+ org.springframework.security.core.Authentication authentication) {
+
+ com.yundage.chat.entity.User user = (com.yundage.chat.entity.User) authentication.getPrincipal();
+
+ try {
+ boolean result = chatService.deleteConversation(conversationId, user.getId());
+ if (result) {
+ return ResponseEntity.ok(ApiResponse.success("会话删除成功"));
+ } else {
+ return ResponseEntity.ok(ApiResponse.error("会话不存在或无权限删除"));
+ }
+ } catch (Exception e) {
+ return ResponseEntity.ok(ApiResponse.error("删除会话失败:" + e.getMessage()));
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/controller/UserController.java b/src/main/java/com/yundage/chat/controller/UserController.java
index bce748e..44b2299 100644
--- a/src/main/java/com/yundage/chat/controller/UserController.java
+++ b/src/main/java/com/yundage/chat/controller/UserController.java
@@ -1,15 +1,21 @@
package com.yundage.chat.controller;
+import com.yundage.chat.common.ApiResponse;
+import com.yundage.chat.dto.UserDTO;
+import com.yundage.chat.dto.UserProfileUpdateRequest;
import com.yundage.chat.entity.User;
import com.yundage.chat.mapper.UserMapper;
+import com.yundage.chat.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.util.List;
@@ -23,57 +29,120 @@ public class UserController {
@Autowired
private UserMapper userMapper;
+ @Autowired
+ private UserService userService;
+
@GetMapping
+ @PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
- @ApiResponse(responseCode = "200", description = "成功获取用户列表")
- public List getAllUsers() {
- return userMapper.selectAll();
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "成功获取用户列表")
+ public ApiResponse> getAllUsers() {
+ return ApiResponse.success(userMapper.selectAll());
+ }
+
+ @GetMapping("/me")
+ @Operation(summary = "获取当前用户信息", description = "根据当前用户的token获取用户信息")
+ @ApiResponses(value = {
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "成功获取用户信息"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "未授权")
+ })
+ public ApiResponse getCurrentUser() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ String username = authentication.getName();
+ User user = userMapper.selectByEmailOrPhone(username);
+ return ApiResponse.success(UserDTO.fromUser(user));
}
@GetMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户信息")
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "成功获取用户信息"),
- @ApiResponse(responseCode = "404", description = "用户不存在")
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "成功获取用户信息"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在")
})
- public User getUserById(@Parameter(description = "用户ID") @PathVariable Long id) {
- return userMapper.selectOneById(id);
+ public ApiResponse getUserById(@Parameter(description = "用户ID") @PathVariable Long id) {
+ User user = userMapper.selectOneById(id);
+ if (user != null) {
+ return ApiResponse.success(user);
+ } else {
+ return ApiResponse.notFound("用户不存在");
+ }
}
@PostMapping
+ @PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "创建用户", description = "创建新的用户")
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "用户创建成功"),
- @ApiResponse(responseCode = "400", description = "请求参数错误")
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "用户创建成功"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数错误")
})
- public User createUser(@RequestBody User user) {
+ public ApiResponse createUser(@RequestBody User user) {
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
userMapper.insert(user);
- return user;
+ return ApiResponse.success(user);
}
@PutMapping("/{id}")
- @Operation(summary = "更新用户", description = "更新指定用户的信息")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "更新用户", description = "管理员更新指定用户的信息")
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "用户更新成功"),
- @ApiResponse(responseCode = "404", description = "用户不存在"),
- @ApiResponse(responseCode = "400", description = "请求参数错误")
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "用户更新成功"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数错误")
})
- public User updateUser(@Parameter(description = "用户ID") @PathVariable Long id, @RequestBody User user) {
+ public ApiResponse updateUser(@Parameter(description = "用户ID") @PathVariable Long id, @RequestBody User user) {
+ User existingUser = userMapper.selectOneById(id);
+ if (existingUser == null) {
+ return ApiResponse.notFound("用户不存在");
+ }
user.setId(id);
user.setUpdatedAt(LocalDateTime.now());
userMapper.update(user);
- return user;
+ return ApiResponse.success(user);
+ }
+
+ @PutMapping("/profile")
+ @Operation(summary = "更新个人资料", description = "普通用户更新自己的个人资料")
+ @ApiResponses(value = {
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "个人资料更新成功"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数错误"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "未授权"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "邮箱或手机号已被占用")
+ })
+ public ApiResponse updateProfile(@RequestBody UserProfileUpdateRequest request) {
+ try {
+ // 获取当前用户
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ User currentUser = (User) authentication.getPrincipal();
+
+ // 调用服务更新用户资料
+ UserDTO updatedUser = userService.updateCurrentUserProfile(request, currentUser.getId());
+
+ return ApiResponse.success(updatedUser);
+ } catch (RuntimeException e) {
+ // 处理可能的错误情况
+ if (e.getMessage().contains("已被其他用户使用")) {
+ return ApiResponse.conflict(e.getMessage());
+ } else {
+ return ApiResponse.badRequest(e.getMessage());
+ }
+ }
}
@DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "删除用户", description = "根据ID删除用户")
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "用户删除成功"),
- @ApiResponse(responseCode = "404", description = "用户不存在")
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "用户删除成功"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在")
})
- public void deleteUser(@Parameter(description = "用户ID") @PathVariable Long id) {
+ public ApiResponse deleteUser(@Parameter(description = "用户ID") @PathVariable Long id) {
+ User existingUser = userMapper.selectOneById(id);
+ if (existingUser == null) {
+ return ApiResponse.notFound("用户不存在");
+ }
userMapper.deleteById(id);
+ return ApiResponse.success();
}
}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/dto/UserDTO.java b/src/main/java/com/yundage/chat/dto/UserDTO.java
new file mode 100644
index 0000000..7bc20e2
--- /dev/null
+++ b/src/main/java/com/yundage/chat/dto/UserDTO.java
@@ -0,0 +1,58 @@
+package com.yundage.chat.dto;
+
+import com.yundage.chat.entity.User;
+import com.yundage.chat.enums.UserType;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "用户数据传输对象")
+public class UserDTO {
+
+ @Schema(description = "用户ID")
+ private Long id;
+
+ @Schema(description = "用户名")
+ private String username;
+
+ @Schema(description = "邮箱")
+ private String email;
+
+ @Schema(description = "手机号")
+ private String phone;
+
+ @Schema(description = "用户类型")
+ private UserType userType;
+
+ @Schema(description = "头像URL")
+ private String avatarUrl;
+
+ @Schema(description = "最后登录时间")
+ private String lastLoginAt;
+
+ /**
+ * 将User实体转换为UserDTO
+ *
+ * @param user User实体
+ * @return UserDTO对象
+ */
+ public static UserDTO fromUser(User user) {
+ if (user == null) {
+ return null;
+ }
+
+ UserDTO dto = new UserDTO();
+ dto.setId(user.getId());
+ dto.setUsername(user.getUsername());
+ dto.setEmail(user.getEmail());
+ dto.setPhone(user.getPhone());
+ dto.setUserType(user.getUserType());
+ dto.setAvatarUrl(user.getAvatarUrl());
+
+ if (user.getLastLoginAt() != null) {
+ dto.setLastLoginAt(user.getLastLoginAt().toString());
+ }
+
+ return dto;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/dto/UserProfileUpdateRequest.java b/src/main/java/com/yundage/chat/dto/UserProfileUpdateRequest.java
new file mode 100644
index 0000000..3f2cf2d
--- /dev/null
+++ b/src/main/java/com/yundage/chat/dto/UserProfileUpdateRequest.java
@@ -0,0 +1,24 @@
+package com.yundage.chat.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 用户个人资料更新请求DTO
+ */
+@Data
+@Schema(description = "用户个人资料更新请求")
+public class UserProfileUpdateRequest {
+
+ @Schema(description = "用户名")
+ private String username;
+
+ @Schema(description = "头像URL")
+ private String avatarUrl;
+
+ @Schema(description = "手机号")
+ private String phone;
+
+ @Schema(description = "邮箱")
+ private String email;
+}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/mapper/UserMapper.java b/src/main/java/com/yundage/chat/mapper/UserMapper.java
index 5df3a0f..3e0cfb6 100644
--- a/src/main/java/com/yundage/chat/mapper/UserMapper.java
+++ b/src/main/java/com/yundage/chat/mapper/UserMapper.java
@@ -2,8 +2,16 @@ package com.yundage.chat.mapper;
import com.yundage.chat.entity.User;
import com.mybatisflex.core.BaseMapper;
+import com.mybatisflex.core.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper extends BaseMapper {
+
+ default User selectByEmailOrPhone(String username) {
+ QueryWrapper queryWrapper = new QueryWrapper();
+ queryWrapper.where("email = ? OR phone = ?", username, username).limit(1);
+ return selectOneByQuery(queryWrapper);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/ChatService.java b/src/main/java/com/yundage/chat/service/ChatService.java
index 22f7f5d..8c189cb 100644
--- a/src/main/java/com/yundage/chat/service/ChatService.java
+++ b/src/main/java/com/yundage/chat/service/ChatService.java
@@ -1,277 +1,55 @@
package com.yundage.chat.service;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.yundage.chat.dto.ChatRequest;
import com.yundage.chat.dto.ChatResponse;
-import com.yundage.chat.dto.StreamResponse;
import com.yundage.chat.entity.Conversation;
import com.yundage.chat.entity.Message;
import com.yundage.chat.entity.User;
-import com.yundage.chat.mapper.ConversationMapper;
-import com.yundage.chat.mapper.MessageMapper;
-import lombok.RequiredArgsConstructor;
-import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import org.springframework.security.core.context.SecurityContext;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
-import reactor.core.publisher.Flux;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-@RequiredArgsConstructor
-@Service
-public class ChatService {
- private ConversationMapper conversationMapper;
-
-
- private MessageMapper messageMapper;
-
-
- /** ChatClient 由 Spring AI Starter 自动装配;可注入 Builder 再 build(),也可直接注入 ChatClient */
- private final ChatClient chatClient;
-
- private final ObjectMapper mapper = new ObjectMapper();
- private final ExecutorService executor = Executors.newCachedThreadPool();
- // 👇 手写构造器完成所有 final 字段的注入
- @Autowired
- public ChatService(ChatClient.Builder builder,
- ConversationMapper conversationMapper,
- MessageMapper messageMapper) {
- this.chatClient = builder.build();;
- this.conversationMapper = conversationMapper;
- this.messageMapper = messageMapper;
- }
- @Transactional
- public SseEmitter processChatStream(ChatRequest request, User user) {
- // 0 表示不超时;如需限制自行调整
- SseEmitter emitter = new SseEmitter(0L);
-
- try {
- long start = System.currentTimeMillis();
-
- // 1️⃣ 取得会话并保存用户消息
- Conversation convo = getOrCreateConversation(request, user);
- saveUserMessage(convo.getId(), request.getMessage());
-
- // 2️⃣ 在独立线程里完成流处理,避免阻塞 servlet 线程
- SecurityContext ctx = SecurityContextHolder.getContext();
- executor.submit(new DelegatingSecurityContextRunnable(() -> {
-
- StringBuilder full = new StringBuilder();
-
- // 3️⃣ 使用 Spring AI 流式接口;`content()` 把 Flux 映射成 Flux
- Flux flux = chatClient
- .prompt() // fluent API
- .user(request.getMessage()) // 用户消息
- .stream() // 进入流模式 [oai_citation:0‡Home](https://docs.spring.io/spring-ai/reference/api/chatclient.html)
- .content(); // 只取内容字符串 [oai_citation:1‡danvega.dev](https://www.danvega.dev/blog/spring-ai-streaming-chatbot)
-
- flux.doOnNext(chunk -> { // 每个 token/块回调
- try {
- full.append(chunk); // 累积完整内容
- StreamResponse resp = StreamResponse.content(chunk);
- emitter.send(SseEmitter.event()
- .name("message")
- .data(mapper.writeValueAsString(resp)));
- } catch (Exception io) {
- emitter.completeWithError(io);
- }
- })
- .doOnComplete(() -> { // 4️⃣ 模型输出结束
- try {
- Message assistantMsg = saveAssistantMessage(convo.getId(), full.toString());
- convo.setUpdatedAt(LocalDateTime.now());
- conversationMapper.update(convo);
-
- List refs =
- "research".equals(request.getMode()) ? generateMockReferences() : List.of();
-
- StreamResponse finish = StreamResponse.finished(
- convo.getId().toString(),
- assistantMsg.getId().toString(),
- refs,
- null,
- estimateTokens(request.getMessage(), full.toString()),
- System.currentTimeMillis() - start
- );
- emitter.send(SseEmitter.event()
- .name("finished")
- .data(mapper.writeValueAsString(finish)));
- emitter.complete();
- } catch (Exception e) {
- emitter.completeWithError(e);
- }
- })
- .doOnError(err -> { // 5️⃣ 流异常
- try {
- StreamResponse error = StreamResponse.error("AI 处理失败:" + err.getMessage());
- emitter.send(SseEmitter.event()
- .name("error")
- .data(mapper.writeValueAsString(error)));
- } catch (Exception ignore) {}
- emitter.completeWithError(err);
- })
- .subscribe(); // 最终触发流
- }, ctx));
- } catch (Exception ex) {
- emitter.completeWithError(ex);
- }
- return emitter;
- }
-
- @Transactional
- public ChatResponse processChat(ChatRequest request, User user) {
- long startTime = System.currentTimeMillis();
-
- // 获取或创建会话
- Conversation conversation = getOrCreateConversation(request, user);
-
- // 保存用户消息
- Message userMessage = saveUserMessage(conversation.getId(), request.getMessage());
-
- // 模拟AI回复生成 - 这里可以接入实际的AI服务
- String aiResponse = generateAIResponse(request);
-
- // 保存AI回复消息
- Message assistantMessage = saveAssistantMessage(conversation.getId(), aiResponse);
-
- // 更新会话的更新时间
- conversation.setUpdatedAt(LocalDateTime.now());
- conversationMapper.update(conversation);
-
- // 构建响应
- ChatResponse response = new ChatResponse();
- response.setConversationId(conversation.getId().toString());
- response.setMessageId(assistantMessage.getId().toString());
- response.setAnswer(aiResponse);
- response.setTimeMs(System.currentTimeMillis() - startTime);
-
- // 根据模式设置不同的响应内容
- if ("research".equals(request.getMode())) {
- // 研究模式可能包含引用资料
- response.setReferences(generateMockReferences());
- response.setReportId(null); // 可以在后续版本中实现报告生成功能
- } else {
- // 普通聊天模式
- response.setReferences(new ArrayList<>());
- response.setReportId(null);
- }
-
- // 模拟token统计
- response.setTokensUsed(estimateTokens(request.getMessage(), aiResponse));
-
- return response;
- }
-
- private String[] generateStreamingAIResponse(ChatRequest request) {
- // 目前先用单段文本模拟,后续可改为 chatClient.stream() 真正流式
- String aiReply = "这是AI的回复内容,模拟流式输出。";
- return new String[]{aiReply};
- }
-
- private Conversation getOrCreateConversation(ChatRequest request, User user) {
- if (request.getConversationId() != null) {
- // 尝试获取现有会话
- try {
- Long conversationId = Long.parseLong(request.getConversationId());
- Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, user.getId());
- if (conversation != null) {
- return conversation;
- }
- } catch (NumberFormatException e) {
- // 忽略无效的会话ID,创建新会话
- }
- }
-
- // 创建新会话
- Conversation conversation = new Conversation();
- conversation.setUserId(user.getId());
- conversation.setChatMode(request.getMode());
- conversation.setTitle(generateConversationTitle(request.getMessage()));
- conversation.setCreatedAt(LocalDateTime.now());
- conversation.setUpdatedAt(LocalDateTime.now());
-
- conversationMapper.insert(conversation);
- return conversation;
- }
-
- private Message saveUserMessage(Long conversationId, String content) {
- Integer nextSequenceNo = messageMapper.getMaxSequenceNo(conversationId) + 1;
-
- Message message = new Message();
- message.setConversationId(conversationId);
- message.setSequenceNo(nextSequenceNo);
- message.setRole("user");
- message.setContent(content);
- message.setCreatedAt(LocalDateTime.now());
-
- messageMapper.insert(message);
- return message;
- }
-
- private Message saveAssistantMessage(Long conversationId, String content) {
- Integer nextSequenceNo = messageMapper.getMaxSequenceNo(conversationId) + 1;
-
- Message message = new Message();
- message.setConversationId(conversationId);
- message.setSequenceNo(nextSequenceNo);
- message.setRole("assistant");
- message.setContent(content);
- message.setCreatedAt(LocalDateTime.now());
-
- messageMapper.insert(message);
- return message;
- }
-
- private String generateAIResponse(ChatRequest request) {
- // 使用 Spring AI ChatClient 生成回复
-
- return "这是AI的回复内容,模拟生成。";
- }
-
- private String generateConversationTitle(String message) {
- // 简单的标题生成逻辑,取前20个字符
- if (message.length() <= 20) {
- return message;
- }
- return message.substring(0, 20) + "...";
- }
-
- private List generateMockReferences() {
- // 模拟的引用资料,实际项目中从真实的知识库获取
- List references = new ArrayList<>();
- references.add(new ChatResponse.ReferenceItem(
- "相关研究文档",
- "https://example.com/research",
- "这是一段引用的文本内容..."
- ));
- return references;
- }
-
- private Integer estimateTokens(String userMessage, String aiResponse) {
- // 简单的token估算,实际项目中需要使用真实的tokenizer
- return (userMessage.length() + aiResponse.length()) / 4;
- }
-
- public List getUserConversations(Long userId) {
- return conversationMapper.findActiveByUserId(userId);
- }
-
- public List getConversationMessages(Long conversationId, Long userId) {
- // 验证会话是否属于用户
- Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, userId);
- if (conversation == null) {
- throw new RuntimeException("会话不存在或无权限访问");
- }
-
- return messageMapper.findByConversationId(conversationId);
- }
+/**
+ * 聊天服务接口
+ */
+public interface ChatService {
+
+ /**
+ * 处理流式聊天请求
+ * @param request 聊天请求
+ * @param user 当前用户
+ * @return SSE发射器
+ */
+ SseEmitter processChatStream(ChatRequest request, User user);
+
+ /**
+ * 处理普通聊天请求
+ * @param request 聊天请求
+ * @param user 当前用户
+ * @return 聊天响应
+ */
+ ChatResponse processChat(ChatRequest request, User user);
+
+ /**
+ * 获取用户的会话列表
+ * @param userId 用户ID
+ * @return 会话列表
+ */
+ List getUserConversations(Long userId);
+
+ /**
+ * 获取会话消息历史
+ * @param conversationId 会话ID
+ * @param userId 用户ID
+ * @return 消息列表
+ */
+ List getConversationMessages(Long conversationId, Long userId);
+
+ /**
+ * 删除用户会话
+ * @param conversationId 会话ID
+ * @param userId 用户ID
+ * @return 是否删除成功
+ */
+ boolean deleteConversation(Long conversationId, Long userId);
}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/EmailService.java b/src/main/java/com/yundage/chat/service/EmailService.java
index 449a1a4..ddc3544 100644
--- a/src/main/java/com/yundage/chat/service/EmailService.java
+++ b/src/main/java/com/yundage/chat/service/EmailService.java
@@ -1,46 +1,15 @@
package com.yundage.chat.service;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.mail.SimpleMailMessage;
-import org.springframework.mail.javamail.JavaMailSender;
-import org.springframework.stereotype.Service;
-
-@Service
-public class EmailService {
+/**
+ * 邮件服务接口
+ */
+public interface EmailService {
- @Autowired
- private JavaMailSender mailSender;
-
- @Value("${spring.mail.username}")
- private String fromEmail;
-
- @Value("${app.reset-password-url:http://localhost:3000/reset-password}")
- private String resetPasswordUrl;
-
- public void sendPasswordResetEmail(String toEmail, String username, String token) {
- try {
- SimpleMailMessage message = new SimpleMailMessage();
- message.setFrom(fromEmail);
- message.setTo(toEmail);
- message.setSubject("密码重置 - 云大AI聊天");
-
- String resetLink = resetPasswordUrl + "?token=" + token;
- String emailBody = String.format(
- "亲爱的 %s,\n\n" +
- "我们收到了您的密码重置请求。请点击以下链接重置您的密码:\n\n" +
- "%s\n\n" +
- "此链接将在1小时后过期。如果您没有请求重置密码,请忽略此邮件。\n\n" +
- "祝好,\n" +
- "云大AI聊天团队",
- username, resetLink
- );
-
- message.setText(emailBody);
- mailSender.send(message);
-
- } catch (Exception e) {
- throw new RuntimeException("发送邮件失败: " + e.getMessage());
- }
- }
+ /**
+ * 发送密码重置邮件
+ * @param toEmail 接收邮件的邮箱
+ * @param username 用户名
+ * @param token 重置密码令牌
+ */
+ void sendPasswordResetEmail(String toEmail, String username, String token);
}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/LLMService.java b/src/main/java/com/yundage/chat/service/LLMService.java
new file mode 100644
index 0000000..61a3081
--- /dev/null
+++ b/src/main/java/com/yundage/chat/service/LLMService.java
@@ -0,0 +1,32 @@
+package com.yundage.chat.service;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * 大语言模型服务接口
+ * 定义与大语言模型交互的通用方法,方便替换不同的模型实现
+ */
+public interface LLMService {
+ /**
+ * 处理流式文本生成请求
+ * @param userMessage 用户消息
+ * @param parameters 额外的参数,如温度、最大长度等
+ * @param callback 接收每个文本块的回调函数
+ * @param onComplete 完成时的回调函数
+ * @param onError 错误时的回调函数
+ */
+ void streamGenerateText(String userMessage,
+ Map parameters,
+ Consumer callback,
+ Runnable onComplete,
+ Consumer onError);
+
+ /**
+ * 处理非流式文本生成请求
+ * @param userMessage 用户消息
+ * @param parameters 额外的参数,如温度、最大长度等
+ * @return 生成的文本
+ */
+ String generateText(String userMessage, Map parameters);
+}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/UserService.java b/src/main/java/com/yundage/chat/service/UserService.java
index 8c05143..d8e02e3 100644
--- a/src/main/java/com/yundage/chat/service/UserService.java
+++ b/src/main/java/com/yundage/chat/service/UserService.java
@@ -2,291 +2,90 @@ package com.yundage.chat.service;
import com.yundage.chat.dto.*;
import com.yundage.chat.entity.User;
-import com.yundage.chat.entity.PasswordResetToken;
-import com.yundage.chat.mapper.UserMapper;
-import com.yundage.chat.mapper.PasswordResetTokenMapper;
-import com.yundage.chat.util.JwtUtil;
-import com.mybatisflex.core.query.QueryWrapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import java.time.LocalDateTime;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-
-@Service
-public class UserService {
-
- @Autowired
- private UserMapper userMapper;
-
- @Autowired
- private PasswordResetTokenMapper tokenMapper;
-
- @Autowired
- private PasswordEncoder passwordEncoder;
-
- @Autowired
- private JwtUtil jwtUtil;
-
- @Autowired
- private AuthenticationManager authenticationManager;
-
- @Autowired
- private EmailService emailService;
-
- // 从配置文件中获取验证码配置
- @Value("${app.verification-code.master-code:123456}")
- private String masterCode;
-
- @Value("${app.verification-code.expiration:300}")
- private int codeExpirationSeconds;
-
- @Value("${app.verification-code.development-mode:false}")
- private boolean developmentMode;
-
- // 验证码存储,实际项目中应该使用Redis
- private final Map verificationCodes = new ConcurrentHashMap<>();
+/**
+ * 用户服务接口
+ */
+public interface UserService {
/**
* 发送验证码
* @param contact 联系方式(邮箱或手机号)
* @return 验证码(仅开发模式返回)
*/
- public String sendVerificationCode(String contact) {
- // 生成6位随机验证码
- String code = generateRandomCode();
-
- // 存储验证码
- verificationCodes.put(contact, new VerificationCodeInfo(code, LocalDateTime.now()));
-
- // TODO: 实际项目中,这里应该通过短信或邮件发送验证码
- // 如果是邮箱,可以使用邮件服务发送
- if (contact.contains("@")) {
- // emailService.sendVerificationCode(contact, code);
- } else {
- // 使用短信服务发送
- // smsService.sendVerificationCode(contact, code);
- }
-
- // 在开发模式下,在控制台打印验证码
- if (developmentMode) {
- System.out.println("向 " + contact + " 发送验证码: " + code);
- }
-
- return developmentMode ? code : null; // 仅在开发模式下返回验证码
- }
+ String sendVerificationCode(String contact);
/**
- * 验证验证码
- * @param contact 联系方式
- * @param code 验证码
- * @return 是否验证成功
+ * 注册用户
+ * @param request 注册请求
+ * @return 认证响应
*/
- private boolean verifyCode(String contact, String code) {
- // 检查万能验证码
- if (masterCode.equals(code)) {
- return true;
- }
-
- // 检查验证码是否存在
- VerificationCodeInfo storedInfo = verificationCodes.get(contact);
- if (storedInfo == null) {
- return false;
- }
-
- // 检查验证码是否过期
- LocalDateTime expirationTime = storedInfo.timestamp.plusSeconds(codeExpirationSeconds);
- if (LocalDateTime.now().isAfter(expirationTime)) {
- verificationCodes.remove(contact); // 移除过期验证码
- return false;
- }
-
- // 检查验证码是否匹配
- boolean isValid = storedInfo.code.equals(code);
- if (isValid) {
- verificationCodes.remove(contact); // 验证成功后移除验证码,防止重复使用
- }
-
- return isValid;
- }
-
- @Transactional
- public AuthResponse register(RegisterRequest request) {
- // 验证验证码
- if (!verifyCode(request.getEmail() != null ? request.getEmail() : request.getPhone(), request.getCode())) {
- throw new RuntimeException("验证码无效或已过期");
- }
-
- // 检查用户是否已存在
- if (request.getEmail() != null && existsByEmail(request.getEmail())) {
- throw new RuntimeException("邮箱已被注册");
- }
-
- if (request.getPhone() != null && existsByPhone(request.getPhone())) {
- throw new RuntimeException("手机号已被注册");
- }
-
- // 创建新用户
- User user = new User();
- user.setUsername(request.getUsername());
- user.setEmail(request.getEmail());
- user.setPhone(request.getPhone());
- // 设置一个随机密码,因为不再使用密码登录
- user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString()));
- user.setCreatedAt(LocalDateTime.now());
- user.setUpdatedAt(LocalDateTime.now());
-
- userMapper.insert(user);
-
- // 生成JWT令牌
- String token = jwtUtil.generateToken(user.getDisplayName(), user.getId());
-
- return new AuthResponse(token, user.getId(), user.getDisplayName(),
- user.getEmail(), user.getPhone());
- }
-
- public AuthResponse login(LoginRequest request) {
- // 验证验证码
- if (!verifyCode(request.getContact(), request.getCode())) {
- throw new RuntimeException("验证码无效或已过期");
- }
-
- // 根据联系方式查找用户
- User user = findByContact(request.getContact());
- if (user == null) {
- throw new RuntimeException("用户不存在");
- }
-
- // 手动设置认证信息
- Authentication authentication = createAuthenticationToken(user);
- SecurityContextHolder.getContext().setAuthentication(authentication);
-
- // 更新最后登录时间
- user.setLastLoginAt(LocalDateTime.now());
- userMapper.update(user);
-
- // 生成JWT令牌
- String token = jwtUtil.generateToken(user.getDisplayName(), user.getId());
-
- return new AuthResponse(token, user.getId(), user.getDisplayName(),
- user.getEmail(), user.getPhone());
- }
+ AuthResponse register(RegisterRequest request);
/**
- * 创建认证令牌
+ * 用户登录
+ * @param request 登录请求
+ * @return 认证响应
*/
- private Authentication createAuthenticationToken(User user) {
- List authorities = new ArrayList<>();
- authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
- return new UsernamePasswordAuthenticationToken(user, null, authorities);
- }
+ AuthResponse login(LoginRequest request);
- @Transactional
- public void requestPasswordReset(PasswordResetRequest request) {
- User user = findByEmail(request.getEmail());
- if (user == null) {
- throw new RuntimeException("用户不存在");
- }
-
- // 生成重置令牌
- String token = UUID.randomUUID().toString();
- LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); // 1小时后过期
-
- PasswordResetToken resetToken = new PasswordResetToken(user.getId(), token, expiresAt);
- tokenMapper.insert(resetToken);
-
- // 发送重置邮件
- emailService.sendPasswordResetEmail(user.getEmail(), user.getDisplayName(), token);
- }
+ /**
+ * 用户登出
+ */
+ void logout();
- @Transactional
- public void resetPassword(ResetPasswordRequest request) {
- // 查找重置令牌
- QueryWrapper queryWrapper = QueryWrapper.create()
- .where(PasswordResetToken::getToken).eq(request.getToken())
- .and(PasswordResetToken::getUsed).eq(false);
-
- PasswordResetToken resetToken = tokenMapper.selectOneByQuery(queryWrapper);
-
- if (resetToken == null || !resetToken.isValid()) {
- throw new RuntimeException("重置令牌无效或已过期");
- }
-
- // 更新用户密码
- User user = userMapper.selectOneById(resetToken.getUserId());
- if (user == null) {
- throw new RuntimeException("用户不存在");
- }
-
- user.setPasswordHash(passwordEncoder.encode(request.getNewPassword()));
- user.setUpdatedAt(LocalDateTime.now());
- userMapper.update(user);
-
- // 标记令牌为已使用
- resetToken.setUsed(true);
- tokenMapper.update(resetToken);
- }
+ /**
+ * 请求密码重置
+ * @param request 密码重置请求
+ */
+ void requestPasswordReset(PasswordResetRequest request);
+
+ /**
+ * 重置密码
+ * @param request 重置密码请求
+ */
+ void resetPassword(ResetPasswordRequest request);
+
+ /**
+ * 更新当前用户的个人资料
+ * @param request 个人资料更新请求
+ * @param currentUserId 当前用户ID
+ * @return 更新后的用户DTO
+ */
+ UserDTO updateCurrentUserProfile(UserProfileUpdateRequest request, Long currentUserId);
/**
* 根据联系方式(邮箱或手机号)查找用户
+ * @param contact 联系方式
+ * @return 用户信息
*/
- public User findByContact(String contact) {
- if (contact.contains("@")) {
- return findByEmail(contact);
- } else {
- return findByPhone(contact);
- }
- }
-
- public User findByEmail(String email) {
- QueryWrapper queryWrapper = QueryWrapper.create()
- .where(User::getEmail).eq(email);
- return userMapper.selectOneByQuery(queryWrapper);
- }
-
- public User findByPhone(String phone) {
- QueryWrapper queryWrapper = QueryWrapper.create()
- .where(User::getPhone).eq(phone);
- return userMapper.selectOneByQuery(queryWrapper);
- }
-
- public boolean existsByEmail(String email) {
- return findByEmail(email) != null;
- }
-
- public boolean existsByPhone(String phone) {
- return findByPhone(phone) != null;
- }
+ User findByContact(String contact);
/**
- * 生成随机验证码
+ * 根据邮箱查找用户
+ * @param email 邮箱
+ * @return 用户信息
*/
- private String generateRandomCode() {
- Random random = new Random();
- int code = 100000 + random.nextInt(900000); // 生成6位随机数
- return String.valueOf(code);
- }
+ User findByEmail(String email);
/**
- * 验证码信息类
+ * 根据手机号查找用户
+ * @param phone 手机号
+ * @return 用户信息
*/
- private static class VerificationCodeInfo {
- private final String code;
- private final LocalDateTime timestamp;
-
- public VerificationCodeInfo(String code, LocalDateTime timestamp) {
- this.code = code;
- this.timestamp = timestamp;
- }
- }
+ User findByPhone(String phone);
+
+ /**
+ * 检查邮箱是否已存在
+ * @param email 邮箱
+ * @return 是否存在
+ */
+ boolean existsByEmail(String email);
+
+ /**
+ * 检查手机号是否已存在
+ * @param phone 手机号
+ * @return 是否存在
+ */
+ boolean existsByPhone(String phone);
}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java b/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java
new file mode 100644
index 0000000..5ba9507
--- /dev/null
+++ b/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java
@@ -0,0 +1,334 @@
+package com.yundage.chat.service.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yundage.chat.dto.ChatRequest;
+import com.yundage.chat.dto.ChatResponse;
+import com.yundage.chat.dto.StreamResponse;
+import com.yundage.chat.entity.Conversation;
+import com.yundage.chat.entity.Message;
+import com.yundage.chat.entity.User;
+import com.yundage.chat.mapper.ConversationMapper;
+import com.yundage.chat.mapper.MessageMapper;
+import com.yundage.chat.service.ChatService;
+import com.yundage.chat.service.LLMService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 聊天服务实现类
+ */
+@Service
+public class ChatServiceImpl implements ChatService {
+
+ private ConversationMapper conversationMapper;
+
+ private MessageMapper messageMapper;
+
+ private final LLMService llmService;
+
+ private final ObjectMapper mapper = new ObjectMapper();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
+ @Autowired
+ public ChatServiceImpl(LLMService llmService,
+ ConversationMapper conversationMapper,
+ MessageMapper messageMapper) {
+ this.llmService = llmService;
+ this.conversationMapper = conversationMapper;
+ this.messageMapper = messageMapper;
+ }
+
+ @Override
+ @Transactional
+ public SseEmitter processChatStream(ChatRequest request, User user) {
+ // 0 表示不超时;如需限制自行调整
+ SseEmitter emitter = new SseEmitter(0L);
+
+ try {
+ long start = System.currentTimeMillis();
+
+ // 1️⃣ 取得会话并保存用户消息
+ Conversation convo = getOrCreateConversation(request, user);
+ saveUserMessage(convo.getId(), request.getMessage());
+
+ // 2️⃣ 在独立线程里完成流处理,避免阻塞 servlet 线程
+ SecurityContext ctx = SecurityContextHolder.getContext();
+ executor.submit(new DelegatingSecurityContextRunnable(() -> {
+
+ StringBuilder full = new StringBuilder();
+
+ // 准备额外参数
+ Map parameters = new HashMap<>();
+ // 可以根据需要添加特定参数
+ if ("research".equals(request.getMode())) {
+ parameters.put("systemPrompt", "你是一个专注于研究和深度分析的AI助手。提供详细、准确和有参考价值的信息。");
+ parameters.put("temperature", 0.3); // 更低的温度,提高回答的确定性
+ } else {
+ parameters.put("systemPrompt", "你是一个友好、有帮助的AI助手。");
+ parameters.put("temperature", 0.7); // 默认温度
+ }
+
+ // 3️⃣ 使用封装的LLM服务接口进行流式生成
+ llmService.streamGenerateText(
+ request.getMessage(),
+ parameters,
+ // 处理每个文本块
+ chunk -> {
+ try {
+ full.append(chunk);
+ StreamResponse resp = StreamResponse.content(chunk);
+ emitter.send(SseEmitter.event()
+ .name("message")
+ .data(mapper.writeValueAsString(resp)));
+ } catch (Exception io) {
+ emitter.completeWithError(io);
+ }
+ },
+ // 处理完成事件
+ () -> {
+ try {
+ Message assistantMsg = saveAssistantMessage(convo.getId(), full.toString());
+ convo.setUpdatedAt(LocalDateTime.now());
+ conversationMapper.update(convo);
+
+ List refs =
+ "research".equals(request.getMode()) ? generateMockReferences() : List.of();
+
+ StreamResponse finish = StreamResponse.finished(
+ convo.getId().toString(),
+ assistantMsg.getId().toString(),
+ refs,
+ null,
+ estimateTokens(request.getMessage(), full.toString()),
+ System.currentTimeMillis() - start
+ );
+ emitter.send(SseEmitter.event()
+ .name("finished")
+ .data(mapper.writeValueAsString(finish)));
+ emitter.complete();
+ } catch (Exception e) {
+ emitter.completeWithError(e);
+ }
+ },
+ // 处理错误事件
+ err -> {
+ try {
+ StreamResponse error = StreamResponse.error("AI 处理失败:" + err.getMessage());
+ emitter.send(SseEmitter.event()
+ .name("error")
+ .data(mapper.writeValueAsString(error)));
+ } catch (Exception ignore) {}
+ emitter.completeWithError(err);
+ }
+ );
+ }, ctx));
+ } catch (Exception ex) {
+ emitter.completeWithError(ex);
+ }
+ return emitter;
+ }
+
+ @Override
+ @Transactional
+ public ChatResponse processChat(ChatRequest request, User user) {
+ long startTime = System.currentTimeMillis();
+
+ // 获取或创建会话
+ Conversation conversation = getOrCreateConversation(request, user);
+
+ // 保存用户消息
+ Message userMessage = saveUserMessage(conversation.getId(), request.getMessage());
+
+ // 准备额外参数
+ Map parameters = new HashMap<>();
+ if ("research".equals(request.getMode())) {
+ parameters.put("systemPrompt", "你是一个专注于研究和深度分析的AI助手。提供详细、准确和有参考价值的信息。");
+ parameters.put("temperature", 0.3); // 更低的温度,提高回答的确定性
+ } else {
+ parameters.put("systemPrompt", "你是一个友好、有帮助的AI助手。");
+ parameters.put("temperature", 0.7); // 默认温度
+ }
+
+ // 使用封装的LLM服务生成回复
+ String aiResponse = llmService.generateText(request.getMessage(), parameters);
+
+ // 保存AI回复消息
+ Message assistantMessage = saveAssistantMessage(conversation.getId(), aiResponse);
+
+ // 更新会话的更新时间
+ conversation.setUpdatedAt(LocalDateTime.now());
+ conversationMapper.update(conversation);
+
+ // 构建响应
+ ChatResponse response = new ChatResponse();
+ response.setConversationId(conversation.getId().toString());
+ response.setMessageId(assistantMessage.getId().toString());
+ response.setAnswer(aiResponse);
+ response.setTimeMs(System.currentTimeMillis() - startTime);
+
+ // 根据模式设置不同的响应内容
+ if ("research".equals(request.getMode())) {
+ // 研究模式可能包含引用资料
+ response.setReferences(generateMockReferences());
+ response.setReportId(null); // 可以在后续版本中实现报告生成功能
+ } else {
+ // 普通聊天模式
+ response.setReferences(new ArrayList<>());
+ response.setReportId(null);
+ }
+
+ // 模拟token统计
+ response.setTokensUsed(estimateTokens(request.getMessage(), aiResponse));
+
+ return response;
+ }
+
+ private String[] generateStreamingAIResponse(ChatRequest request) {
+ // 目前先用单段文本模拟,后续可改为 chatClient.stream() 真正流式
+ String aiReply = "这是AI的回复内容,模拟流式输出。";
+ return new String[]{aiReply};
+ }
+
+ private Conversation getOrCreateConversation(ChatRequest request, User user) {
+ if (request.getConversationId() != null) {
+ // 尝试获取现有会话
+ try {
+ Long conversationId = Long.parseLong(request.getConversationId());
+ Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, user.getId());
+ if (conversation != null) {
+ return conversation;
+ }
+ } catch (NumberFormatException e) {
+ // 忽略无效的会话ID,创建新会话
+ }
+ }
+
+ // 创建新会话
+ Conversation conversation = new Conversation();
+ conversation.setUserId(user.getId());
+ conversation.setChatMode(request.getMode());
+ conversation.setTitle(generateConversationTitle(request.getMessage()));
+ conversation.setCreatedAt(LocalDateTime.now());
+ conversation.setUpdatedAt(LocalDateTime.now());
+
+ conversationMapper.insert(conversation);
+ return conversation;
+ }
+
+ private Message saveUserMessage(Long conversationId, String content) {
+ Integer nextSequenceNo = messageMapper.getMaxSequenceNo(conversationId) + 1;
+
+ Message message = new Message();
+ message.setConversationId(conversationId);
+ message.setSequenceNo(nextSequenceNo);
+ message.setRole("user");
+ message.setContent(content);
+ message.setCreatedAt(LocalDateTime.now());
+
+ messageMapper.insert(message);
+ return message;
+ }
+
+ private Message saveAssistantMessage(Long conversationId, String content) {
+ Integer nextSequenceNo = messageMapper.getMaxSequenceNo(conversationId) + 1;
+
+ Message message = new Message();
+ message.setConversationId(conversationId);
+ message.setSequenceNo(nextSequenceNo);
+ message.setRole("assistant");
+ message.setContent(content);
+ message.setCreatedAt(LocalDateTime.now());
+
+ messageMapper.insert(message);
+ return message;
+ }
+
+ private String generateAIResponse(ChatRequest request) {
+ // 准备额外参数
+ Map parameters = new HashMap<>();
+ if ("research".equals(request.getMode())) {
+ parameters.put("systemPrompt", "你是一个专注于研究和深度分析的AI助手。提供详细、准确和有参考价值的信息。");
+ parameters.put("temperature", 0.3);
+ } else {
+ parameters.put("systemPrompt", "你是一个友好、有帮助的AI助手。");
+ parameters.put("temperature", 0.7);
+ }
+
+ // 使用封装的LLM服务生成回复
+ return llmService.generateText(request.getMessage(), parameters);
+ }
+
+ private String generateConversationTitle(String message) {
+ // 简单的标题生成逻辑,取前20个字符
+ if (message.length() <= 20) {
+ return message;
+ }
+ return message.substring(0, 20) + "...";
+ }
+
+ private List generateMockReferences() {
+ // 模拟的引用资料,实际项目中从真实的知识库获取
+ List references = new ArrayList<>();
+ references.add(new ChatResponse.ReferenceItem(
+ "相关研究文档",
+ "https://example.com/research",
+ "这是一段引用的文本内容..."
+ ));
+ return references;
+ }
+
+ private Integer estimateTokens(String userMessage, String aiResponse) {
+ // 简单的token估算,实际项目中需要使用真实的tokenizer
+ return (userMessage.length() + aiResponse.length()) / 4;
+ }
+
+ @Override
+ public List getUserConversations(Long userId) {
+ return conversationMapper.findActiveByUserId(userId);
+ }
+
+ @Override
+ public List getConversationMessages(Long conversationId, Long userId) {
+ // 验证会话是否属于用户
+ Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, userId);
+ if (conversation == null) {
+ throw new RuntimeException("会话不存在或无权限访问");
+ }
+
+ return messageMapper.findByConversationId(conversationId);
+ }
+
+ /**
+ * 删除用户会话(标记为非活跃)
+ * @param conversationId 会话ID
+ * @param userId 用户ID
+ * @return 删除成功返回true,失败返回false
+ */
+ @Override
+ @Transactional
+ public boolean deleteConversation(Long conversationId, Long userId) {
+ // 验证会话是否属于用户
+ Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, userId);
+ if (conversation == null) {
+ return false;
+ }
+
+ // 标记会话为非活跃
+ conversation.setIsActive(0);
+ conversation.setUpdatedAt(LocalDateTime.now());
+ return conversationMapper.update(conversation) > 0;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/impl/EmailServiceImpl.java b/src/main/java/com/yundage/chat/service/impl/EmailServiceImpl.java
new file mode 100644
index 0000000..f2f2294
--- /dev/null
+++ b/src/main/java/com/yundage/chat/service/impl/EmailServiceImpl.java
@@ -0,0 +1,51 @@
+package com.yundage.chat.service.impl;
+
+import com.yundage.chat.service.EmailService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+/**
+ * 邮件服务实现类
+ */
+@Service
+public class EmailServiceImpl implements EmailService {
+
+ @Autowired
+ private JavaMailSender mailSender;
+
+ @Value("${spring.mail.username}")
+ private String fromEmail;
+
+ @Value("${app.reset-password-url:http://localhost:3000/reset-password}")
+ private String resetPasswordUrl;
+
+ @Override
+ public void sendPasswordResetEmail(String toEmail, String username, String token) {
+ try {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(fromEmail);
+ message.setTo(toEmail);
+ message.setSubject("密码重置 - 云大AI聊天");
+
+ String resetLink = resetPasswordUrl + "?token=" + token;
+ String emailBody = String.format(
+ "亲爱的 %s,\n\n" +
+ "我们收到了您的密码重置请求。请点击以下链接重置您的密码:\n\n" +
+ "%s\n\n" +
+ "此链接将在1小时后过期。如果您没有请求重置密码,请忽略此邮件。\n\n" +
+ "祝好,\n" +
+ "云大AI聊天团队",
+ username, resetLink
+ );
+
+ message.setText(emailBody);
+ mailSender.send(message);
+
+ } catch (Exception e) {
+ throw new RuntimeException("发送邮件失败: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/yundage/chat/service/impl/SpringAIModelService.java b/src/main/java/com/yundage/chat/service/impl/SpringAIModelService.java
new file mode 100644
index 0000000..2e6058d
--- /dev/null
+++ b/src/main/java/com/yundage/chat/service/impl/SpringAIModelService.java
@@ -0,0 +1,65 @@
+package com.yundage.chat.service.impl;
+
+import com.yundage.chat.service.LLMService;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * 基于 Spring AI 1.0.0‑M6 的 LLM 服务
+ */
+@Service
+public class SpringAIModelService implements LLMService {
+
+ private final ChatClient chatClient;
+
+ public SpringAIModelService(ChatClient.Builder builder) {
+ // Spring Boot 已为 Builder 注入模型、API‑Key 等配置
+ this.chatClient = builder.build();
+ }
+
+ @Override
+ public void streamGenerateText(String userMessage,
+ Map parameters,
+ Consumer callback,
+ Runnable onComplete,
+ Consumer onError) {
+
+ // 1. 起一个 fluent builder
+ var prompt = chatClient.prompt();
+
+ // 2. 可选:加入 system prompt
+ if (parameters != null && parameters.containsKey("systemPrompt")) {
+ prompt.system((String) parameters.get("systemPrompt"));
+ }
+
+ // 3. 加入用户消息并开启流式
+ Flux flux = prompt.user(userMessage) // 添加 user role
+ .stream() // 切换为 Flux
+ .content(); // 只取文本
+
+ // 4. 订阅
+ flux.doOnNext(callback)
+ .doOnComplete(onComplete)
+ .doOnError(onError)
+ .subscribe();
+ }
+
+ @Override
+ public String generateText(String userMessage, Map parameters) {
+
+ var prompt = chatClient.prompt();
+
+ if (parameters != null && parameters.containsKey("systemPrompt")) {
+ prompt.system((String) parameters.get("systemPrompt"));
+ }
+
+ // 同步调用,直接返回完整结果
+ return prompt.user(userMessage)
+ .call() // 阻塞请求
+ .content(); // 取文本
+ }
+}
diff --git a/src/main/java/com/yundage/chat/service/impl/UserServiceImpl.java b/src/main/java/com/yundage/chat/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..3e6c2da
--- /dev/null
+++ b/src/main/java/com/yundage/chat/service/impl/UserServiceImpl.java
@@ -0,0 +1,364 @@
+package com.yundage.chat.service.impl;
+
+import com.mybatisflex.core.query.QueryWrapper;
+import com.yundage.chat.dto.*;
+import com.yundage.chat.entity.PasswordResetToken;
+import com.yundage.chat.entity.User;
+import com.yundage.chat.mapper.PasswordResetTokenMapper;
+import com.yundage.chat.mapper.UserMapper;
+import com.yundage.chat.service.EmailService;
+import com.yundage.chat.service.UserService;
+import com.yundage.chat.util.JwtUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 用户服务实现类
+ */
+@Service
+public class UserServiceImpl implements UserService {
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Autowired
+ private PasswordResetTokenMapper tokenMapper;
+
+ @Autowired
+ private PasswordEncoder passwordEncoder;
+
+ @Autowired
+ private JwtUtil jwtUtil;
+
+ @Autowired
+ private AuthenticationManager authenticationManager;
+
+ @Autowired
+ private EmailService emailService;
+
+ // 从配置文件中获取验证码配置
+ @Value("${app.verification-code.master-code:123456}")
+ private String masterCode;
+
+ @Value("${app.verification-code.expiration:300}")
+ private int codeExpirationSeconds;
+
+ @Value("${app.verification-code.development-mode:false}")
+ private boolean developmentMode;
+
+ // 验证码存储,实际项目中应该使用Redis
+ private final Map verificationCodes = new ConcurrentHashMap<>();
+
+ @Override
+ public String sendVerificationCode(String contact) {
+ // 生成6位随机验证码
+ String code = generateRandomCode();
+
+ // 存储验证码
+ verificationCodes.put(contact, new VerificationCodeInfo(code, LocalDateTime.now()));
+
+ // TODO: 实际项目中,这里应该通过短信或邮件发送验证码
+ // 如果是邮箱,可以使用邮件服务发送
+ if (contact.contains("@")) {
+ // emailService.sendVerificationCode(contact, code);
+ } else {
+ // 使用短信服务发送
+ // smsService.sendVerificationCode(contact, code);
+ }
+
+ // 在开发模式下,在控制台打印验证码
+ if (developmentMode) {
+ System.out.println("向 " + contact + " 发送验证码: " + code);
+ }
+
+ return developmentMode ? code : null; // 仅在开发模式下返回验证码
+ }
+
+ /**
+ * 验证验证码
+ * @param contact 联系方式
+ * @param code 验证码
+ * @return 是否验证成功
+ */
+ private boolean verifyCode(String contact, String code) {
+ // 检查万能验证码
+ if (masterCode.equals(code)) {
+ return true;
+ }
+
+ // 检查验证码是否存在
+ VerificationCodeInfo storedInfo = verificationCodes.get(contact);
+ if (storedInfo == null) {
+ return false;
+ }
+
+ // 检查验证码是否过期
+ LocalDateTime expirationTime = storedInfo.timestamp.plusSeconds(codeExpirationSeconds);
+ if (LocalDateTime.now().isAfter(expirationTime)) {
+ verificationCodes.remove(contact); // 移除过期验证码
+ return false;
+ }
+
+ // 检查验证码是否匹配
+ boolean isValid = storedInfo.code.equals(code);
+ if (isValid) {
+ verificationCodes.remove(contact); // 验证成功后移除验证码,防止重复使用
+ }
+
+ return isValid;
+ }
+
+ @Override
+ @Transactional
+ public AuthResponse register(RegisterRequest request) {
+ // 验证验证码
+ if (!verifyCode(request.getEmail() != null ? request.getEmail() : request.getPhone(), request.getCode())) {
+ throw new RuntimeException("验证码无效或已过期");
+ }
+
+ // 检查用户是否已存在
+ if (request.getEmail() != null && existsByEmail(request.getEmail())) {
+ throw new RuntimeException("邮箱已被注册");
+ }
+
+ if (request.getPhone() != null && existsByPhone(request.getPhone())) {
+ throw new RuntimeException("手机号已被注册");
+ }
+
+ // 创建新用户
+ User user = new User();
+ user.setUsername(request.getUsername());
+ user.setEmail(request.getEmail());
+ user.setPhone(request.getPhone());
+ // 设置一个随机密码,因为不再使用密码登录
+ user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString()));
+ user.setCreatedAt(LocalDateTime.now());
+ user.setUpdatedAt(LocalDateTime.now());
+
+ userMapper.insert(user);
+
+ // 生成JWT令牌
+ String token = jwtUtil.generateToken(user.getDisplayName(), user.getId());
+
+ return new AuthResponse(token, user.getId(), user.getDisplayName(),
+ user.getEmail(), user.getPhone());
+ }
+
+ @Override
+ public AuthResponse login(LoginRequest request) {
+ // 验证验证码
+ if (!verifyCode(request.getContact(), request.getCode())) {
+ throw new RuntimeException("验证码无效或已过期");
+ }
+
+ // 根据联系方式查找用户
+ User user = findByContact(request.getContact());
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+
+ // 手动设置认证信息
+ Authentication authentication = createAuthenticationToken(user);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ // 更新最后登录时间
+ user.setLastLoginAt(LocalDateTime.now());
+ userMapper.update(user);
+
+ // 生成JWT令牌
+ String token = jwtUtil.generateToken(user.getDisplayName(), user.getId());
+
+ return new AuthResponse(token, user.getId(), user.getDisplayName(),
+ user.getEmail(), user.getPhone());
+ }
+
+ @Override
+ public void logout() {
+ // 清除认证上下文
+ SecurityContextHolder.clearContext();
+
+ // 注意:由于JWT是无状态的,服务端无法使单个令牌失效
+ // 在实际生产环境中,可以采用以下策略之一:
+ // 1. 将令牌添加到黑名单(通常使用Redis实现)
+ // 2. 使用较短的令牌过期时间
+ // 3. 维护一个令牌版本号,当用户登出时更新版本号,使旧令牌无效
+ }
+
+ /**
+ * 创建认证令牌
+ */
+ private Authentication createAuthenticationToken(User user) {
+ List authorities = new ArrayList<>();
+ authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+ return new UsernamePasswordAuthenticationToken(user, null, authorities);
+ }
+
+ @Override
+ @Transactional
+ public void requestPasswordReset(PasswordResetRequest request) {
+ User user = findByEmail(request.getEmail());
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+
+ // 生成重置令牌
+ String token = UUID.randomUUID().toString();
+ LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); // 1小时后过期
+
+ PasswordResetToken resetToken = new PasswordResetToken(user.getId(), token, expiresAt);
+ tokenMapper.insert(resetToken);
+
+ // 发送重置邮件
+ emailService.sendPasswordResetEmail(user.getEmail(), user.getDisplayName(), token);
+ }
+
+ @Override
+ @Transactional
+ public void resetPassword(ResetPasswordRequest request) {
+ // 查找重置令牌
+ QueryWrapper queryWrapper = QueryWrapper.create()
+ .where(PasswordResetToken::getToken).eq(request.getToken())
+ .and(PasswordResetToken::getUsed).eq(false);
+
+ PasswordResetToken resetToken = tokenMapper.selectOneByQuery(queryWrapper);
+
+ if (resetToken == null || !resetToken.isValid()) {
+ throw new RuntimeException("重置令牌无效或已过期");
+ }
+
+ // 更新用户密码
+ User user = userMapper.selectOneById(resetToken.getUserId());
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+
+ user.setPasswordHash(passwordEncoder.encode(request.getNewPassword()));
+ user.setUpdatedAt(LocalDateTime.now());
+ userMapper.update(user);
+
+ // 标记令牌为已使用
+ resetToken.setUsed(true);
+ tokenMapper.update(resetToken);
+ }
+
+ @Override
+ @Transactional
+ public UserDTO updateCurrentUserProfile(UserProfileUpdateRequest request, Long currentUserId) {
+ // 获取当前用户
+ User user = userMapper.selectOneById(currentUserId);
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+
+ // 验证邮箱和手机号是否被其他用户占用
+ if (request.getEmail() != null && !request.getEmail().equals(user.getEmail()) && existsByEmail(request.getEmail())) {
+ throw new RuntimeException("邮箱已被其他用户使用");
+ }
+
+ if (request.getPhone() != null && !request.getPhone().equals(user.getPhone()) && existsByPhone(request.getPhone())) {
+ throw new RuntimeException("手机号已被其他用户使用");
+ }
+
+ // 更新用户资料
+ boolean hasUpdates = false;
+
+ if (request.getUsername() != null && !request.getUsername().equals(user.getUsername())) {
+ user.setUsername(request.getUsername());
+ hasUpdates = true;
+ }
+
+ if (request.getAvatarUrl() != null && !request.getAvatarUrl().equals(user.getAvatarUrl())) {
+ user.setAvatarUrl(request.getAvatarUrl());
+ hasUpdates = true;
+ }
+
+ if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
+ user.setEmail(request.getEmail());
+ hasUpdates = true;
+ }
+
+ if (request.getPhone() != null && !request.getPhone().equals(user.getPhone())) {
+ user.setPhone(request.getPhone());
+ hasUpdates = true;
+ }
+
+ // 如果有更新,则保存到数据库
+ if (hasUpdates) {
+ user.setUpdatedAt(LocalDateTime.now());
+ userMapper.update(user);
+ }
+
+ // 转换为DTO并返回
+ return UserDTO.fromUser(user);
+ }
+
+ @Override
+ public User findByContact(String contact) {
+ if (contact.contains("@")) {
+ return findByEmail(contact);
+ } else {
+ return findByPhone(contact);
+ }
+ }
+
+ @Override
+ public User findByEmail(String email) {
+ QueryWrapper queryWrapper = QueryWrapper.create()
+ .where(User::getEmail).eq(email);
+ return userMapper.selectOneByQuery(queryWrapper);
+ }
+
+ @Override
+ public User findByPhone(String phone) {
+ QueryWrapper queryWrapper = QueryWrapper.create()
+ .where(User::getPhone).eq(phone);
+ return userMapper.selectOneByQuery(queryWrapper);
+ }
+
+ @Override
+ public boolean existsByEmail(String email) {
+ return findByEmail(email) != null;
+ }
+
+ @Override
+ public boolean existsByPhone(String phone) {
+ return findByPhone(phone) != null;
+ }
+
+ /**
+ * 生成随机验证码
+ */
+ private String generateRandomCode() {
+ java.util.Random random = new java.util.Random();
+ int code = 100000 + random.nextInt(900000); // 生成6位随机数
+ return String.valueOf(code);
+ }
+
+ /**
+ * 验证码信息类
+ */
+ private static class VerificationCodeInfo {
+ private final String code;
+ private final LocalDateTime timestamp;
+
+ public VerificationCodeInfo(String code, LocalDateTime timestamp) {
+ this.code = code;
+ this.timestamp = timestamp;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index f2bd854..63f266d 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -1,113 +1,131 @@
--- Create database
-CREATE DATABASE IF NOT EXISTS chat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+/*
+ Navicat Premium Data Transfer
-USE chat;
+ Source Server : 云大
+ Source Server Type : MySQL
+ Source Server Version : 80405 (8.4.5)
+ Source Host : 101.200.154.78:3306
+ Source Schema : yunda_qa
--- 1. 用户主表
-CREATE TABLE users (
- id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- username VARCHAR(50) DEFAULT NULL COMMENT '昵称/展示名',
- password_hash VARCHAR(255) DEFAULT NULL COMMENT '哈希密码(可为空)',
- phone VARCHAR(20) UNIQUE DEFAULT NULL COMMENT '手机号',
- email VARCHAR(100)UNIQUE DEFAULT NULL COMMENT '邮箱',
- avatar_url VARCHAR(255) DEFAULT NULL,
- user_type ENUM('personal','enterprise','admin')
- NOT NULL DEFAULT 'personal' COMMENT '用户类型',
- membership_level_id SMALLINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '付费等级',
- status TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常, 0=封禁',
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- last_login_at DATETIME NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表';
+ Target Server Type : MySQL
+ Target Server Version : 80405 (8.4.5)
+ File Encoding : 65001
--- 2. 第三方账号绑定表
-CREATE TABLE user_auth_accounts (
- id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- user_id BIGINT UNSIGNED NOT NULL,
- provider ENUM('wechat_mini','wechat_open','google','github','apple','custom')
- NOT NULL COMMENT '登录方式',
- provider_user_id VARCHAR(100) NOT NULL COMMENT '第三方平台唯一ID',
- access_token VARCHAR(255) DEFAULT NULL,
- refresh_token VARCHAR(255) DEFAULT NULL,
- expires_at DATETIME DEFAULT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE KEY uq_provider_user (provider, provider_user_id),
- CONSTRAINT fk_auth_user FOREIGN KEY (user_id) REFERENCES users(id)
- ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方登录账号绑定表';
+ Date: 21/07/2025 09:29:04
+*/
--- 3. 会员等级表
-CREATE TABLE membership_levels (
- id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- name VARCHAR(50) NOT NULL,
- price_month DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '月费',
- daily_msg_limit INT UNSIGNED NOT NULL DEFAULT 20,
- features JSON DEFAULT NULL COMMENT '权限 JSON',
- sort_order TINYINT NOT NULL DEFAULT 0,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级定义';
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
--- 4. 会话表(支持普通与研究模式)
-CREATE TABLE conversations (
- id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- user_id BIGINT UNSIGNED NOT NULL,
- title VARCHAR(100) DEFAULT NULL,
- model_version VARCHAR(50) DEFAULT NULL COMMENT '使用模型版本',
- chat_mode ENUM('chat','research')
- NOT NULL DEFAULT 'chat' COMMENT 'chat=普通,research=深度研究',
- is_active TINYINT NOT NULL DEFAULT 1,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- CONSTRAINT fk_conv_user FOREIGN KEY (user_id) REFERENCES users(id)
- ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话表';
+-- ----------------------------
+-- Table structure for conversation_research_meta
+-- ----------------------------
+DROP TABLE IF EXISTS `conversation_research_meta`;
+CREATE TABLE `conversation_research_meta` (
+ `conversation_id` bigint UNSIGNED NOT NULL,
+ `topic` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '研究主题',
+ `goal` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '研究目标描述',
+ `progress_json` json NULL COMMENT '阶段进度',
+ `draft_report_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '草稿报告 ID(预留)',
+ `cite_style` enum('APA','IEEE','GB/T-7714') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'APA',
+ `tokens_consumed` int UNSIGNED NOT NULL DEFAULT 0,
+ `last_summary_at` datetime NULL DEFAULT NULL,
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`conversation_id`) USING BTREE,
+ CONSTRAINT `fk_research_conv` FOREIGN KEY (`conversation_id`) REFERENCES `conversations` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '深度研究元数据' ROW_FORMAT = Dynamic;
--- 5. 深度研究扩展表
-CREATE TABLE conversation_research_meta (
- conversation_id BIGINT UNSIGNED PRIMARY KEY,
- topic VARCHAR(200) NOT NULL COMMENT '研究主题',
- goal TEXT DEFAULT NULL COMMENT '研究目标描述',
- progress_json JSON DEFAULT NULL COMMENT '阶段进度',
- draft_report_id BIGINT UNSIGNED DEFAULT NULL COMMENT '草稿报告 ID(预留)',
- cite_style ENUM('APA','IEEE','GB/T-7714') DEFAULT 'APA',
- tokens_consumed INT UNSIGNED NOT NULL DEFAULT 0,
- last_summary_at DATETIME DEFAULT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- CONSTRAINT fk_research_conv FOREIGN KEY (conversation_id)
- REFERENCES conversations(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='深度研究元数据';
+-- ----------------------------
+-- Table structure for conversations
+-- ----------------------------
+DROP TABLE IF EXISTS `conversations`;
+CREATE TABLE `conversations` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` bigint UNSIGNED NOT NULL,
+ `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `model_version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用模型版本',
+ `chat_mode` enum('chat','research') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'chat' COMMENT 'chat=普通,research=深度研究',
+ `is_active` tinyint NOT NULL DEFAULT 1,
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `fk_conv_user`(`user_id` ASC) USING BTREE,
+ CONSTRAINT `fk_conv_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 89 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '会话表' ROW_FORMAT = Dynamic;
--- 6. 消息表
-CREATE TABLE messages (
- id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- conversation_id BIGINT UNSIGNED NOT NULL,
- sequence_no INT UNSIGNED NOT NULL COMMENT '会话内顺序编号',
- role ENUM('user','assistant','system','tool')
- NOT NULL,
- content LONGTEXT NOT NULL,
- tokens INT UNSIGNED DEFAULT NULL,
- latency_ms INT UNSIGNED DEFAULT NULL COMMENT '响应耗时',
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- CONSTRAINT fk_msg_conv FOREIGN KEY (conversation_id) REFERENCES conversations(id)
- ON DELETE CASCADE,
- UNIQUE KEY uq_conv_seq (conversation_id, sequence_no)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
+-- ----------------------------
+-- Table structure for membership_levels
+-- ----------------------------
+DROP TABLE IF EXISTS `membership_levels`;
+CREATE TABLE `membership_levels` (
+ `id` smallint UNSIGNED NOT NULL AUTO_INCREMENT,
+ `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `price_month` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '月费',
+ `daily_msg_limit` int UNSIGNED NOT NULL DEFAULT 20,
+ `features` json NULL COMMENT '权限 JSON',
+ `sort_order` tinyint NOT NULL DEFAULT 0,
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '会员等级定义' ROW_FORMAT = Dynamic;
--- 7. 密码重置令牌表(用于密码重置功能)
-CREATE TABLE password_reset_tokens (
- id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- user_id BIGINT UNSIGNED NOT NULL,
- token VARCHAR(255) NOT NULL UNIQUE,
- expires_at DATETIME NOT NULL,
- used TINYINT NOT NULL DEFAULT 0,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- CONSTRAINT fk_reset_user FOREIGN KEY (user_id) REFERENCES users(id)
- ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='密码重置令牌表';
+-- ----------------------------
+-- Table structure for messages
+-- ----------------------------
+DROP TABLE IF EXISTS `messages`;
+CREATE TABLE `messages` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
+ `conversation_id` bigint UNSIGNED NOT NULL,
+ `sequence_no` int UNSIGNED NOT NULL COMMENT '会话内顺序编号',
+ `role` enum('user','assistant','system','tool') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `tokens` int UNSIGNED NULL DEFAULT NULL,
+ `latency_ms` int UNSIGNED NULL DEFAULT NULL COMMENT '响应耗时',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uq_conv_seq`(`conversation_id` ASC, `sequence_no` ASC) USING BTREE,
+ CONSTRAINT `fk_msg_conv` FOREIGN KEY (`conversation_id`) REFERENCES `conversations` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '消息表' ROW_FORMAT = Dynamic;
--- 插入默认会员等级数据
-INSERT INTO membership_levels (id, name, price_month, daily_msg_limit, features, sort_order) VALUES
-(1, '免费版', 0.00, 20, '{"models": ["gpt-3.5"], "features": ["basic_chat"]}', 1),
-(2, '专业版', 29.99, 100, '{"models": ["gpt-3.5", "gpt-4"], "features": ["basic_chat", "research_mode"]}', 2),
-(3, '企业版', 99.99, 1000, '{"models": ["gpt-3.5", "gpt-4", "gpt-4-turbo"], "features": ["basic_chat", "research_mode", "priority_support"]}', 3);
\ No newline at end of file
+-- ----------------------------
+-- Table structure for user_auth_accounts
+-- ----------------------------
+DROP TABLE IF EXISTS `user_auth_accounts`;
+CREATE TABLE `user_auth_accounts` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` bigint UNSIGNED NOT NULL,
+ `provider` enum('wechat_mini','wechat_open','google','github','apple','custom') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '登录方式',
+ `provider_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '第三方平台唯一ID',
+ `access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `refresh_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `expires_at` datetime NULL DEFAULT NULL,
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uq_provider_user`(`provider` ASC, `provider_user_id` ASC) USING BTREE,
+ INDEX `fk_auth_user`(`user_id` ASC) USING BTREE,
+ CONSTRAINT `fk_auth_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '第三方登录账号绑定表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for users
+-- ----------------------------
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
+ `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '昵称/展示名',
+ `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '哈希密码(可为空)',
+ `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
+ `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
+ `avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `user_type` enum('personal','enterprise','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'personal' COMMENT '用户类型',
+ `membership_level_id` smallint UNSIGNED NOT NULL DEFAULT 1 COMMENT '付费等级',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '1=正常, 0=封禁',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `last_login_at` datetime NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `phone`(`phone` ASC) USING BTREE,
+ UNIQUE INDEX `email`(`email` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户主表' ROW_FORMAT = Dynamic;
+
+SET FOREIGN_KEY_CHECKS = 1;