wip: 🧠 初步的框架

This commit is contained in:
tk
2023-08-25 15:33:42 +08:00
parent 57c1ba2002
commit 18b4d7547a
1014 changed files with 122380 additions and 2 deletions

View File

@@ -0,0 +1,20 @@
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// 应用程序帮助类
/// </summary>
public static class ApplicationHelper
{
/// <summary>
/// 获取系统环境
/// </summary>
public static Dictionary<string, object> GetEnvironmentInfo()
{
var ret = typeof(Environment).GetProperties(BindingFlags.Public | BindingFlags.Static)
.Where(x => x.Name != nameof(Environment.StackTrace))
.ToDictionary(x => x.Name, x => x.GetValue(null));
_ = ret.TryAdd( //
"Environment", Environment.GetEnvironmentVariables().ToJson());
return ret;
}
}

View File

@@ -0,0 +1,176 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// 验证码图片工具类
/// </summary>
public static class CaptchaImageHelper
{
/// <summary>
/// 创建一个缺口滑块验证码图片
/// </summary>
/// <param name="resAsm">包含资源的程序集</param>
/// <param name="bgPath">背景图路径</param>
/// <param name="tempPath">滑块模板小图路径</param>
/// <param name="bgIndexScope">背景图随机序号范围1-x</param>
/// <param name="tempIndexScope">模板图随机序号范围1-x</param>
/// <param name="sliderSize">滑块尺寸</param>
/// <returns> 背景图base64滑块图base64缺口坐标 </returns>
#pragma warning disable SA1414
public static async Task<(string BackgroundImage, string SliderImage, Point OffsetSaw)> CreateSawSliderImageAsync(
Assembly resAsm, string bgPath, string tempPath, (int, int) bgIndexScope, (int, int) tempIndexScope
, Size sliderSize)
#pragma warning restore SA1414
{
// 深色模板图
var templateIndex = new[] { tempIndexScope.Item1, tempIndexScope.Item2 }.Rand();
await using var bgStream = resAsm.GetManifestResourceStream(
$"{bgPath}.{new[] { bgIndexScope.Item1, bgIndexScope.Item2 }.Rand()}.jpg");
await using var darkStream = resAsm.GetManifestResourceStream($"{tempPath}._{templateIndex}.dark.png");
await using var tranStream = resAsm.GetManifestResourceStream($"{tempPath}._{templateIndex}.transparent.png");
// 底图
using var backgroundImage = await Image.LoadAsync<Rgba32>(bgStream);
using var darkTemplateImage = await Image.LoadAsync<Rgba32>(darkStream);
// 透明模板图
using var transparentTemplateImage = await Image.LoadAsync<Rgba32>(tranStream);
// 调整模板图大小
darkTemplateImage.Mutate(x => x.Resize(sliderSize));
transparentTemplateImage.Mutate(x => x.Resize(sliderSize));
// 新建拼图
using var blockImage = new Image<Rgba32>(sliderSize.Width, sliderSize.Height);
// 新建滑块拼图
using var sliderBlockImage = new Image<Rgba32>(sliderSize.Width, backgroundImage.Height);
// 随机生成拼图坐标
var offsetRand = GeneratePoint(backgroundImage.Width, backgroundImage.Height, sliderSize.Width
, sliderSize.Height);
// 根据深色模板图计算轮廓形状
var blockShape = CalcBlockShape(darkTemplateImage);
// 生成拼图
blockImage.Mutate(x => {
// ReSharper disable once AccessToDisposedClosure
_ = x.Clip(blockShape, p => p.DrawImage(backgroundImage, new Point(-offsetRand.X, -offsetRand.Y), 1));
});
// 拼图叠加透明模板图层
// ReSharper disable once AccessToDisposedClosure
blockImage.Mutate(x => x.DrawImage(transparentTemplateImage, new Point(0, 0), 1));
// 生成滑块拼图
// ReSharper disable once AccessToDisposedClosure
sliderBlockImage.Mutate(x => x.DrawImage(blockImage, new Point(0, offsetRand.Y), 1));
var opacity = (float)(new[] { 70, 100 }.Rand() * 0.01);
// 底图叠加深色模板图
// ReSharper disable once AccessToDisposedClosure
backgroundImage.Mutate(x => x.DrawImage(darkTemplateImage, new Point(offsetRand.X, offsetRand.Y), opacity));
// 生成干扰图坐标
var interferencePoint = GenerateInterferencePoint(backgroundImage.Width, backgroundImage.Height
, sliderSize.Width, sliderSize.Height, offsetRand.X
, offsetRand.Y);
// 底图叠加深色干扰模板图
// ReSharper disable once AccessToDisposedClosure
backgroundImage.Mutate(x => x.DrawImage(darkTemplateImage, new Point(interferencePoint.X, interferencePoint.Y)
, opacity));
return (backgroundImage.ToBase64String(PngFormat.Instance), sliderBlockImage.ToBase64String(PngFormat.Instance)
, offsetRand);
}
private static ComplexPolygon CalcBlockShape(Image<Rgba32> templateDarkImage)
{
var temp = 0;
var pathList = new List<IPath>();
templateDarkImage.ProcessPixelRows(accessor => {
for (var y = 0; y < templateDarkImage.Height; y++) {
var rowSpan = accessor.GetRowSpan(y);
for (var x = 0; x < rowSpan.Length; x++) {
ref var pixel = ref rowSpan[x];
if (pixel.A != 0) {
temp = temp switch { 0 => x, _ => temp };
}
else {
if (temp == 0) {
continue;
}
pathList.Add(new RectangularPolygon(temp, y, x - temp, 1));
temp = 0;
}
}
}
});
return new ComplexPolygon(new PathCollection(pathList));
}
/// <summary>
/// 随机生成干扰图坐标
/// </summary>
private static Point GenerateInterferencePoint(int originalWidth, int originalHeight, int templateWidth
, int templateHeight, int blockX, int blockY)
{
var x =
// 在原扣图右边插入干扰图
originalWidth - blockX - 5 > templateWidth * 2
? GetRandomInt(blockX + templateWidth + 5, originalWidth - templateWidth)
:
// 在原扣图左边插入干扰图
GetRandomInt(100, blockX - templateWidth - 5);
var y =
// 在原扣图下边插入干扰图
originalHeight - blockY - 5 > templateHeight * 2
? GetRandomInt(blockY + templateHeight + 5, originalHeight - templateHeight)
:
// 在原扣图上边插入干扰图
GetRandomInt(5, blockY - templateHeight - 5);
return new Point(x, y);
}
/// <summary>
/// 随机生成拼图坐标
/// </summary>
private static Point GeneratePoint(int originalWidth, int originalHeight, int templateWidth, int templateHeight)
{
var widthDifference = originalWidth - templateWidth;
var heightDifference = originalHeight - templateHeight;
var x = widthDifference switch {
<= 0 => 5, _ => new[] { 0, originalWidth - templateWidth - 100 }.Rand() + 100
};
var y = heightDifference switch { <= 0 => 5, _ => new[] { 0, originalHeight - templateHeight - 5 }.Rand() + 5 };
return new Point(x, y);
}
/// <summary>
/// 随机范围内数字
/// </summary>
private static int GetRandomInt(int startNum, int endNum)
{
return (endNum > startNum ? new[] { 0, endNum - startNum }.Rand() : 0) + startNum;
}
}

