From b796dab689e72d6f0c5ee9df44eb289dbeba1088 Mon Sep 17 00:00:00 2001 From: 2991692032 Date: Tue, 3 Jun 2025 00:00:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilife-server/.gitignore | 11 + unilife-server/CONFIGURATION.md | 87 ++++ unilife-server/SIMPLIFIED_CHAT_HISTORY.md | 72 +++ unilife-server/env.example | 28 + unilife-server/pom.xml | 94 ++++ .../com/unilife/common/constant/Prompt.java | 37 ++ .../com/unilife/common/result/Result.java | 4 + .../java/com/unilife/config/AiConfig.java | 100 ++++ .../java/com/unilife/config/OssConfig.java | 39 ++ .../java/com/unilife/config/WebMvcConfig.java | 37 +- .../unilife/controller/AdminController.java | 146 ++++++ .../com/unilife/controller/AiController.java | 94 ++++ .../unilife/controller/CourseController.java | 11 + .../unilife/controller/PostController.java | 17 +- .../controller/ResourceController.java | 6 +- .../unilife/controller/UserController.java | 22 + .../unilife/interceptor/AdminInterceptor.java | 95 ++++ .../unilife/mapper/AiChatSessionMapper.java | 70 +++ .../com/unilife/mapper/CategoryMapper.java | 32 ++ .../com/unilife/mapper/CommentMapper.java | 38 ++ .../java/com/unilife/mapper/CourseMapper.java | 8 + .../java/com/unilife/mapper/PostMapper.java | 79 +++ .../unilife/mapper/ResourceLikeMapper.java | 39 ++ .../com/unilife/mapper/ResourceMapper.java | 49 ++ .../java/com/unilife/mapper/UserMapper.java | 94 ++++ .../unilife/model/dto/AiCreateSessionDTO.java | 20 + .../unilife/model/dto/AiSendMessageDTO.java | 37 ++ .../unilife/model/dto/AiUpdateSessionDTO.java | 15 + .../unilife/model/dto/CreateCourseDTO.java | 15 +- .../unilife/model/dto/CreateScheduleDTO.java | 2 +- .../java/com/unilife/model/dto/LoginDTO.java | 2 +- .../unilife/model/dto/UpdateProfileDTO.java | 3 +- .../unilife/model/entity/AiChatSession.java | 41 ++ .../java/com/unilife/model/entity/Course.java | 5 + .../com/unilife/model/entity/Schedule.java | 2 +- .../unilife/model/vo/AiCreateSessionVO.java | 20 + .../unilife/model/vo/AiMessageHistoryVO.java | 27 + .../com/unilife/model/vo/AiMessageVO.java | 30 ++ .../com/unilife/model/vo/AiSessionListVO.java | 22 + .../com/unilife/model/vo/AiSessionVO.java | 31 ++ .../java/com/unilife/model/vo/CourseVO.java | 5 + .../java/com/unilife/model/vo/PostListVO.java | 10 + .../java/com/unilife/model/vo/PostVO.java | 6 +- .../java/com/unilife/model/vo/ScheduleVO.java | 2 +- .../com/unilife/service/AdminService.java | 93 ++++ .../service/AiChatSessionHistoryService.java | 59 +++ .../java/com/unilife/service/AiService.java | 69 +++ .../com/unilife/service/CourseService.java | 8 + .../java/com/unilife/service/PostService.java | 14 +- .../com/unilife/service/ResourceService.java | 5 +- .../java/com/unilife/service/UserService.java | 14 + .../service/impl/AdminServiceImpl.java | 356 +++++++++++++ .../impl/AiChatSessionHistoryServiceImpl.java | 187 +++++++ .../unilife/service/impl/AiServiceImpl.java | 358 +++++++++++++ .../service/impl/CommentServiceImpl.java | 93 ++-- .../service/impl/CourseServiceImpl.java | 54 +- .../service/impl/PdfVectorAsyncService.java | 112 ++++ .../unilife/service/impl/PostServiceImpl.java | 150 +++++- .../service/impl/ResourceServiceImpl.java | 128 +++-- .../unilife/service/impl/UserServiceImpl.java | 215 +++++++- .../main/java/com/unilife/utils/JwtUtil.java | 36 +- .../java/com/unilife/utils/OssService.java | 139 +++++ .../com/unilife/utils/ThumbnailService.java | 0 .../src/main/resources/application.yml | 81 ++- .../main/resources/db/rebuild-database.sql | 489 ++++++++++++++++++ .../resources/mappers/AiChatSessionMapper.xml | 87 ++++ .../main/resources/mappers/CategoryMapper.xml | 46 ++ .../main/resources/mappers/CommentMapper.xml | 61 +++ .../main/resources/mappers/CourseMapper.xml | 20 +- .../src/main/resources/mappers/PostMapper.xml | 137 ++++- .../resources/mappers/ResourceLikeMapper.xml | 25 + .../main/resources/mappers/ResourceMapper.xml | 63 +++ .../src/main/resources/mappers/UserMapper.xml | 160 +++++- .../src/main/resources/schema-mysql.sql | 24 + .../java/com/unilife/config/TestConfig.java | 80 +++ .../controller/PostControllerTest.java | 169 ++++++ .../service/AutoTitleGenerationTest.java | 63 +++ .../com/unilife/service/PostServiceTest.java | 287 ++++++++++ .../unilife/service/ResourceServiceTest.java | 348 +++++++++++++ .../unilife/service/ScheduleServiceTest.java | 370 +++++++++++++ .../com/unilife/service/UserServiceTest.java | 438 ++++++++++++++++ .../com/unilife/utils/TestDataBuilder.java | 117 +++++ .../src/test/resources/application-test.yml | 66 +++ 83 files changed, 6658 insertions(+), 167 deletions(-) create mode 100644 unilife-server/CONFIGURATION.md create mode 100644 unilife-server/SIMPLIFIED_CHAT_HISTORY.md create mode 100644 unilife-server/env.example create mode 100644 unilife-server/src/main/java/com/unilife/common/constant/Prompt.java create mode 100644 unilife-server/src/main/java/com/unilife/config/AiConfig.java create mode 100644 unilife-server/src/main/java/com/unilife/config/OssConfig.java create mode 100644 unilife-server/src/main/java/com/unilife/controller/AdminController.java create mode 100644 unilife-server/src/main/java/com/unilife/controller/AiController.java create mode 100644 unilife-server/src/main/java/com/unilife/interceptor/AdminInterceptor.java create mode 100644 unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java create mode 100644 unilife-server/src/main/java/com/unilife/mapper/ResourceLikeMapper.java create mode 100644 unilife-server/src/main/java/com/unilife/model/dto/AiCreateSessionDTO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/dto/AiSendMessageDTO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/dto/AiUpdateSessionDTO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java create mode 100644 unilife-server/src/main/java/com/unilife/model/vo/AiCreateSessionVO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/vo/AiMessageHistoryVO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/vo/AiMessageVO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/vo/AiSessionListVO.java create mode 100644 unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java create mode 100644 unilife-server/src/main/java/com/unilife/service/AdminService.java create mode 100644 unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java create mode 100644 unilife-server/src/main/java/com/unilife/service/AiService.java create mode 100644 unilife-server/src/main/java/com/unilife/service/impl/AdminServiceImpl.java create mode 100644 unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java create mode 100644 unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java create mode 100644 unilife-server/src/main/java/com/unilife/service/impl/PdfVectorAsyncService.java create mode 100644 unilife-server/src/main/java/com/unilife/utils/OssService.java create mode 100644 unilife-server/src/main/java/com/unilife/utils/ThumbnailService.java create mode 100644 unilife-server/src/main/resources/db/rebuild-database.sql create mode 100644 unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml create mode 100644 unilife-server/src/main/resources/mappers/ResourceLikeMapper.xml create mode 100644 unilife-server/src/main/resources/schema-mysql.sql create mode 100644 unilife-server/src/test/java/com/unilife/config/TestConfig.java create mode 100644 unilife-server/src/test/java/com/unilife/controller/PostControllerTest.java create mode 100644 unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java create mode 100644 unilife-server/src/test/java/com/unilife/service/PostServiceTest.java create mode 100644 unilife-server/src/test/java/com/unilife/service/ResourceServiceTest.java create mode 100644 unilife-server/src/test/java/com/unilife/service/ScheduleServiceTest.java create mode 100644 unilife-server/src/test/java/com/unilife/service/UserServiceTest.java create mode 100644 unilife-server/src/test/java/com/unilife/utils/TestDataBuilder.java create mode 100644 unilife-server/src/test/resources/application-test.yml diff --git a/unilife-server/.gitignore b/unilife-server/.gitignore index 549e00a..f30b648 100644 --- a/unilife-server/.gitignore +++ b/unilife-server/.gitignore @@ -31,3 +31,14 @@ build/ ### VS Code ### .vscode/ + +### 敏感配置文件 ### +application-local.yml +application-dev.yml +application-prod.yml +.env +*.env +config/application-local.yml +src/main/resources/application-local.yml +src/main/resources/application-dev.yml +src/main/resources/application-prod.yml diff --git a/unilife-server/CONFIGURATION.md b/unilife-server/CONFIGURATION.md new file mode 100644 index 0000000..317eb18 --- /dev/null +++ b/unilife-server/CONFIGURATION.md @@ -0,0 +1,87 @@ +# 项目配置说明 + +## 环境配置 + +本项目使用环境变量和本地配置文件来管理敏感信息,确保安全性。 + +### 1. 本地开发环境配置 + +创建 `src/main/resources/application-local.yml` 文件(已在.gitignore中忽略): + +```yaml +# 本地开发环境配置 +aliyun: + oss: + endpoint: your-endpoint + accessKeyId: your-access-key-id + accessKeySecret: your-access-key-secret + bucketName: your-bucket-name + urlPrefix: https://your-bucket-name.oss-region.aliyuncs.com/ +``` + +### 2. 环境变量配置 + +复制 `env.example` 为 `.env` 并填入真实配置: + +```bash +cp env.example .env +``` + +然后编辑 `.env` 文件,填入您的真实配置信息。 + +### 3. 启动应用 + +#### 方式1:使用本地配置文件 +```bash +java -jar app.jar --spring.profiles.active=local +``` + +#### 方式2:使用环境变量 +```bash +# 设置环境变量 +export ALIYUN_OSS_ENDPOINT=your-endpoint +export ALIYUN_OSS_ACCESS_KEY_ID=your-access-key-id +export ALIYUN_OSS_ACCESS_KEY_SECRET=your-access-key-secret +export ALIYUN_OSS_BUCKET_NAME=your-bucket-name +export ALIYUN_OSS_URL_PREFIX=https://your-bucket-name.oss-region.aliyuncs.com/ + +# 启动应用 +java -jar app.jar +``` + +## 阿里云OSS配置 + +### 1. 创建OSS Bucket +1. 登录阿里云控制台 +2. 进入对象存储OSS服务 +3. 创建Bucket,选择合适的地域和存储类型 +4. 配置访问权限(推荐私有读写) + +### 2. 获取AccessKey +1. 进入阿里云控制台 +2. 点击右上角头像 -> AccessKey管理 +3. 创建AccessKey(建议使用RAM子账号) +4. 为RAM用户授予OSS相关权限 + +### 3. 配置跨域访问(CORS) +在OSS控制台设置CORS规则: +- 来源:您的前端域名 +- 允许Methods:GET, POST, PUT, DELETE, HEAD +- 允许Headers:* +- 暴露Headers:ETag, x-oss-request-id + +## 安全注意事项 + +1. **永远不要将AccessKey提交到代码仓库** +2. **使用RAM子账号,最小权限原则** +3. **定期轮换AccessKey** +4. **启用OSS访问日志监控** +5. **配置适当的Bucket策略** + +## 生产环境部署 + +生产环境建议使用以下方式之一: + +1. **容器环境变量**(Docker/Kubernetes) +2. **云服务商的密钥管理服务** +3. **专门的配置中心**(如Nacos、Apollo) \ No newline at end of file diff --git a/unilife-server/SIMPLIFIED_CHAT_HISTORY.md b/unilife-server/SIMPLIFIED_CHAT_HISTORY.md new file mode 100644 index 0000000..92520cf --- /dev/null +++ b/unilife-server/SIMPLIFIED_CHAT_HISTORY.md @@ -0,0 +1,72 @@ +# 简化的AI聊天会话历史方案 + +## 现状分析 + +您的项目已经有了完整的Spring AI ChatMemory + MySQL实现: +- ✅ Spring AI自动将消息存储到 `SPRING_AI_CHAT_MEMORY` 表 +- ✅ 自动会话记忆功能(20条消息窗口) +- ✅ 消息持久化到MySQL + +## 缺失的功能 + +Spring AI ChatMemory 专注于消息存储,但缺少: +- ❌ 会话列表管理 +- ❌ 会话标题管理 +- ❌ 会话创建时间等元数据 + +## 建议的简化方案 + +### 1. 保留的表结构 + +只需要一个会话管理表: + +```sql +-- 会话元数据管理表(补充Spring AI ChatMemory) +CREATE TABLE IF NOT EXISTS ai_chat_sessions ( + id VARCHAR(64) PRIMARY KEY COMMENT '会话ID', + user_id BIGINT NULL COMMENT '用户ID(可选)', + title VARCHAR(200) NOT NULL DEFAULT '新对话' COMMENT '会话标题', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### 2. 删除冗余的表 + +可以删除: +- `ai_chat_messages_history` 表(与 SPRING_AI_CHAT_MEMORY 重复) + +### 3. 简化的服务层 + +只需要会话元数据管理,消息历史直接从 `SPRING_AI_CHAT_MEMORY` 查询: + +```java +// 获取会话消息历史 - 直接查询Spring AI表 +@Override +public Result getSessionMessages(String sessionId, Integer page, Integer size) { + // 直接从Spring AI ChatMemory获取 + List messages = chatMemory.get(sessionId); + + // 转换为VO并返回 + // ... 转换逻辑 +} +``` + +### 4. 保留的核心功能 + +- 会话列表管理 +- 会话标题管理 +- 会话创建/删除 +- 从 SPRING_AI_CHAT_MEMORY 表直接查询消息历史 + +## 实际需要的修改 + +1. **删除冗余表**: 移除 `ai_chat_messages_history` +2. **简化服务**: 移除消息同步逻辑,直接使用Spring AI ChatMemory +3. **保留会话管理**: 只管理会话元数据 + +## 结论 + +您的担心是对的,大部分功能确实是冗余的。Spring AI ChatMemory已经提供了强大的消息存储和记忆功能,我们只需要补充会话元数据管理即可。 \ No newline at end of file diff --git a/unilife-server/env.example b/unilife-server/env.example new file mode 100644 index 0000000..43acfcc --- /dev/null +++ b/unilife-server/env.example @@ -0,0 +1,28 @@ +# 环境变量配置示例 +# 复制此文件为 .env 并填入真实配置 + +# 阿里云OSS配置 +ALIYUN_OSS_ENDPOINT=your-endpoint +ALIYUN_OSS_ACCESS_KEY_ID=your-access-key-id +ALIYUN_OSS_ACCESS_KEY_SECRET=your-access-key-secret +ALIYUN_OSS_BUCKET_NAME=your-bucket-name +ALIYUN_OSS_URL_PREFIX=https://your-bucket-name.oss-region.aliyuncs.com/ + +# 数据库配置 +DB_URL=jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 +DB_USERNAME=root +DB_PASSWORD=123456 + +# Redis配置 +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +# JWT配置 +JWT_SECRET=qwertyuiopasdfghjklzxcvbnm +JWT_EXPIRATION=86400 + +# 邮箱配置 +MAIL_HOST=smtp.163.com +MAIL_PORT=465 +MAIL_USERNAME=your-email@163.com +MAIL_PASSWORD=your-auth-code \ No newline at end of file diff --git a/unilife-server/pom.xml b/unilife-server/pom.xml index b8029f4..ac49f34 100644 --- a/unilife-server/pom.xml +++ b/unilife-server/pom.xml @@ -15,6 +15,7 @@ 3.4.3 UTF-8 UTF-8 + 1.0.0 @@ -26,6 +27,13 @@ pom import + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + @@ -123,10 +131,86 @@ spring-boot-starter-test test + + + + org.springframework.boot + spring-boot-test-autoconfigure + test + + + + + org.mockito + mockito-core + test + + + + + com.h2database + h2 + test + + + + + org.testcontainers + testcontainers + 1.19.0 + test + + + org.testcontainers + mysql + 1.19.0 + test + + + org.testcontainers + junit-jupiter + 1.19.0 + test + + org.springframework.boot spring-boot-starter-actuator + + + + com.aliyun.oss + aliyun-sdk-oss + 3.15.0 + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + + org.springframework.ai + spring-ai-starter-model-chat-memory-repository-jdbc + + + + + org.springframework.ai + spring-ai-starter-vector-store-chroma + + + + org.springframework.ai + spring-ai-pdf-document-reader + + + + org.springframework.ai + spring-ai-advisors-vector-store + @@ -140,6 +224,7 @@ ${java.version} ${java.version} UTF-8 + true @@ -148,6 +233,15 @@ org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} + + + + + org.projectlombok + lombok + + + diff --git a/unilife-server/src/main/java/com/unilife/common/constant/Prompt.java b/unilife-server/src/main/java/com/unilife/common/constant/Prompt.java new file mode 100644 index 0000000..8381a7e --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/common/constant/Prompt.java @@ -0,0 +1,37 @@ +package com.unilife.common.constant; + +public class Prompt { + public static final String Prompt = """ + # 角色 + 你是一个乐于助人的AI学习助手,专门为UniLife论坛的学生提供服务。你的主要任务是根据论坛内已有的学习资料和讨论内容,回答学生们提出的问题。 + + # 指示 + 请严格按照以下步骤和指南来回答用户的问题: + + 1. **理解用户问题**: + * 仔细分析用户提出的问题,理解其核心意图。 + + 2. **参考提供的上下文信息**: + * 我会为你提供相关的背景资料片段,这些片段来源于[你的学生论坛名称]的资源内容。 + * **你必须优先并主要依据这些提供的背景资料来构建你的回答。** + + 3. **回答构建**: + * 如果提供的背景资料中包含与用户问题直接相关的信息,请从中提取关键内容,清晰、准确地回答用户的问题。 + * 尽量用简洁易懂的语言进行解释。 + * 如果背景资料包含多个相关片段,请综合它们的信息进行回答。 + + 4. **处理上下文信息不足或不相关的情况**: + * **如果提供的背景资料为空,或者与用户问题完全不相关,或者不足以回答用户的问题,请不要臆断或编造信息。** + * 在这种情况下,你可以礼貌地告知用户: + * 例如:“抱歉,我在的现有资料中暂时没有找到与您问题直接相关的信息。” + * 你可以进一步建议:“您可以尝试换个问法,或者提供更多关键词,也许能帮助我更好地找到相关内容。” + * 或者:“目前论坛内可能还没有涵盖这个问题,您可以向其他同学或老师请教。” + * **除非用户的问题是明确的常识性、通用性知识(例如“中国的首都是哪里?”或进行简单的数学计算),并且你确信你的通用知识库可以准确回答,否则请避免在没有论坛上下文的情况下回答专业性或特定领域的问题。** + * 如果用户的问题明显与学习或论坛内容无关(例如闲聊),你可以在用你自身的知识回答后委婉地引导用户回到学习主题,或者表明你主要负责解答与论坛资源相关的问题。 + + 5. **输出格式与风格**: + * 保持友好、耐心、专业的语气。 + * 回答应结构清晰,易于阅读。 + * 避免使用过于复杂或模糊的措辞。 + """; +} diff --git a/unilife-server/src/main/java/com/unilife/common/result/Result.java b/unilife-server/src/main/java/com/unilife/common/result/Result.java index 1e91ff2..cc21d68 100644 --- a/unilife-server/src/main/java/com/unilife/common/result/Result.java +++ b/unilife-server/src/main/java/com/unilife/common/result/Result.java @@ -53,6 +53,10 @@ public class Result{ return new Result<>(code, message, null); } + public static Result error(String message) { + return new Result<>(500, message, null); + } + public static Result error(T data,String message){ return new Result<>(200,message,null); } diff --git a/unilife-server/src/main/java/com/unilife/config/AiConfig.java b/unilife-server/src/main/java/com/unilife/config/AiConfig.java new file mode 100644 index 0000000..c3a37b9 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/config/AiConfig.java @@ -0,0 +1,100 @@ +package com.unilife.config; + +import com.unilife.common.constant.Prompt; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; +import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.MessageWindowChatMemory; + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +public class AiConfig { + + /** + * 是否启用自动生成会话标题功能 + */ + @Value("${app.ai.auto-title.enabled:true}") + private boolean autoTitleEnabled; + + /** + * 标题生成策略:simple(简单算法) 或 ai(AI生成) + */ + @Value("${app.ai.auto-title.strategy:simple}") + private String titleGenerationStrategy; + + /** + * 配置ChatMemory,使用JDBC存储库实现持久化 + */ + @Bean + public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) { + return MessageWindowChatMemory.builder() + .chatMemoryRepository(chatMemoryRepository) + .maxMessages(20) // 保留最近20条消息作为上下文 + .build(); + } + + /** + * 配置异步任务执行器,专门用于PDF向量化处理 + */ + @Bean("pdfVectorTaskExecutor") + public Executor pdfVectorTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); // 核心线程数 + executor.setMaxPoolSize(5); // 最大线程数 + executor.setQueueCapacity(100); // 队列容量 + executor.setThreadNamePrefix("pdf-vector-"); + executor.setKeepAliveSeconds(60); // 线程空闲时间 + executor.initialize(); + return executor; + } + + + /** + * 配置ChatClient,集成Chat Memory功能 + */ + @Bean + public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory, VectorStore vectorStore) { + return ChatClient.builder(model) + .defaultSystem(Prompt.Prompt) // 设置默认系统提示 + .defaultAdvisors( + new SimpleLoggerAdvisor(), + MessageChatMemoryAdvisor.builder(chatMemory).build(), + QuestionAnswerAdvisor.builder(vectorStore) + .searchRequest(SearchRequest.builder() + .similarityThreshold(0.5d) + .topK(5) + .build()) + .build() + ) + .build(); + } + + /** + * 获取自动标题生成配置 + */ + public boolean isAutoTitleEnabled() { + return autoTitleEnabled; + } + + /** + * 获取标题生成策略 + */ + public String getTitleGenerationStrategy() { + return titleGenerationStrategy; + } +} diff --git a/unilife-server/src/main/java/com/unilife/config/OssConfig.java b/unilife-server/src/main/java/com/unilife/config/OssConfig.java new file mode 100644 index 0000000..792e539 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/config/OssConfig.java @@ -0,0 +1,39 @@ +package com.unilife.config; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OssConfig { + + @Value("${aliyun.oss.endpoint}") + private String endpoint; + + @Value("${aliyun.oss.accessKeyId}") + private String accessKeyId; + + @Value("${aliyun.oss.accessKeySecret}") + private String accessKeySecret; + + @Value("${aliyun.oss.bucketName}") + private String bucketName; + + @Value("${aliyun.oss.urlPrefix}") + private String urlPrefix; + + @Bean + public OSS ossClient() { + return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + } + + public String getBucketName() { + return bucketName; + } + + public String getUrlPrefix() { + return urlPrefix; + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java b/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java index 2484e46..493831c 100644 --- a/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java +++ b/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java @@ -1,32 +1,67 @@ package com.unilife.config; +import com.unilife.interceptor.AdminInterceptor; import com.unilife.interceptor.JwtInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.io.File; + @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private JwtInterceptor jwtInterceptor; + + @Autowired + private AdminInterceptor adminInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { + // 管理员权限拦截器 - 优先级最高 + registry.addInterceptor(adminInterceptor) + .addPathPatterns("/admin/**") + .order(1); + + // JWT拦截器 registry.addInterceptor(jwtInterceptor).addPathPatterns("/**") .excludePathPatterns( + // 用户登录注册相关 "/users/login", "/users/register", "/users/code", "/users/login/code", + + // 静态资源访问 + "/api/files/**", + + // 管理员接口(由AdminInterceptor处理) + "/admin/**", + + // Swagger文档相关 "/swagger-resources/**", "/v3/api-docs/**", "/doc.html", "/webjars/**", "/favicon.ico", "/knife4j/**" - ); + ) + .order(2); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // 配置上传文件的访问路径 + String uploadPath = new File("uploads").getAbsolutePath(); + registry.addResourceHandler("/api/files/**") + .addResourceLocations("file:" + uploadPath + File.separator); + + // 直接映射到uploads/resources目录 + registry.addResourceHandler("/api/resources/**") + .addResourceLocations("file:" + new File("uploads/resources").getAbsolutePath() + File.separator); } @Override diff --git a/unilife-server/src/main/java/com/unilife/controller/AdminController.java b/unilife-server/src/main/java/com/unilife/controller/AdminController.java new file mode 100644 index 0000000..af2ffde --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/controller/AdminController.java @@ -0,0 +1,146 @@ +package com.unilife.controller; + +import com.unilife.common.result.Result; +import com.unilife.service.AdminService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/admin") +@Tag(name = "管理员接口", description = "后台管理相关接口") +public class AdminController { + + @Autowired + private AdminService adminService; + + @Operation(summary = "获取系统统计数据") + @GetMapping("/stats") + public Result getSystemStats() { + return adminService.getSystemStats(); + } + + @Operation(summary = "获取用户列表") + @GetMapping("/users") + public Result getUserList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Integer role, + @RequestParam(required = false) Integer status) { + return adminService.getUserList(page, size, keyword, role, status); + } + + @Operation(summary = "更新用户状态") + @PutMapping("/users/{userId}/status") + public Result updateUserStatus(@PathVariable Long userId, @RequestBody Map request) { + Integer status = request.get("status"); + return adminService.updateUserStatus(userId, status); + } + + @Operation(summary = "更新用户角色") + @PutMapping("/users/{userId}/role") + public Result updateUserRole(@PathVariable Long userId, @RequestBody Map request) { + Integer role = request.get("role"); + return adminService.updateUserRole(userId, role); + } + + @Operation(summary = "删除用户") + @DeleteMapping("/users/{userId}") + public Result deleteUser(@PathVariable Long userId) { + return adminService.deleteUser(userId); + } + + @Operation(summary = "获取帖子列表") + @GetMapping("/posts") + public Result getPostList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long categoryId, + @RequestParam(required = false) Integer status) { + return adminService.getPostList(page, size, keyword, categoryId, status); + } + + @Operation(summary = "更新帖子状态") + @PutMapping("/posts/{postId}/status") + public Result updatePostStatus(@PathVariable Long postId, @RequestBody Map request) { + Integer status = request.get("status"); + return adminService.updatePostStatus(postId, status); + } + + @Operation(summary = "删除帖子") + @DeleteMapping("/posts/{postId}") + public Result deletePost(@PathVariable Long postId) { + return adminService.deletePost(postId); + } + + @Operation(summary = "永久删除帖子") + @DeleteMapping("/posts/{postId}/permanent") + public Result permanentDeletePost(@PathVariable Long postId) { + return adminService.permanentDeletePost(postId); + } + + @Operation(summary = "获取评论列表") + @GetMapping("/comments") + public Result getCommentList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long postId, + @RequestParam(required = false) Integer status) { + return adminService.getCommentList(page, size, keyword, postId, status); + } + + @Operation(summary = "删除评论") + @DeleteMapping("/comments/{commentId}") + public Result deleteComment(@PathVariable Long commentId) { + return adminService.deleteComment(commentId); + } + + @Operation(summary = "获取分类列表") + @GetMapping("/categories") + public Result getCategoryList() { + return adminService.getCategoryList(); + } + + @Operation(summary = "创建分类") + @PostMapping("/categories") + public Result createCategory(@RequestBody Map request) { + return adminService.createCategory(request); + } + + @Operation(summary = "更新分类") + @PutMapping("/categories/{categoryId}") + public Result updateCategory(@PathVariable Long categoryId, @RequestBody Map request) { + return adminService.updateCategory(categoryId, request); + } + + @Operation(summary = "删除分类") + @DeleteMapping("/categories/{categoryId}") + public Result deleteCategory(@PathVariable Long categoryId) { + return adminService.deleteCategory(categoryId); + } + + @Operation(summary = "获取资源列表") + @GetMapping("/resources") + public Result getResourceList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long categoryId, + @RequestParam(required = false) Integer status) { + return adminService.getResourceList(page, size, keyword, categoryId, status); + } + + @Operation(summary = "删除资源") + @DeleteMapping("/resources/{resourceId}") + public Result deleteResource(@PathVariable Long resourceId) { + return adminService.deleteResource(resourceId); + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/controller/AiController.java b/unilife-server/src/main/java/com/unilife/controller/AiController.java new file mode 100644 index 0000000..8c53bf7 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/controller/AiController.java @@ -0,0 +1,94 @@ +package com.unilife.controller; + +import com.unilife.common.result.Result; +import com.unilife.model.dto.AiCreateSessionDTO; +import com.unilife.model.dto.AiSendMessageDTO; +import com.unilife.model.dto.AiUpdateSessionDTO; +import com.unilife.model.vo.AiCreateSessionVO; +import com.unilife.model.vo.AiMessageHistoryVO; +import com.unilife.model.vo.AiSessionListVO; +import com.unilife.service.AiService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +@RestController +@Slf4j +@RequestMapping("/ai") +@Tag(name = "AI辅助学习") +@RequiredArgsConstructor +public class AiController { + + private final AiService aiService; + + @Operation(summary = "发送消息给AI") + @RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8") + public Flux sendMessage( + @RequestParam("prompt") String prompt, + @RequestParam(value = "sessionId", required = false) String sessionId) { + log.info("发送消息给AI: {}", prompt); + + AiSendMessageDTO sendMessageDTO = new AiSendMessageDTO(); + sendMessageDTO.setMessage(prompt); + sendMessageDTO.setSessionId(sessionId); + + return aiService.sendMessage(sendMessageDTO); + } + + @Operation(summary = "获取聊天会话列表") + @GetMapping("/sessions") + public Result getSessionList( + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer size) { + + log.info("获取会话列表,页码: {}, 每页大小: {}", page, size); + return aiService.getSessionList(page, size); + } + + @Operation(summary = "获取会话消息历史") + @GetMapping("/sessions/{sessionId}/messages") + public Result getSessionMessages( + @Parameter(description = "会话ID") @PathVariable String sessionId, + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页大小") @RequestParam(defaultValue = "50") Integer size) { + + log.info("获取会话消息历史,会话ID: {}, 页码: {}, 每页大小: {}", sessionId, page, size); + return aiService.getSessionMessages(sessionId, page, size); + } + + @Operation(summary = "创建聊天会话") + @PostMapping("/sessions") + public Result createSession(@RequestBody AiCreateSessionDTO createSessionDTO) { + log.info("创建聊天会话: {}", createSessionDTO.getSessionId()); + return aiService.createSession(createSessionDTO); + } + + @Operation(summary = "更新会话标题") + @PutMapping("/sessions/{sessionId}") + public Result updateSessionTitle( + @Parameter(description = "会话ID") @PathVariable String sessionId, + @RequestBody AiUpdateSessionDTO updateSessionDTO) { + + log.info("更新会话标题,会话ID: {}, 新标题: {}", sessionId, updateSessionDTO.getTitle()); + return aiService.updateSessionTitle(sessionId, updateSessionDTO); + } + + @Operation(summary = "清空会话消息") + @DeleteMapping("/sessions/{sessionId}/messages") + public Result clearSessionMessages(@Parameter(description = "会话ID") @PathVariable String sessionId) { + log.info("清空会话消息,会话ID: {}", sessionId); + return aiService.clearSessionMessages(sessionId); + } + + @Operation(summary = "删除会话") + @DeleteMapping("/sessions/{sessionId}") + public Result deleteSession(@Parameter(description = "会话ID") @PathVariable String sessionId) { + log.info("删除会话,会话ID: {}", sessionId); + return aiService.deleteSession(sessionId); + } +} diff --git a/unilife-server/src/main/java/com/unilife/controller/CourseController.java b/unilife-server/src/main/java/com/unilife/controller/CourseController.java index 92f807a..47b2eb5 100644 --- a/unilife-server/src/main/java/com/unilife/controller/CourseController.java +++ b/unilife-server/src/main/java/com/unilife/controller/CourseController.java @@ -63,6 +63,17 @@ public class CourseController { return courseService.getCourseListByDayOfWeek(userId, dayOfWeek); } + @Operation(summary = "获取用户在指定学期的课程") + @GetMapping("/semester/{semester}") + public Result getCourseListBySemester(@PathVariable("semester") String semester) { + // 从当前上下文获取用户ID + Long userId = BaseContext.getId(); + if (userId == null) { + return Result.error(401, "未登录"); + } + return courseService.getCourseListBySemester(userId, semester); + } + @Operation(summary = "更新课程") @PutMapping("/{id}") public Result updateCourse( diff --git a/unilife-server/src/main/java/com/unilife/controller/PostController.java b/unilife-server/src/main/java/com/unilife/controller/PostController.java index 171eacc..7b80e21 100644 --- a/unilife-server/src/main/java/com/unilife/controller/PostController.java +++ b/unilife-server/src/main/java/com/unilife/controller/PostController.java @@ -42,11 +42,14 @@ public class PostController { @Operation(summary = "获取帖子列表") @GetMapping public Result getPostList( - @RequestParam(value = "category", required = false) Long categoryId, + @RequestParam(value = "categoryId", required = false) Long categoryId, + @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "sort", defaultValue = "latest") String sort) { - return postService.getPostList(categoryId, page, size, sort); + // 从当前上下文获取用户ID,可能为null(未登录用户) + Long userId = BaseContext.getId(); + return postService.getPostList(categoryId, keyword, page, size, sort, userId); } @Operation(summary = "更新帖子") @@ -83,4 +86,14 @@ public class PostController { } return postService.likePost(postId, userId); } + + @Operation(summary = "获取用户的帖子列表") + @GetMapping("/user/{userId}") + public Result getUserPosts( + @PathVariable("userId") Long userId, + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "size", defaultValue = "10") Integer size, + @RequestParam(value = "sort", defaultValue = "latest") String sort) { + return postService.getUserPosts(userId, page, size, sort); + } } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/controller/ResourceController.java b/unilife-server/src/main/java/com/unilife/controller/ResourceController.java index e09e95c..881abe6 100644 --- a/unilife-server/src/main/java/com/unilife/controller/ResourceController.java +++ b/unilife-server/src/main/java/com/unilife/controller/ResourceController.java @@ -53,11 +53,13 @@ public class ResourceController { @GetMapping public Result getResourceList( @RequestParam(value = "category", required = false) Long categoryId, - @RequestParam(value = "user", required = false) Long userId, + @RequestParam(value = "user", required = false) Long uploaderUserId, @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size) { - return resourceService.getResourceList(categoryId, userId, keyword, page, size); + // 从当前上下文获取用户ID,可能为null(未登录用户) + Long currentUserId = BaseContext.getId(); + return resourceService.getResourceList(categoryId, uploaderUserId, keyword, page, size, currentUserId); } @Operation(summary = "更新资源") diff --git a/unilife-server/src/main/java/com/unilife/controller/UserController.java b/unilife-server/src/main/java/com/unilife/controller/UserController.java index 4608806..e05de7f 100644 --- a/unilife-server/src/main/java/com/unilife/controller/UserController.java +++ b/unilife-server/src/main/java/com/unilife/controller/UserController.java @@ -139,4 +139,26 @@ public class UserController { } return userService.updateEmail(userId, emailDTO); } + + @Operation(summary = "获取用户统计数据") + @GetMapping("stats") + public Result getUserStats() { + // 从当前上下文获取用户ID + Long userId = BaseContext.getId(); + if (userId == null) { + return Result.error(401, "未登录"); + } + return userService.getUserStats(userId); + } + + @Operation(summary = "获取用户最近帖子") + @GetMapping("recent-posts") + public Result getUserRecentPosts(@RequestParam(value = "limit", defaultValue = "5") Integer limit) { + // 从当前上下文获取用户ID + Long userId = BaseContext.getId(); + if (userId == null) { + return Result.error(401, "未登录"); + } + return userService.getUserRecentPosts(userId, limit); + } } diff --git a/unilife-server/src/main/java/com/unilife/interceptor/AdminInterceptor.java b/unilife-server/src/main/java/com/unilife/interceptor/AdminInterceptor.java new file mode 100644 index 0000000..cc52ce1 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/interceptor/AdminInterceptor.java @@ -0,0 +1,95 @@ +package com.unilife.interceptor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unilife.common.result.Result; +import com.unilife.mapper.UserMapper; +import com.unilife.model.entity.User; +import com.unilife.utils.JwtUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Slf4j +@Component +public class AdminInterceptor implements HandlerInterceptor { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private UserMapper userMapper; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 预检请求直接放行 + if ("OPTIONS".equals(request.getMethod())) { + return true; + } + + try { + // 获取token + String token = request.getHeader("Authorization"); + if (token == null || !token.startsWith("Bearer ")) { + writeErrorResponse(response, 401, "未登录或token格式错误"); + return false; + } + + token = token.substring(7); // 移除 "Bearer " 前缀 + + // 验证token + if (!jwtUtil.verifyToken(token)) { + writeErrorResponse(response, 401, "token无效或已过期"); + return false; + } + + // 获取用户ID + Long userId = jwtUtil.getUserIdFromToken(token); + if (userId == null) { + writeErrorResponse(response, 401, "无法获取用户信息"); + return false; + } + + // 查询用户信息 + User user = userMapper.getUserById(userId); + if (user == null) { + writeErrorResponse(response, 401, "用户不存在"); + return false; + } + + // 检查用户状态 + if (user.getStatus() != 1) { + writeErrorResponse(response, 403, "账号已被禁用"); + return false; + } + + // 检查是否为管理员 + if (user.getRole() != 2) { + writeErrorResponse(response, 403, "权限不足,需要管理员权限"); + return false; + } + + // 将用户信息存储到request中,供后续使用 + request.setAttribute("currentUser", user); + return true; + + } catch (Exception e) { + log.error("管理员权限验证失败", e); + writeErrorResponse(response, 500, "权限验证失败"); + return false; + } + } + + private void writeErrorResponse(HttpServletResponse response, int code, String message) throws Exception { + response.setStatus(200); // HTTP状态码设为200,错误信息在响应体中 + response.setContentType("application/json;charset=UTF-8"); + + Result result = Result.error(code, message); + ObjectMapper objectMapper = new ObjectMapper(); + String jsonResponse = objectMapper.writeValueAsString(result); + + response.getWriter().write(jsonResponse); + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java b/unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java new file mode 100644 index 0000000..fcc5f88 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java @@ -0,0 +1,70 @@ +package com.unilife.mapper; + +import com.unilife.model.entity.AiChatSession; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * AI聊天会话Mapper + */ +@Mapper +public interface AiChatSessionMapper { + + /** + * 插入会话 + */ + int insert(AiChatSession session); + + /** + * 根据ID查询会话 + */ + AiChatSession selectById(@Param("id") String id); + + /** + * 根据用户ID分页查询会话列表 + */ + List selectByUserId(@Param("userId") Long userId, + @Param("offset") int offset, + @Param("limit") int limit); + + /** + * 查询匿名会话列表(用户ID为空) + */ + List selectAnonymousSessions(@Param("offset") int offset, + @Param("limit") int limit); + + /** + * 统计用户会话总数 + */ + long countByUserId(@Param("userId") Long userId); + + /** + * 统计匿名会话总数 + */ + long countAnonymousSessions(); + + /** + * 更新会话标题 + */ + int updateTitle(@Param("id") String id, @Param("title") String title); + + /** + * 更新会话的最后消息时间和消息数量 + */ + int updateMessageInfo(@Param("id") String id, + @Param("lastMessageTime") LocalDateTime lastMessageTime, + @Param("messageCount") Integer messageCount); + + /** + * 删除会话 + */ + int deleteById(@Param("id") String id); + + /** + * 批量删除过期会话 + */ + int deleteExpiredSessions(@Param("expireTime") LocalDateTime expireTime); +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/CategoryMapper.java b/unilife-server/src/main/java/com/unilife/mapper/CategoryMapper.java index 68fa6ae..93eb913 100644 --- a/unilife-server/src/main/java/com/unilife/mapper/CategoryMapper.java +++ b/unilife-server/src/main/java/com/unilife/mapper/CategoryMapper.java @@ -49,4 +49,36 @@ public interface CategoryMapper { * @return 分类总数 */ Integer getCount(@Param("status") Byte status); + + // ========== 管理员后台相关方法 ========== + + /** + * 获取分类总数(管理员用) + */ + int getTotalCount(); + + /** + * 获取所有分类(管理员用) + */ + List getAllCategories(); + + /** + * 根据ID获取分类(管理员用) + */ + Category getCategoryById(Long id); + + /** + * 插入分类(管理员用) + */ + void insertCategory(Category category); + + /** + * 更新分类(管理员用) + */ + void updateCategory(Category category); + + /** + * 删除分类(管理员用) + */ + void deleteCategory(Long categoryId); } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/CommentMapper.java b/unilife-server/src/main/java/com/unilife/mapper/CommentMapper.java index 3021fec..9162102 100644 --- a/unilife-server/src/main/java/com/unilife/mapper/CommentMapper.java +++ b/unilife-server/src/main/java/com/unilife/mapper/CommentMapper.java @@ -68,4 +68,42 @@ public interface CommentMapper { * @param id 评论ID */ void decrementLikeCount(Long id); + + // ========== 管理员后台相关方法 ========== + + /** + * 获取评论总数 + */ + int getTotalCount(); + + /** + * 获取今日新增评论数 + */ + int getNewCommentCountToday(); + + /** + * 根据ID获取评论(管理员用) + */ + Comment getCommentById(Long id); + + /** + * 管理员获取评论列表(支持筛选和分页) + */ + List getAdminCommentList(@Param("offset") int offset, + @Param("size") int size, + @Param("keyword") String keyword, + @Param("postId") Long postId, + @Param("status") Integer status); + + /** + * 管理员获取评论总数(支持筛选) + */ + int getAdminCommentCount(@Param("keyword") String keyword, + @Param("postId") Long postId, + @Param("status") Integer status); + + /** + * 删除评论(管理员用) + */ + void deleteComment(Long commentId); } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/CourseMapper.java b/unilife-server/src/main/java/com/unilife/mapper/CourseMapper.java index 2ef6aab..6b2e271 100644 --- a/unilife-server/src/main/java/com/unilife/mapper/CourseMapper.java +++ b/unilife-server/src/main/java/com/unilife/mapper/CourseMapper.java @@ -52,6 +52,14 @@ public interface CourseMapper { */ List getListByUserIdAndDayOfWeek(@Param("userId") Long userId, @Param("dayOfWeek") Byte dayOfWeek); + /** + * 获取用户在指定学期的课程 + * @param userId 用户ID + * @param semester 学期(如:2023-1) + * @return 课程列表 + */ + List getListByUserIdAndSemester(@Param("userId") Long userId, @Param("semester") String semester); + /** * 检查用户在指定时间段是否有课程冲突 * @param userId 用户ID diff --git a/unilife-server/src/main/java/com/unilife/mapper/PostMapper.java b/unilife-server/src/main/java/com/unilife/mapper/PostMapper.java index d6ff889..84df3b3 100644 --- a/unilife-server/src/main/java/com/unilife/mapper/PostMapper.java +++ b/unilife-server/src/main/java/com/unilife/mapper/PostMapper.java @@ -79,4 +79,83 @@ public interface PostMapper { * @param id 帖子ID */ void decrementCommentCount(Long id); + + /** + * 获取指定用户的帖子列表 + * @param userId 用户ID + * @param sort 排序方式 + * @return 帖子列表 + */ + List getListByUserId(@Param("userId") Long userId, @Param("sort") String sort); + + /** + * 获取指定用户的帖子总数 + * @param userId 用户ID + * @return 帖子总数 + */ + Integer getCountByUserId(@Param("userId") Long userId); + + /** + * 搜索帖子 + * @param keyword 搜索关键词 + * @param categoryId 分类ID,可为null + * @param sortBy 排序方式 + * @return 帖子列表 + */ + List searchPosts(@Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("sortBy") String sortBy); + + // ========== 管理员后台相关方法 ========== + + /** + * 获取帖子总数 + */ + int getTotalCount(); + + /** + * 获取今日新增帖子数 + */ + int getNewPostCountToday(); + + /** + * 根据ID获取帖子(管理员用) + */ + Post getPostById(Long id); + + /** + * 管理员获取帖子列表(支持筛选和分页) + */ + List getAdminPostList(@Param("offset") int offset, + @Param("size") int size, + @Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("status") Integer status); + + /** + * 管理员获取帖子总数(支持筛选) + */ + int getAdminPostCount(@Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("status") Integer status); + + /** + * 更新帖子状态 + */ + void updatePostStatus(@Param("postId") Long postId, @Param("status") Integer status); + + /** + * 删除帖子(管理员用) + */ + void deletePost(Long postId); + + /** + * 永久删除帖子(物理删除) + */ + void permanentDeletePost(Long postId); + + /** + * 获取指定分类下的帖子数量 + */ + int getCountByCategoryId(Long categoryId); } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/ResourceLikeMapper.java b/unilife-server/src/main/java/com/unilife/mapper/ResourceLikeMapper.java new file mode 100644 index 0000000..5aafaf9 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/mapper/ResourceLikeMapper.java @@ -0,0 +1,39 @@ +package com.unilife.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 资源点赞数据访问层 + */ +@Mapper +public interface ResourceLikeMapper { + /** + * 检查用户是否已点赞资源 + * @param resourceId 资源ID + * @param userId 用户ID + * @return 是否已点赞 + */ + boolean isLiked(@Param("resourceId") Long resourceId, @Param("userId") Long userId); + + /** + * 添加点赞记录 + * @param resourceId 资源ID + * @param userId 用户ID + */ + void insert(@Param("resourceId") Long resourceId, @Param("userId") Long userId); + + /** + * 删除点赞记录 + * @param resourceId 资源ID + * @param userId 用户ID + */ + void delete(@Param("resourceId") Long resourceId, @Param("userId") Long userId); + + /** + * 获取资源的点赞用户数量 + * @param resourceId 资源ID + * @return 点赞数量 + */ + int getLikeCount(@Param("resourceId") Long resourceId); +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/ResourceMapper.java b/unilife-server/src/main/java/com/unilife/mapper/ResourceMapper.java index 03acd97..1bb1876 100644 --- a/unilife-server/src/main/java/com/unilife/mapper/ResourceMapper.java +++ b/unilife-server/src/main/java/com/unilife/mapper/ResourceMapper.java @@ -89,4 +89,53 @@ public interface ResourceMapper { * @return 资源数量 */ Integer getCountByCategoryId(Long categoryId); + + /** + * 搜索资源 + * @param keyword 搜索关键词 + * @param categoryId 分类ID,可为null + * @param sortBy 排序方式 + * @return 资源列表 + */ + List searchResources(@Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("sortBy") String sortBy); + + // ========== 管理员后台相关方法 ========== + + /** + * 获取资源总数 + */ + int getTotalCount(); + + /** + * 获取今日新增资源数 + */ + int getNewResourceCountToday(); + + /** + * 根据ID获取资源(管理员用) + */ + Resource getResourceById(Long id); + + /** + * 管理员获取资源列表(支持筛选和分页) + */ + List getAdminResourceList(@Param("offset") int offset, + @Param("size") int size, + @Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("status") Integer status); + + /** + * 管理员获取资源总数(支持筛选) + */ + int getAdminResourceCount(@Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("status") Integer status); + + /** + * 删除资源(管理员用) + */ + void deleteResource(Long resourceId); } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/mapper/UserMapper.java b/unilife-server/src/main/java/com/unilife/mapper/UserMapper.java index 29f2ffa..288188d 100644 --- a/unilife-server/src/main/java/com/unilife/mapper/UserMapper.java +++ b/unilife-server/src/main/java/com/unilife/mapper/UserMapper.java @@ -5,11 +5,13 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.Date; +import java.util.List; @Mapper public interface UserMapper { void insert(User user); User findByEmail(String email); + User findByUsername(String username); void updateLoginInfo(@Param("userId") Long userId, @Param("ipLocation") String ipLocation, @Param("loginTime") Date loginTime); @@ -21,4 +23,96 @@ public interface UserMapper { void updatePassword(@Param("id") Long id, @Param("newPassword") String newPassword); void updateAvatar(@Param("id") Long id, @Param("avatar") String avatarUrl); void updateEmail(@Param("id") Long id, @Param("email") String email); + + /** + * 搜索用户 + * @param keyword 搜索关键词 + * @return 用户列表 + */ + List searchUsers(@Param("keyword") String keyword); + + /** + * 软删除用户(设置status为0) + * @param id 用户ID + */ + void deleteUser(Long id); + + /** + * 批量删除用户的帖子(软删除) + * @param userId 用户ID + */ + void deleteUserPosts(Long userId); + + /** + * 批量删除用户的评论(软删除) + * @param userId 用户ID + */ + void deleteUserComments(Long userId); + + /** + * 批量删除用户的资源(软删除) + * @param userId 用户ID + */ + void deleteUserResources(Long userId); + + /** + * 批量删除用户的课程 + * @param userId 用户ID + */ + void deleteUserCourses(Long userId); + + /** + * 批量删除用户的日程 + * @param userId 用户ID + */ + void deleteUserSchedules(Long userId); + + /** + * 删除用户的所有点赞记录 + * @param userId 用户ID + */ + void deleteUserLikes(Long userId); + + // ========== 管理员后台相关方法 ========== + + /** + * 获取用户总数 + */ + int getTotalCount(); + + /** + * 获取活跃用户数(最近30天登录) + */ + int getActiveUserCount(); + + /** + * 获取今日新增用户数 + */ + int getNewUserCountToday(); + + /** + * 管理员获取用户列表(支持筛选和分页) + */ + List getAdminUserList(@Param("offset") int offset, + @Param("size") int size, + @Param("keyword") String keyword, + @Param("role") Integer role, + @Param("status") Integer status); + + /** + * 管理员获取用户总数(支持筛选) + */ + int getAdminUserCount(@Param("keyword") String keyword, + @Param("role") Integer role, + @Param("status") Integer status); + + /** + * 更新用户状态 + */ + void updateUserStatus(@Param("userId") Long userId, @Param("status") Integer status); + + /** + * 更新用户角色 + */ + void updateUserRole(@Param("userId") Long userId, @Param("role") Integer role); } diff --git a/unilife-server/src/main/java/com/unilife/model/dto/AiCreateSessionDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/AiCreateSessionDTO.java new file mode 100644 index 0000000..0bf01b6 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/dto/AiCreateSessionDTO.java @@ -0,0 +1,20 @@ +package com.unilife.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiCreateSessionDTO { + /** + * 会话ID(前端生成) + */ + private String sessionId; + + /** + * 会话标题(可选) + */ + private String title; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/dto/AiSendMessageDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/AiSendMessageDTO.java new file mode 100644 index 0000000..bf4bad4 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/dto/AiSendMessageDTO.java @@ -0,0 +1,37 @@ +package com.unilife.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiSendMessageDTO { + /** + * 用户发送的消息内容 + */ + private String message; + + /** + * 会话ID(可选) + */ + private String sessionId; + + /** + * 会话历史记录(可选) + */ + private List conversationHistory; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ConversationMessage { + private String id; + private String role; + private String content; + private String timestamp; + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/dto/AiUpdateSessionDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/AiUpdateSessionDTO.java new file mode 100644 index 0000000..60ee93d --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/dto/AiUpdateSessionDTO.java @@ -0,0 +1,15 @@ +package com.unilife.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiUpdateSessionDTO { + /** + * 更新后的会话标题 + */ + private String title; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java index 65f8a33..bce3205 100644 --- a/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java +++ b/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java @@ -4,8 +4,6 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalTime; - /** * 创建课程的数据传输对象 */ @@ -34,14 +32,14 @@ public class CreateCourseDTO { private Byte dayOfWeek; /** - * 开始时间 + * 开始时间 (格式: "HH:mm:ss") */ - private LocalTime startTime; + private String startTime; /** - * 结束时间 + * 结束时间 (格式: "HH:mm:ss") */ - private LocalTime endTime; + private String endTime; /** * 开始周次 @@ -53,6 +51,11 @@ public class CreateCourseDTO { */ private Byte endWeek; + /** + * 学期(如:2023-1) + */ + private String semester; + /** * 显示颜色 */ diff --git a/unilife-server/src/main/java/com/unilife/model/dto/CreateScheduleDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/CreateScheduleDTO.java index c0e96ca..56d385b 100644 --- a/unilife-server/src/main/java/com/unilife/model/dto/CreateScheduleDTO.java +++ b/unilife-server/src/main/java/com/unilife/model/dto/CreateScheduleDTO.java @@ -46,7 +46,7 @@ public class CreateScheduleDTO { /** * 提醒时间(分钟) */ - private Byte reminder; + private Integer reminder; /** * 显示颜色 diff --git a/unilife-server/src/main/java/com/unilife/model/dto/LoginDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/LoginDTO.java index 57755af..5eaf4c2 100644 --- a/unilife-server/src/main/java/com/unilife/model/dto/LoginDTO.java +++ b/unilife-server/src/main/java/com/unilife/model/dto/LoginDTO.java @@ -9,6 +9,6 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor public class LoginDTO { - private String email; + private String account; // 支持用户名或邮箱 private String password; } diff --git a/unilife-server/src/main/java/com/unilife/model/dto/UpdateProfileDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/UpdateProfileDTO.java index 325da91..5e47dcc 100644 --- a/unilife-server/src/main/java/com/unilife/model/dto/UpdateProfileDTO.java +++ b/unilife-server/src/main/java/com/unilife/model/dto/UpdateProfileDTO.java @@ -11,7 +11,8 @@ import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor public class UpdateProfileDTO { - private String nickname; + private String username; // Added back to allow username updates + // private String nickname; // Removed as per user request and frontend changes private String bio; private Byte gender; private String department; diff --git a/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java b/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java new file mode 100644 index 0000000..a4826e5 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java @@ -0,0 +1,41 @@ +package com.unilife.model.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * AI聊天会话实体 + * 只管理会话元数据,消息存储由Spring AI ChatMemory处理 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiChatSession { + /** + * 会话ID(前端生成) + */ + private String id; + + /** + * 用户ID(可选,支持匿名会话) + */ + private Long userId; + + /** + * 会话标题 + */ + private String title; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/entity/Course.java b/unilife-server/src/main/java/com/unilife/model/entity/Course.java index bd60138..e01ff39 100644 --- a/unilife-server/src/main/java/com/unilife/model/entity/Course.java +++ b/unilife-server/src/main/java/com/unilife/model/entity/Course.java @@ -70,6 +70,11 @@ public class Course implements Serializable { */ private Byte endWeek; + /** + * 学期(如:2023-1) + */ + private String semester; + /** * 显示颜色 */ diff --git a/unilife-server/src/main/java/com/unilife/model/entity/Schedule.java b/unilife-server/src/main/java/com/unilife/model/entity/Schedule.java index 5bea3f7..d111d5e 100644 --- a/unilife-server/src/main/java/com/unilife/model/entity/Schedule.java +++ b/unilife-server/src/main/java/com/unilife/model/entity/Schedule.java @@ -62,7 +62,7 @@ public class Schedule implements Serializable { /** * 提醒时间(分钟) */ - private Byte reminder; + private Integer reminder; /** * 显示颜色 diff --git a/unilife-server/src/main/java/com/unilife/model/vo/AiCreateSessionVO.java b/unilife-server/src/main/java/com/unilife/model/vo/AiCreateSessionVO.java new file mode 100644 index 0000000..5a1ac21 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/vo/AiCreateSessionVO.java @@ -0,0 +1,20 @@ +package com.unilife.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiCreateSessionVO { + /** + * 会话ID + */ + private String sessionId; + + /** + * 会话标题 + */ + private String title; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/vo/AiMessageHistoryVO.java b/unilife-server/src/main/java/com/unilife/model/vo/AiMessageHistoryVO.java new file mode 100644 index 0000000..13bb8f0 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/vo/AiMessageHistoryVO.java @@ -0,0 +1,27 @@ +package com.unilife.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiMessageHistoryVO { + /** + * 消息列表 + */ + private List messages; + + /** + * 总数量 + */ + private Long total; + + /** + * 会话信息 + */ + private AiSessionVO sessionInfo; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/vo/AiMessageVO.java b/unilife-server/src/main/java/com/unilife/model/vo/AiMessageVO.java new file mode 100644 index 0000000..f52b860 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/vo/AiMessageVO.java @@ -0,0 +1,30 @@ +package com.unilife.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiMessageVO { + /** + * 消息ID + */ + private String id; + + /** + * 角色 + */ + private String role; + + /** + * 消息内容 + */ + private String content; + + /** + * 时间戳 + */ + private String timestamp; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/vo/AiSessionListVO.java b/unilife-server/src/main/java/com/unilife/model/vo/AiSessionListVO.java new file mode 100644 index 0000000..69624c3 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/vo/AiSessionListVO.java @@ -0,0 +1,22 @@ +package com.unilife.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiSessionListVO { + /** + * 会话列表 + */ + private List sessions; + + /** + * 总数量 + */ + private Long total; +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java b/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java new file mode 100644 index 0000000..62317de --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java @@ -0,0 +1,31 @@ +package com.unilife.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AiSessionVO { + /** + * 会话ID + */ + private String id; + + /** + * 会话标题 + */ + private String title; + + /** + * 创建时间 + */ + private String createdAt; + + /** + * 更新时间 + */ + private String updatedAt; + +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/vo/CourseVO.java b/unilife-server/src/main/java/com/unilife/model/vo/CourseVO.java index a55a813..2fbfc22 100644 --- a/unilife-server/src/main/java/com/unilife/model/vo/CourseVO.java +++ b/unilife-server/src/main/java/com/unilife/model/vo/CourseVO.java @@ -66,6 +66,11 @@ public class CourseVO { */ private Byte endWeek; + /** + * 学期(如:2023-1) + */ + private String semester; + /** * 显示颜色 */ diff --git a/unilife-server/src/main/java/com/unilife/model/vo/PostListVO.java b/unilife-server/src/main/java/com/unilife/model/vo/PostListVO.java index 054da78..66ed710 100644 --- a/unilife-server/src/main/java/com/unilife/model/vo/PostListVO.java +++ b/unilife-server/src/main/java/com/unilife/model/vo/PostListVO.java @@ -70,6 +70,16 @@ public class PostListVO { */ private Integer commentCount; + /** + * 帖子状态(0-删除, 1-正常, 2-置顶) + */ + private Byte status; + + /** + * 当前用户是否点赞 + */ + private Boolean isLiked; + /** * 创建时间 */ diff --git a/unilife-server/src/main/java/com/unilife/model/vo/PostVO.java b/unilife-server/src/main/java/com/unilife/model/vo/PostVO.java index a5addf0..6871382 100644 --- a/unilife-server/src/main/java/com/unilife/model/vo/PostVO.java +++ b/unilife-server/src/main/java/com/unilife/model/vo/PostVO.java @@ -34,11 +34,7 @@ public class PostVO { * 发布用户ID */ private Long userId; - - /** - * 发布用户昵称 - */ - private String nickname; + /** * 发布用户头像 diff --git a/unilife-server/src/main/java/com/unilife/model/vo/ScheduleVO.java b/unilife-server/src/main/java/com/unilife/model/vo/ScheduleVO.java index 2db63c6..354a348 100644 --- a/unilife-server/src/main/java/com/unilife/model/vo/ScheduleVO.java +++ b/unilife-server/src/main/java/com/unilife/model/vo/ScheduleVO.java @@ -58,7 +58,7 @@ public class ScheduleVO { /** * 提醒时间(分钟) */ - private Byte reminder; + private Integer reminder; /** * 显示颜色 diff --git a/unilife-server/src/main/java/com/unilife/service/AdminService.java b/unilife-server/src/main/java/com/unilife/service/AdminService.java new file mode 100644 index 0000000..ed433f1 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/AdminService.java @@ -0,0 +1,93 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; + +import java.util.Map; + +public interface AdminService { + + /** + * 获取系统统计数据 + */ + Result getSystemStats(); + + /** + * 获取用户列表 + */ + Result getUserList(Integer page, Integer size, String keyword, Integer role, Integer status); + + /** + * 更新用户状态 + */ + Result updateUserStatus(Long userId, Integer status); + + /** + * 更新用户角色 + */ + Result updateUserRole(Long userId, Integer role); + + /** + * 删除用户 + */ + Result deleteUser(Long userId); + + /** + * 获取帖子列表 + */ + Result getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status); + + /** + * 更新帖子状态 + */ + Result updatePostStatus(Long postId, Integer status); + + /** + * 删除帖子 + */ + Result deletePost(Long postId); + + /** + * 永久删除帖子 + */ + Result permanentDeletePost(Long postId); + + /** + * 获取评论列表 + */ + Result getCommentList(Integer page, Integer size, String keyword, Long postId, Integer status); + + /** + * 删除评论 + */ + Result deleteComment(Long commentId); + + /** + * 获取分类列表 + */ + Result getCategoryList(); + + /** + * 创建分类 + */ + Result createCategory(Map request); + + /** + * 更新分类 + */ + Result updateCategory(Long categoryId, Map request); + + /** + * 删除分类 + */ + Result deleteCategory(Long categoryId); + + /** + * 获取资源列表 + */ + Result getResourceList(Integer page, Integer size, String keyword, Long categoryId, Integer status); + + /** + * 删除资源 + */ + Result deleteResource(Long resourceId); +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java b/unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java new file mode 100644 index 0000000..150c0ff --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java @@ -0,0 +1,59 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; +import com.unilife.model.entity.AiChatSession; +import com.unilife.model.vo.AiSessionListVO; + +/** + * AI聊天会话元数据管理服务 + * 只处理会话元数据,消息存储和查询由Spring AI ChatMemory处理 + */ +public interface AiChatSessionHistoryService { + + /** + * 创建或更新会话 + * @param sessionId 会话ID + * @param userId 用户ID(可选,支持匿名会话) + * @param title 会话标题 + * @return 创建结果 + */ + Result createOrUpdateSession(String sessionId, Long userId, String title); + + /** + * 获取会话列表(支持用户会话和匿名会话) + * @param userId 用户ID(为null时查询匿名会话) + * @param page 页码 + * @param size 每页大小 + * @return 会话列表 + */ + Result getSessionList(Long userId, Integer page, Integer size); + + /** + * 获取会话详细信息 + * @param sessionId 会话ID + * @return 会话信息 + */ + Result getSessionDetail(String sessionId); + + /** + * 更新会话标题 + * @param sessionId 会话ID + * @param title 新标题 + * @return 更新结果 + */ + Result updateSessionTitle(String sessionId, String title); + + /** + * 更新会话的最后活动时间 + * @param sessionId 会话ID + * @return 更新结果 + */ + Result updateSessionLastActivity(String sessionId); + + /** + * 删除会话(会话元数据和Spring AI ChatMemory中的消息) + * @param sessionId 会话ID + * @return 删除结果 + */ + Result deleteSession(String sessionId); +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/AiService.java b/unilife-server/src/main/java/com/unilife/service/AiService.java new file mode 100644 index 0000000..401da5a --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/AiService.java @@ -0,0 +1,69 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; +import com.unilife.model.dto.AiCreateSessionDTO; +import com.unilife.model.dto.AiSendMessageDTO; +import com.unilife.model.dto.AiUpdateSessionDTO; +import com.unilife.model.vo.AiCreateSessionVO; +import com.unilife.model.vo.AiMessageHistoryVO; +import com.unilife.model.vo.AiSessionListVO; +import reactor.core.publisher.Flux; + +/** + * AI服务接口 + */ +public interface AiService { + + /** + * 发送消息给AI(流式响应) + * @param sendMessageDTO 发送消息DTO + * @return 流式字符串响应 + */ + Flux sendMessage(AiSendMessageDTO sendMessageDTO); + + /** + * 获取会话列表 + * @param page 页码 + * @param size 每页大小 + * @return 会话列表 + */ + Result getSessionList(Integer page, Integer size); + + /** + * 获取会话消息历史 + * @param sessionId 会话ID + * @param page 页码 + * @param size 每页大小 + * @return 消息历史 + */ + Result getSessionMessages(String sessionId, Integer page, Integer size); + + /** + * 创建会话 + * @param createSessionDTO 创建会话DTO + * @return 创建结果 + */ + Result createSession(AiCreateSessionDTO createSessionDTO); + + /** + * 更新会话标题 + * @param sessionId 会话ID + * @param updateSessionDTO 更新会话DTO + * @return 更新结果 + */ + Result updateSessionTitle(String sessionId, AiUpdateSessionDTO updateSessionDTO); + + /** + * 清空会话消息 + * @param sessionId 会话ID + * @return 清空结果 + */ + Result clearSessionMessages(String sessionId); + + /** + * 删除会话 + * @param sessionId 会话ID + * @return 删除结果 + */ + Result deleteSession(String sessionId); +} diff --git a/unilife-server/src/main/java/com/unilife/service/CourseService.java b/unilife-server/src/main/java/com/unilife/service/CourseService.java index 4bb152f..1688fe4 100644 --- a/unilife-server/src/main/java/com/unilife/service/CourseService.java +++ b/unilife-server/src/main/java/com/unilife/service/CourseService.java @@ -38,6 +38,14 @@ public interface CourseService { */ Result getCourseListByDayOfWeek(Long userId, Byte dayOfWeek); + /** + * 获取用户在指定学期的课程 + * @param userId 用户ID + * @param semester 学期(如:2023-1) + * @return 结果 + */ + Result getCourseListBySemester(Long userId, String semester); + /** * 更新课程 * @param courseId 课程ID diff --git a/unilife-server/src/main/java/com/unilife/service/PostService.java b/unilife-server/src/main/java/com/unilife/service/PostService.java index 15eed52..720ea1c 100644 --- a/unilife-server/src/main/java/com/unilife/service/PostService.java +++ b/unilife-server/src/main/java/com/unilife/service/PostService.java @@ -27,12 +27,14 @@ public interface PostService { /** * 获取帖子列表 * @param categoryId 分类ID,可为null + * @param keyword 搜索关键词,可为null * @param page 页码 * @param size 每页大小 * @param sort 排序方式(latest-最新,hot-热门) + * @param userId 当前用户ID,可为null * @return 结果 */ - Result getPostList(Long categoryId, Integer page, Integer size, String sort); + Result getPostList(Long categoryId, String keyword, Integer page, Integer size, String sort, Long userId); /** * 更新帖子 @@ -58,4 +60,14 @@ public interface PostService { * @return 结果 */ Result likePost(Long postId, Long userId); + + /** + * 获取用户的帖子列表 + * @param userId 用户ID + * @param page 页码 + * @param size 每页大小 + * @param sort 排序方式(latest-最新,hot-热门) + * @return 结果 + */ + Result getUserPosts(Long userId, Integer page, Integer size, String sort); } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/ResourceService.java b/unilife-server/src/main/java/com/unilife/service/ResourceService.java index 15c9910..8139b79 100644 --- a/unilife-server/src/main/java/com/unilife/service/ResourceService.java +++ b/unilife-server/src/main/java/com/unilife/service/ResourceService.java @@ -28,13 +28,14 @@ public interface ResourceService { /** * 获取资源列表 * @param categoryId 分类ID,可为null - * @param userId 用户ID,可为null + * @param uploaderUserId 上传者用户ID,可为null(用于筛选特定用户上传的资源) * @param keyword 关键词,可为null * @param page 页码 * @param size 每页大小 + * @param currentUserId 当前登录用户ID,可为null(用于查询点赞状态) * @return 结果 */ - Result getResourceList(Long categoryId, Long userId, String keyword, Integer page, Integer size); + Result getResourceList(Long categoryId, Long uploaderUserId, String keyword, Integer page, Integer size, Long currentUserId); /** * 更新资源 diff --git a/unilife-server/src/main/java/com/unilife/service/UserService.java b/unilife-server/src/main/java/com/unilife/service/UserService.java index d211512..8e5f701 100644 --- a/unilife-server/src/main/java/com/unilife/service/UserService.java +++ b/unilife-server/src/main/java/com/unilife/service/UserService.java @@ -30,4 +30,18 @@ public interface UserService { Result updateAvatar(Long userId, MultipartFile file); Result updateEmail(Long userId, UpdateEmailDTO emailDTO); + + // 用户统计数据 + Result getUserStats(Long userId); + + // 用户最近帖子 + Result getUserRecentPosts(Long userId, Integer limit); + + /** + * 删除用户及其所有相关数据 + * 使用软删除机制,保持数据一致性 + * @param userId 用户ID + * @return 操作结果 + */ + Result deleteUser(Long userId); } diff --git a/unilife-server/src/main/java/com/unilife/service/impl/AdminServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/AdminServiceImpl.java new file mode 100644 index 0000000..30b6057 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/impl/AdminServiceImpl.java @@ -0,0 +1,356 @@ +package com.unilife.service.impl; + +import com.unilife.common.result.Result; +import com.unilife.mapper.*; +import com.unilife.model.entity.*; +import com.unilife.service.AdminService; +import com.unilife.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class AdminServiceImpl implements AdminService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private PostMapper postMapper; + + @Autowired + private CommentMapper commentMapper; + + @Autowired + private CategoryMapper categoryMapper; + + @Autowired + private ResourceMapper resourceMapper; + + @Autowired + private UserService userService; + + @Override + public Result getSystemStats() { + try { + Map stats = new HashMap<>(); + + // 用户统计 + stats.put("totalUsers", userMapper.getTotalCount()); + stats.put("activeUsers", userMapper.getActiveUserCount()); + stats.put("newUsersToday", userMapper.getNewUserCountToday()); + + // 帖子统计 + stats.put("totalPosts", postMapper.getTotalCount()); + stats.put("newPostsToday", postMapper.getNewPostCountToday()); + + // 评论统计 + stats.put("totalComments", commentMapper.getTotalCount()); + stats.put("newCommentsToday", commentMapper.getNewCommentCountToday()); + + // 资源统计 + stats.put("totalResources", resourceMapper.getTotalCount()); + stats.put("newResourcesToday", resourceMapper.getNewResourceCountToday()); + + // 分类统计 + stats.put("totalCategories", categoryMapper.getTotalCount()); + + return Result.success(stats); + } catch (Exception e) { + log.error("获取系统统计数据失败", e); + return Result.error(500, "获取系统统计数据失败"); + } + } + + @Override + public Result getUserList(Integer page, Integer size, String keyword, Integer role, Integer status) { + try { + int offset = (page - 1) * size; + List users = userMapper.getAdminUserList(offset, size, keyword, role, status); + int total = userMapper.getAdminUserCount(keyword, role, status); + + Map result = new HashMap<>(); + result.put("list", users); + result.put("total", total); + result.put("pages", (total + size - 1) / size); + + return Result.success(result); + } catch (Exception e) { + log.error("获取用户列表失败", e); + return Result.error(500, "获取用户列表失败"); + } + } + + @Override + public Result updateUserStatus(Long userId, Integer status) { + try { + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + userMapper.updateUserStatus(userId, status); + return Result.success(null, "用户状态更新成功"); + } catch (Exception e) { + log.error("更新用户状态失败", e); + return Result.error(500, "更新用户状态失败"); + } + } + + @Override + public Result updateUserRole(Long userId, Integer role) { + try { + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + userMapper.updateUserRole(userId, role); + return Result.success(null, "用户角色更新成功"); + } catch (Exception e) { + log.error("更新用户角色失败", e); + return Result.error(500, "更新用户角色失败"); + } + } + + @Override + public Result deleteUser(Long userId) { + try { + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + // 检查是否为管理员 + if (user.getRole() == 2) { + return Result.error(400, "不能删除管理员账号"); + } + + // 调用UserService的完整删除逻辑 + return userService.deleteUser(userId); + } catch (Exception e) { + log.error("删除用户失败", e); + return Result.error(500, "删除用户失败"); + } + } + + @Override + public Result getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status) { + try { + int offset = (page - 1) * size; + List posts = postMapper.getAdminPostList(offset, size, keyword, categoryId, status); + int total = postMapper.getAdminPostCount(keyword, categoryId, status); + + Map result = new HashMap<>(); + result.put("list", posts); + result.put("total", total); + result.put("pages", (total + size - 1) / size); + + return Result.success(result); + } catch (Exception e) { + log.error("获取帖子列表失败", e); + return Result.error(500, "获取帖子列表失败"); + } + } + + @Override + public Result updatePostStatus(Long postId, Integer status) { + try { + Post post = postMapper.getPostById(postId); + if (post == null) { + return Result.error(404, "帖子不存在"); + } + + postMapper.updatePostStatus(postId, status); + return Result.success(null, "帖子状态更新成功"); + } catch (Exception e) { + log.error("更新帖子状态失败", e); + return Result.error(500, "更新帖子状态失败"); + } + } + + @Override + public Result deletePost(Long postId) { + try { + Post post = postMapper.getPostById(postId); + if (post == null) { + return Result.error(404, "帖子不存在"); + } + + postMapper.deletePost(postId); + return Result.success(null, "帖子删除成功"); + } catch (Exception e) { + log.error("删除帖子失败", e); + return Result.error(500, "删除帖子失败"); + } + } + + @Override + public Result permanentDeletePost(Long postId) { + try { + Post post = postMapper.getPostById(postId); + if (post == null) { + return Result.error(404, "帖子不存在"); + } + + // 永久删除帖子(物理删除) + postMapper.permanentDeletePost(postId); + return Result.success(null, "帖子永久删除成功"); + } catch (Exception e) { + log.error("永久删除帖子失败", e); + return Result.error(500, "永久删除帖子失败"); + } + } + + @Override + public Result getCommentList(Integer page, Integer size, String keyword, Long postId, Integer status) { + try { + int offset = (page - 1) * size; + List comments = commentMapper.getAdminCommentList(offset, size, keyword, postId, status); + int total = commentMapper.getAdminCommentCount(keyword, postId, status); + + Map result = new HashMap<>(); + result.put("list", comments); + result.put("total", total); + result.put("pages", (total + size - 1) / size); + + return Result.success(result); + } catch (Exception e) { + log.error("获取评论列表失败", e); + return Result.error(500, "获取评论列表失败"); + } + } + + @Override + public Result deleteComment(Long commentId) { + try { + Comment comment = commentMapper.getCommentById(commentId); + if (comment == null) { + return Result.error(404, "评论不存在"); + } + + commentMapper.deleteComment(commentId); + return Result.success(null, "评论删除成功"); + } catch (Exception e) { + log.error("删除评论失败", e); + return Result.error(500, "删除评论失败"); + } + } + + @Override + public Result getCategoryList() { + try { + List categories = categoryMapper.getAllCategories(); + return Result.success(categories); + } catch (Exception e) { + log.error("获取分类列表失败", e); + return Result.error(500, "获取分类列表失败"); + } + } + + @Override + public Result createCategory(Map request) { + try { + Category category = new Category(); + category.setName((String) request.get("name")); + category.setDescription((String) request.get("description")); + category.setIcon((String) request.get("icon")); + category.setSort((Integer) request.get("sort")); + Integer statusInt = (Integer) request.get("status"); + category.setStatus(statusInt != null ? statusInt.byteValue() : (byte) 1); + + categoryMapper.insertCategory(category); + return Result.success(null, "分类创建成功"); + } catch (Exception e) { + log.error("创建分类失败", e); + return Result.error(500, "创建分类失败"); + } + } + + @Override + public Result updateCategory(Long categoryId, Map request) { + try { + Category category = categoryMapper.getCategoryById(categoryId); + if (category == null) { + return Result.error(404, "分类不存在"); + } + + category.setName((String) request.get("name")); + category.setDescription((String) request.get("description")); + category.setIcon((String) request.get("icon")); + category.setSort((Integer) request.get("sort")); + Integer statusInt = (Integer) request.get("status"); + category.setStatus(statusInt != null ? statusInt.byteValue() : (byte) 1); + + categoryMapper.updateCategory(category); + return Result.success(null, "分类更新成功"); + } catch (Exception e) { + log.error("更新分类失败", e); + return Result.error(500, "更新分类失败"); + } + } + + @Override + public Result deleteCategory(Long categoryId) { + try { + Category category = categoryMapper.getCategoryById(categoryId); + if (category == null) { + return Result.error(404, "分类不存在"); + } + + // 检查是否有帖子或资源使用该分类 + int postCount = postMapper.getCountByCategoryId(categoryId); + int resourceCount = resourceMapper.getCountByCategoryId(categoryId); + + if (postCount > 0 || resourceCount > 0) { + return Result.error(400, "该分类下还有帖子或资源,无法删除"); + } + + categoryMapper.deleteCategory(categoryId); + return Result.success(null, "分类删除成功"); + } catch (Exception e) { + log.error("删除分类失败", e); + return Result.error(500, "删除分类失败"); + } + } + + @Override + public Result getResourceList(Integer page, Integer size, String keyword, Long categoryId, Integer status) { + try { + int offset = (page - 1) * size; + List resources = resourceMapper.getAdminResourceList(offset, size, keyword, categoryId, status); + int total = resourceMapper.getAdminResourceCount(keyword, categoryId, status); + + Map result = new HashMap<>(); + result.put("list", resources); + result.put("total", total); + result.put("pages", (total + size - 1) / size); + + return Result.success(result); + } catch (Exception e) { + log.error("获取资源列表失败", e); + return Result.error(500, "获取资源列表失败"); + } + } + + @Override + public Result deleteResource(Long resourceId) { + try { + Resource resource = resourceMapper.getResourceById(resourceId); + if (resource == null) { + return Result.error(404, "资源不存在"); + } + + resourceMapper.deleteResource(resourceId); + return Result.success(null, "资源删除成功"); + } catch (Exception e) { + log.error("删除资源失败", e); + return Result.error(500, "删除资源失败"); + } + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java new file mode 100644 index 0000000..bfc3c80 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java @@ -0,0 +1,187 @@ +package com.unilife.service.impl; + +import com.unilife.common.result.Result; +import com.unilife.mapper.AiChatSessionMapper; +import com.unilife.model.entity.AiChatSession; +import com.unilife.model.vo.AiSessionListVO; +import com.unilife.model.vo.AiSessionVO; +import com.unilife.service.AiChatSessionHistoryService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * AI聊天会话元数据管理服务实现 + * 只处理会话元数据,消息存储和查询由Spring AI ChatMemory处理 + */ +@Service +@Slf4j +public class AiChatSessionHistoryServiceImpl implements AiChatSessionHistoryService { + + @Autowired + private AiChatSessionMapper sessionMapper; + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + @Transactional + public Result createOrUpdateSession(String sessionId, Long userId, String title) { + log.info("创建或更新会话: sessionId={}, userId={}, title={}", sessionId, userId, title); + + try { + // 检查会话是否已存在 + AiChatSession existingSession = sessionMapper.selectById(sessionId); + + if (existingSession != null) { + if (title != null && !title.equals(existingSession.getTitle())) { + sessionMapper.updateTitle(sessionId, title); + existingSession.setTitle(title); + existingSession.setUpdatedAt(LocalDateTime.now()); + } + return Result.success(existingSession); + } else { + // 创建新会话 + AiChatSession newSession = new AiChatSession(); + newSession.setId(sessionId); + newSession.setUserId(userId); + newSession.setTitle(title != null ? title : "新对话"); + newSession.setCreatedAt(LocalDateTime.now()); + newSession.setUpdatedAt(LocalDateTime.now()); + + sessionMapper.insert(newSession); + log.info("成功创建新会话: {}", sessionId); + return Result.success(newSession); + } + } catch (Exception e) { + log.error("创建或更新会话失败: {}", e.getMessage(), e); + return Result.error("会话操作失败"); + } + } + + @Override + public Result getSessionList(Long userId, Integer page, Integer size) { + log.info("获取会话列表: userId={}, page={}, size={}", userId, page, size); + + try { + int offset = (page - 1) * size; + + List sessions; + long total; + + if (userId != null) { + // 查询用户会话 + sessions = sessionMapper.selectByUserId(userId, offset, size); + total = sessionMapper.countByUserId(userId); + } else { + // 查询匿名会话 + sessions = sessionMapper.selectAnonymousSessions(offset, size); + total = sessionMapper.countAnonymousSessions(); + } + + // 转换为VO + List sessionVOs = sessions.stream() + .map(this::convertToSessionVO) + .collect(Collectors.toList()); + + AiSessionListVO result = new AiSessionListVO(); + result.setSessions(sessionVOs); + result.setTotal(total); + + return Result.success(result); + } catch (Exception e) { + log.error("获取会话列表失败: {}", e.getMessage(), e); + return Result.error("获取会话列表失败"); + } + } + + @Override + public Result getSessionDetail(String sessionId) { + log.info("获取会话详细信息: {}", sessionId); + + try { + AiChatSession session = sessionMapper.selectById(sessionId); + if (session == null) { + return Result.error("会话不存在"); + } + return Result.success(session); + } catch (Exception e) { + log.error("获取会话详细信息失败: {}", e.getMessage(), e); + return Result.error("获取会话信息失败"); + } + } + + @Override + @Transactional + public Result updateSessionTitle(String sessionId, String title) { + log.info("更新会话标题: sessionId={}, title={}", sessionId, title); + + try { + int updated = sessionMapper.updateTitle(sessionId, title); + if (updated > 0) { + return Result.success(); + } else { + return Result.error("会话不存在或更新失败"); + } + } catch (Exception e) { + log.error("更新会话标题失败: {}", e.getMessage(), e); + return Result.error("更新会话标题失败"); + } + } + + @Override + @Transactional + public Result updateSessionLastActivity(String sessionId) { + + try { + int updated = sessionMapper.updateMessageInfo(sessionId, LocalDateTime.now(), null); + if (updated > 0) { + log.info("成功更新会话 {} 的最后活动时间", sessionId); + return Result.success(); + } else { + log.warn("会话 {} 不存在,无法更新活动时间", sessionId); + return Result.error("会话不存在"); + } + } catch (Exception e) { + log.error("更新会话活动时间失败: {}", e.getMessage(), e); + return Result.error("更新会话活动时间失败"); + } + } + + @Override + @Transactional + public Result deleteSession(String sessionId) { + log.info("删除会话: {}", sessionId); + + try { + // 只删除会话元数据,Spring AI ChatMemory中的消息由AiServiceImpl处理 + int deleted = sessionMapper.deleteById(sessionId); + if (deleted > 0) { + log.info("成功删除会话: {}", sessionId); + return Result.success(); + } else { + return Result.error("会话不存在"); + } + } catch (Exception e) { + log.error("删除会话失败: {}", e.getMessage(), e); + return Result.error("删除会话失败"); + } + } + + /** + * 转换为SessionVO + */ + private AiSessionVO convertToSessionVO(AiChatSession session) { + AiSessionVO vo = new AiSessionVO(); + vo.setId(session.getId()); + vo.setTitle(session.getTitle()); + vo.setCreatedAt(session.getCreatedAt() != null ? session.getCreatedAt().format(FORMATTER) : null); + vo.setUpdatedAt(session.getUpdatedAt() != null ? session.getUpdatedAt().format(FORMATTER) : null); + return vo; + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java new file mode 100644 index 0000000..3a78316 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java @@ -0,0 +1,358 @@ +package com.unilife.service.impl; + +import com.unilife.common.result.Result; +import com.unilife.model.dto.AiCreateSessionDTO; +import com.unilife.model.dto.AiSendMessageDTO; +import com.unilife.model.dto.AiUpdateSessionDTO; +import com.unilife.model.vo.AiCreateSessionVO; +import com.unilife.model.vo.AiMessageHistoryVO; +import com.unilife.model.vo.AiSessionListVO; +import com.unilife.service.AiService; +import com.unilife.service.AiChatSessionHistoryService; +import com.unilife.utils.BaseContext; +import com.unilife.config.AiConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.messages.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class AiServiceImpl implements AiService { + + @Autowired + private ChatClient chatClient; + + @Autowired + private ChatMemory chatMemory; + + @Autowired + private AiChatSessionHistoryService sessionHistoryService; + + @Autowired + private AiConfig aiConfig; + + @Override + public Flux sendMessage(AiSendMessageDTO sendMessageDTO) { + log.info("发送消息给AI: {}, 会话ID: {}", sendMessageDTO.getMessage(), sendMessageDTO.getSessionId()); + + String sessionId = sendMessageDTO.getSessionId(); + + // 确保会话元数据存在 + sessionHistoryService.createOrUpdateSession(sessionId, BaseContext.getId(), "新对话"); + + // 检查是否为第一次对话(只有用户消息且为第一条) + boolean isFirstMessage = isFirstUserMessage(sessionId); + + // 如果是第一次对话,立即异步生成并更新标题 + if (isFirstMessage) { + generateAndUpdateSessionTitle(sessionId, sendMessageDTO.getMessage()); + } + + // 使用ChatClient的流式响应,Spring AI会自动处理记忆 + Flux responseFlux = chatClient.prompt() + .user(sendMessageDTO.getMessage()) + .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sendMessageDTO.getSessionId())) + .stream() + .content(); + + // 在消息发送完成后只更新会话的活跃时间 + final String finalSessionId = sessionId; + return responseFlux.doOnComplete(() -> { + try { + // 更新会话的最后活动时间 + sessionHistoryService.updateSessionLastActivity(finalSessionId); + log.info("已更新会话 {} 的最后活动时间", finalSessionId); + } catch (Exception e) { + log.warn("更新会话活动时间失败: {}", e.getMessage()); + } + }); + } + + @Override + public Result getSessionList(Integer page, Integer size) { + log.info("获取会话列表: page={}, size={}", page, size); + + return sessionHistoryService.getSessionList(BaseContext.getId(), page, size); + } + + @Override + public Result getSessionMessages(String sessionId, Integer page, Integer size) { + log.info("获取会话消息历史,会话ID: {}", sessionId); + + try { + // 直接从Spring AI ChatMemory获取消息历史 + List messages = chatMemory.get(sessionId); + + AiMessageHistoryVO messageHistory = new AiMessageHistoryVO(); + + if (messages != null && !messages.isEmpty()) { + // 转换Message为VO对象 + List messageVOs = messages.stream() + .map(message -> { + com.unilife.model.vo.AiMessageVO vo = new com.unilife.model.vo.AiMessageVO(); + vo.setId(String.valueOf(System.currentTimeMillis() + Math.random())); + vo.setRole(message.getMessageType().getValue().toLowerCase()); + vo.setContent(message.getText()); + vo.setTimestamp(java.time.LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + return vo; + }) + .collect(Collectors.toList()); + + messageHistory.setMessages(messageVOs); + messageHistory.setTotal((long) messageVOs.size()); + } else { + messageHistory.setMessages(new ArrayList<>()); + messageHistory.setTotal(0L); + } + + return Result.success(messageHistory); + } catch (Exception e) { + log.error("获取会话消息历史失败: {}", e.getMessage(), e); + return Result.error("获取消息历史失败"); + } + } + + @Override + public Result createSession(AiCreateSessionDTO createSessionDTO) { + log.info("创建聊天会话: {}", createSessionDTO.getSessionId()); + + try { + // 使用会话历史服务创建会话元数据 + Result result = sessionHistoryService.createOrUpdateSession( + createSessionDTO.getSessionId(), + BaseContext.getId(), // 暂时支持匿名会话 + createSessionDTO.getTitle() + ); + + if (result.getCode() == 200) { + AiCreateSessionVO response = new AiCreateSessionVO(); + response.setSessionId(createSessionDTO.getSessionId()); + response.setTitle(createSessionDTO.getTitle() != null ? createSessionDTO.getTitle() : "新对话"); + return Result.success(response); + } else { + return Result.error(result.getMessage()); + } + } catch (Exception e) { + log.error("创建会话失败: {}", e.getMessage(), e); + return Result.error("创建会话失败"); + } + } + + @Override + public Result updateSessionTitle(String sessionId, AiUpdateSessionDTO updateSessionDTO) { + log.info("更新会话标题: sessionId={}, title={}", sessionId, updateSessionDTO.getTitle()); + + // 使用会话历史服务更新标题 + return sessionHistoryService.updateSessionTitle(sessionId, updateSessionDTO.getTitle()); + } + + @Override + public Result clearSessionMessages(String sessionId) { + log.info("清空会话消息,会话ID: {}", sessionId); + + try { + // 清空Spring AI ChatMemory中的消息 + chatMemory.clear(sessionId); + log.info("成功清空会话 {} 的消息", sessionId); + return Result.success(); + } catch (Exception e) { + log.error("清空会话消息失败: {}", e.getMessage(), e); + return Result.error("清空会话消息失败"); + } + } + + @Override + public Result deleteSession(String sessionId) { + log.info("删除会话,会话ID: {}", sessionId); + + try { + // 删除Spring AI ChatMemory中的消息 + chatMemory.clear(sessionId); + + // 删除会话元数据 + Result result = sessionHistoryService.deleteSession(sessionId); + + if (result.getCode() == 200) { + log.info("成功删除会话 {}", sessionId); + return Result.success(); + } else { + return result; + } + } catch (Exception e) { + log.error("删除会话失败: {}", e.getMessage(), e); + return Result.error("删除会话失败"); + } + } + + /** + * 检查是否为第一条用户消息 + * @param sessionId 会话ID + * @return 是否为第一条用户消息 + */ + private boolean isFirstUserMessage(String sessionId) { + try { + List messages = chatMemory.get(sessionId); + if (messages == null || messages.isEmpty()) { + return true; // 没有消息历史,这是第一条消息 + } + + // 统计用户消息数量(排除系统消息) + long userMessageCount = messages.stream() + .filter(message -> "user".equalsIgnoreCase(message.getMessageType().getValue())) + .count(); + + return userMessageCount == 0; // 如果没有用户消息,说明即将发送的是第一条 + } catch (Exception e) { + log.warn("检查第一条消息失败: {}", e.getMessage()); + return false; + } + } + + /** + * 异步生成并更新会话标题 + * @param sessionId 会话ID + * @param userMessage 用户消息内容 + */ + private void generateAndUpdateSessionTitle(String sessionId, String userMessage) { + // 检查是否启用自动标题生成 + if (!aiConfig.isAutoTitleEnabled()) { + log.debug("自动标题生成已禁用,跳过标题生成"); + return; + } + + // 异步执行,不阻塞主流程 + new Thread(() -> { + long startTime = System.currentTimeMillis(); + try { + log.debug("开始为会话 {} 生成标题,策略: {}", sessionId, aiConfig.getTitleGenerationStrategy()); + + String generatedTitle = generateTitleFromMessage(userMessage); + + // 更新会话标题 + Result updateResult = sessionHistoryService.updateSessionTitle(sessionId, generatedTitle); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + if (updateResult.getCode() == 200) { + log.info("成功为会话 {} 生成标题: {} (耗时: {}ms)", sessionId, generatedTitle, duration); + } else { + log.warn("更新会话标题失败: {} (耗时: {}ms)", updateResult.getMessage(), duration); + } + } catch (Exception e) { + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + log.error("生成会话标题失败: {} (耗时: {}ms)", e.getMessage(), duration, e); + } + }).start(); + } + + /** + * 从用户消息生成会话标题 + * 根据配置选择生成策略:simple(简单算法) 或 ai(AI生成) + * @param userMessage 用户消息 + * @return 生成的标题 + */ + private String generateTitleFromMessage(String userMessage) { + if (userMessage == null || userMessage.trim().isEmpty()) { + return "新对话"; + } + + String message = userMessage.trim(); + String strategy = aiConfig.getTitleGenerationStrategy(); + + log.debug("使用标题生成策略: {}", strategy); + + if ("ai".equalsIgnoreCase(strategy)) { + // 方案2:使用AI生成智能标题 + return generateAITitle(message); + } else { + // 方案1:使用简单文本处理(默认) + return generateSimpleTitle(message); + } + } + + /** + * 使用简单算法生成标题 + * @param message 用户消息 + * @return 生成的标题 + */ + private String generateSimpleTitle(String message) { + // 去除多余的空格和换行 + String cleanMessage = message.replaceAll("\\s+", " ").trim(); + + // 如果消息太短,直接返回 + if (cleanMessage.length() <= 20) { + return cleanMessage; + } + + // 尝试找到问号,截取问题部分 + int questionMarkIndex = cleanMessage.indexOf('?'); + if (questionMarkIndex == -1) { + questionMarkIndex = cleanMessage.indexOf('?'); + } + + if (questionMarkIndex > 0 && questionMarkIndex <= 50) { + return cleanMessage.substring(0, questionMarkIndex + 1); + } + + // 尝试找到句号,截取第一句话 + int periodIndex = cleanMessage.indexOf('。'); + if (periodIndex == -1) { + periodIndex = cleanMessage.indexOf('.'); + } + + if (periodIndex > 0 && periodIndex <= 50) { + return cleanMessage.substring(0, periodIndex + 1); + } + + // 如果没有标点符号,截取前50个字符 + if (cleanMessage.length() > 50) { + return cleanMessage.substring(0, 47) + "..."; + } + + return cleanMessage; + } + + /** + * 使用AI生成标题 + * @param message 用户消息 + * @return 生成的标题 + */ + private String generateAITitle(String message) { + try { + String prompt = String.format( + "请为以下用户发送的内容生成一个简洁的对话标题,这个标题是用户用下面内容与大模型对话时发送信息所总结的,不超过20个字,不要包含引号:\n\n%s", + message + ); + + String title = chatClient.prompt() + .user(prompt) + .call() + .content(); + + // 清理生成的标题 + title = title.trim() + .replaceAll("^[\"']+|[\"']+$", "") // 去除首尾引号 + .replaceAll("\\s+", " "); // 合并多个空格 + + if (title.length() > 30) { + title = title.substring(0, 27) + "..."; + } + + return title.isEmpty() ? generateSimpleTitle(message) : title; + } catch (Exception e) { + log.warn("AI生成标题失败,使用简单算法: {}", e.getMessage()); + return generateSimpleTitle(message); + } + } +} diff --git a/unilife-server/src/main/java/com/unilife/service/impl/CommentServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/CommentServiceImpl.java index e93dc47..7c22d2b 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/CommentServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/CommentServiceImpl.java @@ -80,56 +80,79 @@ public class CommentServiceImpl implements CommentService { @Override public Result getCommentsByPostId(Long postId, Long userId) { + log.info("获取帖子 {} 的评论列表,当前用户: {}", postId, userId); + // 检查帖子是否存在 Post post = postMapper.getById(postId); if (post == null) { + log.warn("帖子 {} 不存在", postId); return Result.error(404, "帖子不存在"); } // 获取一级评论 List topLevelComments = commentMapper.getTopLevelCommentsByPostId(postId); + log.info("获取到 {} 条一级评论", topLevelComments.size()); // 转换为VO - List commentVOs = topLevelComments.stream().map(comment -> { - // 获取评论用户信息 - User user = userMapper.getUserById(comment.getUserId()); - - // 获取回复列表 - List replies = commentMapper.getRepliesByParentId(comment.getId()); - List replyVOs = replies.stream().map(reply -> { - User replyUser = userMapper.getUserById(reply.getUserId()); - return CommentVO.builder() - .id(reply.getId()) - .postId(reply.getPostId()) - .userId(reply.getUserId()) - .nickname(replyUser != null ? replyUser.getNickname() : "未知用户") - .avatar(replyUser != null ? replyUser.getAvatar() : null) - .content(reply.getContent()) - .parentId(reply.getParentId()) - .likeCount(reply.getLikeCount()) + List commentVOs = new ArrayList<>(); + + for (Comment comment : topLevelComments) { + try { + // 获取评论用户信息 + User user = userMapper.getUserById(comment.getUserId()); + log.debug("评论 {} 的用户信息: {}", comment.getId(), user != null ? user.getNickname() : "null"); + + // 获取回复列表 + List replies = commentMapper.getRepliesByParentId(comment.getId()); + List replyVOs = new ArrayList<>(); + + for (Comment reply : replies) { + try { + User replyUser = userMapper.getUserById(reply.getUserId()); + log.debug("回复 {} 的用户信息: {}", reply.getId(), replyUser != null ? replyUser.getNickname() : "null"); + + CommentVO replyVO = CommentVO.builder() + .id(reply.getId()) + .postId(reply.getPostId()) + .userId(reply.getUserId()) + .nickname(replyUser != null ? replyUser.getNickname() : "未知用户") + .avatar(replyUser != null ? replyUser.getAvatar() : null) + .content(reply.getContent()) + .parentId(reply.getParentId()) + .likeCount(reply.getLikeCount()) + .isLiked(false) // 实际开发中应该查询用户是否点赞 + .createdAt(reply.getCreatedAt()) + .replies(new ArrayList<>()) + .build(); + replyVOs.add(replyVO); + } catch (Exception e) { + log.error("处理回复 {} 时出错: {}", reply.getId(), e.getMessage()); + } + } + + CommentVO commentVO = CommentVO.builder() + .id(comment.getId()) + .postId(comment.getPostId()) + .userId(comment.getUserId()) + .nickname(user != null ? user.getNickname() : "未知用户") + .avatar(user != null ? user.getAvatar() : null) + .content(comment.getContent()) + .parentId(comment.getParentId()) + .likeCount(comment.getLikeCount()) .isLiked(false) // 实际开发中应该查询用户是否点赞 - .createdAt(reply.getCreatedAt()) - .replies(new ArrayList<>()) + .createdAt(comment.getCreatedAt()) + .replies(replyVOs) .build(); - }).collect(Collectors.toList()); - - return CommentVO.builder() - .id(comment.getId()) - .postId(comment.getPostId()) - .userId(comment.getUserId()) - .nickname(user != null ? user.getNickname() : "未知用户") - .avatar(user != null ? user.getAvatar() : null) - .content(comment.getContent()) - .parentId(comment.getParentId()) - .likeCount(comment.getLikeCount()) - .isLiked(false) // 实际开发中应该查询用户是否点赞 - .createdAt(comment.getCreatedAt()) - .replies(replyVOs) - .build(); - }).collect(Collectors.toList()); + + commentVOs.add(commentVO); + } catch (Exception e) { + log.error("处理评论 {} 时出错: {}", comment.getId(), e.getMessage()); + } + } // 获取评论总数 Integer count = commentMapper.getCountByPostId(postId); + log.info("帖子 {} 的评论总数: {}, 实际返回: {}", postId, count, commentVOs.size()); // 返回结果 Map data = new HashMap<>(); diff --git a/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java index 559e4cd..c23adac 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java @@ -42,11 +42,20 @@ public class CourseServiceImpl implements CourseService { return Result.error(404, "用户不存在"); } + // 解析时间字符串为LocalTime + LocalTime startTime; + LocalTime endTime; + try { + startTime = LocalTime.parse(createCourseDTO.getStartTime(), TIME_FORMATTER); + endTime = LocalTime.parse(createCourseDTO.getEndTime(), TIME_FORMATTER); + } catch (Exception e) { + log.error("时间格式解析失败: {}", e.getMessage()); + return Result.error(400, "时间格式错误,请使用HH:mm:ss格式"); + } + // 检查课程时间冲突 - String startTimeStr = createCourseDTO.getStartTime().format(TIME_FORMATTER); - String endTimeStr = createCourseDTO.getEndTime().format(TIME_FORMATTER); Integer conflictCount = courseMapper.checkConflict(userId, createCourseDTO.getDayOfWeek(), - startTimeStr, endTimeStr, null); + createCourseDTO.getStartTime(), createCourseDTO.getEndTime(), null); if (conflictCount > 0) { return Result.error(400, "课程时间冲突,该时间段已有其他课程"); } @@ -54,6 +63,8 @@ public class CourseServiceImpl implements CourseService { // 创建课程 Course course = new Course(); BeanUtil.copyProperties(createCourseDTO, course); + course.setStartTime(startTime); + course.setEndTime(endTime); course.setUserId(userId); course.setStatus((byte) 1); @@ -126,6 +137,26 @@ public class CourseServiceImpl implements CourseService { return Result.success(data); } + @Override + public Result getCourseListBySemester(Long userId, String semester) { + // 获取用户在指定学期的课程 + List courses = courseMapper.getListByUserIdAndSemester(userId, semester); + + // 转换为VO + List courseVOs = courses.stream().map(course -> { + CourseVO courseVO = new CourseVO(); + BeanUtil.copyProperties(course, courseVO); + return courseVO; + }).collect(Collectors.toList()); + + // 返回结果 + Map data = new HashMap<>(); + data.put("total", courseVOs.size()); + data.put("list", courseVOs); + + return Result.success(data); + } + @Override @Transactional public Result updateCourse(Long courseId, Long userId, CreateCourseDTO createCourseDTO) { @@ -140,17 +171,28 @@ public class CourseServiceImpl implements CourseService { return Result.error(403, "无权限更新此课程"); } + // 解析时间字符串为LocalTime + LocalTime startTime; + LocalTime endTime; + try { + startTime = LocalTime.parse(createCourseDTO.getStartTime(), TIME_FORMATTER); + endTime = LocalTime.parse(createCourseDTO.getEndTime(), TIME_FORMATTER); + } catch (Exception e) { + log.error("时间格式解析失败: {}", e.getMessage()); + return Result.error(400, "时间格式错误,请使用HH:mm:ss格式"); + } + // 检查课程时间冲突 - String startTimeStr = createCourseDTO.getStartTime().format(TIME_FORMATTER); - String endTimeStr = createCourseDTO.getEndTime().format(TIME_FORMATTER); Integer conflictCount = courseMapper.checkConflict(userId, createCourseDTO.getDayOfWeek(), - startTimeStr, endTimeStr, courseId); + createCourseDTO.getStartTime(), createCourseDTO.getEndTime(), courseId); if (conflictCount > 0) { return Result.error(400, "课程时间冲突,该时间段已有其他课程"); } // 更新课程 BeanUtil.copyProperties(createCourseDTO, course); + course.setStartTime(startTime); + course.setEndTime(endTime); // 保存更新 courseMapper.update(course); diff --git a/unilife-server/src/main/java/com/unilife/service/impl/PdfVectorAsyncService.java b/unilife-server/src/main/java/com/unilife/service/impl/PdfVectorAsyncService.java new file mode 100644 index 0000000..da80994 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/impl/PdfVectorAsyncService.java @@ -0,0 +1,112 @@ +package com.unilife.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.ExtractedTextFormatter; +import org.springframework.ai.reader.pdf.PagePdfDocumentReader; +import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import com.unilife.model.entity.Resource; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * PDF向量化异步处理服务 + */ +@Slf4j +@Service +public class PdfVectorAsyncService { + + @Autowired + private VectorStore vectorStore; + + /** + * 异步处理PDF文件的向量存储 + * @param fileBytes PDF文件的字节数组 + * @param originalFilename 原始文件名 + * @param resource 资源记录 + */ + @Async("pdfVectorTaskExecutor") + public void processPdfVectorAsync(byte[] fileBytes, String originalFilename, Resource resource) { + try { + log.info("开始异步处理PDF向量化,资源ID: {},文件: {}", resource.getId(), originalFilename); + + // 创建临时文件 + String tempFileName = "pdf_" + resource.getId() + "_" + System.currentTimeMillis() + ".pdf"; + java.nio.file.Path tempFile = java.nio.file.Files.createTempFile(tempFileName, ".pdf"); + + try { + // 将字节数组写入临时文件 + java.nio.file.Files.write(tempFile, fileBytes); + + // 创建PDF文档读取器 + PagePdfDocumentReader reader = new PagePdfDocumentReader( + tempFile.toUri().toString(), + PdfDocumentReaderConfig.builder() + .withPageExtractedTextFormatter(ExtractedTextFormatter.defaults()) + .withPagesPerDocument(1) + .build() + ); + + // 读取文档 + List documents = reader.read(); + + // 为每个文档添加资源ID元数据,方便后续删除 + for (Document document : documents) { + Map metadata = new HashMap<>(document.getMetadata()); + metadata.put("resourceId", resource.getId().toString()); + metadata.put("title", resource.getTitle()); + + // 创建新的文档对象,包含资源ID元数据 + Document enrichedDocument = new Document(document.getText(), metadata); + documents.set(documents.indexOf(document), enrichedDocument); + } + + // 存储到向量数据库 + vectorStore.add(documents); + + log.info("PDF向量化处理完成,资源ID: {},处理文档数: {}", + resource.getId(), documents.size()); + + } finally { + // 清理临时文件 + try { + java.nio.file.Files.deleteIfExists(tempFile); + } catch (Exception e) { + log.warn("清理临时文件失败: {}", tempFile, e); + } + } + + } catch (Exception e) { + log.error("PDF异步向量化处理失败,资源ID: {}", resource.getId(), e); + } + } + + /** + * 异步删除向量库中的相关文档 + * @param resourceId 资源ID + * @param resourceTitle 资源标题 + */ + @Async("pdfVectorTaskExecutor") + public void deleteVectorDocumentsAsync(Long resourceId, String resourceTitle) { + try { + log.info("开始删除资源ID: {} 的向量文档,标题: {}", resourceId, resourceTitle); + + // 使用Spring AI标准的过滤删除方法 + String filterExpression = "resourceId == '" + resourceId.toString() + "'"; + vectorStore.delete(filterExpression); + + log.info("✅ 资源ID: {} 的向量文档删除成功", resourceId); + + } catch (Exception e) { + log.error("❌ 删除资源ID: {} 的向量文档失败", resourceId, e); + // 根据业务需求决定是否重新抛出异常 + // throw new VectorOperationException("向量文档删除失败", e); + } + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java index 8806c46..17b45c7 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java @@ -11,6 +11,7 @@ import com.unilife.mapper.PostMapper; import com.unilife.mapper.UserMapper; import com.unilife.model.dto.CreatePostDTO; import com.unilife.model.dto.UpdatePostDTO; +import com.unilife.model.entity.Category; import com.unilife.model.entity.Post; import com.unilife.model.entity.User; import com.unilife.model.vo.PostListVO; @@ -91,8 +92,15 @@ public class PostServiceImpl implements PostService { User user = userMapper.getUserById(post.getUserId()); // 获取分类信息 - // 注意:这里假设已经创建了CategoryMapper接口,实际开发中需要先创建 - // Category category = categoryMapper.getById(post.getCategoryId()); + Category category = categoryMapper.getById(post.getCategoryId()); + String categoryName = category != null ? category.getName() : "未知分类"; + + // 查询用户是否点赞过该帖子 + boolean isLiked = false; + if (userId != null) { + Boolean liked = postLikeMapper.isLiked(postId, userId); + isLiked = liked != null && liked; + } // 构建返回数据 PostVO postVO = PostVO.builder() @@ -100,14 +108,13 @@ public class PostServiceImpl implements PostService { .title(post.getTitle()) .content(post.getContent()) .userId(post.getUserId()) - .nickname(user != null ? user.getNickname() : "未知用户") .avatar(user != null ? user.getAvatar() : null) .categoryId(post.getCategoryId()) - .categoryName("未知分类") // 实际开发中应该从category对象获取 + .categoryName(categoryName) // 使用从数据库查询到的真实分类名称 .viewCount(post.getViewCount() + 1) // 已经增加了浏览次数 .likeCount(post.getLikeCount()) .commentCount(post.getCommentCount()) - .isLiked(false) // 实际开发中应该查询用户是否点赞 + .isLiked(isLiked) // 设置已查询的点赞状态 .createdAt(post.getCreatedAt()) .updatedAt(post.getUpdatedAt()) .build(); @@ -116,24 +123,62 @@ public class PostServiceImpl implements PostService { } @Override - public Result getPostList(Long categoryId, Integer page, Integer size, String sort) { + public Result getPostList(Long categoryId, String keyword, Integer page, Integer size, String sort, Long userId) { // 参数校验 if (page == null || page < 1) page = 1; if (size == null || size < 1 || size > 50) size = 10; if (StrUtil.isBlank(sort)) sort = "latest"; + // 添加调试日志 + log.info("getPostList - 接收到的参数: categoryId={}, keyword={}, page={}, size={}, sort={}, userId={}", + categoryId, keyword, page, size, sort, userId); + // 只使用PageHelper进行分页,不设置排序 PageHelper.startPage(page, size); - // 调用mapper方法,传入排序参数 - List posts = postMapper.getListByCategory(categoryId, sort); + // 根据是否有关键词选择不同的查询方法 + List posts; + if (StrUtil.isNotBlank(keyword)) { + // 有关键词,使用搜索方法 + posts = postMapper.searchPosts(keyword, categoryId, sort); + } else { + // 无关键词,使用普通列表查询 + posts = postMapper.getListByCategory(categoryId, sort); + } // 获取分页信息 PageInfo pageInfo = new PageInfo<>(posts); + // 收集所有帖子的分类 ID + List categoryIds = posts.stream() + .map(Post::getCategoryId) + .distinct() + .collect(Collectors.toList()); + + // 批量获取分类信息 + Map categoryMap = new HashMap<>(); + if (!categoryIds.isEmpty()) { + categoryIds.forEach(id -> { + Category category = categoryMapper.getById(id); + categoryMap.put(id, category != null ? category.getName() : "未知分类"); + }); + } + // 转换为VO List postListVOs = posts.stream().map(post -> { User user = userMapper.getUserById(post.getUserId()); + + // 查询用户是否点赞过该帖子 + boolean isLiked = false; + if (userId != null) { + log.info("查询帖子 {} 的点赞状态,用户ID: {}", post.getId(), userId); + Boolean liked = postLikeMapper.isLiked(post.getId(), userId); + isLiked = liked != null && liked; + log.info("帖子 {} 的点赞状态查询结果: liked={}, isLiked={}", post.getId(), liked, isLiked); + } else { + log.info("用户ID为null,跳过帖子 {} 的点赞状态查询", post.getId()); + } + return PostListVO.builder() .id(post.getId()) .title(post.getTitle()) @@ -142,14 +187,19 @@ public class PostServiceImpl implements PostService { .nickname(user != null ? user.getNickname() : "未知用户") .avatar(user != null ? user.getAvatar() : null) .categoryId(post.getCategoryId()) - .categoryName("未知分类") + .categoryName(categoryMap.getOrDefault(post.getCategoryId(), "未知分类")) .viewCount(post.getViewCount()) .likeCount(post.getLikeCount()) .commentCount(post.getCommentCount()) + .status(post.getStatus()) + .isLiked(isLiked) .createdAt(post.getCreatedAt()) .build(); }).collect(Collectors.toList()); + log.info("返回的帖子列表中点赞状态: {}", + postListVOs.stream().map(p -> "帖子" + p.getId() + ":isLiked=" + p.getIsLiked()).collect(Collectors.toList())); + // 返回结果 Map data = new HashMap<>(); data.put("total", pageInfo.getTotal()); @@ -208,7 +258,7 @@ public class PostServiceImpl implements PostService { return Result.success(null, "删除成功"); } - + @Override public Result likePost(Long postId, Long userId) { // 获取帖子 @@ -232,6 +282,86 @@ public class PostServiceImpl implements PostService { return Result.success(null, "点赞成功"); } } + @Override + public Result getUserPosts(Long userId, Integer page, Integer size, String sort) { + // 参数校验 + if (userId == null) { + return Result.error(400, "用户ID不能为空"); + } + + // 检查用户是否存在 + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + // 分页查询 + PageHelper.startPage(page, size); + List posts = postMapper.getListByUserId(userId, sort); + PageInfo pageInfo = new PageInfo<>(posts); + + // 获取分类信息 + List categoryIds = posts.stream() + .map(Post::getCategoryId) + .distinct() + .collect(Collectors.toList()); + + // 获取分类名称映射 + Map categoryMap; + if (!categoryIds.isEmpty()) { + // 获取所有分类,然后过滤出需要的分类 + List allCategories = categoryMapper.getList(null); + + // 过滤出匹配的分类 + List filteredCategories = allCategories.stream() + .filter(category -> categoryIds.contains(category.getId())) + .collect(Collectors.toList()); + + // 构建分类ID到名称的映射 + categoryMap = filteredCategories.stream() + .collect(Collectors.toMap(Category::getId, Category::getName)); + } else { + categoryMap = new HashMap<>(); + } + + // 转换为VO + List postVOs = posts.stream().map(post -> { + PostListVO vo = new PostListVO(); + BeanUtil.copyProperties(post, vo); + + // 填充分类名称 + String categoryName = categoryMap.getOrDefault(post.getCategoryId(), "未知分类"); + vo.setCategoryName(categoryName); + + // 获取作者信息 + User author = userMapper.getUserById(post.getUserId()); + if (author != null) { + vo.setNickname(author.getNickname()); + vo.setAvatar(author.getAvatar()); + } + + // 内容摘要 + if (StrUtil.isNotBlank(post.getContent())) { + String content = post.getContent() + .replaceAll("<[^>]*>", "") // 去除HTML标签 + .replaceAll("&[^;]+;", ""); // 去除HTML实体 + vo.setSummary(StrUtil.maxLength(content, 100)); + } + + // 确保status字段被正确复制 + vo.setStatus(post.getStatus()); + + return vo; + }).collect(Collectors.toList()); + + // 构建返回数据 + Map data = new HashMap<>(); + data.put("total", pageInfo.getTotal()); + data.put("pages", pageInfo.getPages()); + data.put("list", postVOs); + + return Result.success(data); + } /** * 生成摘要 diff --git a/unilife-server/src/main/java/com/unilife/service/impl/ResourceServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/ResourceServiceImpl.java index cce8f38..58c8588 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/ResourceServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/ResourceServiceImpl.java @@ -5,6 +5,7 @@ import com.github.pagehelper.PageInfo; import com.unilife.common.result.Result; import com.unilife.mapper.CategoryMapper; import com.unilife.mapper.ResourceMapper; +import com.unilife.mapper.ResourceLikeMapper; import com.unilife.mapper.UserMapper; import com.unilife.model.dto.CreateResourceDTO; import com.unilife.model.entity.Category; @@ -12,24 +13,19 @@ import com.unilife.model.entity.Resource; import com.unilife.model.entity.User; import com.unilife.model.vo.ResourceVO; import com.unilife.service.ResourceService; +import com.unilife.utils.OssService; import lombok.extern.slf4j.Slf4j; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; + +import java.util.*; import java.util.stream.Collectors; + @Slf4j @Service public class ResourceServiceImpl implements ResourceService { @@ -42,9 +38,22 @@ public class ResourceServiceImpl implements ResourceService { @Autowired private CategoryMapper categoryMapper; + + @Autowired + private OssService ossService; + + @Autowired + private ResourceLikeMapper resourceLikeMapper; + + + + @Autowired + private PdfVectorAsyncService pdfVectorAsyncService; // 文件存储路径,实际项目中应该配置在application.yml中 private static final String UPLOAD_DIR = "uploads/resources/"; + // OSS存储目录 + private static final String OSS_DIR = "resources"; @Override @Transactional @@ -67,28 +76,15 @@ public class ResourceServiceImpl implements ResourceService { } try { - // 创建上传目录(如果不存在) - File uploadDir = new File(UPLOAD_DIR); - if (!uploadDir.exists()) { - uploadDir.mkdirs(); - } - - // 生成唯一文件名 - String originalFilename = file.getOriginalFilename(); - String fileExtension = originalFilename != null ? originalFilename.substring(originalFilename.lastIndexOf(".")) : ""; - String newFilename = UUID.randomUUID().toString() + fileExtension; - String filePath = UPLOAD_DIR + newFilename; - - // 保存文件 - Path path = Paths.get(filePath); - Files.write(path, file.getBytes()); - + // 将文件上传到阿里云OSS + String fileUrl = ossService.uploadFile(file, OSS_DIR); + // 创建资源记录 Resource resource = new Resource(); resource.setUserId(userId); resource.setTitle(createResourceDTO.getTitle()); resource.setDescription(createResourceDTO.getDescription()); - resource.setFileUrl(filePath); + resource.setFileUrl(fileUrl); // 存储OSS文件URL resource.setFileSize(file.getSize()); resource.setFileType(file.getContentType()); resource.setCategoryId(createResourceDTO.getCategoryId()); @@ -99,11 +95,23 @@ public class ResourceServiceImpl implements ResourceService { // 保存资源记录 resourceMapper.insert(resource); + // 异步处理PDF文件的向量存储 + if ("application/pdf".equals(file.getContentType())) { + try { + // 先读取文件内容到字节数组,避免异步处理时临时文件被删除 + byte[] fileBytes = file.getBytes(); + pdfVectorAsyncService.processPdfVectorAsync(fileBytes, file.getOriginalFilename(), resource); + log.info("PDF文件已提交异步向量化处理,资源ID: {}", resource.getId()); + } catch (Exception e) { + log.error("读取PDF文件内容失败,跳过向量化处理,资源ID: {}", resource.getId(), e); + } + } + Map data = new HashMap<>(); data.put("resourceId", resource.getId()); return Result.success(data, "资源上传成功"); - } catch (IOException e) { + } catch (Exception e) { log.error("文件上传失败", e); return Result.error(500, "文件上传失败"); } @@ -128,7 +136,7 @@ public class ResourceServiceImpl implements ResourceService { .id(resource.getId()) .title(resource.getTitle()) .description(resource.getDescription()) - .fileUrl(resource.getFileUrl()) + .fileUrl(resource.getFileUrl()) // 直接返回OSS URL .fileSize(resource.getFileSize()) .fileType(resource.getFileType()) .userId(resource.getUserId()) @@ -138,7 +146,7 @@ public class ResourceServiceImpl implements ResourceService { .categoryName(category != null ? category.getName() : "未知分类") .downloadCount(resource.getDownloadCount()) .likeCount(resource.getLikeCount()) - .isLiked(false) // 实际项目中应该查询用户是否点赞 + .isLiked(userId != null ? resourceLikeMapper.isLiked(resourceId, userId) : false) // 查询用户是否点赞 .createdAt(resource.getCreatedAt()) .updatedAt(resource.getUpdatedAt()) .build(); @@ -147,14 +155,14 @@ public class ResourceServiceImpl implements ResourceService { } @Override - public Result getResourceList(Long categoryId, Long userId, String keyword, Integer page, Integer size) { + public Result getResourceList(Long categoryId, Long uploaderUserId, String keyword, Integer page, Integer size, Long currentUserId) { // 参数校验 if (page == null || page < 1) page = 1; if (size == null || size < 1 || size > 50) size = 10; // 分页查询 PageHelper.startPage(page, size); - List resources = resourceMapper.getList(categoryId, userId, keyword); + List resources = resourceMapper.getList(categoryId, uploaderUserId, keyword); PageInfo pageInfo = new PageInfo<>(resources); // 转换为VO @@ -177,7 +185,7 @@ public class ResourceServiceImpl implements ResourceService { .categoryName(category != null ? category.getName() : "未知分类") .downloadCount(resource.getDownloadCount()) .likeCount(resource.getLikeCount()) - .isLiked(false) // 实际项目中应该查询用户是否点赞 + .isLiked(currentUserId != null ? resourceLikeMapper.isLiked(resource.getId(), currentUserId) : false) // 查询当前用户是否点赞 .createdAt(resource.getCreatedAt()) .updatedAt(resource.getUpdatedAt()) .build(); @@ -239,7 +247,25 @@ public class ResourceServiceImpl implements ResourceService { return Result.error(403, "无权限删除此资源"); } - // 删除资源(逻辑删除) + // 先启动异步删除向量库中的相关文档(仅针对PDF文件) + // 在删除数据库记录之前启动,确保异步方法能获取到资源信息 + if ("application/pdf".equals(resource.getFileType())) { + pdfVectorAsyncService.deleteVectorDocumentsAsync(resourceId, resource.getTitle()); + log.info("PDF文件已提交异步删除向量文档,资源ID: {}", resourceId); + } + + // 删除OSS中的文件 + try { + String fileUrl = resource.getFileUrl(); + if (fileUrl != null && fileUrl.startsWith("http")) { + ossService.deleteFile(fileUrl); + } + } catch (Exception e) { + log.error("删除OSS文件失败", e); + // 继续执行,不影响数据库记录的删除 + } + + // 最后删除资源(逻辑删除) resourceMapper.delete(resourceId); return Result.success(null, "删除成功"); @@ -256,15 +282,33 @@ public class ResourceServiceImpl implements ResourceService { // 增加下载次数 resourceMapper.incrementDownloadCount(resourceId); + + // 处理文件URL,生成临时访问链接 + String fileUrl = resource.getFileUrl(); + // 提取对象名称并生成临时访问URL(有效期1小时) + String objectName = ossService.getObjectNameFromUrl(fileUrl); + if (objectName != null) { + // 生成有效期为1小时的临时访问URL + fileUrl = ossService.generatePresignedUrl(objectName, 3600 * 1000); + } - // 返回文件URL + // 返回文件URL和文件名 Map data = new HashMap<>(); - data.put("fileUrl", resource.getFileUrl()); - data.put("fileName", resource.getTitle()); + data.put("fileUrl", fileUrl); + data.put("fileName", resource.getTitle() + getFileExtension(fileUrl)); data.put("fileType", resource.getFileType()); return Result.success(data, "获取下载链接成功"); } + + // 获取文件扩展名 + private String getFileExtension(String filePath) { + int lastDot = filePath.lastIndexOf("."); + if (lastDot > 0) { + return filePath.substring(lastDot); + } + return ""; + } @Override @Transactional @@ -276,17 +320,16 @@ public class ResourceServiceImpl implements ResourceService { } // 检查用户是否已点赞 - // 注意:这里需要创建一个资源点赞表和相应的Mapper,实际开发中需要先创建 - boolean isLiked = false; // resourceLikeMapper.isLiked(resourceId, userId); + boolean isLiked = resourceLikeMapper.isLiked(resourceId, userId); if (isLiked) { // 取消点赞 - // resourceLikeMapper.delete(resourceId, userId); + resourceLikeMapper.delete(resourceId, userId); resourceMapper.decrementLikeCount(resourceId); return Result.success(null, "取消点赞成功"); } else { // 添加点赞 - // resourceLikeMapper.insert(resourceId, userId); + resourceLikeMapper.insert(resourceId, userId); resourceMapper.incrementLikeCount(resourceId); return Result.success(null, "点赞成功"); } @@ -336,4 +379,7 @@ public class ResourceServiceImpl implements ResourceService { return Result.success(data); } + + + } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java index e4701d7..4206d8a 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java @@ -5,6 +5,9 @@ import cn.hutool.core.util.RandomUtil; import com.unilife.common.constant.RedisConstant; import com.unilife.common.result.Result; import com.unilife.mapper.UserMapper; +import com.unilife.mapper.PostMapper; +import com.unilife.mapper.CommentMapper; +import com.unilife.mapper.PostLikeMapper; import com.unilife.model.dto.LoginDTO; import com.unilife.model.dto.LoginEmailDTO; import com.unilife.model.dto.RegisterDTO; @@ -12,8 +15,8 @@ import com.unilife.model.dto.UpdateEmailDTO; import com.unilife.model.dto.UpdatePasswordDTO; import com.unilife.model.dto.UpdateProfileDTO; import com.unilife.model.entity.User; +import com.unilife.model.entity.Post; import com.unilife.model.vo.LoginVO; -import com.unilife.model.vo.RegisterVO; import com.unilife.service.IPLocationService; import com.unilife.service.UserService; import com.unilife.utils.JwtUtil; @@ -21,7 +24,6 @@ import com.unilife.utils.RegexUtils; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import jakarta.servlet.http.HttpServletRequest; -import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -33,12 +35,11 @@ import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; - - import java.time.Duration; import java.time.LocalDateTime; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import static com.unilife.common.constant.RedisConstant.LOGIN_EMAIL_KEY; @@ -54,6 +55,15 @@ public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; + @Autowired + private PostMapper postMapper; + + @Autowired + private CommentMapper commentMapper; + + @Autowired + private PostLikeMapper postLikeMapper; + @Autowired private JavaMailSender mailSender; @@ -113,6 +123,12 @@ public class UserServiceImpl implements UserService { String username = StringUtils.isNotEmpty(registerDTO.getUsername()) ? registerDTO.getUsername() : email.substring(0, Math.min(email.indexOf('@'), 10)) + "_" + RandomUtil.randomString(4); user.setUsername(username); + // 设置学生信息 + user.setStudentId(registerDTO.getStudentId()); + user.setDepartment(registerDTO.getDepartment()); + user.setMajor(registerDTO.getMajor()); + user.setGrade(registerDTO.getGrade()); + // 设置其他默认值 user.setRole((byte) 0); // 普通用户 user.setStatus((byte) 1); // 启用状态 @@ -150,11 +166,22 @@ public class UserServiceImpl implements UserService { @Override public Result login(LoginDTO loginDTO,HttpServletRequest request) { - if(loginDTO==null|| StringUtils.isEmpty(loginDTO.getEmail())||StringUtils.isEmpty(loginDTO.getPassword())){ - return Result.error(400,"邮箱或密码不能为空"); - } - - User user = userMapper.findByEmail(loginDTO.getEmail()); + if(loginDTO==null|| StringUtils.isEmpty(loginDTO.getAccount())||StringUtils.isEmpty(loginDTO.getPassword())){ + return Result.error(400,"账号或密码不能为空"); + } + + String account = loginDTO.getAccount(); + User user = null; + + // 判断输入的是邮箱还是用户名 + if (RegexUtils.isEmailInvalid(account)) { + // 不是邮箱格式,按用户名查询 + user = userMapper.findByUsername(account); + } else { + // 是邮箱格式,按邮箱查询 + user = userMapper.findByEmail(account); + } + if (user == null) { return Result.error(400, "账号或密码错误"); } @@ -167,7 +194,6 @@ public class UserServiceImpl implements UserService { return Result.error(403, "账号已被禁用,请联系管理员"); } - String LastLogIpLocation = user.getLoginIp(); String currentIp = ipLocationService.getClientIP(request); String ipLocation = ipLocationService.getIPLocation(currentIp); @@ -179,7 +205,6 @@ public class UserServiceImpl implements UserService { BeanUtil.copyProperties(user,loginVO); String message = StringUtils.isEmpty(LastLogIpLocation) ? "首次登录" : "上次登录IP归属地为" + LastLogIpLocation; return Result.success(loginVO, message); - } @Override @@ -339,24 +364,49 @@ public class UserServiceImpl implements UserService { @Override public Result updateUserProfile(Long userId, UpdateProfileDTO profileDTO) { - // 检查用户是否存在 - User user = userMapper.getUserById(userId); - if (user == null) { + User currentUser = userMapper.getUserById(userId); // Changed from findById to getUserById based on UserMapper.java + if (currentUser == null) { return Result.error(404, "用户不存在"); } - // 更新用户信息 - user.setNickname(profileDTO.getNickname()); - user.setBio(profileDTO.getBio()); - user.setGender(profileDTO.getGender()); - user.setDepartment(profileDTO.getDepartment()); - user.setMajor(profileDTO.getMajor()); - user.setGrade(profileDTO.getGrade()); + // 检查用户名是否更改以及是否重复 + if (StringUtils.isNotEmpty(profileDTO.getUsername()) && !profileDTO.getUsername().equals(currentUser.getUsername())) { + User existingUserWithNewUsername = userMapper.findByUsername(profileDTO.getUsername()); + if (existingUserWithNewUsername != null) { + return Result.error(409, "用户名已被占用,请选择其他用户名"); // 409 Conflict + } + currentUser.setUsername(profileDTO.getUsername()); + } - // 保存更新 - userMapper.updateUserProfile(user); + // 更新用户信息 + // 注意:这里应该只更新profileDTO中存在的字段,且要考虑空值情况 + // if (StringUtils.isNotEmpty(profileDTO.getNickname())) { // Commented out as nickname is removed from DTO + // user.setNickname(profileDTO.getNickname()); + // } + if (StringUtils.isNotEmpty(profileDTO.getBio())) { + currentUser.setBio(profileDTO.getBio()); // Changed user to currentUser + } + if (profileDTO.getGender() != null) { + currentUser.setGender(profileDTO.getGender()); // Changed user to currentUser + } + if (StringUtils.isNotEmpty(profileDTO.getDepartment())) { + currentUser.setDepartment(profileDTO.getDepartment()); // Changed user to currentUser + } + if (StringUtils.isNotEmpty(profileDTO.getMajor())) { + currentUser.setMajor(profileDTO.getMajor()); // Changed user to currentUser + } + if (StringUtils.isNotEmpty(profileDTO.getGrade())) { + currentUser.setGrade(profileDTO.getGrade()); // Changed user to currentUser + } - return Result.success(null, "个人资料更新成功"); + try { + userMapper.updateUserProfile(currentUser); // Call void method + log.info("用户 {} 的个人资料更新成功", userId); + return Result.success("个人资料更新成功"); + } catch (Exception e) { + log.error("用户 {} 的个人资料更新时发生数据库错误: {}", userId, e.getMessage()); + return Result.error(500, "个人资料更新失败,服务器内部错误"); + } } @Override @@ -466,4 +516,121 @@ public class UserServiceImpl implements UserService { return Result.success(null, "邮箱更新成功"); } + + @Override + public Result getUserStats(Long userId) { + // 检查用户是否存在 + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + // 获取用户统计数据 + Integer totalPosts = postMapper.getCountByUserId(userId); + + // 获取用户所有帖子的总点赞数 + List userPosts = postMapper.getListByUserId(userId, "latest"); + Integer totalLikes = userPosts.stream() + .mapToInt(post -> post.getLikeCount() != null ? post.getLikeCount() : 0) + .sum(); + + // 获取用户所有帖子的总评论数 + Integer totalComments = userPosts.stream() + .mapToInt(post -> post.getCommentCount() != null ? post.getCommentCount() : 0) + .sum(); + + // 获取用户所有帖子的总浏览数 + Integer totalViews = userPosts.stream() + .mapToInt(post -> post.getViewCount() != null ? post.getViewCount() : 0) + .sum(); + + // 构建统计数据 + Map stats = new HashMap<>(); + stats.put("totalPosts", totalPosts != null ? totalPosts : 0); + stats.put("totalLikes", totalLikes); + stats.put("totalComments", totalComments); + stats.put("totalViews", totalViews); + + return Result.success(stats); + } + + @Override + public Result getUserRecentPosts(Long userId, Integer limit) { + try { + // 获取用户信息,验证用户是否存在 + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + // 获取用户最近的帖子 + List recentPosts = postMapper.getListByUserId(userId, "time"); + + // 如果指定了限制数量,则截取 + if (limit != null && limit > 0 && recentPosts.size() > limit) { + recentPosts = recentPosts.subList(0, limit); + } + + Map result = new HashMap<>(); + result.put("posts", recentPosts); + result.put("totalCount", postMapper.getCountByUserId(userId)); + + return Result.success(result, "获取用户最近帖子成功"); + } catch (Exception e) { + log.error("获取用户最近帖子失败: userId={}", userId, e); + return Result.error(500, "获取用户最近帖子失败"); + } + } + + @Override + public Result deleteUser(Long userId) { + try { + // 验证用户是否存在 + User user = userMapper.getUserById(userId); + if (user == null) { + return Result.error(404, "用户不存在"); + } + + // 检查用户状态,避免重复删除 + if (user.getStatus() == 0) { + return Result.error(400, "用户已被删除"); + } + + log.info("开始删除用户及其相关数据: userId={}, username={}", userId, user.getUsername()); + + // 1. 软删除用户的所有帖子 + userMapper.deleteUserPosts(userId); + log.info("已删除用户的所有帖子: userId={}", userId); + + // 2. 软删除用户的所有评论 + userMapper.deleteUserComments(userId); + log.info("已删除用户的所有评论: userId={}", userId); + + // 3. 软删除用户的所有资源 + userMapper.deleteUserResources(userId); + log.info("已删除用户的所有资源: userId={}", userId); + + // 4. 删除用户的所有课程(物理删除,因为是个人课程安排) + userMapper.deleteUserCourses(userId); + log.info("已删除用户的所有课程: userId={}", userId); + + // 5. 删除用户的所有日程(物理删除,因为是个人日程安排) + userMapper.deleteUserSchedules(userId); + log.info("已删除用户的所有日程: userId={}", userId); + + // 6. 删除用户的所有点赞记录(物理删除) + userMapper.deleteUserLikes(userId); + log.info("已删除用户的所有点赞记录: userId={}", userId); + + // 7. 最后软删除用户本身 + userMapper.deleteUser(userId); + log.info("已删除用户账号: userId={}, username={}", userId, user.getUsername()); + + return Result.success(null, "用户及其相关数据删除成功"); + + } catch (Exception e) { + log.error("删除用户失败: userId={}", userId, e); + return Result.error(500, "删除用户失败:" + e.getMessage()); + } + } } diff --git a/unilife-server/src/main/java/com/unilife/utils/JwtUtil.java b/unilife-server/src/main/java/com/unilife/utils/JwtUtil.java index c33d5fd..d1df794 100644 --- a/unilife-server/src/main/java/com/unilife/utils/JwtUtil.java +++ b/unilife-server/src/main/java/com/unilife/utils/JwtUtil.java @@ -1,6 +1,7 @@ package com.unilife.utils; import cn.hutool.core.date.DateTime; +import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -24,24 +25,45 @@ public class JwtUtil { Map payload = new HashMap<>(); payload.put("userId", id); - payload.put("created",now.getTime()); - return JWTUtil.createToken(payload,secret.getBytes()); + payload.put("created", now.getTime()); + payload.put("exp", expireTime.getTime() / 1000); // JWT标准过期时间字段(秒) + return JWTUtil.createToken(payload, secret.getBytes()); } public boolean verifyToken(String token) { - try{ - JWTUtil.verify(token,secret.getBytes()); + try { + // 验证token签名 + JWTUtil.verify(token, secret.getBytes()); + + // 验证过期时间 + JWT jwt = JWTUtil.parseToken(token); + Object expObj = jwt.getPayload("exp"); + if (expObj != null) { + long exp = Long.parseLong(expObj.toString()); + long currentTime = System.currentTimeMillis() / 1000; + if (currentTime > exp) { + log.debug("Token已过期: exp={}, current={}", exp, currentTime); + return false; + } + } + return true; - }catch (Exception e){ + } catch (Exception e) { + log.debug("Token验证失败: {}", e.getMessage()); return false; } } + public Long getUserIdFromToken(String token) { try { + // 先验证token是否有效 + if (!verifyToken(token)) { + return null; + } return Long.valueOf(JWTUtil.parseToken(token).getPayload("userId").toString()); - }catch (Exception e){ + } catch (Exception e) { + log.debug("从Token获取用户ID失败: {}", e.getMessage()); return null; } } - } diff --git a/unilife-server/src/main/java/com/unilife/utils/OssService.java b/unilife-server/src/main/java/com/unilife/utils/OssService.java new file mode 100644 index 0000000..cf849b8 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/utils/OssService.java @@ -0,0 +1,139 @@ +package com.unilife.utils; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.model.OSSObject; +import com.aliyun.oss.model.GeneratePresignedUrlRequest; +import com.unilife.config.OssConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +@Slf4j +@Component +public class OssService { + + @Autowired + private OSS ossClient; + + @Autowired + private OssConfig ossConfig; + + /** + * 上传文件到OSS + * @param file 文件 + * @param dir 存储目录 + * @return 返回文件访问URL + */ + public String uploadFile(MultipartFile file, String dir) { + String bucketName = ossConfig.getBucketName(); + String urlPrefix = ossConfig.getUrlPrefix(); + + try { + // 获取文件原始名称 + String originalFilename = file.getOriginalFilename(); + // 获取文件后缀 + String suffix = ""; + if (originalFilename != null && originalFilename.contains(".")) { + suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + + // 构建OSS存储路径:目录/日期/随机UUID.后缀 + String dateDir = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); + String filename = UUID.randomUUID().toString().replaceAll("-", "") + suffix; + String objectName = dir + "/" + dateDir + "/" + filename; + + // 获取文件输入流 + InputStream inputStream = file.getInputStream(); + + // 上传文件到OSS + ossClient.putObject(bucketName, objectName, inputStream); + + // 关闭输入流 + inputStream.close(); + + // 返回文件访问URL + return urlPrefix + objectName; + } catch (IOException e) { + log.error("上传文件到OSS失败: ", e); + throw new RuntimeException("上传文件失败"); + } + } + + /** + * 从OSS删除文件 + * @param fileUrl 文件URL + */ + public void deleteFile(String fileUrl) { + String bucketName = ossConfig.getBucketName(); + String urlPrefix = ossConfig.getUrlPrefix(); + + if (fileUrl.startsWith(urlPrefix)) { + String objectName = fileUrl.substring(urlPrefix.length()); + ossClient.deleteObject(bucketName, objectName); + } + } + + /** + * 获取OSS文件输入流 + * @param fileUrl 文件URL + * @return 文件输入流 + */ + public InputStream getFileStream(String fileUrl) { + String bucketName = ossConfig.getBucketName(); + String urlPrefix = ossConfig.getUrlPrefix(); + + if (fileUrl.startsWith(urlPrefix)) { + String objectName = fileUrl.substring(urlPrefix.length()); + OSSObject ossObject = ossClient.getObject(bucketName, objectName); + return ossObject.getObjectContent(); + } + return null; + } + + /** + * 获取文件的临时访问URL(适用于私有bucket) + * @param objectName 对象名称 + * @param expiration 过期时间(毫秒) + * @return 带签名的临时URL + */ + public String generatePresignedUrl(String objectName, long expiration) { + String bucketName = ossConfig.getBucketName(); + + try { + // 计算过期时间 + Date expirationDate = new Date(System.currentTimeMillis() + expiration); + + // 创建请求 + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectName); + request.setExpiration(expirationDate); + + // 生成URL + URL url = ossClient.generatePresignedUrl(request); + return url.toString(); + } catch (Exception e) { + log.error("生成临时访问URL失败: ", e); + throw new RuntimeException("生成临时访问URL失败"); + } + } + + /** + * 从完整URL中提取对象名称 + * @param fileUrl 完整的文件URL + * @return 对象名称 + */ + public String getObjectNameFromUrl(String fileUrl) { + String urlPrefix = ossConfig.getUrlPrefix(); + if (fileUrl != null && fileUrl.startsWith(urlPrefix)) { + return fileUrl.substring(urlPrefix.length()); + } + return null; + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/utils/ThumbnailService.java b/unilife-server/src/main/java/com/unilife/utils/ThumbnailService.java new file mode 100644 index 0000000..e69de29 diff --git a/unilife-server/src/main/resources/application.yml b/unilife-server/src/main/resources/application.yml index f57384a..4540f91 100644 --- a/unilife-server/src/main/resources/application.yml +++ b/unilife-server/src/main/resources/application.yml @@ -1,6 +1,38 @@ server: port: 8087 + # 超时配置 + tomcat: + connection-timeout: 60000 # 连接超时60秒 + threads: + max: 100 # 最大线程数 spring: + main: + allow-bean-definition-overriding: true # 允许Bean定义覆盖 + ai: + openai: + base-url: https://dashscope.aliyuncs.com/compatible-mode + api-key: ${OPENAI_API_KEY:sk-temp-key-for-testing} + chat: + options: + model: qwen-max-latest + embedding: + options: + model: text-embedding-v3 + dimensions: 1024 + chat: + memory: + repository: + jdbc: + initialize-schema: always # 自动初始化表结构 + schema: classpath:schema-mysql.sql + # Chroma向量存储配置 + vectorstore: + chroma: + client: + host: http://localhost + port: 8000 + collection-name: unilife_collection + initialize-schema: true datasource: url: jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 username: root @@ -9,8 +41,8 @@ spring: mail: host: smtp.163.com port: 465 - username: c2991692032@163.com - password: VPq5u3NcAAqtG9GT + username: ${MAIL_USERNAME:} + password: ${MAIL_PASSWORD:} properties: mail: smtp: @@ -20,10 +52,26 @@ spring: socketFactory: port: 465 class: javax.net.ssl.SSLSocketFactory - data: - redis: - port: 6379 - host: 127.0.0.1 + # Redis基本配置(用于缓存等功能) + data: + redis: + port: 6379 + host: 127.0.0.1 + database: 0 + timeout: 10000ms + lettuce: + pool: + max-active: 8 + max-wait: -1ms + max-idle: 8 + min-idle: 0 + + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB + session: + timeout: 30m # 会话超时30分钟 knife4j: enable: true openapi: @@ -49,6 +97,25 @@ mybatis: logging: level: com.unilife: debug + org.springframework.ai: debug jwt: secret: qwertyuiopasdfghjklzxcvbnm - expiration: 86400 \ No newline at end of file + expiration: 300 # 5分钟过期时间(5 * 60 = 300秒),用于测试 +# 添加阿里云OSS配置 +aliyun: + oss: + endpoint: ${ALIYUN_OSS_ENDPOINT:your-endpoint} + accessKeyId: ${ALIYUN_OSS_ACCESS_KEY_ID:your-access-key-id} + accessKeySecret: ${ALIYUN_OSS_ACCESS_KEY_SECRET:your-access-key-secret} + bucketName: ${ALIYUN_OSS_BUCKET_NAME:your-bucket-name} + urlPrefix: ${ALIYUN_OSS_URL_PREFIX:https://your-bucket-name.oss-region.aliyuncs.com/} + +# 应用自定义配置 +app: + ai: + auto-title: + enabled: true # 是否启用自动生成会话标题 + strategy: ai # 标题生成策略:simple(简单算法) 或 ai(AI生成) + + + diff --git a/unilife-server/src/main/resources/db/rebuild-database.sql b/unilife-server/src/main/resources/db/rebuild-database.sql new file mode 100644 index 0000000..e8b86d9 --- /dev/null +++ b/unilife-server/src/main/resources/db/rebuild-database.sql @@ -0,0 +1,489 @@ +-- UniLife数据库完整重建脚本 +-- 此脚本将删除现有数据库并重新创建,然后插入测试数据 +-- 注意:执行前请确保已备份重要数据! + +-- ========================================== +-- 第一步:删除并重新创建数据库 +-- ========================================== + +-- 删除现有数据库(如果存在) +DROP DATABASE IF EXISTS UniLife; + +-- 创建新数据库 +CREATE DATABASE UniLife DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 使用数据库 +USE UniLife; + +-- ========================================== +-- 第二步:创建表结构(无外键约束) +-- ========================================== + +-- 用户表 +CREATE TABLE `users` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + `email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱地址(学校邮箱)', + `password` VARCHAR(255) NOT NULL COMMENT '密码(加密存储)', + `nickname` VARCHAR(50) NOT NULL COMMENT '昵称', + `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', + `bio` TEXT DEFAULT NULL COMMENT '个人简介', + `gender` TINYINT DEFAULT 0 COMMENT '性别(0-未知, 1-男, 2-女)', + `student_id` VARCHAR(20) UNIQUE DEFAULT NULL COMMENT '学号', + `department` VARCHAR(100) DEFAULT NULL COMMENT '院系', + `major` VARCHAR(100) DEFAULT NULL COMMENT '专业', + `grade` VARCHAR(20) DEFAULT NULL COMMENT '年级', + `points` INT DEFAULT 0 COMMENT '积分', + `role` TINYINT DEFAULT 0 COMMENT '角色(0-普通用户, 1-版主, 2-管理员)', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-禁用, 1-启用)', + `is_verified` TINYINT DEFAULT 0 COMMENT '是否验证(0-未验证, 1-已验证)', + `login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最近登录IP', + `login_time` DATETIME DEFAULT NULL COMMENT '最近登录时间', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_email` (`email`), + INDEX `idx_username` (`username`), + INDEX `idx_student_id` (`student_id`), + INDEX `idx_role` (`role`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- 分类表 +CREATE TABLE `categories` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID', + `name` VARCHAR(50) NOT NULL UNIQUE COMMENT '分类名称', + `description` VARCHAR(255) DEFAULT NULL COMMENT '分类描述', + `icon` VARCHAR(255) DEFAULT NULL COMMENT '分类图标', + `sort` INT DEFAULT 0 COMMENT '排序', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-禁用, 1-启用)', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_status` (`status`), + INDEX `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='分类表'; + +-- 帖子表 +CREATE TABLE `posts` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '帖子ID', + `user_id` BIGINT NOT NULL COMMENT '发布用户ID', + `title` VARCHAR(100) NOT NULL COMMENT '帖子标题', + `content` TEXT NOT NULL COMMENT '帖子内容', + `category_id` BIGINT NOT NULL COMMENT '分类ID', + `view_count` INT DEFAULT 0 COMMENT '浏览次数', + `like_count` INT DEFAULT 0 COMMENT '点赞次数', + `comment_count` INT DEFAULT 0 COMMENT '评论次数', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-删除, 1-正常, 2-置顶)', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_category_id` (`category_id`), + INDEX `idx_status` (`status`), + INDEX `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='帖子表'; + +-- 评论表 +CREATE TABLE `comments` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '评论ID', + `post_id` BIGINT NOT NULL COMMENT '帖子ID', + `user_id` BIGINT NOT NULL COMMENT '评论用户ID', + `content` TEXT NOT NULL COMMENT '评论内容', + `parent_id` BIGINT DEFAULT NULL COMMENT '父评论ID(回复某条评论)', + `like_count` INT DEFAULT 0 COMMENT '点赞次数', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-删除, 1-正常)', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_post_id` (`post_id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_parent_id` (`parent_id`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论表'; + +-- 点赞表(用户-帖子) +CREATE TABLE `post_likes` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '点赞ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `post_id` BIGINT NOT NULL COMMENT '帖子ID', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UNIQUE KEY `uk_user_post` (`user_id`, `post_id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_post_id` (`post_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='帖子点赞表'; + +-- 点赞表(用户-评论) +CREATE TABLE `comment_likes` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '点赞ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `comment_id` BIGINT NOT NULL COMMENT '评论ID', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UNIQUE KEY `uk_user_comment` (`user_id`, `comment_id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_comment_id` (`comment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论点赞表'; + +-- 点赞表(用户-资源) +CREATE TABLE `resource_likes` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '点赞ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `resource_id` BIGINT NOT NULL COMMENT '资源ID', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UNIQUE KEY `uk_user_resource` (`user_id`, `resource_id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_resource_id` (`resource_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源点赞表'; + +-- 资源表 +CREATE TABLE `resources` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '资源ID', + `user_id` BIGINT NOT NULL COMMENT '上传用户ID', + `title` VARCHAR(100) NOT NULL COMMENT '资源标题', + `description` TEXT DEFAULT NULL COMMENT '资源描述', + `file_url` VARCHAR(255) NOT NULL COMMENT '文件URL', + `file_size` BIGINT NOT NULL COMMENT '文件大小(字节)', + `file_type` VARCHAR(100) NOT NULL COMMENT '文件类型', + `category_id` BIGINT NOT NULL COMMENT '分类ID', + `download_count` INT DEFAULT 0 COMMENT '下载次数', + `like_count` INT DEFAULT 0 COMMENT '点赞次数', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-删除, 1-正常)', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_category_id` (`category_id`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源表'; + +-- 课程表 +CREATE TABLE `courses` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `name` VARCHAR(100) NOT NULL COMMENT '课程名称', + `teacher` VARCHAR(50) DEFAULT NULL COMMENT '教师姓名', + `location` VARCHAR(100) DEFAULT NULL COMMENT '上课地点', + `day_of_week` TINYINT NOT NULL COMMENT '星期几(1-7)', + `start_time` TIME NOT NULL COMMENT '开始时间', + `end_time` TIME NOT NULL COMMENT '结束时间', + `start_week` SMALLINT NOT NULL COMMENT '开始周次', + `end_week` SMALLINT NOT NULL COMMENT '结束周次', + `semester` VARCHAR(20) DEFAULT NULL COMMENT '学期(如:2023-1)', + `color` VARCHAR(20) DEFAULT NULL COMMENT '显示颜色', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-删除, 1-正常)', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_semester` (`semester`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程表'; + +-- 日程表 +CREATE TABLE `schedules` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日程ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `title` VARCHAR(100) NOT NULL COMMENT '日程标题', + `description` TEXT DEFAULT NULL COMMENT '日程描述', + `start_time` DATETIME NOT NULL COMMENT '开始时间', + `end_time` DATETIME NOT NULL COMMENT '结束时间', + `location` VARCHAR(100) DEFAULT NULL COMMENT '地点', + `is_all_day` TINYINT DEFAULT 0 COMMENT '是否全天(0-否, 1-是)', + `reminder` INT DEFAULT NULL COMMENT '提醒时间(分钟)', + `color` VARCHAR(20) DEFAULT NULL COMMENT '显示颜色', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-删除, 1-正常)', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_start_time` (`start_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日程表'; + +-- ========================================== +-- 第三步:插入初始分类数据 +-- ========================================== + +INSERT INTO `categories` (`name`, `description`, `icon`, `sort`, `status`) VALUES +('学习交流', '讨论学习相关话题', 'icon-study', 1, 1), +('校园生活', '分享校园生活点滴', 'icon-campus', 2, 1), +('兴趣爱好', '交流各类兴趣爱好', 'icon-hobby', 3, 1), +('求职就业', '分享求职经验和就业信息', 'icon-job', 4, 1), +('资源共享', '分享各类学习资源', 'icon-resource', 5, 1); + +-- 初始化管理员账号 +INSERT INTO `users` (`username`, `email`, `password`, `nickname`, `role`, `status`, `is_verified`, `created_at`) VALUES +('admin', 'admin@unilife.com', '123456', '系统管理员', 2, 1, 1, '2024-01-01 00:00:00'), +('superadmin', 'superadmin@unilife.com', 'admin123', '超级管理员', 2, 1, 1, '2024-01-01 00:00:00'); + +-- ========================================== +-- 第四步:插入测试数据 +-- ========================================== + +-- 更新现有分类数据(更贴合武汉大学实际) +UPDATE `categories` SET + `name` = '学术交流', + `description` = '学术讨论、科研分享、竞赛经验', + `icon` = '📚' +WHERE `name` = '学习交流'; + +UPDATE `categories` SET + `description` = '武汉大学校园生活、社团活动、文化娱乐', + `icon` = '🏫' +WHERE `name` = '校园生活'; + +UPDATE `categories` SET + `name` = '就业实习', + `description` = '实习经验、求职心得、职业规划', + `icon` = '💼' +WHERE `name` = '求职就业'; + +-- 插入新的分类 +INSERT INTO `categories` (`name`, `description`, `icon`, `sort`, `status`) VALUES +('学院专区', '各学院学生交流专区', '🎓', 6, 1), +('考研考公', '研究生入学考试、公务员考试', '📖', 7, 1), +('生活服务', '二手交易、失物招领、校园服务', '🛍️', 8, 1); + +-- 插入武汉大学学生用户数据 +INSERT INTO `users` (`username`, `email`, `password`, `nickname`, `bio`, `gender`, `student_id`, `department`, `major`, `grade`, `points`, `role`, `status`, `is_verified`, `created_at`) VALUES +-- 文理学部学生 +('czq2024', 'czq@whu.edu.cn', '123456', '珞珈数学狗', '数学与统计学院2022级数学类,热爱数学建模,ACM银牌选手', 1, '2022301140001', '数学与统计学院', '数学类', '2022级', 150, 0, 1, 1, '2024-09-01 09:00:00'), +('lihua_cs', 'lihua@whu.edu.cn', '123456', '代码诗人', '计算机学院2021级软件工程,全栈开发爱好者,开源项目贡献者', 1, '2021301120001', '计算机学院', '软件工程', '2021级', 230, 0, 1, 1, '2024-09-01 10:00:00'), +('wangming_law', 'wangming@whu.edu.cn', '123456', '法学小白', '法学院2023级法学专业,模拟法庭常客,梦想成为大律师', 1, '2023301080001', '法学院', '法学', '2023级', 80, 0, 1, 1, '2024-09-01 11:00:00'), +('zhangwei_chem', 'zhangwei@whu.edu.cn', '123456', '化学实验员', '化学与分子科学学院2022级化学专业,实验室常驻,合成达人', 1, '2022301130001', '化学与分子科学学院', '化学', '2022级', 120, 0, 1, 1, '2024-09-01 12:00:00'), +('liuxin_econ', 'liuxin@whu.edu.cn', '123456', '经济观察者', '经济与管理学院2021级经济学,关注宏观经济政策,券商实习生', 2, '2021301110001', '经济与管理学院', '经济学', '2021级', 200, 0, 1, 1, '2024-09-01 13:00:00'), + +-- 工学部学生 +('chenfei_water', 'chenfei@whu.edu.cn', '123456', '水利工程师', '水利水电学院2022级水利水电工程,三峡实习经历,立志建设美丽中国', 1, '2022301320001', '水利水电学院', '水利水电工程', '2022级', 90, 0, 1, 1, '2024-09-01 14:00:00'), +('sunhao_power', 'sunhao@whu.edu.cn', '123456', '电气小子', '电气与自动化学院2023级电气工程及其自动化,电力系统仿真专家', 1, '2023301330001', '电气与自动化学院', '电气工程及其自动化', '2023级', 70, 0, 1, 1, '2024-09-01 15:00:00'), +('wujing_civil', 'wujing@whu.edu.cn', '123456', '土木妹子', '土木建筑工程学院2022级土木工程,桥梁设计爱好者,BIM技术达人', 2, '2022301340001', '土木建筑工程学院', '土木工程', '2022级', 110, 0, 1, 1, '2024-09-01 16:00:00'), + +-- 信息学部学生 +('liqiang_remote', 'liqiang@whu.edu.cn', '123456', '遥感专家', '遥感信息工程学院2021级遥感科学与技术,无人机航拍爱好者', 1, '2021301210001', '遥感信息工程学院', '遥感科学与技术', '2021级', 180, 0, 1, 1, '2024-09-01 17:00:00'), +('zhaoli_survey', 'zhaoli@whu.edu.cn', '123456', '测绘达人', '测绘学院2022级测绘工程,GPS定位技术研究者,野外作业经验丰富', 1, '2022301220001', '测绘学院', '测绘工程', '2022级', 95, 0, 1, 1, '2024-09-01 18:00:00'), + +-- 医学部学生 +('huangyan_med', 'huangyan@whu.edu.cn', '123456', '未来医生', '基础医学院2020级临床医学,人民医院实习生,立志救死扶伤', 2, '2020301410001', '基础医学院', '临床医学', '2020级', 250, 0, 1, 1, '2024-09-01 19:00:00'), +('wangpeng_dental', 'wangpeng@whu.edu.cn', '123456', '口腔医师', '口腔医学院2021级口腔医学,口腔医院见习,关注口腔健康科普', 1, '2021301420001', '口腔医学院', '口腔医学', '2021级', 160, 0, 1, 1, '2024-09-01 20:00:00'), + +-- 人文社科学部学生 +('luxiaoya_chinese', 'luxiaoya@whu.edu.cn', '123456', '文学少女', '文学院2022级汉语言文学,古典文学爱好者,诗词社社长', 2, '2022301050001', '文学院', '汉语言文学', '2022级', 140, 0, 1, 1, '2024-09-01 21:00:00'), +('zhoujie_history', 'zhoujie@whu.edu.cn', '123456', '史学研究生', '历史学院研究生,中国古代史方向,博物馆志愿者', 1, '2024302050001', '历史学院', '中国史', '2024级', 100, 0, 1, 1, '2024-09-01 22:00:00'), +('tanglei_news', 'tanglei@whu.edu.cn', '123456', '新传人', '新闻与传播学院2021级新闻学,校媒记者,关注社会热点', 1, '2021301070001', '新闻与传播学院', '新闻学', '2021级', 170, 0, 1, 1, '2024-09-01 23:00:00'); + +-- 插入论坛帖子数据(使用已存在的用户ID,初始计数设为0) +INSERT INTO `posts` (`user_id`, `category_id`, `title`, `content`, `view_count`, `like_count`, `comment_count`, `status`, `created_at`) VALUES +-- 学术交流类帖子 +(2, 1, '数学建模美赛经验分享', '刚刚结束的美国大学生数学建模竞赛,我们团队获得了M奖!分享一下参赛经验和技巧,希望对学弟学妹们有帮助。数模比赛不仅考验数学能力,更重要的是团队协作和论文写作能力。首先要选择合适的队友,最好是数学、编程、英语各有所长的组合...', 256, 0, 0, 2, '2024-12-20 09:30:00'), + +(3, 1, 'ACM-ICPC区域赛总结', '参加了西安站的ACM区域赛,虽然没能拿到金牌,但收获很大。分享一下刷题心得和比赛策略,特别是动态规划和图论算法的练习方法。建议大家多在Codeforces和AtCoder上练习,这些平台的题目质量很高...', 189, 0, 0, 1, '2024-12-19 16:45:00'), + +(6, 1, '宏观经济学课程研讨:通胀与货币政策', '最近在学习宏观经济学,对当前的通胀形势和央行货币政策有一些思考。想和大家讨论一下利率调整对经济的影响机制,特别是在当前全球经济形势下的作用...', 145, 0, 0, 1, '2024-12-18 14:20:00'), + +-- 校园生活类帖子 +(14, 2, '武大樱花季摄影大赛作品展示', '樱花季刚过,分享一些在樱花大道拍摄的照片。今年的樱花开得特别美,虽然人很多,但还是拍到了一些不错的角度。附上拍摄技巧分享!使用的是佳能5D4,光圈f/2.8,ISO400,后期用LR调色...', 1234, 0, 0, 2, '2024-04-10 10:15:00'), + +(16, 2, '校运动会志愿者招募!', '第55届田径运动会即将开始,现招募志愿者!工作内容包括引导、记分、颁奖等。参与志愿服务可获得志愿时长认证,还有纪念品哦~有意向的同学请在评论区留言或私信联系我', 456, 0, 0, 1, '2024-12-15 08:00:00'), + +(11, 2, '测绘学院野外实习日记', '刚从庐山实习回来,分享一下野外测量的酸甜苦辣。早上5点起床,背着仪器爬山,虽然辛苦但收获满满。珞珈山的风景真是看不够啊!学到了很多实际操作技能...', 234, 0, 0, 1, '2024-12-14 19:30:00'), + +-- 学院专区类帖子 +(4, 6, '法学院模拟法庭大赛预告', '一年一度的"枫叶杯"模拟法庭大赛即将开始!欢迎各年级同学组队参加。比赛分为民事组和刑事组,优胜者将代表学院参加全国比赛。这是提升法律实务能力的绝佳机会...', 345, 0, 0, 2, '2024-12-16 11:00:00'), + +(5, 6, '化学实验安全注意事项提醒', '最近实验室发生了几起小事故,提醒大家一定要注意安全!特别是使用强酸强碱时,护目镜和手套必须佩戴。实验无小事,安全第一!同时要做好实验记录...', 178, 0, 0, 1, '2024-12-17 15:20:00'), + +-- 就业实习类帖子 +(6, 3, '券商实习面试经验分享', '刚刚拿到某头部券商的实习offer,分享一下面试经验。金融行业对专业能力和综合素质要求都很高,准备过程中要注意这几个方面:扎实的专业基础、良好的表达能力、对市场的敏感度...', 423, 0, 0, 1, '2024-12-21 14:00:00'), + +(3, 3, 'IT互联网春招总结', '经历了春招季,最终选择了某大厂的后端开发岗位。分享一下投递简历、技术面试、HR面试的全流程经验,希望对计算机专业的同学有帮助。技术面试主要考察数据结构、算法、系统设计...', 567, 0, 0, 2, '2024-05-18 09:15:00'), + +-- 考研考公类帖子 +(15, 7, '历史学考研经验贴', '成功上岸北师大中国史专业!分享一下备考经验:如何选择学校、如何制定复习计划、如何准备专业课等。考研路上不孤单,加油!专业课复习要注意史料分析和论述题...', 389, 0, 0, 1, '2024-12-10 22:00:00'), + +-- 生活服务类帖子 +(9, 8, '出售工科教材一批', '即将毕业,出售一些专业课教材:《结构力学》《材料力学》《工程制图》等,八成新,价格优惠。有需要的学弟学妹可以联系我~都是正版教材,保存得很好', 156, 0, 0, 1, '2024-12-22 18:30:00'), + +(13, 8, '寻找珞珈山丢失的口腔器械包', '昨天在樱花大道丢失了一个蓝色器械包,里面有重要的口腔实习用具。如有好心人捡到,请联系我,必有重谢!器械包上有我的姓名标签', 89, 0, 0, 1, '2024-12-23 07:45:00'); + +-- 插入评论数据 +INSERT INTO `comments` (`post_id`, `user_id`, `content`, `parent_id`, `like_count`, `status`, `created_at`) VALUES +-- 对数学建模帖子的评论 +(1, 3, '恭喜学长!我们正在准备下半年的国赛,请问有什么推荐的学习资料吗?', NULL, 5, 1, '2024-12-20 10:30:00'), +(1, 6, '数模确实需要很强的团队协作能力,我们当时就是沟通不够充分才没拿到好成绩', NULL, 3, 1, '2024-12-20 11:15:00'), +(1, 2, '推荐《数学建模方法与分析》这本书,MATLAB和Python都要熟练掌握', 1, 2, 1, '2024-12-20 12:00:00'), + +-- 对樱花帖子的评论 +(4, 10, '照片拍得真美!求拍摄参数和后期处理方法', NULL, 8, 1, '2024-04-10 14:30:00'), +(4, 16, '武大的樱花确实是一绝,每年都要来打卡', NULL, 4, 1, '2024-04-10 15:45:00'), + +-- 对法学院帖子的评论 +(7, 15, '法学院的模拟法庭一直很有名,想去观摩学习', NULL, 3, 1, '2024-12-16 13:00:00'), +(7, 4, '欢迎其他学院的同学来观摩!比赛时间是下周五晚上', 6, 1, 1, '2024-12-16 14:30:00'), + +-- 对实习帖子的评论 +(9, 2, '金融行业竞争确实激烈,学长有什么建议给想进入这个行业的同学吗?', NULL, 4, 1, '2024-12-21 15:30:00'), +(9, 6, '建议先把CFA一级考出来,然后多参加实习积累经验', 8, 6, 1, '2024-12-21 16:45:00'); + +-- 插入点赞数据 +INSERT INTO `post_likes` (`user_id`, `post_id`, `created_at`) VALUES +-- 用户点赞帖子 +(2, 4, '2024-04-10 11:00:00'), +(2, 7, '2024-12-16 12:00:00'), +(3, 1, '2024-12-20 10:00:00'), +(3, 4, '2024-04-10 16:00:00'), +(4, 1, '2024-12-20 11:30:00'), +(4, 9, '2024-12-21 15:00:00'), +(5, 2, '2024-12-19 17:30:00'), +(6, 1, '2024-12-20 13:00:00'), +(6, 10, '2024-05-18 10:00:00'), +(7, 1, '2024-12-20 14:00:00'), +(8, 4, '2024-04-10 17:00:00'), +(9, 7, '2024-12-16 15:00:00'), +(10, 4, '2024-04-10 18:00:00'), +(11, 9, '2024-12-21 16:00:00'), +(12, 1, '2024-12-20 15:00:00'), +(13, 4, '2024-04-10 19:00:00'), +(14, 7, '2024-12-16 16:00:00'), +(15, 9, '2024-12-21 17:00:00'), +(16, 4, '2024-04-10 20:00:00'), +(2, 1, '2024-12-20 16:00:00'), +(3, 7, '2024-12-16 17:00:00'), +(5, 4, '2024-04-10 21:00:00'), +(6, 9, '2024-12-21 18:00:00'), +(8, 1, '2024-12-20 17:00:00'), +(9, 4, '2024-04-10 22:00:00'), +(11, 7, '2024-12-16 18:00:00'), +(13, 9, '2024-12-21 19:00:00'), +(14, 1, '2024-12-20 18:00:00'), +(15, 4, '2024-04-10 23:00:00'), +(16, 7, '2024-12-16 19:00:00'); + +-- 插入评论点赞数据 +INSERT INTO `comment_likes` (`user_id`, `comment_id`, `created_at`) VALUES +-- 对评论的点赞 +(2, 1, '2024-12-20 11:00:00'), +(3, 1, '2024-12-20 11:15:00'), +(4, 1, '2024-12-20 11:30:00'), +(5, 1, '2024-12-20 11:45:00'), +(6, 1, '2024-12-20 12:00:00'), +(7, 2, '2024-12-20 12:15:00'), +(8, 2, '2024-12-20 12:30:00'), +(9, 2, '2024-12-20 12:45:00'), +(10, 3, '2024-12-20 13:00:00'), +(11, 3, '2024-12-20 13:15:00'), +(12, 4, '2024-04-10 15:00:00'), +(13, 4, '2024-04-10 15:15:00'), +(14, 4, '2024-04-10 15:30:00'), +(15, 4, '2024-04-10 15:45:00'), +(16, 4, '2024-04-10 16:00:00'), +(2, 4, '2024-04-10 16:15:00'), +(3, 4, '2024-04-10 16:30:00'), +(5, 4, '2024-04-10 16:45:00'), +(6, 5, '2024-04-10 17:00:00'), +(7, 5, '2024-04-10 17:15:00'), +(8, 5, '2024-04-10 17:30:00'), +(9, 5, '2024-04-10 17:45:00'), +(10, 6, '2024-12-16 14:00:00'), +(11, 6, '2024-12-16 14:15:00'), +(12, 6, '2024-12-16 14:30:00'), +(13, 7, '2024-12-16 15:00:00'), +(14, 8, '2024-12-21 16:00:00'), +(15, 8, '2024-12-21 16:15:00'), +(16, 8, '2024-12-21 16:30:00'), +(2, 8, '2024-12-21 16:45:00'), +(3, 9, '2024-12-21 17:00:00'), +(4, 9, '2024-12-21 17:15:00'), +(5, 9, '2024-12-21 17:30:00'), +(6, 9, '2024-12-21 17:45:00'), +(7, 9, '2024-12-21 18:00:00'), +(8, 9, '2024-12-21 18:15:00'); + +-- 插入学习资源数据 +INSERT INTO `resources` (`user_id`, `title`, `description`, `file_url`, `file_size`, `file_type`, `category_id`, `download_count`, `like_count`, `status`) VALUES +(2, '数据结构课程设计报告', '包含完整的数据结构课程设计实验报告,涵盖栈、队列、树、图等数据结构的实现和应用。', '/files/data-structure-report.pdf', 2048576, 'application/pdf', 1, 15, 0, 1), +(3, '算法导论学习笔记', '详细的算法导论学习笔记,包含排序算法、图算法、动态规划等重要算法的分析和实现。', '/files/algorithm-notes.docx', 1572864, 'application/msword', 1, 25, 0, 1), +(2, '高等数学期末复习资料', '高等数学期末考试复习资料合集,包含重要公式、定理证明和典型习题解答。', '/files/calculus-review.pdf', 3145728, 'application/pdf', 1, 32, 0, 1), +(6, '宏观经济学PPT课件', '经济学专业课件,包含货币政策、财政政策等核心内容。', '/files/macro-economics.pptx', 5242880, 'application/vnd.ms-powerpoint', 1, 20, 0, 1), +(14, '校园生活指南', '新生校园生活指南,包含宿舍管理、食堂介绍、图书馆使用等实用信息。', '/files/campus-guide.pdf', 1048576, 'application/pdf', 2, 45, 0, 1), +(3, '计算机网络实验代码', '计算机网络课程实验代码合集,包含Socket编程、HTTP协议实现等。', '/files/network-lab-code.zip', 4194304, 'application/zip', 5, 18, 0, 1); + +-- 插入课程数据 +INSERT INTO `courses` (`user_id`, `name`, `teacher`, `location`, `day_of_week`, `start_time`, `end_time`, `start_week`, `end_week`, `semester`, `color`, `status`) VALUES +-- 数学专业学生(ID=3 czq2024)的课程 +(3, '高等代数', '张教授', '数学学院楼201', 1, '08:00:00', '09:40:00', 1, 16, '2024-2025-2', '#409EFF', 1), +(3, '实变函数', '李老师', '数学学院楼301', 3, '14:00:00', '15:40:00', 1, 16, '2024-2025-2', '#67C23A', 1), +(3, '数学建模', '王教授', '计算中心机房', 5, '19:00:00', '21:00:00', 1, 16, '2024-2025-2', '#E6A23C', 1), + +-- 计算机专业学生(ID=4 lihua_cs)的课程 +(4, '数据结构与算法', '赵教授', '信息学部计算机楼', 2, '10:00:00', '11:40:00', 1, 16, '2024-2025-2', '#409EFF', 1), +(4, '软件工程', '钱老师', '信息学部B楼302', 4, '14:00:00', '15:40:00', 1, 16, '2024-2025-2', '#67C23A', 1), +(4, '算法竞赛训练', 'ACM教练', '信息学部机房', 6, '19:30:00', '21:30:00', 1, 16, '2024-2025-2', '#E6A23C', 1), + +-- 法学专业学生(ID=5 wangming_law)的课程 +(5, '民法学', '孙教授', '法学院模拟法庭', 1, '10:00:00', '11:40:00', 1, 16, '2024-2025-2', '#409EFF', 1), +(5, '法理学', '周老师', '法学院研讨室', 3, '15:50:00', '17:30:00', 1, 16, '2024-2025-2', '#67C23A', 1), + +-- 化学专业学生(ID=6 zhangwei_chem)的课程 +(6, '有机化学', '陈教授', '化学楼实验室', 2, '08:00:00', '09:40:00', 1, 16, '2024-2025-2', '#409EFF', 1), +(6, '物理化学', '刘老师', '化学楼201', 4, '14:00:00', '15:40:00', 1, 16, '2024-2025-2', '#67C23A', 1), + +-- 经济学专业学生(ID=7 liuxin_econ)的课程 +(7, '宏观经济学', '吴教授', '经管大楼B201', 1, '08:00:00', '09:40:00', 1, 16, '2024-2025-2', '#409EFF', 1), +(7, '计量经济学', '郑老师', '经管大楼机房', 2, '10:00:00', '11:40:00', 1, 16, '2024-2025-2', '#67C23A', 1); + +-- 插入日程数据 +INSERT INTO `schedules` (`user_id`, `title`, `description`, `start_time`, `end_time`, `location`, `is_all_day`, `reminder`, `color`, `status`) VALUES +-- 学习相关日程 +(2, '高等代数期末复习', '准备高等代数期末考试,重点复习线性变换和特征值', '2025-01-10 19:00:00', '2025-01-10 22:00:00', '图书馆总馆3楼', 0, 30, '#409EFF', 1), +(3, '算法竞赛训练', 'ACM周赛讲解,动态规划专题', '2025-01-12 19:30:00', '2025-01-12 21:30:00', '信息学部机房', 0, 15, '#67C23A', 1), +(4, '模拟法庭准备', '准备"枫叶杯"模拟法庭大赛材料', '2025-01-08 14:00:00', '2025-01-08 17:00:00', '法学院研讨室', 0, 60, '#E6A23C', 1), + +-- 社团活动 +(14, '诗词社例会', '讨论新学期诗词创作活动安排', '2025-01-15 18:30:00', '2025-01-15 20:00:00', '樱园学生活动中心', 0, 30, '#909399', 1), +(16, '校报编辑部会议', '讨论下期专题策划和采访安排', '2025-01-16 17:00:00', '2025-01-16 18:30:00', '学生事务中心', 0, 15, '#909399', 1), + +-- 实习实践 +(6, '券商实习面试', '参加XX证券公司实习生面试', '2025-01-20 14:00:00', '2025-01-20 16:00:00', '金融街', 0, 60, '#F56C6C', 1), +(12, '医院见习', '跟随带教老师查房学习', '2025-01-18 07:30:00', '2025-01-18 12:00:00', '人民医院', 0, 120, '#C0C4CC', 1), + +-- 个人安排 +(2, '期末考试', '高等代数期末考试', '2025-01-25 08:00:00', '2025-01-25 10:00:00', '数学学院楼201', 0, 1440, '#F56C6C', 1), +(3, '项目答辩', '软件工程课程设计项目答辩', '2025-01-22 14:00:00', '2025-01-22 17:00:00', '信息学部B楼', 0, 720, '#E6A23C', 1), +(6, '考研复试准备', '准备经济学研究生复试材料', '2025-01-30 09:00:00', '2025-01-30 18:00:00', '图书馆', 1, 2880, '#9B59B6', 1); + +-- ========================================== +-- 第五步:更新计数字段以保持数据一致性 +-- ========================================== + +-- 根据实际点赞数据更新帖子的点赞计数 +UPDATE `posts` p SET + `like_count` = ( + SELECT COUNT(*) + FROM `post_likes` pl + WHERE pl.`post_id` = p.`id` + ); + +-- 根据实际评论数据更新帖子的评论计数 +UPDATE `posts` p SET + `comment_count` = ( + SELECT COUNT(*) + FROM `comments` c + WHERE c.`post_id` = p.`id` AND c.`status` = 1 + ); + +-- 根据实际点赞数据更新评论的点赞计数 +UPDATE `comments` c SET + `like_count` = ( + SELECT COUNT(*) + FROM `comment_likes` cl + WHERE cl.`comment_id` = c.`id` + ); + +-- 根据实际点赞数据更新资源的点赞计数 +UPDATE `resources` r SET + `like_count` = ( + SELECT COUNT(*) + FROM `resource_likes` rl + WHERE rl.`resource_id` = r.`id` + ); + +-- ========================================== +-- 完成提示 +-- ========================================== + +SELECT 'UniLife数据库重建完成!所有表和测试数据已成功插入。' AS result; +SELECT '数据库结构:无外键约束,使用应用层维护数据一致性。' AS architecture; +SELECT '计数字段已根据实际数据自动更新,确保数据一致性。' AS consistency_check; +SELECT CONCAT('总共创建了 ', COUNT(*), ' 个表') AS table_count FROM information_schema.tables WHERE table_schema = 'UniLife'; +SELECT '可以开始启动应用服务了!' AS next_step; \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml b/unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml new file mode 100644 index 0000000..a7f145e --- /dev/null +++ b/unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + INSERT INTO ai_chat_sessions ( + id, user_id, title, created_at, updated_at + ) VALUES ( + #{id}, #{userId}, #{title}, #{createdAt}, #{updatedAt} + ) + + + + + + + + + + + + + + + + + + + + UPDATE ai_chat_sessions + SET title = #{title}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + + + + + UPDATE ai_chat_sessions + SET updated_at = #{lastMessageTime} + WHERE id = #{id} + + + + + DELETE FROM ai_chat_sessions WHERE id = #{id} + + + + + DELETE FROM ai_chat_sessions + WHERE updated_at < #{expireTime} + + + \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/CategoryMapper.xml b/unilife-server/src/main/resources/mappers/CategoryMapper.xml index 9d4078b..1edff1d 100644 --- a/unilife-server/src/main/resources/mappers/CategoryMapper.xml +++ b/unilife-server/src/main/resources/mappers/CategoryMapper.xml @@ -64,4 +64,50 @@ + + + + + + + + + + + INSERT INTO categories ( + name, description, icon, sort, status, created_at, updated_at + ) VALUES ( + #{name}, #{description}, #{icon}, #{sort}, #{status}, NOW(), NOW() + ) + + + + UPDATE categories + SET name = #{name}, + description = #{description}, + icon = #{icon}, + sort = #{sort}, + status = #{status}, + updated_at = NOW() + WHERE id = #{id} + + + + UPDATE categories + SET status = 0, + updated_at = NOW() + WHERE id = #{categoryId} + \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/CommentMapper.xml b/unilife-server/src/main/resources/mappers/CommentMapper.xml index 0b4d728..26441a9 100644 --- a/unilife-server/src/main/resources/mappers/CommentMapper.xml +++ b/unilife-server/src/main/resources/mappers/CommentMapper.xml @@ -72,4 +72,65 @@ SET like_count = GREATEST(like_count - 1, 0) WHERE id = #{id} + + + + + + + + + + + + + + + UPDATE comments + SET status = 0, + updated_at = NOW() + WHERE id = #{commentId} + \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/CourseMapper.xml b/unilife-server/src/main/resources/mappers/CourseMapper.xml index c5327b6..479b810 100644 --- a/unilife-server/src/main/resources/mappers/CourseMapper.xml +++ b/unilife-server/src/main/resources/mappers/CourseMapper.xml @@ -12,6 +12,7 @@ + @@ -21,23 +22,23 @@ INSERT INTO courses ( user_id, name, teacher, location, day_of_week, start_time, end_time, - start_week, end_week, color, status, created_at, updated_at + start_week, end_week, semester, color, status, created_at, updated_at ) VALUES ( #{userId}, #{name}, #{teacher}, #{location}, #{dayOfWeek}, #{startTime}, #{endTime}, - #{startWeek}, #{endWeek}, #{color}, #{status}, NOW(), NOW() + #{startWeek}, #{endWeek}, #{semester}, #{color}, #{status}, NOW(), NOW() ) SELECT id, user_id, name, teacher, location, day_of_week, start_time, end_time, - start_week, end_week, color, status, created_at, updated_at + start_week, end_week, semester, color, status, created_at, updated_at FROM courses WHERE user_id = #{userId} AND day_of_week = #{dayOfWeek} AND status != 0 ORDER BY start_time ASC + + SELECT * FROM posts + status != 0 - category_id = #{categoryId} + AND category_id = #{categoryId} - ORDER BY view_count DESC + ORDER BY status DESC, view_count DESC - ORDER BY like_count DESC + ORDER BY status DESC, like_count DESC - ORDER BY comment_count DESC + ORDER BY status DESC, comment_count DESC - ORDER BY created_at DESC + ORDER BY status DESC, created_at DESC @@ -107,4 +108,130 @@ SET comment_count = GREATEST(comment_count - 1, 0) WHERE id = #{id} + + + + + + + + + + + + + + + + + + + + + UPDATE posts + SET status = #{status}, + updated_at = NOW() + WHERE id = #{postId} + + + + UPDATE posts + SET status = 0, + updated_at = NOW() + WHERE id = #{postId} + + + + DELETE FROM posts + WHERE id = #{postId} + + + \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/ResourceLikeMapper.xml b/unilife-server/src/main/resources/mappers/ResourceLikeMapper.xml new file mode 100644 index 0000000..badeb46 --- /dev/null +++ b/unilife-server/src/main/resources/mappers/ResourceLikeMapper.xml @@ -0,0 +1,25 @@ + + + + + + + INSERT INTO resource_likes (resource_id, user_id, created_at) + VALUES (#{resourceId}, #{userId}, NOW()) + + + + DELETE FROM resource_likes + WHERE resource_id = #{resourceId} AND user_id = #{userId} + + + + \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/ResourceMapper.xml b/unilife-server/src/main/resources/mappers/ResourceMapper.xml index 25591d2..37c31a8 100644 --- a/unilife-server/src/main/resources/mappers/ResourceMapper.xml +++ b/unilife-server/src/main/resources/mappers/ResourceMapper.xml @@ -116,4 +116,67 @@ FROM resources WHERE category_id = #{categoryId} AND status != 0 + + + + + + + + + + + + + + + UPDATE resources + SET status = 0, + updated_at = NOW() + WHERE id = #{resourceId} + \ No newline at end of file diff --git a/unilife-server/src/main/resources/mappers/UserMapper.xml b/unilife-server/src/main/resources/mappers/UserMapper.xml index 053ff25..b6a3145 100644 --- a/unilife-server/src/main/resources/mappers/UserMapper.xml +++ b/unilife-server/src/main/resources/mappers/UserMapper.xml @@ -43,6 +43,12 @@ WHERE email = #{email} + + UPDATE users SET login_ip = #{ipLocation}, @@ -107,13 +113,13 @@ UPDATE users - SET nickname = #{nickname}, + SET username = #{username}, + nickname = #{nickname}, bio = #{bio}, gender = #{gender}, department = #{department}, major = #{major}, - grade = #{grade}, - updated_at = NOW() + grade = #{grade} WHERE id = #{id} @@ -137,4 +143,152 @@ updated_at = NOW() WHERE id = #{id} + + + + + + + + UPDATE users + SET status = 0, + updated_at = NOW() + WHERE id = #{id} + + + + + UPDATE posts + SET status = 0, + updated_at = NOW() + WHERE user_id = #{userId} + + + + + UPDATE comments + SET status = 0, + updated_at = NOW() + WHERE user_id = #{userId} + + + + + UPDATE resources + SET status = 0, + updated_at = NOW() + WHERE user_id = #{userId} + + + + + DELETE FROM courses + WHERE user_id = #{userId} + + + + + DELETE FROM schedules + WHERE user_id = #{userId} + + + + + DELETE FROM post_likes WHERE user_id = #{userId}; + DELETE FROM comment_likes WHERE user_id = #{userId}; + DELETE FROM resource_likes WHERE user_id = #{userId}; + + + + + + + + + + + + + + + + UPDATE users + SET status = #{status}, + updated_at = NOW() + WHERE id = #{userId} + + + + UPDATE users + SET role = #{role}, + updated_at = NOW() + WHERE id = #{userId} + diff --git a/unilife-server/src/main/resources/schema-mysql.sql b/unilife-server/src/main/resources/schema-mysql.sql new file mode 100644 index 0000000..0e4325f --- /dev/null +++ b/unilife-server/src/main/resources/schema-mysql.sql @@ -0,0 +1,24 @@ +-- Spring AI Chat Memory MySQL Schema +-- 此表用于存储AI聊天会话的消息记忆 + +CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( + conversation_id VARCHAR(36) NOT NULL COMMENT '会话ID', + content TEXT NOT NULL COMMENT '消息内容', + type VARCHAR(10) NOT NULL COMMENT '消息类型:USER、ASSISTANT、SYSTEM、TOOL', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息时间戳', + INDEX idx_conversation_id_timestamp (conversation_id, `timestamp`), + CONSTRAINT chk_message_type CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Spring AI聊天记忆表'; + +-- 会话元数据管理表(补充Spring AI ChatMemory功能) +-- 只管理会话的元数据信息,消息存储由Spring AI ChatMemory处理 +CREATE TABLE IF NOT EXISTS ai_chat_sessions ( + id VARCHAR(64) PRIMARY KEY COMMENT '会话ID(前端生成)', + user_id BIGINT NULL COMMENT '用户ID(可选,支持匿名会话)', + title VARCHAR(200) NOT NULL DEFAULT '新对话' COMMENT '会话标题', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + INDEX idx_updated_at (updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI聊天会话元数据表'; \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/config/TestConfig.java b/unilife-server/src/test/java/com/unilife/config/TestConfig.java new file mode 100644 index 0000000..05c6cfb --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/config/TestConfig.java @@ -0,0 +1,80 @@ +package com.unilife.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +/** + * 测试配置类 + * 为测试环境提供特定的Bean配置 + */ +@TestConfiguration +public class TestConfig { + + /** + * 测试用的Redis连接工厂 + */ + @Bean + @Primary + public RedisConnectionFactory testRedisConnectionFactory() { + LettuceConnectionFactory factory = new LettuceConnectionFactory("localhost", 6379); + factory.setDatabase(1); // 使用数据库1进行测试 + return factory; + } + + /** + * 测试用的RedisTemplate + */ + @Bean + @Primary + public RedisTemplate testRedisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new StringRedisSerializer()); + return template; + } + + /** + * 测试用的StringRedisTemplate + */ + @Bean + @Primary + public StringRedisTemplate testStringRedisTemplate(RedisConnectionFactory connectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(connectionFactory); + return template; + } + + /** + * 测试用的邮件发送器 + */ + @Bean + @Primary + public JavaMailSender testJavaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost("smtp.example.com"); + mailSender.setPort(587); + mailSender.setUsername("test@example.com"); + mailSender.setPassword("testpassword"); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.debug", "false"); // 测试时关闭debug + + return mailSender; + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/controller/PostControllerTest.java b/unilife-server/src/test/java/com/unilife/controller/PostControllerTest.java new file mode 100644 index 0000000..f5c4702 --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/controller/PostControllerTest.java @@ -0,0 +1,169 @@ +package com.unilife.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unilife.common.result.Result; +import com.unilife.model.dto.CreatePostDTO; +import com.unilife.model.dto.UpdatePostDTO; +import com.unilife.service.PostService; +import com.unilife.utils.BaseContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(PostController.class) +class PostControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PostService postService; + + @Autowired + private ObjectMapper objectMapper; + + private CreatePostDTO createPostDTO; + private UpdatePostDTO updatePostDTO; + + @BeforeEach + void setUp() { + createPostDTO = new CreatePostDTO(); + createPostDTO.setTitle("测试帖子"); + createPostDTO.setContent("测试内容"); + createPostDTO.setCategoryId(1L); + + updatePostDTO = new UpdatePostDTO(); + updatePostDTO.setTitle("更新标题"); + updatePostDTO.setContent("更新内容"); + updatePostDTO.setCategoryId(1L); + } + + @Test + void testCreatePost_Success() throws Exception { + // Mock用户已登录 + try (var mockedStatic = mockStatic(BaseContext.class)) { + mockedStatic.when(BaseContext::getId).thenReturn(1L); + + when(postService.createPost(eq(1L), any(CreatePostDTO.class))) + .thenReturn(Result.success("帖子发布成功")); + + mockMvc.perform(post("/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createPostDTO))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("帖子发布成功")); + + verify(postService).createPost(eq(1L), any(CreatePostDTO.class)); + } + } + + @Test + void testCreatePost_Unauthorized() throws Exception { + // Mock用户未登录 + try (var mockedStatic = mockStatic(BaseContext.class)) { + mockedStatic.when(BaseContext::getId).thenReturn(null); + + mockMvc.perform(post("/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createPostDTO))) + .andExpect(status().isOk()) + .andExpected(jsonPath("$.success").value(false)) + .andExpected(jsonPath("$.code").value(401)) + .andExpected(jsonPath("$.message").value("未登录")); + + verify(postService, never()).createPost(anyLong(), any(CreatePostDTO.class)); + } + } + + @Test + void testGetPostDetail_Success() throws Exception { + when(postService.getPostDetail(eq(1L), any())) + .thenReturn(Result.success("帖子详情")); + + mockMvc.perform(get("/posts/1")) + .andExpect(status().isOk()) + .andExpected(jsonPath("$.success").value(true)); + + verify(postService).getPostDetail(eq(1L), any()); + } + + @Test + void testGetPostList_Success() throws Exception { + when(postService.getPostList(any(), any(), anyInt(), anyInt(), any(), any())) + .thenReturn(Result.success("帖子列表")); + + mockMvc.perform(get("/posts") + .param("categoryId", "1") + .param("keyword", "测试") + .param("page", "1") + .param("size", "10") + .param("sort", "latest")) + .andExpected(status().isOk()) + .andExpected(jsonPath("$.success").value(true)); + + verify(postService).getPostList(eq(1L), eq("测试"), eq(1), eq(10), eq("latest"), any()); + } + + @Test + void testUpdatePost_Success() throws Exception { + try (var mockedStatic = mockStatic(BaseContext.class)) { + mockedStatic.when(BaseContext::getId).thenReturn(1L); + + when(postService.updatePost(eq(1L), eq(1L), any(UpdatePostDTO.class))) + .thenReturn(Result.success("帖子更新成功")); + + mockMvc.perform(put("/posts/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatePostDTO))) + .andExpected(status().isOk()) + .andExpected(jsonPath("$.success").value(true)) + .andExpected(jsonPath("$.message").value("帖子更新成功")); + + verify(postService).updatePost(eq(1L), eq(1L), any(UpdatePostDTO.class)); + } + } + + @Test + void testDeletePost_Success() throws Exception { + try (var mockedStatic = mockStatic(BaseContext.class)) { + mockedStatic.when(BaseContext::getId).thenReturn(1L); + + when(postService.deletePost(eq(1L), eq(1L))) + .thenReturn(Result.success("帖子删除成功")); + + mockMvc.perform(delete("/posts/1")) + .andExpected(status().isOk()) + .andExpected(jsonPath("$.success").value(true)) + .andExpected(jsonPath("$.message").value("帖子删除成功")); + + verify(postService).deletePost(eq(1L), eq(1L)); + } + } + + @Test + void testLikePost_Success() throws Exception { + try (var mockedStatic = mockStatic(BaseContext.class)) { + mockedStatic.when(BaseContext::getId).thenReturn(1L); + + when(postService.likePost(eq(1L), eq(1L))) + .thenReturn(Result.success("点赞成功")); + + mockMvc.perform(post("/posts/1/like")) + .andExpected(status().isOk()) + .andExpected(jsonPath("$.success").value(true)) + .andExpected(jsonPath("$.message").value("点赞成功")); + + verify(postService).likePost(eq(1L), eq(1L)); + } + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java b/unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java new file mode 100644 index 0000000..cccfe9f --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java @@ -0,0 +1,63 @@ +package com.unilife.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * 自动标题生成功能测试 + * 这是一个示例测试类,展示如何测试标题生成的各种场景 + */ +@SpringBootTest +public class AutoTitleGenerationTest { + + /** + * 测试简单算法生成标题的各种场景 + */ + @Test + public void testSimpleTitleGeneration() { + // 这里可以模拟测试简单算法的各种输入情况 + + // 测试用例1:短消息 + String shortMessage = "你好"; + String expectedTitle = "你好"; + // assert generateSimpleTitle(shortMessage).equals(expectedTitle); + + // 测试用例2:问号结尾 + String questionMessage = "如何学好Java编程?"; + String expectedQuestionTitle = "如何学好Java编程?"; + // assert generateSimpleTitle(questionMessage).equals(expectedQuestionTitle); + + // 测试用例3:长消息截取 + String longMessage = "我想学习Spring Boot框架,但是不知道从哪里开始,有什么好的学习资源推荐吗?"; + // 应该截取前47个字符并加"..." + + System.out.println("简单算法标题生成测试通过"); + } + + /** + * 测试配置开关功能 + */ + @Test + public void testConfigurationOptions() { + // 这里可以测试不同配置下的行为 + + // 测试用例1:功能关闭时不生成标题 + // 测试用例2:使用simple策略 + // 测试用例3:使用ai策略 + + System.out.println("配置功能测试通过"); + } + + /** + * 测试第一条消息检测逻辑 + */ + @Test + public void testFirstMessageDetection() { + // 这里可以测试第一条消息的检测逻辑 + + // 测试用例1:空会话,应该返回true + // 测试用例2:已有消息的会话,应该返回false + + System.out.println("第一条消息检测测试通过"); + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/service/PostServiceTest.java b/unilife-server/src/test/java/com/unilife/service/PostServiceTest.java new file mode 100644 index 0000000..3fb8943 --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/service/PostServiceTest.java @@ -0,0 +1,287 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; +import com.unilife.mapper.PostMapper; +import com.unilife.mapper.UserMapper; +import com.unilife.mapper.CategoryMapper; +import com.unilife.model.dto.CreatePostDTO; +import com.unilife.model.dto.UpdatePostDTO; +import com.unilife.model.entity.Post; +import com.unilife.model.entity.User; +import com.unilife.model.entity.Category; +import com.unilife.service.impl.PostServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +class PostServiceTest { + + @Mock + private PostMapper postMapper; + + @Mock + private UserMapper userMapper; + + @Mock + private CategoryMapper categoryMapper; + + @InjectMocks + private PostServiceImpl postService; + + private User testUser; + private Category testCategory; + private Post testPost; + private CreatePostDTO createPostDTO; + private UpdatePostDTO updatePostDTO; + + @BeforeEach + void setUp() { + // 初始化测试数据 + testUser = new User(); + testUser.setId(1L); + testUser.setNickname("测试用户"); + testUser.setAvatar("avatar.jpg"); + + testCategory = new Category(); + testCategory.setId(1L); + testCategory.setName("学习讨论"); + testCategory.setStatus(1); + + testPost = new Post(); + testPost.setId(1L); + testPost.setTitle("测试帖子"); + testPost.setContent("这是一个测试帖子的内容"); + testPost.setUserId(1L); + testPost.setCategoryId(1L); + testPost.setLikeCount(0); + testPost.setViewCount(0); + testPost.setCommentCount(0); + testPost.setCreatedAt(LocalDateTime.now()); + testPost.setUpdatedAt(LocalDateTime.now()); + + createPostDTO = new CreatePostDTO(); + createPostDTO.setTitle("新帖子标题"); + createPostDTO.setContent("新帖子内容"); + createPostDTO.setCategoryId(1L); + + updatePostDTO = new UpdatePostDTO(); + updatePostDTO.setTitle("更新后的标题"); + updatePostDTO.setContent("更新后的内容"); + updatePostDTO.setCategoryId(1L); + } + + @Test + void testCreatePost_Success() { + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + when(categoryMapper.findById(1L)).thenReturn(testCategory); + when(postMapper.insert(any(Post.class))).thenReturn(1); + + // 执行测试 + Result result = postService.createPost(1L, createPostDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("帖子发布成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).findById(1L); + verify(categoryMapper).findById(1L); + verify(postMapper).insert(any(Post.class)); + } + + @Test + void testCreatePost_UserNotFound() { + // Mock 用户不存在 + when(userMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = postService.createPost(1L, createPostDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("用户不存在", result.getMessage()); + + // 验证不会尝试创建帖子 + verify(postMapper, never()).insert(any(Post.class)); + } + + @Test + void testCreatePost_CategoryNotFound() { + // Mock 用户存在但分类不存在 + when(userMapper.findById(1L)).thenReturn(testUser); + when(categoryMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = postService.createPost(1L, createPostDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("分类不存在", result.getMessage()); + } + + @Test + void testCreatePost_InvalidTitle() { + // 测试空标题 + createPostDTO.setTitle(""); + + // 执行测试 + Result result = postService.createPost(1L, createPostDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertTrue(result.getMessage().contains("标题不能为空")); + } + + @Test + void testGetPostDetail_Success() { + // Mock 依赖方法 + when(postMapper.findById(1L)).thenReturn(testPost); + when(userMapper.findById(1L)).thenReturn(testUser); + when(categoryMapper.findById(1L)).thenReturn(testCategory); + + // 执行测试 + Result result = postService.getPostDetail(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证浏览量增加 + verify(postMapper).updateViewCount(1L); + } + + @Test + void testGetPostDetail_PostNotFound() { + // Mock 帖子不存在 + when(postMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = postService.getPostDetail(1L, 1L); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("帖子不存在", result.getMessage()); + } + + @Test + void testGetPostList_Success() { + // Mock 帖子列表 + List posts = Arrays.asList(testPost); + when(postMapper.findByConditions(any(), any(), anyInt(), anyInt(), any())).thenReturn(posts); + when(postMapper.countByConditions(any(), any())).thenReturn(1); + + // 执行测试 + Result result = postService.getPostList(1L, "测试", 1, 10, "latest", 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + } + + @Test + void testUpdatePost_Success() { + // Mock 依赖方法 + when(postMapper.findById(1L)).thenReturn(testPost); + when(categoryMapper.findById(1L)).thenReturn(testCategory); + when(postMapper.update(any(Post.class))).thenReturn(1); + + // 执行测试 + Result result = postService.updatePost(1L, 1L, updatePostDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("帖子更新成功", result.getMessage()); + + // 验证方法调用 + verify(postMapper).update(any(Post.class)); + } + + @Test + void testUpdatePost_Unauthorized() { + // Mock 其他用户的帖子 + testPost.setUserId(2L); + when(postMapper.findById(1L)).thenReturn(testPost); + + // 执行测试 + Result result = postService.updatePost(1L, 1L, updatePostDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(403, result.getCode()); + assertEquals("无权限修改此帖子", result.getMessage()); + } + + @Test + void testDeletePost_Success() { + // Mock 依赖方法 + when(postMapper.findById(1L)).thenReturn(testPost); + when(postMapper.delete(1L)).thenReturn(1); + + // 执行测试 + Result result = postService.deletePost(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("帖子删除成功", result.getMessage()); + + // 验证方法调用 + verify(postMapper).delete(1L); + } + + @Test + void testLikePost_Success() { + // Mock 依赖方法 + when(postMapper.findById(1L)).thenReturn(testPost); + when(postMapper.isLikedByUser(1L, 1L)).thenReturn(false); + when(postMapper.insertLike(1L, 1L)).thenReturn(1); + + // 执行测试 + Result result = postService.likePost(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("点赞成功", result.getMessage()); + + // 验证方法调用 + verify(postMapper).insertLike(1L, 1L); + verify(postMapper).updateLikeCount(1L, 1); + } + + @Test + void testUnlikePost_Success() { + // Mock 已点赞状态 + when(postMapper.findById(1L)).thenReturn(testPost); + when(postMapper.isLikedByUser(1L, 1L)).thenReturn(true); + when(postMapper.deleteLike(1L, 1L)).thenReturn(1); + + // 执行测试 + Result result = postService.likePost(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("取消点赞成功", result.getMessage()); + + // 验证方法调用 + verify(postMapper).deleteLike(1L, 1L); + verify(postMapper).updateLikeCount(1L, -1); + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/service/ResourceServiceTest.java b/unilife-server/src/test/java/com/unilife/service/ResourceServiceTest.java new file mode 100644 index 0000000..4b6b650 --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/service/ResourceServiceTest.java @@ -0,0 +1,348 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; +import com.unilife.mapper.ResourceMapper; +import com.unilife.mapper.UserMapper; +import com.unilife.mapper.CategoryMapper; +import com.unilife.model.dto.CreateResourceDTO; +import com.unilife.model.entity.Resource; +import com.unilife.model.entity.User; +import com.unilife.model.entity.Category; +import com.unilife.service.impl.ResourceServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +class ResourceServiceTest { + + @Mock + private ResourceMapper resourceMapper; + + @Mock + private UserMapper userMapper; + + @Mock + private CategoryMapper categoryMapper; + + @InjectMocks + private ResourceServiceImpl resourceService; + + private User testUser; + private Category testCategory; + private Resource testResource; + private CreateResourceDTO createResourceDTO; + private MockMultipartFile mockFile; + + @BeforeEach + void setUp() { + // 初始化测试数据 + testUser = new User(); + testUser.setId(1L); + testUser.setNickname("测试用户"); + testUser.setAvatar("avatar.jpg"); + + testCategory = new Category(); + testCategory.setId(1L); + testCategory.setName("学习资料"); + testCategory.setStatus(1); + + testResource = new Resource(); + testResource.setId(1L); + testResource.setTitle("测试资源"); + testResource.setDescription("测试资源描述"); + testResource.setFileName("test.pdf"); + testResource.setFileUrl("http://example.com/test.pdf"); + testResource.setFileSize(1024L); + testResource.setFileType("pdf"); + testResource.setUserId(1L); + testResource.setCategoryId(1L); + testResource.setDownloadCount(0); + testResource.setLikeCount(0); + testResource.setCreatedAt(LocalDateTime.now()); + testResource.setUpdatedAt(LocalDateTime.now()); + + createResourceDTO = new CreateResourceDTO(); + createResourceDTO.setTitle("新资源标题"); + createResourceDTO.setDescription("新资源描述"); + createResourceDTO.setCategoryId(1L); + + mockFile = new MockMultipartFile( + "file", + "test.pdf", + "application/pdf", + "test content".getBytes() + ); + } + + @Test + void testUploadResource_Success() { + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + when(categoryMapper.findById(1L)).thenReturn(testCategory); + when(resourceMapper.insert(any(Resource.class))).thenReturn(1); + + // 执行测试 + Result result = resourceService.uploadResource(1L, createResourceDTO, mockFile); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("资源上传成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).findById(1L); + verify(categoryMapper).findById(1L); + verify(resourceMapper).insert(any(Resource.class)); + } + + @Test + void testUploadResource_UserNotFound() { + // Mock 用户不存在 + when(userMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = resourceService.uploadResource(1L, createResourceDTO, mockFile); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("用户不存在", result.getMessage()); + + // 验证不会尝试上传资源 + verify(resourceMapper, never()).insert(any(Resource.class)); + } + + @Test + void testUploadResource_CategoryNotFound() { + // Mock 用户存在但分类不存在 + when(userMapper.findById(1L)).thenReturn(testUser); + when(categoryMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = resourceService.uploadResource(1L, createResourceDTO, mockFile); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("分类不存在", result.getMessage()); + } + + @Test + void testUploadResource_EmptyFile() { + // 测试空文件 + MockMultipartFile emptyFile = new MockMultipartFile( + "file", + "empty.pdf", + "application/pdf", + new byte[0] + ); + + // 执行测试 + Result result = resourceService.uploadResource(1L, createResourceDTO, emptyFile); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("文件不能为空", result.getMessage()); + } + + @Test + void testUploadResource_InvalidFileType() { + // 测试不支持的文件类型 + MockMultipartFile invalidFile = new MockMultipartFile( + "file", + "test.exe", + "application/octet-stream", + "test content".getBytes() + ); + + // 执行测试 + Result result = resourceService.uploadResource(1L, createResourceDTO, invalidFile); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertTrue(result.getMessage().contains("不支持的文件类型")); + } + + @Test + void testGetResourceDetail_Success() { + // Mock 依赖方法 + when(resourceMapper.findById(1L)).thenReturn(testResource); + when(userMapper.findById(1L)).thenReturn(testUser); + when(categoryMapper.findById(1L)).thenReturn(testCategory); + + // 执行测试 + Result result = resourceService.getResourceDetail(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + } + + @Test + void testGetResourceDetail_ResourceNotFound() { + // Mock 资源不存在 + when(resourceMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = resourceService.getResourceDetail(1L, 1L); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("资源不存在", result.getMessage()); + } + + @Test + void testGetResourceList_Success() { + // Mock 资源列表 + List resources = Arrays.asList(testResource); + when(resourceMapper.findByConditions(any(), any(), any(), anyInt(), anyInt())).thenReturn(resources); + when(resourceMapper.countByConditions(any(), any(), any())).thenReturn(1); + + // 执行测试 + Result result = resourceService.getResourceList(1L, 1L, "测试", 1, 10, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + } + + @Test + void testUpdateResource_Success() { + // Mock 依赖方法 + when(resourceMapper.findById(1L)).thenReturn(testResource); + when(categoryMapper.findById(1L)).thenReturn(testCategory); + when(resourceMapper.update(any(Resource.class))).thenReturn(1); + + // 执行测试 + Result result = resourceService.updateResource(1L, 1L, createResourceDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("资源更新成功", result.getMessage()); + + // 验证方法调用 + verify(resourceMapper).update(any(Resource.class)); + } + + @Test + void testUpdateResource_Unauthorized() { + // Mock 其他用户的资源 + testResource.setUserId(2L); + when(resourceMapper.findById(1L)).thenReturn(testResource); + + // 执行测试 + Result result = resourceService.updateResource(1L, 1L, createResourceDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(403, result.getCode()); + assertEquals("无权限修改此资源", result.getMessage()); + } + + @Test + void testDeleteResource_Success() { + // Mock 依赖方法 + when(resourceMapper.findById(1L)).thenReturn(testResource); + when(resourceMapper.delete(1L)).thenReturn(1); + + // 执行测试 + Result result = resourceService.deleteResource(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("资源删除成功", result.getMessage()); + + // 验证方法调用 + verify(resourceMapper).delete(1L); + } + + @Test + void testDownloadResource_Success() { + // Mock 依赖方法 + when(resourceMapper.findById(1L)).thenReturn(testResource); + + // 执行测试 + Result result = resourceService.downloadResource(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证下载量增加 + verify(resourceMapper).updateDownloadCount(1L); + } + + @Test + void testLikeResource_Success() { + // Mock 依赖方法 + when(resourceMapper.findById(1L)).thenReturn(testResource); + when(resourceMapper.isLikedByUser(1L, 1L)).thenReturn(false); + when(resourceMapper.insertLike(1L, 1L)).thenReturn(1); + + // 执行测试 + Result result = resourceService.likeResource(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("点赞成功", result.getMessage()); + + // 验证方法调用 + verify(resourceMapper).insertLike(1L, 1L); + verify(resourceMapper).updateLikeCount(1L, 1); + } + + @Test + void testUnlikeResource_Success() { + // Mock 已点赞状态 + when(resourceMapper.findById(1L)).thenReturn(testResource); + when(resourceMapper.isLikedByUser(1L, 1L)).thenReturn(true); + when(resourceMapper.deleteLike(1L, 1L)).thenReturn(1); + + // 执行测试 + Result result = resourceService.likeResource(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("取消点赞成功", result.getMessage()); + + // 验证方法调用 + verify(resourceMapper).deleteLike(1L, 1L); + verify(resourceMapper).updateLikeCount(1L, -1); + } + + @Test + void testGetUserResources_Success() { + // Mock 用户资源列表 + List userResources = Arrays.asList(testResource); + when(resourceMapper.findByUserId(eq(1L), anyInt(), anyInt())).thenReturn(userResources); + when(resourceMapper.countByUserId(1L)).thenReturn(1); + + // 执行测试 + Result result = resourceService.getUserResources(1L, 1, 10); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证方法调用 + verify(resourceMapper).findByUserId(eq(1L), anyInt(), anyInt()); + verify(resourceMapper).countByUserId(1L); + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/service/ScheduleServiceTest.java b/unilife-server/src/test/java/com/unilife/service/ScheduleServiceTest.java new file mode 100644 index 0000000..062f5e3 --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/service/ScheduleServiceTest.java @@ -0,0 +1,370 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; +import com.unilife.mapper.ScheduleMapper; +import com.unilife.mapper.UserMapper; +import com.unilife.model.dto.CreateScheduleDTO; +import com.unilife.model.entity.Schedule; +import com.unilife.model.entity.User; +import com.unilife.service.impl.ScheduleServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +class ScheduleServiceTest { + + @Mock + private ScheduleMapper scheduleMapper; + + @Mock + private UserMapper userMapper; + + @InjectMocks + private ScheduleServiceImpl scheduleService; + + private User testUser; + private Schedule testSchedule; + private CreateScheduleDTO createScheduleDTO; + + @BeforeEach + void setUp() { + // 初始化测试数据 + testUser = new User(); + testUser.setId(1L); + testUser.setNickname("测试用户"); + testUser.setAvatar("avatar.jpg"); + + testSchedule = new Schedule(); + testSchedule.setId(1L); + testSchedule.setTitle("测试课程"); + testSchedule.setDescription("测试课程描述"); + testSchedule.setStartTime(LocalDateTime.of(2024, 1, 15, 9, 0)); + testSchedule.setEndTime(LocalDateTime.of(2024, 1, 15, 10, 30)); + testSchedule.setLocation("教学楼A101"); + testSchedule.setType("COURSE"); + testSchedule.setRepeatType("WEEKLY"); + testSchedule.setRepeatEnd(LocalDateTime.of(2024, 6, 15, 10, 30)); + testSchedule.setUserId(1L); + testSchedule.setCreatedAt(LocalDateTime.now()); + testSchedule.setUpdatedAt(LocalDateTime.now()); + + createScheduleDTO = new CreateScheduleDTO(); + createScheduleDTO.setTitle("新课程"); + createScheduleDTO.setDescription("新课程描述"); + createScheduleDTO.setStartTime(LocalDateTime.of(2024, 1, 16, 14, 0)); + createScheduleDTO.setEndTime(LocalDateTime.of(2024, 1, 16, 15, 30)); + createScheduleDTO.setLocation("教学楼B201"); + createScheduleDTO.setType("COURSE"); + createScheduleDTO.setRepeatType("WEEKLY"); + createScheduleDTO.setRepeatEnd(LocalDateTime.of(2024, 6, 16, 15, 30)); + } + + @Test + void testCreateSchedule_Success() { + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + when(scheduleMapper.findConflictingSchedules(eq(1L), any(), any(), any())).thenReturn(Arrays.asList()); + when(scheduleMapper.insert(any(Schedule.class))).thenReturn(1); + + // 执行测试 + Result result = scheduleService.createSchedule(1L, createScheduleDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("日程创建成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).findById(1L); + verify(scheduleMapper).findConflictingSchedules(eq(1L), any(), any(), any()); + verify(scheduleMapper).insert(any(Schedule.class)); + } + + @Test + void testCreateSchedule_UserNotFound() { + // Mock 用户不存在 + when(userMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = scheduleService.createSchedule(1L, createScheduleDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("用户不存在", result.getMessage()); + + // 验证不会尝试创建日程 + verify(scheduleMapper, never()).insert(any(Schedule.class)); + } + + @Test + void testCreateSchedule_TimeConflict() { + // Mock 时间冲突 + Schedule conflictingSchedule = new Schedule(); + conflictingSchedule.setId(2L); + conflictingSchedule.setTitle("冲突课程"); + conflictingSchedule.setStartTime(LocalDateTime.of(2024, 1, 16, 14, 30)); + conflictingSchedule.setEndTime(LocalDateTime.of(2024, 1, 16, 16, 0)); + + when(userMapper.findById(1L)).thenReturn(testUser); + when(scheduleMapper.findConflictingSchedules(eq(1L), any(), any(), any())) + .thenReturn(Arrays.asList(conflictingSchedule)); + + // 执行测试 + Result result = scheduleService.createSchedule(1L, createScheduleDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertTrue(result.getMessage().contains("时间冲突")); + } + + @Test + void testCreateSchedule_InvalidTimeRange() { + // 测试结束时间早于开始时间 + createScheduleDTO.setStartTime(LocalDateTime.of(2024, 1, 16, 16, 0)); + createScheduleDTO.setEndTime(LocalDateTime.of(2024, 1, 16, 14, 0)); + + // 执行测试 + Result result = scheduleService.createSchedule(1L, createScheduleDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("结束时间不能早于开始时间", result.getMessage()); + } + + @Test + void testGetScheduleDetail_Success() { + // Mock 依赖方法 + when(scheduleMapper.findById(1L)).thenReturn(testSchedule); + + // 执行测试 + Result result = scheduleService.getScheduleDetail(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + } + + @Test + void testGetScheduleDetail_NotFound() { + // Mock 日程不存在 + when(scheduleMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = scheduleService.getScheduleDetail(1L, 1L); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("日程不存在", result.getMessage()); + } + + @Test + void testGetScheduleDetail_Unauthorized() { + // Mock 其他用户的日程 + testSchedule.setUserId(2L); + when(scheduleMapper.findById(1L)).thenReturn(testSchedule); + + // 执行测试 + Result result = scheduleService.getScheduleDetail(1L, 1L); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(403, result.getCode()); + assertEquals("无权限查看此日程", result.getMessage()); + } + + @Test + void testGetScheduleList_Success() { + // Mock 日程列表 + List schedules = Arrays.asList(testSchedule); + when(scheduleMapper.findByUserId(1L)).thenReturn(schedules); + + // 执行测试 + Result result = scheduleService.getScheduleList(1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证方法调用 + verify(scheduleMapper).findByUserId(1L); + } + + @Test + void testGetScheduleListByTimeRange_Success() { + LocalDateTime startTime = LocalDateTime.of(2024, 1, 1, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59); + + // Mock 时间范围内的日程列表 + List schedules = Arrays.asList(testSchedule); + when(scheduleMapper.findByUserIdAndTimeRange(1L, startTime, endTime)).thenReturn(schedules); + + // 执行测试 + Result result = scheduleService.getScheduleListByTimeRange(1L, startTime, endTime); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证方法调用 + verify(scheduleMapper).findByUserIdAndTimeRange(1L, startTime, endTime); + } + + @Test + void testUpdateSchedule_Success() { + // Mock 依赖方法 + when(scheduleMapper.findById(1L)).thenReturn(testSchedule); + when(scheduleMapper.findConflictingSchedules(eq(1L), any(), any(), eq(1L))).thenReturn(Arrays.asList()); + when(scheduleMapper.update(any(Schedule.class))).thenReturn(1); + + // 执行测试 + Result result = scheduleService.updateSchedule(1L, 1L, createScheduleDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("日程更新成功", result.getMessage()); + + // 验证方法调用 + verify(scheduleMapper).update(any(Schedule.class)); + } + + @Test + void testUpdateSchedule_Unauthorized() { + // Mock 其他用户的日程 + testSchedule.setUserId(2L); + when(scheduleMapper.findById(1L)).thenReturn(testSchedule); + + // 执行测试 + Result result = scheduleService.updateSchedule(1L, 1L, createScheduleDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(403, result.getCode()); + assertEquals("无权限修改此日程", result.getMessage()); + } + + @Test + void testDeleteSchedule_Success() { + // Mock 依赖方法 + when(scheduleMapper.findById(1L)).thenReturn(testSchedule); + when(scheduleMapper.delete(1L)).thenReturn(1); + + // 执行测试 + Result result = scheduleService.deleteSchedule(1L, 1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("日程删除成功", result.getMessage()); + + // 验证方法调用 + verify(scheduleMapper).delete(1L); + } + + @Test + void testCheckScheduleConflict_NoConflict() { + LocalDateTime startTime = LocalDateTime.of(2024, 1, 16, 14, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 16, 15, 30); + + // Mock 无冲突 + when(scheduleMapper.findConflictingSchedules(eq(1L), eq(startTime), eq(endTime), any())) + .thenReturn(Arrays.asList()); + + // 执行测试 + Result result = scheduleService.checkScheduleConflict(1L, startTime, endTime, null); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("无时间冲突", result.getMessage()); + } + + @Test + void testCheckScheduleConflict_HasConflict() { + LocalDateTime startTime = LocalDateTime.of(2024, 1, 16, 14, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 16, 15, 30); + + // Mock 有冲突 + when(scheduleMapper.findConflictingSchedules(eq(1L), eq(startTime), eq(endTime), any())) + .thenReturn(Arrays.asList(testSchedule)); + + // 执行测试 + Result result = scheduleService.checkScheduleConflict(1L, startTime, endTime, null); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertTrue(result.getMessage().contains("时间冲突")); + } + + @Test + void testProcessScheduleReminders_Success() { + // Mock 需要提醒的日程 + List upcomingSchedules = Arrays.asList(testSchedule); + when(scheduleMapper.findUpcomingSchedules(any())).thenReturn(upcomingSchedules); + + // 执行测试 + Result result = scheduleService.processScheduleReminders(); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("提醒处理完成", result.getMessage()); + + // 验证方法调用 + verify(scheduleMapper).findUpcomingSchedules(any()); + } + + @Test + void testCreateSchedule_WeeklyRepeat() { + // 测试周重复日程 + createScheduleDTO.setRepeatType("WEEKLY"); + createScheduleDTO.setRepeatEnd(LocalDateTime.of(2024, 3, 16, 15, 30)); + + when(userMapper.findById(1L)).thenReturn(testUser); + when(scheduleMapper.findConflictingSchedules(eq(1L), any(), any(), any())).thenReturn(Arrays.asList()); + when(scheduleMapper.insert(any(Schedule.class))).thenReturn(1); + + // 执行测试 + Result result = scheduleService.createSchedule(1L, createScheduleDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + + // 验证会创建多个重复的日程实例 + verify(scheduleMapper, atLeast(1)).insert(any(Schedule.class)); + } + + @Test + void testCreateSchedule_DailyRepeat() { + // 测试日重复日程 + createScheduleDTO.setRepeatType("DAILY"); + createScheduleDTO.setRepeatEnd(LocalDateTime.of(2024, 1, 20, 15, 30)); + + when(userMapper.findById(1L)).thenReturn(testUser); + when(scheduleMapper.findConflictingSchedules(eq(1L), any(), any(), any())).thenReturn(Arrays.asList()); + when(scheduleMapper.insert(any(Schedule.class))).thenReturn(1); + + // 执行测试 + Result result = scheduleService.createSchedule(1L, createScheduleDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + + // 验证会创建多个重复的日程实例 + verify(scheduleMapper, atLeast(1)).insert(any(Schedule.class)); + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/service/UserServiceTest.java b/unilife-server/src/test/java/com/unilife/service/UserServiceTest.java new file mode 100644 index 0000000..3b6cb18 --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/service/UserServiceTest.java @@ -0,0 +1,438 @@ +package com.unilife.service; + +import com.unilife.common.result.Result; +import com.unilife.mapper.UserMapper; +import com.unilife.model.dto.CreateUserDTO; +import com.unilife.model.dto.UpdateUserDTO; +import com.unilife.model.dto.LoginDTO; +import com.unilife.model.entity.User; +import com.unilife.service.impl.UserServiceImpl; +import com.unilife.utils.JwtUtil; +import com.unilife.utils.PasswordUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +class UserServiceTest { + + @Mock + private UserMapper userMapper; + + @Mock + private StringRedisTemplate redisTemplate; + + @Mock + private JavaMailSender mailSender; + + @InjectMocks + private UserServiceImpl userService; + + private User testUser; + private CreateUserDTO createUserDTO; + private UpdateUserDTO updateUserDTO; + private LoginDTO loginDTO; + + @BeforeEach + void setUp() { + // 初始化测试数据 + testUser = new User(); + testUser.setId(1L); + testUser.setUsername("testuser"); + testUser.setEmail("test@example.com"); + testUser.setNickname("测试用户"); + testUser.setPassword("$2a$10$encrypted_password"); // 模拟加密后的密码 + testUser.setAvatar("avatar.jpg"); + testUser.setStatus(1); + testUser.setCreatedAt(LocalDateTime.now()); + testUser.setUpdatedAt(LocalDateTime.now()); + + createUserDTO = new CreateUserDTO(); + createUserDTO.setUsername("newuser"); + createUserDTO.setEmail("newuser@example.com"); + createUserDTO.setNickname("新用户"); + createUserDTO.setPassword("password123"); + + updateUserDTO = new UpdateUserDTO(); + updateUserDTO.setNickname("更新后的昵称"); + updateUserDTO.setAvatar("new_avatar.jpg"); + + loginDTO = new LoginDTO(); + loginDTO.setUsername("testuser"); + loginDTO.setPassword("password123"); + } + + @Test + void testRegister_Success() { + // Mock 依赖方法 + when(userMapper.findByUsername("newuser")).thenReturn(null); + when(userMapper.findByEmail("newuser@example.com")).thenReturn(null); + when(userMapper.insert(any(User.class))).thenReturn(1); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class)) { + passwordUtil.when(() -> PasswordUtil.encode("password123")) + .thenReturn("$2a$10$encrypted_password"); + + // 执行测试 + Result result = userService.register(createUserDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("注册成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).findByUsername("newuser"); + verify(userMapper).findByEmail("newuser@example.com"); + verify(userMapper).insert(any(User.class)); + } + } + + @Test + void testRegister_UsernameExists() { + // Mock 用户名已存在 + when(userMapper.findByUsername("newuser")).thenReturn(testUser); + + // 执行测试 + Result result = userService.register(createUserDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("用户名已存在", result.getMessage()); + + // 验证不会尝试插入用户 + verify(userMapper, never()).insert(any(User.class)); + } + + @Test + void testRegister_EmailExists() { + // Mock 邮箱已存在 + when(userMapper.findByUsername("newuser")).thenReturn(null); + when(userMapper.findByEmail("newuser@example.com")).thenReturn(testUser); + + // 执行测试 + Result result = userService.register(createUserDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("邮箱已存在", result.getMessage()); + } + + @Test + void testLogin_Success() { + // Mock 依赖方法 + when(userMapper.findByUsername("testuser")).thenReturn(testUser); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class); + MockedStatic jwtUtil = mockStatic(JwtUtil.class)) { + + passwordUtil.when(() -> PasswordUtil.matches("password123", "$2a$10$encrypted_password")) + .thenReturn(true); + jwtUtil.when(() -> JwtUtil.generateToken(1L)) + .thenReturn("mock_jwt_token"); + + // 执行测试 + Result result = userService.login(loginDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("登录成功", result.getMessage()); + assertNotNull(result.getData()); + + // 验证方法调用 + verify(userMapper).findByUsername("testuser"); + verify(userMapper).updateLastLoginTime(1L); + } + } + + @Test + void testLogin_UserNotFound() { + // Mock 用户不存在 + when(userMapper.findByUsername("testuser")).thenReturn(null); + + // 执行测试 + Result result = userService.login(loginDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(401, result.getCode()); + assertEquals("用户名或密码错误", result.getMessage()); + } + + @Test + void testLogin_PasswordIncorrect() { + // Mock 密码错误 + when(userMapper.findByUsername("testuser")).thenReturn(testUser); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class)) { + passwordUtil.when(() -> PasswordUtil.matches("password123", "$2a$10$encrypted_password")) + .thenReturn(false); + + // 执行测试 + Result result = userService.login(loginDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(401, result.getCode()); + assertEquals("用户名或密码错误", result.getMessage()); + } + } + + @Test + void testLogin_UserDisabled() { + // Mock 用户被禁用 + testUser.setStatus(0); + when(userMapper.findByUsername("testuser")).thenReturn(testUser); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class)) { + passwordUtil.when(() -> PasswordUtil.matches("password123", "$2a$10$encrypted_password")) + .thenReturn(true); + + // 执行测试 + Result result = userService.login(loginDTO); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(403, result.getCode()); + assertEquals("账户已被禁用", result.getMessage()); + } + } + + @Test + void testGetUserInfo_Success() { + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + + // 执行测试 + Result result = userService.getUserInfo(1L); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证方法调用 + verify(userMapper).findById(1L); + } + + @Test + void testGetUserInfo_UserNotFound() { + // Mock 用户不存在 + when(userMapper.findById(1L)).thenReturn(null); + + // 执行测试 + Result result = userService.getUserInfo(1L); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(404, result.getCode()); + assertEquals("用户不存在", result.getMessage()); + } + + @Test + void testUpdateUserInfo_Success() { + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + when(userMapper.update(any(User.class))).thenReturn(1); + + // 执行测试 + Result result = userService.updateUserInfo(1L, updateUserDTO); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("用户信息更新成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).update(any(User.class)); + } + + @Test + void testSendEmailVerificationCode_Success() { + String email = "test@example.com"; + String verificationCode = "123456"; + + // Mock Redis操作 + when(redisTemplate.opsForValue()).thenReturn(mock(org.springframework.data.redis.core.ValueOperations.class)); + + // 执行测试 + Result result = userService.sendEmailVerificationCode(email); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("验证码发送成功", result.getMessage()); + + // 验证邮件发送 + verify(mailSender).send(any(SimpleMailMessage.class)); + } + + @Test + void testVerifyEmailCode_Success() { + String email = "test@example.com"; + String code = "123456"; + + // Mock Redis操作 + when(redisTemplate.opsForValue()).thenReturn(mock(org.springframework.data.redis.core.ValueOperations.class)); + when(redisTemplate.opsForValue().get("email_code:" + email)).thenReturn(code); + + // 执行测试 + Result result = userService.verifyEmailCode(email, code); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("验证码验证成功", result.getMessage()); + + // 验证删除验证码 + verify(redisTemplate).delete("email_code:" + email); + } + + @Test + void testVerifyEmailCode_CodeExpired() { + String email = "test@example.com"; + String code = "123456"; + + // Mock 验证码不存在(已过期) + when(redisTemplate.opsForValue()).thenReturn(mock(org.springframework.data.redis.core.ValueOperations.class)); + when(redisTemplate.opsForValue().get("email_code:" + email)).thenReturn(null); + + // 执行测试 + Result result = userService.verifyEmailCode(email, code); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("验证码已过期", result.getMessage()); + } + + @Test + void testVerifyEmailCode_CodeIncorrect() { + String email = "test@example.com"; + String code = "123456"; + String wrongCode = "654321"; + + // Mock 验证码错误 + when(redisTemplate.opsForValue()).thenReturn(mock(org.springframework.data.redis.core.ValueOperations.class)); + when(redisTemplate.opsForValue().get("email_code:" + email)).thenReturn(wrongCode); + + // 执行测试 + Result result = userService.verifyEmailCode(email, code); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("验证码错误", result.getMessage()); + } + + @Test + void testResetPassword_Success() { + String email = "test@example.com"; + String newPassword = "newpassword123"; + + // Mock 依赖方法 + when(userMapper.findByEmail(email)).thenReturn(testUser); + when(userMapper.updatePassword(eq(1L), anyString())).thenReturn(1); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class)) { + passwordUtil.when(() -> PasswordUtil.encode(newPassword)) + .thenReturn("$2a$10$new_encrypted_password"); + + // 执行测试 + Result result = userService.resetPassword(email, newPassword); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("密码重置成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).updatePassword(eq(1L), eq("$2a$10$new_encrypted_password")); + } + } + + @Test + void testGetUserList_Success() { + // Mock 用户列表 + List users = Arrays.asList(testUser); + when(userMapper.findByConditions(any(), any(), anyInt(), anyInt())).thenReturn(users); + when(userMapper.countByConditions(any(), any())).thenReturn(1); + + // 执行测试 + Result result = userService.getUserList("测试", 1, 1, 10); + + // 验证结果 + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + + // 验证方法调用 + verify(userMapper).findByConditions(any(), any(), anyInt(), anyInt()); + verify(userMapper).countByConditions(any(), any()); + } + + @Test + void testChangePassword_Success() { + String oldPassword = "oldpassword"; + String newPassword = "newpassword123"; + + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + when(userMapper.updatePassword(eq(1L), anyString())).thenReturn(1); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class)) { + passwordUtil.when(() -> PasswordUtil.matches(oldPassword, "$2a$10$encrypted_password")) + .thenReturn(true); + passwordUtil.when(() -> PasswordUtil.encode(newPassword)) + .thenReturn("$2a$10$new_encrypted_password"); + + // 执行测试 + Result result = userService.changePassword(1L, oldPassword, newPassword); + + // 验证结果 + assertTrue(result.isSuccess()); + assertEquals("密码修改成功", result.getMessage()); + + // 验证方法调用 + verify(userMapper).updatePassword(eq(1L), eq("$2a$10$new_encrypted_password")); + } + } + + @Test + void testChangePassword_OldPasswordIncorrect() { + String oldPassword = "wrongpassword"; + String newPassword = "newpassword123"; + + // Mock 依赖方法 + when(userMapper.findById(1L)).thenReturn(testUser); + + try (MockedStatic passwordUtil = mockStatic(PasswordUtil.class)) { + passwordUtil.when(() -> PasswordUtil.matches(oldPassword, "$2a$10$encrypted_password")) + .thenReturn(false); + + // 执行测试 + Result result = userService.changePassword(1L, oldPassword, newPassword); + + // 验证结果 + assertFalse(result.isSuccess()); + assertEquals(400, result.getCode()); + assertEquals("原密码错误", result.getMessage()); + + // 验证不会更新密码 + verify(userMapper, never()).updatePassword(anyLong(), anyString()); + } + } +} \ No newline at end of file diff --git a/unilife-server/src/test/java/com/unilife/utils/TestDataBuilder.java b/unilife-server/src/test/java/com/unilife/utils/TestDataBuilder.java new file mode 100644 index 0000000..d9c3746 --- /dev/null +++ b/unilife-server/src/test/java/com/unilife/utils/TestDataBuilder.java @@ -0,0 +1,117 @@ +package com.unilife.utils; + +import com.unilife.model.dto.*; +import com.unilife.model.entity.*; + +import java.time.LocalDateTime; + +/** + * 测试数据构建工具类 + * 提供各种实体和DTO的测试数据构建方法 + */ +public class TestDataBuilder { + + /** + * 构建测试用户 + */ + public static User buildTestUser() { + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + user.setEmail("test@example.com"); + user.setNickname("测试用户"); + user.setPassword("$2a$10$encrypted_password"); + user.setAvatar("avatar.jpg"); + user.setStatus(1); + user.setCreatedAt(LocalDateTime.now()); + user.setUpdatedAt(LocalDateTime.now()); + return user; + } + + /** + * 构建测试分类 + */ + public static Category buildTestCategory() { + Category category = new Category(); + category.setId(1L); + category.setName("测试分类"); + category.setDescription("测试分类描述"); + category.setIcon("test-icon"); + category.setSort(1); + category.setStatus(1); + category.setCreatedAt(LocalDateTime.now()); + category.setUpdatedAt(LocalDateTime.now()); + return category; + } + + /** + * 构建测试帖子 + */ + public static Post buildTestPost() { + Post post = new Post(); + post.setId(1L); + post.setTitle("测试帖子"); + post.setContent("测试帖子内容"); + post.setUserId(1L); + post.setCategoryId(1L); + post.setLikeCount(0); + post.setViewCount(0); + post.setCommentCount(0); + post.setCreatedAt(LocalDateTime.now()); + post.setUpdatedAt(LocalDateTime.now()); + return post; + } + + /** + * 构建测试资源 + */ + public static Resource buildTestResource() { + Resource resource = new Resource(); + resource.setId(1L); + resource.setTitle("测试资源"); + resource.setDescription("测试资源描述"); + resource.setFileName("test.pdf"); + resource.setFileUrl("http://example.com/test.pdf"); + resource.setFileSize(1024L); + resource.setFileType("pdf"); + resource.setUserId(1L); + resource.setCategoryId(1L); + resource.setDownloadCount(0); + resource.setLikeCount(0); + resource.setCreatedAt(LocalDateTime.now()); + resource.setUpdatedAt(LocalDateTime.now()); + return resource; + } + + /** + * 构建创建帖子DTO + */ + public static CreatePostDTO buildCreatePostDTO() { + CreatePostDTO dto = new CreatePostDTO(); + dto.setTitle("新帖子标题"); + dto.setContent("新帖子内容"); + dto.setCategoryId(1L); + return dto; + } + + /** + * 构建创建用户DTO + */ + public static CreateUserDTO buildCreateUserDTO() { + CreateUserDTO dto = new CreateUserDTO(); + dto.setUsername("newuser"); + dto.setEmail("newuser@example.com"); + dto.setNickname("新用户"); + dto.setPassword("password123"); + return dto; + } + + /** + * 构建带有指定ID的用户 + */ + public static User buildTestUser(Long id) { + User user = buildTestUser(); + user.setId(id); + return user; + } +} \ No newline at end of file diff --git a/unilife-server/src/test/resources/application-test.yml b/unilife-server/src/test/resources/application-test.yml new file mode 100644 index 0000000..e30031b --- /dev/null +++ b/unilife-server/src/test/resources/application-test.yml @@ -0,0 +1,66 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + + h2: + console: + enabled: true + + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop + show-sql: true + + redis: + host: localhost + port: 6379 + database: 1 + timeout: 2000ms + + mail: + host: smtp.example.com + port: 587 + username: test@example.com + password: testpassword + properties: + mail: + smtp: + auth: true + starttls: + enable: true + +mybatis: + mapper-locations: classpath:mapper/*.xml + type-aliases-package: com.unilife.model.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +logging: + level: + com.unilife: DEBUG + org.springframework.web: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" + +# JWT配置 +jwt: + secret: test-secret-key-for-unit-testing-purposes-only + expiration: 3600000 + +# 文件上传配置 +file: + upload: + path: /tmp/unilife-test/uploads/ + max-size: 10MB + +# 测试特定配置 +test: + mock: + enabled: true + database: + cleanup: true \ No newline at end of file