- Inbound.CountryCode (ISO alpha-2), set on publish; flag shown before the location name (SVG via country-flag-icons on the site, emoji in bot) - Node.Capacity (optional, not a limit); available inbounds report load percent of capacity or active config count, sorted least loaded first Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
4.5 KiB
C#
97 lines
4.5 KiB
C#
using PnvPanel.Domain.Common;
|
|
|
|
namespace PnvPanel.Domain.Inbounds;
|
|
|
|
/// <summary>
|
|
/// Проекция inbound из 3x-ui. <see cref="RemoteInboundId"/> — идентификатор на стороне панели
|
|
/// (в ThreeXui.Net это string, а не число — так отдаёт API 3x-ui).
|
|
/// AllowedRoleIds хранит только Guid ролей (не навигацию на AppRole — тот живёт в Infrastructure/Identity,
|
|
/// Domain не должен на него ссылаться).
|
|
/// </summary>
|
|
public sealed class Inbound : Entity
|
|
{
|
|
public Guid NodeId { get; private set; }
|
|
public string RemoteInboundId { get; private set; } = string.Empty;
|
|
public VpnProtocol Protocol { get; private set; }
|
|
public string Remark { get; private set; } = string.Empty;
|
|
public int Port { get; private set; }
|
|
public bool IsPublished { get; private set; }
|
|
public bool IsAvailable { get; private set; } = true;
|
|
public string? DisplayName { get; private set; }
|
|
|
|
/// <summary>ISO 3166-1 alpha-2 (верхний регистр) — страна, флаг которой пользователь видит перед
|
|
/// именем инбаунда. Только для отображения, на подключение не влияет.</summary>
|
|
public string? CountryCode { get; private set; }
|
|
public IReadOnlyList<Guid> AllowedRoleIds { get; private set; } = [];
|
|
public DateTimeOffset? LastSyncAt { get; private set; }
|
|
|
|
private Inbound() { }
|
|
|
|
public static Inbound FromRemote(
|
|
Guid nodeId,
|
|
string remoteInboundId,
|
|
VpnProtocol protocol,
|
|
string remark,
|
|
int port
|
|
)
|
|
{
|
|
return new Inbound
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
NodeId = nodeId,
|
|
RemoteInboundId = remoteInboundId,
|
|
Protocol = protocol,
|
|
Remark = remark,
|
|
Port = port,
|
|
IsPublished = false,
|
|
IsAvailable = true,
|
|
LastSyncAt = DateTimeOffset.UtcNow,
|
|
};
|
|
}
|
|
|
|
public void UpdateFromRemote(VpnProtocol protocol, string remark, int port)
|
|
{
|
|
Protocol = protocol;
|
|
Remark = remark;
|
|
Port = port;
|
|
LastSyncAt = DateTimeOffset.UtcNow;
|
|
// Инбаунд снова пришёл в синхронизации — значит панель его видит, снимаем MarkUnavailable()
|
|
// (иначе после ЛЮБОГО временного сбоя синка — не только реального удаления — Revoke навсегда
|
|
// пропускал бы обращение к панели для конфигов на этом инбаунде, хотя клиент в панели жив).
|
|
IsAvailable = true;
|
|
}
|
|
|
|
public void Publish(
|
|
string? displayName,
|
|
string? countryCode,
|
|
IReadOnlyCollection<Guid> allowedRoleIds
|
|
)
|
|
{
|
|
IsPublished = true;
|
|
IsAvailable = true;
|
|
DisplayName = displayName;
|
|
CountryCode = string.IsNullOrWhiteSpace(countryCode)
|
|
? null
|
|
: countryCode.Trim().ToUpperInvariant();
|
|
AllowedRoleIds = allowedRoleIds.Distinct().ToList();
|
|
}
|
|
|
|
public void Unpublish() => IsPublished = false;
|
|
|
|
/// <summary>Роль удалена админом — снимаем её из списка разрешённых, чтобы не оставлять ссылку на
|
|
/// несуществующую роль (иначе она навсегда "съедала" бы место в списке, ни на что не влияя, но и
|
|
/// не позволяя понять, что инбаунд стал доступен меньшему числу ролей, чем задумано).</summary>
|
|
public void RemoveAllowedRole(Guid roleId) => AllowedRoleIds = AllowedRoleIds.Where(id => id != roleId).ToList();
|
|
|
|
/// <summary>
|
|
/// Инбаунд пропал на панели (не пришёл в очередной синхронизации), но по нему есть конфиги —
|
|
/// удалить запись нельзя (FK), поэтому помечаем недоступной вместо тихого "не опубликован":
|
|
/// UI показывает явный статус, а Revoke пропускает обращение к панели как заведомо мёртвое.
|
|
/// </summary>
|
|
public void MarkUnavailable()
|
|
{
|
|
IsAvailable = false;
|
|
IsPublished = false;
|
|
}
|
|
}
|