View File

@@ -0,0 +1,145 @@
using Newtonsoft.Json;
using DataType = FreeSql.DataType;
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// FreeSqlBuilder
/// </summary>
public sealed class FreeSqlBuilder
{
private readonly DatabaseOptions _databaseOptions;
/// <summary>
/// Initializes a new instance of the <see cref="FreeSqlBuilder" /> class.
/// </summary>
public FreeSqlBuilder(DatabaseOptions databaseOptions)
{
_databaseOptions = databaseOptions;
}
/// <summary>
/// 构建freeSql对象
/// </summary>
public IFreeSql Build(FreeSqlInitOptions initOptions)
{
var freeSql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(_databaseOptions.DbType, _databaseOptions.ConnStr)
.UseAutoSyncStructure(false)
.Build();
_ = InitDbAsync(freeSql, initOptions); // 初始化数据库 ,异步
return freeSql;
}
private static void CompareStructure(IFreeSql freeSql, Type[] entityTypes)
{
File.WriteAllText( //
$"{nameof(CompareStructure)}.sql", freeSql.CodeFirst.GetComparisonDDLStatements(entityTypes));
}
/// <summary>
/// 获取所有实体类型定义
/// </summary>
private static Type[] GetEntityTypes()
{
return (from type in App.EffectiveTypes
from attr in type.GetCustomAttributes()
where attr is TableAttribute { DisableSyncStructure: false }
select type).ToArray();
}
private static MethodInfo MakeGetRepositoryMethod(Type entityType)
{
return typeof(FreeSqlDbContextExtensions).GetMethods()
.Where(x => x.Name == nameof(FreeSqlDbContextExtensions.GetRepository))
.FirstOrDefault(x => x.GetGenericArguments().Length == 1)
?.MakeGenericMethod(entityType);
}
private static MethodInfo MakeInsertMethod(Type entityType)
{
return typeof(IBaseRepository<>).MakeGenericType(entityType)
.GetMethod( //
nameof(IBaseRepository<dynamic>.Insert)
, BindingFlags.Public | BindingFlags.Instance, null
, CallingConventions.Any
, new[] { typeof(IEnumerable<>).MakeGenericType(entityType) }, null);
}
/// <summary>
/// 初始化数据库
/// </summary>
private Task InitDbAsync(IFreeSql freeSql, FreeSqlInitOptions initOptions)
{
return Task.Run(() => {
if (initOptions == FreeSqlInitOptions.None) {
return;
}
var entityTypes = GetEntityTypes();
if (initOptions.HasFlag(FreeSqlInitOptions.SyncStructure)) {
SyncStructure(freeSql, entityTypes);
}
if (initOptions.HasFlag(FreeSqlInitOptions.InsertSeedData)) {
InsertSeedData(freeSql, entityTypes);
}
if (initOptions.HasFlag(FreeSqlInitOptions.CompareStructure)) {
CompareStructure(freeSql, entityTypes);
}
});
}
/// <summary>
/// 插入种子数据
/// </summary>
private void InsertSeedData(IFreeSql freeSql, IEnumerable<Type> entityTypes)
{
foreach (var entityType in entityTypes) {
var file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _databaseOptions.SeedDataRelativePath
, $"{entityType.Name}.json");
if (!File.Exists(file)) {
continue;
}
var fileContent = File.ReadAllText(file);
dynamic entities = JsonConvert.DeserializeObject( //
fileContent, typeof(IEnumerable<>).MakeGenericType(entityType));
// 如果表存在数据,跳过
var select = typeof(IFreeSql).GetMethod(nameof(freeSql.Select), 1, Type.EmptyTypes)
?.MakeGenericMethod(entityType)
.Invoke(freeSql, null);
if (select?.GetType()
.GetMethod(nameof(ISelect<dynamic>.Any), 0, Type.EmptyTypes)
?.Invoke(select, null) as bool? ?? true) {
continue;
}
var rep = MakeGetRepositoryMethod(entityType)?.Invoke(null, new object[] { freeSql, null });
if (rep?.GetType().GetProperty(nameof(DbContextOptions))?.GetValue(rep) is DbContextOptions options) {
options.EnableCascadeSave = true;
options.NoneParameter = true;
}
var insert = MakeInsertMethod(entityType);
_ = insert?.Invoke(rep, new[] { entities });
}
}
/// <summary>
/// 同步数据库结构
/// </summary>
private void SyncStructure(IFreeSql freeSql, Type[] entityTypes)
{
if (_databaseOptions.DbType == DataType.Oracle) {
freeSql.CodeFirst.IsSyncStructureToUpper = true;
}
freeSql.CodeFirst.SyncStructure(entityTypes);
}
}

