|
@@ -227,12 +227,24 @@ public class ChatAttachmentService {
|
|
|
throw new ServerException(400, "附件不属于当前案件,拒绝访问: " + att.getFileName());
|
|
throw new ServerException(400, "附件不属于当前案件,拒绝访问: " + att.getFileName());
|
|
|
}
|
|
}
|
|
|
if (att.getSessionId() == null || att.getSessionId() == 0L) {
|
|
if (att.getSessionId() == null || att.getSessionId() == 0L) {
|
|
|
- // 草稿期上传的附件:发送时绑定到当前会话(此后按会话归属校验)
|
|
|
|
|
- attachmentMapper.update(null, Wrappers.<ChatAttachment>lambdaUpdate()
|
|
|
|
|
|
|
+ // 草稿期附件绑定:CAS 条件更新(session_id 仍是 0 才允许绑)——
|
|
|
|
|
+ // 多个会话/多端并发抢同一草稿附件时只有一方能绑成功,
|
|
|
|
|
+ // 后来者拿到 0 行更新就必须重读归属,绝不能沿用旧快照放行
|
|
|
|
|
+ int updated = attachmentMapper.update(null, Wrappers.<ChatAttachment>lambdaUpdate()
|
|
|
.eq(ChatAttachment::getId, att.getId())
|
|
.eq(ChatAttachment::getId, att.getId())
|
|
|
|
|
+ .and(w -> w.isNull(ChatAttachment::getSessionId).or().eq(ChatAttachment::getSessionId, 0L))
|
|
|
.set(ChatAttachment::getSessionId, sessionId)
|
|
.set(ChatAttachment::getSessionId, sessionId)
|
|
|
.set(ChatAttachment::getUpdateTime, LocalDateTime.now()));
|
|
.set(ChatAttachment::getUpdateTime, LocalDateTime.now()));
|
|
|
- att.setSessionId(sessionId);
|
|
|
|
|
|
|
+ if (updated == 0) {
|
|
|
|
|
+ // 被并发请求抢绑了:重读真实归属再判一次
|
|
|
|
|
+ ChatAttachment fresh = attachmentMapper.selectById(att.getId());
|
|
|
|
|
+ if (fresh == null || !sessionId.equals(fresh.getSessionId())) {
|
|
|
|
|
+ throw new ServerException(400, "附件已被其他消息使用,拒绝重复访问: " + att.getFileName());
|
|
|
|
|
+ }
|
|
|
|
|
+ att = fresh;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ att.setSessionId(sessionId);
|
|
|
|
|
+ }
|
|
|
} else if (!att.getSessionId().equals(sessionId)) {
|
|
} else if (!att.getSessionId().equals(sessionId)) {
|
|
|
throw new ServerException(400, "附件不属于当前会话,拒绝访问: " + att.getFileName());
|
|
throw new ServerException(400, "附件不属于当前会话,拒绝访问: " + att.getFileName());
|
|
|
}
|
|
}
|
|
@@ -378,10 +390,20 @@ public class ChatAttachmentService {
|
|
|
.lt(ChatAttachment::getCreateTime, deadline));
|
|
.lt(ChatAttachment::getCreateTime, deadline));
|
|
|
for (ChatAttachment att : orphans) {
|
|
for (ChatAttachment att : orphans) {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 条件删除先行(DB 原子判):仍须「未关联消息且超期」才真正删行 ——
|
|
|
|
|
+ // select 与删除之间用户可能刚刚发送(linkToMessage 抢先落库),
|
|
|
|
|
+ // 无条件删会把正在被消息引用的附件清掉,发送方就会撞上文件缺失
|
|
|
|
|
+ int deleted = attachmentMapper.delete(Wrappers.<ChatAttachment>lambdaQuery()
|
|
|
|
|
+ .eq(ChatAttachment::getId, att.getId())
|
|
|
|
|
+ .isNull(ChatAttachment::getLinkMessageId)
|
|
|
|
|
+ .lt(ChatAttachment::getCreateTime, deadline));
|
|
|
|
|
+ if (deleted == 0) {
|
|
|
|
|
+ continue; // 刚被关联/刚被处理,不删
|
|
|
|
|
+ }
|
|
|
|
|
+ // 行已删才动文件与向量(失败只影响磁盘残留,不影响数据正确性)
|
|
|
docIndexer.deleteAttachmentVectors(att.getId(), att.getCaseId());
|
|
docIndexer.deleteAttachmentVectors(att.getId(), att.getCaseId());
|
|
|
deleteQuietly(att.getFilePath());
|
|
deleteQuietly(att.getFilePath());
|
|
|
deleteQuietly(att.getSidecarPath());
|
|
deleteQuietly(att.getSidecarPath());
|
|
|
- attachmentMapper.deleteById(att.getId());
|
|
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
log.warn("孤儿附件清理失败: id={}, err={}", att.getId(), e.getMessage());
|
|
log.warn("孤儿附件清理失败: id={}, err={}", att.getId(), e.getMessage());
|
|
|
}
|
|
}
|