用MongoDB构建交易系统

交易系统是现代商业和金融活动的核心,涵盖从电子商务订单处理到金融机构的实时清算等多种场景。这类系统通常需要高并发处理能力、实时数据存储和高效检索功能,同时需要在数据一致性与性能之间找到平衡。随着交易数据的规模和复杂性不断增长,传统交易系统架构在应对这些挑战时,往往因固定的表结构和横向扩展能力的不足而受限。

MongoDB 作为一款分布式文档型数据库,以其灵活的架构、高吞吐能力和内置的事务支持,为构建复杂、高效的交易系统提供了一种现代化的解决方案,能够满足多样化的业务需求。

问题描述与挑战

交易系统上的核心功能包括账户管理、余额查询、充值、退款以及交易记录的管理和查询。如下图所示,显示用户的账户余额:

以及显示用户的交易记录:

构建一个高效、稳定的现代交易系统面临诸多挑战,主要体现在以下几个方面:

  1. 高并发性
    交易系统需要能够处理大规模的并发请求,尤其是在金融领域和电子商务中,用户同时进行交易、查询余额和其他操作的场景非常普遍。如果系统无法承载高并发,可能会导致响应延迟甚至系统崩溃。

  2. 数据一致性与事务管理
    交易系统必须确保数据的一致性,尤其是涉及账户余额、交易记录和退款等敏感操作时。一旦数据不一致,可能会引发财务风险和用户信任问题。分布式环境下的事务管理进一步加大了实现难度。

  3. 灵活的数据建模
    传统关系型数据库通常使用固定表结构,难以应对交易系统中复杂多变的业务逻辑和频繁变化的需求。例如,新增业务功能可能需要调整数据模型,这会导致系统修改成本高昂,甚至中断服务。

  4. 实时数据处理与查询性能
    在海量交易数据的背景下,系统需要能够高效存储、检索并实时处理用户的查询请求,例如账户余额查询和历史交易明细的检索。这要求底层数据库在提供高吞吐能力的同时,也要有极快的查询性能。

  5. 系统可扩展性
    随着业务的增长,交易系统的数据规模会持续增加。这就要求系统具备良好的横向扩展能力,以确保在扩展服务器或存储节点时,性能能够线性提升。

数据建模

为达到大规模数据存储和高并发读写的目的,数据建模是问题的关键。MongoDB 的文档模型以灵活和高效著称,可以轻松适应复杂多变的业务需求。

交易系统的核心功能,包括以下 6 个 API:

  1. api/CreateAccount
    创建用户账户,包括初始化用户基本信息和初始余额。

  2. api/GetBalance
    查询账户余额,需要实时返回准确的数据,确保一致性。

  3. api/GetTransactionById
    根据交易 ID 查询具体交易详情,支持快速精准的查询。

  4. api/GetTransactions
    根据账户 ID 获取历史交易记录,支持分页和时间范围过滤,便于用户查询大规模数据。

  5. api/Recharge
    用户账户充值操作,涉及事务处理,确保账户余额更新和交易记录的同步。

  6. api/Refund
    退款功能,涉及事务处理,并记录每次操作的详情。

针对上述需求,设计两张表存储用户的资金状态和资金变动的详细信息。两张表通过 Uuid 关联交易,实现高效查询和更新操作。

  1. AccountEntry表示系统中每个用户的账户基本信息,主要包含以下字段:
  • Uuid (string)
    每个账户的唯一标识符,用于在系统中唯一定位一个账户,全局唯一。

  • Balance (decimal)
    账户当前的余额,精确到小数,用于记录用户可用的资金总额。

代码定义如下:

public class AccountEntry
{
    public string Uuid { get; set; }
    public decimal Balance { get; set; }
    public DateTime CreateTime { get; set; }
    public DateTime UpdateTime { get; set; }
}
  1. TransEntry记录系统中的每一笔交易,支持不同类型的操作,其字段描述如下:
  • Tid (string)
    每笔交易的唯一标识符,唯一定位某一笔具体交易。

  • Uuid (string)
    对应账户的唯一标识符,作为外键关联到 AccountEntry,表示这笔交易属于哪个账户。

  • Amount (decimal)
    交易金额,记录本次交易涉及的资金数额。

  • NewBalance (decimal)
    交易完成后的账户余额快照,便于追踪资金流动,提供账单审计功能。

  • Type (TransType)
    交易类型枚举,表示交易的性质。