View File

@@ -0,0 +1,15 @@
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// 日志帮助类
/// </summary>
public static class LogHelper
{
/// <summary>
/// 获取ILogger
/// </summary>
public static ILogger<T> Get<T>()
{
return App.GetService<ILogger<T>>();
}
}

View File

@@ -0,0 +1,52 @@
using Minio;
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// MinioHelper
/// </summary>
public sealed class MinioHelper : IScoped
{
private readonly UploadOptions _uploadOptions;
/// <summary>
/// Initializes a new instance of the <see cref="MinioHelper" /> class.
/// </summary>
public MinioHelper(IOptions<UploadOptions> uploadOptions)
{
_uploadOptions = uploadOptions.Value;
}
/// <summary>
/// 上传文件
/// </summary>
/// <param name="objectName">对象名称</param>
/// <param name="fileStream">文件流</param>
/// <param name="contentType">文件类型</param>
/// <param name="fileSize">文件大小</param>
/// <returns>可访问的url地址</returns>
public async Task<string> UploadAsync(string objectName, Stream fileStream, string contentType, long fileSize)
{
using var minio = new MinioClient().WithEndpoint(_uploadOptions.Minio.ServerAddress)
.WithCredentials( //
_uploadOptions.Minio.AccessKey, _uploadOptions.Minio.SecretKey)
.WithSSL(_uploadOptions.Minio.Secure)
.Build();
var beArgs = new BucketExistsArgs().WithBucket(_uploadOptions.Minio.BucketName);
if (!await minio.BucketExistsAsync(beArgs)) {
var mbArgs = new MakeBucketArgs().WithBucket(_uploadOptions.Minio.BucketName);
await minio.MakeBucketAsync(mbArgs);
}
var putArgs = new PutObjectArgs().WithBucket(_uploadOptions.Minio.BucketName)
.WithObject(objectName)
.WithStreamData(fileStream)
.WithObjectSize(fileSize)
.WithContentType(contentType);
_ = await minio.PutObjectAsync(putArgs);
return $"{_uploadOptions.Minio.AccessUrl}/{_uploadOptions.Minio.BucketName}/{objectName}";
}
}

