会话静音

This commit is contained in:
hawklin2017 2026-05-15 17:57:22 +08:00
parent 1a3d7020ad
commit 8cd3dff8f8
9 changed files with 117 additions and 118 deletions

View File

@ -1,14 +1,14 @@
# Cursor 代码索引忽略(语法与 .gitignore 相同) # Cursor 代码索引忽略(语法与 .gitignore 相同)
# 与根目录 .gitignore 对齐;未列出的规则仍以 .gitignore 为准Git 不索引的路径 Cursor 通常也不关心) # 与根目录 .gitignore 对齐;以下为补充规则,减少生成物/文档噪音,保留业务源码与 .proto
### OpenIM与 .gitignore 一致)### ### OpenIM与 .gitignore 一致)###
logs logs/
.devcontainer .devcontainer/
components components/
out-test out-test/
Dockerfile.cross Dockerfile.cross
### macOS / 本地工具(不入索引)### ### macOS / 本地工具 ###
.DS_Store .DS_Store
.playwright-mcp/ .playwright-mcp/
@ -17,26 +17,51 @@ tmp/
bin/ bin/
output/ output/
_output/ _output/
build/
dist/
deployments/charts/generated-configs/ deployments/charts/generated-configs/
### 配置与密钥(勿入索引)### ### 配置与密钥(勿入索引)###
.env .env
config/config.yaml config/config.yaml
config/notification.yaml config/notification.yaml
start-config.yml
### 部署生成物 ### ### 部署生成物 ###
deployments/openim-server/charts deployments/openim-server/charts/
### 本地笔记 ### ### 本地笔记 ###
.idea.md .idea.md
.todo.md .todo.md
.note.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 *.bak
*.gho
*.ori
*.orig
*.tmp *.tmp
*~ *~
dist/ *.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
### VS Code除团队共享配置外### ### VS Code除团队共享配置外###
.vscode/* .vscode/*
@ -44,6 +69,7 @@ dist/
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
*.code-workspace
### Go ### ### Go ###
*.exe *.exe
@ -54,13 +80,19 @@ dist/
*.test *.test
*.out *.out
vendor/ vendor/
go.work
go.work.sum go.work.sum
go.sum
### JetBrains / IDE ### ### JetBrains / IDE ###
.idea/ .idea/
out/ out/
### Tags ### ### Git / CI低价值索引###
.git/
.github/
### Tags / 索引工具 ###
TAGS TAGS
tags tags
gtags.files gtags.files
@ -70,3 +102,5 @@ GPATH
GSYMS GSYMS
cscope.files cscope.files
cscope.out cscope.out
cscope.in.out
cscope.po.out

View File

@ -71,3 +71,7 @@ func (o *ConversationApi) GetNotNotifyConversationIDs(c *gin.Context) {
func (o *ConversationApi) GetPinnedConversationIDs(c *gin.Context) { func (o *ConversationApi) GetPinnedConversationIDs(c *gin.Context) {
a2r.Call(c, conversation.ConversationClient.GetPinnedConversationIDs, o.Client) a2r.Call(c, conversation.ConversationClient.GetPinnedConversationIDs, o.Client)
} }
func (o *ConversationApi) SetMute(c *gin.Context) {
a2r.Call(c, conversation.ConversationClient.SetConversationMute, o.Client)
}

View File

@ -131,13 +131,6 @@ func (o *FriendApi) AddOnewayFriend(c *gin.Context) {
a2r.Call(c, relation.FriendClient.AddOnewayFriend, o.Client) 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) { func (o *FriendApi) PinFriend(c *gin.Context) {
a2r.Call(c, relation.FriendClient.PinFriend, o.Client) a2r.Call(c, relation.FriendClient.PinFriend, o.Client)

View File

@ -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_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount)
friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs) friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs)
friendRouterGroup.POST("/add_oneway_friend", f.AddOnewayFriend) 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("/pin", f.PinFriend)
friendRouterGroup.POST("/unpin", f.UnpinFriend) 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_owner_conversation", c.GetOwnerConversation)
conversationGroup.POST("/get_not_notify_conversation_ids", c.GetNotNotifyConversationIDs) conversationGroup.POST("/get_not_notify_conversation_ids", c.GetNotNotifyConversationIDs)
conversationGroup.POST("/get_pinned_conversation_ids", c.GetPinnedConversationIDs) conversationGroup.POST("/get_pinned_conversation_ids", c.GetPinnedConversationIDs)
conversationGroup.POST("/set_mute", c.SetMute)
} }
{ {

View File

@ -49,7 +49,6 @@ type conversationServer struct {
pbconversation.UnimplementedConversationServer pbconversation.UnimplementedConversationServer
conversationDatabase controller.ConversationDatabase conversationDatabase controller.ConversationDatabase
msgBurnDeadlineDB database.MsgBurnDeadline msgBurnDeadlineDB database.MsgBurnDeadline
userMuteDB controller.UserMuteDatabase
conversationNotificationSender *ConversationNotificationSender conversationNotificationSender *ConversationNotificationSender
config *Config config *Config
@ -86,10 +85,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
if err != nil { if err != nil {
return err return err
} }
userMuteMongoDB, err := mgo.NewUserMuteMongo(mgocli.GetDB())
if err != nil {
return err
}
userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User) userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User)
if err != nil { if err != nil {
return err return err
@ -109,7 +104,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
conversationDatabase: controller.NewConversationDatabase(conversationDB, conversationDatabase: controller.NewConversationDatabase(conversationDB,
redis.NewConversationRedis(rdb, &config.LocalCacheConfig, redis.GetRocksCacheOptions(), conversationDB), mgocli.GetTx()), redis.NewConversationRedis(rdb, &config.LocalCacheConfig, redis.GetRocksCacheOptions(), conversationDB), mgocli.GetTx()),
msgBurnDeadlineDB: msgBurnDeadlineDB, msgBurnDeadlineDB: msgBurnDeadlineDB,
userMuteDB: controller.NewUserMuteDatabase(userMuteMongoDB),
userClient: rpcli.NewUserClient(userConn), userClient: rpcli.NewUserClient(userConn),
groupClient: rpcli.NewGroupClient(groupConn), groupClient: rpcli.NewGroupClient(groupConn),
msgClient: msgClient, msgClient: msgClient,
@ -202,14 +196,14 @@ func (c *conversationServer) GetSortedConversationList(ctx context.Context, req
} }
conversation_notPinTime[time] = conversationID conversation_notPinTime[time] = conversationID
} }
if c.userMuteDB != nil { for _, v := range conversations {
for _, v := range conversations { elem, ok := conversationMsg[v.ConversationID]
elem, ok := conversationMsg[v.ConversationID] if !ok {
if !ok { continue
continue
}
c.fillConversationElemUserMute(ctx, c.userMuteDB, req.UserID, elem, v.ConversationType, v.UserID)
} }
elem.MuteDuration = v.MuteDuration
elem.MuteEndTime = v.MuteEndTime
elem.IsMuted = computeIsMuted(v.MuteDuration, v.MuteEndTime)
} }
resp = &pbconversation.GetSortedConversationListResp{ resp = &pbconversation.GetSortedConversationListResp{
ConversationTotal: int64(len(chatLogs)), ConversationTotal: int64(len(chatLogs)),
@ -916,3 +910,34 @@ func (c *conversationServer) ClearBurnExpiredMsgs(ctx context.Context, req *pbco
} }
return &pbconversation.ClearBurnExpiredMsgsResp{Count: processed}, nil 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
}

View File

@ -4,95 +4,37 @@ package conversation
import ( import (
"context" "context"
"math"
"time" "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" pbconversation "github.com/openimsdk/protocol/conversation"
"github.com/openimsdk/tools/log"
) )
// int64MuteDurationToProto 将 user_mute 的秒数配置写入 Conversation.muteDurationint32正数过大时截断。 // computeIsMuted 根据会话模型中存储的 mute_duration 和 mute_end_time 计算当前是否处于静音状态:
func int64MuteDurationToProto(d int64) int32 { // - duration == 0 且 end == 0未静音
if d > int64(math.MaxInt32) { // - duration == -1 且 end == 0永久静音
return math.MaxInt32 // - 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) { if muteDuration == -1 && muteEndTime == 0 {
return math.MinInt32 return true
} }
return int32(d) return muteEndTime > time.Now().Unix()
} }
// conversationMuteFromRecord 与 relation.GetMute 判定一致:未记录/已过期则未静音;永久为 duration=-1 且 end=0。 // fillConversationUserMute 根据会话模型字段(已由 ConversationDB2Pb 通过 CopyStructFields 填入
func conversationMuteFromRecord(rec *model.UserMute, nowUnix int64) (isMuted bool, muteDuration int32, muteEndTime int64) { // conv.MuteDuration / conv.MuteEndTime计算并设置 conv.IsMuted无需额外数据库查询。
if rec == nil { func (c *conversationServer) fillConversationUserMute(_ context.Context, conv *pbconversation.Conversation) {
return false, 0, 0 if conv == nil {
}
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 {
return return
} }
if conv.ConversationType != constant.SingleChatType || conv.UserID == "" { conv.IsMuted = computeIsMuted(conv.MuteDuration, conv.MuteEndTime)
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
} }
func (c *conversationServer) fillConversationsUserMute(ctx context.Context, list []*pbconversation.Conversation) { func (c *conversationServer) fillConversationsUserMute(ctx context.Context, list []*pbconversation.Conversation) {
if len(list) == 0 {
return
}
for _, conv := range list { for _, conv := range list {
c.fillConversationUserMute(ctx, conv) 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
}

View File

@ -72,7 +72,6 @@ type msgServer struct {
conversationClient *rpcli.ConversationClient conversationClient *rpcli.ConversationClient
spamReportDB database.SpamReport spamReportDB database.SpamReport
globalBlackDB controller.UserGlobalBlackDatabase globalBlackDB controller.UserGlobalBlackDatabase
userMuteDB controller.UserMuteDatabase
msgBurnDeadlineDB database.MsgBurnDeadline msgBurnDeadlineDB database.MsgBurnDeadline
} }
@ -138,10 +137,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
if err != nil { if err != nil {
return err return err
} }
userMuteMgo, err := mgo.NewUserMuteMongo(mgocli.GetDB())
if err != nil {
return err
}
s := &msgServer{ s := &msgServer{
MsgDatabase: msgDatabase, MsgDatabase: msgDatabase,
RegisterCenter: client, RegisterCenter: client,
@ -154,7 +149,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
conversationClient: conversationClient, conversationClient: conversationClient,
spamReportDB: spamReportDB, spamReportDB: spamReportDB,
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo), globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo),
userMuteDB: controller.NewUserMuteDatabase(userMuteMgo),
msgBurnDeadlineDB: msgBurnDeadlineDB, msgBurnDeadlineDB: msgBurnDeadlineDB,
} }

View File

@ -325,14 +325,20 @@ func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, us
} }
} }
// 第四优先级用户静音设置user_mute 集合,支持好友与非好友) // 第四优先级:会话静音设置(存储于 conversations 集合的 mute_duration/mute_end_time
// 无论会话记录是否存在均检查,以支持对非好友的静音 conv, convErr := m.ConversationLocalCache.GetConversation(ctx, userID, conversationID)
if m.userMuteDB != nil { if convErr != nil && !errs.ErrRecordNotFound.Is(convErr) {
muted, err := m.userMuteDB.IsMuted(ctx, userID, pb.MsgData.SendID) return false, convErr
if err != nil { }
return false, err 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 { if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10) pb.MsgData.Options = make(map[string]bool, 10)
} }

View File

@ -37,4 +37,6 @@ type Conversation struct {
IsMsgDestruct bool `bson:"is_msg_destruct"` IsMsgDestruct bool `bson:"is_msg_destruct"`
MsgDestructTime int64 `bson:"msg_destruct_time"` MsgDestructTime int64 `bson:"msg_destruct_time"`
LatestMsgDestructTime time.Time `bson:"latest_msg_destruct_time"` LatestMsgDestructTime time.Time `bson:"latest_msg_destruct_time"`
MuteDuration int32 `bson:"mute_duration"`
MuteEndTime int64 `bson:"mute_end_time"`
} }