package cron import ( "context" "encoding/json" "fmt" "net/url" "os" "strconv" "strings" "time" "git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct" mgo "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model" "git.imall.cloud/openim/protocol/constant" "git.imall.cloud/openim/protocol/msg" "git.imall.cloud/openim/protocol/sdkws" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" ) // clearGroupMsg 清理群聊消息 // 注意:每次执行时都会重新从数据库读取配置,确保配置的实时性 func (c *cronServer) clearGroupMsg() { now := time.Now() operationID := fmt.Sprintf("cron_clear_group_msg_%d_%d", os.Getpid(), now.UnixMilli()) ctx := mcontext.SetOperationID(c.ctx, operationID) log.ZDebug(ctx, "[CLEAR_MSG] 定时任务触发:检查清理群聊消息配置", "operationID", operationID, "time", now.Format("2006-01-02 15:04:05")) // 每次执行时都重新读取配置,确保获取最新的配置值 config, err := c.systemConfigDB.FindByKey(ctx, "clear_group_msg") if err != nil { log.ZError(ctx, "[CLEAR_MSG] 读取配置失败", err, "key", "clear_group_msg") return } // 如果配置不存在,跳过执行 if config == nil { log.ZDebug(ctx, "[CLEAR_MSG] 配置不存在,跳过执行", "key", "clear_group_msg") return } // 记录从数据库读取到的配置详细信息(用于排查问题) log.ZInfo(ctx, "[CLEAR_MSG] 从数据库读取到群聊清理配置", "key", config.Key, "value", config.Value, "enabled", config.Enabled, "title", config.Title, "valueType", config.ValueType, "createTime", config.CreateTime.Format("2006-01-02 15:04:05"), "updateTime", config.UpdateTime.Format("2006-01-02 15:04:05")) // 如果配置未启用,跳过执行 if !config.Enabled { log.ZDebug(ctx, "[CLEAR_MSG] 配置未启用,跳过执行", "key", config.Key, "enabled", config.Enabled) return } log.ZInfo(ctx, "[CLEAR_MSG] ====== 开始执行清理群聊消息任务 ======", "key", config.Key, "value", config.Value, "enabled", config.Enabled) // 值为空也跳过,避免解析错误 if strings.TrimSpace(config.Value) == "" { log.ZInfo(ctx, "[CLEAR_MSG] 配置值为空,跳过执行", "key", config.Key) return } log.ZInfo(ctx, "[CLEAR_MSG] 配置读取成功,配置已启用", "key", config.Key, "value", config.Value, "enabled", config.Enabled) // 解析配置值(单位:分钟) minutes, err := strconv.ParseInt(config.Value, 10, 64) if err != nil { log.ZError(ctx, "[CLEAR_MSG] 解析配置值失败", err, "value", config.Value) return } if minutes <= 0 { log.ZInfo(ctx, "[CLEAR_MSG] 配置分钟数无效,跳过执行", "minutes", minutes) return } // 计算删除时间点:查询当前时间减去配置分钟数之前的消息 // 例如:配置30分钟,当前时间09:35:00,则查询09:05:00之前的所有消息 deltime := now.Add(-time.Duration(minutes) * time.Minute) log.ZInfo(ctx, "[CLEAR_MSG] 配置检查通过,开始查询消息", "配置分钟数", minutes, "当前时间", now.Format("2006-01-02 15:04:05"), "查询时间点", deltime.Format("2006-01-02 15:04:05"), "查询时间戳", deltime.UnixMilli(), "说明", fmt.Sprintf("将查询send_time <= %d (即%s之前)的所有消息", deltime.UnixMilli(), deltime.Format("2006-01-02 15:04:05"))) const ( deleteCount = 10000 deleteLimit = 50 ) var totalCount int var fileDeleteCount int for i := 1; i <= deleteCount; i++ { ctx := mcontext.SetOperationID(c.ctx, fmt.Sprintf("%s_%d", operationID, i)) // 先查询消息,提取文件信息并删除S3文件 log.ZInfo(ctx, "[CLEAR_MSG] 开始查询消息", "iteration", i, "timestamp", deltime.UnixMilli(), "limit", deleteLimit, "deltime", deltime.Format("2006-01-02 15:04:05")) docs, err := c.msgDocDB.GetRandBeforeMsg(ctx, deltime.UnixMilli(), deleteLimit) if err != nil { log.ZError(ctx, "[CLEAR_MSG] 查询消息失败", err, "iteration", i, "timestamp", deltime.UnixMilli()) break } log.ZInfo(ctx, "[CLEAR_MSG] 查询消息结果", "iteration", i, "docCount", len(docs), "timestamp", deltime.UnixMilli()) if len(docs) == 0 { log.ZInfo(ctx, "[CLEAR_MSG] 没有更多消息需要删除", "iteration", i) break } // 处理每个文档中的消息,提取文件信息并删除 // 同时收集要删除的消息信息(conversationID -> seqs),用于发送通知 var processedDocs int var deletedDocCount int conversationSeqsMap := make(map[string][]int64) // conversationID -> []seq conversationDocsMap := make(map[string]*model.MsgDocModel) // conversationID -> doc docIDsToDelete := make([]string, 0, len(docs)) // 收集需要删除的文档ID log.ZInfo(ctx, "[CLEAR_MSG] 开始处理文档", "iteration", i, "totalDocs", len(docs)) for docIdx, doc := range docs { log.ZInfo(ctx, "[CLEAR_MSG] 处理文档", "iteration", i, "docIndex", docIdx+1, "totalDocs", len(docs), "docID", doc.DocID) // 判断是否为群聊消息 conversationID := extractConversationID(doc.DocID) log.ZInfo(ctx, "[CLEAR_MSG] 提取会话ID", "docID", doc.DocID, "conversationID", conversationID, "isGroup", isGroupConversationID(conversationID)) if !isGroupConversationID(conversationID) { log.ZInfo(ctx, "[CLEAR_MSG] 跳过非群聊消息", "docID", doc.DocID, "conversationID", conversationID) continue } // 获取完整的消息内容 log.ZInfo(ctx, "[CLEAR_MSG] 获取完整消息文档", "docID", doc.DocID) fullDoc, err := c.msgDocDB.FindOneByDocID(ctx, doc.DocID) if err != nil { log.ZWarn(ctx, "[CLEAR_MSG] 获取完整消息文档失败", err, "docID", doc.DocID) continue } log.ZInfo(ctx, "[CLEAR_MSG] 获取完整消息文档成功", "docID", doc.DocID, "msgCount", len(fullDoc.Msg)) // 收集要删除的消息seq(只收集send_time <= deltime的消息) var seqs []int64 var beforeTimeCount int var afterTimeCount int log.ZInfo(ctx, "[CLEAR_MSG] 开始收集消息seq", "docID", doc.DocID, "msgCount", len(fullDoc.Msg), "查询时间戳", deltime.UnixMilli()) for msgIdx, msgInfo := range fullDoc.Msg { if msgInfo.Msg != nil { isBeforeTime := msgInfo.Msg.SendTime <= deltime.UnixMilli() if isBeforeTime { beforeTimeCount++ } else { afterTimeCount++ } log.ZInfo(ctx, "[CLEAR_MSG] 处理消息", "docID", doc.DocID, "msgIndex", msgIdx+1, "totalMsgs", len(fullDoc.Msg), "seq", msgInfo.Msg.Seq, "sendID", msgInfo.Msg.SendID, "contentType", msgInfo.Msg.ContentType, "sendTime", msgInfo.Msg.SendTime, "sendTimeFormatted", time.Unix(msgInfo.Msg.SendTime/1000, 0).Format("2006-01-02 15:04:05"), "查询时间戳", deltime.UnixMilli(), "是否在查询时间点之前", isBeforeTime) if msgInfo.Msg.Seq > 0 && isBeforeTime { seqs = append(seqs, msgInfo.Msg.Seq) } } else { log.ZWarn(ctx, "[CLEAR_MSG] 消息数据为空", nil, "docID", doc.DocID, "msgIndex", msgIdx+1) } } log.ZInfo(ctx, "[CLEAR_MSG] 收集消息seq完成", "docID", doc.DocID, "seqCount", len(seqs), "seqs", seqs, "在查询时间点之前的消息数", beforeTimeCount, "在查询时间点之后的消息数", afterTimeCount, "说明", fmt.Sprintf("文档中有%d条消息在查询时间点之前,%d条消息在查询时间点之后", beforeTimeCount, afterTimeCount)) if len(seqs) > 0 { conversationSeqsMap[conversationID] = append(conversationSeqsMap[conversationID], seqs...) conversationDocsMap[conversationID] = fullDoc log.ZInfo(ctx, "[CLEAR_MSG] 已添加到通知列表", "conversationID", conversationID, "totalSeqs", len(conversationSeqsMap[conversationID])) } // 提取文件信息并删除S3文件 deletedFiles := c.extractAndDeleteFiles(ctx, fullDoc, true) // true表示只处理群聊消息 fileDeleteCount += deletedFiles // 如果文档中所有消息都在查询时间点之前,则删除整个文档 // 如果文档中只有部分消息在查询时间点之前,则只删除那些消息(通过DeleteMsgsPhysicalBySeqs) if afterTimeCount == 0 { // 文档中所有消息都需要删除,删除整个文档 docIDsToDelete = append(docIDsToDelete, doc.DocID) log.ZInfo(ctx, "[CLEAR_MSG] 文档标记为删除(所有消息都在查询时间点之前)", "docID", doc.DocID, "beforeTimeCount", beforeTimeCount) } else { // 文档中只有部分消息需要删除,使用RPC调用DeleteMsgPhysicalBySeq删除指定消息 if len(seqs) > 0 { log.ZInfo(ctx, "[CLEAR_MSG] 开始删除文档中的部分消息", "docID", doc.DocID, "conversationID", conversationID, "seqs", seqs) _, err := c.msgClient.DeleteMsgPhysicalBySeq(ctx, &msg.DeleteMsgPhysicalBySeqReq{ ConversationID: conversationID, Seqs: seqs, }) if err != nil { log.ZError(ctx, "[CLEAR_MSG] 删除文档中的部分消息失败", err, "docID", doc.DocID, "conversationID", conversationID, "seqs", seqs) } else { log.ZInfo(ctx, "[CLEAR_MSG] 删除文档中的部分消息成功", "docID", doc.DocID, "conversationID", conversationID, "seqCount", len(seqs)) totalCount += len(seqs) } } } processedDocs++ } if processedDocs > 0 { log.ZInfo(ctx, "[CLEAR_MSG] 文档处理完成(群聊)", "processedDocs", processedDocs, "totalDocs", len(docs), "deletedFiles", fileDeleteCount, "docIDsToDelete", len(docIDsToDelete), "iteration", i) } // 删除整个文档(如果文档中所有消息都在查询时间点之前) if len(docIDsToDelete) > 0 { log.ZInfo(ctx, "[CLEAR_MSG] 开始删除整个文档", "iteration", i, "docCount", len(docIDsToDelete)) for _, docID := range docIDsToDelete { if err := c.msgDocDB.DeleteDoc(ctx, docID); err != nil { log.ZError(ctx, "[CLEAR_MSG] 删除文档失败", err, "docID", docID) } else { deletedDocCount++ totalCount++ // 每个文档算作一条删除记录 log.ZInfo(ctx, "[CLEAR_MSG] 删除文档成功", "docID", docID) } } log.ZInfo(ctx, "[CLEAR_MSG] 批次删除文档完成", "deletedDocCount", deletedDocCount, "totalDocCount", len(docIDsToDelete), "totalCount", totalCount, "iteration", i) } // 发送删除通知 if len(conversationSeqsMap) > 0 { c.sendDeleteNotifications(ctx, conversationSeqsMap, conversationDocsMap, true) } if deletedDocCount < deleteLimit && len(docIDsToDelete) == 0 { log.ZInfo(ctx, "[CLEAR_MSG] 已处理完所有消息", "lastBatchCount", deletedDocCount) break } } log.ZInfo(ctx, "[CLEAR_MSG] ====== 清理群聊消息任务完成 ======", "deltime", deltime.Format("2006-01-02 15:04:05"), "duration", time.Since(now), "totalCount", totalCount, "fileDeleteCount", fileDeleteCount, "operationID", operationID) } // clearUserMsg 清理个人聊天消息 // 注意:每次执行时都会重新从数据库读取配置,确保配置的实时性 func (c *cronServer) clearUserMsg() { now := time.Now() operationID := fmt.Sprintf("cron_clear_user_msg_%d_%d", os.Getpid(), now.UnixMilli()) ctx := mcontext.SetOperationID(c.ctx, operationID) log.ZDebug(ctx, "[CLEAR_MSG] 定时任务触发:检查清理个人聊天消息配置", "operationID", operationID, "time", now.Format("2006-01-02 15:04:05")) // 每次执行时都重新读取配置,确保获取最新的配置值 config, err := c.systemConfigDB.FindByKey(ctx, "clear_user_msg") if err != nil { log.ZError(ctx, "[CLEAR_MSG] 读取配置失败", err, "key", "clear_user_msg") return } // 如果配置不存在,跳过执行 if config == nil { log.ZDebug(ctx, "[CLEAR_MSG] 配置不存在,跳过执行", "key", "clear_user_msg") return } // 记录从数据库读取到的配置详细信息(用于排查问题) log.ZInfo(ctx, "[CLEAR_MSG] 从数据库读取到个人消息清理配置", "key", config.Key, "value", config.Value, "enabled", config.Enabled, "title", config.Title, "valueType", config.ValueType, "createTime", config.CreateTime.Format("2006-01-02 15:04:05"), "updateTime", config.UpdateTime.Format("2006-01-02 15:04:05")) // 如果配置未启用,跳过执行 if !config.Enabled { log.ZDebug(ctx, "[CLEAR_MSG] 配置未启用,跳过执行", "key", config.Key, "enabled", config.Enabled) return } log.ZInfo(ctx, "[CLEAR_MSG] ====== 开始执行清理个人聊天消息任务 ======", "key", config.Key, "value", config.Value, "enabled", config.Enabled) // 值为空也跳过,避免解析错误 if strings.TrimSpace(config.Value) == "" { log.ZInfo(ctx, "[CLEAR_MSG] 配置值为空,跳过执行", "key", config.Key) return } log.ZInfo(ctx, "[CLEAR_MSG] 配置读取成功,配置已启用", "key", config.Key, "value", config.Value, "enabled", config.Enabled) // 解析配置值(单位:分钟) minutes, err := strconv.ParseInt(config.Value, 10, 64) if err != nil { log.ZError(ctx, "[CLEAR_MSG] 解析配置值失败", err, "value", config.Value) return } if minutes <= 0 { log.ZInfo(ctx, "[CLEAR_MSG] 配置分钟数无效,跳过执行", "minutes", minutes) return } // 计算删除时间点:查询当前时间减去配置分钟数之前的消息 // 例如:配置30分钟,当前时间09:35:00,则查询09:05:00之前的所有消息 deltime := now.Add(-time.Duration(minutes) * time.Minute) log.ZInfo(ctx, "[CLEAR_MSG] 配置检查通过,开始查询消息(个人聊天)", "配置分钟数", minutes, "当前时间", now.Format("2006-01-02 15:04:05"), "查询时间点", deltime.Format("2006-01-02 15:04:05"), "查询时间戳", deltime.UnixMilli(), "说明", fmt.Sprintf("将查询send_time <= %d (即%s之前)的所有消息", deltime.UnixMilli(), deltime.Format("2006-01-02 15:04:05"))) const ( deleteCount = 10000 deleteLimit = 50 ) var totalCount int var fileDeleteCount int for i := 1; i <= deleteCount; i++ { ctx := mcontext.SetOperationID(c.ctx, fmt.Sprintf("%s_%d", operationID, i)) // 先查询消息,提取文件信息并删除S3文件 log.ZInfo(ctx, "[CLEAR_MSG] 开始查询消息(个人聊天)", "iteration", i, "timestamp", deltime.UnixMilli(), "limit", deleteLimit, "deltime", deltime.Format("2006-01-02 15:04:05")) docs, err := c.msgDocDB.GetRandBeforeMsg(ctx, deltime.UnixMilli(), deleteLimit) if err != nil { log.ZError(ctx, "[CLEAR_MSG] 查询消息失败(个人聊天)", err, "iteration", i, "timestamp", deltime.UnixMilli()) break } log.ZInfo(ctx, "[CLEAR_MSG] 查询消息结果(个人聊天)", "iteration", i, "docCount", len(docs), "timestamp", deltime.UnixMilli()) if len(docs) == 0 { log.ZInfo(ctx, "[CLEAR_MSG] 没有更多消息需要删除(个人聊天)", "iteration", i) break } // 处理每个文档中的消息,提取文件信息并删除 // 同时收集要删除的消息信息(conversationID -> seqs),用于发送通知 var processedDocs int var deletedDocCount int conversationSeqsMap := make(map[string][]int64) // conversationID -> []seq conversationDocsMap := make(map[string]*model.MsgDocModel) // conversationID -> doc docIDsToDelete := make([]string, 0, len(docs)) // 收集需要删除的文档ID log.ZInfo(ctx, "[CLEAR_MSG] 开始处理文档(个人聊天)", "iteration", i, "totalDocs", len(docs)) for docIdx, doc := range docs { log.ZInfo(ctx, "[CLEAR_MSG] 处理文档(个人聊天)", "iteration", i, "docIndex", docIdx+1, "totalDocs", len(docs), "docID", doc.DocID) // 判断是否为个人聊天消息 conversationID := extractConversationID(doc.DocID) log.ZInfo(ctx, "[CLEAR_MSG] 提取会话ID(个人聊天)", "docID", doc.DocID, "conversationID", conversationID, "isSingle", isSingleConversationID(conversationID)) if !isSingleConversationID(conversationID) { log.ZInfo(ctx, "[CLEAR_MSG] 跳过非个人聊天消息", "docID", doc.DocID, "conversationID", conversationID) continue } // 获取完整的消息内容 log.ZInfo(ctx, "[CLEAR_MSG] 获取完整消息文档(个人聊天)", "docID", doc.DocID) fullDoc, err := c.msgDocDB.FindOneByDocID(ctx, doc.DocID) if err != nil { log.ZWarn(ctx, "[CLEAR_MSG] 获取完整消息文档失败(个人聊天)", err, "docID", doc.DocID) continue } log.ZInfo(ctx, "[CLEAR_MSG] 获取完整消息文档成功(个人聊天)", "docID", doc.DocID, "msgCount", len(fullDoc.Msg)) // 收集要删除的消息seq(只收集send_time <= deltime的消息) var seqs []int64 var beforeTimeCount int var afterTimeCount int log.ZInfo(ctx, "[CLEAR_MSG] 开始收集消息seq(个人聊天)", "docID", doc.DocID, "msgCount", len(fullDoc.Msg), "查询时间戳", deltime.UnixMilli()) for msgIdx, msgInfo := range fullDoc.Msg { if msgInfo.Msg != nil { isBeforeTime := msgInfo.Msg.SendTime <= deltime.UnixMilli() if isBeforeTime { beforeTimeCount++ } else { afterTimeCount++ } log.ZInfo(ctx, "[CLEAR_MSG] 处理消息(个人聊天)", "docID", doc.DocID, "msgIndex", msgIdx+1, "totalMsgs", len(fullDoc.Msg), "seq", msgInfo.Msg.Seq, "sendID", msgInfo.Msg.SendID, "contentType", msgInfo.Msg.ContentType, "sendTime", msgInfo.Msg.SendTime, "sendTimeFormatted", time.Unix(msgInfo.Msg.SendTime/1000, 0).Format("2006-01-02 15:04:05"), "查询时间戳", deltime.UnixMilli(), "是否在查询时间点之前", isBeforeTime) if msgInfo.Msg.Seq > 0 && isBeforeTime { seqs = append(seqs, msgInfo.Msg.Seq) } } else { log.ZWarn(ctx, "[CLEAR_MSG] 消息数据为空(个人聊天)", nil, "docID", doc.DocID, "msgIndex", msgIdx+1) } } log.ZInfo(ctx, "[CLEAR_MSG] 收集消息seq完成(个人聊天)", "docID", doc.DocID, "seqCount", len(seqs), "seqs", seqs, "在查询时间点之前的消息数", beforeTimeCount, "在查询时间点之后的消息数", afterTimeCount, "说明", fmt.Sprintf("文档中有%d条消息在查询时间点之前,%d条消息在查询时间点之后", beforeTimeCount, afterTimeCount)) if len(seqs) > 0 { conversationSeqsMap[conversationID] = append(conversationSeqsMap[conversationID], seqs...) conversationDocsMap[conversationID] = fullDoc log.ZInfo(ctx, "[CLEAR_MSG] 已添加到通知列表(个人聊天)", "conversationID", conversationID, "totalSeqs", len(conversationSeqsMap[conversationID])) } // 提取文件信息并删除S3文件 deletedFiles := c.extractAndDeleteFiles(ctx, fullDoc, false) // false表示只处理个人聊天消息 fileDeleteCount += deletedFiles // 如果文档中所有消息都在查询时间点之前,则删除整个文档 // 如果文档中只有部分消息在查询时间点之前,则只删除那些消息(通过DeleteMsgPhysicalBySeq) if afterTimeCount == 0 { // 文档中所有消息都需要删除,删除整个文档 docIDsToDelete = append(docIDsToDelete, doc.DocID) log.ZInfo(ctx, "[CLEAR_MSG] 文档标记为删除(所有消息都在查询时间点之前)(个人聊天)", "docID", doc.DocID, "beforeTimeCount", beforeTimeCount) } else { // 文档中只有部分消息需要删除,使用RPC调用DeleteMsgPhysicalBySeq删除指定消息 if len(seqs) > 0 { log.ZInfo(ctx, "[CLEAR_MSG] 开始删除文档中的部分消息(个人聊天)", "docID", doc.DocID, "conversationID", conversationID, "seqs", seqs) _, err := c.msgClient.DeleteMsgPhysicalBySeq(ctx, &msg.DeleteMsgPhysicalBySeqReq{ ConversationID: conversationID, Seqs: seqs, }) if err != nil { log.ZError(ctx, "[CLEAR_MSG] 删除文档中的部分消息失败(个人聊天)", err, "docID", doc.DocID, "conversationID", conversationID, "seqs", seqs) } else { log.ZInfo(ctx, "[CLEAR_MSG] 删除文档中的部分消息成功(个人聊天)", "docID", doc.DocID, "conversationID", conversationID, "seqCount", len(seqs)) totalCount += len(seqs) } } } processedDocs++ } if processedDocs > 0 { log.ZInfo(ctx, "[CLEAR_MSG] 文档处理完成(个人)", "processedDocs", processedDocs, "totalDocs", len(docs), "deletedFiles", fileDeleteCount, "docIDsToDelete", len(docIDsToDelete), "iteration", i) } // 删除整个文档(如果文档中所有消息都在查询时间点之前) if len(docIDsToDelete) > 0 { log.ZInfo(ctx, "[CLEAR_MSG] 开始删除整个文档(个人聊天)", "iteration", i, "docCount", len(docIDsToDelete)) for _, docID := range docIDsToDelete { if err := c.msgDocDB.DeleteDoc(ctx, docID); err != nil { log.ZError(ctx, "[CLEAR_MSG] 删除文档失败(个人聊天)", err, "docID", docID) } else { deletedDocCount++ totalCount++ // 每个文档算作一条删除记录 log.ZInfo(ctx, "[CLEAR_MSG] 删除文档成功(个人聊天)", "docID", docID) } } log.ZInfo(ctx, "[CLEAR_MSG] 批次删除文档完成(个人聊天)", "deletedDocCount", deletedDocCount, "totalDocCount", len(docIDsToDelete), "totalCount", totalCount, "iteration", i) } // 发送删除通知 if len(conversationSeqsMap) > 0 { c.sendDeleteNotifications(ctx, conversationSeqsMap, conversationDocsMap, false) } if deletedDocCount < deleteLimit && len(docIDsToDelete) == 0 { log.ZInfo(ctx, "[CLEAR_MSG] 已处理完所有消息(个人聊天)", "lastBatchCount", deletedDocCount) break } } log.ZInfo(ctx, "[CLEAR_MSG] ====== 清理个人聊天消息任务完成 ======", "deltime", deltime.Format("2006-01-02 15:04:05"), "duration", time.Since(now), "totalCount", totalCount, "fileDeleteCount", fileDeleteCount, "operationID", operationID) } // isGroupConversationID 判断是否为群聊会话ID func isGroupConversationID(conversationID string) bool { return strings.HasPrefix(conversationID, "g_") || strings.HasPrefix(conversationID, "sg_") } // isSingleConversationID 判断是否为个人聊天会话ID func isSingleConversationID(conversationID string) bool { return strings.HasPrefix(conversationID, "si_") } // extractConversationID 从docID中提取conversationID func extractConversationID(docID string) string { index := strings.LastIndex(docID, ":") if index < 0 { return "" } return docID[:index] } // extractAndDeleteFiles 从消息中提取文件信息并删除S3文件 // isGroupMsg: true表示只处理群聊消息,false表示只处理个人聊天消息 func (c *cronServer) extractAndDeleteFiles(ctx context.Context, doc *model.MsgDocModel, isGroupMsg bool) int { if doc == nil || len(doc.Msg) == 0 { return 0 } // 判断conversationID类型 conversationID := extractConversationID(doc.DocID) if isGroupMsg && !isGroupConversationID(conversationID) { return 0 } if !isGroupMsg && !isSingleConversationID(conversationID) { return 0 } var fileNames []string fileNamesMap := make(map[string]bool) // 用于去重 var fileTypeStats = map[string]int{ "picture": 0, "video": 0, "file": 0, "voice": 0, } // 遍历消息,提取文件信息 totalMsgs := len(doc.Msg) var processedMsgs int for _, msgInfo := range doc.Msg { if msgInfo.Msg == nil { continue } contentType := msgInfo.Msg.ContentType content := msgInfo.Msg.Content processedMsgs++ // 根据消息类型提取文件URL switch contentType { case constant.Picture: // 图片消息 var pictureElem apistruct.PictureElem if err := json.Unmarshal([]byte(content), &pictureElem); err == nil { var extractedCount int if pictureElem.SourcePicture.Url != "" { if name := extractFileNameFromURL(pictureElem.SourcePicture.Url); name != "" { fileNamesMap[name] = true extractedCount++ } } if pictureElem.BigPicture.Url != "" { if name := extractFileNameFromURL(pictureElem.BigPicture.Url); name != "" { fileNamesMap[name] = true extractedCount++ } } if pictureElem.SnapshotPicture.Url != "" { if name := extractFileNameFromURL(pictureElem.SnapshotPicture.Url); name != "" { fileNamesMap[name] = true extractedCount++ } } if extractedCount > 0 { fileTypeStats["picture"]++ } } else { log.ZDebug(ctx, "[CLEAR_MSG] 解析图片消息失败", "err", err, "seq", msgInfo.Msg.Seq) } case constant.Video: // 视频消息 var videoElem apistruct.VideoElem if err := json.Unmarshal([]byte(content), &videoElem); err == nil { var extractedCount int if videoElem.VideoURL != "" { if name := extractFileNameFromURL(videoElem.VideoURL); name != "" { fileNamesMap[name] = true extractedCount++ } } if videoElem.SnapshotURL != "" { if name := extractFileNameFromURL(videoElem.SnapshotURL); name != "" { fileNamesMap[name] = true extractedCount++ } } if extractedCount > 0 { fileTypeStats["video"]++ } } else { log.ZDebug(ctx, "[CLEAR_MSG] 解析视频消息失败", "err", err, "seq", msgInfo.Msg.Seq) } case constant.File: // 文件消息 var fileElem apistruct.FileElem if err := json.Unmarshal([]byte(content), &fileElem); err == nil { if fileElem.SourceURL != "" { if name := extractFileNameFromURL(fileElem.SourceURL); name != "" { fileNamesMap[name] = true fileTypeStats["file"]++ } } } else { log.ZDebug(ctx, "[CLEAR_MSG] 解析文件消息失败", "err", err, "seq", msgInfo.Msg.Seq) } case constant.Voice: // 音频消息 var soundElem apistruct.SoundElem if err := json.Unmarshal([]byte(content), &soundElem); err == nil { if soundElem.SourceURL != "" { if name := extractFileNameFromURL(soundElem.SourceURL); name != "" { fileNamesMap[name] = true fileTypeStats["voice"]++ } } } else { log.ZDebug(ctx, "[CLEAR_MSG] 解析音频消息失败", "err", err, "seq", msgInfo.Msg.Seq) } } } log.ZInfo(ctx, "[CLEAR_MSG] 消息处理统计", "docID", doc.DocID, "totalMsgs", totalMsgs, "processedMsgs", processedMsgs, "fileTypeStats", fileTypeStats) // 将map转换为slice for name := range fileNamesMap { fileNames = append(fileNames, name) } if len(fileNames) == 0 { log.ZDebug(ctx, "[CLEAR_MSG] 消息中未找到文件", "docID", doc.DocID) return 0 } log.ZInfo(ctx, "[CLEAR_MSG] 提取到文件列表", "docID", doc.DocID, "conversationID", conversationID, "fileCount", len(fileNames), "fileNames", fileNames[:min(10, len(fileNames))]) // 删除S3文件 // 通过objectDB查询文件信息,然后删除数据库记录 // 直接按文件名查询(不指定engine),再使用记录中的engine/key处理 deletedCount := 0 notFoundCount := 0 failedCount := 0 var deletedFiles []string var notFoundFiles []string log.ZInfo(ctx, "[CLEAR_MSG] 开始删除文件记录", "docID", doc.DocID, "totalFiles", len(fileNames)) for i, fileName := range fileNames { obj, err := c.objectDB.Take(ctx, "", fileName) if err != nil || obj == nil { // 检查是否是"未找到"错误(正常情况)还是真正的错误 if err != nil && !mgo.IsNotFound(err) { // 真正的错误,记录为警告 log.ZWarn(ctx, "[CLEAR_MSG] 查询文件记录出错", err, "fileName", fileName, "index", i+1, "total", len(fileNames)) } else { // 文件不存在是正常情况,只记录debug日志 log.ZDebug(ctx, "[CLEAR_MSG] 文件记录不存在(正常)", "fileName", fileName, "index", i+1, "total", len(fileNames)) } notFoundCount++ notFoundFiles = append(notFoundFiles, fileName) continue } engine := obj.Engine // 在删除前获取key引用计数,用于判断是否需要删除S3文件 keyCountBeforeDelete, err := c.objectDB.GetKeyCount(ctx, engine, obj.Key) if err != nil { log.ZWarn(ctx, "[CLEAR_MSG] 获取key引用计数失败", err, "engine", engine, "key", obj.Key, "fileName", fileName) keyCountBeforeDelete = 0 // 如果获取失败,假设为0,后续会尝试删除 } log.ZInfo(ctx, "[CLEAR_MSG] 准备删除文件记录", "engine", engine, "fileName", fileName, "key", obj.Key, "index", i+1, "total", len(fileNames), "size", obj.Size, "contentType", obj.ContentType, "keyCountBeforeDelete", keyCountBeforeDelete) // 删除数据库记录 if err := c.objectDB.Delete(ctx, engine, []string{fileName}); err != nil { failedCount++ log.ZWarn(ctx, "[CLEAR_MSG] 删除文件记录失败", err, "engine", engine, "fileName", fileName, "key", obj.Key, "index", i+1, "total", len(fileNames)) continue } deletedCount++ deletedFiles = append(deletedFiles, fileName) // 删除数据库记录后,再次检查key引用计数 keyCountAfterDelete, err := c.objectDB.GetKeyCount(ctx, engine, obj.Key) if err != nil { log.ZWarn(ctx, "[CLEAR_MSG] 删除后获取key引用计数失败", err, "engine", engine, "key", obj.Key, "fileName", fileName) } // 删除缓存 if c.s3Cache != nil { if err := c.s3Cache.DelS3Key(ctx, engine, fileName); err != nil { log.ZWarn(ctx, "[CLEAR_MSG] 删除S3缓存失败", err, "engine", engine, "fileName", fileName) } else { log.ZInfo(ctx, "[CLEAR_MSG] S3缓存删除成功", "engine", engine, "fileName", fileName) } } // 如果删除前引用计数<=1,说明删除后应该为0,S3文件应该被删除 if keyCountBeforeDelete <= 1 { // 删除S3文件 if c.s3Client != nil { if err := c.s3Client.DeleteObject(ctx, obj.Key); err != nil { log.ZWarn(ctx, "[CLEAR_MSG] 删除S3文件失败", err, "engine", engine, "key", obj.Key, "fileName", fileName) } else { log.ZInfo(ctx, "[CLEAR_MSG] S3文件删除成功", "engine", engine, "key", obj.Key, "fileName", fileName, "keyCountBeforeDelete", keyCountBeforeDelete, "keyCountAfterDelete", keyCountAfterDelete) } } else { log.ZWarn(ctx, "[CLEAR_MSG] S3客户端未初始化,无法删除S3文件", nil, "engine", engine, "key", obj.Key, "fileName", fileName, "keyCountBeforeDelete", keyCountBeforeDelete, "keyCountAfterDelete", keyCountAfterDelete) } } else { log.ZInfo(ctx, "[CLEAR_MSG] 文件key仍有其他引用,S3文件保留", "engine", engine, "key", obj.Key, "fileName", fileName, "keyCountBeforeDelete", keyCountBeforeDelete, "keyCountAfterDelete", keyCountAfterDelete) } log.ZInfo(ctx, "[CLEAR_MSG] 文件记录删除成功", "engine", engine, "fileName", fileName, "key", obj.Key, "index", i+1, "total", len(fileNames), "size", obj.Size, "contentType", obj.ContentType) } log.ZInfo(ctx, "[CLEAR_MSG] 文件删除汇总", "docID", doc.DocID, "conversationID", conversationID, "totalFiles", len(fileNames), "deletedCount", deletedCount, "notFoundCount", notFoundCount, "failedCount", failedCount, "deletedFiles", deletedFiles[:min(5, len(deletedFiles))], "notFoundFiles", notFoundFiles[:min(5, len(notFoundFiles))]) return deletedCount } // extractFileNameFromURL 从URL中提取文件名 func extractFileNameFromURL(fileURL string) string { if fileURL == "" { return "" } // 解析URL parsedURL, err := url.Parse(fileURL) if err != nil { // 如果解析失败,尝试从URL路径中提取 parts := strings.Split(fileURL, "/") if len(parts) > 0 { lastPart := parts[len(parts)-1] // 移除查询参数 if idx := strings.Index(lastPart, "?"); idx >= 0 { lastPart = lastPart[:idx] } return lastPart } return "" } // 从URL路径中提取文件名 path := parsedURL.Path parts := strings.Split(path, "/") if len(parts) > 0 { fileName := parts[len(parts)-1] // 移除查询参数 if idx := strings.Index(fileName, "?"); idx >= 0 { fileName = fileName[:idx] } return fileName } return "" } func min(a, b int) int { if a < b { return a } return b } // sendDeleteNotifications 发送消息删除通知 // conversationSeqsMap: conversationID -> []seq // conversationDocsMap: conversationID -> doc // isGroupMsg: true表示群聊消息,false表示个人聊天消息 func (c *cronServer) sendDeleteNotifications(ctx context.Context, conversationSeqsMap map[string][]int64, conversationDocsMap map[string]*model.MsgDocModel, isGroupMsg bool) { log.ZInfo(ctx, "[CLEAR_MSG] 开始发送删除通知", "conversationCount", len(conversationSeqsMap), "isGroupMsg", isGroupMsg) adminUserID := c.config.Share.IMAdminUser.UserIDs[0] for conversationID, seqs := range conversationSeqsMap { if len(seqs) == 0 { continue } // 从conversationDocsMap获取原始消息文档(参考撤销消息的实现) doc, ok := conversationDocsMap[conversationID] if !ok || doc == nil || len(doc.Msg) == 0 { log.ZWarn(ctx, "[CLEAR_MSG] 无法获取原始消息", nil, "conversationID", conversationID) continue } // 获取第一条消息的信息(参考撤销消息的实现:使用原始消息的SessionType、GroupID、RecvID) var firstMsg *model.MsgDataModel for _, msgInfo := range doc.Msg { if msgInfo.Msg != nil { firstMsg = msgInfo.Msg break } } if firstMsg == nil { log.ZWarn(ctx, "[CLEAR_MSG] 无法获取原始消息数据", nil, "conversationID", conversationID) continue } // 构建删除通知 tips := &sdkws.DeleteMsgsTips{ UserID: adminUserID, ConversationID: conversationID, Seqs: seqs, } // 参考撤销消息的实现:根据原始消息的SessionType确定recvID var recvID string var sessionType int32 if firstMsg.SessionType == constant.ReadGroupChatType { recvID = firstMsg.GroupID sessionType = firstMsg.SessionType } else { recvID = firstMsg.RecvID sessionType = firstMsg.SessionType } if recvID == "" { log.ZWarn(ctx, "[CLEAR_MSG] 无法确定通知接收者", nil, "conversationID", conversationID, "sessionType", sessionType, "groupID", firstMsg.GroupID, "recvID", firstMsg.RecvID) continue } // 使用NotificationSender发送通知(参考撤销消息的实现) c.notificationSender.NotificationWithSessionType(ctx, adminUserID, recvID, constant.DeleteMsgsNotification, sessionType, tips) log.ZInfo(ctx, "[CLEAR_MSG] 发送删除通知", "conversationID", conversationID, "recvID", recvID, "sessionType", sessionType, "seqCount", len(seqs)) } log.ZInfo(ctx, "[CLEAR_MSG] 删除通知发送完成", "conversationCount", len(conversationSeqsMap), "isGroupMsg", isGroupMsg) }