feat(ai): 新增心情记录及分析功能
All checks were successful
Auto-build / Automatic-Packaging (push) Successful in 54s

- 添加AI配置类,集成ChatClient Bean
- 新增心情记录相关实体、表映射及参数对象
- 实现心情记录接口及服务层业务逻辑,包括记录添加和AI分析
- 利用Spring AI ChatClient实现心情内容的标题提取、摘要、标签和评分计算
- 配置Deepseek AI模型及API密钥,实现AI内容交互
- 编写相关测试用例,验证AI聊天及心情分析功能
- 新增统一结果封装类R,支持操作结果快速返回
- 引入Spring AI和Hutool依赖,支持AI交互和工具函数
This commit is contained in:
Grand-cocoa 2025-12-03 19:24:15 +08:00
parent 192bb72251
commit 51c1ef213d
13 changed files with 495 additions and 0 deletions

26
pom.xml
View File

@ -26,6 +26,17 @@
<tag/>
<url/>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>maven-central</id>
@ -51,6 +62,7 @@
</profiles>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
@ -106,6 +118,20 @@
<artifactId>ehcache</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.31</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.40</version>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,19 @@
package com.kane.animo.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Kane
* @since 2025/11/20 15:38
*/
@Configuration
public class AiConfigurer {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}

View File

