♻️ Replace normal streams with JetStream

🐛 Fix pass order didn't handled successfully
This commit is contained in:
2025-09-14 19:25:53 +08:00
parent 19bf17200d
commit 4ee387ab76
17 changed files with 213 additions and 100 deletions

View File

@@ -13,6 +13,8 @@ using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NATS.Client.Core;
using NATS.Client.JetStream;
using NATS.Net;
using NodaTime;
using OtpNet;
using AuthService = DysonNetwork.Pass.Auth.AuthService;
@@ -189,7 +191,8 @@ public class AccountService(
);
}
public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId, string? backgroundId)
public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId,
string? backgroundId)
{
var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync();
if (dupeAutomateCount > 0)
@@ -230,7 +233,7 @@ public class AccountService(
);
account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file);
}
db.Accounts.Add(account);
await db.SaveChangesAsync();
@@ -442,7 +445,7 @@ public class AccountService(
if (contact is null)
{
logger.LogWarning(
"Unable to send factor code to #{FactorId} with, due to no contact method was found...",
"Unable to send factor code to #{FactorId} with, due to no contact method was found...",
factor.Id
);
return;
@@ -740,10 +743,14 @@ public class AccountService(
db.Accounts.Remove(account);
await db.SaveChangesAsync();
await nats.PublishAsync(AccountDeletedEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new AccountDeletedEvent
{
AccountId = account.Id,
DeletedAt = SystemClock.Instance.GetCurrentInstant()
}));
var js = nats.CreateJetStreamContext();
await js.PublishAsync(
AccountDeletedEvent.Type,
GrpcTypeHelper.ConvertObjectToByteString(new AccountDeletedEvent
{
AccountId = account.Id,
DeletedAt = SystemClock.Instance.GetCurrentInstant()
}).ToByteArray()
);
}
}

View File

@@ -14,8 +14,6 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Nager.Holiday" Version="1.0.1" />
<PackageReference Include="NATS.Client.Core" Version="2.6.8" />
<PackageReference Include="NATS.Client.JetStream" Version="2.6.8" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -1,7 +1,10 @@
using System.Text.Json;
using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Stream;
using NATS.Client.Core;
using NATS.Client.JetStream.Models;
using NATS.Net;
namespace DysonNetwork.Pass.Startup;
@@ -13,18 +16,30 @@ public class BroadcastEventHandler(
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var msg in nats.SubscribeAsync<byte[]>(PaymentOrderEventBase.Type, cancellationToken: stoppingToken))
var js = nats.CreateJetStreamContext();
await js.EnsureStreamCreated("payment_events", [PaymentOrderEventBase.Type]);
var consumer = await js.CreateOrUpdateConsumerAsync("payment_events",
new ConsumerConfig("pass_payment_handler"),
cancellationToken: stoppingToken);
await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken))
{
PaymentOrderEvent? evt = null;
try
{
evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data);
evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data, GrpcTypeHelper.SerializerOptions);
logger.LogInformation(
"Received order event: {ProductIdentifier} {OrderId}",
evt?.ProductIdentifier,
evt?.OrderId
);
if (evt?.ProductIdentifier is null ||
!evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram))
{
continue;
}
logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId);
@@ -38,19 +53,20 @@ public class BroadcastEventHandler(
);
if (order is null)
{
logger.LogWarning("Order with ID {OrderId} not found.", evt.OrderId);
await nats.PublishAsync(PaymentOrderEventBase.Type, msg.Data, cancellationToken: stoppingToken);
logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId);
await msg.NakAsync(cancellationToken: stoppingToken);
continue;
}
await subscriptions.HandleSubscriptionOrder(order);
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
await msg.AckAsync(cancellationToken: stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing payment order event for order {OrderId}", evt?.OrderId);
await nats.PublishAsync(PaymentOrderEventBase.Type, msg.Data, cancellationToken: stoppingToken);
logger.LogError(ex, "Error processing payment order event for order {OrderId}. Redelivering.", evt?.OrderId);
await msg.NakAsync(cancellationToken: stoppingToken);
}
}
}

View File

@@ -207,9 +207,11 @@ public static class ServiceCollectionExtensions
services.AddScoped<SafetyService>();
services.AddScoped<SocialCreditService>();
services.AddScoped<ExperienceService>();
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
services.AddScoped<OidcProviderService>();
services.AddHostedService<BroadcastEventHandler>();
return services;
}

View File

@@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Localization;
using NATS.Client.Core;
using NATS.Client.JetStream;
using NATS.Net;
using NodaTime;
using AccountService = DysonNetwork.Pass.Account.AccountService;
@@ -253,6 +255,27 @@ public class PaymentService(
throw new InvalidOperationException("Order not found");
}
var js = nats.CreateJetStreamContext();
if (order.Status == OrderStatus.Paid)
{
await js.PublishAsync(
PaymentOrderEventBase.Type,
GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent
{
OrderId = order.Id,
WalletId = payerWallet.Id,
AccountId = payerWallet.AccountId,
AppIdentifier = order.AppIdentifier,
ProductIdentifier = order.ProductIdentifier,
Meta = order.Meta ?? [],
Status = (int)order.Status,
}).ToByteArray()
);
return order;
}
if (order.Status != OrderStatus.Unpaid)
{
throw new InvalidOperationException($"Order is in invalid status: {order.Status}");
@@ -282,16 +305,19 @@ public class PaymentService(
await NotifyOrderPaid(order, payerWallet, order.PayeeWallet);
await nats.PublishAsync(PaymentOrderEventBase.Type, JsonSerializer.SerializeToUtf8Bytes(new PaymentOrderEvent
{
OrderId = order.Id,
WalletId = payerWallet.Id,
AccountId = payerWallet.AccountId,
AppIdentifier = order.AppIdentifier,
ProductIdentifier = order.ProductIdentifier,
Meta = order.Meta ?? [],
Status = (int)order.Status,
}));
await js.PublishAsync(
PaymentOrderEventBase.Type,
GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent
{
OrderId = order.Id,
WalletId = payerWallet.Id,
AccountId = payerWallet.AccountId,
AppIdentifier = order.AppIdentifier,
ProductIdentifier = order.ProductIdentifier,
Meta = order.Meta ?? [],
Status = (int)order.Status,
}).ToByteArray()
);
return order;
}