Implement billing status notification and enhance user management integration
CI / Backend (build + test) (push) Successful in 1m30s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- Added `NotifyBillingStatusChangedAsync` method to `IRealtimeNotifier` for notifying clients about changes in billing status.
- Updated `BillingConfigResumer` to call the new notification method after modifying billing configurations, ensuring users receive real-time updates.
- Enhanced `ListUsersQueryHandler` to include a `BillingPendingReview` property in `UserSummaryDto`, indicating if a user has a pending payment request awaiting confirmation.
- Refactored various command handlers to utilize `AdvisoryLock` for managing concurrent requests, preventing race conditions in billing operations.
- Updated tests to cover new notification behaviors and ensure proper functionality in billing status management.
This commit is contained in:
Leonid Pershin
2026-07-19 23:22:57 +03:00
parent b32756d5bc
commit e19860ba46
49 changed files with 1195 additions and 233 deletions
@@ -75,6 +75,7 @@ public sealed class BillingService(
await BillingConfigResumer.ProtectPendingConfigsAsync(
dbContext,
gateway,
notifier,
logger,
user.UserId,
cancellationToken
@@ -118,6 +118,17 @@ internal sealed class RoleService(
if (usersInRole.Count > 0)
return Result.Failure(RoleErrors.RoleInUse);
// Живых пользователей с этой ролью нет, но инбаунды могли всё ещё указывать её в
// AllowedRoleIds (plain uuid[], без FK) — без подчистки роль пропадёт из RoleManager, а
// "мёртвая" ссылка так и останется висеть в массиве, молча не пуская никого нового.
var affectedInbounds = await dbContext
.Inbounds.Where(i => i.AllowedRoleIds.Contains(roleId))
.ToListAsync(cancellationToken);
foreach (var inbound in affectedInbounds)
inbound.RemoveAllowedRole(roleId);
if (affectedInbounds.Count > 0)
await dbContext.SaveChangesAsync(cancellationToken);
await roleManager.DeleteAsync(role);
return Result.Success();
}
@@ -157,6 +157,23 @@ internal sealed class XuiPanelGateway(
CancellationToken cancellationToken
)
{
// ThreeXui.Net трактует обновление shadowsocks-клиента как rotate password/cipher, вне
// области UpdateClient — молча ничего не меняет и возвращает false внутри себя, а наш уровень
// выше это никак не видит (XuiClient.UpdateClientAsync это глотает и просто логирует debug).
// Раньше это означало Result.Success() без единого реального изменения на панели: блокировка/
// приостановка/возврат по оплате были no-op для SS-конфигов, хотя мы считали их применёнными.
// Явно отказываем, а не молчим — вызывающий код (Block/UnblockUserCommandHandler,
// BillingConfigResumer) увидит ошибку и залогирует предупреждение вместо ложной уверенности.
if (protocol == VpnProtocol.Shadowsocks)
{
return Result.Failure(
Error.Failure(
"Xui.ShadowsocksUpdateNotSupported",
"3x-ui/ThreeXui.Net не поддерживает изменение shadowsocks-клиента (enable/expiresAt/имя) после создания."
)
);
}
try
{
var client = GetClient(node);