复制项目

This commit is contained in:
kim.dev.6789
2026-01-14 22:16:44 +08:00
parent e2577b8cee
commit e50142a3b9
691 changed files with 97009 additions and 1 deletions

View File

@@ -0,0 +1,851 @@
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)
}