代码定义如下:

public class TransEntry
{
    public string Tid { get; set; }
    public string Uuid { get; set; }
    public decimal Amount { get; set; }
    public decimal NewBalance { get; set; }
    public TransType Type { get; set; }
    public DateTime CreateTime { get; set; }
    public DateTime UpdateTime { get; set; }
}

public enum TransType
{
    None = 0,
    Recharge = 1,
    Refund = 2,
}

系统实现

这里具体实现各个API,并通过实际的读取方式来确定所需要的索引和片键。由简单到复杂,逐一介绍。

CreateAccount API

创建账户非常简单,此处并未使用事务处理,原因在于用户的uuid是主键,如果同样uuid的账户存在,则会触发插入失败:

public async Task<AccountEntry> CreateAccountAsync(string uuid, CancellationToken cancellationToken = default)
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
    {
        var newAccount = new AccountEntry
        {
            Uuid = uuid,
            Balance = 0,
            CreateTime = DateTime.UtcNow,
            UpdateTime = DateTime.UtcNow
        };

        await _accountCollection.InsertOneAsync(newAccount, cancellationToken: cts.Token);

        return newAccount;
    }
}

GetBalance API

获取余额的实现很直观,需要注意的是此处IClientSessionHandle是可选参数,当传入该参数时,可实现复杂事务:

public async Task<decimal> GetBalanceAsync(string uuid, CancellationToken ct, IClientSessionHandle s = null)
{
    AccountEntry account;
    if (s == null)
    {
        account = await _accountCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
    }
    else
    {
        account = await _accountCollection.Find(s, x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
    }
    if (account == null)
    {
        throw new KeyNotFoundException($"Account '{uuid}' does not exist.");
    }

    return account.Balance;
}

此API需要Account表索引:(Uuid)。

AddBalance与DecreaseBalance API

对于余额的操作也是构建后续交易API的基础,这两个API的实现使用MongoDB的Inc原语进行原子操作,同时支持事务。它们不会被单独使用,因为增加或扣减余额都需要有对应的交易记录,因此,IClientSessionHandle是必选参数:

private async Task<AccountEntry> AddBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
{
    if (amount <= 0)
    {
        throw new ArgumentException("Amount should > 0");
    }
    var filter = Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId);
    var update = Builders<AccountEntry>.Update
        .Inc(x => x.Balance, amount)
        .Set(x => x.UpdateTime, DateTime.UtcNow);
    var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After, IsUpsert = true };
    return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
}

private async Task<AccountEntry> DecreaseBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
{
    if (amount <= 0)
    {
        throw new ArgumentException("Amount should > 0");
    }
    var filter = Builders<AccountEntry>.Filter.And(
        Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId),
        Builders<AccountEntry>.Filter.Gte(x => x.Balance, amount)
    );
    var update = Builders<AccountEntry>.Update
        .Inc(x => x.Balance, -amount)
        .Set(x => x.UpdateTime, DateTime.UtcNow);
    var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After };
    return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
}

值得一提的是,在我们的设计中,这两个API中的amount都是正数,好处是在扣减余额可以加入Filter.Gte(x => x.Balance, amount),进一步保证扣除余额的正确性。

这两个API需要Account表索引:(Uuid)。

GetTransactionByTid与GetTransactions API

这两个API可以获取交易记录,非常简单:

public async Task<TransEntry> GetTransactionByTidAsync(string tid, CancellationToken ct, IClientSessionHandle s = null)
{
    if (s == null)
    {
        return await _transCollection.Find(x => x.Tid == tid).FirstOrDefaultAsync(ct);
    }
    return await _transCollection.Find(s, x => x.Tid == tid).FirstOrDefaultAsync(ct);
}

public async Task<List<TransEntry>> GetTransactionsAsync(string accountId, CancellationToken ct, int n = 10)
{
    var result = await _transCollection.Find(x => x.Uuid == accountId).SortBy(x => x.CreateTime).Limit(n).ToListAsync(ct);
    return result;
}

