复制项目
This commit is contained in:
851
internal/tools/cron/clear_msg.go
Normal file
851
internal/tools/cron/clear_msg.go
Normal 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,说明删除后应该为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)
|
||||
}
|
||||
307
internal/tools/cron/cron_task.go
Normal file
307
internal/tools/cron/cron_task.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
disetcd "git.imall.cloud/openim/open-im-server-deploy/pkg/common/discovery/etcd"
|
||||
mcache "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/mcache"
|
||||
redisCache "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
pbconversation "git.imall.cloud/openim/protocol/conversation"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/discovery/etcd"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/s3"
|
||||
"github.com/openimsdk/tools/s3/cont"
|
||||
"github.com/openimsdk/tools/s3/disable"
|
||||
"github.com/openimsdk/tools/s3/minio"
|
||||
"github.com/openimsdk/tools/utils/runtimeenv"
|
||||
"github.com/robfig/cron/v3"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CronTask config.CronTask
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
Mongo config.Mongo
|
||||
Redis config.Redis
|
||||
Minio config.Minio
|
||||
Third config.Third
|
||||
Notification config.Notification
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegistry, service grpc.ServiceRegistrar) error {
|
||||
log.CInfo(ctx, "CRON-TASK server is initializing", "runTimeEnv", runtimeenv.RuntimeEnvironment(), "chatRecordsClearTime", conf.CronTask.CronExecuteTime, "msgDestructTime", conf.CronTask.RetainChatRecords)
|
||||
if conf.CronTask.RetainChatRecords < 1 {
|
||||
log.ZInfo(ctx, "disable cron")
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
ctx = mcontext.SetOpUserID(ctx, conf.Share.IMAdminUser.UserIDs[0])
|
||||
|
||||
msgConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thirdConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Third)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conversationConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Conversation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groupConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化数据库连接(用于会议群聊解散)
|
||||
dbb := dbbuild.NewBuilder(&conf.Mongo, &conf.Redis)
|
||||
mgocli, err := dbb.Mongo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meetingDB, err := mgo.NewMeetingMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
systemConfigDB, err := mgo.NewSystemConfigMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgDocDB, err := mgo.NewMsgMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectDB, err := mgo.NewS3Mongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化S3客户端和缓存(用于删除S3文件)
|
||||
rdb, err := dbb.Redis(ctx)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "Redis连接失败,S3文件删除功能可能受限", err)
|
||||
rdb = nil
|
||||
}
|
||||
var s3Client s3.Interface
|
||||
var s3Cache cont.S3Cache
|
||||
switch enable := conf.Third.Object.Enable; enable {
|
||||
case "minio":
|
||||
var minioCache minio.Cache
|
||||
if rdb == nil {
|
||||
mc, err := mgo.NewCacheMgo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "Mongo缓存初始化失败,S3文件删除功能可能受限", err)
|
||||
s3Client = disable.NewDisable()
|
||||
s3Cache = nil
|
||||
} else {
|
||||
minioCache = mcache.NewMinioCache(mc)
|
||||
s3Client, err = minio.NewMinio(ctx, minioCache, *conf.Minio.Build())
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Minio初始化失败", err)
|
||||
return err
|
||||
}
|
||||
s3Cache = nil // MongoDB缓存模式下,S3Cache为nil
|
||||
}
|
||||
} else {
|
||||
minioCache = redisCache.NewMinioCache(rdb)
|
||||
s3Client, err = minio.NewMinio(ctx, minioCache, *conf.Minio.Build())
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Minio初始化失败", err)
|
||||
return err
|
||||
}
|
||||
s3Cache = redisCache.NewS3Cache(rdb, s3Client)
|
||||
}
|
||||
case "":
|
||||
s3Client = disable.NewDisable()
|
||||
s3Cache = nil
|
||||
default:
|
||||
// 其他S3类型暂不支持,使用disable模式
|
||||
log.ZWarn(ctx, "S3类型不支持,使用disable模式", nil, "enable", enable)
|
||||
s3Client = disable.NewDisable()
|
||||
s3Cache = nil
|
||||
}
|
||||
|
||||
var locker Locker
|
||||
if conf.Discovery.Enable == config.ETCD {
|
||||
cm := disetcd.NewConfigManager(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient(), []string{
|
||||
conf.CronTask.GetConfigFileName(),
|
||||
conf.Share.GetConfigFileName(),
|
||||
conf.Discovery.GetConfigFileName(),
|
||||
})
|
||||
cm.Watch(ctx)
|
||||
locker, err = NewEtcdLocker(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if locker == nil {
|
||||
locker = emptyLocker{}
|
||||
}
|
||||
|
||||
// 初始化NotificationSender(用于发送删除消息通知)
|
||||
notificationSender := notification.NewNotificationSender(&conf.Notification,
|
||||
notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {
|
||||
return msg.NewMsgClient(msgConn).SendMsg(ctx, req)
|
||||
}),
|
||||
)
|
||||
|
||||
srv := &cronServer{
|
||||
ctx: ctx,
|
||||
config: conf,
|
||||
cron: cron.New(),
|
||||
msgClient: msg.NewMsgClient(msgConn),
|
||||
conversationClient: pbconversation.NewConversationClient(conversationConn),
|
||||
thirdClient: third.NewThirdClient(thirdConn),
|
||||
groupClient: rpcli.NewGroupClient(groupConn),
|
||||
meetingDB: meetingDB,
|
||||
systemConfigDB: systemConfigDB,
|
||||
msgDocDB: msgDocDB,
|
||||
objectDB: objectDB,
|
||||
s3Client: s3Client,
|
||||
s3Cache: s3Cache,
|
||||
notificationSender: notificationSender,
|
||||
locker: locker,
|
||||
}
|
||||
|
||||
if err := srv.registerClearS3(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv.registerDeleteMsg(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv.registerClearUserMsg(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv.registerDismissMeetingGroups(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv.registerClearGroupMsgByConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv.registerClearUserMsgByConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
log.ZDebug(ctx, "start cron task", "CronExecuteTime", conf.CronTask.CronExecuteTime)
|
||||
srv.cron.Start()
|
||||
log.ZDebug(ctx, "cron task server is running")
|
||||
<-ctx.Done()
|
||||
log.ZDebug(ctx, "cron task server is shutting down")
|
||||
srv.cron.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Locker interface {
|
||||
ExecuteWithLock(ctx context.Context, taskName string, task func())
|
||||
}
|
||||
|
||||
type emptyLocker struct{}
|
||||
|
||||
func (emptyLocker) ExecuteWithLock(ctx context.Context, taskName string, task func()) {
|
||||
task()
|
||||
}
|
||||
|
||||
type cronServer struct {
|
||||
ctx context.Context
|
||||
config *Config
|
||||
cron *cron.Cron
|
||||
msgClient msg.MsgClient
|
||||
conversationClient pbconversation.ConversationClient
|
||||
thirdClient third.ThirdClient
|
||||
groupClient *rpcli.GroupClient
|
||||
meetingDB database.Meeting
|
||||
systemConfigDB database.SystemConfig
|
||||
msgDocDB database.Msg
|
||||
objectDB database.ObjectInfo
|
||||
s3Client s3.Interface
|
||||
s3Cache cont.S3Cache
|
||||
notificationSender *notification.NotificationSender
|
||||
locker Locker
|
||||
}
|
||||
|
||||
func (c *cronServer) registerClearS3() error {
|
||||
if c.config.CronTask.FileExpireTime <= 0 || len(c.config.CronTask.DeleteObjectType) == 0 {
|
||||
log.ZInfo(c.ctx, "disable scheduled cleanup of s3", "fileExpireTime", c.config.CronTask.FileExpireTime, "deleteObjectType", c.config.CronTask.DeleteObjectType)
|
||||
return nil
|
||||
}
|
||||
_, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() {
|
||||
c.locker.ExecuteWithLock(c.ctx, "clearS3", c.clearS3)
|
||||
})
|
||||
return errs.WrapMsg(err, "failed to register clear s3 cron task")
|
||||
}
|
||||
|
||||
func (c *cronServer) registerDeleteMsg() error {
|
||||
if c.config.CronTask.RetainChatRecords <= 0 {
|
||||
log.ZInfo(c.ctx, "disable scheduled cleanup of chat records", "retainChatRecords", c.config.CronTask.RetainChatRecords)
|
||||
return nil
|
||||
}
|
||||
_, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() {
|
||||
c.locker.ExecuteWithLock(c.ctx, "deleteMsg", c.deleteMsg)
|
||||
})
|
||||
return errs.WrapMsg(err, "failed to register delete msg cron task")
|
||||
}
|
||||
|
||||
func (c *cronServer) registerClearUserMsg() error {
|
||||
_, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() {
|
||||
c.locker.ExecuteWithLock(c.ctx, "clearUserMsg", c.clearUserMsg)
|
||||
})
|
||||
return errs.WrapMsg(err, "failed to register clear user msg cron task")
|
||||
}
|
||||
|
||||
func (c *cronServer) registerDismissMeetingGroups() error {
|
||||
// 每分钟执行一次,检查已结束超过10分钟的会议并解散群聊
|
||||
_, err := c.cron.AddFunc("*/1 * * * *", func() {
|
||||
c.locker.ExecuteWithLock(c.ctx, "dismissMeetingGroups", c.dismissMeetingGroups)
|
||||
})
|
||||
return errs.WrapMsg(err, "failed to register dismiss meeting groups cron task")
|
||||
}
|
||||
|
||||
func (c *cronServer) registerClearGroupMsgByConfig() error {
|
||||
// 使用配置文件中的执行时间,清理群聊消息(根据系统配置)
|
||||
cronExpr := c.config.CronTask.CronExecuteTime
|
||||
log.ZInfo(c.ctx, "[CLEAR_MSG] 注册清理群聊消息定时任务", "cron", cronExpr)
|
||||
_, err := c.cron.AddFunc(cronExpr, func() {
|
||||
c.locker.ExecuteWithLock(c.ctx, "clearGroupMsgByConfig", c.clearGroupMsg)
|
||||
})
|
||||
if err != nil {
|
||||
log.ZError(c.ctx, "[CLEAR_MSG] 注册清理群聊消息定时任务失败", err)
|
||||
} else {
|
||||
log.ZInfo(c.ctx, "[CLEAR_MSG] 清理群聊消息定时任务注册成功", "cron", cronExpr)
|
||||
}
|
||||
return errs.WrapMsg(err, "failed to register clear group msg by config cron task")
|
||||
}
|
||||
|
||||
func (c *cronServer) registerClearUserMsgByConfig() error {
|
||||
// 使用配置文件中的执行时间,清理个人聊天消息(根据系统配置)
|
||||
cronExpr := c.config.CronTask.CronExecuteTime
|
||||
log.ZInfo(c.ctx, "[CLEAR_MSG] 注册清理个人聊天消息定时任务", "cron", cronExpr)
|
||||
_, err := c.cron.AddFunc(cronExpr, func() {
|
||||
c.locker.ExecuteWithLock(c.ctx, "clearUserMsgByConfig", c.clearUserMsg)
|
||||
})
|
||||
if err != nil {
|
||||
log.ZError(c.ctx, "[CLEAR_MSG] 注册清理个人聊天消息定时任务失败", err)
|
||||
} else {
|
||||
log.ZInfo(c.ctx, "[CLEAR_MSG] 清理个人聊天消息定时任务注册成功", "cron", cronExpr)
|
||||
}
|
||||
return errs.WrapMsg(err, "failed to register clear user msg by config cron task")
|
||||
}
|
||||
64
internal/tools/cron/cron_test.go
Normal file
64
internal/tools/cron/cron_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
kdisc "git.imall.cloud/openim/open-im-server-deploy/pkg/common/discovery"
|
||||
pbconversation "git.imall.cloud/openim/protocol/conversation"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/mw"
|
||||
"github.com/robfig/cron/v3"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
conf := &config.Discovery{
|
||||
Enable: config.ETCD,
|
||||
Etcd: config.Etcd{
|
||||
RootDirectory: "openim",
|
||||
Address: []string{"localhost:12379"},
|
||||
},
|
||||
}
|
||||
client, err := kdisc.NewDiscoveryRegister(conf, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
client.AddOption(mw.GrpcClient(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
ctx := mcontext.SetOpUserID(context.Background(), "imAdmin")
|
||||
msgConn, err := client.GetConn(ctx, "msg-rpc-service")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
thirdConn, err := client.GetConn(ctx, "third-rpc-service")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
conversationConn, err := client.GetConn(ctx, "conversation-rpc-service")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
srv := &cronServer{
|
||||
ctx: ctx,
|
||||
config: &Config{
|
||||
CronTask: config.CronTask{
|
||||
RetainChatRecords: 1,
|
||||
FileExpireTime: 1,
|
||||
DeleteObjectType: []string{"msg-picture", "msg-file", "msg-voice", "msg-video", "msg-video-snapshot", "sdklog", ""},
|
||||
},
|
||||
},
|
||||
cron: cron.New(),
|
||||
msgClient: msg.NewMsgClient(msgConn),
|
||||
conversationClient: pbconversation.NewConversationClient(conversationConn),
|
||||
thirdClient: third.NewThirdClient(thirdConn),
|
||||
}
|
||||
srv.deleteMsg()
|
||||
//srv.clearS3()
|
||||
//srv.clearUserMsg()
|
||||
}
|
||||
86
internal/tools/cron/dist_look.go
Normal file
86
internal/tools/cron/dist_look.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/openimsdk/tools/log"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"go.etcd.io/etcd/client/v3/concurrency"
|
||||
)
|
||||
|
||||
const (
|
||||
lockLeaseTTL = 300
|
||||
)
|
||||
|
||||
type EtcdLocker struct {
|
||||
client *clientv3.Client
|
||||
instanceID string
|
||||
}
|
||||
|
||||
// NewEtcdLocker creates a new etcd distributed lock
|
||||
func NewEtcdLocker(client *clientv3.Client) (*EtcdLocker, error) {
|
||||
hostname, _ := os.Hostname()
|
||||
pid := os.Getpid()
|
||||
instanceID := fmt.Sprintf("%s-pid-%d-%d", hostname, pid, time.Now().UnixNano())
|
||||
|
||||
locker := &EtcdLocker{
|
||||
client: client,
|
||||
instanceID: instanceID,
|
||||
}
|
||||
|
||||
return locker, nil
|
||||
}
|
||||
|
||||
func (e *EtcdLocker) ExecuteWithLock(ctx context.Context, taskName string, task func()) {
|
||||
session, err := concurrency.NewSession(e.client, concurrency.WithTTL(lockLeaseTTL))
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "Failed to create etcd session", err,
|
||||
"taskName", taskName,
|
||||
"instanceID", e.instanceID)
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
lockKey := fmt.Sprintf("openim/crontask/%s", taskName)
|
||||
mutex := concurrency.NewMutex(session, lockKey)
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err = mutex.TryLock(ctxWithTimeout)
|
||||
if err != nil {
|
||||
// errors.Is(err, concurrency.ErrLocked)
|
||||
log.ZDebug(ctx, "Task is being executed by another instance, skipping",
|
||||
"taskName", taskName,
|
||||
"instanceID", e.instanceID,
|
||||
"error", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := mutex.Unlock(ctx); err != nil {
|
||||
log.ZWarn(ctx, "Failed to release task lock", err,
|
||||
"taskName", taskName,
|
||||
"instanceID", e.instanceID)
|
||||
} else {
|
||||
log.ZInfo(ctx, "Successfully released task lock",
|
||||
"taskName", taskName,
|
||||
"instanceID", e.instanceID)
|
||||
}
|
||||
}()
|
||||
|
||||
log.ZInfo(ctx, "Successfully acquired task lock, starting execution",
|
||||
"taskName", taskName,
|
||||
"instanceID", e.instanceID,
|
||||
"sessionID", session.Lease())
|
||||
|
||||
task()
|
||||
|
||||
log.ZInfo(ctx, "Task execution completed",
|
||||
"taskName", taskName,
|
||||
"instanceID", e.instanceID)
|
||||
}
|
||||
152
internal/tools/cron/meeting.go
Normal file
152
internal/tools/cron/meeting.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
// meetingPagination 简单的分页实现,供定时任务批量扫描使用
|
||||
type meetingPagination struct {
|
||||
pageNumber int32
|
||||
showNumber int32
|
||||
}
|
||||
|
||||
func (p *meetingPagination) GetPageNumber() int32 {
|
||||
if p.pageNumber <= 0 {
|
||||
return 1
|
||||
}
|
||||
return p.pageNumber
|
||||
}
|
||||
|
||||
func (p *meetingPagination) GetShowNumber() int32 {
|
||||
if p.showNumber <= 0 {
|
||||
return 200
|
||||
}
|
||||
return p.showNumber
|
||||
}
|
||||
|
||||
// dismissMeetingGroups 解散已结束超过10分钟的会议群聊
|
||||
func (c *cronServer) dismissMeetingGroups() {
|
||||
now := time.Now()
|
||||
// 计算10分钟前的时间
|
||||
beforeTime := now.Add(-10 * time.Minute)
|
||||
operationID := fmt.Sprintf("cron_dismiss_meeting_groups_%d_%d", os.Getpid(), now.UnixMilli())
|
||||
ctx := mcontext.SetOperationID(c.ctx, operationID)
|
||||
|
||||
log.ZDebug(ctx, "Start dismissing meeting groups", "beforeTime", beforeTime)
|
||||
|
||||
// 先将已过期但状态仍为已预约/进行中的会议标记为已结束
|
||||
c.finishExpiredMeetings(ctx, now)
|
||||
|
||||
// 查询已结束且结束时间在10分钟前的会议
|
||||
// 结束时间 = scheduledTime + duration(分钟)
|
||||
meetings, err := c.meetingDB.FindFinishedMeetingsBefore(ctx, beforeTime)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to find finished meetings", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(meetings) == 0 {
|
||||
log.ZDebug(ctx, "No finished meetings to dismiss groups")
|
||||
return
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "Found finished meetings to dismiss groups", "count", len(meetings))
|
||||
|
||||
dismissedCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, meeting := range meetings {
|
||||
if meeting.GroupID == "" {
|
||||
log.ZWarn(ctx, "Meeting has no group ID, skip", nil, "meetingID", meeting.MeetingID)
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算会议结束时间
|
||||
endTime := meeting.ScheduledTime.Add(time.Duration(meeting.Duration) * time.Minute)
|
||||
// 检查是否已经超过10分钟
|
||||
if now.Sub(endTime) < 10*time.Minute {
|
||||
log.ZDebug(ctx, "Meeting ended less than 10 minutes ago, skip", "meetingID", meeting.MeetingID, "endTime", endTime)
|
||||
continue
|
||||
}
|
||||
|
||||
// 解散群聊,deleteMember设为true表示删除所有成员
|
||||
ctx := mcontext.SetOperationID(c.ctx, fmt.Sprintf("%s_%s", operationID, meeting.MeetingID))
|
||||
err := c.groupClient.DismissGroup(ctx, meeting.GroupID, true)
|
||||
if err != nil {
|
||||
// 如果群不存在或找不到群主(RecordNotFoundError),说明群可能已经被解散或数据不一致
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
log.ZWarn(ctx, "Group not found or owner not found, may already be dismissed, clear groupID", nil, "meetingID", meeting.MeetingID, "groupID", meeting.GroupID)
|
||||
// 清空groupID,避免下次重复处理
|
||||
if updateErr := c.meetingDB.Update(ctx, meeting.MeetingID, map[string]any{"group_id": ""}); updateErr != nil {
|
||||
log.ZWarn(ctx, "Failed to clear groupID after group not found", updateErr, "meetingID", meeting.MeetingID)
|
||||
}
|
||||
// 不增加失败计数,因为这不是真正的失败
|
||||
continue
|
||||
}
|
||||
log.ZError(ctx, "Failed to dismiss meeting group", err, "meetingID", meeting.MeetingID, "groupID", meeting.GroupID)
|
||||
failedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 从webhook配置的attentionIds中移除会议群ID
|
||||
if c.systemConfigDB != nil {
|
||||
if err := webhook.UpdateAttentionIds(ctx, c.systemConfigDB, meeting.GroupID, false); err != nil {
|
||||
log.ZWarn(ctx, "dismissMeetingGroups: failed to remove groupID from webhook attentionIds", err, "meetingID", meeting.MeetingID, "groupID", meeting.GroupID)
|
||||
}
|
||||
}
|
||||
|
||||
// 解散群成功后,清空会议的groupID,避免下次重复处理
|
||||
if updateErr := c.meetingDB.Update(ctx, meeting.MeetingID, map[string]any{"group_id": ""}); updateErr != nil {
|
||||
log.ZWarn(ctx, "Failed to clear groupID after dismissing group", updateErr, "meetingID", meeting.MeetingID, "groupID", meeting.GroupID)
|
||||
} else {
|
||||
log.ZInfo(ctx, "Successfully dismissed meeting group and cleared groupID", "meetingID", meeting.MeetingID, "groupID", meeting.GroupID, "endTime", endTime)
|
||||
}
|
||||
dismissedCount++
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "Finished dismissing meeting groups", "total", len(meetings), "dismissed", dismissedCount, "failed", failedCount, "duration", time.Since(now))
|
||||
}
|
||||
|
||||
// finishExpiredMeetings 将已过结束时间的会议状态更新为已结束
|
||||
func (c *cronServer) finishExpiredMeetings(ctx context.Context, now time.Time) {
|
||||
statuses := []int32{model.MeetingStatusScheduled, model.MeetingStatusOngoing}
|
||||
for _, status := range statuses {
|
||||
page := int32(1)
|
||||
for {
|
||||
total, meetings, err := c.meetingDB.FindByStatus(ctx, status, &meetingPagination{pageNumber: page, showNumber: 200})
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "finishExpiredMeetings: failed to list meetings", err, "status", status, "page", page)
|
||||
break
|
||||
}
|
||||
if len(meetings) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, meeting := range meetings {
|
||||
endTime := meeting.ScheduledTime.Add(time.Duration(meeting.Duration) * time.Minute)
|
||||
if now.After(endTime) {
|
||||
if err := c.meetingDB.UpdateStatus(ctx, meeting.MeetingID, model.MeetingStatusFinished); err != nil {
|
||||
log.ZWarn(ctx, "finishExpiredMeetings: failed to update status", err, "meetingID", meeting.MeetingID)
|
||||
continue
|
||||
}
|
||||
log.ZInfo(ctx, "finishExpiredMeetings: meeting marked finished", "meetingID", meeting.MeetingID, "endTime", endTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页结束条件
|
||||
if int64(page*200) >= total {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
}
|
||||
37
internal/tools/cron/msg.go
Normal file
37
internal/tools/cron/msg.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func (c *cronServer) deleteMsg() {
|
||||
now := time.Now()
|
||||
deltime := now.Add(-time.Hour * 24 * time.Duration(c.config.CronTask.RetainChatRecords))
|
||||
operationID := fmt.Sprintf("cron_msg_%d_%d", os.Getpid(), deltime.UnixMilli())
|
||||
ctx := mcontext.SetOperationID(c.ctx, operationID)
|
||||
log.ZDebug(ctx, "Destruct chat records", "deltime", deltime, "timestamp", deltime.UnixMilli())
|
||||
const (
|
||||
deleteCount = 10000
|
||||
deleteLimit = 50
|
||||
)
|
||||
var count int
|
||||
for i := 1; i <= deleteCount; i++ {
|
||||
ctx := mcontext.SetOperationID(c.ctx, fmt.Sprintf("%s_%d", operationID, i))
|
||||
resp, err := c.msgClient.DestructMsgs(ctx, &msg.DestructMsgsReq{Timestamp: deltime.UnixMilli(), Limit: deleteLimit})
|
||||
if err != nil {
|
||||
log.ZError(ctx, "cron destruct chat records failed", err)
|
||||
break
|
||||
}
|
||||
count += int(resp.Count)
|
||||
if resp.Count < deleteLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
log.ZDebug(ctx, "cron destruct chat records end", "deltime", deltime, "cont", time.Since(now), "count", count)
|
||||
}
|
||||
80
internal/tools/cron/s3.go
Normal file
80
internal/tools/cron/s3.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func (c *cronServer) clearS3() {
|
||||
start := time.Now()
|
||||
deleteTime := start.Add(-time.Hour * 24 * time.Duration(c.config.CronTask.FileExpireTime))
|
||||
operationID := fmt.Sprintf("cron_s3_%d_%d", os.Getpid(), deleteTime.UnixMilli())
|
||||
ctx := mcontext.SetOperationID(c.ctx, operationID)
|
||||
log.ZDebug(ctx, "deleteoutDatedData", "deletetime", deleteTime, "timestamp", deleteTime.UnixMilli())
|
||||
const (
|
||||
deleteCount = 10000
|
||||
deleteLimit = 100
|
||||
)
|
||||
|
||||
var count int
|
||||
for i := 1; i <= deleteCount; i++ {
|
||||
resp, err := c.thirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{ExpireTime: deleteTime.UnixMilli(), ObjectGroup: c.config.CronTask.DeleteObjectType, Limit: deleteLimit})
|
||||
if err != nil {
|
||||
log.ZError(ctx, "cron deleteoutDatedData failed", err)
|
||||
return
|
||||
}
|
||||
count += int(resp.Count)
|
||||
if resp.Count < deleteLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
log.ZDebug(ctx, "cron deleteoutDatedData success", "deltime", deleteTime, "cont", time.Since(start), "count", count)
|
||||
}
|
||||
|
||||
// var req *third.DeleteOutdatedDataReq
|
||||
// count1, err := ExtractField(ctx, c.thirdClient.DeleteOutdatedData, req, (*third.DeleteOutdatedDataResp).GetCount)
|
||||
//
|
||||
// c.thirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{})
|
||||
// msggateway.GetUsersOnlineStatusCaller.Invoke(ctx, &msggateway.GetUsersOnlineStatusReq{})
|
||||
//
|
||||
// var cli ThirdClient
|
||||
//
|
||||
// c111, err := cli.DeleteOutdatedData(ctx, 100)
|
||||
//
|
||||
// cli.ThirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{})
|
||||
//
|
||||
// cli.AuthSign(ctx, &third.AuthSignReq{})
|
||||
//
|
||||
// cli.SetAppBadge()
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func extractField[A, B, C any](ctx context.Context, fn func(ctx context.Context, req *A, opts ...grpc.CallOption) (*B, error), req *A, get func(*B) C) (C, error) {
|
||||
// resp, err := fn(ctx, req)
|
||||
// if err != nil {
|
||||
// var c C
|
||||
// return c, err
|
||||
// }
|
||||
// return get(resp), nil
|
||||
//}
|
||||
//
|
||||
//func ignore(_ any, err error) error {
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//type ThirdClient struct {
|
||||
// third.ThirdClient
|
||||
//}
|
||||
//
|
||||
//func (c *ThirdClient) DeleteOutdatedData(ctx context.Context, expireTime int64) (int32, error) {
|
||||
// return extractField(ctx, c.ThirdClient.DeleteOutdatedData, &third.DeleteOutdatedDataReq{ExpireTime: expireTime}, (*third.DeleteOutdatedDataResp).GetCount)
|
||||
//}
|
||||
//
|
||||
//func (c *ThirdClient) DeleteOutdatedData1(ctx context.Context, expireTime int64) error {
|
||||
// return ignore(c.ThirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{ExpireTime: expireTime}))
|
||||
//}
|
||||
Reference in New Issue
Block a user