山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(八)
问题发现
在做功能自查时发现:调用健康陪诊、健康干预、家属辅诊这三个 Agent 的 DELETE /sessions/{sessionId} 接口,返回 200 但会话并没有真正被删除。再次调用 GET /sessions 列表,数据还在。
问题定位
跟踪 ChatSessionServiceImpl.deleteSession() 方法:
public void deleteSession(String sessionId) {
Path file = findSessionFile(sessionId);
if (file != null && Files.exists(file)) {
Files.delete(file);
}
}
逻辑本身没问题,问题在 findSessionFile:
private Path findSessionFile(String sessionId) {
String[] subDirs = {"medication-safety", "companion"};
for (String sub : subDirs) {
Path file = Path.of(storageDir, sub, sessionId + ".json");
if (Files.exists(file)) {
return file;
}
}
return null; // 找不到就返回null,deleteSession静默跳过
}
根因:这个方法是最早写的,当时只有 2 个 Agent。后来新增了 health-companion、health-intervention、family-assist 三个 Agent,但忘了更新这个目录列表。
同样的 bug 也影响了 getSession()——如果前端传了一个属于健康陪诊的 sessionId,这个方法也找不到对应文件,会抛"会话不存在"异常。但因为实际使用中用户很少跨 Agent 传 sessionId,所以 getSession 的问题没被先发现。
决策:修补还是重构
方案 A:修补 —— 把 subDirs 数组补全为 5 个目录。
- 优点:改动最小,1 行代码
- 缺点:下次新增 Agent 还会忘;文件存储本身有其他问题
方案 B:重构 —— 迁移到数据库存储。
- 优点:彻底消除按目录查找的模式;支持按用户筛选、分页;部署不依赖磁盘
- 缺点:改动面较大,涉及实体类、Service、各 Agent 调用处
选择了方案 B。理由:这个项目的其他数据(用药计划、健康数据、提醒记录)全部存在 PostgreSQL 中,对话记录单独用文件存储本身就是技术债。趁这次修 bug 一并还掉。
实现方案
表结构
-- 会话表
CREATE TABLE chat_session (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL UNIQUE, -- 前端使用的标识
agent_type VARCHAR(32) NOT NULL, -- 5种Agent类型
mode VARCHAR(32), -- 子模式
title VARCHAR(200), -- 首条消息摘要
elderly_id BIGINT, -- 归属哪个老人
family_id BIGINT, -- 归属哪个家属
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
-- 消息表
CREATE TABLE chat_message (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
role VARCHAR(16) NOT NULL, -- user 或 assistant
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT now(),
CONSTRAINT fk_msg_session FOREIGN KEY (session_id)
REFERENCES chat_session(session_id) ON DELETE CASCADE
);
ON DELETE CASCADE 是这次改造的核心——删除会话时消息自动级联删除,不需要应用层操心一致性。
实体类改造
两个实体类加上 MyBatis-Plus 注解。关键变化:
ChatSession:
- 新增数据库主键
id - 新增
elderlyId、familyId,为后续按用户查询铺路 messages标记@TableField(exist = false)——这是个内存字段,不映射到数据库列
ChatMessage:
- 新增数据库主键
id - 新增
sessionId做关联 timestamp改名createdAt,与全项目命名统一
Service 重写
原来的 ChatSessionServiceImpl:
- 依赖
ObjectMapper做 JSON 序列化/反序列化 - 依赖
java.nio.file做文件读写 @PostConstruct里创建目录findSessionFile遍历硬编码目录
新的实现:
- 依赖
ChatSessionMapper+ChatMessageMapper - 无状态,不需要初始化
- 所有操作通过
LambdaQueryWrapper精确定位
核心改动——deleteSession:
// 旧:遍历目录找文件,找不到就静默跳过
Path file = findSessionFile(sessionId);
if (file != null && Files.exists(file)) {
Files.delete(file);
}
// 新:直接按session_id删除,不存在也不报错
chatMessageMapper.delete(
new LambdaQueryWrapper<ChatMessage>().eq(ChatMessage::getSessionId, sessionId)
);
chatSessionMapper.delete(
new LambdaQueryWrapper<ChatSession>().eq(ChatSession::getSessionId, sessionId)
);
核心改动——saveSession(增量保存):
原来是每次把整个 session(含所有历史消息)全量序列化写文件。现在改为只插入新增的消息:
long existingCount = chatMessageMapper.selectCount(
new LambdaQueryWrapper<ChatMessage>().eq(ChatMessage::getSessionId, session.getSessionId())
);
List<ChatMessage> newMessages = allMessages.subList((int) existingCount, allMessages.size());
for (ChatMessage msg : newMessages) {
msg.setSessionId(session.getSessionId());
chatMessageMapper.insert(msg);
}
这样一次对话只写 2 条 INSERT(user 消息 + assistant 消息),而不是把整个对话历史重写一遍。
接口签名变更
ChatSessionService 接口从:
ChatSession createSession(String agentType, String mode);
改为:
ChatSession createSession(String agentType, String mode, Long elderlyId, Long familyId);
5 个 Agent 的 resolveSession 方法对应修改,把请求中的 elderlyId(家属辅诊还有 familyId)传进去。这样每个会话都记录了归属用户,后续可以实现"我的对话历史"功能。
影响面分析
本次改动涉及的文件:
| 文件 | 改动类型 | 说明 |
|---|---|---|
sql/chat_session_tables.sql |
新建 | 建表脚本 |
entity/ChatSession.java |
重写 | 加注解、加字段 |
entity/ChatMessage.java |
重写 | 加注解、加字段 |
mapper/ChatSessionMapper.java |
新建 | MyBatis-Plus Mapper |
mapper/ChatMessageMapper.java |
新建 | MyBatis-Plus Mapper |
service/ChatSessionService.java |
修改 | 接口签名变更 |
service/impl/ChatSessionServiceImpl.java |
重写 | 文件→数据库 |
| 5 个 Agent 的 ServiceImpl | 修改 | createSession 调用处 |
application.yml |
修改 | 删除 storage-dir 配置 |
Controller 层完全不需要改动——接口签名和返回结构没变,前端无感知。
验证清单
- [ ] 执行 SQL 建表
- [ ] 每个 Agent 创建新对话 → 检查 chat_session 表有记录
- [ ] 发送消息 → 检查 chat_message 表有 user + assistant 两条记录
- [ ] 多轮对话 → 消息按时间顺序正确排列
- [ ] 删除会话 → chat_session 和 chat_message 均被清除
- [ ] 获取会话列表 → 只返回对应 agent_type 的会话
- [ ] 获取会话详情 → 包含完整消息历史
经验总结
- 硬编码枚举是定时炸弹。
subDirs数组没有和 Agent 注册逻辑关联,新增 Agent 时没有任何编译期或运行期提示需要更新它。 - 静默失败比报错更危险。
findSessionFile返回 null 后,deleteSession直接跳过不报错,用户以为删除成功了。如果当初返回 null 时抛异常,这个 bug 会更早被发现。 - 存储方案要和项目其他部分保持一致。项目的其他数据都在 PostgreSQL,对话记录单独用文件是为了"快速实现",但最终还是要还债。
- 迁移时顺手把接口签名改对。既然要动 Service 层,顺便把
elderlyId传进去,比之后再做二次改动成本低。
更多推荐




所有评论(0)