这两个API需要Trans表索引:(Uuid, CreateTime),(Tid)。

Recharge API

下面到了最复杂的两个交易API:Recharge与Refund。注意这两个API除了保证一致性,还需要保证幂等性,也就是说支持多次重复调用,不影响最终结果。

对于Recharge,其中涉及三个操作,需要使用事务:

  • GetTransactionByTid: 调用方需要传入Tid,防止重复交易,幂等性由这步操作进行保证。实际应用中,该Tid可以使用第三方金融系统返回的id作为唯一标识。
  • AddBalance:前面已经实现了该API。
  • InsertTransaction:增加一条充值交易记录。

上述三个操作的任意一步失败,都可以利用MongoDB的事务进行回滚,从而保证交易的强一致性。

public async Task<TransResult> RechargeAsync(string accountId, string tid, decimal amount)
{
    using (var session = await _mongoClient.StartSessionAsync())
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
    {
        return await session.WithTransactionAsync(
            async (s, ct) =>
            {
                var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
                if (existingTransEntry != null)
                {
                    return new TransResult { Code = TransCode.DuplicateTrans };
                }

                var newBalanceEntry = await AddBalanceAsync(accountId, amount, ct, s);

                var newTransEntry = new TransEntry
                {
                    Tid = GenerateTid(),
                    Uuid = accountId,
                    Amount = amount,
                    NewBalance = newBalanceEntry.Balance,
                    Type = TransType.Recharge,
                    CreateTime = DateTime.UtcNow,
                    UpdateTime = DateTime.UtcNow,
                };

                await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);

                return new TransResult
                {
                    NewBalance = newTransEntry.NewBalance,
                    Tid = newTransEntry.Tid
                };
            }, cancellationToken: cts.Token);
    }
}

Refund API

Refund API的实现与Recharge基本相同,把AddBalance换为DecreaseBalance

public async Task<TransResult> RefundAsync(string accountId, string tid, decimal amount)
{
    using (var session = await _mongoClient.StartSessionAsync())
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
    {
        return await session.WithTransactionAsync(
            async (s, ct) =>
            {
                var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
                if (existingTransEntry != null)
                {
                    return new TransResult { Code = TransCode.DuplicateTrans };
                }

                var newBalanceEntry = await DecreaseBalanceAsync(accountId, amount, ct, s);
                if (newBalanceEntry == null)
                {
                    return new TransResult { Code = TransCode.InsufficientBalance };
                }

                var newTransEntry = new TransEntry
                {
                    Tid = GenerateTid(),
                    Uuid = accountId,
                    Amount = amount,
                    NewBalance = newBalanceEntry.Balance,
                    Type = TransType.Refund,
                    CreateTime = DateTime.UtcNow,
                    UpdateTime = DateTime.UtcNow,
                };

                await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);

                return new TransResult
                {
                    NewBalance = newTransEntry.NewBalance,
                    Tid = newTransEntry.Tid
                };
            },
            cancellationToken: cts.Token);
    }
}

在实践中,可以通过修改Refund API,在其中加入业务逻辑,实现业务逻辑与交易系统的一致性。

索引与片键

Account表需要索引:

  • (Uuid)

Trans表需要索引:

  • (Uuid, CreateTime)
  • (Tid)

测试代码

创建 10 个账户,并为每个账户进行初始充值 1000。对每个账户生成 5 笔交易,随机决定交易类型(充值或退款),交易金额在 0~300 之间。 打印每个账户的当前余额,以及最近10笔交易的详情。

