feat: 实现情绪记录功能和聊天历史查看

- 完成情绪记录生成功能,支持AI分析聊天内容生成情绪记录
- 实现聊天页面历史记录查看,支持分页和搜索
- 修改日记页面展示情绪记录而非普通日记
- 添加情绪记录的增删改查API
- 优化前端UI,添加情绪强度显示和详细信息展示
- 修复SCSS变量缺失问题
This commit is contained in:
2025-07-25 01:11:01 +08:00
parent 3292a74698
commit 86c2df4784
25 changed files with 1397 additions and 2210 deletions
@@ -0,0 +1,121 @@
package com.emotion.controller;
import com.emotion.service.AIChatService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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 java.util.HashMap;
import java.util.Map;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* 情绪总结控制器测试
*
* @author emotion-museum
* @date 2025-07-25
*/
@Slf4j
@WebMvcTest(EmotionSummaryController.class)
public class EmotionSummaryControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AIChatService aiChatService;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testGenerateEmotionSummarySuccess() throws Exception {
// 准备测试数据
Map<String, Object> mockResult = new HashMap<>();
mockResult.put("success", true);
mockResult.put("message", "情绪记录总结生成成功");
Map<String, Object> emotionRecord = new HashMap<>();
emotionRecord.put("emotionType", "开心");
emotionRecord.put("intensity", 0.8);
emotionRecord.put("triggers", "与AI的愉快对话");
mockResult.put("emotionRecord", emotionRecord);
mockResult.put("summary", "用户今天表现出积极的情绪状态");
mockResult.put("messageCount", 10);
// 模拟服务调用
when(aiChatService.generateEmotionSummary(anyString())).thenReturn(mockResult);
// 执行测试
mockMvc.perform(post("/api/emotion-summary/generate/test_user_123")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.message").value("情绪记录总结生成成功"))
.andExpect(jsonPath("$.data.emotionRecord.emotionType").value("开心"))
.andExpect(jsonPath("$.data.emotionRecord.intensity").value(0.8))
.andExpect(jsonPath("$.data.summary").value("用户今天表现出积极的情绪状态"));
log.info("✅ 情绪记录总结生成成功测试通过");
}
@Test
public void testGenerateEmotionSummaryNoMessages() throws Exception {
// 准备测试数据 - 没有聊天记录的情况
Map<String, Object> mockResult = new HashMap<>();
mockResult.put("success", false);
mockResult.put("message", "今天还没有聊天记录,无法生成情绪总结");
// 模拟服务调用
when(aiChatService.generateEmotionSummary(anyString())).thenReturn(mockResult);
// 执行测试
mockMvc.perform(post("/api/emotion-summary/generate/test_user_456")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value("今天还没有聊天记录,无法生成情绪总结"));
log.info("✅ 无聊天记录情况测试通过");
}
@Test
public void testGenerateEmotionSummaryError() throws Exception {
// 模拟服务异常
when(aiChatService.generateEmotionSummary(anyString()))
.thenThrow(new RuntimeException("AI服务暂时不可用"));
// 执行测试
mockMvc.perform(post("/api/emotion-summary/generate/test_user_789")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").exists());
log.info("✅ 服务异常情况测试通过");
}
@Test
public void testGetEmotionSummaryStatus() throws Exception {
// 执行测试
mockMvc.perform(get("/api/emotion-summary/status/test_user_123")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.userId").value("test_user_123"))
.andExpect(jsonPath("$.data.canGenerate").value(true));
log.info("✅ 情绪记录状态查询测试通过");
}
}
@@ -1,98 +0,0 @@
package com.emotion.service;
import com.emotion.service.impl.AiChatServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
/**
* Coze API测试类
*
* @author emotion-museum
* @date 2025-07-24
*/
@SpringBootTest
@ActiveProfiles("test")
public class CozeApiTest {
@Autowired
private AIChatService aiChatService;
@Test
public void testServiceAvailability() {
// 测试服务可用性检查
boolean isAvailable = aiChatService.isServiceAvailable();
String status = aiChatService.getServiceStatus();
// 验证结果
assertNotNull(status);
assertTrue(status.equals("available") || status.equals("unavailable"));
// 如果配置正确,服务应该可用
if (isAvailable) {
assertEquals("available", status);
} else {
assertEquals("unavailable", status);
}
}
@Test
public void testHealthCheck() {
// 测试健康检查
boolean healthStatus = aiChatService.healthCheck();
// 验证结果 - 健康检查应该返回布尔值
assertNotNull(healthStatus);
}
// 注意:以下测试需要真实的Coze API配置才能通过
// 在测试环境中可能会失败,因为没有真实的API token
/*
@Test
public void testSendMessage() {
// 测试发送消息
String conversationId = "test-conversation-001";
String message = "你好,这是一条测试消息";
String userId = "test-user-001";
String response = aiChatService.sendMessage(conversationId, message, userId);
// 验证响应
assertNotNull(response);
assertFalse(response.isEmpty());
}
@Test
public void testSendChatMessage() {
// 测试聊天消息
String conversationId = "test-conversation-002";
String message = "请介绍一下你自己";
String userId = "test-user-002";
String response = aiChatService.sendChatMessage(conversationId, message, userId);
// 验证响应
assertNotNull(response);
assertFalse(response.isEmpty());
}
@Test
public void testGuestChat() {
// 测试访客聊天
String message = "你好,我是访客用户";
String clientIp = "192.168.1.100";
Map<String, Object> response = aiChatService.guestChat(message, clientIp);
// 验证响应
assertNotNull(response);
assertTrue(response.containsKey("message"));
assertTrue(response.containsKey("error"));
assertTrue(response.containsKey("timestamp"));
}
*/
}
@@ -1,105 +0,0 @@
package com.emotion.service;
import com.emotion.entity.Message;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
/**
* 消息服务测试类
*
* @author emotion-museum
* @date 2025-07-24
*/
@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class MessageServiceTest {
@Autowired
private MessageService messageService;
@Test
public void testCreateMessage() {
// 创建消息对象
Message message = new Message();
message.setConversationId("test-conversation-001");
message.setContent("这是一条测试消息");
message.setType("text");
message.setSender("user");
message.setCreateBy("test-user-001");
// 调用优化后的createMessage方法
Message savedMessage = messageService.createMessage(message);
// 验证结果
assertNotNull(savedMessage);
assertNotNull(savedMessage.getId());
assertEquals("test-conversation-001", savedMessage.getConversationId());
assertEquals("这是一条测试消息", savedMessage.getContent());
assertEquals("text", savedMessage.getType());
assertEquals("user", savedMessage.getSender());
assertEquals("test-user-001", savedMessage.getCreateBy());
// 验证默认值设置
assertNotNull(savedMessage.getTimestamp());
assertEquals("sent", savedMessage.getStatus());
assertEquals(0, savedMessage.getIsRead());
}
@Test
public void testCreateMessageWithCustomValues() {
// 创建消息对象,设置自定义时间戳和状态
Message message = new Message();
message.setConversationId("test-conversation-002");
message.setContent("自定义状态消息");
message.setType("text");
message.setSender("ai");
message.setCreateBy("ai");
message.setTimestamp(LocalDateTime.of(2025, 7, 24, 10, 30, 0));
message.setStatus("processing");
message.setIsRead(1);
// 调用优化后的createMessage方法
Message savedMessage = messageService.createMessage(message);
// 验证结果 - 自定义值应该被保留
assertNotNull(savedMessage);
assertEquals("test-conversation-002", savedMessage.getConversationId());
assertEquals("自定义状态消息", savedMessage.getContent());
assertEquals("ai", savedMessage.getSender());
assertEquals(LocalDateTime.of(2025, 7, 24, 10, 30, 0), savedMessage.getTimestamp());
assertEquals("processing", savedMessage.getStatus());
assertEquals(1, savedMessage.getIsRead());
}
@Test
public void testCreateMessageWithPartialDefaults() {
// 创建消息对象,只设置部分字段
Message message = new Message();
message.setConversationId("test-conversation-003");
message.setContent("部分默认值消息");
message.setType("text");
message.setSender("user");
message.setCreateBy("test-user-003");
message.setStatus("delivered"); // 设置自定义状态
// 不设置timestamp和isRead,应该使用默认值
// 调用优化后的createMessage方法
Message savedMessage = messageService.createMessage(message);
// 验证结果
assertNotNull(savedMessage);
assertEquals("test-conversation-003", savedMessage.getConversationId());
assertEquals("部分默认值消息", savedMessage.getContent());
assertEquals("delivered", savedMessage.getStatus()); // 自定义状态
assertNotNull(savedMessage.getTimestamp()); // 默认时间戳
assertEquals(0, savedMessage.getIsRead()); // 默认未读状态
}
}
@@ -1,152 +0,0 @@
package com.emotion.service;
import com.emotion.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
/**
* 密码加密测试类
*
* @author emotion-museum
* @date 2025-07-24
*/
@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class PasswordEncryptionTest {
@Autowired
private UserService userService;
@Autowired
private AuthService authService;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void testPasswordEncryption() {
String rawPassword = "testPassword123";
// 测试密码编码器
String encodedPassword = passwordEncoder.encode(rawPassword);
// 验证密码不是明文
assertNotEquals(rawPassword, encodedPassword);
// 验证密码匹配
assertTrue(passwordEncoder.matches(rawPassword, encodedPassword));
// 验证错误密码不匹配
assertFalse(passwordEncoder.matches("wrongPassword", encodedPassword));
}
@Test
public void testUserCreationWithPasswordEncryption() {
String account = "testuser001";
String username = "Test User";
String rawPassword = "testPassword123";
String email = "test@example.com";
String phone = "13800138000";
// 创建用户
User user = userService.createUser(account, username, rawPassword, email, phone);
// 验证用户创建成功
assertNotNull(user);
assertNotNull(user.getId());
assertEquals(account, user.getAccount());
assertEquals(username, user.getUsername());
assertEquals(email, user.getEmail());
assertEquals(phone, user.getPhone());
// 验证密码已加密
assertNotEquals(rawPassword, user.getPassword());
// 验证密码验证功能
assertTrue(userService.validatePassword(user.getId(), rawPassword));
assertFalse(userService.validatePassword(user.getId(), "wrongPassword"));
// 验证可以通过账号查询到用户
User foundUser = userService.getByAccount(account);
assertNotNull(foundUser);
assertEquals(user.getId(), foundUser.getId());
// 验证密码匹配
assertTrue(passwordEncoder.matches(rawPassword, foundUser.getPassword()));
}
@Test
public void testPasswordConsistencyBetweenServices() {
String rawPassword = "consistencyTest123";
// 使用UserService加密密码
String userServiceEncoded = passwordEncoder.encode(rawPassword);
// 验证AuthService能正确验证UserService加密的密码
assertTrue(passwordEncoder.matches(rawPassword, userServiceEncoded));
// 测试多次加密产生不同的哈希值(BCrypt的特性)
String encoded1 = passwordEncoder.encode(rawPassword);
String encoded2 = passwordEncoder.encode(rawPassword);
// 哈希值应该不同(因为BCrypt使用随机盐)
assertNotEquals(encoded1, encoded2);
// 但都应该能验证原始密码
assertTrue(passwordEncoder.matches(rawPassword, encoded1));
assertTrue(passwordEncoder.matches(rawPassword, encoded2));
}
@Test
public void testBCryptPasswordFormat() {
String rawPassword = "formatTest123";
String encodedPassword = passwordEncoder.encode(rawPassword);
// BCrypt密码应该以$2a$、$2b$或$2y$开头
assertTrue(encodedPassword.startsWith("$2a$") ||
encodedPassword.startsWith("$2b$") ||
encodedPassword.startsWith("$2y$"),
"密码应该使用BCrypt格式加密");
// BCrypt密码长度通常是60个字符
assertEquals(60, encodedPassword.length(), "BCrypt密码长度应该是60个字符");
}
/*
// 注意:以下测试需要完整的认证流程,可能需要验证码等
@Test
public void testFullAuthenticationFlow() {
// 这个测试需要模拟完整的注册和登录流程
// 由于涉及验证码等复杂逻辑,在实际测试中可能需要mock相关服务
String account = "authtest001";
String password = "authTestPassword123";
String email = "authtest@example.com";
// 1. 注册用户
RegisterRequest registerRequest = new RegisterRequest();
registerRequest.setAccount(account);
registerRequest.setPassword(password);
registerRequest.setEmail(email);
// 需要设置验证码等其他必要字段
// 2. 登录验证
LoginRequest loginRequest = new LoginRequest();
loginRequest.setAccount(account);
loginRequest.setPassword(password);
// 需要设置验证码等其他必要字段
// 验证登录成功
// AuthResponse authResponse = authService.login(loginRequest);
// assertNotNull(authResponse);
// assertNotNull(authResponse.getToken());
}
*/
}