View File

@@ -0,0 +1,98 @@
using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;
using StackExchange.Redis;
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// Redis锁
/// </summary>
#pragma warning disable DesignedForInheritance
public class RedLocker : IDisposable, ISingleton
#pragma warning restore DesignedForInheritance
{
// Track whether Dispose has been called.
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="RedLocker" /> class.
/// </summary>
public RedLocker(IOptions<RedisOptions> redisOptions)
{
RedlockFactory = RedLockFactory.Create( //
new List<RedLockMultiplexer> //
{
ConnectionMultiplexer.Connect( //
redisOptions.Value.Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE).ConnStr)
});
}
/// <summary>
/// Finalizes an instance of the <see cref="RedLocker" /> class.
/// Use C# finalizer syntax for finalization code.
/// This finalizer will run only if the Dispose method
/// does not get called.
/// It gives your base class the opportunity to finalize.
/// Do not provide finalizer in types derived from this class.
/// </summary>
~RedLocker()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(disposing: false) is optimal in terms of
// readability and maintainability.
Dispose(false);
}
/// <summary>
/// RedlockFactory
/// </summary>
public RedLockFactory RedlockFactory { get; }
/// <summary>
/// Implement IDisposable.
/// Do not make this method virtual.
/// A derived class should not be able to override this method.
/// </summary>
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose(bool disposing) executes in two distinct scenarios.
/// If disposing equals true, the method has been called directly
/// or indirectly by a user's code. Managed and unmanaged resources
/// can be disposed.
/// If disposing equals false, the method has been called by the
/// runtime from inside the finalizer and you should not reference
/// other objects. Only unmanaged resources can be disposed.
/// </summary>
protected virtual void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if (_disposed) {
return;
}
// If disposing equals true, dispose all managed
// and unmanaged resources.
if (disposing) {
RedlockFactory.Dispose();
}
// Call the appropriate methods to clean up
// unmanaged resources here.
// If disposing is false,
// only the following code is executed.
// Note disposing has been done.
_disposed = true;
}
}

View File

