From 8cd3dff8f85253bbea2d789b67f87a46de2e59ad Mon Sep 17 00:00:00 2001 From: hawklin2017 <32898629+hawklin2017@users.noreply.github.com> Date: Fri, 15 May 2026 17:57:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E9=9D=99=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorignore | 52 ++++++++++--- internal/api/conversation.go | 4 + internal/api/friend.go | 7 -- internal/api/router.go | 3 +- internal/rpc/conversation/conversation.go | 51 +++++++++---- internal/rpc/conversation/mute_fill.go | 90 ++++------------------- internal/rpc/msg/server.go | 6 -- internal/rpc/msg/verify.go | 20 +++-- pkg/common/storage/model/conversation.go | 2 + 9 files changed, 117 insertions(+), 118 deletions(-) diff --git a/.cursorignore b/.cursorignore index 95694ea0e..777a278ee 100644 --- a/.cursorignore +++ b/.cursorignore @@ -1,14 +1,14 @@ # Cursor 代码索引忽略(语法与 .gitignore 相同) -# 与根目录 .gitignore 对齐;未列出的规则仍以 .gitignore 为准(Git 不索引的路径 Cursor 通常也不关心) +# 与根目录 .gitignore 对齐;以下为补充规则,减少生成物/文档噪音,保留业务源码与 .proto ### OpenIM(与 .gitignore 一致)### -logs -.devcontainer -components -out-test +logs/ +.devcontainer/ +components/ +out-test/ Dockerfile.cross -### macOS / 本地工具(不入索引)### +### macOS / 本地工具 ### .DS_Store .playwright-mcp/ @@ -17,26 +17,51 @@ tmp/ bin/ output/ _output/ +build/ +dist/ deployments/charts/generated-configs/ ### 配置与密钥(勿入索引)### .env config/config.yaml config/notification.yaml +start-config.yml ### 部署生成物 ### -deployments/openim-server/charts +deployments/openim-server/charts/ ### 本地笔记 ### .idea.md .todo.md .note.md +### 生成代码(以 .proto 为准,勿重复索引)### +protocol/**/*.pb.go +protocol/**/*_grpc.pb.go + +### 文档与资源(保留 docs/contrib、根 README;忽略多语言 readme 与静态资源)### +docs/readme/ +docs/.generated_docs +docs/contributing/ +assets/ +virgil_chat_server_design.md +docs/virgil-e2ee-*.md + +### 测试与脚本输出 ### +test/e2e/output/ +scripts/**/*.log + ### 通用备份与临时文件 ### *.bak +*.gho +*.ori +*.orig *.tmp *~ -dist/ +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* ### VS Code(除团队共享配置外)### .vscode/* @@ -44,6 +69,7 @@ dist/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +*.code-workspace ### Go ### *.exe @@ -54,13 +80,19 @@ dist/ *.test *.out vendor/ +go.work go.work.sum +go.sum ### JetBrains / IDE ### .idea/ out/ -### Tags ### +### Git / CI(低价值索引)### +.git/ +.github/ + +### Tags / 索引工具 ### TAGS tags gtags.files @@ -70,3 +102,5 @@ GPATH GSYMS cscope.files cscope.out +cscope.in.out +cscope.po.out diff --git a/internal/api/conversation.go b/internal/api/conversation.go index f7dbc133c..a43721569 100644 --- a/internal/api/conversation.go +++ b/internal/api/conversation.go @@ -71,3 +71,7 @@ func (o *ConversationApi) GetNotNotifyConversationIDs(c *gin.Context) { func (o *ConversationApi) GetPinnedConversationIDs(c *gin.Context) { a2r.Call(c, conversation.ConversationClient.GetPinnedConversationIDs, o.Client) } + +func (o *ConversationApi) SetMute(c *gin.Context) { + a2r.Call(c, conversation.ConversationClient.SetConversationMute, o.Client) +} diff --git a/internal/api/friend.go b/internal/api/friend.go index fc09c77e9..6932c495b 100644 --- a/internal/api/friend.go +++ b/internal/api/friend.go @@ -131,13 +131,6 @@ func (o *FriendApi) AddOnewayFriend(c *gin.Context) { a2r.Call(c, relation.FriendClient.AddOnewayFriend, o.Client) } -func (o *FriendApi) SetMute(c *gin.Context) { - a2r.Call(c, relation.FriendClient.SetMute, o.Client) -} - -func (o *FriendApi) GetMute(c *gin.Context) { - a2r.Call(c, relation.FriendClient.GetMute, o.Client) -} func (o *FriendApi) PinFriend(c *gin.Context) { a2r.Call(c, relation.FriendClient.PinFriend, o.Client) diff --git a/internal/api/router.go b/internal/api/router.go index dbaee2184..dc32db737 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -222,8 +222,6 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount) friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs) friendRouterGroup.POST("/add_oneway_friend", f.AddOnewayFriend) - friendRouterGroup.POST("/set_mute", f.SetMute) - friendRouterGroup.POST("/get_mute", f.GetMute) friendRouterGroup.POST("/pin", f.PinFriend) friendRouterGroup.POST("/unpin", f.UnpinFriend) } @@ -358,6 +356,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co conversationGroup.POST("/get_owner_conversation", c.GetOwnerConversation) conversationGroup.POST("/get_not_notify_conversation_ids", c.GetNotNotifyConversationIDs) conversationGroup.POST("/get_pinned_conversation_ids", c.GetPinnedConversationIDs) + conversationGroup.POST("/set_mute", c.SetMute) } { diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index 15c5a945c..55da122d2 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -49,7 +49,6 @@ type conversationServer struct { pbconversation.UnimplementedConversationServer conversationDatabase controller.ConversationDatabase msgBurnDeadlineDB database.MsgBurnDeadline - userMuteDB controller.UserMuteDatabase conversationNotificationSender *ConversationNotificationSender config *Config @@ -86,10 +85,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg if err != nil { return err } - userMuteMongoDB, err := mgo.NewUserMuteMongo(mgocli.GetDB()) - if err != nil { - return err - } userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User) if err != nil { return err @@ -109,7 +104,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg conversationDatabase: controller.NewConversationDatabase(conversationDB, redis.NewConversationRedis(rdb, &config.LocalCacheConfig, redis.GetRocksCacheOptions(), conversationDB), mgocli.GetTx()), msgBurnDeadlineDB: msgBurnDeadlineDB, - userMuteDB: controller.NewUserMuteDatabase(userMuteMongoDB), userClient: rpcli.NewUserClient(userConn), groupClient: rpcli.NewGroupClient(groupConn), msgClient: msgClient, @@ -202,14 +196,14 @@ func (c *conversationServer) GetSortedConversationList(ctx context.Context, req } conversation_notPinTime[time] = conversationID } - if c.userMuteDB != nil { - for _, v := range conversations { - elem, ok := conversationMsg[v.ConversationID] - if !ok { - continue - } - c.fillConversationElemUserMute(ctx, c.userMuteDB, req.UserID, elem, v.ConversationType, v.UserID) + for _, v := range conversations { + elem, ok := conversationMsg[v.ConversationID] + if !ok { + continue } + elem.MuteDuration = v.MuteDuration + elem.MuteEndTime = v.MuteEndTime + elem.IsMuted = computeIsMuted(v.MuteDuration, v.MuteEndTime) } resp = &pbconversation.GetSortedConversationListResp{ ConversationTotal: int64(len(chatLogs)), @@ -916,3 +910,34 @@ func (c *conversationServer) ClearBurnExpiredMsgs(ctx context.Context, req *pbco } return &pbconversation.ClearBurnExpiredMsgsResp{Count: processed}, nil } + +func (c *conversationServer) SetConversationMute(ctx context.Context, req *pbconversation.SetConversationMuteReq) (*pbconversation.SetConversationMuteResp, error) { + var ( + muteDuration int32 + muteEndTime int64 + ) + switch { + case req.Duration == 0: + // 取消静音:清零所有静音字段 + case req.Duration == -1: + // 永久静音 + muteDuration = -1 + default: + // 定时静音 + muteDuration = req.Duration + muteEndTime = time.Now().Unix() + int64(req.Duration) + } + if err := c.conversationDatabase.UpdateUsersConversationField( + ctx, + []string{req.OwnerUserID}, + req.ConversationID, + map[string]any{ + "mute_duration": muteDuration, + "mute_end_time": muteEndTime, + }, + ); err != nil { + return nil, err + } + c.conversationNotificationSender.ConversationChangeNotification(ctx, req.OwnerUserID, []string{req.ConversationID}) + return &pbconversation.SetConversationMuteResp{}, nil +} diff --git a/internal/rpc/conversation/mute_fill.go b/internal/rpc/conversation/mute_fill.go index 2581f96fd..771b7f36c 100644 --- a/internal/rpc/conversation/mute_fill.go +++ b/internal/rpc/conversation/mute_fill.go @@ -4,95 +4,37 @@ package conversation import ( "context" - "math" "time" - "github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller" - "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" - "github.com/openimsdk/protocol/constant" pbconversation "github.com/openimsdk/protocol/conversation" - "github.com/openimsdk/tools/log" ) -// int64MuteDurationToProto 将 user_mute 的秒数配置写入 Conversation.muteDuration(int32);正数过大时截断。 -func int64MuteDurationToProto(d int64) int32 { - if d > int64(math.MaxInt32) { - return math.MaxInt32 +// computeIsMuted 根据会话模型中存储的 mute_duration 和 mute_end_time 计算当前是否处于静音状态: +// - duration == 0 且 end == 0:未静音 +// - duration == -1 且 end == 0:永久静音 +// - end > 0 且 end > now:定时静音仍有效 +// - end > 0 且 end <= now:定时静音已过期,视为未静音 +func computeIsMuted(muteDuration int32, muteEndTime int64) bool { + if muteDuration == 0 && muteEndTime == 0 { + return false } - if d < int64(math.MinInt32) { - return math.MinInt32 + if muteDuration == -1 && muteEndTime == 0 { + return true } - return int32(d) + return muteEndTime > time.Now().Unix() } -// conversationMuteFromRecord 与 relation.GetMute 判定一致:未记录/已过期则未静音;永久为 duration=-1 且 end=0。 -func conversationMuteFromRecord(rec *model.UserMute, nowUnix int64) (isMuted bool, muteDuration int32, muteEndTime int64) { - if rec == nil { - return false, 0, 0 - } - if rec.MuteEndTime != 0 && rec.MuteEndTime <= nowUnix { - return false, 0, 0 - } - d := rec.MuteDuration - if d == 0 && rec.MuteEndTime == 0 { - d = -1 - } - md := int64MuteDurationToProto(d) - me := rec.MuteEndTime - isMuted = (md == -1) || (me > nowUnix) - return isMuted, md, me -} - -func (c *conversationServer) fillConversationUserMute(ctx context.Context, conv *pbconversation.Conversation) { - if c == nil || c.userMuteDB == nil || conv == nil { +// fillConversationUserMute 根据会话模型字段(已由 ConversationDB2Pb 通过 CopyStructFields 填入 +// conv.MuteDuration / conv.MuteEndTime)计算并设置 conv.IsMuted,无需额外数据库查询。 +func (c *conversationServer) fillConversationUserMute(_ context.Context, conv *pbconversation.Conversation) { + if conv == nil { return } - if conv.ConversationType != constant.SingleChatType || conv.UserID == "" { - return - } - rec, err := c.userMuteDB.Get(ctx, conv.OwnerUserID, conv.UserID) - if err != nil { - log.ZWarn(ctx, "fillConversationUserMute Get", err, "owner", conv.OwnerUserID, "peer", conv.UserID) - return - } - now := time.Now().Unix() - isMuted, dur, end := conversationMuteFromRecord(rec, now) - conv.IsMuted = isMuted - conv.MuteDuration = dur - conv.MuteEndTime = end + conv.IsMuted = computeIsMuted(conv.MuteDuration, conv.MuteEndTime) } func (c *conversationServer) fillConversationsUserMute(ctx context.Context, list []*pbconversation.Conversation) { - if len(list) == 0 { - return - } for _, conv := range list { c.fillConversationUserMute(ctx, conv) } } - -func (c *conversationServer) fillConversationElemUserMute( - ctx context.Context, - db controller.UserMuteDatabase, - ownerUserID string, - elem *pbconversation.ConversationElem, - conversationType int32, - peerUserID string, -) { - if db == nil || elem == nil || ownerUserID == "" { - return - } - if conversationType != constant.SingleChatType || peerUserID == "" { - return - } - rec, err := db.Get(ctx, ownerUserID, peerUserID) - if err != nil { - log.ZWarn(ctx, "fillConversationElemUserMute Get", err, "owner", ownerUserID, "peer", peerUserID) - return - } - now := time.Now().Unix() - isMuted, dur, end := conversationMuteFromRecord(rec, now) - elem.IsMuted = isMuted - elem.MuteDuration = dur - elem.MuteEndTime = end -} diff --git a/internal/rpc/msg/server.go b/internal/rpc/msg/server.go index 8e212e47a..3408a32b6 100644 --- a/internal/rpc/msg/server.go +++ b/internal/rpc/msg/server.go @@ -72,7 +72,6 @@ type msgServer struct { conversationClient *rpcli.ConversationClient spamReportDB database.SpamReport globalBlackDB controller.UserGlobalBlackDatabase - userMuteDB controller.UserMuteDatabase msgBurnDeadlineDB database.MsgBurnDeadline } @@ -138,10 +137,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg if err != nil { return err } - userMuteMgo, err := mgo.NewUserMuteMongo(mgocli.GetDB()) - if err != nil { - return err - } s := &msgServer{ MsgDatabase: msgDatabase, RegisterCenter: client, @@ -154,7 +149,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg conversationClient: conversationClient, spamReportDB: spamReportDB, globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo), - userMuteDB: controller.NewUserMuteDatabase(userMuteMgo), msgBurnDeadlineDB: msgBurnDeadlineDB, } diff --git a/internal/rpc/msg/verify.go b/internal/rpc/msg/verify.go index 05d440e7a..ff7ff1051 100644 --- a/internal/rpc/msg/verify.go +++ b/internal/rpc/msg/verify.go @@ -325,14 +325,20 @@ func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, us } } - // 第四优先级:用户静音设置(user_mute 集合,支持好友与非好友) - // 无论会话记录是否存在均检查,以支持对非好友的静音 - if m.userMuteDB != nil { - muted, err := m.userMuteDB.IsMuted(ctx, userID, pb.MsgData.SendID) - if err != nil { - return false, err + // 第四优先级:会话静音设置(存储于 conversations 集合的 mute_duration/mute_end_time) + conv, convErr := m.ConversationLocalCache.GetConversation(ctx, userID, conversationID) + if convErr != nil && !errs.ErrRecordNotFound.Is(convErr) { + return false, convErr + } + if convErr == nil && conv != nil { + var isMuted bool + switch { + case conv.MuteDuration == -1 && conv.MuteEndTime == 0: + isMuted = true + case conv.MuteEndTime > 0: + isMuted = conv.MuteEndTime > time.Now().Unix() } - if muted { + if isMuted { if pb.MsgData.Options == nil { pb.MsgData.Options = make(map[string]bool, 10) } diff --git a/pkg/common/storage/model/conversation.go b/pkg/common/storage/model/conversation.go index 590899b3f..d052b25f7 100644 --- a/pkg/common/storage/model/conversation.go +++ b/pkg/common/storage/model/conversation.go @@ -37,4 +37,6 @@ type Conversation struct { IsMsgDestruct bool `bson:"is_msg_destruct"` MsgDestructTime int64 `bson:"msg_destruct_time"` LatestMsgDestructTime time.Time `bson:"latest_msg_destruct_time"` + MuteDuration int32 `bson:"mute_duration"` + MuteEndTime int64 `bson:"mute_end_time"` }