Refactor billing configuration management to centralize expiration handling
- Updated `BillingConfigResumer` to manage billing expiration entirely on the application side, using `enable` as the sole control mechanism for client access. - Changed all relevant methods to pass `DateTimeOffset.UnixEpoch` for `expiresAt`, ensuring the panel does not enforce expiration independently of our application logic. - Modified `IXuiPanelGateway` interface to reflect the new expiration handling approach, clarifying the role of `expiresAt` in client management. - Adjusted command handlers for creating and rotating VPN configurations to set `expiresAt` to `null`, preventing unintended expiration enforcement by the panel. - Enhanced documentation to explain the new billing expiration management strategy and its implications for client configurations.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Domain.Billing;
|
||||
using PnvPanel.Domain.Configs;
|
||||
|
||||
namespace PnvPanel.Application.Billing;
|
||||
@@ -9,18 +8,23 @@ namespace PnvPanel.Application.Billing;
|
||||
/// <summary>
|
||||
/// Общая синхронизация панели 3x-ui со статусом оплаты: продление (ResumeConfigsAsync), временная
|
||||
/// защита на время рассмотрения заявки админом (ProtectPendingConfigsAsync) и приостановка за неуплату
|
||||
/// (SuspendConfigsAsync). Xray проверяет expiryTime клиента сам, независимо от нашего локального
|
||||
/// статуса — поэтому "просто не гасить, пока заявка на проверке" недостаточно: если реальный
|
||||
/// BillingPaidUntil уже в прошлом, панель заблокирует клиента сама, пока мы явно не подвинем
|
||||
/// expiryTime вперёд. По той же причине везде передаём enable И expiresAt вместе (см.
|
||||
/// IXuiPanelGateway.UpdateClientAsync) — по факту эксплуатации одного enable недостаточно.
|
||||
/// (SuspendConfigsAsync). Истечение по биллингу управляется полностью на нашей стороне — единственный
|
||||
/// рычаг на панели это <c>enable</c>; <c>expiresAt</c> всегда передаётся как <see cref="NoExpiry"/>
|
||||
/// (== 0 для 3x-ui/Xray, "без срока"), чтобы панель не могла сама enforce'ить доступ по дате
|
||||
/// независимо от нашего локального статуса — это заодно сбрасывает унаследованный от старых версий
|
||||
/// реальный expiryTime, если он когда-то был запушен (создание/ротация конфига тоже больше не
|
||||
/// передают реальные даты — см. CreateVpnConfigCommandHandler/RotateVpnConfigCommandHandler).
|
||||
/// </summary>
|
||||
public static class BillingConfigResumer
|
||||
{
|
||||
/// <summary>Подтверждённая оплата/продление/гифт — реальный новый срок. Пушится на панель для
|
||||
/// ЛЮБОГО статуса конфига, не только Expired: пользователь мог доплатить/продлить заранее, пока
|
||||
/// конфиг ещё Active — панель должна узнать новый срок сразу, а не только когда конфиг реально
|
||||
/// просрочится.</summary>
|
||||
/// <summary>3x-ui/Xray трактует expiryTime == 0 как "без ограничения по сроку" — используем как
|
||||
/// явное значение "сбросить срок", а не просто not-touch (<c>null</c> в UpdateClientAsync означает
|
||||
/// "оставить как есть", что не годится для очистки унаследованных реальных дат).</summary>
|
||||
private static readonly DateTimeOffset NoExpiry = DateTimeOffset.UnixEpoch;
|
||||
|
||||
/// <summary>Подтверждённая оплата/продление/гифт. Локально фиксируем реальный новый срок
|
||||
/// (SetBillingExpiry) для отображения/Subscription-Userinfo, но на панель уходит только
|
||||
/// enable: true — панельный expiryTime истечением больше не управляет.</summary>
|
||||
public static async Task ResumeConfigsAsync(
|
||||
IAppDbContext dbContext,
|
||||
IXuiPanelGateway gateway,
|
||||
@@ -36,13 +40,12 @@ public static class BillingConfigResumer
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var wasExpired = config.Status == ConfigStatus.Expired;
|
||||
var pushed = await PushExpiryAsync(
|
||||
var pushed = await PushEnableAsync(
|
||||
dbContext,
|
||||
gateway,
|
||||
logger,
|
||||
config,
|
||||
enable: true,
|
||||
newPaidUntil,
|
||||
"extending billing",
|
||||
cancellationToken
|
||||
);
|
||||
@@ -65,11 +68,10 @@ public static class BillingConfigResumer
|
||||
}
|
||||
|
||||
/// <summary>Заявка на оплату ждёт решения админа (AwaitingConfirmation) — держим клиента рабочим
|
||||
/// на панели на время рассмотрения (expiresAt = сейчас + BillingSettings.GraceDays), не трогая
|
||||
/// Status/ExpiresAt в БД — это временная мера до Confirm (ResumeConfigsAsync проставит настоящий
|
||||
/// срок) или Reject (SuspendConfigsAsync вернёт как было). Вызывается и сразу при отметке "я
|
||||
/// оплатил" (MarkPaymentSentCommandHandler), и на каждом тике BillingService, пока заявка висит —
|
||||
/// идемпотентно, каждый вызов просто продлевает грейс ещё на GraceDays от текущего момента.</summary>
|
||||
/// на панели на время рассмотрения (enable: true), не трогая Status/ExpiresAt в БД — это временная
|
||||
/// мера до Confirm (ResumeConfigsAsync проставит настоящий срок) или Reject (SuspendConfigsAsync
|
||||
/// вернёт как было). Вызывается и сразу при отметке "я оплатил" (MarkPaymentSentCommandHandler), и
|
||||
/// на каждом тике BillingService, пока заявка висит — идемпотентно.</summary>
|
||||
public static async Task ProtectPendingConfigsAsync(
|
||||
IAppDbContext dbContext,
|
||||
IXuiPanelGateway gateway,
|
||||
@@ -79,20 +81,16 @@ public static class BillingConfigResumer
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var settings = await dbContext.BillingSettings.AsNoTracking().FirstOrDefaultAsync(cancellationToken);
|
||||
var graceUntil = DateTimeOffset.UtcNow.AddDays(settings?.GraceDays ?? BillingSettings.DefaultGraceDays);
|
||||
|
||||
var configs = await ActiveOrExpiredConfigsAsync(dbContext, userId, cancellationToken);
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
await PushExpiryAsync(
|
||||
await PushEnableAsync(
|
||||
dbContext,
|
||||
gateway,
|
||||
logger,
|
||||
config,
|
||||
enable: true,
|
||||
graceUntil,
|
||||
"protecting pending payment for",
|
||||
cancellationToken
|
||||
);
|
||||
@@ -104,9 +102,8 @@ public static class BillingConfigResumer
|
||||
/// <summary>Приостановка за неуплату — общая для фоновой джобы (BillingService) и немедленной
|
||||
/// реакции на отклонение заявки админом (RejectPaymentRequestCommandHandler), чтобы не ждать
|
||||
/// следующего часового тика. Проходит и по уже Expired конфигам — они могли быть временно
|
||||
/// "защищены" ProtectPendingConfigsAsync (enable/expiresAt на панели уехали вперёд, а статус в БД
|
||||
/// остался как был), и при отклонении заявки эту защиту нужно снять с панели тоже, а не только
|
||||
/// локально.</summary>
|
||||
/// "защищены" ProtectPendingConfigsAsync (enable на панели уехал в true, а статус в БД остался как
|
||||
/// был), и при отклонении заявки эту защиту нужно снять с панели тоже, а не только локально.</summary>
|
||||
public static async Task SuspendConfigsAsync(
|
||||
IAppDbContext dbContext,
|
||||
IXuiPanelGateway gateway,
|
||||
@@ -116,19 +113,17 @@ public static class BillingConfigResumer
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var expiredAt = DateTimeOffset.UtcNow.AddDays(-1);
|
||||
var configs = await ActiveOrExpiredConfigsAsync(dbContext, userId, cancellationToken);
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var wasActive = config.Status == ConfigStatus.Active;
|
||||
var pushed = await PushExpiryAsync(
|
||||
var pushed = await PushEnableAsync(
|
||||
dbContext,
|
||||
gateway,
|
||||
logger,
|
||||
config,
|
||||
enable: false,
|
||||
expiredAt,
|
||||
"suspending for non-payment",
|
||||
cancellationToken
|
||||
);
|
||||
@@ -165,13 +160,12 @@ public static class BillingConfigResumer
|
||||
/// (подхватится следующим циклом/событием). Если у конфига нет инбаунда/ноды вовсе — на панели
|
||||
/// нечего обновлять, но локальный статус менять можно (как и раньше в BillingService/
|
||||
/// BillingConfigResumer).</summary>
|
||||
private static async Task<bool> PushExpiryAsync(
|
||||
private static async Task<bool> PushEnableAsync(
|
||||
IAppDbContext dbContext,
|
||||
IXuiPanelGateway gateway,
|
||||
ILogger logger,
|
||||
VpnConfig config,
|
||||
bool enable,
|
||||
DateTimeOffset expiresAt,
|
||||
string action,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
@@ -195,7 +189,7 @@ public static class BillingConfigResumer
|
||||
config.Protocol,
|
||||
name: null,
|
||||
enable,
|
||||
expiresAt,
|
||||
NoExpiry,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user