Files
open-im-server-deploy/internal/tools/cron/clear_msg.go
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

852 lines
34 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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说明删除后应该为0S3文件应该被删除
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)
}