好友和非好友静音设置

pull/3727/head
hawklin2017 4 weeks ago
parent 7defda3c5c
commit 542d479829

@ -0,0 +1,518 @@
# 红包 API 接口文档
**Base URL:** `/redpacket`
**协议:** HTTP POST`Content-Type: application/json`
**认证:** 请求头携带 `token: <JWT令牌>`(标注需要登录的接口)
> **统一响应结构**
>
> ```json
> {
> "errCode": 0,
> "errMsg": "ok",
> "errDlt": "",
> "data": { }
> }
> ```
>
> `errCode``0` 表示成功,非 0 表示错误。
---
## 1. 创建红包订单
**POST** `/redpacket/create_order`
需要登录。创建一条待上链的红包订单,返回业务 IDbizID供后续链上交易关联。
### 请求体
```json
{
"chainType": "EVM",
"chainID": 1,
"contractAddress": "0xAbCd...",
"creatorWallet": "0x1234...",
"groupID": "group_001",
"scopeType": "GROUP",
"receiverUserID": "",
"receiverUserIDs": [],
"packetType": 0,
"token": "0x0000000000000000000000000000000000000000",
"totalAmount": "1000000000000000000",
"totalShares": 10,
"expiryAt": 1800000000,
"remark": "新年快乐"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `chainType` | string | **必填** | 链类型,仅支持 `"EVM"``"TRON"` |
| `chainID` | int64 | 可选 | 链 ID为 0 时从链客户端自动获取 |
| `contractAddress` | string | 可选 | 合约地址;为空时从链客户端自动获取 |
| `creatorWallet` | string | **必填** | 创建者钱包地址 |
| `scopeType` | string | 可选 | 范围类型:`GROUP`(群组)/`DIRECT`(定向)/`PUBLIC`(公开),默认 `PUBLIC` |
| `groupID` | string | **GROUP 时必填** | 群组 ID`scopeType=GROUP` 时必须提供) |
| `receiverUserID` | string | **transfer 时必填** | 接收者用户 ID`packetType=2` 且 `scopeType=DIRECT` 时必须提供) |
| `receiverUserIDs` | []string | **DIRECT + fixed/random 时必填** | 多接收者用户 ID 列表(`scopeType=DIRECT` 且 `packetType=0/1` 时使用) |
| `packetType` | int32 | **必填** | 红包类型:`0`=均分红包,`1`=随机红包,`2`=转账红包 |
| `token` | string | 可选 | ERC20 代币合约地址;为空表示原生代币 |
| `totalAmount` | string | **必填** | 总金额(最小单位整数字符串,如 wei必须为正整数 |
| `totalShares` | int32 | **必填packetType 0/1** | 红包份数;均分/随机红包 >0 且 ≤10000转账红包固定为 1 |
| `expiryAt` | int64 | 可选 | 过期时间Unix 时间戳秒0 表示不过期;必须为将来时间 |
| `remark` | string | 可选 | 备注 |
**packetType 规则说明:**
- `0`(均分):`scopeType` 必须为 `GROUP``totalAmount` 必须被 `totalShares` 整除
- `1`(随机):`scopeType` 必须为 `GROUP``totalAmount` ≥ `totalShares`
- `2`(转账):`scopeType` 必须为 `DIRECT``totalShares` 必须为 1`receiverUserID` 必须提供,不能转给自己
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"bizID": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `bizID` | string | 业务订单 ID用于链上交易完成后提交回调 |
---
## 2. 红包创建回调(链上确认)
**POST** `/redpacket/created_callback`
需要登录。链上交易广播后,由创建者提交交易哈希以激活红包。仅创建者可调用。
### 请求体
```json
{
"bizID": "550e8400-e29b-41d4-a716-446655440000",
"txHash": "0xabc123...",
"packetID": "",
"groupID": "",
"scopeType": "",
"receiverUserID": "",
"receiverUserIDs": []
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `bizID` | string | **必填** | 创建订单时返回的业务 ID |
| `txHash` | string | **必填** | 链上交易哈希 |
| `packetID` | string | 条件必填 | 链上红包 ID链客户端离线时必须手动提供 |
| `groupID` | string | 可选 | 覆盖订单的群组 ID不填则继承订单值 |
| `scopeType` | string | 可选 | 覆盖订单的范围类型 |
| `receiverUserID` | string | 可选 | 覆盖单一接收者 |
| `receiverUserIDs` | []string | 可选 | 覆盖多接收者列表 |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {}
}
```
---
## 3. 查询红包详情
**POST** `/redpacket/detail`
### 请求体
```json
{
"packetID": "12345"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `packetID` | string | **必填** | 链上红包 ID |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"record": {
"bizID": "550e8400-...",
"chainType": "EVM",
"packetID": "12345",
"chainID": 1,
"contractAddress": "0xAbCd...",
"creatorUserID": "user_001",
"creatorWallet": "0x1234...",
"groupID": "group_001",
"scopeType": "GROUP",
"receiverUserID": "",
"receiverUserIDs": [],
"packetType": 0,
"token": "0x0000000000000000000000000000000000000000",
"totalAmount": "1000000000000000000",
"totalShares": 10,
"claimedAmount": "300000000000000000",
"claimedShares": 3,
"expiryAt": 1800000000,
"txHash": "0xabc123...",
"status": "ACTIVE",
"createdAt": 1715500000,
"updatedAt": 1715500100
},
"claims": [
{
"packetID": "12345",
"userID": "user_002",
"claimerWallet": "0x5678...",
"authNonce": "1715500050000000000",
"claimTxHash": "0xdef456...",
"claimedAmount": "100000000000000000",
"blockNumber": 19000000,
"status": "CONFIRMED",
"createdAt": 1715500050,
"updatedAt": 1715500060
}
]
}
}
```
**record 字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `status` | string | 红包状态:`PENDING` / `ACTIVE` / `COMPLETED` / `REFUNDED` / `EXPIRED` |
| `totalAmount` | string | 总金额(最小单位) |
| `claimedAmount` | string | 已领金额 |
| `totalShares` / `claimedShares` | int32 | 总份数 / 已领份数 |
---
## 4. 申请领取签名
**POST** `/redpacket/issue_claim_sign`
需要登录。领取红包前,先获取服务端签名用于链上验证。会校验领取资格(是否群成员/好友、是否已领取、红包状态等)。
### 请求体
```json
{
"packetID": "12345",
"claimer": "0x5678...",
"randomSeed": ""
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `packetID` | string | **必填** | 链上红包 ID |
| `claimer` | string | **必填** | 领取者钱包地址 |
| `randomSeed` | string | 可选 | 随机种子(十进制整数字符串);为空或 `"0"` 时服务端自动生成 |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"authNonce": "1715500050000000000",
"deadline": 1715500350,
"signature": "0xaabbcc...",
"randomSeed": "8765309000000"
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `authNonce` | string | 认证随机数(传给合约) |
| `deadline` | int64 | 签名过期时间Unix 时间戳,约 5 分钟后) |
| `signature` | string | 服务端签名0x 前缀十六进制65 字节) |
| `randomSeed` | string | 使用的随机种子 |
---
## 5. 提交领取结果
**POST** `/redpacket/claim_result`
需要登录。链上领取交易广播后提交,服务端解析链上事件并更新领取记录。
### 请求体
```json
{
"packetID": "12345",
"claimer": "0x5678...",
"txHash": "0xdef456..."
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `packetID` | string | **必填** | 链上红包 ID |
| `claimer` | string | **必填** | 领取者钱包地址 |
| `txHash` | string | **必填** | 领取交易哈希 |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {}
}
```
---
## 6. 申请退款
**POST** `/redpacket/request_refund`
需要登录。仅创建者可调用,红包到期后提交链上退款交易。
### 请求体
```json
{
"packetID": "12345"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `packetID` | string | **必填** | 链上红包 ID红包必须已到期 |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"txHash": "0xghi789...",
"status": "PENDING"
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `txHash` | string | 退款交易哈希 |
| `status` | string | `PENDING`(退款交易已提交)或 `REFUNDED`(已退款) |
---
## 7. 查询退款记录
**POST** `/redpacket/get_refund`
### 请求体
```json
{
"packetID": "12345"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `packetID` | string | **必填** | 链上红包 ID |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"packetID": "12345",
"refundTo": "0x1234...",
"txHash": "0xghi789...",
"amount": "700000000000000000",
"createdAt": 1715600000
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `refundTo` | string | 退款目标钱包地址 |
| `amount` | string | 退款金额(最小单位) |
| `createdAt` | int64 | 退款记录创建时间Unix 时间戳) |
---
## 8. 发起钱包绑定挑战
**POST** `/redpacket/wallet_bind/challenge`
需要登录。生成一条待用户签名的消息用于将钱包地址绑定到当前用户账户EVM 使用 EIP-4361 SIWETRON 使用 signMessageV2
### 请求体
```json
{
"chainType": "EVM",
"chainID": 1,
"walletAddress": "0x5678...",
"domain": "myapp.example.com",
"uri": "https://myapp.example.com/wallet-bind"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `chainType` | string | **必填** | 链类型,`"EVM"` 或 `"TRON"` |
| `walletAddress` | string | **必填** | 待绑定的钱包地址 |
| `chainID` | int64 | 可选 | 链 IDEVM 时建议提供) |
| `domain` | string | 可选 | 应用域名,默认 `"redpacket"` |
| `uri` | string | 可选 | 应用 URI默认 `"https://redpacket.local/wallet-bind"` |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"challengeID": "aaaa-bbbb-cccc-dddd",
"userID": "user_001",
"chainType": "EVM",
"chainID": 1,
"wallet": "0x5678...",
"protocol": "siwe-eip4361",
"signMethod": "personal_sign",
"nonce": "xxxx-yyyy-zzzz",
"message": "myapp.example.com wants you to sign in with your Ethereum account:\n0x5678...\n\nBind wallet ...",
"issuedAt": "2026-05-12T08:39:00Z",
"expiresAt": "2026-05-12T08:49:00Z"
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `challengeID` | string | 挑战 ID确认绑定时使用 |
| `message` | string | 待签名的完整消息文本 |
| `protocol` | string | EVM: `siwe-eip4361`TRON: `tron-signmessagev2` |
| `signMethod` | string | EVM: `personal_sign`TRON: `signMessageV2` |
| `expiresAt` | string | 挑战过期时间RFC333910 分钟有效期) |
---
## 9. 确认钱包绑定
**POST** `/redpacket/wallet_bind/confirm`
提交用户对挑战消息的签名,服务端验证后完成钱包绑定。挑战有效期 10 分钟。
### 请求体
```json
{
"challengeID": "aaaa-bbbb-cccc-dddd",
"signature": "0xaabbccdd..."
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `challengeID` | string | **必填** | 挑战 ID来自 `/wallet_bind/challenge` |
| `signature` | string | **必填** | 钱包签名0x 前缀十六进制65 字节) |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"userID": "user_001",
"chainType": "EVM",
"chainID": 1,
"walletAddress": "0x5678...",
"status": "ACTIVE",
"verifiedAt": "2026-05-12T08:42:00Z"
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `status` | string | 绑定状态,成功时为 `ACTIVE` |
| `verifiedAt` | string | 绑定验证时间RFC3339 |
---
## 10. 查询钱包绑定信息
**POST** `/redpacket/wallet_bind/detail`
需要登录。查询当前登录用户在指定链上的活跃钱包绑定。
### 请求体
```json
{
"chainType": "EVM",
"walletAddress": "0x5678..."
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `chainType` | string | **必填** | 链类型,`"EVM"` 或 `"TRON"` |
| `walletAddress` | string | 可选 | 钱包地址过滤条件 |
### 响应体
```json
{
"errCode": 0,
"errMsg": "ok",
"data": {
"userID": "user_001",
"chainType": "EVM",
"chainID": 1,
"walletAddress": "0x5678...",
"status": "ACTIVE",
"challengeID": "aaaa-bbbb-cccc-dddd",
"verifiedAt": "2026-05-12T08:42:00Z"
}
}
```
---
## 附录:公共枚举值
| 枚举 | 可选值 | 说明 |
|------|--------|------|
| `chainType` | `EVM` / `TRON` | 区块链类型 |
| `scopeType` | `GROUP` / `DIRECT` / `PUBLIC` | 红包范围 |
| `packetType` | `0` / `1` / `2` | 均分 / 随机 / 转账 |
| 红包 `status` | `PENDING` / `ACTIVE` / `COMPLETED` / `REFUNDED` / `EXPIRED` | 红包状态 |
| 领取 `status` | `PENDING` / `CONFIRMED` / `FAILED` | 领取记录状态 |
| 钱包绑定 `status` | `ACTIVE` | 绑定状态(查询时只返回活跃绑定) |
---
## 实现参考
- 路由:`internal/api/router.go``/redpacket` 分组)
- HTTP 处理:`internal/api/redpacket.go`
- Proto 定义:`protocol/redpacket/redpacket.proto`
- RPC 实现:`internal/rpc/redpacket/service.go`、`internal/rpc/redpacket/wallet.go`

@ -126,3 +126,11 @@ func (o *FriendApi) GetPinnedFriendIDs(c *gin.Context) {
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)
}

@ -221,6 +221,8 @@ 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)
}
g := NewGroupApi(group.NewGroupClient(groupConn))

@ -72,6 +72,7 @@ type msgServer struct {
conversationClient *rpcli.ConversationClient
spamReportDB database.SpamReport
globalBlackDB controller.UserGlobalBlackDatabase
userMuteDB controller.UserMuteDatabase
msgBurnDeadlineDB database.MsgBurnDeadline
}
@ -137,6 +138,10 @@ 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,
@ -149,6 +154,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
conversationClient: conversationClient,
spamReportDB: spamReportDB,
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo),
userMuteDB: controller.NewUserMuteDatabase(userMuteMgo),
msgBurnDeadlineDB: msgBurnDeadlineDB,
}

@ -304,25 +304,38 @@ func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, us
// 第三优先级:会话级接收偏好
singleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID)
if errs.ErrRecordNotFound.Is(err) {
return true, nil
} else if err != nil {
if err != nil && !errs.ErrRecordNotFound.Is(err) {
return false, err
}
switch singleOpt {
case constant.ReceiveMessage:
return true, nil
case constant.NotReceiveMessage:
if datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {
if err == nil {
switch singleOpt {
case constant.NotReceiveMessage:
if datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {
return true, nil
}
return false, nil
case constant.ReceiveNotNotifyMessage:
if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10)
}
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
return true, nil
}
return false, nil
case constant.ReceiveNotNotifyMessage:
if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10)
}
// 第四优先级用户静音设置user_mute 集合,支持好友与非好友)
// 无论会话记录是否存在均检查,以支持对非好友的静音
if m.userMuteDB != nil {
muted, err := m.userMuteDB.IsMuted(ctx, userID, pb.MsgData.SendID)
if err != nil {
return false, err
}
if muted {
if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10)
}
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
}
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
return true, nil
}
return true, nil
}

@ -16,6 +16,7 @@ package relation
import (
"context"
"time"
"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user"
"github.com/openimsdk/open-im-server/v3/pkg/rpcli"
@ -56,6 +57,7 @@ type friendServer struct {
db controller.FriendDatabase
blackDatabase controller.BlackDatabase
globalBlackDB controller.UserGlobalBlackDatabase
userMuteDB controller.UserMuteDatabase
notificationSender *FriendNotificationSender
RegisterCenter discovery.SvcDiscoveryRegistry
config *Config
@ -107,6 +109,11 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
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
@ -144,6 +151,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
redis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB, redis.GetRocksCacheOptions()),
),
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMongoDB),
userMuteDB: controller.NewUserMuteDatabase(userMuteMongoDB),
notificationSender: notificationSender,
RegisterCenter: client,
config: config,
@ -718,6 +726,43 @@ func (s *friendServer) AddOnewayFriend(ctx context.Context, req *relation.ApplyT
return &relation.ApplyToAddFriendResp{}, nil
}
func (s *friendServer) SetMute(ctx context.Context, req *relation.SetMuteReq) (*relation.SetMuteResp, error) {
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
return nil, err
}
if req.Duration == 0 {
return &relation.SetMuteResp{}, s.userMuteDB.Delete(ctx, req.OwnerUserID, req.TargetUserID)
}
var muteEndTime int64
if req.Duration != -1 {
muteEndTime = time.Now().Unix() + req.Duration
}
return &relation.SetMuteResp{}, s.userMuteDB.Upsert(ctx, &model.UserMute{
OwnerUserID: req.OwnerUserID,
MutedUserID: req.TargetUserID,
MuteEndTime: muteEndTime,
CreateTime: time.Now(),
})
}
func (s *friendServer) GetMute(ctx context.Context, req *relation.GetMuteReq) (*relation.GetMuteResp, error) {
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
return nil, err
}
rec, err := s.userMuteDB.Get(ctx, req.OwnerUserID, req.TargetUserID)
if err != nil {
return nil, err
}
if rec == nil {
return &relation.GetMuteResp{Muted: false, MuteEndTime: 0}, nil
}
now := time.Now().Unix()
if rec.MuteEndTime != 0 && rec.MuteEndTime <= now {
return &relation.GetMuteResp{Muted: false, MuteEndTime: 0}, nil
}
return &relation.GetMuteResp{Muted: true, MuteEndTime: rec.MuteEndTime}, nil
}
func (s *friendServer) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) {
users, err := s.userClient.GetUsersInfo(ctx, userIDs)
if err != nil {

@ -0,0 +1,44 @@
package controller
import (
"context"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
)
// UserMuteDatabase 用户静音业务接口
type UserMuteDatabase interface {
// Upsert 新增或更新静音记录
Upsert(ctx context.Context, mute *model.UserMute) error
// Delete 取消静音
Delete(ctx context.Context, ownerUserID, mutedUserID string) error
// IsMuted 检查 ownerUserID 是否对 mutedUserID 设置了有效静音
IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error)
// Get 查询静音记录;不存在则 (nil, nil)
Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error)
}
type userMuteDatabase struct {
db database.UserMute
}
func NewUserMuteDatabase(db database.UserMute) UserMuteDatabase {
return &userMuteDatabase{db: db}
}
func (u *userMuteDatabase) Upsert(ctx context.Context, mute *model.UserMute) error {
return u.db.Upsert(ctx, mute)
}
func (u *userMuteDatabase) Delete(ctx context.Context, ownerUserID, mutedUserID string) error {
return u.db.Delete(ctx, ownerUserID, mutedUserID)
}
func (u *userMuteDatabase) IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error) {
return u.db.IsMuted(ctx, ownerUserID, mutedUserID)
}
func (u *userMuteDatabase) Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error) {
return u.db.Get(ctx, ownerUserID, mutedUserID)
}

@ -0,0 +1,95 @@
package mgo
import (
"context"
"time"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/errs"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func NewUserMuteMongo(db *mongo.Database) (database.UserMute, error) {
coll := db.Collection(database.UserMuteName)
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
Keys: bson.D{
{Key: "owner_user_id", Value: 1},
{Key: "muted_user_id", Value: 1},
},
Options: options.Index().SetUnique(true),
})
if err != nil {
return nil, errs.Wrap(err)
}
return &UserMuteMgo{coll: coll}, nil
}
type UserMuteMgo struct {
coll *mongo.Collection
}
func (u *UserMuteMgo) Upsert(ctx context.Context, mute *model.UserMute) error {
if mute.CreateTime.IsZero() {
mute.CreateTime = time.Now()
}
filter := bson.M{
"owner_user_id": mute.OwnerUserID,
"muted_user_id": mute.MutedUserID,
}
update := bson.M{
"$set": bson.M{
"mute_end_time": mute.MuteEndTime,
},
"$setOnInsert": bson.M{
"owner_user_id": mute.OwnerUserID,
"muted_user_id": mute.MutedUserID,
"create_time": mute.CreateTime,
},
}
_, err := u.coll.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
return errs.Wrap(err)
}
func (u *UserMuteMgo) Delete(ctx context.Context, ownerUserID, mutedUserID string) error {
_, err := u.coll.DeleteOne(ctx, bson.M{
"owner_user_id": ownerUserID,
"muted_user_id": mutedUserID,
})
return errs.Wrap(err)
}
func (u *UserMuteMgo) IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error) {
now := time.Now().Unix()
// mute_end_time == 0 means permanent; mute_end_time > now means still active
filter := bson.M{
"owner_user_id": ownerUserID,
"muted_user_id": mutedUserID,
"$or": bson.A{
bson.M{"mute_end_time": 0},
bson.M{"mute_end_time": bson.M{"$gt": now}},
},
}
count, err := u.coll.CountDocuments(ctx, filter)
if err != nil {
return false, errs.Wrap(err)
}
return count > 0, nil
}
func (u *UserMuteMgo) Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error) {
var out model.UserMute
err := u.coll.FindOne(ctx, bson.M{
"owner_user_id": ownerUserID,
"muted_user_id": mutedUserID,
}).Decode(&out)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, errs.Wrap(err)
}
return &out, nil
}

@ -25,4 +25,5 @@ const (
SpamReportName = "spam_report"
MsgBurnDeadlineName = "msg_burn_deadline"
UserOfflineRecordName = "user_offline_record"
UserMuteName = "user_mute"
)

@ -0,0 +1,19 @@
package database
import (
"context"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
)
// UserMute 用户静音持久化接口(支持好友与非好友)
type UserMute interface {
// Upsert 新增或更新静音记录
Upsert(ctx context.Context, mute *model.UserMute) error
// Delete 取消静音(删除记录)
Delete(ctx context.Context, ownerUserID, mutedUserID string) error
// IsMuted 检查 ownerUserID 是否对 mutedUserID 设置了有效的静音
IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error)
// Get 按 owner + muted 查询一条记录;不存在则 (nil, nil)
Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error)
}

@ -0,0 +1,12 @@
package model
import "time"
// UserMute records a mute relationship: OwnerUserID has muted MutedUserID.
// Works for both friends and strangers. MuteEndTime == 0 means permanent mute.
type UserMute struct {
OwnerUserID string `bson:"owner_user_id"` // who set the mute
MutedUserID string `bson:"muted_user_id"` // who is muted
MuteEndTime int64 `bson:"mute_end_time"` // Unix seconds; 0 = permanent
CreateTime time.Time `bson:"create_time"`
}

@ -1 +1 @@
Subproject commit 24c72259373e9080ab005bb10385524753d4dfc3
Subproject commit ab69a8df51841e7e70e637033c3f05b7e40bb747
Loading…
Cancel
Save