From 3a35c5fe2092cd8f436c17d7cdb63ee64b1cdd58 Mon Sep 17 00:00:00 2001 From: shanji <18996038927@163.com> Date: Tue, 20 Feb 2024 22:00:34 +0800 Subject: [PATCH] 1 --- QYMessageCenter.Business/BaseBusiness.cs | 21 + QYMessageCenter.Business/MessageBusiness.cs | 45 ++ .../QYMessageCenter.Business.csproj | 21 + .../TaskSchedulerManager.cs | 12 + .../Extensions/ConverterExtensions.cs | 29 ++ .../Extensions/CopyExtensions.cs | 12 + .../Extensions/DateTimeExtension.cs | 102 ++++ .../Extensions/EncryptionExtension.cs | 82 ++++ .../Extensions/MapperExtension.cs | 59 +++ .../Extensions/StartupExtension.cs | 34 ++ QYMessageCenter.Common/Http/HttpDownloader.cs | 456 ++++++++++++++++++ QYMessageCenter.Common/Http/RestAPIService.cs | 117 +++++ QYMessageCenter.Common/Models/ApiResponse.cs | 34 ++ .../Models/BusinessException.cs | 22 + QYMessageCenter.Common/Models/IDenpendency.cs | 10 + .../QYMessageCenter.Common.csproj | 18 + .../TaskSchedulers/DelayTrigger.cs | 70 +++ .../LimitedConcurrencyLevelTaskScheduler.cs | 153 ++++++ QYMessageCenter.Model/DB/QYNotification.cs | 78 +++ .../DTO/Message/SendMessageRequest.cs | 62 +++ QYMessageCenter.Model/MappingProfiles.cs | 12 + .../QYMessageCenter.Model.csproj | 15 + QYMessageCenter.Model/__重新生成.bat | 2 + QYMessageCenter.sln | 20 +- .../Controllers/BaseApiController.cs | 36 ++ .../Controllers/MessageController.cs | 28 ++ .../Controllers/WeatherForecastController.cs | 33 -- QYMessageCenter/Filters/ResultFilter.cs | 30 ++ .../ClientVersionValidationMiddleWare.cs | 55 +++ .../Middlewares/CustomExceptionMiddleWare.cs | 85 ++++ QYMessageCenter/Program.cs | 119 ++++- QYMessageCenter/QYMessageCenter.csproj | 13 + QYMessageCenter/WeatherForecast.cs | 13 - QYMessageCenter/appsettings.json | 6 +- 34 files changed, 1844 insertions(+), 60 deletions(-) create mode 100644 QYMessageCenter.Business/BaseBusiness.cs create mode 100644 QYMessageCenter.Business/MessageBusiness.cs create mode 100644 QYMessageCenter.Business/QYMessageCenter.Business.csproj create mode 100644 QYMessageCenter.Business/TaskSchedulerManager.cs create mode 100644 QYMessageCenter.Common/Extensions/ConverterExtensions.cs create mode 100644 QYMessageCenter.Common/Extensions/CopyExtensions.cs create mode 100644 QYMessageCenter.Common/Extensions/DateTimeExtension.cs create mode 100644 QYMessageCenter.Common/Extensions/EncryptionExtension.cs create mode 100644 QYMessageCenter.Common/Extensions/MapperExtension.cs create mode 100644 QYMessageCenter.Common/Extensions/StartupExtension.cs create mode 100644 QYMessageCenter.Common/Http/HttpDownloader.cs create mode 100644 QYMessageCenter.Common/Http/RestAPIService.cs create mode 100644 QYMessageCenter.Common/Models/ApiResponse.cs create mode 100644 QYMessageCenter.Common/Models/BusinessException.cs create mode 100644 QYMessageCenter.Common/Models/IDenpendency.cs create mode 100644 QYMessageCenter.Common/QYMessageCenter.Common.csproj create mode 100644 QYMessageCenter.Common/TaskSchedulers/DelayTrigger.cs create mode 100644 QYMessageCenter.Common/TaskSchedulers/LimitedConcurrencyLevelTaskScheduler.cs create mode 100644 QYMessageCenter.Model/DB/QYNotification.cs create mode 100644 QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs create mode 100644 QYMessageCenter.Model/MappingProfiles.cs create mode 100644 QYMessageCenter.Model/QYMessageCenter.Model.csproj create mode 100644 QYMessageCenter.Model/__重新生成.bat create mode 100644 QYMessageCenter/Controllers/BaseApiController.cs create mode 100644 QYMessageCenter/Controllers/MessageController.cs delete mode 100644 QYMessageCenter/Controllers/WeatherForecastController.cs create mode 100644 QYMessageCenter/Filters/ResultFilter.cs create mode 100644 QYMessageCenter/Middlewares/ClientVersionValidationMiddleWare.cs create mode 100644 QYMessageCenter/Middlewares/CustomExceptionMiddleWare.cs delete mode 100644 QYMessageCenter/WeatherForecast.cs diff --git a/QYMessageCenter.Business/BaseBusiness.cs b/QYMessageCenter.Business/BaseBusiness.cs new file mode 100644 index 0000000..0e38afb --- /dev/null +++ b/QYMessageCenter.Business/BaseBusiness.cs @@ -0,0 +1,21 @@ +using QYMessageCenter.Common.Http; +using QYMessageCenter.Common.Log; +using Yitter.IdGenerator; +namespace BBWYB.Server.Business +{ + public class BaseBusiness + { + protected IFreeSql fsql; + protected NLogManager nLogManager; + protected IIdGenerator idGenerator; + protected RestApiService restApiService; + + public BaseBusiness(IFreeSql fsql, NLogManager nLogManager, IIdGenerator idGenerator, RestApiService restApiService) + { + this.fsql = fsql; + this.nLogManager = nLogManager; + this.idGenerator = idGenerator; + this.restApiService = restApiService; + } + } +} diff --git a/QYMessageCenter.Business/MessageBusiness.cs b/QYMessageCenter.Business/MessageBusiness.cs new file mode 100644 index 0000000..9fcd4b4 --- /dev/null +++ b/QYMessageCenter.Business/MessageBusiness.cs @@ -0,0 +1,45 @@ +using BBWYB.Server.Business; +using Microsoft.Extensions.Configuration; +using QYMessageCenter.Common.Http; +using QYMessageCenter.Common.Log; +using QYMessageCenter.Common.Models; +using QYMessageCenter.Model.DTO; +using System.Configuration; +using System.Threading.Channels; +using Yitter.IdGenerator; + +namespace QYMessageCenter.Business +{ + public class MessageBusiness : BaseBusiness, IDenpendency + { + private readonly string Key = "BC-652c4236c6ba4083b026e8cfa2e199b1"; + + public MessageBusiness(IFreeSql fsql, NLogManager nLogManager, IIdGenerator idGenerator, RestApiService restApiService) : base(fsql, nLogManager, idGenerator, restApiService) + { + + } + + /// + /// 发送消息 + /// + /// + public void Send(SendMessageRequest request) + { + #region 调用goeasy + var goeasy_httpResult = restApiService.SendRequest("https://rest-hz.goeasy.io/", "v2/pubsub/publish", new + { + appkey = Key, + channel = request.Channel, + content = request.Content + }, null, HttpMethod.Post); + + if (goeasy_httpResult.StatusCode != System.Net.HttpStatusCode.OK) + throw new BusinessException($"消息发送失败 httpcode {goeasy_httpResult.StatusCode} conent {goeasy_httpResult.Content}"); + #endregion + + #region 保存入库 + + #endregion + } + } +} diff --git a/QYMessageCenter.Business/QYMessageCenter.Business.csproj b/QYMessageCenter.Business/QYMessageCenter.Business.csproj new file mode 100644 index 0000000..98a83de --- /dev/null +++ b/QYMessageCenter.Business/QYMessageCenter.Business.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + True + + + + + + + + + + + + + + diff --git a/QYMessageCenter.Business/TaskSchedulerManager.cs b/QYMessageCenter.Business/TaskSchedulerManager.cs new file mode 100644 index 0000000..6420a41 --- /dev/null +++ b/QYMessageCenter.Business/TaskSchedulerManager.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QYMessageCenter.Business +{ + public class TaskSchedulerManager + { + } +} diff --git a/QYMessageCenter.Common/Extensions/ConverterExtensions.cs b/QYMessageCenter.Common/Extensions/ConverterExtensions.cs new file mode 100644 index 0000000..410bd42 --- /dev/null +++ b/QYMessageCenter.Common/Extensions/ConverterExtensions.cs @@ -0,0 +1,29 @@ +namespace QYMessageCenter.Common.Extensions +{ + public static class ConverterExtensions + { + public static long? ToInt64(this object? o) + { + try + { + return Convert.ToInt64(o); + } + catch + { + return null; + } + } + + public static int? ToInt32(this object? o) + { + try + { + return Convert.ToInt32(o); + } + catch + { + return null; + } + } + } +} diff --git a/QYMessageCenter.Common/Extensions/CopyExtensions.cs b/QYMessageCenter.Common/Extensions/CopyExtensions.cs new file mode 100644 index 0000000..694c2f7 --- /dev/null +++ b/QYMessageCenter.Common/Extensions/CopyExtensions.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace QYMessageCenter.Common.Extensions +{ + public static class CopyExtensions + { + public static T Copy(this T p) + { + return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(p)); + } + } +} diff --git a/QYMessageCenter.Common/Extensions/DateTimeExtension.cs b/QYMessageCenter.Common/Extensions/DateTimeExtension.cs new file mode 100644 index 0000000..3322f15 --- /dev/null +++ b/QYMessageCenter.Common/Extensions/DateTimeExtension.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace QYMessageCenter.Common.Extensions +{ + public static class DateTimeExtension + { + private static readonly DateTime beginTime = new DateTime(1970, 1, 1, 0, 0, 0, 0); + + /// + /// 时间戳转时间 + /// + /// 时间 + /// true:13, false:10 + /// + [Obsolete] + public static DateTime StampToDateTime(this long timeStamp) + { + DateTime dt = TimeZone.CurrentTimeZone.ToLocalTime(beginTime); + return timeStamp.ToString().Length == 13 ? dt.AddMilliseconds(timeStamp) : dt.AddSeconds(timeStamp); + } + + /// + /// 时间转时间戳 + /// + /// 时间 + /// true:13, false:10 + /// + public static long DateTimeToStamp(this DateTime time, bool len13 = true) + { + TimeSpan ts = time.ToUniversalTime() - beginTime; + if (len13) + return Convert.ToInt64(ts.TotalMilliseconds); + else + return Convert.ToInt64(ts.TotalSeconds); + } + + /// + /// 将秒数转换为时分秒形式 + /// + /// + /// + public static string FormatToHHmmss(long second) + { + if (second < 60) + return $"00:00:{(second >= 10 ? $"{second}" : $"0{second}")}"; + if (second < 3600) + { + var minute = second / 60; + var s = second % 60; + return $"00:{(minute >= 10 ? $"{minute}" : $"0{minute}")}:{(s >= 10 ? $"{s}" : $"0{s}")}"; + } + else + { + var hour = second / 3600; + var minute = (second - (hour * 3600)) / 60; + var s = (second - ((hour * 3600) + minute * 60)) % 60; + return $"{(hour >= 10 ? $"{hour}" : $"0{hour}")}:{(minute >= 10 ? $"{minute}" : $"0{minute}")}:{(s >= 10 ? $"{s}" : $"0{s}")}"; + } + } + + #region SetLocalTime + [DllImport("Kernel32.dll")] + private static extern bool SetLocalTime(ref SYSTEMTIME lpSystemTime); + + [StructLayout(LayoutKind.Sequential)] + private struct SYSTEMTIME + { + public ushort wYear; + public ushort wMonth; + public ushort wDayOfWeek; + public ushort wDay; + public ushort wHour; + public ushort wMinute; + public ushort wSecond; + public ushort wMilliseconds; + } + + /// + /// 修改系统时间(需要管理员权限) + /// + /// + public static void SetSystemTime(DateTime date) + { + SYSTEMTIME lpTime = new SYSTEMTIME(); + lpTime.wYear = Convert.ToUInt16(date.Year); + lpTime.wMonth = Convert.ToUInt16(date.Month); + lpTime.wDayOfWeek = Convert.ToUInt16(date.DayOfWeek); + lpTime.wDay = Convert.ToUInt16(date.Day); + DateTime time = date; + lpTime.wHour = Convert.ToUInt16(time.Hour); + lpTime.wMinute = Convert.ToUInt16(time.Minute); + lpTime.wSecond = Convert.ToUInt16(time.Second); + lpTime.wMilliseconds = Convert.ToUInt16(time.Millisecond); + var r = SetLocalTime(ref lpTime); + Console.WriteLine($"修改系统时间 {r}"); + } + #endregion + } +} diff --git a/QYMessageCenter.Common/Extensions/EncryptionExtension.cs b/QYMessageCenter.Common/Extensions/EncryptionExtension.cs new file mode 100644 index 0000000..23cf96d --- /dev/null +++ b/QYMessageCenter.Common/Extensions/EncryptionExtension.cs @@ -0,0 +1,82 @@ +using System.Security.Cryptography; +using System.Text; + +namespace QYMessageCenter.Common.Extensions +{ + public static class EncryptionExtension + { + + public static string Md5Encrypt(this string originStr) + { + using (var md5 = MD5.Create()) + { + return string.Join(string.Empty, md5.ComputeHash(Encoding.UTF8.GetBytes(originStr)).Select(x => x.ToString("x2"))); + } + } + + //AES加密 传入,要加密的串和, 解密key + public static string AESEncrypt(this string input) + { + var key = "dataplatform2019"; + var ivStr = "1012132405963708"; + + var encryptKey = Encoding.UTF8.GetBytes(key); + var iv = Encoding.UTF8.GetBytes(ivStr); //偏移量,最小为16 + using (var aesAlg = Aes.Create()) + { + using (var encryptor = aesAlg.CreateEncryptor(encryptKey, iv)) + { + using (var msEncrypt = new MemoryStream()) + { + using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, + CryptoStreamMode.Write)) + + using (var swEncrypt = new StreamWriter(csEncrypt)) + { + swEncrypt.Write(input); + } + var decryptedContent = msEncrypt.ToArray(); + + return Convert.ToBase64String(decryptedContent); + } + } + } + } + + public static string AESDecrypt(this string cipherText) + { + var fullCipher = Convert.FromBase64String(cipherText); + + var ivStr = "1012132405963708"; + var key = "dataplatform2019"; + + var iv = Encoding.UTF8.GetBytes(ivStr); + var decryptKey = Encoding.UTF8.GetBytes(key); + + using (var aesAlg = Aes.Create()) + { + using (var decryptor = aesAlg.CreateDecryptor(decryptKey, iv)) + { + string result; + using (var msDecrypt = new MemoryStream(fullCipher)) + { + using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) + { + using (var srDecrypt = new StreamReader(csDecrypt)) + { + result = srDecrypt.ReadToEnd(); + } + } + } + + return result; + } + } + } + + public static string Base64Encrypt(this string originStr) + { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(originStr)); + } + } +} diff --git a/QYMessageCenter.Common/Extensions/MapperExtension.cs b/QYMessageCenter.Common/Extensions/MapperExtension.cs new file mode 100644 index 0000000..747cae1 --- /dev/null +++ b/QYMessageCenter.Common/Extensions/MapperExtension.cs @@ -0,0 +1,59 @@ +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; + +namespace QYMessageCenter.Common.Extensions +{ + public static class MapperExtension + { + private static IMapper _mapper; + + /// + /// 注册对象映射器 + /// + /// + /// + /// + public static IServiceCollection AddMapper(this IServiceCollection services, Profile profile) + { + var config = new MapperConfiguration(cfg => + { + cfg.AddProfile(profile); + }); + _mapper = config.CreateMapper(); + return services; + } + + /// + /// 设置对象映射执行者 + /// + /// 映射执行者 + public static void SetMapper(IMapper mapper) + { + _mapper = mapper; + } + + /// + /// 将对象映射为指定类型 + /// + /// 要映射的目标类型 + /// 源对象 + /// 目标类型的对象 + public static TTarget Map(this object source) + { + return _mapper.Map(source); + } + + /// + /// 使用源类型的对象更新目标类型的对象 + /// + /// 源类型 + /// 目标类型 + /// 源对象 + /// 待更新的目标对象 + /// 更新后的目标类型对象 + public static TTarget Map(this TSource source, TTarget target) + { + return _mapper.Map(source, target); + } + } +} diff --git a/QYMessageCenter.Common/Extensions/StartupExtension.cs b/QYMessageCenter.Common/Extensions/StartupExtension.cs new file mode 100644 index 0000000..9c6bf69 --- /dev/null +++ b/QYMessageCenter.Common/Extensions/StartupExtension.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace QYMessageCenter.Common.Extensions +{ + public static class StartupExtension + { + public static void BatchRegisterServices(this IServiceCollection serviceCollection, Assembly[] assemblys, Type baseType, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton, bool registerSelf = true) + { + List typeList = new List(); //所有符合注册条件的类集合 + foreach (var assembly in assemblys) + { + var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsSealed && !t.IsAbstract && baseType.IsAssignableFrom(t)); + typeList.AddRange(types); + } + + if (typeList.Count() == 0) + return; + + foreach (var instanceType in typeList) + { + if (registerSelf) + { + serviceCollection.Add(new ServiceDescriptor(instanceType, instanceType, serviceLifetime)); + continue; + } + var interfaces = instanceType.GetInterfaces(); + if (interfaces != null && interfaces.Length > 0) + foreach (var interfaceType in interfaces) + serviceCollection.Add(new ServiceDescriptor(interfaceType, instanceType, serviceLifetime)); + } + } + } +} diff --git a/QYMessageCenter.Common/Http/HttpDownloader.cs b/QYMessageCenter.Common/Http/HttpDownloader.cs new file mode 100644 index 0000000..f6c53cf --- /dev/null +++ b/QYMessageCenter.Common/Http/HttpDownloader.cs @@ -0,0 +1,456 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; + +namespace QYMessageCenter.Common.Http +{ + /// + /// Http下载器 + /// + public class HttpDownloader + { + + private IHttpClientFactory httpClientFactory; + + public HttpDownloader(IHttpClientFactory httpClientFactory, DownloadSetting dwSetting = null) + { + this.httpClientFactory = httpClientFactory; + complateArgs = new DownloadCompletedEventArgs(); + changeArgs = new DownloadProgressChangedEventArgs(); + if (dwSetting == null) + dwSetting = new DownloadSetting(); + downloadSetting = dwSetting; + buffer = new byte[downloadSetting.BufferSize]; + } + + ~HttpDownloader() + { + this.buffer = null; + this.downloadSetting = null; + this.complateArgs.Error = null; + this.complateArgs = null; + this.changeArgs = null; + } + + #region Property + + /// + /// 下载进度变化参数 + /// + private DownloadProgressChangedEventArgs changeArgs; + + /// + /// 下载完成参数 + /// + private DownloadCompletedEventArgs complateArgs; + + /// + /// 下载参数配置类 + /// + private DownloadSetting downloadSetting; + + /// + /// 下载缓冲区 + /// + private byte[] buffer; + + #endregion + + #region Delegate + public delegate void DownloadProgressChangedEventHandler(object sender, DownloadProgressChangedEventArgs e); + + public delegate void DownloadCompletedEventHandler(object sender, DownloadCompletedEventArgs e); + + public delegate void ReDownloadEventHandler(object sender, DownloadCompletedEventArgs e); + #endregion + + #region Event + /// + /// 下载进度变化事件 + /// + public event DownloadProgressChangedEventHandler OnDownloadProgressChanged; + + /// + /// 下载完成事件 + /// + public event DownloadCompletedEventHandler OnDownloadComplated; + + /// + /// 自动重下事件 + /// + public event ReDownloadEventHandler OnReDownload; + #endregion + + #region Method + /// + /// 设置下载参数 + /// + /// + public void SetDownloadSetting(DownloadSetting dwSetting) + { + if (dwSetting != null) + downloadSetting = dwSetting; + } + + private void DoProcessChangedEvent(DownloadProgressChangedEventArgs args) + { + OnDownloadProgressChanged?.Invoke(this, args); + } + + private void DoCompletedEvent(DownloadCompletedEventArgs args) + { + OnDownloadComplated?.Invoke(this, args); + } + + private void DoReDownloadEvent(DownloadCompletedEventArgs args) + { + OnReDownload?.Invoke(this, args); + } + + private void ArgsInit() + { + changeArgs.Cancel = false; + complateArgs.Cancelled = false; + complateArgs.Error = null; + } + + /// + /// 获取文件总大小 + /// + /// + /// + public long GetTotalLength(string url) + { + long length = 0; + try + { + using (var httpClient = httpClientFactory.CreateClient()) + { + var requestMessage = new HttpRequestMessage(HttpMethod.Head, url); + var httpResponse = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).Result; + if (httpResponse.IsSuccessStatusCode) + length = httpResponse.Content.Headers.ContentLength ?? 0; + } + + //var req = (HttpWebRequest)WebRequest.Create(new Uri(url)); + //req.Method = "HEAD"; + //req.Timeout = 5000; + //var res = (HttpWebResponse)req.GetResponse(); + //if (res.StatusCode == HttpStatusCode.OK) + //{ + // length = res.ContentLength; + //} + //res.Close(); + return length; + } + catch (WebException wex) + { + throw wex; + } + } + + + private bool DownloadFile(string url, string savePath, long startPosition, long totalSize) + { + long currentReadSize = 0; //当前已经读取的字节数 + if (startPosition != 0) + currentReadSize = startPosition; + using (var httpClient = httpClientFactory.CreateClient()) + { + try + { + httpClient.Timeout = new TimeSpan(0, 0, 20); + if (currentReadSize != 0 && currentReadSize == totalSize) + { + changeArgs.CurrentSize = changeArgs.TotalSize = totalSize; + return true; + } + if (currentReadSize != 0) + { + try + { + httpClient.DefaultRequestHeaders.Range = new System.Net.Http.Headers.RangeHeaderValue(currentReadSize, totalSize); + } + catch (Exception ex) + { + //http 416 + currentReadSize = 0; + } + } + + using (var response = httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).Result) + { + using (var responseStream = response.Content.ReadAsStreamAsync().Result) + { + changeArgs.TotalSize = totalSize; + using (var fs = new FileStream(savePath, currentReadSize == 0 ? FileMode.Create : FileMode.Append, FileAccess.Write)) + { + //判断服务器是否支持断点续下 + if (currentReadSize != 0 && + response.StatusCode == HttpStatusCode.PartialContent && + response.Content.Headers.ContentLength != totalSize) + fs.Seek(startPosition, SeekOrigin.Begin); //支持,从本地文件流末尾进行写入 + else + currentReadSize = 0; //不支持,从本地文件流开始进行写入 + + if (buffer.Length != downloadSetting.BufferSize) + buffer = new byte[downloadSetting.BufferSize]; + int size = responseStream.Read(buffer, 0, buffer.Length); + while (size != 0) + { + fs.Write(buffer, 0, size); + if (buffer.Length != downloadSetting.BufferSize) + buffer = new byte[downloadSetting.BufferSize]; + size = responseStream.Read(buffer, 0, buffer.Length); + currentReadSize += size; //累计下载大小 + //执行下载进度变化事件 + changeArgs.CurrentSize = currentReadSize; + DoProcessChangedEvent(changeArgs); + //判断挂起状态 + if (changeArgs.Cancel) + return true; + } + + //执行下载进度变化事件 + changeArgs.CurrentSize = changeArgs.TotalSize; + DoProcessChangedEvent(changeArgs); + fs.Flush(true); + } + + //验证远程服务器文件字节数和本地文件字节数是否一致 + long localFileSize = 0; + try + { + localFileSize = new FileInfo(savePath).Length; + } + catch (Exception ex) + { + localFileSize = changeArgs.CurrentSize; + } + + if (totalSize != localFileSize) + { + complateArgs.Error = new Exception(string.Format("远程文件字节:[{0}]与本地文件字节:[{1}]不一致", totalSize, localFileSize)); + } + } + } + } + catch (IOException ex) + { + complateArgs.Error = ex; + } + catch (WebException ex) + { + complateArgs.Error = ex; + } + catch (Exception ex) + { + complateArgs.Error = ex; + } + } + return complateArgs.Error == null; + } + + + public bool DownloadFile(string url, string saveFolderPath, string saveFileName, long? totalSize) + { + ArgsInit(); + var result = false; + //验证文件夹 + try + { + if (!Directory.Exists(saveFolderPath)) + Directory.CreateDirectory(saveFolderPath); + } + catch (Exception ex) + { + complateArgs.Error = ex; + DoCompletedEvent(complateArgs); + return result; + } + + if (totalSize == null || totalSize.Value == 0) + { + try + { + totalSize = GetTotalLength(url); + } + catch (Exception ex) + { + Console.WriteLine("获取远程服务器字节数失败 {0}", ex.Message); + complateArgs.Error = ex; + DoCompletedEvent(complateArgs); + return result; + } + } + + string saveFilePath = Path.Combine(saveFolderPath, saveFileName); + long startPosition = 0; + for (var i = 1; i <= downloadSetting.TryCount; i++) + { + if (File.Exists(saveFilePath)) + { + try + { + startPosition = new FileInfo(saveFilePath).Length; + } + catch + { + startPosition = 0; + } + } + + result = DownloadFile(url, saveFilePath, startPosition, totalSize.Value); + if (result) + { + break; + } + else + { + if (i != downloadSetting.TryCount) + { + complateArgs.TryCount = i + 1; + DoReDownloadEvent(complateArgs); + Thread.Sleep(downloadSetting.SleepTime); + if (complateArgs.Cancelled) + break; + ArgsInit(); + } + } + if (complateArgs.Cancelled) + break; + } + DoCompletedEvent(complateArgs); + return result; + } + + public byte[] DwonloadBytes(string url) + { + ArgsInit(); + using (var httpClient = httpClientFactory.CreateClient()) + { + return httpClient.GetByteArrayAsync(url).Result; + } + + } + + /// + /// 取消/暂停下载 + /// + public void CancelDownload() + { + this.changeArgs.Cancel = true; + this.complateArgs.Cancelled = true; + } + #endregion + } + + + /// + /// 下载进度变化参数 + /// + public class DownloadProgressChangedEventArgs + { + public DownloadProgressChangedEventArgs() + { + ProgressPercentage = 0.0; + Cancel = false; + } + /// + /// 下载进度百分比 + /// + public double ProgressPercentage; + + + private long currentSize; + + /// + /// 当前下载总大小 + /// + public long CurrentSize + { + get { return currentSize; } + set + { + currentSize = value; + this.ProgressPercentage = Math.Round((CurrentSize * 1.0 / TotalSize) * 100, 2); + } + } + + /// + /// 文件总大小 + /// + public long TotalSize; + + /// + /// 取消状态 + /// + public bool Cancel; + } + + /// + /// 下载完成参数 + /// + public class DownloadCompletedEventArgs + { + public DownloadCompletedEventArgs() + { + Cancelled = false; + Error = null; + } + /// + /// 下载异常 + /// + public Exception Error; + + /// + /// 重试次数 + /// + public int TryCount; + + /// + /// 下载操作是否被取消 【取消则为true;默认false】 + /// + public bool Cancelled; + } + + public class DownloadSetting + { + public DownloadSetting() + { + this.BufferSize = 1024 * 1024 * 1; + this.TryCount = 5; + this.SleepTime = 5000; + } + + /// + /// + /// + /// + /// + /// 毫秒 + public DownloadSetting(int bufferSize, int tryCount, int sleepTime) + { + this.BufferSize = bufferSize; + this.TryCount = tryCount; + this.SleepTime = sleepTime; + } + + /// + /// 下载缓冲区大小 + /// + public int BufferSize; + + /// + /// 重试次数 + /// + public int TryCount; + + /// + /// 自动重下休眠时间, 毫秒 + /// + public int SleepTime; + } +} diff --git a/QYMessageCenter.Common/Http/RestAPIService.cs b/QYMessageCenter.Common/Http/RestAPIService.cs new file mode 100644 index 0000000..a28b059 --- /dev/null +++ b/QYMessageCenter.Common/Http/RestAPIService.cs @@ -0,0 +1,117 @@ +using QYMessageCenter.Common.Extensions; +using Newtonsoft.Json; +using System.Net; +using System.Net.Http.Headers; +using System.Text; + +namespace QYMessageCenter.Common.Http +{ + public class RestApiService + { + public const string ContentType_Json = "application/json"; + public const string ContentType_Form = "application/x-www-form-urlencoded"; + public TimeSpan TimeOut { get; set; } = new TimeSpan(0, 0, 40); + + private IHttpClientFactory httpClientFactory; + + public RestApiService(IHttpClientFactory httpClientFactory) + { + this.httpClientFactory = httpClientFactory; + } + + /// + /// 发送请求 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public RestApiResult SendRequest(string apiHost, + string apiPath, + object param, + IDictionary requestHeaders, + HttpMethod httpMethod, + string contentType = ContentType_Json, + ParamPosition paramPosition = ParamPosition.Body, + bool enableRandomTimeStamp = false, + bool getResponseHeader = false, + HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, + string httpClientName = "", + int timeOutSeconds = 0) + { + //Get和Delete强制使用QueryString形式传参 + if (httpMethod == HttpMethod.Get) + paramPosition = ParamPosition.Query; + + //拼接Url + + var url = $"{apiHost}{(apiHost.EndsWith("/") ? string.Empty : (string.IsNullOrEmpty(apiPath) ? string.Empty : "/"))}{(apiPath.StartsWith("/") ? apiPath.Substring(1) : apiPath)}"; + + var isCombineParam = false; + if (paramPosition == ParamPosition.Query && param != null) + { + url = $"{url}{(param.ToString().StartsWith("?") ? string.Empty : "?")}{param}"; + isCombineParam = true; + } + + //使用时间戳绕过CDN + if (enableRandomTimeStamp) + url = $"{url}{(isCombineParam ? "&" : "?")}t={DateTime.Now.DateTimeToStamp()}"; + + using (var httpClient = string.IsNullOrEmpty(httpClientName) ? httpClientFactory.CreateClient() : httpClientFactory.CreateClient(httpClientName)) + { + if (timeOutSeconds == 0) + httpClient.Timeout = TimeOut; + else + httpClient.Timeout = TimeSpan.FromSeconds(timeOutSeconds); + using (var request = new HttpRequestMessage(httpMethod, url)) + { + if (requestHeaders != null && requestHeaders.Count > 0) + foreach (var key in requestHeaders.Keys) + request.Headers.Add(key, requestHeaders[key]); + + if (paramPosition == ParamPosition.Body && param != null) + request.Content = new StringContent(contentType == ContentType_Json ? JsonConvert.SerializeObject(param) : param.ToString(), Encoding.UTF8, contentType); + + using (var response = httpClient.SendAsync(request, httpCompletionOption).Result) + { + return new RestApiResult() + { + StatusCode = response.StatusCode, + Content = httpCompletionOption == HttpCompletionOption.ResponseContentRead ? response.Content.ReadAsStringAsync().Result : + string.Empty, + Headers = getResponseHeader ? response.Headers : null + }; + } + } + } + } + } + + public class RestApiResult + { + public HttpStatusCode StatusCode { get; set; } + + public string Content { get; set; } + + public HttpResponseHeaders Headers { get; set; } + } + + /// + /// 参数传递位置 + /// + public enum ParamPosition + { + Query, + Body + } +} diff --git a/QYMessageCenter.Common/Models/ApiResponse.cs b/QYMessageCenter.Common/Models/ApiResponse.cs new file mode 100644 index 0000000..fe61a9e --- /dev/null +++ b/QYMessageCenter.Common/Models/ApiResponse.cs @@ -0,0 +1,34 @@ +using System; + +namespace QYMessageCenter.Common.Models +{ + public class ApiResponse + { + public bool Success { get { return Code == 200; } } + + /// + /// 接口调用状态 + /// + public int Code { get; set; } = 200; + + /// + /// 返回消息 + /// + public string Msg { get; set; } + + /// + /// 数据内容 + /// + public T Data { get; set; } + + public static ApiResponse Error(int code, string msg) + { + return new ApiResponse() { Code = code, Msg = msg }; + } + } + + public class ApiResponse : ApiResponse + { + + } +} diff --git a/QYMessageCenter.Common/Models/BusinessException.cs b/QYMessageCenter.Common/Models/BusinessException.cs new file mode 100644 index 0000000..5e5c40c --- /dev/null +++ b/QYMessageCenter.Common/Models/BusinessException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace QYMessageCenter.Common.Models +{ + /// + /// 业务异常 + /// + public class BusinessException : Exception + { + public BusinessException(string message) : base(message) + { + + } + + /// + /// 错误代码 + /// + public int Code { get; set; } + } +} diff --git a/QYMessageCenter.Common/Models/IDenpendency.cs b/QYMessageCenter.Common/Models/IDenpendency.cs new file mode 100644 index 0000000..ff13f0b --- /dev/null +++ b/QYMessageCenter.Common/Models/IDenpendency.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace QYMessageCenter.Common.Models +{ + public interface IDenpendency + { + } +} diff --git a/QYMessageCenter.Common/QYMessageCenter.Common.csproj b/QYMessageCenter.Common/QYMessageCenter.Common.csproj new file mode 100644 index 0000000..2143e62 --- /dev/null +++ b/QYMessageCenter.Common/QYMessageCenter.Common.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + True + + + + + + + + + + + diff --git a/QYMessageCenter.Common/TaskSchedulers/DelayTrigger.cs b/QYMessageCenter.Common/TaskSchedulers/DelayTrigger.cs new file mode 100644 index 0000000..d54f213 --- /dev/null +++ b/QYMessageCenter.Common/TaskSchedulers/DelayTrigger.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace QYMessageCenter.Common.Trigger +{ + /// + /// 延迟触发组件 + /// + public class DelayTrigger + { + public DelayTrigger(int delayTime = 1000) + { + if (delayTime < 1000) + delayTime = 1000; + this.delayTime = delayTime; + } + /// + /// 延迟执行时间(ms) + /// + private int delayTime; + + /// + /// 关键字 + /// + private string currentKey; + + /// + /// 是否可以执行 + /// + private volatile bool canExecute; + + /// + /// 是否正在延迟响应中 + /// + private volatile bool isDelaying; + + + public Action OnExecute; + + + public void SetKey(string key) + { + currentKey = key; + if (isDelaying) + { + canExecute = false; + return; + } + Task.Factory.StartNew(delegate + { + isDelaying = true; + while (true) + { + canExecute = true; + Thread.Sleep(delayTime); + if (canExecute) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"DelayTrigger {currentKey} Execute at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:ffff")}"); + Console.ResetColor(); + OnExecute?.Invoke(currentKey); + isDelaying = false; + break; + } + } + }); + } + } +} diff --git a/QYMessageCenter.Common/TaskSchedulers/LimitedConcurrencyLevelTaskScheduler.cs b/QYMessageCenter.Common/TaskSchedulers/LimitedConcurrencyLevelTaskScheduler.cs new file mode 100644 index 0000000..05a2dcc --- /dev/null +++ b/QYMessageCenter.Common/TaskSchedulers/LimitedConcurrencyLevelTaskScheduler.cs @@ -0,0 +1,153 @@ +//-------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// File: LimitedConcurrencyTaskScheduler.cs +// +//-------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; + +namespace System.Threading.Tasks.Schedulers +{ + /// + /// Provides a task scheduler that ensures a maximum concurrency level while + /// running on top of the ThreadPool. + /// + public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler + { + /// Whether the current thread is processing work items. + [ThreadStatic] + private static bool _currentThreadIsProcessingItems; + /// The list of tasks to be executed. + private readonly LinkedList _tasks = new LinkedList(); // protected by lock(_tasks) + /// The maximum concurrency level allowed by this scheduler. + private readonly int _maxDegreeOfParallelism; + /// Whether the scheduler is currently processing work items. + private int _delegatesQueuedOrRunning = 0; // protected by lock(_tasks) + + /// + /// 最大并发数 + /// + private readonly int maxConcurrencyCountOfSystem = Environment.ProcessorCount * 2; + + /// + /// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the + /// specified degree of parallelism. + /// + /// The maximum degree of parallelism provided by this scheduler. + public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) + { + if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism"); + if (maxDegreeOfParallelism > maxConcurrencyCountOfSystem) + maxDegreeOfParallelism = maxConcurrencyCountOfSystem; + _maxDegreeOfParallelism = maxDegreeOfParallelism; + } + + /// Queues a task to the scheduler. + /// The task to be queued. + protected sealed override void QueueTask(Task task) + { + // Add the task to the list of tasks to be processed. If there aren't enough + // delegates currently queued or running to process tasks, schedule another. + lock (_tasks) + { + _tasks.AddLast(task); + if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) + { + ++_delegatesQueuedOrRunning; + NotifyThreadPoolOfPendingWork(); + } + } + } + + /// + /// Informs the ThreadPool that there's work to be executed for this scheduler. + /// + private void NotifyThreadPoolOfPendingWork() + { + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + // Note that the current thread is now processing work items. + // This is necessary to enable inlining of tasks into this thread. + //new Thread(() => + //{ + _currentThreadIsProcessingItems = true; + try + { + // Process all available items in the queue. + while (true) + { + Task item; + lock (_tasks) + { + // When there are no more items to be processed, + // note that we're done processing, and get out. + if (_tasks.Count == 0) + { + --_delegatesQueuedOrRunning; + break; + } + + // Get the next item from the queue + item = _tasks.First.Value; + _tasks.RemoveFirst(); + } + + // Execute the task we pulled out of the queue + base.TryExecuteTask(item); + } + } + // We're done processing items on the current thread + finally { _currentThreadIsProcessingItems = false; } + //}) + //{ IsBackground = true }.Start(); + }, null); + } + + /// Attempts to execute the specified task on the current thread. + /// The task to be executed. + /// + /// Whether the task could be executed on the current thread. + protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // If this thread isn't already processing a task, we don't support inlining + if (!_currentThreadIsProcessingItems) return false; + + // If the task was previously queued, remove it from the queue + if (taskWasPreviouslyQueued) TryDequeue(task); + + // Try to run the task. + return base.TryExecuteTask(task); + } + + /// Attempts to remove a previously scheduled task from the scheduler. + /// The task to be removed. + /// Whether the task could be found and removed. + protected sealed override bool TryDequeue(Task task) + { + lock (_tasks) return _tasks.Remove(task); + } + + /// Gets the maximum concurrency level supported by this scheduler. + public sealed override int MaximumConcurrencyLevel { get { return _maxDegreeOfParallelism; } } + + /// Gets an enumerable of the tasks currently scheduled on this scheduler. + /// An enumerable of the tasks currently scheduled. + protected sealed override IEnumerable GetScheduledTasks() + { + bool lockTaken = false; + try + { + Monitor.TryEnter(_tasks, ref lockTaken); + if (lockTaken) return _tasks.ToArray(); + else throw new NotSupportedException(); + } + finally + { + if (lockTaken) Monitor.Exit(_tasks); + } + } + } +} \ No newline at end of file diff --git a/QYMessageCenter.Model/DB/QYNotification.cs b/QYMessageCenter.Model/DB/QYNotification.cs new file mode 100644 index 0000000..a1ba6f1 --- /dev/null +++ b/QYMessageCenter.Model/DB/QYNotification.cs @@ -0,0 +1,78 @@ +using FreeSql.DataAnnotations; + +namespace QYMessageCenter.Model.DB +{ + + [Table(Name = "qynotification", DisableSyncStructure = true)] + public partial class Qynotification + { + + [Column(DbType = "bigint", IsPrimary = true)] + public long Id { get; set; } + + /// + /// 消息所属应用 + /// + [Column(StringLength = 50)] + public string AppCode { get; set; } + + /// + /// 消息频道 + /// + [Column(StringLength = 50)] + public string Channel { get; set; } + + /// + /// 消息所属团队Id + /// + [Column(StringLength = 50)] + public string TeamId { get; set; } + + /// + /// 消息所属店铺Id + /// + [Column(StringLength = 50)] + public string ShopId { get; set; } + + /// + /// 消息内容 + /// + [Column(StringLength = 500)] + public string Content { get; set; } + + [Column(DbType = "datetime")] + public DateTime? CreateTime { get; set; } + + /// + /// 自定义类型编码 + /// + [Column(StringLength = 50)] + public string CustomTypeCode { get; set; } + + /// + /// 已读人员 + /// + [Column(StringLength = 1000)] + public string ReaderId { get; set; } + + /// + /// 发送人 + /// + [Column(StringLength = 50)] + public string SenderId { get; set; } + + /// + /// 接收人(可空) + /// + [Column(StringLength = 1000)] + public string RecevierId { get; set; } + + /// + /// 是否为Json消息, 解析规则参考CustomTypeCode的约定 + /// + [Column(DbType = "bit")] + public bool IsJsonMsg { get; set; } + + } + +} diff --git a/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs b/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs new file mode 100644 index 0000000..5d777f5 --- /dev/null +++ b/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; + +namespace QYMessageCenter.Model.DTO +{ + public class SendMessageRequest + { + /// + /// 消息所属应用 + /// + [Required(ErrorMessage = "AppCode不能为空")] + public string AppCode { get; set; } + + /// + /// 消息频道 + /// + [Required(ErrorMessage = "Channel不能为空")] + public string Channel { get; set; } + + /// + /// 消息所属团队Id + /// + [Required(ErrorMessage = "TeamId不能为空")] + public string TeamId { get; set; } + + /// + /// 消息所属店铺Id + /// + public string ShopId { get; set; } + + /// + /// 消息内容 + /// + [Required(ErrorMessage = "消息内容不能为空")] + public string Content { get; set; } + + /// + /// 自定义类型编码 + /// + [Required(ErrorMessage = "自定义类型编码不能为空")] + public string CustomTypeCode { get; set; } + + /// + /// 已读人员 + /// + public string ReaderId { get; set; } + + /// + /// 发送人 + /// + public string SenderId { get; set; } + + /// + /// 接收人(可空) + /// + public string RecevierId { get; set; } + + /// + /// 是否为Json消息, 解析规则参考CustomTypeCode的约定 + /// + public bool IsJsonMsg { get; set; } + } +} diff --git a/QYMessageCenter.Model/MappingProfiles.cs b/QYMessageCenter.Model/MappingProfiles.cs new file mode 100644 index 0000000..e96794f --- /dev/null +++ b/QYMessageCenter.Model/MappingProfiles.cs @@ -0,0 +1,12 @@ +namespace QYMessageCenter.Model +{ + public class MappingProfiles : AutoMapper.Profile + { + // private IDictionary storeDictionary; + + public MappingProfiles() + { + + } + } +} diff --git a/QYMessageCenter.Model/QYMessageCenter.Model.csproj b/QYMessageCenter.Model/QYMessageCenter.Model.csproj new file mode 100644 index 0000000..8e5bf91 --- /dev/null +++ b/QYMessageCenter.Model/QYMessageCenter.Model.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + True + + + + + + + + diff --git a/QYMessageCenter.Model/__重新生成.bat b/QYMessageCenter.Model/__重新生成.bat new file mode 100644 index 0000000..c973547 --- /dev/null +++ b/QYMessageCenter.Model/__重新生成.bat @@ -0,0 +1,2 @@ + +FreeSql.Generator -Razor 1 -NameOptions 1,0,0,0 -NameSpace QYMessageCenter.Model.DB -DB "MySql,data source=rm-bp1508okrh23710yfao.mysql.rds.aliyuncs.com;port=3306;user id=qyroot;password=kaicn1132+-;initial catalog=qymessagecenter;charset=utf8;sslmode=none;" -FileName "{name}.cs" diff --git a/QYMessageCenter.sln b/QYMessageCenter.sln index 18f2a01..82b4c27 100644 --- a/QYMessageCenter.sln +++ b/QYMessageCenter.sln @@ -3,7 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34330.188 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QYMessageCenter", "QYMessageCenter\QYMessageCenter.csproj", "{2EA5C5B7-A4AD-4841-9CBD-3D1039ED1355}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QYMessageCenter", "QYMessageCenter\QYMessageCenter.csproj", "{2EA5C5B7-A4AD-4841-9CBD-3D1039ED1355}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QYMessageCenter.Business", "QYMessageCenter.Business\QYMessageCenter.Business.csproj", "{17CB9C4E-4008-4E5A-98B2-40B666922CE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QYMessageCenter.Model", "QYMessageCenter.Model\QYMessageCenter.Model.csproj", "{91F9B6F7-EB22-42AE-80A9-0FF427B055A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QYMessageCenter.Common", "QYMessageCenter.Common\QYMessageCenter.Common.csproj", "{3414DE6F-7D25-4A09-892E-3A359101EB44}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +21,18 @@ Global {2EA5C5B7-A4AD-4841-9CBD-3D1039ED1355}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EA5C5B7-A4AD-4841-9CBD-3D1039ED1355}.Release|Any CPU.ActiveCfg = Release|Any CPU {2EA5C5B7-A4AD-4841-9CBD-3D1039ED1355}.Release|Any CPU.Build.0 = Release|Any CPU + {17CB9C4E-4008-4E5A-98B2-40B666922CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17CB9C4E-4008-4E5A-98B2-40B666922CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17CB9C4E-4008-4E5A-98B2-40B666922CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17CB9C4E-4008-4E5A-98B2-40B666922CE1}.Release|Any CPU.Build.0 = Release|Any CPU + {91F9B6F7-EB22-42AE-80A9-0FF427B055A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91F9B6F7-EB22-42AE-80A9-0FF427B055A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91F9B6F7-EB22-42AE-80A9-0FF427B055A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91F9B6F7-EB22-42AE-80A9-0FF427B055A6}.Release|Any CPU.Build.0 = Release|Any CPU + {3414DE6F-7D25-4A09-892E-3A359101EB44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3414DE6F-7D25-4A09-892E-3A359101EB44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3414DE6F-7D25-4A09-892E-3A359101EB44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3414DE6F-7D25-4A09-892E-3A359101EB44}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/QYMessageCenter/Controllers/BaseApiController.cs b/QYMessageCenter/Controllers/BaseApiController.cs new file mode 100644 index 0000000..e9a569e --- /dev/null +++ b/QYMessageCenter/Controllers/BaseApiController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; + +namespace QYMessageCenter.Controllers +{ + [Produces("application/json")] + [Route("Api/[Controller]/[Action]")] + [ApiController] + [EnableCors("cors")] + public class BaseApiController : ControllerBase + { + protected IHttpContextAccessor httpContextAccessor; + public BaseApiController(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + protected string GetUserId() + { + return httpContextAccessor?.HttpContext?.User.Claims.Where(x => x.Type == "userId")?.FirstOrDefault()?.Value; + } + + protected string GetToken() + { + httpContextAccessor.HttpContext.Request.Headers.TryGetValue("Authorization", out StringValues token); + return token; + } + + //protected string GetClientCode() + //{ + // httpContextAccessor.HttpContext.Request.Headers.TryGetValue("ClientCode", out StringValues clientCode); + // return clientCode; + //} + } +} diff --git a/QYMessageCenter/Controllers/MessageController.cs b/QYMessageCenter/Controllers/MessageController.cs new file mode 100644 index 0000000..5ead808 --- /dev/null +++ b/QYMessageCenter/Controllers/MessageController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using QYMessageCenter.Business; +using QYMessageCenter.Model.DTO; + +namespace QYMessageCenter.Controllers +{ + + public class MessageController : BaseApiController + { + private MessageBusiness messageBusiness; + + public MessageController(IHttpContextAccessor httpContextAccessor, MessageBusiness messageBusiness) : base(httpContextAccessor) + { + this.messageBusiness = messageBusiness; + } + + /// + /// 发送消息 + /// + /// + [HttpPost] + public void Send([FromBody] SendMessageRequest request) + { + messageBusiness.Send(request); + } + } +} diff --git a/QYMessageCenter/Controllers/WeatherForecastController.cs b/QYMessageCenter/Controllers/WeatherForecastController.cs deleted file mode 100644 index 3bc8299..0000000 --- a/QYMessageCenter/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace QYMessageCenter.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/QYMessageCenter/Filters/ResultFilter.cs b/QYMessageCenter/Filters/ResultFilter.cs new file mode 100644 index 0000000..5fb3284 --- /dev/null +++ b/QYMessageCenter/Filters/ResultFilter.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using QYMessageCenter.Common.Models; + +namespace QYMessageCenter.Filters +{ + public class ResultFilter : IResultFilter + { + public void OnResultExecuted(ResultExecutedContext context) + { + + } + + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult) + { + var objectResult = context.Result as ObjectResult; + if (!(objectResult.Value is ApiResponse)) + { + objectResult.Value = new ApiResponse() { Data = objectResult.Value }; + } + } + else if (context.Result is EmptyResult) + { + context.Result = new ObjectResult(new ApiResponse()); + } + } + } +} diff --git a/QYMessageCenter/Middlewares/ClientVersionValidationMiddleWare.cs b/QYMessageCenter/Middlewares/ClientVersionValidationMiddleWare.cs new file mode 100644 index 0000000..426aa9b --- /dev/null +++ b/QYMessageCenter/Middlewares/ClientVersionValidationMiddleWare.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using QYMessageCenter.Common.Models; + +namespace QYMessageCenter.Middlewares +{ + public class ClientVersionValidationMiddleWare + { + /// + /// 管道请求委托 + /// + private RequestDelegate _next; + + private IDictionary apiVersionDictionary; + + private IOptionsMonitor> _monitor; + + public ClientVersionValidationMiddleWare(RequestDelegate requestDelegate, IOptionsMonitor> monitor) + { + _next = requestDelegate; + _monitor = monitor; + apiVersionDictionary = new Dictionary(); + } + + public async Task Invoke(HttpContext context) + { + try + { + Console.WriteLine(context.Request.Path); + var apiRequirement = _monitor.CurrentValue.FirstOrDefault(x => x.Api.Equals(context.Request.Path, StringComparison.CurrentCultureIgnoreCase)); + if (apiRequirement != null) + { + if (!context.Request.Headers.TryGetValue("ClientVersion", out StringValues clientVersionStr)) + throw new BusinessException("缺少版本信息,请更新步步为盈"); + if (!int.TryParse(clientVersionStr, out int clientVersion)) + throw new BusinessException("版本信息不正确,请更新步步为盈"); + if (clientVersion < apiRequirement.MinimumVersion) + throw new BusinessException("当前请求需更新步步为盈~!"); + } + await _next(context); //调用管道执行下一个中间件 + } + catch + { + throw; + } + } + } + + public class ClientVersionValidationModel + { + public string Api { get; set; } + + public int MinimumVersion { get; set; } + } +} diff --git a/QYMessageCenter/Middlewares/CustomExceptionMiddleWare.cs b/QYMessageCenter/Middlewares/CustomExceptionMiddleWare.cs new file mode 100644 index 0000000..b3d66f3 --- /dev/null +++ b/QYMessageCenter/Middlewares/CustomExceptionMiddleWare.cs @@ -0,0 +1,85 @@ +using Newtonsoft.Json; +using QYMessageCenter.Common.Log; +using QYMessageCenter.Common.Models; +using System.Text; + +namespace QYMessageCenter.Middlewares +{ + public class CustomExceptionMiddleWare + { + /// + /// 管道请求委托 + /// + private RequestDelegate _next; + + /// + /// 需要处理的状态码字典 + /// + private IDictionary _exceptionStatusCodeDic; + + + private NLogManager nLogManager; + + public CustomExceptionMiddleWare(RequestDelegate next, NLogManager nLogManager) + { + _next = next; + //this.logger = logger; + this.nLogManager = nLogManager; + _exceptionStatusCodeDic = new Dictionary + { + { 401, "未授权的请求" }, + { 404, "找不到该资源" }, + { 403, "访问被拒绝" }, + { 500, "服务器发生意外的错误" }, + { 503, "服务不可用" } + //其余状态自行扩展 + }; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); //调用管道执行下一个中间件 + } + catch (Exception ex) + { + if (ex is BusinessException) + { + var busEx = ex as BusinessException; + context.Response.StatusCode = 200; //业务异常时将Http状态码改为200 + await ErrorHandle(context, busEx.Code, busEx.Message); + } + else + { + context.Response.Clear(); + context.Response.StatusCode = 500; //发生未捕获的异常,手动设置状态码 + //logger.Error(ex); //记录错误 + nLogManager.Default().Error(ex); + } + } + finally + { + if (_exceptionStatusCodeDic.TryGetValue(context.Response.StatusCode, out string exMsg)) + { + await ErrorHandle(context, context.Response.StatusCode, exMsg); + } + } + } + + /// + /// 处理方式:返回Json格式 + /// + /// + /// + /// + /// + private async Task ErrorHandle(HttpContext context, int code, string exMsg) + { + var apiResponse = ApiResponse.Error(code, exMsg); + var serialzeStr = JsonConvert.SerializeObject(apiResponse); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(serialzeStr, Encoding.UTF8); + } + } +} diff --git a/QYMessageCenter/Program.cs b/QYMessageCenter/Program.cs index df2434c..aebbdcd 100644 --- a/QYMessageCenter/Program.cs +++ b/QYMessageCenter/Program.cs @@ -1,23 +1,118 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Serialization; +using QYMessageCenter.Business; +using QYMessageCenter.Common.Extensions; +using QYMessageCenter.Common.Http; +using QYMessageCenter.Common.Log; +using QYMessageCenter.Common.Models; +using QYMessageCenter.Filters; +using QYMessageCenter.Middlewares; +using QYMessageCenter.Model; +using System.Reflection; +using Yitter.IdGenerator; + var builder = WebApplication.CreateBuilder(args); +var services = builder.Services; +var configuration = builder.Configuration; + +services.AddMemoryCache(); + +var idOption = new IdGeneratorOptions(1); +var idGenerator = new DefaultIdGenerator(idOption); +services.AddSingleton(typeof(IIdGenerator), idGenerator); +var fsql = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, configuration.GetConnectionString("DB")).Build(); +services.AddSingleton(typeof(IFreeSql), fsql); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); + +services.BatchRegisterServices(new Assembly[] { Assembly.Load("QYMessageCenter.Business") }, typeof(IDenpendency)); +services.AddMapper(new MappingProfiles()); + +services.AddHttpContextAccessor(); +services.AddHttpClient(); +services.AddControllers(c => +{ + c.Filters.Add(); + c.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; +}).AddNewtonsoftJson(setupAction => +{ + setupAction.SerializerSettings.ContractResolver = new DefaultContractResolver(); + setupAction.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + //setupAction.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; + //setupAction.SerializerSettings.DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Include; +}); + +services.AddCors(options => +{ + options.AddPolicy("cors", p => + { + p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); + }); +}); + + +services.AddEndpointsApiExplorer(); +services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1.0.0", + Title = "ԽϢAPI", + Description = "ע\r\n1.زƲôշ\r\n2.ApiResponseΪض(Code,Data,Message),ӿеķֵData\r\n3.Code=200" + }); + //JWT֤ + c.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme + { + Scheme = JwtBearerDefaults.AuthenticationScheme, + BearerFormat = "JWT", + Type = SecuritySchemeType.ApiKey, + Name = "Authorization", + In = ParameterLocation.Header, + Description = "Authorization:Bearer {your JWT token}
", + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme{Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = JwtBearerDefaults.AuthenticationScheme + } + }, + new string[] { } + } + }); -// Add services to the container. + var executingAssembly = Assembly.GetExecutingAssembly(); + var assemblyNames = executingAssembly.GetReferencedAssemblies().Union(new AssemblyName[] { executingAssembly.GetName() }).ToArray(); + Array.ForEach(assemblyNames, (assemblyName) => + { + //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlFile = $"{assemblyName.Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (!File.Exists(xmlPath)) + return; + c.IncludeXmlComments(xmlPath, true); + }); +}); -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} -app.UseAuthorization(); +app.UseSwagger(c => c.SerializeAsV2 = true) + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "QYMessageCenter API"); + c.RoutePrefix = string.Empty; + }); +app.UseMiddleware(); +app.UseRouting(); +app.UseCors("cors"); +app.UseAuthorization(); app.MapControllers(); - app.Run(); diff --git a/QYMessageCenter/QYMessageCenter.csproj b/QYMessageCenter/QYMessageCenter.csproj index 2c33b1c..b278f13 100644 --- a/QYMessageCenter/QYMessageCenter.csproj +++ b/QYMessageCenter/QYMessageCenter.csproj @@ -4,10 +4,23 @@ net6.0 enable enable + True + + + + + + + + + + + + diff --git a/QYMessageCenter/WeatherForecast.cs b/QYMessageCenter/WeatherForecast.cs deleted file mode 100644 index f3c7cdd..0000000 --- a/QYMessageCenter/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace QYMessageCenter -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/QYMessageCenter/appsettings.json b/QYMessageCenter/appsettings.json index 10f68b8..59d531c 100644 --- a/QYMessageCenter/appsettings.json +++ b/QYMessageCenter/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "GoEasyKey": "", + "ConnectionStrings": { + "DB": "data source=rm-bp1508okrh23710yfao.mysql.rds.aliyuncs.com;port=3306;user id=qyroot;password=kaicn1132+-;initial catalog=qymessagecenter;charset=utf8;sslmode=none;" + } }