diff --git a/pom.xml b/pom.xml index c030d16..948ce6a 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,17 @@ + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + maven-central @@ -51,6 +62,7 @@ 17 + 1.1.0 @@ -106,6 +118,20 @@ ehcache 3.11.0 + + org.springframework.ai + spring-ai-starter-model-deepseek + + + cn.hutool + hutool-core + 5.8.31 + + + cn.hutool + hutool-extra + 5.8.40 + diff --git a/src/main/java/com/kane/animo/config/AiConfigurer.java b/src/main/java/com/kane/animo/config/AiConfigurer.java new file mode 100644 index 0000000..83ba6d1 --- /dev/null +++ b/src/main/java/com/kane/animo/config/AiConfigurer.java @@ -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(); + } + +} diff --git a/src/main/java/com/kane/animo/model/R.java b/src/main/java/com/kane/animo/model/R.java index ea40df6..4bc69c9 100644 --- a/src/main/java/com/kane/animo/model/R.java +++ b/src/main/java/com/kane/animo/model/R.java @@ -13,6 +13,11 @@ public class R { private String message; private T data; private long timestamp; + + public static R of(boolean result){ + return result ? success() : error("操作失败"); + } + public static R success() { R r = new R<>(); r.setCode(200); diff --git a/src/main/java/com/kane/animo/record/controller/RecordController.java b/src/main/java/com/kane/animo/record/controller/RecordController.java new file mode 100644 index 0000000..20bb654 --- /dev/null +++ b/src/main/java/com/kane/animo/record/controller/RecordController.java @@ -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 add(@RequestBody RecordAddForm form){ + return R.success(service.add(form)); + } + + + /** + * 心情分析 + * @param id 心情ID + * @return 分析结果 + */ + @GetMapping("/analysis") + public R analysis(Long id){ + return R.success(service.analysis(id)); + } + +} diff --git a/src/main/java/com/kane/animo/record/domain/Record.java b/src/main/java/com/kane/animo/record/domain/Record.java new file mode 100644 index 0000000..79e6bc2 --- /dev/null +++ b/src/main/java/com/kane/animo/record/domain/Record.java @@ -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; + +} diff --git a/src/main/java/com/kane/animo/record/domain/form/RecordAddForm.java b/src/main/java/com/kane/animo/record/domain/form/RecordAddForm.java new file mode 100644 index 0000000..1c8afb4 --- /dev/null +++ b/src/main/java/com/kane/animo/record/domain/form/RecordAddForm.java @@ -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 +) { } diff --git a/src/main/java/com/kane/animo/record/domain/vo/RecordAnalysisVO.java b/src/main/java/com/kane/animo/record/domain/vo/RecordAnalysisVO.java new file mode 100644 index 0000000..34b0c3f --- /dev/null +++ b/src/main/java/com/kane/animo/record/domain/vo/RecordAnalysisVO.java @@ -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; +} diff --git a/src/main/java/com/kane/animo/record/mapper/RecordMapper.java b/src/main/java/com/kane/animo/record/mapper/RecordMapper.java new file mode 100644 index 0000000..c20071a --- /dev/null +++ b/src/main/java/com/kane/animo/record/mapper/RecordMapper.java @@ -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 { +} diff --git a/src/main/java/com/kane/animo/record/service/RecordService.java b/src/main/java/com/kane/animo/record/service/RecordService.java new file mode 100644 index 0000000..17ebf88 --- /dev/null +++ b/src/main/java/com/kane/animo/record/service/RecordService.java @@ -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); +} diff --git a/src/main/java/com/kane/animo/record/service/impl/RecordServiceImpl.java b/src/main/java/com/kane/animo/record/service/impl/RecordServiceImpl.java new file mode 100644 index 0000000..3d4bd8e --- /dev/null +++ b/src/main/java/com/kane/animo/record/service/impl/RecordServiceImpl.java @@ -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; + } +} diff --git a/src/main/java/com/kane/animo/util/AiUtil.java b/src/main/java/com/kane/animo/util/AiUtil.java new file mode 100644 index 0000000..e0c9e09 --- /dev/null +++ b/src/main/java/com/kane/animo/util/AiUtil.java @@ -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(); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8581ea..960dfcd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/test/java/com/kane/animo/AnimoApplicationTests.java b/src/test/java/com/kane/animo/AnimoApplicationTests.java index eb0a944..a043aa9 100644 --- a/src/test/java/com/kane/animo/AnimoApplicationTests.java +++ b/src/test/java/com/kane/animo/AnimoApplicationTests.java @@ -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); + } + }