用MongoDB构建交易系统
交易系统是现代商业和金融活动的核心,涵盖从电子商务订单处理到金融机构的实时清算等多种场景。这类系统通常需要高并发处理能力、实时数据存储和高效检索功能,同时需要在数据一致性与性能之间找到平衡。随着交易数据的规模和复杂性不断增长,传统交易系统架构在应对这些挑战时,往往因固定的表结构和横向扩展能力的不足而受限。
MongoDB 作为一款分布式文档型数据库,以其灵活的架构、高吞吐能力和内置的事务支持,为构建复杂、高效的交易系统提供了一种现代化的解决方案,能够满足多样化的业务需求。
问题描述与挑战
交易系统上的核心功能包括账户管理、余额查询、充值、消费以及交易记录的管理和查询。如下图所示,显示用户的账户余额:
以及显示用户的交易记录:
构建一个高效、稳定的现代交易系统面临诸多挑战,主要体现在以下几个方面:
高并发性 交易系统需要能够处理大规模的并发请求,尤其是在金融领域和电子商务中,用户同时进行交易、查询余额和其他操作的场景非常普遍。如果系统无法承载高并发,可能会导致响应延迟甚至系统崩溃。
数据一致性与事务管理 交易系统必须确保数据的一致性,尤其是涉及账户余额、交易记录和消费等敏感操作时。一旦数据不一致,可能会引发财务风险和用户信任问题。分布式环境下的事务管理进一步加大了实现难度。
灵活的数据建模 传统关系型数据库通常使用固定表结构,难以应对交易系统中复杂多变的业务逻辑和频繁变化的需求。例如,新增业务功能可能需要调整数据模型,这会导致系统修改成本高昂,甚至中断服务。
实时数据处理与查询性能 在海量交易数据的背景下,系统需要能够高效存储、检索并实时处理用户的查询请求,例如账户余额查询和历史交易明细的检索。这要求底层数据库在提供高吞吐能力的同时,也要有极快的查询性能。
系统可扩展性 随着业务的增长,交易系统的数据规模会持续增加。这就要求系统具备良好的横向扩展能力,以确保在扩展服务器或存储节点时,性能能够线性提升。
数据建模
为达到大规模数据存储和高并发读写的目的,数据建模是问题的关键。MongoDB 的文档模型以灵活和高效著称,可以轻松适应复杂多变的业务需求。
交易系统的核心功能,包括以下 6 个 API:
api/CreateAccount
创建用户账户,包括初始化用户基本信息和初始余额。api/GetBalance
查询账户余额,需要实时返回准确的数据,确保一致性。api/GetTransactionById
根据交易 ID 查询具体交易详情,支持快速精准的查询。api/GetTransactions
根据账户 ID 获取历史交易记录,支持分页和时间范围过滤,便于用户查询大规模数据。api/Recharge
用户账户充值操作,涉及事务处理,确保账户余额更新和交易记录的同步。api/Purchase
消费功能,涉及事务处理,并记录消费详情。
针对上述需求,设计两张表存储用户的资金状态和资金变动的详细信息。两张表通过
Uuid
关联交易,实现高效查询和更新操作。
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; }
}
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,
Purchase = 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与Purchase。注意这两个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);
}
}
Purchase API
Purchase
API的实现与Recharge基本相同,把AddBalance
换为DecreaseBalance
:
public async Task<TransResult> PurchaseAsync(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.Purchase,
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);
}
}
在实践中,可以通过修改Purchase
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 PurchaseAsync(accountId, tid, amount);
if (result.Code == TransCode.Success)
{
Console.WriteLine($"Purchase: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
}
else
{
Console.WriteLine($"Purchase 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: Purchase, 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: Purchase, 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,
Purchase = 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,
Purchase = 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> PurchaseAsync(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.Purchase,
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 PurchaseAsync(accountId, tid, amount);
if (result.Code == TransCode.Success)
{
Console.WriteLine($"Purchase: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
}
else
{
Console.WriteLine($"Purchase 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("****************************************************************************");
}
}
}
}