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
@@ -1,8 +1,15 @@
package com.emotion.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.emotion.common.Result;
import com.emotion.entity.EmotionRecord;
import com.emotion.service.EmotionRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
@@ -11,16 +18,20 @@ import java.util.*;
/**
* 情绪记录控制器
*
*
* @author emotion-museum
* @date 2025-07-22
*/
@RestController
@RequestMapping("/emotion/record")
@RequestMapping("/api/emotion-records")
@Tag(name = "情绪记录管理", description = "用户情绪记录的增删改查功能")
public class EmotionRecordController {
private static final Logger log = LoggerFactory.getLogger(EmotionRecordController.class);
@Autowired
private EmotionRecordService emotionRecordService;
/**
* 创建情绪记录
*/
@@ -53,38 +64,26 @@ public class EmotionRecordController {
/**
* 获取用户情绪记录列表
*/
@GetMapping("/list/{userId}")
public Result<List<Map<String, Object>>> getRecordList(@PathVariable String userId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
log.info("获取情绪记录列表: userId={}, page={}, size={}", userId, page, size);
@Operation(summary = "获取用户情绪记录列表", description = "分页获取指定用户的情绪记录,按创建时间倒序")
@GetMapping("/user/{userId}")
public Result<IPage<EmotionRecord>> getRecordList(
@Parameter(description = "用户ID") @PathVariable String userId,
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer current,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer size) {
log.info("获取用户情绪记录列表: userId={}, current={}, size={}", userId, current, size);
try {
List<Map<String, Object>> records = new ArrayList<>();
// 模拟数据
for (int i = 0; i < size; i++) {
Map<String, Object> record = new HashMap<>();
record.put("id", "record-" + (System.currentTimeMillis() + i));
record.put("userId", userId);
record.put("recordDate", LocalDate.now().minusDays(i));
record.put("emotionType", getRandomEmotion());
record.put("intensity", 0.5 + Math.random() * 0.5);
record.put("triggers", "工作压力");
record.put("description", "今天感觉" + getRandomEmotion());
record.put("tags", Arrays.asList("工作", "压力"));
record.put("weather", "晴天");
record.put("location", "办公室");
record.put("activity", "工作");
record.put("createTime", LocalDateTime.now().minusDays(i));
records.add(record);
}
return Result.success(records);
IPage<EmotionRecord> page = emotionRecordService.getByUserIdWithPage(userId, current, size);
log.info("获取用户情绪记录成功: userId={}, total={}, records={}",
userId, page.getTotal(), page.getRecords().size());
return Result.success(page);
} catch (Exception e) {
log.error("获取情绪记录列表失败: {}", e.getMessage());
return Result.error("获取列表失败");
log.error("获取用户情绪记录失败: userId={}", userId, e);
return Result.error("获取情绪记录失败: " + e.getMessage());
}
}
@@ -0,0 +1,78 @@
package com.emotion.controller;
import com.emotion.common.Result;
import com.emotion.service.AIChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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;
/**
* 情绪总结控制器
*
* @author emotion-museum
* @date 2025-07-25
*/
@Slf4j
@RestController
@RequestMapping("/api/emotion-summary")
@Tag(name = "情绪总结管理", description = "用户情绪记录总结和分析功能")
public class EmotionSummaryController {
@Autowired
private AIChatService aiChatService;
@Operation(summary = "生成用户当天的情绪记录总结", description = "基于用户当天的聊天记录生成情绪分析和记录")
@PostMapping("/generate/{userId}")
public Result<Map<String, Object>> generateEmotionSummary(
@Parameter(description = "用户ID") @PathVariable String userId) {
log.info("收到生成情绪记录总结请求: userId={}", userId);
try {
// 调用AI服务生成情绪总结
Map<String, Object> result = aiChatService.generateEmotionSummary(userId);
if ((Boolean) result.get("success")) {
log.info("情绪记录总结生成成功: userId={}", userId);
return Result.success(result, "情绪记录总结生成成功");
} else {
String message = (String) result.get("message");
log.warn("情绪记录总结生成失败: userId={}, message={}", userId, message);
return Result.error(message);
}
} catch (Exception e) {
log.error("生成情绪记录总结时发生异常: userId={}", userId, e);
return Result.error("生成情绪记录总结失败: " + e.getMessage());
}
}
@Operation(summary = "获取用户情绪记录总结状态", description = "检查用户今天是否已经生成过情绪记录")
@GetMapping("/status/{userId}")
public Result<Map<String, Object>> getEmotionSummaryStatus(
@Parameter(description = "用户ID") @PathVariable String userId) {
log.info("查询用户情绪记录总结状态: userId={}", userId);
try {
// 这里可以添加检查用户今天是否已经生成过情绪记录的逻辑
// 暂时返回基本状态信息
Map<String, Object> status = Map.of(
"userId", userId,
"canGenerate", true,
"message", "可以生成情绪记录总结"
);
return Result.success(status);
} catch (Exception e) {
log.error("查询情绪记录总结状态时发生异常: userId={}", userId, e);
return Result.error("查询状态失败: " + e.getMessage());
}
}
}
@@ -123,6 +123,41 @@ public class MessageController {
return Result.success(count);
}
/**
* 根据用户ID分页查询消息
*/
@GetMapping("/user/{userId}/page")
public Result<PageResult<MessageResponse>> getPageByUserId(@PathVariable String userId,
@Valid PageRequest request) {
IPage<Message> page = messageService.getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()), Math.toIntExact(request.getSize()));
List<MessageResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
}
/**
* 根据用户ID和关键词搜索消息
*/
@GetMapping("/user/{userId}/search")
public Result<List<MessageResponse>> searchByUserId(@PathVariable String userId,
@RequestParam String keyword,
@RequestParam(defaultValue = "50") Integer limit) {
List<Message> messages = messageService.searchByUserIdAndKeyword(userId, keyword, limit);
List<MessageResponse> responses = messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
/**
* 转换为响应对象
*/
@@ -3,6 +3,11 @@ package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Message;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息Mapper接口
@@ -12,4 +17,55 @@ import org.apache.ibatis.annotations.Mapper;
*/
@Mapper
public interface MessageMapper extends BaseMapper<Message> {
/**
* 根据用户ID和时间范围查询消息
* 通过conversation表关联查询
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.create_time BETWEEN #{startTime} AND #{endTime} " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time ASC")
List<Message> getByUserIdAndTimeRange(@Param("userId") String userId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 根据用户ID分页查询消息
* 通过conversation表关联查询
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time DESC " +
"LIMIT #{offset}, #{size}")
List<Message> getByUserIdWithPageList(@Param("userId") String userId,
@Param("offset") Integer offset,
@Param("size") Integer size);
/**
* 统计用户消息总数
*/
@Select("SELECT COUNT(*) FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.is_deleted = 0")
Long countByUserId(@Param("userId") String userId);
/**
* 根据用户ID和关键词搜索消息
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.content LIKE CONCAT('%', #{keyword}, '%') " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time DESC " +
"LIMIT #{limit}")
List<Message> searchByUserIdAndKeyword(@Param("userId") String userId,
@Param("keyword") String keyword,
@Param("limit") Integer limit);
}
@@ -59,4 +59,12 @@ public interface AIChatService {
* 健康检查
*/
boolean healthCheck();
/**
* 生成用户当天的情绪记录总结
*
* @param userId 用户ID
* @return 情绪记录结果
*/
Map<String, Object> generateEmotionSummary(String userId);
}
@@ -25,6 +25,11 @@ public interface EmotionRecordService extends IService<EmotionRecord> {
* 根据用户ID分页查询情绪记录
*/
IPage<EmotionRecord> getPageByUserId(BasePageRequest request, String userId);
/**
* 根据用户ID分页查询情绪记录(简化版本)
*/
IPage<EmotionRecord> getByUserIdWithPage(String userId, Integer current, Integer size);
/**
* 根据用户ID查询情绪记录
@@ -40,6 +40,21 @@ public interface MessageService extends IService<Message> {
* 根据时间范围查询消息
*/
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 根据用户ID和时间范围查询消息
*/
List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 根据用户ID分页查询消息
*/
IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size);
/**
* 根据用户ID和关键词搜索消息
*/
List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit);
/**
* 查询会话的最后一条消息
@@ -5,10 +5,12 @@ import com.alibaba.fastjson2.JSONObject;
import com.emotion.entity.Message;
import com.emotion.entity.Conversation;
import com.emotion.entity.CozeApiCall;
import com.emotion.entity.EmotionRecord;
import com.emotion.service.AIChatService;
import com.emotion.service.MessageService;
import com.emotion.service.ConversationService;
import com.emotion.service.CozeApiCallService;
import com.emotion.service.EmotionRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -23,10 +25,14 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* AI聊天服务实现类
@@ -50,6 +56,9 @@ public class AiChatServiceImpl implements AIChatService {
@Autowired
private CozeApiCallService cozeApiCallService;
@Autowired
private EmotionRecordService emotionRecordService;
@Value("${emotion.coze.api.token:}")
private String cozeApiToken;
@@ -897,4 +906,209 @@ public class AiChatServiceImpl implements AIChatService {
throw e;
}
}
@Override
public Map<String, Object> generateEmotionSummary(String userId) {
log.info("开始生成用户情绪记录总结: userId={}", userId);
Map<String, Object> result = new HashMap<>();
try {
// 获取用户当天的所有聊天记录
LocalDate today = LocalDate.now();
LocalDateTime startOfDay = today.atStartOfDay();
LocalDateTime endOfDay = today.atTime(23, 59, 59);
List<Message> todayMessages = messageService.getByUserIdAndTimeRange(userId, startOfDay, endOfDay);
log.info("获取到用户当天聊天记录数量: {}", todayMessages.size());
if (todayMessages.isEmpty()) {
result.put("success", false);
result.put("message", "今天还没有聊天记录,无法生成情绪总结");
return result;
}
// 整合聊天记录
String chatHistory = integrateChatHistory(todayMessages);
log.info("聊天记录整合完成,总长度: {}", chatHistory.length());
// 构建情绪分析提示词
String emotionPrompt = buildEmotionAnalysisPrompt(chatHistory);
// 调用Coze API进行情绪分析总结
String conversationId = "emotion_summary_" + userId + "_" + today.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String emotionSummary = sendSummaryMessage(conversationId, emotionPrompt, userId);
log.info("情绪分析总结生成完成: {}", emotionSummary);
// 解析AI返回的情绪分析结果
EmotionAnalysisResult analysisResult = parseEmotionSummary(emotionSummary);
// 创建情绪记录
EmotionRecord emotionRecord = createEmotionRecord(userId, analysisResult, chatHistory);
result.put("success", true);
result.put("emotionRecord", emotionRecord);
result.put("summary", emotionSummary);
result.put("analysisResult", analysisResult);
result.put("messageCount", todayMessages.size());
result.put("recordDate", today);
log.info("情绪记录总结生成成功: recordId={}", emotionRecord.getId());
} catch (Exception e) {
log.error("生成情绪记录总结失败", e);
result.put("success", false);
result.put("message", "生成情绪总结失败: " + e.getMessage());
}
return result;
}
/**
* 整合聊天记录
*/
private String integrateChatHistory(List<Message> messages) {
StringBuilder chatHistory = new StringBuilder();
chatHistory.append("以下是用户今天的聊天记录:\n\n");
for (Message message : messages) {
String sender = "ai".equals(message.getSender()) ? "AI助手" : "用户";
String timestamp = message.getCreateTime().format(DateTimeFormatter.ofPattern("HH:mm"));
chatHistory.append(String.format("[%s] %s: %s\n", timestamp, sender, message.getContent()));
}
return chatHistory.toString();
}
/**
* 构建情绪分析提示词
*/
private String buildEmotionAnalysisPrompt(String chatHistory) {
return String.format("""
请分析以下聊天记录中用户的情绪状态,并生成一个情绪总结报告。
%s
请从以下几个维度进行分析:
1. 主要情绪类型(如:开心、焦虑、愤怒、悲伤、平静等)
2. 情绪强度(0-1之间的数值,0表示很轻微,1表示很强烈)
3. 情绪触发因素(导致情绪变化的主要原因)
4. 情绪变化趋势(情绪在对话过程中的变化)
5. 建议和关怀(针对用户情绪状态的建议)
请以JSON格式返回分析结果:
{
"primaryEmotion": "主要情绪类型",
"intensity": 0.8,
"triggers": "触发因素描述",
"emotionTrend": "情绪变化趋势",
"suggestions": "建议和关怀",
"summary": "整体情绪总结"
}
""", chatHistory);
}
/**
* 解析情绪分析总结结果
*/
private EmotionAnalysisResult parseEmotionSummary(String summary) {
try {
// 尝试从AI回复中提取JSON
String jsonStr = extractJsonFromSummary(summary);
if (jsonStr != null) {
JSONObject json = JSON.parseObject(jsonStr);
EmotionAnalysisResult result = new EmotionAnalysisResult();
result.setPrimaryEmotion(json.getString("primaryEmotion"));
result.setIntensity(json.getDoubleValue("intensity"));
result.setTriggers(json.getString("triggers"));
result.setEmotionTrend(json.getString("emotionTrend"));
result.setSuggestions(json.getString("suggestions"));
result.setSummary(json.getString("summary"));
return result;
}
} catch (Exception e) {
log.warn("解析情绪分析结果失败,使用默认值: {}", e.getMessage());
}
// 如果解析失败,返回默认结果
EmotionAnalysisResult defaultResult = new EmotionAnalysisResult();
defaultResult.setPrimaryEmotion("平静");
defaultResult.setIntensity(0.5);
defaultResult.setTriggers("日常对话");
defaultResult.setEmotionTrend("相对稳定");
defaultResult.setSuggestions("保持当前的积极状态");
defaultResult.setSummary(summary);
return defaultResult;
}
/**
* 从AI回复中提取JSON字符串
*/
private String extractJsonFromSummary(String summary) {
try {
int startIndex = summary.indexOf("{");
int endIndex = summary.lastIndexOf("}");
if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
return summary.substring(startIndex, endIndex + 1);
}
} catch (Exception e) {
log.warn("提取JSON失败: {}", e.getMessage());
}
return null;
}
/**
* 创建情绪记录
*/
private EmotionRecord createEmotionRecord(String userId, EmotionAnalysisResult analysisResult, String chatHistory) {
EmotionRecord record = EmotionRecord.builder()
.userId(userId)
.recordDate(LocalDate.now())
.emotionType(analysisResult.getPrimaryEmotion())
.intensity(BigDecimal.valueOf(analysisResult.getIntensity()))
.triggers(analysisResult.getTriggers())
.description(analysisResult.getSummary())
.notes("基于当天聊天记录自动生成的情绪分析")
.tags("AI分析,聊天记录,情绪总结")
.build();
emotionRecordService.save(record);
log.info("情绪记录创建成功: recordId={}", record.getId());
return record;
}
/**
* 情绪分析结果内部类
*/
public static class EmotionAnalysisResult {
private String primaryEmotion;
private Double intensity;
private String triggers;
private String emotionTrend;
private String suggestions;
private String summary;
// Getters and Setters
public String getPrimaryEmotion() { return primaryEmotion; }
public void setPrimaryEmotion(String primaryEmotion) { this.primaryEmotion = primaryEmotion; }
public Double getIntensity() { return intensity; }
public void setIntensity(Double intensity) { this.intensity = intensity; }
public String getTriggers() { return triggers; }
public void setTriggers(String triggers) { this.triggers = triggers; }
public String getEmotionTrend() { return emotionTrend; }
public void setEmotionTrend(String emotionTrend) { this.emotionTrend = emotionTrend; }
public String getSuggestions() { return suggestions; }
public void setSuggestions(String suggestions) { this.suggestions = suggestions; }
public String getSummary() { return summary; }
public void setSummary(String summary) { this.summary = summary; }
}
}
@@ -192,4 +192,16 @@ public class EmotionRecordServiceImpl extends ServiceImpl<EmotionRecordMapper, E
this.save(record);
return record;
}
@Override
public IPage<EmotionRecord> getByUserIdWithPage(String userId, Integer current, Integer size) {
Page<EmotionRecord> page = new Page<>(current, size);
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmotionRecord::getUserId, userId)
.eq(EmotionRecord::getIsDeleted, 0)
.orderByDesc(EmotionRecord::getCreateTime);
return this.page(page, wrapper);
}
}
@@ -168,4 +168,31 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
public boolean markAsRead(String messageId) {
return updateReadStatus(messageId, 1);
}
@Override
public List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) {
// 由于Message表没有直接的userId字段,需要通过conversation表关联查询
// 这里先通过conversationService获取用户的所有对话ID,然后查询这些对话的消息
return this.baseMapper.getByUserIdAndTimeRange(userId, startTime, endTime);
}
@Override
public IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size) {
// 手动实现分页
Integer offset = (current - 1) * size;
List<Message> records = this.baseMapper.getByUserIdWithPageList(userId, offset, size);
Long total = this.baseMapper.countByUserId(userId);
Page<Message> page = new Page<>(current, size);
page.setRecords(records);
page.setTotal(total);
return page;
}
@Override
public List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit) {
// 通过conversation表关联查询用户的消息,根据关键词搜索
return this.baseMapper.searchByUserIdAndKeyword(userId, keyword, limit);
}
}