@@ -0,0 +1,347 @@
// ReSharper disable StringLiteralTypo
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// 解析用户代理字符串
/// </summary>
public sealed class UserAgentParser
{
private static readonly Dictionary<string, string> _browsers = new() {
{ "OPR", "Opera" }
, { "Flock", "Flock" }
, { "Edge", "Spartan" }
, { "Chrome", "Chrome" }
, { "Opera.*?Version", "Opera" }
, { "Opera", "Opera" }
, { "MSIE", "Internet Explorer" }
, {
"Internet Explorer"
, "Internet Explorer"
}
, { "Trident.* rv", "Internet Explorer" }
, { "Shiira", "Shiira" }
, { "Firefox", "Firefox" }
, { "Chimera", "Chimera" }
, { "Phoenix", "Phoenix" }
, { "Firebird", "Firebird" }
, { "Camino", "Camino" }
, { "Netscape", "Netscape" }
, { "OmniWeb", "OmniWeb" }
, { "Safari", "Safari" }
, { "Mozilla", "Mozilla" }
, { "Konqueror", "Konqueror" }
, { "icab", "iCab" }
, { "Lynx", "Lynx" }
, { "Links", "Links" }
, { "hotjava", "HotJava" }
, { "amaya", "Amaya" }
, { "IBrowse", "IBrowse" }
, { "Maxthon", "Maxthon" }
, { "Ubuntu", "Ubuntu Web Browser" }
};
private static readonly Dictionary<string, string> _mobiles = new() {
// Legacy
{ "mobileexplorer", "Mobile Explorer" }
, { "palmsource", "Palm" }
, { "palmscape", "Palmscape" }
,
// Phones and Manufacturers
{ "motorola", "Motorola" }
, { "nokia", "Nokia" }
, { "palm", "Palm" }
, { "iphone", "Apple iPhone" }
, { "ipad", "iPad" }
, { "ipod", "Apple iPod Touch" }
, { "sony", "Sony Ericsson" }
, { "ericsson", "Sony Ericsson" }
, { "blackberry", "BlackBerry" }
, { "cocoon", "O2 Cocoon" }
, { "blazer", "Treo" }
, { "lg", "LG" }
, { "amoi", "Amoi" }
, { "xda", "XDA" }
, { "mda", "MDA" }
, { "vario", "Vario" }
, { "htc", "HTC" }
, { "samsung", "Samsung" }
, { "sharp", "Sharp" }
, { "sie-", "Siemens" }
, { "alcatel", "Alcatel" }
, { "benq", "BenQ" }
, { "ipaq", "HP iPaq" }
, { "mot-", "Motorola" }
, {
"playstation portable"
, "PlayStation Portable"
}
, { "playstation 3", "PlayStation 3" }
, { "playstation vita", "PlayStation Vita" }
, { "hiptop", "Danger Hiptop" }
, { "nec-", "NEC" }
, { "panasonic", "Panasonic" }
, { "philips", "Philips" }
, { "sagem", "Sagem" }
, { "sanyo", "Sanyo" }
, { "spv", "SPV" }
, { "zte", "ZTE" }
, { "sendo", "Sendo" }
, { "nintendo dsi", "Nintendo DSi" }
, { "nintendo ds", "Nintendo DS" }
, { "nintendo 3ds", "Nintendo 3DS" }
, { "wii", "Nintendo Wii" }
, { "open web", "Open Web" }
, { "openweb", "OpenWeb" }
,
// Operating Systems
{ "android", "Android" }
, { "symbian", "Symbian" }
, { "SymbianOS", "SymbianOS" }
, { "elaine", "Palm" }
, { "series60", "Symbian S60" }
, { "windows ce", "Windows CE" }
,
// Browsers
{ "obigo", "Obigo" }
, { "netfront", "Netfront Browser" }
, { "openwave", "Openwave Browser" }
, { "mobilexplorer", "Mobile Explorer" }
, { "operamini", "Opera Mini" }
, { "opera mini", "Opera Mini" }
, { "opera mobi", "Opera Mobile" }
, { "fennec", "Firefox Mobile" }
,
// Other
{ "digital paths", "Digital Paths" }
, { "avantgo", "AvantGo" }
, { "xiino", "Xiino" }
, { "novarra", "Novarra Transcoder" }
, { "vodafone", "Vodafone" }
, { "docomo", "NTT DoCoMo" }
, { "o2", "O2" }
,
// Fallback
{ "mobile", "Generic Mobile" }
, { "wireless", "Generic Mobile" }
, { "j2me", "Generic Mobile" }
, { "midp", "Generic Mobile" }
, { "cldc", "Generic Mobile" }
, { "up.link", "Generic Mobile" }
, { "up.browser", "Generic Mobile" }
, { "smartphone", "Generic Mobile" }
, { "cellphone", "Generic Mobile" }
};
private static readonly Dictionary<string, string> _platforms = new() {
{ "windows nt 10.0", "Windows 10" }
, { "windows nt 6.3", "Windows 8.1" }
, { "windows nt 6.2", "Windows 8" }
, { "windows nt 6.1", "Windows 7" }
, { "windows nt 6.0", "Windows Vista" }
, { "windows nt 5.2", "Windows 2003" }
, { "windows nt 5.1", "Windows XP" }
, { "windows nt 5.0", "Windows 2000" }
, { "windows nt 4.0", "Windows NT 4.0" }
, { "winnt4.0", "Windows NT 4.0" }
, { "winnt 4.0", "Windows NT" }
, { "winnt", "Windows NT" }
, { "windows 98", "Windows 98" }
, { "win98", "Windows 98" }
, { "windows 95", "Windows 95" }
, { "win95", "Windows 95" }
, { "windows phone", "Windows Phone" }
, { "windows", "Unknown Windows OS" }
, { "android", "Android" }
, { "blackberry", "BlackBerry" }
, { "iphone", "iOS" }
, { "ipad", "iOS" }
, { "ipod", "iOS" }
, { "os x", "Mac OS X" }
, { "ppc mac", "Power PC Mac" }
, { "freebsd", "FreeBSD" }
, { "ppc", "Macintosh" }
, { "linux", "Linux" }
, { "debian", "Debian" }
, { "sunos", "Sun Solaris" }
, { "beos", "BeOS" }
, { "apachebench", "ApacheBench" }
, { "aix", "AIX" }
, { "irix", "Irix" }
, { "osf", "DEC OSF" }
, { "hp-ux", "HP-UX" }
, { "netbsd", "NetBSD" }
, { "bsdi", "BSDi" }
, { "openbsd", "OpenBSD" }
, { "gnu", "GNU/Linux" }
, { "unix", "Unknown Unix OS" }
, { "symbian", "Symbian OS" }
};
private static readonly Dictionary<string, string> _robots = new() {
{ "googlebot", "Googlebot" }
, { "msnbot", "MSNBot" }
, { "baiduspider", "Baiduspider" }
, { "bingbot", "Bing" }
, { "slurp", "Inktomi Slurp" }
, { "yahoo", "Yahoo" }
, { "ask jeeves", "Ask Jeeves" }
, { "fastcrawler", "FastCrawler" }
, { "infoseek", "InfoSeek Robot 1.0" }
, { "lycos", "Lycos" }
, { "yandex", "YandexBot" }
, {
"mediapartners-google"
, "MediaPartners Google"
}
, { "CRAZYWEBCRAWLER", "Crazy Webcrawler" }
, { "adsbot-google", "AdsBot Google" }
, {
"feedfetcher-google"
, "Feedfetcher Google"
}
, { "curious george", "Curious George" }
, { "ia_archiver", "Alexa Crawler" }
, { "MJ12bot", "Majestic-12" }
, { "Uptimebot", "Uptimebot" }
};
private readonly string _agent;
/// <summary>
/// Initializes a new instance of the <see cref="UserAgentParser" /> class.
/// </summary>
private UserAgentParser(string userAgentString)
{
_agent = userAgentString.Trim();
_ = SetPlatform();
if (SetRobot()) {
return;
}
if (SetBrowser()) {
return;
}
if (SetMobile()) {
//
}
}
/// <summary>
/// 浏览器
/// </summary>
public string Browser { get; set; } = string.Empty;
/// <summary>
/// 浏览器版本
/// </summary>
public string BrowserVersion { get; set; } = string.Empty;
/// <summary>
/// 是浏览器
/// </summary>
public bool IsBrowser { get; set; }
/// <summary>
/// 是手机
/// </summary>
public bool IsMobile { get; set; }
/// <summary>
/// 是机器人
/// </summary>
public bool IsRobot { get; set; }
/// <summary>
/// 手机
/// </summary>
public string Mobile { get; set; } = string.Empty;
/// <summary>
/// 平台
/// </summary>
public string Platform { get; set; } = string.Empty;
/// <summary>
/// 机器人
/// </summary>
public string Robot { get; set; } = string.Empty;
/// <summary>
/// 创建 UserAgentParser
/// </summary>
public static UserAgentParser Create(string userAgentString)
{
return new UserAgentParser(userAgentString);
}
private bool SetBrowser()
{
foreach (var item in _browsers) {
var match = Regex.Match(_agent, $@"{item.Key}.*?([0-9\.]+)", RegexOptions.IgnoreCase);
if (!match.Success) {
continue;
}
IsBrowser = true;
BrowserVersion = match.Groups[1].Value;
Browser = item.Value;
_ = SetMobile();
return true;
}
return false;
}
private bool SetMobile()
{
foreach (var item in _mobiles) {
if (_agent.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) != -1) {
IsMobile = true;
Mobile = item.Value;
return true;
}
}
return false;
}
private bool SetPlatform()
{
#pragma warning disable S3267
foreach (var item in _platforms) {
#pragma warning restore S3267
if (!Regex.IsMatch(_agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)) {
continue;
}
Platform = item.Value;
return true;
}
Platform = "Unknown Platform";
return false;
}
private bool SetRobot()
{
#pragma warning disable S3267
foreach (var item in _robots) {
#pragma warning restore S3267
if (Regex.IsMatch(_agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)) {
IsRobot = true;
Robot = item.Value;
_ = SetMobile();
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,95 @@
using System.Xml;
namespace NetAdmin.Infrastructure.Utils;
/// <summary>
/// 程序集注释文档读取工具
/// </summary>
public sealed class XmlCommentReader : ISingleton
{
private const string _XPATH = "//doc/members/member[@name=\"{0}\"]";
private readonly List<XmlDocument> _xmlDocuments = new();
/// <summary>
/// Initializes a new instance of the <see cref="XmlCommentReader" /> class.
/// </summary>
public XmlCommentReader(IOptions<SpecificationDocumentSettingsOptions> specificationDocumentSettings)
{
var xmlComments = specificationDocumentSettings.Value.XmlComments //
?? App.GetConfig<SpecificationDocumentSettingsOptions>(
nameof(SpecificationDocumentSettingsOptions).TrimEndOptions())
.XmlComments;
foreach (var commentFile in xmlComments.Where(x => x.Contains(nameof(NetAdmin)))) {
var xmlDoc = new XmlDocument();
try {
xmlDoc.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, commentFile));
_xmlDocuments.Add(xmlDoc);
}
catch (FileNotFoundException) {
LogHelper.Get<XmlCommentReader>().Warn(Ln.Xml注释文件不存在);
}
}
}
/// <summary>
/// 获取指定类型的注释
/// </summary>
/// <exception cref="InvalidCastException">InvalidCastException</exception>
public string GetComments(MemberInfo memberInfo)
{
var node = memberInfo switch {
MethodInfo method => GetNodeByMethod(method)
, Type type => GetNodeByType(type)
, _ => throw new InvalidCastException()
};
if (node?.FirstChild?.Name != "inheritdoc") {
return node?.FirstChild?.InnerText.Trim();
}
var cref = node.FirstChild.Attributes?["cref"]?.Value;
if (cref != null) {
return GetComments(App.EffectiveTypes.Single(x => x.FullName == cref[2..]));
}
var methodFromBaseType = memberInfo.DeclaringType?.BaseType?.GetMethod(memberInfo.Name);
if (methodFromBaseType != null) {
return GetComments(methodFromBaseType);
}
var methodFromInterface = memberInfo.DeclaringType?.GetInterfaces()
.Select(x => x.GetMethod(memberInfo.Name))
.FirstOrDefault(x => x != null);
return methodFromInterface == null ? null : GetComments(methodFromInterface);
}
private XmlNode GetNodeByMethod(MethodInfo method)
{
static string Replace(ParameterInfo parameterInfo)
{
return Regex.Replace(parameterInfo.ParameterType.ToString(), @"`\d+", string.Empty)
.Replace("[", "{")
.Replace("]", "}");
}
var nodeName = $"M:{method.DeclaringType}.{method.Name}";
var parameters = method.GetParameters();
if (parameters.Length != 0) {
nodeName += $"({string.Join(',', parameters.Select(Replace))})";
}
return _xmlDocuments
.Select(xmlDoc => xmlDoc.SelectSingleNode(
string.Format(NumberFormatInfo.InvariantInfo, _XPATH, nodeName)))
.FirstOrDefault(ret => ret != null);
}
private XmlNode GetNodeByType(Type type)
{
return _xmlDocuments
.Select(xmlDoc => xmlDoc.SelectSingleNode(
string.Format(NumberFormatInfo.InvariantInfo, _XPATH, $"T:{type.FullName}")))
.FirstOrDefault(ret => ret != null);
}
}