main
2991692032 2 months ago
parent d236217c22
commit b796dab689

@ -31,3 +31,14 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .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

@ -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规则
- 来源:您的前端域名
- 允许MethodsGET, POST, PUT, DELETE, HEAD
- 允许Headers*
- 暴露HeadersETag, x-oss-request-id
## 安全注意事项
1. **永远不要将AccessKey提交到代码仓库**
2. **使用RAM子账号最小权限原则**
3. **定期轮换AccessKey**
4. **启用OSS访问日志监控**
5. **配置适当的Bucket策略**
## 生产环境部署
生产环境建议使用以下方式之一:
1. **容器环境变量**Docker/Kubernetes
2. **云服务商的密钥管理服务**
3. **专门的配置中心**如Nacos、Apollo

@ -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<AiMessageHistoryVO> getSessionMessages(String sessionId, Integer page, Integer size) {
// 直接从Spring AI ChatMemory获取
List<Message> messages = chatMemory.get(sessionId);
// 转换为VO并返回
// ... 转换逻辑
}
```
### 4. 保留的核心功能
- 会话列表管理
- 会话标题管理
- 会话创建/删除
- 从 SPRING_AI_CHAT_MEMORY 表直接查询消息历史
## 实际需要的修改
1. **删除冗余表**: 移除 `ai_chat_messages_history`
2. **简化服务**: 移除消息同步逻辑直接使用Spring AI ChatMemory
3. **保留会话管理**: 只管理会话元数据
## 结论
您的担心是对的大部分功能确实是冗余的。Spring AI ChatMemory已经提供了强大的消息存储和记忆功能我们只需要补充会话元数据管理即可。

@ -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

@ -15,6 +15,7 @@
<spring-boot.version>3.4.3</spring-boot.version> <spring-boot.version>3.4.3</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-ai.version>1.0.0</spring-ai.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@ -26,6 +27,13 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@ -123,10 +131,86 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- MockMvc for web layer testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito for mocking -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- H2 Database for testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- TestContainers for integration testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- 阿里云OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- Spring AI JDBC Chat Memory Repository for MySQL -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- Spring AI Chroma Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-chroma</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -140,6 +224,7 @@
<source>${java.version}</source> <source>${java.version}</source>
<target>${java.version}</target> <target>${java.version}</target>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
<parameters>true</parameters>
</configuration> </configuration>
</plugin> </plugin>
@ -148,6 +233,15 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version> <version>${spring-boot.version}</version>
<configuration>
<!-- 确保Lombok注解处理器不会干扰参数名保留 -->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions> <executions>
<execution> <execution>
<goals> <goals>

@ -0,0 +1,37 @@
package com.unilife.common.constant;
public class Prompt {
public static final String Prompt = """
#
AIUniLife
#
1. ****
*
2. ****
* []
* ****
3. ****
*
*
*
4. ****
* ****
*
*
*
*
* ****
*
5. ****
*
*
* 使
""";
}

@ -53,6 +53,10 @@ public class Result<T>{
return new Result<>(code, message, null); return new Result<>(code, message, null);
} }
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
public static <T> Result<T> error(T data,String message){ public static <T> Result<T> error(T data,String message){
return new Result<>(200,message,null); return new Result<>(200,message,null);
} }

@ -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;
}
/**
* ChatClientChat 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;
}
}

@ -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;
}
}

@ -1,32 +1,67 @@
package com.unilife.config; package com.unilife.config;
import com.unilife.interceptor.AdminInterceptor;
import com.unilife.interceptor.JwtInterceptor; import com.unilife.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 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 org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.File;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired @Autowired
private JwtInterceptor jwtInterceptor; private JwtInterceptor jwtInterceptor;
@Autowired
private AdminInterceptor adminInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// 管理员权限拦截器 - 优先级最高
registry.addInterceptor(adminInterceptor)
.addPathPatterns("/admin/**")
.order(1);
// JWT拦截器
registry.addInterceptor(jwtInterceptor).addPathPatterns("/**") registry.addInterceptor(jwtInterceptor).addPathPatterns("/**")
.excludePathPatterns( .excludePathPatterns(
// 用户登录注册相关
"/users/login", "/users/login",
"/users/register", "/users/register",
"/users/code", "/users/code",
"/users/login/code", "/users/login/code",
// 静态资源访问
"/api/files/**",
// 管理员接口由AdminInterceptor处理
"/admin/**",
// Swagger文档相关
"/swagger-resources/**", "/swagger-resources/**",
"/v3/api-docs/**", "/v3/api-docs/**",
"/doc.html", "/doc.html",
"/webjars/**", "/webjars/**",
"/favicon.ico", "/favicon.ico",
"/knife4j/**" "/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 @Override

@ -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<String, Integer> 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<String, Integer> 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<String, Integer> 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<String, Object> request) {
return adminService.createCategory(request);
}
@Operation(summary = "更新分类")
@PutMapping("/categories/{categoryId}")
public Result updateCategory(@PathVariable Long categoryId, @RequestBody Map<String, Object> 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);
}
}

@ -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<String> 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<AiSessionListVO> 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<AiMessageHistoryVO> 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<AiCreateSessionVO> createSession(@RequestBody AiCreateSessionDTO createSessionDTO) {
log.info("创建聊天会话: {}", createSessionDTO.getSessionId());
return aiService.createSession(createSessionDTO);
}
@Operation(summary = "更新会话标题")
@PutMapping("/sessions/{sessionId}")
public Result<Void> 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<Void> clearSessionMessages(@Parameter(description = "会话ID") @PathVariable String sessionId) {
log.info("清空会话消息会话ID: {}", sessionId);
return aiService.clearSessionMessages(sessionId);
}
@Operation(summary = "删除会话")
@DeleteMapping("/sessions/{sessionId}")
public Result<Void> deleteSession(@Parameter(description = "会话ID") @PathVariable String sessionId) {
log.info("删除会话会话ID: {}", sessionId);
return aiService.deleteSession(sessionId);
}
}

@ -63,6 +63,17 @@ public class CourseController {
return courseService.getCourseListByDayOfWeek(userId, dayOfWeek); 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 = "更新课程") @Operation(summary = "更新课程")
@PutMapping("/{id}") @PutMapping("/{id}")
public Result<?> updateCourse( public Result<?> updateCourse(

@ -42,11 +42,14 @@ public class PostController {
@Operation(summary = "获取帖子列表") @Operation(summary = "获取帖子列表")
@GetMapping @GetMapping
public Result<?> getPostList( 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 = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "sort", defaultValue = "latest") String sort) { @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 = "更新帖子") @Operation(summary = "更新帖子")
@ -83,4 +86,14 @@ public class PostController {
} }
return postService.likePost(postId, userId); 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);
}
} }

@ -53,11 +53,13 @@ public class ResourceController {
@GetMapping @GetMapping
public Result<?> getResourceList( public Result<?> getResourceList(
@RequestParam(value = "category", required = false) Long categoryId, @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 = "keyword", required = false) String keyword,
@RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size) { @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 = "更新资源") @Operation(summary = "更新资源")

@ -139,4 +139,26 @@ public class UserController {
} }
return userService.updateEmail(userId, emailDTO); 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);
}
} }

@ -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);
}
}

@ -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;
/**
* AIMapper
*/
@Mapper
public interface AiChatSessionMapper {
/**
*
*/
int insert(AiChatSession session);
/**
* ID
*/
AiChatSession selectById(@Param("id") String id);
/**
* ID
*/
List<AiChatSession> selectByUserId(@Param("userId") Long userId,
@Param("offset") int offset,
@Param("limit") int limit);
/**
* ID
*/
List<AiChatSession> 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);
}

@ -49,4 +49,36 @@ public interface CategoryMapper {
* @return * @return
*/ */
Integer getCount(@Param("status") Byte status); Integer getCount(@Param("status") Byte status);
// ========== 管理员后台相关方法 ==========
/**
*
*/
int getTotalCount();
/**
*
*/
List<Category> getAllCategories();
/**
* ID
*/
Category getCategoryById(Long id);
/**
*
*/
void insertCategory(Category category);
/**
*
*/
void updateCategory(Category category);
/**
*
*/
void deleteCategory(Long categoryId);
} }

@ -68,4 +68,42 @@ public interface CommentMapper {
* @param id ID * @param id ID
*/ */
void decrementLikeCount(Long id); void decrementLikeCount(Long id);
// ========== 管理员后台相关方法 ==========
/**
*
*/
int getTotalCount();
/**
*
*/
int getNewCommentCountToday();
/**
* ID
*/
Comment getCommentById(Long id);
/**
*
*/
List<Comment> 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);
} }

@ -52,6 +52,14 @@ public interface CourseMapper {
*/ */
List<Course> getListByUserIdAndDayOfWeek(@Param("userId") Long userId, @Param("dayOfWeek") Byte dayOfWeek); List<Course> getListByUserIdAndDayOfWeek(@Param("userId") Long userId, @Param("dayOfWeek") Byte dayOfWeek);
/**
*
* @param userId ID
* @param semester 2023-1
* @return
*/
List<Course> getListByUserIdAndSemester(@Param("userId") Long userId, @Param("semester") String semester);
/** /**
* *
* @param userId ID * @param userId ID

@ -79,4 +79,83 @@ public interface PostMapper {
* @param id ID * @param id ID
*/ */
void decrementCommentCount(Long id); void decrementCommentCount(Long id);
/**
*
* @param userId ID
* @param sort
* @return
*/
List<Post> getListByUserId(@Param("userId") Long userId, @Param("sort") String sort);
/**
*
* @param userId ID
* @return
*/
Integer getCountByUserId(@Param("userId") Long userId);
/**
*
* @param keyword
* @param categoryId IDnull
* @param sortBy
* @return
*/
List<Post> searchPosts(@Param("keyword") String keyword,
@Param("categoryId") Long categoryId,
@Param("sortBy") String sortBy);
// ========== 管理员后台相关方法 ==========
/**
*
*/
int getTotalCount();
/**
*
*/
int getNewPostCountToday();
/**
* ID
*/
Post getPostById(Long id);
/**
*
*/
List<Post> 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);
} }

@ -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);
}

@ -89,4 +89,53 @@ public interface ResourceMapper {
* @return * @return
*/ */
Integer getCountByCategoryId(Long categoryId); Integer getCountByCategoryId(Long categoryId);
/**
*
* @param keyword
* @param categoryId IDnull
* @param sortBy
* @return
*/
List<Resource> searchResources(@Param("keyword") String keyword,
@Param("categoryId") Long categoryId,
@Param("sortBy") String sortBy);
// ========== 管理员后台相关方法 ==========
/**
*
*/
int getTotalCount();
/**
*
*/
int getNewResourceCountToday();
/**
* ID
*/
Resource getResourceById(Long id);
/**
*
*/
List<Resource> 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);
} }

@ -5,11 +5,13 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.Date; import java.util.Date;
import java.util.List;
@Mapper @Mapper
public interface UserMapper { public interface UserMapper {
void insert(User user); void insert(User user);
User findByEmail(String email); User findByEmail(String email);
User findByUsername(String username);
void updateLoginInfo(@Param("userId") Long userId, void updateLoginInfo(@Param("userId") Long userId,
@Param("ipLocation") String ipLocation, @Param("ipLocation") String ipLocation,
@Param("loginTime") Date loginTime); @Param("loginTime") Date loginTime);
@ -21,4 +23,96 @@ public interface UserMapper {
void updatePassword(@Param("id") Long id, @Param("newPassword") String newPassword); void updatePassword(@Param("id") Long id, @Param("newPassword") String newPassword);
void updateAvatar(@Param("id") Long id, @Param("avatar") String avatarUrl); void updateAvatar(@Param("id") Long id, @Param("avatar") String avatarUrl);
void updateEmail(@Param("id") Long id, @Param("email") String email); void updateEmail(@Param("id") Long id, @Param("email") String email);
/**
*
* @param keyword
* @return
*/
List<User> searchUsers(@Param("keyword") String keyword);
/**
* status0
* @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<User> 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);
} }

@ -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;
}

@ -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<ConversationMessage> conversationHistory;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ConversationMessage {
private String id;
private String role;
private String content;
private String timestamp;
}
}

@ -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;
}

@ -4,8 +4,6 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.time.LocalTime;
/** /**
* *
*/ */
@ -34,14 +32,14 @@ public class CreateCourseDTO {
private Byte dayOfWeek; 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; private Byte endWeek;
/**
* 2023-1
*/
private String semester;
/** /**
* *
*/ */

@ -46,7 +46,7 @@ public class CreateScheduleDTO {
/** /**
* *
*/ */
private Byte reminder; private Integer reminder;
/** /**
* *

@ -9,6 +9,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class LoginDTO { public class LoginDTO {
private String email; private String account; // 支持用户名或邮箱
private String password; private String password;
} }

@ -11,7 +11,8 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class UpdateProfileDTO { 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 String bio;
private Byte gender; private Byte gender;
private String department; private String department;

@ -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;
}

@ -70,6 +70,11 @@ public class Course implements Serializable {
*/ */
private Byte endWeek; private Byte endWeek;
/**
* 2023-1
*/
private String semester;
/** /**
* *
*/ */

@ -62,7 +62,7 @@ public class Schedule implements Serializable {
/** /**
* *
*/ */
private Byte reminder; private Integer reminder;
/** /**
* *

@ -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;
}

@ -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<AiMessageVO> messages;
/**
*
*/
private Long total;
/**
*
*/
private AiSessionVO sessionInfo;
}

@ -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;
}

@ -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<AiSessionVO> sessions;
/**
*
*/
private Long total;
}

@ -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;
}

@ -66,6 +66,11 @@ public class CourseVO {
*/ */
private Byte endWeek; private Byte endWeek;
/**
* 2023-1
*/
private String semester;
/** /**
* *
*/ */

@ -70,6 +70,16 @@ public class PostListVO {
*/ */
private Integer commentCount; private Integer commentCount;
/**
* 0-, 1-, 2-
*/
private Byte status;
/**
*
*/
private Boolean isLiked;
/** /**
* *
*/ */

@ -35,10 +35,6 @@ public class PostVO {
*/ */
private Long userId; private Long userId;
/**
*
*/
private String nickname;
/** /**
* *

@ -58,7 +58,7 @@ public class ScheduleVO {
/** /**
* *
*/ */
private Byte reminder; private Integer reminder;
/** /**
* *

@ -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<String, Object> request);
/**
*
*/
Result updateCategory(Long categoryId, Map<String, Object> request);
/**
*
*/
Result deleteCategory(Long categoryId);
/**
*
*/
Result getResourceList(Integer page, Integer size, String keyword, Long categoryId, Integer status);
/**
*
*/
Result deleteResource(Long resourceId);
}

@ -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<AiChatSession> createOrUpdateSession(String sessionId, Long userId, String title);
/**
*
* @param userId IDnull
* @param page
* @param size
* @return
*/
Result<AiSessionListVO> getSessionList(Long userId, Integer page, Integer size);
/**
*
* @param sessionId ID
* @return
*/
Result<AiChatSession> getSessionDetail(String sessionId);
/**
*
* @param sessionId ID
* @param title
* @return
*/
Result<Void> updateSessionTitle(String sessionId, String title);
/**
*
* @param sessionId ID
* @return
*/
Result<Void> updateSessionLastActivity(String sessionId);
/**
* Spring AI ChatMemory
* @param sessionId ID
* @return
*/
Result<Void> deleteSession(String sessionId);
}

@ -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<String> sendMessage(AiSendMessageDTO sendMessageDTO);
/**
*
* @param page
* @param size
* @return
*/
Result<AiSessionListVO> getSessionList(Integer page, Integer size);
/**
*
* @param sessionId ID
* @param page
* @param size
* @return
*/
Result<AiMessageHistoryVO> getSessionMessages(String sessionId, Integer page, Integer size);
/**
*
* @param createSessionDTO DTO
* @return
*/
Result<AiCreateSessionVO> createSession(AiCreateSessionDTO createSessionDTO);
/**
*
* @param sessionId ID
* @param updateSessionDTO DTO
* @return
*/
Result<Void> updateSessionTitle(String sessionId, AiUpdateSessionDTO updateSessionDTO);
/**
*
* @param sessionId ID
* @return
*/
Result<Void> clearSessionMessages(String sessionId);
/**
*
* @param sessionId ID
* @return
*/
Result<Void> deleteSession(String sessionId);
}

@ -38,6 +38,14 @@ public interface CourseService {
*/ */
Result getCourseListByDayOfWeek(Long userId, Byte dayOfWeek); Result getCourseListByDayOfWeek(Long userId, Byte dayOfWeek);
/**
*
* @param userId ID
* @param semester 2023-1
* @return
*/
Result getCourseListBySemester(Long userId, String semester);
/** /**
* *
* @param courseId ID * @param courseId ID

@ -27,12 +27,14 @@ public interface PostService {
/** /**
* *
* @param categoryId IDnull * @param categoryId IDnull
* @param keyword null
* @param page * @param page
* @param size * @param size
* @param sort latest-hot- * @param sort latest-hot-
* @param userId IDnull
* @return * @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 * @return
*/ */
Result likePost(Long postId, Long userId); 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);
} }

@ -28,13 +28,14 @@ public interface ResourceService {
/** /**
* *
* @param categoryId IDnull * @param categoryId IDnull
* @param userId IDnull * @param uploaderUserId IDnull
* @param keyword null * @param keyword null
* @param page * @param page
* @param size * @param size
* @param currentUserId IDnull
* @return * @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);
/** /**
* *

@ -30,4 +30,18 @@ public interface UserService {
Result updateAvatar(Long userId, MultipartFile file); Result updateAvatar(Long userId, MultipartFile file);
Result updateEmail(Long userId, UpdateEmailDTO emailDTO); Result updateEmail(Long userId, UpdateEmailDTO emailDTO);
// 用户统计数据
Result getUserStats(Long userId);
// 用户最近帖子
Result getUserRecentPosts(Long userId, Integer limit);
/**
*
* 使
* @param userId ID
* @return
*/
Result deleteUser(Long userId);
} }

@ -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<String, Object> 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<User> users = userMapper.getAdminUserList(offset, size, keyword, role, status);
int total = userMapper.getAdminUserCount(keyword, role, status);
Map<String, Object> 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<Post> posts = postMapper.getAdminPostList(offset, size, keyword, categoryId, status);
int total = postMapper.getAdminPostCount(keyword, categoryId, status);
Map<String, Object> 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<Comment> comments = commentMapper.getAdminCommentList(offset, size, keyword, postId, status);
int total = commentMapper.getAdminCommentCount(keyword, postId, status);
Map<String, Object> 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<Category> categories = categoryMapper.getAllCategories();
return Result.success(categories);
} catch (Exception e) {
log.error("获取分类列表失败", e);
return Result.error(500, "获取分类列表失败");
}
}
@Override
public Result createCategory(Map<String, Object> 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<String, Object> 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<Resource> resources = resourceMapper.getAdminResourceList(offset, size, keyword, categoryId, status);
int total = resourceMapper.getAdminResourceCount(keyword, categoryId, status);
Map<String, Object> 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, "删除资源失败");
}
}
}

@ -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<AiChatSession> 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<AiSessionListVO> getSessionList(Long userId, Integer page, Integer size) {
log.info("获取会话列表: userId={}, page={}, size={}", userId, page, size);
try {
int offset = (page - 1) * size;
List<AiChatSession> 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<AiSessionVO> 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<AiChatSession> 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<Void> 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<Void> 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<Void> 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;
}
}

@ -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<String> 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<String> 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<AiSessionListVO> getSessionList(Integer page, Integer size) {
log.info("获取会话列表: page={}, size={}", page, size);
return sessionHistoryService.getSessionList(BaseContext.getId(), page, size);
}
@Override
public Result<AiMessageHistoryVO> getSessionMessages(String sessionId, Integer page, Integer size) {
log.info("获取会话消息历史会话ID: {}", sessionId);
try {
// 直接从Spring AI ChatMemory获取消息历史
List<Message> messages = chatMemory.get(sessionId);
AiMessageHistoryVO messageHistory = new AiMessageHistoryVO();
if (messages != null && !messages.isEmpty()) {
// 转换Message为VO对象
List<com.unilife.model.vo.AiMessageVO> 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<AiCreateSessionVO> 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<Void> updateSessionTitle(String sessionId, AiUpdateSessionDTO updateSessionDTO) {
log.info("更新会话标题: sessionId={}, title={}", sessionId, updateSessionDTO.getTitle());
// 使用会话历史服务更新标题
return sessionHistoryService.updateSessionTitle(sessionId, updateSessionDTO.getTitle());
}
@Override
public Result<Void> 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<Void> deleteSession(String sessionId) {
log.info("删除会话会话ID: {}", sessionId);
try {
// 删除Spring AI ChatMemory中的消息
chatMemory.clear(sessionId);
// 删除会话元数据
Result<Void> 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<Message> 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<Void> 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);
}
}
}

@ -80,25 +80,38 @@ public class CommentServiceImpl implements CommentService {
@Override @Override
public Result getCommentsByPostId(Long postId, Long userId) { public Result getCommentsByPostId(Long postId, Long userId) {
log.info("获取帖子 {} 的评论列表,当前用户: {}", postId, userId);
// 检查帖子是否存在 // 检查帖子是否存在
Post post = postMapper.getById(postId); Post post = postMapper.getById(postId);
if (post == null) { if (post == null) {
log.warn("帖子 {} 不存在", postId);
return Result.error(404, "帖子不存在"); return Result.error(404, "帖子不存在");
} }
// 获取一级评论 // 获取一级评论
List<Comment> topLevelComments = commentMapper.getTopLevelCommentsByPostId(postId); List<Comment> topLevelComments = commentMapper.getTopLevelCommentsByPostId(postId);
log.info("获取到 {} 条一级评论", topLevelComments.size());
// 转换为VO // 转换为VO
List<CommentVO> commentVOs = topLevelComments.stream().map(comment -> { List<CommentVO> commentVOs = new ArrayList<>();
for (Comment comment : topLevelComments) {
try {
// 获取评论用户信息 // 获取评论用户信息
User user = userMapper.getUserById(comment.getUserId()); User user = userMapper.getUserById(comment.getUserId());
log.debug("评论 {} 的用户信息: {}", comment.getId(), user != null ? user.getNickname() : "null");
// 获取回复列表 // 获取回复列表
List<Comment> replies = commentMapper.getRepliesByParentId(comment.getId()); List<Comment> replies = commentMapper.getRepliesByParentId(comment.getId());
List<CommentVO> replyVOs = replies.stream().map(reply -> { List<CommentVO> replyVOs = new ArrayList<>();
for (Comment reply : replies) {
try {
User replyUser = userMapper.getUserById(reply.getUserId()); User replyUser = userMapper.getUserById(reply.getUserId());
return CommentVO.builder() log.debug("回复 {} 的用户信息: {}", reply.getId(), replyUser != null ? replyUser.getNickname() : "null");
CommentVO replyVO = CommentVO.builder()
.id(reply.getId()) .id(reply.getId())
.postId(reply.getPostId()) .postId(reply.getPostId())
.userId(reply.getUserId()) .userId(reply.getUserId())
@ -111,9 +124,13 @@ public class CommentServiceImpl implements CommentService {
.createdAt(reply.getCreatedAt()) .createdAt(reply.getCreatedAt())
.replies(new ArrayList<>()) .replies(new ArrayList<>())
.build(); .build();
}).collect(Collectors.toList()); replyVOs.add(replyVO);
} catch (Exception e) {
log.error("处理回复 {} 时出错: {}", reply.getId(), e.getMessage());
}
}
return CommentVO.builder() CommentVO commentVO = CommentVO.builder()
.id(comment.getId()) .id(comment.getId())
.postId(comment.getPostId()) .postId(comment.getPostId())
.userId(comment.getUserId()) .userId(comment.getUserId())
@ -126,10 +143,16 @@ public class CommentServiceImpl implements CommentService {
.createdAt(comment.getCreatedAt()) .createdAt(comment.getCreatedAt())
.replies(replyVOs) .replies(replyVOs)
.build(); .build();
}).collect(Collectors.toList());
commentVOs.add(commentVO);
} catch (Exception e) {
log.error("处理评论 {} 时出错: {}", comment.getId(), e.getMessage());
}
}
// 获取评论总数 // 获取评论总数
Integer count = commentMapper.getCountByPostId(postId); Integer count = commentMapper.getCountByPostId(postId);
log.info("帖子 {} 的评论总数: {}, 实际返回: {}", postId, count, commentVOs.size());
// 返回结果 // 返回结果
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();

@ -42,11 +42,20 @@ public class CourseServiceImpl implements CourseService {
return Result.error(404, "用户不存在"); 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(), Integer conflictCount = courseMapper.checkConflict(userId, createCourseDTO.getDayOfWeek(),
startTimeStr, endTimeStr, null); createCourseDTO.getStartTime(), createCourseDTO.getEndTime(), null);
if (conflictCount > 0) { if (conflictCount > 0) {
return Result.error(400, "课程时间冲突,该时间段已有其他课程"); return Result.error(400, "课程时间冲突,该时间段已有其他课程");
} }
@ -54,6 +63,8 @@ public class CourseServiceImpl implements CourseService {
// 创建课程 // 创建课程
Course course = new Course(); Course course = new Course();
BeanUtil.copyProperties(createCourseDTO, course); BeanUtil.copyProperties(createCourseDTO, course);
course.setStartTime(startTime);
course.setEndTime(endTime);
course.setUserId(userId); course.setUserId(userId);
course.setStatus((byte) 1); course.setStatus((byte) 1);
@ -126,6 +137,26 @@ public class CourseServiceImpl implements CourseService {
return Result.success(data); return Result.success(data);
} }
@Override
public Result getCourseListBySemester(Long userId, String semester) {
// 获取用户在指定学期的课程
List<Course> courses = courseMapper.getListByUserIdAndSemester(userId, semester);
// 转换为VO
List<CourseVO> courseVOs = courses.stream().map(course -> {
CourseVO courseVO = new CourseVO();
BeanUtil.copyProperties(course, courseVO);
return courseVO;
}).collect(Collectors.toList());
// 返回结果
Map<String, Object> data = new HashMap<>();
data.put("total", courseVOs.size());
data.put("list", courseVOs);
return Result.success(data);
}
@Override @Override
@Transactional @Transactional
public Result updateCourse(Long courseId, Long userId, CreateCourseDTO createCourseDTO) { public Result updateCourse(Long courseId, Long userId, CreateCourseDTO createCourseDTO) {
@ -140,17 +171,28 @@ public class CourseServiceImpl implements CourseService {
return Result.error(403, "无权限更新此课程"); 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(), Integer conflictCount = courseMapper.checkConflict(userId, createCourseDTO.getDayOfWeek(),
startTimeStr, endTimeStr, courseId); createCourseDTO.getStartTime(), createCourseDTO.getEndTime(), courseId);
if (conflictCount > 0) { if (conflictCount > 0) {
return Result.error(400, "课程时间冲突,该时间段已有其他课程"); return Result.error(400, "课程时间冲突,该时间段已有其他课程");
} }
// 更新课程 // 更新课程
BeanUtil.copyProperties(createCourseDTO, course); BeanUtil.copyProperties(createCourseDTO, course);
course.setStartTime(startTime);
course.setEndTime(endTime);
// 保存更新 // 保存更新
courseMapper.update(course); courseMapper.update(course);

@ -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<Document> documents = reader.read();
// 为每个文档添加资源ID元数据方便后续删除
for (Document document : documents) {
Map<String, Object> 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);
}
}
}

@ -11,6 +11,7 @@ import com.unilife.mapper.PostMapper;
import com.unilife.mapper.UserMapper; import com.unilife.mapper.UserMapper;
import com.unilife.model.dto.CreatePostDTO; import com.unilife.model.dto.CreatePostDTO;
import com.unilife.model.dto.UpdatePostDTO; import com.unilife.model.dto.UpdatePostDTO;
import com.unilife.model.entity.Category;
import com.unilife.model.entity.Post; import com.unilife.model.entity.Post;
import com.unilife.model.entity.User; import com.unilife.model.entity.User;
import com.unilife.model.vo.PostListVO; import com.unilife.model.vo.PostListVO;
@ -91,8 +92,15 @@ public class PostServiceImpl implements PostService {
User user = userMapper.getUserById(post.getUserId()); 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() PostVO postVO = PostVO.builder()
@ -100,14 +108,13 @@ public class PostServiceImpl implements PostService {
.title(post.getTitle()) .title(post.getTitle())
.content(post.getContent()) .content(post.getContent())
.userId(post.getUserId()) .userId(post.getUserId())
.nickname(user != null ? user.getNickname() : "未知用户")
.avatar(user != null ? user.getAvatar() : null) .avatar(user != null ? user.getAvatar() : null)
.categoryId(post.getCategoryId()) .categoryId(post.getCategoryId())
.categoryName("未知分类") // 实际开发中应该从category对象获取 .categoryName(categoryName) // 使用从数据库查询到的真实分类名称
.viewCount(post.getViewCount() + 1) // 已经增加了浏览次数 .viewCount(post.getViewCount() + 1) // 已经增加了浏览次数
.likeCount(post.getLikeCount()) .likeCount(post.getLikeCount())
.commentCount(post.getCommentCount()) .commentCount(post.getCommentCount())
.isLiked(false) // 实际开发中应该查询用户是否点赞 .isLiked(isLiked) // 设置已查询的点赞状态
.createdAt(post.getCreatedAt()) .createdAt(post.getCreatedAt())
.updatedAt(post.getUpdatedAt()) .updatedAt(post.getUpdatedAt())
.build(); .build();
@ -116,24 +123,62 @@ public class PostServiceImpl implements PostService {
} }
@Override @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 (page == null || page < 1) page = 1;
if (size == null || size < 1 || size > 50) size = 10; if (size == null || size < 1 || size > 50) size = 10;
if (StrUtil.isBlank(sort)) sort = "latest"; if (StrUtil.isBlank(sort)) sort = "latest";
// 添加调试日志
log.info("getPostList - 接收到的参数: categoryId={}, keyword={}, page={}, size={}, sort={}, userId={}",
categoryId, keyword, page, size, sort, userId);
// 只使用PageHelper进行分页不设置排序 // 只使用PageHelper进行分页不设置排序
PageHelper.startPage(page, size); PageHelper.startPage(page, size);
// 调用mapper方法传入排序参数 // 根据是否有关键词选择不同的查询方法
List<Post> posts = postMapper.getListByCategory(categoryId, sort); List<Post> posts;
if (StrUtil.isNotBlank(keyword)) {
// 有关键词,使用搜索方法
posts = postMapper.searchPosts(keyword, categoryId, sort);
} else {
// 无关键词,使用普通列表查询
posts = postMapper.getListByCategory(categoryId, sort);
}
// 获取分页信息 // 获取分页信息
PageInfo<Post> pageInfo = new PageInfo<>(posts); PageInfo<Post> pageInfo = new PageInfo<>(posts);
// 收集所有帖子的分类 ID
List<Long> categoryIds = posts.stream()
.map(Post::getCategoryId)
.distinct()
.collect(Collectors.toList());
// 批量获取分类信息
Map<Long, String> categoryMap = new HashMap<>();
if (!categoryIds.isEmpty()) {
categoryIds.forEach(id -> {
Category category = categoryMapper.getById(id);
categoryMap.put(id, category != null ? category.getName() : "未知分类");
});
}
// 转换为VO // 转换为VO
List<PostListVO> postListVOs = posts.stream().map(post -> { List<PostListVO> postListVOs = posts.stream().map(post -> {
User user = userMapper.getUserById(post.getUserId()); 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() return PostListVO.builder()
.id(post.getId()) .id(post.getId())
.title(post.getTitle()) .title(post.getTitle())
@ -142,14 +187,19 @@ public class PostServiceImpl implements PostService {
.nickname(user != null ? user.getNickname() : "未知用户") .nickname(user != null ? user.getNickname() : "未知用户")
.avatar(user != null ? user.getAvatar() : null) .avatar(user != null ? user.getAvatar() : null)
.categoryId(post.getCategoryId()) .categoryId(post.getCategoryId())
.categoryName("未知分类") .categoryName(categoryMap.getOrDefault(post.getCategoryId(), "未知分类"))
.viewCount(post.getViewCount()) .viewCount(post.getViewCount())
.likeCount(post.getLikeCount()) .likeCount(post.getLikeCount())
.commentCount(post.getCommentCount()) .commentCount(post.getCommentCount())
.status(post.getStatus())
.isLiked(isLiked)
.createdAt(post.getCreatedAt()) .createdAt(post.getCreatedAt())
.build(); .build();
}).collect(Collectors.toList()); }).collect(Collectors.toList());
log.info("返回的帖子列表中点赞状态: {}",
postListVOs.stream().map(p -> "帖子" + p.getId() + ":isLiked=" + p.getIsLiked()).collect(Collectors.toList()));
// 返回结果 // 返回结果
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("total", pageInfo.getTotal()); data.put("total", pageInfo.getTotal());
@ -232,6 +282,86 @@ public class PostServiceImpl implements PostService {
return Result.success(null, "点赞成功"); 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<Post> posts = postMapper.getListByUserId(userId, sort);
PageInfo<Post> pageInfo = new PageInfo<>(posts);
// 获取分类信息
List<Long> categoryIds = posts.stream()
.map(Post::getCategoryId)
.distinct()
.collect(Collectors.toList());
// 获取分类名称映射
Map<Long, String> categoryMap;
if (!categoryIds.isEmpty()) {
// 获取所有分类,然后过滤出需要的分类
List<Category> allCategories = categoryMapper.getList(null);
// 过滤出匹配的分类
List<Category> 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<PostListVO> 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<String, Object> data = new HashMap<>();
data.put("total", pageInfo.getTotal());
data.put("pages", pageInfo.getPages());
data.put("list", postVOs);
return Result.success(data);
}
/** /**
* *

@ -5,6 +5,7 @@ import com.github.pagehelper.PageInfo;
import com.unilife.common.result.Result; import com.unilife.common.result.Result;
import com.unilife.mapper.CategoryMapper; import com.unilife.mapper.CategoryMapper;
import com.unilife.mapper.ResourceMapper; import com.unilife.mapper.ResourceMapper;
import com.unilife.mapper.ResourceLikeMapper;
import com.unilife.mapper.UserMapper; import com.unilife.mapper.UserMapper;
import com.unilife.model.dto.CreateResourceDTO; import com.unilife.model.dto.CreateResourceDTO;
import com.unilife.model.entity.Category; 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.entity.User;
import com.unilife.model.vo.ResourceVO; import com.unilife.model.vo.ResourceVO;
import com.unilife.service.ResourceService; import com.unilife.service.ResourceService;
import com.unilife.utils.OssService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException; import java.util.*;
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.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class ResourceServiceImpl implements ResourceService { public class ResourceServiceImpl implements ResourceService {
@ -43,8 +39,21 @@ public class ResourceServiceImpl implements ResourceService {
@Autowired @Autowired
private CategoryMapper categoryMapper; private CategoryMapper categoryMapper;
@Autowired
private OssService ossService;
@Autowired
private ResourceLikeMapper resourceLikeMapper;
@Autowired
private PdfVectorAsyncService pdfVectorAsyncService;
// 文件存储路径实际项目中应该配置在application.yml中 // 文件存储路径实际项目中应该配置在application.yml中
private static final String UPLOAD_DIR = "uploads/resources/"; private static final String UPLOAD_DIR = "uploads/resources/";
// OSS存储目录
private static final String OSS_DIR = "resources";
@Override @Override
@Transactional @Transactional
@ -67,28 +76,15 @@ public class ResourceServiceImpl implements ResourceService {
} }
try { try {
// 创建上传目录(如果不存在) // 将文件上传到阿里云OSS
File uploadDir = new File(UPLOAD_DIR); String fileUrl = ossService.uploadFile(file, OSS_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());
// 创建资源记录 // 创建资源记录
Resource resource = new Resource(); Resource resource = new Resource();
resource.setUserId(userId); resource.setUserId(userId);
resource.setTitle(createResourceDTO.getTitle()); resource.setTitle(createResourceDTO.getTitle());
resource.setDescription(createResourceDTO.getDescription()); resource.setDescription(createResourceDTO.getDescription());
resource.setFileUrl(filePath); resource.setFileUrl(fileUrl); // 存储OSS文件URL
resource.setFileSize(file.getSize()); resource.setFileSize(file.getSize());
resource.setFileType(file.getContentType()); resource.setFileType(file.getContentType());
resource.setCategoryId(createResourceDTO.getCategoryId()); resource.setCategoryId(createResourceDTO.getCategoryId());
@ -99,11 +95,23 @@ public class ResourceServiceImpl implements ResourceService {
// 保存资源记录 // 保存资源记录
resourceMapper.insert(resource); 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<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("resourceId", resource.getId()); data.put("resourceId", resource.getId());
return Result.success(data, "资源上传成功"); return Result.success(data, "资源上传成功");
} catch (IOException e) { } catch (Exception e) {
log.error("文件上传失败", e); log.error("文件上传失败", e);
return Result.error(500, "文件上传失败"); return Result.error(500, "文件上传失败");
} }
@ -128,7 +136,7 @@ public class ResourceServiceImpl implements ResourceService {
.id(resource.getId()) .id(resource.getId())
.title(resource.getTitle()) .title(resource.getTitle())
.description(resource.getDescription()) .description(resource.getDescription())
.fileUrl(resource.getFileUrl()) .fileUrl(resource.getFileUrl()) // 直接返回OSS URL
.fileSize(resource.getFileSize()) .fileSize(resource.getFileSize())
.fileType(resource.getFileType()) .fileType(resource.getFileType())
.userId(resource.getUserId()) .userId(resource.getUserId())
@ -138,7 +146,7 @@ public class ResourceServiceImpl implements ResourceService {
.categoryName(category != null ? category.getName() : "未知分类") .categoryName(category != null ? category.getName() : "未知分类")
.downloadCount(resource.getDownloadCount()) .downloadCount(resource.getDownloadCount())
.likeCount(resource.getLikeCount()) .likeCount(resource.getLikeCount())
.isLiked(false) // 实际项目中应该查询用户是否点赞 .isLiked(userId != null ? resourceLikeMapper.isLiked(resourceId, userId) : false) // 查询用户是否点赞
.createdAt(resource.getCreatedAt()) .createdAt(resource.getCreatedAt())
.updatedAt(resource.getUpdatedAt()) .updatedAt(resource.getUpdatedAt())
.build(); .build();
@ -147,14 +155,14 @@ public class ResourceServiceImpl implements ResourceService {
} }
@Override @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 (page == null || page < 1) page = 1;
if (size == null || size < 1 || size > 50) size = 10; if (size == null || size < 1 || size > 50) size = 10;
// 分页查询 // 分页查询
PageHelper.startPage(page, size); PageHelper.startPage(page, size);
List<Resource> resources = resourceMapper.getList(categoryId, userId, keyword); List<Resource> resources = resourceMapper.getList(categoryId, uploaderUserId, keyword);
PageInfo<Resource> pageInfo = new PageInfo<>(resources); PageInfo<Resource> pageInfo = new PageInfo<>(resources);
// 转换为VO // 转换为VO
@ -177,7 +185,7 @@ public class ResourceServiceImpl implements ResourceService {
.categoryName(category != null ? category.getName() : "未知分类") .categoryName(category != null ? category.getName() : "未知分类")
.downloadCount(resource.getDownloadCount()) .downloadCount(resource.getDownloadCount())
.likeCount(resource.getLikeCount()) .likeCount(resource.getLikeCount())
.isLiked(false) // 实际项目中应该查询用户是否点赞 .isLiked(currentUserId != null ? resourceLikeMapper.isLiked(resource.getId(), currentUserId) : false) // 查询当前用户是否点赞
.createdAt(resource.getCreatedAt()) .createdAt(resource.getCreatedAt())
.updatedAt(resource.getUpdatedAt()) .updatedAt(resource.getUpdatedAt())
.build(); .build();
@ -239,7 +247,25 @@ public class ResourceServiceImpl implements ResourceService {
return Result.error(403, "无权限删除此资源"); 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); resourceMapper.delete(resourceId);
return Result.success(null, "删除成功"); return Result.success(null, "删除成功");
@ -257,15 +283,33 @@ public class ResourceServiceImpl implements ResourceService {
// 增加下载次数 // 增加下载次数
resourceMapper.incrementDownloadCount(resourceId); resourceMapper.incrementDownloadCount(resourceId);
// 返回文件URL // 处理文件URL生成临时访问链接
String fileUrl = resource.getFileUrl();
// 提取对象名称并生成临时访问URL有效期1小时
String objectName = ossService.getObjectNameFromUrl(fileUrl);
if (objectName != null) {
// 生成有效期为1小时的临时访问URL
fileUrl = ossService.generatePresignedUrl(objectName, 3600 * 1000);
}
// 返回文件URL和文件名
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("fileUrl", resource.getFileUrl()); data.put("fileUrl", fileUrl);
data.put("fileName", resource.getTitle()); data.put("fileName", resource.getTitle() + getFileExtension(fileUrl));
data.put("fileType", resource.getFileType()); data.put("fileType", resource.getFileType());
return Result.success(data, "获取下载链接成功"); return Result.success(data, "获取下载链接成功");
} }
// 获取文件扩展名
private String getFileExtension(String filePath) {
int lastDot = filePath.lastIndexOf(".");
if (lastDot > 0) {
return filePath.substring(lastDot);
}
return "";
}
@Override @Override
@Transactional @Transactional
public Result likeResource(Long resourceId, Long userId) { public Result likeResource(Long resourceId, Long userId) {
@ -276,17 +320,16 @@ public class ResourceServiceImpl implements ResourceService {
} }
// 检查用户是否已点赞 // 检查用户是否已点赞
// 注意这里需要创建一个资源点赞表和相应的Mapper实际开发中需要先创建 boolean isLiked = resourceLikeMapper.isLiked(resourceId, userId);
boolean isLiked = false; // resourceLikeMapper.isLiked(resourceId, userId);
if (isLiked) { if (isLiked) {
// 取消点赞 // 取消点赞
// resourceLikeMapper.delete(resourceId, userId); resourceLikeMapper.delete(resourceId, userId);
resourceMapper.decrementLikeCount(resourceId); resourceMapper.decrementLikeCount(resourceId);
return Result.success(null, "取消点赞成功"); return Result.success(null, "取消点赞成功");
} else { } else {
// 添加点赞 // 添加点赞
// resourceLikeMapper.insert(resourceId, userId); resourceLikeMapper.insert(resourceId, userId);
resourceMapper.incrementLikeCount(resourceId); resourceMapper.incrementLikeCount(resourceId);
return Result.success(null, "点赞成功"); return Result.success(null, "点赞成功");
} }
@ -336,4 +379,7 @@ public class ResourceServiceImpl implements ResourceService {
return Result.success(data); return Result.success(data);
} }
} }

@ -5,6 +5,9 @@ import cn.hutool.core.util.RandomUtil;
import com.unilife.common.constant.RedisConstant; import com.unilife.common.constant.RedisConstant;
import com.unilife.common.result.Result; import com.unilife.common.result.Result;
import com.unilife.mapper.UserMapper; 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.LoginDTO;
import com.unilife.model.dto.LoginEmailDTO; import com.unilife.model.dto.LoginEmailDTO;
import com.unilife.model.dto.RegisterDTO; 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.UpdatePasswordDTO;
import com.unilife.model.dto.UpdateProfileDTO; import com.unilife.model.dto.UpdateProfileDTO;
import com.unilife.model.entity.User; import com.unilife.model.entity.User;
import com.unilife.model.entity.Post;
import com.unilife.model.vo.LoginVO; import com.unilife.model.vo.LoginVO;
import com.unilife.model.vo.RegisterVO;
import com.unilife.service.IPLocationService; import com.unilife.service.IPLocationService;
import com.unilife.service.UserService; import com.unilife.service.UserService;
import com.unilife.utils.JwtUtil; import com.unilife.utils.JwtUtil;
@ -21,7 +24,6 @@ import com.unilife.utils.RegexUtils;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -33,12 +35,11 @@ import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static com.unilife.common.constant.RedisConstant.LOGIN_EMAIL_KEY; import static com.unilife.common.constant.RedisConstant.LOGIN_EMAIL_KEY;
@ -54,6 +55,15 @@ public class UserServiceImpl implements UserService {
@Autowired @Autowired
private UserMapper userMapper; private UserMapper userMapper;
@Autowired
private PostMapper postMapper;
@Autowired
private CommentMapper commentMapper;
@Autowired
private PostLikeMapper postLikeMapper;
@Autowired @Autowired
private JavaMailSender mailSender; 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); String username = StringUtils.isNotEmpty(registerDTO.getUsername()) ? registerDTO.getUsername() : email.substring(0, Math.min(email.indexOf('@'), 10)) + "_" + RandomUtil.randomString(4);
user.setUsername(username); user.setUsername(username);
// 设置学生信息
user.setStudentId(registerDTO.getStudentId());
user.setDepartment(registerDTO.getDepartment());
user.setMajor(registerDTO.getMajor());
user.setGrade(registerDTO.getGrade());
// 设置其他默认值 // 设置其他默认值
user.setRole((byte) 0); // 普通用户 user.setRole((byte) 0); // 普通用户
user.setStatus((byte) 1); // 启用状态 user.setStatus((byte) 1); // 启用状态
@ -150,11 +166,22 @@ public class UserServiceImpl implements UserService {
@Override @Override
public Result login(LoginDTO loginDTO,HttpServletRequest request) { public Result login(LoginDTO loginDTO,HttpServletRequest request) {
if(loginDTO==null|| StringUtils.isEmpty(loginDTO.getEmail())||StringUtils.isEmpty(loginDTO.getPassword())){ if(loginDTO==null|| StringUtils.isEmpty(loginDTO.getAccount())||StringUtils.isEmpty(loginDTO.getPassword())){
return Result.error(400,"邮箱或密码不能为空"); return Result.error(400,"账号或密码不能为空");
}
String account = loginDTO.getAccount();
User user = null;
// 判断输入的是邮箱还是用户名
if (RegexUtils.isEmailInvalid(account)) {
// 不是邮箱格式,按用户名查询
user = userMapper.findByUsername(account);
} else {
// 是邮箱格式,按邮箱查询
user = userMapper.findByEmail(account);
} }
User user = userMapper.findByEmail(loginDTO.getEmail());
if (user == null) { if (user == null) {
return Result.error(400, "账号或密码错误"); return Result.error(400, "账号或密码错误");
} }
@ -167,7 +194,6 @@ public class UserServiceImpl implements UserService {
return Result.error(403, "账号已被禁用,请联系管理员"); return Result.error(403, "账号已被禁用,请联系管理员");
} }
String LastLogIpLocation = user.getLoginIp(); String LastLogIpLocation = user.getLoginIp();
String currentIp = ipLocationService.getClientIP(request); String currentIp = ipLocationService.getClientIP(request);
String ipLocation = ipLocationService.getIPLocation(currentIp); String ipLocation = ipLocationService.getIPLocation(currentIp);
@ -179,7 +205,6 @@ public class UserServiceImpl implements UserService {
BeanUtil.copyProperties(user,loginVO); BeanUtil.copyProperties(user,loginVO);
String message = StringUtils.isEmpty(LastLogIpLocation) ? "首次登录" : "上次登录IP归属地为" + LastLogIpLocation; String message = StringUtils.isEmpty(LastLogIpLocation) ? "首次登录" : "上次登录IP归属地为" + LastLogIpLocation;
return Result.success(loginVO, message); return Result.success(loginVO, message);
} }
@Override @Override
@ -339,24 +364,49 @@ public class UserServiceImpl implements UserService {
@Override @Override
public Result updateUserProfile(Long userId, UpdateProfileDTO profileDTO) { public Result updateUserProfile(Long userId, UpdateProfileDTO profileDTO) {
// 检查用户是否存在 User currentUser = userMapper.getUserById(userId); // Changed from findById to getUserById based on UserMapper.java
User user = userMapper.getUserById(userId); if (currentUser == null) {
if (user == null) {
return Result.error(404, "用户不存在"); return Result.error(404, "用户不存在");
} }
// 更新用户信息 // 检查用户名是否更改以及是否重复
user.setNickname(profileDTO.getNickname()); if (StringUtils.isNotEmpty(profileDTO.getUsername()) && !profileDTO.getUsername().equals(currentUser.getUsername())) {
user.setBio(profileDTO.getBio()); User existingUserWithNewUsername = userMapper.findByUsername(profileDTO.getUsername());
user.setGender(profileDTO.getGender()); if (existingUserWithNewUsername != null) {
user.setDepartment(profileDTO.getDepartment()); return Result.error(409, "用户名已被占用,请选择其他用户名"); // 409 Conflict
user.setMajor(profileDTO.getMajor()); }
user.setGrade(profileDTO.getGrade()); 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 @Override
@ -466,4 +516,121 @@ public class UserServiceImpl implements UserService {
return Result.success(null, "邮箱更新成功"); 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<Post> 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<String, Object> 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<Post> recentPosts = postMapper.getListByUserId(userId, "time");
// 如果指定了限制数量,则截取
if (limit != null && limit > 0 && recentPosts.size() > limit) {
recentPosts = recentPosts.subList(0, limit);
}
Map<String, Object> 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());
}
}
} }

@ -1,6 +1,7 @@
package com.unilife.utils; package com.unilife.utils;
import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateTime;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil; import cn.hutool.jwt.JWTUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -24,24 +25,45 @@ public class JwtUtil {
Map<String, Object> payload = new HashMap<>(); Map<String, Object> payload = new HashMap<>();
payload.put("userId", id); payload.put("userId", id);
payload.put("created",now.getTime()); payload.put("created", now.getTime());
return JWTUtil.createToken(payload,secret.getBytes()); payload.put("exp", expireTime.getTime() / 1000); // JWT标准过期时间字段
return JWTUtil.createToken(payload, secret.getBytes());
} }
public boolean verifyToken(String token) { public boolean verifyToken(String token) {
try{ try {
JWTUtil.verify(token,secret.getBytes()); // 验证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; return true;
}catch (Exception e){ } catch (Exception e) {
log.debug("Token验证失败: {}", e.getMessage());
return false; return false;
} }
} }
public Long getUserIdFromToken(String token) { public Long getUserIdFromToken(String token) {
try { try {
// 先验证token是否有效
if (!verifyToken(token)) {
return null;
}
return Long.valueOf(JWTUtil.parseToken(token).getPayload("userId").toString()); return Long.valueOf(JWTUtil.parseToken(token).getPayload("userId").toString());
}catch (Exception e){ } catch (Exception e) {
log.debug("从Token获取用户ID失败: {}", e.getMessage());
return null; return null;
} }
} }
} }

@ -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;
}
/**
* 访URLbucket
* @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;
}
}

@ -1,6 +1,38 @@
server: server:
port: 8087 port: 8087
# 超时配置
tomcat:
connection-timeout: 60000 # 连接超时60秒
threads:
max: 100 # 最大线程数
spring: 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: datasource:
url: jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 url: jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root username: root
@ -9,8 +41,8 @@ spring:
mail: mail:
host: smtp.163.com host: smtp.163.com
port: 465 port: 465
username: c2991692032@163.com username: ${MAIL_USERNAME:}
password: VPq5u3NcAAqtG9GT password: ${MAIL_PASSWORD:}
properties: properties:
mail: mail:
smtp: smtp:
@ -20,10 +52,26 @@ spring:
socketFactory: socketFactory:
port: 465 port: 465
class: javax.net.ssl.SSLSocketFactory class: javax.net.ssl.SSLSocketFactory
# Redis基本配置用于缓存等功能
data: data:
redis: redis:
port: 6379 port: 6379
host: 127.0.0.1 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: knife4j:
enable: true enable: true
openapi: openapi:
@ -49,6 +97,25 @@ mybatis:
logging: logging:
level: level:
com.unilife: debug com.unilife: debug
org.springframework.ai: debug
jwt: jwt:
secret: qwertyuiopasdfghjklzxcvbnm secret: qwertyuiopasdfghjklzxcvbnm
expiration: 86400 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生成)

@ -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.8ISO400后期用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;

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unilife.mapper.AiChatSessionMapper">
<!-- 结果映射 -->
<resultMap id="AiChatSessionResultMap" type="com.unilife.model.entity.AiChatSession">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="title" property="title"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<!-- 插入会话 -->
<insert id="insert" parameterType="com.unilife.model.entity.AiChatSession">
INSERT INTO ai_chat_sessions (
id, user_id, title, created_at, updated_at
) VALUES (
#{id}, #{userId}, #{title}, #{createdAt}, #{updatedAt}
)
</insert>
<!-- 根据ID查询会话 -->
<select id="selectById" resultMap="AiChatSessionResultMap">
SELECT id, user_id, title, created_at, updated_at
FROM ai_chat_sessions
WHERE id = #{id}
</select>
<!-- 根据用户ID分页查询会话列表 -->
<select id="selectByUserId" resultMap="AiChatSessionResultMap">
SELECT id, user_id, title, created_at, updated_at
FROM ai_chat_sessions
WHERE user_id = #{userId}
ORDER BY updated_at DESC
LIMIT #{offset}, #{limit}
</select>
<!-- 查询匿名会话列表 -->
<select id="selectAnonymousSessions" resultMap="AiChatSessionResultMap">
SELECT id, user_id, title, created_at, updated_at
FROM ai_chat_sessions
WHERE user_id IS NULL
ORDER BY updated_at DESC
LIMIT #{offset}, #{limit}
</select>
<!-- 统计用户会话总数 -->
<select id="countByUserId" resultType="long">
SELECT COUNT(*)
FROM ai_chat_sessions
WHERE user_id = #{userId}
</select>
<!-- 统计匿名会话总数 -->
<select id="countAnonymousSessions" resultType="long">
SELECT COUNT(*)
FROM ai_chat_sessions
WHERE user_id IS NULL
</select>
<!-- 更新会话标题 -->
<update id="updateTitle">
UPDATE ai_chat_sessions
SET title = #{title}, updated_at = CURRENT_TIMESTAMP
WHERE id = #{id}
</update>
<!-- 更新会话的最后消息时间和消息数量(简化方案中保留方法但不使用) -->
<update id="updateMessageInfo">
UPDATE ai_chat_sessions
SET updated_at = #{lastMessageTime}
WHERE id = #{id}
</update>
<!-- 删除会话 -->
<delete id="deleteById">
DELETE FROM ai_chat_sessions WHERE id = #{id}
</delete>
<!-- 批量删除过期会话 -->
<delete id="deleteExpiredSessions">
DELETE FROM ai_chat_sessions
WHERE updated_at &lt; #{expireTime}
</delete>
</mapper>

@ -64,4 +64,50 @@
</if> </if>
</where> </where>
</select> </select>
<!-- ========== 管理员后台相关方法 ========== -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*)
FROM categories
WHERE status != 0
</select>
<select id="getAllCategories" resultMap="categoryResultMap">
SELECT id, name, description, icon, sort, status, created_at, updated_at
FROM categories
ORDER BY sort ASC, id ASC
</select>
<select id="getCategoryById" resultMap="categoryResultMap">
SELECT id, name, description, icon, sort, status, created_at, updated_at
FROM categories
WHERE id = #{id}
</select>
<insert id="insertCategory" parameterType="com.unilife.model.entity.Category" useGeneratedKeys="true" keyProperty="id">
INSERT INTO categories (
name, description, icon, sort, status, created_at, updated_at
) VALUES (
#{name}, #{description}, #{icon}, #{sort}, #{status}, NOW(), NOW()
)
</insert>
<update id="updateCategory" parameterType="com.unilife.model.entity.Category">
UPDATE categories
SET name = #{name},
description = #{description},
icon = #{icon},
sort = #{sort},
status = #{status},
updated_at = NOW()
WHERE id = #{id}
</update>
<update id="deleteCategory">
UPDATE categories
SET status = 0,
updated_at = NOW()
WHERE id = #{categoryId}
</update>
</mapper> </mapper>

@ -72,4 +72,65 @@
SET like_count = GREATEST(like_count - 1, 0) SET like_count = GREATEST(like_count - 1, 0)
WHERE id = #{id} WHERE id = #{id}
</update> </update>
<!-- ========== 管理员后台相关方法 ========== -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*)
FROM comments
WHERE status != 0
</select>
<select id="getNewCommentCountToday" resultType="int">
SELECT COUNT(*)
FROM comments
WHERE status != 0 AND DATE(created_at) = CURDATE()
</select>
<select id="getCommentById" resultMap="commentResultMap">
SELECT id, post_id, user_id, content, parent_id, like_count, status, created_at, updated_at
FROM comments
WHERE id = #{id}
</select>
<select id="getAdminCommentList" resultMap="commentResultMap">
SELECT c.id, c.post_id, c.user_id, c.content, c.parent_id, c.like_count, c.status, c.created_at, c.updated_at
FROM comments c
<where>
<if test="keyword != null and keyword != ''">
AND c.content LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="postId != null">
AND c.post_id = #{postId}
</if>
<if test="status != null">
AND c.status = #{status}
</if>
</where>
ORDER BY c.created_at DESC
LIMIT #{offset}, #{size}
</select>
<select id="getAdminCommentCount" resultType="int">
SELECT COUNT(*)
FROM comments c
<where>
<if test="keyword != null and keyword != ''">
AND c.content LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="postId != null">
AND c.post_id = #{postId}
</if>
<if test="status != null">
AND c.status = #{status}
</if>
</where>
</select>
<update id="deleteComment">
UPDATE comments
SET status = 0,
updated_at = NOW()
WHERE id = #{commentId}
</update>
</mapper> </mapper>

@ -12,6 +12,7 @@
<result column="end_time" property="endTime"/> <result column="end_time" property="endTime"/>
<result column="start_week" property="startWeek"/> <result column="start_week" property="startWeek"/>
<result column="end_week" property="endWeek"/> <result column="end_week" property="endWeek"/>
<result column="semester" property="semester"/>
<result column="color" property="color"/> <result column="color" property="color"/>
<result column="status" property="status"/> <result column="status" property="status"/>
<result column="created_at" property="createdAt"/> <result column="created_at" property="createdAt"/>
@ -21,23 +22,23 @@
<insert id="insert" parameterType="com.unilife.model.entity.Course" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" parameterType="com.unilife.model.entity.Course" useGeneratedKeys="true" keyProperty="id">
INSERT INTO courses ( INSERT INTO courses (
user_id, name, teacher, location, day_of_week, start_time, end_time, 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 ( ) VALUES (
#{userId}, #{name}, #{teacher}, #{location}, #{dayOfWeek}, #{startTime}, #{endTime}, #{userId}, #{name}, #{teacher}, #{location}, #{dayOfWeek}, #{startTime}, #{endTime},
#{startWeek}, #{endWeek}, #{color}, #{status}, NOW(), NOW() #{startWeek}, #{endWeek}, #{semester}, #{color}, #{status}, NOW(), NOW()
) )
</insert> </insert>
<select id="getById" resultMap="courseResultMap"> <select id="getById" resultMap="courseResultMap">
SELECT id, user_id, name, teacher, location, day_of_week, start_time, end_time, 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 FROM courses
WHERE id = #{id} AND status != 0 WHERE id = #{id} AND status != 0
</select> </select>
<select id="getListByUserId" resultMap="courseResultMap"> <select id="getListByUserId" resultMap="courseResultMap">
SELECT id, user_id, name, teacher, location, day_of_week, start_time, end_time, 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 FROM courses
WHERE user_id = #{userId} AND status != 0 WHERE user_id = #{userId} AND status != 0
ORDER BY day_of_week ASC, start_time ASC ORDER BY day_of_week ASC, start_time ASC
@ -53,6 +54,7 @@
end_time = #{endTime}, end_time = #{endTime},
start_week = #{startWeek}, start_week = #{startWeek},
end_week = #{endWeek}, end_week = #{endWeek},
semester = #{semester},
color = #{color}, color = #{color},
updated_at = NOW() updated_at = NOW()
WHERE id = #{id} AND user_id = #{userId} WHERE id = #{id} AND user_id = #{userId}
@ -67,12 +69,20 @@
<select id="getListByUserIdAndDayOfWeek" resultMap="courseResultMap"> <select id="getListByUserIdAndDayOfWeek" resultMap="courseResultMap">
SELECT id, user_id, name, teacher, location, day_of_week, start_time, end_time, 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 FROM courses
WHERE user_id = #{userId} AND day_of_week = #{dayOfWeek} AND status != 0 WHERE user_id = #{userId} AND day_of_week = #{dayOfWeek} AND status != 0
ORDER BY start_time ASC ORDER BY start_time ASC
</select> </select>
<select id="getListByUserIdAndSemester" resultMap="courseResultMap">
SELECT id, user_id, name, teacher, location, day_of_week, start_time, end_time,
start_week, end_week, semester, color, status, created_at, updated_at
FROM courses
WHERE user_id = #{userId} AND semester = #{semester} AND status != 0
ORDER BY day_of_week ASC, start_time ASC
</select>
<select id="checkConflict" resultType="java.lang.Integer"> <select id="checkConflict" resultType="java.lang.Integer">
SELECT COUNT(*) SELECT COUNT(*)
FROM courses FROM courses

@ -32,22 +32,23 @@
<select id="getListByCategory" resultType="com.unilife.model.entity.Post"> <select id="getListByCategory" resultType="com.unilife.model.entity.Post">
SELECT * FROM posts SELECT * FROM posts
<where> <where>
status != 0 <!-- 始终只获取未删除的帖子 -->
<if test="categoryId != null"> <if test="categoryId != null">
category_id = #{categoryId} AND category_id = #{categoryId}
</if> </if>
</where> </where>
<choose> <choose>
<when test="sort == 'hot'"> <when test="sort == 'hot'">
ORDER BY view_count DESC ORDER BY status DESC, view_count DESC
</when> </when>
<when test="sort == 'likes'"> <when test="sort == 'likes'">
ORDER BY like_count DESC ORDER BY status DESC, like_count DESC
</when> </when>
<when test="sort == 'comments'"> <when test="sort == 'comments'">
ORDER BY comment_count DESC ORDER BY status DESC, comment_count DESC
</when> </when>
<otherwise> <otherwise>
ORDER BY created_at DESC ORDER BY status DESC, created_at DESC
</otherwise> </otherwise>
</choose> </choose>
</select> </select>
@ -107,4 +108,130 @@
SET comment_count = GREATEST(comment_count - 1, 0) SET comment_count = GREATEST(comment_count - 1, 0)
WHERE id = #{id} WHERE id = #{id}
</update> </update>
<select id="getListByUserId" resultType="com.unilife.model.entity.Post">
SELECT * FROM posts
WHERE user_id = #{userId} AND status != 0
<choose>
<when test="sort == 'hot'">
ORDER BY status DESC, view_count DESC
</when>
<when test="sort == 'likes'">
ORDER BY status DESC, like_count DESC
</when>
<when test="sort == 'comments'">
ORDER BY status DESC, comment_count DESC
</when>
<otherwise>
ORDER BY status DESC, created_at DESC
</otherwise>
</choose>
</select>
<select id="getCountByUserId" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM posts
WHERE user_id = #{userId} AND status != 0
</select>
<select id="searchPosts" resultType="com.unilife.model.entity.Post">
SELECT * FROM posts
WHERE status != 0
<if test="keyword != null and keyword != ''">
AND (title LIKE CONCAT('%', #{keyword}, '%') OR content LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<choose>
<when test="sortBy == 'time'">
ORDER BY status DESC, created_at DESC
</when>
<when test="sortBy == 'popularity'">
ORDER BY status DESC, like_count DESC, view_count DESC
</when>
<otherwise>
ORDER BY status DESC, created_at DESC
</otherwise>
</choose>
</select>
<!-- ========== 管理员后台相关方法 ========== -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*)
FROM posts
WHERE status != 0
</select>
<select id="getNewPostCountToday" resultType="int">
SELECT COUNT(*)
FROM posts
WHERE status != 0 AND DATE(created_at) = CURDATE()
</select>
<select id="getPostById" resultMap="postResultMap">
SELECT id, user_id, title, content, category_id, view_count, like_count, comment_count, status, created_at, updated_at
FROM posts
WHERE id = #{id}
</select>
<select id="getAdminPostList" resultMap="postResultMap">
SELECT p.id, p.user_id, p.title, p.content, p.category_id, p.view_count, p.like_count, p.comment_count, p.status, p.created_at, p.updated_at
FROM posts p
<where>
<if test="keyword != null and keyword != ''">
AND (p.title LIKE CONCAT('%', #{keyword}, '%') OR p.content LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND p.category_id = #{categoryId}
</if>
<if test="status != null">
AND p.status = #{status}
</if>
</where>
ORDER BY p.created_at DESC
LIMIT #{offset}, #{size}
</select>
<select id="getAdminPostCount" resultType="int">
SELECT COUNT(*)
FROM posts p
<where>
<if test="keyword != null and keyword != ''">
AND (p.title LIKE CONCAT('%', #{keyword}, '%') OR p.content LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND p.category_id = #{categoryId}
</if>
<if test="status != null">
AND p.status = #{status}
</if>
</where>
</select>
<update id="updatePostStatus">
UPDATE posts
SET status = #{status},
updated_at = NOW()
WHERE id = #{postId}
</update>
<update id="deletePost">
UPDATE posts
SET status = 0,
updated_at = NOW()
WHERE id = #{postId}
</update>
<delete id="permanentDeletePost">
DELETE FROM posts
WHERE id = #{postId}
</delete>
<select id="getCountByCategoryId" resultType="int">
SELECT COUNT(*)
FROM posts
WHERE category_id = #{categoryId} AND status != 0
</select>
</mapper> </mapper>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.unilife.mapper.ResourceLikeMapper">
<select id="isLiked" resultType="boolean">
SELECT COUNT(*) > 0
FROM resource_likes
WHERE resource_id = #{resourceId} AND user_id = #{userId}
</select>
<insert id="insert">
INSERT INTO resource_likes (resource_id, user_id, created_at)
VALUES (#{resourceId}, #{userId}, NOW())
</insert>
<delete id="delete">
DELETE FROM resource_likes
WHERE resource_id = #{resourceId} AND user_id = #{userId}
</delete>
<select id="getLikeCount" resultType="int">
SELECT COUNT(*)
FROM resource_likes
WHERE resource_id = #{resourceId}
</select>
</mapper>

@ -116,4 +116,67 @@
FROM resources FROM resources
WHERE category_id = #{categoryId} AND status != 0 WHERE category_id = #{categoryId} AND status != 0
</select> </select>
<!-- ========== 管理员后台相关方法 ========== -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*)
FROM resources
WHERE status != 0
</select>
<select id="getNewResourceCountToday" resultType="int">
SELECT COUNT(*)
FROM resources
WHERE status != 0 AND DATE(created_at) = CURDATE()
</select>
<select id="getResourceById" resultMap="resourceResultMap">
SELECT id, user_id, title, description, file_url, file_size, file_type, category_id,
download_count, like_count, status, created_at, updated_at
FROM resources
WHERE id = #{id}
</select>
<select id="getAdminResourceList" resultMap="resourceResultMap">
SELECT r.id, r.user_id, r.title, r.description, r.file_url, r.file_size, r.file_type, r.category_id,
r.download_count, r.like_count, r.status, r.created_at, r.updated_at
FROM resources r
<where>
<if test="keyword != null and keyword != ''">
AND (r.title LIKE CONCAT('%', #{keyword}, '%') OR r.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND r.category_id = #{categoryId}
</if>
<if test="status != null">
AND r.status = #{status}
</if>
</where>
ORDER BY r.created_at DESC
LIMIT #{offset}, #{size}
</select>
<select id="getAdminResourceCount" resultType="int">
SELECT COUNT(*)
FROM resources r
<where>
<if test="keyword != null and keyword != ''">
AND (r.title LIKE CONCAT('%', #{keyword}, '%') OR r.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND r.category_id = #{categoryId}
</if>
<if test="status != null">
AND r.status = #{status}
</if>
</where>
</select>
<update id="deleteResource">
UPDATE resources
SET status = 0,
updated_at = NOW()
WHERE id = #{resourceId}
</update>
</mapper> </mapper>

@ -43,6 +43,12 @@
WHERE email = #{email} WHERE email = #{email}
</select> </select>
<select id="findByUsername" resultMap="userResultMap">
SELECT id, email, password, username, nickname, avatar, role, is_verified, status, login_ip
FROM users
WHERE username = #{username}
</select>
<update id="updateLoginInfo"> <update id="updateLoginInfo">
UPDATE users UPDATE users
SET login_ip = #{ipLocation}, SET login_ip = #{ipLocation},
@ -107,13 +113,13 @@
<update id="updateUserProfile" parameterType="com.unilife.model.entity.User"> <update id="updateUserProfile" parameterType="com.unilife.model.entity.User">
UPDATE users UPDATE users
SET nickname = #{nickname}, SET username = #{username},
nickname = #{nickname},
bio = #{bio}, bio = #{bio},
gender = #{gender}, gender = #{gender},
department = #{department}, department = #{department},
major = #{major}, major = #{major},
grade = #{grade}, grade = #{grade}
updated_at = NOW()
WHERE id = #{id} WHERE id = #{id}
</update> </update>
@ -137,4 +143,152 @@
updated_at = NOW() updated_at = NOW()
WHERE id = #{id} WHERE id = #{id}
</update> </update>
<select id="searchUsers" resultType="com.unilife.model.entity.User">
SELECT id, username, email, nickname, avatar, bio, gender,
student_id as studentId, department, major, grade, points,
role, status, is_verified as isVerified,
created_at as createdAt, updated_at as updatedAt
FROM users
WHERE status != 0
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%')
OR nickname LIKE CONCAT('%', #{keyword}, '%')
OR department LIKE CONCAT('%', #{keyword}, '%')
OR major LIKE CONCAT('%', #{keyword}, '%'))
</if>
ORDER BY created_at DESC
</select>
<!-- 删除用户及其相关数据的SQL -->
<!-- 软删除用户 -->
<update id="deleteUser">
UPDATE users
SET status = 0,
updated_at = NOW()
WHERE id = #{id}
</update>
<!-- 软删除用户的所有帖子 -->
<update id="deleteUserPosts">
UPDATE posts
SET status = 0,
updated_at = NOW()
WHERE user_id = #{userId}
</update>
<!-- 软删除用户的所有评论 -->
<update id="deleteUserComments">
UPDATE comments
SET status = 0,
updated_at = NOW()
WHERE user_id = #{userId}
</update>
<!-- 软删除用户的所有资源 -->
<update id="deleteUserResources">
UPDATE resources
SET status = 0,
updated_at = NOW()
WHERE user_id = #{userId}
</update>
<!-- 删除用户的所有课程 -->
<delete id="deleteUserCourses">
DELETE FROM courses
WHERE user_id = #{userId}
</delete>
<!-- 删除用户的所有日程 -->
<delete id="deleteUserSchedules">
DELETE FROM schedules
WHERE user_id = #{userId}
</delete>
<!-- 删除用户的所有点赞记录 -->
<delete id="deleteUserLikes">
DELETE FROM post_likes WHERE user_id = #{userId};
DELETE FROM comment_likes WHERE user_id = #{userId};
DELETE FROM resource_likes WHERE user_id = #{userId};
</delete>
<!-- ========== 管理员后台相关方法 ========== -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*)
FROM users
</select>
<select id="getActiveUserCount" resultType="int">
SELECT COUNT(*)
FROM users
WHERE status = 1 AND login_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
</select>
<select id="getNewUserCountToday" resultType="int">
SELECT COUNT(*)
FROM users
WHERE status != 0 AND DATE(created_at) = CURDATE()
</select>
<select id="getAdminUserList" resultType="com.unilife.model.entity.User">
SELECT id, username, email, nickname, avatar, bio, gender,
student_id as studentId, department, major, grade, points,
role, status, is_verified as isVerified,
login_ip as loginIp, login_time as loginTime,
created_at as createdAt, updated_at as updatedAt
FROM users
<where>
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%')
OR nickname LIKE CONCAT('%', #{keyword}, '%')
OR email LIKE CONCAT('%', #{keyword}, '%')
OR department LIKE CONCAT('%', #{keyword}, '%')
OR major LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="role != null">
AND role = #{role}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY created_at DESC
LIMIT #{offset}, #{size}
</select>
<select id="getAdminUserCount" resultType="int">
SELECT COUNT(*)
FROM users
<where>
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%')
OR nickname LIKE CONCAT('%', #{keyword}, '%')
OR email LIKE CONCAT('%', #{keyword}, '%')
OR department LIKE CONCAT('%', #{keyword}, '%')
OR major LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="role != null">
AND role = #{role}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<update id="updateUserStatus">
UPDATE users
SET status = #{status},
updated_at = NOW()
WHERE id = #{userId}
</update>
<update id="updateUserRole">
UPDATE users
SET role = #{role},
updated_at = NOW()
WHERE id = #{userId}
</update>
</mapper> </mapper>

@ -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聊天会话元数据表';

@ -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<String, Object> testRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> 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;
}
}

@ -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));
}
}
}

@ -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("第一条消息检测测试通过");
}
}

@ -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<Post> 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);
}
}

@ -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<Resource> 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<Resource> 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);
}
}

@ -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<Schedule> 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<Schedule> 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<Schedule> 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));
}
}

@ -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> 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> passwordUtil = mockStatic(PasswordUtil.class);
MockedStatic<JwtUtil> 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> 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> 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> 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<User> 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> 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> 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());
}
}
}

@ -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;
}
}

@ -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
Loading…
Cancel
Save