diff --git a/src/CloudGaming/Api/CloudGaming.Api/Controllers/AccountController.cs b/src/CloudGaming/Api/CloudGaming.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..723b9c8 --- /dev/null +++ b/src/CloudGaming/Api/CloudGaming.Api/Controllers/AccountController.cs @@ -0,0 +1,39 @@ +using CloudGaming.Api.Base; +using CloudGaming.Code.Account; +using CloudGaming.Code.AppExtend; +using CloudGaming.Code.DataAccess; +using CloudGaming.Code.MiddlewareExtend; +using CloudGaming.DtoModel.Account; +using CloudGaming.GameModel.Db.Db_Ext; + +using HuanMeng.DotNetCore.AttributeExtend; +using HuanMeng.DotNetCore.Base; + +using HuanMeng.DotNetCore.Utility; + +using Microsoft.AspNetCore.Mvc; + +namespace CloudGaming.Api.Controllers; + +/// +/// 账号管理 +/// +public class AccountController : CloudGamingControllerBase +{ + public AccountController(IServiceProvider _serviceProvider) : base(_serviceProvider) + { + } + + /// + /// 发送验证码 + /// + /// + /// + [HttpPost] + [Message("发送成功")] + public async Task SendPhoneNumber([FromBody] PhoneNumberRequest phoneNumber) + { + AccountBLL account = new AccountBLL(ServiceProvider); + return await account.SendPhoneNumber(phoneNumber.PhoneNumber); + } +} diff --git a/src/CloudGaming/Api/CloudGaming.Api/Controllers/AppController.cs b/src/CloudGaming/Api/CloudGaming.Api/Controllers/AppController.cs index 907168e..eafa2b7 100644 --- a/src/CloudGaming/Api/CloudGaming.Api/Controllers/AppController.cs +++ b/src/CloudGaming/Api/CloudGaming.Api/Controllers/AppController.cs @@ -3,6 +3,7 @@ using CloudGaming.Code.Config; using CloudGaming.DtoModel; using CloudGaming.GameModel.Db.Db_Ext; +using HuanMeng.DotNetCore.AttributeExtend; using HuanMeng.DotNetCore.Base; using Microsoft.AspNetCore.Mvc; @@ -24,6 +25,7 @@ namespace CloudGaming.Api.Controllers /// /// [HttpGet] + [Message("发送成功")] public async Task GetAppConfigAsync() { AppConfigBLL appConfigBLL = new AppConfigBLL(ServiceProvider); diff --git a/src/CloudGaming/Api/CloudGaming.Api/Program.cs b/src/CloudGaming/Api/CloudGaming.Api/Program.cs index 4265974..0bbf42c 100644 --- a/src/CloudGaming/Api/CloudGaming.Api/Program.cs +++ b/src/CloudGaming/Api/CloudGaming.Api/Program.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Options; using CloudGaming.GameModel.Db.Db_Ext; using CloudGaming.Code.MiddlewareExtend; +using CloudGaming.Code.Filter; var builder = WebApplication.CreateBuilder(args); #region 日志 // Add services to the container. @@ -51,6 +52,7 @@ builder.Services.AddControllers(options => { // 添加自定义的 ResultFilter 到全局过滤器中 options.Filters.Add(); + options.Filters.Add(); }) .AddNewtonsoftJson(options => { @@ -68,7 +70,9 @@ builder.Services.AddControllers(options => //options.SerializerSettings.Converters.Add() // 其他配置... }); -builder.Services.AddSingleton(); +//CustomResultFilter +//builder.Services.AddSingleton(); + #endregion // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/src/CloudGaming/Code/CloudGaming.Code/Account/AccountBLL.cs b/src/CloudGaming/Code/CloudGaming.Code/Account/AccountBLL.cs new file mode 100644 index 0000000..d7ec67e --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Account/AccountBLL.cs @@ -0,0 +1,76 @@ +using CloudGaming.Code.Sms; + +using HuanMeng.DotNetCore.Utility; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.Account +{ + /// + /// 账号操作 + /// + public class AccountBLL : CloudGamingBase + { + public AccountBLL(IServiceProvider serviceProvider) : base(serviceProvider) + { + + } + + /// + /// 发送手机号码 + /// + /// + /// + public async Task SendPhoneNumber(string PhoneNumber) + { + if (!PhoneNumberValidator.IsPhoneNumber(PhoneNumber)) + { + throw new MessageException(ResonseCode.PhoneNumberException, "手机号格式错误"); + } + var day = int.Parse(DateTime.Now.ToString("yyyyMMdd")); + var smsCount = await Dao.DaoExt.Context.T_Sms_Log.Where(it => it.SendTimeDay == day && it.PhoneNumber == PhoneNumber).CountAsync(); + if (smsCount >= 5) + { + throw new MessageException(ResonseCode.PhoneNumberMaxException, "当日发送以达到上限"); + } + var phoneNumberCache = RedisCache.StringGetAsync($"App:sms:{PhoneNumber}"); + var verificationCode = new Random().Next(1000, 9999).ToString(); + var sms = AppConfig.AliyunConfig.GetPhoneNumberVerificationService(); + bool isSend = false; + string exMsg = ""; + try + { + isSend = await sms.SendVerificationCodeAsync(PhoneNumber, verificationCode); + } + catch (Exception ex) + { + + exMsg = ex.Message; + if (exMsg.Length > 200) + { + exMsg = exMsg.Substring(0, 200); + } + } + if (isSend) + { + await RedisCache.StringSetAsync($"App:sms:{PhoneNumber}", verificationCode, TimeSpan.FromMinutes(5)); + } + T_Sms_Log t_Sms_Log = new T_Sms_Log() + { + VerificationCode = verificationCode, + ErrorMessage = exMsg, + PhoneNumber = PhoneNumber, + SendStatus = isSend ? 1 : 0, + SendTime = DateTime.Now, + SendTimeDay = day + }; + Dao.DaoExt.Context.T_Sms_Log.Add(t_Sms_Log); + await Dao.DaoExt.Context.SaveChangesAsync(); + return isSend; + } + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AliyunOssConfig.cs b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AliyunConfig.cs similarity index 74% rename from src/CloudGaming/Code/CloudGaming.Code/AppExtend/AliyunOssConfig.cs rename to src/CloudGaming/Code/CloudGaming.Code/AppExtend/AliyunConfig.cs index 5b5afd5..25f87fa 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AliyunOssConfig.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AliyunConfig.cs @@ -6,7 +6,10 @@ using System.Threading.Tasks; namespace CloudGaming.Code.AppExtend { - public class AliyunOssConfig + /// + /// 阿里云配置 + /// + public class AliyunConfig { /// /// @@ -16,6 +19,8 @@ namespace CloudGaming.Code.AppExtend /// 配置环境变量 /// public string AccessKeySecret { get; set; } + + #region 阿里云OSS配置 /// /// 替换为Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。 /// @@ -45,5 +50,18 @@ namespace CloudGaming.Code.AppExtend return this.DomainName; //+ this.UploadPath } } + + #endregion + + /// + /// 短信签名名称 + /// + public string SmsSignName { get; set; } + + /// + /// string 短信模板配置 + /// + public string SmsTemplateCode { get; set; } + } } diff --git a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppConfig.cs b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppConfig.cs index 301b08c..70d9c8f 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppConfig.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppConfig.cs @@ -64,7 +64,7 @@ namespace CloudGaming.Code.AppExtend /// /// oss阿里云配置 /// - public AliyunOssConfig AliyunConfig { get; set; } + public AliyunConfig AliyunConfig { get; set; } /// diff --git a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/CustomObjectResultExecutor.cs b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/CustomObjectResultExecutor.cs deleted file mode 100644 index 45d4d7d..0000000 --- a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/CustomObjectResultExecutor.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Org.BouncyCastle.Asn1.Ocsp; - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CloudGaming.Code.AppExtend -{ - public class CustomObjectResultExecutor : ObjectResultExecutor - { - private readonly IHttpContextAccessor _httpContextAccessor; - - public CustomObjectResultExecutor(OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, ILoggerFactory loggerFactory, IOptions mvcOptions):base(formatterSelector, writerFactory, loggerFactory, mvcOptions) - - { - //_httpContextAccessor = httpContextAccessor; - } - - public override Task ExecuteAsync(ActionContext context, ObjectResult result) - { - var httpContext = _httpContextAccessor.HttpContext; - var user = httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "Anonymous"; - - //// 动态修改返回的结果数据(示例:修改 Message 字段) - //if (result.Value is ResponseData responseData) - //{ - // if (user == "admin") - // { - // responseData.Message += " (admin)"; - // } - //} - - return base.ExecuteAsync(context, result); - } - } -} diff --git a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/CustomResultFilter.cs b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/CustomResultFilter.cs deleted file mode 100644 index 42fd786..0000000 --- a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/CustomResultFilter.cs +++ /dev/null @@ -1,78 +0,0 @@ -using CloudGaming.Code.DataAccess; -using CloudGaming.GameModel.Db.Db_Ext; - -using HuanMeng.DotNetCore.AttributeExtend; -using HuanMeng.DotNetCore.Base; -using HuanMeng.DotNetCore.Utility; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Identity.Client; - -using Swashbuckle.AspNetCore.SwaggerGen; - -using System; -using System.Collections; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Net; -using System.Reflection; - -namespace CloudGaming.Code.AppExtend -{ - /// - /// - /// - public class CustomResultFilter : IResultFilter - { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IServiceProvider _serviceProvider; - private readonly AppConfig _appConfig; - public CustomResultFilter(IHttpContextAccessor httpContextAccessor, AppConfig appConfig, IServiceProvider serviceProvider) - { - _httpContextAccessor = httpContextAccessor; - _appConfig = appConfig; - _serviceProvider = serviceProvider; - } - - public void OnResultExecuting(ResultExecutingContext context) - { - // 获取当前的 HttpContext - var httpContext = context.HttpContext; - var path = httpContext.Request.Path.Value ?? ""; - var apiPrefix = path.Replace('/', '.').TrimStart('.'); - var sw = Stopwatch.StartNew(); - //_appConfig. - CloudGamingBase cloudGamingBase = new CloudGamingBase(_serviceProvider); - // 获取当前用户的信息 - var user = httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "Anonymous"; - //Dictionary keyValuePairs = new Dictionary(); - if (context.Result is ObjectResult objectResult && objectResult.Value != null) - { - var x = objectResult.Value.GetType(); - object? value = null; - if (!x.FullName.Contains("HuanMeng.DotNetCore.Base.BaseResponse")) - { - BaseResponse baseResponse = new BaseResponse(ResonseCode.Success, "", objectResult.Value); - value = baseResponse; - } - else - { - value = objectResult.Value; - } - - var dic = value.ToDictionaryOrList(apiPrefix - , it => cloudGamingBase.Cache.ImageEntityCache[it] - ); - objectResult.Value = dic; - sw.Stop(); - context.HttpContext.Response.Headers.TryAdd("X-Request-Duration-Filter", sw.Elapsed.TotalMilliseconds.ToString()); - } - } - public void OnResultExecuted(ResultExecutedContext context) - { - // 可在执行完结果后处理其他逻辑 - } - } -} diff --git a/src/CloudGaming/Code/CloudGaming.Code/CloudGaming.Code.csproj b/src/CloudGaming/Code/CloudGaming.Code/CloudGaming.Code.csproj index 7ccf7ec..7904fc4 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/CloudGaming.Code.csproj +++ b/src/CloudGaming/Code/CloudGaming.Code/CloudGaming.Code.csproj @@ -10,6 +10,7 @@ + diff --git a/src/CloudGaming/Code/CloudGaming.Code/Extend/MessageAttributeExtend.cs b/src/CloudGaming/Code/CloudGaming.Code/Extend/MessageAttributeExtend.cs new file mode 100644 index 0000000..1d2285d --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Extend/MessageAttributeExtend.cs @@ -0,0 +1,43 @@ +using HuanMeng.DotNetCore.AttributeExtend; + +using Microsoft.AspNetCore.Mvc.Controllers; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.Extend; + + +/// +/// +/// +public static class MessageAttributeExtend +{ + private static readonly ConcurrentDictionary _attributeCache = new ConcurrentDictionary(); + + /// + /// + /// + /// + /// + public static MessageAttribute? GetMessageAttribute(ControllerActionDescriptor controllerActionDescriptor) + { + // 获取方法信息 + var methodInfo = controllerActionDescriptor.MethodInfo; + + // 尝试从缓存中获取MessageAttribute + if (!_attributeCache.TryGetValue(methodInfo, out var messageAttribute)) + { + // 如果缓存中没有,则使用反射获取并存储到缓存中 + messageAttribute = methodInfo.GetCustomAttribute(); + _attributeCache.TryAdd(methodInfo, messageAttribute); + } + + return messageAttribute; + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/Filter/CustomExceptionFilter.cs b/src/CloudGaming/Code/CloudGaming.Code/Filter/CustomExceptionFilter.cs new file mode 100644 index 0000000..acf9489 --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Filter/CustomExceptionFilter.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.Filter +{ + public class CustomExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + var sw = Stopwatch.StartNew(); + // 检查异常是否是特定的异常类型 + if (context.Exception is MessageException message) + { + var obj = new BaseResponse(message.Code, message.Message, message.Data); + //// 处理特定异常:记录日志、设置响应结果等 + //Console.WriteLine($"Custom exception caught: {message.Message}"); + + // 设置自定义的响应结果 + context.Result = new JsonResult(obj) + { + StatusCode = StatusCodes.Status200OK // 或者其他合适的HTTP状态码 + }; + + // 标记异常已经被处理 + context.ExceptionHandled = true; + } + sw.Stop(); + context.HttpContext.Response.Headers.TryAdd("X-Request-Duration-CustomExceptionFilter", sw.Elapsed.TotalMilliseconds.ToString()); + } + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/Filter/CustomResultFilter.cs b/src/CloudGaming/Code/CloudGaming.Code/Filter/CustomResultFilter.cs new file mode 100644 index 0000000..15e43da --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Filter/CustomResultFilter.cs @@ -0,0 +1,80 @@ + + +namespace CloudGaming.Code.Filter; + + +/// +/// +/// +public class CustomResultFilter : IResultFilter +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IServiceProvider _serviceProvider; + private readonly AppConfig _appConfig; + /// + /// + /// + /// + /// + /// + public CustomResultFilter(IHttpContextAccessor httpContextAccessor, AppConfig appConfig, IServiceProvider serviceProvider) + { + _httpContextAccessor = httpContextAccessor; + _appConfig = appConfig; + _serviceProvider = serviceProvider; + } + + /// + /// 结果发送到客户端前 + /// + /// + public void OnResultExecuting(ResultExecutingContext context) + { + var httpContext = context.HttpContext; + var path = httpContext.Request.Path.Value ?? ""; + var apiPrefix = path.Replace('/', '.').TrimStart('.'); + var sw = Stopwatch.StartNew(); + CloudGamingBase cloudGamingBase = new CloudGamingBase(_serviceProvider); + //// 获取当前用户的信息 + //var user = httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "Anonymous"; + if (context.Result is ObjectResult objectResult && objectResult.Value != null) + { + var x = objectResult.Value.GetType(); + object? value = null; + if (!x.FullName.Contains("HuanMeng.DotNetCore.Base.BaseResponse")) + { + BaseResponse baseResponse = new BaseResponse(ResonseCode.Success, "", objectResult.Value); + value = baseResponse; + // 获取当前执行的Action方法的信息并进行类型检查 + if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + var messageAttribute = MessageAttributeExtend.GetMessageAttribute(controllerActionDescriptor); + // 如果存在MessageAttribute,则设置响应消息 + if (messageAttribute != null) + { + baseResponse.Message = messageAttribute.Message; + } + } + + } + else + { + value = objectResult.Value; + } + + var dic = value.ToDictionaryOrList(apiPrefix, it => cloudGamingBase.Cache.ImageEntityCache[it]); + objectResult.Value = dic; + sw.Stop(); + context.HttpContext.Response.Headers.TryAdd("X-Request-Duration-Filter", sw.Elapsed.TotalMilliseconds.ToString()); + } + } + + /// + /// 结果发送到客户端后 + /// + /// + public void OnResultExecuted(ResultExecutedContext context) + { + // 可在执行完结果后处理其他逻辑 + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/GlobalUsings.cs b/src/CloudGaming/Code/CloudGaming.Code/GlobalUsings.cs index 3876d89..a195ca3 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/GlobalUsings.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/GlobalUsings.cs @@ -1,4 +1,5 @@ global using CloudGaming.Code.AppExtend; +global using CloudGaming.Code.Extend; global using CloudGaming.GameModel.Db.Db_Ext; global using CloudGaming.GameModel.Db.Db_Game; global using CloudGaming.Model.DbSqlServer.Db_Phone; @@ -7,9 +8,16 @@ global using CloudGaming.Model.DbSqlServer.Db_User; global using HuanMeng.DotNetCore.Base; global using HuanMeng.DotNetCore.MultiTenant; global using HuanMeng.DotNetCore.MultiTenant.Contract; +global using HuanMeng.DotNetCore.Utility; global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Mvc.Controllers; +global using Microsoft.AspNetCore.Mvc.Filters; global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; \ No newline at end of file +global using Microsoft.Extensions.Hosting; + +global using System.Diagnostics; \ No newline at end of file diff --git a/src/CloudGaming/Code/CloudGaming.Code/MiddlewareExtend/RedisCacheMiddleware.cs b/src/CloudGaming/Code/CloudGaming.Code/MiddlewareExtend/RedisCacheMiddleware.cs index fb1080d..39987d5 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/MiddlewareExtend/RedisCacheMiddleware.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/MiddlewareExtend/RedisCacheMiddleware.cs @@ -111,7 +111,7 @@ namespace CloudGaming.Code.MiddlewareExtend } /// - /// Redis 缓存特性类 + /// Redis 缓存特性类 /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class RedisCacheAttribute : Attribute diff --git a/src/CloudGaming/Code/CloudGaming.Code/Sms/AlibabaPhoneNumberVerificationService.cs b/src/CloudGaming/Code/CloudGaming.Code/Sms/AlibabaPhoneNumberVerificationService.cs new file mode 100644 index 0000000..c80eb1d --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Sms/AlibabaPhoneNumberVerificationService.cs @@ -0,0 +1,113 @@ +using CloudGaming.Code.Sms.Contract; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Tea; + +namespace CloudGaming.Code.Sms +{ + /// + /// + /// + public class AlibabaPhoneNumberVerificationService(AliyunConfig aliyunOssConfig) : IPhoneNumberVerificationService + { + + /// Description: + /// + /// 使用AK&SK初始化账号Client + /// + /// + /// + /// Client + /// + /// + /// Exception: + /// Exception + public AlibabaCloud.SDK.Dysmsapi20170525.Client CreateClient() + { + // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。 + // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378671.html。 + AlibabaCloud.OpenApiClient.Models.Config config = new AlibabaCloud.OpenApiClient.Models.Config + { + // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。 + AccessKeyId = "LTAI5tEMoHbcDC5d9CQWovJk",//aliyunOssConfig.AccessKeyId, //Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID"), + // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。 + AccessKeySecret = "gnYOJr0l9hTnl82vI4BxwVgtE1RdL"// aliyunOssConfig.AccessKeySecret, + }; + // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi + config.Endpoint = "dysmsapi.aliyuncs.com"; + return new AlibabaCloud.SDK.Dysmsapi20170525.Client(config); + } + + /// + /// + /// + /// + /// + /// + /// + public async Task SendVerificationCodeAsync(string phoneNumber, string code) + { + AlibabaCloud.SDK.Dysmsapi20170525.Client client = CreateClient(); + AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest sendSmsRequest = new AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest + { + SignName = aliyunOssConfig.SmsSignName,// "氢荷健康", + TemplateCode = aliyunOssConfig.SmsTemplateCode,// "SMS_154950909", + PhoneNumbers = phoneNumber, + TemplateParam = "{\"code\":\"" + code + "\"}", + }; + try + { + // 复制代码运行请自行打印 API 的返回值 + var response = await client.SendSmsWithOptionsAsync(sendSmsRequest, new AlibabaCloud.TeaUtil.Models.RuntimeOptions()); + } + catch (TeaException error) + { + // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 + // 错误 message + Console.WriteLine(error.Message); + // 诊断地址 + Console.WriteLine(error.Data["Recommend"]); + AlibabaCloud.TeaUtil.Common.AssertAsString(error.Message); + throw error; + } + catch (Exception _error) + { + TeaException error = new TeaException(new Dictionary + { + { "message", _error.Message } + }); + // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 + // 错误 message + Console.WriteLine(error.Message); + // 诊断地址 + Console.WriteLine(error.Data["Recommend"]); + AlibabaCloud.TeaUtil.Common.AssertAsString(error.Message); + throw error; + } + + return true; + } + } + + /// + /// + /// + public static class SmsExtend + { + /// + /// 获取短信 + /// + /// + /// + public static IPhoneNumberVerificationService GetPhoneNumberVerificationService(this AliyunConfig aliyunOssConfig) + { + return new AlibabaPhoneNumberVerificationService(aliyunOssConfig); + } + } + +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/Sms/Contract/IPhoneNumberVerificationService.cs b/src/CloudGaming/Code/CloudGaming.Code/Sms/Contract/IPhoneNumberVerificationService.cs new file mode 100644 index 0000000..4765a88 --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Sms/Contract/IPhoneNumberVerificationService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.Sms.Contract +{ + /// + /// 发送手机短信 + /// + public interface IPhoneNumberVerificationService + { + /// + /// 发送验证码到指定的手机号。 + /// + /// 目标手机号。 + /// 验证码 + /// 返回操作是否成功。 + Task SendVerificationCodeAsync(string phoneNumber, string code); + } +} diff --git a/src/CloudGaming/Model/CloudGaming.DtoModel/Account/PhoneNumberRequest.cs b/src/CloudGaming/Model/CloudGaming.DtoModel/Account/PhoneNumberRequest.cs new file mode 100644 index 0000000..3d04081 --- /dev/null +++ b/src/CloudGaming/Model/CloudGaming.DtoModel/Account/PhoneNumberRequest.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.DtoModel.Account +{ + /// + /// + /// + public class PhoneNumberRequest + { + /// + /// 手机号码 + /// + public string? PhoneNumber { get; set; } + } +} diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs index c0a28a3..ca5ba11 100644 --- a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs @@ -54,6 +54,11 @@ public partial class CloudGamingCBTContext : DbContext /// public virtual DbSet T_App_Image { get; set; } + /// + /// 发送短信日志表 + /// + public virtual DbSet T_Sms_Log { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer("Server=192.168.1.17;Database=CloudGamingCBT;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;"); @@ -139,6 +144,32 @@ public partial class CloudGamingCBTContext : DbContext }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__T_Sms_Lo__3214EC07AD037B45"); + + entity.ToTable(tb => tb.HasComment("发送短信日志表")); + + entity.HasIndex(e => e.SendTimeDay, "T_Sms_Log_SendTimeDay_index_desc").IsDescending(); + + entity.Property(e => e.Id).HasComment("主键"); + entity.Property(e => e.ErrorMessage) + .HasMaxLength(255) + .HasComment("错误信息(如果发送失败)"); + entity.Property(e => e.PhoneNumber) + .HasMaxLength(1) + .HasComment("手机号码"); + entity.Property(e => e.SendStatus).HasComment("发送状态(0: 失败, 1: 成功)\r\n"); + entity.Property(e => e.SendTime) + .HasComment("发送时间") + .HasColumnType("datetime"); + entity.Property(e => e.SendTimeDay).HasComment("发送时间,天"); + entity.Property(e => e.VerificationCode) + .HasMaxLength(1) + .HasComment("发送的验证码"); + + }); + OnModelCreatingPartial(modelBuilder); } diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Sms_Log.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Sms_Log.cs new file mode 100644 index 0000000..c63941c --- /dev/null +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Sms_Log.cs @@ -0,0 +1,47 @@ +using System; + +namespace CloudGaming.GameModel.Db.Db_Ext; + +/// +/// 发送短信日志表 +/// +public partial class T_Sms_Log +{ + public T_Sms_Log() { } + + /// + /// 主键 + /// + public virtual int Id { get; set; } + + /// + /// 手机号码 + /// + public virtual string PhoneNumber { get; set; } = null!; + + /// + /// 发送的验证码 + /// + public virtual string VerificationCode { get; set; } = null!; + + /// + /// 发送状态(0: 失败, 1: 成功) + /// + /// + public virtual int SendStatus { get; set; } + + /// + /// 发送时间 + /// + public virtual DateTime SendTime { get; set; } + + /// + /// 发送时间,天 + /// + public virtual int SendTimeDay { get; set; } + + /// + /// 错误信息(如果发送失败) + /// + public virtual string? ErrorMessage { get; set; } +} diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/AttributeExtend/MessageAttribute.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/AttributeExtend/MessageAttribute.cs new file mode 100644 index 0000000..4800c70 --- /dev/null +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/AttributeExtend/MessageAttribute.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.AttributeExtend +{ + /// + /// 执行成功后返回的消息 + /// + [AttributeUsage(AttributeTargets.Method)] + public class MessageAttribute : Attribute + { + /// + /// 执行成功后返回的消息 + /// + /// 消息内容 + public MessageAttribute(string message) + { + Message = message; + } + + public string Message { get; set; } + } +} diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/EfCoreDaoBase.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/EfCoreDaoBase.cs index 3706142..971ad8c 100644 --- a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/EfCoreDaoBase.cs +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/EfCoreDaoBase.cs @@ -2,162 +2,161 @@ using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; -namespace HuanMeng.DotNetCore.Base +namespace HuanMeng.DotNetCore.Base; + +/// +/// 基本数据库操作,需要安装 Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.Relational +/// +/// +public class EfCoreDaoBase where TDbContext : DbContext { + private TDbContext _context; + + public TDbContext Context => _context; + /// - /// 基本数据库操作,需要安装 Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.Relational + /// 构造函数 /// - /// - public class EfCoreDaoBase where TDbContext : DbContext + /// + public EfCoreDaoBase(TDbContext context) { - private TDbContext _context; + _context = context ?? throw new ArgumentNullException(nameof(context)); + } - public TDbContext Context => _context; + /// + /// 是否手动提交 + /// + public bool IsManualSubmit { get; set; } - /// - /// 构造函数 - /// - /// - public EfCoreDaoBase(TDbContext context) + /// + /// SqlQueryRaw + /// + public async Task SqlQueryAsync(string sql, params object[] parameters) where T : class + { + return await Context.Database.SqlQueryRaw(sql, parameters).FirstOrDefaultAsync(); + } + + /// + /// SqlQueryList + /// + public async Task> SqlQueryListAsync(string sql, params object[] parameters) + { + return await Context.Database.SqlQueryRaw(sql, parameters).ToListAsync(); + } + + /// + /// ExecuteSql + /// + public async Task ExecuteSqlAsync(string sql, params object[] parameters) + { + var result = await Context.Database.ExecuteSqlRawAsync(sql, parameters); + await AutoSaveChangesAsync(); + return result; + } + + /// + /// 添加实体 + /// + public async Task AddAsync(T entity) where T : class + { + Context.Set().Add(entity); + await AutoSaveChangesAsync(); + } + + /// + /// 批量添加实体 + /// + public async Task AddRangeAsync(IEnumerable entities) where T : class + { + Context.Set().AddRange(entities); + await AutoSaveChangesAsync(); + } + + /// + /// 删除某个实体 + /// + public async Task DeleteAsync(T entity) where T : class + { + Context.Entry(entity).State = EntityState.Deleted; + await AutoSaveChangesAsync(); + } + + /// + /// 更新实体 + /// + public async Task UpdateAsync(T entity) where T : class + { + if (Context.Entry(entity).State == EntityState.Detached) { - _context = context ?? throw new ArgumentNullException(nameof(context)); + Context.Set().Attach(entity); + Context.Entry(entity).State = EntityState.Modified; } + await AutoSaveChangesAsync(); + } - /// - /// 是否手动提交 - /// - public bool IsManualSubmit { get; set; } - - /// - /// SqlQueryRaw - /// - public async Task SqlQueryAsync(string sql, params object[] parameters) where T : class + /// + /// 清除上下文跟踪 + /// + public void RemoveTracking(T entity) where T : class + { + if (Context.Entry(entity).State != EntityState.Detached) { - return await Context.Database.SqlQueryRaw(sql, parameters).FirstOrDefaultAsync(); + Context.Entry(entity).State = EntityState.Detached; } + } - /// - /// SqlQueryList - /// - public async Task> SqlQueryListAsync(string sql, params object[] parameters) - { - return await Context.Database.SqlQueryRaw(sql, parameters).ToListAsync(); - } + /// + /// 获取实体,根据主键 + /// + public async Task GetModelAsync(params object[] keyValues) where T : class + { + return await Context.Set().FindAsync(keyValues); + } - /// - /// ExecuteSql - /// - public async Task ExecuteSqlAsync(string sql, params object[] parameters) - { - var result = await Context.Database.ExecuteSqlRawAsync(sql, parameters); - await AutoSaveChangesAsync(); - return result; - } + /// + /// 获取实体,根据条件 + /// + public async Task GetModelAsync(Expression> where, bool isNoTracking = false) where T : class + { + var query = Context.Set().AsQueryable(); + if (isNoTracking) + query = query.AsNoTracking(); + return await query.FirstOrDefaultAsync(where); + } - /// - /// 添加实体 - /// - public async Task AddAsync(T entity) where T : class - { - Context.Set().Add(entity); - await AutoSaveChangesAsync(); - } + /// + /// 获取列表数据 + /// + public IQueryable GetList(Expression> where, bool isNoTracking = false) where T : class + { + var query = Context.Set().Where(where); + return isNoTracking ? query.AsNoTracking() : query; + } - /// - /// 批量添加实体 - /// - public async Task AddRangeAsync(IEnumerable entities) where T : class - { - Context.Set().AddRange(entities); - await AutoSaveChangesAsync(); - } + /// + /// 获取记录数量 + /// + public async Task GetCountAsync(Expression> where) where T : class + { + return await Context.Set().AsNoTracking().CountAsync(where); + } - /// - /// 删除某个实体 - /// - public async Task DeleteAsync(T entity) where T : class - { - Context.Entry(entity).State = EntityState.Deleted; - await AutoSaveChangesAsync(); - } + /// + /// 判断是否存在记录 + /// + public async Task ExistsAsync(Expression> where) where T : class + { + return await Context.Set().AsNoTracking().AnyAsync(where); + } - /// - /// 更新实体 - /// - public async Task UpdateAsync(T entity) where T : class + /// + /// 根据提交状态决定是否自动保存更改 + /// + private async Task AutoSaveChangesAsync() + { + if (!IsManualSubmit) { - if (Context.Entry(entity).State == EntityState.Detached) - { - Context.Set().Attach(entity); - Context.Entry(entity).State = EntityState.Modified; - } - await AutoSaveChangesAsync(); - } - - /// - /// 清除上下文跟踪 - /// - public void RemoveTracking(T entity) where T : class - { - if (Context.Entry(entity).State != EntityState.Detached) - { - Context.Entry(entity).State = EntityState.Detached; - } - } - - /// - /// 获取实体,根据主键 - /// - public async Task GetModelAsync(params object[] keyValues) where T : class - { - return await Context.Set().FindAsync(keyValues); - } - - /// - /// 获取实体,根据条件 - /// - public async Task GetModelAsync(Expression> where, bool isNoTracking = false) where T : class - { - var query = Context.Set().AsQueryable(); - if (isNoTracking) - query = query.AsNoTracking(); - return await query.FirstOrDefaultAsync(where); - } - - /// - /// 获取列表数据 - /// - public IQueryable GetList(Expression> where, bool isNoTracking = false) where T : class - { - var query = Context.Set().Where(where); - return isNoTracking ? query.AsNoTracking() : query; - } - - /// - /// 获取记录数量 - /// - public async Task GetCountAsync(Expression> where) where T : class - { - return await Context.Set().AsNoTracking().CountAsync(where); - } - - /// - /// 判断是否存在记录 - /// - public async Task ExistsAsync(Expression> where) where T : class - { - return await Context.Set().AsNoTracking().AnyAsync(where); - } - - /// - /// 根据提交状态决定是否自动保存更改 - /// - private async Task AutoSaveChangesAsync() - { - if (!IsManualSubmit) - { - await Context.SaveChangesAsync(); - } + await Context.SaveChangesAsync(); } } } diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/IResponse.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/IResponse.cs index ecdf588..e4013fd 100644 --- a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/IResponse.cs +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/IResponse.cs @@ -1,23 +1,22 @@ -namespace XLib.DotNetCore.Base +namespace XLib.DotNetCore.Base; + +/// +/// 接口和服务调用通用响应接口 +/// +public interface IResponse { + ///// + ///// Http状态码 + ///// + //HttpStatusCode StatusCode { get; set; } + /// - /// 接口和服务调用通用响应接口 + /// 功能执行返回代码 /// - public interface IResponse - { - ///// - ///// Http状态码 - ///// - //HttpStatusCode StatusCode { get; set; } + int Code { get; set; } - /// - /// 功能执行返回代码 - /// - int Code { get; set; } - - /// - /// 消息 - /// - string Message { get; set; } - } + /// + /// 消息 + /// + string Message { get; set; } } diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/MessageException.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/MessageException.cs new file mode 100644 index 0000000..86f6879 --- /dev/null +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/MessageException.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.Base +{ + /// + /// + /// + public class MessageException : Exception + { + /// + /// + /// + /// + public MessageException(ResonseCode code) + { + Code = (int)code; + Message = ""; + } + /// + /// + /// + /// + /// + /// + public MessageException(ResonseCode code, string message, object? data = null) + { + Code = (int)code; + Message = message; + Data = data; + } + /// + /// + /// + /// + /// + /// + public MessageException(int code, string message, object? data = null) + { + Code = code; + Message = message; + Data = data; + } + /// + /// + /// + /// + /// + public MessageException(string message, object? data = null) + { + Code = 0; + Message = message; + Data = data; + } + /// + /// 功能执行返回代码 + /// + public int Code { get; set; } + + /// + /// 消息 + /// + public string Message { get; set; } + + /// + /// 数据 + /// + public object? Data { get; set; } + } +} diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/ResonseCode.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/ResonseCode.cs index f5ffee2..c62a436 100644 --- a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/ResonseCode.cs +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Base/ResonseCode.cs @@ -1,53 +1,61 @@ -namespace HuanMeng.DotNetCore.Base +namespace HuanMeng.DotNetCore.Base; + +/// +/// 响应编码参考,实际的项目使用可以自行定义 +/// 基本规则: +/// 成功:大于等于0 +/// 失败:小于0 +/// +public enum ResonseCode { /// - /// 响应编码参考,实际的项目使用可以自行定义 - /// 基本规则: - /// 成功:大于等于0 - /// 失败:小于0 + /// Sign签名错误 /// - public enum ResonseCode - { - /// - /// Sign签名错误 - /// - SignError = -999, - /// - /// jwt用户签名错误 - /// - TwtError = -998, + SignError = -999, + /// + /// jwt用户签名错误 + /// + TwtError = -998, - /// - /// 用户验证失败 - /// - Unauthorized = 401, - /// - /// 重复请求 - /// - ManyRequests = 429, + /// + /// 用户验证失败 + /// + Unauthorized = 401, - /// - /// 正在处理中 - /// - Processing = 102, - /// - /// 通用错误 - /// - Error = -1, + /// + /// 重复请求 + /// + ManyRequests = 429, - /// - /// 参数错误 - /// - ParamError = -2, + /// + /// 手机号异常 + /// + PhoneNumberException = 530, + /// + /// 当日手机号发送已到达上限 + /// + PhoneNumberMaxException = 531, + /// + /// 正在处理中 + /// + Processing = 102, + /// + /// 通用错误 + /// + Error = -1, - /// - /// 没找到数据记录 - /// - NotFoundRecord = -3, + /// + /// 参数错误 + /// + ParamError = -2, - /// - /// 成功 - /// - Success = 0, - } + /// + /// 没找到数据记录 + /// + NotFoundRecord = -3, + + /// + /// 成功 + /// + Success = 0, } diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions.cs new file mode 100644 index 0000000..83b139f --- /dev/null +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions.cs @@ -0,0 +1,160 @@ +using HuanMeng.DotNetCore.AttributeExtend; + +using System.Collections; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace HuanMeng.DotNetCore.Utility; + + +/// +/// object转数据字典 +/// +public static class ObjectExtensions +{ + /// + /// 用于存储每种类型的属性访问器数组的线程安全缓存。 + /// + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 缓存每个属性是否具有 ImagesAttribute 特性。 + /// + public static readonly ConcurrentDictionary _PropertyCache = new(); + + /// + /// 判断对象是否为原始类型或字符串类型。 + /// + /// 要检查的对象。 + /// 如果对象是原始类型或字符串,返回 true;否则返回 false。 + public static bool IsPrimitiveType(object obj) => + obj is not null && (obj.GetType().IsPrimitive || obj is string or ValueType); + + /// + /// 判断对象是否为集合类型(不包括字符串)。 + /// + /// 要检查的对象。 + /// 如果对象是集合类型(但不是字符串),返回 true;否则返回 false。 + public static bool IsCollectionType(object obj) => obj is IEnumerable && obj is not string; + + /// + /// 根据对象类型,将对象转换为字典或列表格式,支持可选的路径前缀。 + /// + /// 要转换的对象。 + /// 属性路径的可选前缀。 + /// 对象的字典或列表表示形式。 + public static object ToDictionaryOrList(this object obj, string prefix = "", Func? imageFunc = null) + { + if (obj == null) return null; + + return obj switch + { + _ when IsPrimitiveType(obj) => obj, + IEnumerable enumerable => TransformCollection(enumerable, prefix, imageFunc), + _ => TransformObject(obj, prefix, imageFunc) + }; + } + + /// + /// 将集合对象转换为包含转换项的列表,每个项保留其路径前缀。 + /// + /// 要转换的集合。 + /// 集合中每个属性路径的前缀。 + /// 转换后的项列表。 + private static List TransformCollection(IEnumerable enumerable, string prefix = "", Func? imageFunc = null) + { + var list = new List(enumerable is ICollection collection ? collection.Count : 10); + int index = 0; + foreach (var item in enumerable) + { + list.Add(ToDictionaryOrList(item, $"{prefix}.[{index}]", imageFunc)); // 为集合中每个项添加路径 + index++; + } + return list; + } + + /// + /// 将对象的属性转换为带有路径前缀的字典,并应用前缀规则。 + /// + /// 要转换的对象。 + /// 每个属性路径的前缀。 + /// 包含属性名和属性值的字典。 + private static Dictionary TransformObject(object obj, string prefix = "", Func? imageFunc = null) + { + if (obj == null) + { + return null; + } + var type = obj.GetType(); + var accessors = PropertyCache.GetOrAdd(type, CreatePropertyAccessors); + var keyValuePairs = new Dictionary(accessors.Length); + + foreach (var accessor in accessors) + { + var propertyPath = $"{prefix}.{accessor.PropertyName}"; // 构建完整的属性路径 + + // 使用访问器获取属性值 + var propertyValue = accessor.Getter(obj); + + // 如果属性是字符串,在其值前添加 "test" + if (propertyValue is string stringValue) + { + keyValuePairs[accessor.PropertyName] = stringValue; + //Console.WriteLine(propertyPath); + continue; + } + + // 如果属性具有 ImagesAttribute,在其值前添加 "image" + // 否则,如果是集合类型,则递归转换 + keyValuePairs[accessor.PropertyName] = accessor.HasImagesAttribute + ? imageFunc?.Invoke((int)propertyValue) ?? "" + : ToDictionaryOrList(propertyValue, propertyPath, imageFunc); // IsCollectionType(propertyValue) ?: propertyValue; + } + + return keyValuePairs; + } + + /// + /// 为给定类型创建属性访问器数组。 + /// + /// 要创建属性访问器的类型。 + /// 属性访问器数组。 + private static PropertyAccessor[] CreatePropertyAccessors(Type type) + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + return properties.Select(property => + { + // 创建用于访问属性值的委托 + var getter = CreatePropertyGetter(type, property); + // 检查属性是否具有 ImagesAttribute,并将结果存储在缓存中 + var isImagesAttribute = _PropertyCache.GetOrAdd(property, p => p.GetCustomAttribute() != null); + return new PropertyAccessor(property.Name, getter, isImagesAttribute); + }).ToArray(); + } + + + + private static Func CreatePropertyGetter(Type type, PropertyInfo property) + { + var parameter = Expression.Parameter(typeof(object), "obj"); + var castParameter = Expression.Convert(parameter, type); + var propertyAccess = Expression.Property(castParameter, property); + var convertPropertyAccess = Expression.Convert(propertyAccess, typeof(object)); + return Expression.Lambda>(convertPropertyAccess, parameter).Compile(); + } + + private class PropertyAccessor + { + public string PropertyName { get; } + public Func Getter { get; } + public bool HasImagesAttribute { get; } + + public PropertyAccessor(string propertyName, Func getter, bool hasImagesAttribute) + { + PropertyName = propertyName; + Getter = getter; + HasImagesAttribute = hasImagesAttribute; + } + } +} diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions1.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions1.cs index 32a5b95..1bc2acf 100644 --- a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions1.cs +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Utility/ObjectExtensions1.cs @@ -11,157 +11,6 @@ using Microsoft.IdentityModel.Tokens; namespace HuanMeng.DotNetCore.Utility; -/// -/// -/// -public static class ObjectExtensions -{ - /// - /// 用于存储每种类型的属性访问器数组的线程安全缓存。 - /// - private static readonly ConcurrentDictionary PropertyCache = new(); - - /// - /// 缓存每个属性是否具有 ImagesAttribute 特性。 - /// - public static readonly ConcurrentDictionary _PropertyCache = new(); - - /// - /// 判断对象是否为原始类型或字符串类型。 - /// - /// 要检查的对象。 - /// 如果对象是原始类型或字符串,返回 true;否则返回 false。 - public static bool IsPrimitiveType(object obj) => - obj is not null && (obj.GetType().IsPrimitive || obj is string or ValueType); - - /// - /// 判断对象是否为集合类型(不包括字符串)。 - /// - /// 要检查的对象。 - /// 如果对象是集合类型(但不是字符串),返回 true;否则返回 false。 - public static bool IsCollectionType(object obj) => obj is IEnumerable && obj is not string; - - /// - /// 根据对象类型,将对象转换为字典或列表格式,支持可选的路径前缀。 - /// - /// 要转换的对象。 - /// 属性路径的可选前缀。 - /// 对象的字典或列表表示形式。 - public static object ToDictionaryOrList(this object obj, string prefix = "", Func? imageFunc = null) - { - if (obj == null) return null; - - return obj switch - { - _ when IsPrimitiveType(obj) => obj, - IEnumerable enumerable => TransformCollection(enumerable, prefix, imageFunc), - _ => TransformObject(obj, prefix, imageFunc) - }; - } - - /// - /// 将集合对象转换为包含转换项的列表,每个项保留其路径前缀。 - /// - /// 要转换的集合。 - /// 集合中每个属性路径的前缀。 - /// 转换后的项列表。 - private static List TransformCollection(IEnumerable enumerable, string prefix = "", Func? imageFunc = null) - { - var list = new List(enumerable is ICollection collection ? collection.Count : 10); - int index = 0; - foreach (var item in enumerable) - { - list.Add(ToDictionaryOrList(item, $"{prefix}.[{index}]", imageFunc)); // 为集合中每个项添加路径 - index++; - } - return list; - } - - /// - /// 将对象的属性转换为带有路径前缀的字典,并应用前缀规则。 - /// - /// 要转换的对象。 - /// 每个属性路径的前缀。 - /// 包含属性名和属性值的字典。 - private static Dictionary TransformObject(object obj, string prefix = "", Func? imageFunc = null) - { - if (obj == null) - { - return null; - } - var type = obj.GetType(); - var accessors = PropertyCache.GetOrAdd(type, CreatePropertyAccessors); - var keyValuePairs = new Dictionary(accessors.Length); - - foreach (var accessor in accessors) - { - var propertyPath = $"{prefix}.{accessor.PropertyName}"; // 构建完整的属性路径 - - // 使用访问器获取属性值 - var propertyValue = accessor.Getter(obj); - - // 如果属性是字符串,在其值前添加 "test" - if (propertyValue is string stringValue) - { - keyValuePairs[accessor.PropertyName] = stringValue; - //Console.WriteLine(propertyPath); - continue; - } - - // 如果属性具有 ImagesAttribute,在其值前添加 "image" - // 否则,如果是集合类型,则递归转换 - keyValuePairs[accessor.PropertyName] = accessor.HasImagesAttribute - ? imageFunc?.Invoke((int)propertyValue) ?? "" - : ToDictionaryOrList(propertyValue, propertyPath, imageFunc); // IsCollectionType(propertyValue) ?: propertyValue; - } - - return keyValuePairs; - } - - /// - /// 为给定类型创建属性访问器数组。 - /// - /// 要创建属性访问器的类型。 - /// 属性访问器数组。 - private static PropertyAccessor[] CreatePropertyAccessors(Type type) - { - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - return properties.Select(property => - { - // 创建用于访问属性值的委托 - var getter = CreatePropertyGetter(type, property); - // 检查属性是否具有 ImagesAttribute,并将结果存储在缓存中 - var isImagesAttribute = _PropertyCache.GetOrAdd(property, p => p.GetCustomAttribute() != null); - return new PropertyAccessor(property.Name, getter, isImagesAttribute); - }).ToArray(); - } - - - - private static Func CreatePropertyGetter(Type type, PropertyInfo property) - { - var parameter = Expression.Parameter(typeof(object), "obj"); - var castParameter = Expression.Convert(parameter, type); - var propertyAccess = Expression.Property(castParameter, property); - var convertPropertyAccess = Expression.Convert(propertyAccess, typeof(object)); - return Expression.Lambda>(convertPropertyAccess, parameter).Compile(); - } - - private class PropertyAccessor - { - public string PropertyName { get; } - public Func Getter { get; } - public bool HasImagesAttribute { get; } - - public PropertyAccessor(string propertyName, Func getter, bool hasImagesAttribute) - { - PropertyName = propertyName; - Getter = getter; - HasImagesAttribute = hasImagesAttribute; - } - } -} - [Obsolete] public static class ObjectExtensions11 {