复制项目
This commit is contained in:
106
pkg/common/storage/database/mgo/black.go
Normal file
106
pkg/common/storage/database/mgo/black.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/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 NewBlackMongo(db *mongo.Database) (database.Black, error) {
|
||||
coll := db.Collection(database.BlackName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "owner_user_id", Value: 1},
|
||||
{Key: "block_user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BlackMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type BlackMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (b *BlackMgo) blackFilter(ownerUserID, blockUserID string) bson.M {
|
||||
return bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"block_user_id": blockUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlackMgo) blacksFilter(blacks []*model.Black) bson.M {
|
||||
if len(blacks) == 0 {
|
||||
return nil
|
||||
}
|
||||
or := make(bson.A, 0, len(blacks))
|
||||
for _, black := range blacks {
|
||||
or = append(or, b.blackFilter(black.OwnerUserID, black.BlockUserID))
|
||||
}
|
||||
return bson.M{"$or": or}
|
||||
}
|
||||
|
||||
func (b *BlackMgo) Create(ctx context.Context, blacks []*model.Black) (err error) {
|
||||
return mongoutil.InsertMany(ctx, b.coll, blacks)
|
||||
}
|
||||
|
||||
func (b *BlackMgo) Delete(ctx context.Context, blacks []*model.Black) (err error) {
|
||||
if len(blacks) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.DeleteMany(ctx, b.coll, b.blacksFilter(blacks))
|
||||
}
|
||||
|
||||
func (b *BlackMgo) UpdateByMap(ctx context.Context, ownerUserID, blockUserID string, args map[string]any) (err error) {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, b.coll, b.blackFilter(ownerUserID, blockUserID), bson.M{"$set": args}, false)
|
||||
}
|
||||
|
||||
func (b *BlackMgo) Find(ctx context.Context, blacks []*model.Black) (blackList []*model.Black, err error) {
|
||||
return mongoutil.Find[*model.Black](ctx, b.coll, b.blacksFilter(blacks))
|
||||
}
|
||||
|
||||
func (b *BlackMgo) Take(ctx context.Context, ownerUserID, blockUserID string) (black *model.Black, err error) {
|
||||
return mongoutil.FindOne[*model.Black](ctx, b.coll, b.blackFilter(ownerUserID, blockUserID))
|
||||
}
|
||||
|
||||
func (b *BlackMgo) FindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error) {
|
||||
return mongoutil.FindPage[*model.Black](ctx, b.coll, bson.M{"owner_user_id": ownerUserID}, pagination)
|
||||
}
|
||||
|
||||
func (b *BlackMgo) FindOwnerBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error) {
|
||||
if len(userIDs) == 0 {
|
||||
return mongoutil.Find[*model.Black](ctx, b.coll, bson.M{"owner_user_id": ownerUserID})
|
||||
}
|
||||
return mongoutil.Find[*model.Black](ctx, b.coll, bson.M{"owner_user_id": ownerUserID, "block_user_id": bson.M{"$in": userIDs}})
|
||||
}
|
||||
|
||||
func (b *BlackMgo) FindBlackUserIDs(ctx context.Context, ownerUserID string) (blackUserIDs []string, err error) {
|
||||
return mongoutil.Find[string](ctx, b.coll, bson.M{"owner_user_id": ownerUserID}, options.Find().SetProjection(bson.M{"_id": 0, "block_user_id": 1}))
|
||||
}
|
||||
183
pkg/common/storage/database/mgo/cache.go
Normal file
183
pkg/common/storage/database/mgo/cache.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"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 NewCacheMgo(db *mongo.Database) (*CacheMgo, error) {
|
||||
coll := db.Collection(database.CacheName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "key", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "expire_at", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetExpireAfterSeconds(0),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &CacheMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type CacheMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (x *CacheMgo) findToMap(res []model.Cache, now time.Time) map[string]string {
|
||||
kv := make(map[string]string)
|
||||
for _, re := range res {
|
||||
if re.ExpireAt != nil && re.ExpireAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
kv[re.Key] = re.Value
|
||||
}
|
||||
return kv
|
||||
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Get(ctx context.Context, key []string) (map[string]string, error) {
|
||||
if len(key) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
now := time.Now()
|
||||
res, err := mongoutil.Find[model.Cache](ctx, x.coll, bson.M{
|
||||
"key": bson.M{"$in": key},
|
||||
"$or": []bson.M{
|
||||
{"expire_at": bson.M{"$gt": now}},
|
||||
{"expire_at": nil},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x.findToMap(res, now), nil
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Prefix(ctx context.Context, prefix string) (map[string]string, error) {
|
||||
now := time.Now()
|
||||
res, err := mongoutil.Find[model.Cache](ctx, x.coll, bson.M{
|
||||
"key": bson.M{"$regex": "^" + prefix},
|
||||
"$or": []bson.M{
|
||||
{"expire_at": bson.M{"$gt": now}},
|
||||
{"expire_at": nil},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x.findToMap(res, now), nil
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Set(ctx context.Context, key string, value string, expireAt time.Duration) error {
|
||||
cv := &model.Cache{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
if expireAt > 0 {
|
||||
now := time.Now().Add(expireAt)
|
||||
cv.ExpireAt = &now
|
||||
}
|
||||
opt := options.Update().SetUpsert(true)
|
||||
return mongoutil.UpdateOne(ctx, x.coll, bson.M{"key": key}, bson.M{"$set": cv}, false, opt)
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Incr(ctx context.Context, key string, value int) (int, error) {
|
||||
pipeline := mongo.Pipeline{
|
||||
{
|
||||
{"$set", bson.M{
|
||||
"value": bson.M{
|
||||
"$toString": bson.M{
|
||||
"$add": bson.A{
|
||||
bson.M{"$toInt": "$value"},
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
opt := options.FindOneAndUpdate().SetReturnDocument(options.After)
|
||||
res, err := mongoutil.FindOneAndUpdate[model.Cache](ctx, x.coll, bson.M{"key": key}, pipeline, opt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(res.Value)
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Del(ctx context.Context, key []string) error {
|
||||
if len(key) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := x.coll.DeleteMany(ctx, bson.M{"key": bson.M{"$in": key}})
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
func (x *CacheMgo) lockKey(key string) string {
|
||||
return "LOCK_" + key
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Lock(ctx context.Context, key string, duration time.Duration) (string, error) {
|
||||
tmp, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if duration <= 0 || duration > time.Minute*10 {
|
||||
duration = time.Minute * 10
|
||||
}
|
||||
cv := &model.Cache{
|
||||
Key: x.lockKey(key),
|
||||
Value: tmp.String(),
|
||||
ExpireAt: nil,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
wait := func() error {
|
||||
timeout := time.NewTimer(time.Millisecond * 100)
|
||||
defer timeout.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timeout.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for {
|
||||
if err := mongoutil.DeleteOne(ctx, x.coll, bson.M{"key": key, "expire_at": bson.M{"$lt": time.Now()}}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
expireAt := time.Now().Add(duration)
|
||||
cv.ExpireAt = &expireAt
|
||||
if err := mongoutil.InsertMany[*model.Cache](ctx, x.coll, []*model.Cache{cv}); err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
if err := wait(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return cv.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CacheMgo) Unlock(ctx context.Context, key string, value string) error {
|
||||
return mongoutil.DeleteOne(ctx, x.coll, bson.M{"key": x.lockKey(key), "value": value})
|
||||
}
|
||||
133
pkg/common/storage/database/mgo/cache_test.go
Normal file
133
pkg/common/storage/database/mgo/cache_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func TestName1111(t *testing.T) {
|
||||
coll := Mongodb().Collection("temp")
|
||||
|
||||
//updatePipeline := mongo.Pipeline{
|
||||
// {
|
||||
// {"$set", bson.M{
|
||||
// "age": bson.M{
|
||||
// "$toString": bson.M{
|
||||
// "$add": bson.A{
|
||||
// bson.M{"$toInt": "$age"},
|
||||
// 1,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }},
|
||||
// },
|
||||
//}
|
||||
|
||||
pipeline := mongo.Pipeline{
|
||||
{
|
||||
{"$set", bson.M{
|
||||
"value": bson.M{
|
||||
"$toString": bson.M{
|
||||
"$add": bson.A{
|
||||
bson.M{"$toInt": "$value"},
|
||||
1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
opt := options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After)
|
||||
res, err := mongoutil.FindOneAndUpdate[model.Cache](context.Background(), coll, bson.M{"key": "123456"}, pipeline, opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log(res)
|
||||
}
|
||||
|
||||
func TestName33333(t *testing.T) {
|
||||
c, err := NewCacheMgo(Mongodb())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := c.Set(context.Background(), "123456", "123456", time.Hour); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := c.Set(context.Background(), "123666", "123666", time.Hour); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res1, err := c.Get(context.Background(), []string{"123456"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log(res1)
|
||||
|
||||
res2, err := c.Prefix(context.Background(), "123")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log(res2)
|
||||
}
|
||||
|
||||
func TestName1111aa(t *testing.T) {
|
||||
|
||||
c, err := NewCacheMgo(Mongodb())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var count int
|
||||
|
||||
key := "123456"
|
||||
|
||||
doFunc := func() {
|
||||
value, err := c.Lock(context.Background(), key, time.Second*30)
|
||||
if err != nil {
|
||||
t.Log("Lock error", err)
|
||||
return
|
||||
}
|
||||
tmp := count
|
||||
tmp++
|
||||
count = tmp
|
||||
t.Log("count", tmp)
|
||||
if err := c.Unlock(context.Background(), key, value); err != nil {
|
||||
t.Log("Unlock error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.Lock(context.Background(), key, time.Second*10); err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 32; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
doFunc()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
}
|
||||
|
||||
func TestName111111a(t *testing.T) {
|
||||
arr := strings.SplitN("1:testkakskdask:1111", ":", 2)
|
||||
t.Log(arr)
|
||||
}
|
||||
99
pkg/common/storage/database/mgo/client_config.go
Normal file
99
pkg/common/storage/database/mgo/client_config.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright © 2023 OpenIM open source community. 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/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"
|
||||
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
func NewClientConfig(db *mongo.Database) (database.ClientConfig, error) {
|
||||
coll := db.Collection("config")
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "key", Value: 1},
|
||||
{Key: "user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &ClientConfig{
|
||||
coll: coll,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (x *ClientConfig) Set(ctx context.Context, userID string, config map[string]string) error {
|
||||
if len(config) == 0 {
|
||||
return nil
|
||||
}
|
||||
for key, value := range config {
|
||||
filter := bson.M{"key": key, "user_id": userID}
|
||||
update := bson.M{
|
||||
"value": value,
|
||||
}
|
||||
err := mongoutil.UpdateOne(ctx, x.coll, filter, bson.M{"$set": update}, false, options.Update().SetUpsert(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientConfig) Get(ctx context.Context, userID string) (map[string]string, error) {
|
||||
cs, err := mongoutil.Find[*model.ClientConfig](ctx, x.coll, bson.M{"user_id": userID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cm := make(map[string]string)
|
||||
for _, config := range cs {
|
||||
cm[config.Key] = config.Value
|
||||
}
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func (x *ClientConfig) Del(ctx context.Context, userID string, keys []string) error {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.DeleteMany(ctx, x.coll, bson.M{"key": bson.M{"$in": keys}, "user_id": userID})
|
||||
}
|
||||
|
||||
func (x *ClientConfig) GetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) {
|
||||
filter := bson.M{}
|
||||
if userID != "" {
|
||||
filter["user_id"] = userID
|
||||
}
|
||||
if key != "" {
|
||||
filter["key"] = key
|
||||
}
|
||||
return mongoutil.FindPage[*model.ClientConfig](ctx, x.coll, filter, pagination)
|
||||
}
|
||||
325
pkg/common/storage/database/mgo/conversation.go
Normal file
325
pkg/common/storage/database/mgo/conversation.go
Normal file
@@ -0,0 +1,325 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
func NewConversationMongo(db *mongo.Database) (*ConversationMgo, error) {
|
||||
coll := db.Collection(database.ConversationName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "owner_user_id", Value: 1},
|
||||
{Key: "conversation_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
version, err := NewVersionLog(db.Collection(database.ConversationVersionName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ConversationMgo{version: version, coll: coll}, nil
|
||||
}
|
||||
|
||||
type ConversationMgo struct {
|
||||
version database.VersionLog
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) Create(ctx context.Context, conversations []*model.Conversation) (err error) {
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.InsertMany(ctx, c.coll, conversations)
|
||||
}, func() error {
|
||||
userConversation := make(map[string][]string)
|
||||
for _, conversation := range conversations {
|
||||
userConversation[conversation.OwnerUserID] = append(userConversation[conversation.OwnerUserID], conversation.ConversationID)
|
||||
}
|
||||
for userID, conversationIDs := range userConversation {
|
||||
if err := c.version.IncrVersion(ctx, userID, conversationIDs, model.VersionStateInsert); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) UpdateByMap(ctx context.Context, userIDs []string, conversationID string, args map[string]any) (int64, error) {
|
||||
if len(args) == 0 || len(userIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
filter := bson.M{
|
||||
"conversation_id": conversationID,
|
||||
"owner_user_id": bson.M{"$in": userIDs},
|
||||
}
|
||||
var rows int64
|
||||
err := mongoutil.IncrVersion(func() error {
|
||||
res, err := mongoutil.UpdateMany(ctx, c.coll, filter, bson.M{"$set": args})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows = res.ModifiedCount
|
||||
return nil
|
||||
}, func() error {
|
||||
for _, userID := range userIDs {
|
||||
if err := c.version.IncrVersion(ctx, userID, []string{conversationID}, model.VersionStateUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) UpdateUserConversations(ctx context.Context, userID string, args map[string]any) ([]*model.Conversation, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
filter := bson.M{
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
conversations, err := mongoutil.Find[*model.Conversation](ctx, c.coll, filter, options.Find().SetProjection(bson.M{"_id": 0, "owner_user_id": 1, "conversation_id": 1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = mongoutil.IncrVersion(func() error {
|
||||
_, err := mongoutil.UpdateMany(ctx, c.coll, filter, bson.M{"$set": args})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, func() error {
|
||||
for _, conversation := range conversations {
|
||||
if err := c.version.IncrVersion(ctx, conversation.OwnerUserID, []string{conversation.ConversationID}, model.VersionStateUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) Update(ctx context.Context, conversation *model.Conversation) (err error) {
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.UpdateOne(ctx, c.coll, bson.M{"owner_user_id": conversation.OwnerUserID, "conversation_id": conversation.ConversationID}, bson.M{"$set": conversation}, true)
|
||||
}, func() error {
|
||||
return c.version.IncrVersion(ctx, conversation.OwnerUserID, []string{conversation.ConversationID}, model.VersionStateUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) Find(ctx context.Context, ownerUserID string, conversationIDs []string) (conversations []*model.Conversation, err error) {
|
||||
return mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{"owner_user_id": ownerUserID, "conversation_id": bson.M{"$in": conversationIDs}})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindUserID(ctx context.Context, userIDs []string, conversationIDs []string) ([]string, error) {
|
||||
return mongoutil.Find[string](
|
||||
ctx,
|
||||
c.coll,
|
||||
bson.M{"owner_user_id": bson.M{"$in": userIDs}, "conversation_id": bson.M{"$in": conversationIDs}},
|
||||
options.Find().SetProjection(bson.M{"_id": 0, "owner_user_id": 1}),
|
||||
)
|
||||
}
|
||||
func (c *ConversationMgo) FindUserIDAllConversationID(ctx context.Context, userID string) ([]string, error) {
|
||||
return mongoutil.Find[string](ctx, c.coll, bson.M{"owner_user_id": userID}, options.Find().SetProjection(bson.M{"_id": 0, "conversation_id": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindUserIDAllNotNotifyConversationID(ctx context.Context, userID string) ([]string, error) {
|
||||
return mongoutil.Find[string](ctx, c.coll, bson.M{
|
||||
"owner_user_id": userID,
|
||||
"recv_msg_opt": constant.ReceiveNotNotifyMessage,
|
||||
}, options.Find().SetProjection(bson.M{"_id": 0, "conversation_id": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindUserIDAllPinnedConversationID(ctx context.Context, userID string) ([]string, error) {
|
||||
return mongoutil.Find[string](ctx, c.coll, bson.M{
|
||||
"owner_user_id": userID,
|
||||
"is_pinned": true,
|
||||
}, options.Find().SetProjection(bson.M{"_id": 0, "conversation_id": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) Take(ctx context.Context, userID, conversationID string) (conversation *model.Conversation, err error) {
|
||||
return mongoutil.FindOne[*model.Conversation](ctx, c.coll, bson.M{"owner_user_id": userID, "conversation_id": conversationID})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindConversationID(ctx context.Context, userID string, conversationIDs []string) (existConversationID []string, err error) {
|
||||
return mongoutil.Find[string](ctx, c.coll, bson.M{"owner_user_id": userID, "conversation_id": bson.M{"$in": conversationIDs}}, options.Find().SetProjection(bson.M{"_id": 0, "conversation_id": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindUserIDAllConversations(ctx context.Context, userID string) (conversations []*model.Conversation, err error) {
|
||||
return mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{"owner_user_id": userID})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindRecvMsgUserIDs(ctx context.Context, conversationID string, recvOpts []int) ([]string, error) {
|
||||
var filter any
|
||||
if len(recvOpts) == 0 {
|
||||
filter = bson.M{"conversation_id": conversationID}
|
||||
} else {
|
||||
filter = bson.M{"conversation_id": conversationID, "recv_msg_opt": bson.M{"$in": recvOpts}}
|
||||
}
|
||||
return mongoutil.Find[string](ctx, c.coll, filter, options.Find().SetProjection(bson.M{"_id": 0, "owner_user_id": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) GetUserRecvMsgOpt(ctx context.Context, ownerUserID, conversationID string) (opt int, err error) {
|
||||
return mongoutil.FindOne[int](ctx, c.coll, bson.M{"owner_user_id": ownerUserID, "conversation_id": conversationID}, options.FindOne().SetProjection(bson.M{"recv_msg_opt": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) GetAllConversationIDs(ctx context.Context) ([]string, error) {
|
||||
return mongoutil.Aggregate[string](ctx, c.coll, []bson.M{
|
||||
{"$group": bson.M{"_id": "$conversation_id"}},
|
||||
{"$project": bson.M{"_id": 0, "conversation_id": "$_id"}},
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) GetAllConversationIDsNumber(ctx context.Context) (int64, error) {
|
||||
counts, err := mongoutil.Aggregate[int64](ctx, c.coll, []bson.M{
|
||||
{"$group": bson.M{"_id": "$conversation_id"}},
|
||||
{"$group": bson.M{"_id": nil, "count": bson.M{"$sum": 1}}},
|
||||
{"$project": bson.M{"_id": 0}},
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(counts) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return counts[0], nil
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) PageConversationIDs(ctx context.Context, pagination pagination.Pagination) (conversationIDs []string, err error) {
|
||||
return mongoutil.FindPageOnly[string](ctx, c.coll, bson.M{}, pagination, options.Find().SetProjection(bson.M{"conversation_id": 1}))
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) GetConversationsByConversationID(ctx context.Context, conversationIDs []string) ([]*model.Conversation, error) {
|
||||
return mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{"conversation_id": bson.M{"$in": conversationIDs}})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) GetConversationIDsNeedDestruct(ctx context.Context) ([]*model.Conversation, error) {
|
||||
// "is_msg_destruct = 1 && msg_destruct_time != 0 && (UNIX_TIMESTAMP(NOW()) > (msg_destruct_time + UNIX_TIMESTAMP(latest_msg_destruct_time)) || latest_msg_destruct_time is NULL)"
|
||||
return mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{
|
||||
"is_msg_destruct": 1,
|
||||
"msg_destruct_time": bson.M{"$ne": 0},
|
||||
"$or": []bson.M{
|
||||
{
|
||||
"$expr": bson.M{
|
||||
"$gt": []any{
|
||||
time.Now(),
|
||||
bson.M{"$add": []any{"$msg_destruct_time", "$latest_msg_destruct_time"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"latest_msg_destruct_time": nil,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) GetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error) {
|
||||
return mongoutil.Find[string](
|
||||
ctx,
|
||||
c.coll,
|
||||
bson.M{"conversation_id": conversationID, "recv_msg_opt": bson.M{"$ne": constant.ReceiveMessage}},
|
||||
options.Find().SetProjection(bson.M{"_id": 0, "owner_user_id": 1}),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindConversationUserVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error) {
|
||||
return c.version.FindChangeLog(ctx, userID, version, limit)
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) FindRandConversation(ctx context.Context, ts int64, limit int) ([]*model.Conversation, error) {
|
||||
pipeline := []bson.M{
|
||||
{
|
||||
"$match": bson.M{
|
||||
"is_msg_destruct": true,
|
||||
"msg_destruct_time": bson.M{"$ne": 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"next_msg_destruct_timestamp": bson.M{
|
||||
"$add": []any{
|
||||
bson.M{
|
||||
"$toLong": "$latest_msg_destruct_time",
|
||||
},
|
||||
bson.M{
|
||||
"$multiply": []any{
|
||||
"$msg_destruct_time",
|
||||
1000, // convert to milliseconds
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$match": bson.M{
|
||||
"next_msg_destruct_timestamp": bson.M{"$lt": ts},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$sample": bson.M{
|
||||
"size": limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
return mongoutil.Aggregate[*model.Conversation](ctx, c.coll, pipeline)
|
||||
}
|
||||
|
||||
func (c *ConversationMgo) DeleteUsersConversations(ctx context.Context, userID string, conversationIDs []string) error {
|
||||
if len(conversationIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
filter := bson.M{
|
||||
"owner_user_id": userID,
|
||||
"conversation_id": bson.M{"$in": conversationIDs},
|
||||
}
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.DeleteMany(ctx, c.coll, filter)
|
||||
}, func() error {
|
||||
return c.version.IncrVersion(ctx, userID, conversationIDs, model.VersionStateDelete)
|
||||
})
|
||||
}
|
||||
15
pkg/common/storage/database/mgo/doc.go
Normal file
15
pkg/common/storage/database/mgo/doc.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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 "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
271
pkg/common/storage/database/mgo/friend.go
Normal file
271
pkg/common/storage/database/mgo/friend.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// FriendMgo implements Friend using MongoDB as the storage backend.
|
||||
type FriendMgo struct {
|
||||
coll *mongo.Collection
|
||||
owner database.VersionLog
|
||||
}
|
||||
|
||||
// NewFriendMongo creates a new instance of FriendMgo with the provided MongoDB database.
|
||||
func NewFriendMongo(db *mongo.Database) (database.Friend, error) {
|
||||
coll := db.Collection(database.FriendName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "owner_user_id", Value: 1},
|
||||
{Key: "friend_user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := NewVersionLog(db.Collection(database.FriendVersionName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FriendMgo{coll: coll, owner: owner}, nil
|
||||
}
|
||||
|
||||
func (f *FriendMgo) friendSort() any {
|
||||
return bson.D{{"is_pinned", -1}, {"_id", 1}}
|
||||
}
|
||||
|
||||
// Create inserts multiple friend records.
|
||||
func (f *FriendMgo) Create(ctx context.Context, friends []*model.Friend) error {
|
||||
for i, friend := range friends {
|
||||
if friend.ID.IsZero() {
|
||||
friends[i].ID = primitive.NewObjectID()
|
||||
}
|
||||
if friend.CreateTime.IsZero() {
|
||||
friends[i].CreateTime = time.Now()
|
||||
}
|
||||
}
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.InsertMany(ctx, f.coll, friends)
|
||||
}, func() error {
|
||||
mp := make(map[string][]string)
|
||||
for _, friend := range friends {
|
||||
mp[friend.OwnerUserID] = append(mp[friend.OwnerUserID], friend.FriendUserID)
|
||||
}
|
||||
for ownerUserID, friendUserIDs := range mp {
|
||||
if err := f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, model.VersionStateInsert); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Delete removes specified friends of the owner user.
|
||||
func (f *FriendMgo) Delete(ctx context.Context, ownerUserID string, friendUserIDs []string) error {
|
||||
filter := bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"friend_user_id": bson.M{"$in": friendUserIDs},
|
||||
}
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.DeleteOne(ctx, f.coll, filter)
|
||||
}, func() error {
|
||||
return f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, model.VersionStateDelete)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateByMap updates specific fields of a friend document using a map.
|
||||
func (f *FriendMgo) UpdateByMap(ctx context.Context, ownerUserID string, friendUserID string, args map[string]any) error {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
filter := bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"friend_user_id": friendUserID,
|
||||
}
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.UpdateOne(ctx, f.coll, filter, bson.M{"$set": args}, true)
|
||||
}, func() error {
|
||||
var friendUserIDs []string
|
||||
if f.IsUpdateIsPinned(args) {
|
||||
friendUserIDs = []string{model.VersionSortChangeID, friendUserID}
|
||||
} else {
|
||||
friendUserIDs = []string{friendUserID}
|
||||
}
|
||||
return f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, model.VersionStateUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRemark updates the remark for a specific friend.
|
||||
func (f *FriendMgo) UpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) error {
|
||||
return f.UpdateByMap(ctx, ownerUserID, friendUserID, map[string]any{"remark": remark})
|
||||
}
|
||||
|
||||
func (f *FriendMgo) fillTime(friends ...*model.Friend) {
|
||||
for i, friend := range friends {
|
||||
if friend.CreateTime.IsZero() {
|
||||
friends[i].CreateTime = friend.ID.Timestamp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FriendMgo) findOne(ctx context.Context, filter any) (*model.Friend, error) {
|
||||
friend, err := mongoutil.FindOne[*model.Friend](ctx, f.coll, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.fillTime(friend)
|
||||
return friend, nil
|
||||
}
|
||||
|
||||
func (f *FriendMgo) find(ctx context.Context, filter any) ([]*model.Friend, error) {
|
||||
friends, err := mongoutil.Find[*model.Friend](ctx, f.coll, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.fillTime(friends...)
|
||||
return friends, nil
|
||||
}
|
||||
|
||||
func (f *FriendMgo) findPage(ctx context.Context, filter any, pagination pagination.Pagination, opts ...*options.FindOptions) (int64, []*model.Friend, error) {
|
||||
return mongoutil.FindPage[*model.Friend](ctx, f.coll, filter, pagination, opts...)
|
||||
}
|
||||
|
||||
// Take retrieves a single friend document. Returns an error if not found.
|
||||
func (f *FriendMgo) Take(ctx context.Context, ownerUserID, friendUserID string) (*model.Friend, error) {
|
||||
filter := bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"friend_user_id": friendUserID,
|
||||
}
|
||||
return f.findOne(ctx, filter)
|
||||
}
|
||||
|
||||
// FindUserState finds the friendship status between two users.
|
||||
func (f *FriendMgo) FindUserState(ctx context.Context, userID1, userID2 string) ([]*model.Friend, error) {
|
||||
filter := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"owner_user_id": userID1, "friend_user_id": userID2},
|
||||
{"owner_user_id": userID2, "friend_user_id": userID1},
|
||||
},
|
||||
}
|
||||
return f.find(ctx, filter)
|
||||
}
|
||||
|
||||
// FindFriends retrieves a list of friends for a given owner. Missing friends do not cause an error.
|
||||
func (f *FriendMgo) FindFriends(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*model.Friend, error) {
|
||||
filter := bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"friend_user_id": bson.M{"$in": friendUserIDs},
|
||||
}
|
||||
return f.find(ctx, filter)
|
||||
}
|
||||
|
||||
// FindReversalFriends finds users who have added the specified user as a friend.
|
||||
func (f *FriendMgo) FindReversalFriends(ctx context.Context, friendUserID string, ownerUserIDs []string) ([]*model.Friend, error) {
|
||||
filter := bson.M{
|
||||
"owner_user_id": bson.M{"$in": ownerUserIDs},
|
||||
"friend_user_id": friendUserID,
|
||||
}
|
||||
return f.find(ctx, filter)
|
||||
}
|
||||
|
||||
// FindOwnerFriends retrieves a paginated list of friends for a given owner.
|
||||
func (f *FriendMgo) FindOwnerFriends(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (int64, []*model.Friend, error) {
|
||||
filter := bson.M{"owner_user_id": ownerUserID}
|
||||
opt := options.Find().SetSort(f.friendSort())
|
||||
return f.findPage(ctx, filter, pagination, opt)
|
||||
}
|
||||
|
||||
func (f *FriendMgo) FindOwnerFriendUserIds(ctx context.Context, ownerUserID string, limit int) ([]string, error) {
|
||||
filter := bson.M{"owner_user_id": ownerUserID}
|
||||
opt := options.Find().SetProjection(bson.M{"_id": 0, "friend_user_id": 1}).SetSort(f.friendSort()).SetLimit(int64(limit))
|
||||
return mongoutil.Find[string](ctx, f.coll, filter, opt)
|
||||
}
|
||||
|
||||
// FindInWhoseFriends finds users who have added the specified user as a friend, with pagination.
|
||||
func (f *FriendMgo) FindInWhoseFriends(ctx context.Context, friendUserID string, pagination pagination.Pagination) (int64, []*model.Friend, error) {
|
||||
filter := bson.M{"friend_user_id": friendUserID}
|
||||
opt := options.Find().SetSort(f.friendSort())
|
||||
return f.findPage(ctx, filter, pagination, opt)
|
||||
}
|
||||
|
||||
// FindFriendUserIDs retrieves a list of friend user IDs for a given owner.
|
||||
func (f *FriendMgo) FindFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error) {
|
||||
filter := bson.M{"owner_user_id": ownerUserID}
|
||||
return mongoutil.Find[string](ctx, f.coll, filter, options.Find().SetProjection(bson.M{"_id": 0, "friend_user_id": 1}).SetSort(f.friendSort()))
|
||||
}
|
||||
|
||||
func (f *FriendMgo) UpdateFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, val map[string]any) error {
|
||||
// Ensure there are IDs to update
|
||||
if len(friendUserIDs) == 0 || len(val) == 0 {
|
||||
return nil // Or return an error if you expect there to always be IDs
|
||||
}
|
||||
|
||||
// Create a filter to match documents with the specified ownerUserID and any of the friendUserIDs
|
||||
filter := bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"friend_user_id": bson.M{"$in": friendUserIDs},
|
||||
}
|
||||
|
||||
// Create an update document
|
||||
update := bson.M{"$set": val}
|
||||
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.Ignore(mongoutil.UpdateMany(ctx, f.coll, filter, update))
|
||||
}, func() error {
|
||||
var userIDs []string
|
||||
if f.IsUpdateIsPinned(val) {
|
||||
userIDs = append([]string{model.VersionSortChangeID}, friendUserIDs...)
|
||||
} else {
|
||||
userIDs = friendUserIDs
|
||||
}
|
||||
return f.owner.IncrVersion(ctx, ownerUserID, userIDs, model.VersionStateUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
func (f *FriendMgo) FindIncrVersion(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error) {
|
||||
return f.owner.FindChangeLog(ctx, ownerUserID, version, limit)
|
||||
}
|
||||
|
||||
func (f *FriendMgo) FindFriendUserID(ctx context.Context, friendUserID string) ([]string, error) {
|
||||
filter := bson.M{
|
||||
"friend_user_id": friendUserID,
|
||||
}
|
||||
return mongoutil.Find[string](ctx, f.coll, filter, options.Find().SetProjection(bson.M{"_id": 0, "owner_user_id": 1}).SetSort(f.friendSort()))
|
||||
}
|
||||
|
||||
func (f *FriendMgo) IncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error {
|
||||
return f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, state)
|
||||
}
|
||||
|
||||
func (f *FriendMgo) IsUpdateIsPinned(data map[string]any) bool {
|
||||
if data == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := data["is_pinned"]
|
||||
return ok
|
||||
}
|
||||
143
pkg/common/storage/database/mgo/friend_request.go
Normal file
143
pkg/common/storage/database/mgo/friend_request.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
)
|
||||
|
||||
func NewFriendRequestMongo(db *mongo.Database) (database.FriendRequest, error) {
|
||||
coll := db.Collection(database.FriendRequestName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "from_user_id", Value: 1},
|
||||
{Key: "to_user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "create_time", Value: -1},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FriendRequestMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type FriendRequestMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) sort() any {
|
||||
return bson.D{{Key: "create_time", Value: -1}}
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) FindToUserID(ctx context.Context, toUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) {
|
||||
filter := bson.M{"to_user_id": toUserID}
|
||||
if len(handleResults) > 0 {
|
||||
filter["handle_result"] = bson.M{"$in": handleResults}
|
||||
}
|
||||
return mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, filter, pagination, options.Find().SetSort(f.sort()))
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) FindFromUserID(ctx context.Context, fromUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) {
|
||||
filter := bson.M{"from_user_id": fromUserID}
|
||||
if len(handleResults) > 0 {
|
||||
filter["handle_result"] = bson.M{"$in": handleResults}
|
||||
}
|
||||
return mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, filter, pagination, options.Find().SetSort(f.sort()))
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) FindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error) {
|
||||
filter := bson.M{"$or": []bson.M{
|
||||
{"from_user_id": fromUserID, "to_user_id": toUserID},
|
||||
{"from_user_id": toUserID, "to_user_id": fromUserID},
|
||||
}}
|
||||
return mongoutil.Find[*model.FriendRequest](ctx, f.coll, filter)
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) Create(ctx context.Context, friendRequests []*model.FriendRequest) error {
|
||||
return mongoutil.InsertMany(ctx, f.coll, friendRequests)
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) Delete(ctx context.Context, fromUserID, toUserID string) (err error) {
|
||||
return mongoutil.DeleteOne(ctx, f.coll, bson.M{"from_user_id": fromUserID, "to_user_id": toUserID})
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) UpdateByMap(ctx context.Context, formUserID, toUserID string, args map[string]any) (err error) {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, f.coll, bson.M{"from_user_id": formUserID, "to_user_id": toUserID}, bson.M{"$set": args}, true)
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) Update(ctx context.Context, friendRequest *model.FriendRequest) (err error) {
|
||||
updater := bson.M{}
|
||||
if friendRequest.HandleResult != 0 {
|
||||
updater["handle_result"] = friendRequest.HandleResult
|
||||
}
|
||||
if friendRequest.ReqMsg != "" {
|
||||
updater["req_msg"] = friendRequest.ReqMsg
|
||||
}
|
||||
if friendRequest.HandlerUserID != "" {
|
||||
updater["handler_user_id"] = friendRequest.HandlerUserID
|
||||
}
|
||||
if friendRequest.HandleMsg != "" {
|
||||
updater["handle_msg"] = friendRequest.HandleMsg
|
||||
}
|
||||
if !friendRequest.HandleTime.IsZero() {
|
||||
updater["handle_time"] = friendRequest.HandleTime
|
||||
}
|
||||
if friendRequest.Ex != "" {
|
||||
updater["ex"] = friendRequest.Ex
|
||||
}
|
||||
if len(updater) == 0 {
|
||||
return nil
|
||||
}
|
||||
filter := bson.M{"from_user_id": friendRequest.FromUserID, "to_user_id": friendRequest.ToUserID}
|
||||
return mongoutil.UpdateOne(ctx, f.coll, filter, bson.M{"$set": updater}, true)
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) Find(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) {
|
||||
return mongoutil.FindOne[*model.FriendRequest](ctx, f.coll, bson.M{"from_user_id": fromUserID, "to_user_id": toUserID})
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) Take(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) {
|
||||
return f.Find(ctx, fromUserID, toUserID)
|
||||
}
|
||||
|
||||
func (f *FriendRequestMgo) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) {
|
||||
filter := bson.M{"to_user_id": userID, "handle_result": 0}
|
||||
if ts != 0 {
|
||||
filter["create_time"] = bson.M{"$gt": time.UnixMilli(ts)}
|
||||
}
|
||||
return mongoutil.Count(ctx, f.coll, filter)
|
||||
}
|
||||
162
pkg/common/storage/database/mgo/group.go
Normal file
162
pkg/common/storage/database/mgo/group.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"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 NewGroupMongo(db *mongo.Database) (database.Group, error) {
|
||||
coll := db.Collection(database.GroupName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "group_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &GroupMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type GroupMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (g *GroupMgo) sortGroup() any {
|
||||
return bson.D{{"group_name", 1}, {"create_time", 1}}
|
||||
}
|
||||
|
||||
func (g *GroupMgo) Create(ctx context.Context, groups []*model.Group) (err error) {
|
||||
return mongoutil.InsertMany(ctx, g.coll, groups)
|
||||
}
|
||||
|
||||
func (g *GroupMgo) UpdateStatus(ctx context.Context, groupID string, status int32) (err error) {
|
||||
return g.UpdateMap(ctx, groupID, map[string]any{"status": status})
|
||||
}
|
||||
|
||||
func (g *GroupMgo) UpdateMap(ctx context.Context, groupID string, args map[string]any) (err error) {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, g.coll, bson.M{"group_id": groupID}, bson.M{"$set": args}, true)
|
||||
}
|
||||
|
||||
func (g *GroupMgo) Find(ctx context.Context, groupIDs []string) (groups []*model.Group, err error) {
|
||||
return mongoutil.Find[*model.Group](ctx, g.coll, bson.M{"group_id": bson.M{"$in": groupIDs}})
|
||||
}
|
||||
|
||||
func (g *GroupMgo) Take(ctx context.Context, groupID string) (group *model.Group, err error) {
|
||||
return mongoutil.FindOne[*model.Group](ctx, g.coll, bson.M{"group_id": groupID})
|
||||
}
|
||||
|
||||
func (g *GroupMgo) Search(ctx context.Context, keyword string, pagination pagination.Pagination) (total int64, groups []*model.Group, err error) {
|
||||
// Define the sorting options
|
||||
opts := options.Find().SetSort(bson.D{{Key: "create_time", Value: -1}})
|
||||
|
||||
// Perform the search with pagination and sorting
|
||||
return mongoutil.FindPage[*model.Group](ctx, g.coll, bson.M{
|
||||
"group_name": bson.M{"$regex": keyword},
|
||||
"status": bson.M{"$ne": constant.GroupStatusDismissed},
|
||||
}, pagination, opts)
|
||||
}
|
||||
|
||||
func (g *GroupMgo) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) {
|
||||
if before == nil {
|
||||
return mongoutil.Count(ctx, g.coll, bson.M{})
|
||||
}
|
||||
return mongoutil.Count(ctx, g.coll, bson.M{"create_time": bson.M{"$lt": before}})
|
||||
}
|
||||
|
||||
func (g *GroupMgo) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{
|
||||
"$match": bson.M{
|
||||
"create_time": bson.M{
|
||||
"$gte": start,
|
||||
"$lt": end,
|
||||
},
|
||||
},
|
||||
},
|
||||
bson.M{
|
||||
"$group": bson.M{
|
||||
"_id": bson.M{
|
||||
"$dateToString": bson.M{
|
||||
"format": "%Y-%m-%d",
|
||||
"date": "$create_time",
|
||||
},
|
||||
},
|
||||
"count": bson.M{
|
||||
"$sum": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
type Item struct {
|
||||
Date string `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
items, err := mongoutil.Aggregate[Item](ctx, g.coll, pipeline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[string]int64, len(items))
|
||||
for _, item := range items {
|
||||
res[item.Date] = item.Count
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (g *GroupMgo) FindJoinSortGroupID(ctx context.Context, groupIDs []string) ([]string, error) {
|
||||
if len(groupIDs) < 2 {
|
||||
return groupIDs, nil
|
||||
}
|
||||
filter := bson.M{
|
||||
"group_id": bson.M{"$in": groupIDs},
|
||||
"status": bson.M{"$ne": constant.GroupStatusDismissed},
|
||||
}
|
||||
opt := options.Find().SetSort(g.sortGroup()).SetProjection(bson.M{"_id": 0, "group_id": 1})
|
||||
return mongoutil.Find[string](ctx, g.coll, filter, opt)
|
||||
}
|
||||
|
||||
func (g *GroupMgo) SearchJoin(ctx context.Context, groupIDs []string, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error) {
|
||||
if len(groupIDs) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
filter := bson.M{
|
||||
"group_id": bson.M{"$in": groupIDs},
|
||||
"status": bson.M{"$ne": constant.GroupStatusDismissed},
|
||||
}
|
||||
if keyword != "" {
|
||||
filter["group_name"] = bson.M{"$regex": keyword}
|
||||
}
|
||||
// Define the sorting options
|
||||
opts := options.Find().SetSort(g.sortGroup())
|
||||
// Perform the search with pagination and sorting
|
||||
return mongoutil.FindPage[*model.Group](ctx, g.coll, filter, pagination, opts)
|
||||
}
|
||||
282
pkg/common/storage/database/mgo/group_member.go
Normal file
282
pkg/common/storage/database/mgo/group_member.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/log"
|
||||
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"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 NewGroupMember(db *mongo.Database) (database.GroupMember, error) {
|
||||
coll := db.Collection(database.GroupMemberName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "group_id", Value: 1},
|
||||
{Key: "user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
member, err := NewVersionLog(db.Collection(database.GroupMemberVersionName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
join, err := NewVersionLog(db.Collection(database.GroupJoinVersionName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GroupMemberMgo{coll: coll, member: member, join: join}, nil
|
||||
}
|
||||
|
||||
type GroupMemberMgo struct {
|
||||
coll *mongo.Collection
|
||||
member database.VersionLog
|
||||
join database.VersionLog
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) memberSort() any {
|
||||
return bson.D{{Key: "role_level", Value: -1}, {Key: "create_time", Value: 1}}
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) Create(ctx context.Context, groupMembers []*model.GroupMember) (err error) {
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.InsertMany(ctx, g.coll, groupMembers)
|
||||
}, func() error {
|
||||
gms := make(map[string][]string)
|
||||
for _, member := range groupMembers {
|
||||
gms[member.GroupID] = append(gms[member.GroupID], member.UserID)
|
||||
}
|
||||
for groupID, userIDs := range gms {
|
||||
if err := g.member.IncrVersion(ctx, groupID, userIDs, model.VersionStateInsert); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, func() error {
|
||||
gms := make(map[string][]string)
|
||||
for _, member := range groupMembers {
|
||||
gms[member.UserID] = append(gms[member.UserID], member.GroupID)
|
||||
}
|
||||
for userID, groupIDs := range gms {
|
||||
if err := g.join.IncrVersion(ctx, userID, groupIDs, model.VersionStateInsert); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) Delete(ctx context.Context, groupID string, userIDs []string) (err error) {
|
||||
filter := bson.M{"group_id": groupID}
|
||||
if len(userIDs) > 0 {
|
||||
filter["user_id"] = bson.M{"$in": userIDs}
|
||||
}
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.DeleteMany(ctx, g.coll, filter)
|
||||
}, func() error {
|
||||
if len(userIDs) == 0 {
|
||||
return g.member.Delete(ctx, groupID)
|
||||
} else {
|
||||
return g.member.IncrVersion(ctx, groupID, userIDs, model.VersionStateDelete)
|
||||
}
|
||||
}, func() error {
|
||||
for _, userID := range userIDs {
|
||||
if err := g.join.IncrVersion(ctx, userID, []string{groupID}, model.VersionStateDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) UpdateRoleLevel(ctx context.Context, groupID string, userID string, roleLevel int32) error {
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.UpdateOne(ctx, g.coll, bson.M{"group_id": groupID, "user_id": userID},
|
||||
bson.M{"$set": bson.M{"role_level": roleLevel}}, true)
|
||||
}, func() error {
|
||||
return g.member.IncrVersion(ctx, groupID, []string{model.VersionSortChangeID, userID}, model.VersionStateUpdate)
|
||||
})
|
||||
}
|
||||
func (g *GroupMemberMgo) UpdateUserRoleLevels(ctx context.Context, groupID string, firstUserID string, firstUserRoleLevel int32, secondUserID string, secondUserRoleLevel int32) error {
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
if err := mongoutil.UpdateOne(ctx, g.coll, bson.M{"group_id": groupID, "user_id": firstUserID},
|
||||
bson.M{"$set": bson.M{"role_level": firstUserRoleLevel}}, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mongoutil.UpdateOne(ctx, g.coll, bson.M{"group_id": groupID, "user_id": secondUserID},
|
||||
bson.M{"$set": bson.M{"role_level": secondUserRoleLevel}}, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, func() error {
|
||||
return g.member.IncrVersion(ctx, groupID, []string{model.VersionSortChangeID, firstUserID, secondUserID}, model.VersionStateUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) Update(ctx context.Context, groupID string, userID string, data map[string]any) (err error) {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.IncrVersion(func() error {
|
||||
return mongoutil.UpdateOne(ctx, g.coll, bson.M{"group_id": groupID, "user_id": userID}, bson.M{"$set": data}, true)
|
||||
}, func() error {
|
||||
var userIDs []string
|
||||
if g.IsUpdateRoleLevel(data) {
|
||||
userIDs = []string{model.VersionSortChangeID, userID}
|
||||
} else {
|
||||
userIDs = []string{userID}
|
||||
}
|
||||
return g.member.IncrVersion(ctx, groupID, userIDs, model.VersionStateUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindMemberUserID(ctx context.Context, groupID string) (userIDs []string, err error) {
|
||||
return mongoutil.Find[string](ctx, g.coll, bson.M{"group_id": groupID}, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1}).SetSort(g.memberSort()))
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) Find(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupMember, error) {
|
||||
filter := bson.M{"group_id": groupID}
|
||||
if len(userIDs) > 0 {
|
||||
filter["user_id"] = bson.M{"$in": userIDs}
|
||||
}
|
||||
return mongoutil.Find[*model.GroupMember](ctx, g.coll, filter)
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindInGroup(ctx context.Context, userID string, groupIDs []string) ([]*model.GroupMember, error) {
|
||||
filter := bson.M{"user_id": userID}
|
||||
if len(groupIDs) > 0 {
|
||||
filter["group_id"] = bson.M{"$in": groupIDs}
|
||||
}
|
||||
return mongoutil.Find[*model.GroupMember](ctx, g.coll, filter)
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) Take(ctx context.Context, groupID string, userID string) (groupMember *model.GroupMember, err error) {
|
||||
return mongoutil.FindOne[*model.GroupMember](ctx, g.coll, bson.M{"group_id": groupID, "user_id": userID})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) TakeOwner(ctx context.Context, groupID string) (groupMember *model.GroupMember, err error) {
|
||||
return mongoutil.FindOne[*model.GroupMember](ctx, g.coll, bson.M{"group_id": groupID, "role_level": constant.GroupOwner})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindRoleLevelUserIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error) {
|
||||
return mongoutil.Find[string](ctx, g.coll, bson.M{"group_id": groupID, "role_level": roleLevel}, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1}))
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) SearchMember(ctx context.Context, keyword string, groupID string, pagination pagination.Pagination) (int64, []*model.GroupMember, error) {
|
||||
// 支持通过昵称、user_id(账号)搜索
|
||||
// 使用 $or 条件,匹配昵称或 user_id
|
||||
filter := bson.M{
|
||||
"group_id": groupID,
|
||||
"$or": []bson.M{
|
||||
{"nickname": bson.M{"$regex": keyword, "$options": "i"}}, // 昵称模糊匹配,不区分大小写
|
||||
{"user_id": bson.M{"$regex": keyword, "$options": "i"}}, // user_id(账号)模糊匹配,不区分大小写
|
||||
},
|
||||
}
|
||||
return mongoutil.FindPage[*model.GroupMember](ctx, g.coll, filter, pagination, options.Find().SetSort(g.memberSort()))
|
||||
}
|
||||
|
||||
// SearchMemberByFields 支持通过多个独立字段搜索群成员:昵称、账号(userID)、手机号
|
||||
// nickname: 用户昵称(群内昵称)
|
||||
// userID: 用户账号(user_id)
|
||||
// phone: 手机号(如果群成员表中有相关字段,或通过 Ex 字段存储)
|
||||
func (g *GroupMemberMgo) SearchMemberByFields(ctx context.Context, groupID string, nickname, userID, phone string, pagination pagination.Pagination) (int64, []*model.GroupMember, error) {
|
||||
filter := bson.M{"group_id": groupID}
|
||||
|
||||
// 构建多个搜索条件,使用 $and 确保所有提供的条件都满足
|
||||
conditions := []bson.M{}
|
||||
|
||||
if nickname != "" {
|
||||
conditions = append(conditions, bson.M{"nickname": bson.M{"$regex": nickname, "$options": "i"}})
|
||||
}
|
||||
|
||||
if userID != "" {
|
||||
conditions = append(conditions, bson.M{"user_id": bson.M{"$regex": userID, "$options": "i"}})
|
||||
}
|
||||
|
||||
if phone != "" {
|
||||
// 手机号可能存储在 Ex 字段中,使用正则表达式匹配
|
||||
// 如果 Ex 字段是 JSON 格式,可能需要更复杂的查询
|
||||
conditions = append(conditions, bson.M{"ex": bson.M{"$regex": phone, "$options": "i"}})
|
||||
}
|
||||
|
||||
// 如果有搜索条件,添加到 filter 中
|
||||
if len(conditions) > 0 {
|
||||
filter["$and"] = conditions
|
||||
}
|
||||
|
||||
return mongoutil.FindPage[*model.GroupMember](ctx, g.coll, filter, pagination, options.Find().SetSort(g.memberSort()))
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindUserJoinedGroupID(ctx context.Context, userID string) (groupIDs []string, err error) {
|
||||
return mongoutil.Find[string](ctx, g.coll, bson.M{"user_id": userID}, options.Find().SetProjection(bson.M{"_id": 0, "group_id": 1}).SetSort(g.memberSort()))
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) TakeGroupMemberNum(ctx context.Context, groupID string) (count int64, err error) {
|
||||
return mongoutil.Count(ctx, g.coll, bson.M{"group_id": groupID})
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindUserManagedGroupID(ctx context.Context, userID string) (groupIDs []string, err error) {
|
||||
filter := bson.M{
|
||||
"user_id": userID,
|
||||
"role_level": bson.M{
|
||||
"$in": []int{constant.GroupOwner, constant.GroupAdmin},
|
||||
},
|
||||
}
|
||||
return mongoutil.Find[string](ctx, g.coll, filter, options.Find().SetProjection(bson.M{"_id": 0, "group_id": 1}))
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) IsUpdateRoleLevel(data map[string]any) bool {
|
||||
if len(data) == 0 {
|
||||
return false
|
||||
}
|
||||
_, ok := data["role_level"]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) JoinGroupIncrVersion(ctx context.Context, userID string, groupIDs []string, state int32) error {
|
||||
return g.join.IncrVersion(ctx, userID, groupIDs, state)
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) MemberGroupIncrVersion(ctx context.Context, groupID string, userIDs []string, state int32) error {
|
||||
return g.member.IncrVersion(ctx, groupID, userIDs, state)
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindMemberIncrVersion(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error) {
|
||||
log.ZDebug(ctx, "find member incr version", "groupID", groupID, "version", version)
|
||||
return g.member.FindChangeLog(ctx, groupID, version, limit)
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) BatchFindMemberIncrVersion(ctx context.Context, groupIDs []string, versions []uint, limits []int) ([]*model.VersionLog, error) {
|
||||
log.ZDebug(ctx, "Batch find member incr version", "groupIDs", groupIDs, "versions", versions)
|
||||
return g.member.BatchFindChangeLog(ctx, groupIDs, versions, limits)
|
||||
}
|
||||
|
||||
func (g *GroupMemberMgo) FindJoinIncrVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error) {
|
||||
log.ZDebug(ctx, "find join incr version", "userID", userID, "version", version)
|
||||
return g.join.FindChangeLog(ctx, userID, version, limit)
|
||||
}
|
||||
115
pkg/common/storage/database/mgo/group_request.go
Normal file
115
pkg/common/storage/database/mgo/group_request.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
func NewGroupRequestMgo(db *mongo.Database) (database.GroupRequest, error) {
|
||||
coll := db.Collection(database.GroupRequestName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "group_id", Value: 1},
|
||||
{Key: "user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "req_time", Value: -1},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &GroupRequestMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type GroupRequestMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) Create(ctx context.Context, groupRequests []*model.GroupRequest) (err error) {
|
||||
return mongoutil.InsertMany(ctx, g.coll, groupRequests)
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) Delete(ctx context.Context, groupID string, userID string) (err error) {
|
||||
return mongoutil.DeleteOne(ctx, g.coll, bson.M{"group_id": groupID, "user_id": userID})
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) UpdateHandler(ctx context.Context, groupID string, userID string, handledMsg string, handleResult int32) (err error) {
|
||||
return mongoutil.UpdateOne(ctx, g.coll, bson.M{"group_id": groupID, "user_id": userID}, bson.M{"$set": bson.M{"handle_msg": handledMsg, "handle_result": handleResult}}, true)
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) Take(ctx context.Context, groupID string, userID string) (groupRequest *model.GroupRequest, err error) {
|
||||
return mongoutil.FindOne[*model.GroupRequest](ctx, g.coll, bson.M{"group_id": groupID, "user_id": userID})
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) FindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error) {
|
||||
return mongoutil.Find[*model.GroupRequest](ctx, g.coll, bson.M{"group_id": groupID, "user_id": bson.M{"$in": userIDs}})
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) sort() any {
|
||||
return bson.D{{Key: "req_time", Value: -1}}
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) Page(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) {
|
||||
filter := bson.M{"user_id": userID}
|
||||
if len(groupIDs) > 0 {
|
||||
filter["group_id"] = bson.M{"$in": datautil.Distinct(groupIDs)}
|
||||
}
|
||||
if len(handleResults) > 0 {
|
||||
filter["handle_result"] = bson.M{"$in": handleResults}
|
||||
}
|
||||
return mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, filter, pagination, options.Find().SetSort(g.sort()))
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) PageGroup(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) {
|
||||
if len(groupIDs) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
filter := bson.M{"group_id": bson.M{"$in": groupIDs}}
|
||||
if len(handleResults) > 0 {
|
||||
filter["handle_result"] = bson.M{"$in": handleResults}
|
||||
}
|
||||
return mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, filter, pagination, options.Find().SetSort(g.sort()))
|
||||
}
|
||||
|
||||
func (g *GroupRequestMgo) GetUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) {
|
||||
if len(groupIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
filter := bson.M{"group_id": bson.M{"$in": groupIDs}, "handle_result": 0}
|
||||
if ts != 0 {
|
||||
filter["req_time"] = bson.M{"$gt": time.UnixMilli(ts)}
|
||||
}
|
||||
return mongoutil.Count(ctx, g.coll, filter)
|
||||
}
|
||||
24
pkg/common/storage/database/mgo/helpers.go
Normal file
24
pkg/common/storage/database/mgo/helpers.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright © 2023 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 (
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return errs.Unwrap(err) == mongo.ErrNoDocuments
|
||||
}
|
||||
85
pkg/common/storage/database/mgo/log.go
Normal file
85
pkg/common/storage/database/mgo/log.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/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 NewLogMongo(db *mongo.Database) (database.Log, error) {
|
||||
coll := db.Collection(database.LogName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "log_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "user_id", Value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "create_time", Value: -1},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LogMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type LogMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (l *LogMgo) Create(ctx context.Context, log []*model.Log) error {
|
||||
return mongoutil.InsertMany(ctx, l.coll, log)
|
||||
}
|
||||
|
||||
func (l *LogMgo) Search(ctx context.Context, keyword string, start time.Time, end time.Time, pagination pagination.Pagination) (int64, []*model.Log, error) {
|
||||
filter := bson.M{"create_time": bson.M{"$gte": start, "$lte": end}}
|
||||
if keyword != "" {
|
||||
filter["user_id"] = bson.M{"$regex": keyword}
|
||||
}
|
||||
return mongoutil.FindPage[*model.Log](ctx, l.coll, filter, pagination, options.Find().SetSort(bson.M{"create_time": -1}))
|
||||
}
|
||||
|
||||
func (l *LogMgo) Delete(ctx context.Context, logID []string, userID string) error {
|
||||
if userID == "" {
|
||||
return mongoutil.DeleteMany(ctx, l.coll, bson.M{"log_id": bson.M{"$in": logID}})
|
||||
}
|
||||
return mongoutil.DeleteMany(ctx, l.coll, bson.M{"log_id": bson.M{"$in": logID}, "user_id": userID})
|
||||
}
|
||||
|
||||
func (l *LogMgo) Get(ctx context.Context, logIDs []string, userID string) ([]*model.Log, error) {
|
||||
if userID == "" {
|
||||
return mongoutil.Find[*model.Log](ctx, l.coll, bson.M{"log_id": bson.M{"$in": logIDs}})
|
||||
}
|
||||
return mongoutil.Find[*model.Log](ctx, l.coll, bson.M{"log_id": bson.M{"$in": logIDs}, "user_id": userID})
|
||||
}
|
||||
183
pkg/common/storage/database/mgo/meeting.go
Normal file
183
pkg/common/storage/database/mgo/meeting.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// MeetingMgo implements Meeting using MongoDB as the storage backend.
|
||||
type MeetingMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewMeetingMongo creates a new instance of MeetingMgo with the provided MongoDB database.
|
||||
func NewMeetingMongo(db *mongo.Database) (database.Meeting, error) {
|
||||
coll := db.Collection(database.MeetingName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "meeting_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "creator_user_id", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "status", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "scheduled_time", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "create_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "update_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "subject", Value: "text"}, {Key: "description", Value: "text"}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &MeetingMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new meeting record.
|
||||
func (m *MeetingMgo) Create(ctx context.Context, meeting *model.Meeting) error {
|
||||
if meeting.CreateTime.IsZero() {
|
||||
meeting.CreateTime = time.Now()
|
||||
}
|
||||
if meeting.UpdateTime.IsZero() {
|
||||
meeting.UpdateTime = time.Now()
|
||||
}
|
||||
return mongoutil.InsertOne(ctx, m.coll, meeting)
|
||||
}
|
||||
|
||||
// Take retrieves a meeting by meeting ID. Returns an error if not found.
|
||||
func (m *MeetingMgo) Take(ctx context.Context, meetingID string) (*model.Meeting, error) {
|
||||
return mongoutil.FindOne[*model.Meeting](ctx, m.coll, bson.M{"meeting_id": meetingID})
|
||||
}
|
||||
|
||||
// Update updates meeting information.
|
||||
func (m *MeetingMgo) Update(ctx context.Context, meetingID string, data map[string]any) error {
|
||||
data["update_time"] = time.Now()
|
||||
update := bson.M{"$set": data}
|
||||
return mongoutil.UpdateOne(ctx, m.coll, bson.M{"meeting_id": meetingID}, update, false)
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of a meeting.
|
||||
func (m *MeetingMgo) UpdateStatus(ctx context.Context, meetingID string, status int32) error {
|
||||
return m.Update(ctx, meetingID, map[string]any{"status": status})
|
||||
}
|
||||
|
||||
// Find finds meetings by meeting IDs.
|
||||
func (m *MeetingMgo) Find(ctx context.Context, meetingIDs []string) ([]*model.Meeting, error) {
|
||||
if len(meetingIDs) == 0 {
|
||||
return []*model.Meeting{}, nil
|
||||
}
|
||||
filter := bson.M{"meeting_id": bson.M{"$in": meetingIDs}}
|
||||
return mongoutil.Find[*model.Meeting](ctx, m.coll, filter)
|
||||
}
|
||||
|
||||
// FindByCreator finds meetings created by a specific user.
|
||||
func (m *MeetingMgo) FindByCreator(ctx context.Context, creatorUserID string, pagination pagination.Pagination) (total int64, meetings []*model.Meeting, err error) {
|
||||
filter := bson.M{"creator_user_id": creatorUserID}
|
||||
return mongoutil.FindPage[*model.Meeting](ctx, m.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "scheduled_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindAll finds all meetings with pagination.
|
||||
func (m *MeetingMgo) FindAll(ctx context.Context, pagination pagination.Pagination) (total int64, meetings []*model.Meeting, err error) {
|
||||
return mongoutil.FindPage[*model.Meeting](ctx, m.coll, bson.M{}, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "scheduled_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// Search searches meetings by keyword (subject, description).
|
||||
func (m *MeetingMgo) Search(ctx context.Context, keyword string, pagination pagination.Pagination) (total int64, meetings []*model.Meeting, err error) {
|
||||
filter := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"subject": bson.M{"$regex": keyword, "$options": "i"}},
|
||||
{"description": bson.M{"$regex": keyword, "$options": "i"}},
|
||||
},
|
||||
}
|
||||
return mongoutil.FindPage[*model.Meeting](ctx, m.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "scheduled_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindByStatus finds meetings by status.
|
||||
func (m *MeetingMgo) FindByStatus(ctx context.Context, status int32, pagination pagination.Pagination) (total int64, meetings []*model.Meeting, err error) {
|
||||
filter := bson.M{"status": status}
|
||||
return mongoutil.FindPage[*model.Meeting](ctx, m.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "scheduled_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindByScheduledTimeRange finds meetings within a scheduled time range.
|
||||
func (m *MeetingMgo) FindByScheduledTimeRange(ctx context.Context, startTime, endTime int64, pagination pagination.Pagination) (total int64, meetings []*model.Meeting, err error) {
|
||||
filter := bson.M{
|
||||
"scheduled_time": bson.M{
|
||||
"$gte": time.UnixMilli(startTime),
|
||||
"$lte": time.UnixMilli(endTime),
|
||||
},
|
||||
}
|
||||
return mongoutil.FindPage[*model.Meeting](ctx, m.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "scheduled_time", Value: 1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindFinishedMeetingsBefore finds finished meetings that ended before the specified time.
|
||||
// A meeting is considered finished if its status is 3 (Finished) and its end time (scheduledTime + duration) is before beforeTime.
|
||||
// Only returns meetings with a non-empty group_id to avoid processing meetings that have already been handled.
|
||||
func (m *MeetingMgo) FindFinishedMeetingsBefore(ctx context.Context, beforeTime time.Time) ([]*model.Meeting, error) {
|
||||
// 查询状态为3(已结束)且group_id不为空的会议
|
||||
// 结束时间 = scheduledTime + duration(分钟)
|
||||
// 需要计算:scheduledTime + duration * 60秒 <= beforeTime
|
||||
filter := bson.M{
|
||||
"status": 3, // 已结束
|
||||
"group_id": bson.M{"$ne": ""}, // 只查询group_id不为空的会议,避免重复处理已清空groupID的会议
|
||||
"$expr": bson.M{
|
||||
"$lte": []interface{}{
|
||||
bson.M{
|
||||
"$add": []interface{}{
|
||||
"$scheduled_time",
|
||||
bson.M{"$multiply": []interface{}{"$duration", int64(60)}}, // duration是分钟,转换为秒
|
||||
},
|
||||
},
|
||||
beforeTime,
|
||||
},
|
||||
},
|
||||
}
|
||||
return mongoutil.Find[*model.Meeting](ctx, m.coll, filter)
|
||||
}
|
||||
|
||||
// Delete deletes a meeting by meeting ID.
|
||||
func (m *MeetingMgo) Delete(ctx context.Context, meetingID string) error {
|
||||
return mongoutil.DeleteOne(ctx, m.coll, bson.M{"meeting_id": meetingID})
|
||||
}
|
||||
110
pkg/common/storage/database/mgo/meeting_checkin.go
Normal file
110
pkg/common/storage/database/mgo/meeting_checkin.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// MeetingCheckInMgo implements MeetingCheckIn using MongoDB as the storage backend.
|
||||
type MeetingCheckInMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewMeetingCheckInMongo creates a new instance of MeetingCheckInMgo with the provided MongoDB database.
|
||||
func NewMeetingCheckInMongo(db *mongo.Database) (database.MeetingCheckIn, error) {
|
||||
coll := db.Collection(database.MeetingCheckInName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "check_in_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "meeting_id", Value: 1}, {Key: "user_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true), // 一个用户在一个会议中只能签到一次
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "meeting_id", Value: 1}, {Key: "check_in_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "check_in_time", Value: -1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &MeetingCheckInMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new meeting check-in record.
|
||||
func (m *MeetingCheckInMgo) Create(ctx context.Context, checkIn *model.MeetingCheckIn) error {
|
||||
if checkIn.CreateTime.IsZero() {
|
||||
checkIn.CreateTime = time.Now()
|
||||
}
|
||||
if checkIn.CheckInTime.IsZero() {
|
||||
checkIn.CheckInTime = time.Now()
|
||||
}
|
||||
return mongoutil.InsertOne(ctx, m.coll, checkIn)
|
||||
}
|
||||
|
||||
// Take retrieves a check-in by check-in ID. Returns an error if not found.
|
||||
func (m *MeetingCheckInMgo) Take(ctx context.Context, checkInID string) (*model.MeetingCheckIn, error) {
|
||||
return mongoutil.FindOne[*model.MeetingCheckIn](ctx, m.coll, bson.M{"check_in_id": checkInID})
|
||||
}
|
||||
|
||||
// FindByMeetingID finds all check-ins for a meeting with pagination.
|
||||
func (m *MeetingCheckInMgo) FindByMeetingID(ctx context.Context, meetingID string, pagination pagination.Pagination) (total int64, checkIns []*model.MeetingCheckIn, err error) {
|
||||
filter := bson.M{"meeting_id": meetingID}
|
||||
return mongoutil.FindPage[*model.MeetingCheckIn](ctx, m.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "check_in_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindByUserAndMeetingID finds if a user has checked in for a specific meeting.
|
||||
func (m *MeetingCheckInMgo) FindByUserAndMeetingID(ctx context.Context, userID, meetingID string) (*model.MeetingCheckIn, error) {
|
||||
return mongoutil.FindOne[*model.MeetingCheckIn](ctx, m.coll, bson.M{
|
||||
"user_id": userID,
|
||||
"meeting_id": meetingID,
|
||||
})
|
||||
}
|
||||
|
||||
// CountByMeetingID counts the number of check-ins for a meeting.
|
||||
func (m *MeetingCheckInMgo) CountByMeetingID(ctx context.Context, meetingID string) (int64, error) {
|
||||
return mongoutil.Count(ctx, m.coll, bson.M{"meeting_id": meetingID})
|
||||
}
|
||||
|
||||
// FindByUser finds all check-ins by a user with pagination.
|
||||
func (m *MeetingCheckInMgo) FindByUser(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, checkIns []*model.MeetingCheckIn, err error) {
|
||||
filter := bson.M{"user_id": userID}
|
||||
return mongoutil.FindPage[*model.MeetingCheckIn](ctx, m.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "check_in_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a check-in by check-in ID.
|
||||
func (m *MeetingCheckInMgo) Delete(ctx context.Context, checkInID string) error {
|
||||
return mongoutil.DeleteOne(ctx, m.coll, bson.M{"check_in_id": checkInID})
|
||||
}
|
||||
|
||||
1501
pkg/common/storage/database/mgo/msg.go
Normal file
1501
pkg/common/storage/database/mgo/msg.go
Normal file
File diff suppressed because it is too large
Load Diff
178
pkg/common/storage/database/mgo/msg_test.go
Normal file
178
pkg/common/storage/database/mgo/msg_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func TestName1(t *testing.T) {
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
|
||||
//defer cancel()
|
||||
//cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.66:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
//
|
||||
//v := &MsgMgo{
|
||||
// coll: cli.Database("openim_v3").Collection("msg3"),
|
||||
//}
|
||||
//
|
||||
//req := &msg.SearchMessageReq{
|
||||
// //RecvID: "3187706596",
|
||||
// //SendID: "7009965934",
|
||||
// ContentType: 101,
|
||||
// //SendTime: "2024-05-06",
|
||||
// //SessionType: 3,
|
||||
// Pagination: &sdkws.RequestPagination{
|
||||
// PageNumber: 1,
|
||||
// ShowNumber: 10,
|
||||
// },
|
||||
//}
|
||||
//total, res, err := v.SearchMessage(ctx, req)
|
||||
//if err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
//
|
||||
//for i, re := range res {
|
||||
// t.Logf("%d => %d | %+v", i+1, re.Msg.Seq, re.Msg.Content)
|
||||
//}
|
||||
//
|
||||
//t.Log(total)
|
||||
//
|
||||
//msg, err := NewMsgMongo(cli.Database("openim_v3"))
|
||||
//if err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
//res, err := msg.GetBeforeMsg(ctx, time.Now().UnixMilli(), []string{"1:0"}, 1000)
|
||||
//if err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
//t.Log(len(res))
|
||||
}
|
||||
|
||||
func TestName10(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.48:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
|
||||
v := &MsgMgo{
|
||||
coll: cli.Database("openim_v3").Collection("msg3"),
|
||||
}
|
||||
opt := options.Find().SetLimit(1000)
|
||||
|
||||
res, err := mongoutil.Find[model.MsgDocModel](ctx, v.coll, bson.M{}, opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ctx = context.Background()
|
||||
for i := 0; i < 100000; i++ {
|
||||
for j := range res {
|
||||
res[j].DocID = strconv.FormatUint(rand.Uint64(), 10) + ":0"
|
||||
}
|
||||
if err := mongoutil.InsertMany(ctx, v.coll, res); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log("====>", time.Now(), i)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestName3(t *testing.T) {
|
||||
t.Log(uint64(math.MaxUint64))
|
||||
t.Log(int64(math.MaxInt64))
|
||||
|
||||
t.Log(int64(math.MinInt64))
|
||||
}
|
||||
|
||||
func TestName4(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
|
||||
defer cancel()
|
||||
cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
|
||||
msg, err := NewMsgMongo(cli.Database("openim_v3"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ts := time.Now().Add(-time.Hour * 24 * 5).UnixMilli()
|
||||
t.Log(ts)
|
||||
res, err := msg.GetLastMessageSeqByTime(ctx, "sg_1523453548", ts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log(res)
|
||||
}
|
||||
|
||||
func TestName5(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
|
||||
defer cancel()
|
||||
cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
|
||||
tmp, err := NewMsgMongo(cli.Database("openim_v3"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
msg := tmp.(*MsgMgo)
|
||||
ts := time.Now().Add(-time.Hour * 24 * 5).UnixMilli()
|
||||
t.Log(ts)
|
||||
var seqs []int64
|
||||
for i := 1; i < 256; i++ {
|
||||
seqs = append(seqs, int64(i))
|
||||
}
|
||||
res, err := msg.FindSeqs(ctx, "si_4924054191_9511766539", seqs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log(res)
|
||||
}
|
||||
|
||||
//func TestName6(t *testing.T) {
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
|
||||
// defer cancel()
|
||||
// cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
//
|
||||
// tmp, err := NewMsgMongo(cli.Database("openim_v3"))
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// msg := tmp.(*MsgMgo)
|
||||
// seq, sendTime, err := msg.findBeforeSendTime(ctx, "si_4924054191_9511766539", 1144)
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// t.Log(seq, sendTime)
|
||||
//}
|
||||
|
||||
func TestSearchMessage(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
|
||||
defer cancel()
|
||||
cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
|
||||
msgMongo, err := NewMsgMongo(cli.Database("openim_v3"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ts := time.Now().Add(-time.Hour * 24 * 5).UnixMilli()
|
||||
t.Log(ts)
|
||||
req := &msg.SearchMessageReq{
|
||||
//SendID: "yjz",
|
||||
//RecvID: "aibot",
|
||||
Pagination: &sdkws.RequestPagination{
|
||||
PageNumber: 1,
|
||||
ShowNumber: 20,
|
||||
},
|
||||
}
|
||||
count, resp, err := msgMongo.SearchMessage(ctx, req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Log(resp, count)
|
||||
}
|
||||
126
pkg/common/storage/database/mgo/object.go
Normal file
126
pkg/common/storage/database/mgo/object.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"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 NewS3Mongo(db *mongo.Database) (database.ObjectInfo, error) {
|
||||
coll := db.Collection(database.ObjectName)
|
||||
|
||||
// Create index for name
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "name", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
// Create index for create_time
|
||||
_, err = coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "create_time", Value: 1},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
// Create index for key
|
||||
_, err = coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "key", Value: 1},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
return &S3Mongo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type S3Mongo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (o *S3Mongo) SetObject(ctx context.Context, obj *model.Object) error {
|
||||
filter := bson.M{"name": obj.Name, "engine": obj.Engine}
|
||||
update := bson.M{
|
||||
"name": obj.Name,
|
||||
"engine": obj.Engine,
|
||||
"key": obj.Key,
|
||||
"size": obj.Size,
|
||||
"content_type": obj.ContentType,
|
||||
"group": obj.Group,
|
||||
"create_time": obj.CreateTime,
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, o.coll, filter, bson.M{"$set": update}, false, options.Update().SetUpsert(true))
|
||||
}
|
||||
|
||||
func (o *S3Mongo) Take(ctx context.Context, engine string, name string) (*model.Object, error) {
|
||||
if engine == "" {
|
||||
return mongoutil.FindOne[*model.Object](ctx, o.coll, bson.M{"name": name})
|
||||
}
|
||||
return mongoutil.FindOne[*model.Object](ctx, o.coll, bson.M{"name": name, "engine": engine})
|
||||
}
|
||||
|
||||
func (o *S3Mongo) Delete(ctx context.Context, engine string, name []string) error {
|
||||
if len(name) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.DeleteOne(ctx, o.coll, bson.M{"engine": engine, "name": bson.M{"$in": name}})
|
||||
}
|
||||
|
||||
func (o *S3Mongo) FindExpirationObject(ctx context.Context, engine string, expiration time.Time, needDelType []string, count int64) ([]*model.Object, error) {
|
||||
opt := options.Find()
|
||||
if count > 0 {
|
||||
opt.SetLimit(count)
|
||||
}
|
||||
return mongoutil.Find[*model.Object](ctx, o.coll, bson.M{
|
||||
"engine": engine,
|
||||
"create_time": bson.M{"$lt": expiration},
|
||||
"group": bson.M{"$in": needDelType},
|
||||
}, opt)
|
||||
}
|
||||
|
||||
func (o *S3Mongo) GetKeyCount(ctx context.Context, engine string, key string) (int64, error) {
|
||||
return mongoutil.Count(ctx, o.coll, bson.M{"engine": engine, "key": key})
|
||||
}
|
||||
|
||||
func (o *S3Mongo) GetEngineCount(ctx context.Context, engine string) (int64, error) {
|
||||
return mongoutil.Count(ctx, o.coll, bson.M{"engine": engine})
|
||||
}
|
||||
|
||||
func (o *S3Mongo) GetEngineInfo(ctx context.Context, engine string, limit int, skip int) ([]*model.Object, error) {
|
||||
return mongoutil.Find[*model.Object](ctx, o.coll, bson.M{"engine": engine}, options.Find().SetLimit(int64(limit)).SetSkip(int64(skip)))
|
||||
}
|
||||
|
||||
func (o *S3Mongo) UpdateEngine(ctx context.Context, oldEngine, oldName string, newEngine string) error {
|
||||
return mongoutil.UpdateOne(ctx, o.coll, bson.M{"engine": oldEngine, "name": oldName}, bson.M{"$set": bson.M{"engine": newEngine}}, false)
|
||||
}
|
||||
234
pkg/common/storage/database/mgo/redpacket.go
Normal file
234
pkg/common/storage/database/mgo/redpacket.go
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// RedPacketMgo implements RedPacket using MongoDB as the storage backend.
|
||||
type RedPacketMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewRedPacketMongo creates a new instance of RedPacketMgo with the provided MongoDB database.
|
||||
func NewRedPacketMongo(db *mongo.Database) (database.RedPacket, error) {
|
||||
coll := db.Collection(database.RedPacketName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "red_packet_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "send_user_id", Value: 1}, {Key: "create_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "group_id", Value: 1}, {Key: "create_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "expire_time", Value: 1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RedPacketMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new red packet record.
|
||||
func (r *RedPacketMgo) Create(ctx context.Context, redPacket *model.RedPacket) error {
|
||||
if redPacket.CreateTime.IsZero() {
|
||||
redPacket.CreateTime = time.Now()
|
||||
}
|
||||
return mongoutil.InsertOne(ctx, r.coll, redPacket)
|
||||
}
|
||||
|
||||
// Take retrieves a red packet by ID. Returns an error if not found.
|
||||
func (r *RedPacketMgo) Take(ctx context.Context, redPacketID string) (*model.RedPacket, error) {
|
||||
return mongoutil.FindOne[*model.RedPacket](ctx, r.coll, bson.M{"red_packet_id": redPacketID})
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of a red packet.
|
||||
func (r *RedPacketMgo) UpdateStatus(ctx context.Context, redPacketID string, status int32) error {
|
||||
return mongoutil.UpdateOne(ctx, r.coll, bson.M{"red_packet_id": redPacketID}, bson.M{"$set": bson.M{"status": status}}, false)
|
||||
}
|
||||
|
||||
// UpdateRemain updates the remain amount and count of a red packet.
|
||||
func (r *RedPacketMgo) UpdateRemain(ctx context.Context, redPacketID string, remainAmount int64, remainCount int32) error {
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"remain_amount": remainAmount,
|
||||
"remain_count": remainCount,
|
||||
},
|
||||
}
|
||||
// If remain count is 0, update status to finished
|
||||
if remainCount == 0 {
|
||||
update["$set"].(bson.M)["status"] = model.RedPacketStatusFinished
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, r.coll, bson.M{"red_packet_id": redPacketID}, update, false)
|
||||
}
|
||||
|
||||
// DecreaseRemainAtomic 原子性地减少红包剩余数量和金额(防止并发问题)
|
||||
// 只有在 remain_count > 0 且状态为 Active 时才会更新
|
||||
func (r *RedPacketMgo) DecreaseRemainAtomic(ctx context.Context, redPacketID string, amount int64) (*model.RedPacket, error) {
|
||||
// 过滤条件:红包ID匹配、剩余数量>0、状态为Active
|
||||
filter := bson.M{
|
||||
"red_packet_id": redPacketID,
|
||||
"remain_count": bson.M{"$gt": 0},
|
||||
"status": model.RedPacketStatusActive,
|
||||
}
|
||||
|
||||
// 使用 $inc 原子性地减少剩余数量和金额
|
||||
update := bson.M{
|
||||
"$inc": bson.M{
|
||||
"remain_amount": -amount,
|
||||
"remain_count": -1,
|
||||
},
|
||||
}
|
||||
|
||||
// 使用 findOneAndUpdate 返回更新后的文档
|
||||
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
|
||||
var updatedRedPacket model.RedPacket
|
||||
err := r.coll.FindOneAndUpdate(ctx, filter, update, opts).Decode(&updatedRedPacket)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
// 红包不存在、已领完或状态不正确
|
||||
return nil, errs.ErrArgs.WrapMsg("red packet not available (already finished or expired)")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果剩余数量为0,更新状态为已完成
|
||||
if updatedRedPacket.RemainCount == 0 {
|
||||
statusUpdate := bson.M{"$set": bson.M{"status": model.RedPacketStatusFinished}}
|
||||
_ = mongoutil.UpdateOne(ctx, r.coll, bson.M{"red_packet_id": redPacketID}, statusUpdate, false)
|
||||
updatedRedPacket.Status = model.RedPacketStatusFinished
|
||||
}
|
||||
|
||||
return &updatedRedPacket, nil
|
||||
}
|
||||
|
||||
// FindExpiredRedPackets finds red packets that have expired.
|
||||
func (r *RedPacketMgo) FindExpiredRedPackets(ctx context.Context, beforeTime time.Time) ([]*model.RedPacket, error) {
|
||||
filter := bson.M{
|
||||
"expire_time": bson.M{"$lt": beforeTime},
|
||||
"status": model.RedPacketStatusActive,
|
||||
}
|
||||
return mongoutil.Find[*model.RedPacket](ctx, r.coll, filter)
|
||||
}
|
||||
|
||||
// FindRedPacketsByUser finds red packets sent by a user with pagination.
|
||||
func (r *RedPacketMgo) FindRedPacketsByUser(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, redPackets []*model.RedPacket, err error) {
|
||||
filter := bson.M{"send_user_id": userID}
|
||||
return mongoutil.FindPage[*model.RedPacket](ctx, r.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "create_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindRedPacketsByGroup finds red packets in a group with pagination.
|
||||
func (r *RedPacketMgo) FindRedPacketsByGroup(ctx context.Context, groupID string, pagination pagination.Pagination) (total int64, redPackets []*model.RedPacket, err error) {
|
||||
filter := bson.M{"group_id": groupID}
|
||||
return mongoutil.FindPage[*model.RedPacket](ctx, r.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "create_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindAllRedPackets finds all red packets with pagination.
|
||||
func (r *RedPacketMgo) FindAllRedPackets(ctx context.Context, pagination pagination.Pagination) (total int64, redPackets []*model.RedPacket, err error) {
|
||||
return mongoutil.FindPage[*model.RedPacket](ctx, r.coll, bson.M{}, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "create_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// RedPacketReceiveMgo implements RedPacketReceive using MongoDB as the storage backend.
|
||||
type RedPacketReceiveMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewRedPacketReceiveMongo creates a new instance of RedPacketReceiveMgo with the provided MongoDB database.
|
||||
func NewRedPacketReceiveMongo(db *mongo.Database) (database.RedPacketReceive, error) {
|
||||
coll := db.Collection(database.RedPacketReceiveName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "receive_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "red_packet_id", Value: 1}, {Key: "receive_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "receive_user_id", Value: 1}, {Key: "red_packet_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "receive_user_id", Value: 1}, {Key: "receive_time", Value: -1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RedPacketReceiveMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new red packet receive record.
|
||||
func (r *RedPacketReceiveMgo) Create(ctx context.Context, receive *model.RedPacketReceive) error {
|
||||
if receive.ReceiveTime.IsZero() {
|
||||
receive.ReceiveTime = time.Now()
|
||||
}
|
||||
return mongoutil.InsertOne(ctx, r.coll, receive)
|
||||
}
|
||||
|
||||
// Take retrieves a receive record by ID. Returns an error if not found.
|
||||
func (r *RedPacketReceiveMgo) Take(ctx context.Context, receiveID string) (*model.RedPacketReceive, error) {
|
||||
return mongoutil.FindOne[*model.RedPacketReceive](ctx, r.coll, bson.M{"receive_id": receiveID})
|
||||
}
|
||||
|
||||
// FindByRedPacketID finds all receive records for a red packet.
|
||||
func (r *RedPacketReceiveMgo) FindByRedPacketID(ctx context.Context, redPacketID string) ([]*model.RedPacketReceive, error) {
|
||||
return mongoutil.Find[*model.RedPacketReceive](ctx, r.coll, bson.M{"red_packet_id": redPacketID}, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "receive_time", Value: 1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindByUserAndRedPacketID finds if a user has received a specific red packet.
|
||||
func (r *RedPacketReceiveMgo) FindByUserAndRedPacketID(ctx context.Context, userID, redPacketID string) (*model.RedPacketReceive, error) {
|
||||
return mongoutil.FindOne[*model.RedPacketReceive](ctx, r.coll, bson.M{
|
||||
"receive_user_id": userID,
|
||||
"red_packet_id": redPacketID,
|
||||
})
|
||||
}
|
||||
|
||||
// FindByUser finds all red packets received by a user with pagination.
|
||||
func (r *RedPacketReceiveMgo) FindByUser(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, receives []*model.RedPacketReceive, err error) {
|
||||
filter := bson.M{"receive_user_id": userID}
|
||||
return mongoutil.FindPage[*model.RedPacketReceive](ctx, r.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "receive_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteByReceiveID deletes a receive record by receive ID (for cleanup on failure).
|
||||
func (r *RedPacketReceiveMgo) DeleteByReceiveID(ctx context.Context, receiveID string) error {
|
||||
return mongoutil.DeleteOne(ctx, r.coll, bson.M{"receive_id": receiveID})
|
||||
}
|
||||
104
pkg/common/storage/database/mgo/seq_conversation.go
Normal file
104
pkg/common/storage/database/mgo/seq_conversation.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewSeqConversationMongo(db *mongo.Database) (database.SeqConversation, error) {
|
||||
coll := db.Collection(database.SeqConversationName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "conversation_id", Value: 1},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &seqConversationMongo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type seqConversationMongo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) setSeq(ctx context.Context, conversationID string, seq int64, field string) error {
|
||||
filter := map[string]any{
|
||||
"conversation_id": conversationID,
|
||||
}
|
||||
insert := bson.M{
|
||||
"conversation_id": conversationID,
|
||||
"min_seq": 0,
|
||||
"max_seq": 0,
|
||||
}
|
||||
delete(insert, field)
|
||||
update := map[string]any{
|
||||
"$set": bson.M{
|
||||
field: seq,
|
||||
},
|
||||
"$setOnInsert": insert,
|
||||
}
|
||||
opt := options.Update().SetUpsert(true)
|
||||
return mongoutil.UpdateOne(ctx, s.coll, filter, update, false, opt)
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) Malloc(ctx context.Context, conversationID string, size int64) (int64, error) {
|
||||
if size < 0 {
|
||||
return 0, errors.New("size must be greater than 0")
|
||||
}
|
||||
if size == 0 {
|
||||
return s.GetMaxSeq(ctx, conversationID)
|
||||
}
|
||||
filter := map[string]any{"conversation_id": conversationID}
|
||||
update := map[string]any{
|
||||
"$inc": map[string]any{"max_seq": size},
|
||||
"$set": map[string]any{"min_seq": int64(0)},
|
||||
}
|
||||
opt := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After).SetProjection(map[string]any{"_id": 0, "max_seq": 1})
|
||||
lastSeq, err := mongoutil.FindOneAndUpdate[int64](ctx, s.coll, filter, update, opt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return lastSeq - size, nil
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) SetMaxSeq(ctx context.Context, conversationID string, seq int64) error {
|
||||
return s.setSeq(ctx, conversationID, seq, "max_seq")
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) GetMaxSeq(ctx context.Context, conversationID string) (int64, error) {
|
||||
seq, err := mongoutil.FindOne[int64](ctx, s.coll, bson.M{"conversation_id": conversationID}, options.FindOne().SetProjection(map[string]any{"_id": 0, "max_seq": 1}))
|
||||
if err == nil {
|
||||
return seq, nil
|
||||
} else if IsNotFound(err) {
|
||||
return 0, nil
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) GetMinSeq(ctx context.Context, conversationID string) (int64, error) {
|
||||
seq, err := mongoutil.FindOne[int64](ctx, s.coll, bson.M{"conversation_id": conversationID}, options.FindOne().SetProjection(map[string]any{"_id": 0, "min_seq": 1}))
|
||||
if err == nil {
|
||||
return seq, nil
|
||||
} else if IsNotFound(err) {
|
||||
return 0, nil
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) SetMinSeq(ctx context.Context, conversationID string, seq int64) error {
|
||||
return s.setSeq(ctx, conversationID, seq, "min_seq")
|
||||
}
|
||||
|
||||
func (s *seqConversationMongo) GetConversation(ctx context.Context, conversationID string) (*model.SeqConversation, error) {
|
||||
return mongoutil.FindOne[*model.SeqConversation](ctx, s.coll, bson.M{"conversation_id": conversationID})
|
||||
}
|
||||
43
pkg/common/storage/database/mgo/seq_conversation_test.go
Normal file
43
pkg/common/storage/database/mgo/seq_conversation_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func Result[V any](val V, err error) V {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func Mongodb() *mongo.Database {
|
||||
return Result(
|
||||
mongo.Connect(context.Background(),
|
||||
options.Client().
|
||||
ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").
|
||||
SetConnectTimeout(5*time.Second)),
|
||||
).Database("openim_v3")
|
||||
}
|
||||
|
||||
func TestUserSeq(t *testing.T) {
|
||||
uSeq := Result(NewSeqUserMongo(Mongodb())).(*seqUserMongo)
|
||||
t.Log(uSeq.SetUserMinSeq(context.Background(), "1000", "2000", 4))
|
||||
}
|
||||
|
||||
func TestConversationSeq(t *testing.T) {
|
||||
cSeq := Result(NewSeqConversationMongo(Mongodb())).(*seqConversationMongo)
|
||||
t.Log(cSeq.SetMaxSeq(context.Background(), "2000", 10))
|
||||
t.Log(cSeq.Malloc(context.Background(), "2000", 10))
|
||||
t.Log(cSeq.GetMaxSeq(context.Background(), "2000"))
|
||||
}
|
||||
|
||||
func TestUserGetUserReadSeqs(t *testing.T) {
|
||||
uSeq := Result(NewSeqUserMongo(Mongodb())).(*seqUserMongo)
|
||||
t.Log(uSeq.GetUserReadSeqs(context.Background(), "2110910952", []string{"sg_345762580", "2000", "3000"}))
|
||||
}
|
||||
127
pkg/common/storage/database/mgo/seq_user.go
Normal file
127
pkg/common/storage/database/mgo/seq_user.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewSeqUserMongo(db *mongo.Database) (database.SeqUser, error) {
|
||||
coll := db.Collection(database.SeqUserName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "user_id", Value: 1},
|
||||
{Key: "conversation_id", Value: 1},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &seqUserMongo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type seqUserMongo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) setSeq(ctx context.Context, conversationID string, userID string, seq int64, field string) error {
|
||||
filter := map[string]any{
|
||||
"user_id": userID,
|
||||
"conversation_id": conversationID,
|
||||
}
|
||||
insert := bson.M{
|
||||
"user_id": userID,
|
||||
"conversation_id": conversationID,
|
||||
"min_seq": 0,
|
||||
"max_seq": 0,
|
||||
"read_seq": 0,
|
||||
}
|
||||
delete(insert, field)
|
||||
update := map[string]any{
|
||||
"$set": bson.M{
|
||||
field: seq,
|
||||
},
|
||||
"$setOnInsert": insert,
|
||||
}
|
||||
opt := options.Update().SetUpsert(true)
|
||||
return mongoutil.UpdateOne(ctx, s.coll, filter, update, false, opt)
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) getSeq(ctx context.Context, conversationID string, userID string, failed string) (int64, error) {
|
||||
filter := map[string]any{
|
||||
"user_id": userID,
|
||||
"conversation_id": conversationID,
|
||||
}
|
||||
opt := options.FindOne().SetProjection(bson.M{"_id": 0, failed: 1})
|
||||
seq, err := mongoutil.FindOne[int64](ctx, s.coll, filter, opt)
|
||||
if err == nil {
|
||||
return seq, nil
|
||||
} else if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return 0, nil
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) GetUserMaxSeq(ctx context.Context, conversationID string, userID string) (int64, error) {
|
||||
return s.getSeq(ctx, conversationID, userID, "max_seq")
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) SetUserMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error {
|
||||
return s.setSeq(ctx, conversationID, userID, seq, "max_seq")
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) GetUserMinSeq(ctx context.Context, conversationID string, userID string) (int64, error) {
|
||||
return s.getSeq(ctx, conversationID, userID, "min_seq")
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) SetUserMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error {
|
||||
return s.setSeq(ctx, conversationID, userID, seq, "min_seq")
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error) {
|
||||
return s.getSeq(ctx, conversationID, userID, "read_seq")
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) notFoundSet0(seq map[string]int64, conversationIDs []string) {
|
||||
for _, conversationID := range conversationIDs {
|
||||
if _, ok := seq[conversationID]; !ok {
|
||||
seq[conversationID] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) GetUserReadSeqs(ctx context.Context, userID string, conversationID []string) (map[string]int64, error) {
|
||||
if len(conversationID) == 0 {
|
||||
return map[string]int64{}, nil
|
||||
}
|
||||
filter := bson.M{"user_id": userID, "conversation_id": bson.M{"$in": conversationID}}
|
||||
opt := options.Find().SetProjection(bson.M{"_id": 0, "conversation_id": 1, "read_seq": 1})
|
||||
seqs, err := mongoutil.Find[*model.SeqUser](ctx, s.coll, filter, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[string]int64)
|
||||
for _, seq := range seqs {
|
||||
res[seq.ConversationID] = seq.ReadSeq
|
||||
}
|
||||
s.notFoundSet0(res, conversationID)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *seqUserMongo) SetUserReadSeq(ctx context.Context, conversationID string, userID string, seq int64) error {
|
||||
dbSeq, err := s.GetUserReadSeq(ctx, conversationID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dbSeq > seq {
|
||||
return nil
|
||||
}
|
||||
return s.setSeq(ctx, conversationID, userID, seq, "read_seq")
|
||||
}
|
||||
99
pkg/common/storage/database/mgo/system_config.go
Normal file
99
pkg/common/storage/database/mgo/system_config.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// SystemConfigMgo implements SystemConfig using MongoDB as the storage backend.
|
||||
type SystemConfigMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewSystemConfigMongo creates a new instance of SystemConfigMgo with the provided MongoDB database.
|
||||
func NewSystemConfigMongo(db *mongo.Database) (database.SystemConfig, error) {
|
||||
coll := db.Collection(database.SystemConfigName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "key", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "enabled", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "create_time", Value: -1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SystemConfigMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new system config record.
|
||||
func (s *SystemConfigMgo) Create(ctx context.Context, config *model.SystemConfig) error {
|
||||
config.CreateTime = time.Now()
|
||||
config.UpdateTime = time.Now()
|
||||
return mongoutil.InsertOne(ctx, s.coll, config)
|
||||
}
|
||||
|
||||
// Take retrieves a system config by key. Returns an error if not found.
|
||||
func (s *SystemConfigMgo) Take(ctx context.Context, key string) (*model.SystemConfig, error) {
|
||||
return mongoutil.FindOne[*model.SystemConfig](ctx, s.coll, bson.M{"key": key})
|
||||
}
|
||||
|
||||
// Update updates system config information.
|
||||
func (s *SystemConfigMgo) Update(ctx context.Context, key string, data map[string]any) error {
|
||||
data["update_time"] = time.Now()
|
||||
return mongoutil.UpdateOne(ctx, s.coll, bson.M{"key": key}, bson.M{"$set": data}, true)
|
||||
}
|
||||
|
||||
// Find finds system configs by keys.
|
||||
func (s *SystemConfigMgo) Find(ctx context.Context, keys []string) ([]*model.SystemConfig, error) {
|
||||
return mongoutil.Find[*model.SystemConfig](ctx, s.coll, bson.M{"key": bson.M{"$in": keys}})
|
||||
}
|
||||
|
||||
// FindEnabled finds all enabled system configs.
|
||||
func (s *SystemConfigMgo) FindEnabled(ctx context.Context) ([]*model.SystemConfig, error) {
|
||||
return mongoutil.Find[*model.SystemConfig](ctx, s.coll, bson.M{"enabled": true})
|
||||
}
|
||||
|
||||
// FindByKey finds a system config by key (returns nil if not found, no error).
|
||||
func (s *SystemConfigMgo) FindByKey(ctx context.Context, key string) (*model.SystemConfig, error) {
|
||||
config, err := mongoutil.FindOne[*model.SystemConfig](ctx, s.coll, bson.M{"key": key})
|
||||
if err != nil {
|
||||
if errs.ErrRecordNotFound.Is(err) || err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Delete deletes a system config by key.
|
||||
func (s *SystemConfigMgo) Delete(ctx context.Context, key string) error {
|
||||
return mongoutil.DeleteOne(ctx, s.coll, bson.M{"key": key})
|
||||
}
|
||||
699
pkg/common/storage/database/mgo/user.go
Normal file
699
pkg/common/storage/database/mgo/user.go
Normal file
@@ -0,0 +1,699 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"git.imall.cloud/openim/protocol/user"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewUserMongo(db *mongo.Database) (database.User, error) {
|
||||
coll := db.Collection(database.UserName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &UserMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type UserMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (u *UserMgo) Create(ctx context.Context, users []*model.User) error {
|
||||
return mongoutil.InsertMany(ctx, u.coll, users)
|
||||
}
|
||||
|
||||
func (u *UserMgo) UpdateByMap(ctx context.Context, userID string, args map[string]any) (err error) {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, u.coll, bson.M{"user_id": userID}, bson.M{"$set": args}, true)
|
||||
}
|
||||
|
||||
func (u *UserMgo) Find(ctx context.Context, userIDs []string) (users []*model.User, err error) {
|
||||
query := bson.M{"user_id": bson.M{"$in": userIDs}}
|
||||
log.ZInfo(ctx, "UserMongo Find query", "collection", u.coll.Name(), "query", query)
|
||||
users, err = mongoutil.Find[*model.User](ctx, u.coll, query)
|
||||
log.ZInfo(ctx, "UserMongo Find result", "userCount", len(users), "err", err)
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (u *UserMgo) Take(ctx context.Context, userID string) (user *model.User, err error) {
|
||||
return mongoutil.FindOne[*model.User](ctx, u.coll, bson.M{"user_id": userID})
|
||||
}
|
||||
|
||||
func (u *UserMgo) TakeNotification(ctx context.Context, level int64) (user []*model.User, err error) {
|
||||
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"app_manger_level": level})
|
||||
}
|
||||
|
||||
func (u *UserMgo) TakeGTEAppManagerLevel(ctx context.Context, level int64) (user []*model.User, err error) {
|
||||
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"app_manger_level": bson.M{"$gte": level}})
|
||||
}
|
||||
|
||||
func (u *UserMgo) TakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error) {
|
||||
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname})
|
||||
}
|
||||
|
||||
func (u *UserMgo) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) {
|
||||
return mongoutil.FindPage[*model.User](ctx, u.coll, bson.M{}, pagination)
|
||||
}
|
||||
|
||||
func (u *UserMgo) PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error) {
|
||||
query := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"app_manger_level": level1},
|
||||
{"app_manger_level": level2},
|
||||
},
|
||||
}
|
||||
|
||||
return mongoutil.FindPage[*model.User](ctx, u.coll, query, pagination)
|
||||
}
|
||||
|
||||
func (u *UserMgo) PageFindUserWithKeyword(
|
||||
ctx context.Context,
|
||||
level1 int64,
|
||||
level2 int64,
|
||||
userID string,
|
||||
nickName string,
|
||||
pagination pagination.Pagination,
|
||||
) (count int64, users []*model.User, err error) {
|
||||
// Initialize the base query with level conditions
|
||||
query := bson.M{
|
||||
"$and": []bson.M{
|
||||
{"app_manger_level": bson.M{"$in": []int64{level1, level2}}},
|
||||
},
|
||||
}
|
||||
|
||||
// Add userID and userName conditions to the query if they are provided
|
||||
if userID != "" || nickName != "" {
|
||||
userConditions := []bson.M{}
|
||||
if userID != "" {
|
||||
// Use regex for userID
|
||||
regexPattern := primitive.Regex{Pattern: userID, Options: "i"} // 'i' for case-insensitive matching
|
||||
userConditions = append(userConditions, bson.M{"user_id": regexPattern})
|
||||
}
|
||||
if nickName != "" {
|
||||
// Use regex for userName
|
||||
regexPattern := primitive.Regex{Pattern: nickName, Options: "i"} // 'i' for case-insensitive matching
|
||||
userConditions = append(userConditions, bson.M{"nickname": regexPattern})
|
||||
}
|
||||
query["$and"] = append(query["$and"].([]bson.M), bson.M{"$or": userConditions})
|
||||
}
|
||||
|
||||
// Perform the paginated search
|
||||
return mongoutil.FindPage[*model.User](ctx, u.coll, query, pagination)
|
||||
}
|
||||
|
||||
func (u *UserMgo) GetAllUserID(ctx context.Context, pagination pagination.Pagination) (int64, []string, error) {
|
||||
return mongoutil.FindPage[string](ctx, u.coll, bson.M{}, pagination, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1}))
|
||||
}
|
||||
|
||||
func (u *UserMgo) Exist(ctx context.Context, userID string) (exist bool, err error) {
|
||||
return mongoutil.Exist(ctx, u.coll, bson.M{"user_id": userID})
|
||||
}
|
||||
|
||||
func (u *UserMgo) GetUserGlobalRecvMsgOpt(ctx context.Context, userID string) (opt int, err error) {
|
||||
return mongoutil.FindOne[int](ctx, u.coll, bson.M{"user_id": userID}, options.FindOne().SetProjection(bson.M{"_id": 0, "global_recv_msg_opt": 1}))
|
||||
}
|
||||
|
||||
// SearchUsersByFields 根据多个字段搜索用户:account(userID)、phone、nickname
|
||||
// 返回匹配的用户ID列表
|
||||
// 使用 MongoDB $lookup 实现连表查询:attribute 集合和 user 集合
|
||||
func (u *UserMgo) SearchUsersByFields(ctx context.Context, account, phone, nickname string) (userIDs []string, err error) {
|
||||
log.ZInfo(ctx, "SearchUsersByFields START", "account", account, "phone", phone, "nickname", nickname)
|
||||
|
||||
// 获取 attribute 集合
|
||||
attributeColl := u.coll.Database().Collection("attribute")
|
||||
log.ZInfo(ctx, "SearchUsersByFields collections", "attributeCollection", "attribute", "userCollection", u.coll.Name(), "database", u.coll.Database().Name())
|
||||
|
||||
// 构建聚合管道,使用 $lookup 实现连表查询
|
||||
pipeline := bson.A{}
|
||||
|
||||
// 第一步:从 attribute 集合开始,构建匹配条件
|
||||
attributeMatch := bson.M{}
|
||||
attributeOrConditions := []bson.M{}
|
||||
|
||||
if account != "" {
|
||||
attributeOrConditions = append(attributeOrConditions, bson.M{"account": bson.M{"$regex": account, "$options": "i"}})
|
||||
log.ZInfo(ctx, "SearchUsersByFields add account condition", "account", account)
|
||||
}
|
||||
|
||||
if phone != "" {
|
||||
// phone_number 在 attribute 集合中搜索(注意:字段名是 phone_number,不是 phone)
|
||||
attributeOrConditions = append(attributeOrConditions, bson.M{"phone_number": bson.M{"$regex": phone, "$options": "i"}})
|
||||
log.ZInfo(ctx, "SearchUsersByFields add phone condition", "phone", phone, "field", "phone_number")
|
||||
}
|
||||
|
||||
// 判断查询策略:如果有 account 或 phone,从 attribute 开始;如果只有 nickname,从 user 开始
|
||||
hasAttributeSearch := len(attributeOrConditions) > 0
|
||||
hasUserSearch := nickname != ""
|
||||
|
||||
var startCollection *mongo.Collection // 记录起始集合
|
||||
|
||||
if hasAttributeSearch {
|
||||
// 从 attribute 集合开始查询
|
||||
startCollection = attributeColl
|
||||
|
||||
// 先直接查询 attribute 集合,看看实际的数据结构
|
||||
type FullAttributeDoc map[string]interface{}
|
||||
allDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, bson.M{}, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0}))
|
||||
if err == nil && len(allDocs) > 0 {
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute sample documents (all fields)", "sampleCount", len(allDocs), "samples", allDocs)
|
||||
}
|
||||
|
||||
// 尝试精确匹配手机号(使用正确的字段名 phone_number)
|
||||
exactPhoneMatch := bson.M{"phone_number": phone}
|
||||
exactDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, exactPhoneMatch, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0}))
|
||||
if err == nil {
|
||||
log.ZInfo(ctx, "SearchUsersByFields exact phone_number match", "phone", phone, "matchCount", len(exactDocs), "docs", exactDocs)
|
||||
}
|
||||
|
||||
// 尝试正则匹配(不区分大小写,使用正确的字段名 phone_number)
|
||||
regexPhoneMatch := bson.M{"phone_number": bson.M{"$regex": phone, "$options": "i"}}
|
||||
regexDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, regexPhoneMatch, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0}))
|
||||
if err == nil {
|
||||
log.ZInfo(ctx, "SearchUsersByFields regex phone_number match", "phone", phone, "matchCount", len(regexDocs), "docs", regexDocs)
|
||||
}
|
||||
|
||||
// 尝试查询包含 phone_number 字段的所有记录
|
||||
hasPhoneFieldMatch := bson.M{"phone_number": bson.M{"$exists": true, "$ne": ""}}
|
||||
hasPhoneDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, hasPhoneFieldMatch, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0}))
|
||||
if err == nil {
|
||||
log.ZInfo(ctx, "SearchUsersByFields documents with phone_number field", "matchCount", len(hasPhoneDocs), "docs", hasPhoneDocs)
|
||||
}
|
||||
|
||||
attributeMatch["$or"] = attributeOrConditions
|
||||
pipeline = append(pipeline, bson.M{"$match": attributeMatch})
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute match stage", "match", attributeMatch)
|
||||
|
||||
// 使用 $lookup 关联 user 集合
|
||||
lookupStage := bson.M{
|
||||
"$lookup": bson.M{
|
||||
"from": u.coll.Name(), // user 集合名称
|
||||
"localField": "user_id", // attribute 集合的字段
|
||||
"foreignField": "user_id", // user 集合的字段
|
||||
"as": "userInfo", // 关联后的字段名
|
||||
},
|
||||
}
|
||||
pipeline = append(pipeline, lookupStage)
|
||||
log.ZInfo(ctx, "SearchUsersByFields add lookup stage", "from", u.coll.Name(), "localField", "user_id", "foreignField", "user_id")
|
||||
|
||||
// 展开 userInfo 数组
|
||||
pipeline = append(pipeline, bson.M{"$unwind": bson.M{
|
||||
"path": "$userInfo",
|
||||
"preserveNullAndEmptyArrays": true,
|
||||
}})
|
||||
|
||||
// 如果有 nickname 条件,需要匹配 user 集合的 nickname
|
||||
if hasUserSearch {
|
||||
userMatch := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"userInfo.nickname": bson.M{"$regex": nickname, "$options": "i"}},
|
||||
{"userInfo": bson.M{"$exists": false}}, // 如果没有关联到 user,也保留
|
||||
},
|
||||
}
|
||||
pipeline = append(pipeline, bson.M{"$match": userMatch})
|
||||
log.ZInfo(ctx, "SearchUsersByFields add nickname match", "nickname", nickname)
|
||||
}
|
||||
|
||||
// 从 attribute 集合开始,user_id 字段在根级别
|
||||
pipeline = append(pipeline, bson.M{
|
||||
"$project": bson.M{
|
||||
"_id": 0,
|
||||
"user_id": 1,
|
||||
},
|
||||
})
|
||||
} else if hasUserSearch {
|
||||
// 只有 nickname 条件,从 user 集合开始查询
|
||||
startCollection = u.coll
|
||||
userMatch := bson.M{"nickname": bson.M{"$regex": nickname, "$options": "i"}}
|
||||
pipeline = append(pipeline, bson.M{"$match": userMatch})
|
||||
log.ZInfo(ctx, "SearchUsersByFields user match stage", "match", userMatch)
|
||||
|
||||
// 从 user 集合开始,user_id 字段在根级别
|
||||
pipeline = append(pipeline, bson.M{
|
||||
"$project": bson.M{
|
||||
"_id": 0,
|
||||
"user_id": 1,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 没有任何搜索条件,返回空
|
||||
log.ZInfo(ctx, "SearchUsersByFields no search conditions", "returning empty")
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// 第五步:去重
|
||||
pipeline = append(pipeline, bson.M{
|
||||
"$group": bson.M{
|
||||
"_id": "$user_id",
|
||||
"user_id": bson.M{"$first": "$user_id"},
|
||||
},
|
||||
})
|
||||
|
||||
// 第六步:只返回 user_id
|
||||
pipeline = append(pipeline, bson.M{
|
||||
"$project": bson.M{
|
||||
"_id": 0,
|
||||
"user_id": 1,
|
||||
},
|
||||
})
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields pipeline", "pipeline", pipeline, "startCollection", startCollection.Name())
|
||||
|
||||
// 执行聚合查询
|
||||
type ResultDoc struct {
|
||||
UserID string `bson:"user_id"`
|
||||
}
|
||||
|
||||
results, err := mongoutil.Aggregate[*ResultDoc](ctx, startCollection, pipeline)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "SearchUsersByFields Aggregate failed", err, "pipeline", pipeline)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields Aggregate result", "resultCount", len(results), "results", results)
|
||||
|
||||
// 提取 user_id 列表
|
||||
userIDs = make([]string, 0, len(results))
|
||||
for _, result := range results {
|
||||
if result.UserID != "" {
|
||||
userIDs = append(userIDs, result.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields FINAL result", "totalUserIDs", len(userIDs), "userIDs", userIDs)
|
||||
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
// 旧版本的实现(保留作为备份)
|
||||
func (u *UserMgo) SearchUsersByFields_old(ctx context.Context, account, phone, nickname string) (userIDs []string, err error) {
|
||||
log.ZInfo(ctx, "SearchUsersByFields START", "account", account, "phone", phone, "nickname", nickname)
|
||||
|
||||
userIDMap := make(map[string]bool) // 用于去重
|
||||
|
||||
// 获取 attribute 集合
|
||||
attributeColl := u.coll.Database().Collection("attribute")
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute collection", "collectionName", "attribute", "database", u.coll.Database().Name())
|
||||
|
||||
// 从 attribute 集合查询 account 和 phone
|
||||
if account != "" || phone != "" {
|
||||
attributeFilter := bson.M{}
|
||||
attributeConditions := []bson.M{}
|
||||
|
||||
if account != "" {
|
||||
// account 在 attribute 集合中搜索
|
||||
attributeConditions = append(attributeConditions, bson.M{"account": bson.M{"$regex": account, "$options": "i"}})
|
||||
log.ZInfo(ctx, "SearchUsersByFields add account condition", "account", account)
|
||||
}
|
||||
|
||||
if phone != "" {
|
||||
// phone 在 attribute 集合中搜索
|
||||
attributeConditions = append(attributeConditions, bson.M{"phone": bson.M{"$regex": phone, "$options": "i"}})
|
||||
log.ZInfo(ctx, "SearchUsersByFields add phone condition", "phone", phone)
|
||||
}
|
||||
|
||||
if len(attributeConditions) > 0 {
|
||||
attributeFilter["$or"] = attributeConditions
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields query attribute", "filter", attributeFilter, "account", account, "phone", phone, "conditionsCount", len(attributeConditions))
|
||||
|
||||
// attribute 集合的结构包含:user_id, account, phone 等字段
|
||||
type AttributeDoc struct {
|
||||
UserID string `bson:"user_id"`
|
||||
}
|
||||
|
||||
// 先尝试查询,看看集合是否存在数据
|
||||
count, err := mongoutil.Count(ctx, attributeColl, bson.M{})
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute collection total count", "count", count, "err", err)
|
||||
|
||||
// 尝试查询多条记录看看结构,特别是包含 phone 数据的记录
|
||||
type SampleDoc struct {
|
||||
UserID string `bson:"user_id"`
|
||||
Account string `bson:"account"`
|
||||
Phone string `bson:"phone"`
|
||||
}
|
||||
// 查询所有记录,看看实际的数据结构
|
||||
samples, err := mongoutil.Find[*SampleDoc](ctx, attributeColl, bson.M{}, options.Find().SetLimit(10).SetProjection(bson.M{"_id": 0, "user_id": 1, "account": 1, "phone": 1}))
|
||||
if err == nil && len(samples) > 0 {
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute sample documents", "sampleCount", len(samples), "samples", samples)
|
||||
|
||||
// 尝试查询包含 phone 字段不为空的记录
|
||||
phoneFilter := bson.M{"phone": bson.M{"$exists": true, "$ne": ""}}
|
||||
phoneSamples, err := mongoutil.Find[*SampleDoc](ctx, attributeColl, phoneFilter, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0, "user_id": 1, "account": 1, "phone": 1}))
|
||||
if err == nil {
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute documents with phone", "phoneSampleCount", len(phoneSamples), "phoneSamples", phoneSamples)
|
||||
} else {
|
||||
log.ZWarn(ctx, "SearchUsersByFields cannot find documents with phone", err)
|
||||
}
|
||||
|
||||
// 尝试查询所有字段,看看实际的数据结构(只查一条)
|
||||
type FullDoc map[string]interface{}
|
||||
fullSample, err := mongoutil.FindOne[*FullDoc](ctx, attributeColl, bson.M{}, options.FindOne().SetProjection(bson.M{"_id": 0}))
|
||||
if err == nil && fullSample != nil {
|
||||
log.ZInfo(ctx, "SearchUsersByFields attribute full document structure", "fullSample", fullSample)
|
||||
}
|
||||
} else {
|
||||
log.ZWarn(ctx, "SearchUsersByFields cannot get samples from attribute", err, "sampleCount", len(samples))
|
||||
}
|
||||
|
||||
// 尝试精确匹配手机号,看看是否有数据
|
||||
exactPhoneFilter := bson.M{"phone": phone}
|
||||
exactCount, err := mongoutil.Count(ctx, attributeColl, exactPhoneFilter)
|
||||
log.ZInfo(ctx, "SearchUsersByFields exact phone match count", "phone", phone, "count", exactCount, "err", err)
|
||||
|
||||
attributeDocs, err := mongoutil.Find[*AttributeDoc](ctx, attributeColl, attributeFilter, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1}))
|
||||
if err != nil {
|
||||
log.ZError(ctx, "SearchUsersByFields Find failed in attribute collection", err, "filter", attributeFilter)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields Find result from attribute", "userCount", len(attributeDocs), "userIDs", attributeDocs)
|
||||
|
||||
for i, doc := range attributeDocs {
|
||||
log.ZDebug(ctx, "SearchUsersByFields processing attribute doc", "index", i, "userID", doc.UserID)
|
||||
if doc.UserID != "" && !userIDMap[doc.UserID] {
|
||||
userIDMap[doc.UserID] = true
|
||||
log.ZDebug(ctx, "SearchUsersByFields added userID from attribute", "userID", doc.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从 user 集合查询 nickname
|
||||
if nickname != "" {
|
||||
userFilter := bson.M{"nickname": bson.M{"$regex": nickname, "$options": "i"}}
|
||||
log.ZInfo(ctx, "SearchUsersByFields query user", "filter", userFilter, "nickname", nickname)
|
||||
|
||||
users, err := mongoutil.Find[*model.User](ctx, u.coll, userFilter, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1}))
|
||||
if err != nil {
|
||||
log.ZError(ctx, "SearchUsersByFields Find failed in user collection", err, "filter", userFilter)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields Find result from user", "userCount", len(users))
|
||||
|
||||
for i, user := range users {
|
||||
log.ZDebug(ctx, "SearchUsersByFields processing user doc", "index", i, "userID", user.UserID)
|
||||
if user.UserID != "" && !userIDMap[user.UserID] {
|
||||
userIDMap[user.UserID] = true
|
||||
log.ZDebug(ctx, "SearchUsersByFields added userID from user", "userID", user.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将 map 转换为 slice
|
||||
userIDs = make([]string, 0, len(userIDMap))
|
||||
for userID := range userIDMap {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "SearchUsersByFields FINAL result", "totalUserIDs", len(userIDs), "userIDs", userIDs)
|
||||
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
func (u *UserMgo) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) {
|
||||
if before == nil {
|
||||
return mongoutil.Count(ctx, u.coll, bson.M{})
|
||||
}
|
||||
return mongoutil.Count(ctx, u.coll, bson.M{"create_time": bson.M{"$lt": before}})
|
||||
}
|
||||
|
||||
func (u *UserMgo) AddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error {
|
||||
collection := u.coll.Database().Collection("userCommands")
|
||||
|
||||
// Create a new document instead of updating an existing one
|
||||
doc := bson.M{
|
||||
"userID": userID,
|
||||
"type": Type,
|
||||
"uuid": UUID,
|
||||
"createTime": time.Now().Unix(), // assuming you want the creation time in Unix timestamp
|
||||
"value": value,
|
||||
"ex": ex,
|
||||
}
|
||||
|
||||
_, err := collection.InsertOne(ctx, doc)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
func (u *UserMgo) DeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error {
|
||||
collection := u.coll.Database().Collection("userCommands")
|
||||
|
||||
filter := bson.M{"userID": userID, "type": Type, "uuid": UUID}
|
||||
|
||||
result, err := collection.DeleteOne(ctx, filter)
|
||||
// when err is not nil, result might be nil
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
if result.DeletedCount == 0 {
|
||||
// No records found to update
|
||||
return errs.Wrap(errs.ErrRecordNotFound)
|
||||
}
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
func (u *UserMgo) UpdateUserCommand(ctx context.Context, userID string, Type int32, UUID string, val map[string]any) error {
|
||||
if len(val) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
collection := u.coll.Database().Collection("userCommands")
|
||||
|
||||
filter := bson.M{"userID": userID, "type": Type, "uuid": UUID}
|
||||
update := bson.M{"$set": val}
|
||||
|
||||
result, err := collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
// No records found to update
|
||||
return errs.Wrap(errs.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserMgo) GetUserCommand(ctx context.Context, userID string, Type int32) ([]*user.CommandInfoResp, error) {
|
||||
collection := u.coll.Database().Collection("userCommands")
|
||||
filter := bson.M{"userID": userID, "type": Type}
|
||||
|
||||
cursor, err := collection.Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
// Initialize commands as a slice of pointers
|
||||
commands := []*user.CommandInfoResp{}
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
var document struct {
|
||||
Type int32 `bson:"type"`
|
||||
UUID string `bson:"uuid"`
|
||||
Value string `bson:"value"`
|
||||
CreateTime int64 `bson:"createTime"`
|
||||
Ex string `bson:"ex"`
|
||||
}
|
||||
|
||||
if err := cursor.Decode(&document); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commandInfo := &user.CommandInfoResp{
|
||||
Type: document.Type,
|
||||
Uuid: document.UUID,
|
||||
Value: document.Value,
|
||||
CreateTime: document.CreateTime,
|
||||
Ex: document.Ex,
|
||||
}
|
||||
|
||||
commands = append(commands, commandInfo)
|
||||
}
|
||||
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
func (u *UserMgo) GetAllUserCommand(ctx context.Context, userID string) ([]*user.AllCommandInfoResp, error) {
|
||||
collection := u.coll.Database().Collection("userCommands")
|
||||
filter := bson.M{"userID": userID}
|
||||
|
||||
cursor, err := collection.Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
// Initialize commands as a slice of pointers
|
||||
commands := []*user.AllCommandInfoResp{}
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
var document struct {
|
||||
Type int32 `bson:"type"`
|
||||
UUID string `bson:"uuid"`
|
||||
Value string `bson:"value"`
|
||||
CreateTime int64 `bson:"createTime"`
|
||||
Ex string `bson:"ex"`
|
||||
}
|
||||
|
||||
if err := cursor.Decode(&document); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
commandInfo := &user.AllCommandInfoResp{
|
||||
Type: document.Type,
|
||||
Uuid: document.UUID,
|
||||
Value: document.Value,
|
||||
CreateTime: document.CreateTime,
|
||||
Ex: document.Ex,
|
||||
}
|
||||
|
||||
commands = append(commands, commandInfo)
|
||||
}
|
||||
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return commands, nil
|
||||
}
|
||||
func (u *UserMgo) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{
|
||||
"$match": bson.M{
|
||||
"create_time": bson.M{
|
||||
"$gte": start,
|
||||
"$lt": end,
|
||||
},
|
||||
},
|
||||
},
|
||||
bson.M{
|
||||
"$group": bson.M{
|
||||
"_id": bson.M{
|
||||
"$dateToString": bson.M{
|
||||
"format": "%Y-%m-%d",
|
||||
"date": "$create_time",
|
||||
},
|
||||
},
|
||||
"count": bson.M{
|
||||
"$sum": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
type Item struct {
|
||||
Date string `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
items, err := mongoutil.Aggregate[Item](ctx, u.coll, pipeline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[string]int64, len(items))
|
||||
for _, item := range items {
|
||||
res[item.Date] = item.Count
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u *UserMgo) SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error) {
|
||||
if len(userIDName) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
userIDs := make([]string, 0, len(userIDName))
|
||||
attached := make(map[string]string)
|
||||
for userID, name := range userIDName {
|
||||
userIDs = append(userIDs, userID)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
attached[userID] = name
|
||||
}
|
||||
var sortValue int
|
||||
if asc {
|
||||
sortValue = 1
|
||||
} else {
|
||||
sortValue = -1
|
||||
}
|
||||
if len(attached) == 0 {
|
||||
filter := bson.M{"user_id": bson.M{"$in": userIDs}}
|
||||
opt := options.Find().SetSort(bson.M{"nickname": sortValue})
|
||||
return mongoutil.Find[*model.User](ctx, u.coll, filter, opt)
|
||||
}
|
||||
pipeline := []bson.M{
|
||||
{
|
||||
"$match": bson.M{
|
||||
"user_id": bson.M{"$in": userIDs},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"_query_sort_name": bson.M{
|
||||
"$arrayElemAt": []any{
|
||||
bson.M{
|
||||
"$filter": bson.M{
|
||||
"input": bson.M{
|
||||
"$objectToArray": attached,
|
||||
},
|
||||
"as": "item",
|
||||
"cond": bson.M{
|
||||
"$eq": []any{"$$item.k", "$user_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"_query_sort_name": bson.M{
|
||||
"$ifNull": []any{"$_query_sort_name.v", "$nickname"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$sort": bson.M{
|
||||
"_query_sort_name": sortValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
return mongoutil.Aggregate[*model.User](ctx, u.coll, pipeline)
|
||||
}
|
||||
304
pkg/common/storage/database/mgo/version_log.go
Normal file
304
pkg/common/storage/database/mgo/version_log.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/versionctx"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewVersionLog(coll *mongo.Collection) (database.VersionLog, error) {
|
||||
lm := &VersionLogMgo{coll: coll}
|
||||
if err := lm.initIndex(context.Background()); err != nil {
|
||||
return nil, errs.WrapMsg(err, "init version log index failed", "coll", coll.Name())
|
||||
}
|
||||
return lm, nil
|
||||
}
|
||||
|
||||
type VersionLogMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) initIndex(ctx context.Context) error {
|
||||
_, err := l.coll.Indexes().CreateOne(ctx, mongo.IndexModel{
|
||||
Keys: bson.M{
|
||||
"d_id": 1,
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) IncrVersion(ctx context.Context, dId string, eIds []string, state int32) error {
|
||||
_, err := l.IncrVersionResult(ctx, dId, eIds, state)
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) IncrVersionResult(ctx context.Context, dId string, eIds []string, state int32) (*model.VersionLog, error) {
|
||||
vl, err := l.incrVersionResult(ctx, dId, eIds, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
versionctx.GetVersionLog(ctx).Append(versionctx.Collection{
|
||||
Name: l.coll.Name(),
|
||||
Doc: vl,
|
||||
})
|
||||
return vl, nil
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) incrVersionResult(ctx context.Context, dId string, eIds []string, state int32) (*model.VersionLog, error) {
|
||||
if len(eIds) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("elem id is empty", "dId", dId)
|
||||
}
|
||||
now := time.Now()
|
||||
if res, err := l.writeLogBatch2(ctx, dId, eIds, state, now); err == nil {
|
||||
return res, nil
|
||||
} else if !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, err
|
||||
}
|
||||
if res, err := l.initDoc(ctx, dId, eIds, state, now); err == nil {
|
||||
return res, nil
|
||||
} else if !mongo.IsDuplicateKeyError(err) {
|
||||
return nil, err
|
||||
}
|
||||
return l.writeLogBatch2(ctx, dId, eIds, state, now)
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) initDoc(ctx context.Context, dId string, eIds []string, state int32, now time.Time) (*model.VersionLog, error) {
|
||||
wl := model.VersionLogTable{
|
||||
ID: primitive.NewObjectID(),
|
||||
DID: dId,
|
||||
Logs: make([]model.VersionLogElem, 0, len(eIds)),
|
||||
Version: database.FirstVersion,
|
||||
Deleted: database.DefaultDeleteVersion,
|
||||
LastUpdate: now,
|
||||
}
|
||||
for _, eId := range eIds {
|
||||
wl.Logs = append(wl.Logs, model.VersionLogElem{
|
||||
EID: eId,
|
||||
State: state,
|
||||
Version: database.FirstVersion,
|
||||
LastUpdate: now,
|
||||
})
|
||||
}
|
||||
if _, err := l.coll.InsertOne(ctx, &wl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wl.VersionLog(), nil
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) writeLogBatch2(ctx context.Context, dId string, eIds []string, state int32, now time.Time) (*model.VersionLog, error) {
|
||||
if eIds == nil {
|
||||
eIds = []string{}
|
||||
}
|
||||
filter := bson.M{
|
||||
"d_id": dId,
|
||||
}
|
||||
elems := make([]bson.M, 0, len(eIds))
|
||||
for _, eId := range eIds {
|
||||
elems = append(elems, bson.M{
|
||||
"e_id": eId,
|
||||
"version": "$version",
|
||||
"state": state,
|
||||
"last_update": now,
|
||||
})
|
||||
}
|
||||
pipeline := []bson.M{
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"delete_e_ids": eIds,
|
||||
},
|
||||
},
|
||||
{
|
||||
"$set": bson.M{
|
||||
"version": bson.M{"$add": []any{"$version", 1}},
|
||||
"last_update": now,
|
||||
},
|
||||
},
|
||||
{
|
||||
"$set": bson.M{
|
||||
"logs": bson.M{
|
||||
"$filter": bson.M{
|
||||
"input": "$logs",
|
||||
"as": "log",
|
||||
"cond": bson.M{
|
||||
"$not": bson.M{
|
||||
"$in": []any{"$$log.e_id", "$delete_e_ids"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$set": bson.M{
|
||||
"logs": bson.M{
|
||||
"$concatArrays": []any{
|
||||
"$logs",
|
||||
elems,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$unset": "delete_e_ids",
|
||||
},
|
||||
}
|
||||
projection := bson.M{
|
||||
"logs": 0,
|
||||
}
|
||||
opt := options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After).SetProjection(projection)
|
||||
res, err := mongoutil.FindOneAndUpdate[*model.VersionLog](ctx, l.coll, filter, pipeline, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Logs = make([]model.VersionLogElem, 0, len(eIds))
|
||||
for _, id := range eIds {
|
||||
res.Logs = append(res.Logs, model.VersionLogElem{
|
||||
EID: id,
|
||||
State: state,
|
||||
Version: res.Version,
|
||||
LastUpdate: res.LastUpdate,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) findDoc(ctx context.Context, dId string) (*model.VersionLog, error) {
|
||||
vl, err := mongoutil.FindOne[*model.VersionLogTable](ctx, l.coll, bson.M{"d_id": dId}, options.FindOne().SetProjection(bson.M{"logs": 0}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vl.VersionLog(), nil
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) FindChangeLog(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error) {
|
||||
if wl, err := l.findChangeLog(ctx, dId, version, limit); err == nil {
|
||||
return wl, nil
|
||||
} else if !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, err
|
||||
}
|
||||
log.ZDebug(ctx, "init doc", "dId", dId)
|
||||
if res, err := l.initDoc(ctx, dId, nil, 0, time.Now()); err == nil {
|
||||
log.ZDebug(ctx, "init doc success", "dId", dId)
|
||||
return res, nil
|
||||
} else if mongo.IsDuplicateKeyError(err) {
|
||||
return l.findChangeLog(ctx, dId, version, limit)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) BatchFindChangeLog(ctx context.Context, dIds []string, versions []uint, limits []int) (vLogs []*model.VersionLog, err error) {
|
||||
for i := 0; i < len(dIds); i++ {
|
||||
if vLog, err := l.findChangeLog(ctx, dIds[i], versions[i], limits[i]); err == nil {
|
||||
vLogs = append(vLogs, vLog)
|
||||
} else if !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
log.ZError(ctx, "findChangeLog error:", errs.Wrap(err))
|
||||
}
|
||||
log.ZDebug(ctx, "init doc", "dId", dIds[i])
|
||||
if res, err := l.initDoc(ctx, dIds[i], nil, 0, time.Now()); err == nil {
|
||||
log.ZDebug(ctx, "init doc success", "dId", dIds[i])
|
||||
vLogs = append(vLogs, res)
|
||||
} else if mongo.IsDuplicateKeyError(err) {
|
||||
l.findChangeLog(ctx, dIds[i], versions[i], limits[i])
|
||||
} else {
|
||||
log.ZError(ctx, "init doc error:", errs.Wrap(err))
|
||||
}
|
||||
}
|
||||
return vLogs, errs.Wrap(err)
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) findChangeLog(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error) {
|
||||
if version == 0 && limit == 0 {
|
||||
return l.findDoc(ctx, dId)
|
||||
}
|
||||
pipeline := []bson.M{
|
||||
{
|
||||
"$match": bson.M{
|
||||
"d_id": dId,
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"logs": bson.M{
|
||||
"$cond": bson.M{
|
||||
"if": bson.M{
|
||||
"$or": []bson.M{
|
||||
{"$lt": []any{"$version", version}},
|
||||
{"$gte": []any{"$deleted", version}},
|
||||
},
|
||||
},
|
||||
"then": []any{},
|
||||
"else": "$logs",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"logs": bson.M{
|
||||
"$filter": bson.M{
|
||||
"input": "$logs",
|
||||
"as": "l",
|
||||
"cond": bson.M{
|
||||
"$gt": []any{"$$l.version", version},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"log_len": bson.M{"$size": "$logs"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$addFields": bson.M{
|
||||
"logs": bson.M{
|
||||
"$cond": bson.M{
|
||||
"if": bson.M{
|
||||
"$gt": []any{"$log_len", limit},
|
||||
},
|
||||
"then": []any{},
|
||||
"else": "$logs",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if limit <= 0 {
|
||||
pipeline = pipeline[:len(pipeline)-1]
|
||||
}
|
||||
vl, err := mongoutil.Aggregate[*model.VersionLog](ctx, l.coll, pipeline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(vl) == 0 {
|
||||
return nil, mongo.ErrNoDocuments
|
||||
}
|
||||
return vl[0], nil
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) DeleteAfterUnchangedLog(ctx context.Context, deadline time.Time) error {
|
||||
return mongoutil.DeleteMany(ctx, l.coll, bson.M{
|
||||
"last_update": bson.M{
|
||||
"$lt": deadline,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (l *VersionLogMgo) Delete(ctx context.Context, dId string) error {
|
||||
return mongoutil.DeleteOne(ctx, l.coll, bson.M{"d_id": dId})
|
||||
}
|
||||
40
pkg/common/storage/database/mgo/version_test.go
Normal file
40
pkg/common/storage/database/mgo/version_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
//func Result[V any](val V, err error) V {
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// return val
|
||||
//}
|
||||
|
||||
func Check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
cli := Result(mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.48:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second)))
|
||||
coll := cli.Database("openim_v3").Collection("version_test")
|
||||
tmp, err := NewVersionLog(coll)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vl := tmp.(*VersionLogMgo)
|
||||
res, err := vl.incrVersionResult(context.Background(), "100", []string{"1000", "1001", "1003"}, model.VersionStateInsert)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
t.Logf("%+v", res)
|
||||
}
|
||||
231
pkg/common/storage/database/mgo/wallet.go
Normal file
231
pkg/common/storage/database/mgo/wallet.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright © 2023 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"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// WalletMgo implements Wallet using MongoDB as the storage backend.
|
||||
type WalletMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewWalletMongo creates a new instance of WalletMgo with the provided MongoDB database.
|
||||
func NewWalletMongo(db *mongo.Database) (database.Wallet, error) {
|
||||
coll := db.Collection(database.WalletName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "create_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "update_time", Value: -1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WalletMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new wallet record.
|
||||
func (w *WalletMgo) Create(ctx context.Context, wallet *model.Wallet) error {
|
||||
if wallet.CreateTime.IsZero() {
|
||||
wallet.CreateTime = time.Now()
|
||||
}
|
||||
if wallet.UpdateTime.IsZero() {
|
||||
wallet.UpdateTime = time.Now()
|
||||
}
|
||||
if wallet.Version == 0 {
|
||||
wallet.Version = 1
|
||||
}
|
||||
return mongoutil.InsertOne(ctx, w.coll, wallet)
|
||||
}
|
||||
|
||||
// Take retrieves a wallet by user ID. Returns an error if not found.
|
||||
func (w *WalletMgo) Take(ctx context.Context, userID string) (*model.Wallet, error) {
|
||||
return mongoutil.FindOne[*model.Wallet](ctx, w.coll, bson.M{"user_id": userID})
|
||||
}
|
||||
|
||||
// UpdateBalance updates the balance of a wallet.
|
||||
func (w *WalletMgo) UpdateBalance(ctx context.Context, userID string, balance int64) error {
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"balance": balance,
|
||||
"update_time": time.Now(),
|
||||
},
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, w.coll, bson.M{"user_id": userID}, update, false)
|
||||
}
|
||||
|
||||
// UpdateBalanceByAmount updates the balance by adding/subtracting an amount.
|
||||
func (w *WalletMgo) UpdateBalanceByAmount(ctx context.Context, userID string, amount int64) error {
|
||||
update := bson.M{
|
||||
"$inc": bson.M{
|
||||
"balance": amount,
|
||||
},
|
||||
"$set": bson.M{
|
||||
"update_time": time.Now(),
|
||||
},
|
||||
}
|
||||
return mongoutil.UpdateOne(ctx, w.coll, bson.M{"user_id": userID}, update, false)
|
||||
}
|
||||
|
||||
// UpdateBalanceWithVersion 使用版本号更新余额(防止并发覆盖)
|
||||
func (w *WalletMgo) UpdateBalanceWithVersion(ctx context.Context, params *database.WalletUpdateParams) (*database.WalletUpdateResult, error) {
|
||||
// 基于单文档原子操作,避免并发覆盖
|
||||
if params.Amount < 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("amount cannot be negative")
|
||||
}
|
||||
|
||||
// 兼容旧数据(无 version 字段或 version=0)
|
||||
filter := bson.M{
|
||||
"user_id": params.UserID,
|
||||
"$or": []bson.M{
|
||||
{"version": params.OldVersion},
|
||||
{"version": bson.M{"$exists": false}},
|
||||
{"version": 0},
|
||||
},
|
||||
}
|
||||
|
||||
// 默认更新:更新时间、版本号自增
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"update_time": time.Now(),
|
||||
},
|
||||
"$inc": bson.M{
|
||||
"version": 1,
|
||||
},
|
||||
}
|
||||
|
||||
switch params.Operation {
|
||||
case "add":
|
||||
update["$inc"].(bson.M)["balance"] = params.Amount
|
||||
case "subtract":
|
||||
update["$inc"].(bson.M)["balance"] = -params.Amount
|
||||
// 防止出现负数,过滤条件要求当前余额 >= amount
|
||||
filter["balance"] = bson.M{"$gte": params.Amount}
|
||||
case "set":
|
||||
// 直接设置余额,同时递增版本
|
||||
delete(update, "$inc")
|
||||
update["$set"].(bson.M)["balance"] = params.Amount
|
||||
update["$set"].(bson.M)["version"] = params.OldVersion + 1
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid operation: " + params.Operation)
|
||||
}
|
||||
|
||||
// 如果 set 分支删除了 $inc,需要补上版本递增
|
||||
if _, ok := update["$inc"]; !ok {
|
||||
update["$inc"] = bson.M{"version": 1}
|
||||
} else if _, ok := update["$inc"].(bson.M)["version"]; !ok {
|
||||
update["$inc"].(bson.M)["version"] = 1
|
||||
}
|
||||
|
||||
// 使用 findOneAndUpdate 返回更新后的文档
|
||||
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
|
||||
var updatedWallet model.Wallet
|
||||
err := w.coll.FindOneAndUpdate(ctx, filter, update, opts).Decode(&updatedWallet)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
// 版本号或余额不匹配,说明有并发修改
|
||||
return nil, errs.ErrInternalServer.WrapMsg("concurrent modification detected: version or balance mismatch")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &database.WalletUpdateResult{
|
||||
NewBalance: updatedWallet.Balance,
|
||||
NewVersion: updatedWallet.Version,
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FindAllWallets finds all wallets with pagination.
|
||||
func (w *WalletMgo) FindAllWallets(ctx context.Context, pagination pagination.Pagination) (total int64, wallets []*model.Wallet, err error) {
|
||||
return mongoutil.FindPage[*model.Wallet](ctx, w.coll, bson.M{}, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "create_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
|
||||
// FindWalletsByUserIDs finds wallets by user IDs.
|
||||
func (w *WalletMgo) FindWalletsByUserIDs(ctx context.Context, userIDs []string) ([]*model.Wallet, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return []*model.Wallet{}, nil
|
||||
}
|
||||
filter := bson.M{"user_id": bson.M{"$in": userIDs}}
|
||||
return mongoutil.Find[*model.Wallet](ctx, w.coll, filter)
|
||||
}
|
||||
|
||||
// WalletBalanceRecordMgo implements WalletBalanceRecord using MongoDB as the storage backend.
|
||||
type WalletBalanceRecordMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
// NewWalletBalanceRecordMongo creates a new instance of WalletBalanceRecordMgo with the provided MongoDB database.
|
||||
func NewWalletBalanceRecordMongo(db *mongo.Database) (database.WalletBalanceRecord, error) {
|
||||
coll := db.Collection(database.WalletBalanceRecordName)
|
||||
|
||||
// 先尝试删除可能存在的旧 record_id 索引(如果不是稀疏索引)
|
||||
// 忽略删除失败的错误(索引可能不存在)
|
||||
_, _ = coll.Indexes().DropOne(context.Background(), "record_id_1")
|
||||
|
||||
// 创建索引,使用稀疏索引允许 record_id 为 null
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "create_time", Value: -1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "record_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "create_time", Value: -1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WalletBalanceRecordMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
// Create creates a new wallet balance record.
|
||||
func (w *WalletBalanceRecordMgo) Create(ctx context.Context, record *model.WalletBalanceRecord) error {
|
||||
if record.CreateTime.IsZero() {
|
||||
record.CreateTime = time.Now()
|
||||
}
|
||||
return mongoutil.InsertOne(ctx, w.coll, record)
|
||||
}
|
||||
|
||||
// FindByUserID finds all balance records for a user with pagination.
|
||||
func (w *WalletBalanceRecordMgo) FindByUserID(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, records []*model.WalletBalanceRecord, err error) {
|
||||
filter := bson.M{"user_id": userID}
|
||||
return mongoutil.FindPage[*model.WalletBalanceRecord](ctx, w.coll, filter, pagination, &options.FindOptions{
|
||||
Sort: bson.D{{Key: "create_time", Value: -1}},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user