diff --git a/Core/MAX.Core/Caches/ICache.cs b/Core/MAX.Core/Caches/ICache.cs new file mode 100644 index 0000000..9407565 --- /dev/null +++ b/Core/MAX.Core/Caches/ICache.cs @@ -0,0 +1,99 @@ +using System; + +namespace MAX.Core.Caches; + +/// +/// 缓存 +/// +public interface ICache +{ + /// + /// 是否存在指定键的缓存 + /// + /// 缓存键 + bool Exists(string key); + /// + /// 从缓存中获取数据 + /// + /// + /// + T Get(string key); + /// + /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中 + /// + /// 缓存数据类型 + /// 缓存键 + /// 获取数据操作 + T Get(string key, Func func); + /// + /// 当缓存数据不存在则添加,已存在不会添加,添加成功返回true + /// + /// 缓存数据类型 + /// 缓存键 + /// 值 + bool TryAdd(string key, T value); + /// + /// 覆盖添加 + /// + /// + /// + /// + /// + bool OverlayAdd(string key, T value); + /// + /// 移除缓存 + /// + /// 缓存键 + void Remove(string key); + /// + /// 清空缓存 + /// + void Clear(); +} + +public interface ICache +{ + /// + /// 是否存在指定键的缓存 + /// + /// 缓存键 + bool Exists(string key); + + /// + /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中 + /// + /// 缓存数据类型 + /// 缓存键 + T Get(string key); + /// + /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中 + /// + /// 缓存数据类型 + /// 缓存键 + /// 获取数据操作 + T Get(string key, Func func); + /// + /// 当缓存数据不存在则添加,已存在不会添加,添加成功返回true + /// + /// 缓存数据类型 + /// 缓存键 + /// 值 + bool TryAdd(string key, T value); + /// + /// 覆盖添加 + /// + /// + /// + /// + /// + bool OverlayAdd(string key, T value); + /// + /// 移除缓存 + /// + /// 缓存键 + void Remove(string key); + /// + /// 清空缓存 + /// + void Clear(); +} diff --git a/Core/MAX.Core/Caches/MemoryCache.cs b/Core/MAX.Core/Caches/MemoryCache.cs new file mode 100644 index 0000000..b2bc36a --- /dev/null +++ b/Core/MAX.Core/Caches/MemoryCache.cs @@ -0,0 +1,168 @@ + +using MAX.Core.DependencyInjection; +using MAX.Core.Infrastructure; +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace MAX.Core.Caches; + +/// +/// 内存缓存 +/// +public class MemoryCache : ICache, ISingletonDependency +{ + //private static readonly Object _locker = new object(); + private readonly ConcurrentDictionary _cache = new(); + + public virtual void Clear() + { + _cache.Clear(); + } + + public virtual bool Exists(string key) + { + return _cache.ContainsKey(key); + } + public virtual T Get(string key) + { + if (Exists(key)) + { + return (T)_cache[key]; + } + return default; + } + public virtual TResult Get(string key, Func func) + { + if (Exists(key)) + { + return (TResult)_cache[key]; + } + else + { + if (func != null) + { + TResult t = func(); + TryAdd(key, t); + return t; + } + } + return default; + } + + public virtual bool OverlayAdd(string key, T value) + { + if (Exists(key)) + { + _cache[key] = value; + } + else + { + _cache.TryAdd(key, value); + } + return true; + } + + public virtual void Remove(string key) + { + _cache.TryRemove(key, out _); + } + + public virtual bool TryAdd(string key, T value) + { + _cache.AddOrUpdate(key, value, (oldkey, oldvalue) => value); + return true; + } + /// + /// 获取所有Key + /// + /// + public string[] AllKeys() + { + return _cache.Keys.ToArray(); + } +} +/// +/// 单例缓存抽象类 +/// +/// 单例对象 +/// 缓存类型 +public abstract class SingleInstanceLittleCache : SingleInstance, ICache where TModel : class +{ + private readonly ConcurrentDictionary _cache = new(); + /// + /// 自定义前缀 + /// + //public abstract string KeyPrefix { get; } + //private string FormatKey(string key) + //{ + // return KeyPrefix + key; + //} + + public virtual void Clear() + { + _cache.Clear(); + } + + public virtual bool Exists(string key) + { + return _cache.ContainsKey(key); + } + public virtual TValue Get(string key) + { + if (Exists(key)) + { + return _cache[key]; + } + return default; + } + public virtual TValue Get(string key, Func func) + { + if (Exists(key)) + { + return _cache[key]; + } + else + { + if (func != null) + { + TValue t = func(); + TryAdd(key, t); + return t; + } + } + return default; + } + + public virtual bool OverlayAdd(string key, TValue value) + { + if (Exists(key)) + { + _cache[key] = value; + } + else + { + _cache.TryAdd(key, value); + } + return true; + } + + public virtual void Remove(string key) + { + _cache.TryRemove(key, out _); + } + + public virtual bool TryAdd(string key, TValue value) + { + _cache.AddOrUpdate(key, value, (oldkey, oldvalue) => value); + return true; + } + /// + /// 获取所有Key + /// + /// + public string[] AllKeys() + { + return _cache.Keys.ToArray(); + } +} diff --git a/Core/MAX.Core/Commons/CommonValue.cs b/Core/MAX.Core/Commons/CommonValue.cs new file mode 100644 index 0000000..ddcd731 --- /dev/null +++ b/Core/MAX.Core/Commons/CommonValue.cs @@ -0,0 +1,37 @@ +namespace MAX.Core.Commons; + +/// +/// 公共Value +/// +/// +public class CommonValue +{ + public CommonValue() { } + public CommonValue(TValue value) + { + Value = value; + } + public TValue Value { get; set; } +} +/// +/// 公共TextValue +/// +/// +public class CommonTextValue : CommonValue +{ + public string Text { get; set; } +} +/// +/// 公共TextValue +/// +public class CommonTextValue : CommonTextValue +{ + +} +/// +/// 默认公共键值对 Key Value +/// +public class CommonKeyValue : CommonValue +{ + public string Key { get; set; } +} diff --git a/Core/MAX.Core/Commons/Result.cs b/Core/MAX.Core/Commons/Result.cs new file mode 100644 index 0000000..5d71941 --- /dev/null +++ b/Core/MAX.Core/Commons/Result.cs @@ -0,0 +1,55 @@ +using System; + +namespace MAX.Core.Commons; + +/// +/// 通用返回 +/// +public class Result +{ + /// + /// 返回结果是否成功 + /// + public bool Success { get; set; } = false; + /// + /// 返回消息 + /// + public string Msg { get; set; } = ""; + /// + /// 返回内容 + /// + public object Data { get; set; } = null; + + /// + /// 返回状态码 + /// + public int Code { get; set; } + +} +/// +/// 通用返回 指定Data类型 +/// +/// 数据类型 +public class Result : Result +{ + /// + /// 返回内容 + /// + public new TData Data { get; set; } +} +/// +/// 通用返回 +/// +/// 状态类型枚举 +/// 数据类型 +public class Result : Result where TStatus : Enum +{ + /// + /// 状态 + /// + public TStatus Status { get; set; } + /// + /// 返回内容 + /// + public new TData Data { get; set; } +} diff --git a/Core/MAX.Core/Cryptography/Base64Helper.cs b/Core/MAX.Core/Cryptography/Base64Helper.cs new file mode 100644 index 0000000..768c732 --- /dev/null +++ b/Core/MAX.Core/Cryptography/Base64Helper.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; + +namespace MAX.Core.Cryptography; + +public class Base64Helper +{ + /// + /// Base64加密 + /// + /// 加密采用的编码方式 + /// 待加密的明文 + /// + public static string Encode(string source, Encoding encode) + { + string encodeValue = ""; + byte[] bytes = encode.GetBytes(source); + try + { + encodeValue = Convert.ToBase64String(bytes); + } + catch + { + encodeValue = source; + } + return encodeValue; + } + + /// + /// Base64加密,采用utf8编码方式加密 + /// + /// 待加密的明文 + /// 加密后的字符串 + public static string Encode(string source) + { + return Encode(source, Encoding.UTF8); + } + + /// + /// Base64解密 + /// + /// 解密采用的编码方式,注意和加密时采用的方式一致 + /// 待解密的密文 + /// 解密后的字符串 + public static string Decode(string result, Encoding encode) + { + string decode = ""; + byte[] bytes = Convert.FromBase64String(result); + try + { + decode = encode.GetString(bytes); + } + catch + { + decode = result; + } + return decode; + } + + /// + /// Base64解密,采用utf8编码方式解密 + /// + /// 待解密的密文 + /// 解密后的字符串 + public static string Decode(string result) + { + return Decode(result, Encoding.UTF8); + } +} diff --git a/Core/MAX.Core/Cryptography/DESHelper.cs b/Core/MAX.Core/Cryptography/DESHelper.cs new file mode 100644 index 0000000..67e37f0 --- /dev/null +++ b/Core/MAX.Core/Cryptography/DESHelper.cs @@ -0,0 +1,215 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace MAX.Core.Cryptography; + +public sealed class DESHelper +{ + + //默认密钥向量 + private static readonly byte[] IVKey = { 36, 109, 97, 120, 111, 110, 120, 36 }; + /// + /// DES加密字符串 + /// + /// 待加密的字符串 + /// 加密密钥,要求为8位 + /// 加密成功返回加密后的字符串,失败返回源串 + public static string Encrypt(string encryptString, string encryptKey) + { + try + { + encryptKey = GetSubString(encryptKey, 8, ""); + encryptKey = encryptKey.PadRight(8, ' '); + byte[] rgbKey = Encoding.UTF8.GetBytes(encryptKey.Substring(0, 8)); + byte[] rgbIV = IVKey; + byte[] inputByteArray = Encoding.UTF8.GetBytes(encryptString); + DES dCSP = DES.Create(); + MemoryStream mStream = new(); + CryptoStream cStream = new(mStream, dCSP.CreateEncryptor(rgbKey, rgbIV), CryptoStreamMode.Write); + cStream.Write(inputByteArray, 0, inputByteArray.Length); + cStream.FlushFinalBlock(); + return Convert.ToBase64String(mStream.ToArray()); + } + catch + { + return encryptString; + } + } + + /// + /// DES解密字符串 + /// + /// 待解密的字符串 + /// 解密密钥,要求为8位,和加密密钥相同 + /// 解密成功返回解密后的字符串,失败返源串 + public static string Decrypt(string decryptString, string decryptKey) + { + try + { + decryptKey = GetSubString(decryptKey, 0, 8, ""); + decryptKey = decryptKey.PadRight(8, ' '); + byte[] rgbKey = Encoding.UTF8.GetBytes(decryptKey); + byte[] rgbIV = IVKey; + byte[] inputByteArray = Convert.FromBase64String(decryptString); + DES DCSP = DES.Create(); + + MemoryStream mStream = new(); + CryptoStream cStream = new(mStream, DCSP.CreateDecryptor(rgbKey, rgbIV), CryptoStreamMode.Write); + cStream.Write(inputByteArray, 0, inputByteArray.Length); + cStream.FlushFinalBlock(); + return Encoding.UTF8.GetString(mStream.ToArray()); + } + catch + { + return string.Empty; + } + + } + + #region 截取字符串函数 + + /// + /// 截取字符串函数 + /// + /// 所要截取的字符串 + /// 截取字符串的长度 + /// + public static string GetSubString(string Str, int Num) + { + if (Str is null or "") + { + return ""; + } + string outstr = string.Empty; + int n = 0; + foreach (char ch in Str) + { + n += Encoding.Default.GetByteCount(ch.ToString()); + + if (n > Num) + { + break; + } + else + { + outstr += ch; + } + } + + return outstr; + } + /// + /// 截取字符串函数,如果超过指定长度则将超出的部分用指定字符串代替 + /// + /// 所要截取的字符串 + /// 截取字符串的长度 + /// 截取字符串后省略部分的字符串 + /// + public static string GetSubString(string Str, int Num, string LastStr) + { + return GetSubString(Str, 0, Num, LastStr); + } + + #endregion + + #region 截取字符串,超过部分用指定字符串替换 + + /* 宗波于 2010-6-12 日添加, 来源于DiscuzNt */ + /// + /// 取指定长度的字符串 + /// + /// 要检查的字符串 + /// 起始位置 + /// 指定长度 + /// 用于替换的字符串 + /// 截取后的字符串 + public static string GetSubString(string p_SrcString, int p_StartIndex, int p_Length, string p_TailString) + { + string myResult = p_SrcString; + + byte[] bComments = Encoding.UTF8.GetBytes(p_SrcString); + foreach (char c in Encoding.UTF8.GetChars(bComments)) + { //当是日文或韩文时(注:中文的范围:\u4e00 - \u9fa5, 日文在\u0800 - \u4e00, 韩文为\xAC00-\xD7A3) + if (c is (> '\u0800' and < '\u4e00') or (> '\xAC00' and < '\xD7A3')) + { + //if (System.Text.RegularExpressions.Regex.IsMatch(p_SrcString, "[\u0800-\u4e00]+") || System.Text.RegularExpressions.Regex.IsMatch(p_SrcString, "[\xAC00-\xD7A3]+")) + //当截取的起始位置超出字段串长度时 + if (p_StartIndex >= p_SrcString.Length) + { + return ""; + } + else + { + return p_SrcString.Substring(p_StartIndex, + p_Length + p_StartIndex > p_SrcString.Length ? p_SrcString.Length - p_StartIndex : p_Length); + } + } + } + + if (p_Length >= 0) + { + byte[] bsSrcString = Encoding.Default.GetBytes(p_SrcString); + + //当字符串长度大于起始位置 + if (bsSrcString.Length > p_StartIndex) + { + int p_EndIndex = bsSrcString.Length; + + //当要截取的长度在字符串的有效长度范围内 + if (bsSrcString.Length > p_StartIndex + p_Length) + { + p_EndIndex = p_Length + p_StartIndex; + } + else + { //当不在有效范围内时,只取到字符串的结尾 + + p_Length = bsSrcString.Length - p_StartIndex; + p_TailString = ""; + } + + int nRealLength = p_Length; + int[] anResultFlag = new int[p_Length]; + byte[] bsResult = null; + + int nFlag = 0; + for (int i = p_StartIndex; i < p_EndIndex; i++) + { + if (bsSrcString[i] > 127) + { + nFlag++; + if (nFlag == 3) + { + nFlag = 1; + } + } + else + { + nFlag = 0; + } + + anResultFlag[i] = nFlag; + } + + if (bsSrcString[p_EndIndex - 1] > 127 && anResultFlag[p_Length - 1] == 1) + { + nRealLength = p_Length + 1; + } + + bsResult = new byte[nRealLength]; + + Array.Copy(bsSrcString, p_StartIndex, bsResult, 0, nRealLength); + + myResult = Encoding.Default.GetString(bsResult); + myResult += p_TailString; + } + } + + return myResult; + } + + + #endregion + +} diff --git a/Core/MAX.Core/Cryptography/MD5Helper.cs b/Core/MAX.Core/Cryptography/MD5Helper.cs new file mode 100644 index 0000000..8397396 --- /dev/null +++ b/Core/MAX.Core/Cryptography/MD5Helper.cs @@ -0,0 +1,62 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace MAX.Core.Cryptography; + +public class MD5Helper +{ + /// + /// MD5加密 + /// + /// + /// + public static string Md5_32(string instring) + { + return _Md532(instring); + } + public static string Md5_Lower32(string instring) + { + return _Md532(instring, true); + } + public static string _Md532(string instring, bool IsLower = false) + { + byte[] result = Encoding.UTF8.GetBytes(string.IsNullOrEmpty(instring) ? "" : instring.Trim()); + MD5 md5 = MD5.Create(); + byte[] output = md5.ComputeHash(result); + string value = BitConverter.ToString(output).Replace("-", ""); + if (IsLower) + { + return value.ToLower(); + } + return value; + } + + + /// + /// MD5加密 + /// + /// + public static long GetLong(byte[] result) + { + return + Convert.ToInt64( + BitConverter.ToString(MD5.Create().ComputeHash(result), 4, + 8).Replace("-", ""), 16); + } + + /// + /// MD5 16位加密转long + /// + /// + /// + public static long GetLong(string url) + { + if (string.IsNullOrEmpty(url)) + { + return 0; + } + + return Convert.ToInt64(BitConverter.ToString(MD5.Create().ComputeHash(Encoding.Default.GetBytes(url)), 4, 8).Replace("-", ""), 16); + } +} diff --git a/Core/MAX.Core/Cryptography/UrlHelper.cs b/Core/MAX.Core/Cryptography/UrlHelper.cs new file mode 100644 index 0000000..35f2521 --- /dev/null +++ b/Core/MAX.Core/Cryptography/UrlHelper.cs @@ -0,0 +1,51 @@ +using System.Text; + +namespace MAX.Core.Cryptography; + +/// +/// Url 加密 解密 +/// +public class UrlHelper +{ + /// + /// Url加密 + /// + /// 加密采用的编码方式 + /// 待加密的明文 + /// + public static string Encode(string source, Encoding encode) + { + return System.Web.HttpUtility.UrlEncode(source, encode); + } + + /// + /// Url加密,采用utf8编码方式加密 + /// + /// 待加密的明文 + /// 加密后的字符串 + public static string Encode(string source) + { + return Encode(source, Encoding.UTF8); + } + + /// + /// Url解密 + /// + /// 解密采用的编码方式,注意和加密时采用的方式一致 + /// 待解密的密文 + /// 解密后的字符串 + public static string Decode(string result, Encoding encode) + { + return System.Web.HttpUtility.UrlDecode(result, Encoding.UTF8); + } + + /// + /// Url解密,采用utf8编码方式解密 + /// + /// 待解密的密文 + /// 解密后的字符串 + public static string Decode(string result) + { + return Decode(result, Encoding.UTF8); + } +} diff --git a/Core/MAX.Core/DependencyInjection/IDependencyRegistrar.cs b/Core/MAX.Core/DependencyInjection/IDependencyRegistrar.cs new file mode 100644 index 0000000..92cb66b --- /dev/null +++ b/Core/MAX.Core/DependencyInjection/IDependencyRegistrar.cs @@ -0,0 +1,13 @@ +namespace MAX.Core.DependencyInjection; + +/// +/// 依赖注册器:把当前模块的服务注册到 。 +/// 底层使用哪个容器由宿主项目决定,MAX.Core 不引用任何容器。 +/// +public interface IDependencyRegistrar +{ + /// + /// 注册服务 + /// + void Register(IServiceRegistrar registrar); +} diff --git a/Core/MAX.Core/DependencyInjection/IScopedDependency.cs b/Core/MAX.Core/DependencyInjection/IScopedDependency.cs new file mode 100644 index 0000000..eb004ed --- /dev/null +++ b/Core/MAX.Core/DependencyInjection/IScopedDependency.cs @@ -0,0 +1,8 @@ +namespace MAX.Core.DependencyInjection; + +/// +/// 作用域(每个 Scope/请求一个实例) +/// +public interface IScopedDependency +{ +} diff --git a/Core/MAX.Core/DependencyInjection/IServiceRegistrar.cs b/Core/MAX.Core/DependencyInjection/IServiceRegistrar.cs new file mode 100644 index 0000000..d02ba79 --- /dev/null +++ b/Core/MAX.Core/DependencyInjection/IServiceRegistrar.cs @@ -0,0 +1,80 @@ +using System; + +namespace MAX.Core.DependencyInjection; + +/// +/// 依赖注册抽象(由 MAX.Core 自己定义,不依赖任何第三方 DI 容器)。 +/// 具体实现由宿主项目提供,内部适配外部容器(Autofac、Microsoft.Extensions.DependencyInjection 等)。 +/// +public interface IServiceRegistrar +{ + /// + /// 注册单例: 实现 + /// + void RegisterSingleton() where TService : class where TImplementation : class, TService; + + /// + /// 注册单例实例 + /// + void RegisterSingleton(TService instance) where TService : class; + + /// + /// 注册单例(匿名工厂) + /// + void RegisterSingleton(Func factory) where TService : class; + + /// + /// 注册单例(key): 实现 + /// + void RegisterSingleton(object? serviceKey) where TService : class where TImplementation : class, TService; + + /// + /// 注册单例实例(key) + /// + void RegisterSingleton(object? serviceKey, TService instance) where TService : class; + + /// + /// 注册单例(key + 匿名工厂) + /// + void RegisterSingleton(object? serviceKey, Func factory) where TService : class; + + /// + /// 注册作用域生命周期 + /// + void RegisterScoped() where TService : class where TImplementation : class, TService; + + /// + /// 注册作用域生命周期(匿名工厂) + /// + void RegisterScoped(Func factory) where TService : class; + + /// + /// 注册作用域生命周期(key): 实现 + /// + void RegisterScoped(object? serviceKey) where TService : class where TImplementation : class, TService; + + /// + /// 注册作用域生命周期(key + 匿名工厂) + /// + void RegisterScoped(object? serviceKey, Func factory) where TService : class; + + /// + /// 注册瞬时生命周期 + /// + void RegisterTransient() where TService : class where TImplementation : class, TService; + + /// + /// 注册瞬时生命周期(匿名工厂) + /// + void RegisterTransient(Func factory) where TService : class; + + /// + /// 注册瞬时生命周期(key): 实现 + /// + void RegisterTransient(object? serviceKey) where TService : class where TImplementation : class, TService; + + /// + /// 注册瞬时生命周期(key + 匿名工厂) + /// + void RegisterTransient(object? serviceKey, Func factory) where TService : class; +} diff --git a/Core/MAX.Core/DependencyInjection/ISingletonDependency.cs b/Core/MAX.Core/DependencyInjection/ISingletonDependency.cs new file mode 100644 index 0000000..bcacfcb --- /dev/null +++ b/Core/MAX.Core/DependencyInjection/ISingletonDependency.cs @@ -0,0 +1,8 @@ +namespace MAX.Core.DependencyInjection; + +/// +/// 单例 +/// +public interface ISingletonDependency +{ +} diff --git a/Core/MAX.Core/DependencyInjection/ITransientDependency.cs b/Core/MAX.Core/DependencyInjection/ITransientDependency.cs new file mode 100644 index 0000000..3da15a0 --- /dev/null +++ b/Core/MAX.Core/DependencyInjection/ITransientDependency.cs @@ -0,0 +1,8 @@ +namespace MAX.Core.DependencyInjection; + +/// +/// 每次实例化 +/// +public interface ITransientDependency +{ +} diff --git a/Core/MAX.Core/DependencyInjection/LazyServiceProvider.cs b/Core/MAX.Core/DependencyInjection/LazyServiceProvider.cs new file mode 100644 index 0000000..ccc84dd --- /dev/null +++ b/Core/MAX.Core/DependencyInjection/LazyServiceProvider.cs @@ -0,0 +1,37 @@ +//using MAX.Core.Autofac; +//using MAX.Core.Infrastructure; +//using System; +//using System.Collections.Concurrent; + +//namespace MAX.Core.DependencyInjection; + +//public interface ILazyServiceProvider +//{ +// object LazyGetService(Type serviceType); +// T LazyGetService(); +//} + +//public class LazyServiceProvider : SingleInstance, ILazyServiceProvider +//{ +// protected ConcurrentDictionary> CachedServices { get; } +// private LazyServiceProvider() +// { +// CachedServices = new ConcurrentDictionary>(); +// } +// public virtual T LazyGetService() +// { +// return (T)LazyGetService(typeof(T)); +// } + +// public virtual object LazyGetService(Type serviceType) +// { +// return GetService(serviceType); +// } +// public virtual object GetService(Type serviceType) +// { +// return CachedServices.GetOrAdd( +// serviceType, +// _ => new Lazy(() => IocManager.Resolve(serviceType)) +// ).Value; +// } +//} diff --git a/Core/MAX.Core/Extensions/Convert.cs b/Core/MAX.Core/Extensions/Convert.cs new file mode 100644 index 0000000..382d8b2 --- /dev/null +++ b/Core/MAX.Core/Extensions/Convert.cs @@ -0,0 +1,218 @@ +using MAX.Core.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MAX.Core.Extensions; + +/// +/// 系统扩展 - 类型转换 +/// +public static partial class Extensions +{ + /// + /// IsNullOrEmpty + /// + /// + /// + public static bool IsNullOrEmpty(this string input) + { + return string.IsNullOrEmpty(input); + } + /// + /// 安全转换为字符串,去除两端空格,当值为null时返回"" + /// + /// 输入值 + public static string SafeString(this object input) + { + return input?.ToString().Trim() ?? string.Empty; + } + + /// + /// 转换为bool + /// + /// 数据 + public static bool ToBool(this string obj, bool defvalue = false) + { + return Helpers.Convert.ToBool(obj, defvalue); + } + + /// + /// 转换为可空bool + /// + /// 数据 + public static bool? ToBoolOrNull(this string obj) + { + return Helpers.Convert.ToBoolOrNull(obj); + } + + /// + /// 转换为int + /// + /// 数据 + public static int ToInt(this string obj, int defvalue = 0) + { + return Helpers.Convert.ToInt(obj, defvalue); + } + + /// + /// 转换为可空int + /// + /// 数据 + public static int? ToIntOrNull(this string obj) + { + return Helpers.Convert.ToIntOrNull(obj); + } + + /// + /// 转换为long + /// + /// 数据 + public static long ToLong(this string obj, long defvalue = 0) + { + return Helpers.Convert.ToLong(obj, defvalue); + } + + /// + /// 转换为可空long + /// + /// 数据 + public static long? ToLongOrNull(this string obj) + { + return Helpers.Convert.ToLongOrNull(obj); + } + + /// + /// 转换为double + /// + /// 数据 + public static double ToDouble(this string obj, double defvalue = 0) + { + return Helpers.Convert.ToDouble(obj, defvalue: defvalue); + } + + /// + /// 转换为可空double + /// + /// 数据 + public static double? ToDoubleOrNull(this string obj) + { + return Helpers.Convert.ToDoubleOrNull(obj); + } + + /// + /// 转换为decimal + /// + /// 数据 + public static decimal ToDecimal(this string obj, decimal defvalue = 0) + { + return Helpers.Convert.ToDecimal(obj, defvalue: defvalue); + } + + /// + /// 转换为可空decimal + /// + /// 数据 + public static decimal? ToDecimalOrNull(this string obj) + { + return Helpers.Convert.ToDecimalOrNull(obj); + } + + /// + /// 转换为日期 + /// + /// 数据 + public static DateTime ToDate(this string obj) + { + return Helpers.Convert.ToDate(obj); + } + + /// + /// 转换为可空日期 + /// + /// 数据 + public static DateTime? ToDateOrNull(this string obj) + { + return Helpers.Convert.ToDateOrNull(obj); + } + + /// + /// 转换为Guid + /// + /// 数据 + public static Guid ToGuid(this string obj) + { + return Helpers.Convert.ToGuid(obj); + } + + /// + /// 转换为可空Guid + /// + /// 数据 + public static Guid? ToGuidOrNull(this string obj) + { + return Helpers.Convert.ToGuidOrNull(obj); + } + + /// + /// 转换为Guid集合 + /// + /// 数据,范例: "83B0233C-A24F-49FD-8083-1337209EBC9A,EAB523C6-2FE7-47BE-89D5-C6D440C3033A" + public static List ToGuidList(this string obj) + { + return Helpers.Convert.ToGuidList(obj); + } + + /// + /// 转换为Guid集合 + /// + /// 字符串集合 + public static List ToGuidList(this IList obj) + { + if (obj == null) + { + return []; + } + + return obj.Select(t => t.ToGuid()).ToList(); + } + /// + /// 转换枚举描述 + /// + /// + /// + public static string ToDescription(this Enum obj) + { + return EnumHelper.GetDescription(obj.GetType(), obj); + } + + /// + /// 枚举值 → 单个 EnumItem + /// + /// + /// + /// + public static EnumItem ToItem(this TEnum value) where TEnum : struct, Enum + { + return new EnumItem(value); + } + /// + /// 是否单一标记 + /// + /// + /// + public static bool IsSingleFlag(Enum obj) + { + return EnumHelper.IsSingleFlag(obj); + } + /// + /// 获取第一个标记 + /// + /// + /// + /// + public static Enum GetFirstFlag(this Enum obj) + { + return EnumHelper.GetFirstFlag(obj); + } +} diff --git a/Core/MAX.Core/Extensions/DictionaryExtension.cs b/Core/MAX.Core/Extensions/DictionaryExtension.cs new file mode 100644 index 0000000..d71917e --- /dev/null +++ b/Core/MAX.Core/Extensions/DictionaryExtension.cs @@ -0,0 +1,33 @@ +using MAX.Core.Cryptography; +using System.Collections.Generic; +using System.Text; + +namespace MAX.Core.Extensions; + +public static class DictionaryExtension +{ + /// + /// 字典转url Get 参数 + /// + /// + /// + public static string ToUrlParams(this IDictionary parameters, bool IsUrlEncode = true) + { + IDictionary sortedParams = new SortedDictionary(parameters); + IEnumerator> dem = sortedParams.GetEnumerator(); + StringBuilder sb = new(""); + while (dem.MoveNext()) + { + string key = dem.Current.Key; + string value = dem.Current.Value; + if (!string.IsNullOrEmpty(key)) + { + sb.Append(key).Append("=").Append(IsUrlEncode ? UrlHelper.Encode(value) : value).Append("&"); + + } + } + string content = sb.ToString().Substring(0, sb.Length - 1); + + return content; + } +} diff --git a/Core/MAX.Core/Helpers/Common.cs b/Core/MAX.Core/Helpers/Common.cs new file mode 100644 index 0000000..172d10d --- /dev/null +++ b/Core/MAX.Core/Helpers/Common.cs @@ -0,0 +1,55 @@ +using System; + +namespace MAX.Core.Helpers; + +/// +/// 常用公共操作 +/// +public static partial class Common +{ + /// + /// 获取类型 + /// + /// 类型 + public static Type GetType() + { + return GetType(typeof(T)); + } + + /// + /// 获取类型 + /// + /// 类型 + public static Type GetType(Type type) + { + return Nullable.GetUnderlyingType(type) ?? type; + } + + /// + /// 换行符 + /// + public static string Line => Environment.NewLine; + + +#if NET5_0 + /// + /// 是否Linux操作系统 + /// + public static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + /// + /// 是否Windows操作系统 + /// + public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + /// + /// 是否苹果操作系统 + /// + public static bool IsOsx => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + /// + /// 当前操作系统 + /// + public static string System => IsWindows ? "Windows" : IsLinux ? "Linux" : IsOsx ? "OSX" : string.Empty; +#endif +} diff --git a/Core/MAX.Core/Helpers/CommonItem.cs b/Core/MAX.Core/Helpers/CommonItem.cs new file mode 100644 index 0000000..7d83ee4 --- /dev/null +++ b/Core/MAX.Core/Helpers/CommonItem.cs @@ -0,0 +1,78 @@ +using System; + +namespace MAX.Core.Helpers; + +public class CommonItem : IComparable +{ + /// + /// 初始化 + /// + /// 文本 + /// 值 + /// 排序号 + /// 组 + /// 禁用 + public CommonItem(string text, object value, string des = "", int? sortId = null, string group = null, bool? disabled = null) + { + Text = text; + Value = value; + Description = des; + SortId = sortId; + Group = group; + Disabled = disabled; + } + + /// + /// 文本 + /// + public string Text { get; } + + /// + /// 值 + /// + public object Value { get; protected set; } + /// + /// 描述 + /// + public string Description { get; set; } + /// + /// 排序号 + /// + public int? SortId { get; set; } + + /// + /// 组 + /// + public string Group { get; } + /// + /// 是否选中 + /// + public bool IsSelected { get; set; } + /// + /// 禁用 + /// + public bool? Disabled { get; } + + /// + /// 比较 + /// + /// 其它列表项 + public int CompareTo(CommonItem other) + { + return string.Compare(Text, other.Text, StringComparison.CurrentCulture); + } +} + +public class CommonItem : CommonItem +{ + public new T Value + { + get { return (T)base.Value; } + set { base.Value = value; } + } + public CommonItem(string text, T value, string des = "", int? sortId = null, string group = null, bool? disabled = null) : base(text, value, des, sortId, group, disabled) + { + } + + +} diff --git a/Core/MAX.Core/Helpers/Convert.cs b/Core/MAX.Core/Helpers/Convert.cs new file mode 100644 index 0000000..03dc271 --- /dev/null +++ b/Core/MAX.Core/Helpers/Convert.cs @@ -0,0 +1,385 @@ +using MAX.Core.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MAX.Core.Helpers; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] +public class ConvertAttribute : Attribute +{ + public Type ConverterType { get; set; } + public ConvertAttribute(Type type) + { + ConverterType = type; + } +} +public interface IConverter +{ + object Converter(object input); +} +/// +/// 类型转换 +/// +public static partial class Convert +{ + /// + /// 转换为32位整型 + /// + /// 输入值 + public static int ToInt(object input, int defvalue = 0) + { + return ToIntOrNull(input) ?? defvalue; + } + + /// + /// 转换为32位可空整型 + /// + /// 输入值 + public static int? ToIntOrNull(object input) + { + bool success = int.TryParse(input.SafeString(), out int result); + if (success) + { + return result; + } + + try + { + double? temp = ToDoubleOrNull(input, 0); + if (temp == null) + { + return null; + } + + return System.Convert.ToInt32(temp); + } + catch + { + return null; + } + } + + /// + /// 转换为64位整型 + /// + /// 输入值 + public static long ToLong(object input, long defvalue = 0) + { + return ToLongOrNull(input) ?? defvalue; + } + + /// + /// 转换为64位可空整型 + /// + /// 输入值 + public static long? ToLongOrNull(object input) + { + bool success = long.TryParse(input.SafeString(), out long result); + if (success) + { + return result; + } + + try + { + decimal? temp = ToDecimalOrNull(input, 0); + if (temp == null) + { + return null; + } + + return System.Convert.ToInt64(temp); + } + catch + { + return null; + } + } + + /// + /// 转换为32位浮点型,并按指定小数位舍入 + /// + /// 输入值 + /// 小数位数 + public static float ToFloat(object input, int? digits = null) + { + return ToFloatOrNull(input, digits) ?? 0; + } + + /// + /// 转换为32位可空浮点型,并按指定小数位舍入 + /// + /// 输入值 + /// 小数位数 + public static float? ToFloatOrNull(object input, int? digits = null) + { + bool success = float.TryParse(input.SafeString(), out float result); + if (!success) + { + return null; + } + + if (digits == null) + { + return result; + } + + return (float)Math.Round(result, digits.Value); + } + + /// + /// 转换为64位浮点型,并按指定小数位舍入 + /// + /// 输入值 + /// 小数位数 + public static double ToDouble(object input, int? digits = null, double defvalue = 0) + { + return ToDoubleOrNull(input, digits) ?? defvalue; + } + + /// + /// 转换为64位可空浮点型,并按指定小数位舍入 + /// + /// 输入值 + /// 小数位数 + public static double? ToDoubleOrNull(object input, int? digits = null) + { + if (input is bool b) + { + return b ? 1 : 0; + } + bool success = double.TryParse(input.SafeString(), out double result); + if (!success) + { + return null; + } + + if (digits == null) + { + return result; + } + + return Math.Round(result, digits.Value); + } + + /// + /// 转换为128位浮点型,并按指定小数位舍入 + /// + /// 输入值 + /// 小数位数 + public static decimal ToDecimal(object input, int? digits = null, decimal defvalue = 0) + { + return ToDecimalOrNull(input, digits) ?? defvalue; + } + + /// + /// 转换为128位可空浮点型,并按指定小数位舍入 + /// + /// 输入值 + /// 小数位数 + public static decimal? ToDecimalOrNull(object input, int? digits = null) + { + bool success = decimal.TryParse(input.SafeString(), out decimal result); + if (!success) + { + return null; + } + + if (digits == null) + { + return result; + } + + return Math.Round(result, digits.Value); + } + + /// + /// 转换为布尔值 + /// + /// 输入值 + public static bool ToBool(object input, bool defvalue = false) + { + return ToBoolOrNull(input) ?? defvalue; + } + + /// + /// 转换为可空布尔值 + /// + /// 输入值 + public static bool? ToBoolOrNull(object input) + { + bool? value = GetBool(input); + if (value != null) + { + return value.Value; + } + + return bool.TryParse(input.SafeString(), out bool result) ? result : null; + } + + /// + /// 获取布尔值 + /// + private static bool? GetBool(object input) + { + return input.SafeString().ToLower() switch + { + "0" => false, + "否" => false, + "不" => false, + "no" => false, + "fail" => false, + "1" => true, + "是" => true, + "ok" => true, + "yes" => true, + _ => null, + }; + } + + /// + /// 转换为日期 + /// + /// 输入值 + public static DateTime ToDate(object input) + { + return ToDateOrNull(input) ?? DateTime.MinValue; + } + + /// + /// 转换为可空日期 + /// + /// 输入值 + public static DateTime? ToDateOrNull(object input) + { + return DateTime.TryParse(input.SafeString(), out DateTime result) ? result : null; + } + + /// + /// 转换为Guid + /// + /// 输入值 + public static Guid ToGuid(object input) + { + return ToGuidOrNull(input) ?? Guid.Empty; + } + + /// + /// 转换为可空Guid + /// + /// 输入值 + public static Guid? ToGuidOrNull(object input) + { + return Guid.TryParse(input.SafeString(), out Guid result) ? result : null; + } + + /// + /// 转换为Guid集合 + /// + /// 以逗号分隔的Guid集合字符串,范例:83B0233C-A24F-49FD-8083-1337209EBC9A,EAB523C6-2FE7-47BE-89D5-C6D440C3033A + public static List ToGuidList(string input) + { + return ToList(input); + } + + /// + /// 泛型集合转换 + /// + /// 目标元素类型 + /// 以逗号分隔的元素集合字符串,范例:83B0233C-A24F-49FD-8083-1337209EBC9A,EAB523C6-2FE7-47BE-89D5-C6D440C3033A + public static List ToList(string input) + { + List result = []; + if (string.IsNullOrWhiteSpace(input)) + { + return result; + } + + string[] array = input.Split(','); + result.AddRange(from each in array where !string.IsNullOrWhiteSpace(each) select To(each)); + return result; + } + + /// + /// 通用泛型转换 + /// + /// 目标类型 + /// 输入值 + public static T To(object input) + { + Type type = Common.GetType(); + return (T)To(type, input); + } + public static object To(Type type, object input) + { + object defValue = null; + if (type != null) + { + defValue = type.IsValueType ? Activator.CreateInstance(type) : null; + } + if (input == null) + { + return defValue; + } + + if (input is string && string.IsNullOrWhiteSpace(input.ToString())) + { + return defValue; + } + + string typeName = type.Name.ToLower(); + try + { + if (typeName == "string") + { + return input.ToString(); + } + + if (typeName == "guid") + { + return new Guid(input.ToString()); + } + + if (type.IsEnum) + { + return EnumHelper.Parse(type, input); + } + + if (typeof(IConverter).IsAssignableFrom(type)) + { + object instance = Activator.CreateInstance(type); + return ((IConverter)instance).Converter(input); + } + if (input is IConvertible) + { + return System.Convert.ChangeType(input, type); + } + + return input; + } + catch + { + return defValue; + } + } + /// + /// 转换为字节数组 + /// + /// 输入值 + public static byte[] ToBytes(string input) + { + return ToBytes(input, Encoding.UTF8); + } + + /// + /// 转换为字节数组 + /// + /// 输入值 + /// 字符编码 + public static byte[] ToBytes(string input, Encoding encoding) + { + return string.IsNullOrWhiteSpace(input) ? new byte[] { } : encoding.GetBytes(input); + } +} diff --git a/Core/MAX.Core/Helpers/CopyHelper.cs b/Core/MAX.Core/Helpers/CopyHelper.cs new file mode 100644 index 0000000..dfb4e5f --- /dev/null +++ b/Core/MAX.Core/Helpers/CopyHelper.cs @@ -0,0 +1,1151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace MAX.Core.Helpers; + +public class CopyIgnoreAttribute : Attribute { } + +/// +/// 拷贝帮助 +/// +public class CopyHelper +{ + /// + /// 反射深拷贝 + /// + /// + /// + /// + public static T DeepCopy(T obj) + { + return (T)DeepCopy((Object)obj, null); + } + + public static T DeepCopy(T obj, Func filter = null) + { + return (T)DeepCopy((Object)obj, filter); + } + + public static Object DeepCopy(Object originalObject, Func filter = null) + { + return InternalCopy( + originalObject, + new Dictionary(new ReferenceEqualityComparer()), + filter + ); + } + + private static readonly MethodInfo CloneMethod = typeof(Object).GetMethod( + "MemberwiseClone", + BindingFlags.NonPublic | BindingFlags.Instance + ); + + public static bool IsPrimitive(Type type) + { + if (type == typeof(String)) + { + return true; + } + + return type.IsValueType & type.IsPrimitive; + } + + private static Object InternalCopy( + Object originalObject, + IDictionary visited, + Func filter = null + ) + { + if (originalObject == null) + { + return null; + } + + Type typeToReflect = originalObject.GetType(); + if (IsPrimitive(typeToReflect)) + { + return originalObject; + } + + if (visited.ContainsKey(originalObject)) + { + return visited[originalObject]; + } + + if (typeof(Delegate).IsAssignableFrom(typeToReflect)) + { + return null; + } + + object cloneObject = CloneMethod.Invoke(originalObject, null); + if (typeToReflect.IsArray) + { + Type arrayType = typeToReflect.GetElementType(); + if (IsPrimitive(arrayType) == false) + { + Array clonedArray = (Array)cloneObject; + clonedArray.ForEach( + (array, indices) => + array.SetValue( + InternalCopy(clonedArray.GetValue(indices), visited, filter), + indices + ) + ); + } + } + visited.Add(originalObject, cloneObject); + CopyFields(originalObject, visited, cloneObject, typeToReflect, filter: filter); + RecursiveCopyBaseTypePrivateFields( + originalObject, + visited, + cloneObject, + typeToReflect, + filter + ); + return cloneObject; + } + + private static void RecursiveCopyBaseTypePrivateFields( + object originalObject, + IDictionary visited, + object cloneObject, + Type typeToReflect, + Func filter = null + ) + { + if (typeToReflect.BaseType != null) + { + RecursiveCopyBaseTypePrivateFields( + originalObject, + visited, + cloneObject, + typeToReflect.BaseType + ); + CopyFields( + originalObject, + visited, + cloneObject, + typeToReflect.BaseType, + BindingFlags.Instance | BindingFlags.NonPublic, + info => info.IsPrivate + ); + } + } + + private static void CopyFields( + object originalObject, + IDictionary visited, + object cloneObject, + Type typeToReflect, + BindingFlags bindingFlags = + BindingFlags.Instance + | BindingFlags.NonPublic + | BindingFlags.Public + | BindingFlags.FlattenHierarchy, + Func filter = null + ) + { + foreach (FieldInfo fieldInfo in typeToReflect.GetFields(bindingFlags)) + { + if (filter != null && filter(fieldInfo) == false) + { + continue; + } + + if (IsPrimitive(fieldInfo.FieldType)) + { + continue; + } + + object originalFieldValue = fieldInfo.GetValue(originalObject); + object clonedFieldValue = InternalCopy(originalFieldValue, visited, filter: filter); + fieldInfo.SetValue(cloneObject, clonedFieldValue); + } + } +} + +/// +/// Superfast deep copier class, which uses Expression trees. +/// +public static class DeepCopyByExpressionTrees +{ + private static readonly object IsStructTypeToDeepCopyDictionaryLocker = new(); + private static Dictionary IsStructTypeToDeepCopyDictionary = + []; + + private static readonly object CompiledCopyFunctionsDictionaryLocker = new(); + private static Dictionary< + Type, + Func, object> + > CompiledCopyFunctionsDictionary = + []; + + private static readonly Type ObjectType = typeof(Object); + private static readonly Type ObjectDictionaryType = typeof(Dictionary); + + /// + /// Creates a deep copy of an object. + /// + /// Object type. + /// Object to copy. + /// Dictionary of already copied objects (Keys: original objects, Values: their copies). + /// + public static T DeepCopyByExpressionTree( + this T original, + Dictionary copiedReferencesDict = null + ) + { + return (T)DeepCopyByExpressionTreeObj( + original, + false, + copiedReferencesDict + ?? new Dictionary(new ReferenceEqualityComparer()) + ); + } + + private static object DeepCopyByExpressionTreeObj( + object original, + bool forceDeepCopy, + Dictionary copiedReferencesDict + ) + { + if (original == null) + { + return null; + } + + Type type = original.GetType(); + + if (IsDelegate(type)) + { + return null; + } + + if (!forceDeepCopy && !IsTypeToDeepCopy(type)) + { + return original; + } + + + if (copiedReferencesDict.TryGetValue(original, out object alreadyCopiedObject)) + { + return alreadyCopiedObject; + } + + if (type == ObjectType) + { + return new object(); + } + + Func, object> compiledCopyFunction = GetOrCreateCompiledLambdaCopyFunction(type); + + object copy = compiledCopyFunction(original, copiedReferencesDict); + + return copy; + } + + private static Func< + object, + Dictionary, + object + > GetOrCreateCompiledLambdaCopyFunction(Type type) + { + // The following structure ensures that multiple threads can use the dictionary + // even while dictionary is locked and being updated by other thread. + // That is why we do not modify the old dictionary instance but + // we replace it with a new instance everytime. + + + if (!CompiledCopyFunctionsDictionary.TryGetValue(type, out Func, object> compiledCopyFunction)) + { + lock (CompiledCopyFunctionsDictionaryLocker) + { + if ( + !CompiledCopyFunctionsDictionary.TryGetValue(type, out compiledCopyFunction) + ) + { + Expression, object>> uncompiledCopyFunction = CreateCompiledLambdaCopyFunctionForType(type); + + compiledCopyFunction = uncompiledCopyFunction.Compile(); + + Dictionary, object>> dictionaryCopy = CompiledCopyFunctionsDictionary.ToDictionary( + pair => pair.Key, + pair => pair.Value + ); + + dictionaryCopy.Add(type, compiledCopyFunction); + + CompiledCopyFunctionsDictionary = dictionaryCopy; + } + } + } + + return compiledCopyFunction; + } + + private static Expression< + Func, object> + > CreateCompiledLambdaCopyFunctionForType(Type type) + { + + ///// INITIALIZATION OF EXPRESSIONS AND VARIABLES + + InitializeExpressions( + type, + out ParameterExpression inputParameter, + out ParameterExpression inputDictionary, + out ParameterExpression outputVariable, + out ParameterExpression boxingVariable, + out LabelTarget endLabel, + out List variables, + out List expressions + ); + + ///// RETURN NULL IF ORIGINAL IS NULL + + IfNullThenReturnNullExpression(inputParameter, endLabel, expressions); + + ///// MEMBERWISE CLONE ORIGINAL OBJECT + + MemberwiseCloneInputToOutputExpression( + type, + inputParameter, + outputVariable, + expressions + ); + + ///// STORE COPIED OBJECT TO REFERENCES DICTIONARY + + if (IsClassOtherThanString(type)) + { + StoreReferencesIntoDictionaryExpression( + inputParameter, + inputDictionary, + outputVariable, + expressions + ); + } + + ///// COPY ALL NONVALUE OR NONPRIMITIVE FIELDS + + FieldsCopyExpressions( + type, + inputParameter, + inputDictionary, + outputVariable, + boxingVariable, + expressions + ); + + ///// COPY ELEMENTS OF ARRAY + + if (IsArray(type) && IsTypeToDeepCopy(type.GetElementType())) + { + CreateArrayCopyLoopExpression( + type, + inputParameter, + inputDictionary, + outputVariable, + variables, + expressions + ); + } + + ///// COMBINE ALL EXPRESSIONS INTO LAMBDA FUNCTION + + Expression, object>> lambda = CombineAllIntoLambdaFunctionExpression( + inputParameter, + inputDictionary, + outputVariable, + endLabel, + variables, + expressions + ); + + return lambda; + } + + private static void InitializeExpressions( + Type type, + out ParameterExpression inputParameter, + out ParameterExpression inputDictionary, + out ParameterExpression outputVariable, + out ParameterExpression boxingVariable, + out LabelTarget endLabel, + out List variables, + out List expressions + ) + { + inputParameter = Expression.Parameter(ObjectType); + + inputDictionary = Expression.Parameter(ObjectDictionaryType); + + outputVariable = Expression.Variable(type); + + boxingVariable = Expression.Variable(ObjectType); + + endLabel = Expression.Label(); + + variables = []; + + expressions = []; + + variables.Add(outputVariable); + variables.Add(boxingVariable); + } + + private static void IfNullThenReturnNullExpression( + ParameterExpression inputParameter, + LabelTarget endLabel, + List expressions + ) + { + ///// Intended code: + ///// + ///// if (input == null) + ///// { + ///// return null; + ///// } + + ConditionalExpression ifNullThenReturnNullExpression = Expression.IfThen( + Expression.Equal(inputParameter, Expression.Constant(null, ObjectType)), + Expression.Return(endLabel) + ); + + expressions.Add(ifNullThenReturnNullExpression); + } + + private static void MemberwiseCloneInputToOutputExpression( + Type type, + ParameterExpression inputParameter, + ParameterExpression outputVariable, + List expressions + ) + { + ///// Intended code: + ///// + ///// var output = ()input.MemberwiseClone(); + + MethodInfo memberwiseCloneMethod = ObjectType.GetMethod( + "MemberwiseClone", + BindingFlags.NonPublic | BindingFlags.Instance + ); + + BinaryExpression memberwiseCloneInputExpression = Expression.Assign( + outputVariable, + Expression.Convert(Expression.Call(inputParameter, memberwiseCloneMethod), type) + ); + + expressions.Add(memberwiseCloneInputExpression); + } + + private static void StoreReferencesIntoDictionaryExpression( + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + List expressions + ) + { + ///// Intended code: + ///// + ///// inputDictionary[(Object)input] = (Object)output; + + BinaryExpression storeReferencesExpression = Expression.Assign( + Expression.Property( + inputDictionary, + ObjectDictionaryType.GetProperty("Item"), + inputParameter + ), + Expression.Convert(outputVariable, ObjectType) + ); + + expressions.Add(storeReferencesExpression); + } + + private static Expression< + Func, object> + > CombineAllIntoLambdaFunctionExpression( + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + LabelTarget endLabel, + List variables, + List expressions + ) + { + expressions.Add(Expression.Label(endLabel)); + + expressions.Add(Expression.Convert(outputVariable, ObjectType)); + + BlockExpression finalBody = Expression.Block(variables, expressions); + + Expression, object>> lambda = Expression.Lambda, object>>( + finalBody, + inputParameter, + inputDictionary + ); + + return lambda; + } + + private static void CreateArrayCopyLoopExpression( + Type type, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + List variables, + List expressions + ) + { + ///// Intended code: + ///// + ///// int i1, i2, ..., in; + ///// + ///// int length1 = inputarray.GetLength(0); + ///// i1 = 0; + ///// while (true) + ///// { + ///// if (i1 >= length1) + ///// { + ///// goto ENDLABELFORLOOP1; + ///// } + ///// int length2 = inputarray.GetLength(1); + ///// i2 = 0; + ///// while (true) + ///// { + ///// if (i2 >= length2) + ///// { + ///// goto ENDLABELFORLOOP2; + ///// } + ///// ... + ///// ... + ///// ... + ///// int lengthn = inputarray.GetLength(n); + ///// in = 0; + ///// while (true) + ///// { + ///// if (in >= lengthn) + ///// { + ///// goto ENDLABELFORLOOPn; + ///// } + ///// outputarray[i1, i2, ..., in] + ///// = ()DeepCopyByExpressionTreeObj( + ///// (Object)inputarray[i1, i2, ..., in]) + ///// in++; + ///// } + ///// ENDLABELFORLOOPn: + ///// ... + ///// ... + ///// ... + ///// i2++; + ///// } + ///// ENDLABELFORLOOP2: + ///// i1++; + ///// } + ///// ENDLABELFORLOOP1: + + int rank = type.GetArrayRank(); + + List indices = GenerateIndices(rank); + + variables.AddRange(indices); + + Type elementType = type.GetElementType(); + + BinaryExpression assignExpression = ArrayFieldToArrayFieldAssignExpression( + inputParameter, + inputDictionary, + outputVariable, + elementType, + type, + indices + ); + + Expression forExpression = assignExpression; + + for (int dimension = 0; dimension < rank; dimension++) + { + ParameterExpression indexVariable = indices[dimension]; + + forExpression = LoopIntoLoopExpression( + inputParameter, + indexVariable, + forExpression, + dimension + ); + } + + expressions.Add(forExpression); + } + + private static List GenerateIndices(int arrayRank) + { + ///// Intended code: + ///// + ///// int i1, i2, ..., in; + + List indices = []; + + for (int i = 0; i < arrayRank; i++) + { + ParameterExpression indexVariable = Expression.Variable(typeof(Int32)); + + indices.Add(indexVariable); + } + + return indices; + } + + private static BinaryExpression ArrayFieldToArrayFieldAssignExpression( + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + Type elementType, + Type arrayType, + List indices + ) + { + ///// Intended code: + ///// + ///// outputarray[i1, i2, ..., in] + ///// = ()DeepCopyByExpressionTreeObj( + ///// (Object)inputarray[i1, i2, ..., in]); + + IndexExpression indexTo = Expression.ArrayAccess(outputVariable, indices); + + MethodCallExpression indexFrom = Expression.ArrayIndex( + Expression.Convert(inputParameter, arrayType), + indices + ); + + bool forceDeepCopy = elementType != ObjectType; + + UnaryExpression rightSide = Expression.Convert( + Expression.Call( + DeepCopyByExpressionTreeObjMethod, + Expression.Convert(indexFrom, ObjectType), + Expression.Constant(forceDeepCopy, typeof(Boolean)), + inputDictionary + ), + elementType + ); + + BinaryExpression assignExpression = Expression.Assign(indexTo, rightSide); + + return assignExpression; + } + + private static BlockExpression LoopIntoLoopExpression( + ParameterExpression inputParameter, + ParameterExpression indexVariable, + Expression loopToEncapsulate, + int dimension + ) + { + ///// Intended code: + ///// + ///// int length = inputarray.GetLength(dimension); + ///// i = 0; + ///// while (true) + ///// { + ///// if (i >= length) + ///// { + ///// goto ENDLABELFORLOOP; + ///// } + ///// loopToEncapsulate; + ///// i++; + ///// } + ///// ENDLABELFORLOOP: + + ParameterExpression lengthVariable = Expression.Variable(typeof(Int32)); + + LabelTarget endLabelForThisLoop = Expression.Label(); + + LoopExpression newLoop = Expression.Loop( + Expression.Block( + new ParameterExpression[0], + Expression.IfThen( + Expression.GreaterThanOrEqual(indexVariable, lengthVariable), + Expression.Break(endLabelForThisLoop) + ), + loopToEncapsulate, + Expression.PostIncrementAssign(indexVariable) + ), + endLabelForThisLoop + ); + + BinaryExpression lengthAssignment = GetLengthForDimensionExpression( + lengthVariable, + inputParameter, + dimension + ); + + BinaryExpression indexAssignment = Expression.Assign(indexVariable, Expression.Constant(0)); + + return Expression.Block( + new[] { lengthVariable }, + lengthAssignment, + indexAssignment, + newLoop + ); + } + + private static BinaryExpression GetLengthForDimensionExpression( + ParameterExpression lengthVariable, + ParameterExpression inputParameter, + int i + ) + { + ///// Intended code: + ///// + ///// length = ((Array)input).GetLength(i); + + MethodInfo getLengthMethod = typeof(Array).GetMethod( + "GetLength", + BindingFlags.Public | BindingFlags.Instance + ); + + ConstantExpression dimensionConstant = Expression.Constant(i); + + return Expression.Assign( + lengthVariable, + Expression.Call( + Expression.Convert(inputParameter, typeof(Array)), + getLengthMethod, + new[] { dimensionConstant } + ) + ); + } + + private static void FieldsCopyExpressions( + Type type, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + ParameterExpression boxingVariable, + List expressions + ) + { + FieldInfo[] fields = GetAllRelevantFields(type); + + List readonlyFields = fields.Where(f => f.IsInitOnly).ToList(); + List writableFields = fields.Where(f => !f.IsInitOnly).ToList(); + + ///// READONLY FIELDS COPY (with boxing) + + bool shouldUseBoxing = readonlyFields.Any(); + + if (shouldUseBoxing) + { + BinaryExpression boxingExpression = Expression.Assign( + boxingVariable, + Expression.Convert(outputVariable, ObjectType) + ); + + expressions.Add(boxingExpression); + } + + foreach (FieldInfo field in readonlyFields) + { + if (IsDelegate(field.FieldType)) + { + ReadonlyFieldToNullExpression(field, boxingVariable, expressions); + } + else + { + ReadonlyFieldCopyExpression( + type, + field, + inputParameter, + inputDictionary, + boxingVariable, + expressions + ); + } + } + + if (shouldUseBoxing) + { + BinaryExpression unboxingExpression = Expression.Assign( + outputVariable, + Expression.Convert(boxingVariable, type) + ); + + expressions.Add(unboxingExpression); + } + + ///// NOT-READONLY FIELDS COPY + + foreach (FieldInfo field in writableFields) + { + if (IsDelegate(field.FieldType)) + { + WritableFieldToNullExpression(field, outputVariable, expressions); + } + else + { + WritableFieldCopyExpression( + type, + field, + inputParameter, + inputDictionary, + outputVariable, + expressions + ); + } + } + } + + private static FieldInfo[] GetAllRelevantFields(Type type, bool forceAllFields = false) + { + List fieldsList = []; + + Type typeCache = type; + + while (typeCache != null) + { + fieldsList.AddRange( + typeCache + .GetFields( + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.FlattenHierarchy + ) + .Where(field => forceAllFields || IsTypeToDeepCopy(field.FieldType)) + ); + + typeCache = typeCache.BaseType; + } + + return fieldsList.ToArray(); + } + + private static FieldInfo[] GetAllFields(Type type) + { + return GetAllRelevantFields(type, forceAllFields: true); + } + + private static readonly Type FieldInfoType = typeof(FieldInfo); + private static readonly MethodInfo SetValueMethod = FieldInfoType.GetMethod( + "SetValue", + new[] { ObjectType, ObjectType } + ); + + private static void ReadonlyFieldToNullExpression( + FieldInfo field, + ParameterExpression boxingVariable, + List expressions + ) + { + // This option must be implemented by Reflection because of the following: + // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f + + ///// Intended code: + ///// + ///// fieldInfo.SetValue(boxing, null); + + MethodCallExpression fieldToNullExpression = Expression.Call( + Expression.Constant(field), + SetValueMethod, + boxingVariable, + Expression.Constant(null, field.FieldType) + ); + + expressions.Add(fieldToNullExpression); + } + + private static readonly Type ThisType = typeof(DeepCopyByExpressionTrees); + private static readonly MethodInfo DeepCopyByExpressionTreeObjMethod = ThisType.GetMethod( + "DeepCopyByExpressionTreeObj", + BindingFlags.NonPublic | BindingFlags.Static + ); + + private static void ReadonlyFieldCopyExpression( + Type type, + FieldInfo field, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression boxingVariable, + List expressions + ) + { + // This option must be implemented by Reflection (SetValueMethod) because of the following: + // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f + + ///// Intended code: + ///// + ///// fieldInfo.SetValue(boxing, DeepCopyByExpressionTreeObj((Object)(()input).)) + + MemberExpression fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); + + bool forceDeepCopy = field.FieldType != ObjectType; + + MethodCallExpression fieldDeepCopyExpression = Expression.Call( + Expression.Constant(field, FieldInfoType), + SetValueMethod, + boxingVariable, + Expression.Call( + DeepCopyByExpressionTreeObjMethod, + Expression.Convert(fieldFrom, ObjectType), + Expression.Constant(forceDeepCopy, typeof(Boolean)), + inputDictionary + ) + ); + + expressions.Add(fieldDeepCopyExpression); + } + + private static void WritableFieldToNullExpression( + FieldInfo field, + ParameterExpression outputVariable, + List expressions + ) + { + ///// Intended code: + ///// + ///// output. = ()null; + + MemberExpression fieldTo = Expression.Field(outputVariable, field); + + BinaryExpression fieldToNullExpression = Expression.Assign( + fieldTo, + Expression.Constant(null, field.FieldType) + ); + + expressions.Add(fieldToNullExpression); + } + + private static void WritableFieldCopyExpression( + Type type, + FieldInfo field, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + List expressions + ) + { + ///// Intended code: + ///// + ///// output. = ()DeepCopyByExpressionTreeObj((Object)(()input).); + + MemberExpression fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); + + Type fieldType = field.FieldType; + + MemberExpression fieldTo = Expression.Field(outputVariable, field); + + bool forceDeepCopy = field.FieldType != ObjectType; + + BinaryExpression fieldDeepCopyExpression = Expression.Assign( + fieldTo, + Expression.Convert( + Expression.Call( + DeepCopyByExpressionTreeObjMethod, + Expression.Convert(fieldFrom, ObjectType), + Expression.Constant(forceDeepCopy, typeof(Boolean)), + inputDictionary + ), + fieldType + ) + ); + + expressions.Add(fieldDeepCopyExpression); + } + + private static bool IsArray(Type type) + { + return type.IsArray; + } + + private static bool IsDelegate(Type type) + { + return typeof(Delegate).IsAssignableFrom(type); + } + + private static bool IsTypeToDeepCopy(Type type) + { + return IsClassOtherThanString(type) || IsStructWhichNeedsDeepCopy(type); + } + + private static bool IsClassOtherThanString(Type type) + { + return !type.IsValueType && type != typeof(String); + } + + private static bool IsStructWhichNeedsDeepCopy(Type type) + { + // The following structure ensures that multiple threads can use the dictionary + // even while dictionary is locked and being updated by other thread. + // That is why we do not modify the old dictionary instance but + // we replace it with a new instance everytime. + + + if (!IsStructTypeToDeepCopyDictionary.TryGetValue(type, out bool isStructTypeToDeepCopy)) + { + lock (IsStructTypeToDeepCopyDictionaryLocker) + { + if ( + !IsStructTypeToDeepCopyDictionary.TryGetValue( + type, + out isStructTypeToDeepCopy + ) + ) + { + isStructTypeToDeepCopy = IsStructWhichNeedsDeepCopy_NoDictionaryUsed(type); + + Dictionary newDictionary = IsStructTypeToDeepCopyDictionary.ToDictionary( + pair => pair.Key, + pair => pair.Value + ); + + newDictionary[type] = isStructTypeToDeepCopy; + + IsStructTypeToDeepCopyDictionary = newDictionary; + } + } + } + + return isStructTypeToDeepCopy; + } + + private static bool IsStructWhichNeedsDeepCopy_NoDictionaryUsed(Type type) + { + return IsStructOtherThanBasicValueTypes(type) + && HasInItsHierarchyFieldsWithClasses(type); + } + + private static bool IsStructOtherThanBasicValueTypes(Type type) + { + return type.IsValueType && !type.IsPrimitive && !type.IsEnum && type != typeof(Decimal); + } + + private static bool HasInItsHierarchyFieldsWithClasses( + Type type, + HashSet alreadyCheckedTypes = null + ) + { + alreadyCheckedTypes ??= []; + + alreadyCheckedTypes.Add(type); + + FieldInfo[] allFields = GetAllFields(type); + + List allFieldTypes = allFields.Select(f => f.FieldType).Distinct().ToList(); + + bool hasFieldsWithClasses = allFieldTypes.Any(IsClassOtherThanString); + + if (hasFieldsWithClasses) + { + return true; + } + + List notBasicStructsTypes = allFieldTypes + .Where(IsStructOtherThanBasicValueTypes) + .ToList(); + + List typesToCheck = notBasicStructsTypes + .Where(t => !alreadyCheckedTypes.Contains(t)) + .ToList(); + + foreach (Type typeToCheck in typesToCheck) + { + if (HasInItsHierarchyFieldsWithClasses(typeToCheck, alreadyCheckedTypes)) + { + return true; + } + } + + return false; + } + + public class ReferenceEqualityComparer : EqualityComparer + { + public override bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + + public override int GetHashCode(object obj) + { + if (obj == null) + { + return 0; + } + + return obj.GetHashCode(); + } + } +} + +public class ReferenceEqualityComparer : EqualityComparer +{ + public override bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + + public override int GetHashCode(object obj) + { + if (obj == null) + { + return 0; + } + + return obj.GetHashCode(); + } +} + +public static class ArrayExtensions +{ + public static void ForEach(this Array array, Action action) + { + if (array.LongLength == 0) + { + return; + } + + ArrayTraverse walker = new(array); + do + { + action(array, walker.Position); + } + while (walker.Step()); + } +} + +internal class ArrayTraverse +{ + public int[] Position; + private readonly int[] maxLengths; + + public ArrayTraverse(Array array) + { + maxLengths = new int[array.Rank]; + for (int i = 0; i < array.Rank; ++i) + { + maxLengths[i] = array.GetLength(i) - 1; + } + Position = new int[array.Rank]; + } + + public bool Step() + { + for (int i = 0; i < Position.Length; ++i) + { + if (Position[i] < maxLengths[i]) + { + Position[i]++; + for (int j = 0; j < i; j++) + { + Position[j] = 0; + } + return true; + } + } + return false; + } +} diff --git a/Core/MAX.Core/Helpers/DateHelper.cs b/Core/MAX.Core/Helpers/DateHelper.cs new file mode 100644 index 0000000..d9eb110 --- /dev/null +++ b/Core/MAX.Core/Helpers/DateHelper.cs @@ -0,0 +1,110 @@ +using System; + +namespace MAX.Core.Helpers; + +public class DateHelper +{ + /// + /// Unix纪元时间 + /// + private static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// 将时间转换为秒级时间戳 + /// + /// + /// + public static long GetUnixTimestampInSeconds(DateTime dateTime) + { + TimeSpan timeSpan = dateTime.ToUniversalTime() - UnixEpoch; + return (long)timeSpan.TotalSeconds; + } + + /// + /// 将时间转换为毫秒级时间戳 + /// + /// + /// + public static long GetUnixTimestampInMilliseconds(DateTime dateTime) + { + TimeSpan timeSpan = dateTime.ToUniversalTime() - UnixEpoch; + return (long)timeSpan.TotalMilliseconds; + } + + /// + /// 将时间转换为微秒时间戳 + /// + /// + /// + public static long GetUnixTimestampInMicroseconds(DateTime dateTime) + { + long us = dateTime.ToUniversalTime().Ticks - UnixEpoch.Ticks; + return us; + } + /// + /// 将时间Ticks转换为毫微秒时间戳 + /// + /// + /// + public static long GetUnixTimestampInMicroseconds(long ticks) + { + long us = ticks - UnixEpoch.Ticks; + return us; + } + + /// + /// 将秒级时间戳转换为时间 + /// + /// + /// + public static DateTime ConvertFromUnixTimestampInSeconds(long timestamp) + { + return UnixEpoch.AddSeconds(timestamp).ToLocalTime(); + } + + /// + /// 将毫秒级时间戳转换为时间 + /// + /// + /// + public static DateTime ConvertFromUnixTimestampInMilliseconds(long timestamp) + { + return UnixEpoch.AddMilliseconds(timestamp).ToLocalTime(); + } + /// + /// 将微秒级时间戳转换为时间 + /// + /// + /// + public static DateTime ConvertFromUnixTimestampInMicroseconds(long timestamp) + { + long us = timestamp + UnixEpoch.Ticks; + return new DateTime(us); + } + /// + /// 获取当前时间的秒级时间戳 + /// + /// + public static long GetCurrentUnixTimestampInSeconds() + { + return GetUnixTimestampInSeconds(DateTime.UtcNow); + } + + /// + /// 获取当前时间的毫秒级时间戳 + /// + /// + public static long GetCurrentUnixTimestampInMilliseconds() + { + return GetUnixTimestampInMilliseconds(DateTime.UtcNow); + } + + /// + /// 获取当前时间的微秒级时间戳 + /// + /// + public static long GetCurrentUnixTimestampInMicroseconds() + { + return GetUnixTimestampInMicroseconds(DateTime.UtcNow); + } +} diff --git a/Core/MAX.Core/Helpers/EnumHelper.cs b/Core/MAX.Core/Helpers/EnumHelper.cs new file mode 100644 index 0000000..439042d --- /dev/null +++ b/Core/MAX.Core/Helpers/EnumHelper.cs @@ -0,0 +1,341 @@ +using MAX.Core.Extensions; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace MAX.Core.Helpers; + +/// +/// 枚举操作 +/// +public static partial class EnumHelper +{ + /// + /// 获取实例 + /// + /// 枚举类型 + /// 成员名或值,范例:Enum1枚举有成员A=0,则传入"A"或"0"获取 Enum1.A + public static TEnum Parse(object member) + { + string value = member.SafeString(); + if (string.IsNullOrWhiteSpace(value)) + { + if (typeof(TEnum).IsGenericType) + { + return default; + } + + throw new ArgumentNullException(nameof(member)); + } + return (TEnum)Enum.Parse(Common.GetType(), value, true); + } + public static object Parse(Type enumObj, object member) + { + string value = member.SafeString(); + if (string.IsNullOrWhiteSpace(value)) + { + if (enumObj.IsGenericType) + { + return null; + } + + throw new ArgumentNullException(nameof(member)); + } + return Enum.Parse(enumObj, value, true); + } + public static TEnum ParseByDescription(string description) + { + return (TEnum)ParseByDescription(Common.GetType(), description); + } + public static object ParseByDescription(Type enumObj, string description) + { + List listitem = GetItems(enumObj); + foreach (EnumItem item in listitem) + { + if (item.Description == description) + { + return Enum.Parse(enumObj, item.Numeric.ToString(), true); + } + } + return null; + } + /// + /// 获取成员名 + /// + /// 枚举类型 + /// 成员名、值、实例均可,范例:Enum1枚举有成员A=0,则传入Enum1.A或0,获取成员名"A" + public static string GetName(object member) + { + return GetName(Common.GetType(), member); + } + + /// + /// 获取成员名 + /// + /// 枚举类型 + /// 成员名、值、实例均可 + public static string GetName(Type type, object member) + { + if (type == null) + { + return string.Empty; + } + + if (member == null) + { + return string.Empty; + } + + if (member is string) + { + return member.ToString(); + } + + if (type.GetTypeInfo().IsEnum == false) + { + return string.Empty; + } + + return Enum.GetName(type, member); + } + + /// + /// 获取成员值 + /// + /// 枚举类型 + /// 成员名、值、实例均可,范例:Enum1枚举有成员A=0,可传入"A"、0、Enum1.A,获取值0 + public static int GetValue(object member) + { + return GetValue(Common.GetType(), member); + } + + /// + /// 获取成员值 + /// + /// 枚举类型 + /// 成员名、值、实例均可 + public static int GetValue(Type type, object member) + { + string value = member.SafeString(); + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(nameof(member)); + } + + return (int)Enum.Parse(type, member.ToString(), true); + } + /// + /// 获取描述,使用System.ComponentModel.Description特性设置描述 + /// + /// 枚举类型 + /// 成员名、值、实例均可 + public static string GetDescription(object member) + { + return ReflectionHelper.GetDescription(GetName(member)); + } + + /// + /// 获取描述,使用System.ComponentModel.Description特性设置描述 + /// + /// 枚举类型 + /// 成员名、值、实例均可 + public static string GetDescription(Type type, object member) + { + return ReflectionHelper.GetDescription(type, GetName(type, member)); + } + + + /// + /// 获取项集合,文本设置为Description,值为Value + /// + /// 枚举类型 + public static List GetItems() + { + return GetItems(typeof(TEnum)); + } + + /// + /// 获取项集合,文本设置为Description,值为Value + /// + /// 枚举类型 + public static List GetItems(Type type) + { + type = Common.GetType(type); + if (type.IsEnum == false) + { + throw new InvalidOperationException($"类型 {type} 不是枚举"); + } + + List result = []; + foreach (FieldInfo field in type.GetFields()) + { + AddItem(type, result, field); + } + + return result.OrderBy(t => t.Numeric).ToList(); + } + + public static CommonItem GetItem(object obj) + { + CommonItem item = null; + if (obj is Enum em) + { + //item.Text = em.get + string text = em.ToString(); + int value = GetValue(obj.GetType(), obj); + string description = em.ToDescription(); + item = new CommonItem(text, value, des: description); + } + return item; + } + + /// + /// 添加描述项 + /// + private static void AddItem(Type type, ICollection result, FieldInfo field) + { + if (!field.FieldType.IsEnum) + { + return; + } + + string text = field.Name; + int value = GetValue(type, field.Name); + string description = ReflectionHelper.GetDescription(field); + result.Add(new EnumItem(text, value, description)); + } + + /// + /// 获取名称集合 + /// + /// 枚举类型 + public static List GetNames() + { + return GetNames(typeof(TEnum)); + } + + /// + /// 获取名称集合 + /// + /// 枚举类型 + public static List GetNames(Type type) + { + type = Common.GetType(type); + if (type.IsEnum == false) + { + throw new InvalidOperationException($"类型 {type} 不是枚举"); + } + + List result = []; + foreach (FieldInfo field in type.GetFields()) + { + if (!field.FieldType.IsEnum) + { + continue; + } + + result.Add(field.Name); + } + return result; + } + + + /// + /// 获取名称集合 + /// + /// 枚举类型 + public static List GetDescriptions() + { + return GetDescriptions(typeof(TEnum)); + } + + /// + /// 获取名称集合 + /// + /// 枚举类型 + public static List GetDescriptions(Type type) + { + type = Common.GetType(type); + if (type.IsEnum == false) + { + throw new InvalidOperationException($"类型 {type} 不是枚举"); + } + + List result = []; + foreach (FieldInfo field in type.GetFields()) + { + if (!field.FieldType.IsEnum) + { + continue; + } + + string description = ReflectionHelper.GetDescription(field); + result.Add(description); + } + return result; + } + /// + /// 是否单一标记 + /// + /// + /// + public static bool IsSingleFlag(Enum obj) + { + int intValue = GetValue(obj.GetType(), obj); + return (intValue > 0) && ((intValue & (intValue - 1)) == 0); + } + /// + /// 获取第一个标记 + /// + /// + /// + /// + public static T GetFirstFlag(T value) where T : Enum + { + long v = System.Convert.ToInt64(value); + long first = v & -v; + return Enum.GetValues(typeof(T)).Cast().First(flag => System.Convert.ToInt64(flag) == first); + } +} +/// +/// Enum展示类型 +/// +public enum EnumDisplayType +{ + Text, + Value, + Description, + CommonItem +} +public class EnumItem +{ + public string Name { get; init; } + public int Numeric { get; init; } + public string Description { get; init; } + + public EnumItem(string name, int numeric, string description) + { + Name = name; + Numeric = numeric; + Description = description; + } +} +public class EnumItem : EnumItem where TEnum : struct, Enum +{ + public TEnum Value { get; } + + public EnumItem(TEnum value) + : base(value.ToString(), System.Convert.ToInt32(value), GetDescription(value)) + { + Value = value; + } + private static string GetDescription(TEnum value) + { + var field = typeof(TEnum).GetField(value.ToString())!; + var attr = field.GetCustomAttribute(); + return attr?.Description ?? string.Empty; + } + +} \ No newline at end of file diff --git a/Core/MAX.Core/Helpers/EnvironmentHelper.cs b/Core/MAX.Core/Helpers/EnvironmentHelper.cs new file mode 100644 index 0000000..0435141 --- /dev/null +++ b/Core/MAX.Core/Helpers/EnvironmentHelper.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MAX.Core.Helpers; +public class EnvironmentHelper +{ + private static string? _currentEnvironment; + + // 获取当前环境 + public static string CurrentEnvironment + { + get + { + if (_currentEnvironment == null) + { + _currentEnvironment = DetermineEnvironment(); + } + return _currentEnvironment; + } + } + public static bool IsTest => CurrentEnvironment.Equals("Test", StringComparison.OrdinalIgnoreCase); + public static bool IsDevelopment => CurrentEnvironment.Equals("Development", StringComparison.OrdinalIgnoreCase); + + public static bool IsProduction => CurrentEnvironment.Equals("Production", StringComparison.OrdinalIgnoreCase); + + private static string DetermineEnvironment() + { + // 1. 检查环境变量 + var envVar = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? + Environment.GetEnvironmentVariable("ENVIRONMENT"); + + if (!string.IsNullOrEmpty(envVar)) + { + return envVar; + } + + + // 检查当前机器名 + var machineName = Environment.MachineName; + if (machineName.Contains("DEV", StringComparison.OrdinalIgnoreCase) || + machineName.Contains("TEST", StringComparison.OrdinalIgnoreCase)) + { + return machineName.Contains("TEST") ? "Test" : "Development"; + } + + // 4. 默认返回 Production + return "Production"; + } + + // 获取环境描述 + public static string EnvironmentDescription + { + get + { + if (IsTest) return "测试环境"; + if (IsDevelopment) return "开发环境"; + if (IsProduction) return "生产环境"; + return $"未知环境 ({CurrentEnvironment})"; + } + } +} diff --git a/Core/MAX.Core/Helpers/FileHelper.cs b/Core/MAX.Core/Helpers/FileHelper.cs new file mode 100644 index 0000000..e03a4d6 --- /dev/null +++ b/Core/MAX.Core/Helpers/FileHelper.cs @@ -0,0 +1,188 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace MAX.Core.Helpers; + +/// +/// 文件和流操作 +/// +public static partial class FileHelper +{ + /// + /// 流转换为字节流 + /// + /// 流 + public static async Task ToBytesAsync(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + byte[] buffer = new byte[stream.Length]; + await stream.ReadAsync(buffer, 0, buffer.Length); + return buffer; + } + + /// + /// 流转换为字节流 + /// + /// 流 + public static byte[] ToBytes(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + byte[] buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return buffer; + } + + /// + /// 字符串转换成字节数组 + /// + /// 数据,默认字符编码utf-8 + public static byte[] ToBytes(string data) + { + return ToBytes(data, Encoding.UTF8); + } + + /// + /// 字符串转换成字节数组 + /// + /// 数据 + /// 字符编码 + public static byte[] ToBytes(string data, Encoding encoding) + { + if (string.IsNullOrWhiteSpace(data)) + { + return new byte[] { }; + } + + return encoding.GetBytes(data); + } + + /// + /// 将文件读取到字节流中 + /// + /// 文件的绝对路径 + public static byte[] Read(string filePath) + { + if (!File.Exists(filePath)) + { + return null; + } + + FileInfo fileInfo = new(filePath); + using BinaryReader reader = new(fileInfo.Open(FileMode.Open)); + return reader.ReadBytes((int)fileInfo.Length); + } + + /// + /// 流转换成字符串 + /// + /// 流 + /// 字符编码 + /// 缓冲区大小 + /// 读取完成是否释放流,默认为true + public static string ToString(Stream stream, Encoding encoding = null, int bufferSize = 1024 * 2, bool isCloseStream = true) + { + if (stream == null) + { + return string.Empty; + } + + encoding ??= Encoding.UTF8; + + if (stream.CanRead == false) + { + return string.Empty; + } + + using StreamReader reader = new(stream, encoding, true, bufferSize, !isCloseStream); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + string result = reader.ReadToEnd(); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + return result; + } + + /// + /// 流转换成字符串 + /// + /// 流 + /// 字符编码 + /// 缓冲区大小 + /// 读取完成是否释放流,默认为true + public static async Task ToStringAsync(Stream stream, Encoding encoding = null, int bufferSize = 1024 * 2, bool isCloseStream = true) + { + if (stream == null) + { + return string.Empty; + } + + encoding ??= Encoding.UTF8; + + if (stream.CanRead == false) + { + return string.Empty; + } + + using StreamReader reader = new(stream, encoding, true, bufferSize, !isCloseStream); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + string result = await reader.ReadToEndAsync(); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + return result; + } + + /// + /// 复制流并转换成字符串 + /// + /// 流 + /// 字符编码 + public static async Task CopyToStringAsync(Stream stream, Encoding encoding = null) + { + if (stream == null) + { + return string.Empty; + } + + encoding ??= Encoding.UTF8; + + if (stream.CanRead == false) + { + return string.Empty; + } + + using MemoryStream memoryStream = new(); + using StreamReader reader = new(memoryStream, encoding); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + stream.CopyTo(memoryStream); + if (memoryStream.CanSeek) + { + memoryStream.Seek(0, SeekOrigin.Begin); + } + + string result = await reader.ReadToEndAsync(); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + return result; + } +} diff --git a/Core/MAX.Core/Helpers/HttpHelper.cs b/Core/MAX.Core/Helpers/HttpHelper.cs new file mode 100644 index 0000000..f85098f --- /dev/null +++ b/Core/MAX.Core/Helpers/HttpHelper.cs @@ -0,0 +1,1047 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; + +namespace MAX.Core.Helpers; + + +/// +/// Http连接操作帮助类 +/// +public class HttpHelper +{ + public static HttpResult Get(string Url) + { + return new HttpHelper().GetHtml(new HttpItem + { + URL = Url, + Method = MethodType.Get, + }); + } + + public static HttpResult Post(string Url, string PostData) + { + return new HttpHelper().GetHtml(new HttpItem + { + URL = Url, + Method = MethodType.Get, + Postdata = PostData, + PostDataType = PostDataType.String + }); + } + public static HttpResult Request(HttpItem item) + { + return new HttpHelper().GetHtml(item); + } + /// + /// 下载文件 + /// + /// + /// + public static byte[] DownloadFile(string address) + { + HttpItem item = new() + { + URL = address, + Timeout = 3000, + ResultType = ResultType.Byte + }; + HttpHelper http = new(); + HttpResult result = http.GetHtml(item); + if (result.StatusCode == HttpStatusCode.OK) + { + return result.ResultByte; + } + else + { + throw new HttpListenerException((int)result.StatusCode, result.StatusDescription); + } + } + + /// + /// 下载文件 + /// + /// + /// + public static bool DownloadFile(string address, string fileName) + { + HttpItem item = new() + { + URL = address, + Timeout = 3000, + ResultType = ResultType.Byte + }; + HttpHelper http = new(); + HttpResult result = http.GetHtml(item); + if (result.StatusCode == HttpStatusCode.OK) + { + FileStream fs = new(fileName, FileMode.CreateNew); + BinaryWriter bw = new(fs); + bw.Write(result.ResultByte); + fs.Close(); + bw.Close(); + } + else + { + throw new HttpListenerException((int)result.StatusCode, result.StatusDescription); + } + + return true; + } + + #region 预定义方法或者变更 + //默认的编码 + private Encoding encoding = Encoding.Default; + //Post数据编码 + private Encoding postencoding = Encoding.Default; + //HttpWebRequest对象用来发起请求 + private HttpWebRequest request = null; + //获取影响流的数据对象 + private HttpWebResponse response = null; + /// + /// 根据相传入的数据,得到相应页面数据 + /// + /// 参数类对象 + /// 返回HttpResult类型 + public HttpResult GetHtml(HttpItem item) + { + //返回参数 + HttpResult result = new() + { + item = item + }; + try + { + //准备参数 + SetRequest(item); + } + catch (Exception ex) + { + result = new HttpResult + { + Cookie = string.Empty, + Header = null, + Html = ex.Message, + StatusDescription = "配置参数时出错:" + ex.Message + }; + return result; + } + try + { + //请求数据 + using (response = (HttpWebResponse)request.GetResponse()) + { + GetData(item, result); + } + //#region 得到请求的response + //using (response = (HttpWebResponse)request.GetResponse()) + //{ + // result.StatusCode = response.StatusCode; + // result.StatusDescription = response.StatusDescription; + // result.Header = response.Headers; + // if (response.Cookies != null) result.CookieCollection = response.Cookies; + // if (response.Headers["set-cookie"] != null) result.Cookie = response.Headers["set-cookie"]; + // MemoryStream _stream = new MemoryStream(); + // //GZIIP处理 + // if (response.ContentEncoding != null && response.ContentEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)) + // { + // //开始读取流并设置编码方式 + // //new GZipStream(response.GetResponseStream(), CompressionMode.Decompress).CopyTo(_stream, 10240); + // //.net4.0以下写法 + // _stream = GetMemoryStream(new GZipStream(response.GetResponseStream(), CompressionMode.Decompress)); + + // } + // else + // { + // //开始读取流并设置编码方式 + // //response.GetResponseStream().CopyTo(_stream, 10240); + // //.net4.0以下写法 + // _stream = GetMemoryStream(response.GetResponseStream()); + // } + // //获取Byte + // byte[] ResponseByte = _stream.ToArray(); + // _stream.Close(); + // if (ResponseByte != null & ResponseByte.Length > 0) + // { + // //是否返回Byte类型数据 + // if (item.ResultType == ResultType.Byte) result.ResultByte = ResponseByte; + // //从这里开始我们要无视编码了 + // if (encoding == null) + // { + // Match meta = Regex.Match(Encoding.Default.GetString(ResponseByte), " 1) ? meta.Groups[2].Value.ToLower().Trim() : string.Empty; + // if (c.Length > 2) + // { + // try + // { + // if (c.IndexOf(" ") > 0) c = c.Substring(0, c.IndexOf(" ")); + // encoding = Encoding.GetEncoding(c.Replace("\"", "").Replace("'", "").Replace(";", "").Replace("iso-8859-1", "gbk").Trim()); + // } + // catch + // { + // if (string.IsNullOrEmpty(response.CharacterSet)) encoding = Encoding.UTF8; + // else encoding = Encoding.GetEncoding(response.CharacterSet); + // } + // } + // else + // { + // if (string.IsNullOrEmpty(response.CharacterSet)) encoding = Encoding.UTF8; + // else encoding = Encoding.GetEncoding(response.CharacterSet); + // } + // } + // //得到返回的HTML + // result.Html = encoding.GetString(ResponseByte); + // } + // else + // { + // //得到返回的HTML + // result.Html = "本次请求并未返回任何数据"; + // } + //} + //#endregion + } + catch (WebException ex) + { + if (ex.Response != null) + { + using (response = (HttpWebResponse)ex.Response) + { + GetData(item, result); + } + } + else + { + result.Html = ex.Message; + } + + } + catch (Exception ex) + { + result.Html = ex.Message; + } + if (item.IsToLower) + { + result.Html = result.Html.ToLower(); + } + + return result; + } + + #region GetData + /// + /// 获取数据的并解析的方法 + /// + /// + /// + private void GetData(HttpItem item, HttpResult result) + { + if (response == null) + { + return; + } + #region base + //获取StatusCode + result.StatusCode = response.StatusCode; + //获取最后访问的URl + //获取StatusDescription + result.StatusDescription = response.StatusDescription; + //获取Headers + result.Header = response.Headers; + //获取CookieCollection + if (response.Cookies != null) + { + result.CookieCollection = response.Cookies; + } + //获取set-cookie + if (response.Headers["set-cookie"] != null) + { + result.Cookie = response.Headers["set-cookie"]; + } + + #endregion + #region 用户设置用编码 + //处理网页Byte + byte[] ResponseByte = GetByte(item); + + if (ResponseByte != null && ResponseByte.Length > 0) + { + //设置编码 + SetEncoding(item, result, ResponseByte); + + //设置返回的Byte + SetResultByte(item, result, ResponseByte); + } + else + { + //没有返回任何Html代码 + result.Html = string.Empty; + } + + #endregion + } + /// + /// 设置返回的Byte + /// + /// HttpItem + /// result + /// byte + private void SetResultByte(HttpItem item, HttpResult result, byte[] enByte) + { + //是否返回Byte类型数据 + if (item.ResultType == ResultType.Byte) + { + //Byte数据 + result.ResultByte = enByte; + } + else if (item.ResultType == ResultType.String) + { + //得到返回的HTML + result.Html = encoding.GetString(enByte); + } + + } + /// + /// 设置编码 + /// + /// HttpItem + /// HttpResult + /// byte[] + private void SetEncoding(HttpItem item, HttpResult result, byte[] ResponseByte) + { + //从这里开始我们要无视编码了 + if (encoding == null) + { + Match meta = Regex.Match(Encoding.Default.GetString(ResponseByte), " 0) + { + c = meta.Groups[1].Value.ToLower().Trim(); + } + string cs = string.Empty; + if (!string.IsNullOrWhiteSpace(response.CharacterSet)) + { + cs = response.CharacterSet.Trim().Replace("\"", "").Replace("\'", ""); + } + + if (c.Length > 2) + { + try + { + encoding = Encoding.GetEncoding(c.Replace("\"", string.Empty).Replace("'", "").Replace(";", "").Replace("iso-8859-1", "gbk").Trim()); + } + catch + { + if (string.IsNullOrEmpty(cs)) + { + encoding = Encoding.UTF8; + } + else + { + encoding = Encoding.GetEncoding(cs); + } + } + } + else + { + if (string.IsNullOrEmpty(cs)) + { + encoding = Encoding.UTF8; + } + else + { + encoding = Encoding.GetEncoding(cs); + } + } + } + } + /// + /// 提取网页Byte + /// + /// + private byte[] GetByte(HttpItem item) + { + byte[] ResponseByte = null; + using (MemoryStream _stream = new()) + { + + //GZIIP处理 + if (response.ContentEncoding != null && response.ContentEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)) + { + //开始读取流并设置编码方式 + new GZipStream(response.GetResponseStream(), CompressionMode.Decompress).CopyTo(_stream, 10240); + } + else + { + //开始读取流并设置编码方式 + response.GetResponseStream().CopyTo(_stream, 10240); + } + + //获取Byte + ResponseByte = _stream.ToArray(); + } + return ResponseByte; + } + #endregion + + + /// + /// 4.0以下.net版本取数据使用 + /// + /// 流 + private MemoryStream GetMemoryStream(Stream streamResponse) + { + MemoryStream _stream = new(); + int Length = 256; + Byte[] buffer = new Byte[Length]; + int bytesRead = streamResponse.Read(buffer, 0, Length); + while (bytesRead > 0) + { + _stream.Write(buffer, 0, bytesRead); + bytesRead = streamResponse.Read(buffer, 0, Length); + } + return _stream; + } + /// + /// 为请求准备参数 + /// + ///参数列表 + private void SetRequest(HttpItem item) + + { + // 验证证书 + SetCer(item); + //设置Header参数 + if (item.Header != null && item.Header.Count > 0) + { + foreach (string key in item.Header.AllKeys) + { + request.Headers.Add(key, item.Header[key]); + } + } + // 设置代理 + SetProxy(item); + if (item.ProtocolVersion != null) + { + request.ProtocolVersion = item.ProtocolVersion; + } + + request.ServicePoint.Expect100Continue = item.Expect100Continue; + //请求方式Get或者Post + request.Method = item.Method.ToString(); + request.Timeout = item.Timeout; + request.KeepAlive = item.KeepAlive; + request.ReadWriteTimeout = item.ReadWriteTimeout; + //Accept + request.Accept = item.Accept; + //ContentType返回类型 + request.ContentType = item.ContentType; + //UserAgent客户端的访问类型,包括浏览器版本和操作系统信息 + request.UserAgent = item.UserAgent; + // 编码 + encoding = item.Encoding; + //设置安全凭证 + request.Credentials = item.ICredentials; + //设置Cookie + SetCookie(item); + //来源地址 + request.Referer = item.Referer; + //是否执行跳转功能 + request.AllowAutoRedirect = item.Allowautoredirect; + //设置Post数据 + SetPostData(item); + //设置最大连接 + if (item.Connectionlimit > 0) + { + request.ServicePoint.ConnectionLimit = item.Connectionlimit; + } + } + /// + /// 设置证书 + /// + /// +#pragma warning disable SYSLIB0014 // Type or member is obsolete + private void SetCer(HttpItem item) + { + if (!string.IsNullOrEmpty(item.CerPath)) + { + //这一句一定要写在创建连接的前面。使用回调的方法进行证书验证。 + ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult); + //初始化对像,并设置请求的URL地址 + request = (HttpWebRequest)WebRequest.Create(item.URL); + SetCerList(item); + //将证书添加到请求里 + request.ClientCertificates.Add(new X509Certificate(item.CerPath)); + } + else + { + //初始化对像,并设置请求的URL地址 + request = (HttpWebRequest)WebRequest.Create(item.URL); + SetCerList(item); + } + } +#pragma warning restore SYSLIB0014 // Type or member is obsolete + /// + /// 设置多个证书 + /// + /// + private void SetCerList(HttpItem item) + { + if (item.ClentCertificates != null && item.ClentCertificates.Count > 0) + { + foreach (X509Certificate c in item.ClentCertificates) + { + request.ClientCertificates.Add(c); + } + } + } + /// + /// 设置Cookie + /// + /// Http参数 + private void SetCookie(HttpItem item) + { + if (!string.IsNullOrEmpty(item.Cookie)) + { + request.Headers[HttpRequestHeader.Cookie] = item.Cookie; + } + //设置CookieCollection + if (item.ResultCookieType == ResultCookieType.CookieCollection) + { + request.CookieContainer = new CookieContainer(); + if (item.CookieCollection != null && item.CookieCollection.Count > 0) + { + request.CookieContainer.Add(item.CookieCollection); + } + } + } + /// + /// 设置Post数据 + /// + /// Http参数 + private void SetPostData(HttpItem item) + { + //验证在得到结果时是否有传入数据 + if (request.Method.Trim().ToLower().Contains("post")) + { + if (item.PostEncoding != null) + { + postencoding = item.PostEncoding; + } + byte[] buffer = null; + //写入Byte类型 + if (item.PostDataType == PostDataType.Byte && item.PostdataByte != null && item.PostdataByte.Length > 0) + { + //验证在得到结果时是否有传入数据 + buffer = item.PostdataByte; + }//写入文件 + else if (item.PostDataType == PostDataType.FilePath && !string.IsNullOrEmpty(item.Postdata)) + { + StreamReader r = new(item.Postdata, postencoding); + buffer = postencoding.GetBytes(r.ReadToEnd()); + r.Close(); + } //写入字符串 + else if (!string.IsNullOrEmpty(item.Postdata)) + { + buffer = postencoding.GetBytes(item.Postdata); + } + if (buffer != null) + { + request.ContentLength = buffer.Length; + request.GetRequestStream().Write(buffer, 0, buffer.Length); + } + } + } + /// + /// 设置代理 + /// + /// 参数对象 + private void SetProxy(HttpItem item) + { + bool isIeProxy = item.ProxyIp.ToLower().Contains("ieproxy"); + if (!string.IsNullOrEmpty(item.ProxyIp) && !isIeProxy) + { + //设置代理服务器 + if (item.ProxyIp.Contains(":")) + { + string[] plist = item.ProxyIp.Split(':'); + WebProxy myProxy = new(plist[0].Trim(), Convert.ToInt(plist[1].Trim())) + { + //建议连接 + Credentials = new NetworkCredential(item.ProxyUserName, item.ProxyPwd) + }; + //给当前请求对象 + request.Proxy = myProxy; + } + else + { + WebProxy myProxy = new(item.ProxyIp, false) + { + //建议连接 + Credentials = new NetworkCredential(item.ProxyUserName, item.ProxyPwd) + }; + //给当前请求对象 + request.Proxy = myProxy; + } + } + else if (isIeProxy) + { + //设置为IE代理 + } + else + { + request.Proxy = item.WebProxy; + } + } + /// + /// 回调验证证书问题 + /// + /// 流对象 + /// 证书 + /// X509Chain + /// SslPolicyErrors + /// bool + public bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { return true; } + #endregion +} +/// +/// Http请求参考类 +/// +public class HttpItem +{ + private string _URL = string.Empty; + /// + /// 请求URL必须填写 + /// + public string URL + { + get { return _URL; } + set { _URL = value; } + } + + private MethodType _Method = MethodType.Get; + /// + /// 请求方式默认为GET方式,当为POST方式时必须设置Postdata的值 + /// + public MethodType Method + { + get { return _Method; } + set + { + _Method = value; + if (_Method == MethodType.Post) + { + _ContentType = "application/x-www-form-urlencoded"; + } + } + } + + private int _Timeout = 100000; + /// + /// 默认请求超时时间 + /// + public int Timeout + { + get { return _Timeout; } + set { _Timeout = value; } + } + + private int _ReadWriteTimeout = 30000; + /// + /// 默认写入Post数据超时间 + /// + public int ReadWriteTimeout + { + get { return _ReadWriteTimeout; } + set { _ReadWriteTimeout = value; } + } + + private Boolean _KeepAlive = true; + /// + /// 获取或设置一个值,该值指示是否与 Internet 资源建立持久性连接默认为true。 + /// + public Boolean KeepAlive + { + get { return _KeepAlive; } + set { _KeepAlive = value; } + } + + private string _Accept = "text/html, application/xhtml+xml, */*"; + /// + /// 请求标头值 默认为text/html, application/xhtml+xml, */* + /// + public string Accept + { + get { return _Accept; } + set { _Accept = value; } + } + + private string _ContentType = "text/html"; + /// + /// 请求返回类型默认 text/html + /// + public string ContentType + { + get { return _ContentType; } + set { _ContentType = value; } + } + + private string _UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"; + /// + /// 客户端访问信息默认Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) + /// + public string UserAgent + { + get { return _UserAgent; } + set { _UserAgent = value; } + } + + private Encoding _Encoding = null; + /// + /// 返回数据编码默认为NUll,可以自动识别,一般为utf-8,gbk,gb2312 + /// + public Encoding Encoding + { + get { return _Encoding; } + set { _Encoding = value; } + } + private PostDataType _PostDataType = PostDataType.String; + /// + /// Post的数据类型 + /// + public PostDataType PostDataType + { + get { return _PostDataType; } + set { _PostDataType = value; } + } + + private string _Postdata = string.Empty; + /// + /// Post请求时要发送的字符串Post数据 + /// + public string Postdata + { + get { return _Postdata; } + set { _Postdata = value; } + } + private byte[] _PostdataByte = null; + /// + /// Post请求时要发送的Byte类型的Post数据 + /// + public byte[] PostdataByte + { + get { return _PostdataByte; } + set { _PostdataByte = value; } + } + private WebProxy _WebProxy; + /// + /// 设置代理对象,不想使用IE默认配置就设置为Null,而且不要设置ProxyIp + /// + public WebProxy WebProxy + { + get { return _WebProxy; } + set { _WebProxy = value; } + } + + private CookieCollection cookiecollection = null; + /// + /// Cookie对象集合 + /// + public CookieCollection CookieCollection + { + get { return cookiecollection; } + set { cookiecollection = value; } + } + + private string _Cookie = string.Empty; + /// + /// 请求时的Cookie + /// + public string Cookie + { + get { return _Cookie; } + set { _Cookie = value; } + } + + private string _Referer = string.Empty; + /// + /// 来源地址,上次访问地址 + /// + public string Referer + { + get { return _Referer; } + set { _Referer = value; } + } + + private string _CerPath = string.Empty; + /// + /// 证书绝对路径 + /// + public string CerPath + { + get { return _CerPath; } + set { _CerPath = value; } + } + private Boolean isToLower = false; + /// + /// 是否设置为全文小写,默认为不转化 + /// + public Boolean IsToLower + { + get { return isToLower; } + set { isToLower = value; } + } + private Boolean allowautoredirect = true; + /// + /// 支持跳转页面,查询结果将是跳转后的页面 + /// + public Boolean Allowautoredirect + { + get { return allowautoredirect; } + set { allowautoredirect = value; } + } + private int connectionlimit = 1024; + /// + /// 最大连接数 + /// + public int Connectionlimit + { + get { return connectionlimit; } + set { connectionlimit = value; } + } + private string proxyusername = string.Empty; + /// + /// 代理Proxy 服务器用户名 + /// + public string ProxyUserName + { + get { return proxyusername; } + set { proxyusername = value; } + } + private string proxypwd = string.Empty; + /// + /// 代理 服务器密码 + /// + public string ProxyPwd + { + get { return proxypwd; } + set { proxypwd = value; } + } + private string proxyip = string.Empty; + /// + /// 代理 服务IP ,如果要使用IE代理就设置为ieproxy + /// + public string ProxyIp + { + get { return proxyip; } + set { proxyip = value; } + } + private ResultType resulttype = ResultType.String; + /// + /// 设置返回类型String和Byte + /// + public ResultType ResultType + { + get { return resulttype; } + set { resulttype = value; } + } + private WebHeaderCollection header = []; + /// + /// header对象 + /// + public WebHeaderCollection Header + { + get { return header; } + set { header = value; } + } + + private Version _ProtocolVersion; + + /// + /// 获取或设置用于请求的 HTTP 版本。返回结果:用于请求的 HTTP 版本。默认为 System.Net.HttpVersion.Version11。 + /// + public Version ProtocolVersion + { + get { return _ProtocolVersion; } + set { _ProtocolVersion = value; } + } + private Boolean _expect100continue = true; + /// + /// 获取或设置一个 System.Boolean 值,该值确定是否使用 100-Continue 行为。如果 POST 请求需要 100-Continue 响应,则为 true;否则为 false。默认值为 true。 + /// + public Boolean Expect100Continue + { + get { return _expect100continue; } + set { _expect100continue = value; } + } + private X509CertificateCollection _ClentCertificates; + /// + /// 设置509证书集合 + /// + public X509CertificateCollection ClentCertificates + { + get { return _ClentCertificates; } + set { _ClentCertificates = value; } + } + private Encoding _PostEncoding; + /// + /// 设置或获取Post参数编码,默认的为Default编码 + /// + public Encoding PostEncoding + { + get { return _PostEncoding; } + set { _PostEncoding = value; } + } + private ResultCookieType _ResultCookieType = ResultCookieType.String; + /// + /// Cookie返回类型,默认的是只返回字符串类型 + /// + public ResultCookieType ResultCookieType + { + get { return _ResultCookieType; } + set { _ResultCookieType = value; } + } + + private ICredentials _ICredentials = CredentialCache.DefaultCredentials; + /// + /// 获取或设置请求的身份验证信息。 + /// + public ICredentials ICredentials + { + get { return _ICredentials; } + set { _ICredentials = value; } + } + +} +/// +/// Http返回参数类 +/// +public class HttpResult +{ + private string _Cookie; + /// + /// Http请求返回的Cookie + /// + public string Cookie + { + get { return _Cookie; } + set { _Cookie = value; } + } + + private CookieCollection _CookieCollection; + /// + /// Cookie对象集合 + /// + public CookieCollection CookieCollection + { + get { return _CookieCollection; } + set { _CookieCollection = value; } + } + private string _Html; + /// + /// 返回的String类型数据 只有ResultType.String时才返回数据,其它情况为空 + /// + public string Html + { + get { return _Html; } + set { _Html = value; } + } + private byte[] _ResultByte; + /// + /// 返回的Byte数组 只有ResultType.Byte时才返回数据,其它情况为空 + /// + public byte[] ResultByte + { + get { return _ResultByte; } + set { _ResultByte = value; } + } + + private WebHeaderCollection _Header; + /// + /// header对象 + /// + public WebHeaderCollection Header + { + get { return _Header; } + set { _Header = value; } + } + + private string _StatusDescription; + /// + /// 返回状态说明 + /// + public string StatusDescription + { + get { return _StatusDescription; } + set { _StatusDescription = value; } + } + private HttpStatusCode _StatusCode; + /// + /// 返回状态码,默认为OK + /// + public HttpStatusCode StatusCode + { + get { return _StatusCode; } + set { _StatusCode = value; } + } + /// + /// HttpItem类,用于存储实际执行所用到的HttpItem对象 + /// + public HttpItem item { get; set; } +} +/// +/// 返回类型 +/// +public enum ResultType +{ + /// + /// 表示只返回字符串 只有Html有数据 + /// + String, + /// + /// 表示返回字符串和字节流 ResultByte和Html都有数据返回 + /// + Byte +} +/// +/// Post的数据格式默认为string +/// +public enum PostDataType +{ + /// + /// 字符串类型,这时编码Encoding可不设置 + /// + String, + /// + /// Byte类型,需要设置PostdataByte参数的值编码Encoding可设置为空 + /// + Byte, + /// + /// 传文件,Postdata必须设置为文件的绝对路径,必须设置Encoding的值 + /// + FilePath +} +/// +/// Cookie返回类型 +/// +public enum ResultCookieType +{ + /// + /// 只返回字符串类型的Cookie + /// + String, + /// + /// CookieCollection格式的Cookie集合同时也返回String类型的cookie + /// + CookieCollection +} +public enum MethodType +{ + Get, + Post +} + diff --git a/Core/MAX.Core/Helpers/IdHelper.cs b/Core/MAX.Core/Helpers/IdHelper.cs new file mode 100644 index 0000000..224a70c --- /dev/null +++ b/Core/MAX.Core/Helpers/IdHelper.cs @@ -0,0 +1,45 @@ +namespace MAX.Core.Helpers; + +/// +/// 标识生成器 +/// +public static partial class IdHelper +{ + /// + /// 标识 + /// + private static string _id; + + /// + /// 设置标识 + /// + /// Id + public static void SetId(string id) + { + _id = id; + } + + /// + /// 重置标识 + /// + public static void Reset() + { + _id = null; + } + + /// + /// 用Guid创建标识,去掉分隔符 + /// + public static string Guid() + { + return string.IsNullOrWhiteSpace(_id) ? System.Guid.NewGuid().ToString("N") : _id; + } + + ///// + ///// 获取Guid + ///// + //public static Guid GetGuid() + //{ + // return string.IsNullOrWhiteSpace(_id) ? System.Guid.NewGuid() : _id.ToGuid(); + //} +} diff --git a/Core/MAX.Core/Helpers/ParametersBase.cs b/Core/MAX.Core/Helpers/ParametersBase.cs new file mode 100644 index 0000000..9cf8d97 --- /dev/null +++ b/Core/MAX.Core/Helpers/ParametersBase.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; + + +namespace MAX.Core.Helpers; + +/// +/// This is a generic parameters base class used for Dialog Parameters and Navigation Parameters. +/// +public abstract class ParametersBase : IEnumerable> +{ + private readonly List> _entries = []; + + /// + /// Default constructor. + /// + protected ParametersBase() + { + } + + /// + /// Constructs a list of parameters. + /// + /// Query string to be parsed. + protected ParametersBase(string query) + { + if (!string.IsNullOrWhiteSpace(query)) + { + int num = query.Length; + for (int i = ((query.Length > 0) && (query[0] == '?')) ? 1 : 0; i < num; i++) + { + int startIndex = i; + int num4 = -1; + while (i < num) + { + char ch = query[i]; + if (ch == '=') + { + if (num4 < 0) + { + num4 = i; + } + } + else if (ch == '&') + { + break; + } + i++; + } + string key = null; + string value; + if (num4 >= 0) + { + key = query.Substring(startIndex, num4 - startIndex); + value = query.Substring(num4 + 1, i - num4 - 1); + } + else + { + value = query.Substring(startIndex, i - startIndex); + } + + if (key != null) + { + Add(Uri.UnescapeDataString(key), Uri.UnescapeDataString(value)); + } + } + } + } + + /// + /// Searches Parameter collection and returns value if Collection contains key. + /// Otherwise returns null. + /// + /// The key for the value to be returned. + /// The value of the parameter referenced by the key; otherwise null. + public object this[string key] + { + get + { + foreach (KeyValuePair entry in _entries) + { + if (string.Compare(entry.Key, key, StringComparison.Ordinal) == 0) + { + return entry.Value; + } + } + + return null; + } + } + + /// + /// The count, or number, of parameters in collection. + /// + public int Count => _entries.Count; + + /// + /// Returns an IEnumerable of the Keys in the collection. + /// + public IEnumerable Keys => + _entries.Select(x => x.Key); + + /// + /// Adds the key and value to the parameters collection. + /// + /// The key to reference this value in the parameters collection. + /// The value of the parameter to store. + public void Add(string key, object value) => + _entries.Add(new KeyValuePair(key, value)); + + + public void Remove(string key) + { + _entries.RemoveAll(x => x.Key == key); + } + + public void Update(string key, object value) + { + _entries.RemoveAll(x => x.Key == key); + Add(key, value); + } + + /// + /// Checks collection for presence of key. + /// + /// The key to check in the collection. + /// true if key exists; else returns false. + public bool ContainsKey(string key) + { + return _entries.Any(x => string.Compare(x.Key, key, StringComparison.Ordinal) == 0); + } + + + /// + /// Gets an enumerator for the KeyValuePairs in parameter collection. + /// + /// Enumerator. + public IEnumerator> GetEnumerator() => + _entries.GetEnumerator(); + + /// + /// Returns the value of the member referenced by key. + /// + /// The type of object to be returned. + /// The key for the value to be returned. + /// Returns a matching parameter of if one exists in the Collection. + public T GetValue(string key) => + _entries.GetValue(key); + + /// + /// Returns an IEnumerable of all parameters. + /// + /// The type for the values to be returned. + /// The key for the values to be returned. + ///Returns a IEnumerable of all the instances of type . + public IEnumerable GetValues(string key) => + _entries.GetValues(key); + + /// + /// Checks to see if the parameter collection contains the value. + /// + /// The type for the values to be returned. + /// The key for the value to be returned. + /// Value of the returned parameter if it exists. + public bool TryGetValue(string key, out T value) => + _entries.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + /// + /// Converts parameter collection to a parameter string. + /// + /// A string representation of the parameters. + public override string ToString() + { + StringBuilder queryBuilder = new(); + + if (_entries.Count > 0) + { + queryBuilder.Append('?'); + bool first = true; + + foreach (KeyValuePair kvp in _entries) + { + if (!first) + { + queryBuilder.Append('&'); + } + else + { + first = false; + } + + queryBuilder.Append(Uri.EscapeDataString(kvp.Key)); + queryBuilder.Append('='); + queryBuilder.Append(Uri.EscapeDataString(kvp.Value != null ? kvp.Value.ToString() : "")); + } + } + + return queryBuilder.ToString(); + } + + /// + /// Adds a collection of parameters to the local parameter list. + /// + /// An IEnumerable of KeyValuePairs to add to the current parameter list. + public void FromParameters(IEnumerable> parameters) => + _entries.AddRange(parameters); +} + + +public static class ParametersExtensions +{ + /// + /// Searches for + /// + /// The type of the parameter to return + /// A collection of parameters to search + /// The key of the parameter to find + /// A matching value of if it exists + [EditorBrowsable(EditorBrowsableState.Never)] + public static T GetValue(this IEnumerable> parameters, string key) => + (T)GetValue(parameters, key, typeof(T)); + + /// + /// Searches for value referenced by + /// + /// A collection of parameters to search + /// The key of the parameter to find + /// The type of the parameter to return + /// A matching value of if it exists + /// Unable to convert the value of Type + [EditorBrowsable(EditorBrowsableState.Never)] + public static object GetValue(this IEnumerable> parameters, string key, Type type) + { + foreach (KeyValuePair kvp in parameters) + { + if (string.Compare(kvp.Key, key, StringComparison.Ordinal) == 0) + { + if (TryGetValueInternal(kvp, type, out object value)) + { + return value; + } + + throw new InvalidCastException($"Unable to convert the value of Type '{kvp.Value.GetType().FullName}' to '{type.FullName}' for the key '{key}' "); + } + } + + return GetDefault(type); + } + + /// + /// Searches for value referenced by + /// + /// The type of the parameter to return + /// A collection of parameters to search + /// The key of the parameter to find + /// The value of parameter to return + /// Success if value is found; otherwise returns false + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool TryGetValue(this IEnumerable> parameters, string key, out T value) + { + Type type = typeof(T); + + foreach (KeyValuePair kvp in parameters) + { + if (string.Compare(kvp.Key, key, StringComparison.Ordinal) == 0) + { + bool success = TryGetValueInternal(kvp, typeof(T), out object valueAsObject); + value = (T)valueAsObject; + return success; + } + } + + value = default; + return false; + } + + /// + /// Searches for value referenced by + /// + /// The type of the parameter to return + /// A collection of parameters to search + /// The key of the parameter to find + /// An IEnumerable{T} of all the values referenced by key + [EditorBrowsable(EditorBrowsableState.Never)] + public static IEnumerable GetValues(this IEnumerable> parameters, string key) + { + List values = []; + Type type = typeof(T); + + foreach (KeyValuePair kvp in parameters) + { + if (string.Compare(kvp.Key, key, StringComparison.Ordinal) == 0) + { + TryGetValueInternal(kvp, type, out object value); + values.Add((T)value); + } + } + + return values.AsEnumerable(); + } + + private static bool TryGetValueInternal(KeyValuePair kvp, Type type, out object value) + { + value = GetDefault(type); + bool success = false; + if (kvp.Value == null) + { + success = true; + } + else if (kvp.Value.GetType() == type) + { + success = true; + value = kvp.Value; + } + else if (type.IsAssignableFrom(kvp.Value.GetType())) + { + success = true; + value = kvp.Value; + } + else if (type.IsEnum) + { + string valueAsString = kvp.Value.ToString(); + if (Enum.IsDefined(type, valueAsString)) + { + success = true; + value = Enum.Parse(type, valueAsString); + } + else if (int.TryParse(valueAsString, out int numericValue)) + { + success = true; + value = Enum.ToObject(type, numericValue); + } + } + + if (!success && type.GetInterface("System.IConvertible") != null) + { + success = true; + value = System.Convert.ChangeType(kvp.Value, type); + } + + return success; + } + + /// + /// Checks to see if key exists in parameter collection + /// + /// IEnumerable to search + /// The key to search the for existence + /// true if key exists; false otherwise + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool ContainsKey(this IEnumerable> parameters, string key) => + parameters.Any(x => string.Compare(x.Key, key, StringComparison.Ordinal) == 0); + + private static object GetDefault(Type type) + { + if (type.IsValueType) + { + return Activator.CreateInstance(type); + } + return null; + } +} diff --git a/Core/MAX.Core/Helpers/ReflectionHelper.cs b/Core/MAX.Core/Helpers/ReflectionHelper.cs new file mode 100644 index 0000000..bbb0048 --- /dev/null +++ b/Core/MAX.Core/Helpers/ReflectionHelper.cs @@ -0,0 +1,618 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace MAX.Core.Helpers; + +/// +/// 反射操作 +/// +public static class ReflectionHelper +{ + /// + /// 获取类型描述,使用DescriptionAttribute设置描述 + /// + /// 类型 + public static string GetDescription() + { + return GetDescription(Common.GetType()); + } + + /// + /// 获取类型成员描述,使用DescriptionAttribute设置描述 + /// + /// 类型 + /// 成员名称 + public static string GetDescription(string memberName) + { + return GetDescription(Common.GetType(), memberName); + } + + /// + /// 获取类型成员描述,使用DescriptionAttribute设置描述 + /// + /// 类型 + /// 成员名称 + public static string GetDescription(Type type, string memberName) + { + if (type == null) + { + return string.Empty; + } + + if (string.IsNullOrWhiteSpace(memberName)) + { + return string.Empty; + } + + return GetDescription(type.GetTypeInfo().GetMember(memberName).FirstOrDefault()); + } + + /// + /// 获取类型成员描述,使用DescriptionAttribute设置描述 + /// + /// 成员 + public static string GetDescription(MemberInfo member) + { + if (member == null) + { + return string.Empty; + } + + return member.GetCustomAttribute() is DescriptionAttribute attribute ? attribute.Description : member.Name; + } + + /// + /// 获取显示名称,使用DisplayNameAttribute设置显示名称 + /// + /// 类型 + public static string GetDisplayName() + { + return GetDisplayName(Common.GetType()); + } + + /// + /// 获取显示名称,使用DisplayAttribute或DisplayNameAttribute设置显示名称 + /// + public static string GetDisplayName(MemberInfo member) + { + if (member == null) + { + return string.Empty; + } + + if (member.GetCustomAttribute() is DisplayAttribute displayAttribute) + { + return displayAttribute.Name; + } + + if (member.GetCustomAttribute() is DisplayNameAttribute displayNameAttribute) + { + return displayNameAttribute.DisplayName; + } + + return string.Empty; + } + + /// + /// 获取显示名称或描述,使用DisplayNameAttribute设置显示名称,使用DescriptionAttribute设置描述 + /// + /// 类型 + public static string GetDisplayNameOrDescription() + { + return GetDisplayNameOrDescription(Common.GetType()); + } + + /// + /// 获取属性显示名称或描述,使用DisplayAttribute或DisplayNameAttribute设置显示名称,使用DescriptionAttribute设置描述 + /// + public static string GetDisplayNameOrDescription(MemberInfo member) + { + string result = GetDisplayName(member); + return string.IsNullOrWhiteSpace(result) ? GetDescription(member) : result; + } + + /// + /// 查找类型列表 + /// + /// 查找类型 + /// 待查找的程序集列表 + public static List FindTypes(params Assembly[] assemblies) + { + Type findType = typeof(TFind); + return FindTypes(findType, assemblies); + } + + /// + /// 查找类型列表 + /// + /// 查找类型 + /// 待查找的程序集列表 + public static List FindTypes(Type findType, params Assembly[] assemblies) + { + List result = []; + foreach (Assembly assembly in assemblies) + { + result.AddRange(GetTypes(findType, assembly)); + } + + return result.Distinct().ToList(); + } + + /// + /// 获取类型列表 + /// + private static List GetTypes(Type findType, Assembly assembly) + { + List result = []; + if (assembly == null) + { + return result; + } + + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + Debug.WriteLine(ex); + return result; + } + foreach (Type type in types) + { + AddType(result, findType, type); + } + + return result; + } + + /// + /// 添加类型 + /// + private static void AddType(List result, Type findType, Type type) + { + if (type.IsInterface || type.IsAbstract) + { + return; + } + + if (findType.IsAssignableFrom(type) == false && MatchGeneric(findType, type) == false) + { + return; + } + + result.Add(type); + } + + /// + /// 泛型匹配 + /// + private static bool MatchGeneric(Type findType, Type type) + { + if (findType.IsGenericTypeDefinition == false) + { + return false; + } + + Type definition = findType.GetGenericTypeDefinition(); + foreach (Type implementedInterface in type.FindInterfaces((filter, criteria) => true, null)) + { + if (implementedInterface.IsGenericType == false) + { + continue; + } + + return definition.IsAssignableFrom(implementedInterface.GetGenericTypeDefinition()); + } + return false; + } + + /// + /// 获取实现了接口的所有实例 + /// + /// 接口类型 + /// 待查找的程序集列表 + public static List GetInstancesByInterface(params Assembly[] assemblies) + { + return FindTypes(assemblies) + .Select(t => CreateInstance(t)).ToList(); + } + + /// + /// 动态创建实例 + /// + /// 目标类型 + /// 类型 + /// 传递给构造函数的参数 + public static T CreateInstance(Type type, params object[] parameters) + { + return Convert.To(Activator.CreateInstance(type, parameters)); + } + + public static Type GetType(string typeName) + { + + Type type = null; + + Assembly[] assemblyArray = AppDomain.CurrentDomain.GetAssemblies(); + + int assemblyArrayLength = assemblyArray.Length; + + for (int i = 0; i < assemblyArrayLength; ++i) + { + + type = assemblyArray[i].GetType(typeName); + + if (type != null) + { + return type; + } + } + + for (int i = 0; i < assemblyArrayLength; ++i) + { + Type[] typeArray = assemblyArray[i].GetTypes(); + int typeArrayLength = typeArray.Length; + for (int j = 0; j < typeArrayLength; ++j) + { + if (typeArray[j].Name.Equals(typeName)) + { + return typeArray[j]; + } + } + } + + return type; + + } + + /// + /// 获取程序集 + /// + /// 程序集名称 + public static Assembly GetAssembly(string assemblyName) + { + return Assembly.Load(new AssemblyName(assemblyName)); + } + + /// + /// 是否布尔类型 + /// + /// 成员 + public static bool IsBool(MemberInfo member) + { + if (member == null) + { + return false; + } + + return member.MemberType switch + { + MemberTypes.TypeInfo => member.ToString() == "System.Boolean", + MemberTypes.Property => IsBool((PropertyInfo)member), + _ => false, + }; + } + + /// + /// 是否布尔类型 + /// + private static bool IsBool(PropertyInfo property) + { + return property.PropertyType == typeof(bool) || property.PropertyType == typeof(bool?); + } + + /// + /// 是否枚举类型 + /// + /// 成员 + public static bool IsEnum(MemberInfo member) + { + if (member == null) + { + return false; + } + + return member.MemberType switch + { + MemberTypes.TypeInfo => ((TypeInfo)member).IsEnum, + MemberTypes.Property => IsEnum((PropertyInfo)member), + _ => false, + }; + } + + /// + /// 是否枚举类型 + /// + private static bool IsEnum(PropertyInfo property) + { + if (property.PropertyType.GetTypeInfo().IsEnum) + { + return true; + } + + Type value = Nullable.GetUnderlyingType(property.PropertyType); + if (value == null) + { + return false; + } + + return value.GetTypeInfo().IsEnum; + } + + /// + /// 是否日期类型 + /// + /// 成员 + public static bool IsDate(MemberInfo member) + { + if (member == null) + { + return false; + } + + return member.MemberType switch + { + MemberTypes.TypeInfo => member.ToString() == "System.DateTime", + MemberTypes.Property => IsDate((PropertyInfo)member), + _ => false, + }; + } + + /// + /// 是否日期类型 + /// + private static bool IsDate(PropertyInfo property) + { + if (property.PropertyType == typeof(DateTime)) + { + return true; + } + + if (property.PropertyType == typeof(DateTime?)) + { + return true; + } + + return false; + } + + /// + /// 是否整型 + /// + /// 成员 + public static bool IsInt(MemberInfo member) + { + if (member == null) + { + return false; + } + + return member.MemberType switch + { + MemberTypes.TypeInfo => member.ToString() is "System.Int32" or "System.Int16" or "System.Int64", + MemberTypes.Property => IsInt((PropertyInfo)member), + _ => false, + }; + } + + /// + /// 是否整型 + /// + private static bool IsInt(PropertyInfo property) + { + if (property.PropertyType == typeof(int)) + { + return true; + } + + if (property.PropertyType == typeof(int?)) + { + return true; + } + + if (property.PropertyType == typeof(short)) + { + return true; + } + + if (property.PropertyType == typeof(short?)) + { + return true; + } + + if (property.PropertyType == typeof(long)) + { + return true; + } + + if (property.PropertyType == typeof(long?)) + { + return true; + } + + return false; + } + + /// + /// 是否数值类型 + /// + /// 成员 + public static bool IsNumber(MemberInfo member) + { + if (member == null) + { + return false; + } + + if (IsInt(member)) + { + return true; + } + + return member.MemberType switch + { + MemberTypes.TypeInfo => member.ToString() is "System.Double" or "System.Decimal" or "System.Single", + MemberTypes.Property => IsNumber((PropertyInfo)member), + _ => false, + }; + } + + /// + /// 是否数值类型 + /// + private static bool IsNumber(PropertyInfo property) + { + if (property.PropertyType == typeof(double)) + { + return true; + } + + if (property.PropertyType == typeof(double?)) + { + return true; + } + + if (property.PropertyType == typeof(decimal)) + { + return true; + } + + if (property.PropertyType == typeof(decimal?)) + { + return true; + } + + if (property.PropertyType == typeof(float)) + { + return true; + } + + if (property.PropertyType == typeof(float?)) + { + return true; + } + + return false; + } + + /// + /// 是否集合 + /// + /// 类型 + public static bool IsCollection(Type type) + { + if (type.IsArray) + { + return true; + } + + return IsGenericCollection(type); + } + + /// + /// 是否泛型集合 + /// + /// 类型 + public static bool IsGenericCollection(Type type) + { + if (!type.IsGenericType) + { + return false; + } + + Type typeDefinition = type.GetGenericTypeDefinition(); + return typeDefinition == typeof(IEnumerable<>) + || typeDefinition == typeof(IReadOnlyCollection<>) + || typeDefinition == typeof(IReadOnlyList<>) + || typeDefinition == typeof(ICollection<>) + || typeDefinition == typeof(IList<>) + || typeDefinition == typeof(List<>); + } + + /// + /// 从目录中获取所有程序集 + /// + /// 目录绝对路径 + public static List GetAssemblies(string directoryPath) + { + return Directory.GetFiles(directoryPath, "*.*", SearchOption.AllDirectories).ToList() + .Where(t => t.EndsWith(".exe") || t.EndsWith(".dll")) + .Select(path => Assembly.Load(new AssemblyName(path))).ToList(); + } + + /// + /// 获取公共属性列表 + /// + /// 实例 + public static List GetPublicProperties(object instance) + { + PropertyInfo[] properties = instance.GetType().GetProperties(); + return properties.ToList().Select(t => new CommonItem(t.Name, t.GetValue(instance))).ToList(); + } + + /// + /// 获取顶级基类 + /// + /// 类型 + public static Type GetTopBaseType() + { + return GetTopBaseType(typeof(T)); + } + + /// + /// 获取顶级基类 + /// + /// 类型 + public static Type GetTopBaseType(Type type) + { + if (type == null) + { + return null; + } + + if (type.IsInterface) + { + return type; + } + + if (type.BaseType == typeof(object)) + { + return type; + } + + return GetTopBaseType(type.BaseType); + } + + /// + /// 获取元素类型,如果是集合,返回集合的元素类型 + /// + /// 类型 + public static Type GetElementType(Type type) + { + if (IsCollection(type) == false) + { + return type; + } + + if (type.IsArray) + { + return type.GetElementType(); + } + + Type[] genericArgumentsTypes = type.GetTypeInfo().GetGenericArguments(); + if (genericArgumentsTypes == null || genericArgumentsTypes.Length == 0) + { + throw new ArgumentException("泛型类型参数不能为空"); + } + + return genericArgumentsTypes[0]; + } +} diff --git a/Core/MAX.Core/Helpers/UriHelper.cs b/Core/MAX.Core/Helpers/UriHelper.cs new file mode 100644 index 0000000..bfa9f33 --- /dev/null +++ b/Core/MAX.Core/Helpers/UriHelper.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MAX.Core.Helpers; + +public class UriHelper +{ + #region 对外公开方法 + /// + /// 带有前缀的域名 + /// + /// + /// + public static string GetSchemeDomain(string url) + { + Uri uri = new(url); + return $"{uri.Scheme}://{uri.Authority}"; + } + /// + /// 获取域名 + /// + /// + /// + public static string GetDomain(string url) + { + Uri uri = new(url); + return uri.Host; + } + /// + /// 获取 URL + /// + /// URL 地址 + /// + public static string GetUrl(string url) + { + Uri uri = new(url); + return uri.AbsolutePath; + } + /// + /// URL 解码 + /// + /// URL 地址 + /// + public static string UrlDecode(string url) + { + return System.Web.HttpUtility.UrlDecode(url); + } + /// + /// URL 解码 + /// + /// URL 地址 + /// 编码格式 + /// + public static string UrlDecode(string url, Encoding encoding) + { + return System.Web.HttpUtility.UrlDecode(url, encoding); + } + /// + /// URL 编码 + /// + /// URL 地址 + /// + public static string UrlEncode(string url) + { + return System.Web.HttpUtility.UrlEncode(url); + } + /// + /// URL 编码 + /// + /// URL 地址 + /// 编码格式 + /// + public static string UrlEncode(string url, Encoding encoding) + { + return System.Web.HttpUtility.UrlEncode(url, encoding); + } + /// + /// 获取参数列表 + /// + /// Url 地址 + /// + public static List GetQueryList(string url) + { + return GetQueryList(url, Encoding.UTF8); + } + /// + /// 获取参数列表 + /// + /// Url 地址 + /// 编码格式 + /// + public static List GetQueryList(string url, Encoding encoding) + { + Uri uri = new(url); + string query = uri.PathAndQuery; + if (query.IndexOf(".") > 0) + { + query = query.Substring(0, query.IndexOf(".")); + } + + List dataList = []; + string[] queryDataList = query.Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries); + if (queryDataList != null && queryDataList.Length > 0) + { + foreach (string queryData in queryDataList) + { + dataList.Add(System.Web.HttpUtility.UrlDecode(queryData, encoding)); + } + } + return dataList; + } + /// + /// 获取参数字典 + /// + /// Url 地址 + /// + public static Dictionary GetQueryDict(string url) + { + return GetQueryDict(url, Encoding.UTF8); + } + /// + /// 获取参数字典 + /// + /// Url 地址 + /// 编码格式 + /// + public static Dictionary GetQueryDict(string url, Encoding encoding) + { + Uri uri = new(url); + string query = uri.Query.TrimStart(new char[] { '?' }); + + Dictionary dataDict = []; + string[] queryDataList = query.Split(new string[] { "&" }, StringSplitOptions.RemoveEmptyEntries); + if (queryDataList != null && queryDataList.Length > 0) + { + foreach (string queryData in queryDataList) + { + string[] keyValue = queryData.Split(new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries); + if (!dataDict.ContainsKey(keyValue[0])) + { + dataDict.Add(keyValue[0].ToLower(), System.Web.HttpUtility.UrlDecode(keyValue[1], encoding)); + } + } + } + return dataDict; + } + #endregion +} diff --git a/Core/MAX.Core/Infrastructure/IStartup.cs b/Core/MAX.Core/Infrastructure/IStartup.cs new file mode 100644 index 0000000..edbe252 --- /dev/null +++ b/Core/MAX.Core/Infrastructure/IStartup.cs @@ -0,0 +1,18 @@ +using MAX.Core.DependencyInjection; + +namespace MAX.Core.Infrastructure; + +/// +/// 启动器:实现 ,由宿主项目统一发现并按 排序执行。 +/// +public interface IStartup : IDependencyRegistrar +{ + /// + /// 优先级(优先级越高越先执行) + /// + int Priority { get; } + /// + /// 初始化 + /// + void Initialize(); +} diff --git a/Core/MAX.Core/Infrastructure/SingleInstance.cs b/Core/MAX.Core/Infrastructure/SingleInstance.cs new file mode 100644 index 0000000..b6eb50a --- /dev/null +++ b/Core/MAX.Core/Infrastructure/SingleInstance.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; + +namespace MAX.Core.Infrastructure; + +/// +/// 单例,需要在当前类中定义 +/// +/// +public class SingleInstance where T : class +{ + private static readonly object lockObj = new(); + private static T mySelf;//= default(T); + public static T Instance + { + get + { + lock (lockObj) + { + mySelf ??= InstanceCreator.CreateInstance(); + } + + return mySelf; + } + } +} + +public static class InstanceCreator +{ + public static T CreateInstance() + { + Type type = typeof(T); + try + { + return (T)type.Assembly.CreateInstance(type.FullName, true, BindingFlags.NonPublic | BindingFlags.Instance, null, null, null, null); + } + catch (MissingMethodException ex) + { + throw new Exception($"{ex.Message}\n(单例模式下,构造函数必须为 private)"); + } + } +} diff --git a/Core/MAX.Core/Logger/ConsoleLogger.cs b/Core/MAX.Core/Logger/ConsoleLogger.cs new file mode 100644 index 0000000..2c330dd --- /dev/null +++ b/Core/MAX.Core/Logger/ConsoleLogger.cs @@ -0,0 +1,124 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +namespace MAX.Core.Logger; + +/// +/// Implementation of that logs into a . +/// +public class ConsoleLogger : ILoggerFacade +{ + private readonly bool _debug; + private readonly bool _console; + + public bool IsDebugEnabled => true; + + /// + /// Initializes a new instance of that writes to + /// the console output. + /// + public ConsoleLogger(bool debug = true, bool console = true) + { + _debug = debug; + _console = console; + } + + + + + + + ///// + ///// Disposes the associated . + ///// + ///// When , disposes the associated . + //protected virtual void Dispose(bool disposing) + //{ + // if (disposing) + // { + // writer?.Dispose(); + // } + //} + + /// + ///Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Calls . + ///2 + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public void Debug(Exception ex, string message) + { + Log(message, LogLevel.Debug); + } + + public void Debug(string message) + { + Log(message, LogLevel.Debug); + } + + public void Info(Exception ex, string message) + { + Log(message, LogLevel.Info); + } + + public void Info(string message) + { + Log(message, LogLevel.Info); + } + + public void Warn(Exception ex, string message) + { + Log(message, LogLevel.Warn); + } + + public void Warn(string message) + { + Log(message, LogLevel.Warn); + } + + public void Error(Exception ex, string message) + { + Log(message, LogLevel.Error); + } + + public void Error(string message) + { + Log(message, LogLevel.Error); + } + + public void Fatal(Exception ex, string message) + { + Log(message, LogLevel.Fatal); + } + + public void Fatal(string message) + { + Log(message, LogLevel.Fatal); + } + + public void Error(Exception ex) + { + Log(ex.Message, LogLevel.Error); + } + + public void Log(string message, LogLevel level) + { + string messageToLog = string.Format(CultureInfo.InvariantCulture, "{1}: {2}. Timestamp:{0:u}.", DateTime.Now, + level.ToString().ToUpper(CultureInfo.InvariantCulture), message); + //Trace.WriteLine(messageToLog); + if (_debug) + { + System.Diagnostics.Debug.WriteLine(messageToLog); + } + if (_console) + { + Console.WriteLine(messageToLog); + } + } +} \ No newline at end of file diff --git a/Core/MAX.Core/Logger/ICustomLogger.cs b/Core/MAX.Core/Logger/ICustomLogger.cs new file mode 100644 index 0000000..456a96b --- /dev/null +++ b/Core/MAX.Core/Logger/ICustomLogger.cs @@ -0,0 +1,12 @@ +using MAX.Core.Infrastructure; + +namespace MAX.Core.Logger; + +/// 自定义日志记录器接口 +public interface ICustomLogger +{ + /// 记录日志信息 + /// 日志分组 + /// 日志内容 + void Log(string group, string message); +} diff --git a/Core/MAX.Core/Logger/ILoggerFacade.cs b/Core/MAX.Core/Logger/ILoggerFacade.cs new file mode 100644 index 0000000..02bd991 --- /dev/null +++ b/Core/MAX.Core/Logger/ILoggerFacade.cs @@ -0,0 +1,68 @@ +using System; + +namespace MAX.Core.Logger; + +/// +/// 日志操作 +/// +public interface ILoggerFacade +{ + /// + /// 调试级别是否启用 + /// + bool IsDebugEnabled { get; } + + /// + /// 调试 + /// + void Debug(Exception ex, string message); + /// + /// 调试 + /// + /// 日志消息 + void Debug(string message); + /// + /// 信息 + /// + void Info(Exception ex, string message); + /// + /// 信息 + /// + /// 日志消息 + void Info(string message); + /// + /// 警告 + /// + void Warn(Exception ex, string message); + /// + /// 警告 + /// + /// 日志消息 + void Warn(string message); + /// + /// 错误 + /// + void Error(Exception ex, string message); + /// + /// 错误 + /// + /// 日志消息 + void Error(string message); + /// + /// 致命错误 + /// + void Fatal(Exception ex, string message); + /// + /// 致命错误 + /// + /// 日志消息 + void Fatal(string message); + void Error(Exception ex); + + /// + /// 写入日志 + /// + /// + /// + void Log(string message, LogLevel level); +} diff --git a/Core/MAX.Core/Logger/LogLevel.cs b/Core/MAX.Core/Logger/LogLevel.cs new file mode 100644 index 0000000..941b991 --- /dev/null +++ b/Core/MAX.Core/Logger/LogLevel.cs @@ -0,0 +1,15 @@ +namespace MAX.Core.Logger; + +/// +/// ÈÕ־µȼ¶ +/// +public enum LogLevel +{ + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Fatal = 5, + None = 6 +} \ No newline at end of file diff --git a/Core/MAX.Core/MAX.Core.csproj b/Core/MAX.Core/MAX.Core.csproj new file mode 100644 index 0000000..22869f7 --- /dev/null +++ b/Core/MAX.Core/MAX.Core.csproj @@ -0,0 +1,12 @@ + + + + NU1608;CS1574;CS1573;CS1572;CS1571;CS1711;CS1570;CS1591 + True + + + + + + + diff --git a/Core/MAX.Core/Reflections/AppTypeFinder.cs b/Core/MAX.Core/Reflections/AppTypeFinder.cs new file mode 100644 index 0000000..d712adb --- /dev/null +++ b/Core/MAX.Core/Reflections/AppTypeFinder.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace MAX.Core.Reflections; + +public partial class AppTypeFinder : Finder +{ + public static AppTypeFinder Instance { get; } = new AppTypeFinder(); + + /// + /// 所有程序集 + /// + public static List Assemblies => Instance.GetAssemblies(); + + public static Exception LoadDLL(string dllPath, out Assembly assembly) + { + return Instance.LoadAssemblyToAppDomain(dllPath, out assembly); + } + + /// + /// 程序集是否匹配 + /// + protected override bool Match(string assemblyName) + { + if (base.Match(assemblyName)) + { + if (AvailablePrefix().Exists(assemblyName.StartsWith)) + { + return true; + } + return Regex.IsMatch(assemblyName, "^(MAX\\.)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + return false; + } + + + protected virtual List AvailablePrefix() + { + return ["MAX."]; + } + + public static IEnumerable GetAllTypes() + { + List types = []; + foreach (Assembly item in Assemblies) + { + try + { + types.AddRange(item.GetTypes()); + } + catch + { + + } + } + return types; + } + + /// + /// 查找当前程序集中所有继承或实现了指定类型的非抽象类。 + /// + /// + /// 要匹配的目标类型,可以是: + /// + /// 普通接口类型(例如 IMyService) + /// 开放泛型接口类型(例如 IRepository<>) + /// 普通基类类型(例如 BaseHandler) + /// 开放泛型基类类型(例如 BaseHandler<>) + /// + /// + /// + /// 所有满足条件的具体类(非抽象类)的 数组。 + /// + /// + /// 当 为 null 时抛出。 + /// + /// + /// + /// 此方法会遍历 GetAllTypes() 返回的所有类型,并筛选出: + /// + /// 实现了指定接口的类(包括开放泛型接口的封闭实现) + /// 继承了指定基类的类(包括开放泛型基类的封闭实现) + /// + /// + /// 示例: + /// + /// // 查找所有实现了 IEnumDisplayHandler<T> 的类 + /// var handlers = FindAllDerivedTypes(typeof(IEnumDisplayHandler<>)); + /// + /// // 查找所有实现了 IMyService 的类 + /// var services = FindAllDerivedTypes(typeof(IMyService)); + /// + /// // 查找所有继承 BaseHandler 的类 + /// var subclasses = FindAllDerivedTypes(typeof(BaseHandler)); + /// + /// // 查找所有继承开放泛型基类 BaseHandler<T> 的类 + /// var genericSubclasses = FindAllDerivedTypes(typeof(BaseHandler<>)); + /// + /// + public static Type[] FindAllDerivedTypes(Type targetType) + { + if (targetType == null) + { + throw new ArgumentNullException(nameof(targetType)); + } + + return GetAllTypes().Where(t => + { + try + { + if (!t.IsClass || t.IsAbstract) + { + return false; + } + + // 开放泛型接口 + if (targetType.IsInterface && targetType.IsGenericTypeDefinition) + { + return t.GetInterfaces().Any(i => + i.IsGenericType && i.GetGenericTypeDefinition() == targetType); + } + + // 普通接口 + if (targetType.IsInterface) + { + return targetType.IsAssignableFrom(t); + } + + // 开放泛型基类 + if (targetType.IsGenericTypeDefinition) + { + return IsSubclassOfRawGeneric(t, targetType); + } + + // 普通基类 + return targetType.IsAssignableFrom(t) && t != targetType; + } + catch + { + return false; + } + }) + .ToArray(); + } + + /// + /// 检查指定类型是否继承自某个开放泛型基类。 + /// + /// 要检查的类型。 + /// 开放泛型基类类型,例如 BaseHandler<>。 + /// + /// 如果 的继承链中存在指定的开放泛型基类,则返回 true;否则返回 false。 + /// + /// + /// 此方法会沿着 的继承链向上遍历, + /// 并在每一级检查是否为指定的开放泛型基类定义。 + /// + private static bool IsSubclassOfRawGeneric(Type type, Type generic) + { + while (type != null && type != typeof(object)) + { + Type cur = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + if (generic == cur) + { + return true; + } + + type = type.BaseType; + } + return false; + } + + /// + /// 泛型包装方法,查找所有继承或实现了指定类型 的非抽象类。 + /// + /// + /// 目标类型,可以是接口、开放泛型接口、基类或开放泛型基类。 + /// + /// + /// 所有满足条件的具体类(非抽象类)的 数组。 + /// + /// + /// 调用示例: + /// + /// var handlers = FindAllDerivedTypes<IEnumDisplayHandler<>>(); + /// var services = FindAllDerivedTypes<IMyService>(); + /// var subclasses = FindAllDerivedTypes<BaseHandler>(); + /// var genericSubclasses = FindAllDerivedTypes<BaseHandler<>>(); + /// + /// + public static Type[] FindAllDerivedTypes() => FindAllDerivedTypes(typeof(T)); + + +} diff --git a/Core/MAX.Core/Reflections/Finder.cs b/Core/MAX.Core/Reflections/Finder.cs new file mode 100644 index 0000000..a941c95 --- /dev/null +++ b/Core/MAX.Core/Reflections/Finder.cs @@ -0,0 +1,129 @@ +using MAX.Core.Helpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace MAX.Core.Reflections; + +public class Finder +{ + /// + /// 跳过的程序集 + /// + private const string SkipAssemblies = "^System|^Microsoft|^mscorlib|^netstandard|^Autofac|^AutoMapper|^EntityFramework|^Newtonsoft|^Castle|^NLog|^Pomelo|^AspectCore|^Xunit|^Nito|^Npgsql|^Exceptionless|^MySqlConnector|^Anonymously|^libuv|^api-ms|^clrcompression|^clretwrc|^clrjit|^coreclr|^dbgshim|^e_sqlite3|^hostfxr|^hostpolicy|^MessagePack|^mscordaccore|^mscordbi|^mscorrc|^sni|^sos|^SOS\\.NETCore|^sos_amd64|^SQLitePCLRaw|^StackExchange|^Swashbuckle|^WindowsBase|^ucrtbase|^DotNetCore\\.CAP|^MongoDB|^Confluent\\.Kafka|^librdkafka|^EasyCaching|^RabbitMQ|^Consul|^Dapper|^EnyimMemcachedCore|^Pipelines|^DnsClient|^IdentityModel|^zlib|^Runtime\\.|^Google|^Grpc|^Internal\\.|^Azure|^DevExpress\\.|^FreeSql\\.|^PresentationFramework|^PresentationCore|^Prism|^DirectWriteForwarder|^MAX\\.Common|^ReflectionHelperTypesPublicWrappers|^UIAutomation|^WindowsFormsIntegration"; + + /// + /// 获取程序集列表 + /// + public virtual List GetAssemblies() + { + LoadAssemblies(AppDomain.CurrentDomain.BaseDirectory); + return GetAssembliesFromCurrentAppDomain(); + } + + /// + /// 加载程序集到当前应用程序域 + /// + /// 目录绝对路径 + protected void LoadAssemblies(string path) + { + foreach (string file in Directory.GetFiles(path, "*.dll")) + { + if (Match(Path.GetFileName(file)) == false) + { + continue; + } + + LoadAssemblyToAppDomain(file, out _); + } + } + + /// + /// 程序集是否匹配 + /// + protected virtual bool Match(string assemblyName) + { + return Regex.IsMatch(assemblyName, SkipAssemblies, RegexOptions.IgnoreCase | RegexOptions.Compiled) == false; + } + + /// + /// 将程序集添加当前应用程序域 + /// + protected Exception LoadAssemblyToAppDomain(string file, out Assembly assembly) + { + try + { + //Debug.WriteLine($"Load Assembly : {Path.GetFileName(file)}"); + AssemblyName assemblyName = AssemblyName.GetAssemblyName(file); + assembly = AppDomain.CurrentDomain.Load(assemblyName); + return null; + } + catch (FileNotFoundException) + { + // 如果找不到文件,回退到 LoadFrom 方式 + try + { + assembly = Assembly.LoadFrom(file); + return null; + } + catch (Exception ex) + { + assembly = null; + return ex; + } + } + catch (Exception ex) + { + assembly = null; + return ex; + } + } + + /// + /// 从当前应用程序域获取程序集列表 + /// + private List GetAssembliesFromCurrentAppDomain() + { + List result = []; + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (Match(assembly)) + { + result.Add(assembly); + } + } + return result.Distinct().ToList(); + } + + /// + /// 程序集是否匹配 + /// + private bool Match(Assembly assembly) + { + return !Regex.IsMatch(assembly.FullName, SkipAssemblies, RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + + /// + /// 查找类型列表 + /// + /// 查找类型 + /// 在指定的程序集列表中查找 + public List Find(List assemblies = null) + { + return Find(typeof(T), assemblies); + } + + /// + /// 查找类型列表 + /// + /// 查找类型 + /// 在指定的程序集列表中查找 + public List Find(Type findType, List assemblies = null) + { + assemblies ??= GetAssemblies(); + return ReflectionHelper.FindTypes(findType, assemblies.ToArray()); + } +} diff --git a/Core/MAX.Core/Startup.cs b/Core/MAX.Core/Startup.cs new file mode 100644 index 0000000..724d8fd --- /dev/null +++ b/Core/MAX.Core/Startup.cs @@ -0,0 +1,45 @@ +/*** + * _ooOoo_ + * o8888888o + * 88" . "88 + * (| -_- |) + * O\ = /O + * ____/`---'\____ + * . ' \\| |// `. + * / \\||| : |||// \ + * / _||||| -:- |||||- \ + * | | \\\ - /// | | + * | \_| ''\---/'' | | + * \ .-\__ `-` ___/-. / + * ___`. .' /--.--\ `. . __ + * ."" '< `.___\_<|>_/___.' >'"". + * | | : `- \`.;`\ _ /`;.`/ - ` : | | + * \ \ `-. \_ __\ /__ _/ .-` / / + * ======`-.____`-.___\_____/___.-`____.-'====== + * `=---=' + * + * ............................................. + * 佛祖保佑 永无BUG + * 快加工资 不改需求 + */ + +using MAX.Core.DependencyInjection; +using MAX.Core.Infrastructure; + +namespace MAX.Core; + +public class Startup : IStartup +{ + public int Priority => int.MaxValue; + + public void Initialize() + { + + } + + public void Register(IServiceRegistrar registrar) + { + // 示例:恢复 LazyServiceProvider 后,可在这里注册为单例 + // registrar.RegisterSingleton(); + } +} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ba7e914 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,23 @@ + + + + + net10.0 + $(DefaultTargetFramework) + 12.0 + true + false + true + annotations + CS1591 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/StoreHub.Framework/Commons/ApiResult.cs b/StoreHub.Framework/Commons/ApiResult.cs new file mode 100644 index 0000000..3e29ae4 --- /dev/null +++ b/StoreHub.Framework/Commons/ApiResult.cs @@ -0,0 +1,30 @@ +namespace StoreHub.Framework.Commons +{ + public class ApiResult + { + public int Code { get; set; } + + public T? Data { get; set; } + + public string? Msg { get; set; } + } + + public static class ApiResult + { + public static ApiResult Success(T data, string? msg = null) + => new() + { + Code = 200, + Data = data, + Msg = msg ?? "success" + }; + + public static ApiResult Error(string msg, int code = 500) + => new() + { + Code = code, + Data = null, + Msg = msg + }; + } +} diff --git a/StoreHub.Framework/DependencyInjection/FreeSqlDB.cs b/StoreHub.Framework/DependencyInjection/FreeSqlDB.cs new file mode 100644 index 0000000..a5483c4 --- /dev/null +++ b/StoreHub.Framework/DependencyInjection/FreeSqlDB.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace StoreHub.Framework.DependencyInjection +{ + public static class FreeSqlDB + { + public static IServiceCollection AddFreeSqlDB(this IServiceCollection services) + { + Func fsqlFactory = r => + { + var config = r.GetRequiredService(); + var connection = config.GetConnectionString("DefaultConnection"); + IFreeSql fsql = new FreeSql.FreeSqlBuilder() + .UseConnectionString(FreeSql.DataType.MySql, connection) + .UseAdoConnectionPool(true) + .Build(); + return fsql; + }; + services.AddSingleton(fsqlFactory); + return services; + } + } +} diff --git a/StoreHub.Framework/DependencyInjection/JwtServiceCollectionExtensions.cs b/StoreHub.Framework/DependencyInjection/JwtServiceCollectionExtensions.cs new file mode 100644 index 0000000..fb2fec9 --- /dev/null +++ b/StoreHub.Framework/DependencyInjection/JwtServiceCollectionExtensions.cs @@ -0,0 +1,127 @@ +using Asp.Versioning.OpenApi; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi; +using StoreHub.Framework.Security; +using System.Security.Claims; +using System.Text; + +namespace StoreHub.Framework.DependencyInjection; + +/// +/// JWT 认证服务注册入口(由宿主 Program.cs 调用)。 +/// +public static class JwtServiceCollectionExtensions +{ + /// + /// 注册 JWT 认证: + /// + /// 绑定 Jwt 配置节到 ,并在启动时校验签名密钥长度; + /// 注册 (令牌生成/校验)与 (当前用户访问器); + /// 注册 认证,认证失败/授权失败统一返回 格式; + /// 注册 (基于 IMemoryCache 的在线会话存储):单点登录、在线人员查询、退出销毁; + /// 为 OpenAPI 文档声明 Bearer(JWT) 安全方案,并对标记了 [Authorize] 的接口声明需要携带令牌。 + /// + /// + /// + /// 需要在请求管道中调用 app.UseAuthentication()(位于 app.UseAuthorization() 之前)。 + /// + public static IServiceCollection AddStoreHubJwt(this IServiceCollection services, IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + // JwtOptions:绑定 Jwt 配置节,启动时校验 HS256 签名密钥长度 + services.AddOptions() + .Bind(configuration.GetSection(JwtOptions.SectionName)) + .Validate( + o => !string.IsNullOrWhiteSpace(o.SigningKey) + && Encoding.UTF8.GetByteCount(o.SigningKey) >= 32, + "Jwt:SigningKey 长度不足,HS256 签名密钥至少需要 32 字节(256位)。") + .ValidateOnStart(); + + services.AddHttpContextAccessor(); + + // 令牌服务与当前用户访问器 + services.AddSingleton(); + services.AddScoped(); + + // 在线会话存储(单点登录):基于 IMemoryCache 的进程内缓存,用于在线人员管理与退出销毁 + services.AddMemoryCache(); + services.AddSingleton(); + + // JwtBearer 认证 + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + JwtOptions jwt = configuration.GetSection(JwtOptions.SectionName).Get() ?? new JwtOptions(); + options.TokenValidationParameters = jwt.ToTokenValidationParameters(); + options.Events = new JwtBearerEvents + { + OnChallenge = JwtBearerResponseHandler.OnChallenge, + OnForbidden = JwtBearerResponseHandler.OnForbidden, + // 单点登录校验:会话必须仍有效(未退出、未被新登录顶替) + OnTokenValidated = context => + { + IOnlineUserStore onlineUserStore = context.HttpContext.RequestServices.GetRequiredService(); + ClaimsPrincipal? principal = context.Principal; + string? userIdValue = principal?.FindFirst(JwtClaimTypes.UserId)?.Value; + string? jti = principal?.FindFirst(JwtClaimTypes.Jti)?.Value; + + if (!int.TryParse(userIdValue, out int userId) + || string.IsNullOrWhiteSpace(jti) + || !onlineUserStore.IsTokenActive(userId, jti)) + { + context.Fail(new SecurityTokenException("登录已失效,请重新登录")); + } + + return Task.CompletedTask; + }, + }; + }); + + // OpenAPI:声明 Bearer(JWT) 安全方案(Scalar 页面即可显示 Authorize/输入 token), + // 并仅对标记了 [Authorize] 的接口声明需要携带令牌(与宿主的 AddOpenApi 配置合并生效) + services.Configure(options => + { + // 在 OpenAPI 文档中声明 Bearer(JWT) 安全方案 + options.Document.AddDocumentTransformer((document, _, _) => + { + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes ??= new Dictionary(); + document.Components.SecuritySchemes.TryAdd("Bearer", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + Description = "登录接口返回的 accessToken,直接粘贴令牌内容即可(无需手动加 Bearer 前缀)。", + }); + return Task.CompletedTask; + }); + + // 仅对标记了 [Authorize] 的接口声明需要携带令牌 + options.Document.AddOperationTransformer((operation, context, _) => + { + IList metadata = context.Description.ActionDescriptor.EndpointMetadata; + bool requiresAuth = metadata.OfType().Any() + && !metadata.OfType().Any(); + if (requiresAuth) + { + operation.Security = + [ + new OpenApiSecurityRequirement + { + [new OpenApiSecuritySchemeReference("Bearer", context.Document, "")] = [], + }, + ]; + } + return Task.CompletedTask; + }); + }); + + return services; + } +} diff --git a/StoreHub.Framework/DependencyInjection/ServiceCollectionExtensions.cs b/StoreHub.Framework/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f4e90a7 --- /dev/null +++ b/StoreHub.Framework/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,122 @@ +using MAX.Core.DependencyInjection; +using MAX.Core.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; +using System.Reflection; + +namespace StoreHub.Framework.DependencyInjection +{ + /// + /// IOC 注册入口(由宿主 Program.cs 调用) + /// + public static class ServiceCollectionExtensions + { + private static readonly Type[] DependencyMarkers = + [ + typeof(ISingletonDependency), + typeof(IScopedDependency), + typeof(ITransientDependency), + ]; + + /// + /// 注册应用服务: + /// + /// 自动注册所有实现 / / 的类型; + /// 发现所有 ,按 降序执行 Initialize + Register。 + /// + /// + public static IServiceCollection AddStoreHub(this IServiceCollection services) + { + IEnumerable assemblies = GetApplicationAssemblies(); + + RegisterDependencyMarkers(services, assemblies); + RunStartups(services, assemblies); + + return services; + } + + /// + /// 自动注册标记接口(ISingletonDependency / IScopedDependency / ITransientDependency)的实现类型 + /// + private static void RegisterDependencyMarkers(IServiceCollection services, IEnumerable assemblies) + { + foreach (Type type in GetConcreteTypes(assemblies)) + { + Debug.WriteLine(type.Namespace); + Type? marker = DependencyMarkers.FirstOrDefault(m => m.IsAssignableFrom(type)); + if (marker is null) + { + continue; + } + + ServiceLifetime lifetime = marker == typeof(ISingletonDependency) ? ServiceLifetime.Singleton + : marker == typeof(IScopedDependency) ? ServiceLifetime.Scoped + : ServiceLifetime.Transient; + + // 注册为自身 + services.Add(new ServiceDescriptor(type, type, lifetime)); + + // 注册为实现的接口(排除标记接口与系统接口) + foreach (Type iface in type.GetInterfaces()) + { + if (DependencyMarkers.Contains(iface) || IsSystemInterface(iface)) + { + continue; + } + + services.Add(new ServiceDescriptor(iface, type, lifetime)); + } + } + } + + /// + /// 发现所有 实现,按 降序执行 + /// + private static void RunStartups(IServiceCollection services, IEnumerable assemblies) + { + IServiceRegistrar registrar = new ServiceRegistrarProxy(services); + + foreach (IStartup startup in GetConcreteTypes(assemblies) + .Where(t => typeof(IStartup).IsAssignableFrom(t)) + .Select(t => (IStartup)Activator.CreateInstance(t, true)!) + .OrderByDescending(s => s.Priority)) + { + startup.Initialize(); + startup.Register(registrar); + } + } + + private static IEnumerable GetConcreteTypes(IEnumerable assemblies) + { + return assemblies + .SelectMany(SafeGetTypes) + .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericTypeDefinition); + } + + private static IEnumerable SafeGetTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.OfType(); + } + } + + private static IEnumerable GetApplicationAssemblies() + { + // 使用 AppTypeFinder:扫描应用输出目录 + 已加载程序集, + return StoreHubAppTypeFinder.Current.GetAssemblies(); + } + + private static bool IsSystemInterface(Type type) + { + return type.Namespace is null + || type.Namespace.StartsWith("System", StringComparison.Ordinal) + || type.Namespace.StartsWith("Microsoft", StringComparison.Ordinal); + } + + } +} diff --git a/StoreHub.Framework/DependencyInjection/ServiceRegistrarProxy.cs b/StoreHub.Framework/DependencyInjection/ServiceRegistrarProxy.cs new file mode 100644 index 0000000..e32b629 --- /dev/null +++ b/StoreHub.Framework/DependencyInjection/ServiceRegistrarProxy.cs @@ -0,0 +1,88 @@ +using MAX.Core.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace StoreHub.Framework.DependencyInjection +{ + /// + /// 将 适配到 Microsoft.Extensions.DependencyInjection + /// + internal sealed class ServiceRegistrarProxy : IServiceRegistrar + { + private readonly IServiceCollection _services; + + public ServiceRegistrarProxy(IServiceCollection services) + { + _services = services; + } + + public void RegisterSingleton() where TService : class where TImplementation : class, TService + { + _services.AddSingleton(); + } + + public void RegisterSingleton(TService instance) where TService : class + { + _services.AddSingleton(instance); + } + + public void RegisterSingleton(Func factory) where TService : class + { + _services.AddSingleton(factory); + } + + public void RegisterSingleton(object? serviceKey) where TService : class where TImplementation : class, TService + { + _services.AddKeyedSingleton(serviceKey); + } + + public void RegisterSingleton(object? serviceKey, TService instance) where TService : class + { + _services.AddKeyedSingleton(serviceKey, instance); + } + + public void RegisterSingleton(object? serviceKey, Func factory) where TService : class + { + _services.AddKeyedSingleton(serviceKey, factory); + } + + public void RegisterScoped() where TService : class where TImplementation : class, TService + { + _services.AddScoped(); + } + + public void RegisterScoped(Func factory) where TService : class + { + _services.AddScoped(factory); + } + + public void RegisterScoped(object? serviceKey) where TService : class where TImplementation : class, TService + { + _services.AddKeyedScoped(serviceKey); + } + + public void RegisterScoped(object? serviceKey, Func factory) where TService : class + { + _services.AddKeyedScoped(serviceKey, factory); + } + + public void RegisterTransient() where TService : class where TImplementation : class, TService + { + _services.AddTransient(); + } + + public void RegisterTransient(Func factory) where TService : class + { + _services.AddTransient(factory); + } + + public void RegisterTransient(object? serviceKey) where TService : class where TImplementation : class, TService + { + _services.AddKeyedTransient(serviceKey); + } + + public void RegisterTransient(object? serviceKey, Func factory) where TService : class + { + _services.AddKeyedTransient(serviceKey, factory); + } + } +} diff --git a/StoreHub.Framework/DependencyInjection/StoreHubAppTypeFinder.cs b/StoreHub.Framework/DependencyInjection/StoreHubAppTypeFinder.cs new file mode 100644 index 0000000..b24cf32 --- /dev/null +++ b/StoreHub.Framework/DependencyInjection/StoreHubAppTypeFinder.cs @@ -0,0 +1,18 @@ +using MAX.Core.Reflections; + +namespace StoreHub.Framework.DependencyInjection +{ + /// + /// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外, + /// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。 + /// + internal sealed class StoreHubAppTypeFinder : AppTypeFinder + { + public static StoreHubAppTypeFinder Current { get; } = new(); + + protected override List AvailablePrefix() + { + return ["StoreHub"]; + } + } +} diff --git a/StoreHub.Framework/Helpers/ObjectHelper.cs b/StoreHub.Framework/Helpers/ObjectHelper.cs new file mode 100644 index 0000000..0fdbcf3 --- /dev/null +++ b/StoreHub.Framework/Helpers/ObjectHelper.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace StoreHub.Framework.Helpers; + +public static class ObjectHelper +{ + private static readonly ConcurrentDictionary CachedObjectProperties = + new(); + + public static void TrySetProperty( + TObject obj, + Expression> propertySelector, + Func valueFactory, + params Type[] ignoreAttributeTypes + ) + { + TrySetProperty(obj, propertySelector, x => valueFactory(), ignoreAttributeTypes); + } + + public static void TrySetProperty( + TObject obj, + Expression> propertySelector, + Func valueFactory, + params Type[] ignoreAttributeTypes + ) + { + string cacheKey = + $"{obj?.GetType().FullName}-" + + $"{propertySelector}-" + + $"{(ignoreAttributeTypes != null ? "-" + string.Join("-", ignoreAttributeTypes.Select(x => x.FullName)) : "")}"; + + PropertyInfo property = CachedObjectProperties.GetOrAdd( + cacheKey, + (name) => + { + if (propertySelector.Body.NodeType != ExpressionType.MemberAccess) + { + return null; + } + + MemberExpression memberExpression = (MemberExpression)propertySelector.Body; + + PropertyInfo? propertyInfo = obj?.GetType() + .GetProperties() + .FirstOrDefault( + x => + x.Name == memberExpression.Member.Name + && x.GetSetMethod(true) != null + ); + + if (propertyInfo == null) + { + return null; + } + + if ( + ignoreAttributeTypes != null + && ignoreAttributeTypes.Any( + ignoreAttribute => propertyInfo.IsDefined(ignoreAttribute, true) + ) + ) + { + return null; + } + + return propertyInfo; + } + ); + + property?.SetValue(obj, valueFactory(obj)); + } +} diff --git a/StoreHub.Framework/Helpers/WinCmdHelper.cs b/StoreHub.Framework/Helpers/WinCmdHelper.cs new file mode 100644 index 0000000..ac7215b --- /dev/null +++ b/StoreHub.Framework/Helpers/WinCmdHelper.cs @@ -0,0 +1,156 @@ +using MAX.Core.Commons; +using System; +using System.Diagnostics; +using System.Text; + +namespace StoreHub.Framework.Helpers; + +public class WinCmdHelper +{ + #region 执行Dos命令 + + /// + /// 执行Dos命令 + /// + /// Dos命令及参数 + /// 是否显示cmd窗口 + /// 执行完毕后是否关闭cmd进程 + /// 成功返回true,失败返回false + public static Result ExcuteDosCommand(string cmd, string workdir, bool isShowCmdWindow = false, bool isWaitForExit = false, bool isCloseCmdProcess = true) + { + Result result = new(); + try + { + StringBuilder errString = new(); + StringBuilder outString = new(); + Process p = new(); + p.StartInfo.FileName = "cmd"; + p.StartInfo.WorkingDirectory = workdir; + p.StartInfo.Arguments = "/c " + cmd; + p.StartInfo.StandardOutputEncoding = Encoding.UTF8; + p.StartInfo.StandardErrorEncoding = Encoding.UTF8; + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.RedirectStandardError = true; + p.StartInfo.CreateNoWindow = !isShowCmdWindow; + p.OutputDataReceived += new DataReceivedEventHandler(delegate (object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + { + outString.AppendLine(e.Data); + Debug.WriteLine(e.Data); + } + }); + p.ErrorDataReceived += new DataReceivedEventHandler(delegate (object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + { + errString.AppendLine(e.Data); + } + }); + p.Start(); + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + if (isWaitForExit) + { + p.WaitForExit(); + } + + if (isCloseCmdProcess) + { + p.Close(); + } + if (errString.Length <= 0) + { + result.Success = true; + result.Data = outString.ToString(); + } + Console.WriteLine(string.Format("成功执行Dos命令[{0}]!", cmd)); + return result; + } + catch (Exception ex) + { + Console.WriteLine("执行命令失败,请检查输入的命令是否正确:" + ex.Message, ex); + result.Msg = ex.Message; + } + + return result; + } + + private static void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + throw new NotImplementedException(); + } + + #endregion + + #region 判断指定的进程是否在运行中 + + /// + /// 判断指定的进程是否在运行中 + /// + /// 要判断的进程名称,不包括扩展名exe + /// 进程文件的完整路径 + /// 存在返回true,否则返回false + public static bool CheckProcessExists(string processName, string processFileName) + { + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (!string.IsNullOrEmpty(processFileName)) + { + if (processFileName == p?.MainModule?.FileName) + { + return true; + } + } + else + { + return true; + } + } + return false; + } + + #endregion + + #region 结束指定的windows进程 + + /// + /// 结束指定的windows进程,如果进程存在 + /// + /// 进程名称,不包含扩展名 + /// 进程文件完整路径,如果为空则删除所有进程名为processName参数值的进程 + public static bool KillProcessExists(string processName, string processFileName) + { + try + { + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (!string.IsNullOrEmpty(processFileName)) + { + if (processFileName == p.MainModule.FileName) + { + p.Kill(); + p.Close(); + } + } + else + { + p.Kill(); + p.Close(); + } + } + Console.WriteLine(string.Format("成功结束[{0}]进程!", processes)); + return true; + } + catch (Exception ex) + { + Console.WriteLine("结束指定的Widnows进程异常:" + ex.Message, ex); + return false; + } + } + + #endregion +} diff --git a/StoreHub.Framework/Logger/NLogger.cs b/StoreHub.Framework/Logger/NLogger.cs new file mode 100644 index 0000000..0b81559 --- /dev/null +++ b/StoreHub.Framework/Logger/NLogger.cs @@ -0,0 +1,138 @@ +using MAX.Core.Infrastructure; +using MAX.Core.Logger; +using NLog; +using System; +using LLogLevel = MAX.Core.Logger.LogLevel; +using NLogLevel = NLog.LogLevel; + +namespace StoreHub.Framework.Logger; + +public class NLogger : SingleInstance, ILoggerFacade +{ + private readonly ILogger _log; + + private NLogger() + { + _log = LogManager.GetLogger("GlobalLogger"); + } + + public bool IsDebugEnabled { get; private set; } = true; + + public void Debug(Exception ex, string message) + { + Execute(NLogLevel.Debug, ex, message); + } + + public void Debug(string message) + { + Execute(NLogLevel.Debug, message); + } + public void Error(Exception ex) + { + Execute(NLogLevel.Error, ex); + } + public void Error(Exception ex, string message) + { + Execute(NLogLevel.Error, ex, message); + } + + public void Error(string message) + { + Execute(NLogLevel.Error, message); + } + + public void Fatal(Exception ex, string message) + { + Execute(NLogLevel.Fatal, ex, message); + } + + public void Fatal(string message) + { + Execute(NLogLevel.Fatal, message); + } + + public void Info(Exception ex, string message) + { + Execute(NLogLevel.Info, ex, message); + } + + public void Info(string message) + { + Execute(NLogLevel.Info, message); + } + + public void Warn(Exception ex, string message) + { + Execute(NLogLevel.Warn, ex, message); + } + + public void Warn(string message) + { + Execute(NLogLevel.Warn, message); + } + public void Log(string message, LLogLevel level) + { + NLogLevel lv = NLogLevel.Info; + switch (level) + { + case LLogLevel.Trace: + lv = NLogLevel.Trace; + break; + case LLogLevel.Debug: + lv = NLogLevel.Debug; + break; + case LLogLevel.Info: + lv = NLogLevel.Info; + break; + case LLogLevel.Warn: + lv = NLogLevel.Warn; + break; + case LLogLevel.Error: + lv = NLogLevel.Error; + break; + case LLogLevel.Fatal: + lv = NLogLevel.Fatal; + break; + case LLogLevel.None: + lv = NLogLevel.Off; + break; + } + Execute(lv, message); + } + /// + /// 是否启用 + /// + private bool Enabled(NLogLevel level) + { + if (level > NLogLevel.Debug) + { + return true; + } + + return IsDebugEnabled; + } + + /// + /// 执行 + /// + private void Execute(NLogLevel level, Exception ex, string message = "") + { + if (Enabled(level)) + { + _log.Log(level, ex, message, null); + } + } + + + /// + /// 执行 + /// + private void Execute(NLogLevel level, string message) + { + if (Enabled(level)) + { + _log.Log(level, message); + } + } + +} diff --git a/StoreHub.Framework/Models/Users.cs b/StoreHub.Framework/Models/Users.cs new file mode 100644 index 0000000..8dfb42f --- /dev/null +++ b/StoreHub.Framework/Models/Users.cs @@ -0,0 +1,146 @@ +using FreeSql.DataAnnotations; +using System; +using System.Collections.Generic; +using System.Text; + +namespace StoreHub.Framework.Models +{ + /// + /// 用户实体 + /// + //[Table(Name = "user")] + public class User + { + /// + /// 用户ID,主键,自增 + /// + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public int Id { get; set; } + + /// + /// 用户昵称 + /// + [Column(Name = "nick_name", DbType = "varchar(50)")] + public string NickName { get; set; } + + /// + /// 用户名称(登录账户) + /// + [Column(Name = "user_name", DbType = "varchar(20)")] + public string UserName { get; set; } + + /// + /// 登录密码 + /// + [Column(Name = "password", DbType = "varchar(32)")] + public string Password { get; set; } + + /// + /// 身份证号 + /// + [Column(Name = "card_id", DbType = "varchar(50)")] + public string CardId { get; set; } + + /// + /// 用户分组:3主号 4客服 5加盟 8登录 9采购 10加盟 11上货,默认10 + /// + [Column(Name = "group_id")] + public byte GroupId { get; set; } = 10; + + /// + /// 父账户用户名 + /// + [Column(Name = "parent_user_name", DbType = "varchar(20)")] + public string ParentUserName { get; set; } + + /// + /// OEM编号,仅OEM用户有值 + /// + [Column(Name = "oem_id", DbType = "varchar(20)")] + public string OemId { get; set; } + + /// + /// 手机号 + /// + [Column(Name = "mobile", DbType = "varchar(20)")] + public string Mobile { get; set; } + + /// + /// 添加时间 + /// + [Column(Name = "add_time")] + public DateTime? AddTime { get; set; } + + /// + /// 修改时间 + /// + [Column(Name = "mdy_time")] + public DateTime? MdyTime { get; set; } + + /// + /// 公司名称 + /// + [Column(Name = "company_name", DbType = "varchar(50)")] + public string CompanyName { get; set; } + + /// + /// 备注 + /// + [Column(Name = "notes", DbType = "varchar(50)")] + public string Notes { get; set; } + + /// + /// 超管等级:0普通用户 1超级管理员,默认0 + /// + [Column(Name = "sa_level")] + public short SaLevel { get; set; } = 0; + + /// + /// 账户权限 + /// + [Column(Name = "authority", DbType = "varchar(255)")] + public string Authority { get; set; } + + /// + /// 子账号被禁止展示的功能页面 + /// + [Column(Name = "forbidden_page", DbType = "varchar(500)")] + public string ForbiddenPage { get; set; } + + /// + /// 账户积分,1积分=1元,默认0.00 + /// + [Column(Name = "points", DbType = "decimal(10,2)")] + public decimal Points { get; set; } = 0.00m; + + /// + /// 积分历史累计充值金额 + /// + [Column(Name = "recharge_points_total", DbType = "decimal(10,2)")] + public decimal? RechargePointsTotal { get; set; } + + /// + /// 店铺每续费一个月需要的积分(单位:元),默认30 + /// + [Column(Name = "shop_points_per_month")] + public int ShopPointsPerMonth { get; set; } = 30; + + /// + /// 可用余额,默认0.00 + /// + [Column(Name = "balance", DbType = "decimal(10,2)")] + public decimal? Balance { get; set; } = 0.00m; + + /// + /// 累计充值金额,默认0.00 + /// + [Column(Name = "recharge_amount_total", DbType = "decimal(10,2)")] + public decimal? RechargeAmountTotal { get; set; } = 0.00m; + + /// + /// 每单中转仓快递费用(单位:元,两位小数),默认2.20 + /// + [Column(Name = "postage_per_order", DbType = "decimal(10,2)")] + public decimal? PostagePerOrder { get; set; } = 2.20m; + } +} diff --git a/StoreHub.Framework/Repositorys/SqlRepository.cs b/StoreHub.Framework/Repositorys/SqlRepository.cs new file mode 100644 index 0000000..b970a79 --- /dev/null +++ b/StoreHub.Framework/Repositorys/SqlRepository.cs @@ -0,0 +1,34 @@ +using FreeSql; +using MAX.Core.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using StoreHub.Framework.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace StoreHub.Framework.Repositorys +{ + public interface IUserRepository : IBaseRepository + { + + } + public class SqlRepository : WxRepository, IUserRepository, IScopedDependency + { + public SqlRepository([FromKeyedServices("KJDDB")] IFreeSql fsql) : base(fsql) + { + + } + } + public abstract class WxRepository : BaseRepository + where TEntity : class + { + protected WxRepository(IFreeSql fsql) + : base(fsql) + { + fsql.CodeFirst.ConfigEntity(a => + { + a.Name($"wxxd.{typeof(TEntity).Name}"); + }); + } + } +} diff --git a/StoreHub.Framework/Security/CurrentUser.cs b/StoreHub.Framework/Security/CurrentUser.cs new file mode 100644 index 0000000..0d247fd --- /dev/null +++ b/StoreHub.Framework/Security/CurrentUser.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Http; +using System.Globalization; +using System.Security.Claims; + +namespace StoreHub.Framework.Security; + +/// +/// 从 读取当前请求认证票据的默认实现。 +/// +public class CurrentUser : ICurrentUser +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public CurrentUser(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + /// 当前请求的 + /// + private ClaimsPrincipal? Principal => _httpContextAccessor.HttpContext?.User; + + /// + public bool IsAuthenticated => Principal?.Identity?.IsAuthenticated == true; + + /// + public int? Id + { + get + { + string? value = GetClaimValue(JwtClaimTypes.UserId); + return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int id) ? id : null; + } + } + + /// + public string? UserName => GetClaimValue(JwtClaimTypes.UserName); + + /// + public string? NickName => GetClaimValue(JwtClaimTypes.NickName); + + /// + public IEnumerable Claims => Principal?.Claims ?? []; + + /// + public string? GetClaimValue(string claimType) + { + if (string.IsNullOrWhiteSpace(claimType)) + { + return null; + } + + return Principal?.FindFirst(claimType)?.Value; + } + + /// + public T? GetClaimValue(string claimType) + { + string? value = GetClaimValue(claimType); + if (value is null) + { + return default; + } + + try + { + return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); + } + catch + { + return default; + } + } +} + diff --git a/StoreHub.Framework/Security/ICurrentUser.cs b/StoreHub.Framework/Security/ICurrentUser.cs new file mode 100644 index 0000000..33989ce --- /dev/null +++ b/StoreHub.Framework/Security/ICurrentUser.cs @@ -0,0 +1,44 @@ +using System.Security.Claims; + +namespace StoreHub.Framework.Security; + +/// +/// 当前登录用户访问器:从当前请求的认证票据中读取用户信息。 +/// +public interface ICurrentUser +{ + /// + /// 当前请求是否已认证 + /// + bool IsAuthenticated { get; } + + /// + /// 当前用户ID(未登录为 null) + /// + int? Id { get; } + + /// + /// 当前登录账户名(未登录为 null) + /// + string? UserName { get; } + + /// + /// 当前用户昵称(未登录为 null) + /// + string? NickName { get; } + + /// + /// 当前用户的全部声明 + /// + IEnumerable Claims { get; } + + /// + /// 获取指定类型的声明值 + /// + string? GetClaimValue(string claimType); + + /// + /// 获取指定类型的声明值并转换为目标类型;不存在或转换失败返回默认值 + /// + T? GetClaimValue(string claimType); +} diff --git a/StoreHub.Framework/Security/IOnlineUserStore.cs b/StoreHub.Framework/Security/IOnlineUserStore.cs new file mode 100644 index 0000000..3f40302 --- /dev/null +++ b/StoreHub.Framework/Security/IOnlineUserStore.cs @@ -0,0 +1,34 @@ +namespace StoreHub.Framework.Security; + +/// +/// 在线用户会话存储:基于 的进程内实现, +/// 用于单点登录(同一用户仅保留最新会话)、在线人员管理与退出销毁。 +/// +public interface IOnlineUserStore +{ + /// + /// 用户登录/刷新会话:同用户的新登录会覆盖旧会话,旧令牌立即失效(单点登录)。 + /// + void SignIn(OnlineUserInfo info); + + /// + /// 判断指定用户、指定令牌会话是否仍然有效(未被退出、未被新登录顶替)。 + /// + bool IsTokenActive(int userId, string jti); + + /// + /// 退出登录:销毁该用户的在线会话,令牌立即失效。 + /// + void SignOut(int userId); + + /// + /// 获取当前在线人员列表。 + /// + IReadOnlyList GetOnlineUsers(); + + /// + /// 当前在线人数。 + /// + int Count { get; } +} + diff --git a/StoreHub.Framework/Security/ITokenService.cs b/StoreHub.Framework/Security/ITokenService.cs new file mode 100644 index 0000000..7f43600 --- /dev/null +++ b/StoreHub.Framework/Security/ITokenService.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; + +namespace StoreHub.Framework.Security; + +/// +/// 令牌生成结果 +/// +public class TokenResult +{ + /// + /// 令牌类型(Bearer) + /// + public string TokenType { get; set; } = "Bearer"; + + /// + /// 访问令牌 + /// + public string Token { get; set; } = string.Empty; + + /// + /// 过期时间(本地时间) + /// + public DateTime ExpiresAt { get; set; } +} + +/// +/// JWT 令牌服务:生成与校验访问令牌。 +/// +public interface ITokenService +{ + /// + /// 根据声明集合生成访问令牌 + /// + TokenResult CreateToken(IEnumerable claims); + + /// + /// 校验令牌,校验失败返回 null + /// + Task ValidateTokenAsync(string token); +} \ No newline at end of file diff --git a/StoreHub.Framework/Security/JwtBearerResponseHandler.cs b/StoreHub.Framework/Security/JwtBearerResponseHandler.cs new file mode 100644 index 0000000..b9db50a --- /dev/null +++ b/StoreHub.Framework/Security/JwtBearerResponseHandler.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using StoreHub.Framework.Commons; +using System.Text; +using System.Text.Json; + +namespace StoreHub.Framework.Security; + +/// +/// JwtBearer 认证失败/授权失败时,统一返回 格式的 JSON 响应。 +/// +internal static class JwtBearerResponseHandler +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + /// + /// 未携带令牌或令牌无效(401) + /// + public static Task OnChallenge(JwtBearerChallengeContext context) + { + context.HandleResponse(); + return WriteAsync(context.HttpContext, StatusCodes.Status401Unauthorized, "未授权:请先登录或携带有效的访问令牌"); + } + + /// + /// 已认证但无权限(403) + /// + public static Task OnForbidden(ForbiddenContext context) + { + return WriteAsync(context.HttpContext, StatusCodes.Status403Forbidden, "禁止访问:当前账号没有权限执行该操作"); + } + + private static async Task WriteAsync(HttpContext httpContext, int statusCode, string message) + { + httpContext.Response.StatusCode = statusCode; + httpContext.Response.ContentType = "application/json; charset=utf-8"; + + string body = JsonSerializer.Serialize(ApiResult.Error(message, statusCode), JsonOptions); + await httpContext.Response.WriteAsync(body, Encoding.UTF8); + } +} + diff --git a/StoreHub.Framework/Security/JwtClaimTypes.cs b/StoreHub.Framework/Security/JwtClaimTypes.cs new file mode 100644 index 0000000..57f100f --- /dev/null +++ b/StoreHub.Framework/Security/JwtClaimTypes.cs @@ -0,0 +1,19 @@ +namespace StoreHub.Framework.Security; + +/// +/// JWT 自定义声明类型常量。 +/// +public static class JwtClaimTypes +{ + /// 用户ID + public const string UserId = "uid"; + + /// 登录账户 + public const string UserName = "user_name"; + + /// 用户昵称 + public const string NickName = "nick_name"; + + /// 令牌唯一标识(jti,用于单点登录/退出校验) + public const string Jti = "jti"; +} diff --git a/StoreHub.Framework/Security/JwtOptions.cs b/StoreHub.Framework/Security/JwtOptions.cs new file mode 100644 index 0000000..4fbc39a --- /dev/null +++ b/StoreHub.Framework/Security/JwtOptions.cs @@ -0,0 +1,74 @@ +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace StoreHub.Framework.Security; + +/// +/// JWT 配置选项,默认绑定配置节 Jwt。 +/// +public class JwtOptions +{ + /// + /// 配置节名称 + /// + public const string SectionName = "Jwt"; + + /// + /// 签发者 + /// + public string Issuer { get; set; } = "StoreHub"; + + /// + /// 受众 + /// + public string Audience { get; set; } = "StoreHub.Client"; + + /// + /// 签名密钥(HS256,至少 32 字节) + /// + public string SigningKey { get; set; } = "StoreHub-Development-SigningKey-0123456789abcdef0123456789abcdef"; + + /// + /// 令牌有效期(分钟) + /// + public int ExpiresMinutes { get; set; } = 120; + + /// + /// 时钟偏移(分钟),容忍客户端与服务器的时间偏差 + /// + public int ClockSkewMinutes { get; set; } = 5; + + /// + /// 是否校验签发者 + /// + public bool ValidateIssuer { get; set; } = true; + + /// + /// 是否校验受众 + /// + public bool ValidateAudience { get; set; } = true; + + /// + /// 是否校验有效期 + /// + public bool ValidateLifetime { get; set; } = true; + + /// + /// 根据当前选项构建 ,用于 JwtBearer 中间件与令牌校验。 + /// + public TokenValidationParameters ToTokenValidationParameters() + { + byte[] keyBytes = Encoding.UTF8.GetBytes(SigningKey); + return new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(keyBytes), + ValidateIssuer = ValidateIssuer, + ValidIssuer = Issuer, + ValidateAudience = ValidateAudience, + ValidAudience = Audience, + ValidateLifetime = ValidateLifetime, + ClockSkew = TimeSpan.FromMinutes(ClockSkewMinutes), + }; + } +} diff --git a/StoreHub.Framework/Security/OnlineUserInfo.cs b/StoreHub.Framework/Security/OnlineUserInfo.cs new file mode 100644 index 0000000..6491f82 --- /dev/null +++ b/StoreHub.Framework/Security/OnlineUserInfo.cs @@ -0,0 +1,26 @@ +namespace StoreHub.Framework.Security; + +/// +/// 在线用户会话信息(单点登录:同一用户仅保留一个有效会话)。 +/// +public class OnlineUserInfo +{ + /// 用户ID + public int UserId { get; set; } + + /// 登录账户 + public string UserName { get; set; } = string.Empty; + + /// 用户昵称 + public string NickName { get; set; } = string.Empty; + + /// 当前有效会话的令牌唯一标识(jti) + public string Jti { get; set; } = string.Empty; + + /// 登录时间(本地时间) + public DateTime LoginTime { get; set; } + + /// 会话过期时间(本地时间,与令牌 exp 一致) + public DateTime ExpiresAt { get; set; } +} + diff --git a/StoreHub.Framework/Security/OnlineUserStore.cs b/StoreHub.Framework/Security/OnlineUserStore.cs new file mode 100644 index 0000000..844e083 --- /dev/null +++ b/StoreHub.Framework/Security/OnlineUserStore.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.Caching.Memory; +using System.Collections.Concurrent; + +namespace StoreHub.Framework.Security; + +/// +/// 基于 的在线用户会话存储。 +/// +public class OnlineUserStore : IOnlineUserStore +{ + private const string CacheKeyPrefix = "online:user:"; + private readonly IMemoryCache _cache; + // 在线人员索引:IMemoryCache 不提供公共枚举 API,这里用字典维护便于查询在线列表 + private readonly ConcurrentDictionary _onlineIndex = new(); + + public OnlineUserStore(IMemoryCache cache) + { + _cache = cache; + } + + /// + public void SignIn(OnlineUserInfo info) + { + ArgumentNullException.ThrowIfNull(info); + + // 以 userId 为键覆盖写入:旧会话(jti)被顶替,实现单点登录 + _cache.Set(GetKey(info.UserId), info, new MemoryCacheEntryOptions + { + // 缓存条目与令牌同时过期,过期后自动失效 + AbsoluteExpiration = info.ExpiresAt, + }); + _onlineIndex[info.UserId] = info; + } + + /// + public bool IsTokenActive(int userId, string jti) + { + if (string.IsNullOrWhiteSpace(jti)) + { + return false; + } + + return _cache.TryGetValue(GetKey(userId), out OnlineUserInfo? info) + && info is not null + && string.Equals(info.Jti, jti, StringComparison.Ordinal); + } + + /// + public void SignOut(int userId) + { + _cache.Remove(GetKey(userId)); + _onlineIndex.TryRemove(userId, out _); + } + + /// + public IReadOnlyList GetOnlineUsers() + { + DateTime now = DateTime.Now; + List result = []; + foreach (KeyValuePair pair in _onlineIndex) + { + if (pair.Value.ExpiresAt > now) + { + result.Add(pair.Value); + } + else + { + _onlineIndex.TryRemove(pair.Key, out _); + } + } + + return result; + } + + /// + public int Count => GetOnlineUsers().Count; + + private static string GetKey(int userId) => $"{CacheKeyPrefix}{userId}"; +} + diff --git a/StoreHub.Framework/Security/TokenService.cs b/StoreHub.Framework/Security/TokenService.cs new file mode 100644 index 0000000..244490b --- /dev/null +++ b/StoreHub.Framework/Security/TokenService.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using System.Security.Claims; +using System.Text; + +namespace StoreHub.Framework.Security; + +/// +/// 基于 的 JWT 令牌服务。 +/// +public class TokenService : ITokenService +{ + private readonly JwtOptions _options; + private readonly IOnlineUserStore _onlineUserStore; + + public TokenService(IOptions options, IOnlineUserStore onlineUserStore) + { + _options = options.Value; + _onlineUserStore = onlineUserStore; + } + + /// + public TokenResult CreateToken(IEnumerable claims) + { + ArgumentNullException.ThrowIfNull(claims); + + DateTime now = DateTime.Now; + DateTime expires = now.AddMinutes(_options.ExpiresMinutes); + string jti = Guid.NewGuid().ToString("N"); + + List allClaims = [.. claims, new Claim(JwtClaimTypes.Jti, jti)]; + + SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.SigningKey)); + JsonWebTokenHandler handler = new(); + + string token = handler.CreateToken(new SecurityTokenDescriptor + { + Issuer = _options.Issuer, + Audience = _options.Audience, + Claims = ToClaimDictionary(allClaims), + NotBefore = now, + Expires = expires, + SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256), + }); + + // 注册在线会话(单点登录:同用户的新登录会顶替旧会话,旧令牌立即失效) + RegisterSession(allClaims, jti, now, expires); + + return new TokenResult + { + Token = token, + ExpiresAt = expires, + }; + } + + /// + public async Task ValidateTokenAsync(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + return null; + } + + try + { + JsonWebTokenHandler handler = new(); + TokenValidationResult result = await handler.ValidateTokenAsync(token, _options.ToTokenValidationParameters()); + if (!result.IsValid) + { + return null; + } + + ClaimsPrincipal principal = new(result.ClaimsIdentity); + + // 单点登录校验:会话必须仍然有效(未被退出、未被新登录顶替) + string? userIdValue = principal.FindFirst(JwtClaimTypes.UserId)?.Value; + string? jti = principal.FindFirst(JwtClaimTypes.Jti)?.Value; + if (!int.TryParse(userIdValue, out int userId) || string.IsNullOrWhiteSpace(jti) || !_onlineUserStore.IsTokenActive(userId, jti)) + { + return null; + } + + return principal; + } + catch + { + return null; + } + } + + /// + /// 将令牌声明注册为在线会话。 + /// + private void RegisterSession(IEnumerable claims, string jti, DateTime loginTime, DateTime expiresAt) + { + string? userIdValue = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserId)?.Value; + if (!int.TryParse(userIdValue, out int userId)) + { + return; // 未携带用户ID的令牌不纳入在线会话管理 + } + + _onlineUserStore.SignIn(new OnlineUserInfo + { + UserId = userId, + UserName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserName)?.Value ?? string.Empty, + NickName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.NickName)?.Value ?? string.Empty, + Jti = jti, + LoginTime = loginTime, + ExpiresAt = expiresAt, + }); + } + + /// + /// 将声明集合转换为 所需的字典; + /// 相同类型的多个声明合并为数组。 + /// + private static Dictionary ToClaimDictionary(IEnumerable claims) + { + Dictionary result = new(StringComparer.Ordinal); + foreach (IGrouping group in claims.GroupBy(c => c.Type, StringComparer.Ordinal)) + { + object[] values = group.Select(c => (object)c.Value).ToArray(); + result[group.Key] = values.Length == 1 ? values[0] : values; + } + + return result; + } +} + diff --git a/StoreHub.Framework/Startup.cs b/StoreHub.Framework/Startup.cs new file mode 100644 index 0000000..a16e7bc --- /dev/null +++ b/StoreHub.Framework/Startup.cs @@ -0,0 +1,53 @@ +using FreeSql; +using MAX.Core.DependencyInjection; +using MAX.Core.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StoreHub.Framework.Repositorys; + +namespace StoreHub.Framework +{ + public class Startup : IStartup + { + public int Priority => int.MaxValue - 1; + + public void Initialize() + { + + } + + public void Register(IServiceRegistrar registrar) + { + Func userFsqlFactory = r => + { + var config = r.GetRequiredService(); + var connection = config.GetConnectionString("UserConnection"); + IFreeSql fsql = new FreeSql.FreeSqlBuilder() + .UseConnectionString(FreeSql.DataType.MySql, connection) + .UseAdoConnectionPool(true) + .Build(); + return fsql; + }; + registrar.RegisterSingleton("UserDB", (s, k) => + { + return userFsqlFactory(s); + }); + + + Func KJDFsqlFactory = r => + { + var config = r.GetRequiredService(); + var connection = config.GetConnectionString("KJDConnection"); + IFreeSql fsql = new FreeSql.FreeSqlBuilder() + .UseConnectionString(FreeSql.DataType.MySql, connection) + .UseAdoConnectionPool(true) + .Build(); + return fsql; + }; + registrar.RegisterSingleton("KJDDB", (s, k) => + { + return KJDFsqlFactory(s); + }); + } + } +} diff --git a/StoreHub.Framework/StoreHub.Framework.csproj b/StoreHub.Framework/StoreHub.Framework.csproj new file mode 100644 index 0000000..4a6063e --- /dev/null +++ b/StoreHub.Framework/StoreHub.Framework.csproj @@ -0,0 +1,26 @@ + + + + enable + + + + + + + + + + + + + + + + + + + + + + diff --git a/StoreHub.KJDWebService/Controllers/Login.cs b/StoreHub.KJDWebService/Controllers/Login.cs new file mode 100644 index 0000000..e78e127 --- /dev/null +++ b/StoreHub.KJDWebService/Controllers/Login.cs @@ -0,0 +1,26 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using StoreHub.Framework.Commons; +using StoreHub.KJDWebService.Services; + +namespace StoreHub.KJDWebService.Controllers +{ + [ApiController] + [ApiVersion("1.0")] + [Route("api/v{version:apiVersion}/[controller]/[action]")] + public class Login : ControllerBase + { + private readonly DemoService _userService; + + public Login(DemoService userService) + { + _userService = userService; + } + [HttpGet] + public ApiResult Loinout() + { + _userService.Login(); + return ApiResult.Success(true); + } + } +} diff --git a/StoreHub.KJDWebService/Properties/launchSettings.json b/StoreHub.KJDWebService/Properties/launchSettings.json new file mode 100644 index 0000000..117ccda --- /dev/null +++ b/StoreHub.KJDWebService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "KJD_WebService": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57509;http://localhost:57510" + } + } +} \ No newline at end of file diff --git a/StoreHub.KJDWebService/Repositorys/SqlRepository.cs b/StoreHub.KJDWebService/Repositorys/SqlRepository.cs new file mode 100644 index 0000000..394c275 --- /dev/null +++ b/StoreHub.KJDWebService/Repositorys/SqlRepository.cs @@ -0,0 +1,21 @@ +using FreeSql; +using MAX.Core.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using StoreHub.Framework.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace StoreHub.Framework.Repositorys +{ + + public class SqlRepository : IScopedDependency + { + public IAdo Ado { get; } + public SqlRepository([FromKeyedServices("KJDDB")] IFreeSql fsql) + { + Ado = fsql.Ado; + } + } + +} diff --git a/StoreHub.KJDWebService/Services/DemoService.cs b/StoreHub.KJDWebService/Services/DemoService.cs new file mode 100644 index 0000000..7ea7a96 --- /dev/null +++ b/StoreHub.KJDWebService/Services/DemoService.cs @@ -0,0 +1,27 @@ +using MAX.Core.DependencyInjection; +using StoreHub.Framework.Repositorys; +using System; +using System.Collections.Generic; +using System.Text; + +namespace StoreHub.KJDWebService.Services +{ + public class DemoService : ITransientDependency + { + private readonly SqlRepository _sqlRepository; + + public DemoService(SqlRepository sqlRepository) + { + _sqlRepository = sqlRepository; + } + public bool Test() + { + _sqlRepository.Ado.ExecuteDataTable("select * from user"); + return true; + } + public bool Test2() + { + return true; + } + } +} diff --git a/StoreHub.KJDWebService/Startup.cs b/StoreHub.KJDWebService/Startup.cs new file mode 100644 index 0000000..7883e2d --- /dev/null +++ b/StoreHub.KJDWebService/Startup.cs @@ -0,0 +1,23 @@ +using MAX.Core.DependencyInjection; +using MAX.Core.Infrastructure; +using System; +using System.Collections.Generic; +using System.Text; + +namespace StoreHub.KJDWebService +{ + internal class Startup : MAX.Core.Infrastructure.IStartup + { + public int Priority => 0; + + public void Initialize() + { + + } + + public void Register(IServiceRegistrar registrar) + { + + } + } +} diff --git a/StoreHub.KJDWebService/StoreHub.KJDWebService.csproj b/StoreHub.KJDWebService/StoreHub.KJDWebService.csproj new file mode 100644 index 0000000..850cf91 --- /dev/null +++ b/StoreHub.KJDWebService/StoreHub.KJDWebService.csproj @@ -0,0 +1,18 @@ + + + enable + enable + + + + + + + + + + + + + + diff --git a/StoreHub.Services/Dtos/UserDto.cs b/StoreHub.Services/Dtos/UserDto.cs new file mode 100644 index 0000000..2aa5825 --- /dev/null +++ b/StoreHub.Services/Dtos/UserDto.cs @@ -0,0 +1,17 @@ +namespace StoreHub.Services.Dtos; + +/// +/// 用户信息 DTO(对外暴露的用户数据,不包含密码等敏感字段)。 +/// +public class UserDto +{ + /// 用户ID + public int Id { get; set; } + + /// 登录账户 + public string UserName { get; set; } = string.Empty; + + /// 用户昵称 + public string NickName { get; set; } = string.Empty; +} + diff --git a/StoreHub.Services/Mappers/UserMapper.cs b/StoreHub.Services/Mappers/UserMapper.cs new file mode 100644 index 0000000..2999ffb --- /dev/null +++ b/StoreHub.Services/Mappers/UserMapper.cs @@ -0,0 +1,19 @@ +using Riok.Mapperly.Abstractions; +using StoreHub.Framework.Models; +using StoreHub.Services.Dtos; + +namespace StoreHub.Services.Mappers; + +/// +/// User 实体 → UserDto 映射(Riok.Mapperly 源码生成)。 +/// UserDto 只暴露部分字段,因此仅要求目标成员都被映射(忽略源端未映射警告)。 +/// +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public static partial class UserMapper +{ + /// + /// 将用户实体映射为用户 DTO。 + /// + public static partial UserDto ToDto(User user); +} + diff --git a/StoreHub.Services/StoreHub.Services.csproj b/StoreHub.Services/StoreHub.Services.csproj new file mode 100644 index 0000000..aa71604 --- /dev/null +++ b/StoreHub.Services/StoreHub.Services.csproj @@ -0,0 +1,15 @@ + + + + enable + + + + + + + + + + + diff --git a/StoreHub.Services/UserService.cs b/StoreHub.Services/UserService.cs new file mode 100644 index 0000000..bd9308c --- /dev/null +++ b/StoreHub.Services/UserService.cs @@ -0,0 +1,64 @@ +using FreeSql; +using MAX.Core.Cryptography; +using MAX.Core.DependencyInjection; +using StoreHub.Framework.Models; +using StoreHub.Framework.Repositorys; +using StoreHub.Services.Dtos; +using StoreHub.Services.Mappers; + +namespace StoreHub.Services; + +/// +/// 用户业务服务:登录校验、用户查询等用例。 +/// +public interface IUserService +{ + /// + /// 校验用户名/密码,成功返回用户 DTO,失败返回 null。 + /// + Task LoginAsync(string userName, string password); + + /// + /// 根据用户ID获取用户 DTO,不存在返回 null。 + /// + Task GetByIdAsync(int id); +} + +/// +/// 用户业务服务实现(,由 AddStoreHub 自动注册)。 +/// +public class UserService : IUserService, IScopedDependency +{ + private readonly IUserRepository _userRepository; + + public UserService(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + /// + public async Task LoginAsync(string userName, string password) + { + if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password)) + { + return null; + } + var sql = _userRepository.Where(u => u.UserName == userName).ToSql(); + User? user = await _userRepository.Where(u => u.UserName == userName).FirstAsync(); + string pwd = MD5Helper.Md5_32(password); + if (user is null || !string.Equals(user.Password, pwd, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return UserMapper.ToDto(user); + } + + /// + public async Task GetByIdAsync(int id) + { + User? user = await _userRepository.Where(u => u.Id == id).FirstAsync(); + return user is null ? null : UserMapper.ToDto(user); + } +} + diff --git a/StoreHub.slnx b/StoreHub.slnx new file mode 100644 index 0000000..f6ac79b --- /dev/null +++ b/StoreHub.slnx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/StoreHub/Controllers/AuthController.cs b/StoreHub/Controllers/AuthController.cs new file mode 100644 index 0000000..d2e8b1e --- /dev/null +++ b/StoreHub/Controllers/AuthController.cs @@ -0,0 +1,112 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using StoreHub.Framework.Commons; +using StoreHub.Framework.Security; +using StoreHub.Services; +using StoreHub.Services.Dtos; +using System.Security.Claims; + +namespace StoreHub.Controllers +{ + [ApiController] + [ApiVersion("1.0")] + [Route("api/v{version:apiVersion}/[controller]/[action]")] + public class AuthController : ControllerBase + { + private readonly IUserService _userService; + private readonly ITokenService _tokenService; + private readonly ICurrentUser _currentUser; + private readonly IOnlineUserStore _onlineUserStore; + + public AuthController(IUserService userService, ITokenService tokenService, ICurrentUser currentUser, IOnlineUserStore onlineUserStore) + { + _userService = userService; + _tokenService = tokenService; + _currentUser = currentUser; + _onlineUserStore = onlineUserStore; + } + + /// + /// 登录:校验用户名/密码,签发 JWT(单点登录:同用户的新登录会使旧令牌失效) + /// + [HttpPost] + [AllowAnonymous] + public async Task> Login([FromBody] LoginRequest request) + { + if (request is null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password)) + { + return ApiResult.Error("用户名或密码不能为空", 400); + } + + UserDto? user = await _userService.LoginAsync(request.UserName, request.Password); + if (user is null) + { + return ApiResult.Error("用户名或密码错误", 401); + } + + var claims = new List + { + new(JwtClaimTypes.UserId, user.Id.ToString()), + new(JwtClaimTypes.UserName, user.UserName), + new(JwtClaimTypes.NickName, user.NickName), + }; + + TokenResult token = _tokenService.CreateToken(claims); + return ApiResult.Success(token); + } + + /// + /// 退出登录:销毁当前用户的服务端在线会话,令牌立即失效 + /// + [HttpPost] + [Authorize] + public ApiResult Logout() + { + if (_currentUser.Id is int userId) + { + _onlineUserStore.SignOut(userId); + } + + return ApiResult.Success(null); + } + + /// + /// 获取当前登录用户信息(需携带 Bearer Token) + /// + [HttpGet] + [Authorize] + public ApiResult Me() + { + return ApiResult.Success(new + { + UserId = _currentUser.Id, + UserName = _currentUser.UserName, + NickName = _currentUser.NickName, + }); + } + + /// + /// 在线人员列表(需携带 Bearer Token) + /// + [HttpGet] + [Authorize] + public ApiResult OnlineUsers() + { + IReadOnlyList users = _onlineUserStore.GetOnlineUsers(); + return ApiResult.Success(new + { + Count = users.Count, + Users = users, + }); + } + } + + public class LoginRequest + { + public string UserName { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; + } +} + diff --git a/StoreHub/Controllers/UserController.cs b/StoreHub/Controllers/UserController.cs new file mode 100644 index 0000000..25e6c16 --- /dev/null +++ b/StoreHub/Controllers/UserController.cs @@ -0,0 +1,28 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using StoreHub.Framework.Repositorys; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StoreHub.Controllers +{ + [ApiController] + [Description("测试温度")] + [ApiVersion("1.0")] + [Route("api/v{version:apiVersion}/[controller]")] + public class UserController : ControllerBase + { + private readonly IUserRepository _userRepository; + + public UserController(IUserRepository userRepository) + { + _userRepository = userRepository; + } + [HttpGet] + public string GetUser() + { + return JsonSerializer.Serialize(_userRepository.Select.First()); + } + } +} diff --git a/StoreHub/Controllers/WeatherForecastController.cs b/StoreHub/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..604387d --- /dev/null +++ b/StoreHub/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using StoreHub.Framework.Commons; +using System.ComponentModel; + +namespace StoreHub.Controllers +{ + [ApiController] + [Description("测试温度")] + [ApiVersion("1.0")] + [Route("api/v{version:apiVersion}/[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [HttpGet(Name = "GetWeatherForecast")] + public ApiResult> Get() + { + IEnumerable data = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + + return ApiResult.Success(data); + } + } +} diff --git a/StoreHub/Controllers/v2/WeatherForecastController.cs b/StoreHub/Controllers/v2/WeatherForecastController.cs new file mode 100644 index 0000000..bd97d1b --- /dev/null +++ b/StoreHub/Controllers/v2/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using StoreHub.Framework.Commons; + +namespace StoreHub.Controllers.v2 +{ + [ApiController] + [ApiVersion("2.0")] + [Route("api/v{version:apiVersion}/[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [HttpGet(Name = "GetWeatherForecastV2")] + public ApiResult> Get() + { + IEnumerable data = Enumerable.Range(1, 10).Select(index => new WeatherForecastV2 + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + FeelsLikeC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + + return ApiResult.Success(data); + } + } +} diff --git a/StoreHub/Program.cs b/StoreHub/Program.cs new file mode 100644 index 0000000..45167a5 --- /dev/null +++ b/StoreHub/Program.cs @@ -0,0 +1,62 @@ +using Asp.Versioning; +using Scalar.AspNetCore; +using StoreHub.Framework.DependencyInjection; + +public partial class Program +{ + private static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // IOC 注册:自动注册 ISingletonDependency / IScopedDependency / ITransientDependency,并执行所有 IStartup(实现见 StoreHub.Framework) + builder.Services.AddStoreHub(); + builder.Services.AddFreeSqlDB(); + // JWT 验证:注册 JwtBearer 认证 + 令牌服务 + OpenAPI Bearer 安全方案(配置节 Jwt) + builder.Services.AddStoreHubJwt(builder.Configuration); + // Add services to the container. + + builder.Services.AddControllers(); + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi + builder.Services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + }).AddMvc().AddApiExplorer(options => + { + options.GroupNameFormat = "'v'V"; + options.SubstituteApiVersionInUrl = true; + }).AddOpenApi(); + + #region DB + + + #endregion + + + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi().WithDocumentPerVersion(); + app.MapScalarApiReference(options => + { + + options.Title = "商铺管理API V1"; + options.AddDocuments(new[] { "v1", "v2" }); + }); + } + + app.UseHttpsRedirection(); + + app.UseAuthentication(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } +} \ No newline at end of file diff --git a/StoreHub/Properties/launchSettings.json b/StoreHub/Properties/launchSettings.json new file mode 100644 index 0000000..7f2307f --- /dev/null +++ b/StoreHub/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "scalar/v1", + "applicationUrl": "http://localhost:5295", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "scalar/v1", + "applicationUrl": "https://localhost:7241;http://localhost:5295", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/StoreHub/StoreHub.csproj b/StoreHub/StoreHub.csproj new file mode 100644 index 0000000..0c2b535 --- /dev/null +++ b/StoreHub/StoreHub.csproj @@ -0,0 +1,20 @@ + + + + enable + enable + + + + + + + + + + + + + + + diff --git a/StoreHub/StoreHub.http b/StoreHub/StoreHub.http new file mode 100644 index 0000000..a90691f --- /dev/null +++ b/StoreHub/StoreHub.http @@ -0,0 +1,47 @@ +@StoreHub_HostAddress = http://localhost:5295 + +# ============ 认证(JWT) ============ + +### 登录:返回 accessToken(单点登录,同用户新登录会使旧 token 失效) +# @name login +POST {{StoreHub_HostAddress}}/api/v1/Auth/Login +Content-Type: application/json + +{ + "userName": "18990011234", + "password": "18990011234" +} + +### 获取当前登录用户信息(需携带 Bearer Token,先执行上面的 login) +GET {{StoreHub_HostAddress}}/api/v1/Auth/Me +Authorization: Bearer {{login.response.body.$.data.token}} + +### 退出登录:销毁服务端会话,token 立即失效 +POST {{StoreHub_HostAddress}}/api/v1/Auth/Logout +Authorization: Bearer {{login.response.body.$.data.token}} + +### 在线人员列表(需携带 Bearer Token) +GET {{StoreHub_HostAddress}}/api/v1/Auth/OnlineUsers +Authorization: Bearer {{login.response.body.$.data.token}} + +# ============ 示例接口 ============ + +### 模板自带的 WeatherForecast +GET {{StoreHub_HostAddress}}/api/v1/weatherforecast/ +Accept: application/json + +POST {{StoreHub_HostAddress}}/api/v1/Auth/Login +Content-Type: application/json + +{ + //LoginRequest +} + +### + +POST {{StoreHub_HostAddress}}/api/v1/Auth/Login +Content-Type: application/json + +{ + //LoginRequest +} diff --git a/StoreHub/WeatherForecast.cs b/StoreHub/WeatherForecast.cs new file mode 100644 index 0000000..ae119e2 --- /dev/null +++ b/StoreHub/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace StoreHub +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/StoreHub/WeatherForecastV2.cs b/StoreHub/WeatherForecastV2.cs new file mode 100644 index 0000000..256e82b --- /dev/null +++ b/StoreHub/WeatherForecastV2.cs @@ -0,0 +1,15 @@ +namespace StoreHub +{ + public class WeatherForecastV2 + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public int FeelsLikeC { get; set; } + + public string? Summary { get; set; } + } +} diff --git a/StoreHub/appsettings.Development.json b/StoreHub/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/StoreHub/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/StoreHub/appsettings.json b/StoreHub/appsettings.json new file mode 100644 index 0000000..d9215b9 --- /dev/null +++ b/StoreHub/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Port=3306;Database=erp;Uid=root;Pwd=123456;", + "UserConnection": "Server=localhost;Port=3306;Database=wxxd;Uid=root;Pwd=123456;", + "KJDConnection": "Server=localhost;Port=3306;Database=mysql;Uid=root;Pwd=123456;" + }, + "Jwt": { + "Issuer": "StoreHub", + "Audience": "StoreHub.Client", + "SigningKey": "StoreHub--Smxf8DtV8a5dFdaCVCXtFfWbmMRfw53ectEjr8JvhUjGpPP7fuBdRn", + "ExpiresMinutes": 120, + "ClockSkewMinutes": 5, + "ValidateIssuer": true, + "ValidateAudience": true, + "ValidateLifetime": true + }, + "AllowedHosts": "*" +}