mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-07-14 01:54:26 +08:00
举报垃圾消息
This commit is contained in:
parent
a3bda263a7
commit
7e65b21c5e
@ -379,3 +379,15 @@ func (m *MessageApi) GetStreamMsg(c *gin.Context) {
|
|||||||
func (m *MessageApi) AppendStreamMsg(c *gin.Context) {
|
func (m *MessageApi) AppendStreamMsg(c *gin.Context) {
|
||||||
a2r.Call(c, msg.MsgClient.GetServerTime, m.Client)
|
a2r.Call(c, msg.MsgClient.GetServerTime, m.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MessageApi) ReportSpam(c *gin.Context) {
|
||||||
|
a2r.Call(c, msg.MsgClient.ReportSpam, m.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageApi) GetSpamReports(c *gin.Context) {
|
||||||
|
a2r.Call(c, msg.MsgClient.GetSpamReports, m.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageApi) HandleSpamReport(c *gin.Context) {
|
||||||
|
a2r.Call(c, msg.MsgClient.HandleSpamReport, m.Client)
|
||||||
|
}
|
||||||
|
|||||||
@ -288,6 +288,9 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
msgGroup.POST("/batch_send_msg", m.BatchSendMsg)
|
msgGroup.POST("/batch_send_msg", m.BatchSendMsg)
|
||||||
msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess)
|
msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess)
|
||||||
msgGroup.POST("/get_server_time", m.GetServerTime)
|
msgGroup.POST("/get_server_time", m.GetServerTime)
|
||||||
|
msgGroup.POST("/report_spam", m.ReportSpam)
|
||||||
|
msgGroup.POST("/get_spam_reports", m.GetSpamReports)
|
||||||
|
msgGroup.POST("/handle_spam_report", m.HandleSpamReport)
|
||||||
}
|
}
|
||||||
// Conversation
|
// Conversation
|
||||||
{
|
{
|
||||||
|
|||||||
143
internal/rpc/msg/report.go
Normal file
143
internal/rpc/msg/report.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package msg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||||
|
"github.com/openimsdk/protocol/msg"
|
||||||
|
"github.com/openimsdk/tools/errs"
|
||||||
|
"github.com/openimsdk/tools/mcontext"
|
||||||
|
"github.com/openimsdk/tools/utils/datautil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func genReportID() string {
|
||||||
|
const dataLen = 12
|
||||||
|
data := make([]byte, dataLen)
|
||||||
|
rand.Read(data)
|
||||||
|
chars := []byte("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
data[i] = chars[data[i]%byte(len(chars))]
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgServer) ReportSpam(ctx context.Context, req *msg.ReportSpamReq) (*msg.ReportSpamResp, error) {
|
||||||
|
if req.ReportedUserID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("reportedUserID is required")
|
||||||
|
}
|
||||||
|
if req.ReasonType <= 0 {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("reasonType must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
reporterUserID := mcontext.GetOpUserID(ctx)
|
||||||
|
|
||||||
|
report := &model.SpamReport{
|
||||||
|
ReporterUserID: reporterUserID,
|
||||||
|
ReportedUserID: req.ReportedUserID,
|
||||||
|
ConversationID: req.ConversationID,
|
||||||
|
ClientMsgID: req.ClientMsgID,
|
||||||
|
Seq: req.Seq,
|
||||||
|
ReasonType: req.ReasonType,
|
||||||
|
Reason: req.Reason,
|
||||||
|
Status: model.SpamReportStatusPending,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
Ex: req.Ex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique reportID.
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
id := genReportID()
|
||||||
|
existing, err := m.spamReportDB.Get(ctx, id)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
report.ReportID = id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if report.ReportID == "" {
|
||||||
|
return nil, errs.ErrInternalServer.WrapMsg("failed to generate report ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.spamReportDB.Create(ctx, report); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &msg.ReportSpamResp{ReportID: report.ReportID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgServer) GetSpamReports(ctx context.Context, req *msg.GetSpamReportsReq) (*msg.GetSpamReportsResp, error) {
|
||||||
|
if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var start, end time.Time
|
||||||
|
if req.StartTime > 0 {
|
||||||
|
start = time.UnixMilli(req.StartTime)
|
||||||
|
}
|
||||||
|
if req.EndTime > 0 {
|
||||||
|
end = time.UnixMilli(req.EndTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
total, reports, err := m.spamReportDB.Find(ctx, req.Status, req.ReportedUserID, req.ReporterUserID,
|
||||||
|
start, end, req.Pagination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pbReports := datautil.Slice(reports, func(r *model.SpamReport) *msg.SpamReportInfo {
|
||||||
|
return &msg.SpamReportInfo{
|
||||||
|
ReportID: r.ReportID,
|
||||||
|
ReporterUserID: r.ReporterUserID,
|
||||||
|
ReportedUserID: r.ReportedUserID,
|
||||||
|
ConversationID: r.ConversationID,
|
||||||
|
ClientMsgID: r.ClientMsgID,
|
||||||
|
Seq: r.Seq,
|
||||||
|
ReasonType: r.ReasonType,
|
||||||
|
Reason: r.Reason,
|
||||||
|
Status: r.Status,
|
||||||
|
CreateTime: r.CreateTime.UnixMilli(),
|
||||||
|
HandleTime: r.HandleTime.UnixMilli(),
|
||||||
|
HandlerUserID: r.HandlerUserID,
|
||||||
|
Ex: r.Ex,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &msg.GetSpamReportsResp{
|
||||||
|
Reports: pbReports,
|
||||||
|
Total: uint32(total),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgServer) HandleSpamReport(ctx context.Context, req *msg.HandleSpamReportReq) (*msg.HandleSpamReportResp, error) {
|
||||||
|
if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.ReportID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("reportID is required")
|
||||||
|
}
|
||||||
|
if req.Status != model.SpamReportStatusHandled && req.Status != model.SpamReportStatusIgnored {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("status must be 1 (handled) or 2 (ignored)")
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerUserID := mcontext.GetOpUserID(ctx)
|
||||||
|
if err := m.spamReportDB.UpdateStatus(ctx, req.ReportID, req.Status, handlerUserID, time.Now()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &msg.HandleSpamReportResp{}, nil
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/webhook"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/webhook"
|
||||||
"github.com/openimsdk/protocol/sdkws"
|
"github.com/openimsdk/protocol/sdkws"
|
||||||
@ -69,6 +70,7 @@ type msgServer struct {
|
|||||||
config *Config // Global configuration settings.
|
config *Config // Global configuration settings.
|
||||||
webhookClient *webhook.Client
|
webhookClient *webhook.Client
|
||||||
conversationClient *rpcli.ConversationClient
|
conversationClient *rpcli.ConversationClient
|
||||||
|
spamReportDB database.SpamReport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
|
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
|
||||||
@ -121,6 +123,10 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conversationClient := rpcli.NewConversationClient(conversationConn)
|
conversationClient := rpcli.NewConversationClient(conversationConn)
|
||||||
|
spamReportDB, err := mgo.NewSpamReportMongo(mgocli.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s := &msgServer{
|
s := &msgServer{
|
||||||
MsgDatabase: msgDatabase,
|
MsgDatabase: msgDatabase,
|
||||||
RegisterCenter: client,
|
RegisterCenter: client,
|
||||||
@ -131,6 +137,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
config: config,
|
config: config,
|
||||||
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
|
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
|
||||||
conversationClient: conversationClient,
|
conversationClient: conversationClient,
|
||||||
|
spamReportDB: spamReportDB,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))
|
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))
|
||||||
|
|||||||
110
pkg/common/storage/database/mgo/spam_report.go
Normal file
110
pkg/common/storage/database/mgo/spam_report.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
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/db/mongoutil"
|
||||||
|
"github.com/openimsdk/tools/db/pagination"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSpamReportMongo(db *mongo.Database) (database.SpamReport, error) {
|
||||||
|
coll := db.Collection(database.SpamReportName)
|
||||||
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "report_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "reporter_user_id", Value: 1},
|
||||||
|
{Key: "create_time", Value: -1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "reported_user_id", Value: 1},
|
||||||
|
{Key: "create_time", Value: -1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "status", Value: 1},
|
||||||
|
{Key: "create_time", Value: -1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &SpamReportMgo{coll: coll}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpamReportMgo struct {
|
||||||
|
coll *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) Create(ctx context.Context, report *model.SpamReport) error {
|
||||||
|
return mongoutil.InsertOne(ctx, s.coll, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) Find(ctx context.Context, status int32, reportedUserID, reporterUserID string,
|
||||||
|
start, end time.Time, pagination pagination.Pagination) (int64, []*model.SpamReport, error) {
|
||||||
|
filter := bson.M{}
|
||||||
|
if status >= 0 {
|
||||||
|
filter["status"] = status
|
||||||
|
}
|
||||||
|
if reportedUserID != "" {
|
||||||
|
filter["reported_user_id"] = reportedUserID
|
||||||
|
}
|
||||||
|
if reporterUserID != "" {
|
||||||
|
filter["reporter_user_id"] = reporterUserID
|
||||||
|
}
|
||||||
|
if !start.IsZero() || !end.IsZero() {
|
||||||
|
timeFilter := bson.M{}
|
||||||
|
if !start.IsZero() {
|
||||||
|
timeFilter["$gte"] = start
|
||||||
|
}
|
||||||
|
if !end.IsZero() {
|
||||||
|
timeFilter["$lte"] = end
|
||||||
|
}
|
||||||
|
filter["create_time"] = timeFilter
|
||||||
|
}
|
||||||
|
return mongoutil.FindPage[*model.SpamReport](ctx, s.coll, filter, pagination,
|
||||||
|
options.Find().SetSort(bson.D{{Key: "create_time", Value: -1}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) UpdateStatus(ctx context.Context, reportID string, status int32, handlerUserID string, handleTime time.Time) error {
|
||||||
|
return mongoutil.UpdateOne(ctx, s.coll,
|
||||||
|
bson.M{"report_id": reportID},
|
||||||
|
bson.M{"$set": bson.M{
|
||||||
|
"status": status,
|
||||||
|
"handler_user_id": handlerUserID,
|
||||||
|
"handle_time": handleTime,
|
||||||
|
}},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) Get(ctx context.Context, reportID string) (*model.SpamReport, error) {
|
||||||
|
return mongoutil.FindOne[*model.SpamReport](ctx, s.coll, bson.M{"report_id": reportID})
|
||||||
|
}
|
||||||
@ -21,4 +21,5 @@ const (
|
|||||||
PhoneSNInfoName = "phone_sn_info"
|
PhoneSNInfoName = "phone_sn_info"
|
||||||
SignalInvitationName = "signal_invitation"
|
SignalInvitationName = "signal_invitation"
|
||||||
SignalRecordName = "signal_record"
|
SignalRecordName = "signal_record"
|
||||||
|
SpamReportName = "spam_report"
|
||||||
)
|
)
|
||||||
|
|||||||
35
pkg/common/storage/database/spam_report.go
Normal file
35
pkg/common/storage/database/spam_report.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||||
|
"github.com/openimsdk/tools/db/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpamReport interface {
|
||||||
|
// Create inserts a new spam report record.
|
||||||
|
Create(ctx context.Context, report *model.SpamReport) error
|
||||||
|
// Find queries spam reports with optional filters, returns total count and records.
|
||||||
|
Find(ctx context.Context, status int32, reportedUserID, reporterUserID string,
|
||||||
|
start, end time.Time, pagination pagination.Pagination) (int64, []*model.SpamReport, error)
|
||||||
|
// UpdateStatus updates the handling status of a spam report.
|
||||||
|
UpdateStatus(ctx context.Context, reportID string, status int32, handlerUserID string, handleTime time.Time) error
|
||||||
|
// Get retrieves a single spam report by its reportID.
|
||||||
|
Get(ctx context.Context, reportID string) (*model.SpamReport, error)
|
||||||
|
}
|
||||||
53
pkg/common/storage/model/spam_report.go
Normal file
53
pkg/common/storage/model/spam_report.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpamReport status constants.
|
||||||
|
const (
|
||||||
|
SpamReportStatusPending int32 = 0 // 待处理
|
||||||
|
SpamReportStatusHandled int32 = 1 // 已处理
|
||||||
|
SpamReportStatusIgnored int32 = 2 // 已忽略
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpamReport reason type constants.
|
||||||
|
const (
|
||||||
|
SpamReasonTypeSpam int32 = 1 // 垃圾消息
|
||||||
|
SpamReasonTypePorn int32 = 2 // 色情内容
|
||||||
|
SpamReasonTypeIllegal int32 = 3 // 违法内容
|
||||||
|
SpamReasonTypeOther int32 = 4 // 其他
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpamReport struct {
|
||||||
|
ID primitive.ObjectID `bson:"_id"`
|
||||||
|
ReportID string `bson:"report_id"`
|
||||||
|
ReporterUserID string `bson:"reporter_user_id"`
|
||||||
|
ReportedUserID string `bson:"reported_user_id"`
|
||||||
|
ConversationID string `bson:"conversation_id"` // 举报具体消息时填写
|
||||||
|
ClientMsgID string `bson:"client_msg_id"` // 举报具体消息时填写
|
||||||
|
Seq int64 `bson:"seq"`
|
||||||
|
ReasonType int32 `bson:"reason_type"` // 1垃圾 2色情 3违法 4其他
|
||||||
|
Reason string `bson:"reason"`
|
||||||
|
Status int32 `bson:"status"` // 0待处理 1已处理 2已忽略
|
||||||
|
CreateTime time.Time `bson:"create_time"`
|
||||||
|
HandleTime time.Time `bson:"handle_time"`
|
||||||
|
HandlerUserID string `bson:"handler_user_id"`
|
||||||
|
Ex string `bson:"ex"`
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user