public async Task RunAsync()
{
    var random = new Random(42);

    // Create 10 accounts
    var accountIds = new List<string>();
    for (int i = 0; i < 10; i++)
    {
        var accountId = GenerateAccountUuid();
        var account = await CreateAccountAsync(accountId);
        accountIds.Add(account.Uuid);

        await RechargeAsync(account.Uuid, GenerateTid(), 1000);
        Console.WriteLine($"Created account {account.Uuid} with initial balance of 1000.");
    }

    Console.WriteLine("------------------------------------------------------------------------------");

    foreach (var accountId in accountIds)
    {
        for (int i = 0; i < 5; i++)
        {
            var isRecharge = random.Next(0, 2) == 0;
            var amount = (decimal)(random.NextDouble() * 300);
            var tid = GenerateTid();

            if (isRecharge)
            {
                var result = await RechargeAsync(accountId, tid, amount);
                if (result.Code == TransCode.Success)
                {
                    Console.WriteLine($"Recharge: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
                }
                else
                {
                    Console.WriteLine($"Recharge failed: {result.Code}");
                }
            }
            else
            {
                var result = await RefundAsync(accountId, tid, amount);
                if (result.Code == TransCode.Success)
                {
                    Console.WriteLine($"Refund: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
                }
                else
                {
                    Console.WriteLine($"Refund failed: {result.Code}");
                }
            }
        }
    }

    Console.WriteLine("------------------------------------------------------------------------------");
    foreach (var accountId in accountIds)
    {
        var balance = await GetBalanceAsync(accountId, CancellationToken.None);
        var transactions = await GetTransactionsAsync(accountId, CancellationToken.None);

        Console.WriteLine($"Account {accountId} Balance: {Math.Round(balance, 2)}");
        Console.WriteLine("Recent Transactions:");
        foreach (var transaction in transactions)
        {
            Console.WriteLine($"Tid: {transaction.Tid}, Type: {transaction.Type}, Amount: {Math.Round(transaction.Amount, 2)}, New Balance: {Math.Round(transaction.NewBalance, 2)}, Time: {transaction.CreateTime}");
        }
        Console.WriteLine("****************************************************************************");
    }
}

部分运行结果:

Account Acc_BE2E2B2FD3A2490CBEEA9625C3BD7C18 Balance: 1267.83 Recent Transactions: Tid: T_6DA267BECAC945E2973903C7EE36C599, Type: Recharge, Amount: 1000, New Balance: 1000, Time: 11/24/2024 4:00:42 AM Tid: T_6BB3C3B2E9CD4197B609957C2937D218, Type: Refund, Amount: 42.27, New Balance: 957.73, Time: 11/24/2024 4:00:43 AM Tid: T_0D63FB52BCBF4FF382D7C611252329F4, Type: Recharge, Amount: 156.83, New Balance: 1114.56, Time: 11/24/2024 4:00:43 AM Tid: T_30201F58B3AA4BD6B14C4B9DC0A20272, Type: Recharge, Amount: 78.78, New Balance: 1193.33, Time: 11/24/2024 4:00:43 AM Tid: T_42837124561748BFBECA1677FD951338, Type: Refund, Amount: 153.88, New Balance: 1039.46, Time: 11/24/2024 4:00:43 AM Tid: T_16DC47F772814D4A8A3E1DBCF26B661D, Type: Recharge, Amount: 228.38, New Balance: 1267.83, Time: 11/24/2024 4:00:43 AM

使用MongoDB Compass连接部署在MongoDB Atlas上的DB:

本文基于 MongoDB 提供了完整的账户管理与交易支持功能,适合于中小型交易系统,易于扩展到更加复杂的应用场景,例如多货币支持、国际支付等。每个功能模块通过事务、过滤器和幂等性处理相互配合,构建了一个安全、高效且易于扩展的交易系统框架。这种设计能够满足实际业务中高并发、数据一致性和复杂交易的需求,同时提供了明确的错误处理路径,便于维护与扩展。

Demo代码

填充ConnectionString即可:

using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Driver;

namespace MongoTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var model = new TransModel();

            model.RunAsync().Wait();

            Console.WriteLine("Done");
        }


    }

    public class AccountEntry
    {
        public string Uuid { get; set; }
        public decimal Balance { get; set; }
        public DateTime CreateTime { get; set; }
        public DateTime UpdateTime { get; set; }
    }

    public class TransEntry
    {
        public string Tid { get; set; }
        public string Uuid { get; set; }
        public decimal Amount { get; set; }
        public decimal NewBalance { get; set; }
        public TransType Type { get; set; }
        public DateTime CreateTime { get; set; }
        public DateTime UpdateTime { get; set; }
    }

    public enum TransType
    {
        None = 0,
        Recharge = 1,
        Refund = 2,
    }

    public class TransResult
    {
        public string Tid { get; set; }
        public decimal NewBalance { get; set; }
        public TransCode Code { get; set; }
    }

    public enum TransCode
    {
        Success = 0,
        DuplicateTrans = 1,
        Refund = 2,
        InsufficientBalance = 3
    }

    public class TransModel
    {
        private readonly IMongoClient _mongoClient;
        private readonly IMongoCollection<AccountEntry> _accountCollection;
        private readonly IMongoCollection<TransEntry> _transCollection;

        private const string DatabaseName = "Transaction";
        private const string AccountCollectionName = "Account";
        private const string TransCollectionName = "Trans";
        public const string ConnectionString = "";

        public TransModel()
        {
            _mongoClient = new MongoClient(ConnectionString);
            var database = _mongoClient.GetDatabase(DatabaseName);

            var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
            ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);
            _accountCollection = database.GetCollection<AccountEntry>(AccountCollectionName);
            _transCollection = database.GetCollection<TransEntry>(TransCollectionName);
        }

        public static string GenerateTid() => $"T_{Guid.NewGuid().ToString("N").ToUpper()}";
        public static string GenerateAccountUuid() => $"Acc_{Guid.NewGuid().ToString("N").ToUpper()}";

        public async Task<AccountEntry> CreateAccountAsync(string uuid, CancellationToken cancellationToken = default)
        {
            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
            {
                var newAccount = new AccountEntry
                {
                    Uuid = uuid,
                    Balance = 0,
                    CreateTime = DateTime.UtcNow,
                    UpdateTime = DateTime.UtcNow
                };

                await _accountCollection.InsertOneAsync(newAccount, cancellationToken: cts.Token);

                return newAccount;
            }
        }

        public async Task<decimal> GetBalanceAsync(string uuid, CancellationToken ct, IClientSessionHandle s = null)
        {
            AccountEntry account;
            if (s == null)
            {
                account = await _accountCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
            }
            else
            {
                account = await _accountCollection.Find(s, x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
            }
            if (account == null)
            {
                throw new KeyNotFoundException($"Account '{uuid}' does not exist.");
            }

            return account.Balance;
        }

        private async Task<AccountEntry> AddBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
        {
            if (amount <= 0)
            {
                throw new ArgumentException("Amount should > 0");
            }
            var filter = Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId);
            var update = Builders<AccountEntry>.Update
                .Inc(x => x.Balance, amount)
                .Set(x => x.UpdateTime, DateTime.UtcNow);
            var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After, IsUpsert = true };
            return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
        }

        private async Task<AccountEntry> DecreaseBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
        {
            if (amount <= 0)
            {
                throw new ArgumentException("Amount should > 0");
            }
            var filter = Builders<AccountEntry>.Filter.And(
                Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId),
                Builders<AccountEntry>.Filter.Gte(x => x.Balance, amount)
            );
            var update = Builders<AccountEntry>.Update
                .Inc(x => x.Balance, -amount)
                .Set(x => x.UpdateTime, DateTime.UtcNow);
            var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After };
            return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
        }

        public async Task<TransEntry> GetTransactionByTidAsync(string tid, CancellationToken ct, IClientSessionHandle s = null)
        {
            if (s == null)
            {
                return await _transCollection.Find(x => x.Tid == tid).FirstOrDefaultAsync(ct);
            }
            return await _transCollection.Find(s, x => x.Tid == tid).FirstOrDefaultAsync(ct);
        }

        public async Task<List<TransEntry>> GetTransactionsAsync(string accountId, CancellationToken ct, int n = 10)
        {
            var result = await _transCollection.Find(x => x.Uuid == accountId).SortBy(x => x.CreateTime).Limit(n).ToListAsync(ct);
            return result;
        }

        public async Task<TransResult> RechargeAsync(string accountId, string tid, decimal amount)
        {
            using (var session = await _mongoClient.StartSessionAsync())
            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
            {
                return await session.WithTransactionAsync(
                    async (s, ct) =>
                    {
                        var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
                        if (existingTransEntry != null)
                        {
                            return new TransResult { Code = TransCode.DuplicateTrans };
                        }

                        var newBalanceEntry = await AddBalanceAsync(accountId, amount, ct, s);

                        var newTransEntry = new TransEntry
                        {
                            Tid = GenerateTid(),
                            Uuid = accountId,
                            Amount = amount,
                            NewBalance = newBalanceEntry.Balance,
                            Type = TransType.Recharge,
                            CreateTime = DateTime.UtcNow,
                            UpdateTime = DateTime.UtcNow,
                        };

                        await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);

                        return new TransResult
                        {
                            NewBalance = newTransEntry.NewBalance,
                            Tid = newTransEntry.Tid
                        };
                    }, cancellationToken: cts.Token);
            }
        }

        public async Task<TransResult> RefundAsync(string accountId, string tid, decimal amount)
        {
            using (var session = await _mongoClient.StartSessionAsync())
            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
            {
                return await session.WithTransactionAsync(
                    async (s, ct) =>
                    {
                        var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
                        if (existingTransEntry != null)
                        {
                            return new TransResult { Code = TransCode.DuplicateTrans };
                        }

                        var newBalanceEntry = await DecreaseBalanceAsync(accountId, amount, ct, s);
                        if (newBalanceEntry == null)
                        {
                            return new TransResult { Code = TransCode.InsufficientBalance };
                        }

                        var newTransEntry = new TransEntry
                        {
                            Tid = GenerateTid(),
                            Uuid = accountId,
                            Amount = amount,
                            NewBalance = newBalanceEntry.Balance,
                            Type = TransType.Refund,
                            CreateTime = DateTime.UtcNow,
                            UpdateTime = DateTime.UtcNow,
                        };

                        await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);

                        return new TransResult
                        {
                            NewBalance = newTransEntry.NewBalance,
                            Tid = newTransEntry.Tid
                        };
                    },
                    cancellationToken: cts.Token);
            }
        }

        public async Task RunAsync()
        {
            var random = new Random(42);

            // Create 10 accounts
            var accountIds = new List<string>();
            for (int i = 0; i < 10; i++)
            {
                var accountId = GenerateAccountUuid();
                var account = await CreateAccountAsync(accountId);
                accountIds.Add(account.Uuid);

                await RechargeAsync(account.Uuid, GenerateTid(), 1000);
                Console.WriteLine($"Created account {account.Uuid} with initial balance of 1000.");
            }

            Console.WriteLine("------------------------------------------------------------------------------");

            foreach (var accountId in accountIds)
            {
                for (int i = 0; i < 5; i++)
                {
                    var isRecharge = random.Next(0, 2) == 0;
                    var amount = (decimal)(random.NextDouble() * 300);
                    var tid = GenerateTid();

                    if (isRecharge)
                    {
                        var result = await RechargeAsync(accountId, tid, amount);
                        if (result.Code == TransCode.Success)
                        {
                            Console.WriteLine($"Recharge: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
                        }
                        else
                        {
                            Console.WriteLine($"Recharge failed: {result.Code}");
                        }
                    }
                    else
                    {
                        var result = await RefundAsync(accountId, tid, amount);
                        if (result.Code == TransCode.Success)
                        {
                            Console.WriteLine($"Refund: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
                        }
                        else
                        {
                            Console.WriteLine($"Refund failed: {result.Code}");
                        }
                    }
                }
            }

            Console.WriteLine("------------------------------------------------------------------------------");
            foreach (var accountId in accountIds)
            {
                var balance = await GetBalanceAsync(accountId, CancellationToken.None);
                var transactions = await GetTransactionsAsync(accountId, CancellationToken.None);

                Console.WriteLine($"Account {accountId} Balance: {Math.Round(balance, 2)}");
                Console.WriteLine("Recent Transactions:");
                foreach (var transaction in transactions)
                {
                    Console.WriteLine($"Tid: {transaction.Tid}, Type: {transaction.Type}, Amount: {Math.Round(transaction.Amount, 2)}, New Balance: {Math.Round(transaction.NewBalance, 2)}, Time: {transaction.CreateTime}");
                }
                Console.WriteLine("****************************************************************************");
            }
        }
    }
}