You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
546 lines
15 KiB
546 lines
15 KiB
package mgo
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
|
"github.com/openimsdk/tools/errs"
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
)
|
|
|
|
// ---- RedPacket ----
|
|
|
|
type RedPacketMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewRedPacketMongo(db *mongo.Database) (database.RedPacket, error) {
|
|
coll := db.Collection("red_packet")
|
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
|
{
|
|
Keys: bson.D{{Key: "biz_id", Value: 1}},
|
|
Options: options.Index().SetUnique(true),
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "packet_id", Value: 1}},
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "group_id", Value: 1}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &RedPacketMgo{coll: coll}, nil
|
|
}
|
|
|
|
func (m *RedPacketMgo) Create(ctx context.Context, rp *model.RedPacket) error {
|
|
_, err := m.coll.InsertOne(ctx, rp)
|
|
return err
|
|
}
|
|
|
|
func (m *RedPacketMgo) GetByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
|
|
var rp model.RedPacket
|
|
err := m.coll.FindOne(ctx, bson.M{"biz_id": bizID}).Decode(&rp)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("red packet not found", "bizID", bizID)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &rp, nil
|
|
}
|
|
|
|
func (m *RedPacketMgo) GetByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
|
|
var rp model.RedPacket
|
|
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&rp)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &rp, nil
|
|
}
|
|
|
|
func (m *RedPacketMgo) UpdateCreated(ctx context.Context, rp *model.RedPacket) error {
|
|
updates := bson.M{
|
|
"chain_type": rp.ChainType,
|
|
"packet_id": rp.PacketID,
|
|
"tx_hash": rp.TxHash,
|
|
"chain_id": rp.ChainID,
|
|
"contract_address": rp.ContractAddress,
|
|
"creator_wallet": rp.CreatorWallet,
|
|
"packet_type": rp.PacketType,
|
|
"token": rp.Token,
|
|
"total_amount": rp.TotalAmount,
|
|
"total_shares": rp.TotalShares,
|
|
"expiry_at": rp.ExpiryAt,
|
|
"group_id": rp.GroupID,
|
|
"scope_type": rp.ScopeType,
|
|
"receiver_user_id": rp.ReceiverUserID,
|
|
"receiver_user_ids": rp.ReceiverUserIDs,
|
|
"status": rp.Status,
|
|
"updated_at": time.Now(),
|
|
}
|
|
res, err := m.coll.UpdateOne(ctx, bson.M{"biz_id": rp.BizID}, bson.M{"$set": updates})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.MatchedCount == 0 {
|
|
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "bizID", rp.BizID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *RedPacketMgo) UpdateStatus(ctx context.Context, packetID, status string) error {
|
|
res, err := m.coll.UpdateOne(ctx, bson.M{"packet_id": packetID},
|
|
bson.M{"$set": bson.M{"status": status, "updated_at": time.Now()}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.MatchedCount == 0 {
|
|
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *RedPacketMgo) UpdateClaimProgress(ctx context.Context, packetID, claimedAmount, status, claimTxHash string) error {
|
|
var rp model.RedPacket
|
|
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&rp)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
|
|
}
|
|
return err
|
|
}
|
|
|
|
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
|
|
nextShares := rp.ClaimedShares + 1
|
|
|
|
// Auto-derive status when the caller does not force one.
|
|
nextStatus := status
|
|
if nextStatus == "" {
|
|
if rp.PacketType == 2 {
|
|
nextStatus = "COMPLETED"
|
|
} else if rp.TotalShares > 0 && nextShares >= rp.TotalShares {
|
|
nextStatus = "COMPLETED"
|
|
} else {
|
|
tcBig, tok := new(big.Int).SetString(totalClaimed, 10)
|
|
taBig, taok := new(big.Int).SetString(rp.TotalAmount, 10)
|
|
if tok && taok && tcBig.Cmp(taBig) >= 0 {
|
|
nextStatus = "COMPLETED"
|
|
}
|
|
}
|
|
}
|
|
|
|
setFields := bson.M{
|
|
"claimed_amount": totalClaimed,
|
|
"claimed_shares": nextShares,
|
|
"updated_at": time.Now(),
|
|
}
|
|
if nextStatus != "" {
|
|
setFields["status"] = nextStatus
|
|
}
|
|
|
|
// The $addToSet + $ne filter makes the whole update idempotent per claimTxHash:
|
|
// if two code paths (RPC handler and indexer) both attempt to process the same
|
|
// transaction, only the first UpdateOne will match and the second is a no-op.
|
|
filter := bson.M{"packet_id": packetID}
|
|
if claimTxHash != "" {
|
|
filter["processed_claim_hashes"] = bson.M{"$ne": claimTxHash}
|
|
}
|
|
update := bson.M{"$set": setFields}
|
|
if claimTxHash != "" {
|
|
update["$addToSet"] = bson.M{"processed_claim_hashes": claimTxHash}
|
|
}
|
|
|
|
_, err = m.coll.UpdateOne(ctx, filter, update)
|
|
return err
|
|
}
|
|
|
|
func addNumericStrings(current, delta string) string {
|
|
left := new(big.Int)
|
|
if current != "" {
|
|
left.SetString(current, 10)
|
|
}
|
|
right := new(big.Int)
|
|
if delta != "" {
|
|
right.SetString(delta, 10)
|
|
}
|
|
return new(big.Int).Add(left, right).String()
|
|
}
|
|
|
|
// ---- RedPacketClaim ----
|
|
|
|
type RedPacketClaimMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewRedPacketClaimMongo(db *mongo.Database) (database.RedPacketClaim, error) {
|
|
coll := db.Collection("red_packet_claim")
|
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
|
{
|
|
Keys: bson.D{{Key: "claim_tx_hash", Value: 1}},
|
|
Options: options.Index().SetUnique(true),
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "user_id", Value: 1}},
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "claimer_wallet", Value: 1}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &RedPacketClaimMgo{coll: coll}, nil
|
|
}
|
|
|
|
func (m *RedPacketClaimMgo) Save(ctx context.Context, claim *model.RedPacketClaim) error {
|
|
if claim.UserID != "" {
|
|
var existing model.RedPacketClaim
|
|
err := m.coll.FindOne(ctx, bson.M{
|
|
"packet_id": claim.PacketID,
|
|
"user_id": claim.UserID,
|
|
}).Decode(&existing)
|
|
if err == nil {
|
|
updates := bson.M{
|
|
"claimer_wallet": claim.ClaimerWallet,
|
|
"auth_nonce": claim.AuthNonce,
|
|
"claim_tx_hash": claim.ClaimTxHash,
|
|
"claimed_amount": claim.ClaimedAmount,
|
|
"block_number": claim.BlockNumber,
|
|
"status": claim.Status,
|
|
"updated_at": claim.UpdatedAt,
|
|
}
|
|
_, err := m.coll.UpdateOne(ctx,
|
|
bson.M{"packet_id": claim.PacketID, "user_id": claim.UserID},
|
|
bson.M{"$set": updates})
|
|
return err
|
|
}
|
|
if err != mongo.ErrNoDocuments {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err := m.coll.UpdateOne(ctx,
|
|
bson.M{"claim_tx_hash": claim.ClaimTxHash},
|
|
bson.M{"$set": claim},
|
|
options.Update().SetUpsert(true),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (m *RedPacketClaimMgo) GetByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
|
|
var claim model.RedPacketClaim
|
|
err := m.coll.FindOne(ctx,
|
|
bson.M{"packet_id": packetID, "claimer_wallet": claimer},
|
|
options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
|
).Decode(&claim)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("claim not found", "packetID", packetID, "claimer", claimer)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &claim, nil
|
|
}
|
|
|
|
func (m *RedPacketClaimMgo) GetByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
|
|
var claim model.RedPacketClaim
|
|
err := m.coll.FindOne(ctx,
|
|
bson.M{"packet_id": packetID, "user_id": userID},
|
|
options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
|
).Decode(&claim)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("claim not found", "packetID", packetID, "userID", userID)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &claim, nil
|
|
}
|
|
|
|
func (m *RedPacketClaimMgo) ListByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error) {
|
|
cursor, err := m.coll.Find(ctx,
|
|
bson.M{"packet_id": packetID},
|
|
options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var claims []*model.RedPacketClaim
|
|
if err := cursor.All(ctx, &claims); err != nil {
|
|
return nil, err
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
// ---- RedPacketClaimAuth ----
|
|
|
|
type RedPacketClaimAuthMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewRedPacketClaimAuthMongo(db *mongo.Database) (database.RedPacketClaimAuth, error) {
|
|
coll := db.Collection("red_packet_claim_auth")
|
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
|
{
|
|
Keys: bson.D{{Key: "auth_nonce", Value: 1}},
|
|
Options: options.Index().SetUnique(true),
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "claimer", Value: 1}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &RedPacketClaimAuthMgo{coll: coll}, nil
|
|
}
|
|
|
|
func (m *RedPacketClaimAuthMgo) Create(ctx context.Context, auth *model.RedPacketClaimAuth) error {
|
|
_, err := m.coll.InsertOne(ctx, auth)
|
|
return err
|
|
}
|
|
|
|
func (m *RedPacketClaimAuthMgo) Get(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
|
|
var auth model.RedPacketClaimAuth
|
|
err := m.coll.FindOne(ctx, bson.M{
|
|
"packet_id": packetID,
|
|
"claimer": claimer,
|
|
"used": false,
|
|
}).Decode(&auth)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("claim auth not found", "packetID", packetID, "claimer", claimer)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &auth, nil
|
|
}
|
|
|
|
func (m *RedPacketClaimAuthMgo) MarkUsed(ctx context.Context, authNonce string) error {
|
|
res, err := m.coll.UpdateOne(ctx,
|
|
bson.M{"auth_nonce": authNonce},
|
|
bson.M{"$set": bson.M{"used": true}},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.MatchedCount == 0 {
|
|
return errs.ErrRecordNotFound.WrapMsg("claim auth not found", "authNonce", authNonce)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---- RedPacketRefund ----
|
|
|
|
type RedPacketRefundMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewRedPacketRefundMongo(db *mongo.Database) (database.RedPacketRefund, error) {
|
|
coll := db.Collection("red_packet_refund")
|
|
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
|
Keys: bson.D{{Key: "tx_hash", Value: 1}},
|
|
Options: options.Index().SetUnique(true),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &RedPacketRefundMgo{coll: coll}, nil
|
|
}
|
|
|
|
func (m *RedPacketRefundMgo) Save(ctx context.Context, refund *model.RedPacketRefund) error {
|
|
_, err := m.coll.UpdateOne(ctx,
|
|
bson.M{"tx_hash": refund.TxHash},
|
|
bson.M{"$setOnInsert": refund},
|
|
options.Update().SetUpsert(true),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (m *RedPacketRefundMgo) GetByPacketID(ctx context.Context, packetID string) (*model.RedPacketRefund, error) {
|
|
var r model.RedPacketRefund
|
|
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&r)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("refund not found", "packetID", packetID)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// ---- WalletBindingChallenge ----
|
|
|
|
type WalletBindingChallengeMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewWalletBindingChallengeMongo(db *mongo.Database) (database.WalletBindingChallenge, error) {
|
|
coll := db.Collection("wallet_binding_challenge")
|
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
|
{
|
|
Keys: bson.D{{Key: "challenge_id", Value: 1}},
|
|
Options: options.Index().SetUnique(true),
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "user_id", Value: 1}},
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "wallet_address", Value: 1}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &WalletBindingChallengeMgo{coll: coll}, nil
|
|
}
|
|
|
|
func (m *WalletBindingChallengeMgo) Create(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
|
_, err := m.coll.InsertOne(ctx, challenge)
|
|
return err
|
|
}
|
|
|
|
func (m *WalletBindingChallengeMgo) Get(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
|
|
var c model.WalletBindingChallenge
|
|
err := m.coll.FindOne(ctx, bson.M{"challenge_id": challengeID}).Decode(&c)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("wallet binding challenge not found", "challengeID", challengeID)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func (m *WalletBindingChallengeMgo) Update(ctx context.Context, c *model.WalletBindingChallenge) error {
|
|
updates := bson.M{
|
|
"status": c.Status,
|
|
"signature": c.Signature,
|
|
"verified_at": c.VerifiedAt,
|
|
"updated_at": c.UpdatedAt,
|
|
}
|
|
res, err := m.coll.UpdateOne(ctx, bson.M{"challenge_id": c.ChallengeID}, bson.M{"$set": updates})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.MatchedCount == 0 {
|
|
return errs.ErrRecordNotFound.WrapMsg("wallet binding challenge not found", "challengeID", c.ChallengeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---- WalletBinding ----
|
|
|
|
type WalletBindingMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewWalletBindingMongo(db *mongo.Database) (database.WalletBinding, error) {
|
|
coll := db.Collection("wallet_binding")
|
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
|
{
|
|
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "chain_type", Value: 1}, {Key: "wallet_address", Value: 1}},
|
|
Options: options.Index().SetUnique(true),
|
|
},
|
|
{
|
|
Keys: bson.D{{Key: "user_id", Value: 1}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &WalletBindingMgo{coll: coll}, nil
|
|
}
|
|
|
|
// GetExpiredPending returns red packets that have expired but are still in
|
|
// "ACTIVE" status (i.e., on-chain creation confirmed, not yet fully claimed or refunded).
|
|
func (m *RedPacketMgo) GetExpiredPending(ctx context.Context, now int64) ([]*model.RedPacket, error) {
|
|
cur, err := m.coll.Find(ctx, bson.M{
|
|
"status": "ACTIVE",
|
|
"expiry_at": bson.M{"$lt": now, "$gt": 0},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cur.Close(ctx)
|
|
var out []*model.RedPacket
|
|
if err := cur.All(ctx, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *WalletBindingMgo) Upsert(ctx context.Context, b *model.WalletBinding) error {
|
|
filter := bson.M{
|
|
"user_id": b.UserID,
|
|
"chain_type": b.ChainType,
|
|
"wallet_address": b.WalletAddress,
|
|
}
|
|
updates := bson.M{
|
|
"chain_id": b.ChainID,
|
|
"status": b.Status,
|
|
"challenge_id": b.ChallengeID,
|
|
"verified_at": b.VerifiedAt,
|
|
"revoked_at": b.RevokedAt,
|
|
"updated_at": b.UpdatedAt,
|
|
}
|
|
setOnInsert := bson.M{
|
|
"created_at": b.CreatedAt,
|
|
}
|
|
_, err := m.coll.UpdateOne(ctx, filter,
|
|
bson.M{"$set": updates, "$setOnInsert": setOnInsert},
|
|
options.Update().SetUpsert(true),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (m *WalletBindingMgo) GetActive(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
|
|
var b model.WalletBinding
|
|
err := m.coll.FindOne(ctx, bson.M{
|
|
"user_id": userID,
|
|
"chain_type": chainType,
|
|
"wallet_address": walletAddress,
|
|
"status": "ACTIVE",
|
|
}).Decode(&b)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, errs.ErrRecordNotFound.WrapMsg("active wallet binding not found", "userID", userID, "chainType", chainType, "walletAddress", walletAddress)
|
|
}
|
|
return nil, err
|
|
}
|
|
return &b, nil
|
|
}
|
|
|
|
// ---- AdminAuditLog ----
|
|
|
|
type AdminAuditLogMgo struct {
|
|
coll *mongo.Collection
|
|
}
|
|
|
|
func NewAdminAuditLogMongo(db *mongo.Database) (database.AdminAuditLog, error) {
|
|
coll := db.Collection("admin_audit_log")
|
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
|
{Keys: bson.D{{Key: "operator_id", Value: 1}}},
|
|
{Keys: bson.D{{Key: "created_at", Value: -1}}},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AdminAuditLogMgo{coll: coll}, nil
|
|
}
|
|
|
|
func (m *AdminAuditLogMgo) Create(ctx context.Context, entry *model.AdminAuditLog) error {
|
|
_, err := m.coll.InsertOne(ctx, entry)
|
|
return err
|
|
}
|