feat(ai): 新增心情记录及分析功能
All checks were successful
Auto-build / Automatic-Packaging (push) Successful in 54s
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:
parent
192bb72251
commit
51c1ef213d
26
pom.xml
26
pom.xml
@ -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>
|
||||
|
||||
19
src/main/java/com/kane/animo/config/AiConfigurer.java
Normal file
19
src/main/java/com/kane/animo/config/AiConfigurer.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
78
src/main/java/com/kane/animo/record/domain/Record.java
Normal file
78
src/main/java/com/kane/animo/record/domain/Record.java
Normal 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;
|
||||
|
||||
}
|
||||
@ -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
|
||||
) { }
|
||||
@ -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;
|
||||
}
|
||||
14
src/main/java/com/kane/animo/record/mapper/RecordMapper.java
Normal file
14
src/main/java/com/kane/animo/record/mapper/RecordMapper.java
Normal 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> {
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
79
src/main/java/com/kane/animo/util/AiUtil.java
Normal file
79
src/main/java/com/kane/animo/util/AiUtil.java
Normal 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到100,0表示最消极、愤怒、糟糕的心情,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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user