2023-09-12 18:11:47 +08:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
2023-09-14 23:55:38 +08:00
|
|
|
|
using Aitex.Core.RT.DBCore;
|
2023-09-12 18:11:47 +08:00
|
|
|
|
using Aitex.Core.Util;
|
|
|
|
|
|
|
|
|
|
namespace Aitex.Core.Account;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 登录凭据管理器。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class CredentialManager : Singleton<CredentialManager>
|
|
|
|
|
{
|
|
|
|
|
#region Variables
|
|
|
|
|
|
2023-09-15 23:57:38 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// 已登录的凭据激活超时时间。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// 如果凭据超过此时间没有被激活,则自动移除此凭据。
|
|
|
|
|
/// </remarks>
|
|
|
|
|
public const int LOGIN_CRED_KEEP_ALIVE_TIMEOUT_SEC = 60;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 正在请求登录的凭据的生命时长。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public const int REQ_LOGIN_CRED_LIFT_TIME_SEC = REQ_LOGIN_DIALOG_LIFT_TIME__SEC + 5;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// UI中的请求登录/授权登录对话框存活时间
|
|
|
|
|
/// </summary>
|
|
|
|
|
public const int REQ_LOGIN_DIALOG_LIFT_TIME__SEC = 30;
|
|
|
|
|
|
|
|
|
|
|
2023-09-13 17:31:22 +08:00
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
private readonly object _syncRoot = new();
|
|
|
|
|
|
2023-09-13 17:31:22 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// 已登录的凭据。
|
|
|
|
|
/// </summary>
|
|
|
|
|
private readonly Dictionary<Guid, Credential> _dictCredentialsLoggedIn = new ();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2023-09-14 14:17:16 +08:00
|
|
|
|
/// 正在等在登录请求确认的凭据,LoginName为字典Key。
|
2023-09-13 17:31:22 +08:00
|
|
|
|
/// </summary>
|
2023-09-14 14:17:16 +08:00
|
|
|
|
private readonly Dictionary<string, Credential> _dictCredentialsRequesting = new ();
|
2023-09-13 17:31:22 +08:00
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
private readonly PeriodicJob _threadMonitorCred;
|
|
|
|
|
private bool _isInitialized;
|
|
|
|
|
private int _maxCredentialAllowed;
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructors
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 登录凭据管理的构造函数。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CredentialManager()
|
|
|
|
|
{
|
|
|
|
|
_threadMonitorCred = new(1000, OnTimer, "CredentialAliveMonitorThread", true, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 返回当前凭据管理器是否支持多用户登录。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsSupportMultiUserLogin => _maxCredentialAllowed > 1;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
/// 返回已登录的用户凭据的数量。
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// </summary>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
public int LoggedInCount
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
|
|
|
|
return _dictCredentialsLoggedIn.Count;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-12 18:11:47 +08:00
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Methods
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 初始化登录凭据管理器。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="isSupportMultiUsersLogin">是否支持多用户同时登录。</param>
|
|
|
|
|
public void Initialize(bool isSupportMultiUsersLogin)
|
|
|
|
|
{
|
|
|
|
|
if (_isInitialized)
|
|
|
|
|
throw new InvalidOperationException($"{nameof(CredentialManager)} has been initialized.");
|
|
|
|
|
|
|
|
|
|
_isInitialized = true;
|
|
|
|
|
_maxCredentialAllowed = isSupportMultiUsersLogin ? int.MaxValue : 1;
|
|
|
|
|
}
|
2023-09-15 17:44:30 +08:00
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
private bool OnTimer()
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-15 23:57:38 +08:00
|
|
|
|
#region 已登录凭据存活检测
|
|
|
|
|
|
2023-09-14 14:17:16 +08:00
|
|
|
|
var loginRemovableList = new List<Guid>();
|
2023-09-13 17:31:22 +08:00
|
|
|
|
foreach (var kvp in _dictCredentialsLoggedIn)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
var cred = kvp.Value;
|
2023-09-15 23:57:38 +08:00
|
|
|
|
if ((DateTime.Now - cred.LastAliveTime).TotalSeconds > LOGIN_CRED_KEEP_ALIVE_TIMEOUT_SEC)
|
2023-09-14 14:17:16 +08:00
|
|
|
|
loginRemovableList.Add(cred.Token);
|
2023-09-13 17:31:22 +08:00
|
|
|
|
}
|
2023-09-15 17:44:30 +08:00
|
|
|
|
|
2023-09-14 14:17:16 +08:00
|
|
|
|
if (loginRemovableList.Count > 0)
|
2023-09-13 17:31:22 +08:00
|
|
|
|
{
|
2023-09-14 14:17:16 +08:00
|
|
|
|
foreach (var token in loginRemovableList)
|
2023-09-15 17:44:30 +08:00
|
|
|
|
{
|
|
|
|
|
WriteHistory(_dictCredentialsLoggedIn[token], "EXPIRED");
|
2023-09-13 17:31:22 +08:00
|
|
|
|
_dictCredentialsLoggedIn.Remove(token);
|
2023-09-15 17:44:30 +08:00
|
|
|
|
}
|
2023-09-13 17:31:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-15 23:57:38 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region 请求登录凭据存活检测
|
|
|
|
|
|
2023-09-14 14:17:16 +08:00
|
|
|
|
var requestRemovableList = new List<string>();
|
2023-09-13 17:31:22 +08:00
|
|
|
|
foreach (var kvp in _dictCredentialsRequesting)
|
|
|
|
|
{
|
|
|
|
|
var cred = kvp.Value;
|
2023-09-15 23:57:38 +08:00
|
|
|
|
if ((DateTime.Now - cred.LastAliveTime).TotalSeconds > REQ_LOGIN_CRED_LIFT_TIME_SEC)
|
2023-09-14 14:17:16 +08:00
|
|
|
|
requestRemovableList.Add(kvp.Key);
|
2023-09-18 14:17:34 +08:00
|
|
|
|
|
|
|
|
|
// 移除被取消或拒绝的凭据
|
|
|
|
|
if(cred.State is CredentialState.RequestCanceled or CredentialState.Reject)
|
|
|
|
|
requestRemovableList.Add(kvp.Key);
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-14 14:17:16 +08:00
|
|
|
|
if (requestRemovableList.Count > 0)
|
2023-09-13 17:31:22 +08:00
|
|
|
|
{
|
2023-09-14 14:17:16 +08:00
|
|
|
|
foreach (var loginName in requestRemovableList)
|
2023-09-15 17:44:30 +08:00
|
|
|
|
{
|
2023-09-18 14:17:34 +08:00
|
|
|
|
var reason = "";
|
|
|
|
|
switch (_dictCredentialsRequesting[loginName].State)
|
|
|
|
|
{
|
|
|
|
|
case CredentialState.RequestCanceled:
|
|
|
|
|
reason = "REQ CANCELED";
|
|
|
|
|
break;
|
|
|
|
|
case CredentialState.Reject:
|
|
|
|
|
reason = "REQ REJECTED";
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
reason = "REQ EXPIRED";
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
WriteHistory(_dictCredentialsRequesting[loginName], reason);
|
2023-09-14 14:17:16 +08:00
|
|
|
|
_dictCredentialsRequesting.Remove(loginName);
|
2023-09-15 17:44:30 +08:00
|
|
|
|
}
|
2023-09-13 17:31:22 +08:00
|
|
|
|
}
|
2023-09-15 17:44:30 +08:00
|
|
|
|
|
|
|
|
|
return true;
|
2023-09-15 23:57:38 +08:00
|
|
|
|
|
|
|
|
|
#endregion
|
2023-09-13 17:31:22 +08:00
|
|
|
|
}
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 检查指定的用户名是否已经登录。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userName"></param>
|
|
|
|
|
/// <returns></returns>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
internal bool IsLoggedIn(string userName)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-13 17:31:22 +08:00
|
|
|
|
return _dictCredentialsLoggedIn.Values.FirstOrDefault(x => x.AccountInfo.LoginName == userName) != null;
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 检查指定令牌的登录凭据是否存在。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="token"></param>
|
|
|
|
|
/// <returns></returns>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
internal bool IsLoggedIn(Guid token)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-13 17:31:22 +08:00
|
|
|
|
return _dictCredentialsLoggedIn.ContainsKey(token);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 检查指定的令牌是否已经过期。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="token"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
internal bool IsTokenExpired(Guid token)
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
|
|
|
|
return _dictCredentialsLoggedIn.ContainsKey(token);
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-14 14:17:16 +08:00
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// 报告客户端处于活动状态。
|
|
|
|
|
/// </summary>
|
2023-09-18 14:17:34 +08:00
|
|
|
|
/// <param name="myToken">客户端登录凭据</param>
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// <returns></returns>
|
2023-09-18 10:13:40 +08:00
|
|
|
|
public CredentialKeepAliveResults KeepAlive(Guid myToken)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-18 10:13:40 +08:00
|
|
|
|
if (_dictCredentialsLoggedIn.TryGetValue(myToken, out var loginCred))
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
2023-09-14 14:17:16 +08:00
|
|
|
|
loginCred.LastAliveTime = DateTime.Now; // 刷新时间
|
2023-09-12 18:11:47 +08:00
|
|
|
|
|
2023-09-14 14:17:16 +08:00
|
|
|
|
// 如果当前用户名在请求登录列表中,则返回CredentialKeepAliveResults.RequestingLogin,通知
|
|
|
|
|
// 已登录的客户端,当前用户正在请求异地登录。
|
2023-09-18 14:17:34 +08:00
|
|
|
|
if (_dictCredentialsRequesting.TryGetValue(loginCred.AccountInfo.LoginName,
|
|
|
|
|
out var requestingLoginCred) && requestingLoginCred.State == CredentialState.Requesting)
|
|
|
|
|
{
|
|
|
|
|
return CredentialKeepAliveResults.RequestingLogin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return CredentialKeepAliveResults.Alive;
|
2023-09-14 14:17:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
return CredentialKeepAliveResults.NotFound;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
/// 将凭据加入请求列表。
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="cred"></param>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
|
|
|
public void AddRequestingList(Credential cred)
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-14 14:17:16 +08:00
|
|
|
|
if (_dictCredentialsRequesting.ContainsKey(cred.AccountInfo.LoginName))
|
2023-09-13 17:31:22 +08:00
|
|
|
|
throw new InvalidOperationException("the credential has been existed in requesting list.");
|
|
|
|
|
|
2023-09-15 17:44:30 +08:00
|
|
|
|
WriteHistory(cred, "REQ LOGIN");
|
2023-09-14 14:17:16 +08:00
|
|
|
|
_dictCredentialsRequesting[cred.AccountInfo.LoginName] = cred;
|
2023-09-13 17:31:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 将指定的凭据加入字典。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="cred">待授权的凭据</param>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// 给凭据授权前必须调用<see cref="AddRequestingList"/>方法将凭据加入到等待列表中。
|
|
|
|
|
/// </remarks>
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// <exception cref="Exception"></exception>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
public void Grant(Credential cred)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
2023-09-13 17:31:22 +08:00
|
|
|
|
if (IsLoggedIn(cred.AccountInfo.LoginName))
|
2023-09-12 18:11:47 +08:00
|
|
|
|
throw new Exception($"user {cred.AccountInfo.LoginName} has been logged in.");
|
|
|
|
|
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-13 17:31:22 +08:00
|
|
|
|
if (_dictCredentialsLoggedIn.Count >= _maxCredentialAllowed)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
throw new InvalidOperationException("maximum number of login credentials reached");
|
|
|
|
|
|
2023-09-14 14:17:16 +08:00
|
|
|
|
if (!_dictCredentialsRequesting.ContainsKey(cred.AccountInfo.LoginName))
|
2023-09-13 17:31:22 +08:00
|
|
|
|
throw new InvalidOperationException("the credential is not found in requesting list.");
|
|
|
|
|
|
|
|
|
|
cred.State = CredentialState.Alive;
|
|
|
|
|
_dictCredentialsLoggedIn[cred.Token] = cred;
|
2023-09-14 14:17:16 +08:00
|
|
|
|
_dictCredentialsRequesting.Remove(cred.AccountInfo.LoginName);
|
2023-09-14 23:55:38 +08:00
|
|
|
|
|
2023-09-15 17:44:30 +08:00
|
|
|
|
WriteHistory(cred, "LOGIN");
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 移除指定令牌的凭据。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="token"></param>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
public void Remove(Guid token)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-14 23:55:38 +08:00
|
|
|
|
if (_dictCredentialsLoggedIn.TryGetValue(token, out var cred))
|
|
|
|
|
{
|
2023-09-15 17:44:30 +08:00
|
|
|
|
WriteHistory(cred, "LOGOUT");
|
2023-09-14 23:55:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-13 17:31:22 +08:00
|
|
|
|
_dictCredentialsLoggedIn.Remove(token);
|
2023-09-14 23:55:38 +08:00
|
|
|
|
|
2023-09-13 17:31:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// 获取指定用户名的登录凭据。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userName"></param>
|
|
|
|
|
/// <returns></returns>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
public Credential GetCredential(string userName)
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
|
|
|
|
return _dictCredentialsLoggedIn.Values.FirstOrDefault(x => x.AccountInfo.LoginName == userName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 获取指定用户名的登录凭据。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="token"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public Credential GetCredential(Guid token)
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
|
|
|
|
return _dictCredentialsLoggedIn.TryGetValue(token, out var cred) ? cred : null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 获取指定用户名的正在等在登录请求的凭据。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userName">用户名</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public Credential GetRequestingCredential(string userName)
|
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-14 14:17:16 +08:00
|
|
|
|
return _dictCredentialsRequesting.TryGetValue(userName, out var cred) ? cred : null;
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 校验指定令牌的凭据是否有效。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="token"></param>
|
|
|
|
|
/// <returns></returns>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
public bool ValidateCredential(Guid token)
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
lock (_syncRoot)
|
|
|
|
|
{
|
2023-09-13 17:31:22 +08:00
|
|
|
|
if (_dictCredentialsLoggedIn.TryGetValue(token, out var cred))
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
|
|
|
|
return cred.State == CredentialState.Alive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Static Methods
|
|
|
|
|
|
2023-09-15 17:44:30 +08:00
|
|
|
|
private static void WriteHistory(Credential cred, string operation)
|
|
|
|
|
{
|
|
|
|
|
DB.InsertSql(
|
|
|
|
|
"insert into credentials_history (\"login_name\", \"role_id\", \"host_name\", \"host_ip\", \"host_port\", " +
|
|
|
|
|
"\"os_version\", \"computer_info\", \"cpu_info\", \"disk_info\", \"operation\", \"operation_time\") " +
|
|
|
|
|
"values" +
|
|
|
|
|
$"($sic${cred.AccountInfo.LoginName}$sic$, $sic${cred.RoleID}$sic$, $sic${cred.ClientInfo?.HostName??""}$sic$, " +
|
|
|
|
|
$"$sic${cred.LoginIP}$sic$, $sic${cred.LoginPort}$sic$, $sic${cred.ClientInfo?.OSVersion??""}$sic$," +
|
|
|
|
|
$"$sic${cred.ClientInfo?.ComputerSystem??""}$sic$, $sic${cred.ClientInfo?.CpuInfo??""}$sic$, $sic${cred.ClientInfo?.LogicalDisk??""}$sic$, " +
|
|
|
|
|
$"$sic${operation}$sic$, NOW())");
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-12 18:11:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// 创建一个令牌。
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
2023-09-13 17:31:22 +08:00
|
|
|
|
public static Guid GenerateToken()
|
2023-09-12 18:11:47 +08:00
|
|
|
|
{
|
2023-09-13 17:31:22 +08:00
|
|
|
|
return Guid.NewGuid();
|
2023-09-12 18:11:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|