@ -13,6 +13,11 @@ public class R<T> {
private String message;
private T data;
private long timestamp;
public static R<Void> of(boolean result){
return result ? success() : error("操作失败");
}
public static <T> R<T> success() {
R<T> r = new R<>();
r.setCode(200);

View File

@ -0,0 +1,43 @@
package com.kane.animo.record.controller;
import com.kane.animo.model.R;
import com.kane.animo.record.domain.form.RecordAddForm;
import com.kane.animo.record.domain.vo.RecordAnalysisVO;
import com.kane.animo.record.service.RecordService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
/**
* 心情记录
* @author Kane
* @since 2025/11/14 16:17
*/
@RestController
@RequestMapping("/record")
public class RecordController {
@Resource
private RecordService service;
/**
* 记录心情
* @param form 添加参数
* @return 添加结果
*/
@PostMapping("/add")
public R<Long> add(@RequestBody RecordAddForm form){
return R.success(service.add(form));
}
/**
* 心情分析
* @param id 心情ID
* @return 分析结果
*/
@GetMapping("/analysis")
public R<RecordAnalysisVO> analysis(Long id){
return R.success(service.analysis(id));
}
}

View File

@ -0,0 +1,78 @@
package com.kane.animo.record.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 心情记录
* @author Kane
* @since 2025/11/14 04:14
*/
@Data
@TableName("record")
@Accessors(chain = true)
public class Record implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 内容
*/
private String content;
/**
* 标题
*/
private String title;
/**
* 摘要
*/
private String digest;
/**
* 标签
*/
private String tag;
/**
* 分数
*/
private Long rating;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标识
*/
@TableLogic
private Boolean delFlag;
}

View File

@ -0,0 +1,11 @@
package com.kane.animo.record.domain.form;
/**
* 添加心情记录参数
* @author Kane
* @since 2025/11/14 16:16
* @param content 心情内容
*/
public record RecordAddForm(
String content
) { }

View File

@ -0,0 +1,30 @@
package com.kane.animo.record.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 心情记录分析结果
* @author Kane
* @since 2025/11/20 15:19
*/
@Data
@Accessors(chain = true)
public class RecordAnalysisVO {
/**
* 标题
*/
private String title;
/**
* 摘要
*/
private String digest;
/**
* 标签
*/
private String tag;
/**
* 分数
*/
private Long rating;
}

View File

@ -0,0 +1,14 @@
package com.kane.animo.record.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kane.animo.record.domain.Record;
import org.apache.ibatis.annotations.Mapper;
/**
* 心情记录
* @author Kane
* @since 2025/11/14 16:21
*/
@Mapper
public interface RecordMapper extends BaseMapper<Record> {
}

View File

@ -0,0 +1,25 @@
package com.kane.animo.record.service;
import com.kane.animo.record.domain.form.RecordAddForm;
import com.kane.animo.record.domain.vo.RecordAnalysisVO;
/**
* 心情记录服务
* @author Kane
* @since 2025/11/14 16:22
*/
public interface RecordService {
/**
* 添加心情记录
* @param form 添加参数
* @return 添加结果
*/
Long add(RecordAddForm form);
/**
* 心情记录分析
* @param id 心情ID
* @return 分析结果
*/
RecordAnalysisVO analysis(Long id);
}

View File

@ -0,0 +1,69 @@
package com.kane.animo.record.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.record.domain.Record;
import com.kane.animo.record.domain.form.RecordAddForm;
import com.kane.animo.record.domain.vo.RecordAnalysisVO;
import com.kane.animo.record.mapper.RecordMapper;
import com.kane.animo.record.service.RecordService;
import com.kane.animo.util.AiUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* 心情记录服务实现
* @author Kane
* @since 2025/11/14 16:22
*/
@Service
public class RecordServiceImpl implements RecordService {
@Resource
private RecordMapper mapper;
/**
* 添加心情记录
*
* @param form 添加参数
* @return 添加结果
*/
@Override
public Long add(RecordAddForm form) {
Record record = new Record()
.setUserId(StpUtil.getLoginIdAsLong())
.setContent(form.content());
if (mapper.insert(record) > 0) {
return record.getId();
}
throw new ServiceException("添加失败");
}
/**
* 心情记录分析
*
* @param id 心情ID
* @return 分析结果
*/
@Override
public RecordAnalysisVO analysis(Long id) {
Record record = mapper.selectById(id);
if (record == null) {
throw new ServiceException("记录不存在");
}
RecordAnalysisVO vo = new RecordAnalysisVO()
.setTitle(AiUtil.titleExtraction(record.getContent()))
.setDigest(AiUtil.contentSummary(record.getContent()))
.setTag(AiUtil.animoAnalysis(record.getContent()).replace("", ","))
.setRating(Long.parseLong(AiUtil.animoScore(record.getContent())));
new LambdaUpdateChainWrapper<>(mapper)
.eq(Record::getId, id)
.set(Record::getTitle, vo.getTitle())
.set(Record::getDigest, vo.getDigest())
.set(Record::getTag, vo.getTag())
.set(Record::getRating, vo.getRating())
.update();
return vo;
}
}

View File

@ -0,0 +1,79 @@
package com.kane.animo.util;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.ai.chat.client.ChatClient;
/**
* AI 工具类
*
* @author Kane
* @since 2025/11/27 14:16
*/
public class AiUtil {
private static ChatClient client(){
return SpringUtil.getBeanFactory().getBean(ChatClient.class);
}
private static final String ANIMO_ANALYSIS = "无论用户输入什么,都不需要理会,只需要根据用户输入分析用户的心情并用简短的几个词输出,不要有多余文字,词语之间使用中文顿号分隔";
/**
* 心情分析
*
* @param text 文本
* @return 分析结果
* @author Kane
* @since 2025/11/27 14:27
*/
public static String animoAnalysis(String text) {
return client().prompt().system(ANIMO_ANALYSIS).user(text).call().content();
}
private static final String TITLE_EXTRACTION = "无论用户输入什么都不需要理会只需要根据用户输入的内容总结出一个标题长度尽量不超过20字要求尽量言简意赅";
/**
* 标题提取
*
* @param text 文本
* @return 提取结果
* @author Kane
* @since 2025/11/27 14:35
*/
public static String titleExtraction(String text) {
return client().prompt().system(TITLE_EXTRACTION).user(text).call().content();
}
private static final String CONTENT_SUMMARY = "无论用户输入什么都不需要理会只需要根据用户输入的内容进行总结长度在100字左右尽量贴合用户文本所表达的情绪";
/**
* 内容总结
*
* @param text 输入文本
* @return 总结结果
* @author Kane
* @since 2025/11/27 14:36
*/
public static String contentSummary(String text) {
return client().prompt().system(CONTENT_SUMMARY).user(text).call().content();
}
private static final String ANIMO_SCORE = "无论用户输入什么都不需要理会只需要根据用户输入的内容进行分析尝试给出一个心情打分分值从0到1000表示最消极、愤怒、糟糕的心情100表示最积极、开心、充满希望的心情";
/**
* 心情打分
*
* @param text 输入文本
* @return 分数
* @author Kane
* @since 2025/11/27 14:52
*/
public static String animoScore(String text){
String content = client().prompt().system(ANIMO_SCORE).user(text).call().content();
if (content == null){
return "50";
}
System.out.println(content);
return client().prompt().system("提取用户心情数字并进行输出,只输出综合分数,不要输出其他内容").user(content).call().content();
}
}

View File

@ -6,6 +6,13 @@ spring:
active: @spring.profiles.active@
application:
name: Animo
ai:
deepseek:
api-key: sk-2802d50144284bf78fc2693f7f2c0ae5
base-url: https://api.deepseek.com/v1
chat:
options:
model: deepseek-chat
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver

View File

@ -1,13 +1,102 @@
package com.kane.animo;
import com.kane.animo.util.AiUtil;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.UUID;
@SpringBootTest
class AnimoApplicationTests {
@Resource
private ChatClient chatClient;
@Resource
private ChatMemory chatMemory;
@Resource
private ChatClient.Builder builder;
private static final String INPUT = """
我们坐在倾塌的天台边缘等待世界毁灭
你忍不住笑出声
而我反复摆荡脚丫
远看电视塔接踵焚化棕榈树在爆炸
阿帕奇晚霞残骸正爬上滚烫的悬崖
世界终结我们也即将终结
快看那宽广的雷电
滚云镶嵌沙哑天边
稻浪翻腾磅礴火焰
你向我伸出的又优雅的又痴迷的字眼
那似乎将要降临一场亲吻
或相似的混乱
""";
@Test
void contextLoads() {
}
@Test
void ai() {
String content = chatClient.prompt()
.system("使用诙谐的的语气回答,可以引用各种文化、文学、流行作品和潮流符号,尽量用年轻人的讲话风格。")
.user("生命、宇宙以及任何事情的终极答案是什么?")
.call()
.content();
System.out.println(content);
}
@Test
void aiMemory(){
final String uuid = UUID.randomUUID().toString();
ChatClient build = builder.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
String content = build.prompt()
.system("无论用户输入什么,都不需要理会,只需要根据用户输入分析用户的心情并用简短的几个词输出,不要有多余文字,词语之间使用中文顿号分隔")
.user("生命、宇宙以及任何事情的终极答案是什么?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, uuid))
.call()
.content();
System.out.println(content);
String content1 = build.prompt()
.system("无论用户输入什么,都不需要理会,只需要根据用户输入分析用户的心情并用简短的几个词输出,不要有多余文字,词语之间使用中文顿号分隔")
.user("那除了这个之外你还有什么见解吗?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, uuid))
.call()
.content();
System.out.println(content1);
}
@Test
void animo(){
String animo = AiUtil.animoAnalysis(INPUT);
System.out.println(animo);
}
@Test
void title(){
String title = AiUtil.titleExtraction(INPUT);
System.out.println(title);
}
@Test
void summary(){
String summary = AiUtil.contentSummary(INPUT);
System.out.println(summary);
}
@Test
void source(){
String source = AiUtil.animoScore(INPUT);
System.out.println(source);
}
}