From 9133eae4e0dd307d7d87e1900d3acd4af10d5ebc Mon Sep 17 00:00:00 2001 From: HuiJiOnGit <40553940+HuiJiOnGit@users.noreply.github.com> Date: Thu, 21 Jan 2021 17:18:10 +0800 Subject: [PATCH 001/289] Update .gitignore Add.vscode ignores folders --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9a7ee7a9..5ac1fe08 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ bld/ # Visual Studio 2017 auto generated files Generated\ Files/ +# Visual Studio Code +.vscode + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* From d166b02cd58b206d567555178c57524b438c79dd Mon Sep 17 00:00:00 2001 From: __Leo__ Date: Fri, 4 Mar 2022 15:31:33 +0800 Subject: [PATCH 002/289] Optimize PageModel --- .../Controllers/BaseApiController.cs | 21 ++------ Blog.Core.Model/PageModel.cs | 49 +++++++++++++++++-- Blog.Core.Repository/BASE/BaseRepository.cs | 11 ++--- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/Blog.Core.Api/Controllers/BaseApiController.cs b/Blog.Core.Api/Controllers/BaseApiController.cs index 00c59158..5a374ded 100644 --- a/Blog.Core.Api/Controllers/BaseApiController.cs +++ b/Blog.Core.Api/Controllers/BaseApiController.cs @@ -16,7 +16,7 @@ public MessageModel Success(T data, string msg = "成功") response = data, }; } - // [NonAction] + // [NonAction] //public MessageModel Success(T data, string msg = "成功",bool success = true) //{ // return new MessageModel() @@ -59,20 +59,15 @@ public MessageModel Failed(string msg = "失败", int status = 500) }; } [NonAction] - public MessageModel> SuccessPage(int page, int dataCount, List data, int pageCount, string msg = "获取成功") + public MessageModel> SuccessPage(int page, int dataCount, int pageSize, List data, int pageCount, string msg = "获取成功") { return new MessageModel>() { success = true, msg = msg, - response = new PageModel() - { - page = page, - dataCount = dataCount, - data = data, - pageCount = pageCount, - } + response = new PageModel(page, dataCount, pageSize, data) + }; } [NonAction] @@ -83,13 +78,7 @@ public MessageModel> SuccessPage(PageModel pageModel, string { success = true, msg = msg, - response = new PageModel() - { - page = pageModel.page, - dataCount = pageModel.dataCount, - data = pageModel.data, - pageCount = pageModel.pageCount, - } + response = pageModel }; } } diff --git a/Blog.Core.Model/PageModel.cs b/Blog.Core.Model/PageModel.cs index 45a3798e..f6872b66 100644 --- a/Blog.Core.Model/PageModel.cs +++ b/Blog.Core.Model/PageModel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using AutoMapper; +using System; +using System.Collections.Generic; namespace Blog.Core.Model { @@ -14,7 +16,7 @@ public class PageModel /// /// 总页数 /// - public int pageCount { get; set; } = 6; + public int pageCount => (int)Math.Ceiling((decimal)dataCount / PageSize); /// /// 数据总数 /// @@ -22,12 +24,53 @@ public class PageModel /// /// 每页大小 /// - public int PageSize { set; get; } + public int PageSize { set; get; } = 20; /// /// 返回数据 /// public List data { get; set; } + public PageModel() { } + + public PageModel(int page, int dataCount, int pageSize, List data) + { + this.page = page; + this.dataCount = dataCount; + PageSize = pageSize; + this.data = data; + } + + public PageModel ConvertTo() + { + return new PageModel(page, dataCount, PageSize, default); + } + + + public PageModel ConvertTo(IMapper mapper) + { + var model = ConvertTo(); + + if (data != null) + { + model.data = mapper.Map>(data); + } + + return model; + } + + + public PageModel ConvertTo(IMapper mapper, Action options) + { + var model = ConvertTo(); + if (data != null) + { + model.data = mapper.Map>(data, options); + } + + return model; + + } + } } diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 381801d7..cd088930 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -97,7 +97,7 @@ public async Task Add(TEntity entity) //return (int)i; var insert = _db.Insertable(entity); - + //这里你可以返回TEntity,这样的话就可以获取id值,无论主键是什么类型 //var return3 = await insert.ExecuteReturnEntityAsync(); @@ -453,8 +453,7 @@ public async Task> QueryPage(Expression> .WhereIF(whereExpression != null, whereExpression) .ToPageListAsync(intPageIndex, intPageSize, totalCount); - int pageCount = (Math.Ceiling(totalCount.ObjToDecimal() / intPageSize.ObjToDecimal())).ObjToInt(); - return new PageModel() { dataCount = totalCount, pageCount = pageCount, page = intPageIndex, PageSize = intPageSize, data = list }; + return new PageModel(intPageIndex, totalCount, intPageSize, list); } @@ -510,8 +509,7 @@ public async Task> QueryTabsPage( .OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds) .WhereIF(whereExpression != null, whereExpression) .ToPageListAsync(intPageIndex, intPageSize, totalCount); - int pageCount = (Math.Ceiling(totalCount.ObjToDecimal() / intPageSize.ObjToDecimal())).ObjToInt(); - return new PageModel() { dataCount = totalCount, pageCount = pageCount, page = intPageIndex, PageSize = intPageSize, data = list }; + return new PageModel(intPageIndex, totalCount, intPageSize, list); } /// @@ -543,8 +541,7 @@ public async Task> QueryTabsPage( .OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds) .WhereIF(whereExpression != null, whereExpression) .ToPageListAsync(intPageIndex, intPageSize, totalCount); - int pageCount = (Math.Ceiling(totalCount.ObjToDecimal() / intPageSize.ObjToDecimal())).ObjToInt(); - return new PageModel() { dataCount = totalCount, pageCount = pageCount, page = intPageIndex, PageSize = intPageSize, data = list }; + return new PageModel(intPageIndex, totalCount, intPageSize, list); } //var exp = Expressionable.Create() From 113c8fd94a344dfd0f4950870c3ef8406e8bb04e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 4 Mar 2022 21:49:07 +0800 Subject: [PATCH 003/289] Fixed #251 bug --- Blog.Core.Common/Blog.Core.Common.csproj | 4 ---- Blog.Core.Model/Blog.Core.Model.csproj | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index 1fb4ba6c..1887b4eb 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -27,10 +27,6 @@ - - - - diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index ed0fb852..e97f4e95 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -16,6 +16,8 @@ + + From ca11b2a627494d34f0bd739d66cd68df61bad250 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 8 Mar 2022 11:40:26 +0800 Subject: [PATCH 004/289] fix: update package version --- Blog.Core.Api/Blog.Core.xml | 6 ++++++ Blog.Core.Api/Controllers/LoginController.cs | 17 ++++++++++++++--- CreateYourProject.bat | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 32ac8c50..4c062d5f 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -239,6 +239,12 @@ + + + weixin登录 + + + 接口管理 diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 55c20827..52a7a8b7 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - + namespace Blog.Core.Controllers { @@ -62,11 +62,11 @@ public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServic [Route("Token")] public async Task> GetJwtStr(string name, string pass) { - + string jwtStr = string.Empty; bool suc = false; //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 - + var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); if (user != null) { @@ -285,6 +285,17 @@ public dynamic SwgLogin([FromBody] SwaggerLoginRequest loginRequest) return new { result = false }; } + + /// + /// weixin登录 + /// + /// + [HttpGet] + [Route("wxLogin")] + public dynamic WxLogin(string g = "", string token = "") + { + return new { g, token }; + } } public class SwaggerLoginRequest diff --git a/CreateYourProject.bat b/CreateYourProject.bat index 6bee0420..c15c6efb 100644 --- a/CreateYourProject.bat +++ b/CreateYourProject.bat @@ -3,7 +3,7 @@ echo "if u install template error,pls connect QQ:3143422472" color 3 -dotnet new -i Blog.Core.Webapi.Template::2.6.2 +dotnet new -i Blog.Core.Webapi.Template::2.6.3 set /p OP=Please set your project name(for example:BlogMicService): From c06e3f16b0650cfddaed30b4974cf7aac86259b6 Mon Sep 17 00:00:00 2001 From: KimiDing Date: Wed, 9 Mar 2022 17:59:49 +0800 Subject: [PATCH 005/289] =?UTF-8?q?Http=20Polly=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.xml | 2 +- Blog.Core.Api/Controllers/ValuesController.cs | 25 +- Blog.Core.Api/Startup.cs | 1 + Blog.Core.Common/HttpPolly/HttpPollyHelper.cs | 354 ++++++++++++++++++ .../HttpPolly/IHttpPollyHelper.cs | 19 + .../Blog.Core.Extensions.csproj | 1 + .../ServiceExtensions/HttpPollySetup.cs | 56 +++ Blog.Core.Model/HttpEnum.cs | 10 + 8 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 Blog.Core.Common/HttpPolly/HttpPollyHelper.cs create mode 100644 Blog.Core.Common/HttpPolly/IHttpPollyHelper.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs create mode 100644 Blog.Core.Model/HttpEnum.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 4c062d5f..a2fa3a8e 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -722,7 +722,7 @@ Values控制器 - + ValuesController diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index bdc83da9..9f53ed82 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -1,6 +1,7 @@ using AutoMapper; using Blog.Core.Common; using Blog.Core.Common.HttpContextUser; +using Blog.Core.Common.HttpPolly; using Blog.Core.Common.HttpRestSharp; using Blog.Core.Common.WebApiClients.HttpApis; using Blog.Core.EventBus; @@ -44,6 +45,7 @@ public class ValuesController : ControllerBase private readonly IBlogApi _blogApi; private readonly IDoubanApi _doubanApi; readonly IBlogArticleServices _blogArticleServices; + private readonly IHttpPollyHelper _httpPollyHelper; /// /// ValuesController @@ -64,7 +66,8 @@ public ValuesController(IBlogArticleServices blogArticleServices , IRoleModulePermissionServices roleModulePermissionServices , IUser user, IPasswordLibServices passwordLibServices , IBlogApi blogApi - , IDoubanApi doubanApi) + , IDoubanApi doubanApi + , IHttpPollyHelper httpPollyHelper) { // 测试 Authorize 和 mapper _mapper = mapper; @@ -82,6 +85,8 @@ public ValuesController(IBlogArticleServices blogArticleServices _blogArticleServices = blogArticleServices; // 测试redis消息队列 _blogArticleServices = blogArticleServices; + // httpPolly + _httpPollyHelper = httpPollyHelper; } [HttpGet] @@ -390,6 +395,24 @@ public async Task GetConfigByAppllo(string key) return await Task.FromResult(Appsettings.app(key)); } #endregion + + #region HttpPolly + [HttpPost] + [AllowAnonymous] + public async Task HttpPollyPost() + { + var response = await _httpPollyHelper.PostAsync(HttpEnum.LocalHost, "/api/ElasticDemo/EsSearchTest", "{\"from\": 0,\"size\": 10,\"word\": \"非那雄安\"}"); + + return response; + } + + [HttpGet] + [AllowAnonymous] + public async Task HttpPollyGet() + { + return await _httpPollyHelper.GetAsync(HttpEnum.LocalHost, "/api/ElasticDemo/GetDetailInfo?esid=3130&esindex=chinacodex"); + } + #endregion } public class ClaimDto { diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index bc3d779f..1166fc4e 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -97,6 +97,7 @@ public void ConfigureServices(IServiceCollection services) services.AddDistributedMemoryCache(); services.AddSession(); + services.AddHttpPollySetup(); services.AddControllers(o => { diff --git a/Blog.Core.Common/HttpPolly/HttpPollyHelper.cs b/Blog.Core.Common/HttpPolly/HttpPollyHelper.cs new file mode 100644 index 00000000..f1a1e84c --- /dev/null +++ b/Blog.Core.Common/HttpPolly/HttpPollyHelper.cs @@ -0,0 +1,354 @@ +using Blog.Core.Model; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Common.HttpPolly +{ + public class HttpPollyHelper : IHttpPollyHelper + { + private readonly IHttpClientFactory _clientFactory; + + public HttpPollyHelper(IHttpClientFactory httpClientFactory) + { + _clientFactory = httpClientFactory; + } + + public async Task PostAsync(HttpEnum httpEnum, string url, R request, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var response = await client.PostAsync(url, stringContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string result = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(result); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + } + catch (Exception) + { + + throw; + } + + } + + public async Task PostAsync(HttpEnum httpEnum, string url, string request, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var stringContent = new StringContent(request, Encoding.UTF8, "application/json"); + var response = await client.PostAsync(url, stringContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string result = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(result); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + + public async Task PostAsync(HttpEnum httpEnum, string url, R request, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var response = await client.PostAsync(url, stringContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + return await response.Content.ReadAsStringAsync(); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + } + catch (Exception) + { + + throw; + } + + } + + public async Task PostAsync(HttpEnum httpEnum, string url, string request, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var stringContent = new StringContent(request, Encoding.UTF8, "application/json"); + var response = await client.PostAsync(url, stringContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + return await response.Content.ReadAsStringAsync(); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + + public async Task GetAsync(HttpEnum httpEnum, string url, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var response = await client.GetAsync(url); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string result = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(result); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + + public async Task GetAsync(HttpEnum httpEnum, string url, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var response = await client.GetAsync(url); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + return await response.Content.ReadAsStringAsync(); ; + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + + public async Task PutAsync(HttpEnum httpEnum, string url, R request, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var response = await client.PutAsync(url, stringContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string result = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(result); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + + public async Task PutAsync(HttpEnum httpEnum, string url, string request, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var stringContent = new StringContent(request, Encoding.UTF8, "application/json"); + var response = await client.PutAsync(url, stringContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string result = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(result); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + + public async Task DeleteAsync(HttpEnum httpEnum, string url, Dictionary headers = null) + { + try + { + var client = _clientFactory.CreateClient(httpEnum.ToString()); + if (headers != null) + { + foreach (var header in headers) + { + if (!client.DefaultRequestHeaders.Contains(header.Key)) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } + + var response = await client.DeleteAsync(url); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string result = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(result); + } + else + { + throw new Exception($"Http Error StatusCode:{response.StatusCode}"); + } + + } + catch (Exception) + { + + throw; + } + + } + } +} diff --git a/Blog.Core.Common/HttpPolly/IHttpPollyHelper.cs b/Blog.Core.Common/HttpPolly/IHttpPollyHelper.cs new file mode 100644 index 00000000..a3c8b3f6 --- /dev/null +++ b/Blog.Core.Common/HttpPolly/IHttpPollyHelper.cs @@ -0,0 +1,19 @@ +using Blog.Core.Model; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.HttpPolly +{ + public interface IHttpPollyHelper + { + Task PostAsync(HttpEnum httpEnum, string url, R request, Dictionary headers = null); + Task PostAsync(HttpEnum httpEnum, string url, string request, Dictionary headers = null); + Task PostAsync(HttpEnum httpEnum, string url, string request, Dictionary headers = null); + Task PostAsync(HttpEnum httpEnum, string url, R request, Dictionary headers = null); + Task GetAsync(HttpEnum httpEnum, string url, Dictionary headers = null); + Task GetAsync(HttpEnum httpEnum, string url, Dictionary headers = null); + Task PutAsync(HttpEnum httpEnum, string url, R request, Dictionary headers = null); + Task PutAsync(HttpEnum httpEnum, string url, string request, Dictionary headers = null); + Task DeleteAsync(HttpEnum httpEnum, string url, Dictionary headers = null); + } +} diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 626abbf3..4adc30df 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -15,6 +15,7 @@ + diff --git a/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs b/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs new file mode 100644 index 00000000..e8e3929f --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs @@ -0,0 +1,56 @@ +using Blog.Core.Common.HttpPolly; +using Blog.Core.Model; +using Microsoft.Extensions.DependencyInjection; +using Polly; +using Polly.Extensions.Http; +using Polly.Timeout; +using System; +using System.Net.Http; + +namespace Blog.Core.Extensions +{ + /// + /// Cors 启动服务 + /// + public static class HttpPollySetup + { + public static void AddHttpPollySetup(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + #region Polly策略 + var retryPolicy = HttpPolicyExtensions + .HandleTransientHttpError() + .Or() // 若超时则抛出此异常 + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }); + + // 为每个重试定义超时策略 + var timeoutPolicy = Policy.TimeoutAsync(10); + #endregion + + services.AddHttpClient(HttpEnum.Common.ToString(), c => + { + c.DefaultRequestHeaders.Add("Accept", "application/json"); + }) + .AddPolicyHandler(retryPolicy) + // 将超时策略放在重试策略之内,每次重试会应用此超时策略 + .AddPolicyHandler(timeoutPolicy); + + services.AddHttpClient(HttpEnum.LocalHost.ToString(), c => + { + c.BaseAddress = new Uri("http://www.localhost.com"); + c.DefaultRequestHeaders.Add("Accept", "application/json"); + }) + .AddPolicyHandler(retryPolicy) + // 将超时策略放在重试策略之内,每次重试会应用此超时策略 + .AddPolicyHandler(timeoutPolicy); + + services.AddSingleton(); + } + } +} diff --git a/Blog.Core.Model/HttpEnum.cs b/Blog.Core.Model/HttpEnum.cs new file mode 100644 index 00000000..05e25582 --- /dev/null +++ b/Blog.Core.Model/HttpEnum.cs @@ -0,0 +1,10 @@ +using System; + +namespace Blog.Core.Model +{ + public enum HttpEnum + { + Common, + LocalHost + } +} From ce13a5466d39c391a9c9ece195489879d09a3d2c Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Thu, 10 Mar 2022 14:52:17 +0800 Subject: [PATCH 006/289] Update README.md --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6a58321f..2b3e7185 100644 --- a/README.md +++ b/README.md @@ -115,14 +115,12 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x ## 贡献者们 -Thanks goes to these wonderful people ([✨](https://github.com/anjoy8/Blog.Core/graphs/contributors)):(排名暂时按提交顺序) - - -| [
anjoy8](https://github.com/anjoy8)
💻📖 💡 | [
hudingwen](https://github.com/hudingwen)
💻 👀 | [
binyly ](https://github.com/binyly)
💻 👀 📖 👍 | [
wuare ](https://github.com/wuare)
💻😀 | [
skang0401 ](https://github.com/skang0401)
📖| [
Jamnine](https://github.com/Jamnine)
💻 🌍| -| :---: | :---: | :---: | :---: | :---: | :---: | -|[
aion1998 ](https://github.com/aion1998)
👍|[
RLei123 ](https://github.com/RLei123)
😄|[
cluyun ](https://github.com/cluyun)
🍬|[
blue20171027 ](https://github.com/blue20171027)
✈|[
anewboyz ](https://github.com/anewboyz)
💻|[
jxd728 ](https://github.com/jxd728)
🌍| -|[
wmchuang ](https://github.com/wmchuang)
🍟|[
liuzhenyulive ](https://github.com/liuzhenyulive)
💻|[
JsonBy ](https://github.com/JsonBy)
💻 💡 🤔|[
hsxian ](https://github.com/hsxian)
🎉|[
cuno92 ](https://github.com/cuno92)
📖|[
317447880 ](https://github.com/317447880)
💻| -|[
Shuisen ](https://github.com/Shuisen)
💻|[
www5255977 ](https://github.com/www5255977)
🌍|[
867824092 ](https://github.com/867824092)
🍳| +Thanks goes to these wonderful people ([✨](https://github.com/anjoy8/Blog.Core/graphs/contributors)):(排名暂时按提交顺序) + + + + + This project follows the [all-contributors](https://github.com/anjoy8/Blog.Core/graphs/contributors) specification. From 39ecd79804492043e3ecc4836ea5d34edb28d1be Mon Sep 17 00:00:00 2001 From: __Leo__ Date: Fri, 11 Mar 2022 13:25:29 +0800 Subject: [PATCH 007/289] Enable description of enum in swagger --- Blog.Core.Api/Startup.cs | 3 +++ Blog.Core.Extensions/Blog.Core.Extensions.csproj | 1 + Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index bc3d779f..f7c9231e 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System.IdentityModel.Tokens.Jwt; using System.Reflection; @@ -125,6 +126,8 @@ public void ConfigureServices(IServiceCollection services) //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; //设置本地时间而非UTC时间 options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; + //添加Enum转string + options.SerializerSettings.Converters.Add(new StringEnumConverter()); }); services.Replace(ServiceDescriptor.Transient()); diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 626abbf3..47307d63 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -27,6 +27,7 @@ + diff --git a/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs index e563b335..0acb131c 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs @@ -45,7 +45,7 @@ public static void AddSwaggerSetup(this IServiceCollection services) c.OrderActionsBy(o => o.RelativePath); }); - + c.UseInlineDefinitionsForEnums(); try { //这个就是刚刚配置的xml文件名 @@ -106,6 +106,7 @@ public static void AddSwaggerSetup(this IServiceCollection services) }); + services.AddSwaggerGenNewtonsoftSupport(); } } From 9f77e70772af8d7fd2e3807cabadcea1b1821999 Mon Sep 17 00:00:00 2001 From: __Leo__ Date: Fri, 11 Mar 2022 15:01:06 +0800 Subject: [PATCH 008/289] Add enum demo to #254 --- Blog.Core.Api/Blog.Core.Model.xml | 5 ++++ Blog.Core.Api/Controllers/ValuesController.cs | 4 ++++ Blog.Core.Model/ViewModels/EnumDemoDto.cs | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 Blog.Core.Model/ViewModels/EnumDemoDto.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 2a49c502..2d8c7661 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1689,6 +1689,11 @@
+ + + Type Description balabala + + 留言信息展示类 diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 9f53ed82..e42efca7 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -413,6 +413,10 @@ public async Task HttpPollyGet() return await _httpPollyHelper.GetAsync(HttpEnum.LocalHost, "/api/ElasticDemo/GetDetailInfo?esid=3130&esindex=chinacodex"); } #endregion + + [HttpPost] + [AllowAnonymous] + public string TestEnum(EnumDemoDto dto)=>dto.Type.ToString(); } public class ClaimDto { diff --git a/Blog.Core.Model/ViewModels/EnumDemoDto.cs b/Blog.Core.Model/ViewModels/EnumDemoDto.cs new file mode 100644 index 00000000..ae6ffbdd --- /dev/null +++ b/Blog.Core.Model/ViewModels/EnumDemoDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Model.ViewModels +{ + public class EnumDemoDto + { + public int Id { get; set; } + /// + /// Type Description balabala + /// + public EnumType Type { get; set; } + } + + + public enum EnumType + { + foo, bar, baz + } +} From e3ca4540750cb7f8c2a4faf667e021df022ad109 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 11 Mar 2022 16:38:18 +0800 Subject: [PATCH 009/289] :feet: add param --- Blog.Core.Api/Controllers/ValuesController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index e42efca7..d214ca07 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -59,6 +59,7 @@ public class ValuesController : ControllerBase /// /// /// + /// public ValuesController(IBlogArticleServices blogArticleServices , IMapper mapper , IAdvertisementServices advertisementServices From 945ce5faa16ac57e23e4f223d0aaab91a861a211 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 11 Mar 2022 18:18:40 +0800 Subject: [PATCH 010/289] Fixed #256 bug --- Blog.Core.Api/connectionstrings.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 Blog.Core.Api/connectionstrings.json diff --git a/Blog.Core.Api/connectionstrings.json b/Blog.Core.Api/connectionstrings.json deleted file mode 100644 index 5f941362..00000000 --- a/Blog.Core.Api/connectionstrings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "connectionstrings": { - "sqlserver": "Server=xxx.picp;Database=blogcorets;User Id=;Password=;", - "oracle": "oracle-connectionstring", - "postgresql": "Server=log4net.database.test;Port=5432;Database=log4net;User ID=;Pooling=true;Password=;", - "mysql": "Server=log4net.database.test;Port=3306;Database=log4net;Uid=;Pwd=", - "mysqlOLD": "Server=log4net.database.test;Port=3306;Database=log4net;Uid=;Pwd=;allow user variables=true;SslMode=Required;maxpoolsize=100;Convert Zero Datetime=true;CharSet=utf8;CheckParameters=false;" - } -} From cf457503aee5dca30d8a3f99dea93cde8c4a9dbe Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 11 Mar 2022 18:55:36 +0800 Subject: [PATCH 011/289] Update CreateYourProject.bat --- CreateYourProject.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CreateYourProject.bat b/CreateYourProject.bat index c15c6efb..9f8dcc22 100644 --- a/CreateYourProject.bat +++ b/CreateYourProject.bat @@ -3,7 +3,7 @@ echo "if u install template error,pls connect QQ:3143422472" color 3 -dotnet new -i Blog.Core.Webapi.Template::2.6.3 +dotnet new -i Blog.Core.Webapi.Template set /p OP=Please set your project name(for example:BlogMicService): From 1093a7b76aa66f39b60aa1f6948413731d7f0ddf Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 14 Mar 2022 15:15:05 +0800 Subject: [PATCH 012/289] fix: format the code. #236 --- Blog.Core.Api/Blog.Core.Model.xml | 8 +- Blog.Core.Api/Blog.Core.xml | 1 + .../Blog.Core.ConsoleApp.csproj | 8 - Blog.Core.ConsoleApp/Program.cs | 12 -- Blog.Core.IServices/BASE/IBaseServices.cs | 26 +-- Blog.Core.Model/PaginationModel.cs | 14 +- Blog.Core.Repository/BASE/BaseRepository.cs | 199 ++++++++---------- Blog.Core.Repository/BASE/IBaseRepository.cs | 64 +++--- Blog.Core.Services/BASE/BaseServices.cs | 116 +++++----- Blog.Core.sln | 7 - 10 files changed, 197 insertions(+), 258 deletions(-) delete mode 100644 Blog.Core.ConsoleApp/Blog.Core.ConsoleApp.csproj delete mode 100644 Blog.Core.ConsoleApp/Program.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 2d8c7661..d6c167e3 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1514,22 +1514,22 @@ 时间:2020-4-3 20:31:26 - + 当前页 - + 每页大小 - + 排序字段(例如:id desc,time asc) - + 查询条件( 例如:id = 1 and name = 小明) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index a2fa3a8e..eb1ea542 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -735,6 +735,7 @@ + diff --git a/Blog.Core.ConsoleApp/Blog.Core.ConsoleApp.csproj b/Blog.Core.ConsoleApp/Blog.Core.ConsoleApp.csproj deleted file mode 100644 index 41f1d5ad..00000000 --- a/Blog.Core.ConsoleApp/Blog.Core.ConsoleApp.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - Exe - net6.0 - - - diff --git a/Blog.Core.ConsoleApp/Program.cs b/Blog.Core.ConsoleApp/Program.cs deleted file mode 100644 index 87e5d801..00000000 --- a/Blog.Core.ConsoleApp/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Blog.Core.ConsoleApp -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index dbc242c1..27daae98 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -26,32 +26,32 @@ public interface IBaseServices where TEntity : class Task DeleteByIds(object[] ids); Task Update(TEntity model); - Task Update(TEntity entity, string strWhere); + Task Update(TEntity entity, string where); Task Update(object operateAnonymousObjects); - Task Update(TEntity entity, List lstColumns = null, List lstIgnoreColumns = null, string strWhere = ""); + Task Update(TEntity entity, List lstColumns = null, List lstIgnoreColumns = null, string where = ""); Task> Query(); - Task> Query(string strWhere); + Task> Query(string where); Task> Query(Expression> whereExpression); - Task> Query(Expression> whereExpression, string strOrderByFileds); + Task> Query(Expression> whereExpression, string orderByFields); Task> Query(Expression> expression); - Task> Query(Expression> expression, Expression> whereExpression,string strOrderByFileds); + Task> Query(Expression> expression, Expression> whereExpression, string orderByFields); Task> Query(Expression> whereExpression, Expression> orderByExpression, bool isAsc = true); - Task> Query(string strWhere, string strOrderByFileds); - Task> QuerySql(string strSql, SugarParameter[] parameters = null); - Task QueryTable(string strSql, SugarParameter[] parameters = null); + Task> Query(string where, string orderByFields); + Task> QuerySql(string sql, SugarParameter[] parameters = null); + Task QueryTable(string sql, SugarParameter[] parameters = null); - Task> Query(Expression> whereExpression, int intTop, string strOrderByFileds); - Task> Query(string strWhere, int intTop, string strOrderByFileds); + Task> Query(Expression> whereExpression, int top, string orderByFields); + Task> Query(string where, int top, string orderByFields); Task> Query( - Expression> whereExpression, int intPageIndex, int intPageSize, string strOrderByFileds); - Task> Query(string strWhere, int intPageIndex, int intPageSize, string strOrderByFileds); + Expression> whereExpression, int pageIndex, int pageSize, string orderByFields); + Task> Query(string where, int pageIndex, int pageSize, string orderByFields); - Task> QueryPage(Expression> whereExpression, int intPageIndex = 1, int intPageSize = 20, string strOrderByFileds = null); + Task> QueryPage(Expression> whereExpression, int pageIndex = 1, int pageSize = 20, string orderByFields = null); Task> QueryMuch( Expression> joinExpression, diff --git a/Blog.Core.Model/PaginationModel.cs b/Blog.Core.Model/PaginationModel.cs index 3afee0b2..f79b3316 100644 --- a/Blog.Core.Model/PaginationModel.cs +++ b/Blog.Core.Model/PaginationModel.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Blog.Core.Model +namespace Blog.Core.Model { /// /// 所需分页参数 @@ -14,18 +10,18 @@ public class PaginationModel /// /// 当前页 /// - public int intPageIndex { get; set; } = 1; + public int PageIndex { get; set; } = 1; /// /// 每页大小 /// - public int intPageSize { get; set; } = 10; + public int PageSize { get; set; } = 10; /// /// 排序字段(例如:id desc,time asc) /// - public string strOrderByFileds { get; set; } + public string OrderByFileds { get; set; } /// /// 查询条件( 例如:id = 1 and name = 小明) /// - public string conditions { get; set; } + public string Conditions { get; set; } } } diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index cd088930..bb83b4c5 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -16,7 +16,7 @@ namespace Blog.Core.Repository.Base public class BaseRepository : IBaseRepository where TEntity : class, new() { private readonly IUnitOfWork _unitOfWork; - private SqlSugarScope _dbBase; + private readonly SqlSugarScope _dbBase; private ISqlSugarClient _db { @@ -42,10 +42,7 @@ private ISqlSugarClient _db } } - public ISqlSugarClient Db - { - get { return _db; } - } + public ISqlSugarClient Db => _db; public BaseRepository(IUnitOfWork unitOfWork) { @@ -148,16 +145,14 @@ public async Task Update(TEntity entity) return await _db.Updateable(entity).ExecuteCommandHasChangeAsync(); } - public async Task Update(TEntity entity, string strWhere) + public async Task Update(TEntity entity, string where) { - //return await Task.Run(() => _db.Updateable(entity).Where(strWhere).ExecuteCommand() > 0); - return await _db.Updateable(entity).Where(strWhere).ExecuteCommandHasChangeAsync(); + return await _db.Updateable(entity).Where(where).ExecuteCommandHasChangeAsync(); } - public async Task Update(string strSql, SugarParameter[] parameters = null) + public async Task Update(string sql, SugarParameter[] parameters = null) { - //return await Task.Run(() => _db.Ado.ExecuteCommand(strSql, parameters) > 0); - return await _db.Ado.ExecuteCommandAsync(strSql, parameters) > 0; + return await _db.Ado.ExecuteCommandAsync(sql, parameters) > 0; } public async Task Update(object operateAnonymousObjects) @@ -169,24 +164,9 @@ public async Task Update( TEntity entity, List lstColumns = null, List lstIgnoreColumns = null, - string strWhere = "" + string where = "" ) { - //IUpdateable up = await Task.Run(() => _db.Updateable(entity)); - //if (lstIgnoreColumns != null && lstIgnoreColumns.Count > 0) - //{ - // up = await Task.Run(() => up.IgnoreColumns(it => lstIgnoreColumns.Contains(it))); - //} - //if (lstColumns != null && lstColumns.Count > 0) - //{ - // up = await Task.Run(() => up.UpdateColumns(it => lstColumns.Contains(it))); - //} - //if (!string.IsNullOrEmpty(strWhere)) - //{ - // up = await Task.Run(() => up.Where(strWhere)); - //} - //return await Task.Run(() => up.ExecuteCommand()) > 0; - IUpdateable up = _db.Updateable(entity); if (lstIgnoreColumns != null && lstIgnoreColumns.Count > 0) { @@ -196,9 +176,9 @@ public async Task Update( { up = up.UpdateColumns(lstColumns.ToArray()); } - if (!string.IsNullOrEmpty(strWhere)) + if (!string.IsNullOrEmpty(where)) { - up = up.Where(strWhere); + up = up.Where(where); } return await up.ExecuteCommandHasChangeAsync(); } @@ -210,8 +190,6 @@ public async Task Update( /// public async Task Delete(TEntity entity) { - //var i = await Task.Run(() => _db.Deleteable(entity).ExecuteCommand()); - //return i > 0; return await _db.Deleteable(entity).ExecuteCommandHasChangeAsync(); } @@ -222,8 +200,6 @@ public async Task Delete(TEntity entity) /// public async Task DeleteById(object id) { - //var i = await Task.Run(() => _db.Deleteable(id).ExecuteCommand()); - //return i > 0; return await _db.Deleteable(id).ExecuteCommandHasChangeAsync(); } @@ -234,8 +210,6 @@ public async Task DeleteById(object id) /// public async Task DeleteByIds(object[] ids) { - //var i = await Task.Run(() => _db.Deleteable().In(ids).ExecuteCommand()); - //return i > 0; return await _db.Deleteable().In(ids).ExecuteCommandHasChangeAsync(); } @@ -255,12 +229,11 @@ public async Task> Query() /// 功能描述:查询数据列表 /// 作  者:Blog.Core /// - /// 条件 + /// 条件 /// 数据列表 - public async Task> Query(string strWhere) + public async Task> Query(string where) { - //return await Task.Run(() => _db.Queryable().WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToList()); - return await _db.Queryable().WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToListAsync(); + return await _db.Queryable().WhereIF(!string.IsNullOrEmpty(where), where).ToListAsync(); } /// @@ -293,11 +266,11 @@ public async Task> Query(Expression /// 过滤条件 /// 查询实体条件 - /// 排序条件 + /// 排序条件 /// - public async Task> Query(Expression> expression, Expression> whereExpression, string strOrderByFileds) + public async Task> Query(Expression> expression, Expression> whereExpression, string orderByFields) { - return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).Select(expression).ToListAsync(); + return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields).WhereIF(whereExpression != null, whereExpression).Select(expression).ToListAsync(); } /// @@ -305,12 +278,11 @@ public async Task> Query(Expression /// 条件表达式 - /// 排序字段,如name asc,age desc + /// 排序字段,如name asc,age desc /// 数据列表 - public async Task> Query(Expression> whereExpression, string strOrderByFileds) + public async Task> Query(Expression> whereExpression, string orderByFields) { - //return await Task.Run(() => _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).ToList()); - return await _db.Queryable().WhereIF(whereExpression != null, whereExpression).OrderByIF(strOrderByFileds != null, strOrderByFileds).ToListAsync(); + return await _db.Queryable().WhereIF(whereExpression != null, whereExpression).OrderByIF(orderByFields != null, orderByFields).ToListAsync(); } /// /// 功能描述:查询一个列表 @@ -329,13 +301,12 @@ public async Task> Query(Expression> whereExpr /// 功能描述:查询一个列表 /// 作  者:Blog.Core /// - /// 条件 - /// 排序字段,如name asc,age desc + /// 条件 + /// 排序字段,如name asc,age desc /// 数据列表 - public async Task> Query(string strWhere, string strOrderByFileds) + public async Task> Query(string where, string orderByFields) { - //return await Task.Run(() => _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToList()); - return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToListAsync(); + return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields).WhereIF(!string.IsNullOrEmpty(where), where).ToListAsync(); } @@ -344,55 +315,53 @@ public async Task> Query(string strWhere, string strOrderByFileds) /// 作  者:Blog.Core /// /// 条件表达式 - /// 前N条 - /// 排序字段,如name asc,age desc + /// 前N条 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( Expression> whereExpression, - int intTop, - string strOrderByFileds) + int top, + string orderByFields) { - //return await Task.Run(() => _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).Take(intTop).ToList()); - return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).Take(intTop).ToListAsync(); + return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields).WhereIF(whereExpression != null, whereExpression).Take(top).ToListAsync(); } /// /// 功能描述:查询前N条数据 /// 作  者:Blog.Core /// - /// 条件 - /// 前N条 - /// 排序字段,如name asc,age desc + /// 条件 + /// 前N条 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string strWhere, - int intTop, - string strOrderByFileds) + string where, + int top, + string orderByFields) { - //return await Task.Run(() => _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).Take(intTop).ToList()); - return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).Take(intTop).ToListAsync(); + return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields).WhereIF(!string.IsNullOrEmpty(where), where).Take(top).ToListAsync(); } /// /// 根据sql语句查询 /// - /// 完整的sql语句 + /// 完整的sql语句 /// 参数 /// 泛型集合 - public async Task> QuerySql(string strSql, SugarParameter[] parameters = null) + public async Task> QuerySql(string sql, SugarParameter[] parameters = null) { - return await _db.Ado.SqlQueryAsync(strSql, parameters); + return await _db.Ado.SqlQueryAsync(sql, parameters); } /// /// 根据sql语句查询 /// - /// 完整的sql语句 + /// 完整的sql语句 /// 参数 /// DataTable - public async Task QueryTable(string strSql, SugarParameter[] parameters = null) + public async Task QueryTable(string sql, SugarParameter[] parameters = null) { - return await _db.Ado.GetDataTableAsync(strSql, parameters); + return await _db.Ado.GetDataTableAsync(sql, parameters); } /// @@ -400,38 +369,38 @@ public async Task QueryTable(string strSql, SugarParameter[] paramete /// 作  者:Blog.Core /// /// 条件表达式 - /// 页码(下标0) - /// 页大小 - /// 排序字段,如name asc,age desc + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( Expression> whereExpression, - int intPageIndex, - int intPageSize, - string strOrderByFileds) + int pageIndex, + int pageSize, + string orderByFields) { - //return await Task.Run(() => _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).ToPageList(intPageIndex, intPageSize)); - return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).ToPageListAsync(intPageIndex, intPageSize); + return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression).ToPageListAsync(pageIndex, pageSize); } /// /// 功能描述:分页查询 /// 作  者:Blog.Core /// - /// 条件 - /// 页码(下标0) - /// 页大小 - /// 排序字段,如name asc,age desc + /// 条件 + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string strWhere, - int intPageIndex, - int intPageSize, + string where, + int pageIndex, + int pageSize, - string strOrderByFileds) + string orderByFields) { - //return await Task.Run(() => _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToPageList(intPageIndex, intPageSize)); - return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToPageListAsync(intPageIndex, intPageSize); + return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(!string.IsNullOrEmpty(where), where).ToPageListAsync(pageIndex, pageSize); } @@ -440,20 +409,20 @@ public async Task> Query( /// 分页查询[使用版本,其他分页未测试] /// /// 条件表达式 - /// 页码(下标0) - /// 页大小 - /// 排序字段,如name asc,age desc + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc /// - public async Task> QueryPage(Expression> whereExpression, int intPageIndex = 1, int intPageSize = 20, string strOrderByFileds = null) + public async Task> QueryPage(Expression> whereExpression, int pageIndex = 1, int pageSize = 20, string orderByFields = null) { RefAsync totalCount = 0; var list = await _db.Queryable() - .OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(intPageIndex, intPageSize, totalCount); + .ToPageListAsync(pageIndex, pageSize, totalCount); - return new PageModel(intPageIndex, totalCount, intPageSize, list); + return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -490,26 +459,26 @@ public async Task> QueryMuch( /// 关联表达式 /// 返回表达式 /// 查询表达式 - /// 页码 - /// 页大小 - /// 排序字段 + /// 页码 + /// 页大小 + /// 排序字段 /// public async Task> QueryTabsPage( Expression> joinExpression, Expression> selectExpression, Expression> whereExpression, - int intPageIndex = 1, - int intPageSize = 20, - string strOrderByFileds = null) + int pageIndex = 1, + int pageSize = 20, + string orderByFields = null) { RefAsync totalCount = 0; var list = await _db.Queryable(joinExpression) .Select(selectExpression) - .OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(intPageIndex, intPageSize, totalCount); - return new PageModel(intPageIndex, totalCount, intPageSize, list); + .ToPageListAsync(pageIndex, pageSize, totalCount); + return new PageModel(pageIndex, totalCount, pageSize, list); } /// @@ -521,27 +490,27 @@ public async Task> QueryTabsPage( /// 关联表达式 /// 返回表达式 /// 查询表达式 - /// 页码 - /// 页大小 - /// 排序字段 + /// group表达式 + /// 页码 + /// 页大小 + /// 排序字段 /// public async Task> QueryTabsPage( Expression> joinExpression, Expression> selectExpression, Expression> whereExpression, Expression> groupExpression, - int intPageIndex = 1, - int intPageSize = 20, - string strOrderByFileds = null) + int pageIndex = 1, + int pageSize = 20, + string orderByFields = null) { - RefAsync totalCount = 0; var list = await _db.Queryable(joinExpression).GroupBy(groupExpression) .Select(selectExpression) - .OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(intPageIndex, intPageSize, totalCount); - return new PageModel(intPageIndex, totalCount, intPageSize, list); + .ToPageListAsync(pageIndex, pageSize, totalCount); + return new PageModel(pageIndex, totalCount, pageSize, list); } //var exp = Expressionable.Create() diff --git a/Blog.Core.Repository/BASE/IBaseRepository.cs b/Blog.Core.Repository/BASE/IBaseRepository.cs index 1d74d9af..29783505 100644 --- a/Blog.Core.Repository/BASE/IBaseRepository.cs +++ b/Blog.Core.Repository/BASE/IBaseRepository.cs @@ -74,9 +74,9 @@ public interface IBaseRepository where TEntity : class /// 根据model,更新,带where条件 /// /// - /// + /// /// - Task Update(TEntity entity, string strWhere); + Task Update(TEntity entity, string where); Task Update(object operateAnonymousObjects); /// @@ -85,9 +85,9 @@ public interface IBaseRepository where TEntity : class /// /// /// - /// + /// /// - Task Update(TEntity entity, List lstColumns = null, List lstIgnoreColumns = null, string strWhere = ""); + Task Update(TEntity entity, List lstColumns = null, List lstIgnoreColumns = null, string where = ""); /// /// 查询 @@ -98,9 +98,9 @@ public interface IBaseRepository where TEntity : class /// /// 带sql where查询 /// - /// + /// /// - Task> Query(string strWhere); + Task> Query(string where); /// /// 根据表达式查询 @@ -123,31 +123,31 @@ public interface IBaseRepository where TEntity : class /// /// /// - /// + /// /// - Task> Query(Expression> expression, Expression> whereExpression, string strOrderByFileds); - Task> Query(Expression> whereExpression, string strOrderByFileds); + Task> Query(Expression> expression, Expression> whereExpression, string orderByFields); + Task> Query(Expression> whereExpression, string orderByFields); Task> Query(Expression> whereExpression, Expression> orderByExpression, bool isAsc = true); - Task> Query(string strWhere, string strOrderByFileds); + Task> Query(string where, string orderByFields); - Task> Query(Expression> whereExpression, int intTop, string strOrderByFileds); - Task> Query(string strWhere, int intTop, string strOrderByFileds); - Task> QuerySql(string strSql, SugarParameter[] parameters = null); - Task QueryTable(string strSql, SugarParameter[] parameters = null); + Task> Query(Expression> whereExpression, int intTop, string orderByFields); + Task> Query(string where, int intTop, string orderByFields); + Task> QuerySql(string sql, SugarParameter[] parameters = null); + Task QueryTable(string sql, SugarParameter[] parameters = null); Task> Query( - Expression> whereExpression, int intPageIndex, int intPageSize, string strOrderByFileds); - Task> Query(string strWhere, int intPageIndex, int intPageSize, string strOrderByFileds); + Expression> whereExpression, int pageIndex, int pageSize, string orderByFields); + Task> Query(string where, int pageIndex, int pageSize, string orderByFields); /// /// 根据表达式,排序字段,分页查询 /// /// - /// - /// - /// + /// + /// + /// /// - Task> QueryPage(Expression> whereExpression, int intPageIndex = 1, int intPageSize = 20, string strOrderByFileds = null); + Task> QueryPage(Expression> whereExpression, int pageIndex = 1, int pageSize = 20, string orderByFields = null); /// /// 三表联查 @@ -174,17 +174,17 @@ Task> QueryMuch( /// /// /// - /// - /// - /// + /// + /// + /// /// Task> QueryTabsPage( Expression> joinExpression, Expression> selectExpression, Expression> whereExpression, - int intPageIndex = 1, - int intPageSize = 20, - string strOrderByFileds = null); + int pageIndex = 1, + int pageSize = 20, + string orderByFields = null); /// /// 两表联合查询-分页-分组 @@ -196,17 +196,17 @@ Task> QueryTabsPage( /// /// /// - /// - /// - /// + /// + /// + /// /// Task> QueryTabsPage( Expression> joinExpression, Expression> selectExpression, Expression> whereExpression, Expression> groupExpression, - int intPageIndex = 1, - int intPageSize = 20, - string strOrderByFileds = null); + int pageIndex = 1, + int pageSize = 20, + string orderByFields = null); } } diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index d110b587..5db1efeb 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -72,9 +72,9 @@ public async Task Update(TEntity entity) { return await BaseDal.Update(entity); } - public async Task Update(TEntity entity, string strWhere) + public async Task Update(TEntity entity, string where) { - return await BaseDal.Update(entity, strWhere); + return await BaseDal.Update(entity, where); } public async Task Update(object operateAnonymousObjects) { @@ -85,10 +85,10 @@ public async Task Update( TEntity entity, List lstColumns = null, List lstIgnoreColumns = null, - string strWhere = "" + string where = "" ) { - return await BaseDal.Update(entity, lstColumns, lstIgnoreColumns, strWhere); + return await BaseDal.Update(entity, lstColumns, lstIgnoreColumns, where); } @@ -138,11 +138,11 @@ public async Task> Query() /// 功能描述:查询数据列表 /// 作  者:AZLinli.Blog.Core /// - /// 条件 + /// 条件 /// 数据列表 - public async Task> Query(string strWhere) + public async Task> Query(string where) { - return await BaseDal.Query(strWhere); + return await BaseDal.Query(where); } /// @@ -175,11 +175,11 @@ public async Task> Query(Expression /// 过滤条件 /// 查询实体条件 - /// 排序条件 + /// 排序条件 /// - public async Task> Query(Expression> expression, Expression> whereExpression,string strOrderByFileds) + public async Task> Query(Expression> expression, Expression> whereExpression,string orderByFileds) { - return await BaseDal.Query(expression, whereExpression, strOrderByFileds); + return await BaseDal.Query(expression, whereExpression, orderByFileds); } /// @@ -194,44 +194,44 @@ public async Task> Query(Expression> whereExpr return await BaseDal.Query(whereExpression, orderByExpression, isAsc); } - public async Task> Query(Expression> whereExpression, string strOrderByFileds) + public async Task> Query(Expression> whereExpression, string orderByFileds) { - return await BaseDal.Query(whereExpression, strOrderByFileds); + return await BaseDal.Query(whereExpression, orderByFileds); } /// /// 功能描述:查询一个列表 /// 作  者:AZLinli.Blog.Core /// - /// 条件 - /// 排序字段,如name asc,age desc + /// 条件 + /// 排序字段,如name asc,age desc /// 数据列表 - public async Task> Query(string strWhere, string strOrderByFileds) + public async Task> Query(string where, string orderByFileds) { - return await BaseDal.Query(strWhere, strOrderByFileds); + return await BaseDal.Query(where, orderByFileds); } /// /// 根据sql语句查询 /// - /// 完整的sql语句 + /// 完整的sql语句 /// 参数 /// 泛型集合 - public async Task> QuerySql(string strSql, SugarParameter[] parameters = null) + public async Task> QuerySql(string sql, SugarParameter[] parameters = null) { - return await BaseDal.QuerySql(strSql, parameters); + return await BaseDal.QuerySql(sql, parameters); } /// /// 根据sql语句查询 /// - /// 完整的sql语句 + /// 完整的sql语句 /// 参数 /// DataTable - public async Task QueryTable(string strSql, SugarParameter[] parameters = null) + public async Task QueryTable(string sql, SugarParameter[] parameters = null) { - return await BaseDal.QueryTable(strSql, parameters); + return await BaseDal.QueryTable(sql, parameters); } /// @@ -239,28 +239,28 @@ public async Task QueryTable(string strSql, SugarParameter[] paramete /// 作  者:AZLinli.Blog.Core /// /// 条件表达式 - /// 前N条 - /// 排序字段,如name asc,age desc + /// 前N条 + /// 排序字段,如name asc,age desc /// 数据列表 - public async Task> Query(Expression> whereExpression, int intTop, string strOrderByFileds) + public async Task> Query(Expression> whereExpression, int top, string orderByFileds) { - return await BaseDal.Query(whereExpression, intTop, strOrderByFileds); + return await BaseDal.Query(whereExpression, top, orderByFileds); } /// /// 功能描述:查询前N条数据 /// 作  者:AZLinli.Blog.Core /// - /// 条件 - /// 前N条 - /// 排序字段,如name asc,age desc + /// 条件 + /// 前N条 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string strWhere, - int intTop, - string strOrderByFileds) + string where, + int top, + string orderByFileds) { - return await BaseDal.Query(strWhere, intTop, strOrderByFileds); + return await BaseDal.Query(where, top, orderByFileds); } /// @@ -268,50 +268,50 @@ public async Task> Query( /// 作  者:AZLinli.Blog.Core /// /// 条件表达式 - /// 页码(下标0) - /// 页大小 - /// 排序字段,如name asc,age desc + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( Expression> whereExpression, - int intPageIndex, - int intPageSize, - string strOrderByFileds) + int pageIndex, + int pageSize, + string orderByFileds) { return await BaseDal.Query( whereExpression, - intPageIndex, - intPageSize, - strOrderByFileds); + pageIndex, + pageSize, + orderByFileds); } /// /// 功能描述:分页查询 /// 作  者:AZLinli.Blog.Core /// - /// 条件 - /// 页码(下标0) - /// 页大小 - /// 排序字段,如name asc,age desc + /// 条件 + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string strWhere, - int intPageIndex, - int intPageSize, - string strOrderByFileds) + string where, + int pageIndex, + int pageSize, + string orderByFileds) { return await BaseDal.Query( - strWhere, - intPageIndex, - intPageSize, - strOrderByFileds); + where, + pageIndex, + pageSize, + orderByFileds); } public async Task> QueryPage(Expression> whereExpression, - int intPageIndex = 1, int intPageSize = 20, string strOrderByFileds = null) + int pageIndex = 1, int pageSize = 20, string orderByFileds = null) { return await BaseDal.QueryPage(whereExpression, - intPageIndex, intPageSize, strOrderByFileds); + pageIndex, pageSize, orderByFileds); } public async Task> QueryMuch(Expression> joinExpression, Expression> selectExpression, Expression> whereLambda = null) where T : class, new() @@ -320,8 +320,8 @@ public async Task> QueryPage(Expression> } public async Task> QueryPage(PaginationModel pagination) { - var express = DynamicLinqFactory.CreateLambda(pagination.conditions); - return await QueryPage(express, pagination.intPageIndex, pagination.intPageSize, pagination.strOrderByFileds); + var express = DynamicLinqFactory.CreateLambda(pagination.Conditions); + return await QueryPage(express, pagination.PageIndex, pagination.PageSize, pagination.OrderByFileds); } } diff --git a/Blog.Core.sln b/Blog.Core.sln index 1d40c8a8..c8f61505 100644 --- a/Blog.Core.sln +++ b/Blog.Core.sln @@ -51,8 +51,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EventBus", "EventBus", "{A5 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blog.Core.EventBus", "Blog.Core.EventBus\Blog.Core.EventBus.csproj", "{17C9E9DC-E926-4C90-9025-3DAC55D7EDA3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blog.Core.ConsoleApp", "Blog.Core.ConsoleApp\Blog.Core.ConsoleApp.csproj", "{0B3265A9-6716-4D28-8648-C64D5E692ACA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blog.Core.Gateway", "Blog.Core.Gateway\Blog.Core.Gateway.csproj", "{A11C0DF2-1E13-4EED-BA49-44A57136B189}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blog.Core.Serilog.Es", "Blog.Core.Serilog.Es\Blog.Core.Serilog.Es.csproj", "{52AFAB53-D1CA-4014-8B63-3550FDCDA6E1}" @@ -109,10 +107,6 @@ Global {17C9E9DC-E926-4C90-9025-3DAC55D7EDA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {17C9E9DC-E926-4C90-9025-3DAC55D7EDA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {17C9E9DC-E926-4C90-9025-3DAC55D7EDA3}.Release|Any CPU.Build.0 = Release|Any CPU - {0B3265A9-6716-4D28-8648-C64D5E692ACA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B3265A9-6716-4D28-8648-C64D5E692ACA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B3265A9-6716-4D28-8648-C64D5E692ACA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B3265A9-6716-4D28-8648-C64D5E692ACA}.Release|Any CPU.Build.0 = Release|Any CPU {A11C0DF2-1E13-4EED-BA49-44A57136B189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A11C0DF2-1E13-4EED-BA49-44A57136B189}.Debug|Any CPU.Build.0 = Debug|Any CPU {A11C0DF2-1E13-4EED-BA49-44A57136B189}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -133,7 +127,6 @@ Global {300A8113-8033-4184-BE28-FC48D8349CD0} = {EDA8901E-541E-4ADC-B71E-59697D5F9549} {52D318A2-F44E-4CB7-8DD4-483357D4333F} = {047A9723-9AAC-42E3-8C69-B3835F15FF96} {17C9E9DC-E926-4C90-9025-3DAC55D7EDA3} = {A592C96A-4E44-4F2A-AC21-30683AF6C493} - {0B3265A9-6716-4D28-8648-C64D5E692ACA} = {047A9723-9AAC-42E3-8C69-B3835F15FF96} {A11C0DF2-1E13-4EED-BA49-44A57136B189} = {E2BD7D4D-9ED5-41CD-8401-C3FB26F203BB} {6463FB13-5F01-4A1D-8B62-A454FB3812EB} = {E2BD7D4D-9ED5-41CD-8401-C3FB26F203BB} EndGlobalSection From 43a21517a7d182d1877ae8428a6ac2671bb38128 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 14 Mar 2022 16:05:54 +0800 Subject: [PATCH 013/289] fix: rename the middlewares . #236 --- .../Controllers/MonitorController.cs | 2 +- Blog.Core.Api/Startup.cs | 26 +++++++++--------- Blog.Core.Api/skyapm.json | 2 +- ...vicesMildd.cs => AllServicesMiddleware.cs} | 17 ++++++------ ...assAuthMidd.cs => ByPassAuthMiddleware.cs} | 10 +++---- .../{ConsulMildd.cs => ConsulMiddleware.cs} | 10 +++---- ...rMidd.cs => ExceptionHandlerMiddleware.cs} | 20 +++++++------- .../{IpLimitMildd.cs => IpLimitMiddleware.cs} | 14 +++++----- .../{IPLogMildd.cs => IpLogMiddleware.cs} | 15 ++++++----- ...TokenAuth.cs => JwtTokenAuthMiddleware.cs} | 10 +++---- .../Middlewares/MiddlewareHelpers.cs | 27 +++++++++---------- ...ilerMildd.cs => MiniProfilerMiddleware.cs} | 14 +++++----- ...artzJobMildd.cs => QuartzJobMiddleware.cs} | 20 +++++++------- ...Mildd.cs => RecordAccessLogsMiddleware.cs} | 24 ++++++++--------- ...spLogMildd.cs => RequRespLogMiddleware.cs} | 26 +++++++++--------- ...SeedDataMildd.cs => SeedDataMiddleware.cs} | 14 +++++----- ...RSendMildd.cs => SignalRSendMiddleware.cs} | 12 ++++----- ...rAuthMildd.cs => SwaggerAuthMiddleware.cs} | 16 +++++------ .../{SwaggerMildd.cs => SwaggerMiddleware.cs} | 24 ++++++++--------- README.md | 4 +++ 20 files changed, 154 insertions(+), 153 deletions(-) rename Blog.Core.Extensions/Middlewares/{AllServicesMildd.cs => AllServicesMiddleware.cs} (87%) rename Blog.Core.Extensions/Middlewares/{ByPassAuthMidd.cs => ByPassAuthMiddleware.cs} (95%) rename Blog.Core.Extensions/Middlewares/{ConsulMildd.cs => ConsulMiddleware.cs} (87%) rename Blog.Core.Extensions/Middlewares/{ExceptionHandlerMidd.cs => ExceptionHandlerMiddleware.cs} (81%) rename Blog.Core.Extensions/Middlewares/{IpLimitMildd.cs => IpLimitMiddleware.cs} (60%) rename Blog.Core.Extensions/Middlewares/{IPLogMildd.cs => IpLogMiddleware.cs} (93%) rename Blog.Core.Extensions/Middlewares/{JwtTokenAuth.cs => JwtTokenAuthMiddleware.cs} (92%) rename Blog.Core.Extensions/Middlewares/{MiniProfilerMildd.cs => MiniProfilerMiddleware.cs} (61%) rename Blog.Core.Extensions/Middlewares/{QuartzJobMildd.cs => QuartzJobMiddleware.cs} (64%) rename Blog.Core.Extensions/Middlewares/{RecordAccessLogsMildd.cs => RecordAccessLogsMiddleware.cs} (92%) rename Blog.Core.Extensions/Middlewares/{RequRespLogMildd.cs => RequRespLogMiddleware.cs} (86%) rename Blog.Core.Extensions/Middlewares/{SeedDataMildd.cs => SeedDataMiddleware.cs} (62%) rename Blog.Core.Extensions/Middlewares/{SignalRSendMildd.cs => SignalRSendMiddleware.cs} (79%) rename Blog.Core.Extensions/Middlewares/{SwaggerAuthMildd.cs => SwaggerAuthMiddleware.cs} (87%) rename Blog.Core.Extensions/Middlewares/{SwaggerMildd.cs => SwaggerMiddleware.cs} (76%) diff --git a/Blog.Core.Api/Controllers/MonitorController.cs b/Blog.Core.Api/Controllers/MonitorController.cs index db170fb0..f725d4e3 100644 --- a/Blog.Core.Api/Controllers/MonitorController.cs +++ b/Blog.Core.Api/Controllers/MonitorController.cs @@ -3,7 +3,6 @@ using Blog.Core.Common.LogHelper; using Blog.Core.Hubs; using Blog.Core.IServices; -using Blog.Core.Middlewares; using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; @@ -20,6 +19,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Blog.Core.Extensions.Middlewares; namespace Blog.Core.Controllers { diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index 2e0d6a08..ea815687 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -6,7 +6,6 @@ using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; -using Blog.Core.Middlewares; using Blog.Core.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -23,6 +22,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Text; +using Blog.Core.Extensions.Middlewares; namespace Blog.Core { @@ -149,17 +149,17 @@ public void ConfigureContainer(ContainerBuilder builder) public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContext myContext, ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IHostApplicationLifetime lifetime) { // Ip限流,尽量放管道外层 - app.UseIpLimitMildd(); + app.UseIpLimitMiddle(); // 记录请求与返回数据 - app.UseReuestResponseLog(); + app.UseRequestResponseLogMiddle(); // 用户访问记录(必须放到外层,不然如果遇到异常,会报错,因为不能返回流) - app.UseRecordAccessLogsMildd(); + app.UseRecordAccessLogsMiddle(); // signalr - app.UseSignalRSendMildd(); + app.UseSignalRSendMiddle(); // 记录ip请求 - app.UseIPLogMildd(); + app.UseIpLogMiddle(); // 查看注入的所有服务 - app.UseAllServicesMildd(_services); + app.UseAllServicesMiddle(_services); if (env.IsDevelopment()) { @@ -177,7 +177,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex app.UseSession(); app.UseSwaggerAuthorized(); // 封装Swagger展示 - app.UseSwaggerMildd(() => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Blog.Core.Api.index.html")); + app.UseSwaggerMiddle(() => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Blog.Core.Api.index.html")); // ↓↓↓↓↓↓ 注意下边这些中间件的顺序,很重要 ↓↓↓↓↓↓ @@ -203,14 +203,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex // 测试用户,用来通过鉴权 if (Configuration.GetValue("AppSettings:UseLoadTest")) { - app.UseMiddleware(); + app.UseMiddleware(); } // 先开启认证 app.UseAuthentication(); // 然后是授权中间件 app.UseAuthorization(); //开启性能分析 - app.UseMiniProfilerMildd(); + app.UseMiniProfilerMiddleware(); // 开启异常中间件,要放到最后 //app.UseExceptionHandlerMidd(); @@ -225,11 +225,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex }); // 生成种子数据 - app.UseSeedDataMildd(myContext, Env.WebRootPath); + app.UseSeedDataMiddle(myContext, Env.WebRootPath); // 开启QuartzNetJob调度服务 - app.UseQuartzJobMildd(tasksQzServices, schedulerCenter); + app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); // 服务注册 - app.UseConsulMildd(Configuration, lifetime); + app.UseConsulMiddle(Configuration, lifetime); // 事件总线,订阅服务 app.ConfigureEventBus(); diff --git a/Blog.Core.Api/skyapm.json b/Blog.Core.Api/skyapm.json index 357eb12d..cd5ed0ee 100644 --- a/Blog.Core.Api/skyapm.json +++ b/Blog.Core.Api/skyapm.json @@ -11,7 +11,7 @@ }, "Logging": { "Level": "Information", - "FilePath": "logs/skyapm-{Date}.log" + "FilePath": "Log/skyapm-{Date}.log" }, "Transport": { "Interval": 3000, diff --git a/Blog.Core.Extensions/Middlewares/AllServicesMildd.cs b/Blog.Core.Extensions/Middlewares/AllServicesMiddleware.cs similarity index 87% rename from Blog.Core.Extensions/Middlewares/AllServicesMildd.cs rename to Blog.Core.Extensions/Middlewares/AllServicesMiddleware.cs index e92211e5..2346967c 100644 --- a/Blog.Core.Extensions/Middlewares/AllServicesMildd.cs +++ b/Blog.Core.Extensions/Middlewares/AllServicesMiddleware.cs @@ -1,19 +1,18 @@ -using Autofac.Extensions.DependencyInjection; +using System; +using System.Linq; +using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using System; -using System.IO; -using System.Linq; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// 查看所有注入的服务 /// - public static class AllServicesMildd + public static class AllServicesMiddleware { - public static void UseAllServicesMildd(this IApplicationBuilder app, IServiceCollection _services) + public static void UseAllServicesMiddle(this IApplicationBuilder app, IServiceCollection _services) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -21,7 +20,7 @@ public static void UseAllServicesMildd(this IApplicationBuilder app, IServiceCol //tsDIAutofac.AddRange(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "Blog.Core.Services.dll")).GetTypes().ToList()); //tsDIAutofac.AddRange(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "Blog.Core.Repository.dll")).GetTypes().ToList()); - var autofacContaniers = (app.ApplicationServices.GetAutofacRoot())?.ComponentRegistry?.Registrations; + var autofacContainers = (app.ApplicationServices.GetAutofacRoot())?.ComponentRegistry?.Registrations; app.Map("/allservices", builder => builder.Run(async context => @@ -39,7 +38,7 @@ public static void UseAllServicesMildd(this IApplicationBuilder app, IServiceCol await context.Response.WriteAsync($"{svc.ImplementationType?.Name}"); await context.Response.WriteAsync(""); } - foreach (var item in autofacContaniers.ToList()) + foreach (var item in autofacContainers.ToList()) { var interfaceType = item.Services; foreach (var typeArray in interfaceType) diff --git a/Blog.Core.Extensions/Middlewares/ByPassAuthMidd.cs b/Blog.Core.Extensions/Middlewares/ByPassAuthMiddleware.cs similarity index 95% rename from Blog.Core.Extensions/Middlewares/ByPassAuthMidd.cs rename to Blog.Core.Extensions/Middlewares/ByPassAuthMiddleware.cs index d9a36089..c1f1f4a5 100644 --- a/Blog.Core.Extensions/Middlewares/ByPassAuthMidd.cs +++ b/Blog.Core.Extensions/Middlewares/ByPassAuthMiddleware.cs @@ -1,22 +1,22 @@ -using Microsoft.AspNetCore.Http; -using System; +using System; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { /// /// 测试用户,用来通过鉴权 /// JWT:?userid=8&rolename=AdminTest /// - public class ByPassAuthMidd + public class ByPassAuthMiddleware { private readonly RequestDelegate _next; // 定义变量:当前用户Id,会常驻内存。 private string _currentUserId; // 同理定义:当前角色名 private string _currentRoleName; - public ByPassAuthMidd(RequestDelegate next) + public ByPassAuthMiddleware(RequestDelegate next) { _next = next; _currentUserId = null; diff --git a/Blog.Core.Extensions/Middlewares/ConsulMildd.cs b/Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs similarity index 87% rename from Blog.Core.Extensions/Middlewares/ConsulMildd.cs rename to Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs index 428c778b..c1aae8cc 100644 --- a/Blog.Core.Extensions/Middlewares/ConsulMildd.cs +++ b/Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs @@ -1,17 +1,17 @@ -using Consul; +using System; +using Consul; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using System; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// Consul 注册服务 /// - public static class ConsulMildd + public static class ConsulMiddleware { - public static IApplicationBuilder UseConsulMildd(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime) + public static IApplicationBuilder UseConsulMiddle(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime) { if (configuration["Middleware:Consul:Enabled"].ObjToBool()) { diff --git a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMidd.cs b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs similarity index 81% rename from Blog.Core.Extensions/Middlewares/ExceptionHandlerMidd.cs rename to Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs index 056bc6c2..85d96e9b 100644 --- a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMidd.cs +++ b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs @@ -1,19 +1,19 @@ -using Blog.Core.Model; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using System; +using System; using System.Net; using System.Threading.Tasks; +using Blog.Core.Model; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { - public class ExceptionHandlerMidd + public class ExceptionHandlerMiddleware { private readonly RequestDelegate _next; - private static readonly log4net.ILog log = - log4net.LogManager.GetLogger(typeof(ExceptionHandlerMidd)); + private static readonly log4net.ILog Log = + log4net.LogManager.GetLogger(typeof(ExceptionHandlerMiddleware)); - public ExceptionHandlerMidd(RequestDelegate next) + public ExceptionHandlerMiddleware(RequestDelegate next) { _next = next; } @@ -34,7 +34,7 @@ private async Task HandleExceptionAsync(HttpContext context, Exception e) { if (e == null) return; - log.Error(e.GetBaseException().ToString()); + Log.Error(e.GetBaseException().ToString()); await WriteExceptionAsync(context, e).ConfigureAwait(false); } diff --git a/Blog.Core.Extensions/Middlewares/IpLimitMildd.cs b/Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs similarity index 60% rename from Blog.Core.Extensions/Middlewares/IpLimitMildd.cs rename to Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs index 591bfb16..1a1ab344 100644 --- a/Blog.Core.Extensions/Middlewares/IpLimitMildd.cs +++ b/Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs @@ -1,18 +1,18 @@ -using AspNetCoreRateLimit; +using System; +using AspNetCoreRateLimit; using Blog.Core.Common; using log4net; using Microsoft.AspNetCore.Builder; -using System; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// ip 限流 /// - public static class IpLimitMildd + public static class IpLimitMiddleware { - private static readonly ILog log = LogManager.GetLogger(typeof(IpLimitMildd)); - public static void UseIpLimitMildd(this IApplicationBuilder app) + private static readonly ILog Log = LogManager.GetLogger(typeof(IpLimitMiddleware)); + public static void UseIpLimitMiddle(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -25,7 +25,7 @@ public static void UseIpLimitMildd(this IApplicationBuilder app) } catch (Exception e) { - log.Error($"Error occured limiting ip rate.\n{e.Message}"); + Log.Error($"Error occured limiting ip rate.\n{e.Message}"); throw; } } diff --git a/Blog.Core.Extensions/Middlewares/IPLogMildd.cs b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs similarity index 93% rename from Blog.Core.Extensions/Middlewares/IPLogMildd.cs rename to Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs index eda2e369..39b258cd 100644 --- a/Blog.Core.Extensions/Middlewares/IPLogMildd.cs +++ b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs @@ -1,31 +1,31 @@ -using Blog.Core.Common; +using System; +using System.Threading.Tasks; +using Blog.Core.Common; using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; -using System; -using System.Threading.Tasks; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { /// /// 中间件 /// 记录IP请求数据 /// - public class IPLogMildd + public class IpLogMiddleware { /// /// /// private readonly RequestDelegate _next; private readonly IWebHostEnvironment _environment; - private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(IPLogMildd)); + private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(IpLogMiddleware)); /// /// /// /// - public IPLogMildd(RequestDelegate next, IWebHostEnvironment environment) + public IpLogMiddleware(RequestDelegate next, IWebHostEnvironment environment) { _next = next; _environment = environment; @@ -83,6 +83,7 @@ public async Task InvokeAsync(HttpContext context) } catch (Exception) { + // ignored } } else diff --git a/Blog.Core.Extensions/Middlewares/JwtTokenAuth.cs b/Blog.Core.Extensions/Middlewares/JwtTokenAuthMiddleware.cs similarity index 92% rename from Blog.Core.Extensions/Middlewares/JwtTokenAuth.cs rename to Blog.Core.Extensions/Middlewares/JwtTokenAuthMiddleware.cs index 931ad8cb..76786f87 100644 --- a/Blog.Core.Extensions/Middlewares/JwtTokenAuth.cs +++ b/Blog.Core.Extensions/Middlewares/JwtTokenAuthMiddleware.cs @@ -1,16 +1,16 @@ -using Microsoft.AspNetCore.Http; -using System; +using System; using System.Threading.Tasks; using Blog.Core.AuthHelper.OverWrite; +using Microsoft.AspNetCore.Http; -namespace Blog.Core.AuthHelper +namespace Blog.Core.Extensions.Middlewares { /// /// 中间件 /// 原做为自定义授权中间件 /// 先做检查 header token的使用 /// - public class JwtTokenAuth + public class JwtTokenAuthMiddleware { /// /// @@ -20,7 +20,7 @@ public class JwtTokenAuth /// /// /// - public JwtTokenAuth(RequestDelegate next) + public JwtTokenAuthMiddleware(RequestDelegate next) { _next = next; } diff --git a/Blog.Core.Extensions/Middlewares/MiddlewareHelpers.cs b/Blog.Core.Extensions/Middlewares/MiddlewareHelpers.cs index 07069a4b..fa5c3835 100644 --- a/Blog.Core.Extensions/Middlewares/MiddlewareHelpers.cs +++ b/Blog.Core.Extensions/Middlewares/MiddlewareHelpers.cs @@ -1,7 +1,6 @@ -using Blog.Core.AuthHelper; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { public static class MiddlewareHelpers { @@ -12,7 +11,7 @@ public static class MiddlewareHelpers /// public static IApplicationBuilder UseJwtTokenAuth(this IApplicationBuilder app) { - return app.UseMiddleware(); + return app.UseMiddleware(); } /// @@ -20,9 +19,9 @@ public static IApplicationBuilder UseJwtTokenAuth(this IApplicationBuilder app) /// /// /// - public static IApplicationBuilder UseReuestResponseLog(this IApplicationBuilder app) + public static IApplicationBuilder UseRequestResponseLogMiddle(this IApplicationBuilder app) { - return app.UseMiddleware(); + return app.UseMiddleware(); } /// @@ -30,9 +29,9 @@ public static IApplicationBuilder UseReuestResponseLog(this IApplicationBuilder /// /// /// - public static IApplicationBuilder UseSignalRSendMildd(this IApplicationBuilder app) + public static IApplicationBuilder UseSignalRSendMiddle(this IApplicationBuilder app) { - return app.UseMiddleware(); + return app.UseMiddleware(); } /// @@ -40,9 +39,9 @@ public static IApplicationBuilder UseSignalRSendMildd(this IApplicationBuilder a /// /// /// - public static IApplicationBuilder UseExceptionHandlerMidd(this IApplicationBuilder app) + public static IApplicationBuilder UseExceptionHandlerMiddle(this IApplicationBuilder app) { - return app.UseMiddleware(); + return app.UseMiddleware(); } /// @@ -50,9 +49,9 @@ public static IApplicationBuilder UseExceptionHandlerMidd(this IApplicationBuild /// /// /// - public static IApplicationBuilder UseIPLogMildd(this IApplicationBuilder app) + public static IApplicationBuilder UseIpLogMiddle(this IApplicationBuilder app) { - return app.UseMiddleware(); + return app.UseMiddleware(); } /// @@ -60,9 +59,9 @@ public static IApplicationBuilder UseIPLogMildd(this IApplicationBuilder app) /// /// /// - public static IApplicationBuilder UseRecordAccessLogsMildd(this IApplicationBuilder app) + public static IApplicationBuilder UseRecordAccessLogsMiddle(this IApplicationBuilder app) { - return app.UseMiddleware(); + return app.UseMiddleware(); } } } diff --git a/Blog.Core.Extensions/Middlewares/MiniProfilerMildd.cs b/Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs similarity index 61% rename from Blog.Core.Extensions/Middlewares/MiniProfilerMildd.cs rename to Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs index 74030daf..e4d50ec8 100644 --- a/Blog.Core.Extensions/Middlewares/MiniProfilerMildd.cs +++ b/Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs @@ -1,17 +1,17 @@ -using Blog.Core.Common; +using System; +using Blog.Core.Common; using log4net; using Microsoft.AspNetCore.Builder; -using System; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// MiniProfiler性能分析 /// - public static class MiniProfilerMildd + public static class MiniProfilerMiddleware { - private static readonly ILog log = LogManager.GetLogger(typeof(MiniProfilerMildd)); - public static void UseMiniProfilerMildd(this IApplicationBuilder app) + private static readonly ILog Log = LogManager.GetLogger(typeof(MiniProfilerMiddleware)); + public static void UseMiniProfilerMiddleware(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -26,7 +26,7 @@ public static void UseMiniProfilerMildd(this IApplicationBuilder app) } catch (Exception e) { - log.Error($"An error was reported when starting the MiniProfilerMildd.\n{e.Message}"); + Log.Error($"An error was reported when starting the MiniProfilerMildd.\n{e.Message}"); throw; } } diff --git a/Blog.Core.Extensions/Middlewares/QuartzJobMildd.cs b/Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs similarity index 64% rename from Blog.Core.Extensions/Middlewares/QuartzJobMildd.cs rename to Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs index a64fdb2b..af269e80 100644 --- a/Blog.Core.Extensions/Middlewares/QuartzJobMildd.cs +++ b/Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs @@ -1,19 +1,19 @@ -using Blog.Core.Common; +using System; +using Blog.Core.Common; using Blog.Core.IServices; using Blog.Core.Tasks; using log4net; using Microsoft.AspNetCore.Builder; -using System; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// Quartz 启动服务 /// - public static class QuartzJobMildd + public static class QuartzJobMiddleware { - private static readonly ILog log = LogManager.GetLogger(typeof(QuartzJobMildd)); - public static void UseQuartzJobMildd(this IApplicationBuilder app, ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter) + private static readonly ILog Log = LogManager.GetLogger(typeof(QuartzJobMiddleware)); + public static void UseQuartzJobMiddleware(this IApplicationBuilder app, ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -27,14 +27,14 @@ public static void UseQuartzJobMildd(this IApplicationBuilder app, ITasksQzServi { if (item.IsStart) { - var ResuleModel = schedulerCenter.AddScheduleJobAsync(item).Result; - if (ResuleModel.success) + var result = schedulerCenter.AddScheduleJobAsync(item).Result; + if (result.success) { Console.WriteLine($"QuartzNetJob{item.Name}启动成功!"); } else { - Console.WriteLine($"QuartzNetJob{item.Name}启动失败!错误信息:{ResuleModel.msg}"); + Console.WriteLine($"QuartzNetJob{item.Name}启动失败!错误信息:{result.msg}"); } } } @@ -43,7 +43,7 @@ public static void UseQuartzJobMildd(this IApplicationBuilder app, ITasksQzServi } catch (Exception e) { - log.Error($"An error was reported when starting the job service.\n{e.Message}"); + Log.Error($"An error was reported when starting the job service.\n{e.Message}"); throw; } } diff --git a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMildd.cs b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs similarity index 92% rename from Blog.Core.Extensions/Middlewares/RecordAccessLogsMildd.cs rename to Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs index 4174a459..9785a3fc 100644 --- a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMildd.cs +++ b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs @@ -1,4 +1,10 @@ -using Blog.Core.Common; +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Blog.Core.Common; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; using Blog.Core.Common.LogHelper; @@ -6,27 +12,21 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using System.Web; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { /// /// 中间件 /// 记录用户方访问数据 /// - public class RecordAccessLogsMildd + public class RecordAccessLogsMiddleware { /// /// /// private readonly RequestDelegate _next; private readonly IUser _user; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IWebHostEnvironment _environment; private Stopwatch _stopwatch; @@ -34,7 +34,7 @@ public class RecordAccessLogsMildd /// /// /// - public RecordAccessLogsMildd(RequestDelegate next, IUser user, ILogger logger, IWebHostEnvironment environment) + public RecordAccessLogsMiddleware(RequestDelegate next, IUser user, ILogger logger, IWebHostEnvironment environment) { _next = next; _user = user; @@ -60,7 +60,7 @@ public async Task InvokeAsync(HttpContext context) userAccessModel.API = api; userAccessModel.User = _user.Name; - userAccessModel.IP = IPLogMildd.GetClientIP(context); + userAccessModel.IP = IpLogMiddleware.GetClientIP(context); userAccessModel.BeginTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); userAccessModel.RequestMethod = request.Method; userAccessModel.Agent = request.Headers["User-Agent"].ObjToString(); diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMildd.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs similarity index 86% rename from Blog.Core.Extensions/Middlewares/RequRespLogMildd.cs rename to Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index fa5a5e31..e69e7476 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMildd.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -1,31 +1,31 @@ -using Blog.Core.Common; -using Blog.Core.Common.LogHelper; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using System; +using System; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.Common.LogHelper; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { /// /// 中间件 /// 记录请求和响应数据 /// - public class RequRespLogMildd + public class RequRespLogMiddleware { /// /// /// private readonly RequestDelegate _next; - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// /// /// - public RequRespLogMildd(RequestDelegate next, ILogger logger) + public RequRespLogMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; @@ -105,20 +105,20 @@ private async Task RequestDataLog(HttpContext context) private void ResponseDataLog(HttpResponse response, MemoryStream ms) { ms.Position = 0; - var ResponseBody = new StreamReader(ms).ReadToEnd(); + var responseBody = new StreamReader(ms).ReadToEnd(); // 去除 Html var reg = "<[^>]+>"; - var isHtml = Regex.IsMatch(ResponseBody, reg); + var isHtml = Regex.IsMatch(responseBody, reg); - if (!string.IsNullOrEmpty(ResponseBody)) + if (!string.IsNullOrEmpty(responseBody)) { //Parallel.For(0, 1, e => //{ // LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); //}); - SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); + SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); } } } diff --git a/Blog.Core.Extensions/Middlewares/SeedDataMildd.cs b/Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs similarity index 62% rename from Blog.Core.Extensions/Middlewares/SeedDataMildd.cs rename to Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs index 0c64bc8e..34af1a87 100644 --- a/Blog.Core.Extensions/Middlewares/SeedDataMildd.cs +++ b/Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs @@ -1,18 +1,18 @@ -using Blog.Core.Common; +using System; +using Blog.Core.Common; using Blog.Core.Common.Seed; using log4net; using Microsoft.AspNetCore.Builder; -using System; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// 生成种子数据中间件服务 /// - public static class SeedDataMildd + public static class SeedDataMiddleware { - private static readonly ILog log = LogManager.GetLogger(typeof(SeedDataMildd)); - public static void UseSeedDataMildd(this IApplicationBuilder app, MyContext myContext, string webRootPath) + private static readonly ILog Log = LogManager.GetLogger(typeof(SeedDataMiddleware)); + public static void UseSeedDataMiddle(this IApplicationBuilder app, MyContext myContext, string webRootPath) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -25,7 +25,7 @@ public static void UseSeedDataMildd(this IApplicationBuilder app, MyContext myCo } catch (Exception e) { - log.Error($"Error occured seeding the Database.\n{e.Message}"); + Log.Error($"Error occured seeding the Database.\n{e.Message}"); throw; } } diff --git a/Blog.Core.Extensions/Middlewares/SignalRSendMildd.cs b/Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs similarity index 79% rename from Blog.Core.Extensions/Middlewares/SignalRSendMildd.cs rename to Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs index 9a02404d..ed96f430 100644 --- a/Blog.Core.Extensions/Middlewares/SignalRSendMildd.cs +++ b/Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs @@ -1,17 +1,17 @@ -using Microsoft.AspNetCore.Http; -using System.Threading.Tasks; +using System.Threading.Tasks; +using Blog.Core.Common; using Blog.Core.Common.LogHelper; using Blog.Core.Hubs; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; -using Blog.Core.Common; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { /// /// 中间件 /// SignalR发送数据 /// - public class SignalRSendMildd + public class SignalRSendMiddleware { /// /// @@ -24,7 +24,7 @@ public class SignalRSendMildd /// /// /// - public SignalRSendMildd(RequestDelegate next, IHubContext hubContext) + public SignalRSendMiddleware(RequestDelegate next, IHubContext hubContext) { _next = next; _hubContext = hubContext; diff --git a/Blog.Core.Extensions/Middlewares/SwaggerAuthMildd.cs b/Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs similarity index 87% rename from Blog.Core.Extensions/Middlewares/SwaggerAuthMildd.cs rename to Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs index 2aace753..1438588c 100644 --- a/Blog.Core.Extensions/Middlewares/SwaggerAuthMildd.cs +++ b/Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs @@ -1,18 +1,16 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using System; -using System.Net; -using System.Text; +using System.Net; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; -namespace Blog.Core.Middlewares +namespace Blog.Core.Extensions.Middlewares { - public class SwaggerAuthMildd + public class SwaggerAuthMiddleware { private readonly RequestDelegate next; - public SwaggerAuthMildd(RequestDelegate next) + public SwaggerAuthMiddleware(RequestDelegate next) { this.next = next; } @@ -72,7 +70,7 @@ public static class SwaggerAuthorizeExtensions { public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder) { - return builder.UseMiddleware(); + return builder.UseMiddleware(); } } } diff --git a/Blog.Core.Extensions/Middlewares/SwaggerMildd.cs b/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs similarity index 76% rename from Blog.Core.Extensions/Middlewares/SwaggerMildd.cs rename to Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs index a7409458..5188237a 100644 --- a/Blog.Core.Extensions/Middlewares/SwaggerMildd.cs +++ b/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs @@ -1,20 +1,20 @@ -using Blog.Core.Common; -using log4net; -using Microsoft.AspNetCore.Builder; -using System; +using System; using System.IO; using System.Linq; +using Blog.Core.Common; +using log4net; +using Microsoft.AspNetCore.Builder; using static Blog.Core.Extensions.CustomApiVersion; -namespace Blog.Core.Extensions +namespace Blog.Core.Extensions.Middlewares { /// /// Swagger中间件 /// - public static class SwaggerMildd + public static class SwaggerMiddleware { - private static readonly ILog log = LogManager.GetLogger(typeof(SwaggerMildd)); - public static void UseSwaggerMildd(this IApplicationBuilder app, Func streamHtml) + private static readonly ILog Log = LogManager.GetLogger(typeof(SwaggerMiddleware)); + public static void UseSwaggerMiddle(this IApplicationBuilder app, Func streamHtml) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -22,19 +22,19 @@ public static void UseSwaggerMildd(this IApplicationBuilder app, Func st app.UseSwaggerUI(c => { //根据版本名称倒序 遍历展示 - var ApiName = Appsettings.app(new string[] { "Startup", "ApiName" }); + var apiName = Appsettings.app(new string[] { "Startup", "ApiName" }); typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version => { - c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}"); + c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{apiName} {version}"); }); - c.SwaggerEndpoint($"https://petstore.swagger.io/v2/swagger.json", $"{ApiName} pet"); + c.SwaggerEndpoint($"https://petstore.swagger.io/v2/swagger.json", $"{apiName} pet"); // 将swagger首页,设置成我们自定义的页面,记得这个字符串的写法:{项目名.index.html} if (streamHtml.Invoke() == null) { var msg = "index.html的属性,必须设置为嵌入的资源"; - log.Error(msg); + Log.Error(msg); throw new Exception(msg); } c.IndexStream = streamHtml; diff --git a/README.md b/README.md index 2b3e7185..3341ae5d 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,10 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 可配合 Ids4 实现认证中心; +### 自定义(中间件/服务)启动图 +![系统架构图](https://img.neters.club/github/load-tool.png) + +   ## 给个星星! ⭐️ From 2bbc4279e5f982fdca4dfdea0b3238c78abed2cc Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Mon, 14 Mar 2022 16:43:06 +0800 Subject: [PATCH 014/289] Update README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3341ae5d..b73c7c83 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x +   #### Dokcer 快速启动 @@ -55,6 +56,14 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x -it -p 9291:9291 laozhangisphi/apkimg ``` + +  + +## 给个星星! ⭐️ +如果你喜欢这个项目或者它帮助你, 请给 Star~ +如果你的项目中借鉴了本项目,请稍微说明下[https://github.com/anjoy8/Blog.Core/issues/75](https://github.com/anjoy8/Blog.Core/issues/75),开源不易✨。 + + ### 功能与进度 @@ -113,9 +122,6 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x   -## 给个星星! ⭐️ -如果你喜欢这个项目或者它帮助你, 请给 Star~ -如果你的项目中借鉴了本项目,请稍微说明下[https://github.com/anjoy8/Blog.Core/issues/75](https://github.com/anjoy8/Blog.Core/issues/75),开源不易✨。 ## 贡献者们 From 7c43929729b5408c77a04b3ad88e4b01c3ad5790 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 15 Mar 2022 10:52:55 +0800 Subject: [PATCH 015/289] fix: format the code. #232 --- Blog.Core.Api/Blog.Core.Api.csproj | 2 +- Blog.Core.Api/Blog.Core.Model.xml | 28 +- Blog.Core.Api/Blog.Core.xml | 4 +- Blog.Core.Api/Controllers/LoginController.cs | 8 +- Blog.Core.Api/Controllers/UserController.cs | 32 +- .../Controllers/UserRoleController.cs | 4 +- .../BlogCore.Data.json/sysUserInfo.tsv | 1170 ++++++++--------- Blog.Core.Common/Seed/DBSeed.cs | 6 +- Blog.Core.IServices/IsysUserInfoServices.cs | 4 +- Blog.Core.Model/Models/OperateLog.cs | 2 +- .../Models/RootTkey/sysUserInfoRoot.cs | 4 +- Blog.Core.Model/Models/sysUserInfo.cs | 58 +- Blog.Core.Services/sysUserInfoServices.cs | 18 +- 13 files changed, 669 insertions(+), 671 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index b5725edd..a9499b1a 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -64,7 +64,7 @@ - + diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index d6c167e3..6aef9cdb 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -691,57 +691,57 @@ 修改时间 - + 用户信息表 - + 登录账号 - + 登录密码 - + 真实姓名 - + 状态 - + 备注 - + 创建时间 - + 更新时间 - + - 最后登录时间 + 最后异常时间 - + 错误次数 - + 登录账号 @@ -1445,12 +1445,12 @@ 泛型主键Tkey - + 用户信息表 - + uID 泛型主键Tkey diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index eb1ea542..ad1d8a9e 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -660,14 +660,14 @@ 令牌 - + 添加一个用户 - + 更新用户与角色 diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 52a7a8b7..63ab1430 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -152,14 +152,14 @@ public async Task> GetJwtToken3(string name = " pass = MD5Helper.MD5Encrypt32(pass); - var user = await _sysUserInfoServices.Query(d => d.uLoginName == name && d.uLoginPWD == pass && d.tdIsDelete == false); + var user = await _sysUserInfoServices.Query(d => d.LoginName == name && d.LoginPWD == pass && d.IsDeleted == false); if (user.Count > 0) { var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 var claims = new List { new Claim(ClaimTypes.Name, name), - new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().uID.ToString()), + new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); @@ -209,10 +209,10 @@ public async Task> RefreshToken(string token = var user = await _sysUserInfoServices.QueryById(tokenModel.Uid); if (user != null) { - var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.uLoginName, user.uLoginPWD); + var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 var claims = new List { - new Claim(ClaimTypes.Name, user.uLoginName), + new Claim(ClaimTypes.Name, user.LoginName), new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index fa37fd1a..2e233550 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -57,7 +57,7 @@ public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoSe /// // GET: api/User [HttpGet] - public async Task>> Get(int page = 1, string key = "") + public async Task>> Get(int page = 1, string key = "") { if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { @@ -66,7 +66,7 @@ public async Task>> Get(int page = 1, string int intPageSize = 50; - var data = await _sysUserInfoServices.QueryPage(a => a.tdIsDelete != true && a.uStatus >= 0 && ((a.uLoginName != null && a.uLoginName.Contains(key)) || (a.uRealName != null && a.uRealName.Contains(key))), page, intPageSize, " uID desc "); + var data = await _sysUserInfoServices.QueryPage(a => a.IsDeleted != true && a.Status >= 0 && ((a.LoginName != null && a.LoginName.Contains(key)) || (a.RealName != null && a.RealName.Contains(key))), page, intPageSize, " uID desc "); #region MyRegion @@ -78,7 +78,7 @@ public async Task>> Get(int page = 1, string var sysUserInfos = data.data; foreach (var item in sysUserInfos) { - var currentUserRoles = allUserRoles.Where(d => d.UserId == item.uID).Select(d => d.RoleId).ToList(); + var currentUserRoles = allUserRoles.Where(d => d.UserId == item.Id).Select(d => d.RoleId).ToList(); item.RIDs = currentUserRoles; item.RoleNames = allRoles.Where(d => currentUserRoles.Contains(d.Id)).Select(d => d.Name).ToList(); } @@ -87,7 +87,7 @@ public async Task>> Get(int page = 1, string #endregion - return new MessageModel>() + return new MessageModel>() { msg = "获取成功", success = data.dataCount >= 0, @@ -114,9 +114,9 @@ public string Get(string id) /// [HttpGet] [AllowAnonymous] - public async Task> GetInfoByToken(string token) + public async Task> GetInfoByToken(string token) { - var data = new MessageModel(); + var data = new MessageModel(); if (!string.IsNullOrEmpty(token)) { var tokenModel = JwtHelper.SerializeJwt(token); @@ -142,12 +142,12 @@ public async Task> GetInfoByToken(string token) /// // POST: api/User [HttpPost] - public async Task> Post([FromBody] sysUserInfo sysUserInfo) + public async Task> Post([FromBody] SysUserInfo sysUserInfo) { var data = new MessageModel(); - sysUserInfo.uLoginPWD = MD5Helper.MD5Encrypt32(sysUserInfo.uLoginPWD); - sysUserInfo.uRemark = _user.Name; + sysUserInfo.LoginPWD = MD5Helper.MD5Encrypt32(sysUserInfo.LoginPWD); + sysUserInfo.Remark = _user.Name; var id = await _sysUserInfoServices.Add(sysUserInfo); data.success = id > 0; @@ -167,7 +167,7 @@ public async Task> Post([FromBody] sysUserInfo sysUserInfo) /// // PUT: api/User/5 [HttpPut] - public async Task> Put([FromBody] sysUserInfo sysUserInfo) + public async Task> Put([FromBody] SysUserInfo sysUserInfo) { // 这里使用事务处理 @@ -176,12 +176,12 @@ public async Task> Put([FromBody] sysUserInfo sysUserInfo) { _unitOfWork.BeginTran(); - if (sysUserInfo != null && sysUserInfo.uID > 0) + if (sysUserInfo != null && sysUserInfo.Id > 0) { if (sysUserInfo.RIDs.Count > 0) { // 无论 Update Or Add , 先删除当前用户的全部 U_R 关系 - var usreroles = (await _userRoleServices.Query(d => d.UserId == sysUserInfo.uID)).Select(d => d.Id.ToString()).ToArray(); + var usreroles = (await _userRoleServices.Query(d => d.UserId == sysUserInfo.Id)).Select(d => d.Id.ToString()).ToArray(); if (usreroles.Count() > 0) { var isAllDeleted = await _userRoleServices.DeleteByIds(usreroles); @@ -191,7 +191,7 @@ public async Task> Put([FromBody] sysUserInfo sysUserInfo) var userRolsAdd = new List(); sysUserInfo.RIDs.ForEach(rid => { - userRolsAdd.Add(new UserRole(sysUserInfo.uID, rid)); + userRolsAdd.Add(new UserRole(sysUserInfo.Id, rid)); }); await _userRoleServices.Add(userRolsAdd); @@ -205,7 +205,7 @@ public async Task> Put([FromBody] sysUserInfo sysUserInfo) if (data.success) { data.msg = "更新成功"; - data.response = sysUserInfo?.uID.ObjToString(); + data.response = sysUserInfo?.Id.ObjToString(); } } } @@ -231,12 +231,12 @@ public async Task> Delete(int id) if (id > 0) { var userDetail = await _sysUserInfoServices.QueryById(id); - userDetail.tdIsDelete = true; + userDetail.IsDeleted = true; data.success = await _sysUserInfoServices.Update(userDetail); if (data.success) { data.msg = "删除成功"; - data.response = userDetail?.uID.ObjToString(); + data.response = userDetail?.Id.ObjToString(); } } diff --git a/Blog.Core.Api/Controllers/UserRoleController.cs b/Blog.Core.Api/Controllers/UserRoleController.cs index 5225fbdc..d36fef55 100644 --- a/Blog.Core.Api/Controllers/UserRoleController.cs +++ b/Blog.Core.Api/Controllers/UserRoleController.cs @@ -42,9 +42,9 @@ public UserRoleController(ISysUserInfoServices sysUserInfoServices, IUserRoleSer /// /// [HttpGet] - public async Task> AddUser(string loginName, string loginPwd) + public async Task> AddUser(string loginName, string loginPwd) { - return new MessageModel() + return new MessageModel() { success = true, msg = "添加成功", diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/sysUserInfo.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/sysUserInfo.tsv index cdd73aee..4eb742bd 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/sysUserInfo.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/sysUserInfo.tsv @@ -1,704 +1,704 @@ [ { "Id": 1, - "uLoginName": "laozhang", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "老张", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": "老张的哲学", - "sex": 1, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "laozhang", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "老张", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": "老张的哲学", + "Sex": 1, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 2, - "uLoginName": "laoli", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "laoli", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 1, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "laoli", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "laoli", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 1, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 3, - "uLoginName": "user", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "userli", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": "广告", - "sex": 1, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "user", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "userli", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": "广告", + "Sex": 1, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 4, - "uLoginName": "admins", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "admins", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 5, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 6, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 7, - "uLoginName": "tibug", - "uLoginPWD": "BB1C0516F0F4469549CD4A95833A78E5", - "uRealName": "提bug账号", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 1, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "tibug", + "LoginPWD": "BB1C0516F0F4469549CD4A95833A78E5", + "RealName": "提bug账号", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 1, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 8, - "uLoginName": "test", - "uLoginPWD": "098F6BCD4621D373CADE4E832627B4F6", - "uRealName": "后台测试1号", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": "测试是", - "sex": 1, - "age": 3, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "test", + "LoginPWD": "098F6BCD4621D373CADE4E832627B4F6", + "RealName": "后台测试1号", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": "测试是", + "Sex": 1, + "Age": 3, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 9, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 10, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 11, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 12, - "uLoginName": "blogadmin", - "uLoginPWD": "3FACF26687DAB7254848976256EDB56F", - "uRealName": "后台总管理员", - "uStatus": 0, - "uRemark": "t15", - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 1, - "age": 10, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "blogadmin", + "LoginPWD": "3FACF26687DAB7254848976256EDB56F", + "RealName": "后台总管理员", + "Status": 0, + "Remark": "t15", + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 1, + "Age": 10, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 }, { "Id": 13, - "uLoginName": "test2", - "uLoginPWD": "AD0234829205B9033196BA818F7A872B", - "uRealName": "后台测试2号", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 12, - "birth": "\/Date(1546272000000+0800)\/", - "addr": "北京市", - "tdIsDelete": 0 + "LoginName": "test2", + "LoginPWD": "AD0234829205B9033196BA818F7A872B", + "RealName": "后台测试2号", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 12, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": "北京市", + "IsDeleted": 0 }, { "Id": 14, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 15, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 16, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 17, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 18, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 19, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 20, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 21, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 22, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 23, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 24, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 25, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 26, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 27, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 28, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 29, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 30, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 31, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 32, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 33, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 34, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 35, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 36, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 37, - "uLoginName": "xx", - "uLoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", - "uRealName": "admins", - "uStatus": 0, - "uRemark": null, - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 0, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "xx", + "LoginPWD": "2AEFC34200A294A3CC7DB81B43A81873", + "RealName": "admins", + "Status": 0, + "Remark": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 0, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 38, - "uLoginName": "99", - "uLoginPWD": "AC627AB1CCBDB62EC96E702F7F6425B", - "uRealName": "99", - "uStatus": 0, - "uRemark": "blogadmin", - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": -1, - "age": 0, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 1 + "LoginName": "99", + "LoginPWD": "AC627AB1CCBDB62EC96E702F7F6425B", + "RealName": "99", + "Status": 0, + "Remark": "blogadmin", + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": -1, + "Age": 0, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 1 }, { "Id": 39, - "uLoginName": "Kawhi", - "uLoginPWD": "96FEE3FD714358658BFB881A4E1642BE", - "uRealName": "Kawhi 测试员", - "uStatus": 0, - "uRemark": "blogadmin", - "uCreateTime": "\/Date(1546272000000+0800)\/", - "uUpdateTime": "\/Date(1546272000000+0800)\/", - "uLastErrTime": "\/Date(1546272000000+0800)\/", - "uErrorCount": 0, - "name": null, - "sex": 1, - "age": 18, - "birth": "\/Date(1546272000000+0800)\/", - "addr": null, - "tdIsDelete": 0 + "LoginName": "Kawhi", + "LoginPWD": "96FEE3FD714358658BFB881A4E1642BE", + "RealName": "Kawhi 测试员", + "Status": 0, + "Remark": "blogadmin", + "CreateTime": "\/Date(1546272000000+0800)\/", + "UpdateTime": "\/Date(1546272000000+0800)\/", + "LastErrorTime": "\/Date(1546272000000+0800)\/", + "ErrorCount": 0, + "Name": null, + "Sex": 1, + "Age": 18, + "Birth": "\/Date(1546272000000+0800)\/", + "Address": null, + "IsDeleted": 0 } ] diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 1d00bbc7..11fcdae5 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -253,11 +253,11 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) #region sysUserInfo - if (!await myContext.Db.Queryable().AnyAsync()) + if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); - myContext.GetEntityDB().InsertRange(data); + myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:sysUserInfo created success!"); } else diff --git a/Blog.Core.IServices/IsysUserInfoServices.cs b/Blog.Core.IServices/IsysUserInfoServices.cs index f46cba0b..a2db873f 100644 --- a/Blog.Core.IServices/IsysUserInfoServices.cs +++ b/Blog.Core.IServices/IsysUserInfoServices.cs @@ -9,9 +9,9 @@ namespace Blog.Core.IServices /// /// sysUserInfoServices /// - public interface ISysUserInfoServices :IBaseServices + public interface ISysUserInfoServices :IBaseServices { - Task SaveUserInfo(string loginName, string loginPwd); + Task SaveUserInfo(string loginName, string loginPwd); Task GetUserRoleNameStr(string loginName, string loginPwd); } } diff --git a/Blog.Core.Model/Models/OperateLog.cs b/Blog.Core.Model/Models/OperateLog.cs index 5d9cfe16..4086781c 100644 --- a/Blog.Core.Model/Models/OperateLog.cs +++ b/Blog.Core.Model/Models/OperateLog.cs @@ -55,6 +55,6 @@ public class OperateLog : RootEntityTkey public int UserId { get; set; } [SugarColumn(IsIgnore = true)] - public virtual sysUserInfo User { get; set; } + public virtual SysUserInfo User { get; set; } } } diff --git a/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs b/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs index 5605374d..1c09f188 100644 --- a/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs +++ b/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs @@ -7,14 +7,14 @@ namespace Blog.Core.Model /// /// 用户信息表 /// - public class sysUserInfoRoot where Tkey : IEquatable + public class SysUserInfoRoot where Tkey : IEquatable { /// /// uID /// 泛型主键Tkey /// [SugarColumn(IsNullable = false, IsPrimaryKey = true)] - public Tkey uID { get; set; } + public Tkey Id { get; set; } [SugarColumn(IsIgnore = true)] public List RIDs { get; set; } diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index ef51c745..d8ffeee3 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -8,90 +8,88 @@ namespace Blog.Core.Model.Models /// 用户信息表 /// [SugarTable("SysUserInfo")] - public class sysUserInfo : sysUserInfoRoot + public class SysUserInfo : SysUserInfoRoot { - public sysUserInfo() { } + public SysUserInfo() { } - public sysUserInfo(string loginName, string loginPWD) + public SysUserInfo(string loginName, string loginPWD) { - uLoginName = loginName; - uLoginPWD = loginPWD; - uRealName = uLoginName; - uStatus = 0; - uCreateTime = DateTime.Now; - uUpdateTime = DateTime.Now; - uLastErrTime = DateTime.Now; - uErrorCount = 0; - name = ""; - + LoginName = loginName; + LoginPWD = loginPWD; + RealName = LoginName; + Status = 0; + CreateTime = DateTime.Now; + UpdateTime = DateTime.Now; + LastErrorTime = DateTime.Now; + ErrorCount = 0; + Name = ""; } /// /// 登录账号 /// [SugarColumn(Length = 200, IsNullable = true)] - public string uLoginName { get; set; } + public string LoginName { get; set; } /// /// 登录密码 /// [SugarColumn(Length = 200, IsNullable = true)] - public string uLoginPWD { get; set; } + public string LoginPWD { get; set; } /// /// 真实姓名 /// [SugarColumn(Length = 200, IsNullable = true)] - public string uRealName { get; set; } + public string RealName { get; set; } /// /// 状态 /// - public int uStatus { get; set; } + public int Status { get; set; } /// /// 备注 /// [SugarColumn(Length = 2000, IsNullable = true)] - public string uRemark { get; set; } + public string Remark { get; set; } /// /// 创建时间 /// - public System.DateTime uCreateTime { get; set; } = DateTime.Now; + public DateTime CreateTime { get; set; } = DateTime.Now; /// /// 更新时间 /// - public System.DateTime uUpdateTime { get; set; } = DateTime.Now; + public DateTime UpdateTime { get; set; } = DateTime.Now; /// - ///最后登录时间 + ///最后异常时间 /// - public DateTime uLastErrTime { get; set; } = DateTime.Now; + public DateTime LastErrorTime { get; set; } = DateTime.Now; /// ///错误次数 /// - public int uErrorCount { get; set; } - + public int ErrorCount { get; set; } /// /// 登录账号 /// [SugarColumn(Length = 200, IsNullable = true)] - public string name { get; set; } + public string Name { get; set; } // 性别 [SugarColumn(IsNullable = true)] - public int sex { get; set; } = 0; + public int Sex { get; set; } = 0; // 年龄 [SugarColumn(IsNullable = true)] - public int age { get; set; } + public int Age { get; set; } // 生日 [SugarColumn(IsNullable = true)] - public DateTime birth { get; set; } = DateTime.Now; + public DateTime Birth { get; set; } = DateTime.Now; // 地址 [SugarColumn(Length = 200, IsNullable = true)] - public string addr { get; set; } + public string Address { get; set; } [SugarColumn(IsNullable = true)] - public bool tdIsDelete { get; set; } + public bool IsDeleted { get; set; } [SugarColumn(IsIgnore = true)] diff --git a/Blog.Core.Services/sysUserInfoServices.cs b/Blog.Core.Services/sysUserInfoServices.cs index 3d5521d2..f536cf7a 100644 --- a/Blog.Core.Services/sysUserInfoServices.cs +++ b/Blog.Core.Services/sysUserInfoServices.cs @@ -10,13 +10,13 @@ namespace Blog.Core.FrameWork.Services /// /// sysUserInfoServices /// - public class SysUserInfoServices : BaseServices, ISysUserInfoServices + public class SysUserInfoServices : BaseServices, ISysUserInfoServices { - private readonly IBaseRepository _dal; + private readonly IBaseRepository _dal; private readonly IBaseRepository _userRoleRepository; private readonly IBaseRepository _roleRepository; - public SysUserInfoServices(IBaseRepository dal, IBaseRepository userRoleRepository, IBaseRepository roleRepository) + public SysUserInfoServices(IBaseRepository dal, IBaseRepository userRoleRepository, IBaseRepository roleRepository) { this._dal = dal; _userRoleRepository = userRoleRepository; @@ -29,11 +29,11 @@ public SysUserInfoServices(IBaseRepository dal, IBaseRepository /// /// - public async Task SaveUserInfo(string loginName, string loginPwd) + public async Task SaveUserInfo(string loginName, string loginPwd) { - sysUserInfo sysUserInfo = new sysUserInfo(loginName, loginPwd); - sysUserInfo model = new sysUserInfo(); - var userList = await base.Query(a => a.uLoginName == sysUserInfo.uLoginName && a.uLoginPWD == sysUserInfo.uLoginPWD); + SysUserInfo sysUserInfo = new SysUserInfo(loginName, loginPwd); + SysUserInfo model = new SysUserInfo(); + var userList = await base.Query(a => a.LoginName == sysUserInfo.LoginName && a.LoginPWD == sysUserInfo.LoginPWD); if (userList.Count > 0) { model = userList.FirstOrDefault(); @@ -57,11 +57,11 @@ public async Task SaveUserInfo(string loginName, string loginPwd) public async Task GetUserRoleNameStr(string loginName, string loginPwd) { string roleName = ""; - var user = (await base.Query(a => a.uLoginName == loginName && a.uLoginPWD == loginPwd)).FirstOrDefault(); + var user = (await base.Query(a => a.LoginName == loginName && a.LoginPWD == loginPwd)).FirstOrDefault(); var roleList = await _roleRepository.Query(a => a.IsDeleted == false); if (user != null) { - var userRoles = await _userRoleRepository.Query(ur => ur.UserId == user.uID); + var userRoles = await _userRoleRepository.Query(ur => ur.UserId == user.Id); if (userRoles.Count > 0) { var arr = userRoles.Select(ur => ur.RoleId.ObjToString()).ToList(); From cb2016c2bb53237bb0c59ecb62f53be4fee0eeda Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 15 Mar 2022 15:12:21 +0800 Subject: [PATCH 016/289] fix: format the code. #232 --- Blog.Core.Api/Blog.Core.Model.xml | 2 +- Blog.Core.Api/Controllers/UserController.cs | 2 +- Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 6aef9cdb..af88da3e 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1452,7 +1452,7 @@ - uID + Id 泛型主键Tkey diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 2e233550..803ae276 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -66,7 +66,7 @@ public async Task>> Get(int page = 1, string int intPageSize = 50; - var data = await _sysUserInfoServices.QueryPage(a => a.IsDeleted != true && a.Status >= 0 && ((a.LoginName != null && a.LoginName.Contains(key)) || (a.RealName != null && a.RealName.Contains(key))), page, intPageSize, " uID desc "); + var data = await _sysUserInfoServices.QueryPage(a => a.IsDeleted != true && a.Status >= 0 && ((a.LoginName != null && a.LoginName.Contains(key)) || (a.RealName != null && a.RealName.Contains(key))), page, intPageSize, " Id desc "); #region MyRegion diff --git a/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs b/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs index 1c09f188..60531b1c 100644 --- a/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs +++ b/Blog.Core.Model/Models/RootTkey/sysUserInfoRoot.cs @@ -10,7 +10,7 @@ namespace Blog.Core.Model public class SysUserInfoRoot where Tkey : IEquatable { /// - /// uID + /// Id /// 泛型主键Tkey /// [SugarColumn(IsNullable = false, IsPrimaryKey = true)] From 6fa0b153b0b1688f0d72767a91d92c9f5d28e156 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 15 Mar 2022 18:25:27 +0800 Subject: [PATCH 017/289] fix: try to use dto in user manage --- Blog.Core.Api/Blog.Core.xml | 10 ++-- Blog.Core.Api/Controllers/UserController.cs | 46 ++++++++++--------- .../Controllers/UserRoleController.cs | 26 +++++++---- .../AutoMapper/CustomProfile.cs | 37 +++++++++++++++ Blog.Core.Model/ViewModels/SysUserInfoDto.cs | 27 +++++++++++ 5 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 Blog.Core.Model/ViewModels/SysUserInfoDto.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index ad1d8a9e..44e2e1e3 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -633,7 +633,7 @@ 用户管理 - + 构造函数 @@ -642,6 +642,7 @@ + @@ -660,14 +661,14 @@ 令牌 - + 添加一个用户 - + 更新用户与角色 @@ -686,12 +687,13 @@ 用户角色关系 - + 构造函数 + diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 803ae276..3da0310c 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AutoMapper; using Blog.Core.AuthHelper.OverWrite; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; @@ -9,6 +10,7 @@ using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; +using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -21,13 +23,14 @@ namespace Blog.Core.Controllers [Route("api/[controller]/[action]")] [ApiController] [Authorize(Permissions.Name)] - public class UserController : ControllerBase + public class UserController : BaseApiController { private readonly IUnitOfWork _unitOfWork; readonly ISysUserInfoServices _sysUserInfoServices; readonly IUserRoleServices _userRoleServices; readonly IRoleServices _roleServices; private readonly IUser _user; + private readonly IMapper _mapper; private readonly ILogger _logger; /// @@ -38,14 +41,18 @@ public class UserController : ControllerBase /// /// /// + /// /// - public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IRoleServices roleServices, IUser user, ILogger logger) + public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoServices, + IUserRoleServices userRoleServices, IRoleServices roleServices, + IUser user, IMapper mapper, ILogger logger) { _unitOfWork = unitOfWork; _sysUserInfoServices = sysUserInfoServices; _userRoleServices = userRoleServices; _roleServices = roleServices; _user = user; + _mapper = mapper; _logger = logger; } @@ -57,7 +64,7 @@ public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoSe /// // GET: api/User [HttpGet] - public async Task>> Get(int page = 1, string key = "") + public async Task>> Get(int page = 1, string key = "") { if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { @@ -87,12 +94,7 @@ public async Task>> Get(int page = 1, string #endregion - return new MessageModel>() - { - msg = "获取成功", - success = data.dataCount >= 0, - response = data - }; + return SuccessPage(data.page, data.dataCount, data.PageSize, _mapper.Map>(sysUserInfos), data.pageCount); } @@ -114,9 +116,9 @@ public string Get(string id) /// [HttpGet] [AllowAnonymous] - public async Task> GetInfoByToken(string token) + public async Task> GetInfoByToken(string token) { - var data = new MessageModel(); + var data = new MessageModel(); if (!string.IsNullOrEmpty(token)) { var tokenModel = JwtHelper.SerializeJwt(token); @@ -125,7 +127,7 @@ public async Task> GetInfoByToken(string token) var userinfo = await _sysUserInfoServices.QueryById(tokenModel.Uid); if (userinfo != null) { - data.response = userinfo; + data.response = _mapper.Map(userinfo); data.success = true; data.msg = "获取成功"; } @@ -142,14 +144,14 @@ public async Task> GetInfoByToken(string token) /// // POST: api/User [HttpPost] - public async Task> Post([FromBody] SysUserInfo sysUserInfo) + public async Task> Post([FromBody] SysUserInfoDto sysUserInfo) { var data = new MessageModel(); - sysUserInfo.LoginPWD = MD5Helper.MD5Encrypt32(sysUserInfo.LoginPWD); - sysUserInfo.Remark = _user.Name; + sysUserInfo.uLoginPWD = MD5Helper.MD5Encrypt32(sysUserInfo.uLoginPWD); + sysUserInfo.uRemark = _user.Name; - var id = await _sysUserInfoServices.Add(sysUserInfo); + var id = await _sysUserInfoServices.Add(_mapper.Map(sysUserInfo)); data.success = id > 0; if (data.success) { @@ -167,7 +169,7 @@ public async Task> Post([FromBody] SysUserInfo sysUserInfo) /// // PUT: api/User/5 [HttpPut] - public async Task> Put([FromBody] SysUserInfo sysUserInfo) + public async Task> Put([FromBody] SysUserInfoDto sysUserInfo) { // 这里使用事务处理 @@ -176,12 +178,12 @@ public async Task> Put([FromBody] SysUserInfo sysUserInfo) { _unitOfWork.BeginTran(); - if (sysUserInfo != null && sysUserInfo.Id > 0) + if (sysUserInfo != null && sysUserInfo.uID > 0) { if (sysUserInfo.RIDs.Count > 0) { // 无论 Update Or Add , 先删除当前用户的全部 U_R 关系 - var usreroles = (await _userRoleServices.Query(d => d.UserId == sysUserInfo.Id)).Select(d => d.Id.ToString()).ToArray(); + var usreroles = (await _userRoleServices.Query(d => d.UserId == sysUserInfo.uID)).Select(d => d.Id.ToString()).ToArray(); if (usreroles.Count() > 0) { var isAllDeleted = await _userRoleServices.DeleteByIds(usreroles); @@ -191,21 +193,21 @@ public async Task> Put([FromBody] SysUserInfo sysUserInfo) var userRolsAdd = new List(); sysUserInfo.RIDs.ForEach(rid => { - userRolsAdd.Add(new UserRole(sysUserInfo.Id, rid)); + userRolsAdd.Add(new UserRole(sysUserInfo.uID, rid)); }); await _userRoleServices.Add(userRolsAdd); } - data.success = await _sysUserInfoServices.Update(sysUserInfo); + data.success = await _sysUserInfoServices.Update(_mapper.Map(sysUserInfo)); _unitOfWork.CommitTran(); if (data.success) { data.msg = "更新成功"; - data.response = sysUserInfo?.Id.ObjToString(); + data.response = sysUserInfo?.uID.ObjToString(); } } } diff --git a/Blog.Core.Api/Controllers/UserRoleController.cs b/Blog.Core.Api/Controllers/UserRoleController.cs index d36fef55..d14d6a73 100644 --- a/Blog.Core.Api/Controllers/UserRoleController.cs +++ b/Blog.Core.Api/Controllers/UserRoleController.cs @@ -1,7 +1,9 @@ using System.Threading.Tasks; +using AutoMapper; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; +using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -16,21 +18,24 @@ namespace Blog.Core.Controllers [Authorize(Permissions.Name)] public class UserRoleController : Controller { - readonly ISysUserInfoServices _sysUserInfoServices; - readonly IUserRoleServices _userRoleServices; - readonly IRoleServices _roleServices; + private readonly ISysUserInfoServices _sysUserInfoServices; + private readonly IUserRoleServices _userRoleServices; + private readonly IRoleServices _roleServices; + private readonly IMapper _mapper; /// /// 构造函数 /// /// /// + /// /// - public UserRoleController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IRoleServices roleServices) + public UserRoleController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IMapper mapper, IRoleServices roleServices) { - this._sysUserInfoServices = sysUserInfoServices; - this._userRoleServices = userRoleServices; - this._roleServices = roleServices; + _sysUserInfoServices = sysUserInfoServices; + _userRoleServices = userRoleServices; + _roleServices = roleServices; + _mapper = mapper; } @@ -42,13 +47,14 @@ public UserRoleController(ISysUserInfoServices sysUserInfoServices, IUserRoleSer /// /// [HttpGet] - public async Task> AddUser(string loginName, string loginPwd) + public async Task> AddUser(string loginName, string loginPwd) { - return new MessageModel() + var userInfo = await _sysUserInfoServices.SaveUserInfo(loginName, loginPwd); + return new MessageModel() { success = true, msg = "添加成功", - response = await _sysUserInfoServices.SaveUserInfo(loginName, loginPwd) + response = _mapper.Map(userInfo) }; } diff --git a/Blog.Core.Extensions/AutoMapper/CustomProfile.cs b/Blog.Core.Extensions/AutoMapper/CustomProfile.cs index 9e1957ff..7b05f079 100644 --- a/Blog.Core.Extensions/AutoMapper/CustomProfile.cs +++ b/Blog.Core.Extensions/AutoMapper/CustomProfile.cs @@ -13,6 +13,43 @@ public CustomProfile() { CreateMap(); CreateMap(); + + CreateMap() + .ForMember(a => a.uID, o => o.MapFrom(d => d.Id)) + .ForMember(a => a.RIDs, o => o.MapFrom(d => d.RIDs)) + .ForMember(a => a.addr, o => o.MapFrom(d => d.Address)) + .ForMember(a => a.age, o => o.MapFrom(d => d.Age)) + .ForMember(a => a.birth, o => o.MapFrom(d => d.Birth)) + .ForMember(a => a.uStatus, o => o.MapFrom(d => d.Status)) + .ForMember(a => a.uUpdateTime, o => o.MapFrom(d => d.UpdateTime)) + .ForMember(a => a.uCreateTime, o => o.MapFrom(d => d.CreateTime)) + .ForMember(a => a.uErrorCount, o => o.MapFrom(d => d.ErrorCount)) + .ForMember(a => a.uLastErrTime, o => o.MapFrom(d => d.LastErrorTime)) + .ForMember(a => a.uLoginName, o => o.MapFrom(d => d.LoginName)) + .ForMember(a => a.uLoginPWD, o => o.MapFrom(d => d.LoginPWD)) + .ForMember(a => a.uRemark, o => o.MapFrom(d => d.Remark)) + .ForMember(a => a.uRealName, o => o.MapFrom(d => d.RealName)) + .ForMember(a => a.name, o => o.MapFrom(d => d.Name)) + .ForMember(a => a.tdIsDelete, o => o.MapFrom(d => d.IsDeleted)) + .ForMember(a => a.RoleNames, o => o.MapFrom(d => d.RoleNames)); + CreateMap() + .ForMember(a => a.Id, o => o.MapFrom(d => d.uID)) + .ForMember(a => a.Address, o => o.MapFrom(d => d.addr)) + .ForMember(a => a.RIDs, o => o.MapFrom(d => d.RIDs)) + .ForMember(a => a.Age, o => o.MapFrom(d => d.age)) + .ForMember(a => a.Birth, o => o.MapFrom(d => d.birth)) + .ForMember(a => a.Status, o => o.MapFrom(d => d.uStatus)) + .ForMember(a => a.UpdateTime, o => o.MapFrom(d => d.uUpdateTime)) + .ForMember(a => a.CreateTime, o => o.MapFrom(d => d.uCreateTime)) + .ForMember(a => a.ErrorCount, o => o.MapFrom(d => d.uErrorCount)) + .ForMember(a => a.LastErrorTime, o => o.MapFrom(d => d.uLastErrTime)) + .ForMember(a => a.LoginName, o => o.MapFrom(d => d.uLoginName)) + .ForMember(a => a.LoginPWD, o => o.MapFrom(d => d.uLoginPWD)) + .ForMember(a => a.Remark, o => o.MapFrom(d => d.uRemark)) + .ForMember(a => a.RealName, o => o.MapFrom(d => d.uRealName)) + .ForMember(a => a.Name, o => o.MapFrom(d => d.name)) + .ForMember(a => a.IsDeleted, o => o.MapFrom(d => d.tdIsDelete)) + .ForMember(a => a.RoleNames, o => o.MapFrom(d => d.RoleNames)); } } } diff --git a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs new file mode 100644 index 00000000..6e538b91 --- /dev/null +++ b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace Blog.Core.Model.ViewModels +{ + public class SysUserInfoDto + { + public int uID { get; set; } + public string uLoginName { get; set; } + public string uLoginPWD { get; set; } + public string uRealName { get; set; } + public int uStatus { get; set; } + public string uRemark { get; set; } + public System.DateTime uCreateTime { get; set; } = DateTime.Now; + public System.DateTime uUpdateTime { get; set; } = DateTime.Now; + public DateTime uLastErrTime { get; set; } = DateTime.Now; + public int uErrorCount { get; set; } + public string name { get; set; } + public int sex { get; set; } = 0; + public int age { get; set; } + public DateTime birth { get; set; } = DateTime.Now; + public string addr { get; set; } + public bool tdIsDelete { get; set; } + public List RoleNames { get; set; } + public List RIDs { get; set; } + } +} From 33975b33470a44ee8f2d0c2c8072f505fe786f98 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 15 Mar 2022 19:30:58 +0800 Subject: [PATCH 018/289] fix: dot root add Tkey --- .../ViewModels/RootTKey/SysUserInfoDtoRoot.cs | 13 +++++++++++++ Blog.Core.Model/ViewModels/SysUserInfoDto.cs | 4 +--- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 Blog.Core.Model/ViewModels/RootTKey/SysUserInfoDtoRoot.cs diff --git a/Blog.Core.Model/ViewModels/RootTKey/SysUserInfoDtoRoot.cs b/Blog.Core.Model/ViewModels/RootTKey/SysUserInfoDtoRoot.cs new file mode 100644 index 00000000..bcccb8e7 --- /dev/null +++ b/Blog.Core.Model/ViewModels/RootTKey/SysUserInfoDtoRoot.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Blog.Core.Model.ViewModels +{ + public class SysUserInfoDtoRoot where Tkey : IEquatable + { + public Tkey uID { get; set; } + + public List RIDs { get; set; } + + } +} diff --git a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs index 6e538b91..9a9e8325 100644 --- a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs +++ b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs @@ -3,9 +3,8 @@ namespace Blog.Core.Model.ViewModels { - public class SysUserInfoDto + public class SysUserInfoDto : SysUserInfoDtoRoot { - public int uID { get; set; } public string uLoginName { get; set; } public string uLoginPWD { get; set; } public string uRealName { get; set; } @@ -22,6 +21,5 @@ public class SysUserInfoDto public string addr { get; set; } public bool tdIsDelete { get; set; } public List RoleNames { get; set; } - public List RIDs { get; set; } } } From f3bfaae3f456a71b37bd547fa8ec0ecefe6f6988 Mon Sep 17 00:00:00 2001 From: __Leo__ Date: Mon, 21 Mar 2022 12:13:00 +0800 Subject: [PATCH 019/289] remove ModulePermission --- Blog.Core.Api/Blog.Core.Model.xml | 56 ------------------- Blog.Core.Api/Controllers/UserController.cs | 2 +- .../IModulePermissionServices.cs | 9 --- Blog.Core.Model/Models/ModulePermission.cs | 52 ----------------- .../Models/RootTkey/ModulePermissionRoot.cs | 21 ------- .../ModulePermissionServices.cs | 23 -------- 6 files changed, 1 insertion(+), 162 deletions(-) delete mode 100644 Blog.Core.IServices/IModulePermissionServices.cs delete mode 100644 Blog.Core.Model/Models/ModulePermission.cs delete mode 100644 Blog.Core.Model/Models/RootTkey/ModulePermissionRoot.cs delete mode 100644 Blog.Core.Services/ModulePermissionServices.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index af88da3e..3b4fd36f 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -306,46 +306,6 @@ - - - 菜单与按钮关系表 - - - - - 获取或设置是否禁用,逻辑上的删除,非物理删除 - - - - - 创建ID - - - - - 创建者 - - - - - 创建时间 - - - - - 修改ID - - - - - 修改者 - - - - - 修改时间 - - 接口API地址信息表 @@ -1376,22 +1336,6 @@ ID - - - 菜单与按钮关系表 - 父类 - - - - - 菜单ID - - - - - 按钮ID - - 接口API地址信息表 diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 3da0310c..d26146d1 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -94,7 +94,7 @@ public async Task>> Get(int page = 1, str #endregion - return SuccessPage(data.page, data.dataCount, data.PageSize, _mapper.Map>(sysUserInfos), data.pageCount); + return Success(data.ConvertTo(_mapper)); } diff --git a/Blog.Core.IServices/IModulePermissionServices.cs b/Blog.Core.IServices/IModulePermissionServices.cs deleted file mode 100644 index a0adcb07..00000000 --- a/Blog.Core.IServices/IModulePermissionServices.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Blog.Core.IServices.BASE; -using Blog.Core.Model.Models; - -namespace Blog.Core.IServices -{ - public partial interface IModulePermissionServices : IBaseServices - { - } -} \ No newline at end of file diff --git a/Blog.Core.Model/Models/ModulePermission.cs b/Blog.Core.Model/Models/ModulePermission.cs deleted file mode 100644 index 74252009..00000000 --- a/Blog.Core.Model/Models/ModulePermission.cs +++ /dev/null @@ -1,52 +0,0 @@ -using SqlSugar; -using System; - -namespace Blog.Core.Model.Models -{ - /// - /// 菜单与按钮关系表 - /// - public class ModulePermission : ModulePermissionRoot - { - - /// - ///获取或设置是否禁用,逻辑上的删除,非物理删除 - /// - [SugarColumn(IsNullable = true)] - public bool? IsDeleted { get; set; } - - /// - /// 创建ID - /// - [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } - /// - /// 创建者 - /// - [SugarColumn(Length = 50, IsNullable = true)] - public string CreateBy { get; set; } - /// - /// 创建时间 - /// - [SugarColumn(IsNullable = true)] - public DateTime? CreateTime { get; set; } - /// - /// 修改ID - /// - [SugarColumn(IsNullable = true)] - public int? ModifyId { get; set; } - /// - /// 修改者 - /// - [SugarColumn(Length = 50, IsNullable = true)] - public string ModifyBy { get; set; } - /// - ///修改时间 - /// - [SugarColumn(IsNullable = true)] - public DateTime? ModifyTime { get; set; } - - //public virtual Module Module { get; set; } - //public virtual Permission Permission { get; set; } - } -} diff --git a/Blog.Core.Model/Models/RootTkey/ModulePermissionRoot.cs b/Blog.Core.Model/Models/RootTkey/ModulePermissionRoot.cs deleted file mode 100644 index 86873f69..00000000 --- a/Blog.Core.Model/Models/RootTkey/ModulePermissionRoot.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Blog.Core.Model -{ - /// - /// 菜单与按钮关系表 - /// 父类 - /// - public class ModulePermissionRoot : RootEntityTkey where Tkey : IEquatable - { - /// - /// 菜单ID - /// - public Tkey ModuleId { get; set; } - /// - /// 按钮ID - /// - public Tkey PermissionId { get; set; } - - } -} diff --git a/Blog.Core.Services/ModulePermissionServices.cs b/Blog.Core.Services/ModulePermissionServices.cs deleted file mode 100644 index cf80e492..00000000 --- a/Blog.Core.Services/ModulePermissionServices.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Blog.Core.Services.BASE; -using Blog.Core.Model.Models; -using Blog.Core.IRepository; -using Blog.Core.IServices; -using Blog.Core.IRepository.Base; - -namespace Blog.Core.Services -{ - /// - /// ModulePermissionServices - /// - public class ModulePermissionServices : BaseServices, IModulePermissionServices - { - - IBaseRepository _dal; - public ModulePermissionServices(IBaseRepository dal) - { - this._dal = dal; - base.BaseDal = dal; - } - - } -} From 23a08dbd351e7c4165a82dee8469403910759a8a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 23 Mar 2022 17:08:15 +0800 Subject: [PATCH 020/289] fix: add department api --- Blog.Core.Api/Blog.Core.Model.xml | 87 +++++++++++++++++ .../Controllers/DepartmentController.cs | 97 +++++++++++++++++++ Blog.Core.IServices/IDepartmentServices.cs | 12 +++ Blog.Core.Model/Models/Department.cs | 77 +++++++++++++++ Blog.Core.Services/DepartmentServices.cs | 20 ++++ 5 files changed, 293 insertions(+) create mode 100644 Blog.Core.Api/Controllers/DepartmentController.cs create mode 100644 Blog.Core.IServices/IDepartmentServices.cs create mode 100644 Blog.Core.Model/Models/Department.cs create mode 100644 Blog.Core.Services/DepartmentServices.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 3b4fd36f..6eb3c6a1 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -271,6 +271,93 @@ 逻辑删除 + + + 部门表 + + + + + Desc:部门Code + Nullable:True + + + + + Desc:父部门Code + Nullable:True + + + + + Desc:部门关系编码 + Default: + Nullable:True + + + + + Desc:部门名称 + Default: + Nullable:True + + + + + Desc:负责人 + Default: + Nullable:True + + + + + Desc:排序 + Default: + Nullable:True + + + + + Desc:部门状态(0正常 1停用) + Default:0 + Nullable:True + + + + + Desc:删除标志(0代表存在 2代表删除) + Default:0 + Nullable:True + + + + + Desc:创建者 + Default: + Nullable:True + + + + + Desc:创建时间 + Default: + Nullable:True + + + + + Desc:更新者 + Default: + Nullable:True + + + + + Desc:更新时间 + Default: + Nullable:True + + 博客ID diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs new file mode 100644 index 00000000..efa4c16f --- /dev/null +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -0,0 +1,97 @@ +using Blog.Core.IServices; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Blog.Core.Api.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + [Authorize(Permissions.Name)] + public class DepartmentController : ControllerBase + { + private readonly IDepartmentServices _departmentServices; + + public DepartmentController(IDepartmentServices departmentServices) + { + _departmentServices = departmentServices; + } + + [HttpGet] + public async Task>> Get(int page = 1, string key = "",int intPageSize = 50) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) + { + key = ""; + } + + Expression> whereExpression = a => true; + + return new MessageModel>() + { + msg = "获取成功", + success = true, + response = await _departmentServices.QueryPage(whereExpression, page, intPageSize) + }; + + } + + [HttpGet("{id}")] + public async Task> Get(string id) + { + return new MessageModel() + { + msg = "获取成功", + success = true, + response = await _departmentServices.QueryById(id) + }; + } + + [HttpPost] + public async Task> Post([FromBody] Department request) + { + var data = new MessageModel(); + + var id = await _departmentServices.Add(request); + if (data.success) + { + data.response = id.ObjToString(); + data.msg = "添加成功"; + } + + return data; + } + + [HttpPut] + public async Task> Put([FromBody] Department request) + { + var data = new MessageModel(); + data.success = await _departmentServices.Update(request); + if (data.success) + { + data.msg = "更新成功"; + data.response = request?.Id.ObjToString(); + } + + return data; + } + + [HttpDelete("{id}")] + public async Task> Delete(string id) + { + var data = new MessageModel(); + data.success = await _departmentServices.DeleteById(id); + if (data.success) + { + data.msg = "删除成功"; + data.response = id; + } + + return data; + } + } +} \ No newline at end of file diff --git a/Blog.Core.IServices/IDepartmentServices.cs b/Blog.Core.IServices/IDepartmentServices.cs new file mode 100644 index 00000000..fdd650d7 --- /dev/null +++ b/Blog.Core.IServices/IDepartmentServices.cs @@ -0,0 +1,12 @@ +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; + +namespace Blog.Core.IServices +{ + /// + /// IDepartmentServices + /// + public interface IDepartmentServices : IBaseServices + { + } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/Department.cs b/Blog.Core.Model/Models/Department.cs new file mode 100644 index 00000000..e9cd2143 --- /dev/null +++ b/Blog.Core.Model/Models/Department.cs @@ -0,0 +1,77 @@ +using System; + + +namespace Blog.Core.Model.Models +{ + /// + /// 部门表 + /// + public class Department : RootEntityTkey + { + /// + /// Desc:父部门 + /// Nullable:True + /// + public long PId { get; set; } + /// + /// Desc:部门关系编码 + /// Default: + /// Nullable:True + /// + public string CodeRelationship { get; set; } + /// + /// Desc:部门名称 + /// Default: + /// Nullable:True + /// + public string DepartName { get; set; } + /// + /// Desc:负责人 + /// Default: + /// Nullable:True + /// + public string Leader { get; set; } + /// + /// Desc:排序 + /// Default: + /// Nullable:True + /// + public int? OrderNum { get; set; } = 0; + /// + /// Desc:部门状态(0正常 1停用) + /// Default:0 + /// Nullable:True + /// + public bool Status { get; set; } + /// + /// Desc:删除标志(0代表存在 2代表删除) + /// Default:0 + /// Nullable:True + /// + public bool IsDeleted { get; set; } = false; + /// + /// Desc:创建者 + /// Default: + /// Nullable:True + /// + public string CreateBy { get; set; } + /// + /// Desc:创建时间 + /// Default: + /// Nullable:True + /// + public DateTime? CreateTime { get; set; } + /// + /// Desc:更新者 + /// Default: + /// Nullable:True + /// + public string ModifyBy { get; set; } + /// + /// Desc:更新时间 + /// Default: + /// Nullable:True + /// + public DateTime? ModifyTime { get; set; } + } +} \ No newline at end of file diff --git a/Blog.Core.Services/DepartmentServices.cs b/Blog.Core.Services/DepartmentServices.cs new file mode 100644 index 00000000..a6f1eed5 --- /dev/null +++ b/Blog.Core.Services/DepartmentServices.cs @@ -0,0 +1,20 @@ +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Services.BASE; +using Blog.Core.IRepository.Base; + +namespace Blog.Core.Services +{ + /// + /// DepartmentServices + /// + public class DepartmentServices : BaseServices, IDepartmentServices + { + private readonly IBaseRepository _dal; + public DepartmentServices(IBaseRepository dal) + { + this._dal = dal; + base.BaseDal = dal; + } + } +} \ No newline at end of file From 2d58445a5952eb581c33b71c11cbf5c0b5fd54d0 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 23 Mar 2022 17:52:03 +0800 Subject: [PATCH 021/289] Update Blog.Core.Model.xml --- Blog.Core.Api/Blog.Core.Model.xml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 6eb3c6a1..f43cb30e 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -276,15 +276,9 @@ 部门表 - + - Desc:部门Code - Nullable:True - - - - - Desc:父部门Code + Desc:父部门 Nullable:True From 55e9aa90c1f01637812d900bfced810699a8530d Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 23 Mar 2022 20:15:12 +0800 Subject: [PATCH 022/289] Update ModuleController.cs --- Blog.Core.Api/Controllers/ModuleController.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Api/Controllers/ModuleController.cs b/Blog.Core.Api/Controllers/ModuleController.cs index c160e0da..334a8ea4 100644 --- a/Blog.Core.Api/Controllers/ModuleController.cs +++ b/Blog.Core.Api/Controllers/ModuleController.cs @@ -47,7 +47,17 @@ public async Task>> Get(int page = 1, string key Expression> whereExpression = a => a.IsDeleted != true && (a.Name != null && a.Name.Contains(key)); - var data = await _moduleServices.QueryPage(whereExpression, page, intPageSize, " Id desc "); + PageModel data = new PageModel(); + + if (page == -1) + { + var modules = await _moduleServices.Query(whereExpression, " Id desc "); + data.data = modules; + } + else + { + data = await _moduleServices.QueryPage(whereExpression, page, intPageSize, " Id desc "); + } return Success(data, "获取成功"); From 3bbb4bb882a1c9049527c48a51e6d9b54c9dd87f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 23 Mar 2022 20:23:16 +0800 Subject: [PATCH 023/289] add department table seed data #260 --- .../wwwroot/BlogCore.Data.json/Modules.tsv | 84 ++++++++ .../wwwroot/BlogCore.Data.json/Permission.tsv | 186 ++++++++++++++++++ .../RoleModulePermission.tsv | 77 ++++++++ 3 files changed, 347 insertions(+) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv index 9acf2b3c..9c82d93b 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv @@ -1407,5 +1407,89 @@ "ModifyBy": null, "ModifyTime": "2020-04-06 00:00:00", "ParentId": 0 + }, + { + "IsDeleted": false, + "Name": "获取部门数据", + "LinkUrl": "/api/department/get", + "OrderSort": 0, + "IsMenu": false, + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "ParentId": 0, + "Id": 65 + }, + { + "IsDeleted": false, + "Name": "获取部门数据树表格", + "LinkUrl": "/api/permission/GetTreeTable", + "OrderSort": 0, + "IsMenu": false, + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "ParentId": 0, + "Id": 66 + }, + { + "IsDeleted": false, + "Name": "删除部门", + "LinkUrl": "/api/department/delete", + "OrderSort": 0, + "IsMenu": false, + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "ParentId": 0, + "Id": 67 + }, + { + "IsDeleted": false, + "Name": "更新部门", + "LinkUrl": "/api/department/put", + "OrderSort": 0, + "IsMenu": false, + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "ParentId": 0, + "Id": 68 + }, + { + "IsDeleted": false, + "Name": "添加部门", + "LinkUrl": "/api/department/post", + "OrderSort": 0, + "IsMenu": false, + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "ParentId": 0, + "Id": 69 + }, + { + "IsDeleted": false, + "Name": "获取部门树", + "LinkUrl": "/api/department/getDepartmentTree", + "OrderSort": 0, + "IsMenu": false, + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "ParentId": 0, + "Id": 70 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index 81984e1b..05375d34 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -2344,5 +2344,191 @@ "IsDeleted": 0, "Pid": 95, "Mid": 0 + }, + { + "Code": "-", + "Name": "部门权限管理", + "IsButton": false, + "IsHide": false, + "IskeepAlive": false, + "OrderSort": -10, + "Icon": "fa-address-book", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 0, + "Mid": 0, + "Id": 114 + }, + { + "Code": "/Department/Department", + "Name": "部门管理", + "IsButton": false, + "IsHide": false, + "IskeepAlive": false, + "OrderSort": 0, + "Icon": "", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 114, + "Mid": 66, + "Id": 115 + }, + { + "Code": " ", + "Name": "查询", + "IsButton": true, + "IsHide": false, + "IskeepAlive": false, + "Func": "handleQuery", + "OrderSort": 0, + "Icon": "", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 115, + "Mid": 66, + "Id": 116 + }, + { + "Code": " ", + "Name": "新增", + "IsButton": true, + "IsHide": false, + "IskeepAlive": false, + "Func": "handleAdd", + "OrderSort": 0, + "Icon": "", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 115, + "Mid": 69, + "Id": 117 + }, + { + "Code": " ", + "Name": "编辑", + "IsButton": true, + "IsHide": false, + "IskeepAlive": false, + "Func": "handleEdit", + "OrderSort": 0, + "Icon": "", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 115, + "Mid": 68, + "Id": 118 + }, + { + "Code": " ", + "Name": "删除", + "IsButton": true, + "IsHide": false, + "IskeepAlive": false, + "Func": "handleDel", + "OrderSort": 0, + "Icon": "", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 115, + "Mid": 67, + "Id": 119 + }, + { + "Code": " ", + "Name": "部门树", + "IsButton": true, + "IsHide": true, + "IskeepAlive": false, + "OrderSort": 0, + "Icon": "", + "Description": "", + "Enabled": true, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 00:00:00", + "ModifyTime": "2022-03-23 00:00:00", + "IsDeleted": false, + "PnameArr": [ + + ], + "PCodeArr": [ + + ], + "hasChildren": true, + "Pid": 115, + "Mid": 70, + "Id": 120 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv index 4307f6ee..48d5c2c1 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv @@ -1558,5 +1558,82 @@ "ModifyBy": null, "ModifyTime": "\/Date(1546272000000+0800)\/", "Id": 157 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 114, + "Id": 121 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 66, + "PermissionId": 115, + "Id": 122 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 66, + "PermissionId": 116, + "Id": 123 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 69, + "PermissionId": 117, + "Id": 124 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 68, + "PermissionId": 118, + "Id": 125 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 67, + "PermissionId": 119, + "Id": 126 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyTime": "2022-03-23 19:21:58", + "RoleId": 4, + "ModuleId": 70, + "PermissionId": 120, + "Id": 127 } ] From ddd9769973aa005f462abf3dc5e78652dacbdc2c Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 24 Mar 2022 17:50:08 +0800 Subject: [PATCH 024/289] fix: sort permission.tsv #260 --- Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index 05375d34..37896ab0 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -5,7 +5,7 @@ "IsButton": 0, "Pid": 0, "Mid": 0, - "OrderSort": 0, + "OrderSort": -90, "Icon": "fa-home", "Description": "33", "Enabled": 1, @@ -25,7 +25,7 @@ "IsButton": 0, "Pid": 0, "Mid": 0, - "OrderSort": 0, + "OrderSort": -80, "Icon": "fa-users", "Description": "11", "Enabled": 1, From ca9103fa9bd4aeca1199215edc9c241c5dc16c3e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 25 Mar 2022 23:27:42 +0800 Subject: [PATCH 025/289] fix: set department tree #261 --- Blog.Core.Api/Blog.Core.Model.xml | 20 +- Blog.Core.Api/Blog.Core.xml | 15 ++ .../Controllers/DepartmentController.cs | 209 +++++++++++++----- Blog.Core.Common/Helper/RecursionHelper.cs | 35 +++ Blog.Core.Common/Seed/FrameSeed.cs | 1 + Blog.Core.Model/Models/Department.cs | 26 ++- .../Models/RootTkey/DepartmentRoot.cs | 20 ++ 7 files changed, 247 insertions(+), 79 deletions(-) create mode 100644 Blog.Core.Model/Models/RootTkey/DepartmentRoot.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index f43cb30e..41ab4514 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -276,12 +276,6 @@ 部门表 - - - Desc:父部门 - Nullable:True - - Desc:部门关系编码 @@ -289,7 +283,7 @@ Nullable:True - + Desc:部门名称 Default: @@ -303,7 +297,7 @@ Nullable:True - + Desc:排序 Default: @@ -1417,6 +1411,16 @@ ID + + + 部门表 + + + + + 上一级(0表示无上一级) + + 接口API地址信息表 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 44e2e1e3..730ddfd4 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1155,6 +1155,21 @@ + + + 查询树形 Table + + 父节点 + 关键字 + + + + + 获取部门树 + + + + 服务管理 diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index efa4c16f..d865ea88 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -1,97 +1,184 @@ -using Blog.Core.IServices; +using Blog.Core.Common.Helper; +using Blog.Core.Controllers; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; namespace Blog.Core.Api.Controllers { - [Route("api/[controller]/[action]")] - [ApiController] + [Route("api/[controller]/[action]")] + [ApiController] [Authorize(Permissions.Name)] - public class DepartmentController : ControllerBase + public class DepartmentController : BaseApiController { - private readonly IDepartmentServices _departmentServices; - - public DepartmentController(IDepartmentServices departmentServices) + private readonly IDepartmentServices _departmentServices; + + public DepartmentController(IDepartmentServices departmentServices) + { + _departmentServices = departmentServices; + } + + [HttpGet] + public async Task>> Get(int page = 1, string key = "", int intPageSize = 50) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { - _departmentServices = departmentServices; + key = ""; } - - [HttpGet] - public async Task>> Get(int page = 1, string key = "",int intPageSize = 50) + + Expression> whereExpression = a => true; + + return new MessageModel>() { - if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) - { - key = ""; - } - - Expression> whereExpression = a => true; - - return new MessageModel>() - { - msg = "获取成功", - success = true, - response = await _departmentServices.QueryPage(whereExpression, page, intPageSize) - }; + msg = "获取成功", + success = true, + response = await _departmentServices.QueryPage(whereExpression, page, intPageSize) + }; + } + + [HttpGet("{id}")] + public async Task> Get(string id) + { + return new MessageModel() + { + msg = "获取成功", + success = true, + response = await _departmentServices.QueryById(id) + }; + } + + /// + /// 查询树形 Table + /// + /// 父节点 + /// 关键字 + /// + [HttpGet] + [AllowAnonymous] + public async Task>> GetTreeTable(long f = 0, string key = "") + { + List departments = new List(); + var departmentList = await _departmentServices.Query(d => d.IsDeleted == false); + if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) + { + key = ""; } - [HttpGet("{id}")] - public async Task> Get(string id) + if (key != "") { - return new MessageModel() - { - msg = "获取成功", - success = true, - response = await _departmentServices.QueryById(id) - }; + departments = departmentList.Where(a => a.Name.Contains(key)).OrderBy(a => a.OrderSort).ToList(); + } + else + { + departments = departmentList.Where(a => a.Pid == f).OrderBy(a => a.OrderSort).ToList(); } - [HttpPost] - public async Task> Post([FromBody] Department request) + foreach (var item in departments) { - var data = new MessageModel(); + List pidarr = new() { }; + var parent = departmentList.FirstOrDefault(d => d.Id == item.Pid); - var id = await _departmentServices.Add(request); - if (data.success) + while (parent != null) { - data.response = id.ObjToString(); - data.msg = "添加成功"; - } + pidarr.Add(parent.Id); + parent = departmentList.FirstOrDefault(d => d.Id == parent.Pid); + } - return data; + pidarr.Reverse(); + pidarr.Insert(0, 0); + item.PidArr = pidarr; + + item.hasChildren = departmentList.Where(d => d.Pid == item.Id).Any(); } - [HttpPut] - public async Task> Put([FromBody] Department request) + + return Success(departments, "获取成功"); + } + + /// + /// 获取部门树 + /// + /// + /// + [HttpGet] + public async Task> GetDepartmentTree(int pid = 0) + { + var departments = await _departmentServices.Query(d => d.IsDeleted == false); + var departmentTrees = (from child in departments + where child.IsDeleted == false + orderby child.Id + select new DepartmentTree + { + value = child.Id, + label = child.Name, + Pid = child.Pid, + order = child.OrderSort, + }).ToList(); + DepartmentTree rootRoot = new DepartmentTree { - var data = new MessageModel(); - data.success = await _departmentServices.Update(request); - if (data.success) - { - data.msg = "更新成功"; - data.response = request?.Id.ObjToString(); - } + value = 0, + Pid = 0, + label = "根节点" + }; + + departmentTrees = departmentTrees.OrderBy(d => d.order).ToList(); + + + RecursionHelper.LoopToAppendChildren(departmentTrees, rootRoot, pid); + + return Success(rootRoot, "获取成功"); + } + + [HttpPost] + public async Task> Post([FromBody] Department request) + { + var data = new MessageModel(); - return data; + var id = await _departmentServices.Add(request); + data.success = id > 0; + if (data.success) + { + data.response = id.ObjToString(); + data.msg = "添加成功"; } - [HttpDelete("{id}")] - public async Task> Delete(string id) + return data; + } + + [HttpPut] + public async Task> Put([FromBody] Department request) + { + var data = new MessageModel(); + data.success = await _departmentServices.Update(request); + if (data.success) { - var data = new MessageModel(); - data.success = await _departmentServices.DeleteById(id); - if (data.success) - { - data.msg = "删除成功"; - data.response = id; - } + data.msg = "更新成功"; + data.response = request?.Id.ObjToString(); + } + + return data; + } - return data; + [HttpDelete("{id}")] + public async Task> Delete(string id) + { + var data = new MessageModel(); + data.success = await _departmentServices.DeleteById(id); + if (data.success) + { + data.msg = "删除成功"; + data.response = id; } + + return data; + } } } \ No newline at end of file diff --git a/Blog.Core.Common/Helper/RecursionHelper.cs b/Blog.Core.Common/Helper/RecursionHelper.cs index 508b03df..3de28d18 100644 --- a/Blog.Core.Common/Helper/RecursionHelper.cs +++ b/Blog.Core.Common/Helper/RecursionHelper.cs @@ -52,6 +52,30 @@ public static void LoopToAppendChildren(List all, PermissionTree LoopToAppendChildren(all, subItem, pid, needbtn); } } + public static void LoopToAppendChildren(List all, DepartmentTree curItem, int pid) + { + + var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); + + if (subItems.Count > 0) + { + curItem.children = new List(); + curItem.children.AddRange(subItems); + } + else + { + curItem.children = null; + } + + foreach (var subItem in subItems) + { + if (subItem.value == pid && pid > 0) + { + //subItem.disabled = true;//禁用当前节点 + } + LoopToAppendChildren(all, subItem, pid); + } + } @@ -102,6 +126,17 @@ public class PermissionTree public List children { get; set; } public List btns { get; set; } } + + public class DepartmentTree + { + public long value { get; set; } + public long Pid { get; set; } + public string label { get; set; } + public int order { get; set; } + public bool disabled { get; set; } + public List children { get; set; } + } + public class NavigationBar { public int id { get; set; } diff --git a/Blog.Core.Common/Seed/FrameSeed.cs b/Blog.Core.Common/Seed/FrameSeed.cs index ddd273c1..89e60ded 100644 --- a/Blog.Core.Common/Seed/FrameSeed.cs +++ b/Blog.Core.Common/Seed/FrameSeed.cs @@ -194,6 +194,7 @@ public async Task> Post([FromBody] {ClassName} request) var data = new MessageModel(); var id = await _{ClassName}Services.Add(request); + data.success = id > 0; if (data.success) { data.response = id.ObjToString(); diff --git a/Blog.Core.Model/Models/Department.cs b/Blog.Core.Model/Models/Department.cs index e9cd2143..3583bcff 100644 --- a/Blog.Core.Model/Models/Department.cs +++ b/Blog.Core.Model/Models/Department.cs @@ -1,4 +1,5 @@ -using System; +using SqlSugar; +using System; namespace Blog.Core.Model.Models @@ -6,13 +7,8 @@ namespace Blog.Core.Model.Models /// /// 部门表 /// - public class Department : RootEntityTkey + public class Department : DepartmentRoot { - /// - /// Desc:父部门 - /// Nullable:True - /// - public long PId { get; set; } /// /// Desc:部门关系编码 /// Default: @@ -24,25 +20,26 @@ public class Department : RootEntityTkey /// Default: /// Nullable:True /// - public string DepartName { get; set; } + public string Name { get; set; } /// /// Desc:负责人 /// Default: /// Nullable:True /// + [SugarColumn(IsNullable = true)] public string Leader { get; set; } /// /// Desc:排序 /// Default: /// Nullable:True /// - public int? OrderNum { get; set; } = 0; + public int OrderSort { get; set; } = 0; /// /// Desc:部门状态(0正常 1停用) /// Default:0 /// Nullable:True /// - public bool Status { get; set; } + public bool Status { get; set; } = false; /// /// Desc:删除标志(0代表存在 2代表删除) /// Default:0 @@ -54,24 +51,33 @@ public class Department : RootEntityTkey /// Default: /// Nullable:True /// + [SugarColumn(IsNullable = true)] public string CreateBy { get; set; } /// /// Desc:创建时间 /// Default: /// Nullable:True /// + [SugarColumn(IsNullable = true)] public DateTime? CreateTime { get; set; } /// /// Desc:更新者 /// Default: /// Nullable:True /// + [SugarColumn(IsNullable = true)] public string ModifyBy { get; set; } /// /// Desc:更新时间 /// Default: /// Nullable:True /// + [SugarColumn(IsNullable = true)] public DateTime? ModifyTime { get; set; } + + + + [SugarColumn(IsIgnore = true)] + public bool hasChildren { get; set; } = true; } } \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/DepartmentRoot.cs b/Blog.Core.Model/Models/RootTkey/DepartmentRoot.cs new file mode 100644 index 00000000..ab10c9f8 --- /dev/null +++ b/Blog.Core.Model/Models/RootTkey/DepartmentRoot.cs @@ -0,0 +1,20 @@ +using SqlSugar; +using System; +using System.Collections.Generic; + +namespace Blog.Core.Model +{ + /// + /// 部门表 + /// + public class DepartmentRoot : RootEntityTkey where Tkey : IEquatable + { + /// + /// 上一级(0表示无上一级) + /// + public Tkey Pid { get; set; } + + [SugarColumn(IsIgnore = true)] + public List PidArr { get; set; } + } +} From b21c5ca017830a1772996a7129df803abd84740a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 1 Apr 2022 15:05:22 +0800 Subject: [PATCH 026/289] fix: change seed logic #260 about. --- .../Controllers/DbFirst/MigrateController.cs | 95 +++++++++++++++---- Blog.Core.Model/Models/Permission.cs | 6 ++ 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs index 730a9e8d..e4a23600 100644 --- a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs +++ b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -45,6 +46,58 @@ public MigrateController(IUnitOfWork unitOfWork, _env = env; } + private void InitPermissionTree(List permissions, List all, List apis) + { + foreach (var item in permissions) + { + item.Children = all.Where(d => d.Pid == item.Id).ToList(); + item.Module = apis.FirstOrDefault(d => d.Id == item.Mid); + InitPermissionTree(item.Children, all, apis); + } + } + + private async Task SavePermissionTreeAsync(List permissions, List pms, int permissionId = 0) + { + var parendId = permissionId; + + foreach (var item in permissions) + { + PM pm = new PM(); + // 保留原始主键id + pm.PidOld = item.Id; + pm.MidOld = (item.Module?.Id).ObjToInt(); + + var mid = 0; + // 接口 + if (item.Module != null) + { + var moduleModel = (await _moduleServices.Query(d => d.LinkUrl == item.Module.LinkUrl)).FirstOrDefault(); + if (moduleModel != null) + { + mid = moduleModel.Id; + } + else + { + mid = await _moduleServices.Add(item.Module); + } + pm.MidNew = mid; + Console.WriteLine($"Moudle Added:{item.Module.Name}"); + } + // 菜单 + if (item != null) + { + item.Pid = parendId; + item.Mid = mid; + permissionId = await _permissionServices.Add(item); + pm.PidNew = permissionId; + Console.WriteLine($"Permission Added:{item.Name}"); + } + pms.Add(pm); + + await SavePermissionTreeAsync(item.Children, pms, permissionId); + } + } + /// /// 获取权限部分Map数据(从库) /// 迁移到新库(主库) @@ -58,14 +111,24 @@ public async Task> DataMigrateFromOld2New() { try { - // 获取权限集合数据 + var apiList = await _moduleServices.Query(d => d.IsDeleted == false); + var permissionsList = await _permissionServices.Query(d => d.IsDeleted == false); + var permissions = permissionsList.Where(d => d.Pid == 0).ToList(); var rmps = await _roleModulePermissionServices.GetRMPMaps(); + List pms = new List(); + // 当然,你可以做个where查询 - //rmps = rmps.Where(d => d.ModuleId > 88).ToList(); + rmps = rmps.Where(d => d.PermissionId >= 114).ToList(); + + InitPermissionTree(permissions, permissionsList, apiList); + + permissions = permissions.Where(d => d.Id >= 114).ToList(); // 开启事务,保证数据一致性 _unitOfWork.BeginTran(); + await SavePermissionTreeAsync(permissions, pms); + var rid = 0; var pid = 0; var mid = 0; @@ -89,22 +152,10 @@ public async Task> DataMigrateFromOld2New() } } - // 菜单 - if (item.Permission != null) - { - pid = await _permissionServices.Add(item.Permission); - Console.WriteLine($"Permission Added:{item.Permission.Name}"); - } - - // 接口 - if (item.Module != null) - { - mid = await _moduleServices.Add(item.Module); - Console.WriteLine($"Module Added:{item.Module.LinkUrl}"); - } - + pid = (pms.FirstOrDefault(d => d.PidOld == item.PermissionId)?.PidNew).ObjToInt(); + mid = (pms.FirstOrDefault(d => d.MidOld == item.ModuleId)?.MidNew).ObjToInt(); // 关系 - if (rid > 0 && pid > 0 && mid > 0) + if (rid > 0 && pid > 0) { rpmid = await _roleModulePermissionServices.Add(new RoleModulePermission() { @@ -152,7 +203,7 @@ public async Task> SaveData2TsvAsync() var data = new MessageModel() { success = true, msg = "" }; if (_env.IsDevelopment()) { - + JsonSerializerSettings microsoftDateFormatSettings = new JsonSerializerSettings { DateFormatHandling = DateFormatHandling.MicrosoftDateFormat @@ -189,4 +240,12 @@ public async Task> SaveData2TsvAsync() } } + + public class PM + { + public int PidOld { get; set; } + public int MidOld { get; set; } + public int PidNew { get; set; } + public int MidNew { get; set; } + } } diff --git a/Blog.Core.Model/Models/Permission.cs b/Blog.Core.Model/Models/Permission.cs index a8240d1c..c650cb5e 100644 --- a/Blog.Core.Model/Models/Permission.cs +++ b/Blog.Core.Model/Models/Permission.cs @@ -113,6 +113,12 @@ public Permission() [SugarColumn(IsIgnore = true)] public bool hasChildren { get; set; } = true; + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new List(); + + [SugarColumn(IsIgnore = true)] + public Modules Module { get; set; } + //public virtual ICollection ModulePermission { get; set; } //public virtual ICollection RoleModulePermission { get; set; } } From 455ae0bfbe8ce25cabf4a2e21cd63ced6bbbe34f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 1 Apr 2022 17:23:03 +0800 Subject: [PATCH 027/289] fix: produce department data --- .../Controllers/DepartmentController.cs | 37 ++++++++++++++++++- .../wwwroot/BlogCore.Data.json/Department.tsv | 1 + Blog.Core.Common/Seed/DBSeed.cs | 14 +++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Blog.Core.Api/wwwroot/BlogCore.Data.json/Department.tsv diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index d865ea88..9ae0ebae 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -4,11 +4,16 @@ using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Text; using System.Threading.Tasks; namespace Blog.Core.Api.Controllers @@ -19,10 +24,12 @@ namespace Blog.Core.Api.Controllers public class DepartmentController : BaseApiController { private readonly IDepartmentServices _departmentServices; + private readonly IWebHostEnvironment _env; - public DepartmentController(IDepartmentServices departmentServices) + public DepartmentController(IDepartmentServices departmentServices, IWebHostEnvironment env) { _departmentServices = departmentServices; + _env = env; } [HttpGet] @@ -180,5 +187,33 @@ public async Task> Delete(string id) return data; } + + [HttpGet] + [AllowAnonymous] + public async Task> SaveData2Tsv() + { + var data = new MessageModel() { success = true, msg = "" }; + if (_env.IsDevelopment()) + { + + JsonSerializerSettings microsoftDateFormatSettings = new JsonSerializerSettings + { + DateFormatHandling = DateFormatHandling.MicrosoftDateFormat + }; + + var rolesJson = JsonConvert.SerializeObject(await _departmentServices.Query(d => d.IsDeleted == false), microsoftDateFormatSettings); + FileHelper.WriteFile(Path.Combine(_env.WebRootPath, "BlogCore.Data.json", "Department_New.tsv"), rolesJson, Encoding.UTF8); + + data.success = true; + data.msg = "生成成功!"; + } + else + { + data.success = false; + data.msg = "当前不处于开发模式,代码生成不可用!"; + } + + return data; + } } } \ No newline at end of file diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Department.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Department.tsv new file mode 100644 index 00000000..dfb887b1 --- /dev/null +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Department.tsv @@ -0,0 +1 @@ +[{"CodeRelationship":"0,","Name":"BCVP开发社区","Leader":"老张的哲学","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:47:25","ModifyTime":"2022-04-01 15:47:25","hasChildren":true,"Pid":0,"Id":1},{"CodeRelationship":"0,","Name":"DDD思想社区组织","Leader":"DDD","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:48:08","ModifyTime":"2022-04-01 15:48:08","hasChildren":true,"Pid":0,"Id":2},{"CodeRelationship":"0,1,","Name":"BCVP-北京分部","Leader":"北京","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:48:41","ModifyTime":"2022-04-01 15:48:41","hasChildren":true,"Pid":1,"Id":3},{"CodeRelationship":"0,1,","Name":"BCVP-上海分部","Leader":"上海","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:49:27","ModifyTime":"2022-04-01 15:49:27","hasChildren":true,"Pid":1,"Id":4},{"CodeRelationship":"0,1,","Name":"BCVP-广州分部","Leader":"广州","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:50:23","ModifyTime":"2022-04-01 15:50:44","hasChildren":true,"Pid":1,"Id":5},{"CodeRelationship":"0,1,3,","Name":"前端小组(1群)","Leader":"--","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:51:43","ModifyTime":"2022-04-01 15:51:43","hasChildren":true,"Pid":3,"Id":6},{"CodeRelationship":"0,1,4,","Name":"后端小组(2群)","Leader":"--","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 15:58:13","ModifyTime":"2022-04-01 15:58:13","hasChildren":true,"Pid":4,"Id":7},{"CodeRelationship":"0,","Name":"VUE学习联盟","Leader":"vue","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 16:14:21","ModifyTime":"2022-04-01 16:14:21","hasChildren":true,"Pid":0,"Id":8},{"CodeRelationship":"0,8,","Name":"ES指导(1组)","Leader":"es","OrderSort":0,"Status":true,"IsDeleted":false,"CreateBy":"老张的哲学","CreateTime":"2022-04-01 16:14:47","ModifyTime":"2022-04-01 16:15:00","hasChildren":true,"Pid":8,"Id":9}] \ No newline at end of file diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 11fcdae5..a3fa7e00 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -281,6 +281,20 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) } #endregion + #region Department + if (!await myContext.Db.Queryable().AnyAsync()) + { + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Department"), Encoding.UTF8), setting); + + myContext.GetEntityDB().InsertRange(data); + Console.WriteLine("Table:Department created success!"); + } + else + { + Console.WriteLine("Table:Department already exists..."); + } + #endregion + ConsoleHelper.WriteSuccessLine($"Done seeding database!"); } From f8c569ebd7ea8293de483a7a941eca6d683f1c00 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 1 Apr 2022 17:42:55 +0800 Subject: [PATCH 028/289] fix: update ui.zip --- .../wwwroot/BlogCore.Data.json/Permission.tsv | 2 +- Blog.Core.Api/wwwroot/ui.zip | Bin 5244445 -> 5263957 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index 37896ab0..d96fbc8a 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -25,7 +25,7 @@ "IsButton": 0, "Pid": 0, "Mid": 0, - "OrderSort": -80, + "OrderSort": 0, "Icon": "fa-users", "Description": "11", "Enabled": 1, diff --git a/Blog.Core.Api/wwwroot/ui.zip b/Blog.Core.Api/wwwroot/ui.zip index 025010ddf356f964de80fd344811ac9d2c590838..0e968aa0a8c8dc3e4e3f8baf3f53f7e27e28b695 100644 GIT binary patch delta 431536 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 @vFcT|;s=vyUc?D4^ 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%0Da+<>jA4SJXTDqv|2V3tc8sNscG+Q(5 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 tyL~Fa-BjdLTW}95B@xwv+(s{{bd=H4Xp( From 9fda8ff78b0564c758b8abaceda995d46f82620b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 4 Apr 2022 19:07:26 +0800 Subject: [PATCH 029/289] fix: change test project data --- Blog.Core.Tests/WMBlog.db | Bin 192512 -> 200704 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Blog.Core.Tests/WMBlog.db b/Blog.Core.Tests/WMBlog.db index 6b7702ff555d8d591666d1fd9b8c27f96e976123..044a620194889190494c7d17ebbab62233f460e8 100644 GIT binary patch delta 11832 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 SO3?)F(;repa|Xc~@;?DET5$jX delta 5037 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 From 18f012ca16181bf6785dd9e2d5b816aaa038b1aa Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 4 Apr 2022 20:00:17 +0800 Subject: [PATCH 030/289] fix: program.cs for 6.0 --- Blog.Core.Api/Blog.Core.Api.csproj | 2 +- Blog.Core.Api/Blog.Core.xml | 6 - Blog.Core.Api/Program.cs | 250 +++++++++++++----- Blog.Core.Common/Helper/JsonConfigUtils.cs | 46 ---- .../ServiceExtensions/NacosSetup.cs | 3 - .../DependencyInjection/DI_Test.cs | 2 +- 6 files changed, 181 insertions(+), 128 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index a9499b1a..9c221839 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -5,7 +5,7 @@ Exe net6.0 - + enable OutOfProcess Linux true diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 730ddfd4..0c743f75 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1274,11 +1274,5 @@ 全局路由前缀公约

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%0DADvCo`a)t}AcY4OIqi#BH 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 VF9GSHNhsI@E$>eyv+0a}tJ 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 UyK#ZL@ab;bpWOOFz_q-L8Jog&X%XPf_i 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 delta 412313 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̱xCH9)`xo%&`wkm=oSjLLq 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%0DO+x|Z(M7A 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%0A45B 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

- - - 根据环境变量定向配置文件名称 - - - diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 9e416297..1e55871b 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -1,74 +1,182 @@ -using Autofac.Extensions.DependencyInjection; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Blog.Core; +using Blog.Core.Common; +using Blog.Core.Common.LogHelper; +using Blog.Core.Common.Seed; +using Blog.Core.Extensions; using Blog.Core.Extensions.Apollo; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.IO; - -namespace Blog.Core +using Blog.Core.Extensions.Middlewares; +using Blog.Core.Filter; +using Blog.Core.Hubs; +using Blog.Core.IServices; +using Blog.Core.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +// 1、配置host与容器 +builder.Host +.UseServiceProviderFactory(new AutofacServiceProviderFactory()) +.ConfigureContainer(builder => +{ + builder.RegisterModule(new AutofacModuleRegister()); + builder.RegisterModule(); +}) +.ConfigureLogging((hostingContext, builder) => { - public class Program - { - public static void Main(string[] args) - { - //初始化默认主机Builder - Host.CreateDefaultBuilder(args) - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder - .UseStartup() - .ConfigureAppConfiguration((hostingContext, config) => - { - config.Sources.Clear(); - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - //.AddJsonFile($"appsettings{ GetAppSettingsConfigName() }json", optional: true, reloadOnChange: false) - ; - //接入Apollo配置中心 - config.AddConfigurationApollo("appsettings.apollo.json"); - }) - .UseUrls("http://*:9291") - .ConfigureLogging((hostingContext, builder) => - { - // 1.过滤掉系统默认的一些日志 - builder.AddFilter("System", LogLevel.Error); - builder.AddFilter("Microsoft", LogLevel.Error); - - // 2.也可以在appsettings.json中配置,LogLevel节点 - - // 3.统一设置 - builder.SetMinimumLevel(LogLevel.Error); - - // 默认log4net.confg - builder.AddLog4Net(Path.Combine(Directory.GetCurrentDirectory(), "Log4net.config")); - }) - ; - }) - // 生成承载 web 应用程序的 Microsoft.AspNetCore.Hosting.IWebHost。Build是WebHostBuilder最终的目的,将返回一个构造的WebHost,最终生成宿主。 - .Build() - // 运行 web 应用程序并阻止调用线程, 直到主机关闭。 - // ※※※※ 有异常,查看 Log 文件夹下的异常日志 ※※※※ - .Run(); - } - - - /// - /// 根据环境变量定向配置文件名称 - /// - /// - private static string GetAppSettingsConfigName() - { - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != null - && Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "") - { - return $".{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}."; - } - else - { - return "."; - } - } - } + builder.AddFilter("System", LogLevel.Error); + builder.AddFilter("Microsoft", LogLevel.Error); + builder.SetMinimumLevel(LogLevel.Error); + builder.AddLog4Net(Path.Combine(Directory.GetCurrentDirectory(), "Log4net.config")); +}) +.ConfigureAppConfiguration((hostingContext, config) => +{ + config.Sources.Clear(); + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); + config.AddConfigurationApollo("appsettings.apollo.json"); +}); + + +// 2、配置服务 +builder.Services.AddSingleton(new Appsettings(builder.Configuration)); +builder.Services.AddSingleton(new LogLock(builder.Environment.ContentRootPath)); +builder.Services.AddUiFilesZipSetup(builder.Environment); + +Permissions.IsUseIds4 = Appsettings.app(new string[] { "Startup", "IdentityServer4", "Enabled" }).ObjToBool(); +RoutePrefix.Name = Appsettings.app(new string[] { "AppSettings", "SvcName" }).ObjToString(); + +JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + +builder.Services.AddMemoryCacheSetup(); +builder.Services.AddRedisCacheSetup(); +builder.Services.AddSqlsugarSetup(); +builder.Services.AddDbSetup(); +builder.Services.AddAutoMapperSetup(); +builder.Services.AddCorsSetup(); +builder.Services.AddMiniProfilerSetup(); +builder.Services.AddSwaggerSetup(); +builder.Services.AddJobSetup(); +builder.Services.AddHttpContextSetup(); +builder.Services.AddAppTableConfigSetup(builder.Environment); +builder.Services.AddHttpApi(); +builder.Services.AddRedisInitMqSetup(); +builder.Services.AddRabbitMQSetup(); +builder.Services.AddKafkaSetup(builder.Configuration); +builder.Services.AddEventBusSetup(); +builder.Services.AddNacosSetup(builder.Configuration); + +builder.Services.AddAuthorizationSetup(); +if (Permissions.IsUseIds4) +{ + builder.Services.AddAuthentication_Ids4Setup(); +} +else +{ + builder.Services.AddAuthentication_JWTSetup(); } + +builder.Services.AddIpPolicyRateLimitSetup(builder.Configuration); +builder.Services.AddSignalR().AddNewtonsoftJsonProtocol(); +builder.Services.AddScoped(); +builder.Services.Configure(x => x.AllowSynchronousIO = true) + .Configure(x => x.AllowSynchronousIO = true); + +builder.Services.AddDistributedMemoryCache(); +builder.Services.AddSession(); +builder.Services.AddHttpPollySetup(); +builder.Services.AddControllers(o => +{ + o.Filters.Add(typeof(GlobalExceptionsFilter)); + //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); + o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); +}) +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); +}); +builder.Services.AddEndpointsApiExplorer(); + +builder.Services.Replace(ServiceDescriptor.Transient()); +Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + +// 3、配置中间件 +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else +{ + app.UseExceptionHandler("/Error"); + //app.UseHsts(); +} + +app.UseIpLimitMiddle(); +app.UseRequestResponseLogMiddle(); +app.UseRecordAccessLogsMiddle(); +app.UseSignalRSendMiddle(); +app.UseIpLogMiddle(); +app.UseAllServicesMiddle(builder.Services); + +app.UseSession(); +app.UseSwaggerAuthorized(); +app.UseSwaggerMiddle(() => Assembly.GetExecutingAssembly().GetManifestResourceStream("Blog.Core.Api.index.html")); + +app.UseCors(Appsettings.app(new string[] { "Startup", "Cors", "PolicyName" })); +DefaultFilesOptions defaultFilesOptions = new DefaultFilesOptions(); +defaultFilesOptions.DefaultFileNames.Clear(); +defaultFilesOptions.DefaultFileNames.Add("index.html"); +app.UseDefaultFiles(defaultFilesOptions); +app.UseStaticFiles(); +app.UseCookiePolicy(); +app.UseStatusCodePages(); +app.UseRouting(); + +if (builder.Configuration.GetValue("AppSettings:UseLoadTest")) +{ + app.UseMiddleware(); +} +app.UseAuthentication(); +app.UseAuthorization(); +app.UseMiniProfilerMiddleware(); +//app.UseExceptionHandlerMidd(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + + endpoints.MapHub("/api2/chatHub"); +}); + + +var scope = app.Services.GetRequiredService().CreateScope(); +var myContext = scope.ServiceProvider.GetRequiredService(); +var tasksQzServices = scope.ServiceProvider.GetRequiredService(); +var schedulerCenter = scope.ServiceProvider.GetRequiredService(); +var lifetime = scope.ServiceProvider.GetRequiredService(); +app.UseSeedDataMiddle(myContext, builder.Environment.WebRootPath); +app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); +app.UseConsulMiddle(builder.Configuration, lifetime); +app.ConfigureEventBus(); + +// 4、运行 +app.Run(); diff --git a/Blog.Core.Common/Helper/JsonConfigUtils.cs b/Blog.Core.Common/Helper/JsonConfigUtils.cs index 38ceb46e..3986a698 100644 --- a/Blog.Core.Common/Helper/JsonConfigUtils.cs +++ b/Blog.Core.Common/Helper/JsonConfigUtils.cs @@ -63,52 +63,6 @@ public static string GetJson(string jsonPath, string key) } } - - /// - /// 配置文件管理器 - /// - public interface IConfigurationManager - { - T GetAppConfig(string key, T defaultValue = default(T)); - } - - /// - /// 配置读取 根据环境变量 - /// - public class ConfigurationManager : IConfigurationManager - { - private readonly IConfigurationRoot config; - - public ConfigurationManager(IConfigurationRoot _config) - { - config = _config; - } - - public T GetAppConfig(string key, T defaultValue = default(T)) - { - T value = default(T); - try - { - var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env}.json", true, false) - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - value = (T)Convert.ChangeType(configuration[key], typeof(T)); - if (value == null) - value = defaultValue; - } - catch (Exception) - { - value = defaultValue; - } - - return value; - } - } - - #region Nacos 配置清单 public class JsonConfigSettings { diff --git a/Blog.Core.Extensions/ServiceExtensions/NacosSetup.cs b/Blog.Core.Extensions/ServiceExtensions/NacosSetup.cs index cf1ee933..c8863323 100644 --- a/Blog.Core.Extensions/ServiceExtensions/NacosSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/NacosSetup.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using Nacos.V2.DependencyInjection; using System; -using ConfigurationManager = Blog.Core.Common.Helper.ConfigurationManager; namespace Blog.Core.Extensions { @@ -48,9 +47,7 @@ public static void AddNacosSetup(this IServiceCollection services, IConfiguratio services.AddHostedService();//增加配置文件监听事件 } - services.AddSingleton(new ConfigurationManager((ConfigurationRoot)Configuration)); services.AddSingleton(Configuration); - } } } diff --git a/Blog.Core.Tests/DependencyInjection/DI_Test.cs b/Blog.Core.Tests/DependencyInjection/DI_Test.cs index 23e46bf6..fe12e2fe 100644 --- a/Blog.Core.Tests/DependencyInjection/DI_Test.cs +++ b/Blog.Core.Tests/DependencyInjection/DI_Test.cs @@ -110,7 +110,7 @@ public IContainer DICollections() // 属性注入 var controllerBaseType = typeof(ControllerBase); - builder.RegisterAssemblyTypes(typeof(Program).Assembly) + builder.RegisterAssemblyTypes(typeof(Startup).Assembly) .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType) .PropertiesAutowired(); From 3772aa643ad32f14e1983cf856ea316e41d543bd Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Mon, 4 Apr 2022 21:03:23 +0800 Subject: [PATCH 031/289] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b73c7c83..93c37844 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 新增 - Apollo 配置; - [x] 新增 Kafka 消息队列,并配合实现EventBus ✨; - [x] 新增 微信公众号管理,并集成到Blog.Admin后台 ✨; -- [ ] 计划 - 数据部门权限; +- [x] 新增 - 数据部门权限; 微服务模块: - [x] 可配合 Docker 实现容器化; From 93361ce67df07dfefac2d747b42e5fa44df41b84 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 7 Apr 2022 19:39:18 +0800 Subject: [PATCH 032/289] fix: GetNavigationBarPro --- Blog.Core.Api/Blog.Core.xml | 7 ++ .../Controllers/PermissionController.cs | 83 +++++++++++++- .../wwwroot/BlogCore.Data.json/Modules.tsv | 22 ++++ .../wwwroot/BlogCore.Data.json/Permission.tsv | 26 ++++- .../RoleModulePermission.tsv | 13 +++ Blog.Core.Common/Blog.Core.Common.csproj | 1 + Blog.Core.Common/Helper/PingYinHelper.cs | 108 ++++++++++++++++++ Blog.Core.Common/Helper/RecursionHelper.cs | 23 ++++ 8 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 Blog.Core.Common/Helper/PingYinHelper.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 0c743f75..add15ba1 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -414,6 +414,13 @@ + + + 获取路由树【PRO】 + + + + 通过角色获取菜单【无权限】 diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 316f20ba..01121063 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Blog.Core.AuthHelper; +using Blog.Core.AuthHelper; using Blog.Core.AuthHelper.OverWrite; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; @@ -10,7 +6,6 @@ using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Blog.Core.Controllers @@ -442,6 +437,82 @@ orderby child.Id return data; } + /// + /// 获取路由树【PRO】 + /// + /// + /// + [HttpGet] + public async Task>> GetNavigationBarPro(int uid) + { + var data = new MessageModel>(); + + var uidInHttpcontext1 = 0; + var roleIds = new List(); + // ids4和jwt切换 + if (Permissions.IsUseIds4) + { + // ids4 + uidInHttpcontext1 = (from item in _httpContext.HttpContext.User.Claims + where item.Type == "sub" + select item.Value).FirstOrDefault().ObjToInt(); + roleIds = (from item in _httpContext.HttpContext.User.Claims + where item.Type == "role" + select item.Value.ObjToInt()).ToList(); + } + else + { + // jwt + uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); + roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToInt()).Distinct().ToList(); + } + + if (uid > 0 && uid == uidInHttpcontext1) + { + if (roleIds.Any()) + { + var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))) + .Select(d => d.PermissionId.ObjToInt()).Distinct(); + if (pids.Any()) + { + var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id) && d.IsButton == false)).OrderBy(c => c.OrderSort); + var permissionTrees = (from item in rolePermissionMoudles + where item.IsDeleted == false + orderby item.Id + select new NavigationBarPro + { + id = item.Id, + name = item.Name, + parentId = item.Pid, + order = item.OrderSort, + path = item.Code == "-" ? item.Name.GetTotalPingYin().FirstOrDefault() : (item.Code == "/" ? "/dashboard/workplace" : item.Code), + component = item.Pid == 0 ? (item.Code == "/" ? "dashboard/Workplace" : "RouteView") : item.Code?.TrimStart('/'), + iconCls = item.Icon, + Func = item.Func, + IsHide = item.IsHide.ObjToBool(), + IsButton = item.IsButton.ObjToBool(), + meta = new NavigationBarMetaPro + { + show = true, + title = item.Name, + icon = "user"//item.Icon + } + }).ToList(); + + permissionTrees = permissionTrees.OrderBy(d => d.order).ToList(); + + data.success = true; + if (data.success) + { + data.response = permissionTrees; + data.msg = "获取成功"; + } + } + } + } + return data; + } + /// /// 通过角色获取菜单【无权限】 /// diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv index 9c82d93b..a906c91b 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv @@ -1491,5 +1491,27 @@ "ModifyTime": "2022-03-23 00:00:00", "ParentId": 0, "Id": 70 + }, + { + "IsDeleted": 0, + "ParentId": null, + "Name": "Get导航条Pro", + "LinkUrl": "\/api\/permission\/GetNavigationBarPro", + "Area": null, + "Controller": null, + "Action": null, + "Icon": null, + "Code": null, + "OrderSort": 0, + "Description": null, + "IsMenu": 0, + "Enabled": 1, + "CreateId": 23, + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "Id": 71 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index d96fbc8a..67eb6975 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -140,10 +140,10 @@ "IsHide": 0 }, { - "Code": "\/Thanks", - "Name": "致谢页", + "Code": "\/System\/BasicSetting", + "Name": "个人设置", "IsButton": 0, - "Pid": 0, + "Pid": 68, "Mid": 0, "OrderSort": 5, "Icon": "fa-star ", @@ -2530,5 +2530,25 @@ "Pid": 115, "Mid": 70, "Id": 120 + }, + { + "Code": " ", + "Name": "左侧导航Pro", + "IsButton": 1, + "Pid": 7, + "Mid": 71, + "OrderSort": 0, + "Icon": null, + "Description": null, + "Enabled": 1, + "CreateId": 23, + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "IsDeleted": 0, + "Id": 121, + "IsHide": 1 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv index 48d5c2c1..a16c2eab 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv @@ -1635,5 +1635,18 @@ "ModuleId": 70, "PermissionId": 120, "Id": 127 + }, + { + "IsDeleted": 0, + "RoleId": 4, + "ModuleId": 71, + "PermissionId": 121, + "CreateId": null, + "CreateBy": null, + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "Id": 128 } ] diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index 1887b4eb..d5943cc2 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -15,6 +15,7 @@ + diff --git a/Blog.Core.Common/Helper/PingYinHelper.cs b/Blog.Core.Common/Helper/PingYinHelper.cs new file mode 100644 index 00000000..5bf1c1e4 --- /dev/null +++ b/Blog.Core.Common/Helper/PingYinHelper.cs @@ -0,0 +1,108 @@ +using Microsoft.International.Converters.PinYinConverter; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Blog.Core.Common.Helper +{ + /// + /// 汉字转换拼音 + /// + public static class PingYinUtil + { + private static Dictionary> GetTotalPingYinDictionary(string text) + { + var chs = text.ToCharArray(); + + //记录每个汉字的全拼 + Dictionary> totalPingYinList = new Dictionary>(); + + for (int i = 0; i < chs.Length; i++) + { + var pinyinList = new List(); + + //是否是有效的汉字 + if (ChineseChar.IsValidChar(chs[i])) + { + ChineseChar cc = new ChineseChar(chs[i]); + pinyinList = cc.Pinyins.Where(p => !string.IsNullOrWhiteSpace(p)).ToList(); + } + else + { + pinyinList.Add(chs[i].ToString()); + } + + //去除声调,转小写 + pinyinList = pinyinList.ConvertAll(p => Regex.Replace(p, @"\d", "").ToLower()); + + //去重 + pinyinList = pinyinList.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct().ToList(); + if (pinyinList.Any()) + { + totalPingYinList[i] = pinyinList; + } + } + + return totalPingYinList; + } + /// + /// 获取汉语拼音全拼 + /// + /// The string. + /// + public static List GetTotalPingYin(this string text) + { + var result = new List(); + foreach (var pys in GetTotalPingYinDictionary(text)) + { + var items = pys.Value; + if (result.Count <= 0) + { + result = items; + } + else + { + //全拼循环匹配 + var newTotalPingYinList = new List(); + foreach (var totalPingYin in result) + { + newTotalPingYinList.AddRange(items.Select(item => totalPingYin + item)); + } + newTotalPingYinList = newTotalPingYinList.Distinct().ToList(); + result = newTotalPingYinList; + } + } + return result; + } + + /// + /// 获取汉语拼音首字母 + /// + /// + /// + public static List GetFirstPingYin(this string text) + { + var result = new List(); + foreach (var pys in GetTotalPingYinDictionary(text)) + { + var items = pys.Value; + if (result.Count <= 0) + { + result = items.ConvertAll(p => p.Substring(0, 1)).Distinct().ToList(); + } + else + { + //首字母循环匹配 + var newFirstPingYinList = new List(); + foreach (var firstPingYin in result) + { + newFirstPingYinList.AddRange(items.Select(item => firstPingYin + item.Substring(0, 1))); + } + newFirstPingYinList = newFirstPingYinList.Distinct().ToList(); + result = newFirstPingYinList; + } + } + return result; + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/RecursionHelper.cs b/Blog.Core.Common/Helper/RecursionHelper.cs index 3de28d18..9b27a37d 100644 --- a/Blog.Core.Common/Helper/RecursionHelper.cs +++ b/Blog.Core.Common/Helper/RecursionHelper.cs @@ -161,4 +161,27 @@ public class NavigationBarMeta } + + + public class NavigationBarPro + { + public int id { get; set; } + public int parentId { get; set; } + public int order { get; set; } + public string name { get; set; } + public bool IsHide { get; set; } = false; + public bool IsButton { get; set; } = false; + public string path { get; set; } + public string component { get; set; } + public string Func { get; set; } + public string iconCls { get; set; } + public NavigationBarMetaPro meta { get; set; } + } + + public class NavigationBarMetaPro + { + public string title { get; set; } + public string icon { get; set; } + public bool show { get; set; } = false; + } } From 3ade35f4ddccfff6be89e2aadbe74182e67761be Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 10 Apr 2022 08:45:56 +0800 Subject: [PATCH 033/289] fix: department set --- Blog.Core.Api/Blog.Core.Model.xml | 46 ++++++++++++ Blog.Core.Api/Blog.Core.xml | 2 +- Blog.Core.Api/Controllers/UserController.cs | 31 +++++++- Blog.Core.Api/Program.cs | 4 +- Blog.Core.Api/Program.five.cs | 75 +++++++++++++++++++ .../CustomEnums/AuthorityScopeEnum.cs | 30 ++++++++ Blog.Core.Model/Models/Role.cs | 11 +++ Blog.Core.Model/Models/sysUserInfo.cs | 9 +++ Blog.Core.Model/ViewModels/SysUserInfoDto.cs | 3 + 9 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 Blog.Core.Api/Program.five.cs create mode 100644 Blog.Core.Model/CustomEnums/AuthorityScopeEnum.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 41ab4514..8437e227 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -4,6 +4,36 @@ Blog.Core.Model + + + 无任何权限 + + + + + 自定义权限 + + + + + 本部门 + + + + + 本部门及以下 + + + + + 仅自己 + + + + + 所有 + + 以下model 来自ids4项目,多库模式,为了调取ids4数据 @@ -651,6 +681,17 @@ 排序 + + + 自定义权限的部门ids + + + + + 权限范围 + -1 无任何权限;1 自定义权限;2 本部门;3 本部门及以下;4 仅自己;9 全部; + + 是否激活 @@ -751,6 +792,11 @@ 状态 + + + 部门 + + 备注 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index add15ba1..27a3c815 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -640,7 +640,7 @@ 用户管理 - + 构造函数 diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index d26146d1..9d713026 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -29,9 +29,11 @@ public class UserController : BaseApiController readonly ISysUserInfoServices _sysUserInfoServices; readonly IUserRoleServices _userRoleServices; readonly IRoleServices _roleServices; + private readonly IDepartmentServices _departmentServices; private readonly IUser _user; private readonly IMapper _mapper; private readonly ILogger _logger; + private string fullName; /// /// 构造函数 @@ -44,13 +46,16 @@ public class UserController : BaseApiController /// /// public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoServices, - IUserRoleServices userRoleServices, IRoleServices roleServices, + IUserRoleServices userRoleServices, + IRoleServices roleServices, + IDepartmentServices departmentServices, IUser user, IMapper mapper, ILogger logger) { _unitOfWork = unitOfWork; _sysUserInfoServices = sysUserInfoServices; _userRoleServices = userRoleServices; _roleServices = roleServices; + _departmentServices = departmentServices; _user = user; _mapper = mapper; _logger = logger; @@ -81,6 +86,7 @@ public async Task>> Get(int page = 1, str // 这里可以封装到多表查询,此处简单处理 var allUserRoles = await _userRoleServices.Query(d => d.IsDeleted == false); var allRoles = await _roleServices.Query(d => d.IsDeleted == false); + var allDepartments = await _departmentServices.Query(d => d.IsDeleted == false); var sysUserInfos = data.data; foreach (var item in sysUserInfos) @@ -88,6 +94,12 @@ public async Task>> Get(int page = 1, str var currentUserRoles = allUserRoles.Where(d => d.UserId == item.Id).Select(d => d.RoleId).ToList(); item.RIDs = currentUserRoles; item.RoleNames = allRoles.Where(d => currentUserRoles.Contains(d.Id)).Select(d => d.Name).ToList(); + List dids = new List(); + fullName = ""; + var departmentName = GetFullDepartmentName(allDepartments, item.DepartmentId, dids); + item.DepartmentName = departmentName; + dids.Insert(0, 0); + item.Dids = dids; } data.data = sysUserInfos; @@ -98,6 +110,21 @@ public async Task>> Get(int page = 1, str } + private string GetFullDepartmentName(List departments, int departmentId, List dids) + { + var departmentModel = departments.FirstOrDefault(d => d.Id == departmentId); + if (departmentModel == null) + { + return fullName; + } + + fullName = $"{departmentModel.Name}/{fullName}"; + dids.Insert(0, departmentModel.Id); + GetFullDepartmentName(departments, departmentModel.Pid, dids); + + return fullName; + } + // GET: api/User/5 [HttpGet("{id}")] [AllowAnonymous] @@ -127,7 +154,7 @@ public async Task> GetInfoByToken(string token) var userinfo = await _sysUserInfoServices.QueryById(tokenModel.Uid); if (userinfo != null) { - data.response = _mapper.Map(userinfo); + data.response = _mapper.Map(userinfo); data.success = true; data.msg = "获取成功"; } diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 1e55871b..f9dda121 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -1,4 +1,6 @@ -using Autofac; + +// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 +using Autofac; using Autofac.Extensions.DependencyInjection; using Blog.Core; using Blog.Core.Common; diff --git a/Blog.Core.Api/Program.five.cs b/Blog.Core.Api/Program.five.cs new file mode 100644 index 00000000..900c9495 --- /dev/null +++ b/Blog.Core.Api/Program.five.cs @@ -0,0 +1,75 @@ +//using Autofac.Extensions.DependencyInjection; +//using Blog.Core.Extensions.Apollo; +//using Microsoft.AspNetCore.Hosting; +//using Microsoft.Extensions.Configuration; +//using Microsoft.Extensions.Hosting; +//using Microsoft.Extensions.Logging; +//using System; +//using System.IO; + +// 这是asp.net5.0的写法,如果用5.0,请用本文件代码替换Program.cs代码 +//namespace Blog.Core +//{ +// public class Program +// { +// public static void Main(string[] args) +// { +// //初始化默认主机Builder +// Host.CreateDefaultBuilder(args) +// .UseServiceProviderFactory(new AutofacServiceProviderFactory()) +// .ConfigureWebHostDefaults(webBuilder => +// { +// webBuilder +// .UseStartup() +// .ConfigureAppConfiguration((hostingContext, config) => +// { +// config.Sources.Clear(); +// config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) +// //.AddJsonFile($"appsettings{ GetAppSettingsConfigName() }json", optional: true, reloadOnChange: false) +// ; +// //接入Apollo配置中心 +// config.AddConfigurationApollo("appsettings.apollo.json"); +// }) +// .UseUrls("http://*:9291") +// .ConfigureLogging((hostingContext, builder) => +// { +// // 1.过滤掉系统默认的一些日志 +// builder.AddFilter("System", LogLevel.Error); +// builder.AddFilter("Microsoft", LogLevel.Error); + +// // 2.也可以在appsettings.json中配置,LogLevel节点 + +// // 3.统一设置 +// builder.SetMinimumLevel(LogLevel.Error); + +// // 默认log4net.confg +// builder.AddLog4Net(Path.Combine(Directory.GetCurrentDirectory(), "Log4net.config")); +// }) +// ; +// }) +// // 生成承载 web 应用程序的 Microsoft.AspNetCore.Hosting.IWebHost。Build是WebHostBuilder最终的目的,将返回一个构造的WebHost,最终生成宿主。 +// .Build() +// // 运行 web 应用程序并阻止调用线程, 直到主机关闭。 +// // ※※※※ 有异常,查看 Log 文件夹下的异常日志 ※※※※ +// .Run(); +// } + + +// /// +// /// 根据环境变量定向配置文件名称 +// /// +// /// +// private static string GetAppSettingsConfigName() +// { +// if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != null +// && Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "") +// { +// return $".{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}."; +// } +// else +// { +// return "."; +// } +// } +// } +//} diff --git a/Blog.Core.Model/CustomEnums/AuthorityScopeEnum.cs b/Blog.Core.Model/CustomEnums/AuthorityScopeEnum.cs new file mode 100644 index 00000000..422b2c56 --- /dev/null +++ b/Blog.Core.Model/CustomEnums/AuthorityScopeEnum.cs @@ -0,0 +1,30 @@ +namespace Blog.Core.Model +{ + public enum AuthorityScopeEnum + { + /// + /// 无任何权限 + /// + NONE = -1, + /// + /// 自定义权限 + /// + Custom = 1, + /// + /// 本部门 + /// + MyDepart = 2, + /// + /// 本部门及以下 + /// + MyDepartAndDown = 3, + /// + /// 仅自己 + /// + OnlySelf = 4, + /// + /// 所有 + /// + ALL = 9 + } +} diff --git a/Blog.Core.Model/Models/Role.cs b/Blog.Core.Model/Models/Role.cs index 0d537837..e34ccdd9 100644 --- a/Blog.Core.Model/Models/Role.cs +++ b/Blog.Core.Model/Models/Role.cs @@ -46,6 +46,17 @@ public Role(string name) /// public int OrderSort { get; set; } /// + /// 自定义权限的部门ids + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string Dids { get; set; } + /// + /// 权限范围 + /// -1 无任何权限;1 自定义权限;2 本部门;3 本部门及以下;4 仅自己;9 全部; + /// + [SugarColumn(IsNullable = true)] + public int AuthorityScope { get; set; } = -1; + /// /// 是否激活 /// public bool Enabled { get; set; } diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index d8ffeee3..5b4b8b84 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -45,6 +45,11 @@ public SysUserInfo(string loginName, string loginPWD) /// public int Status { get; set; } /// + /// 部门 + /// + [SugarColumn(IsNullable = true)] + public int DepartmentId { get; set; } = -1; + /// /// 备注 /// [SugarColumn(Length = 2000, IsNullable = true)] @@ -94,6 +99,10 @@ public SysUserInfo(string loginName, string loginPWD) [SugarColumn(IsIgnore = true)] public List RoleNames { get; set; } + [SugarColumn(IsIgnore = true)] + public List Dids { get; set; } + [SugarColumn(IsIgnore = true)] + public string DepartmentName { get; set; } } } diff --git a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs index 9a9e8325..33d84161 100644 --- a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs +++ b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs @@ -9,6 +9,7 @@ public class SysUserInfoDto : SysUserInfoDtoRoot public string uLoginPWD { get; set; } public string uRealName { get; set; } public int uStatus { get; set; } + public int DepartmentId { get; set; } public string uRemark { get; set; } public System.DateTime uCreateTime { get; set; } = DateTime.Now; public System.DateTime uUpdateTime { get; set; } = DateTime.Now; @@ -21,5 +22,7 @@ public class SysUserInfoDto : SysUserInfoDtoRoot public string addr { get; set; } public bool tdIsDelete { get; set; } public List RoleNames { get; set; } + public List Dids { get; set; } + public string DepartmentName { get; set; } } } From 7c0755657665628c2caaefaf6acd580234cfce36 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 10 Apr 2022 08:58:52 +0800 Subject: [PATCH 034/289] Fixed #265 bug. --- Blog.Core.Api/Controllers/UserController.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 9d713026..61e2214d 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -207,16 +207,20 @@ public async Task> Put([FromBody] SysUserInfoDto sysUserInf if (sysUserInfo != null && sysUserInfo.uID > 0) { - if (sysUserInfo.RIDs.Count > 0) + // 无论 Update Or Add , 先删除当前用户的全部 U_R 关系 + var usreroles = (await _userRoleServices.Query(d => d.UserId == sysUserInfo.uID)).Select(d => d.Id.ToString()).ToArray(); + if (usreroles.Any()) { - // 无论 Update Or Add , 先删除当前用户的全部 U_R 关系 - var usreroles = (await _userRoleServices.Query(d => d.UserId == sysUserInfo.uID)).Select(d => d.Id.ToString()).ToArray(); - if (usreroles.Count() > 0) + var isAllDeleted = await _userRoleServices.DeleteByIds(usreroles); + if (!isAllDeleted) { - var isAllDeleted = await _userRoleServices.DeleteByIds(usreroles); + return Failed("服务器更新异常"); } + } - // 然后再执行添加操作 + // 然后再执行添加操作 + if (sysUserInfo.RIDs.Count > 0) + { var userRolsAdd = new List(); sysUserInfo.RIDs.ForEach(rid => { From 6d188c37f45c71e1835b969238cb3d4b515cb218 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 10 Apr 2022 11:16:43 +0800 Subject: [PATCH 035/289] Update UserController.cs --- Blog.Core.Api/Controllers/UserController.cs | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 61e2214d..e42a5359 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -33,7 +33,6 @@ public class UserController : BaseApiController private readonly IUser _user; private readonly IMapper _mapper; private readonly ILogger _logger; - private string fullName; /// /// 构造函数 @@ -94,12 +93,9 @@ public async Task>> Get(int page = 1, str var currentUserRoles = allUserRoles.Where(d => d.UserId == item.Id).Select(d => d.RoleId).ToList(); item.RIDs = currentUserRoles; item.RoleNames = allRoles.Where(d => currentUserRoles.Contains(d.Id)).Select(d => d.Name).ToList(); - List dids = new List(); - fullName = ""; - var departmentName = GetFullDepartmentName(allDepartments, item.DepartmentId, dids); - item.DepartmentName = departmentName; - dids.Insert(0, 0); - item.Dids = dids; + var departmentNameAndIds = GetFullDepartmentName(allDepartments, item.DepartmentId); + item.DepartmentName = departmentNameAndIds.Item1; + item.Dids = departmentNameAndIds.Item2; } data.data = sysUserInfos; @@ -110,19 +106,20 @@ public async Task>> Get(int page = 1, str } - private string GetFullDepartmentName(List departments, int departmentId, List dids) + private (string, List) GetFullDepartmentName(List departments, int departmentId) { var departmentModel = departments.FirstOrDefault(d => d.Id == departmentId); if (departmentModel == null) { - return fullName; + return ("", new List()); } - fullName = $"{departmentModel.Name}/{fullName}"; - dids.Insert(0, departmentModel.Id); - GetFullDepartmentName(departments, departmentModel.Pid, dids); + var pids = departmentModel.CodeRelationship?.TrimEnd(',').Split(',').Select(d => d.ObjToInt()).ToList(); + pids.Add(departmentModel.Id); + var pnams = departments.Where(d => pids.Contains(d.Id)).ToList().Select(d => d.Name).ToArray(); + var fullName = string.Join("/", pnams); - return fullName; + return (fullName, pids); } // GET: api/User/5 From 7d52cd3556d8f6de29c2d8f08f73a94f760da86e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 10 Apr 2022 14:30:45 +0800 Subject: [PATCH 036/289] fix: update ui.zip --- Blog.Core.Api/appsettings.json | 3 ++- Blog.Core.Api/wwwroot/ui.zip | Bin 5263957 -> 5271315 bytes 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 92d39d4d..23ab9c65 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -5,13 +5,14 @@ "Default": "Information", //加入Default否则log4net本地写入不了日志 "Blog.Core.AuthHelper.ApiResponseHandler": "Error" }, - "IncludeScopes": false, "Debug": { + "IncludeScopes": false, "LogLevel": { "Default": "Warning" } }, "Console": { + "IncludeScopes": false, "LogLevel": { "Default": "Warning", "Microsoft.Hosting.Lifetime": "Debug" diff --git a/Blog.Core.Api/wwwroot/ui.zip b/Blog.Core.Api/wwwroot/ui.zip index 0e968aa0a8c8dc3e4e3f8baf3f53f7e27e28b695..c6bc7dbd760e47d8b45c381d6519694b3483de4d 100644 GIT binary patch delta 162912 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 =B(m7r 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 F{{xTNKH&fW delta 155134 zcmYJab9CU|6Zc=++S;~lZEb9AV{2{O)V6Kg+S=XPHnz6;?DzBhkaJ#{x%WLc$^9d9 zPbNv?Rv*}6Q6Cthk}Nm`2FU+5tZ4suL{jj7rk=G3@qf0_5cwSJe@@~mA|VW?8Jjr| zrwMoC7otAsf3hOlC;ESm5+%-mJ&Cqd-v7m)Q8oN$^Qg=Iv$8b9{{>>uzWmqGh%(;= z`JXbgLjIrfw4!7GKc`jwJLs6h|GZF=Hqcg*Hn1fB|5|N4;H_;u5Ty+NwTvwwT8%9r z#s05Wb`05Cb_~_y`#&w!+5-B&X@tgFK>q`#HP!+ah48;&1;MbP1;KFtfd2==KM?cx?5QLS$voQTtVQYfb?=2p{%MrK!cmIBb!1g5 zf6~?mC~UUB*xbF8PKMq%1TC}^(g`8KhAse|Z-*vw{_I~*Nzn*97^A4@33d+U!Xysj{{t^kmZ%dw|fi&+oI}MUp4NtM8MneFC zjqb-}l%x(`K|2I(u)zTC8MQSzLc#KIJ7$$;2HAduRHYAwe~nSI3lt`rNcLX*4W{jk z97%MD-?Tm(nMNIUdcB+%8igO?!#d}-U}02YS>XbNlB5I3K}*uYALYbkCmq#GF?uR+ zNTZ@rUS$>gXWTi}S`1WFVcH7Kcrd^ZUEWiv*}QU;nsi#^5bR95hUD&-QG$b2HYc@4 zJh7gyO_WGp7@d^H-P+Op1HI+pa(w(^Kwc^jqv2BAgkXXo>0i_fT2{;auscalq5(Ko zh!L9!-t4A7DI=;gD44`#W!2z?e_Y}00*@$DF^3Rg)N6^wQ0PJGwU;*TZp#4rLoQlE z%c}7!H=6D!u~8h|&j!qltomjuVlCgFCGYm?RRZoX;sKDkiLGhgG*4-7K^BL;N1rIn zse1k8O|y}=yhR+7IZnh2KTgRRrXi}+TM}pjR6yo1Z*;X)!?fWm#+^rHN1La?mDQ}x z>N+187^+j?NNUOqWyR+Cm!E-m8Y)nUFdkQSa~xOd>F#1wFLY&G9es{eP8e1p?hiLL zLOyEzSaBN*u<_V)_4V`bFqjjQS%lqOgxQWT`|RjvYV7TASdndA6-WHBak@PUYmj%U z?sD7rk6MidA`T~>^+90!Ojy$ldz!no`l~jkzp^7Yu?Zi48+mh@D9i)g8|PnxPv{zV z-15lDe8s0`*wy`Ech`dIPx~&K1f@4&s$WW*?&Sn`eFnP-JqD)g+`>5p;N^l3Fl>U= zm5l0EopO_#4x$a~g5?`_230Vd?WdxN*`w>8@fRI`vZ%g}5G_n^j^Iy$8*f*)UtJLw zheCtE|DVTz|J{!NWwifi2ldaoS}|EWGyT6oF#k(-Y7Cr{M}9^iAigZ1AejGOjFE!_ zBO9BUktrLOIit1ng7)Um!(rDi%_4ugI24Kar#F+;Az$p1k}bWFwHD+GVuaAp4qfE~ z2A(91_6|Q)3uRKt&X;k){m_xhCZMFc}%DU)F?odzY(BPP7VEPvmg^sv)y!v?F+ZkR~h|l`iD3mXkn5c<* zglQiB4D%-ywxDD2sgl*p8bZDkBR5m>(3{z*7>lV!ml9wWUS?)db0j0dp&L_NM-;g1 zh9H^rL>vF1Kh>9YbN?c7?~67~5}#VKV^jtkc*e(vi+@Y@eMjikrMr>sJ4flaMq-Fb zI*b_swrt4m1sC~TO5#THHMk@=%G~JzSqA3*%1SXc^0|AnW<&4tCHN3=$iuEY-NZo- z^@UMbq(dOSFWYWBjNljUcBD*q+H)9{E2-|`!kSyw0kZ^|<*5CcY>c9R`qY51ALY{{ zf(}EZPATtt~Bj#{@`g!TJ6eXqcJ=&0 zg_U$O?&)K0-{dA5j$LZ!gL5vPLT&rG?|m+vrlNoZPW-l|GN!mCf^`i|4`2B^`YpVN zU)K2d)t6=KMX<66P*P_vSx?YQ4#7+NosmuXiw{tbb&aBTzXWH!G+LosHh<1_#%!M? zGJe$a;fwqX9)7EdWuHB9m}5<2!u`MKjZ7k0EdqZsf7RoNj zt;y=AXHVc8N5uoao1_@_I=ekg*OZitbvm%)F1fEDL=+ldCcEb!(%0ckJE z(7688tA2K-{ZY^4)y#w#ie)`t?6_oU+wL=NGs8RJ*dx{w;u{;dnD&z!_cs=Tbje@j zPyw4C>vH!zuX)7j;FUu7QF)IdEW*`%dI?rf(7hiax#31c&G>N>M&Eo0zbdWpy>bAd zQ|jNuxfhF%_aWJ!KLl6hqP&k|hXo{|k&xHNlNLuwVPwD!_QP6H;IfPI3G;6G2f2 z8?iyebb0af$M(gma8B|2x2#x!--OG;gYxN!|Hdwct%z3s-|?Yu#@tSuoZ=hU|I|;9 zFvhyy^zJhce6B@5iT6S#ph%tkTh5|r#Ru<3n0@jOf=q7rhDiAWr8209w-Nmb z=R;GOp2ygHp>P5Cz~iKoG-vJkrm6lp*57a};IcGt2v+alG>N^FGCLt#yRbtw?KX&^AN^Hr=hthPT9wL|hskj&}@YJ^QiNgY{Yto#iG3 z?xxA2FK+cp=E0QvdPa$pk+}F_MEbG(xoKJd!qyyUor5-H4gI}_nfFr6RLvngmOUWG z;|YgL{nP_~vw&Z+5U`z=D_RMO>(khmXF5SC6VToV!;vkiESJ9gLH91L&MO;mh1>K( zb>P+zc*W?)NQw{82~cDl<{3;aEEcqbaR~4(K*`=M`H+)9o1nm@XBqLB(Uo7)z< z{v(HzC6XmQa`_{KnXJAsG5sV$QNmGNiwV^g-!Bt5Qi}F-)p5>kw0>%wqbQ(9Ktu&o z)IUhH%+V*Omai12Eeyx431AG{KjD@>ba=v9BbTT(OneWFah-@}T)b3leV&658Yz~g zf&TuZ+?$S*v`S^nZh9z8BgFf*hwP;yxN<~LQG+yA{G~)2rGP=IU)xq;<7{=?xKy?`Um~R#~CpSAg zvt;9d!{KWH!S0d$kyX5>*nWdOjetl@yDk|RXsOmUXm9X6)P6*I!AKs?I~%tAo}vVE z5Fk*{@I$^>m(c*@{CwuiTtHob_Gwr2_j{ey!8x4stm*Lt*D)auLeDk5cNAWcbRnqg4E-v|1 zUHCy2pS^gj>=2C|KR)cQ8|Z@>oN!(CNgsFeL}Fuc`pirvvAs&_$E5pF*nT6zilg+~ zP^|3Nx0xtZx05A<`AqcV!5MCq>}pOFut@X;H`eg=?^K{$+Du$UkI-t89SL&*LB_)! zqf_Z`y>El~XpPGR@ljDZzlbfdVsm~!j`sg;_i|(K&iNyDFHsdH;m!GdA-W7p5QZ5z z8WX3^a3oIPM%ey~Dku72W|ak58$g|nKehN4FEX}Rvd%O&o^%@o`wM%R3c0h9*jV!B z!Z0K=G-zbP1M1sJE|gWC!f!hmAS-HG_4@em&EvqsgL(fr`d+_mY50@+z>3!ho2)_L z9x=w@y!_;oJeXWF5C{8nZUpqZDiwK+7CXKmYjT47U;z)R$K6dB><%AMW z(g%x-b@=52=&|%+ecf#2$C_4UW2G#fd$NFE^Dut-#Thl;NAvIpzAU6n$l2A{Hn zrdxEeGgHx9rpg3k@jD)xBMRCL8L!DYUFvf;ks$fx0kfO)WsP{_D_@`ZOAfUNV_fV$Tlo3yxrzwD0fw|9toM!1~m$iHl%Z)wA= ztpYmY{0F^@)0DfgJ!xmLJL&fiLR#o4#Z-q7+VPf_mf?`8TXo1*z@Q@-L z1ipQ!7V}*iYevqn_GC6EglDTt*VW+%Dn;6qtp+ z(lNxLccs3P-RC0aue)93f9d4@LGkPK_3Cr~HF7#~r9&lQ7p8_VZj=3qMc7~N$qLsw z=3(?|J*E}Ait~I?J=lmxdRqf2QVLiyHlb}`W4#RsxTCdSkZKP29t`D%6X84VXVN6o zc&((X^vJ1o;MLX%tdLrZYLP#T5eY9qZADV)VQud7BzgFMm_9FMNdLEEvd zny$;~JCn@IP2r^2%HhC%StP?Fzbub@N6wF(v`nMizgu$Zd=#yMlX^o-U05Z;%aQg?V7>GfK(xyB(ayh?$eF=d*oN zyicqSKwpga+LDM;I%ufdF%$)`#R)UjQojGqvI%NaNmnfW{cnY&{))oA;r>ehK{%6u zzPPSfso>jGG@8a0r^#$(ww*C*$DD5JC-T%n?kP}PTCMXEpwpPsRS5{_@00Ky5UlVs z+Q>!ePEq?9asFk9?tdgKvZk$hA}*J8e|s*oCjHqT8p@~EjCz)SYfd^@#8Nwd!sg`Z z?Z(hL5>u5bRXe{eXM7Lz3NZ!lMgIjt+1`Ny{n34T z*wfv=IMgvEQ9c>7hd=tU%}N|~?*}_gl5OqE0aQ~7x!iK%A#fo@X7tVl#v?`ZTp|q;I8uOJe~1-Clz!S;V)(=k-0F z1wKzI-vKCJv6$_;h;6fvR5F)Rn43P>k`f|}BO1^SRW@g?v7-cBn)`eygorGhSYy~@ zJO>C4d=Yqj4qZ(95z_LyGS-HUruQ$yLydv>c1OtOE_m>@?H173;|^JPWSon2RYFH= zB%<<2*tV5FDBQqr0&Mpy;i*x%qW1|kI>DYMWiG2*wZGJ3G&4!NadF-HH zp5(5c{ZSIM_136E@}d$e<7fQ*ZxP-^3Tbv)K<|uXDD@?VVfRS+*L2iC zg`kEyc|0M?p|4uY%i|Pyn;|b2)5tN*_fyS3Tv20MlxKjL6~c#8dJQ&AHecbcQvM6r z+3^EDBlGdMqok^tA&ZAtLHS7!1vk0LeG^f((5?|CM_lyX@L5D<*VgcI(U-*U8@2J4 z?Y3lKdJLQOvAB4z1A7H$EkD$CwJ4_cLii}v&DFlKiM$dQf?>Bxn<1?~Hh3o2r67I! zBGt*HZSw-FAJwsrpedfj)&8}E(oRoFS-Mzb(X?TsgfSfgC5Y;^lJa_uXcYO04}DRa zoV|=kS3c`?vnk-8mpTu2Kj-W?zW-h4RQF8HoIwrR&HjGv?muURShRRmJh{&{`N!zk zZfGN5RNtW+X%Ow^Dd~~{))-t!;rt$6ctVr`yg!mTI8EJtnv0W*Qh z!gFUVLf{e0c#YwQCsTz-Mc5D9h%)m)hM+xE+*5F@ZgQ5RdIV2*mdBTUIpZV7c(c8XbE~cZT z?O^&w0)wL@`(D&5UaJxe88>r9640OT;(Q&*_#Cmjw?Kq%rM=dljBA-SSaPu}6skV?8d(S}#=qriy6C(#68 zkG{uOC_%#l2ey}ty9rsa22c!-22Yr`zAn@E6OS@mhu)!XC2tRBm&;dOzMCJflaVd& z5B$JT%2y+QkKK4r=3j&Q!|l<#RNI%-Xw)XOoe~m@mUqotZ=08IckdE9pV|Ch1q@%1 zVyG*(wbcD>^~y=FR}vYwynI-fwL=VuMWS2I5_o$}?C(_=lJ$_m|P11^Q=cgE>A z1G3UG*ULuy%IP->veE!L2qkP>O~y>DLMLu4%wCNm55DJ$Cp-euW+E}mMZ9DS(hbU7 z2YQd-hlnRU4syIkMFn190=~iuV!sZu2Gtwm^U6xB-w1k+4bqJ{R0Dbwfe09`gRC?b zU}K|j*@FCn61J46H%*v_sTHo5zGj*8m;_wWV?->Lv~WGiss6O)viK-{D0%cY(ct}Kpkc@TTnFOI zS?0-MmPonk8t2)br<>}jladF<{Ip+k-WS0-`$Xn&!oT;DIp?;_N*`>9SVUxT9n0Hz zzUXj4W@B}kobtQu8>?cLRBoV93fS?y;uE<8iAy}!%+;LdyQhV+S3M9F?A)MKv&U&Z zL$|@PUP&9B2h@{`8T5lKHs5RJ9;NbDp(M?);yfldf_ep;UqZ!xz07|{ChV0#UJgAF zTq(&kO--q(teifRwNq&E$MvIpZ2?;-`>xox?|TQi* z7L(u|Ot$!mpZpP1AHd3p6y+srSt{O7r}k1R{#_5I-_@>K1Mdb8YHU}&tpA3MD6)u$ zB0XP9(h1AmIb$lw!>^UZ$om6Af9*x~u*m;A8WUA(0P{CLM?^sBGP5RBr)+||ZWJNG z760AU)!?LjmH&?Wg!*X&aTGCFRujqXTED;X`Py>n;8(eBYP#?_V#A zxrE&x$98~e*evcB=nI%&{zM+rl^h8!Tf%~GN1%>KiNJ&SIM*N9C(R zjG9C%@=*LH!PeX4?m%l6s?_lN@w$5m5g_|{tqltbZY}6*tUP(0f}go@?rOR>WFylp@sxrXvw!q%(rnlh z2vOvjyg0sk;mh^$eQ=k)P<0|JJ#I{AZ@s0@1HV8XEwfwMYz}`Bp891 z|A~*=?I(zY{x>C_AzGdJqu%Z`e+=6CsCP3|hNkB`OIf1lkMR8NdPASS)+0BYmnE|uPp^y491PeVbZhC!|Jq8o< zwEjoH%g70%w2h?lqsxUuJJAv6-<-n4so@tqJ@?|t@E=KjSv4T(NA`nE2;zx#0lWm1 zDDchh4)xz4Ok+2boPI}rG?&*K6lG%ycXR)El!U#0%>}Rq%lFH8tgcsSRWgAw;Q;Mmf+COr?z#QiW(-xFOJU)~TwlXIAE{DwI zqLJX*T}v6Uc-8IE?_bFKaYu5RZ&%*nmZaU6$ys-Of?0y*I)BaCZWw%WINd zT~ZwM8nmtwD1XL8!DC%XQ$okMF+DZ3r~oCIir>^9VRb)O2z7kdU-SPh01(Jnl5dGI znc&Gu@R-?C2T65wm1{R+xc<`Tu4B2IySE+TzS!t|QJp31o5GuKU^C%-epym0Od$D# z?d4gataV1Eqe)g7p_L9t!n!TlW27FCTG9`*VmqmmX%C@b6wCEAXCtkHdby`4+;lMH z8*}m+^l?YwLM;)ia=x=-2Lug`y{z=wrbaKqwoeJCHEt|Q(^wdfQS3Y_lCC>Fw(xW$ zGk?zQq_d6aWSw((M8 z|Ki(D$~F%oT<+f7Y)2*H^jnr1;l8Y##UE~Uhq*3edOpX#B{u`!&5cD~hWZ zy;e9|IV>?Jx}!>WJnWy3S7m?iEj$92lr0;5*TOtKB@;t{PVjp-KJ&W*$Lk<9bq+SB zmpVdH5kEu;&8w}3)OUCZ#Wzr|wb+o!1dQXtiMPL=k&Jwsf@RpPcABxhNs-&})R%<* z1x2uuH~6lR8GtCS;4EYcnNJ`6Y5kzw{#BXBBOh%}cVPiz-gR^Wp&lr`{wABnvc&ap)hOe9AQ1H3!vL@G*5>6C5^P-5M*YM* zW*~S`Hh-#JD^d6@FYzfe8)N~`>nx54J9fA_8wZ*UZ-9F$ZE;MS1i|c48?>Wa*)jH9 zZU8>kHmca!3d1!a3ucBz#!j=-NP9zJ_yZOfV z>0RSKegL`+M6!(^w}Z*DeJ7b4&I(4={zAio(!x^LTT00NU|ftY`K%VI{G8X@C}V)p zF*fc8_H`{kGmRd%>0F>w`hI4e$p{Ft4ULkS5efq3{eBtr>jny}67~h>*4bKrGz9}p zg#hG%}UiM1?{`X5Vd4{{5n}C&v z0EY>+9STqdp~8;12y^~Bi*~Oo+{enhZ+F%6w-cQ#%QWrOvOK};5N3W{8UDQuc*&;@ z&y1EF!xgu^Ar-&9R@Dv<5q@l#rx7lngaUop-Brd(D05FSl>xGSs#w``d670hcBRg! z-EODa?$yKj+ZX$hYaJ#Rol=B;&p04in&*erVsq<;0*B0gT4-J6)SX(SJx8y(1Dfq6 z6z0~SK?7>muC*tDQE;+bfx7ubld_h+ev|4$HY);6%=C?AAuNBDvr|GB@ejYx38j6Cj5zv>8Gbt>H}z*;y?67%<|`f!rT(W6{vhE#jLb}SlR_{>9>jf zYU3YdX3Am~qN?=-LOeS?6Hxiou_k{{Z-sJxkNiLZWfo1=Hq-Vnf8y@OVZLOoj8Bgk zpKWCB>>07gW;^(!f3CACiaS!HR<#!PC`sjI$M}7dQA&>T@kc02I@^h}GbzwE{u|xj z`~vk(?P)#I$W?hLBS=Ao*TuMU=+HWmksg(b*$9JmQw%ej%_3^R-S_Kxe;dF1?gAO; zbblw$%+HrhibL22{^ig&viTCElYq2RtpcqObvoXw8s?>dI!>8ZUT_KcaZ<+#+ZwSg z3v?Hf91qG}49LZH)B-D}8lawtw@TZE-`z?Q*PXze*Vt$0$@&}F6CbvJa8U1VU}gvH zImSW0--)-3j*y`Jb-5ydK|7b<5i{s#@EFO%X|c)D07pLcKqneyupS4c?v*TBoNSREKY7Q|LtQIdA zNSUCro2!TjxM1eFZ{G%YTrGw?oExM_2`v~iYh%_gq00*Nf#XE^++T=G*&FSsb0S<- z>7Lh&av&~W9H2Ab8bIP4n_!!((BbpAt3eEEc4GH&w;3H%8Z5|sg463)tbWWRc05bR z$oqn*pAW5bgD+&;oQ^sEq*C@6L$=g`ARCAC^E+u~$YYF5>`Bx!;Kz|r>y&84k7;PW-9j|b_Bf+zO z13XBYaO@fvf9D+uUr){M1ifQc2u{am)^U^e~R{*Z4K2)_Pajy5>qay>Ed zxbba~TW3?Ur|L7m6LVu0vMaC45`XvQ$OOBciMRiC1%8#WG2MXsR#5bH0ZLoXbjQY! z!6is<&Po`*5WdMNTb0f+3Kd7;cbP;p6X=E|hU9e~H!Aolr=#d)$MMHCF>*ud@#QJq#=5o>!R&~jAD5^h9r)8lxdPEka=l-s_@qB) z^-QXURcjQfHL=kqdj6-4p>{@rGlr})jLzENH=t--4%ZN6o#hUWQn?I{QkH`pSpWj(z`YI;*Jhbb6dp@8f&>Jhdt0my>GCUMeNp!L1#H0y9z6{pyKtwdKVAP zA^FBx5gj~w5e?B;N1kDa&efc~ykWa`#j9DP=qxh16hNuffifw9W^^$s3g?|s-YUEA_0vyO= zmrR34Qv#efp%=iK8x;^8LfA=GHk9xR+B%DqoXc<&w19;$FpRqt@ zA*o|hHMH#w+z+fY&y&=(1_HJ%24!Xz5@uwMVd9XU4E>LMHee_;4rcQ~4hq^K$7S-P znsARDY2Yx5cF#$ry*oBQ>}WM*JFkX7SDavXxuSQAQZkL?9wb)9Q;lW`4y?`Ki)c>9 zC|49nljaoL^3$TwMpY$dfT46m^h*e%87d#lAZF?~XpCEkaz5M+(7E{ElmnFX;Zn=_ zeuXHA$17S@aczZ6>=hh_R;l*{w^pR-Z0*xv`$oiefG_ReCe^i~Dyz(d->ttX7NzKJ zb_Jv{5~LjcAWkrgUgCOxi*TH5YgMkE=@mB zp_E{;Nc`9H^rzUeEx+BPUr^-3%m~`%zk1G9ixYDC8`|&lI(AiwH)@DPUauL?VHO&Z`w5N@mw`7E&=CxoANsd+_zyZzWIeWCR zT*BNqbZCfdZSn+)5y^dc5}*n}(0p*j7JEqRQpQn&e?etB1kBVOd2vdUTPikZ@lChu z!>`AN+ZE7((+pUk0}lK%X<~$aux;z}TUy7V!F(rWox)u6Ot_i)8yf*Eg$8xr-nvIG z-JHnHc3~8bHb~V9AA>hinkg@L@s78%CE z-uj4Gbiy;%(Sv&2_Z&PKHf^hnyfIK)RfbgQX|l_tT`W$n-L+m`NSw3$9^x@pg$NLF z)o)Uo^kZox@|gaM3owR#f_bp0n5j9KBJ>a!LV<0q0G|k+qX;;3K_IVQIlHA53SIPH zFX0MnHDn{<{IS}5SPH`BJ@!s`m^@o_A6kfu9x}SU+mNSxU1q&r>#+scpR@3! zo&iTX^j6LBsb$fdgESMa-1bdvt4wz#pICp3MZH?A(Xd59mu)SDZ6ye_`N~PC&}HRU zXJmBIfeA~TqP2=+Qf>bZM??;lyCP~9$oO4WxtlFa@+C&Z4y7q?fjaH^bhw0ca|P01 zqd)OOVDQiccQAGFw*|>_zRvJM`Z9O~*O6F=JiS;na^(^_;yM>aLyPx_87&kn>_L`& z^nI#SDOHbB!SxQ;V90&*o1@eTw$LOU(lCd}z$qN;5+~ggKAee|iK%^7RbmN3-$HmV zicTtn#-b^8)QX%fFL_nt`WRhad;`o$ne}m>vc>QeTCT-mE~~6s#ts+NS5UT0&nPry zW=r(g$d}vI&9^W0t>pKcPo00oP4l3cfnQfhJbDNzBS4>X?!NcFQwv36BYA!dqw<@phXHkven!AlLGchoWn4 zu3tOa5Q2*!r^B561&+VEl1@I4oO$;%BQVljJfV3REAc?tt&F^!zH#MsqU{wh{QjNN zh|>^-e$>?2sa()UQykYp+V$Mt#{`-*s;;P`L&iQTYyp>5tJdE5dTwCtu?xG3J-A)wOOOc6rYZHYuG!-2&JFUEOMLBUG<~<$ISR^ z?4p;ehT7nb?2YOO6JBj@+@A{6F7k2BGv`!px7ALAa=WXz7orIm^N*LiwY4(dW=1$$ z31>x7e$_Br<$}XQv64kPwW^9yME;P&-4LMy)&En?*XUSq8 ziPJ;r+8VbDo@73c15StD`dYanV}|gb9%Nl8L75!gPtQBS%0hkB;izZ3XO3sOB1gwGFeP6 zvE+-hBJtIR0X)Hh6fZzr?d~g8TY*`=OY`zJ!s$1V@S>v85RVa5950g>C#t zvG2>a^e7wgl;UIfDhgXik3eijE=LQADbNqf!pW7`pWeLodCRK|6_;|#oo!4llx*Pa z*vZqBt~yDQQAhg|C!<71{N&@IxlWR*dL#VZz>}GmO5g#(r`q0)g>NG^HcSignavNY z!-{mx(Uq1FPQgc;x*-f$w4^qv{zTvanT<_!X)3b}#vJ}efO78~<+ptHQKXZ_bbl{b zh_WUYAw_un-u$BG0mT@qy&noa_VhyKnzgvHYN=Fs>wyPvu$-_;26R#|nSuy>?CEMt zD_+B%_Tm`d`04m~+c~r$tQ=>KE?A1L4qjEP0^h7mO0Pcf;%#U4;aYe5f~t`I2JKfSZq+SPWO#`UmmHx~!t|tMbdQ#KI0)oA zt!)Q7!GiRnHAjVXZBl-`tu=ir<#t%G=0LKSmx(m zzyZ3g+ZY3SF?{yuNmhd#*{f(AoE@>DuwA`D>*cMN0^*7nwIlGS2IU|p&?d{7BMd`l za25dGhA=g@!0hSxy?UdJ$#w@uBcc6)$-hGJClx4Z)mI9lhT|=GE?I_1IuSOYCxW zRiJa=sB1nOWC&x<4Ds0u>b)hP_ZRTk9+qRdu}f<-##XXoMp$V6gpI@(d+ylw#^4Y` zPv#eZf*%^{XS(a3)#=*Vl&~OBPCJ}t5=TnUdCuaRyVzCz9_7&6*|&+6R$l?h z5D@Y_(Nk|Y&XA1oK^mjdqtva0eWIpOSmB?uVhynCGwQlfWTWXB)09z};McAP5OKD) zF^=$Q{zusqX;$=zOS~9@*GUYr&w%+l3@6%D;!IjO-0CVR9h1>1?m+G0dM!yxa-i<6 zL^F>kY-{R>&WuMyOP@^H&&{AA{q-Cyx>KA+7KHU~uM4;RS6Z#M0KFoO$mn&ZhTwdz zIuaJx=Nq@jF4-?i2L#H7dcX%B0*0C> zh|Fm}BeJ?iPl;1`^%Jh4Z)qi@ACmh!U1v$S(&$!e3vn(b-j7*!l7ZG3EX1W?R03hK zKN%~;WI`aNm?$_=ynn)YgO1#k>P_-L?{S^V@X)1DJEgMhi^}md{RY5vmeAs&rA<@S z)E3-Sv9VOzH?Xly?NAXGz<_8LlMP(fVKsl9JQn#e>{r8}VL7h>GjRC|(qw^5!%ARo0iYUHcUhw1h6>;`z z$r{H?veMj|ty|sF{4Tq_VISTJL7I&i&cn#3^}Oe;Sx# zl-HF;Y^Pc_QdgQ-mdZ-NG99TlKveE;gpu935rR>mhddeyGGRt|vTOA= zXgjdy%fH%Gy!ap$(gR&SG&rswGFF{rb&JnpXms5^@zlErT56vYvy{TojD+cE0Vzd> zl1qXXwgIeQ=3~CO*K1bA&@$5&%~7Kmj|+b1UxcK44E_x$3U&P2}8Uhf1bp$xjalOzVu*Nmjjsy z`XtCZBnef_B)Fdj+@C6Q9FXu7odJ`@f4>~T80&# zl`>$6Y~>76FWaqPyFjnsqf)*d@ET~Yb$reT+3yY)bcF%4O@q>{O3WVywo3yu24a@) zC5PsrxfG;DYO$v0e|-^2njEF02D9oeGp7ct+HU(x9q5mh35K||kV{4(C&!WCZQM&r z+Kv_7rGmr1l*0`+5AIj2kMtyH;a8CGNaQ8?GZG=4i&DvHgF(46yR1-r?ZLYU9kpWb z-m*ThO@sBRj{#qvl)<{oY}W+uSfSIWaA9c5AE09on050bnU5Rkaf8T zrcNM#8-rDAOOH{fZ}ScS}CbbF|8t3XA@%Nro4a!w{KK^pQ;mS!Pme)BUN! zT;%W2mIsp|(ia!}tx<~YuwgOAis;v!{Ps_Sf2E;xY5*bUAhz#q{LYdT|cv} z@v@c~2}yIQjqS2W6u-k*)Fh#bQd3C4jdqsM2%zFbMAr)lj?|aR-hSi_PMjCL7i}N# zA$_cW;}+b%lo7widHQ>g^WUOEgPueBp%>zIkjC<-4a~U&KXla{r!g-X$9_ES@@FVg`=DH-Um6QmW|SM8+@w6S zJCMmEuMKq?y^H6K&1)j6DZ~{ws11RK#!gM!NYuncwQ(+NWdpgWUa;i4U5X{9HwB0W zMlD1Yk-A<>rgf?1273+l|Tj)o1X&3Lr_eb(k?`EK-x zv}h_JX=+OoqP?>t`D*#lanW~YFVvWbW+tXeJ>=+81#W_av4;2pPSqIF_iL*gpkp9+ zI#!bPqpCLbisJhJpVe!Ge zx^2DWGTy`JapQi}AO}h1CG(QD8!Ds=oWz`QLYUEXh58GoCZ#@s zb?o%{15RoIurHQ$_qL8jpIdTg?Z%?lG8Vj-Y%I7pgkCWiunI!{uws=enIM1J@APyc z7E|a9dIPkC=sBm?3t}j&p59XCtHX&Uly+ zcc3c*d`JX@ff$vDuiIFN?iGK-@F|K5N{N7;JJ1DV->n#P<7HzfvpJ*pr!X|=4H*uZ+UV@eI?Z0K?+On4nYd9e( zK^Yi_v9i2)_(OLmvPE*>p^uY?VQ=!FFu3fW{k zqhw@m;#1=a@h($V*`M@x*=1!8wlo$eq45I)aRhu3yBg1O>00B^l49b}veXq%n)6YI zBHDUa6408mE<$Eqif|Akom0*Wp|*GwQBqzhWYZVncxs!qNRdHmxy*&v9T|ENm609H)wkmh&JUCioPPlE zx7eG<%Lxx&zs8)Oo%;BTPmf5y^PdwA_C|s$D4#fS z#?c&HtZ%(yukQYwarl1TykFZ{QYY7?T*~uhp??1gDqGanHtN*}WO==@x=gmOI3T6} zXZl!0oulu654Uc!ccot|agJg#ZD~{&zB)vf*45d^^_g*cQi*nUat}Vx-Y*NDY`&-S z+uly9WYP(X3NLb`bfFW(RQ@UYG?x4QkfB^E`@mn$&L0hs38dyLpxtI%2BrI1r8BPj|x(WUjhK6{rBq*qOMgdkP!*+pHi60#wWRO`yiwhT0nU0QKDmzY-Io;U~x1HBCe!++1 z8_U|J(CdjAE5#~dmrNzUaWUAy$(M(AZx(f`BRBA6$@NuInW|M6>ZAC=h(8t|c|e|y zlZmN++S&_`2k)#)d3Ko^Py&eMc}k}hDUEiqq1s$Ni|2f1${V8SdTfb<=}jtmSZ^1E z7AvMa@v$LEp%l06B4&ssTpg}V2g`#%e$n$srFc@hm}18LKa%n;8rLCcz#lw_5E}!+#js)KGN&hhFpB>= zLDX~Vl#G4{T)4d`v{?7Kb6&UKcOGqy8}PZJzE=a+&*jqJFxD>Rd+3E=p`$ElyX@-F z5uVm=n=l@eH@|+^EfE8Y3X&}!;j3Rh=lm0{{)~;Exl|I}Eiqz2#|rQBj=QuGaPmig zY=TA@9SKbwL1$&Q-DG0;BxD!IfH=EH+eJhfqO=cUX66G}Q*g6apFe8sT<4GOZr%lv zKy$-rAYQlA@$=>-x)L2^*h$faG>xa-AU{9jAkgTy1_o3!{Rs;%CtI05Wv3 zVnTGq*sgB2(IaUqnj1guLPl)YHc>|vB*DjXa74qIl(eho_o-eq+IGgkB9}un9!LSX zmy9$XAp$?|m#;J)7Y6V@gBz7hm&`OCAvjVoIO`O|Wajx~rrJ$r!7`Zwo;6Qj!7zQ1 zK)lQDgoS!*d`|St-@Y{d1u&jU4kuOCKO@SVB(S}H-sH#ZlkENEzmPs=hBG`QI)2`DfC!L8PWM!I zS65e8S5;SCU%v>GpNladek}#9{!%a!P5}D>tZ`l7!G1&gg6%DF1T$v&Jh50zFkOf| z$IEX=NR-Dziz$wwJ-|C9($_3&@PB3t_9pD{oyt(B2M_RA6Qf&Np5Z%9bdpkwhu*lTg%BRF?Jos6KS5@NVuXEA7F) zD2BR{s)?61utw%q8q4c70Be~w|IDf-D90ko1n~%E6I2aFfOb`KC4YY*FZ74UR^c9E z=o=Cy; zgELia=Ac$`y+QAVQxC(TtN9jun8j`|g2^^)(G(pW;JqEtFUHVe;%ev$t_z@NLztY? z9c=>#np?|hX5-+*Y=3cEX0t!l1pUy7GrNn>i`SLNYYnEy%(QjipaGZQdN6&*_F-=x zDpovL_j)}r>6vTvW>CF5MsGL0`}mi=y#oWzAvtk?p2OUTD`cYr&d_Ha#EL4K`E^%% zZx(Iqv01#Dht7QLIMsahAvj09Y2wII18>Uy2d45EdOxkEV1Mf~DqLyo5HQw0z&`0% z8DOj`!^^{Cm{%5_@sLv-zJY*}0n39prjoVsnbZ^9HbARU{YSRy5QaeIf2dK%m)Cu=JH!zSa_FpKzQMi!+#-fsg<+?Ls`L5fs7?dkc}gm z;`&8LoA-gRqN%Qexb|9n)$Iz1XYc~v944IzG*#V#s5$lwbip_y3koz}n87JbUgmsv z1r0GI4%*D?A$Iv(+cRS(epgEEp_}DLtDNW?fhX^SPl5)vLSVs&8NM$M?t-$c1W>fP zK(_1Q;eWpe8?g>{2bqVqq?PmNzHnr&P3y!4T0X?e)jbV0xxep$4#E#~*Lsib`%S_9 zk!5B%)i(%6cGwVo(@1SQf%bxDWU80f4F#($2fPA0i-C;~{_ktj!@)+4F|0wL3WNt7 z_i%{2o41}I)Os$Y-P+9BJ}f(*Z)iS0IG`-J>WO~Z~>oHpV+8hf5L;*G6)2lw08zLl|fIa5wB zXmF4LeDLbcf4z*?)Xm@j$_zG(17H64ySG352mu{f#{3*usfjsy844vx;kKF401mUd z+y@T&3%GjbR6==>vyJY$y?OL#=Xc@CTz~KE9fn#&Zl{8i?^d_jXKk_t+o2P&Z@ho| zx1Zd+@-apn*119T8emvpa;z!Bs=nJ_{58VqR%^TLGD$*t&?cMh_IG!sqodO;2E(;k zf%tkJW^L_mv$pPtg&-Y-1aywY9i+>U* zbpcjn-+cP-Sg*a58YEQ^2FMT5h$&tkossx3GTRN7w$_n_3f?rc8r#gQqs=z+4Tjdy z($LC8g~$eLC`=B&qnXtoVc9h^E3_H!_&y&LgKO_Y+wF#K!A$Wxf^`~CNij^?##5Xo z-a2}2HGWz;^QEox1kyF24R{zAPJd0NjqtB8w`ubMBWb8MisT4Rz*h8vQ+Z=N0|CbR z=o>m(QSg$ZwfDOQ3zSLdw=Q*{2C|pGBF2mue}Lq%S}MrvoYlt)H)iE#%9hdX1D8OiI&Lx)fA#(mV= zvC-*e&;b?Q^c7>+{nu$DN2It3r^FZm`^c_2U8QflE~TOD&>!4)>2d;7zQfHx7iV0WnI zI$`Wk-)2SjMU020V>voW93Gtx4_DlBkaP(nGnwM0ej)-kQbA!Fj!Z$*iA54&5waQP z2#1FNGk{_3$E*j3ZpP?ZY)9DTWQ8%iUx3Sdc(mk<>T}|Z>VLt3YH>ygSH@gW-N*&y z4P4NrFT&m3!z&br&f19)9(kS2bO%Og;sZOWV*brSGsVQ^w*eWVn`U_{$Fb;l7wMHl1 zkie%51^y(2Wc(8RPmcZLk^w8k+ojrtKNnXuiVIiagNs465lDbKv+i_4UsfCNVV+BO zuDEGDp3=_2*GMYP@ssfxe4#a~H7*j(Y)6P(=`1_bGk=+LF;p5%E9qo>OvRg2F*{FJ z)Yd4+*5pw2tkBH$vq?FVEHo5mBxGx{QRt85xSzc!Op1|yJzdIGLl=}%Xl9KlSqslX zWa)|)yOXQP)j2l^)!NZ!n28J)l`uWfNnUG4pFHMB=`3fbKrRpM_PfcY~ zOytCPZxPC;s9vj*&x`TuGz6;1=e1^?$am9eo@ibwVmmYI#i!>fwIB=Av&Nn#j@`*352*vTO%R{tAB(tsX(^Zq=FP%$gqVf)m2EMmkW{2 z%XX@lm<7mjkEJIp%k^rKIv|^^TszFw1Iclu8)1V{-#`(o7Z-y_Q?B>|jfO%m_^5b3 zU}vGGJUV0bc^=@c*bYrtD%$oBwQi82>$!R)&~E55HB3uaL25`hvcjB^xV-Ny9t`!w zR)29;4s(fFdK#tXRl3(!c|Vns3kkB2uT#ZtL8WT>5>>s>+NmZRtrm)j^ZdBPS4#C< zs9neh&*|*+szdej=ZUjMxRx#xy>WO_Boao|sB~v^`>dTv@_Bi{&C987P6#z<`LdrK zMdZ@&nmJgS7c>WYkw>8%gQv!2YIR+4$rtsHQtXBVvz7@GEwh^ zDj89(W%xM|=w4|$Ic03>obe(7slTyXEXoVE26C(L+ zt0nSjT9iqtnIcO=f3BoPYEw?;_;N&SWaq=lh%9w3TOu(hYYTDQ;dN>#WIME0W`Fuu z{Z60O^C4fQ5N>utnX~MSF&bK=A8PSzC11W!(^X%Q6^jY>EIOgXf$?Q!Q6l>#f~L*~ zkqVpQM^!&bE0-;vDCQTl%Dh5m(*E=yTrRYgkXTGf#f93-7-!L|k#?10BSugUH7C>R zMWaH6yBu*zmQpO$OhlAuy#nU35`SJ?`Fyh?D@(G~-I4K*|!ZhWtr3I-3gh(zz z`%(*0lF9`k+t2yyy+od865&LyGA}l!axFlxtzaoo?^9w_$hYE#-0&^N;eYX1&2;CP z%UOo4#HZ?|!TWPjCK*mtEAw+9t5>H?EwZ2kDK1i`jbJ!k))LXQo;8@pEIYcq4Ei-% zOe&3Hre3S1S`q%6XAT3Qk`rtT=1+jN=529e=A)SMX@IPl!%&N5L91m`I(;3 zR~GUjNV3f?6Hl{rJ|B{=nniy(=lAQ~V6R!KCwO0LTq4xwEKUTI$$!!OycU@+=#ZjD zqun+i9aSl{K8h}Ctff~DudVpPhL1NSHW@virP}SYiBZpHF6ew;80qtcocSX%%XR4XOiXv^MLr)%_k5i~FDpgz znFxP1lB+^di>3`aA+vmn(wllm>gV<7Bod?~AK@n%u^FzU!+a^wo@eBg6dAXMgW70u zRjd1ETCSFFi<4S3-0VhYEz#eV#MFYw(A8Xc6ddvwhHB`8p?@(ukINHb6lmr9Oy4Lp zYic(=rK63C+MPFo*=G2htege1^XRb2s;OXCWG_>#fX+s;f~thb@q`-)L|h*D#slVJ za@FL>$t*!eiBL8liUP#7=vF2fXtE8lD1;cMPz}$Dm3Ts&QI$ZmFi#UgZam1hrdnY( z&XZ~~UN(B;E`K8@df|R;R0H!}NX!yO@;Uw{G(qHB{d6^=RA&_0=o9&iMkuZJ@{%OQ zVxDU+AEd=y6d?}1#sAiQ6b24N5zFy4JmfQpA0(IFkS^ygT)M$oaw5R=DvW|0_?M0` z(~clGB3SL_7=zkyIK`~R=yH0#kXW5ExHHxmh3?guVSk|z_zKD~zQq9s+JH~p@wo87 zTiCgF{gpk3QFjG{=2PgGyY51QJF~$cBb*@Ar36K19zUl1e|{{S@bJr}X+}PA^$3Ww z_+MEwiQ-tar#gvt5$La(A2hQ#h%tKdi`D5mE z^cT)Y02Qtpqazd_O^eB14%;arg6OtWC=Qbi(tr7A+w2s|dVDaO+-+k8RJ|)pGX*pf zY*5HTbIC4z2)5?_s9Lolk?LkYxM1Z;qn1sO!FuBPOSP>XilxHi^C2@KK zJG`X*bs478^n-l%^tIccy?^_kPrv%~N7(R=Lr84BPH>ELwlEa#3O=AuFhuEqKlP{* zdq@Vu-aV2Efj^tA${528=c^hHCG!!c)Q--PD36P{gfmpaFe^b0%HM|UZ%H&LnO6u&9j zDgwZ85I@HQV>xd~UvxY<4o65B#SLCn@VJqB7xg9aY(KoW4qW8Ky|s?)7%krale6&C zk8l3zAHS`Ee_LZ*L&lIr;rj)M{NL~yJ)MhPImNGVes!a%?N;uRbj?b*|(H?O_>)yF@&`G5SEUw`p> z0Q*RN`ShKyK79AtXD{9U`rWU;c-7-rJKqm+0Flsh69O0Vn2JIHIN(R}uO*zLbWD&Q9pd?Sv1oqstn@<2r*RSE=!M14S3W!6h^Lw{}8Yd}OoatKK= z0ZeHU2$)MLmoy2OLJAH<_|JIO_EY~t&zYIM8sjyThf0yMUdCt6+|Ffu&Uenq^}7_O ztX!*+OJ!z4S$>M8(0`xcf91aoUokV2Zm*bY$VQ8Z}`B z?eQO^`|7YQ2Hf>fyHX7BY()jAr{x}xg8z~#Uf$6 zap>fA7HXhRv&fZ|LuF;W0{wg4Z85PQz9xc#s*$|9;qZ!3!f1&|9D^__^kiwJrE%Il zFRah+q~0q17+PFxIG)bi4`56&sgRri1E)eacVe&{!;#pdcHK_C;F$1O?<|n%HL!Uq z^wuH$6p4-8fq!w)T5h!sjLc?Gvdtfs=08@NG#Ch26Awa`Er!Pd@v&+>$!DMA)*D~y z+Lm2~d9PO2`s&HXpFCaT;4OJRr7Z0ij&mTd&0{LJ*Rcp?EH1lE#uC!L>22iv4oQgr zCFz1Th(9Bo;pfqM=wN1IcnChYc&%vJ*tna&YiSRR(0@sKyQt)0e39q_sf- zS6i0~S@?!!a2b~A>Xt;$W-jjLO7pwuCg$d?_-fT#&4wQ@sPd*6T)}K;nhJ zffx%H0}CA!fmIMEn$qob#R9{f^v#f4d#3^m420zPUG~xh9AJ2lX~t*mc;>{N!MMx3 zWvrgW0DpMtE@wX&qyuP_y2Zg57B-{~C|dlzw1g{niVdUW<;|TIq>P8u1Xoc7=(HCT z&vaL_i7r1@nx%DYod$q{^T>YNr4!8@DSwTT^d4DGSm@fL^^F9(5#^7CBokW~A_<19 zWzY;PNh4;H&VA|UK}n8x(4Xuyit0ywrEN2IuYb!lQ1aU}Iev))eU-ZqJJKyCC5zymMvOmAmgvKWaGar!{M^xH-+!5HF$ge?x$Z;tu8;lvIEm6^_K^>OHdRsNUerL1vZo|=F zsb$S-60n`dQva2iZ8C8P9+RUT3|sUJ@P3URWOXLa>%eR2fR##Mv%|7)nIgj6+<)v= zK0E2?VTntA2X4B|OR_Zv4?-Xs5_&|fcfX)ELnr_9>tg0t2BfF#{DIfl_L%com@AyT zDWwy|EjV^-Z0QP%hu~!vWGf>e!op# z8ZErA*>})^Blwws!QX7&aAFv0;D6D6;UraDf1l4SE0Y^GdusAnS6#g&O1okzO{NhAk)$&DlTC{F zOZVpLW-+s_OeITmc+Hdlv}$R+XpaypM` zw6o7`cGzO+(3q*;hC6^KPpivoG{rO>zz@DEJN!@87)zPadBS4vep*&-0^@6aWAK2mlt1 zfmEY;<<6#;0a6|ne={>>VPrEmW-e-TE^T3O>^y06+qjZ{Ws2obTXAg3hwS+>B_Bzg z@imr{)V`g!ERX~xj46T(l0LG3e)}Q`opvU9wRu%`l_i{wMt7sp-2n8z!M%2+v3c<8 z&YPgLwTpEb{Q6(PgN_On-~GC^wI#`|$l9u`oGOAhe)Zd}f6~QU!=iY5U+drYqJzEJ zdarg_$9tn6%B|t`*@=w_!p>lCf9VKyHQ7={k(pRkfN@=FH&wYBYxnlAay964^6!sa@! z%B_(}bfg^1ks9=$X%168H~AftBT@^^>&j$X_bM}qs`OHhEC==t!|rZx$1iWb76EVa z-$15;PIq74d`Zh+GWGT8m;e6r&$oa6$rAeZKOh5*e>Z-~)l`?zTK{6BzjR!!lgFLI zbg4W#Sm@eqg$D3(RaiKi)G?}cMh!L9C~y#J&E8h`9owDoYJ3 zGP7g*w#diNE!^1IEW1`YkKNnu&239-+kYGIK@_QP>hS`m{g$S?hu;MBH_&E-wOVFB zUCPBpPVMa{YnGtjvcI>pW;XtI{m(Z6sYK6)fBRV4ylB=kK!L3W={N-~m+7+vlJTU@ z@BCCr^-f<<8_elP9fQ<#1qS||E^T&C*-Yo-YJ%l?S;1I3IYv-eRn#R`Ts8U`1hKN! zQ-L#@sPgI|zjn6Ju6jZQpl^#D^;QnJKy!Fk=cJaQw49g}VI?}%&grDX3oS$fG|}B*J)t12v(zBskp7Dr6hx8|Elz9@hCHn#X(_x6!{+vDXv}7pDmJ)y) z0>`<4P?@3{Ioz6yhVYyH9lhI2_Xb@7D30xey|^n#w$<*z-mYH|Z^!%lGX=X!?aUOU zyXj0p(og$)GX-idO1*-9Z>M*-KU0wQe|!G0{r=&6K{D9&yCg}p)6;Fi?sk&yE8Xu3 z28YRD=fG<*NRr)tqPl`;e|vvF>RRmUo%C=&KJW?-cJ>Z?9f1b@gMmKmbuDz-KRE1r zB-~GX@lK?>Q3gAEV19Ub3dZk1rg6AsUJ;QnS6|P->n)Wgv7@66xJFf$GSSrG-k@9C#b#zl>)_;!u+g=qv|Qs0z2bje-LMYKO2D$T4&mH$3tVlwq_)bz zh>$30-9SV5JeB6GC@yWnm`v@xKAxBiVNNuGhUKU8q$#W>8a2tRzB*yuShU2nIj_`X z*bg#Ivn;6qr!rBlBz!6})v&2TDo4c1S+c~LR_?ZKLd<6Lt-Iyv15vV3e={kXP7S6` zYI0IX0I%yV3K9XqKUU|=9Uu*C>xgqe>J2)3idE;pT&JQ;n&P1FKYE%*76Ddv1Q_RI z<`TFhw0~l=DU&n|b=fM*oWze<35h$6BSc`GVHINF>G}o%OQf(l2q&gU3l5!ASoZ*3 z?E^N-k_A=YsNWp$2xVgkf2SrlA;^EIA1kL~u-da4_Vh3T*$YvWv}=dlm+Em!@P`E_Ue6c7P4G>5!R7l4V$;pkLdvI&a6F=~h6MHx>x*s`2`B2rAgc znr<|+7`jNW?fL11fmD>V|T83b&P%-}F0^})3m+cUJWui_Pe||YVx*i_?9-duX zU)|iEo`e@yC!fwwp;7qClb??#A*_4F22N+L$Yw$u>+D)w{OopP?K&kpG|34$;E($DoWCz zUqN$$hMg!@Gcv^`e{GL5aDW&xq9fzv0QkfxaEgxo+TlHH7f3$X7|U}*dsJ#2>Sm|M ztiCX?)01!9>Jy!rd+nYA-l#F)N~I-gb8>S_28fe^)P0L%WzkT~G#S37&OpQJgw?f5 zQ}ZZW?NnD2OBVdtR|3ub_jCI;&_*sCT?icAqAf6{!S@+8w&1#OX_-lcnG zNgUe-$C+(*$vT=E+AW?0ac*H51ZQGt&N-d}Vh{fjBgb1@hzRzZ1Y!#NMrw$#nFSYN z1Rk*(cN_g#rKdy`@^8CZ4!__Kd3DoFc7y!sfYt)zd^QTtPDW_UvLG&xGOrfP172FH zijhbo*oO^yf6in3-c*ws=O%P$uA9?kbmJMA~Wu2f3 z02CLM3-xjNpXI3q^Vr@I8!P!Hyik@h3^H6Rr>3OhRAK0U<+KMRMbwMMr>x^xlfK!= zZ3QGFf9%VWk%lD!5*Lsf0o6+~Ro{ReDUy)XPKgPHLX1$l&Ql9KB1x)va^sd_A4gZ0 z0h!YT@>1QKF#$RvRoCi_xU#A%|l0=IZwPD52b z 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 Date: Mon, 11 Apr 2022 11:53:44 +0800 Subject: [PATCH 037/289] Fixed #263 bug --- Blog.Core.Publish.Linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Publish.Linux.sh b/Blog.Core.Publish.Linux.sh index f3c3f98d..882a62ad 100644 --- a/Blog.Core.Publish.Linux.sh +++ b/Blog.Core.Publish.Linux.sh @@ -1,5 +1,5 @@ git pull; -rm -rf .PublishFiles; +find .PublishFiles/ -type f -and ! -path '*/wwwroot/images/*' ! -name 'appsettings.*' |xargs rm -rf dotnet build; dotnet publish -o /home/Blog.Core/Blog.Core.Api/bin/Debug/net6.0; cp -r /home/Blog.Core/Blog.Core.Api/bin/Debug/net6.0 .PublishFiles; From 9a013d502f2b375849ccfc7dc08718ed71b55c9e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 11 Apr 2022 14:44:38 +0800 Subject: [PATCH 038/289] Fixed #263 bug --- Blog.Core.Publish.Linux.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Publish.Linux.sh b/Blog.Core.Publish.Linux.sh index 882a62ad..05eb8103 100644 --- a/Blog.Core.Publish.Linux.sh +++ b/Blog.Core.Publish.Linux.sh @@ -1,6 +1,8 @@ git pull; find .PublishFiles/ -type f -and ! -path '*/wwwroot/images/*' ! -name 'appsettings.*' |xargs rm -rf dotnet build; -dotnet publish -o /home/Blog.Core/Blog.Core.Api/bin/Debug/net6.0; -cp -r /home/Blog.Core/Blog.Core.Api/bin/Debug/net6.0 .PublishFiles; +rm -rf /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles; +dotnet publish -o /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles; +# cp -r /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles ./; +awk 'BEGIN { cmd="cp -ri /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles ./"; print "n" |cmd; }' echo "Successfully!!!! ^ please see the file .PublishFiles"; \ No newline at end of file From 6f6ed915d7b8da466705dc1adfd428fc2855299f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 11 Apr 2022 15:58:45 +0800 Subject: [PATCH 039/289] fix: update test project data --- .../Service_Test/BlogArticleService_Should.cs | 2 ++ Blog.Core.Tests/WMBlog.db | Bin 200704 -> 200704 bytes 2 files changed, 2 insertions(+) diff --git a/Blog.Core.Tests/Service_Test/BlogArticleService_Should.cs b/Blog.Core.Tests/Service_Test/BlogArticleService_Should.cs index 00073c6c..df16d174 100644 --- a/Blog.Core.Tests/Service_Test/BlogArticleService_Should.cs +++ b/Blog.Core.Tests/Service_Test/BlogArticleService_Should.cs @@ -60,6 +60,8 @@ public async void Add_Blog_Test() [Fact] public async void Delete_Blog_Test() { + Add_Blog_Test(); + var deleteModel = (await blogArticleServices.Query(d => d.btitle == "xuint test title")).FirstOrDefault(); Assert.NotNull(deleteModel); diff --git a/Blog.Core.Tests/WMBlog.db b/Blog.Core.Tests/WMBlog.db index 044a620194889190494c7d17ebbab62233f460e8..4931b9674dbdfa61661ba06a34e3c55254029529 100644 GIT binary patch delta 3383 zcmeH}Yfuwc6vywqyU9MXyEi;Sh?tP@kXnR@ii`!O!f0s~MFn3KYNJGmRYFNnaC9Uc zMXT+Mx@xQsDpu=^S{xfS4)rb5`s#;{)3KxD`)#XZKlmD|>0Q9b$Y!SB`o&B>?EL=c z+;i`_XV0F7jY7jlVVN7+c2zIHj3EX9AmTtf*6Cv>0{j49!)Ndb+=4gbmADbtVi%qm zh|p(a`D6Roe3ORCL}T1DoFm-jwKJTfoHa9D9*@gi1sYhIGC)N#OB7{;ucUe%qT*nu zgQpfbFK#X;u|-I%nGR9Uj-zVyD;Zs;aoY zZ`<|O*4r%yZyjin59}BMhkOTrh%yOvKuetm+%@K@HkUdtel4scyg=E~}@yvGDloz_DD35^&ZPTfI zkr62V!|qhuu61TB`T~E8CHo1tLVcjIIJRtRm8B05LVHJRrs(97gk}zD10SD?$nl| z)X4e^78sI0Z_Z1Zrd3KLSt${ip_E8o`Ak<;iPPh~xo7F^ovUuOG~Zg**kK!2q9IFn zmQ&}e{n9oh!_C?JC$thEnY{e+PZ!N7Wb|KPs4TvOBG_nG;>KPwlekL3jzR>Ql(_O| zmrR{7u?Q-b9h>Q_*Ny-;ziGMCu=d49i1Uw^u|Bm{I4|Vsn)v&C3D?S5SvPZ#NzyLS zJkX4z_tOc~Vssysz*cAljmv$7yw7Ng1Q8=lER>Ht=uhJP-Co8SV^x?@g~g~ag9?jQ zVNoh9QiVmRFue-HDopCm(#1$%9Fa!Od*$(9a$eh$3NBrU1o0y>2wV2#13Ki*$Rv2E zY_F~ur238(zgDVcxL3%KJ zhJ)}^82Yw{s67Ta+6F|6aE)KfyO`t5D(xxSru~96dxrK?S;!6({k9Sq9W2N~We6tu zw+uq#VM6fPAVg_{kBi||1o;4L2*vNMhO~YwJs4S8Bs~JyCp{vo+?9s;*a8D!Q;U^d z0~WAdZ#P@u^xoLH|Muc-H#anos&U&L)h>JW%W1(*EW zACg?0;ZrK>4FIQdWPBh4#EV2gXuT2q{`$eKo$2U0C}IkNleeQSNj!zq0S@9SoK6<8 zVk*}MWZR$FsGvSrbqTG}4`>8B*E;IkPV^r-k^fu>^|90gJWV<+^%K_! z&xLZ`Niue?=KkTPu*ccnOg*W(u?+7^OHq1TDhvHh3`Lr&@=2}WdiEk6b9e-yg)F6v wdxVbkA7?4j$dkJ_Q)7|}OH^SAD$J(BtST&Cg;`XXS%vjfVR0&~$D6G5Utn(n0{{R3 delta 3110 zcmeIzdu$X%7y$5@*}L1B+ufVn_O5qFxwh8_CARjg1%b4*Jp{tTQm_?y#88A93iN@; z8>+pCpc1srwk((sTP;YW(82+SVgiaFM5H`wcqEo8DJ?M?Vq$1)pw0reG0uM`{>A40 z+24FS-|Wpd-_|tBHH~s@B}_h8wTX%`E&zaH5BF2a?w$weU3!aNq338T-Axqj+9f&uoJ|_V~hdx0s>RA0mx=|{^BJIE0zU&w;b+m`}j`N z5$(&CiJ!pz(H{0%BB}?NjoY1-sU7wrfkz#poq3Kt;QGB3@4*$P}J?x(ShylIU_e z;pQw&ID1L!5}Wn*DKI^J>s$kxps);Hu^rGubP=`N!nP8nU#VA$6^Gm**UQDSL+X&~ zrDDlp?XcEci>(gQLF!2{ao`U8K6Z)i?0?@TIwpG+K-6f0q04yb(Z;2HaCH=3HCzGM zQ%JP$uV&~!?Lq25yU-Ly~pswyKYcNiOt9vpl&Gs|E#*Ygh! z2K7sY$gk$QS=OQGL3Z&uhR;quJNO*U=O{j_e75tM^4Z2`h0ijdB|cmEO!$m>j7vqf zJHAw+z58RF7HSM6FR~b0oI(i9G`2XfxV)^QxHP!(_K~f{MT6^bMJWF8U@)J$H={2gtSAZCJSei$bRJ1O*vb4M> ze7xuQM~`!`s04N(*kglH4|Ck!dEidXheJV!x~M&^TJDw$q!#M~>ucl!c@A$8uZj7V zy%xK9t*Og2MQ9V;s0Q}Jnc!Gm=nWEbdQ_kmc(7J`H%q@h;Rm`D4W21P*fMnG+C|-&HA?%R0Mr`tccb# zK$j!uMRWm0;uQ2VGO^ffeP9Yq&_xBgBjYDRw;4Jkv!}w@(8L;fwjK&VKl^T=sV=V< zMp~x90NPUm8;@2&_>7C}23QmTJ*{k1rdvO=t|9wHfh=YX{1=PvGa)}rjLb5j6$mEj zr!44Y=!>v*2$rMy$dq7#Gz4gf1+Srcs1m(Btpduo?+T z!)0j!piOjw?UrqmZG>`I3CNvtwHzybAuV8Q&66Sv_MzR$7*V#|2(4iDDvea!L_6%G zjsaR^``qSHo*ew4f_FYS_z&Coe>nL1jRBM%*8u1e+eO=0rBT6hm2^$Yx9+vt$y(fn zpBEcg9j*{bC@t0KBxzWGOBC|7i=AVP-l?lt@Ze!1SeKfvVm%8B8G}P@#*j0G*T9^~ fTutI?B3FG}P2g%gSL3+q Date: Mon, 11 Apr 2022 16:19:36 +0800 Subject: [PATCH 040/289] Update appsettings.json --- .../RoleModulePermission.tsv | 44 +++++++ Blog.Core.Tests/appsettings.json | 114 ++++++++++++++++-- 2 files changed, 145 insertions(+), 13 deletions(-) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv index a16c2eab..ef19ade1 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv @@ -1648,5 +1648,49 @@ "ModifyBy": null, "ModifyTime": "\/Date(1546272000000+0800)\/", "Id": 128 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-04-11 16:08:48", + "ModifyTime": "2022-04-11 00:00:00", + "RoleId": 6, + "ModuleId": 0, + "PermissionId": 114, + "Id": 129 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-04-11 16:08:48", + "ModifyTime": "2022-04-11 00:00:00", + "RoleId": 6, + "ModuleId": 66, + "PermissionId": 115, + "Id": 130 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-04-11 16:08:49", + "ModifyTime": "2022-04-11 00:00:00", + "RoleId": 6, + "ModuleId": 70, + "PermissionId": 120, + "Id": 131 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-04-11 16:08:49", + "ModifyTime": "2022-04-11 00:00:00", + "RoleId": 6, + "ModuleId": 66, + "PermissionId": 116, + "Id": 132 } ] diff --git a/Blog.Core.Tests/appsettings.json b/Blog.Core.Tests/appsettings.json index 95a95f16..23ab9c65 100644 --- a/Blog.Core.Tests/appsettings.json +++ b/Blog.Core.Tests/appsettings.json @@ -1,12 +1,18 @@ { + "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 "Logging": { - "IncludeScopes": false, + "LogLevel": { + "Default": "Information", //加入Default否则log4net本地写入不了日志 + "Blog.Core.AuthHelper.ApiResponseHandler": "Error" + }, "Debug": { + "IncludeScopes": false, "LogLevel": { "Default": "Warning" } }, "Console": { + "IncludeScopes": false, "LogLevel": { "Default": "Warning", "Microsoft.Hosting.Lifetime": "Debug" @@ -18,7 +24,25 @@ }, "AllowedHosts": "*", "Redis": { - "ConnectionString": "127.0.0.1:6319" + "ConnectionString": "127.0.0.1:6319,password=admin" + }, + "RabbitMQ": { + "Enabled": false, + "Connection": "118.25.251.13", + "UserName": "", + "Password": "!", + "RetryCount": 3 + }, + "Kafka": { + "Enabled": false, + "Servers": "localhost:9092", + "Topic": "blog", + "GroupId": "blog-consumer", + "NumPartitions": 3 //主题分区数量 + }, + "EventBus": { + "Enabled": false, + "SubscriptionClientName": "Blog.Core" }, "AppSettings": { "RedisCachingAOP": { @@ -34,18 +58,23 @@ "Enabled": false }, "SqlAOP": { - "Enabled": false, + "Enabled": true, "OutToLogFile": { "Enabled": false }, "OutToConsole": { - "Enabled": false + "Enabled": true } }, + "LogToDb": { + "Enabled": true + }, "Date": "2018-08-28", "SeedDBEnabled": true, //只生成表结构 "SeedDBDataEnabled": true, //生成表,并初始化数据 - "Author": "Blog.Core" + "Author": "Blog.Core", + "SvcName": "", // /svc/blog + "UseLoadTest": false }, // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; @@ -63,7 +92,9 @@ SqlServer = 1, Sqlite = 2, Oracle = 3, - PostgreSQL = 4 + PostgreSQL = 4, + Dm = 5,//达梦 + Kdbndp = 6,//人大金仓 */ { "ConnId": "WMBLOG_SQLITE", @@ -100,15 +131,28 @@ "DBType": 0, "Enabled": true, "HitRate": 20, - "Connection": "server=.;Database=ddd;Uid=root;Pwd=123456;Port=10060;Allow User Variables=True;" + "Connection": "server=.;Database=blogcore001;Uid=root;Pwd=123456;Port=3096;Allow User Variables=True;" }, { "ConnId": "WMBLOG_ORACLE", "DBType": 3, "Enabled": false, "HitRate": 10, - "Connection": "Provider=OraOLEDB.Oracle; Data Source=WMBlogDB; User Id=sss; Password=789;", - "OracleConnection_other1": "User ID=sss;Password=789;Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.8.65)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME = orcl)))" + "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" + }, + { + "ConnId": "WMBLOG_DM", + "DBType": 5, + "Enabled": false, + "HitRate": 10, + "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" + }, + { + "ConnId": "WMBLOG_KDBNDP", + "DBType": 6, + "Enabled": true, + "HitRate": 10, + "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" } ], "Audience": { @@ -117,10 +161,14 @@ "Issuer": "Blog.Core", "Audience": "wr" }, + "Mongo": { + "ConnectionString": "mongodb://nosql.data", + "Database": "BlogCoreDb" + }, "Startup": { "Cors": { "PolicyName": "CorsIpAccess", //策略名称 - "EnableAllIPs": false, //是否应用所有的IP + "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 "IPs": "http://127.0.0.1:2364,http://localhost:2364" @@ -131,11 +179,17 @@ "ApiName": "Blog.Core", "IdentityServer4": { "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 - "AuthorizationUrl": "https://ids.neters.club", // 认证中心域名 + "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 "ApiName": "blog.core.api" // 资源服务器 }, "RedisMq": { - "Enabled": false + "Enabled": false //redis 消息队列 + }, + "MiniProfiler": { + "Enabled": false //性能分析开启 + }, + "Nacos": { + "Enabled": false //Nacos注册中心 } }, "Middleware": { @@ -146,7 +200,8 @@ "Enabled": true }, "RecordAccessLogs": { - "Enabled": true + "Enabled": true, + "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," }, "SignalR": { "Enabled": false @@ -169,6 +224,11 @@ "IpWhitelist": [], //白名单 "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], + "QuotaExceededResponse": { + "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", + "ContentType": "application/json", + "StatusCode": 429 + }, "HttpStatusCode": 429, //返回状态码 "GeneralRules": [ //api规则,结尾一定要带* { @@ -200,5 +260,33 @@ "ServicePort": "9291", "ServiceHealthCheck": "/healthcheck", "ConsulAddress": "http://localhost:8500" + }, + "PayInfo": { //建行聚合支付信息 + "MERCHANTID": "", //商户号 + "POSID": "", //柜台号 + "BRANCHID": "", //分行号 + "pubKey": "", //公钥 + "USER_ID": "", //操作员号 + "PASSWORD": "", //密码 + "OutAddress": "http://127.0.0.1:12345" //外联地址 + }, + "nacos": { + "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 + "DefaultTimeOut": 15000, // 默认超时时间 + "Namespace": "public", // 命名空间 + "ListenInterval": 10000, // 监听的频率 + "ServiceName": "blog.Core.Api", // 服务名 + "Port": "9291", // 服务端口号 + "RegisterEnabled": true // 是否直接注册nacos + }, + "LogFiedOutPutConfigs": { + "tcpAddressHost": "", // 输出elk的tcp连接地址 + "tcpAddressPort": 0, // 输出elk的tcp端口号 + "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 + { + "FiedName": "applicationName", + "FiedValue": "Blog.Core.Api" + } + ] } } From 07ed57413659ebf70b682218800fbf2b08bf20ca Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 12 Apr 2022 17:43:41 +0800 Subject: [PATCH 041/289] fix: change department delete api --- Blog.Core.Api/Controllers/DepartmentController.cs | 11 +++++++---- Blog.Core.Common/Seed/FrameSeed.cs | 10 ++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index 9ae0ebae..1674883f 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -174,16 +174,19 @@ public async Task> Put([FromBody] Department request) return data; } - [HttpDelete("{id}")] - public async Task> Delete(string id) + [HttpDelete] + public async Task> Delete(int id) { var data = new MessageModel(); - data.success = await _departmentServices.DeleteById(id); + var model = await _departmentServices.QueryById(id); + model.IsDeleted = true; + data.success = await _departmentServices.Update(model); if (data.success) { data.msg = "删除成功"; - data.response = id; + data.response = model?.Id.ObjToString(); } + return data; } diff --git a/Blog.Core.Common/Seed/FrameSeed.cs b/Blog.Core.Common/Seed/FrameSeed.cs index 89e60ded..9e1fd145 100644 --- a/Blog.Core.Common/Seed/FrameSeed.cs +++ b/Blog.Core.Common/Seed/FrameSeed.cs @@ -218,15 +218,17 @@ public async Task> Put([FromBody] {ClassName} request) return data; } - [HttpDelete(""{id}"")] - public async Task> Delete(string id) + [HttpDelete] + public async Task> Delete(int id) { var data = new MessageModel(); - data.success = await _{ClassName}Services.DeleteById(id); + var model = await _{ClassName}Services.QueryById(id); + model.IsDeleted = true; + data.success = await _departmentServices.Update(model); if (data.success) { data.msg = ""删除成功""; - data.response = id; + data.response = model?.Id.ObjToString(); } return data; From 5bb9cc236bfc210680ba035547b717cfb827b901 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Wed, 13 Apr 2022 09:45:53 +0800 Subject: [PATCH 042/289] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..3df0ef21 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '32 13 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp', 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From 141187d1120c32640e65acd2f8101d2ed40241cb Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 14 Apr 2022 09:13:23 +0800 Subject: [PATCH 043/289] Update MigrateController.cs --- .../Controllers/DbFirst/MigrateController.cs | 147 +++++++++++------- 1 file changed, 89 insertions(+), 58 deletions(-) diff --git a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs index e4a23600..a04f2552 100644 --- a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs +++ b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs @@ -46,57 +46,6 @@ public MigrateController(IUnitOfWork unitOfWork, _env = env; } - private void InitPermissionTree(List permissions, List all, List apis) - { - foreach (var item in permissions) - { - item.Children = all.Where(d => d.Pid == item.Id).ToList(); - item.Module = apis.FirstOrDefault(d => d.Id == item.Mid); - InitPermissionTree(item.Children, all, apis); - } - } - - private async Task SavePermissionTreeAsync(List permissions, List pms, int permissionId = 0) - { - var parendId = permissionId; - - foreach (var item in permissions) - { - PM pm = new PM(); - // 保留原始主键id - pm.PidOld = item.Id; - pm.MidOld = (item.Module?.Id).ObjToInt(); - - var mid = 0; - // 接口 - if (item.Module != null) - { - var moduleModel = (await _moduleServices.Query(d => d.LinkUrl == item.Module.LinkUrl)).FirstOrDefault(); - if (moduleModel != null) - { - mid = moduleModel.Id; - } - else - { - mid = await _moduleServices.Add(item.Module); - } - pm.MidNew = mid; - Console.WriteLine($"Moudle Added:{item.Module.Name}"); - } - // 菜单 - if (item != null) - { - item.Pid = parendId; - item.Mid = mid; - permissionId = await _permissionServices.Add(item); - pm.PidNew = permissionId; - Console.WriteLine($"Permission Added:{item.Name}"); - } - pms.Add(pm); - - await SavePermissionTreeAsync(item.Children, pms, permissionId); - } - } /// /// 获取权限部分Map数据(从库) @@ -107,26 +56,33 @@ private async Task SavePermissionTreeAsync(List permissions, List> DataMigrateFromOld2New() { var data = new MessageModel() { success = true, msg = "" }; + var filterPermissionId = 122; if (_env.IsDevelopment()) { try { var apiList = await _moduleServices.Query(d => d.IsDeleted == false); - var permissionsList = await _permissionServices.Query(d => d.IsDeleted == false); - var permissions = permissionsList.Where(d => d.Pid == 0).ToList(); + var permissionsAllList = await _permissionServices.Query(d => d.IsDeleted == false); + var permissions = permissionsAllList.Where(d => d.Pid == 0).ToList(); var rmps = await _roleModulePermissionServices.GetRMPMaps(); - List pms = new List(); + List pms = new(); // 当然,你可以做个where查询 - rmps = rmps.Where(d => d.PermissionId >= 114).ToList(); + rmps = rmps.Where(d => d.PermissionId >= filterPermissionId).ToList(); - InitPermissionTree(permissions, permissionsList, apiList); + InitPermissionTree(permissions, permissionsAllList, apiList); - permissions = permissions.Where(d => d.Id >= 114).ToList(); + var actionPermissionIds = permissionsAllList.Where(d => d.Id >= filterPermissionId).Select(d => d.Id).ToList(); + List filterPermissionIds = new(); + FilterPermissionTree(permissionsAllList, actionPermissionIds, filterPermissionIds); + permissions = permissions.Where(d => filterPermissionIds.Contains(d.Id)).ToList(); // 开启事务,保证数据一致性 _unitOfWork.BeginTran(); + // 注意信息的完整性,不要重复添加,确保主库没有要添加的数据 + + // 1、保持菜单和接口 await SavePermissionTreeAsync(permissions, pms); var rid = 0; @@ -134,7 +90,7 @@ public async Task> DataMigrateFromOld2New() var mid = 0; var rpmid = 0; - // 注意信息的完整性,不要重复添加,确保主库没有要添加的数据 + // 2、保存关系表 foreach (var item in rmps) { // 角色信息,防止重复添加,做了判断 @@ -239,6 +195,81 @@ public async Task> SaveData2TsvAsync() return data; } + private void InitPermissionTree(List permissionsTree, List all, List apis) + { + foreach (var item in permissionsTree) + { + item.Children = all.Where(d => d.Pid == item.Id).ToList(); + item.Module = apis.FirstOrDefault(d => d.Id == item.Mid); + InitPermissionTree(item.Children, all, apis); + } + } + + private void FilterPermissionTree(List permissionsAll, List actionPermissionId, List filterPermissionIds) + { + actionPermissionId = actionPermissionId.Distinct().ToList(); + var doneIds = permissionsAll.Where(d => actionPermissionId.Contains(d.Id) && d.Pid == 0).Select(d => d.Id).ToList(); + filterPermissionIds.AddRange(doneIds); + + var hasDoIds = permissionsAll.Where(d => actionPermissionId.Contains(d.Id) && d.Pid != 0).Select(d => d.Pid).ToList(); + if (hasDoIds.Any()) + { + FilterPermissionTree(permissionsAll, hasDoIds, filterPermissionIds); + } + } + + private async Task SavePermissionTreeAsync(List permissionsTree, List pms, int permissionId = 0) + { + var parendId = permissionId; + + foreach (var item in permissionsTree) + { + PM pm = new PM(); + // 保留原始主键id + pm.PidOld = item.Id; + pm.MidOld = (item.Module?.Id).ObjToInt(); + + var mid = 0; + // 接口 + if (item.Module != null) + { + var moduleModel = (await _moduleServices.Query(d => d.LinkUrl == item.Module.LinkUrl)).FirstOrDefault(); + if (moduleModel != null) + { + mid = moduleModel.Id; + } + else + { + mid = await _moduleServices.Add(item.Module); + } + pm.MidNew = mid; + Console.WriteLine($"Moudle Added:{item.Module.Name}"); + } + // 菜单 + if (item != null) + { + var permissionModel = (await _permissionServices.Query(d => d.Name == item.Name && d.Pid == item.Pid && d.Mid == item.Mid)).FirstOrDefault(); + item.Pid = parendId; + item.Mid = mid; + if (permissionModel != null) + { + permissionId = permissionModel.Id; + } + else + { + permissionId = await _permissionServices.Add(item); + } + + pm.PidNew = permissionId; + Console.WriteLine($"Permission Added:{item.Name}"); + } + pms.Add(pm); + + await SavePermissionTreeAsync(item.Children, pms, permissionId); + } + } + + } public class PM From 361beb135dceb012b4c93511defddac3d69c7ecf Mon Sep 17 00:00:00 2001 From: "Nine.Designmini" Date: Thu, 14 Apr 2022 16:52:35 +0800 Subject: [PATCH 044/289] =?UTF-8?q?[1]=20AOP=E6=97=A5=E5=BF=97=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=EF=BC=8CApiLogAopInfo=20[2]=20=E5=AF=B9?= =?UTF-8?q?=E4=BA=8E=E8=AE=B0=E5=BD=95=E6=95=B0=E6=8D=AE=E5=BA=93=E7=9A=84?= =?UTF-8?q?=E8=AF=9D=E8=AE=B0=E5=BD=95=E4=B8=BAjson=EF=BC=8C=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E5=8F=AF=E4=BB=A5=E6=96=B9=E4=BE=BF=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=8C=96=E8=BF=9B=E6=95=B0=E6=8D=AE=E8=A1=A8?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E4=BA=8E=E4=BD=BF=E7=94=A8=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5=E8=BF=98=E6=98=AF=E5=92=8C=E4=BB=A5?= =?UTF-8?q?=E5=89=8D=E4=B8=80=E6=A0=B7=E4=B8=AD=E6=96=87=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E6=9B=B4=E6=B8=85=E6=99=B0=EF=BC=8C=E5=A2=9E=E5=8A=A0=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=93=8D=E5=BA=94=E9=97=B4=E9=9A=94=E6=97=B6=E9=97=B4?= =?UTF-8?q?=EF=BC=8C=E8=AF=B7=E6=B1=82=E6=97=B6=E9=97=B4=EF=BC=8C=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=97=B6=E9=97=B4=20[3]=20log4Net=E6=8F=92=E5=85=A5?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E4=BF=AE=E5=A4=8D=20=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=E4=B8=8D=E5=A4=9F=E5=AF=BC=E8=87=B4=E4=B8=8D=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=20=E5=8E=9F=E5=9B=A0=EF=BC=9A=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=95=BF=E5=BA=A6=E4=B8=8D=E5=A4=9F=E5=AF=BC=E8=87=B4=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=95=B0=E6=8D=AE=E6=B2=A1=E8=AE=B0=E5=BD=95=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=9A=E4=BF=AE=E6=94=B9=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=99=A8=E5=AD=97=E6=AE=B5=EF=BC=88message?= =?UTF-8?q?=E5=92=8Cexception=EF=BC=89=E9=95=BF=E5=BA=A6=E4=B8=BA999999?= =?UTF-8?q?=EF=BC=9B=E4=BB=A3=E7=A0=81=EF=BC=9A=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=BA=93=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BAtext=20[4]=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=BB=BA=E8=A1=A8=E8=84=9A=E6=9C=AC?= =?UTF-8?q?,=E4=BD=BF=E7=94=A8text=E7=B1=BB=E5=9E=8B=20[5]=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E7=99=BB=E5=BD=95=E9=A6=96=E9=A1=B5=20[6]=20=E5=8F=91?= =?UTF-8?q?=E9=80=81=E6=97=A5=E5=BF=97=E4=BC=9A=E5=A2=9E=E5=A4=A7=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B6=88=E8=80=97=E5=AF=BC=E8=87=B4=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BE=88=E6=85=A2=EF=BC=8C=E6=B7=BB=E5=8A=A0=E4=B8=8A=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=BC=80=E5=85=B3SignalrSendLog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/MonitorController.cs | 7 +- Blog.Core.Api/Filter/GlobalExceptionFilter.cs | 8 +- Blog.Core.Api/Log4net.config | 231 ++++++++++++------ Blog.Core.Api/appsettings.json | 3 + Blog.Core.Api/wwwroot/css/site.css | 71 ++++++ Blog.Core.Api/wwwroot/css/style.css | 132 ++++++++++ Blog.Core.Api/wwwroot/js/anime.min.js | 33 +++ Blog.Core.Api/wwwroot/js/site.js | 4 + Blog.Core.Api/wwwroot/swg-login.html | 109 +++++++-- Blog.Core.Common/Hubs/ChatHub.cs | 10 +- Blog.Core.Common/LogHelper/LogLock.cs | 44 +++- Blog.Core.Common/LogHelper/RequestInfo.cs | 49 ++++ Blog.Core.Extensions/AOP/BlogLogAOP.cs | 103 ++++++-- 13 files changed, 672 insertions(+), 132 deletions(-) create mode 100644 Blog.Core.Api/wwwroot/css/site.css create mode 100644 Blog.Core.Api/wwwroot/css/style.css create mode 100644 Blog.Core.Api/wwwroot/js/anime.min.js create mode 100644 Blog.Core.Api/wwwroot/js/site.js diff --git a/Blog.Core.Api/Controllers/MonitorController.cs b/Blog.Core.Api/Controllers/MonitorController.cs index f725d4e3..ba7194c4 100644 --- a/Blog.Core.Api/Controllers/MonitorController.cs +++ b/Blog.Core.Api/Controllers/MonitorController.cs @@ -69,9 +69,10 @@ public MessageModel Server() [HttpGet] public MessageModel> Get() { - - _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); - + if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + { + _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); + } return Success>(null, "执行成功"); } diff --git a/Blog.Core.Api/Filter/GlobalExceptionFilter.cs b/Blog.Core.Api/Filter/GlobalExceptionFilter.cs index 4a39efc2..f51795d2 100644 --- a/Blog.Core.Api/Filter/GlobalExceptionFilter.cs +++ b/Blog.Core.Api/Filter/GlobalExceptionFilter.cs @@ -1,4 +1,5 @@ -using Blog.Core.Common.Helper; +using Blog.Core.Common; +using Blog.Core.Common.Helper; using Blog.Core.Common.LogHelper; using Blog.Core.Hubs; using Blog.Core.Model; @@ -55,8 +56,11 @@ public void OnException(ExceptionContext context) //采用log4net 进行错误日志记录 _loggerHelper.LogError(json.msg + WriteLog(json.msg, context.Exception)); + if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + { + _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); + } - _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); } diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config index ab6106de..1e65e805 100644 --- a/Blog.Core.Api/Log4net.config +++ b/Blog.Core.Api/Log4net.config @@ -204,14 +204,15 @@ + - + - + @@ -253,7 +254,7 @@ - + @@ -261,7 +262,7 @@ - + @@ -274,9 +275,9 @@ - + - + @@ -318,7 +319,7 @@ - + @@ -326,7 +327,7 @@ - + @@ -339,9 +340,9 @@ - + - + @@ -383,7 +384,7 @@ - + @@ -391,7 +392,7 @@ - + @@ -435,77 +436,153 @@ Source Server : Nine 何拾玖 Source Server Type : SQL Server - Source Server Version : 14001000 - Source Host : fangding.picp - Source Catalog : WitCloud_pdman + Source Server Version : 15004198 + Source Host : Nine.Design.Jamnine + Source Catalog : NineDesignApiByAntD Source Schema : dbo Target Server Type : SQL Server Target Server Version : 14001000 File Encoding : 65001 - Date: 15/10/2021 14:54:44 + Date: 12/04/2022 15:14:23 */ -CREATE TABLE GblLogError( - Id INT NOT NULL IDENTITY(1,1) , - Date DATETIME NOT NULL , - Thread VARCHAR(255) NOT NULL , - Level VARCHAR(50) NOT NULL , - Logger VARCHAR(255) NOT NULL , - Message VARCHAR(4000) NOT NULL , - Exception VARCHAR(2000) , - CONSTRAINT PK_GblLogError PRIMARY KEY CLUSTERED (Id ASC ) ON [PRIMARY] -) ;; - -EXECUTE sp_addextendedproperty N'MS_Description', '错误日志记录表', N'user', N'dbo', N'table', N'GblLogError', NULL, NULL;; -EXECUTE sp_addextendedproperty N'MS_Description', 'ID', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Id';; -EXECUTE sp_addextendedproperty N'MS_Description', '时间', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Date';; -EXECUTE sp_addextendedproperty N'MS_Description', '线程', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Thread';; -EXECUTE sp_addextendedproperty N'MS_Description', '等级', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Level';; -EXECUTE sp_addextendedproperty N'MS_Description', '记录器', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Logger';; -EXECUTE sp_addextendedproperty N'MS_Description', '错误信息', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Message';; -EXECUTE sp_addextendedproperty N'MS_Description', '异常', N'user', N'dbo', N'table', N'GblLogError', N'column', N'Exception';; - -CREATE TABLE GblLogDebug( - Id INT NOT NULL IDENTITY(1,1) , - Date DATETIME NOT NULL , - Thread VARCHAR(255) NOT NULL , - Level VARCHAR(50) NOT NULL , - Logger VARCHAR(255) NOT NULL , - Message VARCHAR(4000) NOT NULL , - Exception VARCHAR(2000) , - CONSTRAINT PK_GblLogDebug PRIMARY KEY CLUSTERED (Id ASC ) ON [PRIMARY] -) ;; - -EXECUTE sp_addextendedproperty N'MS_Description', '调试日志记录表', N'user', N'dbo', N'table', N'GblLogDebug', NULL, NULL;; -EXECUTE sp_addextendedproperty N'MS_Description', 'ID', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Id';; -EXECUTE sp_addextendedproperty N'MS_Description', '时间', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Date';; -EXECUTE sp_addextendedproperty N'MS_Description', '线程', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Thread';; -EXECUTE sp_addextendedproperty N'MS_Description', '等级', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Level';; -EXECUTE sp_addextendedproperty N'MS_Description', '记录器', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Logger';; -EXECUTE sp_addextendedproperty N'MS_Description', '错误信息', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Message';; -EXECUTE sp_addextendedproperty N'MS_Description', '异常', N'user', N'dbo', N'table', N'GblLogDebug', N'column', N'Exception';; - -CREATE TABLE GblLogInfo( - Id INT NOT NULL IDENTITY(1,1) , - Date DATETIME NOT NULL , - Thread VARCHAR(255) NOT NULL , - Level VARCHAR(50) NOT NULL , - Logger VARCHAR(255) NOT NULL , - Message VARCHAR(4000) NOT NULL , - Exception VARCHAR(2000) , - CONSTRAINT PK_GblLogInfo PRIMARY KEY CLUSTERED (Id ASC ) ON [PRIMARY] -) ;; - -EXECUTE sp_addextendedproperty N'MS_Description', '信息日志记录表', N'user', N'dbo', N'table', N'GblLogInfo', NULL, NULL;; -EXECUTE sp_addextendedproperty N'MS_Description', 'ID', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Id';; -EXECUTE sp_addextendedproperty N'MS_Description', '时间', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Date';; -EXECUTE sp_addextendedproperty N'MS_Description', '线程', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Thread';; -EXECUTE sp_addextendedproperty N'MS_Description', '等级', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Level';; -EXECUTE sp_addextendedproperty N'MS_Description', '记录器', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Logger';; -EXECUTE sp_addextendedproperty N'MS_Description', '错误信息', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Message';; -EXECUTE sp_addextendedproperty N'MS_Description', '异常', N'user', N'dbo', N'table', N'GblLogInfo', N'column', N'Exception';; - - +IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[GblLogDebug]') AND type IN ('U')) + DROP TABLE [dbo].[GblLogDebug] +GO + +CREATE TABLE [dbo].[GblLogDebug] ( + [Id] int IDENTITY(1,1) NOT NULL, + [Date] datetime NOT NULL, + [Thread] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Level] varchar(50) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Logger] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Message] text COLLATE Chinese_PRC_CI_AS NOT NULL, + [Exception] text COLLATE Chinese_PRC_CI_AS NULL +) +GO + +ALTER TABLE [dbo].[GblLogDebug] SET (LOCK_ESCALATION = TABLE) +GO + +EXEC sp_addextendedproperty'MS_Description', N'ID','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Id' +GO +EXEC sp_addextendedproperty'MS_Description', N'时间','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Date' +GO +EXEC sp_addextendedproperty'MS_Description', N'线程','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Thread' +GO +EXEC sp_addextendedproperty'MS_Description', N'等级','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Level' +GO +EXEC sp_addextendedproperty'MS_Description', N'记录器','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Logger' +GO +EXEC sp_addextendedproperty'MS_Description', N'错误信息','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Message' +GO +EXEC sp_addextendedproperty'MS_Description', N'异常','SCHEMA', N'dbo','TABLE', N'GblLogDebug','COLUMN', N'Exception' +GO +EXEC sp_addextendedproperty'MS_Description', N'调试日志记录表','SCHEMA', N'dbo','TABLE', N'GblLogDebug' +GO +DBCC CHECKIDENT ('[dbo].[GblLogDebug]', RESEED, 1) +GO +ALTER TABLE [dbo].[GblLogDebug] ADD CONSTRAINT [PK_GblLogDebug] PRIMARY KEY CLUSTERED ([Id]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +ON [PRIMARY] +GO + + +IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[GblLogError]') AND type IN ('U')) + DROP TABLE [dbo].[GblLogError] +GO + +CREATE TABLE [dbo].[GblLogError] ( + [Id] int IDENTITY(1,1) NOT NULL, + [Date] datetime NOT NULL, + [Thread] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Level] varchar(50) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Logger] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Message] text COLLATE Chinese_PRC_CI_AS NOT NULL, + [Exception] text COLLATE Chinese_PRC_CI_AS NULL +) +GO + +ALTER TABLE [dbo].[GblLogError] SET (LOCK_ESCALATION = TABLE) +GO +EXEC sp_addextendedproperty'MS_Description', N'ID','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Id' +GO +EXEC sp_addextendedproperty'MS_Description', N'时间','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Date' +GO +EXEC sp_addextendedproperty'MS_Description', N'线程','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Thread' +GO +EXEC sp_addextendedproperty'MS_Description', N'等级','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Level' +GO +EXEC sp_addextendedproperty'MS_Description', N'记录器','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Logger' +GO +EXEC sp_addextendedproperty'MS_Description', N'错误信息','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Message' +GO +EXEC sp_addextendedproperty'MS_Description', N'异常','SCHEMA', N'dbo','TABLE', N'GblLogError','COLUMN', N'Exception' +GO +EXEC sp_addextendedproperty'MS_Description', N'错误日志记录表','SCHEMA', N'dbo','TABLE', N'GblLogError' +GO +DBCC CHECKIDENT ('[dbo].[GblLogError]', RESEED, 1) +GO +ALTER TABLE [dbo].[GblLogError] ADD CONSTRAINT [PK_GblLogError] PRIMARY KEY CLUSTERED ([Id]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +ON [PRIMARY] +GO + +IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[GblLogInfo]') AND type IN ('U')) + DROP TABLE [dbo].[GblLogInfo] +GO + +CREATE TABLE [dbo].[GblLogInfo] ( + [Id] int IDENTITY(1,1) NOT NULL, + [Date] datetime NOT NULL, + [Thread] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Level] varchar(50) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Logger] varchar(255) COLLATE Chinese_PRC_CI_AS NOT NULL, + [Message] text COLLATE Chinese_PRC_CI_AS NOT NULL, + [Exception] text COLLATE Chinese_PRC_CI_AS NULL +) +GO + +ALTER TABLE [dbo].[GblLogInfo] SET (LOCK_ESCALATION = TABLE) +GO + +EXEC sp_addextendedproperty'MS_Description', N'ID','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Id' +GO +EXEC sp_addextendedproperty'MS_Description', N'时间','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Date' +GO +EXEC sp_addextendedproperty'MS_Description', N'线程','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Thread' +GO +EXEC sp_addextendedproperty'MS_Description', N'等级','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Level' +GO +EXEC sp_addextendedproperty'MS_Description', N'记录器','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Logger' +GO +EXEC sp_addextendedproperty'MS_Description', N'错误信息','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Message' +GO +EXEC sp_addextendedproperty'MS_Description', N'异常','SCHEMA', N'dbo','TABLE', N'GblLogInfo','COLUMN', N'Exception' +GO +EXEC sp_addextendedproperty'MS_Description', N'信息日志记录表','SCHEMA', N'dbo','TABLE', N'GblLogInfo' +GO + +DBCC CHECKIDENT ('[dbo].[GblLogInfo]', RESEED, 1) +GO + +ALTER TABLE [dbo].[GblLogInfo] ADD CONSTRAINT [PK_GblLogInfo] PRIMARY KEY CLUSTERED ([Id]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +ON [PRIMARY] +GO + +--> + + + \ No newline at end of file diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 23ab9c65..58da11e0 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -206,6 +206,9 @@ "SignalR": { "Enabled": false }, + "SignalRSendLog": { + "Enabled": false + }, "QuartzNetJob": { "Enabled": true }, diff --git a/Blog.Core.Api/wwwroot/css/site.css b/Blog.Core.Api/wwwroot/css/site.css new file mode 100644 index 00000000..e679a8ea --- /dev/null +++ b/Blog.Core.Api/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/Blog.Core.Api/wwwroot/css/style.css b/Blog.Core.Api/wwwroot/css/style.css new file mode 100644 index 00000000..f8fe18dd --- /dev/null +++ b/Blog.Core.Api/wwwroot/css/style.css @@ -0,0 +1,132 @@ +@charset "utf-8"; +::selection { + background: #2D2F36; +} +::-webkit-selection { + background: #2D2F36; +} +::-moz-selection { + background: #2D2F36; +} +body { + background: white; + font-family: 'Inter UI', sans-serif; + margin: 0; + padding: 20px; +} +.page { + background: #e2e2e5; + display: flex; + flex-direction: column; + height: calc(100% - 40px); + position: absolute; + place-content: center; + width: calc(100% - 40px); +} +@media (max-width: 767px) { + .page { + height: auto; + margin-bottom: 20px; + padding-bottom: 20px; + } +} +.container { + display: flex; + height: 320px; + margin: 0 auto; + width: 640px; +} +@media (max-width: 767px) { + .container { + flex-direction: column; + height: 630px; + width: 320px; + } +} +.left { + background: white; + height: calc(100% - 40px); + top: 20px; + position: relative; + width: 50%; +} +@media (max-width: 767px) { + .left { + height: 100%; + left: 20px; + width: calc(100% - 40px); + max-height: 270px; + } +} +.login { + font-size: 33px; + font-weight: 900; + margin: 50px 40px 40px; +} +.eula { + color: #999; + font-size: 14px; + line-height: 1.5; + margin: 40px; +} +.right { + background: #474A59; + box-shadow: 0px 0px 40px 16px rgba(0,0,0,0.22); + color: #F1F1F2; + position: relative; + width: 50%; +} +@media (max-width: 767px) { + .right { + flex-shrink: 0; + height: 100%; + width: 100%; + max-height: 350px; + } +} +svg { + position: absolute; + width: 320px; +} +path { + fill: none; + stroke: url(#linearGradient);; + stroke-width: 4; + stroke-dasharray: 240 1386; +} +.form { + margin: 40px; + position: absolute; +} +label { + color: #c2c2c5; + display: block; + font-size: 14px; + height: 16px; + margin-top: 20px; + margin-bottom: 5px; +} +input { + background: transparent; + border: 0; + color: #f2f2f2; + font-size: 20px; + height: 30px; + line-height: 30px; + outline: none !important; + width: 100%; +} +input::-moz-focus-inner { + border: 0; +} +#submit { + color: #707075; + margin-top: 40px; + transition: color 300ms; +} +#submit:focus { + color: #f2f2f2; +} +#submit:active { + color: #d0d0d2; +} \ No newline at end of file diff --git a/Blog.Core.Api/wwwroot/js/anime.min.js b/Blog.Core.Api/wwwroot/js/anime.min.js new file mode 100644 index 00000000..c3993246 --- /dev/null +++ b/Blog.Core.Api/wwwroot/js/anime.min.js @@ -0,0 +1,33 @@ +/* + 2017 Julian Garnier + Released under the MIT license +*/ +var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(e,r,p){if(p.get||p.set)throw new TypeError("ES3 does not support getters and setters.");e!=Array.prototype&&e!=Object.prototype&&(e[r]=p.value)};$jscomp.getGlobal=function(e){return"undefined"!=typeof window&&window===e?e:"undefined"!=typeof global&&null!=global?global:e};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_"; +$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(e){return $jscomp.SYMBOL_PREFIX+(e||"")+$jscomp.symbolCounter_++}; +$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var e=$jscomp.global.Symbol.iterator;e||(e=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[e]&&$jscomp.defineProperty(Array.prototype,e,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(e){var r=0;return $jscomp.iteratorPrototype(function(){return rb&&(b+=1);1b?c:b<2/3?a+(c-a)*(2/3-b)*6:a}var d=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(a)||/hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(a);a=parseInt(d[1])/360;var b=parseInt(d[2])/100,f=parseInt(d[3])/100,d=d[4]||1;if(0==b)f=b=a=f;else{var n=.5>f?f*(1+b):f+b-f*b,k=2*f-n,f=c(k,n,a+1/3),b=c(k,n,a);a=c(k,n,a-1/3)}return"rgba("+ +255*f+","+255*b+","+255*a+","+d+")"}function y(a){if(a=/([\+\-]?[0-9#\.]+)(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(a))return a[2]}function V(a){if(-1=g.currentTime)for(var G=0;G=w||!k)g.began||(g.began=!0,f("begin")),f("run");if(q>n&&q=k&&r!==k||!k)b(k),x||e();f("update");a>=k&&(g.remaining?(t=h,"alternate"===g.direction&&(g.reversed=!g.reversed)):(g.pause(),g.completed||(g.completed=!0,f("complete"),"Promise"in window&&(p(),m=c()))),l=0)}a=void 0===a?{}:a;var h,t,l=0,p=null,m=c(),g=fa(a);g.reset=function(){var a=g.direction,c=g.loop;g.currentTime= +0;g.progress=0;g.paused=!0;g.began=!1;g.completed=!1;g.reversed="reverse"===a;g.remaining="alternate"===a&&1===c?2:c;b(0);for(a=g.children.length;a--;)g.children[a].reset()};g.tick=function(a){h=a;t||(t=h);k((l+h-t)*q.speed)};g.seek=function(a){k(d(a))};g.pause=function(){var a=v.indexOf(g);-1=c&&0<=b&&1>=b){var e=new Float32Array(11);if(c!==d||b!==f)for(var k=0;11>k;++k)e[k]=a(.1*k,c,b);return function(k){if(c===d&&b===f)return k;if(0===k)return 0;if(1===k)return 1;for(var h=0,l=1;10!==l&&e[l]<=k;++l)h+=.1;--l;var l=h+(k-e[l])/(e[l+1]-e[l])*.1,n=3*(1-3*b+3*c)*l*l+2*(3*b-6*c)*l+3*c;if(.001<=n){for(h=0;4>h;++h){n=3*(1-3*b+3*c)*l*l+2*(3*b-6*c)*l+3*c;if(0===n)break;var m=a(l,c,b)-k,l=l-m/n}k=l}else if(0=== +n)k=l;else{var l=h,h=h+.1,g=0;do m=l+(h-l)/2,n=a(m,c,b)-k,0++g);k=m}return a(k,d,f)}}}}(),Q=function(){function a(a,b){return 0===a||1===a?a:-Math.pow(2,10*(a-1))*Math.sin(2*(a-1-b/(2*Math.PI)*Math.asin(1))*Math.PI/b)}var c="Quad Cubic Quart Quint Sine Expo Circ Back Elastic".split(" "),d={In:[[.55,.085,.68,.53],[.55,.055,.675,.19],[.895,.03,.685,.22],[.755,.05,.855,.06],[.47,0,.745,.715],[.95,.05,.795,.035],[.6,.04,.98,.335],[.6,-.28,.735,.045],a],Out:[[.25, +.46,.45,.94],[.215,.61,.355,1],[.165,.84,.44,1],[.23,1,.32,1],[.39,.575,.565,1],[.19,1,.22,1],[.075,.82,.165,1],[.175,.885,.32,1.275],function(b,c){return 1-a(1-b,c)}],InOut:[[.455,.03,.515,.955],[.645,.045,.355,1],[.77,0,.175,1],[.86,0,.07,1],[.445,.05,.55,.95],[1,0,0,1],[.785,.135,.15,.86],[.68,-.55,.265,1.55],function(b,c){return.5>b?a(2*b,c)/2:1-a(-2*b+2,c)/2}]},b={linear:A(.25,.25,.75,.75)},f={},e;for(e in d)f.type=e,d[f.type].forEach(function(a){return function(d,f){b["ease"+a.type+c[f]]=h.fnc(d)? +d:A.apply($jscomp$this,d)}}(f)),f={type:f.type};return b}(),ha={css:function(a,c,d){return a.style[c]=d},attribute:function(a,c,d){return a.setAttribute(c,d)},object:function(a,c,d){return a[c]=d},transform:function(a,c,d,b,f){b[f]||(b[f]=[]);b[f].push(c+"("+d+")")}},v=[],B=0,ia=function(){function a(){B=requestAnimationFrame(c)}function c(c){var b=v.length;if(b){for(var d=0;db&&(c.duration=d.duration);c.children.push(d)});c.seek(0);c.reset();c.autoplay&&c.restart();return c};return c};q.random=function(a,c){return Math.floor(Math.random()*(c-a+1))+a};return q}); \ No newline at end of file diff --git a/Blog.Core.Api/wwwroot/js/site.js b/Blog.Core.Api/wwwroot/js/site.js new file mode 100644 index 00000000..ac49c186 --- /dev/null +++ b/Blog.Core.Api/wwwroot/js/site.js @@ -0,0 +1,4 @@ +// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +// for details on configuring this project to bundle and minify static web assets. + +// Write your JavaScript code. diff --git a/Blog.Core.Api/wwwroot/swg-login.html b/Blog.Core.Api/wwwroot/swg-login.html index fdd32fda..ee133d27 100644 --- a/Blog.Core.Api/wwwroot/swg-login.html +++ b/Blog.Core.Api/wwwroot/swg-login.html @@ -2,25 +2,106 @@ - 默认首页 - - + 登录 - 接口文档 + + + -
-
-

用户名:admin,密码:admin

- -
- -
- +
+
+
+
+ + +
欢迎使用!
+
用户名:admin,密码:admin
+
+
+ + + + + + + + + +
+ + + + + +
+
+
+ + \ No newline at end of file diff --git a/Blog.Core.Common/Hubs/ChatHub.cs b/Blog.Core.Common/Hubs/ChatHub.cs index 1033ccb6..1e1ea55a 100644 --- a/Blog.Core.Common/Hubs/ChatHub.cs +++ b/Blog.Core.Common/Hubs/ChatHub.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Blog.Core.Common; using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.SignalR; @@ -72,7 +73,7 @@ public override Task OnDisconnectedAsync(System.Exception ex) public async Task SendMessage(string user, string message) { - await Clients.All.ReceiveMessage( user, message); + await Clients.All.ReceiveMessage(user, message); } //定于一个通讯管道,用来管理我们和客户端的连接 @@ -80,7 +81,12 @@ public async Task SendMessage(string user, string message) public async Task GetLatestCount(string random) { //2、服务端主动向客户端发送数据,名字千万不能错 - await Clients.All.ReceiveUpdate(LogLock.GetLogData()); + if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + { + await Clients.All.ReceiveUpdate(LogLock.GetLogData()); + } + + //3、客户端再通过 ReceiveUpdate ,来接收 diff --git a/Blog.Core.Common/LogHelper/LogLock.cs b/Blog.Core.Common/LogHelper/LogLock.cs index 0a55df53..5cd7668c 100644 --- a/Blog.Core.Common/LogHelper/LogLock.cs +++ b/Blog.Core.Common/LogHelper/LogLock.cs @@ -53,7 +53,38 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe } //string logFilePath = Path.Combine(path, $@"{filename}.log"); var logFilePath = FileHelper.GetAvailableFileWithPrefixOrderSize(folderPath, prefix); - + switch (prefix) + { + case "AOPLog": + ApiLogAopInfo apiLogAopInfo = JsonConvert.DeserializeObject(dataParas[0]); + //记录被拦截方法信息的日志信息 + var dataIntercept = "" + + $"【操作时间】:{apiLogAopInfo.RequestTime}\r\n" + + $"【当前操作用户】:{ apiLogAopInfo.OpUserName} \r\n" + + $"【当前执行方法】:{ apiLogAopInfo.RequestMethodName} \r\n" + + $"【携带的参数有】: {apiLogAopInfo.RequestParamsName} \r\n" + + $"【携带的参数JSON】: {apiLogAopInfo.RequestParamsData} \r\n" + + $"【响应时间】:{apiLogAopInfo.ResponseIntervalTime}\r\n" + + $"【执行完成时间】:{apiLogAopInfo.ResponseTime}\r\n" + + $"【执行完成结果】:{apiLogAopInfo.ResponseJsonData}\r\n"; + dataParas = new string[] { dataIntercept }; + break; + case "AOPLogEx": + ApiLogAopExInfo apiLogAopExInfo = JsonConvert.DeserializeObject(dataParas[0]); + var dataInterceptEx = "" + + $"【操作时间】:{apiLogAopExInfo.ApiLogAopInfo.RequestTime}\r\n" + + $"【当前操作用户】:{ apiLogAopExInfo.ApiLogAopInfo.OpUserName} \r\n" + + $"【当前执行方法】:{ apiLogAopExInfo.ApiLogAopInfo.RequestMethodName} \r\n" + + $"【携带的参数有】: {apiLogAopExInfo.ApiLogAopInfo.RequestParamsName} \r\n" + + $"【携带的参数JSON】: {apiLogAopExInfo.ApiLogAopInfo.RequestParamsData} \r\n" + + $"【响应时间】:{apiLogAopExInfo.ApiLogAopInfo.ResponseIntervalTime}\r\n" + + $"【执行完成时间】:{apiLogAopExInfo.ApiLogAopInfo.ResponseTime}\r\n" + + $"【执行完成结果】:{apiLogAopExInfo.ApiLogAopInfo.ResponseJsonData}\r\n" + + $"【执行完成异常信息】:方法中出现异常:{apiLogAopExInfo.ExMessage}\r\n" + + $"【执行完成异常】:方法中出现异常:{apiLogAopExInfo.InnerException}\r\n"; + dataParas = new string[] { dataInterceptEx }; + break; + } var now = DateTime.Now; string logContent = String.Join("\r\n", dataParas); if (IsHeader) @@ -97,14 +128,10 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe public static void OutSql2LogToDB(string prefix, string[] dataParas, bool IsHeader = true) { - string logContent = String.Join("\r\n", dataParas); + string logContent = String.Join("", dataParas); if (IsHeader) { - logContent = ( - "--------------------------------\r\n" + - DateTime.Now + "|\r\n" + - String.Join("\r\n", dataParas) + "\r\n" - ); + logContent = (String.Join("", dataParas)); } switch (prefix) { @@ -123,6 +150,9 @@ public static void OutSql2LogToDB(string prefix, string[] dataParas, bool IsHead case "SqlLog": log.Info(logContent); break; + case "RequestResponseLog": + log.Debug(logContent); + break; default: break; } diff --git a/Blog.Core.Common/LogHelper/RequestInfo.cs b/Blog.Core.Common/LogHelper/RequestInfo.cs index 9e4aac27..c5973f89 100644 --- a/Blog.Core.Common/LogHelper/RequestInfo.cs +++ b/Blog.Core.Common/LogHelper/RequestInfo.cs @@ -39,4 +39,53 @@ public class RequestInfo public string Week { get; set; } } + + public class ApiLogAopInfo + { + /// + /// 请求时间 + /// + public string RequestTime { get; set; } = string.Empty; + /// + /// 操作人员 + /// + public string OpUserName { get; set; } = string.Empty; + /// + /// 请求方法名 + /// + public string RequestMethodName { get; set; } = string.Empty; + /// + /// 请求参数名 + /// + public string RequestParamsName { get; set; } = string.Empty; + /// + /// 请求参数数据JSON + /// + public string RequestParamsData { get; set; } = string.Empty; + /// + /// 请求响应间隔时间 + /// + public string ResponseIntervalTime { get; set; } = string.Empty; + /// + /// 响应时间 + /// + public string ResponseTime { get; set; } = string.Empty; + /// + /// 响应结果 + /// + public string ResponseJsonData { get; set; } = string.Empty; + } + + public class ApiLogAopExInfo + { + public ApiLogAopInfo ApiLogAopInfo { get; set; } + /// + /// 异常 + /// + public string InnerException { get; set; } = string.Empty; + /// + /// 异常信息 + /// + public string ExMessage { get; set; } = string.Empty; + } } diff --git a/Blog.Core.Extensions/AOP/BlogLogAOP.cs b/Blog.Core.Extensions/AOP/BlogLogAOP.cs index e7153c78..46d1d1c9 100644 --- a/Blog.Core.Extensions/AOP/BlogLogAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogLogAOP.cs @@ -1,4 +1,5 @@ -using Blog.Core.Common.LogHelper; +using Blog.Core.Common; +using Blog.Core.Common.LogHelper; using Blog.Core.Hubs; using Castle.DynamicProxy; using Microsoft.AspNetCore.Http; @@ -34,12 +35,33 @@ public BlogLogAOP(IHubContext hubContext, IHttpContextAccessor accessor public void Intercept(IInvocation invocation) { string UserName = _accessor.HttpContext?.User?.Identity?.Name; + string json; + try + { + json = JsonConvert.SerializeObject(invocation.Arguments); + } + catch (Exception ex) + { + json = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); + } + DateTime startTime = DateTime.Now; + ApiLogAopInfo apiLogAopInfo = new ApiLogAopInfo + { + RequestTime = startTime.ToString("yyyy-MM-dd hh:mm:ss fff"), + OpUserName = UserName, + RequestMethodName = invocation.Method.Name, + RequestParamsName = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()), + ResponseJsonData = json + }; + + //测试异常记录 + //Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff")); //记录被拦截方法信息的日志信息 - var dataIntercept = "" + - $"【当前操作用户】:{ UserName} \r\n" + - $"【当前执行方法】:{ invocation.Method.Name} \r\n" + - $"【携带的参数有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; + //var dataIntercept = "" + + // $"【当前操作用户】:{ UserName} \r\n" + + // $"【当前执行方法】:{ invocation.Method.Name} \r\n" + + // $"【携带的参数有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; try { @@ -58,10 +80,10 @@ public void Intercept(IInvocation invocation) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally( (Task)invocation.ReturnValue, - async () => await SuccessAction(invocation, dataIntercept),/*成功时执行*/ + async () => await SuccessAction(invocation, apiLogAopInfo, startTime),/*成功时执行*/ ex => { - LogEx(ex, dataIntercept); + LogEx(ex, apiLogAopInfo); }); } //Task @@ -71,10 +93,10 @@ public void Intercept(IInvocation invocation) invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, //async () => await SuccessAction(invocation, dataIntercept),/*成功时执行*/ - async (o) => await SuccessAction(invocation, dataIntercept, o),/*成功时执行*/ + async (o) => await SuccessAction(invocation, apiLogAopInfo, startTime, o),/*成功时执行*/ ex => { - LogEx(ex, dataIntercept); + LogEx(ex, apiLogAopInfo); }); } #endregion @@ -85,35 +107,54 @@ public void Intercept(IInvocation invocation) //var type = invocation.Method.ReturnType; //var resultProperty = type.GetProperty("Result"); - //dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue))}"); + //DateTime endTime = DateTime.Now; + //string ResponseTime = (endTime - startTime).Milliseconds.ToString(); + //apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); + //apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; + //apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue)); + + ////dataIntercept += ($"【响应时间】:{ResponseTime}ms\r\n"); + ////dataIntercept += ($"【执行完成时间】:{endTime.ToString("yyyy-MM-dd hh:mm:ss fff")}\r\n"); + ////dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue))}\r\n"); //Parallel.For(0, 1, e => //{ - // LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept }); + // //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); + // LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); //}); #endregion } else - {// 同步1 - - dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}"); + { + // 同步1 + string jsonResult; + try + { + jsonResult = JsonConvert.SerializeObject(invocation.ReturnValue); + } + catch (Exception ex) + { + jsonResult = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); + } + apiLogAopInfo.ResponseJsonData = jsonResult; Parallel.For(0, 1, e => { - LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept }); + LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); }); } } catch (Exception ex)// 同步2 { - LogEx(ex, dataIntercept); - + LogEx(ex, apiLogAopInfo); + } + if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + { + _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); } - - _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); } - private async Task SuccessAction(IInvocation invocation, string dataIntercept, object o = null) + private async Task SuccessAction(IInvocation invocation, ApiLogAopInfo apiLogAopInfo, DateTime startTime, object o = null) { //invocation.ReturnValue = o; //var type = invocation.Method.ReturnType; @@ -127,32 +168,40 @@ private async Task SuccessAction(IInvocation invocation, string dataIntercept, o //{ // dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}"); //} - - dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(o)}"); + DateTime endTime = DateTime.Now; + string ResponseTime = (endTime - startTime).Milliseconds.ToString(); + apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); + apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; + apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(o); await Task.Run(() => { Parallel.For(0, 1, e => { - LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept }); + LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); }); }); } - private void LogEx(Exception ex, string dataIntercept) + private void LogEx(Exception ex, ApiLogAopInfo apiLogAopInfo) { if (ex != null) { //执行的 service 中,收录异常 MiniProfiler.Current.CustomTiming("Errors:", ex.Message); //执行的 service 中,捕获异常 - dataIntercept += ($"【执行完成结果】:方法中出现异常:{ex.Message + ex.InnerException}\r\n"); - + //dataIntercept += ($"【执行完成结果】:方法中出现异常:{ex.Message + ex.InnerException}\r\n"); + ApiLogAopExInfo apiLogAopExInfo = new ApiLogAopExInfo + { + ExMessage = ex.Message, + InnerException = ex.InnerException.ToString(), + ApiLogAopInfo = apiLogAopInfo + }; // 异常日志里有详细的堆栈信息 Parallel.For(0, 1, e => { - LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept }); + LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopExInfo) }); }); } } From a5e7f74a699081e4b54bd2e9d71953cf8c92f758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E6=8B=BE=E7=8E=96?= Date: Fri, 15 Apr 2022 11:45:24 +0800 Subject: [PATCH 045/289] Update MonitorController.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改发送signalR逻辑,粗心大意写错逻辑判断if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) --- Blog.Core.Api/Controllers/MonitorController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/Controllers/MonitorController.cs b/Blog.Core.Api/Controllers/MonitorController.cs index ba7194c4..1ad278ab 100644 --- a/Blog.Core.Api/Controllers/MonitorController.cs +++ b/Blog.Core.Api/Controllers/MonitorController.cs @@ -69,7 +69,7 @@ public MessageModel Server() [HttpGet] public MessageModel> Get() { - if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) { _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); } From 895b788660110223628750286b65c278a82b3b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E6=8B=BE=E7=8E=96?= Date: Fri, 15 Apr 2022 11:45:58 +0800 Subject: [PATCH 046/289] Update GlobalExceptionFilter.cs if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) --- Blog.Core.Api/Filter/GlobalExceptionFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/Filter/GlobalExceptionFilter.cs b/Blog.Core.Api/Filter/GlobalExceptionFilter.cs index f51795d2..9abd32fc 100644 --- a/Blog.Core.Api/Filter/GlobalExceptionFilter.cs +++ b/Blog.Core.Api/Filter/GlobalExceptionFilter.cs @@ -56,7 +56,7 @@ public void OnException(ExceptionContext context) //采用log4net 进行错误日志记录 _loggerHelper.LogError(json.msg + WriteLog(json.msg, context.Exception)); - if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) { _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); } From d9aa83e46290840b24030b9f222d12f4909920a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E6=8B=BE=E7=8E=96?= Date: Fri, 15 Apr 2022 11:46:52 +0800 Subject: [PATCH 047/289] Update ChatHub.cs if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) --- Blog.Core.Common/Hubs/ChatHub.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Common/Hubs/ChatHub.cs b/Blog.Core.Common/Hubs/ChatHub.cs index 1e1ea55a..09a08c7d 100644 --- a/Blog.Core.Common/Hubs/ChatHub.cs +++ b/Blog.Core.Common/Hubs/ChatHub.cs @@ -81,7 +81,7 @@ public async Task SendMessage(string user, string message) public async Task GetLatestCount(string random) { //2、服务端主动向客户端发送数据,名字千万不能错 - if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) { await Clients.All.ReceiveUpdate(LogLock.GetLogData()); } From 9bee0c65970c4037f65ea66d1085c9d1b81022d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E6=8B=BE=E7=8E=96?= Date: Fri, 15 Apr 2022 11:47:44 +0800 Subject: [PATCH 048/289] Update BlogLogAOP.cs if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) --- Blog.Core.Extensions/AOP/BlogLogAOP.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Extensions/AOP/BlogLogAOP.cs b/Blog.Core.Extensions/AOP/BlogLogAOP.cs index 46d1d1c9..a9c38bb1 100644 --- a/Blog.Core.Extensions/AOP/BlogLogAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogLogAOP.cs @@ -148,7 +148,7 @@ public void Intercept(IInvocation invocation) { LogEx(ex, apiLogAopInfo); } - if (!Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + if (Appsettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) { _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); } From f798b0c6f4c06f2141e08047a0c4319973ac4ab4 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 18 Apr 2022 11:33:04 +0800 Subject: [PATCH 049/289] fix: add fluent validator demo --- Blog.Core.Api/Blog.Core.Api.csproj | 2 + Blog.Core.Api/Blog.Core.xml | 7 +++ Blog.Core.Api/Controllers/ValuesController.cs | 13 ++++++ Blog.Core.Api/Filter/UserRegisterVo.cs | 44 +++++++++++++++++++ Blog.Core.Api/Program.cs | 9 ++++ 5 files changed, 75 insertions(+) create mode 100644 Blog.Core.Api/Filter/UserRegisterVo.cs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 9c221839..bb20ce43 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -52,6 +52,8 @@ + + diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 27a3c815..f4b35058 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -837,6 +837,13 @@
+ + + 测试Fulent做参数校验 + + + + Put方法 diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index d214ca07..0a2e4f18 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -356,6 +356,19 @@ public async Task WebApiClientGetAsync() return await _blogApi.DetailNuxtNoPerAsync(id); } + /// + /// 测试Fulent做参数校验 + /// + /// + /// + [HttpPost] + [AllowAnonymous] + public async Task FluentVaTest([FromBody] UserRegisterVo param) + { + await Task.CompletedTask; + return "Okay"; + } + /// /// Put方法 /// diff --git a/Blog.Core.Api/Filter/UserRegisterVo.cs b/Blog.Core.Api/Filter/UserRegisterVo.cs new file mode 100644 index 00000000..67b707e3 --- /dev/null +++ b/Blog.Core.Api/Filter/UserRegisterVo.cs @@ -0,0 +1,44 @@ +using FluentValidation; +using System.Text.RegularExpressions; + +namespace Blog.Core.Filter +{ + public class UserRegisterVo + { + public string WxUid { get; set; } + + public string Telphone { get; set; } + + public string NickName { get; set; } + + public string SourceType { get; set; } + + } + + public class UserRegisterVoValidator : AbstractValidator + { + public UserRegisterVoValidator() + { + When(x => !string.IsNullOrEmpty(x.NickName) || !string.IsNullOrEmpty(x.Telphone), () => + { + RuleFor(x => x.NickName).Must(e => IsLegalName(e)).WithMessage("请填写合法的姓名,必须是汉字和字母"); + RuleFor(x => x.Telphone).Must(e => IsLegalPhone(e)).WithMessage("请填写正确的手机号码"); + }); + + } + + public static bool IsLegalName(string username) + { + //判断用户名是否合法 + const string pattern = "(^([A-Za-z]|[\u4E00-\u9FA5]){1,10}$)"; + return (!string.IsNullOrEmpty(username) && Regex.IsMatch(username, pattern)); + } + public static bool IsLegalPhone(string phone) + { + //判断手机号 + const string pattern = "(^1\\d{10}$)"; + return (!string.IsNullOrEmpty(phone) && Regex.IsMatch(phone, pattern)); + } + } + +} \ No newline at end of file diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index f9dda121..285025c6 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -13,6 +13,7 @@ using Blog.Core.Hubs; using Blog.Core.IServices; using Blog.Core.Tasks; +using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -110,7 +111,15 @@ //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; options.SerializerSettings.Converters.Add(new StringEnumConverter()); +}) +.AddFluentValidation(config => +{ + //程序集方式添加验证 + config.RegisterValidatorsFromAssemblyContaining(typeof(UserRegisterVoValidator)); + //是否与MvcValidation共存 + config.DisableDataAnnotationsValidation = true; }); + builder.Services.AddEndpointsApiExplorer(); builder.Services.Replace(ServiceDescriptor.Transient()); From 2bc4fbf2972cc53c5b9a322b046b8da0fe192a6f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 20 Apr 2022 18:14:27 +0800 Subject: [PATCH 050/289] Fixex #271 bug --- Blog.Core.Api/Controllers/ValuesController.cs | 16 +++-- .../ServiceExtensions/HttpRuntimeCache.cs | 67 +++++++++++++++++++ .../ServiceExtensions/SqlsugarSetup.cs | 6 ++ Blog.Core.Repository/BASE/BaseRepository.cs | 2 +- 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 0a2e4f18..155d6d19 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -14,13 +14,8 @@ using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Linq.Expressions; -using System.Threading.Tasks; namespace Blog.Core.Controllers { @@ -106,6 +101,13 @@ public MessageModel> MyClaims() }; } + [HttpGet] + [AllowAnonymous] + public async Task TestSqlsugarWithCache() + { + return await _blogArticleServices.QueryById("1", true); + } + /// /// Get方法 /// @@ -425,12 +427,12 @@ public async Task HttpPollyPost() public async Task HttpPollyGet() { return await _httpPollyHelper.GetAsync(HttpEnum.LocalHost, "/api/ElasticDemo/GetDetailInfo?esid=3130&esindex=chinacodex"); - } + } #endregion [HttpPost] [AllowAnonymous] - public string TestEnum(EnumDemoDto dto)=>dto.Type.ToString(); + public string TestEnum(EnumDemoDto dto) => dto.Type.ToString(); } public class ClaimDto { diff --git a/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs b/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs new file mode 100644 index 00000000..b9b4d7b3 --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.Caching.Memory; +using SqlSugar; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Blog.Core.Extensions +{ + /// + /// 实现SqlSugar的ICacheService接口 + /// + public class SqlSugarMemoryCacheService : ICacheService + { + protected IMemoryCache _memoryCache; + public SqlSugarMemoryCacheService(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + } + public void Add(string key, V value) + { + _memoryCache.Set(key, value); + } + public void Add(string key, V value, int cacheDurationInSeconds) + { + _memoryCache.Set(key, value, DateTimeOffset.Now.AddSeconds(cacheDurationInSeconds)); + } + public bool ContainsKey(string key) + { + return _memoryCache.TryGetValue(key, out _); + } + + public V Get(string key) + { + return _memoryCache.Get(key); + } + + public IEnumerable GetAllKey() + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; + var entries = _memoryCache.GetType().GetField("_entries", flags).GetValue(_memoryCache); + var cacheItems = entries as IDictionary; + var keys = new List(); + if (cacheItems == null) return keys; + foreach (DictionaryEntry cacheItem in cacheItems) + { + keys.Add(cacheItem.Key.ToString()); + } + return keys; + } + + public V GetOrCreate(string cacheKey, Func create, int cacheDurationInSeconds = int.MaxValue) + { + if (!_memoryCache.TryGetValue(cacheKey, out V value)) + { + value = create(); + _memoryCache.Set(cacheKey, value, DateTime.Now.AddSeconds(cacheDurationInSeconds)); + } + return value; + } + + public void Remove(string key) + { + _memoryCache.Remove(key); + } + } +} diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 8dd04c30..18498ac0 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -2,6 +2,7 @@ using Blog.Core.Common.DB; using Blog.Core.Common.Helper; using Blog.Core.Common.LogHelper; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using SqlSugar; using StackExchange.Profiling; @@ -16,6 +17,8 @@ namespace Blog.Core.Extensions /// public static class SqlsugarSetup { + private static readonly MemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); + public static void AddSqlsugarSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); @@ -26,6 +29,8 @@ public static void AddSqlsugarSetup(this IServiceCollection services) // 把多个连接对象注入服务,这里必须采用Scope,因为有事务操作 services.AddScoped(o => { + var memoryCache = o.GetRequiredService(); + // 连接字符串 var listConfig = new List(); // 从库 @@ -81,6 +86,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) // 自定义特性 ConfigureExternalServices = new ConfigureExternalServices() { + DataInfoCacheService = new SqlSugarMemoryCacheService(memoryCache), EntityService = (property, column) => { if (column.IsPrimarykey && property.PropertyType == typeof(int)) diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index bb83b4c5..0af39b85 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -67,7 +67,7 @@ public async Task QueryById(object objId) public async Task QueryById(object objId, bool blnUseCache = false) { //return await Task.Run(() => _db.Queryable().WithCacheIF(blnUseCache).InSingle(objId)); - return await _db.Queryable().WithCacheIF(blnUseCache).In(objId).SingleAsync(); + return await _db.Queryable().WithCacheIF(blnUseCache, 10).In(objId).SingleAsync(); } /// From 4e07c20e40b3a10b9dc6c28db6445aaff8405e54 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 21 Apr 2022 13:19:30 +0800 Subject: [PATCH 051/289] Update UserRegisterVo.cs --- Blog.Core.Api/Filter/UserRegisterVo.cs | 27 ++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Filter/UserRegisterVo.cs b/Blog.Core.Api/Filter/UserRegisterVo.cs index 67b707e3..163a39e3 100644 --- a/Blog.Core.Api/Filter/UserRegisterVo.cs +++ b/Blog.Core.Api/Filter/UserRegisterVo.cs @@ -12,17 +12,29 @@ public class UserRegisterVo public string NickName { get; set; } public string SourceType { get; set; } + public IEnumerable Cars { get; set; } } + public class CarInfo + { + public int CarCount { get; set; } + public int CarSize { get; set; } + } + public class UserRegisterVoValidator : AbstractValidator { public UserRegisterVoValidator() { When(x => !string.IsNullOrEmpty(x.NickName) || !string.IsNullOrEmpty(x.Telphone), () => { - RuleFor(x => x.NickName).Must(e => IsLegalName(e)).WithMessage("请填写合法的姓名,必须是汉字和字母"); - RuleFor(x => x.Telphone).Must(e => IsLegalPhone(e)).WithMessage("请填写正确的手机号码"); + RuleFor(x => x.NickName) + .Must(e => IsLegalName(e)).WithMessage("请填写合法的姓名,必须是汉字和字母"); + RuleFor(x => x.Telphone) + .Must(e => IsLegalPhone(e)).WithMessage("请填写正确的手机号码"); + RuleFor(x => x.Cars) + .NotNull().NotEmpty().WithMessage("车辆信息不正确"); + RuleForEach(x => x.Cars).SetValidator(new CarInfoValidator()); }); } @@ -40,5 +52,16 @@ public static bool IsLegalPhone(string phone) return (!string.IsNullOrEmpty(phone) && Regex.IsMatch(phone, pattern)); } } + public class CarInfoValidator : AbstractValidator + { + public CarInfoValidator() + { + RuleFor(x => x.CarCount) + .GreaterThanOrEqualTo(0).WithMessage("车辆数量必须大于等于0") + .LessThanOrEqualTo(500).WithMessage($"存在车型数量已达上限"); + RuleFor(x => x.CarSize) + .IsInEnum().WithMessage("车型不正确"); + } + } } \ No newline at end of file From 4caf3cdfa1be10b68a08f8c29c95de52e4beb988 Mon Sep 17 00:00:00 2001 From: weiguang3100 Date: Mon, 25 Apr 2022 11:31:25 +0800 Subject: [PATCH 052/289] =?UTF-8?q?=E4=BF=AE=E6=94=B9DeleteById(object=20i?= =?UTF-8?q?d)=20=E4=BD=8E=E7=89=88=E6=9C=AC=E6=8A=A5=E9=94=99=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Repository/BASE/BaseRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 0af39b85..9ac75731 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -200,7 +200,7 @@ public async Task Delete(TEntity entity) /// public async Task DeleteById(object id) { - return await _db.Deleteable(id).ExecuteCommandHasChangeAsync(); + return await _db.Deleteable().In(id).ExecuteCommandHasChangeAsync(); } /// From 45de851fd3295e7e774060a8fae97827ab447857 Mon Sep 17 00:00:00 2001 From: __Leo__ Date: Wed, 27 Apr 2022 15:50:13 +0800 Subject: [PATCH 053/289] use model binding in InsertPicture --- Blog.Core.Api/Blog.Core.xml | 6 +- Blog.Core.Api/Controllers/ImgController.cs | 71 ++++++++------------- Blog.Core.Model/Blog.Core.Model.csproj | 1 + Blog.Core.Model/ViewModels/UploadFileDto.cs | 25 ++++++++ 4 files changed, 55 insertions(+), 48 deletions(-) create mode 100644 Blog.Core.Model/ViewModels/UploadFileDto.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index f4b35058..b796db32 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -160,12 +160,12 @@ - + - 上传图片,多文件,可以使用 postman 测试, - 如果是单文件,可以 参数写 IFormFile file1 + 上传图片,多文件 + diff --git a/Blog.Core.Api/Controllers/ImgController.cs b/Blog.Core.Api/Controllers/ImgController.cs index 716db28e..2f52b54b 100644 --- a/Blog.Core.Api/Controllers/ImgController.cs +++ b/Blog.Core.Api/Controllers/ImgController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Blog.Core.Model; +using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -16,7 +17,7 @@ namespace Blog.Core.Controllers [Route("api/[controller]")] [ApiController] [Authorize] - public class ImgController : Controller + public class ImgController : BaseApiController { // GET: api/Download /// @@ -42,71 +43,51 @@ public FileStreamResult DownImg([FromServices] IWebHostEnvironment environment) } /// - /// 上传图片,多文件,可以使用 postman 测试, - /// 如果是单文件,可以 参数写 IFormFile file1 + /// 上传图片,多文件 /// /// + /// /// [HttpPost] [Route("/images/Upload/Pic")] - public async Task> InsertPicture([FromServices] IWebHostEnvironment environment) + public async Task> InsertPicture([FromServices] IWebHostEnvironment environment, [FromForm]UploadFileDto dto) { - var data = new MessageModel(); - string path = string.Empty; - string foldername = "images"; - IFormFileCollection files = null; - - - // 获取提交的文件 - files = Request.Form.Files; - // 获取附带的数据 - var max_ver = Request.Form["max_ver"].ObjToString(); - - - if (files == null || !files.Any()) { data.msg = "请选择上传的文件。"; return data; } + + if (dto.Files == null || !dto.Files.Any()) return Failed("请选择上传的文件。"); //格式限制 var allowType = new string[] { "image/jpg", "image/png", "image/jpeg" }; + + var allowedFile = dto.Files.Where(c => allowType.Contains(c.ContentType)); + if (!allowedFile.Any()) return Failed("图片格式错误"); + if (allowedFile.Sum(c => c.Length) > 1024 * 1024 * 4) return Failed("图片过大"); + string foldername = "images"; string folderpath = Path.Combine(environment.WebRootPath, foldername); if (!Directory.Exists(folderpath)) { Directory.CreateDirectory(folderpath); } - - if (files.Any(c => allowType.Contains(c.ContentType))) + foreach (var file in allowedFile) { - if (files.Sum(c => c.Length) <= 1024 * 1024 * 4) - { - //foreach (var file in files) - var file = files.FirstOrDefault(); - string strpath = Path.Combine(foldername, DateTime.Now.ToString("MMddHHmmss") + Path.GetFileName(file.FileName)); - path = Path.Combine(environment.WebRootPath, strpath); - - using (var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite)) - { - await file.CopyToAsync(stream); - } - - data = new MessageModel() - { - response = strpath, - msg = "上传成功", - success = true, - }; - return data; - } - else + string strpath = Path.Combine(foldername, DateTime.Now.ToString("MMddHHmmss") + Path.GetFileName(file.FileName)); + var path = Path.Combine(environment.WebRootPath, strpath); + + using (var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { - data.msg = "图片过大"; - return data; + await file.CopyToAsync(stream); } } - else + var excludeFiles = dto.Files.Except(allowedFile); + + if (excludeFiles.Any()) { - data.msg = "图片格式错误"; - return data; + var infoMsg = $"{string.Join('、', excludeFiles.Select(c => c.FileName))} 图片格式错误"; + return Success(null, infoMsg); } + + return Success(null, "上传成功"); + } diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index e97f4e95..72dd7025 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -15,6 +15,7 @@ + diff --git a/Blog.Core.Model/ViewModels/UploadFileDto.cs b/Blog.Core.Model/ViewModels/UploadFileDto.cs new file mode 100644 index 00000000..fc2b3cf7 --- /dev/null +++ b/Blog.Core.Model/ViewModels/UploadFileDto.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Model.ViewModels +{ + public class UploadFileDto + { + //多文件 + [Required] + public IFormFileCollection Files { get; set; } + + //单文件 + //public IFormFile File { get; set; } + + //其他数据 + public string Foo { get; set; } + + + } +} From d021ddda8348d851ffde7f5b5c5f526b1d598e93 Mon Sep 17 00:00:00 2001 From: __Leo__ Date: Thu, 5 May 2022 09:59:08 +0800 Subject: [PATCH 054/289] Using DI in ImgController --- Blog.Core.Api/Blog.Core.xml | 6 ++-- Blog.Core.Api/Controllers/ImgController.cs | 33 +++++++++++----------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index b796db32..3292c434 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -153,18 +153,16 @@ 图片管理 - + 下载图片(支持中文字符) - - + 上传图片,多文件 - diff --git a/Blog.Core.Api/Controllers/ImgController.cs b/Blog.Core.Api/Controllers/ImgController.cs index 2f52b54b..11d9d209 100644 --- a/Blog.Core.Api/Controllers/ImgController.cs +++ b/Blog.Core.Api/Controllers/ImgController.cs @@ -1,12 +1,6 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Blog.Core.Model; +using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Blog.Core.Controllers @@ -19,18 +13,26 @@ namespace Blog.Core.Controllers [Authorize] public class ImgController : BaseApiController { + + private readonly IWebHostEnvironment _env; + + public ImgController(IWebHostEnvironment webHostEnvironment) + { + _env = webHostEnvironment; + } + + // GET: api/Download /// /// 下载图片(支持中文字符) /// - /// /// [HttpGet] [Route("/images/Down/Pic")] - public FileStreamResult DownImg([FromServices] IWebHostEnvironment environment) + public FileStreamResult DownImg() { string foldername = ""; - string filepath = Path.Combine(environment.WebRootPath, foldername, "测试下载中文名称的图片.png"); + string filepath = Path.Combine(_env.WebRootPath, foldername, "测试下载中文名称的图片.png"); var stream = System.IO.File.OpenRead(filepath); string fileExt = ".jpg"; // 这里可以写一个获取文件扩展名的方法,获取扩展名 //获取文件的ContentType @@ -45,12 +47,11 @@ public FileStreamResult DownImg([FromServices] IWebHostEnvironment environment) /// /// 上传图片,多文件 /// - /// /// /// [HttpPost] [Route("/images/Upload/Pic")] - public async Task> InsertPicture([FromServices] IWebHostEnvironment environment, [FromForm]UploadFileDto dto) + public async Task> InsertPicture([FromForm]UploadFileDto dto) { if (dto.Files == null || !dto.Files.Any()) return Failed("请选择上传的文件。"); @@ -62,7 +63,7 @@ public async Task> InsertPicture([FromServices] IWebHostEnv if (allowedFile.Sum(c => c.Length) > 1024 * 1024 * 4) return Failed("图片过大"); string foldername = "images"; - string folderpath = Path.Combine(environment.WebRootPath, foldername); + string folderpath = Path.Combine(_env.WebRootPath, foldername); if (!Directory.Exists(folderpath)) { Directory.CreateDirectory(folderpath); @@ -70,7 +71,7 @@ public async Task> InsertPicture([FromServices] IWebHostEnv foreach (var file in allowedFile) { string strpath = Path.Combine(foldername, DateTime.Now.ToString("MMddHHmmss") + Path.GetFileName(file.FileName)); - var path = Path.Combine(environment.WebRootPath, strpath); + var path = Path.Combine(_env.WebRootPath, strpath); using (var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { @@ -95,14 +96,14 @@ public async Task> InsertPicture([FromServices] IWebHostEnv [HttpGet] [Route("/images/Down/Bmd")] [AllowAnonymous] - public FileStreamResult DownBmd([FromServices] IWebHostEnvironment environment, string filename) + public FileStreamResult DownBmd(string filename) { if (string.IsNullOrEmpty(filename)) { return null; } // 前端 blob 接收,具体查看前端admin代码 - string filepath = Path.Combine(environment.WebRootPath, Path.GetFileName(filename)); + string filepath = Path.Combine(_env.WebRootPath, Path.GetFileName(filename)); if (System.IO.File.Exists(filepath)) { var stream = System.IO.File.OpenRead(filepath); From 4a942dd4e426459dabb5aa5d2d87b0d9604b653c Mon Sep 17 00:00:00 2001 From: KimiDing Date: Mon, 6 Jun 2022 12:10:17 +0800 Subject: [PATCH 055/289] =?UTF-8?q?MongoRepository=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MongoRepository/IMongoBaseRepository.cs | 5 +- .../MongoRepository/MongoBaseRepository.cs | 16 +++++ .../DependencyInjection/DI_Test.cs | 2 + .../MongoRepository_Base_Should.cs | 60 +++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 Blog.Core.Tests/Repository_Test/MongoRepository_Base_Should.cs diff --git a/Blog.Core.Repository/MongoRepository/IMongoBaseRepository.cs b/Blog.Core.Repository/MongoRepository/IMongoBaseRepository.cs index 28278d39..627ffff7 100644 --- a/Blog.Core.Repository/MongoRepository/IMongoBaseRepository.cs +++ b/Blog.Core.Repository/MongoRepository/IMongoBaseRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using MongoDB.Driver; +using System.Collections.Generic; using System.Threading.Tasks; namespace Blog.Core.Repository.MongoRepository @@ -9,5 +10,7 @@ public interface IMongoBaseRepository where TEntity : class Task AddAsync(TEntity entity); Task GetAsync(int Id); Task> GetListAsync(); + Task GetByObjectIdAsync(string Id); + Task> GetListFilterAsync(FilterDefinition filter); } } diff --git a/Blog.Core.Repository/MongoRepository/MongoBaseRepository.cs b/Blog.Core.Repository/MongoRepository/MongoBaseRepository.cs index 20899f71..feca1f51 100644 --- a/Blog.Core.Repository/MongoRepository/MongoBaseRepository.cs +++ b/Blog.Core.Repository/MongoRepository/MongoBaseRepository.cs @@ -31,11 +31,27 @@ public async Task GetAsync(int Id) .FirstOrDefaultAsync(); } + public async Task GetByObjectIdAsync(string Id) + { + var filter = Builders.Filter.Eq("_id", ObjectId.Parse(Id)); + + return await _context.Db.GetCollection(typeof(TEntity).Name) + .Find(filter) + .FirstOrDefaultAsync(); + } + public async Task> GetListAsync() { return await _context.Db.GetCollection(typeof(TEntity).Name) .Find(new BsonDocument()) .ToListAsync(); } + + public async Task> GetListFilterAsync(FilterDefinition filter) + { + + return await _context.Db.GetCollection(typeof(TEntity).Name) + .Find(filter).ToListAsync(); + } } } diff --git a/Blog.Core.Tests/DependencyInjection/DI_Test.cs b/Blog.Core.Tests/DependencyInjection/DI_Test.cs index fe12e2fe..3bab74e3 100644 --- a/Blog.Core.Tests/DependencyInjection/DI_Test.cs +++ b/Blog.Core.Tests/DependencyInjection/DI_Test.cs @@ -11,6 +11,7 @@ using Blog.Core.Extensions; using Blog.Core.IRepository.Base; using Blog.Core.Repository.Base; +using Blog.Core.Repository.MongoRepository; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.DependencyInjection; @@ -107,6 +108,7 @@ public IContainer DICollections() //指定已扫描程序集中的类型注册为提供所有其实现的接口。 builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>)).InstancePerDependency();//注册仓储 + builder.RegisterGeneric(typeof(MongoBaseRepository<>)).As(typeof(IMongoBaseRepository<>)).InstancePerDependency();//注册仓储 // 属性注入 var controllerBaseType = typeof(ControllerBase); diff --git a/Blog.Core.Tests/Repository_Test/MongoRepository_Base_Should.cs b/Blog.Core.Tests/Repository_Test/MongoRepository_Base_Should.cs new file mode 100644 index 00000000..678125c3 --- /dev/null +++ b/Blog.Core.Tests/Repository_Test/MongoRepository_Base_Should.cs @@ -0,0 +1,60 @@ +using Blog.Core.Model.Models; +using Xunit; +using System; +using System.Linq; +using Autofac; +using Blog.Core.IRepository.Base; +using Blog.Core.Repository.MongoRepository; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Blog.Core.Tests +{ + public class MongoRepository_Base_Should + { + public class MongoTest + { + [BsonId] + public ObjectId id { get; set; } + public string name { get; set; } + public bool isDel { get; set; } + public DateTime time { get; set; } + } + + private IMongoBaseRepository baseRepository; + DI_Test dI_Test = new DI_Test(); + + public MongoRepository_Base_Should() + { + + var container = dI_Test.DICollections(); + + baseRepository = container.Resolve>(); + } + + + [Fact] + public async void Add_Test() + { + await baseRepository.AddAsync(new MongoTest { isDel = false, name = "test", time = DateTime.UtcNow }); + } + + [Fact] + public async void GetObjectId_Test() + { + var data = await baseRepository.GetByObjectIdAsync("612b9b0be677976fa0f0cfa2"); + + Assert.NotNull(data); + } + + + [Fact] + public async void GetListFilter_Test() + { + var data = await baseRepository.GetListFilterAsync(new FilterDefinitionBuilder().Gte("time", DateTime.Parse("2022-06-01"))); + + Assert.NotNull(data); + } + } +} From 78b68aaaf23adf1486bfa295229804ddb911f48c Mon Sep 17 00:00:00 2001 From: zhanganzhong <3143422472@qq.com> Date: Thu, 23 Jun 2022 15:37:21 +0800 Subject: [PATCH 056/289] fix: bump newtonsoft --- Blog.Core.Api/Blog.Core.Api.csproj | 2 +- Blog.Core.Api/Blog.Core.xml | 1 + Blog.Core.Api/Controllers/UserController.cs | 1 + Blog.Core.EventBus/Blog.Core.EventBus.csproj | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index bb20ce43..95c91140 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -6,7 +6,7 @@ net6.0 enable - OutOfProcess + Linux true diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 3292c434..4d68aaad 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -646,6 +646,7 @@ + diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index e42a5359..e100b884 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -41,6 +41,7 @@ public class UserController : BaseApiController /// /// /// + /// /// /// /// diff --git a/Blog.Core.EventBus/Blog.Core.EventBus.csproj b/Blog.Core.EventBus/Blog.Core.EventBus.csproj index c61cc131..a8a478ab 100644 --- a/Blog.Core.EventBus/Blog.Core.EventBus.csproj +++ b/Blog.Core.EventBus/Blog.Core.EventBus.csproj @@ -12,7 +12,7 @@ - + From 6caf37a63fc784a81e1435644822b6fbb1e142f6 Mon Sep 17 00:00:00 2001 From: zhanganzhong <3143422472@qq.com> Date: Thu, 30 Jun 2022 14:10:16 +0800 Subject: [PATCH 057/289] Update Log4net.config --- Blog.Core.Api/Log4net.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config index 1e65e805..a576a246 100644 --- a/Blog.Core.Api/Log4net.config +++ b/Blog.Core.Api/Log4net.config @@ -412,8 +412,8 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -421,9 +498,10 @@ --> - + + diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 4fd3243b..e7338230 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -52,23 +52,29 @@ "Enabled": true }, "LogAOP": { - "Enabled": false + "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + } }, "TranAOP": { "Enabled": true }, "SqlAOP": { "Enabled": true, - "OutToLogFile": { + "LogToFile": { "Enabled": false }, - "OutToConsole": { + "LogToDB": { + "Enabled": false + }, + "LogToConsole": { "Enabled": true } }, - "LogToDb": { - "Enabled": true - }, "Date": "2018-08-28", "SeedDBEnabled": true, //只生成表结构 "SeedDBDataEnabled": true, //生成表,并初始化数据 @@ -194,13 +200,31 @@ }, "Middleware": { "RequestResponseLog": { - "Enabled": false + "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + } }, "IPLog": { - "Enabled": true + "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + } }, "RecordAccessLogs": { "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + }, "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," }, "SignalR": { diff --git a/Blog.Core.Common/LogHelper/LogLock.cs b/Blog.Core.Common/LogHelper/LogLock.cs index 404eb7be..ecff315a 100644 --- a/Blog.Core.Common/LogHelper/LogLock.cs +++ b/Blog.Core.Common/LogHelper/LogLock.cs @@ -23,17 +23,56 @@ public LogLock(string contentPath) _contentRoot = contentPath; } - public static void OutSql2Log(string prefix, string[] dataParas, bool IsHeader = true, bool isWrt = false) + public static void OutLogAOP(string prefix, string[] dataParas, bool IsHeader = true, bool isWrt = false) { - - if (AppSettings.app(new string[] { "AppSettings", "LogToDb", "Enabled" }).ObjToBool()) + string AppSetingNodeName = "AppSettings"; + string AppSetingName = "LogAOP"; + switch (prefix) { - OutSql2LogToDB(prefix, dataParas, IsHeader); + case "AOPLog": + AppSetingName = "LogAOP"; + break; + case "AOPLogEx": + AppSetingName = "LogAOP"; + break; + case "RequestIpInfoLog": + AppSetingNodeName = "Middleware"; + AppSetingName = "IPLog"; + break; + case "RecordAccessLogs": + AppSetingNodeName = "Middleware"; + AppSetingName = "RecordAccessLogs"; + break; + case "SqlLog": + AppSetingName = "SqlAOP"; + break; + case "RequestResponseLog": + AppSetingNodeName = "Middleware"; + AppSetingName = "RequestResponseLog"; + break; + default: + break; } - else + if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "Enabled" }).ObjToBool()) { - OutSql2LogToFile(prefix, dataParas, IsHeader, isWrt); + if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "LogToDB", "Enabled" }).ObjToBool()) + { + OutSql2LogToDB(prefix, dataParas, IsHeader); + } + if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "LogToFile", "Enabled" }).ObjToBool()) + { + OutSql2LogToFile(prefix, dataParas, IsHeader); + } } + + //if (AppSettings.app(new string[] { "AppSettings", "LogFile", "Enabled" }).ObjToBool()) + //{ + // OutSql2LogFile(prefix, dataParas, IsHeader); + //} + //else + //{ + // OutSql2Log(prefix, dataParas, IsHeader); + //} } public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHeader = true, bool isWrt = false) @@ -56,7 +95,7 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe switch (prefix) { case "AOPLog": - ApiLogAopInfo apiLogAopInfo = JsonConvert.DeserializeObject(dataParas[0]); + AOPLogInfo apiLogAopInfo = JsonConvert.DeserializeObject(dataParas[1]); //记录被拦截方法信息的日志信息 var dataIntercept = "" + $"【操作时间】:{apiLogAopInfo.RequestTime}\r\n" + @@ -70,7 +109,7 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe dataParas = new string[] { dataIntercept }; break; case "AOPLogEx": - ApiLogAopExInfo apiLogAopExInfo = JsonConvert.DeserializeObject(dataParas[0]); + AOPLogExInfo apiLogAopExInfo = JsonConvert.DeserializeObject(dataParas[1]); var dataInterceptEx = "" + $"【操作时间】:{apiLogAopExInfo.ApiLogAopInfo.RequestTime}\r\n" + $"【当前操作用户】:{ apiLogAopExInfo.ApiLogAopInfo.OpUserName} \r\n" + @@ -95,6 +134,12 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe String.Join("\r\n", dataParas) + "\r\n" ); } + else + { + logContent = ( + dataParas[1] + ",\r\n" + ); + } //if (logContent.IsNotEmptyOrNull() && logContent.Length > 500) //{ @@ -127,6 +172,13 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe } public static void OutSql2LogToDB(string prefix, string[] dataParas, bool IsHeader = true) { + log4net.LogicalThreadContext.Properties["LogType"] = prefix; + if (dataParas.Length >= 2) + { + log4net.LogicalThreadContext.Properties["DataType"] = dataParas[0]; + } + + dataParas = dataParas.Skip(1).ToArray(); string logContent = String.Join("", dataParas); if (IsHeader) @@ -135,6 +187,7 @@ public static void OutSql2LogToDB(string prefix, string[] dataParas, bool IsHead } switch (prefix) { + //DEBUG | INFO | WARN | ERROR | FATAL case "AOPLog": log.Info(logContent); break; diff --git a/Blog.Core.Common/LogHelper/RequestInfo.cs b/Blog.Core.Common/LogHelper/RequestInfo.cs index c5973f89..02160475 100644 --- a/Blog.Core.Common/LogHelper/RequestInfo.cs +++ b/Blog.Core.Common/LogHelper/RequestInfo.cs @@ -40,7 +40,7 @@ public class RequestInfo } - public class ApiLogAopInfo + public class AOPLogInfo { /// /// 请求时间 @@ -76,9 +76,9 @@ public class ApiLogAopInfo public string ResponseJsonData { get; set; } = string.Empty; } - public class ApiLogAopExInfo + public class AOPLogExInfo { - public ApiLogAopInfo ApiLogAopInfo { get; set; } + public AOPLogInfo ApiLogAopInfo { get; set; } /// /// 异常 /// @@ -88,4 +88,22 @@ public class ApiLogAopExInfo /// public string ExMessage { get; set; } = string.Empty; } + + public class RequestLogInfo + { + /// + /// 请求地址 + /// + public string Path { get; set; } + + /// + /// 请求参数 + /// + public string QueryString { get; set; } + + /// + /// Body参数 + /// + public string BodyData { get; set; } + } } diff --git a/Blog.Core.Extensions/AOP/BlogLogAOP.cs b/Blog.Core.Extensions/AOP/BlogLogAOP.cs index 86344c96..18868f43 100644 --- a/Blog.Core.Extensions/AOP/BlogLogAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogLogAOP.cs @@ -45,7 +45,7 @@ public void Intercept(IInvocation invocation) json = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); } DateTime startTime = DateTime.Now; - ApiLogAopInfo apiLogAopInfo = new ApiLogAopInfo + AOPLogInfo apiLogAopInfo = new AOPLogInfo { RequestTime = startTime.ToString("yyyy-MM-dd hh:mm:ss fff"), OpUserName = UserName, @@ -103,7 +103,7 @@ public void Intercept(IInvocation invocation) // 如果方案一不行,试试这个方案 - #region 方案二 + //#region 方案二 //var type = invocation.Method.ReturnType; //var resultProperty = type.GetProperty("Result"); @@ -120,10 +120,10 @@ public void Intercept(IInvocation invocation) //Parallel.For(0, 1, e => //{ // //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); - // LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); + // LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString() + " - ResponseJsonDataType:" + type, JsonConvert.SerializeObject(apiLogAopInfo) }); //}); - #endregion + //#endregion } else { @@ -137,10 +137,19 @@ public void Intercept(IInvocation invocation) { jsonResult = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); } + var type = invocation.Method.ReturnType; + var resultProperty = type.GetProperty("Result"); + DateTime endTime = DateTime.Now; + string ResponseTime = (endTime - startTime).Milliseconds.ToString(); + apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); + apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; + //apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue)); apiLogAopInfo.ResponseJsonData = jsonResult; + //dataIntercept += ($"【执行完成结果】:{jsonResult}"); Parallel.For(0, 1, e => { - LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); + //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); + LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); }); } } @@ -154,7 +163,7 @@ public void Intercept(IInvocation invocation) } } - private async Task SuccessAction(IInvocation invocation, ApiLogAopInfo apiLogAopInfo, DateTime startTime, object o = null) + private async Task SuccessAction(IInvocation invocation, AOPLogInfo apiLogAopInfo, DateTime startTime, object o = null) { //invocation.ReturnValue = o; //var type = invocation.Method.ReturnType; @@ -179,12 +188,13 @@ await Task.Run(() => { Parallel.For(0, 1, e => { - LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); + //LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); + LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); }); }); } - private void LogEx(Exception ex, ApiLogAopInfo apiLogAopInfo) + private void LogEx(Exception ex, AOPLogInfo dataIntercept) { if (ex != null) { @@ -192,16 +202,18 @@ private void LogEx(Exception ex, ApiLogAopInfo apiLogAopInfo) MiniProfiler.Current.CustomTiming("Errors:", ex.Message); //执行的 service 中,捕获异常 //dataIntercept += ($"【执行完成结果】:方法中出现异常:{ex.Message + ex.InnerException}\r\n"); - ApiLogAopExInfo apiLogAopExInfo = new ApiLogAopExInfo + AOPLogExInfo apiLogAopExInfo = new AOPLogExInfo { ExMessage = ex.Message, - InnerException = ex.InnerException.ToString(), - ApiLogAopInfo = apiLogAopInfo + InnerException = "InnerException-内部异常:\r\n" + (ex.InnerException == null ? "" : ex.InnerException.InnerException.ToString()) + ("\r\nStackTrace-堆栈跟踪:\r\n") + (ex.StackTrace == null ? "" : ex.StackTrace.ToString()), + ApiLogAopInfo = dataIntercept }; // 异常日志里有详细的堆栈信息 Parallel.For(0, 1, e => { - LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopExInfo) }); + //LogLock.OutLogAOP("AOPLogEx", new string[] { dataIntercept }); + LogLock.OutLogAOP("AOPLogEx", new string[] { apiLogAopExInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopExInfo) }); + }); } } diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index abb3736a..aca15b55 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -20,6 +20,7 @@ + diff --git a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs index dd2930fb..2710e8a5 100644 --- a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs @@ -44,6 +44,7 @@ public async Task InvokeAsync(HttpContext context) { // 存储请求数据 var request = context.Request; + var requestInfo = JsonConvert.SerializeObject(new RequestInfo() { Ip = GetClientIP(context), @@ -58,7 +59,8 @@ public async Task InvokeAsync(HttpContext context) // 自定义log输出 Parallel.For(0, 1, e => { - LogLock.OutSql2Log("RequestIpInfoLog", new string[] { requestInfo + "," }, false); + //LogLock.OutSql2Log("RequestIpInfoLog", new string[] { requestInfo + "," }, false); + LogLock.OutLogAOP("RequestIpInfoLog", new string[] { requestInfo.GetType().ToString(), requestInfo }, false); }); //try diff --git a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs index eb2e75f4..d2f5e0e5 100644 --- a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs @@ -106,15 +106,13 @@ public async Task InvokeAsync(HttpContext context) // 自定义log输出 var requestInfo = JsonConvert.SerializeObject(userAccessModel); - //Parallel.For(0, 1, e => - //{ - // LogLock.OutSql2Log("RecordAccessLogs", new string[] { requestInfo + "," }, false); - //}); - - var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RecordAccessLogs"); - SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false); - - + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RecordAccessLogs", new string[] { requestInfo + "," }, false); + LogLock.OutLogAOP("RecordAccessLogs", new string[] { userAccessModel.GetType().ToString(), requestInfo }, false); + }); + //var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RecordAccessLogs"); + //SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false); return Task.CompletedTask; }); diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index 4ecde84c..c9281d83 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -6,6 +6,8 @@ using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ubiety.Dns.Core.Common; namespace Blog.Core.Extensions.Middlewares { @@ -86,17 +88,24 @@ private async Task RequestDataLog(HttpContext context) { var request = context.Request; var sr = new StreamReader(request.Body); - - var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{await sr.ReadToEndAsync()}"; + RequestLogInfo requestResponse = new RequestLogInfo() + { + Path = request.Path, + QueryString = request.QueryString.ToString(), + BodyData = await sr.ReadToEndAsync() + }; + var content = JsonConvert.SerializeObject(requestResponse); + //var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{await sr.ReadToEndAsync()}"; if (!string.IsNullOrEmpty(content)) { - //Parallel.For(0, 1, e => - //{ - // LogLock.OutSql2Log("RequestResponseLog", new string[] { "Request Data:", content }); + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Request Data:", content }); + LogLock.OutLogAOP("RequestResponseLog", new string[] { "Request Data - RequestJsonDataType:" + requestResponse.GetType().ToString(), content }); - //}); - SerilogServer.WriteLog("RequestResponseLog", new string[] { "Request Data:", content }); + }); + //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Request Data:", content }); request.Body.Position = 0; } @@ -113,12 +122,13 @@ private void ResponseDataLog(HttpResponse response, MemoryStream ms) if (!string.IsNullOrEmpty(responseBody)) { - //Parallel.For(0, 1, e => - //{ - // LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); + LogLock.OutLogAOP("RequestResponseLog", new string[] { "Response Data - ResponseJsonDataType:" + responseBody.GetType().ToString(), responseBody }); - //}); - SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); + }); + //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); } } } diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index cec466a7..2102f580 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -66,7 +66,8 @@ public static void AddSqlsugarSetup(this IServiceCollection services) Parallel.For(0, 1, e => { MiniProfiler.Current.CustomTiming("SQL:", GetParas(p) + "【SQL语句】:" + sql); - LogLock.OutSql2Log("SqlLog", new string[] { GetParas(p), "【SQL语句】:" + sql }); + //LogLock.OutSql2Log("SqlLog", new string[] { GetParas(p), "【SQL语句】:" + sql }); + LogLock.OutLogAOP("SqlLog", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); }); } diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs new file mode 100644 index 00000000..8b620684 --- /dev/null +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -0,0 +1,64 @@ +using SqlSugar; +using System; + +namespace Blog.Core.Model.Models +{ + /// + /// 用户团队表 + /// + [SugarTable("GblLogAudit", TableDescription = "日志审计")] + public class GblLogAudit + { + /// + ///ID + /// + [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = false, IsIdentity = true)] + public int Id { get; set; } + + /// + ///时间 + /// + [SugarColumn(ColumnDescription = "时间", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "datetime")] + public DateTime Date { get; set; } = DateTime.Now; + + /// + ///线程 + /// + [SugarColumn(ColumnDescription = "线程", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + public string Thread { get; set; } + + /// + ///等级 + /// + [SugarColumn(ColumnDescription = "等级", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + public string Level { get; set; } + /// + ///记录器 + /// + [SugarColumn(ColumnDescription = "记录器", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + public string Logger { get; set; } + /// + ///日志类型 + /// + [SugarColumn(ColumnDescription = "日志类型", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + public string LogType { get; set; } + /// + ///数据类型 + /// + [SugarColumn(ColumnDescription = "数据类型", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + public string DataType { get; set; } + + /// + ///错误信息 + /// + [SugarColumn(ColumnDescription = "错误信息", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "text")] + public string Message { get; set; } + + /// + ///异常 + /// + [SugarColumn(ColumnDescription = "异常", IsNullable = true, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "text")] + public string Exception { get; set; } + + } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/TopicDetail.cs b/Blog.Core.Model/Models/TopicDetail.cs index f2f19a3f..87e16ebf 100644 --- a/Blog.Core.Model/Models/TopicDetail.cs +++ b/Blog.Core.Model/Models/TopicDetail.cs @@ -19,7 +19,7 @@ public TopicDetail() [SugarColumn(Length = 200, IsNullable = true)] public string tdName { get; set; } - [SugarColumn(Length = 2000, IsNullable = true)] + [SugarColumn(Length = 6000, IsNullable = true)] public string tdContent { get; set; } [SugarColumn(Length = 2000, IsNullable = true)] diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index 5f433cf6..8aafb164 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -36,6 +36,8 @@ public SysUserInfo(string loginName, string loginPWD) //[SugarColumn(IsNullable = false, ColumnDescription = "登录账号", IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "nvarchar", Length = 50)] //ColumnDescription 表字段备注, 已在MSSQL测试,配合 [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') //可以完整生成 表备注和各个字段的中文备注 + //2022/10/11 + //测试mssql 发现 不写ColumnDescription,写好注释在mssql下也能生成表字段备注 public string LoginName { get; set; } /// diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs index 225266b6..b4420153 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs @@ -93,7 +93,7 @@ await _accessTrendLogServices.Add(new AccessTrendLog() Parallel.For(0, 1, e => { - LogLock.OutSql2Log("ACCESSTRENDLOG", new string[] { JsonConvert.SerializeObject(activeUserVMs) }, false, true); + LogLock.OutLogAOP("ACCESSTRENDLOG", new string[] { activeUserVMs.GetType().ToString(), JsonConvert.SerializeObject(activeUserVMs) }, false, true); }); } From be0472c598fce09e8e95bd5f576810284c17c10b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 13 Nov 2022 12:30:15 +0800 Subject: [PATCH 092/289] fix: make some change --- Blog.Core.Api/Blog.Core.Model.xml | 50 +++++++++++++++++++++++++++ Blog.Core.Api/appsettings.json | 8 ++--- Blog.Core.Model/Models/GblLogAudit.cs | 2 +- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 48c8b1e6..3bbf8fe6 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -376,6 +376,56 @@ Nullable:True + + + 用户团队表 + + + + + ID + + + + + 时间 + + + + + 线程 + + + + + 等级 + + + + + 记录器 + + + + + 日志类型 + + + + + 数据类型 + + + + + 错误信息 + + + + + 异常 + + 博客ID diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index e7338230..e46aef2d 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -52,7 +52,7 @@ "Enabled": true }, "LogAOP": { - "Enabled": true, + "Enabled": false, "LogToFile": { "Enabled": true }, @@ -64,7 +64,7 @@ "Enabled": true }, "SqlAOP": { - "Enabled": true, + "Enabled": false, "LogToFile": { "Enabled": false }, @@ -200,7 +200,7 @@ }, "Middleware": { "RequestResponseLog": { - "Enabled": true, + "Enabled": false, "LogToFile": { "Enabled": true }, @@ -218,7 +218,7 @@ } }, "RecordAccessLogs": { - "Enabled": true, + "Enabled": false, "LogToFile": { "Enabled": true }, diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs index 8b620684..1ee9ba95 100644 --- a/Blog.Core.Model/Models/GblLogAudit.cs +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -12,7 +12,7 @@ public class GblLogAudit /// ///ID /// - [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = false, IsIdentity = true)] + [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] public int Id { get; set; } /// From 64a628fa62a74f34df5cb419afb038ad0df83923 Mon Sep 17 00:00:00 2001 From: Nine Date: Wed, 23 Nov 2022 14:05:04 +0800 Subject: [PATCH 093/289] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E4=B8=BA?= =?UTF-8?q?SQLite=E7=BB=93=E6=9E=84=E5=8C=96=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=8CLogAOP=EF=BC=8CRecordAccessLogs=EF=BC=8CIPLog?= =?UTF-8?q?=EF=BC=8CRequestResponseLog=EF=BC=8C=E9=BB=98=E8=AE=A4=E8=AE=B0?= =?UTF-8?q?=E5=BD=95sqlite=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=96=87=E4=BB=B6=EF=BC=8CSqlAOP=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=8F=AA=E5=9C=A8=E6=8E=A7=E5=88=B6=E5=8F=B0=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E4=B8=8D=E8=AE=B0=E5=BD=95=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=92=8C=E6=96=87=E4=BB=B6=202.=E8=AE=BE=E7=BD=AEswag?= =?UTF-8?q?ger=E6=89=93=E5=BC=80=E9=BB=98=E8=AE=A4=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=8E=A5=E5=8F=A3=E9=87=8F=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=90=8E=EF=BC=8C=E6=9F=A5=E6=89=BE=E5=8F=98=E5=BE=97?= =?UTF-8?q?=E9=BA=BB=E7=83=A6=EF=BC=8C=E8=87=AA=E5=8A=A8=E6=8A=98=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Api.csproj | 3 + Blog.Core.Api/Log4net.config | 601 +----------------- Blog.Core.Api/appsettings.json | 14 +- .../Middlewares/SwaggerMiddleware.cs | 2 + 4 files changed, 23 insertions(+), 597 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index fd45bf7d..38575a55 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -49,6 +49,8 @@ + + @@ -92,6 +94,7 @@ + diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config index 5275d675..132f47d0 100644 --- a/Blog.Core.Api/Log4net.config +++ b/Blog.Core.Api/Log4net.config @@ -1,417 +1,12 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - + + + @@ -477,190 +72,16 @@ - - + + - - - - - - - - - - - - - + + - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index e46aef2d..381c0671 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -52,9 +52,9 @@ "Enabled": true }, "LogAOP": { - "Enabled": false, + "Enabled": true, "LogToFile": { - "Enabled": true + "Enabled": false }, "LogToDB": { "Enabled": true @@ -200,9 +200,9 @@ }, "Middleware": { "RequestResponseLog": { - "Enabled": false, + "Enabled": true, "LogToFile": { - "Enabled": true + "Enabled": false }, "LogToDB": { "Enabled": true @@ -211,16 +211,16 @@ "IPLog": { "Enabled": true, "LogToFile": { - "Enabled": true + "Enabled": false }, "LogToDB": { "Enabled": true } }, "RecordAccessLogs": { - "Enabled": false, + "Enabled": true, "LogToFile": { - "Enabled": true + "Enabled": false }, "LogToDB": { "Enabled": true diff --git a/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs b/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs index 34dfd04c..099b2576 100644 --- a/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs @@ -4,6 +4,7 @@ using Blog.Core.Common; using log4net; using Microsoft.AspNetCore.Builder; +using Swashbuckle.AspNetCore.SwaggerUI; using static Blog.Core.Extensions.CustomApiVersion; namespace Blog.Core.Extensions.Middlewares @@ -38,6 +39,7 @@ public static void UseSwaggerMiddle(this IApplicationBuilder app, Func s throw new Exception(msg); } c.IndexStream = streamHtml; + c.DocExpansion(DocExpansion.None); //->修改界面打开时自动折叠 if (Permissions.IsUseIds4) { From ab3a2c7b82a30a1664be4e7546d653833cdffca5 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Wed, 23 Nov 2022 22:25:17 +0800 Subject: [PATCH 094/289] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 229a36fc..ef68b193 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 优化权限处理器,解决多实例分布式下,权限不同步问题(必须配置Redis); - [x] 增加在线用户查看功能,并实现强制用户下线功能(必须配置Redis); - [x] 增加用户黑名单功能(必须配置Redis); -- [ ] 打算增加岗位功能(单独建表),配合部门使用,目前是字符串(设计中,未完全实现); +- [x] 增加岗位功能(单独建表),配合部门使用; - [ ] 后期优化站内通知功能,其实目前已经有SignalR来实现消息推送了,可以直接用; - [ ] 前端`Blog.Admin.Pro`使用`AntDesignVue`框架(设计中,未完全实现); - [x] 铁粉奖励:如果参与上述功能和其他付费功能开发,可半价获取商业授权; @@ -78,7 +78,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 支持自由切换多种数据库,MySql/SqlServer/Sqlite/Oracle/Postgresql/达梦/人大金仓; - [x] 实现项目启动,自动生成种子数据 ✨; - [x] 实现数据库主键类型配置化,什么类型都可以自定义 ✨; -- [x] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等; +- [x] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等,并自动持久化到数据库表🎶; - [x] 支持项目事务处理(若要分布式,用cap即可)✨; - [x] 设计4种 AOP 切面编程,功能涵盖:日志、缓存、审计、事务 ✨; - [x] 支持 T4 代码模板,自动生成每层代码; @@ -121,6 +121,10 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 可配合 Ids4 实现认证中心; +### 自定义全部日志记录图 +![系统架构图](https://img.neters.club/github/log5.png) + + ### 自定义(中间件/服务)启动图 ![系统架构图](https://img.neters.club/github/load-tool.png) From aeae9bc362acf89ce60a888efd2374575afd61d0 Mon Sep 17 00:00:00 2001 From: Nine Date: Thu, 24 Nov 2022 14:12:19 +0800 Subject: [PATCH 095/289] =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E5=8A=A0=E5=85=A5TraceId=EF=BC=8C=E9=93=BE=E8=B7=AFID=E7=94=A8?= =?UTF-8?q?=E4=BA=8E=E6=9F=A5=E8=AF=A2=E6=95=B4=E4=B8=AA=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=89=80=E6=9C=89=E7=9B=B8=E5=85=B3=E6=97=A5=E5=BF=97=EF=BC=8C?= =?UTF-8?q?HttpContext.TraceIdentifier=20=E5=B1=9E=E6=80=A7=EF=BC=88?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=88=96=E8=AE=BE=E7=BD=AE=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E6=A0=87=E8=AF=86=E7=AC=A6=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=E4=BA=8E=E5=9C=A8=E8=B7=9F=E8=B8=AA=E6=97=A5=E5=BF=97=E4=B8=AD?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E6=AD=A4=E8=AF=B7=E6=B1=82=E3=80=82=EF=BC=89?= =?UTF-8?q?=20=E7=9B=AE=E5=89=8Dsql=E6=89=93=E5=8D=B0=E7=9A=84=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=8A=A0=E5=85=A5=E5=92=8C=E8=AE=A1=E5=88=92=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E7=9A=84=E6=B2=A1=E6=9C=89=E6=B7=BB=E5=8A=A0TraceId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Model.xml | 5 +++++ Blog.Core.Api/Log4net.config | 10 +++++++++- Blog.Core.Common/LogHelper/LogLock.cs | 11 ++++++----- Blog.Core.Extensions/AOP/BlogLogAOP.cs | 6 +++--- Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs | 2 +- .../Middlewares/RecordAccessLogsMiddleware.cs | 2 +- .../Middlewares/RequRespLogMiddleware.cs | 4 ++-- .../ServiceExtensions/SqlsugarSetup.cs | 2 +- Blog.Core.Model/Models/GblLogAudit.cs | 6 ++++++ .../QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs | 2 +- 10 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 3bbf8fe6..36975d11 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -386,6 +386,11 @@ ID + + + HttpContext.TraceIdentifier 事件链路ID(获取或设置一个唯一标识符,用于在跟踪日志中表示此请求。) + + 时间 diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config index 132f47d0..3e718044 100644 --- a/Blog.Core.Api/Log4net.config +++ b/Blog.Core.Api/Log4net.config @@ -7,7 +7,7 @@ - + @@ -67,6 +67,14 @@ + + + + + + + + diff --git a/Blog.Core.Common/LogHelper/LogLock.cs b/Blog.Core.Common/LogHelper/LogLock.cs index ecff315a..70c5f7f2 100644 --- a/Blog.Core.Common/LogHelper/LogLock.cs +++ b/Blog.Core.Common/LogHelper/LogLock.cs @@ -23,7 +23,7 @@ public LogLock(string contentPath) _contentRoot = contentPath; } - public static void OutLogAOP(string prefix, string[] dataParas, bool IsHeader = true, bool isWrt = false) + public static void OutLogAOP(string prefix, string traceId, string[] dataParas, bool IsHeader = true) { string AppSetingNodeName = "AppSettings"; string AppSetingName = "LogAOP"; @@ -57,11 +57,11 @@ public static void OutLogAOP(string prefix, string[] dataParas, bool IsHeader = { if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "LogToDB", "Enabled" }).ObjToBool()) { - OutSql2LogToDB(prefix, dataParas, IsHeader); + OutSql2LogToDB(prefix, traceId, dataParas, IsHeader); } if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "LogToFile", "Enabled" }).ObjToBool()) { - OutSql2LogToFile(prefix, dataParas, IsHeader); + OutSql2LogToFile(prefix, traceId, dataParas, IsHeader); } } @@ -75,7 +75,7 @@ public static void OutLogAOP(string prefix, string[] dataParas, bool IsHeader = //} } - public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHeader = true, bool isWrt = false) + public static void OutSql2LogToFile(string prefix, string traceId, string[] dataParas, bool IsHeader = true, bool isWrt = false) { try { @@ -170,9 +170,10 @@ public static void OutSql2LogToFile(string prefix, string[] dataParas, bool IsHe LogWriteLock.ExitWriteLock(); } } - public static void OutSql2LogToDB(string prefix, string[] dataParas, bool IsHeader = true) + public static void OutSql2LogToDB(string prefix, string traceId, string[] dataParas, bool IsHeader = true) { log4net.LogicalThreadContext.Properties["LogType"] = prefix; + log4net.LogicalThreadContext.Properties["TraceId"] = traceId; if (dataParas.Length >= 2) { log4net.LogicalThreadContext.Properties["DataType"] = dataParas[0]; diff --git a/Blog.Core.Extensions/AOP/BlogLogAOP.cs b/Blog.Core.Extensions/AOP/BlogLogAOP.cs index 18868f43..29861578 100644 --- a/Blog.Core.Extensions/AOP/BlogLogAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogLogAOP.cs @@ -149,7 +149,7 @@ public void Intercept(IInvocation invocation) Parallel.For(0, 1, e => { //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); - LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); + LogLock.OutLogAOP("AOPLog", _accessor.HttpContext?.TraceIdentifier, new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); }); } } @@ -189,7 +189,7 @@ await Task.Run(() => Parallel.For(0, 1, e => { //LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); - LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); + LogLock.OutLogAOP("AOPLog", _accessor.HttpContext?.TraceIdentifier, new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); }); }); } @@ -212,7 +212,7 @@ private void LogEx(Exception ex, AOPLogInfo dataIntercept) Parallel.For(0, 1, e => { //LogLock.OutLogAOP("AOPLogEx", new string[] { dataIntercept }); - LogLock.OutLogAOP("AOPLogEx", new string[] { apiLogAopExInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopExInfo) }); + LogLock.OutLogAOP("AOPLogEx", _accessor.HttpContext?.TraceIdentifier, new string[] { apiLogAopExInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopExInfo) }); }); } diff --git a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs index 2710e8a5..b6b91bd5 100644 --- a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs @@ -60,7 +60,7 @@ public async Task InvokeAsync(HttpContext context) Parallel.For(0, 1, e => { //LogLock.OutSql2Log("RequestIpInfoLog", new string[] { requestInfo + "," }, false); - LogLock.OutLogAOP("RequestIpInfoLog", new string[] { requestInfo.GetType().ToString(), requestInfo }, false); + LogLock.OutLogAOP("RequestIpInfoLog", context.TraceIdentifier, new string[] { requestInfo.GetType().ToString(), requestInfo }, false); }); //try diff --git a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs index d2f5e0e5..4e12dd93 100644 --- a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs @@ -109,7 +109,7 @@ public async Task InvokeAsync(HttpContext context) Parallel.For(0, 1, e => { //LogLock.OutSql2Log("RecordAccessLogs", new string[] { requestInfo + "," }, false); - LogLock.OutLogAOP("RecordAccessLogs", new string[] { userAccessModel.GetType().ToString(), requestInfo }, false); + LogLock.OutLogAOP("RecordAccessLogs", context.TraceIdentifier, new string[] { userAccessModel.GetType().ToString(), requestInfo }, false); }); //var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RecordAccessLogs"); //SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false); diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index c9281d83..6ff8d044 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -102,7 +102,7 @@ private async Task RequestDataLog(HttpContext context) Parallel.For(0, 1, e => { //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Request Data:", content }); - LogLock.OutLogAOP("RequestResponseLog", new string[] { "Request Data - RequestJsonDataType:" + requestResponse.GetType().ToString(), content }); + LogLock.OutLogAOP("RequestResponseLog", context.TraceIdentifier, new string[] { "Request Data - RequestJsonDataType:" + requestResponse.GetType().ToString(), content }); }); //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Request Data:", content }); @@ -125,7 +125,7 @@ private void ResponseDataLog(HttpResponse response, MemoryStream ms) Parallel.For(0, 1, e => { //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); - LogLock.OutLogAOP("RequestResponseLog", new string[] { "Response Data - ResponseJsonDataType:" + responseBody.GetType().ToString(), responseBody }); + LogLock.OutLogAOP("RequestResponseLog", response.HttpContext.TraceIdentifier, new string[] { "Response Data - ResponseJsonDataType:" + responseBody.GetType().ToString(), responseBody }); }); //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 2102f580..8148313a 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -67,7 +67,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { MiniProfiler.Current.CustomTiming("SQL:", GetParas(p) + "【SQL语句】:" + sql); //LogLock.OutSql2Log("SqlLog", new string[] { GetParas(p), "【SQL语句】:" + sql }); - LogLock.OutLogAOP("SqlLog", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); + LogLock.OutLogAOP("SqlLog","", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); }); } diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs index 1ee9ba95..58e04dd6 100644 --- a/Blog.Core.Model/Models/GblLogAudit.cs +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -15,6 +15,12 @@ public class GblLogAudit [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] public int Id { get; set; } + /// + ///HttpContext.TraceIdentifier 事件链路ID(获取或设置一个唯一标识符,用于在跟踪日志中表示此请求。) + /// + [SugarColumn(ColumnDescription = "事件链路ID", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + public string TraceId { get; set; } + /// ///时间 /// diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs index b4420153..743180c3 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs @@ -93,7 +93,7 @@ await _accessTrendLogServices.Add(new AccessTrendLog() Parallel.For(0, 1, e => { - LogLock.OutLogAOP("ACCESSTRENDLOG", new string[] { activeUserVMs.GetType().ToString(), JsonConvert.SerializeObject(activeUserVMs) }, false, true); + LogLock.OutLogAOP("ACCESSTRENDLOG","",new string[] { activeUserVMs.GetType().ToString(), JsonConvert.SerializeObject(activeUserVMs) }, false); }); } From 37e355361c41c75a0ef4970eb1ffba0bd119f87c Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 25 Nov 2022 11:40:08 +0800 Subject: [PATCH 096/289] feat:add permission migrate --- .../Controllers/PermissionController.cs | 123 +++++++++++++++++- README.md | 1 + 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 01121063..887b4ee5 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -1,5 +1,6 @@ using Blog.Core.AuthHelper; using Blog.Core.AuthHelper.OverWrite; +using Blog.Core.Common; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; using Blog.Core.IServices; @@ -7,6 +8,8 @@ using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Blog.Core.Controllers { @@ -22,6 +25,7 @@ public class PermissionController : BaseApiController readonly IModuleServices _moduleServices; readonly IRoleModulePermissionServices _roleModulePermissionServices; readonly IUserRoleServices _userRoleServices; + private readonly IHttpClientFactory _httpClientFactory; readonly IHttpContextAccessor _httpContext; readonly IUser _user; private readonly PermissionRequirement _requirement; @@ -33,15 +37,20 @@ public class PermissionController : BaseApiController /// /// /// + /// /// /// /// - public PermissionController(IPermissionServices permissionServices, IModuleServices moduleServices, IRoleModulePermissionServices roleModulePermissionServices, IUserRoleServices userRoleServices, IHttpContextAccessor httpContext, IUser user, PermissionRequirement requirement) + public PermissionController(IPermissionServices permissionServices, IModuleServices moduleServices, + IRoleModulePermissionServices roleModulePermissionServices, IUserRoleServices userRoleServices, + IHttpClientFactory httpClientFactory, + IHttpContextAccessor httpContext, IUser user, PermissionRequirement requirement) { _permissionServices = permissionServices; _moduleServices = moduleServices; _roleModulePermissionServices = roleModulePermissionServices; _userRoleServices = userRoleServices; + this._httpClientFactory = httpClientFactory; _httpContext = httpContext; _user = user; _requirement = requirement; @@ -646,6 +655,118 @@ public async Task> BatchPost([FromBody] List pe return data; } + /// + /// 系统接口菜单同步接口 + /// + /// 接口module的控制器名称 + /// 菜单permission的父id + /// 是否执行迁移到数据 + /// + [HttpGet] + [AllowAnonymous] + public async Task>> MigratePermission(string controllerName = "", int pid = 0, bool isAction = false) + { + var data = new MessageModel>(); + if (controllerName.IsNullOrEmpty()) + { + data.msg = "必须填写要迁移的所属接口的控制器名称"; + return data; + } + + controllerName = controllerName.ToLower(); + + using var client = _httpClientFactory.CreateClient(); + var jsonFileDomain = AppSettings.GetValue("SystemCfg:Domain"); + + if (jsonFileDomain.IsNullOrEmpty()) + { + data.msg = "Swagger.json在线文件域名不能为空"; + return data; + } + + var url = $"{jsonFileDomain}/swagger/V2/swagger.json"; + var response = await client.GetAsync(url); + var body = await response.Content.ReadAsStringAsync(); + + var resultJObj = (JObject)JsonConvert.DeserializeObject(body); + var paths = resultJObj["paths"].ObjToString(); + var pathsJObj = (JObject)JsonConvert.DeserializeObject(paths); + + List permissions = new List(); + foreach (JProperty jProperty in pathsJObj.Properties()) + { + var apiPath = jProperty.Name.ToLower(); + string httpmethod = ""; + if (jProperty.Value.ToString().ToLower().Contains("get")) + { + httpmethod = "get"; + } + else if (jProperty.Value.ToString().ToLower().Contains("post")) + { + httpmethod = "post"; + } + else if (jProperty.Value.ToString().ToLower().Contains("put")) + { + httpmethod = "put"; + } + else if (jProperty.Value.ToString().ToLower().Contains("delete")) + { + httpmethod = "delete"; + } + + var summary = jProperty.Value.SelectToken($"{httpmethod}.summary").ObjToString(); + + permissions.Add(new Permission() + { + Code = " ", + Name = summary, + IsButton = true, + IsHide = false, + Enabled = true, + CreateTime = DateTime.Now, + IsDeleted = false, + Pid = pid, + Module = new Modules() + { + LinkUrl = apiPath ?? "", + Name = summary, + Enabled = true, + CreateTime = DateTime.Now, + ModifyTime = DateTime.Now, + IsDeleted = false, + } + }); + } + + var modulesList = (await _moduleServices.Query(d => d.IsDeleted == false && d.LinkUrl != null)).Select(d => d.LinkUrl.ToLower()).ToList(); + permissions = permissions.Where(d => !modulesList.Contains(d.Module.LinkUrl.ToLower()) && d.Module.LinkUrl.Contains($"/{controllerName}/")).ToList(); + + + if (isAction) + { + foreach (var item in permissions) + { + List modules = await _moduleServices.Query(d => d.LinkUrl != null && d.LinkUrl.ToLower() == item.Module.LinkUrl); + if (!modules.Any()) + { + int mid = await _moduleServices.Add(item.Module); + if (mid > 0) + { + item.Mid = mid; + int permissionid = await _permissionServices.Add(item); + } + + } + } + } + + data.response = permissions; + data.status = 200; + data.success = isAction; + + return data; + } + } public class AssignView diff --git a/README.md b/README.md index ef68b193..7f37ce66 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等,并自动持久化到数据库表🎶; - [x] 支持项目事务处理(若要分布式,用cap即可)✨; - [x] 设计4种 AOP 切面编程,功能涵盖:日志、缓存、审计、事务 ✨; +- [x] 设计并支持按钮级别的RBAC权限控制,同时支持一键同步接口和菜单 🎶; - [x] 支持 T4 代码模板,自动生成每层代码; - [x] 或使用 DbFirst 一键创建自己项目的四层文件(支持多库); - [x] 封装`Blog.Core.Webapi.Template`项目模板,一键重建自己的项目 ✨; From 6ee7eb69ebdcc1b1167e06bade0cfdcfbd6c3396 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 27 Nov 2022 14:36:14 +0800 Subject: [PATCH 097/289] feat:make some change --- Blog.Core.Api/Blog.Core.xml | 16 +++++++++++--- .../Controllers/PermissionController.cs | 17 +++++--------- Blog.Core.Api/appsettings.json | 3 ++- .../wwwroot/BlogCore.Data.json/Modules.tsv | 22 +++++++++++++++++++ .../wwwroot/BlogCore.Data.json/Permission.tsv | 21 ++++++++++++++++++ .../RoleModulePermission.tsv | 11 ++++++++++ .../ServiceExtensions/SqlsugarSetup.cs | 8 +++---- 7 files changed, 78 insertions(+), 20 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index b9c09b8f..b48d95ee 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -361,7 +361,7 @@ 菜单管理 - + 构造函数 @@ -369,6 +369,7 @@ + @@ -420,14 +421,14 @@ - 获取路由树【PRO】 + 获取路由树 - 通过角色获取菜单【无权限】 + 通过角色获取菜单 @@ -453,6 +454,15 @@ + + + 菜单同步 + + 接口module的控制器名称 + 菜单permission的父id + 是否执行迁移到数据 + + 角色管理 diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 887b4ee5..edb9dd8c 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -206,13 +206,6 @@ public async Task>> GetTreeTable(int f = 0, string return Success(permissions, "获取成功"); } - // GET: api/User/5 - [HttpGet("{id}")] - public string Get(string id) - { - return "value"; - } - /// /// 添加一个菜单 /// @@ -447,7 +440,7 @@ orderby child.Id } /// - /// 获取路由树【PRO】 + /// 获取路由树 /// /// /// @@ -523,7 +516,7 @@ orderby item.Id } /// - /// 通过角色获取菜单【无权限】 + /// 通过角色获取菜单 /// /// /// @@ -656,14 +649,13 @@ public async Task> BatchPost([FromBody] List pe } /// - /// 系统接口菜单同步接口 + /// 菜单同步 /// /// 接口module的控制器名称 /// 菜单permission的父id /// 是否执行迁移到数据 /// [HttpGet] - [AllowAnonymous] public async Task>> MigratePermission(string controllerName = "", int pid = 0, bool isAction = false) { var data = new MessageModel>(); @@ -676,7 +668,7 @@ public async Task>> MigratePermission(string contr controllerName = controllerName.ToLower(); using var client = _httpClientFactory.CreateClient(); - var jsonFileDomain = AppSettings.GetValue("SystemCfg:Domain"); + var jsonFileDomain = AppSettings.GetValue("Startup:Domain"); if (jsonFileDomain.IsNullOrEmpty()) { @@ -726,6 +718,7 @@ public async Task>> MigratePermission(string contr CreateTime = DateTime.Now, IsDeleted = false, Pid = pid, + MName = apiPath ?? "", Module = new Modules() { LinkUrl = apiPath ?? "", diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 381c0671..2adeb137 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -64,7 +64,7 @@ "Enabled": true }, "SqlAOP": { - "Enabled": false, + "Enabled": true, "LogToFile": { "Enabled": false }, @@ -172,6 +172,7 @@ "Database": "BlogCoreDb" }, "Startup": { + "Domain": "http://localhost:9291", "Cors": { "PolicyName": "CorsIpAccess", //策略名称 "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv index a906c91b..2bb130a1 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv @@ -1513,5 +1513,27 @@ "ModifyBy": null, "ModifyTime": "\/Date(1546272000000+0800)\/", "Id": 71 + }, + { + "IsDeleted": 0, + "ParentId": null, + "Name": "菜单同步", + "LinkUrl": "\/api\/permission\/migratepermission", + "Area": null, + "Controller": null, + "Action": null, + "Icon": null, + "Code": null, + "OrderSort": 0, + "Description": null, + "IsMenu": 0, + "Enabled": 1, + "CreateId": 23, + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "Id": 72 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index 67eb6975..52493a12 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -2550,5 +2550,26 @@ "IsDeleted": 0, "Id": 121, "IsHide": 1 + }, + { + "Code": " ", + "Name": "菜单同步", + "IsButton": 1, + "Pid": 7, + "Mid": 72, + "OrderSort": 0, + "Icon": null, + "Description": null, + "Enabled": 1, + "CreateId": 23, + "Func": "handleSync", + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "IsDeleted": 0, + "Id": 122, + "IsHide": 0 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv index ef19ade1..b7923ab4 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv @@ -1692,5 +1692,16 @@ "ModuleId": 66, "PermissionId": 116, "Id": 132 + }, + { + "IsDeleted": false, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-04-11 16:08:49", + "ModifyTime": "2022-04-11 00:00:00", + "RoleId": 4, + "ModuleId": 72, + "PermissionId": 122, + "Id": 133 } ] diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 8148313a..b3eb4bdc 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -61,19 +61,19 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "Enabled" }).ObjToBool()) { - if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "OutToLogFile", "Enabled" }).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToFile", "Enabled" }).ObjToBool()) { Parallel.For(0, 1, e => { MiniProfiler.Current.CustomTiming("SQL:", GetParas(p) + "【SQL语句】:" + sql); //LogLock.OutSql2Log("SqlLog", new string[] { GetParas(p), "【SQL语句】:" + sql }); - LogLock.OutLogAOP("SqlLog","", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); + LogLock.OutLogAOP("SqlLog", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); }); } - if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "OutToConsole", "Enabled" }).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToConsole", "Enabled" }).ObjToBool()) { - ConsoleHelper.WriteColorLine(string.Join("\r\n", new string[] { "--------", "【SQL语句】:" + GetWholeSql(p, sql) }), ConsoleColor.DarkCyan); + ConsoleHelper.WriteColorLine(string.Join("\r\n", new string[] { "--------", $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} :" + GetWholeSql(p, sql) }), ConsoleColor.DarkCyan); } } }, From f1fb486adcb232f1b31bc0543de8341139294bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E6=8B=BE=E7=8E=96?= Date: Sun, 27 Nov 2022 18:48:40 +0800 Subject: [PATCH 098/289] =?UTF-8?q?=E5=A2=9E=E5=8A=A0sqlserver=20=EF=BC=8C?= =?UTF-8?q?mysql=20=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=99=A8=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Log4net.config | 191 +++++++++++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 6 deletions(-) diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config index 3e718044..3ce11b6a 100644 --- a/Blog.Core.Api/Log4net.config +++ b/Blog.Core.Api/Log4net.config @@ -1,8 +1,8 @@  - + - + @@ -80,16 +80,195 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + \ No newline at end of file From 3ba534bf119d589d74129cc2e8bfedfe575bc5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E7=95=85=E7=95=85?= Date: Mon, 12 Dec 2022 12:25:08 +0800 Subject: [PATCH 099/289] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BB=93?= =?UTF-8?q?=E5=82=A8=E5=BC=80=E5=90=AF=E5=A4=9A=E5=BA=93=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8B=BC=E5=86=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Repository/BASE/BaseRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 9dc1e303..82900689 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -23,10 +23,10 @@ private ISqlSugarClient _db { ISqlSugarClient db = _dbBase; /* 如果要开启多库支持, - * 1、在appsettings.json 中开启MultiDBEnabled节点为true,必填 + * 1、在appsettings.json 中开启MutiDBEnabled节点为true,必填 * 2、设置一个主连接的数据库ID,节点MainDB,对应的连接字符串的Enabled也必须true,必填 */ - if (AppSettings.app(new[] {"MultiDBEnabled"}).ObjToBool()) + if (AppSettings.app(new[] {"MutiDBEnabled"}).ObjToBool()) { //修改使用 model备注字段作为切换数据库条件,使用sqlsugar TenantAttribute存放数据库ConnId //参考 https://www.donet5.com/Home/Doc?typeId=2246 From 4a04ac69468ef181b775b2d882b045edf0d3294b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 25 Dec 2022 13:33:36 +0800 Subject: [PATCH 100/289] Update PermissionHandler.cs --- .../Policys/PermissionHandler.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs index 0fca02b7..4ed55525 100644 --- a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs +++ b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs @@ -191,16 +191,19 @@ orderby item.Id } //校验签发时间 - var value = httpContext.User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; - if (value != null) + if (!Permissions.IsUseIds4) { - var user = await _userServices.QueryById(_user.ID, true); - if (user.CriticalModifyTime > value.ObjToDate()) + var value = httpContext.User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; + if (value != null) { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权").MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } + var user = await _userServices.QueryById(_user.ID, true); + if (user.CriticalModifyTime > value.ObjToDate()) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + } } context.Succeed(requirement); From ff03c4a462459b4523cae744b5c3a58004486355 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 17 Jan 2023 17:00:31 +0800 Subject: [PATCH 101/289] feat:update to net 7.0 --- Blog.Core.Api/Blog.Core.Api.csproj | 14 ++++----- Blog.Core.Api/Program.cs | 12 ++------ Blog.Core.Common/Blog.Core.Common.csproj | 20 ++++++------- Blog.Core.Common/Helper/SM/SM4.cs | 1 - Blog.Core.EventBus/Blog.Core.EventBus.csproj | 10 +++---- .../Blog.Core.Extensions.csproj | 30 +++++++++---------- .../Middlewares/RequRespLogMiddleware.cs | 1 - .../ServiceExtensions/HttpRuntimeCache.cs | 3 +- .../Blog.Core.FrameWork.csproj | 2 +- Blog.Core.Gateway/Blog.Core.Gateway.csproj | 10 +++---- .../Blog.Core.IServices.csproj | 2 +- Blog.Core.Model/Blog.Core.Model.csproj | 8 ++--- Blog.Core.Publish.Docker.Jenkins.sh | 2 +- Blog.Core.Publish.bat | 4 +-- .../Blog.Core.Repository.csproj | 8 ++--- .../Blog.Core.Serilog.Es.csproj | 14 ++++----- Blog.Core.Services/Blog.Core.Services.csproj | 2 +- Blog.Core.Tasks/Blog.Core.Tasks.csproj | 4 +-- Blog.Core.Tests/Blog.Core.Tests.csproj | 6 ++-- .../Ocelot.Provider.Nacos.csproj | 12 ++++---- 20 files changed, 78 insertions(+), 87 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 38575a55..9016c427 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -4,7 +4,7 @@ Exe - net6.0 + net7.0 enable Linux @@ -49,13 +49,13 @@ - - + + - - - - + + + + diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index b3bf2387..6f4cf041 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -13,7 +13,6 @@ using Blog.Core.Hubs; using Blog.Core.IServices; using Blog.Core.Tasks; -using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -170,15 +169,8 @@ app.UseMiniProfilerMiddleware(); //app.UseExceptionHandlerMidd(); -app.UseEndpoints(endpoints => -{ - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - - endpoints.MapHub("/api2/chatHub"); -}); - +app.MapControllers(); +app.MapHub("/api2/chatHub"); var scope = app.Services.GetRequiredService().CreateScope(); var myContext = scope.ServiceProvider.GetRequiredService(); diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index 43f65ab9..b3d20137 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 @@ -16,21 +16,21 @@ - - + + - - + + - + - - + + - - + + diff --git a/Blog.Core.Common/Helper/SM/SM4.cs b/Blog.Core.Common/Helper/SM/SM4.cs index 4b1f1996..e0984558 100644 --- a/Blog.Core.Common/Helper/SM/SM4.cs +++ b/Blog.Core.Common/Helper/SM/SM4.cs @@ -207,7 +207,6 @@ public SByte[] sm4_crypt_ecb(SM4_Context ctx, SByte[] input) int length = input.Length; SByte[] bins = new SByte[length]; SByte[] bous = new SByte[length]; - SByte[] output = null; Array.Copy(input, 0, bins, 0, length); diff --git a/Blog.Core.EventBus/Blog.Core.EventBus.csproj b/Blog.Core.EventBus/Blog.Core.EventBus.csproj index 070c0a48..bb75b1b0 100644 --- a/Blog.Core.EventBus/Blog.Core.EventBus.csproj +++ b/Blog.Core.EventBus/Blog.Core.EventBus.csproj @@ -1,20 +1,20 @@ - net6.0 + net7.0 - + - + - + - + diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index aca15b55..3323fd2b 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -1,35 +1,35 @@  - net6.0 + net7.0 - + - - - - - + + + + + - - - - + + + + - - - + + + - + diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index 6ff8d044..4c61ed3b 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Ubiety.Dns.Core.Common; namespace Blog.Core.Extensions.Middlewares { diff --git a/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs b/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs index b9b4d7b3..ac7713f6 100644 --- a/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs +++ b/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs @@ -38,7 +38,8 @@ public V Get(string key) public IEnumerable GetAllKey() { const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; - var entries = _memoryCache.GetType().GetField("_entries", flags).GetValue(_memoryCache); + var coherentState = _memoryCache.GetType().GetField("_coherentState", flags).GetValue(_memoryCache); + var entries = coherentState.GetType().GetField("_entries", flags).GetValue(coherentState); var cacheItems = entries as IDictionary; var keys = new List(); if (cacheItems == null) return keys; diff --git a/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj b/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj index b167521c..4f60a46d 100644 --- a/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj +++ b/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.csproj b/Blog.Core.Gateway/Blog.Core.Gateway.csproj index 47398520..819eaa36 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.csproj +++ b/Blog.Core.Gateway/Blog.Core.Gateway.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 @@ -15,13 +15,13 @@ - - - + + + - + diff --git a/Blog.Core.IServices/Blog.Core.IServices.csproj b/Blog.Core.IServices/Blog.Core.IServices.csproj index 820e1511..8ccfcc2f 100644 --- a/Blog.Core.IServices/Blog.Core.IServices.csproj +++ b/Blog.Core.IServices/Blog.Core.IServices.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index de7e6467..120f3222 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 @@ -16,9 +16,9 @@ - - - + + + diff --git a/Blog.Core.Publish.Docker.Jenkins.sh b/Blog.Core.Publish.Docker.Jenkins.sh index 3c561c67..d82c2d2f 100644 --- a/Blog.Core.Publish.Docker.Jenkins.sh +++ b/Blog.Core.Publish.Docker.Jenkins.sh @@ -4,7 +4,7 @@ cd Blog.Core.Api dotnet publish echo "Successfully!!!! ^ please see the file ." -cd bin/Debug/net6.0/publish/ +cd bin/Debug/net7.0/publish/ #rm -f appsettings.json #\cp -rf /var/jenkins_home/workspace/SecurityConfig/Blog.Core/appsettings.json appsettings.json diff --git a/Blog.Core.Publish.bat b/Blog.Core.Publish.bat index f022508d..cebbbf58 100644 --- a/Blog.Core.Publish.bat +++ b/Blog.Core.Publish.bat @@ -8,11 +8,11 @@ dotnet build cd Blog.Core.Api -dotnet publish -o ..\Blog.Core.Api\bin\Debug\net6.0\ +dotnet publish -o ..\Blog.Core.Api\bin\Debug\net7.0\ md ..\.PublishFiles -xcopy ..\Blog.Core.Api\bin\Debug\net6.0\*.* ..\.PublishFiles\ /s /e +xcopy ..\Blog.Core.Api\bin\Debug\net7.0\*.* ..\.PublishFiles\ /s /e echo "Successfully!!!! ^ please see the file .PublishFiles" diff --git a/Blog.Core.Repository/Blog.Core.Repository.csproj b/Blog.Core.Repository/Blog.Core.Repository.csproj index 5c9764b0..3547007f 100644 --- a/Blog.Core.Repository/Blog.Core.Repository.csproj +++ b/Blog.Core.Repository/Blog.Core.Repository.csproj @@ -1,14 +1,14 @@  - net6.0 + net7.0 - - - + + + diff --git a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj index 5cf82020..d98cfafb 100644 --- a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj +++ b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj @@ -1,17 +1,17 @@ - net6.0 + net7.0 - - - - - - + + + + + + diff --git a/Blog.Core.Services/Blog.Core.Services.csproj b/Blog.Core.Services/Blog.Core.Services.csproj index ae5e969a..fbfa7bbc 100644 --- a/Blog.Core.Services/Blog.Core.Services.csproj +++ b/Blog.Core.Services/Blog.Core.Services.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 diff --git a/Blog.Core.Tasks/Blog.Core.Tasks.csproj b/Blog.Core.Tasks/Blog.Core.Tasks.csproj index 5e173559..968fb795 100644 --- a/Blog.Core.Tasks/Blog.Core.Tasks.csproj +++ b/Blog.Core.Tasks/Blog.Core.Tasks.csproj @@ -1,11 +1,11 @@  - net6.0 + net7.0 - + diff --git a/Blog.Core.Tests/Blog.Core.Tests.csproj b/Blog.Core.Tests/Blog.Core.Tests.csproj index 2ceef163..fd488226 100644 --- a/Blog.Core.Tests/Blog.Core.Tests.csproj +++ b/Blog.Core.Tests/Blog.Core.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 false false @@ -19,8 +19,8 @@ - - + + all diff --git a/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj b/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj index c713416a..23fd3e3d 100644 --- a/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj +++ b/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 softlgl softlgl https://github.com/softlgl/Ocelot.Provider.Nacos @@ -11,10 +11,10 @@ - - - - - + + + + + From 8d1c0bde1480da0ffe1b1565990bb18c679b86ad Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 17 Jan 2023 19:04:36 +0800 Subject: [PATCH 102/289] Update Dockerfile --- Dockerfile | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 95bdaf92..69a3d132 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,28 +5,32 @@ #如果你想先手动dotnet build成可执行的二进制文件,然后再构建镜像,请看.Api层下的dockerfile。 -FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 -FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src COPY ["Blog.Core.Api/Blog.Core.Api.csproj", "Blog.Core.Api/"] COPY ["Blog.Core.Extensions/Blog.Core.Extensions.csproj", "Blog.Core.Extensions/"] -COPY ["Blog.Core.Tasks/Blog.Core.Tasks.csproj", "Blog.Core.Tasks/"] -COPY ["Blog.Core.IServices/Blog.Core.IServices.csproj", "Blog.Core.IServices/"] -COPY ["Blog.Core.Model/Blog.Core.Model.csproj", "Blog.Core.Model/"] +COPY ["Blog.Core.EventBus/Blog.Core.EventBus.csproj", "Blog.Core.EventBus/"] COPY ["Blog.Core.Common/Blog.Core.Common.csproj", "Blog.Core.Common/"] +COPY ["Blog.Core.Model/Blog.Core.Model.csproj", "Blog.Core.Model/"] +COPY ["Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj", "Blog.Core.Serilog.Es/"] +COPY ["Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj", "Ocelot.Provider.Nacos/"] COPY ["Blog.Core.Services/Blog.Core.Services.csproj", "Blog.Core.Services/"] +COPY ["Blog.Core.IServices/Blog.Core.IServices.csproj", "Blog.Core.IServices/"] COPY ["Blog.Core.Repository/Blog.Core.Repository.csproj", "Blog.Core.Repository/"] -COPY ["Blog.Core.EventBus/Blog.Core.EventBus.csproj", "Blog.Core.EventBus/"] +COPY ["Blog.Core.Tasks/Blog.Core.Tasks.csproj", "Blog.Core.Tasks/"] RUN dotnet restore "Blog.Core.Api/Blog.Core.Api.csproj" COPY . . WORKDIR "/src/Blog.Core.Api" RUN dotnet build "Blog.Core.Api.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "Blog.Core.Api.csproj" -c Release -o /app/publish +RUN dotnet publish "Blog.Core.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app From 2dd3ed1e02bbe34c07e714780f4045a4dc37d8bb Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 17 Jan 2023 19:06:07 +0800 Subject: [PATCH 103/289] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index faa57fc4..2dd1ed9c 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -12,7 +12,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Build with dotnet run: dotnet build --configuration Release - name: Build image From 509208737997c6e3d6de193132ca386b635cb042 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 29 Jan 2023 09:39:18 +0800 Subject: [PATCH 104/289] feat: change sqlsugar column to postpresql --- Blog.Core.Model/Models/AccessTrendLog.cs | 2 +- Blog.Core.Model/Models/GblLogAudit.cs | 18 +++++++++--------- Blog.Core.Model/Models/sysUserInfo.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Blog.Core.Model/Models/AccessTrendLog.cs b/Blog.Core.Model/Models/AccessTrendLog.cs index 0bad6836..4a87b13e 100644 --- a/Blog.Core.Model/Models/AccessTrendLog.cs +++ b/Blog.Core.Model/Models/AccessTrendLog.cs @@ -11,7 +11,7 @@ public class AccessTrendLog : RootEntityTkey /// /// 用户 /// - [SugarColumn(Length = 128, IsNullable = true, ColumnDataType = "nvarchar")] + [SugarColumn(Length = 128, IsNullable = true)] public string User { get; set; } /// diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs index 58e04dd6..4b1bd9cd 100644 --- a/Blog.Core.Model/Models/GblLogAudit.cs +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -18,52 +18,52 @@ public class GblLogAudit /// ///HttpContext.TraceIdentifier 事件链路ID(获取或设置一个唯一标识符,用于在跟踪日志中表示此请求。) /// - [SugarColumn(ColumnDescription = "事件链路ID", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + [SugarColumn(ColumnDescription = "事件链路ID", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 255)] public string TraceId { get; set; } /// ///时间 /// - [SugarColumn(ColumnDescription = "时间", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "datetime")] + [SugarColumn(ColumnDescription = "时间", IsNullable = false, IsPrimaryKey = false, IsIdentity = false)] public DateTime Date { get; set; } = DateTime.Now; /// ///线程 /// - [SugarColumn(ColumnDescription = "线程", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + [SugarColumn(ColumnDescription = "线程", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 255)] public string Thread { get; set; } /// ///等级 /// - [SugarColumn(ColumnDescription = "等级", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + [SugarColumn(ColumnDescription = "等级", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 255)] public string Level { get; set; } /// ///记录器 /// - [SugarColumn(ColumnDescription = "记录器", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + [SugarColumn(ColumnDescription = "记录器", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 255)] public string Logger { get; set; } /// ///日志类型 /// - [SugarColumn(ColumnDescription = "日志类型", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + [SugarColumn(ColumnDescription = "日志类型", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 255)] public string LogType { get; set; } /// ///数据类型 /// - [SugarColumn(ColumnDescription = "数据类型", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "varchar", Length = 255)] + [SugarColumn(ColumnDescription = "数据类型", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 255)] public string DataType { get; set; } /// ///错误信息 /// - [SugarColumn(ColumnDescription = "错误信息", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "text")] + [SugarColumn(ColumnDescription = "错误信息", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 5000)] public string Message { get; set; } /// ///异常 /// - [SugarColumn(ColumnDescription = "异常", IsNullable = true, IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "text")] + [SugarColumn(ColumnDescription = "异常", IsNullable = true, IsPrimaryKey = false, IsIdentity = false, Length = 5000)] public string Exception { get; set; } } diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index 8aafb164..c60e4bce 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -33,7 +33,7 @@ public SysUserInfo(string loginName, string loginPWD) /// [SugarColumn(Length = 200, IsNullable = true, ColumnDescription = "登录账号")] //:eg model 根据sqlsugar的完整定义可以如下定义,ColumnDescription可定义表字段备注 - //[SugarColumn(IsNullable = false, ColumnDescription = "登录账号", IsPrimaryKey = false, IsIdentity = false, ColumnDataType = "nvarchar", Length = 50)] + //[SugarColumn(IsNullable = false, ColumnDescription = "登录账号", IsPrimaryKey = false, IsIdentity = false, Length = 50)] //ColumnDescription 表字段备注, 已在MSSQL测试,配合 [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') //可以完整生成 表备注和各个字段的中文备注 //2022/10/11 From a30f18499682fd8e4e2b433a122a8b81cd94451d Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 29 Jan 2023 16:19:30 +0800 Subject: [PATCH 105/289] feat: add authing sso --- Blog.Core.Api/Program.cs | 6 ++- Blog.Core.Api/appsettings.json | 6 +++ Blog.Core.Common/GlobalVar/GlobalVars.cs | 6 +++ .../Blog.Core.Extensions.csproj | 3 +- .../Authentication_AuthingSetup.cs | 53 +++++++++++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 Blog.Core.Extensions/ServiceExtensions/Authentication_AuthingSetup.cs diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index b3bf2387..7a9a25c0 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -56,6 +56,7 @@ builder.Services.AddUiFilesZipSetup(builder.Environment); Permissions.IsUseIds4 = AppSettings.app(new string[] { "Startup", "IdentityServer4", "Enabled" }).ObjToBool(); +Permissions.IsUseAuthing = AppSettings.app(new string[] { "Startup", "Authing", "Enabled" }).ObjToBool(); RoutePrefix.Name = AppSettings.app(new string[] { "AppSettings", "SvcName" }).ObjToString(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); @@ -79,9 +80,10 @@ builder.Services.AddNacosSetup(builder.Configuration); builder.Services.AddAuthorizationSetup(); -if (Permissions.IsUseIds4) +if (Permissions.IsUseIds4 || Permissions.IsUseAuthing) { - builder.Services.AddAuthentication_Ids4Setup(); + if (Permissions.IsUseIds4) builder.Services.AddAuthentication_Ids4Setup(); + else if (Permissions.IsUseAuthing) builder.Services.AddAuthentication_AuthingSetup(); } else { diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 2adeb137..d639a62e 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -189,6 +189,12 @@ "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 "ApiName": "blog.core.api" // 资源服务器 }, + "Authing": { + "Enabled": true, + "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", + "Audience": "63d51c4205c2849803be5178", + "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" + }, "RedisMq": { "Enabled": false //redis 消息队列 }, diff --git a/Blog.Core.Common/GlobalVar/GlobalVars.cs b/Blog.Core.Common/GlobalVar/GlobalVars.cs index cb281e26..84dd9e19 100644 --- a/Blog.Core.Common/GlobalVar/GlobalVars.cs +++ b/Blog.Core.Common/GlobalVar/GlobalVars.cs @@ -20,6 +20,12 @@ public static class Permissions /// true:表示启动IDS4 /// false:表示使用JWT public static bool IsUseIds4 = false; + + /// + /// 当前项目是否启用Authing权限方案 + /// true:表示启动 + /// false:表示使用JWT + public static bool IsUseAuthing = false; } /// diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index aca15b55..50969b05 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -11,7 +11,7 @@ - + @@ -20,6 +20,7 @@ + diff --git a/Blog.Core.Extensions/ServiceExtensions/Authentication_AuthingSetup.cs b/Blog.Core.Extensions/ServiceExtensions/Authentication_AuthingSetup.cs new file mode 100644 index 00000000..4c2c68c8 --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/Authentication_AuthingSetup.cs @@ -0,0 +1,53 @@ +using Blog.Core.AuthHelper; +using Blog.Core.Common; +using Blog.Core.Common.HttpContextUser; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.JwtExtensions; +using System; + +namespace Blog.Core.Extensions +{ + /// + /// Authing权限 认证服务 + /// + public static class Authentication_AuthingSetup + { + public static void AddAuthentication_AuthingSetup(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + var tokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = AppSettings.app(new string[] { "Startup", "Authing", "Issuer" }), + ValidAudience = AppSettings.app(new string[] { "Startup", "Authing", "Audience" }), + ValidAlgorithms = new string[] { "RS256" }, + //ValidateLifetime = true, + //ClockSkew = TimeSpan.FromSeconds(30), + //RequireExpirationTime = true, + }; + + services.AddAuthentication(o => + { + //认证middleware配置 + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = nameof(ApiResponseHandler); + o.DefaultForbidScheme = nameof(ApiResponseHandler); + }) + .AddJwtBearer(o => + { + //主要是jwt token参数设置 + o.TokenValidationParameters = tokenValidationParameters; + o.RequireHttpsMetadata = false; + o.SaveToken = false; + o.IncludeErrorDetails = true; + o.SetJwksOptions(new JwkOptions(AppSettings.app(new string[] { "Startup", "Authing", "JwksUri" }), AppSettings.app(new string[] { "Startup", "Authing", "Issuer" }), new TimeSpan(TimeSpan.TicksPerDay))); + }) + .AddScheme(nameof(ApiResponseHandler), o => { }); + + } + } +} From 41fa6122597bb3eb0d2b473adb1bb3f47d1aa4d6 Mon Sep 17 00:00:00 2001 From: Nine Date: Tue, 31 Jan 2023 16:40:28 +0800 Subject: [PATCH 106/289] =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97,=E5=A2=9E=E5=8A=A0PostgreSql=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Log4net.config | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config index 3ce11b6a..61bd3739 100644 --- a/Blog.Core.Api/Log4net.config +++ b/Blog.Core.Api/Log4net.config @@ -251,6 +251,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -267,6 +354,9 @@ + + + From d0e4ff7757aed635dd240426cc3160e5a68a65f5 Mon Sep 17 00:00:00 2001 From: Geralt_Zhang <40553940+HuiJiOnGit@users.noreply.github.com> Date: Thu, 2 Feb 2023 20:42:46 +0800 Subject: [PATCH 107/289] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=B1=9E=E6=80=A7=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将`TargetFramework`属性迁移到`common.targets`作为方便管理 --- Blog.Core.Api/Blog.Core.Api.csproj | 4 +--- Blog.Core.Common/Blog.Core.Common.csproj | 4 +--- Blog.Core.EventBus/Blog.Core.EventBus.csproj | 4 +--- Blog.Core.Extensions/Blog.Core.Extensions.csproj | 4 +--- Blog.Core.FrameWork/Blog.Core.FrameWork.csproj | 4 +--- Blog.Core.Gateway/Blog.Core.Gateway.csproj | 4 +--- Blog.Core.IServices/Blog.Core.IServices.csproj | 4 +--- Blog.Core.Model/Blog.Core.Model.csproj | 4 +--- Blog.Core.Repository/Blog.Core.Repository.csproj | 4 +--- Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj | 4 +--- Blog.Core.Services/Blog.Core.Services.csproj | 4 +--- Blog.Core.Tasks/Blog.Core.Tasks.csproj | 4 +--- Blog.Core.Tests/Blog.Core.Tests.csproj | 3 +-- Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj | 4 ++-- build/common.targets | 5 +++++ 15 files changed, 20 insertions(+), 40 deletions(-) create mode 100644 build/common.targets diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 9016c427..fcaef62a 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -1,10 +1,8 @@  + - Exe - - net7.0 enable Linux diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index b3d20137..30816b97 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -1,8 +1,6 @@  - - net7.0 - + diff --git a/Blog.Core.EventBus/Blog.Core.EventBus.csproj b/Blog.Core.EventBus/Blog.Core.EventBus.csproj index bb75b1b0..8c50f8e2 100644 --- a/Blog.Core.EventBus/Blog.Core.EventBus.csproj +++ b/Blog.Core.EventBus/Blog.Core.EventBus.csproj @@ -1,8 +1,6 @@ - - net7.0 - + diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 3323fd2b..437841b7 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -1,8 +1,6 @@  - - net7.0 - + diff --git a/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj b/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj index 4f60a46d..51022b3c 100644 --- a/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj +++ b/Blog.Core.FrameWork/Blog.Core.FrameWork.csproj @@ -1,8 +1,6 @@ - - net7.0 - + diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.csproj b/Blog.Core.Gateway/Blog.Core.Gateway.csproj index 819eaa36..ca3a7f72 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.csproj +++ b/Blog.Core.Gateway/Blog.Core.Gateway.csproj @@ -1,8 +1,6 @@  - - net7.0 - + ..\Blog.Core.Gateway\Blog.Core.Gateway.xml diff --git a/Blog.Core.IServices/Blog.Core.IServices.csproj b/Blog.Core.IServices/Blog.Core.IServices.csproj index 8ccfcc2f..20e8e658 100644 --- a/Blog.Core.IServices/Blog.Core.IServices.csproj +++ b/Blog.Core.IServices/Blog.Core.IServices.csproj @@ -1,8 +1,6 @@  - - net7.0 - + diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index 120f3222..3c2ee04a 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -1,8 +1,6 @@  - - net7.0 - + ..\Blog.Core.Api\Blog.Core.Model.xml diff --git a/Blog.Core.Repository/Blog.Core.Repository.csproj b/Blog.Core.Repository/Blog.Core.Repository.csproj index 3547007f..783014e0 100644 --- a/Blog.Core.Repository/Blog.Core.Repository.csproj +++ b/Blog.Core.Repository/Blog.Core.Repository.csproj @@ -1,8 +1,6 @@  - - net7.0 - + diff --git a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj index d98cfafb..cd71cf26 100644 --- a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj +++ b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj @@ -1,8 +1,6 @@ - - net7.0 - + diff --git a/Blog.Core.Services/Blog.Core.Services.csproj b/Blog.Core.Services/Blog.Core.Services.csproj index fbfa7bbc..04437f0c 100644 --- a/Blog.Core.Services/Blog.Core.Services.csproj +++ b/Blog.Core.Services/Blog.Core.Services.csproj @@ -1,8 +1,6 @@  - - net7.0 - + ..\Blog.Core.Api\bin\Debug\ diff --git a/Blog.Core.Tasks/Blog.Core.Tasks.csproj b/Blog.Core.Tasks/Blog.Core.Tasks.csproj index 968fb795..408f379c 100644 --- a/Blog.Core.Tasks/Blog.Core.Tasks.csproj +++ b/Blog.Core.Tasks/Blog.Core.Tasks.csproj @@ -1,8 +1,6 @@  - - net7.0 - + diff --git a/Blog.Core.Tests/Blog.Core.Tests.csproj b/Blog.Core.Tests/Blog.Core.Tests.csproj index fd488226..98eff6fc 100644 --- a/Blog.Core.Tests/Blog.Core.Tests.csproj +++ b/Blog.Core.Tests/Blog.Core.Tests.csproj @@ -1,8 +1,7 @@  + - net7.0 - false false diff --git a/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj b/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj index 23fd3e3d..6bdeb841 100644 --- a/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj +++ b/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj @@ -1,7 +1,7 @@  - + + - net7.0 softlgl softlgl https://github.com/softlgl/Ocelot.Provider.Nacos diff --git a/build/common.targets b/build/common.targets new file mode 100644 index 00000000..bfd5899f --- /dev/null +++ b/build/common.targets @@ -0,0 +1,5 @@ + + + net7.0 + + \ No newline at end of file From 21307c64e6ec9f0b2ab78878db3005bfb1c59f3c Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 3 Feb 2023 14:56:10 +0800 Subject: [PATCH 108/289] Update PermissionController.cs --- .../Controllers/PermissionController.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index edb9dd8c..d3000766 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -649,14 +649,15 @@ public async Task> BatchPost([FromBody] List pe } /// - /// 菜单同步 + /// 系统接口菜单同步接口 /// + /// /// 接口module的控制器名称 /// 菜单permission的父id /// 是否执行迁移到数据 /// [HttpGet] - public async Task>> MigratePermission(string controllerName = "", int pid = 0, bool isAction = false) + public async Task>> MigratePermission(string action = "", string controllerName = "", int pid = 0, bool isAction = false) { var data = new MessageModel>(); if (controllerName.IsNullOrEmpty()) @@ -677,6 +678,7 @@ public async Task>> MigratePermission(string contr } var url = $"{jsonFileDomain}/swagger/V2/swagger.json"; + var response = await client.GetAsync(url); var body = await response.Content.ReadAsStringAsync(); @@ -688,6 +690,13 @@ public async Task>> MigratePermission(string contr foreach (JProperty jProperty in pathsJObj.Properties()) { var apiPath = jProperty.Name.ToLower(); + if (action.IsNotEmptyOrNull()) + { + if (!apiPath.Contains(action.ToLower())) + { + continue; + } + } string httpmethod = ""; if (jProperty.Value.ToString().ToLower().Contains("get")) { @@ -708,6 +717,12 @@ public async Task>> MigratePermission(string contr var summary = jProperty.Value.SelectToken($"{httpmethod}.summary").ObjToString(); + var subIx = summary.IndexOf("(Auth"); + if (subIx > 0) + { + summary = summary.Substring(0, subIx - 1); + } + permissions.Add(new Permission() { Code = " ", From 9e888246cc25a16da57d04f847c1d1621f122f7b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 3 Feb 2023 17:08:02 +0800 Subject: [PATCH 109/289] Update appsettings.json --- Blog.Core.Api/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index d639a62e..ea678174 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -190,7 +190,7 @@ "ApiName": "blog.core.api" // 资源服务器 }, "Authing": { - "Enabled": true, + "Enabled": false, "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", "Audience": "63d51c4205c2849803be5178", "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" From bc03131ae0dcc810588662a0ac31ac5a7b04515a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 5 Feb 2023 09:10:24 +0800 Subject: [PATCH 110/289] Update Blog.Core.xml --- Blog.Core.Api/Blog.Core.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index b48d95ee..3321d53a 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -454,10 +454,11 @@ - + - 菜单同步 + 系统接口菜单同步接口 + 接口module的控制器名称 菜单permission的父id 是否执行迁移到数据 From 569e968c82a8a6c087d6ec59e54d7df6a166e91a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 5 Feb 2023 17:25:46 +0800 Subject: [PATCH 111/289] feat: add ExpressionExtensions.cs --- .../Extensions/ExpressionExtensions.cs | 339 ++++++++++++++++++ Blog.Core.Common/Helper/DynamicLinqFactory.cs | 327 +---------------- 2 files changed, 340 insertions(+), 326 deletions(-) create mode 100644 Blog.Core.Common/Extensions/ExpressionExtensions.cs diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions.cs b/Blog.Core.Common/Extensions/ExpressionExtensions.cs new file mode 100644 index 00000000..4e1b066d --- /dev/null +++ b/Blog.Core.Common/Extensions/ExpressionExtensions.cs @@ -0,0 +1,339 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Helper +{ + /// + /// Linq扩展 + /// + public static class ExpressionExtensions + { + #region Nacos NamingService + + private static readonly HttpClient httpclient = new HttpClient(); + + private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl) + { + try + { + var instance = serv.SelectOneHealthyInstance(ServiceName, Group).GetAwaiter().GetResult(); + var host = $"{instance.Ip}:{instance.Port}"; + if (instance.Metadata.ContainsKey("endpoint")) host = instance.Metadata["endpoint"]; + + + var baseUrl = instance.Metadata.TryGetValue("secure", out _) + ? $"https://{host}" + : $"http://{host}"; + + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return ""; + } + + return $"{baseUrl}{apiurl}"; + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters = null) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + if (Parameters != null && Parameters.Any()) + { + StringBuilder sb = new StringBuilder(); + foreach (var pitem in Parameters) + { + sb.Append($"{pitem.Key}={pitem.Value}&"); + } + + url = $"{url}?{sb.ToString().Trim('&')}"; + } + + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.GetAsync(url); + return await result.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + + var content = (Parameters != null && Parameters.Any()) ? new FormUrlEncodedContent(Parameters) : null; + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.PostAsync(url, content); + return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, string jSonData) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.PostAsync(url, new StringContent(jSonData, Encoding.UTF8, "application/json")); + return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); + + //httpClient.BaseAddress = new Uri("https://www.testapi.com"); + //httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + + var content = new MultipartFormDataContent(); + foreach (var pitem in Parameters) + { + content.Add(new ByteArrayContent(pitem.Value), "files", pitem.Key); + } + + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.PostAsync(url, content); + return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); + } + catch (Exception e) + { + //InfluxdbHelper.GetInstance().AddLog("Cof_NaocePostFile.Err", ee); + Console.WriteLine(e.Message); + } + + return ""; + } + + #endregion + + #region HttpContext + + /// + /// 返回请求上下文 + /// + /// + /// + /// + /// + /// + public static async Task Cof_SendResponse(this HttpContext context, System.Net.HttpStatusCode code, string message, string ContentType = "text/html;charset=utf-8") + { + context.Response.StatusCode = (int)code; + context.Response.ContentType = ContentType; + await context.Response.WriteAsync(message); + } + + #endregion + + #region ICaching + + /// + /// 从缓存里取数据,如果不存在则执行查询方法, + /// + /// 类型 + /// ICaching + /// 键值 + /// 查询方法 + /// 有效期 单位分钟/param> + /// + public static T Cof_GetICaching(this ICaching cache, string key, Func GetFun, int timeSpanMin) where T : class + { + var obj = cache.Get(key); + obj = GetFun(); + if (obj == null) + { + obj = GetFun(); + cache.Set(key, obj, timeSpanMin); + } + + return obj as T; + } + + /// + /// 异步从缓存里取数据,如果不存在则执行查询方法 + /// + /// 类型 + /// ICaching + /// 键值 + /// 查询方法 + /// 有效期 单位分钟/param> + /// + public static async Task Cof_AsyncGetICaching(this ICaching cache, string key, Func> GetFun, int timeSpanMin) where T : class + { + var obj = cache.Get(key); + if (obj == null) + { + obj = await GetFun(); + cache.Set(key, obj, timeSpanMin); + } + + return obj as T; + } + + #endregion + + #region 常用扩展方法 + + public static bool Cof_CheckAvailable(this IEnumerable Tlist) + { + return Tlist != null && Tlist.Count() > 0; + } + + /// + /// 调用内部方法 + /// + public static Expression Call(this Expression instance, string methodName, params Expression[] arguments) + { + if (instance.Type == typeof(string)) + return Expression.Call(instance, instance.Type.GetMethod(methodName, new Type[] { typeof(string) }), arguments); //修复string contains 出现的问题 Ambiguous match found. + else + return Expression.Call(instance, instance.Type.GetMethod(methodName), arguments); + } + + /// + /// 获取内部成员 + /// + public static Expression Property(this Expression expression, string propertyName) + { + // Todo:左边条件如果是dynamic, + // 则Expression.Property无法获取子内容 + // 报错在这里,由于expression内的对象为Object,所以无法解析到 + // var x = (expression as IQueryable).ElementType; + var exp = Expression.Property(expression, propertyName); + if (exp.Type.IsGenericType && exp.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return Expression.Convert(exp, exp.Type.GetGenericArguments()[0]); + } + + return exp; + } + + /// + /// 转Lambda + /// + public static Expression ToLambda(this Expression body, + params ParameterExpression[] parameters) + { + return Expression.Lambda(body, parameters); + } + + #endregion + + #region 常用运算符 [ > , >= , == , < , <= , != , || , && ] + + /// + /// && + /// + public static Expression AndAlso(this Expression left, Expression right) + { + return Expression.AndAlso(left, right); + } + + /// + /// || + /// + public static Expression OrElse(this Expression left, Expression right) + { + return Expression.OrElse(left, right); + } + + /// + /// Contains + /// + public static Expression Contains(this Expression left, Expression right) + { + return left.Call("Contains", right); + } + + /// + /// > + /// + public static Expression GreaterThan(this Expression left, Expression right) + { + return Expression.GreaterThan(left, right); + } + + /// + /// >= + /// + public static Expression GreaterThanOrEqual(this Expression left, Expression right) + { + return Expression.GreaterThanOrEqual(left, right); + } + + /// + /// < + /// + public static Expression LessThan(this Expression left, Expression right) + { + return Expression.LessThan(left, right); + } + + /// + /// <= + /// + public static Expression LessThanOrEqual(this Expression left, Expression right) + { + return Expression.LessThanOrEqual(left, right); + } + + /// + /// == + /// + public static Expression Equal(this Expression left, Expression right) + { + return Expression.Equal(left, right); + } + + /// + /// != + /// + public static Expression NotEqual(this Expression left, Expression right) + { + return Expression.NotEqual(left, right); + } + + #endregion + } + +} diff --git a/Blog.Core.Common/Helper/DynamicLinqFactory.cs b/Blog.Core.Common/Helper/DynamicLinqFactory.cs index a12c6bbb..43fdd7e3 100644 --- a/Blog.Core.Common/Helper/DynamicLinqFactory.cs +++ b/Blog.Core.Common/Helper/DynamicLinqFactory.cs @@ -528,332 +528,7 @@ public enum OperationSymbol #endregion - /// - /// Linq扩展 - /// - public static class ExpressionExtensions - { - #region Nacos NamingService - - private static readonly HttpClient httpclient = new HttpClient(); - - private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl) - { - try - { - var instance = serv.SelectOneHealthyInstance(ServiceName, Group).GetAwaiter().GetResult(); - var host = $"{instance.Ip}:{instance.Port}"; - if (instance.Metadata.ContainsKey("endpoint")) host = instance.Metadata["endpoint"]; - - - var baseUrl = instance.Metadata.TryGetValue("secure", out _) - ? $"https://{host}" - : $"http://{host}"; - - if (string.IsNullOrWhiteSpace(baseUrl)) - { - return ""; - } - - return $"{baseUrl}{apiurl}"; - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters = null) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - if (Parameters != null && Parameters.Any()) - { - StringBuilder sb = new StringBuilder(); - foreach (var pitem in Parameters) - { - sb.Append($"{pitem.Key}={pitem.Value}&"); - } - - url = $"{url}?{sb.ToString().Trim('&')}"; - } - - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.GetAsync(url); - return await result.Content.ReadAsStringAsync(); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - - var content = (Parameters != null && Parameters.Any()) ? new FormUrlEncodedContent(Parameters) : null; - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, content); - return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, string jSonData) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, new StringContent(jSonData, Encoding.UTF8, "application/json")); - return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); - - //httpClient.BaseAddress = new Uri("https://www.testapi.com"); - //httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - - var content = new MultipartFormDataContent(); - foreach (var pitem in Parameters) - { - content.Add(new ByteArrayContent(pitem.Value), "files", pitem.Key); - } - - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, content); - return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); - } - catch (Exception e) - { - //InfluxdbHelper.GetInstance().AddLog("Cof_NaocePostFile.Err", ee); - Console.WriteLine(e.Message); - } - - return ""; - } - - #endregion - - #region HttpContext - - /// - /// 返回请求上下文 - /// - /// - /// - /// - /// - /// - public static async Task Cof_SendResponse(this HttpContext context, System.Net.HttpStatusCode code, string message, string ContentType = "text/html;charset=utf-8") - { - context.Response.StatusCode = (int)code; - context.Response.ContentType = ContentType; - await context.Response.WriteAsync(message); - } - - #endregion - - #region ICaching - - /// - /// 从缓存里取数据,如果不存在则执行查询方法, - /// - /// 类型 - /// ICaching - /// 键值 - /// 查询方法 - /// 有效期 单位分钟/param> - /// - public static T Cof_GetICaching(this ICaching cache, string key, Func GetFun, int timeSpanMin) where T : class - { - var obj = cache.Get(key); - obj = GetFun(); - if (obj == null) - { - obj = GetFun(); - cache.Set(key, obj, timeSpanMin); - } - - return obj as T; - } - - /// - /// 异步从缓存里取数据,如果不存在则执行查询方法 - /// - /// 类型 - /// ICaching - /// 键值 - /// 查询方法 - /// 有效期 单位分钟/param> - /// - public static async Task Cof_AsyncGetICaching(this ICaching cache, string key, Func> GetFun, int timeSpanMin) where T : class - { - var obj = cache.Get(key); - if (obj == null) - { - obj = await GetFun(); - cache.Set(key, obj, timeSpanMin); - } - - return obj as T; - } - - #endregion - - #region 常用扩展方法 - - public static bool Cof_CheckAvailable(this IEnumerable Tlist) - { - return Tlist != null && Tlist.Count() > 0; - } - - /// - /// 调用内部方法 - /// - public static Expression Call(this Expression instance, string methodName, params Expression[] arguments) - { - if (instance.Type == typeof(string)) - return Expression.Call(instance, instance.Type.GetMethod(methodName, new Type[] { typeof(string) }), arguments); //修复string contains 出现的问题 Ambiguous match found. - else - return Expression.Call(instance, instance.Type.GetMethod(methodName), arguments); - } - - /// - /// 获取内部成员 - /// - public static Expression Property(this Expression expression, string propertyName) - { - // Todo:左边条件如果是dynamic, - // 则Expression.Property无法获取子内容 - // 报错在这里,由于expression内的对象为Object,所以无法解析到 - // var x = (expression as IQueryable).ElementType; - var exp = Expression.Property(expression, propertyName); - if (exp.Type.IsGenericType && exp.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - return Expression.Convert(exp, exp.Type.GetGenericArguments()[0]); - } - - return exp; - } - - /// - /// 转Lambda - /// - public static Expression ToLambda(this Expression body, - params ParameterExpression[] parameters) - { - return Expression.Lambda(body, parameters); - } - - #endregion - - #region 常用运算符 [ > , >= , == , < , <= , != , || , && ] - - /// - /// && - /// - public static Expression AndAlso(this Expression left, Expression right) - { - return Expression.AndAlso(left, right); - } - - /// - /// || - /// - public static Expression OrElse(this Expression left, Expression right) - { - return Expression.OrElse(left, right); - } - - /// - /// Contains - /// - public static Expression Contains(this Expression left, Expression right) - { - return left.Call("Contains", right); - } - - /// - /// > - /// - public static Expression GreaterThan(this Expression left, Expression right) - { - return Expression.GreaterThan(left, right); - } - - /// - /// >= - /// - public static Expression GreaterThanOrEqual(this Expression left, Expression right) - { - return Expression.GreaterThanOrEqual(left, right); - } - - /// - /// < - /// - public static Expression LessThan(this Expression left, Expression right) - { - return Expression.LessThan(left, right); - } - - /// - /// <= - /// - public static Expression LessThanOrEqual(this Expression left, Expression right) - { - return Expression.LessThanOrEqual(left, right); - } - - /// - /// == - /// - public static Expression Equal(this Expression left, Expression right) - { - return Expression.Equal(left, right); - } - - /// - /// != - /// - public static Expression NotEqual(this Expression left, Expression right) - { - return Expression.NotEqual(left, right); - } - - #endregion - } - + /// /// Queryable扩展 /// From d0b3cfe8054cb64b63a184f1cfb036fe9b3fee14 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 5 Feb 2023 18:40:35 +0800 Subject: [PATCH 112/289] feat: add ExpressionExtensions_Nacos.cs --- .../Extensions/ExpressionExtensions.cs | 134 ---------------- .../Extensions/ExpressionExtensions_Nacos.cs | 148 ++++++++++++++++++ 2 files changed, 148 insertions(+), 134 deletions(-) create mode 100644 Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions.cs b/Blog.Core.Common/Extensions/ExpressionExtensions.cs index 4e1b066d..7f9e69e5 100644 --- a/Blog.Core.Common/Extensions/ExpressionExtensions.cs +++ b/Blog.Core.Common/Extensions/ExpressionExtensions.cs @@ -3,9 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Net.Http.Headers; -using System.Net.Http; -using System.Text; using System.Threading.Tasks; namespace Blog.Core.Common.Helper @@ -15,137 +12,6 @@ namespace Blog.Core.Common.Helper /// public static class ExpressionExtensions { - #region Nacos NamingService - - private static readonly HttpClient httpclient = new HttpClient(); - - private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl) - { - try - { - var instance = serv.SelectOneHealthyInstance(ServiceName, Group).GetAwaiter().GetResult(); - var host = $"{instance.Ip}:{instance.Port}"; - if (instance.Metadata.ContainsKey("endpoint")) host = instance.Metadata["endpoint"]; - - - var baseUrl = instance.Metadata.TryGetValue("secure", out _) - ? $"https://{host}" - : $"http://{host}"; - - if (string.IsNullOrWhiteSpace(baseUrl)) - { - return ""; - } - - return $"{baseUrl}{apiurl}"; - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters = null) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - if (Parameters != null && Parameters.Any()) - { - StringBuilder sb = new StringBuilder(); - foreach (var pitem in Parameters) - { - sb.Append($"{pitem.Key}={pitem.Value}&"); - } - - url = $"{url}?{sb.ToString().Trim('&')}"; - } - - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.GetAsync(url); - return await result.Content.ReadAsStringAsync(); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - - var content = (Parameters != null && Parameters.Any()) ? new FormUrlEncodedContent(Parameters) : null; - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, content); - return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, string jSonData) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, new StringContent(jSonData, Encoding.UTF8, "application/json")); - return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); - - //httpClient.BaseAddress = new Uri("https://www.testapi.com"); - //httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return ""; - } - - public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) - { - try - { - var url = GetServiceUrl(serv, ServiceName, Group, apiurl); - if (string.IsNullOrEmpty(url)) return ""; - - var content = new MultipartFormDataContent(); - foreach (var pitem in Parameters) - { - content.Add(new ByteArrayContent(pitem.Value), "files", pitem.Key); - } - - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, content); - return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); - } - catch (Exception e) - { - //InfluxdbHelper.GetInstance().AddLog("Cof_NaocePostFile.Err", ee); - Console.WriteLine(e.Message); - } - - return ""; - } - - #endregion - #region HttpContext /// diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs b/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs new file mode 100644 index 00000000..c187ee6a --- /dev/null +++ b/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Helper +{ + /// + /// Linq扩展 + /// + public static class ExpressionExtensions_Nacos + { + #region Nacos NamingService + + private static readonly HttpClient httpclient = new HttpClient(); + + private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl) + { + try + { + var instance = serv.SelectOneHealthyInstance(ServiceName, Group).GetAwaiter().GetResult(); + var host = $"{instance.Ip}:{instance.Port}"; + if (instance.Metadata.ContainsKey("endpoint")) host = instance.Metadata["endpoint"]; + + + var baseUrl = instance.Metadata.TryGetValue("secure", out _) + ? $"https://{host}" + : $"http://{host}"; + + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return ""; + } + + return $"{baseUrl}{apiurl}"; + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters = null) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + if (Parameters != null && Parameters.Any()) + { + StringBuilder sb = new StringBuilder(); + foreach (var pitem in Parameters) + { + sb.Append($"{pitem.Key}={pitem.Value}&"); + } + + url = $"{url}?{sb.ToString().Trim('&')}"; + } + + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.GetAsync(url); + return await result.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + + var content = (Parameters != null && Parameters.Any()) ? new FormUrlEncodedContent(Parameters) : null; + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.PostAsync(url, content); + return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, string jSonData) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.PostAsync(url, new StringContent(jSonData, Encoding.UTF8, "application/json")); + return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); + + //httpClient.BaseAddress = new Uri("https://www.testapi.com"); + //httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return ""; + } + + public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) + { + try + { + var url = GetServiceUrl(serv, ServiceName, Group, apiurl); + if (string.IsNullOrEmpty(url)) return ""; + + var content = new MultipartFormDataContent(); + foreach (var pitem in Parameters) + { + content.Add(new ByteArrayContent(pitem.Value), "files", pitem.Key); + } + + httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await httpclient.PostAsync(url, content); + return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); + } + catch (Exception e) + { + //InfluxdbHelper.GetInstance().AddLog("Cof_NaocePostFile.Err", ee); + Console.WriteLine(e.Message); + } + + return ""; + } + + #endregion + } + +} From 316adaa3671dc0384591e26a148e61cf39e494fb Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 5 Feb 2023 21:50:46 +0800 Subject: [PATCH 113/289] feat: add SerilogServer_Es.cs --- .../LogHelper/Seri/SerilogServer.cs | 17 +--- .../LogHelper/Seri/SerilogServer_Es.cs | 89 +++++++++++++++++++ 2 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs diff --git a/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs b/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs index 98affb9e..cb5ec0ff 100644 --- a/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs +++ b/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs @@ -1,6 +1,4 @@ using Blog.Core.Common.Helper; -using Blog.Core.Serilog.Es; -using Blog.Core.Serilog.Es.Formatters; using Serilog; using Serilog.Events; using System; @@ -19,23 +17,12 @@ public class SerilogServer public static void WriteLog(string filename, string[] dataParas, bool IsHeader = true, string defaultFolder = "", bool isJudgeJsonFormat = false) { Log.Logger = new LoggerConfiguration() - // TCPSink 集成Serilog 使用tcp的方式向elk 输出log日志 LogstashJsonFormatter 这个是按照自定义格式化输出内容 - .WriteTo.TCPSink(new LogstashJsonFormatter()) .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Error) //.WriteTo.File(Path.Combine($"log/Serilog/{filename}/", ".log"), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}") .WriteTo.File(Path.Combine("Log", defaultFolder, $"{filename}.log"), - rollingInterval: RollingInterval.Infinite, - outputTemplate: "{Message}{NewLine}{Exception}") - - // 将日志托送到远程ES - // docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d --name ES01 elasticsearch:7.2.0 - //.Enrich.FromLogContext() - //.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://x.xxx.xx.xx:9200/")) - //{ - // AutoRegisterTemplate = true, - //}) - + rollingInterval: RollingInterval.Infinite, + outputTemplate: "{Message}{NewLine}{Exception}") .CreateLogger(); var now = DateTime.Now; diff --git a/Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs b/Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs new file mode 100644 index 00000000..048d5597 --- /dev/null +++ b/Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs @@ -0,0 +1,89 @@ +using Blog.Core.Common.Helper; +using Blog.Core.Serilog.Es; +using Blog.Core.Serilog.Es.Formatters; +using Serilog; +using Serilog.Events; +using System; +using System.IO; + +namespace Blog.Core.Common.LogHelper +{ + public class SerilogServer_Es + { + /// + /// 记录日常日志 + /// + /// + /// + /// + public static void WriteLog(string filename, string[] dataParas, bool IsHeader = true, string defaultFolder = "", bool isJudgeJsonFormat = false) + { + Log.Logger = new LoggerConfiguration() + // TCPSink 集成Serilog 使用tcp的方式向elk 输出log日志 LogstashJsonFormatter 这个是按照自定义格式化输出内容 + .WriteTo.TCPSink(new LogstashJsonFormatter()) + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + //.WriteTo.File(Path.Combine($"log/Serilog/{filename}/", ".log"), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}") + .WriteTo.File(Path.Combine("Log", defaultFolder, $"{filename}.log"), + rollingInterval: RollingInterval.Infinite, + outputTemplate: "{Message}{NewLine}{Exception}") + + // 将日志托送到远程ES + // docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d --name ES01 elasticsearch:7.2.0 + //.Enrich.FromLogContext() + //.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://x.xxx.xx.xx:9200/")) + //{ + // AutoRegisterTemplate = true, + //}) + + .CreateLogger(); + + var now = DateTime.Now; + string logContent = String.Join("\r\n", dataParas); + var isJsonFormat = true; + if (isJudgeJsonFormat) + { + var judCont = logContent.Substring(0, logContent.LastIndexOf(",")); + isJsonFormat = JsonHelper.IsJson(judCont); + } + + if (isJsonFormat) + { + if (IsHeader) + { + logContent = ( + "--------------------------------\r\n" + + DateTime.Now + "|\r\n" + + String.Join("\r\n", dataParas) + "\r\n" + ); + } + // 展示elk支持输出4种日志级别 + Log.Information(logContent); + //Log.Warning(logContent); + //Log.Error(logContent); + //Log.Debug(logContent); + } + else + { + Console.WriteLine("【JSON格式异常:】"+logContent + now.ObjToString()); + } + Log.CloseAndFlush(); + } + /// + /// 记录异常日志 + /// + /// + /// + /// + public static void WriteErrorLog(string filename, string message, Exception ex) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .WriteTo.File(Path.Combine($"log/Error/{filename}/", ".txt"), rollingInterval: RollingInterval.Day) + .CreateLogger(); + Log.Error(ex, message); + Log.CloseAndFlush(); + } + } +} From 04905f07150a84eda6185d591182570c29eadb60 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 5 Feb 2023 22:36:15 +0800 Subject: [PATCH 114/289] feat:JobSetup_HostedService --- Blog.Core.Api/Program.cs | 1 + .../HostedService/Job1TimedService.cs | 2 +- .../HostedService/Job2TimedService.cs | 2 +- .../JobSetup_HostedService.cs | 20 +++++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) rename {Blog.Core.Tasks => Blog.Core.Extensions}/HostedService/Job1TimedService.cs (98%) rename {Blog.Core.Tasks => Blog.Core.Extensions}/HostedService/Job2TimedService.cs (97%) create mode 100644 Blog.Core.Extensions/ServiceExtensions/JobSetup_HostedService.cs diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 7a9a25c0..8ab4b93d 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -70,6 +70,7 @@ builder.Services.AddMiniProfilerSetup(); builder.Services.AddSwaggerSetup(); builder.Services.AddJobSetup(); +//builder.Services.AddJobSetup_HostedService(); builder.Services.AddHttpContextSetup(); builder.Services.AddAppTableConfigSetup(builder.Environment); builder.Services.AddHttpApi(); diff --git a/Blog.Core.Tasks/HostedService/Job1TimedService.cs b/Blog.Core.Extensions/HostedService/Job1TimedService.cs similarity index 98% rename from Blog.Core.Tasks/HostedService/Job1TimedService.cs rename to Blog.Core.Extensions/HostedService/Job1TimedService.cs index 9777affc..bd515194 100644 --- a/Blog.Core.Tasks/HostedService/Job1TimedService.cs +++ b/Blog.Core.Extensions/HostedService/Job1TimedService.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Blog.Core.Tasks +namespace Blog.Core.Extensions { public class Job1TimedService : IHostedService, IDisposable { diff --git a/Blog.Core.Tasks/HostedService/Job2TimedService.cs b/Blog.Core.Extensions/HostedService/Job2TimedService.cs similarity index 97% rename from Blog.Core.Tasks/HostedService/Job2TimedService.cs rename to Blog.Core.Extensions/HostedService/Job2TimedService.cs index ee7f2c5c..a1d0d88a 100644 --- a/Blog.Core.Tasks/HostedService/Job2TimedService.cs +++ b/Blog.Core.Extensions/HostedService/Job2TimedService.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Blog.Core.Tasks +namespace Blog.Core.Extensions { public class Job2TimedService : IHostedService, IDisposable { diff --git a/Blog.Core.Extensions/ServiceExtensions/JobSetup_HostedService.cs b/Blog.Core.Extensions/ServiceExtensions/JobSetup_HostedService.cs new file mode 100644 index 00000000..ccf5eb4d --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/JobSetup_HostedService.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Blog.Core.Extensions +{ + /// + /// 任务调度 启动服务 + /// + public static class JobSetup_HostedService + { + public static void AddJobSetup_HostedService(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + services.AddHostedService(); + services.AddHostedService(); + + } + } +} From a21e2055edbedc0bce480b5fa3c2a6c197ac55cf Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Mon, 6 Feb 2023 22:11:56 +0800 Subject: [PATCH 115/289] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20=E5=8D=87=E7=B4=9A?= =?UTF-8?q?SqlSugar=20->=205.1.3.49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Model/Blog.Core.Model.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index de7e6467..67bb5ad7 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -16,9 +16,13 @@ - + + + + + From 60653a383ddd3a9f1ba80acb0d6b550742ff6366 Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Tue, 7 Feb 2023 00:00:05 +0800 Subject: [PATCH 116/289] =?UTF-8?q?=E2=9C=A8=E3=80=80=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=A7=8D=E5=AD=90=E6=95=B0=E6=8D=AE=E6=8E=A5=E5=8F=A3=E3=80=81?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B8=B8=E7=94=A8=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/App.cs | 19 +++++ Blog.Core.Common/Core/InternalApp.cs | 15 ++++ .../Extensions/AssemblysExtensions.cs | 24 ++++++ .../Extensions/GenericTypeExtensions.cs | 28 +++++- Blog.Core.Common/Helper/UtilConvert.cs | 12 +++ Blog.Core.Common/Seed/DBSeed.cs | 85 +++++++++++++++++-- Blog.Core.Common/Seed/IEntitySeedData.cs | 36 ++++++++ 7 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 Blog.Core.Common/App.cs create mode 100644 Blog.Core.Common/Core/InternalApp.cs create mode 100644 Blog.Core.Common/Extensions/AssemblysExtensions.cs create mode 100644 Blog.Core.Common/Seed/IEntitySeedData.cs diff --git a/Blog.Core.Common/App.cs b/Blog.Core.Common/App.cs new file mode 100644 index 00000000..008aea5d --- /dev/null +++ b/Blog.Core.Common/App.cs @@ -0,0 +1,19 @@ +using Blog.Core.Common.Core; +using Blog.Core.Common.HttpContextUser; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Blog.Core.Common; + +public class App +{ + public static IServiceProvider RootServices => InternalApp.RootServices ; + + /// + /// 获取请求上下文 + /// + public static HttpContext HttpContext => RootServices?.GetService()?.HttpContext; + + public static IUser User => HttpContext == null ? null : RootServices?.GetService(); +} \ No newline at end of file diff --git a/Blog.Core.Common/Core/InternalApp.cs b/Blog.Core.Common/Core/InternalApp.cs new file mode 100644 index 00000000..b1f94f9d --- /dev/null +++ b/Blog.Core.Common/Core/InternalApp.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Hosting; +using System; + +namespace Blog.Core.Common.Core; + +public static class InternalApp +{ + /// 根服务 + public static IServiceProvider RootServices; + + public static void ConfigureApplication(this IHost app) + { + RootServices = app.Services; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/AssemblysExtensions.cs b/Blog.Core.Common/Extensions/AssemblysExtensions.cs new file mode 100644 index 00000000..06fa943b --- /dev/null +++ b/Blog.Core.Common/Extensions/AssemblysExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyModel; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace Blog.Core.Common.Extensions; + +public static class AssemblysExtensions +{ + public static List GetAllAssemblies() + { + var list = new List(); + var deps = DependencyContext.Default; + var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package" && lib.Name.StartsWith("Baiqian")); + foreach (var lib in libs) + { + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); + list.Add(assembly); + } + + return list; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/GenericTypeExtensions.cs b/Blog.Core.Common/Extensions/GenericTypeExtensions.cs index 368b6676..c0b6150f 100644 --- a/Blog.Core.Common/Extensions/GenericTypeExtensions.cs +++ b/Blog.Core.Common/Extensions/GenericTypeExtensions.cs @@ -26,5 +26,31 @@ public static string GetGenericTypeName(this object @object) { return @object.GetType().GetGenericTypeName(); } + + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + public static bool HasImplementedRawGeneric(this Type type, Type generic) + { + // 检查接口类型 + var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + if (isTheRawGenericType) return true; + + // 检查类型 + while (type != null && type != typeof(object)) + { + isTheRawGenericType = IsTheRawGenericType(type); + if (isTheRawGenericType) return true; + type = type.BaseType; + } + + return false; + + // 判断逻辑 + bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); + } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/UtilConvert.cs b/Blog.Core.Common/Helper/UtilConvert.cs index 282989e3..ea3b7c05 100644 --- a/Blog.Core.Common/Helper/UtilConvert.cs +++ b/Blog.Core.Common/Helper/UtilConvert.cs @@ -43,6 +43,18 @@ public static int ObjToInt(this object thisValue, int errorValue) return errorValue; } + public static long ObjToLong(this object thisValue) + { + long reval = 0; + if (thisValue == null) return 0; + if (thisValue != DBNull.Value && long.TryParse(thisValue.ToString(), out reval)) + { + return reval; + } + + return reval; + } + /// /// /// diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 9afdc30d..adb185f2 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -1,4 +1,5 @@ using Blog.Core.Common.DB; +using Blog.Core.Common.Extensions; using Blog.Core.Common.Helper; using Blog.Core.Model.Models; using Magicodes.ExporterAndImporter.Excel; @@ -96,7 +97,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) var modelTypes = referencedAssemblies .SelectMany(a => a.DefinedTypes) .Select(type => type.AsType()) - .Where(x => x.IsClass && x.Namespace != null && x.Namespace.Equals("Blog.Core.Model.Models")).ToList(); + .Where(x => x.IsClass && x.Namespace is "Blog.Core.Model.Models").ToList(); modelTypes.ForEach(t => { // 这里只支持添加表,不支持删除 @@ -110,8 +111,6 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) ConsoleHelper.WriteSuccessLine($"Tables created successfully!"); Console.WriteLine(); - - if (AppSettings.app(new string[] { "AppSettings", "SeedDBDataEnabled" }).ObjToBool()) { JsonSerializerSettings setting = new JsonSerializerSettings(); @@ -135,6 +134,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) var importer = new ExcelImporter(); #region BlogArticle + if (!await myContext.Db.Queryable().AnyAsync()) { myContext.GetEntityDB().InsertRange(JsonHelper.ParseFormByJson>(FileHelper.ReadFile(string.Format(SeedDataFolder, "BlogArticle"), Encoding.UTF8))); @@ -144,15 +144,14 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:BlogArticle already exists..."); } + #endregion #region Modules + if (!await myContext.Db.Queryable().AnyAsync()) { - - - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Modules"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); @@ -162,10 +161,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Modules already exists..."); } + #endregion #region Permission + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Permission"), Encoding.UTF8), setting); @@ -177,10 +178,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Permission already exists..."); } + #endregion #region Role + if (!await myContext.Db.Queryable().AnyAsync()) { //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); @@ -195,10 +198,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Role already exists..."); } + #endregion #region RoleModulePermission + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "RoleModulePermission"), Encoding.UTF8), setting); @@ -210,10 +215,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:RoleModulePermission already exists..."); } + #endregion #region Topic + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Topic"), Encoding.UTF8), setting); @@ -225,10 +232,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Topic already exists..."); } + #endregion #region TopicDetail + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TopicDetail"), Encoding.UTF8), setting); @@ -240,10 +249,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:TopicDetail already exists..."); } + #endregion #region UserRole + if (!await myContext.Db.Queryable().AnyAsync()) { //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); @@ -258,10 +269,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:UserRole already exists..."); } + #endregion #region sysUserInfo + if (!await myContext.Db.Queryable().AnyAsync()) { //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); @@ -276,10 +289,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:sysUserInfo already exists..."); } + #endregion #region TasksQz + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TasksQz"), Encoding.UTF8), setting); @@ -291,9 +306,11 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:TasksQz already exists..."); } + #endregion #region Department + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Department"), Encoding.UTF8), setting); @@ -305,13 +322,16 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Department already exists..."); } + #endregion + //种子初始化 + await SeedDataAsync(myContext); + ConsoleHelper.WriteSuccessLine($"Done seeding database!"); } Console.WriteLine(); - } catch (Exception ex) { @@ -321,5 +341,56 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) "3、其他错误:" + ex.Message); } } + + /// + /// 种子初始化数据 + /// + /// + /// + private static async Task SeedDataAsync(MyContext myContext) + { + // 获取所有种子配置-初始化数据 + var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes) + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && + u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>)))); + if (!seedDataTypes.Any()) return; + foreach (var seedType in seedDataTypes) + { + dynamic instance = Activator.CreateInstance(seedType); + //初始化数据 + { + var seedData = instance.InitSeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = myContext.Db.EntityMaintenance.GetEntityInfo(entityType); + + if (!await myContext.Db.Queryable(entity.DbTableName, "").AnyAsync()) + { + await myContext.Db.Insertable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} init success!"); + } + } + } + + //种子数据 + { + var seedData = instance.SeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = myContext.Db.EntityMaintenance.GetEntityInfo(entityType); + + await myContext.Db.Storageable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} seedData success!"); + } + } + + //自定义处理 + { + await instance.CustomizeSeedData(myContext.Db); + } + } + } } } \ No newline at end of file diff --git a/Blog.Core.Common/Seed/IEntitySeedData.cs b/Blog.Core.Common/Seed/IEntitySeedData.cs new file mode 100644 index 00000000..3e2f4859 --- /dev/null +++ b/Blog.Core.Common/Seed/IEntitySeedData.cs @@ -0,0 +1,36 @@ +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed; + +/// +/// 种子数据 接口 +/// +/// +public interface IEntitySeedData + where T : class, new() +{ + /// + /// 初始化种子数据
+ /// 只要表不存在数据,程序启动就会自动初始化 + ///
+ /// + IEnumerable InitSeedData(); + + /// + /// 种子数据
+ /// 存在不操作、不存在Insert
+ /// 适合系统内置数据,项目开发后续增加内置数据 + ///
+ /// + IEnumerable SeedData(); + + /// + /// 自定义操作
+ /// 以上满不足了,可以自己编写 + ///
+ /// + /// + Task CustomizeSeedData(ISqlSugarClient db); +} \ No newline at end of file From 151e7fc0ab97989c9545e2b1d406d254993edcfc Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Tue, 7 Feb 2023 00:27:55 +0800 Subject: [PATCH 117/289] =?UTF-8?q?=E2=9C=A8=F0=9F=8E=A8=20=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.兼容使用多租户多种方案 2.增加系统租户表、用户增加租户id、新增一张业务表用于租户测试数据 3.TenantByIdController 租户使用 --- Blog.Core.Api/Blog.Core.Api.csproj | 6 +- Blog.Core.Api/Blog.Core.Model.xml | 172 ++++++++++++++++++ Blog.Core.Api/Blog.Core.xml | 17 ++ Blog.Core.Api/Controllers/LoginController.cs | 37 ++-- .../Tenant/TenantByIdController.cs | 49 +++++ Blog.Core.Api/Program.cs | 3 +- .../BlogCore.Data.excel/SysUserInfo.xlsx | Bin 11177 -> 11197 bytes Blog.Core.Common/Blog.Core.Common.csproj | 4 + Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 86 +++++++++ Blog.Core.Common/DB/RepositorySetting.cs | 43 +++++ .../Extensions/AssemblysExtensions.cs | 2 +- .../HttpContextUser/AspNetUser.cs | 6 +- Blog.Core.Common/HttpContextUser/IUser.cs | 3 +- .../Seed/SeedData/BusinessDataSeedData.cs | 79 ++++++++ .../Seed/SeedData/TenantSeedData.cs | 44 +++++ .../Seed/SeedData/UserInfoSeedData.cs | 42 +++++ Blog.Core.EventBus/Blog.Core.EventBus.csproj | 2 +- .../Blog.Core.Extensions.csproj | 2 +- .../Middlewares/RequRespLogMiddleware.cs | 1 - .../ServiceExtensions/SqlsugarSetup.cs | 17 +- Blog.Core.IServices/BASE/IBaseServices.cs | 1 + Blog.Core.Model/Blog.Core.Model.csproj | 2 +- Blog.Core.Model/CustomEnums/TenantTypeEnum.cs | 23 +++ Blog.Core.Model/Models/BusinessTable.cs | 27 +++ Blog.Core.Model/Models/RootTkey/BaseEntity.cs | 86 +++++++++ .../RootTkey/Interface/IDeleteFilter.cs | 9 + .../RootTkey/Interface/ITenantEntity.cs | 15 ++ .../Models/RootTkey/RootEntityTkey.cs | 2 - Blog.Core.Model/Models/SysTenant.cs | 67 +++++++ Blog.Core.Model/Models/sysUserInfo.cs | 10 +- Blog.Core.Services/BASE/BaseServices.cs | 56 +++--- 31 files changed, 854 insertions(+), 59 deletions(-) create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs create mode 100644 Blog.Core.Common/DB/Aop/SqlsugarAop.cs create mode 100644 Blog.Core.Common/DB/RepositorySetting.cs create mode 100644 Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/TenantSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs create mode 100644 Blog.Core.Model/CustomEnums/TenantTypeEnum.cs create mode 100644 Blog.Core.Model/Models/BusinessTable.cs create mode 100644 Blog.Core.Model/Models/RootTkey/BaseEntity.cs create mode 100644 Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs create mode 100644 Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs create mode 100644 Blog.Core.Model/Models/SysTenant.cs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 38575a55..7e17937a 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -26,18 +26,22 @@ + + + + @@ -94,7 +98,7 @@ - + diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 36975d11..f5fd195c 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -34,6 +34,21 @@ 所有
+ + + 租户隔离方案 + + + + + Id隔离 + + + + + 库隔离 + + 以下model 来自ids4项目,多库模式,为了调取ids4数据 @@ -301,6 +316,27 @@ 逻辑删除 + + + 业务数据
+ 多租户 (Id 隔离) +
+
+ + + 无需手动赋值 + + + + + 名称 + + + + + 金额 + + 部门表 @@ -822,6 +858,137 @@ 修改时间 + + + 雪花Id + + + + + 状态
+ 中立字段,某些表可使用某些表不使用 +
+
+ + + 中立字段,某些表可使用某些表不使用
+ 逻辑上的删除,非物理删除
+ 例如:单据删除并非直接删除 +
+
+ + + 中立字段
+ 是否内置数据 +
+
+ + + 创建ID + + + + + 创建者 + + + + + 创建时间 + + + + + 修改ID + + + + + 更新者 + + + + + 修改日期 + + + + + 数据版本 + + + + + 软删除 过滤器 + + + + + 租户模型接口 + + + + + 租户Id + + + + + 系统租户表
+ 根据TenantType 分为两种方案:
+ 1.按租户字段区分
+ 2.按租户分库
+ +
+ + 注意:
+ 使用租户Id方案,无需配置分库的连接 +
+
+ + + 名称 + + + + + 租户类型 + + + + + 数据库/租户标识 不可重复
+ 使用Id方案,可无需配置 +
+
+ + + 主机
+ 使用Id方案,可无需配置 +
+
+ + + 数据库类型
+ 使用Id方案,可无需配置 +
+
+ + + 数据库连接
+ 使用Id方案,可无需配置 +
+
+ + + 状态 + + + + + 备注 + + 用户信息表 @@ -887,6 +1054,11 @@ 登录账号 + + + 租户Id + + 任务计划表 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 3321d53a..8b5b79fd 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1236,6 +1236,23 @@ + + + 多租户测试 + + + + + 获取租户下全部业务数据
+
+ +
+ + + 新增业务数据 + + + 自定义路由 /api/{version}/[controler]/[action] diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 25818c13..87f5c0c9 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Blog.Core.AuthHelper; +using Blog.Core.AuthHelper; using Blog.Core.AuthHelper.OverWrite; using Blog.Core.Common.Helper; using Blog.Core.IServices; @@ -12,8 +6,9 @@ using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; namespace Blog.Core.Controllers @@ -52,6 +47,7 @@ public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServic #region 获取token的第1种方法 + /// /// 获取JWT的方法1 /// @@ -62,7 +58,6 @@ public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServic [Route("Token")] public async Task> GetJwtStr(string name, string pass) { - string jwtStr = string.Empty; bool suc = false; //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 @@ -70,7 +65,6 @@ public async Task> GetJwtStr(string name, string pass) var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); if (user != null) { - TokenModelJwt tokenModel = new TokenModelJwt { Uid = 1, Role = user }; jwtStr = JwtHelper.IssueJwt(tokenModel); @@ -119,6 +113,7 @@ public MessageModel GetJwtStrForNuxt(string name, string pass) { jwtStr = "login fail!!!"; } + var result = new { data = new { success = suc, token = jwtStr } @@ -131,8 +126,8 @@ public MessageModel GetJwtStrForNuxt(string name, string pass) response = jwtStr }; } - #endregion + #endregion /// @@ -157,11 +152,14 @@ public async Task> GetJwtToken3(string name = " { var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List { + var claims = new List + { new Claim(ClaimTypes.Name, name), new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), + new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; + new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); @@ -218,11 +216,13 @@ public async Task> RefreshToken(string token = { var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List { - new Claim(ClaimTypes.Name, user.LoginName), - new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; + var claims = new List + { + new Claim(ClaimTypes.Name, user.LoginName), + new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); //用户标识 @@ -233,6 +233,7 @@ public async Task> RefreshToken(string token = return Success(refreshToken, "获取成功"); } } + return Failed("认证失败!"); } diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs new file mode 100644 index 00000000..a0271780 --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs @@ -0,0 +1,49 @@ +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Controllers; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 多租户测试 +/// +[Produces("application/json")] +[Route("api/Tenant/ById")] +[Authorize] +public class TenantByIdController : BaseApiController +{ + private readonly IBaseServices _services; + private readonly IUser _user; + + public TenantByIdController(IUser user, IBaseServices services) + { + _user = user; + _services = services; + } + + /// + /// 获取租户下全部业务数据
+ ///
+ /// + [HttpGet] + public async Task>> GetAll() + { + var data = await _services.Query(); + return Success(data); + } + + /// + /// 新增业务数据 + /// + /// + [HttpPost] + public async Task Post([FromBody] BusinessTable data) + { + await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync(); + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 8ab4b93d..21bb55c0 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -4,6 +4,7 @@ using Autofac.Extensions.DependencyInjection; using Blog.Core; using Blog.Core.Common; +using Blog.Core.Common.Core; using Blog.Core.Common.LogHelper; using Blog.Core.Common.Seed; using Blog.Core.Extensions; @@ -13,7 +14,6 @@ using Blog.Core.Hubs; using Blog.Core.IServices; using Blog.Core.Tasks; -using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -132,6 +132,7 @@ // 3、配置中间件 var app = builder.Build(); +app.ConfigureApplication(); if (app.Environment.IsDevelopment()) { diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.excel/SysUserInfo.xlsx b/Blog.Core.Api/wwwroot/BlogCore.Data.excel/SysUserInfo.xlsx index 51c0e125ec7a2b31b7d3e932bee4a0cde662b4d0..6a1e4ca6b01b92e1ab3095a7090a48ed41119a21 100644 GIT binary patch delta 4254 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 delta 4220 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 + + + + diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs new file mode 100644 index 00000000..3646625d --- /dev/null +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -0,0 +1,86 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Models.RootTkey.Interface; +using SqlSugar; +using System; + +namespace Blog.Core.Common.DB.Aop; + +public static class SqlSugarAop +{ + public static void DataExecuting(object oldValue, DataFilterModel entityInfo) + { + if (entityInfo.EntityValue is BaseEntity root) + { + if (root.Id == 0) + { + root.Id = SnowFlakeSingle.Instance.NextId(); + } + } + + if (entityInfo.EntityValue is BaseEntity baseEntity) + { + // 新增操作 + if (entityInfo.OperationType == DataFilterType.InsertByObject) + { + if (baseEntity.CreateTime == DateTime.MinValue) + { + baseEntity.CreateTime = DateTime.Now; + } + } + + if (entityInfo.OperationType == DataFilterType.UpdateByObject) + { + baseEntity.ModifyTime = DateTime.Now; + } + + + if (App.User?.ID > 0) + { + if (baseEntity is ITenantEntity tenant && App.User.TenantId > 0) + { + if (tenant.TenantId == 0) + { + tenant.TenantId = App.User.TenantId; + } + } + + switch (entityInfo.OperationType) + { + case DataFilterType.UpdateByObject: + baseEntity.ModifyId = App.User.ID; + baseEntity.ModifyBy = App.User.Name; + break; + case DataFilterType.InsertByObject: + if (baseEntity.CreateBy.IsNullOrEmpty() || baseEntity.CreateId is null or <= 0) + { + baseEntity.CreateId = App.User.ID; + baseEntity.CreateBy = App.User.Name; + } + + break; + } + } + } + } + + private static string GetWholeSql(SugarParameter[] paramArr, string sql) + { + foreach (var param in paramArr) + { + sql = sql.Replace(param.ParameterName, $@"'{param.Value.ObjToString()}'"); + } + + return sql; + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/RepositorySetting.cs b/Blog.Core.Common/DB/RepositorySetting.cs new file mode 100644 index 00000000..c80ec7f8 --- /dev/null +++ b/Blog.Core.Common/DB/RepositorySetting.cs @@ -0,0 +1,43 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Models.RootTkey.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Blog.Core.Common.DB; + +public class RepositorySetting +{ + private static readonly Lazy> AllEntitys = new(() => + { + return typeof(BaseEntity).Assembly + .GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(BaseEntity))) + .Where(it => it.FullName != null && it.FullName.StartsWith("Ivytalk.FoodSafety.Model.Models")); + }); + + public static IEnumerable Entitys => AllEntitys.Value; + + /// + /// 配置实体软删除过滤器
+ /// 统一过滤 软删除 无需自己写条件 + ///
+ public static void SetDeletedEntityFilter(SqlSugarScopeProvider db) + { + db.QueryFilter.AddTableFilter(it => it.IsDeleted == false); + } + + /// + /// 配置租户 + /// + public static void SetTenantEntityFilter(SqlSugarScopeProvider db) + { + if (App.User is not { ID: > 0, TenantId: > 0 }) + { + return; + } + + db.QueryFilter.AddTableFilter(it => it.TenantId == App.User.TenantId || it.TenantId == 0); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/AssemblysExtensions.cs b/Blog.Core.Common/Extensions/AssemblysExtensions.cs index 06fa943b..5e5d4349 100644 --- a/Blog.Core.Common/Extensions/AssemblysExtensions.cs +++ b/Blog.Core.Common/Extensions/AssemblysExtensions.cs @@ -12,7 +12,7 @@ public static List GetAllAssemblies() { var list = new List(); var deps = DependencyContext.Default; - var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package" && lib.Name.StartsWith("Baiqian")); + var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package" ); foreach (var lib in libs) { var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); diff --git a/Blog.Core.Common/HttpContextUser/AspNetUser.cs b/Blog.Core.Common/HttpContextUser/AspNetUser.cs index e4e27d87..b37e4a24 100644 --- a/Blog.Core.Common/HttpContextUser/AspNetUser.cs +++ b/Blog.Core.Common/HttpContextUser/AspNetUser.cs @@ -5,6 +5,7 @@ using Blog.Core.Model; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using SqlSugar.Extensions; namespace Blog.Core.Common.HttpContextUser { @@ -40,6 +41,7 @@ private string GetName() } public int ID => GetClaimValueByType("jti").FirstOrDefault().ObjToInt(); + public long TenantId => GetClaimValueByType("TenantId").FirstOrDefault().ObjToLong(); public bool IsAuthenticated() { @@ -87,11 +89,9 @@ public IEnumerable GetClaimsIdentity() public List GetClaimValueByType(string ClaimType) { - return (from item in GetClaimsIdentity() where item.Type == ClaimType select item.Value).ToList(); - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/HttpContextUser/IUser.cs b/Blog.Core.Common/HttpContextUser/IUser.cs index beef3b7d..3849bd38 100644 --- a/Blog.Core.Common/HttpContextUser/IUser.cs +++ b/Blog.Core.Common/HttpContextUser/IUser.cs @@ -8,6 +8,7 @@ public interface IUser { string Name { get; } int ID { get; } + long TenantId { get; } bool IsAuthenticated(); IEnumerable GetClaimsIdentity(); List GetClaimValueByType(string ClaimType); @@ -17,4 +18,4 @@ public interface IUser MessageModel MessageModel { get; set; } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs new file mode 100644 index 00000000..361cd725 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Blog.Core.Model.Models; +using SqlSugar; + +namespace Blog.Core.Common.Seed.SeedData; + +/// +/// 初始化 业务数据 +/// +public class BusinessDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new[] + { + new BusinessTable() + { + Id = 1, + TenantId = 1000001, + Name = "张三的数据01", + Amount = 150, + IsDeleted = true, + }, + new BusinessTable() + { + Id = 2, + TenantId = 1000001, + Name = "张三的数据02", + Amount = 200, + }, + new BusinessTable() + { + Id = 3, + TenantId = 1000001, + Name = "张三的数据03", + Amount = 250, + }, + new BusinessTable() + { + Id = 4, + TenantId = 1000002, + Name = "李四的数据01", + Amount = 300, + }, + new BusinessTable() + { + Id = 5, + TenantId = 1000002, + Name = "李四的数据02", + Amount = 500, + }, + new BusinessTable() + { + Id = 6, + TenantId = 0, + Name = "公共数据01", + Amount = 16600, + }, + new BusinessTable() + { + Id = 7, + TenantId = 0, + Name = "公共数据02", + Amount = 19800, + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs new file mode 100644 index 00000000..89945547 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Blog.Core.Model.CustomEnums; +using Blog.Core.Model.Models; +using SqlSugar; + +namespace Blog.Core.Common.Seed.SeedData; + +/// +/// 租户 种子数据 +/// +public class TenantSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new[] + { + new SysTenant() + { + Id = 1000001, + ConfigId = "Tenant_1", + Name = "张三", + TenantType = TenantTypeEnum.Id + }, + new SysTenant() + { + Id = 1000002, + ConfigId = "Tenant_2", + Name = "李四", + TenantType = TenantTypeEnum.Id + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs new file mode 100644 index 00000000..c00eb798 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Blog.Core.Model.Models; +using SqlSugar; + +namespace Blog.Core.Common.Seed.SeedData; + +public class UserInfoSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return default; + } + + public IEnumerable SeedData() + { + return new[] + { + new SysUserInfo() + { + Id = 10001, + LoginName = "zhangsan", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "张三", + TenantId = 1000001, //租户Id + }, + new SysUserInfo() + { + Id = 10002, + LoginName = "lisi", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "李四", + TenantId = 1000002, //租户Id + }, + }; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.EventBus/Blog.Core.EventBus.csproj b/Blog.Core.EventBus/Blog.Core.EventBus.csproj index 070c0a48..09d4e6a2 100644 --- a/Blog.Core.EventBus/Blog.Core.EventBus.csproj +++ b/Blog.Core.EventBus/Blog.Core.EventBus.csproj @@ -12,7 +12,7 @@ - + diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 50969b05..451d24f1 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -21,7 +21,7 @@ - + diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index 6ff8d044..4c61ed3b 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Ubiety.Dns.Core.Common; namespace Blog.Core.Extensions.Middlewares { diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index b3eb4bdc..3ecf224a 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Blog.Core.Common.DB.Aop; namespace Blog.Core.Extensions { @@ -101,7 +102,21 @@ public static void AddSqlsugarSetup(this IServiceCollection services) } ); }); - return new SqlSugarScope(listConfig); + return new SqlSugarScope(listConfig, db => + { + listConfig.ForEach(config => + { + var dbProvider = db.GetConnectionScope((string)config.ConfigId); + + // 数据审计 + dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; + + // 配置实体假删除过滤器 + RepositorySetting.SetDeletedEntityFilter(dbProvider); + // 配置实体数据权限 + RepositorySetting.SetTenantEntityFilter(dbProvider); + }); + }); }); } diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index 27daae98..7856f8bf 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -10,6 +10,7 @@ namespace Blog.Core.IServices.BASE { public interface IBaseServices where TEntity : class { + ISqlSugarClient Db { get; } Task QueryById(object objId); Task QueryById(object objId, bool blnUseCache = false); diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index 67bb5ad7..e66c5c5b 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -22,7 +22,7 @@ - + diff --git a/Blog.Core.Model/CustomEnums/TenantTypeEnum.cs b/Blog.Core.Model/CustomEnums/TenantTypeEnum.cs new file mode 100644 index 00000000..a445414b --- /dev/null +++ b/Blog.Core.Model/CustomEnums/TenantTypeEnum.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; + +namespace Blog.Core.Model.CustomEnums; + +/// +/// 租户隔离方案 +/// +public enum TenantTypeEnum +{ + None = 0, + + /// + /// Id隔离 + /// + [Description("Id隔离")] + Id = 1, + + /// + /// 库隔离 + /// + [Description("库隔离")] + Db = 2, +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/BusinessTable.cs b/Blog.Core.Model/Models/BusinessTable.cs new file mode 100644 index 00000000..01a18716 --- /dev/null +++ b/Blog.Core.Model/Models/BusinessTable.cs @@ -0,0 +1,27 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Models.RootTkey.Interface; + +namespace Blog.Core.Model.Models; + +/// +/// 业务数据
+/// 多租户 (Id 隔离) +///
+public class BusinessTable : BaseEntity, ITenantEntity +{ + /// + /// 无需手动赋值 + /// + public long TenantId { get; set; } + + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/BaseEntity.cs b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs new file mode 100644 index 00000000..efbde8fd --- /dev/null +++ b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs @@ -0,0 +1,86 @@ +using Blog.Core.Model.Models.RootTkey.Interface; +using SqlSugar; +using System; + +namespace Blog.Core.Model.Models.RootTkey; + +public class BaseEntity : IDeleteFilter +{ + /// + /// 雪花Id + /// + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long Id { get; set; } + + #region 数据状态管理 + + /// + /// 状态
+ /// 中立字段,某些表可使用某些表不使用 + ///
+ public bool Enabled { get; set; } = true; + + /// + /// 中立字段,某些表可使用某些表不使用
+ /// 逻辑上的删除,非物理删除
+ /// 例如:单据删除并非直接删除 + ///
+ public bool IsDeleted { get; set; } + + /// + /// 中立字段
+ /// 是否内置数据 + ///
+ public bool IsInternal { get; set; } + + #endregion + + #region 创建 + + /// + /// 创建ID + /// + [SugarColumn(IsNullable = true, IsOnlyIgnoreUpdate = true)] + public long? CreateId { get; set; } + + /// + /// 创建者 + /// + [SugarColumn(IsNullable = true, IsOnlyIgnoreUpdate = true)] + public string CreateBy { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(IsOnlyIgnoreUpdate = true)] + public DateTime CreateTime { get; set; } = DateTime.Now; + + #endregion + + #region 修改 + + /// + /// 修改ID + /// + [SugarColumn(IsNullable = true)] + public long? ModifyId { get; set; } + + /// + /// 更新者 + /// + [SugarColumn(IsNullable = true)] + public string ModifyBy { get; set; } + + /// + /// 修改日期 + /// + public DateTime? ModifyTime { get; set; } = DateTime.Now; + + /// + /// 数据版本 + /// + [SugarColumn(DefaultValue = "0", IsEnableUpdateVersionValidation = true)] //标识版本字段 + public long Version { get; set; } + + #endregion +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs b/Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs new file mode 100644 index 00000000..57d421e9 --- /dev/null +++ b/Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs @@ -0,0 +1,9 @@ +namespace Blog.Core.Model.Models.RootTkey.Interface; + +/// +/// 软删除 过滤器 +/// +public interface IDeleteFilter +{ + public bool IsDeleted { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs b/Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs new file mode 100644 index 00000000..350312b8 --- /dev/null +++ b/Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs @@ -0,0 +1,15 @@ +using SqlSugar; + +namespace Blog.Core.Model.Models.RootTkey.Interface; + +/// +/// 租户模型接口 +/// +public interface ITenantEntity +{ + /// + /// 租户Id + /// + [SugarColumn(DefaultValue = "0")] + public long TenantId { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs b/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs index afecdec9..20bdda0d 100644 --- a/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs +++ b/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs @@ -11,7 +11,5 @@ public class RootEntityTkey where Tkey : IEquatable ///
[SugarColumn(IsNullable = false, IsPrimaryKey = true)] public Tkey Id { get; set; } - - } } \ No newline at end of file diff --git a/Blog.Core.Model/Models/SysTenant.cs b/Blog.Core.Model/Models/SysTenant.cs new file mode 100644 index 00000000..7e6f195d --- /dev/null +++ b/Blog.Core.Model/Models/SysTenant.cs @@ -0,0 +1,67 @@ +using Blog.Core.Model.CustomEnums; +using SqlSugar; + +namespace Blog.Core.Model.Models; + +/// +/// 系统租户表
+/// 根据TenantType 分为两种方案:
+/// 1.按租户字段区分
+/// 2.按租户分库
+/// +///
+/// +/// 注意:
+/// 使用租户Id方案,无需配置分库的连接 +///
+public class SysTenant : RootEntityTkey +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 租户类型 + /// + public TenantTypeEnum TenantType { get; set; } + + /// + /// 数据库/租户标识 不可重复
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(Length = 64)] + public string ConfigId { get; set; } + + /// + /// 主机
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(IsNullable = true)] + public string Host { get; set; } + + /// + /// 数据库类型
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(IsNullable = true)] + public SqlSugar.DbType? DbType { get; set; } + + /// + /// 数据库连接
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(IsNullable = true)] + public string Connection { get; set; } + + /// + /// 状态 + /// + public bool Status { get; set; } = true; + + /// + /// 备注 + /// + [SugarColumn(IsNullable = true)] + public string Remark { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index c60e4bce..366b2291 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -8,7 +8,7 @@ namespace Blog.Core.Model.Models /// 用户信息表 ///
//[SugarTable("SysUserInfo")] - [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') + [SugarTable("SysUserInfo", "用户表")] //('数据库表名','数据库表备注') public class SysUserInfo : SysUserInfoRoot { public SysUserInfo() @@ -120,6 +120,14 @@ public SysUserInfo(string loginName, string loginPWD) [SugarColumn(IsNullable = true)] public bool IsDeleted { get; set; } + /// + /// 租户Id + /// + [SugarColumn(IsNullable = false,DefaultValue = "0")] + public long TenantId { get; set; } + + [Navigate(NavigateType.OneToOne, nameof(TenantId))] + public SysTenant Tenant { get; set; } [SugarColumn(IsIgnore = true)] public List RoleNames { get; set; } diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index ec685be2..f8105165 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -17,13 +17,17 @@ public BaseServices(IBaseRepository BaseDal = null) { this.BaseDal = BaseDal; } + //public IBaseRepository baseDal = new BaseRepository(); - public IBaseRepository BaseDal { get; set; }//通过在子类的构造函数中注入,这里是基类,不用构造函数 + public IBaseRepository BaseDal { get; set; } //通过在子类的构造函数中注入,这里是基类,不用构造函数 + + public ISqlSugarClient Db => BaseDal.Db; public async Task QueryById(object objId) { return await BaseDal.QueryById(objId); } + /// /// 功能描述:根据ID查询一条数据 /// 作  者:AZLinli.Blog.Core @@ -76,21 +80,23 @@ public async Task Update(TEntity entity) { return await BaseDal.Update(entity); } + public async Task Update(TEntity entity, string where) { return await BaseDal.Update(entity, where); } + public async Task Update(object operateAnonymousObjects) { return await BaseDal.Update(operateAnonymousObjects); } public async Task Update( - TEntity entity, - List lstColumns = null, - List lstIgnoreColumns = null, - string where = "" - ) + TEntity entity, + List lstColumns = null, + List lstIgnoreColumns = null, + string where = "" + ) { return await BaseDal.Update(entity, lstColumns, lstIgnoreColumns, where); } @@ -127,7 +133,6 @@ public async Task DeleteByIds(object[] ids) } - /// /// 功能描述:查询所有数据 /// 作  者:AZLinli.Blog.Core @@ -181,7 +186,7 @@ public async Task> Query(Expression查询实体条件 /// 排序条件 /// - public async Task> Query(Expression> expression, Expression> whereExpression,string orderByFileds) + public async Task> Query(Expression> expression, Expression> whereExpression, string orderByFileds) { return await BaseDal.Query(expression, whereExpression, orderByFileds); } @@ -224,7 +229,6 @@ public async Task> Query(string where, string orderByFileds) public async Task> QuerySql(string sql, SugarParameter[] parameters = null) { return await BaseDal.QuerySql(sql, parameters); - } /// @@ -236,8 +240,8 @@ public async Task> QuerySql(string sql, SugarParameter[] parameter public async Task QueryTable(string sql, SugarParameter[] parameters = null) { return await BaseDal.QueryTable(sql, parameters); - } + /// /// 功能描述:查询前N条数据 /// 作  者:AZLinli.Blog.Core @@ -283,10 +287,10 @@ public async Task> Query( string orderByFileds) { return await BaseDal.Query( - whereExpression, - pageIndex, - pageSize, - orderByFileds); + whereExpression, + pageIndex, + pageSize, + orderByFileds); } /// @@ -299,34 +303,34 @@ public async Task> Query( /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string where, - int pageIndex, - int pageSize, - string orderByFileds) + string where, + int pageIndex, + int pageSize, + string orderByFileds) { return await BaseDal.Query( - where, - pageIndex, - pageSize, - orderByFileds); + where, + pageIndex, + pageSize, + orderByFileds); } public async Task> QueryPage(Expression> whereExpression, - int pageIndex = 1, int pageSize = 20, string orderByFileds = null) + int pageIndex = 1, int pageSize = 20, string orderByFileds = null) { return await BaseDal.QueryPage(whereExpression, - pageIndex, pageSize, orderByFileds); + pageIndex, pageSize, orderByFileds); } public async Task> QueryMuch(Expression> joinExpression, Expression> selectExpression, Expression> whereLambda = null) where T : class, new() { return await BaseDal.QueryMuch(joinExpression, selectExpression, whereLambda); } + public async Task> QueryPage(PaginationModel pagination) { var express = DynamicLinqFactory.CreateLambda(pagination.Conditions); return await QueryPage(express, pagination.PageIndex, pagination.PageSize, pagination.OrderByFileds); } } - -} +} \ No newline at end of file From 657e67d164b64996d2ee2ca790f81c3800304b8f Mon Sep 17 00:00:00 2001 From: HuiJiOnGit <40553940+HuiJiOnGit@users.noreply.github.com> Date: Tue, 7 Feb 2023 08:30:39 +0800 Subject: [PATCH 118/289] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=9C=A8docke?= =?UTF-8?q?r=E4=B8=AD=E6=97=A0=E6=B3=95=E8=BF=98=E5=8E=9F=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.sln | 1 + Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/Blog.Core.sln b/Blog.Core.sln index c8f61505..c8989b24 100644 --- a/Blog.Core.sln +++ b/Blog.Core.sln @@ -30,6 +30,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt Blog.Core.Publish.Docker.sh = Blog.Core.Publish.Docker.sh Blog.Core.Publish.Linux.sh = Blog.Core.Publish.Linux.sh codecov.yml = codecov.yml + build\common.targets = build\common.targets CreateYourProject.bat = CreateYourProject.bat DockerBuild.bat = DockerBuild.bat Dockerfile = Dockerfile diff --git a/Dockerfile b/Dockerfile index 69a3d132..c80e6896 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY ["Blog.Core.Services/Blog.Core.Services.csproj", "Blog.Core.Services/"] COPY ["Blog.Core.IServices/Blog.Core.IServices.csproj", "Blog.Core.IServices/"] COPY ["Blog.Core.Repository/Blog.Core.Repository.csproj", "Blog.Core.Repository/"] COPY ["Blog.Core.Tasks/Blog.Core.Tasks.csproj", "Blog.Core.Tasks/"] +COPY ["build", "build/"] RUN dotnet restore "Blog.Core.Api/Blog.Core.Api.csproj" COPY . . WORKDIR "/src/Blog.Core.Api" From 822e2ebd1fe6e4d848ad91f2556158410f8411bd Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 7 Feb 2023 10:39:00 +0800 Subject: [PATCH 119/289] Update DBSeed.cs --- Blog.Core.Common/Seed/DBSeed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 9afdc30d..9b2c911f 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -291,7 +291,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:TasksQz already exists..."); } - #endregion + #endregion TasksQz #region Department if (!await myContext.Db.Queryable().AnyAsync()) From 552d94beb8d8fe6d1aa6fc002894d19f2444208c Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Sat, 11 Feb 2023 23:39:44 +0800 Subject: [PATCH 120/289] Update DBSeed.cs --- Blog.Core.Common/Seed/DBSeed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 9b2c911f..da313e3c 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -291,7 +291,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:TasksQz already exists..."); } - #endregion TasksQz + #endregion #region Department if (!await myContext.Db.Queryable().AnyAsync()) @@ -322,4 +322,4 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) } } } -} \ No newline at end of file +} From 0228aed85fdd62946bb7bb0f64c1983f5cf0c538 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 12 Feb 2023 00:06:39 +0800 Subject: [PATCH 121/289] Update RepositorySetting.cs --- Blog.Core.Common/DB/RepositorySetting.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Common/DB/RepositorySetting.cs b/Blog.Core.Common/DB/RepositorySetting.cs index c80ec7f8..4f2e67d2 100644 --- a/Blog.Core.Common/DB/RepositorySetting.cs +++ b/Blog.Core.Common/DB/RepositorySetting.cs @@ -14,7 +14,7 @@ public class RepositorySetting return typeof(BaseEntity).Assembly .GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(BaseEntity))) - .Where(it => it.FullName != null && it.FullName.StartsWith("Ivytalk.FoodSafety.Model.Models")); + .Where(it => it.FullName != null && it.FullName.StartsWith("Blog.Core.Model.Models")); }); public static IEnumerable Entitys => AllEntitys.Value; From d09de26d6ea095c626bdfa623a23d91d1902aae8 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sun, 12 Feb 2023 22:00:04 +0800 Subject: [PATCH 122/289] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=BB=E5=88=86?= =?UTF-8?q?=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Model.xml | 137 ++++++ Blog.Core.Api/Blog.Core.xml | 160 +++++++ Blog.Core.Api/Controllers/TrojanController.cs | 451 ++++++++++++++++++ Blog.Core.Api/Controllers/WeChatController.cs | 12 + Blog.Core.Api/Dockerfile | 2 +- Blog.Core.IServices/BASE/IBaseServices.cs | 1 + Blog.Core.IServices/ITrojanUsersServices.cs | 14 + Blog.Core.Model/Models/TrojanCusServers.cs | 26 + Blog.Core.Model/Models/TrojanDetails.cs | 63 +++ Blog.Core.Model/Models/TrojanServers.cs | 31 ++ Blog.Core.Model/Models/TrojanUrlServers.cs | 26 + Blog.Core.Model/Models/TrojanUsers.cs | 39 ++ .../ViewModels/TrojanLimitFlowDto.cs | 23 + Blog.Core.Model/ViewModels/TrojanServerDto.cs | 14 + .../ViewModels/TrojanServerSpliceDto.cs | 28 ++ .../ViewModels/TrojanUseDetailDto.cs | 35 ++ Blog.Core.Repository/BASE/BaseRepository.cs | 13 + Blog.Core.Repository/BASE/IBaseRepository.cs | 6 + Blog.Core.Services/BASE/BaseServices.cs | 9 + Blog.Core.Services/TrojanUsersServices.cs | 18 + Blog.Core.Tasks/Blog.Core.Tasks.csproj | 1 + .../HostedService/Job1TimedService.cs | 60 +++ .../HostedService/Job2TimedService.cs | 47 ++ .../QuartzNet/Jobs/Job_Trojan_Quartz.cs | 79 +++ .../QuartzNet/Jobs/Job_URL_Quartz.cs | 51 ++ 25 files changed, 1345 insertions(+), 1 deletion(-) create mode 100644 Blog.Core.Api/Controllers/TrojanController.cs create mode 100644 Blog.Core.IServices/ITrojanUsersServices.cs create mode 100644 Blog.Core.Model/Models/TrojanCusServers.cs create mode 100644 Blog.Core.Model/Models/TrojanDetails.cs create mode 100644 Blog.Core.Model/Models/TrojanServers.cs create mode 100644 Blog.Core.Model/Models/TrojanUrlServers.cs create mode 100644 Blog.Core.Model/Models/TrojanUsers.cs create mode 100644 Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs create mode 100644 Blog.Core.Model/ViewModels/TrojanServerDto.cs create mode 100644 Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs create mode 100644 Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs create mode 100644 Blog.Core.Services/TrojanUsersServices.cs create mode 100644 Blog.Core.Tasks/HostedService/Job1TimedService.cs create mode 100644 Blog.Core.Tasks/HostedService/Job2TimedService.cs create mode 100644 Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs create mode 100644 Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 36975d11..ea9857bc 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -982,6 +982,76 @@ Tibug 博文 + + + users自定义服务器 + + + + + 用户流量每月汇总表 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Trojan服务器 + + + + + users自定义URL服务器 + + + + + Trojan用户 + + + + + 历史流量记录 + + 用户跟角色关联表 @@ -2407,6 +2477,73 @@ + + + 限制流量dto + 作者:胡丁文 + 时间:2020-4-27 16:57:07 + + + + + 用户 + + + + + 流量(-1为无限,单位为最小单位byte) + + + + + Trojan服务器拼接服务器和订阅地址 + + + + + 普通订阅连接 + + + + + clash订阅连接 + + + + + 备用clash订阅连接 + + + + + Trojan用户流量统计分组 + + + + + 用户ID + + + + + 月度 + + + + + 上传流量 + + + + + 下载流量 + + + + + 下载流量 + + 微信接口消息DTO diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 3321d53a..a14a9006 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -650,6 +650,159 @@ + + + 获取Trojan用户 + + + + + + + + + 获取Trojan用户-下拉列表用 + + + + + + 添加Trojan用户 + + + + + + + 更新Trojan用户 + + + + + + + 删除用户 + + + + + + + 重置流量 + + + + + + + 限制流量 + + + + + + + 重置链接密码 + + + + + + + 获取Trojan服务器 + + + + + + 获取拼接后的Trojan服务器 + + passwordshow + + + + + 删除Trojan服务器 + + + + + + + 更新Trojan服务器 + + + + + + + 添加Trojan服务器 + + + + + + + 获取Cus服务器 + + + + + + 删除Cus服务器 + + + + + + + 更新Cus服务器 + + + + + + + 添加Cus服务器 + + + + + + + 获取Url服务器 + + + + + + 删除Url服务器 + + + + + + + 更新Url服务器 + + + + + + + 添加Url服务器 + + + + + + + 获取订阅数据 + + 链接密码 + 是否使用base64加密 + + 用户管理 @@ -1062,6 +1215,13 @@ 卡片消息对象 + + + 推送卡片消息接口 + + 卡片消息对象 + + 推送文本消息 diff --git a/Blog.Core.Api/Controllers/TrojanController.cs b/Blog.Core.Api/Controllers/TrojanController.cs new file mode 100644 index 00000000..8ec64c8a --- /dev/null +++ b/Blog.Core.Api/Controllers/TrojanController.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.Common.Extensions; +using Blog.Core.Common.Helper; +using Blog.Core.Common.HttpContextUser; +using Blog.Core.IServices; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Blog.Core.Model.ViewModels; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + [Authorize(Permissions.Name)] + public class TrojanController : ControllerBase + { + private ITrojanUsersServices _trojanUsersServices; + public IBaseServices _baseServicesServers; + public IBaseServices _baseServicesDetails; + public IBaseServices _baseServicesCusServers; + public IBaseServices _baseServicesUrlServers; + private IUser _user; + public TrojanController(ITrojanUsersServices trojanUsersServices,IUser user + , IBaseServices baseServicesServers + , IBaseServices baseServicesDetails + , IBaseServices baseServicesCusServers + , IBaseServices baseServicesUrlServers) + { + _baseServicesDetails = baseServicesDetails; + _baseServicesServers = baseServicesServers; + _trojanUsersServices = trojanUsersServices; + _baseServicesCusServers = baseServicesCusServers; + _baseServicesUrlServers = baseServicesUrlServers; + _user = user; + } + /// + /// 获取Trojan用户 + /// + /// + /// + /// + /// + [HttpGet] + public async Task>> GetUser([FromQuery]PaginationModel pagination, [FromQuery] string name, [FromQuery] bool isuse) + { + var whereFind = LinqHelper.True(); + if (!string.IsNullOrEmpty(name)) + whereFind = whereFind.And(t=>t.username.Contains(name)); + if (isuse) + whereFind = whereFind.And(t => t.upload > 0 || t.download > 0); + var data = await _trojanUsersServices.QueryPage(whereFind, pagination.PageIndex, pagination.PageSize); + if (data.data.Count > 0) + { + var ids = data.data.Select(t => t.id).ToList(); + var where = LinqHelper.True(); + where = where.And(t => ids.Contains(t.userId));//.And(t => t.calDate < DateTime.Now).And(t => t.calDate > DateTime.Now.AddMonths(-12)); + var userDetails = await _baseServicesDetails.Query(where); + foreach (var trojanUser in data.data) + { + var ls = from t in userDetails + where t.userId == trojanUser.id + group t by new { moth = t.calDate.ToString("yyyy-MM"), id = t.userId } into g + orderby g.Key.moth descending + select new TrojanUseDetailDto { userId = g.Key.id, moth = g.Key.moth, up = g.Sum(t => Convert.ToDecimal(t.upload)), down = g.Sum(t => Convert.ToDecimal(t.download)) }; + var lsData = ls.ToList(); + trojanUser.useList = lsData; + } + } + return MessageModel>.Success("获取成功", data); + } + + /// + /// 获取Trojan用户-下拉列表用 + /// + /// + [HttpGet] + public async Task> GetAllTrojanUser() + { + var data = await _trojanUsersServices.QueryTable("select id,username from users"); + return MessageModel.Success("获取成功", data); + } + /// + /// 添加Trojan用户 + /// + /// + /// + [HttpPost] + public async Task> AddUser([FromBody]TrojanUsers user) + { + var find = await _trojanUsersServices.Query(t => t.username == user.username); + if(find!=null && find.Count>0) return MessageModel.Fail("用户名已存在"); + var pass = StringHelper.GetGUID(); + var passEcrypt = ShaHelper.Sha224(pass); + //user.quota = 0; + user.upload = 0; + user.download = 0; + user.password = passEcrypt; + user.passwordshow = pass; + var data = await _trojanUsersServices.Add(user); + return MessageModel.Success("添加成功", data); + } + /// + /// 更新Trojan用户 + /// + /// + /// + [HttpPut] + public async Task> UpdateUser([FromBody]TrojanUsers user) + { + var find = await _trojanUsersServices.QueryById(user.id); + if (find == null) return MessageModel.Fail("用户名不存在"); + find.username = user.username; + var data = await _trojanUsersServices.Update(find, new List { "username" }); + return MessageModel.Success("更新成功", data); + } + + /// + /// 删除用户 + /// + /// + /// + [HttpPut] + public async Task> DelUser([FromBody]int[] users) + { + var data = await _trojanUsersServices.Query(t => users.Contains(t.id)); + var list = data.Select(t => t.id.ToString()).ToArray(); + await _trojanUsersServices.DeleteByIds(list); + return MessageModel.Success("删除成功"); + } + /// + /// 重置流量 + /// + /// + /// + [HttpPut] + public async Task> ResetFlow([FromBody]int[] users) + { + var data = await _trojanUsersServices.Query(t => users.Contains(t.id)); + foreach (var item in data) + { + item.upload = 0; + item.download = 0; + await _trojanUsersServices.Update(item, new List { "upload", "download" }); + } + return MessageModel.Success("重置流量成功"); + } + /// + /// 限制流量 + /// + /// + /// + [HttpPut] + public async Task> LimitFlow([FromBody] TrojanLimitFlowDto limit) + { + var data = await _trojanUsersServices.Query(t => limit.users.Contains(t.id)); + foreach (var item in data) + { + item.quota = limit.quota; + await _trojanUsersServices.Update(item, new List { "quota" }); + } + return MessageModel.Success("限制流量成功"); + } + /// + /// 重置链接密码 + /// + /// + /// + [HttpPut] + public async Task> ResetPass([FromBody]int[] users) + { + var data = await _trojanUsersServices.Query(t => users.Contains(t.id)); + var pass = StringHelper.GetGUID(); + var passEcrypt = ShaHelper.Sha224(pass); + foreach (var item in data) + { + item.password = passEcrypt; + item.passwordshow = pass; + await _trojanUsersServices.Update(item, new List { "password" , "passwordshow" }); + } + return MessageModel.Success("重置链接密码成功"); + } + /// + /// 获取Trojan服务器 + /// + /// + [HttpGet] + public async Task>> GetServers() + { + var data = await _baseServicesServers.Query(); + data = data.OrderBy(t => t.servername).ToList(); + return MessageModel>.Success("获取成功", data); + } + /// + /// 获取拼接后的Trojan服务器 + /// + /// passwordshow + /// + [HttpGet] + public async Task> GetSpliceServers(string id) + { + var data = await _baseServicesServers.Query(); + data = data.OrderBy(t => t.servername).ToList(); + var res = new TrojanServerSpliceDto(); + res.normalApi = AppSettings.app(new string[] { "trojan", "normalApi" }).ObjToString(); + res.clashApi = AppSettings.app(new string[] { "trojan", "clashApi" }).ObjToString(); + res.clashApiBackup = AppSettings.app(new string[] { "trojan", "clashApiBackup" }).ObjToString(); + foreach (var item in data) + { + var serverSplice = GetSplice(item, id); + res.list.Add(new TrojanServerDto { name = item.servername, value = serverSplice }); + } + return MessageModel.Success("获取成功", res); ; + + } + /// + /// 删除Trojan服务器 + /// + /// + /// + [HttpPut] + public async Task>> DelServers([FromBody]int[] servers) + { + var data = await _baseServicesServers.DeleteByIds(servers.Select(t=>t.ToString()).ToArray()); + if (data) + return MessageModel>.Success("删除成功"); + else + return MessageModel>.Fail("删除失败"); + } + /// + /// 更新Trojan服务器 + /// + /// + /// + [HttpPut] + public async Task>> UpdateServers(TrojanServers server) + { + var data = await _baseServicesServers.Update(server); + return MessageModel>.Success("更新成功"); + } + /// + /// 添加Trojan服务器 + /// + /// + /// + [HttpPost] + public async Task>> AddServers(TrojanServers server) + { + var data = await _baseServicesServers.Add(server); + return MessageModel>.Success("添加成功"); + } + + /// + /// 获取Cus服务器 + /// + /// + [HttpGet] + public async Task>> GetCusServers() + { + var data = await _baseServicesCusServers.Query(); + data = data.OrderBy(t => t.servername).ToList(); + return MessageModel>.Success("获取成功", data); + } + /// + /// 删除Cus服务器 + /// + /// + /// + [HttpPut] + public async Task>> DelCusServers([FromBody] int[] servers) + { + var data = await _baseServicesCusServers.DeleteByIds(servers.Select(t => t.ToString()).ToArray()); + if (data) + return MessageModel>.Success("删除成功"); + else + return MessageModel>.Fail("删除失败"); + } + /// + /// 更新Cus服务器 + /// + /// + /// + [HttpPut] + public async Task>> UpdateCusServers(TrojanCusServers server) + { + var data = await _baseServicesCusServers.Update(server); + return MessageModel>.Success("更新成功"); + } + /// + /// 添加Cus服务器 + /// + /// + /// + [HttpPost] + public async Task>> AddCusServers(TrojanCusServers server) + { + var data = await _baseServicesCusServers.Add(server); + return MessageModel>.Success("添加成功"); + } + + + /// + /// 获取Url服务器 + /// + /// + [HttpGet] + public async Task>> GetUrlServers() + { + var data = await _baseServicesUrlServers.Query(); + data = data.OrderBy(t => t.servername).ToList(); + return MessageModel>.Success("获取成功", data); + } + /// + /// 删除Url服务器 + /// + /// + /// + [HttpPut] + public async Task>> DelUrlServers([FromBody] int[] servers) + { + var data = await _baseServicesUrlServers.DeleteByIds(servers.Select(t => t.ToString()).ToArray()); + if (data) + return MessageModel>.Success("删除成功"); + else + return MessageModel>.Fail("删除失败"); + } + /// + /// 更新Url服务器 + /// + /// + /// + [HttpPut] + public async Task>> UpdateUrlServers(TrojanUrlServers server) + { + var data = await _baseServicesUrlServers.Update(server); + return MessageModel>.Success("更新成功"); + } + /// + /// 添加Url服务器 + /// + /// + /// + [HttpPost] + public async Task>> AddUrlServers(TrojanUrlServers server) + { + var data = await _baseServicesUrlServers.Add(server); + return MessageModel>.Success("添加成功"); + } + private string GetSplice(TrojanServers item,string passwordshow) + { + if ("0".Equals(item.servertype)) + return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?allowinsecure=0&tfo=0&peer={(string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress)}#{item.servername}"; + else if ("1".Equals(item.servertype)) + return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?wspath={item.serverpath}&ws=1&peer={(string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress)}#{item.servername}"; + else + return $"servertype:({item.servertype})错误"; + } + private List GetSplice(List items, string passwordshow) + { + List ls = new List(); + foreach (var item in items) + { + ls.Add(GetSplice(item, passwordshow)); + } + return ls; + } + /// + /// 获取订阅数据 + /// + /// 链接密码 + /// 是否使用base64加密 + /// + [HttpGet] + [AllowAnonymous] + public async Task RSS(string id,bool isUseBase64=true) + { + StringBuilder sb = new StringBuilder(); + try + { + var user = (await _trojanUsersServices.Query(t => t.passwordshow == id)).FirstOrDefault(); + if (user == null) throw new Exception("用户不存在"); + var data = await _baseServicesServers.Query(t => (t.userid == user.id || t.userid <= 0) && t.serverenable); + if (data != null) + { + data = data.OrderBy(t => t.servername).ToList(); + foreach (var item in data) + { + sb.AppendLine(GetSplice(item, user.passwordshow)); + } + } + var cusData = await _baseServicesCusServers.Query(t=> (t.userid == user.id || t.userid <=0) && t.serverenable); + if (cusData != null) + { + cusData = cusData.OrderBy(t => t.servername).ToList(); + foreach (var item in cusData) + { + sb.AppendLine(item.serveraddress); + } + } + var urlData = await _baseServicesUrlServers.Query(t => (t.userid == user.id || t.userid <= 0) && t.serverenable); + if (urlData != null) + { + urlData = urlData.OrderBy(t => t.servername).ToList(); + foreach (var item in urlData) + { + try + { + var urlStrObj = await HttpHelper.GetAsync(item.serveraddress); + var lines = ""; + try + { + lines = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(urlStrObj)); + } + catch (Exception) + { + lines = urlStrObj; + } + finally + { + sb.AppendLine(lines); + } + } + catch (Exception ex) + { + sb.AppendLine($"trojan://xxxxxx@xxxxxx.xx:443?allowinsecure=0&tfo=0#{ex.Message}"); + } + } + } + } + catch (Exception ex) + { + sb.AppendLine($"trojan://xxxxxx@xxxxxx.xx:443?allowinsecure=0&tfo=0#{ex.Message}"); + } + if (isUseBase64) + { + return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sb.ToString())); + } + else{ + return sb.ToString(); + } + } + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/WeChatController.cs b/Blog.Core.Api/Controllers/WeChatController.cs index 4c4877f6..c215f563 100644 --- a/Blog.Core.Api/Controllers/WeChatController.cs +++ b/Blog.Core.Api/Controllers/WeChatController.cs @@ -146,6 +146,18 @@ public async Task> PushCardMsg(WeChatCardMs return await _weChatConfigServices.PushCardMsg(msg, pushUserIP); } /// + /// 推送卡片消息接口 + /// + /// 卡片消息对象 + /// + [HttpGet] + [AllowAnonymous] + public async Task> PushCardMsgGet([FromQuery] WeChatCardMsgDataDto msg) + { + string pushUserIP = $"{Request.HttpContext.Connection.RemoteIpAddress}:{Request.HttpContext.Connection.RemotePort}"; + return await _weChatConfigServices.PushCardMsg(msg, pushUserIP); + } + /// /// 推送文本消息 /// /// 消息对象 diff --git a/Blog.Core.Api/Dockerfile b/Blog.Core.Api/Dockerfile index 43618077..1eb0572a 100644 --- a/Blog.Core.Api/Dockerfile +++ b/Blog.Core.Api/Dockerfile @@ -3,7 +3,7 @@ #FROM swr.cn-south-1.myhuaweicloud.com/mcr/aspnet:5.0-alpine #FROM mcr.microsoft.com/dotnet/core/aspnet:5.0-buster-slim -FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN echo 'Asia/Shanghai' >/etc/timezone diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index 27daae98..de4a7a06 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -26,6 +26,7 @@ public interface IBaseServices where TEntity : class Task DeleteByIds(object[] ids); Task Update(TEntity model); + Task Update(List model); Task Update(TEntity entity, string where); Task Update(object operateAnonymousObjects); diff --git a/Blog.Core.IServices/ITrojanUsersServices.cs b/Blog.Core.IServices/ITrojanUsersServices.cs new file mode 100644 index 00000000..92bac1d8 --- /dev/null +++ b/Blog.Core.IServices/ITrojanUsersServices.cs @@ -0,0 +1,14 @@ +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; + +namespace Blog.Core.IServices +{ + /// + /// TrojanUsersServices + /// + public interface ITrojanUsersServices : IBaseServices + { + + } +} + diff --git a/Blog.Core.Model/Models/TrojanCusServers.cs b/Blog.Core.Model/Models/TrojanCusServers.cs new file mode 100644 index 00000000..03f02539 --- /dev/null +++ b/Blog.Core.Model/Models/TrojanCusServers.cs @@ -0,0 +1,26 @@ + +using System; +using System.Linq; +using System.Text; +using SqlSugar; + +namespace Blog.Core.Model.Models +{ + /// + ///users自定义服务器 + /// + [SugarTable("users_cus", "users自定义服务器")] + [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + public partial class TrojanCusServers + { + + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] + public int id { set; get; } + public int userid { get; set; } + public string servername { set; get; } + public string serveraddress { set; get; } + [SugarColumn(IsNullable = true)] + public string serverremark { get; set; } + public bool serverenable { get; set; } + } +} diff --git a/Blog.Core.Model/Models/TrojanDetails.cs b/Blog.Core.Model/Models/TrojanDetails.cs new file mode 100644 index 00000000..dda53d9b --- /dev/null +++ b/Blog.Core.Model/Models/TrojanDetails.cs @@ -0,0 +1,63 @@ + +//模板自动生成(请勿修改) +//作者:胡丁文 +using System; +using System.Linq; +using System.Text; +using SqlSugar; + +namespace Blog.Core.Model.Models +{ + /// + ///用户流量每月汇总表 + /// + [SugarTable("users_detail", "用户流量每月汇总表")] + [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + public partial class TrojanDetails + { + + /// + /// + /// + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public int id { get; set; } + + /// + /// + /// + public int userId { get; set; } + + /// + /// + /// + public DateTime calDate { get; set; } + + /// + /// + /// + public ulong download { get; set; } + + /// + /// + /// + public ulong upload { get; set; } + + /// + /// + /// + [SugarColumn(IsNullable = true)] + public int? CreateId { get; set; } + + /// + /// + /// + [SugarColumn(IsNullable = true)] + public string CreateBy { get; set; } + + /// + /// + /// + [SugarColumn(IsNullable = true)] + public DateTime? CreateTime { get; set; } + } +} diff --git a/Blog.Core.Model/Models/TrojanServers.cs b/Blog.Core.Model/Models/TrojanServers.cs new file mode 100644 index 00000000..d9d8275f --- /dev/null +++ b/Blog.Core.Model/Models/TrojanServers.cs @@ -0,0 +1,31 @@ + +using System; +using System.Linq; +using System.Text; +using SqlSugar; + +namespace Blog.Core.Model.Models +{ + /// + ///Trojan服务器 + /// + [SugarTable("servers", "Trojan服务器")] + [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + public partial class TrojanServers + { + + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] + public int id { set; get; } + public int userid { get; set; } + public string servername { set; get; } + public string serveraddress { set; get; } + public int serverport { get; set; } + [SugarColumn(IsNullable = true)] + public string serverremark { get; set; } + public bool serverenable { get; set; } + public string serverpeer { get; set; } + [SugarColumn(IsNullable = true)] + public string serverpath { get; set; } + public string servertype { get; set; } + } +} diff --git a/Blog.Core.Model/Models/TrojanUrlServers.cs b/Blog.Core.Model/Models/TrojanUrlServers.cs new file mode 100644 index 00000000..db48343a --- /dev/null +++ b/Blog.Core.Model/Models/TrojanUrlServers.cs @@ -0,0 +1,26 @@ + +using System; +using System.Linq; +using System.Text; +using SqlSugar; + +namespace Blog.Core.Model.Models +{ + /// + ///users自定义URL服务器 + /// + [SugarTable("users_url", "users自定义URL服务器")] + [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + public partial class TrojanUrlServers + { + + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] + public int id { set; get; } + public int userid { get; set; } + public string servername { set; get; } + public string serveraddress { set; get; } + [SugarColumn(IsNullable = true)] + public string serverremark { get; set; } + public bool serverenable { get; set; } + } +} diff --git a/Blog.Core.Model/Models/TrojanUsers.cs b/Blog.Core.Model/Models/TrojanUsers.cs new file mode 100644 index 00000000..796c044e --- /dev/null +++ b/Blog.Core.Model/Models/TrojanUsers.cs @@ -0,0 +1,39 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Blog.Core.Model.ViewModels; +using SqlSugar; + +namespace Blog.Core.Model.Models +{ + /// + ///Trojan用户 + /// + [SugarTable("users", "Trojan用户表")] + [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + public partial class TrojanUsers + { + + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] + public int id { set; get; } + public string username { set; get; } + public string password { set; get; } + public Int64 quota { set; get; } + public UInt64 download { set; get; } + public UInt64 upload { set; get; } + public string passwordshow { set; get; } + [SugarColumn(IsNullable = true)] + public int CreateId { get; set; } + [SugarColumn(IsNullable = true)] + public string CreateBy { get; set; } + [SugarColumn(IsNullable = true)] + public DateTime? CreateTime { get; set; } + /// + /// 历史流量记录 + /// + [SugarColumn(IsIgnore = true)] + public List useList { get; set; } + } +} diff --git a/Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs b/Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs new file mode 100644 index 00000000..c0efd347 --- /dev/null +++ b/Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Blog.Core.Model.ViewModels +{ + /// + /// 限制流量dto + /// 作者:胡丁文 + /// 时间:2020-4-27 16:57:07 + /// + public class TrojanLimitFlowDto + { + /// + /// 用户 + /// + public int[] users { get; set; } + /// + /// 流量(-1为无限,单位为最小单位byte) + /// + public Int64 quota { get; set; } + } +} diff --git a/Blog.Core.Model/ViewModels/TrojanServerDto.cs b/Blog.Core.Model/ViewModels/TrojanServerDto.cs new file mode 100644 index 00000000..89cb87e3 --- /dev/null +++ b/Blog.Core.Model/ViewModels/TrojanServerDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Model.ViewModels +{ + public class TrojanServerDto + { + public string name { get; set; } + public string value { get; set; } + } +} diff --git a/Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs b/Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs new file mode 100644 index 00000000..e83adfd5 --- /dev/null +++ b/Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Model.ViewModels +{ + /// + /// Trojan服务器拼接服务器和订阅地址 + /// + public class TrojanServerSpliceDto + { + /// + /// 普通订阅连接 + /// + public string normalApi { get; set; } + /// + /// clash订阅连接 + /// + public string clashApi { get; set; } + /// + /// 备用clash订阅连接 + /// + public string clashApiBackup { get; set; } + public List list { get; set; } = new List(); + } +} diff --git a/Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs b/Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs new file mode 100644 index 00000000..7746b105 --- /dev/null +++ b/Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Model.ViewModels +{ + /// + /// Trojan用户流量统计分组 + /// + public class TrojanUseDetailDto + { + /// + /// 用户ID + /// + public int userId { get; set; } + /// + /// 月度 + /// + public string moth { get; set; } + /// + /// 上传流量 + /// + public decimal up { get; set; } + /// + /// 下载流量 + /// + public decimal down { get; set; } + /// + /// 下载流量 + /// + public decimal total { get { return up + down; } } + } +} diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 82900689..ead9f9af 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -144,6 +144,19 @@ public async Task Update(TEntity entity) //这种方式会以主键为条件 return await _db.Updateable(entity).ExecuteCommandHasChangeAsync(); } + /// + /// 更新实体数据 + /// + /// 博文实体类 + /// + public async Task Update(List entity) + { + ////这种方式会以主键为条件 + //var i = await Task.Run(() => _db.Updateable(entity).ExecuteCommand()); + //return i > 0; + //这种方式会以主键为条件 + return await _db.Updateable(entity).ExecuteCommandHasChangeAsync(); + } public async Task Update(TEntity entity, string where) { diff --git a/Blog.Core.Repository/BASE/IBaseRepository.cs b/Blog.Core.Repository/BASE/IBaseRepository.cs index 29783505..9b72a00d 100644 --- a/Blog.Core.Repository/BASE/IBaseRepository.cs +++ b/Blog.Core.Repository/BASE/IBaseRepository.cs @@ -69,6 +69,12 @@ public interface IBaseRepository where TEntity : class /// /// Task Update(TEntity model); + /// + /// 更新model + /// + /// + /// + Task Update(List model); /// /// 根据model,更新,带where条件 diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index ec685be2..232c6fae 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -76,6 +76,15 @@ public async Task Update(TEntity entity) { return await BaseDal.Update(entity); } + /// + /// 更新实体数据 + /// + /// 博文实体类 + /// + public async Task Update(List entity) + { + return await BaseDal.Update(entity); + } public async Task Update(TEntity entity, string where) { return await BaseDal.Update(entity, where); diff --git a/Blog.Core.Services/TrojanUsersServices.cs b/Blog.Core.Services/TrojanUsersServices.cs new file mode 100644 index 00000000..f42f51d9 --- /dev/null +++ b/Blog.Core.Services/TrojanUsersServices.cs @@ -0,0 +1,18 @@ +using Blog.Core.Common; +using Blog.Core.IRepository.Base; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Services.BASE; +using System.Linq; +using System.Threading.Tasks; + +namespace Blog.Core.Services +{ + /// + /// TrojanUsersServices + /// + public class TrojanUsersServices : BaseServices, ITrojanUsersServices + { + + } +} diff --git a/Blog.Core.Tasks/Blog.Core.Tasks.csproj b/Blog.Core.Tasks/Blog.Core.Tasks.csproj index 5e173559..bb748f64 100644 --- a/Blog.Core.Tasks/Blog.Core.Tasks.csproj +++ b/Blog.Core.Tasks/Blog.Core.Tasks.csproj @@ -11,6 +11,7 @@ + diff --git a/Blog.Core.Tasks/HostedService/Job1TimedService.cs b/Blog.Core.Tasks/HostedService/Job1TimedService.cs new file mode 100644 index 00000000..9777affc --- /dev/null +++ b/Blog.Core.Tasks/HostedService/Job1TimedService.cs @@ -0,0 +1,60 @@ +using Blog.Core.Common; +using Blog.Core.IServices; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Blog.Core.Tasks +{ + public class Job1TimedService : IHostedService, IDisposable + { + private Timer _timer; + private readonly IBlogArticleServices _blogArticleServices; + + // 这里可以注入 + public Job1TimedService(IBlogArticleServices blogArticleServices) + { + _blogArticleServices = blogArticleServices; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Job 1 is starting."); + + _timer = new Timer(DoWork, null, TimeSpan.Zero, + TimeSpan.FromSeconds(60 * 60));//一个小时 + + return Task.CompletedTask; + } + + private void DoWork(object state) + { + try + { + var model = _blogArticleServices.GetBlogDetails(1).Result; + Console.WriteLine($"Job 1 启动成功,获取id=1的博客title为:{model?.btitle}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error:{ex.Message}"); + } + + ConsoleHelper.WriteSuccessLine($"Job 1: {DateTime.Now}"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Job 1 is stopping."); + + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} diff --git a/Blog.Core.Tasks/HostedService/Job2TimedService.cs b/Blog.Core.Tasks/HostedService/Job2TimedService.cs new file mode 100644 index 00000000..ee7f2c5c --- /dev/null +++ b/Blog.Core.Tasks/HostedService/Job2TimedService.cs @@ -0,0 +1,47 @@ +using Blog.Core.Common; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Blog.Core.Tasks +{ + public class Job2TimedService : IHostedService, IDisposable + { + private Timer _timer; + + // 这里可以注入 + public Job2TimedService() + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Job 2 is starting."); + + _timer = new Timer(DoWork, null, TimeSpan.Zero, + TimeSpan.FromSeconds(60 * 60 * 2));//两个小时 + + return Task.CompletedTask; + } + + private void DoWork(object state) + { + ConsoleHelper.WriteWarningLine($"Job 2: {DateTime.Now}"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Job 2 is stopping."); + + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs new file mode 100644 index 00000000..f9640749 --- /dev/null +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs @@ -0,0 +1,79 @@ +using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.IServices; +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +/// +/// 这里要注意下,命名空间和程序集是一样的,不然反射不到(任务类要去JobSetup添加注入) +/// +namespace Blog.Core.Tasks +{ + public class Job_Trojan_Quartz : JobBase, IJob + { + private readonly IUnitOfWorkManage _unitOfWorkManage; + public IBaseServices_DetailServices; + private readonly ITrojanUsersServices _TrojanUsers; + private readonly ILogger _logger; + + public Job_Trojan_Quartz(IUnitOfWorkManage unitOfWorkManage, IBaseServices iusers_DetailServices, ITrojanUsersServices trojanUsers, ITasksQzServices tasksQzServices, ILogger logger) + { + _tasksQzServices = tasksQzServices; + _unitOfWorkManage = unitOfWorkManage; + _DetailServices = iusers_DetailServices; + _TrojanUsers = trojanUsers; + _logger = logger; + } + public async Task Execute(IJobExecutionContext context) + { + //var param = context.MergedJobDataMap; + // 可以直接获取 JobDetail 的值 + var jobKey = context.JobDetail.Key; + var jobId = jobKey.Name; + var executeLog = await ExecuteJob(context, async () => await Run(context, jobId.ObjToInt())); + + } + public async Task Run(IJobExecutionContext context, int jobid) + { + if (jobid > 0) + { + try + { + //获取每月用户的数据 + _unitOfWorkManage.BeginTran(); + var now = DateTime.Now.AddMonths(-1); + + var list = await _TrojanUsers.Query(); + List ls = new List(); + foreach (var us in list) + { + TrojanDetails u = new TrojanDetails(); + u.calDate = now; + u.userId = us.id; + u.download = us.download; + u.upload = us.upload; + //清零 + us.download = 0; + us.upload = 0; + ls.Add(u); + } + await _TrojanUsers.Update(list); + await _DetailServices.Add(ls); + _unitOfWorkManage.CommitTran(); + } + catch (Exception) + { + _unitOfWorkManage.RollbackTran(); + throw; + } + } + } + } + + + +} diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs new file mode 100644 index 00000000..666a8d41 --- /dev/null +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs @@ -0,0 +1,51 @@ +using Blog.Core.Common.Helper; +using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.IServices; +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +/// +/// 这里要注意下,命名空间和程序集是一样的,不然反射不到(任务类要去JobSetup添加注入) +/// +namespace Blog.Core.Tasks +{ + public class Job_URL_Quartz : JobBase, IJob + { + private readonly ILogger _logger; + + public Job_URL_Quartz(ITasksQzServices tasksQzServices, ILogger logger) + { + _tasksQzServices = tasksQzServices; + _logger = logger; + } + public async Task Execute(IJobExecutionContext context) + { + // 可以直接获取 JobDetail 的值 + var jobKey = context.JobDetail.Key; + var jobId = jobKey.Name; + var executeLog = await ExecuteJob(context, async () => await Run(context, jobId.ObjToInt())); + + } + public async Task Run(IJobExecutionContext context, int jobid) + { + if (jobid > 0) + { + JobDataMap data = context.JobDetail.JobDataMap; + string pars = data.GetString("JobParam"); + if (!string.IsNullOrWhiteSpace(pars)) + { + var log = await HttpHelper.GetAsync(pars); + _logger.LogInformation(log); + } + } + } + } + + + +} From b36db59e1abdd2ebece8caf034ae6238a58885d7 Mon Sep 17 00:00:00 2001 From: HuiJiOnGit <40553940+HuiJiOnGit@users.noreply.github.com> Date: Mon, 13 Feb 2023 13:47:18 +0800 Subject: [PATCH 123/289] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=90=AF=E5=8A=A8=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=88=B0`HostedService`=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Program.cs | 30 ++------ Blog.Core.Api/Startup.cs | 39 ++++------ .../HostedService/ConsulHostedService.cs | 71 +++++++++++++++++++ .../HostedService/EventBusHostedService.cs | 45 ++++++++++++ .../HostedService/QuartzJobHostedService.cs | 67 +++++++++++++++++ .../HostedService/SeedDataHostedService.cs | 55 ++++++++++++++ .../Middlewares/ConsulMiddleware.cs | 52 -------------- .../Middlewares/QuartzJobMiddleware.cs | 51 ------------- .../Middlewares/SeedDataMiddleware.cs | 33 --------- .../ServiceExtensions/EventBusSetup.cs | 21 ++---- .../InitializationHostServiceSetup.cs | 24 +++++++ 11 files changed, 287 insertions(+), 201 deletions(-) create mode 100644 Blog.Core.Extensions/HostedService/ConsulHostedService.cs create mode 100644 Blog.Core.Extensions/HostedService/EventBusHostedService.cs create mode 100644 Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs create mode 100644 Blog.Core.Extensions/HostedService/SeedDataHostedService.cs delete mode 100644 Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs delete mode 100644 Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs delete mode 100644 Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 8ab4b93d..d7619781 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -1,19 +1,17 @@ - -// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 +// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Text; using Autofac; using Autofac.Extensions.DependencyInjection; using Blog.Core; using Blog.Core.Common; using Blog.Core.Common.LogHelper; -using Blog.Core.Common.Seed; using Blog.Core.Extensions; using Blog.Core.Extensions.Apollo; using Blog.Core.Extensions.Middlewares; using Blog.Core.Filter; using Blog.Core.Hubs; -using Blog.Core.IServices; -using Blog.Core.Tasks; -using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -21,9 +19,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Text; var builder = WebApplication.CreateBuilder(args); @@ -49,7 +44,6 @@ config.AddConfigurationApollo("appsettings.apollo.json"); }); - // 2、配置服务 builder.Services.AddSingleton(new AppSettings(builder.Configuration)); builder.Services.AddSingleton(new LogLock(builder.Environment.ContentRootPath)); @@ -79,7 +73,7 @@ builder.Services.AddKafkaSetup(builder.Configuration); builder.Services.AddEventBusSetup(); builder.Services.AddNacosSetup(builder.Configuration); - +builder.Services.AddInitializationHostServiceSetup(); builder.Services.AddAuthorizationSetup(); if (Permissions.IsUseIds4 || Permissions.IsUseAuthing) { @@ -129,7 +123,6 @@ builder.Services.Replace(ServiceDescriptor.Transient()); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - // 3、配置中间件 var app = builder.Build(); @@ -182,16 +175,5 @@ endpoints.MapHub("/api2/chatHub"); }); - -var scope = app.Services.GetRequiredService().CreateScope(); -var myContext = scope.ServiceProvider.GetRequiredService(); -var tasksQzServices = scope.ServiceProvider.GetRequiredService(); -var schedulerCenter = scope.ServiceProvider.GetRequiredService(); -var lifetime = scope.ServiceProvider.GetRequiredService(); -app.UseSeedDataMiddle(myContext, builder.Environment.WebRootPath); -app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); -app.UseConsulMiddle(builder.Configuration, lifetime); -app.ConfigureEventBus(); - // 4、运行 -app.Run(); +app.Run(); \ No newline at end of file diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index b341819e..bc1630f1 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -1,34 +1,28 @@ -using Autofac; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Text; +using Autofac; using Blog.Core.Common; using Blog.Core.Common.LogHelper; using Blog.Core.Common.Seed; using Blog.Core.Extensions; +using Blog.Core.Extensions.Middlewares; using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; using Blog.Core.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Text; -using Blog.Core.Extensions.Middlewares; namespace Blog.Core { public class Startup { - private IServiceCollection _services; public Startup(IConfiguration configuration, IWebHostEnvironment env) @@ -75,7 +69,7 @@ public void ConfigureServices(IServiceCollection services) services.AddEventBusSetup(); services.AddNacosSetup(Configuration); - + services.AddInitializationHostServiceSetup(); // 授权+认证 (jwt or ids4) services.AddAuthorizationSetup(); if (Permissions.IsUseIds4) @@ -95,7 +89,7 @@ public void ConfigureServices(IServiceCollection services) services.Configure(x => x.AllowSynchronousIO = true) .Configure(x => x.AllowSynchronousIO = true); - + services.AddDistributedMemoryCache(); services.AddSession(); services.AddHttpPollySetup(); @@ -134,7 +128,7 @@ public void ConfigureServices(IServiceCollection services) services.Replace(ServiceDescriptor.Transient()); _services = services; - //支持编码大全 例如:支持 System.Text.Encoding.GetEncoding("GB2312") System.Text.Encoding.GetEncoding("GB18030") + //支持编码大全 例如:支持 System.Text.Encoding.GetEncoding("GB2312") System.Text.Encoding.GetEncoding("GB18030") Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } @@ -150,11 +144,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex { // Ip限流,尽量放管道外层 app.UseIpLimitMiddle(); - // 记录请求与返回数据 + // 记录请求与返回数据 app.UseRequestResponseLogMiddle(); // 用户访问记录(必须放到外层,不然如果遇到异常,会报错,因为不能返回流) app.UseRecordAccessLogsMiddle(); - // signalr + // signalr app.UseSignalRSendMiddle(); // 记录ip请求 app.UseIpLogMiddle(); @@ -214,7 +208,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex // 开启异常中间件,要放到最后 //app.UseExceptionHandlerMidd(); - app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( @@ -225,15 +218,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex }); // 生成种子数据 - app.UseSeedDataMiddle(myContext, Env.WebRootPath); + //app.UseSeedDataMiddle(myContext, Env.WebRootPath); // 开启QuartzNetJob调度服务 - app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); + //app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); // 服务注册 - app.UseConsulMiddle(Configuration, lifetime); + //app.UseConsulMiddle(Configuration, lifetime); // 事件总线,订阅服务 - app.ConfigureEventBus(); - + //app.ConfigureEventBus(); } - } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/ConsulHostedService.cs b/Blog.Core.Extensions/HostedService/ConsulHostedService.cs new file mode 100644 index 00000000..df866e6a --- /dev/null +++ b/Blog.Core.Extensions/HostedService/ConsulHostedService.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Consul; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions.HostedService; + +public class ConsulHostedService : IHostedService +{ + private readonly IConfiguration _configuration; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILogger _logger; + + public ConsulHostedService(IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) + { + _configuration = configuration; + _hostApplicationLifetime = hostApplicationLifetime; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start Consul Service!"); + await DoWork(); + } + + public async Task DoWork() + { + if (_configuration["Middleware:Consul:Enabled"].ObjToBool()) + { + var consulClient = new ConsulClient(c => + { + //consul地址 + c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]); + }); + + var registration = new AgentServiceRegistration() + { + ID = Guid.NewGuid().ToString(),//服务实例唯一标识 + Name = _configuration["ConsulSetting:ServiceName"],//服务名 + Address = _configuration["ConsulSetting:ServiceIP"], //服务IP + Port = int.Parse(_configuration["ConsulSetting:ServicePort"]),//服务端口 + Check = new AgentServiceCheck() + { + DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册 + Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔 + HTTP = $"http://{_configuration["ConsulSetting:ServiceIP"]}:{_configuration["ConsulSetting:ServicePort"]}{_configuration["ConsulSetting:ServiceHealthCheck"]}",//健康检查地址 + Timeout = TimeSpan.FromSeconds(5)//超时时间 + } + }; + + //服务注册 + await consulClient.Agent.ServiceRegister(registration); + + //应用程序终止时,取消注册 + _hostApplicationLifetime.ApplicationStopping.Register(async () => + { + await consulClient.Agent.ServiceDeregister(registration.ID); + }); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop Consul Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/EventBusHostedService.cs b/Blog.Core.Extensions/HostedService/EventBusHostedService.cs new file mode 100644 index 00000000..7f18ed19 --- /dev/null +++ b/Blog.Core.Extensions/HostedService/EventBusHostedService.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.EventBus; +using Blog.Core.EventBus.EventHandling; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions.HostedService; + +public class EventBusHostedService : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public EventBusHostedService(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start EventBus Service!"); + await DoWork(); + } + + private Task DoWork() + { + if (AppSettings.app(new string[] { "EventBus", "Enabled" }).ObjToBool()) + { + var eventBus = _serviceProvider.GetRequiredService(); + eventBus.Subscribe(); + } + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop EventBus Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs b/Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs new file mode 100644 index 00000000..d8f4d602 --- /dev/null +++ b/Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.IServices; +using Blog.Core.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions.HostedService; + +public class QuartzJobHostedService : IHostedService +{ + private readonly ITasksQzServices _tasksQzServices; + private readonly ISchedulerCenter _schedulerCenter; + private readonly ILogger _logger; + + public QuartzJobHostedService(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, ILogger logger) + { + _tasksQzServices = tasksQzServices; + _schedulerCenter = schedulerCenter; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start QuartzJob Service!"); + await DoWork(); + } + + private async Task DoWork() + { + try + { + if (AppSettings.app("Middleware", "QuartzNetJob", "Enabled").ObjToBool()) + { + var allQzServices = await _tasksQzServices.Query(); + foreach (var item in allQzServices) + { + if (item.IsStart) + { + var result = await _schedulerCenter.AddScheduleJobAsync(item); + if (result.success) + { + Console.WriteLine($"QuartzNetJob{item.Name}启动成功!"); + } + else + { + Console.WriteLine($"QuartzNetJob{item.Name}启动失败!错误信息:{result.msg}"); + } + } + } + } + } + catch (Exception e) + { + _logger.LogError(e, "An error was reported when starting the job service."); + throw; + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop QuartzJob Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs new file mode 100644 index 00000000..beef5a75 --- /dev/null +++ b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.Common.Seed; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions; + +public sealed class SeedDataHostedService : IHostedService +{ + private readonly MyContext _myContext; + private readonly ILogger _logger; + private readonly string _webRootPath; + + public SeedDataHostedService( + MyContext myContext, + IWebHostEnvironment webHostEnvironment, + ILogger logger) + { + _myContext = myContext; + _logger = logger; + _webRootPath = webHostEnvironment.WebRootPath; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start Initialization Db Seed Service!"); + await DoWork(); + } + + private async Task DoWork() + { + try + { + if (AppSettings.app("AppSettings", "SeedDBEnabled").ObjToBool() || AppSettings.app("AppSettings", "SeedDBDataEnabled").ObjToBool()) + { + await DBSeed.SeedAsync(_myContext, _webRootPath); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured seeding the Database."); + throw; + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop Initialization Db Seed Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs b/Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs deleted file mode 100644 index c1aae8cc..00000000 --- a/Blog.Core.Extensions/Middlewares/ConsulMiddleware.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Consul; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; - -namespace Blog.Core.Extensions.Middlewares -{ - /// - /// Consul 注册服务 - /// - public static class ConsulMiddleware - { - public static IApplicationBuilder UseConsulMiddle(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime) - { - if (configuration["Middleware:Consul:Enabled"].ObjToBool()) - { - var consulClient = new ConsulClient(c => - { - //consul地址 - c.Address = new Uri(configuration["ConsulSetting:ConsulAddress"]); - }); - - var registration = new AgentServiceRegistration() - { - ID = Guid.NewGuid().ToString(),//服务实例唯一标识 - Name = configuration["ConsulSetting:ServiceName"],//服务名 - Address = configuration["ConsulSetting:ServiceIP"], //服务IP - Port = int.Parse(configuration["ConsulSetting:ServicePort"]),//服务端口 - Check = new AgentServiceCheck() - { - DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册 - Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔 - HTTP = $"http://{configuration["ConsulSetting:ServiceIP"]}:{configuration["ConsulSetting:ServicePort"]}{configuration["ConsulSetting:ServiceHealthCheck"]}",//健康检查地址 - Timeout = TimeSpan.FromSeconds(5)//超时时间 - } - }; - - //服务注册 - consulClient.Agent.ServiceRegister(registration).Wait(); - - //应用程序终止时,取消注册 - lifetime.ApplicationStopping.Register(() => - { - consulClient.Agent.ServiceDeregister(registration.ID).Wait(); - }); - - } - return app; - } - } -} diff --git a/Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs b/Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs deleted file mode 100644 index 69c52de3..00000000 --- a/Blog.Core.Extensions/Middlewares/QuartzJobMiddleware.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Blog.Core.Common; -using Blog.Core.IServices; -using Blog.Core.Tasks; -using log4net; -using Microsoft.AspNetCore.Builder; - -namespace Blog.Core.Extensions.Middlewares -{ - /// - /// Quartz 启动服务 - /// - public static class QuartzJobMiddleware - { - private static readonly ILog Log = LogManager.GetLogger(typeof(QuartzJobMiddleware)); - public static void UseQuartzJobMiddleware(this IApplicationBuilder app, ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - - try - { - if (AppSettings.app("Middleware", "QuartzNetJob", "Enabled").ObjToBool()) - { - - var allQzServices = tasksQzServices.Query().Result; - foreach (var item in allQzServices) - { - if (item.IsStart) - { - var result = schedulerCenter.AddScheduleJobAsync(item).Result; - if (result.success) - { - Console.WriteLine($"QuartzNetJob{item.Name}启动成功!"); - } - else - { - Console.WriteLine($"QuartzNetJob{item.Name}启动失败!错误信息:{result.msg}"); - } - } - } - - } - } - catch (Exception e) - { - Log.Error($"An error was reported when starting the job service.\n{e.Message}"); - throw; - } - } - } -} diff --git a/Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs b/Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs deleted file mode 100644 index ad944bfd..00000000 --- a/Blog.Core.Extensions/Middlewares/SeedDataMiddleware.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Blog.Core.Common; -using Blog.Core.Common.Seed; -using log4net; -using Microsoft.AspNetCore.Builder; - -namespace Blog.Core.Extensions.Middlewares -{ - /// - /// 生成种子数据中间件服务 - /// - public static class SeedDataMiddleware - { - private static readonly ILog Log = LogManager.GetLogger(typeof(SeedDataMiddleware)); - public static void UseSeedDataMiddle(this IApplicationBuilder app, MyContext myContext, string webRootPath) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - - try - { - if (AppSettings.app("AppSettings", "SeedDBEnabled").ObjToBool() || AppSettings.app("AppSettings", "SeedDBDataEnabled").ObjToBool()) - { - DBSeed.SeedAsync(myContext, webRootPath).Wait(); - } - } - catch (Exception e) - { - Log.Error($"Error occured seeding the Database.\n{e.Message}"); - throw; - } - } - } -} diff --git a/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs b/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs index 1aae5ed5..4ae98830 100644 --- a/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs @@ -1,11 +1,9 @@ -using Autofac; +using System; +using Autofac; using Blog.Core.Common; using Blog.Core.EventBus; -using Blog.Core.EventBus.EventHandling; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System; namespace Blog.Core.Extensions { @@ -43,23 +41,12 @@ public static void AddEventBusSetup(this IServiceCollection services) return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); }); } - if(AppSettings.app(new string[] { "Kafka", "Enabled" }).ObjToBool()) + if (AppSettings.app(new string[] { "Kafka", "Enabled" }).ObjToBool()) { services.AddHostedService(); services.AddSingleton(); } } } - - - public static void ConfigureEventBus(this IApplicationBuilder app) - { - if (AppSettings.app(new string[] { "EventBus", "Enabled" }).ObjToBool()) - { - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe(); - } - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs b/Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs new file mode 100644 index 00000000..fea6a5ad --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs @@ -0,0 +1,24 @@ +using System; +using Blog.Core.Extensions.HostedService; +using Microsoft.Extensions.DependencyInjection; + +namespace Blog.Core.Extensions; + +public static class InitializationHostServiceSetup +{ + /// + /// 应用初始化服务注入 + /// + /// + public static void AddInitializationHostServiceSetup(this IServiceCollection services) + { + if (services is null) + { + ArgumentNullException.ThrowIfNull(nameof(services)); + } + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + } +} \ No newline at end of file From 71e0f4a7fe99520614a43c6c536ffcc5a24db132 Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Mon, 13 Feb 2023 19:23:16 +0800 Subject: [PATCH 124/289] =?UTF-8?q?=E2=9C=A8=20=E5=AE=8C=E5=96=84=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8A=A8=E6=80=81=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.支持一对多导航属性 2.支持混合查询 测试代码看 DynamicLambdaTest 每次修改,都会验证通过测试 --- Blog.Core.Api/Blog.Core.Model.xml | 10 ++ .../Extensions/ExpressionExtensions.cs | 13 ++- Blog.Core.Common/Helper/DynamicLinqFactory.cs | 100 +++++++++++++++--- .../Helper/GenericTypeExtensions.cs | 56 ++++++++++ Blog.Core.Model/Models/BlogArticle.cs | 14 ++- Blog.Core.Model/Models/BlogArticleComment.cs | 19 ++++ Blog.Core.Model/Models/sysUserInfo.cs | 2 +- .../Common_Test/DynamicLambdaTest.cs | 54 +++++++++- .../DependencyInjection/DI_Test.cs | 35 +++--- 9 files changed, 259 insertions(+), 44 deletions(-) create mode 100644 Blog.Core.Common/Helper/GenericTypeExtensions.cs create mode 100644 Blog.Core.Model/Models/BlogArticleComment.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 36975d11..f2b73257 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -301,6 +301,16 @@ 逻辑删除 + + + 评论 + + + + + 博客文章 评论 + + 部门表 diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions.cs b/Blog.Core.Common/Extensions/ExpressionExtensions.cs index 7f9e69e5..4058b95a 100644 --- a/Blog.Core.Common/Extensions/ExpressionExtensions.cs +++ b/Blog.Core.Common/Extensions/ExpressionExtensions.cs @@ -151,6 +151,16 @@ public static Expression Contains(this Expression left, Expression right) return left.Call("Contains", right); } + public static Expression StartContains(this Expression left, Expression right) + { + return left.Call("StartsWith", right); + } + + public static Expression EndContains(this Expression left, Expression right) + { + return left.Call("EndsWith", right); + } + /// /// > /// @@ -201,5 +211,4 @@ public static Expression NotEqual(this Expression left, Expression right) #endregion } - -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/DynamicLinqFactory.cs b/Blog.Core.Common/Helper/DynamicLinqFactory.cs index 43fdd7e3..7d45bbd2 100644 --- a/Blog.Core.Common/Helper/DynamicLinqFactory.cs +++ b/Blog.Core.Common/Helper/DynamicLinqFactory.cs @@ -1,15 +1,11 @@ -using Microsoft.AspNetCore.Http; -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; -using System.Net.Http; -using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Blog.Core.Common.Helper { @@ -20,10 +16,10 @@ namespace Blog.Core.Common.Helper /// public static class DynamicLinqFactory { - private static readonly Dictionary _operatingSystems = new(); + private static readonly Dictionary _operatingSystems = new Dictionary(); public static Dictionary OperatingSystems => GetOperationSymbol(); - private static readonly Dictionary _linkSymbols = new(); + private static readonly Dictionary _linkSymbols = new Dictionary(); public static Dictionary LinkSymbols => GetLinkSymbol(); /// @@ -70,11 +66,16 @@ public static Expression ExpressionStudio(Expression left, DynamicLinqHelper Dyn var properties = DynamicLinq.Left.Split('.'); - // 从1开始,是不想用自定义种子,外层种子已经定义好了 - // 暂时也不会有多个自定义种子,先这样 - for (var i = 0; i < properties.Length; i++) + int index = 0; + foreach (var t in properties) { - mainExpression = mainExpression.Property(properties[i]); + if (mainExpression.Type.HasImplementedRawGeneric(typeof(IEnumerable<>))) + { + return ExpressionStudioEnumerable(left, mainExpression, DynamicLinq.Clone(), properties.Skip(index).ToArray()); + } + + mainExpression = mainExpression.Property(t); + index++; } left = left == null @@ -85,6 +86,32 @@ public static Expression ExpressionStudio(Expression left, DynamicLinqHelper Dyn return left; } + public static Expression ExpressionStudioEnumerable(Expression left, Expression property, DynamicLinqHelper dynamicLinq, string[] properties) + { + var realType = property.Type.GenericTypeArguments[0]; + + var parameter = Expression.Parameter(realType, "z"); + Expression mainExpression = property; + if (!properties.Any()) + { + throw new ApplicationException("条件表达式错误,属性为集合时,需要明确具体属性"); + } + + dynamicLinq.Left = string.Join(".", properties); + mainExpression = ExpressionStudio(null, dynamicLinq, parameter); + + var lambda = Expression.Lambda(mainExpression, parameter); + + mainExpression = Expression.Call(typeof(Enumerable), "Any", new[] { realType }, property, lambda); + + left = left == null + ? mainExpression + : ChangeLinkSymbol(dynamicLinq.LinkSymbol, left, mainExpression); + + return left; + } + + /// /// 将字符串装换成动态帮助类(内含递归) /// @@ -132,6 +159,7 @@ public static List SplitOperationSymbol(string str) { var outList = new List(); var tokens = Regex.Matches(FormatString(str), _pattern, RegexOptions.Compiled) + .Cast() .Select(m => m.Groups[1].Value.Trim()) .ToList(); @@ -139,7 +167,7 @@ public static List SplitOperationSymbol(string str) int lastOperatingSymbolIndex = -1; for (int i = tokens.Count - 1; i >= 0; i--) { - var token = tokens[i]; + var token = tokens[i].ToLower(); if (OperatingSystems.ContainsKey(token)) { @@ -189,6 +217,7 @@ public static List SplitOperationSymbol(string str) } } + outList.Reverse(); return outList; } @@ -259,7 +288,7 @@ public static Dictionary GetOperationSymbol() { foreach (var name in attr.Name.Split(';')) { - _operatingSystems.Add(name, (OperationSymbol)item.GetValue(null)); + _operatingSystems.Add(name.ToLower(), (OperationSymbol)item.GetValue(null)); } } } @@ -353,7 +382,14 @@ public static string FormatString(string str) public static readonly string _pattern = @"\s*(" + string.Join("|", new string[] { // operators and punctuation that are longer than one char: longest first - string.Join("|", new[] { "||", "&&", "==", "!=", "<=", ">=", "like", "contains" }.Select(Regex.Escape)), + string.Join("|", new[] + { + "||", "&&", "==", "!=", "<=", ">=", + "in", + "like", "contains", "%=", + "startslike", "startscontains", "%>", + "endlike", "endcontains", "%<", + }.Select(Regex.Escape)), @"""(?:\\.|[^""])*""", // string @"\d+(?:\.\d+)?", // number with optional decimal part @"\w+", // word @@ -365,7 +401,7 @@ public static string FormatString(string str) /// public static OperationSymbol ChangeOperationSymbol(string str) { - switch (str) + switch (str.ToLower()) { case "<": return OperationSymbol.LessThan; @@ -382,7 +418,16 @@ public static OperationSymbol ChangeOperationSymbol(string str) return OperationSymbol.NotEqual; case "contains": case "like": + case "%=": return OperationSymbol.Contains; + case "startslike": + case "startscontains": + case "%>": + return OperationSymbol.StartsContains; + case "endlike": + case "endcontains": + case "%<": + return OperationSymbol.EndContains; } throw new Exception("OperationSymbol IS NULL"); @@ -451,6 +496,10 @@ public static Expression ChangeOperationSymbol(OperationSymbol symbol, Expressio return key.NotEqual(Expression.Constant(newTypeRight)); case OperationSymbol.Contains: return key.Contains(Expression.Constant(newTypeRight)); + case OperationSymbol.StartsContains: + return key.StartContains(Expression.Constant(newTypeRight)); + case OperationSymbol.EndContains: + return key.EndContains(Expression.Constant(newTypeRight)); case OperationSymbol.In: var contains = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) .Single(x => x.Name == "Contains" && x.GetParameters().Length == 2) @@ -480,6 +529,17 @@ public class DynamicLinqHelper [Display(Name = "连接符")] public LinkSymbol LinkSymbol { get; set; } + + public DynamicLinqHelper Clone() + { + return new DynamicLinqHelper() + { + Left = this.Left, + Right = this.Right, + OperationSymbol = this.OperationSymbol, + LinkSymbol = this.LinkSymbol, + }; + } } /// @@ -504,9 +564,15 @@ public enum OperationSymbol [Display(Name = "in")] In, - [Display(Name = "like;contains")] + [Display(Name = "like;contains;%=")] Contains, + [Display(Name = "StartsLike;StartsContains;%>")] + StartsContains, + + [Display(Name = "EndLike;EndContains;%<")] + EndContains, + [Display(Name = ">")] GreaterThan, @@ -528,7 +594,7 @@ public enum OperationSymbol #endregion - + /// /// Queryable扩展 /// diff --git a/Blog.Core.Common/Helper/GenericTypeExtensions.cs b/Blog.Core.Common/Helper/GenericTypeExtensions.cs new file mode 100644 index 00000000..aa095e2a --- /dev/null +++ b/Blog.Core.Common/Helper/GenericTypeExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; + +namespace Blog.Core.Common.Helper +{ + public static class GenericTypeExtensions + { + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + public static bool HasImplementedRawGeneric(this Type type, Type generic) + { + // 检查接口类型 + var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + if (isTheRawGenericType) return true; + + // 检查类型 + while (type != null && type != typeof(object)) + { + isTheRawGenericType = IsTheRawGenericType(type); + if (isTheRawGenericType) return true; + type = type.BaseType; + } + + return false; + + // 判断逻辑 + bool IsTheRawGenericType(Type t) => generic == (t.IsGenericType ? t.GetGenericTypeDefinition() : t); + } + + public static string GetGenericTypeName(this Type type) + { + var typeName = string.Empty; + + if (type.IsGenericType) + { + var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); + typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; + } + else + { + typeName = type.Name; + } + + return typeName; + } + + public static string GetGenericTypeName(this object @object) + { + return @object.GetType().GetGenericTypeName(); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/BlogArticle.cs b/Blog.Core.Model/Models/BlogArticle.cs index 52176190..66f05bdf 100644 --- a/Blog.Core.Model/Models/BlogArticle.cs +++ b/Blog.Core.Model/Models/BlogArticle.cs @@ -1,5 +1,6 @@ using SqlSugar; using System; +using System.Collections.Generic; namespace Blog.Core.Model.Models { @@ -14,12 +15,16 @@ public class BlogArticle /// 这里之所以没用RootEntity,是想保持和之前的数据库一致,主键是bID,不是Id [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] public int bID { get; set; } + /// /// 创建人 /// [SugarColumn(Length = 600, IsNullable = true)] public string bsubmitter { get; set; } + [Navigate(NavigateType.OneToOne, nameof(bsubmitter))] + public SysUserInfo User { get; set; } + /// /// 标题blog /// @@ -57,6 +62,7 @@ public class BlogArticle /// 创建时间 /// public System.DateTime bCreateTime { get; set; } + /// /// 备注 /// @@ -69,5 +75,11 @@ public class BlogArticle [SugarColumn(IsNullable = true)] public bool? IsDeleted { get; set; } + + /// + /// 评论 + /// + [Navigate(NavigateType.OneToMany, nameof(BlogArticleComment.bID))] + public List Comments { get; set; } } -} +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/BlogArticleComment.cs b/Blog.Core.Model/Models/BlogArticleComment.cs new file mode 100644 index 00000000..08010863 --- /dev/null +++ b/Blog.Core.Model/Models/BlogArticleComment.cs @@ -0,0 +1,19 @@ +using SqlSugar; + +namespace Blog.Core.Model.Models; + +/// +/// 博客文章 评论 +/// +public class BlogArticleComment : RootEntityTkey +{ + public int bID { get; set; } + + public string Comment { get; set; } + + + public string UserId { get; set; } + + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUserInfo User { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index c60e4bce..455f6e9e 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -8,7 +8,7 @@ namespace Blog.Core.Model.Models /// 用户信息表 /// //[SugarTable("SysUserInfo")] - [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') + [SugarTable("SysUserInfo", "用户表")] //('数据库表名','数据库表备注') public class SysUserInfo : SysUserInfoRoot { public SysUserInfo() diff --git a/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs b/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs index 99757e4f..8f6ad096 100644 --- a/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs +++ b/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs @@ -3,6 +3,7 @@ using Blog.Core.Common.Helper; using Blog.Core.IRepository.Base; using Blog.Core.Model.Models; +using SqlSugar; using Xunit; using Xunit.Abstractions; @@ -21,13 +22,42 @@ public DynamicLambdaTest(ITestOutputHelper testOutputHelper) var container = dI_Test.DICollections(); _baseRepository = container.Resolve>(); - + _baseRepository.Db.Aop.OnLogExecuting = (sql, p) => + { + _testOutputHelper.WriteLine(""); + _testOutputHelper.WriteLine("==================FullSql=====================", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); + _testOutputHelper.WriteLine("【SQL语句】:" + sql); + _testOutputHelper.WriteLine(GetParas(p)); + _testOutputHelper.WriteLine("=============================================="); + _testOutputHelper.WriteLine(""); + }; //DbContext.Init(BaseDBConfig.ConnectionString,(DbType)BaseDBConfig.DbType); + + Init(); + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } + + private void Init() + { + _baseRepository.Db.CodeFirst.InitTables(); + _baseRepository.Db.CodeFirst.InitTables(); } [Fact] public async void Get_Blogs_DynamicTest() { + //方便前端自定义条件查询 + //语法更舒服 var data = await _baseRepository.Query(); _testOutputHelper.WriteLine(data.ToJson()); @@ -39,12 +69,26 @@ public async void Get_Blogs_DynamicTest() await TestConditions("btitle like \"测试数据\" && bId>0"); await TestConditions("btitle like \"测试!@#$%^&*()_+|}{\":<>?LP\"数据\" && bId>0"); await TestConditions("btitle like \"测试!@+)(*()_&%^&^$^%$IUYWIQOJVLXKZM>?Z<>??LP\"数据\" && bId>0"); - - //比如文章下 过滤创建人 - //await TestConditions("btitle.user.name like \"老张\""); - await TestConditions("IsDeleted == false"); await TestConditions("IsDeleted == true"); + + //导航属性 + + //一对一 + + //查询 老张的文章 + await TestConditions("User.RealName like \"老张\""); + //查询 2019年后的老张文章 + await TestConditions("User.RealName like \"老张\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + + //一对多 + + //查询 评论中有"写的不错"的文章 + await TestConditions("Comments.Comment like \"写的不错\""); + //查询 2019后的 评论中有"写的不错"的文章 + await TestConditions("Comments.Comment like \"写的不错\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + //查询 有老张评论的文章 + await TestConditions("Comments.User.LoginName like \"老张\""); } private async Task TestConditions(string conditions) diff --git a/Blog.Core.Tests/DependencyInjection/DI_Test.cs b/Blog.Core.Tests/DependencyInjection/DI_Test.cs index 6fad1f45..ff2c74cd 100644 --- a/Blog.Core.Tests/DependencyInjection/DI_Test.cs +++ b/Blog.Core.Tests/DependencyInjection/DI_Test.cs @@ -74,13 +74,13 @@ public IContainer DICollections() var permission = new List(); var permissionRequirement = new PermissionRequirement( - "/api/denied", - permission, - ClaimTypes.Role, - AppSettings.app(new string[] { "Audience", "Issuer" }), - AppSettings.app(new string[] { "Audience", "Audience" }), - signingCredentials,//签名凭据 - expiration: TimeSpan.FromSeconds(60 * 60)//接口的过期时间 + "/api/denied", + permission, + ClaimTypes.Role, + AppSettings.app(new string[] { "Audience", "Issuer" }), + AppSettings.app(new string[] { "Audience", "Audience" }), + signingCredentials, //签名凭据 + expiration: TimeSpan.FromSeconds(60 * 60) //接口的过期时间 ); services.AddSingleton(permissionRequirement); @@ -88,17 +88,16 @@ public IContainer DICollections() services.AddAuthorization(options => { options.AddPolicy(Permissions.Name, - policy => policy.Requirements.Add(permissionRequirement)); + policy => policy.Requirements.Add(permissionRequirement)); }); services.AddScoped(o => { return new SqlSugar.SqlSugarScope(new SqlSugar.ConnectionConfig() { - ConnectionString = GetMainConnectionDb().Connection,//必填, 数据库连接字符串 - DbType = (SqlSugar.DbType)GetMainConnectionDb().DbType,//必填, 数据库类型 - IsAutoCloseConnection = true,//默认false, 时候知道关闭数据库连接, 设置为true无需使用using或者Close操作 - InitKeyType = SqlSugar.InitKeyType.SystemTable//默认SystemTable, 字段信息读取, 如:该属性是不是主键,标识列等等信息 + ConnectionString = GetMainConnectionDb().Connection, //必填, 数据库连接字符串 + DbType = (SqlSugar.DbType)GetMainConnectionDb().DbType, //必填, 数据库类型 + IsAutoCloseConnection = true, //默认false, 时候知道关闭数据库连接, 设置为true无需使用using或者Close操作 }); }); @@ -121,19 +120,19 @@ public IContainer DICollections() builder.RegisterAssemblyTypes(typeof(Startup).Assembly) .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType) .PropertiesAutowired(); - + var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); var assemblysServices = Assembly.LoadFrom(servicesDllFile); builder.RegisterAssemblyTypes(assemblysServices) - .AsImplementedInterfaces() - .InstancePerLifetimeScope() - .PropertiesAutowired() - .EnableInterfaceInterceptors(); + .AsImplementedInterfaces() + .InstancePerLifetimeScope() + .PropertiesAutowired() + .EnableInterfaceInterceptors(); var repositoryDllFile = Path.Combine(basePath, "Blog.Core.Repository.dll"); var assemblysRepository = Assembly.LoadFrom(repositoryDllFile); builder.RegisterAssemblyTypes(assemblysRepository) - .PropertiesAutowired().AsImplementedInterfaces(); + .PropertiesAutowired().AsImplementedInterfaces(); services.Replace(ServiceDescriptor.Transient()); From 1cc3ef25fa1afcb74e159ec18dd65da7ce2a7770 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 14 Feb 2023 22:14:41 +0800 Subject: [PATCH 125/289] Update DynamicLinqFactory.cs --- .../Extensions/GenericTypeExtensions.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Blog.Core.Common/Extensions/GenericTypeExtensions.cs b/Blog.Core.Common/Extensions/GenericTypeExtensions.cs index c0b6150f..6c067773 100644 --- a/Blog.Core.Common/Extensions/GenericTypeExtensions.cs +++ b/Blog.Core.Common/Extensions/GenericTypeExtensions.cs @@ -33,24 +33,24 @@ public static string GetGenericTypeName(this object @object) /// 类型 /// 泛型类型 /// bool - public static bool HasImplementedRawGeneric(this Type type, Type generic) - { - // 检查接口类型 - var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); - if (isTheRawGenericType) return true; - - // 检查类型 - while (type != null && type != typeof(object)) - { - isTheRawGenericType = IsTheRawGenericType(type); - if (isTheRawGenericType) return true; - type = type.BaseType; - } - - return false; - - // 判断逻辑 - bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); - } + // public static bool HasImplementedRawGeneric(this Type type, Type generic) + // { + // // 检查接口类型 + // var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + // if (isTheRawGenericType) return true; + + // // 检查类型 + // while (type != null && type != typeof(object)) + // { + // isTheRawGenericType = IsTheRawGenericType(type); + // if (isTheRawGenericType) return true; + // type = type.BaseType; + // } + + // return false; + + // // 判断逻辑 + // bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); + // } } } \ No newline at end of file From 296201998d4997397c396b6fba1c544d2c9d9926 Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Sat, 18 Feb 2023 01:17:19 +0800 Subject: [PATCH 126/289] =?UTF-8?q?=E2=9C=A8=F0=9F=8E=A8=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=A4=9A=E7=A7=9F=E6=88=B7-=E5=88=86=E5=BA=93?= =?UTF-8?q?=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.自动初始化维护租户库 2.多租户库种子数据维护 分库方案 TenantByDbController --- .gitignore | 1 + Blog.Core.Api/Blog.Core.Model.xml | 81 +++++---- Blog.Core.Api/Blog.Core.xml | 13 +- .../Tenant/TenantByDbController.cs | 38 +++++ .../Tenant/TenantByIdController.cs | 2 +- Blog.Core.Common/Core/InternalApp.cs | 8 +- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 2 +- Blog.Core.Common/DB/BaseDBConfig.cs | 17 +- Blog.Core.Common/DB/RepositorySetting.cs | 1 + Blog.Core.Common/DB/TenantUtil.cs | 28 ++++ Blog.Core.Common/Seed/DBSeed.cs | 158 ++++++++++++++++-- .../Seed/SeedData/SubBusinessDataSeedData.cs | 70 ++++++++ .../Seed/SeedData/TenantSeedData.cs | 25 ++- .../Seed/SeedData/UserInfoSeedData.cs | 40 ++++- .../HostedService/SeedDataHostedService.cs | 3 + Blog.Core.Model/Models/BusinessTable.cs | 2 +- .../Models/SubLibraryBusinessTable.cs | 22 +++ Blog.Core.Model/Models/SysTenant.cs | 2 +- .../Interface => Tenants}/ITenantEntity.cs | 2 +- .../Tenants/MultiTenantAttribute.cs | 12 ++ .../TenantTypeEnum.cs | 2 +- Blog.Core.Repository/BASE/BaseRepository.cs | 84 ++++++---- 22 files changed, 520 insertions(+), 93 deletions(-) create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs create mode 100644 Blog.Core.Common/DB/TenantUtil.cs create mode 100644 Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs create mode 100644 Blog.Core.Model/Models/SubLibraryBusinessTable.cs rename Blog.Core.Model/{Models/RootTkey/Interface => Tenants}/ITenantEntity.cs (81%) create mode 100644 Blog.Core.Model/Tenants/MultiTenantAttribute.cs rename Blog.Core.Model/{CustomEnums => Tenants}/TenantTypeEnum.cs (89%) diff --git a/.gitignore b/.gitignore index de02add3..b7645c45 100644 --- a/.gitignore +++ b/.gitignore @@ -355,3 +355,4 @@ Blog.Core/WMBlog.db Blog.Core/Blog.Core*.xml Blog.Core.Api/WMBlog.db Blog.Core.Api/wwwroot/ui/ +*.db diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 3c1b8ef7..eb7eaab1 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -34,21 +34,6 @@ 所有 - - - 租户隔离方案 - - - - - Id隔离 - - - - - 库隔离 - - 以下model 来自ids4项目,多库模式,为了调取ids4数据 @@ -316,7 +301,16 @@ 逻辑删除 - + + + 评论 + + + + + 博客文章 评论 + + 业务数据
@@ -336,16 +330,6 @@ 金额 - - - - 评论 - - - - - 博客文章 评论 - @@ -933,14 +917,20 @@ 软删除 过滤器 - + - 租户模型接口 + 多租户-多库方案 业务表
+ 公共库无需标记[MultiTenant]特性
- + - 租户Id + 名称 + + + + + 金额 @@ -1892,6 +1882,37 @@ 返回数据集
+ + + 租户模型接口 + + + + + 租户Id + + + + + 标识 多租户-分库 的业务表
+ 公共表无需区分 直接使用主库 各自业务在各自库中 +
+
+ + + 租户隔离方案 + + + + + Id隔离 + + + + + 库隔离 + + 广告类 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 8b5b79fd..8effc9f8 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1236,11 +1236,22 @@ - + 多租户测试 + + + 获取租户下全部业务数据
+
+ +
+ + + 多租户-Id方案 测试 + + 获取租户下全部业务数据
diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs new file mode 100644 index 00000000..0d403adb --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs @@ -0,0 +1,38 @@ +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Controllers; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 多租户测试 +/// +[Produces("application/json")] +[Route("api/Tenant/ByDb")] +[Authorize] +public class TenantByDbController : BaseApiController +{ + private readonly IBaseServices _services; + private readonly IUser _user; + + public TenantByDbController(IUser user, IBaseServices services) + { + _user = user; + _services = services; + } + + /// + /// 获取租户下全部业务数据
+ ///
+ /// + [HttpGet] + public async Task>> GetAll() + { + var data = await _services.Query(); + return Success(data); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs index a0271780..b015bc6d 100644 --- a/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs +++ b/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs @@ -9,7 +9,7 @@ namespace Blog.Core.Api.Controllers.Tenant; /// -/// 多租户测试 +/// 多租户-Id方案 测试 /// [Produces("application/json")] [Route("api/Tenant/ById")] diff --git a/Blog.Core.Common/Core/InternalApp.cs b/Blog.Core.Common/Core/InternalApp.cs index b1f94f9d..c1ae8dcd 100644 --- a/Blog.Core.Common/Core/InternalApp.cs +++ b/Blog.Core.Common/Core/InternalApp.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; using System; namespace Blog.Core.Common.Core; @@ -8,8 +8,10 @@ public static class InternalApp /// 根服务 public static IServiceProvider RootServices; - public static void ConfigureApplication(this IHost app) + public static void ConfigureApplication(this WebApplication app) { - RootServices = app.Services; + app.Lifetime.ApplicationStarted.Register(() => { InternalApp.RootServices = app.Services; }); + + app.Lifetime.ApplicationStopped.Register(() => { InternalApp.RootServices = null; }); } } \ No newline at end of file diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index 3646625d..3d83b002 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -1,5 +1,5 @@ using Blog.Core.Model.Models.RootTkey; -using Blog.Core.Model.Models.RootTkey.Interface; +using Blog.Core.Model.Tenants; using SqlSugar; using System; diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index dc7fee4e..b832b6d0 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -11,6 +11,7 @@ public class BaseDBConfig * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); + private static string DifDBConnOfSecurity(params string[] conn) { foreach (var item in conn) @@ -22,7 +23,9 @@ private static string DifDBConnOfSecurity(params string[] conn) return File.ReadAllText(item).Trim(); } } - catch (System.Exception) { } + catch (System.Exception) + { + } } return conn[conn.Length - 1]; @@ -37,8 +40,9 @@ public static (List, List) MutiInitConn() { SpecialDbString(i); } - List listdatabaseSimpleDB = new List();//单库 - List listdatabaseSlaveDB = new List();//从库 + + List listdatabaseSimpleDB = new List(); //单库 + List listdatabaseSlaveDB = new List(); //从库 // 单库,且不开启读写分离,只保留一个 if (!AppSettings.app(new string[] { "CQRSEnabled" }).ObjToBool() && !AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) @@ -54,6 +58,7 @@ public static (List, List) MutiInitConn() { dbFirst = listdatabase.FirstOrDefault(); } + listdatabaseSimpleDB.Add(dbFirst); return (listdatabaseSimpleDB, listdatabaseSlaveDB); } @@ -70,7 +75,6 @@ public static (List, List) MutiInitConn() } - return (listdatabase, listdatabaseSlaveDB); //} } @@ -115,24 +119,29 @@ public enum DataBaseType Dm = 5, Kdbndp = 6, } + public class MutiDBOperate { /// /// 连接启用开关 /// public bool Enabled { get; set; } + /// /// 连接ID /// public string ConnId { get; set; } + /// /// 从库执行级别,越大越先执行 /// public int HitRate { get; set; } + /// /// 连接字符串 /// public string Connection { get; set; } + /// /// 数据库类型 /// diff --git a/Blog.Core.Common/DB/RepositorySetting.cs b/Blog.Core.Common/DB/RepositorySetting.cs index 4f2e67d2..e55c7a03 100644 --- a/Blog.Core.Common/DB/RepositorySetting.cs +++ b/Blog.Core.Common/DB/RepositorySetting.cs @@ -1,5 +1,6 @@ using Blog.Core.Model.Models.RootTkey; using Blog.Core.Model.Models.RootTkey.Interface; +using Blog.Core.Model.Tenants; using SqlSugar; using System; using System.Collections.Generic; diff --git a/Blog.Core.Common/DB/TenantUtil.cs b/Blog.Core.Common/DB/TenantUtil.cs new file mode 100644 index 00000000..37ad6909 --- /dev/null +++ b/Blog.Core.Common/DB/TenantUtil.cs @@ -0,0 +1,28 @@ +using System; +using Blog.Core.Model.Models; +using SqlSugar; + +namespace Blog.Core.Common.DB; + +public static class TenantUtil +{ + public static ConnectionConfig GetConnectionConfig(this SysTenant tenant) + { + if (tenant.DbType is null) + { + throw new ArgumentException("Tenant DbType Must"); + } + + return new ConnectionConfig() + { + ConfigId = tenant.ConfigId, + DbType = tenant.DbType.Value, + ConnectionString = tenant.Connection, + IsAutoCloseConnection = true, + MoreSettings = new ConnMoreSettings() + { + IsAutoRemoveDataCache = true + }, + }; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 64aeba95..fdbb1ee8 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -11,6 +11,8 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using Blog.Core.Model.Tenants; +using SqlSugar; namespace Blog.Core.Common.Seed { @@ -97,7 +99,9 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) var modelTypes = referencedAssemblies .SelectMany(a => a.DefinedTypes) .Select(type => type.AsType()) - .Where(x => x.IsClass && x.Namespace is "Blog.Core.Model.Models").ToList(); + .Where(x => x.IsClass && x.Namespace is "Blog.Core.Model.Models") + .Where(s => !s.IsDefined(typeof(MultiTenantAttribute), false)) + .ToList(); modelTypes.ForEach(t => { // 这里只支持添加表,不支持删除 @@ -326,7 +330,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) #endregion //种子初始化 - await SeedDataAsync(myContext); + await SeedDataAsync(myContext.Db); ConsoleHelper.WriteSuccessLine($"Done seeding database!"); } @@ -347,12 +351,142 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) ///
/// /// - private static async Task SeedDataAsync(MyContext myContext) + private static async Task SeedDataAsync(ISqlSugarClient db) { // 获取所有种子配置-初始化数据 var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes) - .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && - u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>)))); + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(u => + { + var esd = u.GetInterfaces().FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); + if (esd is null) + { + return false; + } + + var eType = esd.GenericTypeArguments[0]; + if (eType.GetCustomAttribute() is null) + { + return true; + } + + return false; + }); + + if (!seedDataTypes.Any()) return; + foreach (var seedType in seedDataTypes) + { + dynamic instance = Activator.CreateInstance(seedType); + //初始化数据 + { + var seedData = instance.InitSeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); + + if (!await db.Queryable(entity.DbTableName, "").AnyAsync()) + { + await db.Insertable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} init success!"); + } + } + } + + //种子数据 + { + var seedData = instance.SeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); + + await db.Storageable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} seedData success!"); + } + } + + //自定义处理 + { + await instance.CustomizeSeedData(db); + } + } + } + + + /// + /// 初始化 多租户 + /// + /// + /// + public static async Task TenantSeedAsync(MyContext myContext) + { + var tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Db).ToListAsync(); + if (!tenants.Any()) + { + return; + } + + Console.WriteLine($@"Init Multi Tenant Db"); + foreach (var tenant in tenants) + { + Console.WriteLine($@"Init Multi Tenant Db : {tenant.ConfigId}/{tenant.Name}"); + await InitTenantSeedAsync(myContext.Db.AsTenant(), tenant.GetConnectionConfig()); + } + } + + public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig config) + { + itenant.AddConnection(config); + + var db = itenant.GetConnectionScope(config.ConfigId); + + db.DbMaintenance.CreateDatabase(); + ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Db : {config.ConfigId} Database created successfully!"); + + + Console.WriteLine($@"Init Multi Tenant Db : {config.ConfigId} Create Tables"); + // 获取所有实体表-初始化租户业务表 + var entityTypes = RepositorySetting.Entitys + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(s => s.IsDefined(typeof(MultiTenantAttribute), false)); + if (!entityTypes.Any()) return; + foreach (var entityType in entityTypes) + { + var splitTable = entityType.GetCustomAttribute(); + if (splitTable == null) + db.CodeFirst.InitTables(entityType); + else + db.CodeFirst.SplitTables().InitTables(entityType); + + Console.WriteLine(entityType.Name); + } + + //多租户初始化种子数据 + await TenantSeedDataAsync(db); + } + + private static async Task TenantSeedDataAsync(ISqlSugarClient db) + { + // 获取所有种子配置-初始化数据 + var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes) + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(u => + { + var esd = u.GetInterfaces().FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); + if (esd is null) + { + return false; + } + + var eType = esd.GenericTypeArguments[0]; + if (eType.GetCustomAttribute() is null) + { + return false; + } + + return true; + }); if (!seedDataTypes.Any()) return; foreach (var seedType in seedDataTypes) { @@ -363,11 +497,11 @@ private static async Task SeedDataAsync(MyContext myContext) if (seedData != null && Enumerable.Any(seedData)) { var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); - var entity = myContext.Db.EntityMaintenance.GetEntityInfo(entityType); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); - if (!await myContext.Db.Queryable(entity.DbTableName, "").AnyAsync()) + if (!await db.Queryable(entity.DbTableName, "").AnyAsync()) { - await myContext.Db.Insertable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + await db.Insertable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); Console.WriteLine($"Table:{entity.DbTableName} init success!"); } } @@ -379,18 +513,18 @@ private static async Task SeedDataAsync(MyContext myContext) if (seedData != null && Enumerable.Any(seedData)) { var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); - var entity = myContext.Db.EntityMaintenance.GetEntityInfo(entityType); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); - await myContext.Db.Storageable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + await db.Storageable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); Console.WriteLine($"Table:{entity.DbTableName} seedData success!"); } } //自定义处理 { - await instance.CustomizeSeedData(myContext.Db); + await instance.CustomizeSeedData(db); } } } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs new file mode 100644 index 00000000..be8462e8 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs @@ -0,0 +1,70 @@ +using Blog.Core.Model.Models; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed.SeedData; + +public class SubBusinessDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return default; + } + + public IEnumerable SeedData() + { + return default; + } + + public async Task CustomizeSeedData(ISqlSugarClient db) + { + //初始化分库数据 + //只是用于测试 + if (db.CurrentConnectionConfig.ConfigId == "Tenant_3") + { + if (!await db.Queryable().AnyAsync()) + { + await db.Insertable(new List() + { + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "王五业务数据1", + Amount = 100, + }, + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "王五业务数据2", + Amount = 1000, + }, + }).ExecuteReturnSnowflakeIdListAsync(); + } + } + else if (db.CurrentConnectionConfig.ConfigId == "Tenant_4") + { + if (!await db.Queryable().AnyAsync()) + { + await db.Insertable(new List() + { + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "赵六业务数据1", + Amount = 50, + }, + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "赵六业务数据2", + Amount = 60, + }, + }).ExecuteReturnSnowflakeIdListAsync(); + } + } + + + await Task.Delay(1); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs index 89945547..0a0fb5b4 100644 --- a/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs +++ b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; -using Blog.Core.Model.CustomEnums; +using Blog.Core.Common.DB; using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; using SqlSugar; namespace Blog.Core.Common.Seed.SeedData; @@ -29,6 +32,24 @@ public IEnumerable InitSeedData() Name = "李四", TenantType = TenantTypeEnum.Id }, + new SysTenant() + { + Id = 1000003, + ConfigId = "Tenant_3", + Name = "王五", + TenantType = TenantTypeEnum.Db, + DbType = DbType.Sqlite, + Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, "WangWu.db"), + }, + new SysTenant() + { + Id = 1000004, + ConfigId = "Tenant_4", + Name = "赵六", + TenantType = TenantTypeEnum.Db, + DbType = DbType.Sqlite, + Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, "ZhaoLiu.db"), + }, }; } diff --git a/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs index c00eb798..8a96a030 100644 --- a/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs +++ b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Blog.Core.Model.Models; using SqlSugar; @@ -14,7 +15,12 @@ public IEnumerable InitSeedData() public IEnumerable SeedData() { - return new[] + return default; + } + + public async Task CustomizeSeedData(ISqlSugarClient db) + { + var data = new List() { new SysUserInfo() { @@ -32,11 +38,35 @@ public IEnumerable SeedData() Name = "李四", TenantId = 1000002, //租户Id }, + new SysUserInfo() + { + Id = 10003, + LoginName = "wangwu", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "王五", + TenantId = 1000003, //租户Id + }, + new SysUserInfo() + { + Id = 10004, + LoginName = "zhaoliu", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "赵六", + TenantId = 1000004, //租户Id + }, }; - } - public Task CustomizeSeedData(ISqlSugarClient db) - { - return Task.CompletedTask; + var names = data.Select(s => s.LoginName).ToList(); + names = await db.Queryable() + .Where(s => names.Contains(s.LoginName)) + .Select(s => s.LoginName).ToListAsync(); + + var sysUserInfos = data.Where(s => !names.Contains(s.LoginName)).ToList(); + if (sysUserInfos.Any()) + { + await db.Insertable(sysUserInfos).ExecuteReturnIdentityAsync(); + } + + await Task.CompletedTask; } } \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs index beef5a75..868d750e 100644 --- a/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs +++ b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs @@ -38,6 +38,9 @@ private async Task DoWork() if (AppSettings.app("AppSettings", "SeedDBEnabled").ObjToBool() || AppSettings.app("AppSettings", "SeedDBDataEnabled").ObjToBool()) { await DBSeed.SeedAsync(_myContext, _webRootPath); + + //多租户 同步 + await DBSeed.TenantSeedAsync(_myContext); } } catch (Exception ex) diff --git a/Blog.Core.Model/Models/BusinessTable.cs b/Blog.Core.Model/Models/BusinessTable.cs index 01a18716..b3b0140a 100644 --- a/Blog.Core.Model/Models/BusinessTable.cs +++ b/Blog.Core.Model/Models/BusinessTable.cs @@ -1,5 +1,5 @@ using Blog.Core.Model.Models.RootTkey; -using Blog.Core.Model.Models.RootTkey.Interface; +using Blog.Core.Model.Tenants; namespace Blog.Core.Model.Models; diff --git a/Blog.Core.Model/Models/SubLibraryBusinessTable.cs b/Blog.Core.Model/Models/SubLibraryBusinessTable.cs new file mode 100644 index 00000000..446fb436 --- /dev/null +++ b/Blog.Core.Model/Models/SubLibraryBusinessTable.cs @@ -0,0 +1,22 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; + +namespace Blog.Core.Model.Models; + +/// +/// 多租户-多库方案 业务表
+/// 公共库无需标记[MultiTenant]特性 +///
+[MultiTenant] +public class SubLibraryBusinessTable : BaseEntity +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/SysTenant.cs b/Blog.Core.Model/Models/SysTenant.cs index 7e6f195d..61d03866 100644 --- a/Blog.Core.Model/Models/SysTenant.cs +++ b/Blog.Core.Model/Models/SysTenant.cs @@ -1,4 +1,4 @@ -using Blog.Core.Model.CustomEnums; +using Blog.Core.Model.Tenants; using SqlSugar; namespace Blog.Core.Model.Models; diff --git a/Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs b/Blog.Core.Model/Tenants/ITenantEntity.cs similarity index 81% rename from Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs rename to Blog.Core.Model/Tenants/ITenantEntity.cs index 350312b8..2d0c5dc9 100644 --- a/Blog.Core.Model/Models/RootTkey/Interface/ITenantEntity.cs +++ b/Blog.Core.Model/Tenants/ITenantEntity.cs @@ -1,6 +1,6 @@ using SqlSugar; -namespace Blog.Core.Model.Models.RootTkey.Interface; +namespace Blog.Core.Model.Tenants; /// /// 租户模型接口 diff --git a/Blog.Core.Model/Tenants/MultiTenantAttribute.cs b/Blog.Core.Model/Tenants/MultiTenantAttribute.cs new file mode 100644 index 00000000..9c3938a9 --- /dev/null +++ b/Blog.Core.Model/Tenants/MultiTenantAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Blog.Core.Model.Tenants; + +/// +/// 标识 多租户-分库 的业务表
+/// 公共表无需区分 直接使用主库 各自业务在各自库中 +///
+[AttributeUsage(AttributeTargets.Class)] +public class MultiTenantAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Blog.Core.Model/CustomEnums/TenantTypeEnum.cs b/Blog.Core.Model/Tenants/TenantTypeEnum.cs similarity index 89% rename from Blog.Core.Model/CustomEnums/TenantTypeEnum.cs rename to Blog.Core.Model/Tenants/TenantTypeEnum.cs index a445414b..46ae999c 100644 --- a/Blog.Core.Model/CustomEnums/TenantTypeEnum.cs +++ b/Blog.Core.Model/Tenants/TenantTypeEnum.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace Blog.Core.Model.CustomEnums; +namespace Blog.Core.Model.Tenants; /// /// 租户隔离方案 diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 82900689..1845e958 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -8,6 +8,10 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using Blog.Core.Common.DB; +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; using Blog.Core.Repository.UnitOfWorks; namespace Blog.Core.Repository.Base @@ -22,11 +26,12 @@ private ISqlSugarClient _db get { ISqlSugarClient db = _dbBase; + /* 如果要开启多库支持, * 1、在appsettings.json 中开启MutiDBEnabled节点为true,必填 * 2、设置一个主连接的数据库ID,节点MainDB,对应的连接字符串的Enabled也必须true,必填 */ - if (AppSettings.app(new[] {"MutiDBEnabled"}).ObjToBool()) + if (AppSettings.app(new[] { "MutiDBEnabled" }).ObjToBool()) { //修改使用 model备注字段作为切换数据库条件,使用sqlsugar TenantAttribute存放数据库ConnId //参考 https://www.donet5.com/Home/Doc?typeId=2246 @@ -35,6 +40,27 @@ private ISqlSugarClient _db { //统一处理 configId 小写 db = _dbBase.GetConnectionScope(tenantAttr.configId.ToString().ToLower()); + return db; + } + } + + //多租户 + if (typeof(TEntity).GetCustomAttribute() != null) + { + //获取租户信息 租户信息可以提前缓存下来 + if (App.User is { TenantId: > 0 }) + { + var tenant = db.Queryable().WithCache().Where(s => s.Id == App.User.TenantId).First(); + if (tenant != null) + { + var iTenant = db.AsTenant(); + if (!iTenant.IsAnyConnection(tenant.ConfigId)) + { + iTenant.AddConnection(tenant.GetConnectionConfig()); + } + + return iTenant.GetConnectionScope(tenant.ConfigId); + } } } @@ -51,12 +77,12 @@ public BaseRepository(IUnitOfWorkManage unitOfWorkManage) } - public async Task QueryById(object objId) { //return await Task.Run(() => _db.Queryable().InSingle(objId)); return await _db.Queryable().In(objId).SingleAsync(); } + /// /// 功能描述:根据ID查询一条数据 /// 作  者:Blog.Core @@ -161,25 +187,28 @@ public async Task Update(object operateAnonymousObjects) } public async Task Update( - TEntity entity, - List lstColumns = null, - List lstIgnoreColumns = null, - string where = "" - ) + TEntity entity, + List lstColumns = null, + List lstIgnoreColumns = null, + string where = "" + ) { IUpdateable up = _db.Updateable(entity); if (lstIgnoreColumns != null && lstIgnoreColumns.Count > 0) { up = up.IgnoreColumns(lstIgnoreColumns.ToArray()); } + if (lstColumns != null && lstColumns.Count > 0) { up = up.UpdateColumns(lstColumns.ToArray()); } + if (!string.IsNullOrEmpty(where)) { up = up.Where(where); } + return await up.ExecuteCommandHasChangeAsync(); } @@ -214,7 +243,6 @@ public async Task DeleteByIds(object[] ids) } - /// /// 功能描述:查询所有数据 /// 作  者:Blog.Core @@ -284,6 +312,7 @@ public async Task> Query(Expression> whereExpr { return await _db.Queryable().WhereIF(whereExpression != null, whereExpression).OrderByIF(orderByFields != null, orderByFields).ToListAsync(); } + /// /// 功能描述:查询一个列表 /// @@ -393,18 +422,16 @@ public async Task> Query( /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string where, - int pageIndex, - int pageSize, - - string orderByFields) + string where, + int pageIndex, + int pageSize, + string orderByFields) { return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) .WhereIF(!string.IsNullOrEmpty(where), where).ToPageListAsync(pageIndex, pageSize); } - /// /// 分页查询[使用版本,其他分页未测试] /// @@ -415,12 +442,11 @@ public async Task> Query( /// public async Task> QueryPage(Expression> whereExpression, int pageIndex = 1, int pageSize = 20, string orderByFields = null) { - RefAsync totalCount = 0; var list = await _db.Queryable() - .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) - .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(pageIndex, pageSize, totalCount); + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -446,6 +472,7 @@ public async Task> QueryMuch( { return await _db.Queryable(joinExpression).Select(selectExpression).ToListAsync(); } + return await _db.Queryable(joinExpression).Where(whereLambda).Select(selectExpression).ToListAsync(); } @@ -471,13 +498,12 @@ public async Task> QueryTabsPage( int pageSize = 20, string orderByFields = null) { - RefAsync totalCount = 0; var list = await _db.Queryable(joinExpression) - .Select(selectExpression) - .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) - .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(pageIndex, pageSize, totalCount); + .Select(selectExpression) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -506,10 +532,10 @@ public async Task> QueryTabsPage( { RefAsync totalCount = 0; var list = await _db.Queryable(joinExpression).GroupBy(groupExpression) - .Select(selectExpression) - .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) - .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(pageIndex, pageSize, totalCount); + .Select(selectExpression) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -531,7 +557,5 @@ public async Task> QueryTabsPage( // groupName = s.groupName, // jobName = s.jobName // }, exp, s => new { s.uID, s.uRealName, s.groupName, s.jobName }, model.currentPage, model.pageSize, model.orderField + " " + model.orderType); - } - -} +} \ No newline at end of file From d85087cbdb3b44e559da5d65adf7448f96363f35 Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Sat, 18 Feb 2023 20:58:43 +0800 Subject: [PATCH 127/289] =?UTF-8?q?=F0=9F=8E=A8=E2=9C=A8=20=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.完善多租户-多库方案 2.增加租户管理 (实际业务中 也是运维、系统管理员等角色来操作 甚至直接维护数据库而不会开放接口) --- Blog.Core.Api/Blog.Core.xml | 44 ++++++++++ .../Tenant/TenantByDbController.cs | 14 ++- .../Tenant/TenantManagerController.cs | 87 +++++++++++++++++++ Blog.Core.Common/DB/BaseDBConfig.cs | 5 +- Blog.Core.Common/DB/TenantUtil.cs | 22 +++++ Blog.Core.Common/Seed/DBSeed.cs | 3 +- Blog.Core.IServices/ITenantService.cs | 12 +++ Blog.Core.Services/TenantService.cs | 57 ++++++++++++ 8 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs create mode 100644 Blog.Core.IServices/ITenantService.cs create mode 100644 Blog.Core.Services/TenantService.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 8effc9f8..b41b293c 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1247,6 +1247,12 @@ + + + 新增数据 + + + 多租户-Id方案 测试 @@ -1264,6 +1270,44 @@ + + + 租户管理 + + + + + 获取全部租户 + + + + + + 获取租户信息 + + + + + + 新增租户信息
+ 此处只做演示,具体要以实际业务为准 +
+ +
+ + + 修改租户信息
+ 此处只做演示,具体要以实际业务为准 +
+ +
+ + + 删除租户
+ 此处只做演示,具体要以实际业务为准 +
+ +
自定义路由 /api/{version}/[controler]/[action] diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs index 0d403adb..046f7f7b 100644 --- a/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs +++ b/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs @@ -9,7 +9,7 @@ namespace Blog.Core.Api.Controllers.Tenant; /// -/// 多租户测试 +/// 多租户-多库方案 测试 /// [Produces("application/json")] [Route("api/Tenant/ByDb")] @@ -35,4 +35,16 @@ public async Task>> GetAll() var data = await _services.Query(); return Success(data); } + + /// + /// 新增数据 + /// + /// + [HttpPost] + public async Task Post(SubLibraryBusinessTable data) + { + await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync(); + + return Success(); + } } \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs b/Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs new file mode 100644 index 00000000..90133fdb --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs @@ -0,0 +1,87 @@ +using Blog.Core.Controllers; +using Blog.Core.IServices; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 租户管理 +/// +[Produces("application/json")] +[Route("api/TenantManager")] +[Authorize] +public class TenantManagerController : BaseApiController +{ + private readonly ITenantService _services; + + public TenantManagerController(ITenantService services) + { + _services = services; + } + + + /// + /// 获取全部租户 + /// + /// + [HttpGet] + public async Task>> GetAll() + { + var data = await _services.Query(); + return Success(data); + } + + + /// + /// 获取租户信息 + /// + /// + [HttpGet("{id}")] + public async Task> GetInfo(long id) + { + var data = await _services.QueryById(id); + return Success(data); + } + + /// + /// 新增租户信息
+ /// 此处只做演示,具体要以实际业务为准 + ///
+ /// + [HttpPost] + public async Task Post(SysTenant tenant) + { + await _services.SaveTenant(tenant); + return Success(); + } + + /// + /// 修改租户信息
+ /// 此处只做演示,具体要以实际业务为准 + ///
+ /// + [HttpPut] + public async Task Put(SysTenant tenant) + { + await _services.SaveTenant(tenant); + return Success(); + } + + /// + /// 删除租户
+ /// 此处只做演示,具体要以实际业务为准 + ///
+ /// + [HttpDelete] + public async Task Delete(long id) + { + //是否删除租户库? + //要根据实际情况而定 + //例如直接删除租户库、备份租户库到xx + await _services.DeleteById(id); + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index b832b6d0..1d86369a 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -1,4 +1,5 @@ -using System; +using SqlSugar; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -106,6 +107,8 @@ private static MutiDBOperate SpecialDbString(MutiDBOperate mutiDBOperate) return mutiDBOperate; } + + } diff --git a/Blog.Core.Common/DB/TenantUtil.cs b/Blog.Core.Common/DB/TenantUtil.cs index 37ad6909..deac60a4 100644 --- a/Blog.Core.Common/DB/TenantUtil.cs +++ b/Blog.Core.Common/DB/TenantUtil.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Blog.Core.Model.Models; using SqlSugar; @@ -6,6 +7,26 @@ namespace Blog.Core.Common.DB; public static class TenantUtil { + public static SysTenant DefaultTenantConfig(this SysTenant tenant) + { + tenant.DbType ??= DbType.Sqlite; + + //如果没有配置连接 + if (tenant.Connection.IsNullOrEmpty()) + { + //此处默认配置 Sqlite 地址 + //实际业务中 也会有运维、系统管理员等来维护 + switch (tenant.DbType.Value) + { + case DbType.Sqlite: + tenant.Connection = $"DataSource={Path.Combine(Environment.CurrentDirectory, tenant.ConfigId)}.db" ; + break; + } + } + + return tenant; + } + public static ConnectionConfig GetConnectionConfig(this SysTenant tenant) { if (tenant.DbType is null) @@ -13,6 +34,7 @@ public static ConnectionConfig GetConnectionConfig(this SysTenant tenant) throw new ArgumentException("Tenant DbType Must"); } + return new ConnectionConfig() { ConfigId = tenant.ConfigId, diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index fdbb1ee8..a6952e6e 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -372,7 +372,7 @@ private static async Task SeedDataAsync(ISqlSugarClient db) return false; }); - + if (!seedDataTypes.Any()) return; foreach (var seedType in seedDataTypes) { @@ -437,6 +437,7 @@ public static async Task TenantSeedAsync(MyContext myContext) public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig config) { + itenant.RemoveConnection(config.ConfigId); itenant.AddConnection(config); var db = itenant.GetConnectionScope(config.ConfigId); diff --git a/Blog.Core.IServices/ITenantService.cs b/Blog.Core.IServices/ITenantService.cs new file mode 100644 index 00000000..0927b793 --- /dev/null +++ b/Blog.Core.IServices/ITenantService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; + +namespace Blog.Core.IServices; + +public interface ITenantService : IBaseServices +{ + public Task SaveTenant(SysTenant tenant); + + public Task InitTenantDb(SysTenant tenant); +} \ No newline at end of file diff --git a/Blog.Core.Services/TenantService.cs b/Blog.Core.Services/TenantService.cs new file mode 100644 index 00000000..a552442b --- /dev/null +++ b/Blog.Core.Services/TenantService.cs @@ -0,0 +1,57 @@ +using Blog.Core.Common.DB; +using Blog.Core.Common.Seed; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.Services.BASE; +using System.Threading.Tasks; + +namespace Blog.Core.Services; + +public class TenantService : BaseServices, ITenantService +{ + private readonly IUnitOfWorkManage _uowManager; + + public TenantService(IUnitOfWorkManage uowManage) + { + this._uowManager = uowManage; + } + + + public async Task SaveTenant(SysTenant tenant) + { + bool initDb = tenant.Id == 0; + using (var uow = _uowManager.CreateUnitOfWork()) + { + + tenant.DefaultTenantConfig(); + + if (tenant.Id == 0) + { + await Db.Insertable(tenant).ExecuteReturnSnowflakeIdAsync(); + } + else + { + var oldTenant = await QueryById(tenant.Id); + if (oldTenant.Connection != tenant.Connection) + { + initDb = true; + } + + await Db.Updateable(tenant).ExecuteCommandAsync(); + } + + uow.Commit(); + } + + if (initDb) + { + await InitTenantDb(tenant); + } + } + + public async Task InitTenantDb(SysTenant tenant) + { + await DBSeed.InitTenantSeedAsync(Db.AsTenant(), tenant.GetConnectionConfig()); + } +} \ No newline at end of file From bd484137a65431c6ba5af07f93991b72e6afaf38 Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Tue, 21 Feb 2023 01:50:51 +0800 Subject: [PATCH 128/289] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=F0=9F=8E=A8=20?= =?UTF-8?q?=E5=AE=8C=E7=BE=8E=E4=BC=98=E9=9B=85=E7=9A=84=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=A4=9A=E7=A7=9F=E6=88=B7-=E5=88=86=E8=A1=A8=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.扩展原有的MultiTenantAttribute 标识多库、多表 2.扩展原有的种子数据生成 用于多表的种子数据 3.巧妙优雅使用Sqlsugar表映射 解决多租户分表问题,原有代码无需改动 登录用户如果是租户用户自动切换到租户分表 目前来看(如果想要升级业务 扩展SAAS) 多表方案:代码侵入最小 id方案:侵入最大,需要增加列 多库方案:相对少 如果是从0到1 最推荐多库 如果是从0.5到1 最推荐多表 --- Blog.Core.Api/Blog.Core.Api.csproj | 1 - Blog.Core.Api/Blog.Core.Model.xml | 104 +++++++++++------- Blog.Core.Api/Blog.Core.xml | 19 +++- .../Tenant/TenantByTableController.cs | 57 ++++++++++ Blog.Core.Common/DB/RepositorySetting.cs | 4 + Blog.Core.Common/DB/TenantUtil.cs | 54 ++++++++- .../Extensions/UntilExtensions.cs | 18 +++ Blog.Core.Common/Seed/DBSeed.cs | 78 +++++++++---- .../SeedData/MultiBusinessDataSeedData.cs | 38 +++++++ .../SeedData/MultiBusinessSubDataSeedData.cs | 38 +++++++ .../Seed/SeedData/TenantSeedData.cs | 7 ++ .../Seed/SeedData/UserInfoSeedData.cs | 8 ++ .../Models/{ => Tenant}/BusinessTable.cs | 0 .../Models/Tenant/MultiBusinessSubTable.cs | 14 +++ .../Models/Tenant/MultiBusinessTable.cs | 26 +++++ .../{ => Tenant}/SubLibraryBusinessTable.cs | 0 .../Tenants/MultiTenantAttribute.cs | 16 ++- Blog.Core.Model/Tenants/TenantTypeEnum.cs | 6 + Blog.Core.Repository/BASE/BaseRepository.cs | 12 +- Blog.Core.Tests/Repository_Test/OrmTest.cs | 73 ++++++++++++ 20 files changed, 503 insertions(+), 70 deletions(-) create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs create mode 100644 Blog.Core.Common/Extensions/UntilExtensions.cs create mode 100644 Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs rename Blog.Core.Model/Models/{ => Tenant}/BusinessTable.cs (100%) create mode 100644 Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs create mode 100644 Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs rename Blog.Core.Model/Models/{ => Tenant}/SubLibraryBusinessTable.cs (100%) create mode 100644 Blog.Core.Tests/Repository_Test/OrmTest.cs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 7e17937a..77d681f1 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -98,7 +98,6 @@ - diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index eb7eaab1..0e9ddbb0 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -311,27 +311,6 @@ 博客文章 评论
- - - 业务数据
- 多租户 (Id 隔离) -
-
- - - 无需手动赋值 - - - - - 名称 - - - - - 金额 - - 部门表 @@ -917,22 +896,6 @@ 软删除 过滤器 - - - 多租户-多库方案 业务表
- 公共库无需标记[MultiTenant]特性 -
-
- - - 名称 - - - - - 金额 - - 系统租户表
@@ -1145,6 +1108,63 @@ 任务内存中的状态
+ + + 业务数据
+ 多租户 (Id 隔离) +
+
+ + + 无需手动赋值 + + + + + 名称 + + + + + 金额 + + + + + 多租户-多表方案 业务表 子表
+
+
+ + + 多租户-多表方案 业务表
+
+
+ + + 名称 + + + + + 金额 + + + + + 多租户-多库方案 业务表
+ 公共库无需标记[MultiTenant]特性 +
+
+ + + 名称 + + + + + 金额 + + Tibug 类别 @@ -1894,8 +1914,9 @@ - 标识 多租户-分库 的业务表
- 公共表无需区分 直接使用主库 各自业务在各自库中 + 标识 多租户 的业务表
+ 默认设置是多库
+ 公共表无需区分 直接使用主库 各自业务在各自库中
@@ -1913,6 +1934,11 @@ 库隔离
+ + + 表隔离 + + 广告类 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index b41b293c..8180287b 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1238,7 +1238,7 @@ - 多租户测试 + 多租户-多库方案 测试 @@ -1270,6 +1270,23 @@ + + + 多租户-多表方案 测试 + + + + + 获取租户下全部业务数据
+
+ +
+ + + 新增数据 + + + 租户管理 diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs new file mode 100644 index 00000000..6c0b110e --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs @@ -0,0 +1,57 @@ +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Controllers; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 多租户-多表方案 测试 +/// +[Produces("application/json")] +[Route("api/Tenant/ByTable")] +[Authorize] +public class TenantByTableController : BaseApiController +{ + private readonly IBaseServices _services; + private readonly IUser _user; + + public TenantByTableController(IUser user, IBaseServices services) + { + _user = user; + _services = services; + } + + /// + /// 获取租户下全部业务数据
+ ///
+ /// + [HttpGet] + public async Task>> GetAll() + { + //查询 + // var data = await _services.Query(); + + //关联查询 + var data = await _services.Db + .Queryable() + .Includes(s => s.Child) + .ToListAsync(); + return Success(data); + } + + /// + /// 新增数据 + /// + /// + [HttpPost] + public async Task Post(MultiBusinessTable data) + { + await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync(); + + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/RepositorySetting.cs b/Blog.Core.Common/DB/RepositorySetting.cs index e55c7a03..bfca7174 100644 --- a/Blog.Core.Common/DB/RepositorySetting.cs +++ b/Blog.Core.Common/DB/RepositorySetting.cs @@ -39,6 +39,10 @@ public static void SetTenantEntityFilter(SqlSugarScopeProvider db) return; } + //多租户 单表 db.QueryFilter.AddTableFilter(it => it.TenantId == App.User.TenantId || it.TenantId == 0); + + //多租户 多表 + db.SetTenantTable(App.User.TenantId.ToString()); } } \ No newline at end of file diff --git a/Blog.Core.Common/DB/TenantUtil.cs b/Blog.Core.Common/DB/TenantUtil.cs index deac60a4..8d57189b 100644 --- a/Blog.Core.Common/DB/TenantUtil.cs +++ b/Blog.Core.Common/DB/TenantUtil.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reflection; using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; using SqlSugar; namespace Blog.Core.Common.DB; @@ -19,7 +23,7 @@ public static SysTenant DefaultTenantConfig(this SysTenant tenant) switch (tenant.DbType.Value) { case DbType.Sqlite: - tenant.Connection = $"DataSource={Path.Combine(Environment.CurrentDirectory, tenant.ConfigId)}.db" ; + tenant.Connection = $"DataSource={Path.Combine(Environment.CurrentDirectory, tenant.ConfigId)}.db"; break; } } @@ -47,4 +51,52 @@ public static ConnectionConfig GetConnectionConfig(this SysTenant tenant) }, }; } + + public static List GetTenantEntityTypes(TenantTypeEnum? tenantType = null) + { + return RepositorySetting.Entitys + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(s => IsTenantEntity(s, tenantType)) + .ToList(); + } + + public static bool IsTenantEntity(this Type u, TenantTypeEnum? tenantType = null) + { + var mta = u.GetCustomAttribute(); + if (mta is null) + { + return false; + } + + if (tenantType != null) + { + if (mta.TenantType != tenantType) + { + return false; + } + } + + return true; + } + + public static string GetTenantTableName(this Type type, ISqlSugarClient db, string id) + { + var entityInfo = db.EntityMaintenance.GetEntityInfo(type); + return $@"{entityInfo.DbTableName}_{id}"; + } + + public static string GetTenantTableName(this Type type, ISqlSugarClient db, SysTenant tenant) + { + return GetTenantTableName(type, db, tenant.Id.ToString()); + } + + public static void SetTenantTable(this ISqlSugarClient db, string id) + { + var types = GetTenantEntityTypes(TenantTypeEnum.Tables); + + foreach (var type in types) + { + db.MappingTables.Add(type.Name, type.GetTenantTableName(db, id)); + } + } } \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/UntilExtensions.cs b/Blog.Core.Common/Extensions/UntilExtensions.cs new file mode 100644 index 00000000..1ae503b2 --- /dev/null +++ b/Blog.Core.Common/Extensions/UntilExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Blog.Core.Common.Extensions; + +public static class UntilExtensions +{ + public static void AddOrModify(this IDictionary dic, TKey key, TValue value) + { + if (dic.TryGetValue(key, out _)) + { + dic[key] = value; + } + else + { + dic.Add(key, value); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index a6952e6e..36bf184c 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -2,8 +2,10 @@ using Blog.Core.Common.Extensions; using Blog.Core.Common.Helper; using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; using Magicodes.ExporterAndImporter.Excel; using Newtonsoft.Json; +using SqlSugar; using System; using System.Collections.Generic; using System.IO; @@ -11,8 +13,6 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Blog.Core.Model.Tenants; -using SqlSugar; namespace Blog.Core.Common.Seed { @@ -422,19 +422,62 @@ private static async Task SeedDataAsync(ISqlSugarClient db) public static async Task TenantSeedAsync(MyContext myContext) { var tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Db).ToListAsync(); - if (!tenants.Any()) + if (tenants.Any()) { - return; + Console.WriteLine($@"Init Multi Tenant Db"); + foreach (var tenant in tenants) + { + Console.WriteLine($@"Init Multi Tenant Db : {tenant.ConfigId}/{tenant.Name}"); + await InitTenantSeedAsync(myContext.Db.AsTenant(), tenant.GetConnectionConfig()); + } } - Console.WriteLine($@"Init Multi Tenant Db"); - foreach (var tenant in tenants) + tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Tables).ToListAsync(); + if (tenants.Any()) { - Console.WriteLine($@"Init Multi Tenant Db : {tenant.ConfigId}/{tenant.Name}"); - await InitTenantSeedAsync(myContext.Db.AsTenant(), tenant.GetConnectionConfig()); + await InitTenantSeedAsync(myContext, tenants); } } + #region 多租户 多表 初始化 + + private static async Task InitTenantSeedAsync(MyContext myContext, List tenants) + { + ConsoleHelper.WriteInfoLine($"Init Multi Tenant Tables : {myContext.Db.CurrentConnectionConfig.ConfigId}"); + + // 获取所有实体表-初始化租户业务表 + var entityTypes = TenantUtil.GetTenantEntityTypes(TenantTypeEnum.Tables); + if (!entityTypes.Any()) return; + + foreach (var sysTenant in tenants) + { + foreach (var entityType in entityTypes) + { + myContext.Db.CodeFirst + .As(entityType, entityType.GetTenantTableName(myContext.Db, sysTenant)) + .InitTables(entityType); + + Console.WriteLine($@"Init Tables:{entityType.GetTenantTableName(myContext.Db, sysTenant)}"); + } + + myContext.Db.SetTenantTable(sysTenant.Id.ToString()); + //多租户初始化种子数据 + await TenantSeedDataAsync(myContext.Db, TenantTypeEnum.Tables); + } + + ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Tables : {myContext.Db.CurrentConnectionConfig.ConfigId} created successfully!"); + } + + #endregion + + #region 多租户 多库 初始化 + + /// + /// 初始化多库 + /// + /// + /// + /// public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig config) { itenant.RemoveConnection(config.ConfigId); @@ -445,12 +488,10 @@ public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig c db.DbMaintenance.CreateDatabase(); ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Db : {config.ConfigId} Database created successfully!"); - Console.WriteLine($@"Init Multi Tenant Db : {config.ConfigId} Create Tables"); + // 获取所有实体表-初始化租户业务表 - var entityTypes = RepositorySetting.Entitys - .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) - .Where(s => s.IsDefined(typeof(MultiTenantAttribute), false)); + var entityTypes = TenantUtil.GetTenantEntityTypes(TenantTypeEnum.Db); if (!entityTypes.Any()) return; foreach (var entityType in entityTypes) { @@ -464,10 +505,12 @@ public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig c } //多租户初始化种子数据 - await TenantSeedDataAsync(db); + await TenantSeedDataAsync(db, TenantTypeEnum.Db); } - private static async Task TenantSeedDataAsync(ISqlSugarClient db) + #endregion + + private static async Task TenantSeedDataAsync(ISqlSugarClient db, TenantTypeEnum tenantType) { // 获取所有种子配置-初始化数据 var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes) @@ -481,12 +524,7 @@ private static async Task TenantSeedDataAsync(ISqlSugarClient db) } var eType = esd.GenericTypeArguments[0]; - if (eType.GetCustomAttribute() is null) - { - return false; - } - - return true; + return eType.IsTenantEntity(tenantType); }); if (!seedDataTypes.Any()) return; foreach (var seedType in seedDataTypes) diff --git a/Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs new file mode 100644 index 00000000..4ca1a7dd --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs @@ -0,0 +1,38 @@ +using Blog.Core.Model.Models; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed.SeedData; + +public class MultiBusinessDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new List() + { + new() + { + Id = 1001, + Name = "业务数据1", + Amount = 100, + }, + new() + { + Id = 1002, + Name = "业务数据2", + Amount = 1000, + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs new file mode 100644 index 00000000..e73d4603 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs @@ -0,0 +1,38 @@ +using Blog.Core.Model.Models; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed.SeedData; + +public class MultiBusinessSubDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new List() + { + new() + { + Id = 100, + MainId = 1001, + Memo = "子数据", + }, + new() + { + Id = 1001, + MainId = 1001, + Memo = "子数据2", + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs index 0a0fb5b4..f33f83b2 100644 --- a/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs +++ b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs @@ -50,6 +50,13 @@ public IEnumerable InitSeedData() DbType = DbType.Sqlite, Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, "ZhaoLiu.db"), }, + new SysTenant() + { + Id = 1000005, + ConfigId = "Tenant_5", + Name = "孙七", + TenantType = TenantTypeEnum.Tables, + }, }; } diff --git a/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs index 8a96a030..414a506b 100644 --- a/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs +++ b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs @@ -54,6 +54,14 @@ public async Task CustomizeSeedData(ISqlSugarClient db) Name = "赵六", TenantId = 1000004, //租户Id }, + new SysUserInfo() + { + Id = 10005, + LoginName = "sunqi", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "孙七", + TenantId = 1000005, //租户Id + }, }; var names = data.Select(s => s.LoginName).ToList(); diff --git a/Blog.Core.Model/Models/BusinessTable.cs b/Blog.Core.Model/Models/Tenant/BusinessTable.cs similarity index 100% rename from Blog.Core.Model/Models/BusinessTable.cs rename to Blog.Core.Model/Models/Tenant/BusinessTable.cs diff --git a/Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs b/Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs new file mode 100644 index 00000000..1ada394d --- /dev/null +++ b/Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs @@ -0,0 +1,14 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; + +namespace Blog.Core.Model.Models; + +/// +/// 多租户-多表方案 业务表 子表
+///
+[MultiTenant(TenantTypeEnum.Tables)] +public class MultiBusinessSubTable : BaseEntity +{ + public long MainId { get; set; } + public string Memo { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs b/Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs new file mode 100644 index 00000000..619bdaaf --- /dev/null +++ b/Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; +using SqlSugar; + +namespace Blog.Core.Model.Models; + +/// +/// 多租户-多表方案 业务表
+///
+[MultiTenant(TenantTypeEnum.Tables)] +public class MultiBusinessTable : BaseEntity +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } + + [Navigate(NavigateType.OneToMany, nameof(MultiBusinessSubTable.MainId))] + public List Child { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/SubLibraryBusinessTable.cs b/Blog.Core.Model/Models/Tenant/SubLibraryBusinessTable.cs similarity index 100% rename from Blog.Core.Model/Models/SubLibraryBusinessTable.cs rename to Blog.Core.Model/Models/Tenant/SubLibraryBusinessTable.cs diff --git a/Blog.Core.Model/Tenants/MultiTenantAttribute.cs b/Blog.Core.Model/Tenants/MultiTenantAttribute.cs index 9c3938a9..443745bf 100644 --- a/Blog.Core.Model/Tenants/MultiTenantAttribute.cs +++ b/Blog.Core.Model/Tenants/MultiTenantAttribute.cs @@ -3,10 +3,22 @@ namespace Blog.Core.Model.Tenants; /// -/// 标识 多租户-分库 的业务表
-/// 公共表无需区分 直接使用主库 各自业务在各自库中 +/// 标识 多租户 的业务表
+/// 默认设置是多库
+/// 公共表无需区分 直接使用主库 各自业务在各自库中
///
[AttributeUsage(AttributeTargets.Class)] public class MultiTenantAttribute : Attribute { + public MultiTenantAttribute() + { + } + + public MultiTenantAttribute(TenantTypeEnum tenantType) + { + TenantType = tenantType; + } + + + public TenantTypeEnum TenantType { get; set; } = TenantTypeEnum.Db; } \ No newline at end of file diff --git a/Blog.Core.Model/Tenants/TenantTypeEnum.cs b/Blog.Core.Model/Tenants/TenantTypeEnum.cs index 46ae999c..f4af3bda 100644 --- a/Blog.Core.Model/Tenants/TenantTypeEnum.cs +++ b/Blog.Core.Model/Tenants/TenantTypeEnum.cs @@ -20,4 +20,10 @@ public enum TenantTypeEnum ///
[Description("库隔离")] Db = 2, + + /// + /// 表隔离 + /// + [Description("表隔离")] + Tables = 3, } \ No newline at end of file diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 1845e958..828f9506 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -1,6 +1,10 @@ using Blog.Core.Common; +using Blog.Core.Common.DB; using Blog.Core.IRepository.Base; using Blog.Core.Model; +using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; +using Blog.Core.Repository.UnitOfWorks; using SqlSugar; using System; using System.Collections.Generic; @@ -8,11 +12,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; -using Blog.Core.Common.DB; -using Blog.Core.Common.HttpContextUser; -using Blog.Core.Model.Models; -using Blog.Core.Model.Tenants; -using Blog.Core.Repository.UnitOfWorks; namespace Blog.Core.Repository.Base { @@ -45,7 +44,8 @@ private ISqlSugarClient _db } //多租户 - if (typeof(TEntity).GetCustomAttribute() != null) + var mta = typeof(TEntity).GetCustomAttribute(); + if (mta is { TenantType: TenantTypeEnum.Db }) { //获取租户信息 租户信息可以提前缓存下来 if (App.User is { TenantId: > 0 }) diff --git a/Blog.Core.Tests/Repository_Test/OrmTest.cs b/Blog.Core.Tests/Repository_Test/OrmTest.cs new file mode 100644 index 00000000..fa4629f7 --- /dev/null +++ b/Blog.Core.Tests/Repository_Test/OrmTest.cs @@ -0,0 +1,73 @@ +using System; +using Autofac; +using Blog.Core.Common.Extensions; +using Blog.Core.IRepository.Base; +using Blog.Core.Model.Models; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Math; +using SqlSugar; +using Xunit; +using Xunit.Abstractions; + +namespace Blog.Core.Tests; + +public class OrmTest +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly IBaseRepository _baseRepository; + DI_Test dI_Test = new DI_Test(); + + public OrmTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + + var container = dI_Test.DICollections(); + + _baseRepository = container.Resolve>(); + _baseRepository.Db.Aop.OnLogExecuting = (sql, p) => + { + _testOutputHelper.WriteLine(""); + _testOutputHelper.WriteLine("==================FullSql=====================", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); + _testOutputHelper.WriteLine("【SQL语句】:" + sql); + _testOutputHelper.WriteLine(GetParas(p)); + _testOutputHelper.WriteLine("=============================================="); + _testOutputHelper.WriteLine(""); + }; + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } + + [Fact] + public void MultiTables() + { + var sql = _baseRepository.Db.Queryable() + .AS($@"{nameof(BlogArticle)}_TenantA") + .ToSqlString(); + //_testOutputHelper.WriteLine(sql); + + _baseRepository.Db.MappingTables.Add(nameof(BlogArticle), $@"{nameof(BlogArticle)}_TenantA"); + + var query = _baseRepository.Db.Queryable() + .LeftJoin((a, c) => a.bID == c.bID); + // query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticle), $@"{nameof(BlogArticle)}_TenantA"); + //query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticleComment), $@"{nameof(BlogArticleComment)}_TenantA"); + // query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticleComment), $@"{nameof(BlogArticleComment)}_TenantA"); + // query.QueryBuilder.AsTables.AddOrModify(nameof(SysUserInfo), $@"{nameof(SysUserInfo)}_TenantA"); + + + sql = query.ToSqlString(); + + _testOutputHelper.WriteLine(sql); + + sql = _baseRepository.Db.Deleteable().ToSqlString(); + _testOutputHelper.WriteLine(sql); + } +} \ No newline at end of file From 8d9aeed6229069b800396cd4a93fe0746afc544e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 24 Feb 2023 09:31:27 +0800 Subject: [PATCH 129/289] feat: change permission api para --- Blog.Core.Api/Blog.Core.xml | 3 ++- Blog.Core.Api/Controllers/PermissionController.cs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 8180287b..1464a9b1 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -374,12 +374,13 @@
- + 获取菜单 + diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index d3000766..70f8f689 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -61,13 +61,13 @@ public PermissionController(IPermissionServices permissionServices, IModuleServi ///
/// /// + /// /// // GET: api/User [HttpGet] - public async Task>> Get(int page = 1, string key = "") + public async Task>> Get(int page = 1, string key = "", int pageSize = 50) { PageModel permissions = new PageModel(); - int intPageSize = 50; if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { key = ""; @@ -88,7 +88,7 @@ public async Task>> Get(int page = 1, string - permissions = await _permissionServices.QueryPage(a => a.IsDeleted != true && (a.Name != null && a.Name.Contains(key)), page, intPageSize, " Id desc "); + permissions = await _permissionServices.QueryPage(a => a.IsDeleted != true && (a.Name != null && a.Name.Contains(key)), page, pageSize, " Id desc "); #region 单独处理 From 99bb71816543d59e89a83229ddf2c085c9d246a6 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 24 Feb 2023 17:37:58 +0800 Subject: [PATCH 130/289] feat:change quartz endtime --- Blog.Core.Api/wwwroot/BlogCore.Data.json/TasksQz.tsv | 8 ++++---- Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/TasksQz.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/TasksQz.tsv index c9f80890..2fa2c648 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/TasksQz.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/TasksQz.tsv @@ -2,16 +2,16 @@ { "Name": "博客管理", "JobGroup": "博客测试组", - "TriggerType": 1, - "Cron": "0 */10 * * * ?", + "TriggerType": 1, + "Cron": "0 */5 * * * ?", "AssemblyName": "Blog.Core.Tasks", "ClassName": "Job_Blogs_Quartz", "Remark": "", "RunTimes": 0, "BeginTime": "\/Date(1546272000000+0800)\/", - "EndTime": "\/Date(1640966400000+0800)\/", + "EndTime": "\/Date(8888888800000+0800)\/", "IntervalSecond": 0, - "CycleRunTimes": 0, + "CycleRunTimes": 0, "IsStart": true, "JobParams": 1, "IsDeleted": false, diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs index 173e996e..41310533 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs @@ -1,5 +1,6 @@ using Blog.Core.IServices; using Quartz; +using System; using System.Threading.Tasks; /// @@ -22,6 +23,7 @@ public async Task Execute(IJobExecutionContext context) } public async Task Run(IJobExecutionContext context) { + System.Console.WriteLine($"Job_Blogs_Quartz 执行 {DateTime.Now.ToShortTimeString()}"); var list = await _blogArticleServices.Query(); // 也可以通过数据库配置,获取传递过来的参数 JobDataMap data = context.JobDetail.JobDataMap; From 1f1525640c4ff5aeb97357ce042d4c9d983809cc Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 25 Feb 2023 14:02:29 +0800 Subject: [PATCH 131/289] Fixed #334 BUG --- Blog.Core.Common/Helper/SM/SM4.cs | 3 ++- Blog.Core.Gateway/Blog.Core.Gateway.csproj | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Common/Helper/SM/SM4.cs b/Blog.Core.Common/Helper/SM/SM4.cs index 4b1f1996..b4aa3ebf 100644 --- a/Blog.Core.Common/Helper/SM/SM4.cs +++ b/Blog.Core.Common/Helper/SM/SM4.cs @@ -10,7 +10,9 @@ public class SM4 private long GET_ULONG_BE(SByte[] b, int i) { +#pragma warning disable CS0675 // 对进行了带符号扩展的操作数使用了按位或运算符 long n2 = (b[i] & 0xFF) << 24 | (b[(i + 1)] & 0xFF) << 16 | (b[(i + 2)] & 0xFF) << 8 | b[(i + 3)] & 0xFF & 0xFFFFFFFF; +#pragma warning restore CS0675 // 对进行了带符号扩展的操作数使用了按位或运算符 return n2; } @@ -207,7 +209,6 @@ public SByte[] sm4_crypt_ecb(SM4_Context ctx, SByte[] input) int length = input.Length; SByte[] bins = new SByte[length]; SByte[] bous = new SByte[length]; - SByte[] output = null; Array.Copy(input, 0, bins, 0, length); diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.csproj b/Blog.Core.Gateway/Blog.Core.Gateway.csproj index 47398520..f784da5c 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.csproj +++ b/Blog.Core.Gateway/Blog.Core.Gateway.csproj @@ -15,9 +15,9 @@ - - - + + + From 82bea16164be19860f63ca1221dcba0710823f16 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Sat, 25 Feb 2023 16:27:45 +0800 Subject: [PATCH 132/289] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f37ce66..74076edc 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x #### 框架模块: - [x] 采用`仓储+服务+接口`的形式封装框架; - [x] 异步 async/await 开发; -- [x] 接入国产数据库ORM组件 —— SqlSugar,封装数据库操作; +- [x] 接入国产数据库ORM组件 —— SqlSugar,封装数据库操作,支持级联操作; - [x] 支持自由切换多种数据库,MySql/SqlServer/Sqlite/Oracle/Postgresql/达梦/人大金仓; - [x] 实现项目启动,自动生成种子数据 ✨; - [x] 实现数据库主键类型配置化,什么类型都可以自定义 ✨; @@ -87,6 +87,8 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 封装`Blog.Core.Webapi.Template`项目模板,一键重建自己的项目 ✨; - [x] 搭配多个前端案例供参考和借鉴:Blog.Vue、Blog.Admin、Nuxt.tbug、Blog.Mvp.Blazor ✨; - [x] 统一集成 IdentityServer4 认证 ✨; +- [x] 统一实现多租户; + 组件模块: - [x] 提供 Redis 做缓存处理; @@ -111,6 +113,9 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 新增 Kafka 消息队列,并配合实现EventBus ✨; - [x] 新增 微信公众号管理,并集成到Blog.Admin后台 ✨; - [x] 新增 - 数据部门权限; +- [x] 新增 - Log4net集成日志数据持久化到数据库; +- [x] 新增 - 多租户模式(单表,多表,多库三种模式); + 微服务模块: - [x] 可配合 Docker 实现容器化; From 6ad74a5385c447c3ca190450869f2fcd3b5c6732 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Sun, 26 Feb 2023 17:36:39 +0800 Subject: [PATCH 133/289] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 74076edc..9b88b0a0 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等,并自动持久化到数据库表🎶; - [x] 支持项目事务处理(若要分布式,用cap即可)✨; - [x] 设计4种 AOP 切面编程,功能涵盖:日志、缓存、审计、事务 ✨; +- [x] Log4net 多种日志自动生成到数据库中,目前支持MySql/SqlServer/Sqlite/Oracle/Postgresql🎉; - [x] 设计并支持按钮级别的RBAC权限控制,同时支持一键同步接口和菜单 🎶; - [x] 支持 T4 代码模板,自动生成每层代码; - [x] 或使用 DbFirst 一键创建自己项目的四层文件(支持多库); From 77f16b55e18a9105ad97fc35b11cee7ea87690e9 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 26 Feb 2023 18:32:49 +0800 Subject: [PATCH 134/289] feat: :flipper: update publish linux sh --- Blog.Core.Build.bat | 1 - Blog.Core.Gateway/Program.cs | 4 +++- ...tings.Development.json => appsettings.gw.Development.json} | 0 Blog.Core.Gateway/{appsettings.json => appsettings.gw.json} | 0 Blog.Core.Publish.Linux.sh | 3 ++- 5 files changed, 5 insertions(+), 3 deletions(-) rename Blog.Core.Gateway/{appsettings.Development.json => appsettings.gw.Development.json} (100%) rename Blog.Core.Gateway/{appsettings.json => appsettings.gw.json} (100%) diff --git a/Blog.Core.Build.bat b/Blog.Core.Build.bat index 632e841c..7d614aa4 100644 --- a/Blog.Core.Build.bat +++ b/Blog.Core.Build.bat @@ -1,4 +1,3 @@ -git pull @echo off diff --git a/Blog.Core.Gateway/Program.cs b/Blog.Core.Gateway/Program.cs index 1ba46816..9c1ba1ce 100644 --- a/Blog.Core.Gateway/Program.cs +++ b/Blog.Core.Gateway/Program.cs @@ -15,7 +15,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { - config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: true) + config.AddJsonFile("appsettings.gw.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.gw.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false) + .AddJsonFile("ocelot.json", optional: true, reloadOnChange: true) .AddJsonFile($"ocelot.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true); }) .ConfigureWebHostDefaults(webBuilder => diff --git a/Blog.Core.Gateway/appsettings.Development.json b/Blog.Core.Gateway/appsettings.gw.Development.json similarity index 100% rename from Blog.Core.Gateway/appsettings.Development.json rename to Blog.Core.Gateway/appsettings.gw.Development.json diff --git a/Blog.Core.Gateway/appsettings.json b/Blog.Core.Gateway/appsettings.gw.json similarity index 100% rename from Blog.Core.Gateway/appsettings.json rename to Blog.Core.Gateway/appsettings.gw.json diff --git a/Blog.Core.Publish.Linux.sh b/Blog.Core.Publish.Linux.sh index 05eb8103..7599f204 100644 --- a/Blog.Core.Publish.Linux.sh +++ b/Blog.Core.Publish.Linux.sh @@ -1,8 +1,9 @@ -git pull; + find .PublishFiles/ -type f -and ! -path '*/wwwroot/images/*' ! -name 'appsettings.*' |xargs rm -rf dotnet build; rm -rf /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles; dotnet publish -o /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles; +rm -rf /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles/WMBlog.db; # cp -r /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles ./; awk 'BEGIN { cmd="cp -ri /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles ./"; print "n" |cmd; }' echo "Successfully!!!! ^ please see the file .PublishFiles"; \ No newline at end of file From 56082ca0ecc464f1b16a378ff2be12bc822a0afe Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Mon, 27 Feb 2023 20:30:58 +0800 Subject: [PATCH 135/289] =?UTF-8?q?=E4=B8=BB=E8=A6=81=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?1.=E6=B7=BB=E5=8A=A0=E8=B0=83=E5=BA=A6=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E5=AD=90=E8=A1=A8=202.=E6=B7=BB=E5=8A=A0=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=9F=A5=E8=AF=A2=203.=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E6=97=A5=E5=BF=97=E6=8A=98=E7=BA=BF=E5=9B=BE?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=204.=E5=AE=8C=E5=96=84=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E6=8C=89=E5=BE=AA=E7=8E=AF=E6=AC=A1=E6=95=B0=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90=E5=90=8E=E4=B8=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=BB=93=E6=9D=9F=E4=BB=BB=E5=8A=A1=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E4=B8=8B=E6=AC=A1=E5=90=AF=E5=8A=A8=E9=A1=B9=E7=9B=AE=E4=BC=9A?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E6=89=A7=E8=A1=8C=E8=B0=83=E5=BA=A6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Model.xml | 90 +++ Blog.Core.Api/Blog.Core.xml | 18 + .../Controllers/TasksQzController.cs | 35 +- Blog.Core.Api/appsettings.json | 596 +++++++++--------- .../wwwroot/BlogCore.Data.json/Modules.tsv | 44 ++ .../wwwroot/BlogCore.Data.json/Permission.tsv | 42 ++ Blog.Core.Common/Seed/DBSeed.cs | 13 + Blog.Core.IServices/ITasksLogServices.cs | 19 + Blog.Core.Model/Models/TasksLog.cs | 87 +++ Blog.Core.Model/Models/TasksQz.cs | 4 + Blog.Core.Services/TasksLogServices.cs | 133 ++++ Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs | 43 +- .../Jobs/Job_AccessTrendLog_Quartz.cs | 3 +- .../QuartzNet/Jobs/Job_Blogs_Quartz.cs | 4 +- .../QuartzNet/Jobs/Job_OperateLog_Quartz.cs | 6 +- .../QuartzNet/SchedulerCenterServer.cs | 6 + 16 files changed, 824 insertions(+), 319 deletions(-) create mode 100644 Blog.Core.IServices/ITasksLogServices.cs create mode 100644 Blog.Core.Model/Models/TasksLog.cs create mode 100644 Blog.Core.Services/TasksLogServices.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 0e9ddbb0..6bbfd4fe 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1023,6 +1023,91 @@ 租户Id + + + 任务日志表 + + + + + 任务ID + + + + + 任务耗时 + + + + + 执行结果(0-失败 1-成功) + + + + + 运行时间 + + + + + 结束时间 + + + + + 执行参数 + + + + + 异常信息 + + + + + 异常堆栈 + + + + + 创建ID + + + + + 创建者 + + + + + 创建时间 + + + + + 修改ID + + + + + 修改者 + + + + + 修改时间 + + + + + 任务名称 + + + + + 任务分组 + + 任务计划表 @@ -1088,6 +1173,11 @@ 循环执行次数 + + + 已循环次数 + + 是否启动 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 1464a9b1..af6321da 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -576,6 +576,24 @@ + + + 获取任务运行日志 + + + + + + + + + 任务概况 + + + + + + 类别管理【无权限】 diff --git a/Blog.Core.Api/Controllers/TasksQzController.cs b/Blog.Core.Api/Controllers/TasksQzController.cs index 264fa195..e5c866c3 100644 --- a/Blog.Core.Api/Controllers/TasksQzController.cs +++ b/Blog.Core.Api/Controllers/TasksQzController.cs @@ -4,11 +4,14 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using Blog.Core.Common.Extensions; +using Blog.Core.Common.WebApiClients.HttpApis; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Blog.Core.Model.ViewModels; using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.Services; using Blog.Core.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -22,14 +25,16 @@ namespace Blog.Core.Controllers public class TasksQzController : ControllerBase { private readonly ITasksQzServices _tasksQzServices; + private readonly ITasksLogServices _tasksLogServices; private readonly ISchedulerCenter _schedulerCenter; private readonly IUnitOfWorkManage _unitOfWorkManage; - public TasksQzController(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IUnitOfWorkManage unitOfWorkManage) + public TasksQzController(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IUnitOfWorkManage unitOfWorkManage,ITasksLogServices tasksLogServices) { _unitOfWorkManage = unitOfWorkManage; - _tasksQzServices = tasksQzServices; + _tasksQzServices = tasksQzServices; _schedulerCenter = schedulerCenter; + _tasksLogServices = tasksLogServices; } /// @@ -523,6 +528,32 @@ public async Task> ExecuteJob(int jobId) } return data; } + /// + /// 获取任务运行日志 + /// + /// + /// + /// + /// + [HttpGet] + public async Task>> GetTaskLogs(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart =null, DateTime? runTimeEnd = null) + { + var model = await _tasksLogServices.GetTaskLogs(jobId, page, pageSize, runTimeStart, runTimeEnd); + return MessageModel>.Message(model.dataCount >= 0, "获取成功", model); + } + /// + /// 任务概况 + /// + /// + /// + /// + /// + [HttpGet] + public async Task> GetTaskOverview(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null, string type ="month") + { + var model = await _tasksLogServices.GetTaskOverview(jobId, runTimeStart, runTimeEnd, type); + return MessageModel.Message(true, "获取成功", model); + } } } diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index ea678174..3c117375 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -1,98 +1,98 @@ { - "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 - "Logging": { - "LogLevel": { - "Default": "Information", //加入Default否则log4net本地写入不了日志 - "Blog.Core.AuthHelper.ApiResponseHandler": "Error" + "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 + "Logging": { + "LogLevel": { + "Default": "Information", //加入Default否则log4net本地写入不了日志 + "Blog.Core.AuthHelper.ApiResponseHandler": "Error" + }, + "Debug": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Debug" + } + }, + "Log4Net": { + "Name": "Blog.Core" + } }, - "Debug": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } + "AllowedHosts": "*", + "Redis": { + "ConnectionString": "127.0.0.1:6319,password=admin" }, - "Console": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "Microsoft.Hosting.Lifetime": "Debug" - } + "RabbitMQ": { + "Enabled": false, + "Connection": "118.25.251.13", + "UserName": "", + "Password": "!", + "RetryCount": 3 }, - "Log4Net": { - "Name": "Blog.Core" - } - }, - "AllowedHosts": "*", - "Redis": { - "ConnectionString": "127.0.0.1:6319,password=admin" - }, - "RabbitMQ": { - "Enabled": false, - "Connection": "118.25.251.13", - "UserName": "", - "Password": "!", - "RetryCount": 3 - }, - "Kafka": { - "Enabled": false, - "Servers": "localhost:9092", - "Topic": "blog", - "GroupId": "blog-consumer", - "NumPartitions": 3 //主题分区数量 - }, - "EventBus": { - "Enabled": false, - "SubscriptionClientName": "Blog.Core" - }, - "AppSettings": { - "RedisCachingAOP": { - "Enabled": false - }, - "MemoryCachingAOP": { - "Enabled": true - }, - "LogAOP": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } + "Kafka": { + "Enabled": false, + "Servers": "localhost:9092", + "Topic": "blog", + "GroupId": "blog-consumer", + "NumPartitions": 3 //主题分区数量 }, - "TranAOP": { - "Enabled": true + "EventBus": { + "Enabled": false, + "SubscriptionClientName": "Blog.Core" }, - "SqlAOP": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": false - }, - "LogToConsole": { - "Enabled": true - } + "AppSettings": { + "RedisCachingAOP": { + "Enabled": false + }, + "MemoryCachingAOP": { + "Enabled": true + }, + "LogAOP": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + } + }, + "TranAOP": { + "Enabled": true + }, + "SqlAOP": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": false + }, + "LogToConsole": { + "Enabled": true + } + }, + "Date": "2018-08-28", + "SeedDBEnabled": true, //只生成表结构 + "SeedDBDataEnabled": true, //生成表,并初始化数据 + "Author": "Blog.Core", + "SvcName": "", // /svc/blog + "UseLoadTest": false }, - "Date": "2018-08-28", - "SeedDBEnabled": true, //只生成表结构 - "SeedDBDataEnabled": true, //生成表,并初始化数据 - "Author": "Blog.Core", - "SvcName": "", // /svc/blog - "UseLoadTest": false - }, - // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; - // *** 单库操作,把 MutiDBEnabled 设为false ***; - // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; - // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 + // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; + // *** 单库操作,把 MutiDBEnabled 设为false ***; + // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; + // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 - "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true - "MutiDBEnabled": false, //是否开启多库模式 - "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer - "DBS": [ - /* + "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true + "MutiDBEnabled": false, //是否开启多库模式 + "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer + "DBS": [ + /* 对应下边的 DBType MySql = 0, SqlServer = 1, @@ -102,225 +102,225 @@ Dm = 5,//达梦 Kdbndp = 6,//人大金仓 */ - { - "ConnId": "WMBLOG_SQLITE", - "DBType": 2, - "Enabled": true, - "HitRate": 50, // 值越大,优先级越高 - "Connection": "WMBlog.db" //sqlite只写数据库名就行 - }, - { - "ConnId": "WMBLOG_MSSQL_1", - "DBType": 1, - "Enabled": false, - "HitRate": 40, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", - "ProviderName": "System.Data.SqlClient" - }, - { - "ConnId": "WMBLOG_MSSQL_2", - "DBType": 1, - "Enabled": false, - "HitRate": 30, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", - "ProviderName": "System.Data.SqlClient" - }, - { - "ConnId": "WMBLOG_MYSQL", - "DBType": 0, - "Enabled": false, - "HitRate": 20, - "Connection": "server=.;Database=ddd;Uid=root;Pwd=123456;Port=10060;Allow User Variables=True;" - }, - { - "ConnId": "WMBLOG_MYSQL_2", - "DBType": 0, - "Enabled": true, - "HitRate": 20, - "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" - }, - { - "ConnId": "WMBLOG_ORACLE", - "DBType": 3, - "Enabled": false, - "HitRate": 10, - "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" - }, - { - "ConnId": "WMBLOG_DM", - "DBType": 5, - "Enabled": false, - "HitRate": 10, - "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" - }, - { - "ConnId": "WMBLOG_KDBNDP", - "DBType": 6, - "Enabled": true, - "HitRate": 10, - "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" - } - ], - "Audience": { - "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ - "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret - "Issuer": "Blog.Core", - "Audience": "wr" - }, - "Mongo": { - "ConnectionString": "mongodb://nosql.data", - "Database": "BlogCoreDb" - }, - "Startup": { - "Domain": "http://localhost:9291", - "Cors": { - "PolicyName": "CorsIpAccess", //策略名称 - "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 - // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 - // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 - "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" - }, - "AppConfigAlert": { - "Enabled": true - }, - "ApiName": "Blog.Core", - "IdentityServer4": { - "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 - "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 - "ApiName": "blog.core.api" // 资源服务器 - }, - "Authing": { - "Enabled": false, - "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", - "Audience": "63d51c4205c2849803be5178", - "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" - }, - "RedisMq": { - "Enabled": false //redis 消息队列 + { + "ConnId": "WMBLOG_SQLITE", + "DBType": 2, + "Enabled": true, + "HitRate": 50, // 值越大,优先级越高 + "Connection": "WMBlog.db" //sqlite只写数据库名就行 + }, + { + "ConnId": "WMBLOG_MSSQL_1", + "DBType": 1, + "Enabled": false, + "HitRate": 40, + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "ProviderName": "System.Data.SqlClient" + }, + { + "ConnId": "WMBLOG_MSSQL_2", + "DBType": 1, + "Enabled": false, + "HitRate": 30, + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "ProviderName": "System.Data.SqlClient" + }, + { + "ConnId": "WMBLOG_MYSQL", + "DBType": 0, + "Enabled": false, + "HitRate": 20, + "Connection": "server=localhost;Database=blog;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" + }, + { + "ConnId": "WMBLOG_MYSQL_2", + "DBType": 0, + "Enabled": false, + "HitRate": 20, + "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" + }, + { + "ConnId": "WMBLOG_ORACLE", + "DBType": 3, + "Enabled": false, + "HitRate": 10, + "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" + }, + { + "ConnId": "WMBLOG_DM", + "DBType": 5, + "Enabled": false, + "HitRate": 10, + "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" + }, + { + "ConnId": "WMBLOG_KDBNDP", + "DBType": 6, + "Enabled": false, + "HitRate": 10, + "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" + } + ], + "Audience": { + "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ + "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret + "Issuer": "Blog.Core", + "Audience": "wr" }, - "MiniProfiler": { - "Enabled": false //性能分析开启 + "Mongo": { + "ConnectionString": "mongodb://nosql.data", + "Database": "BlogCoreDb" }, - "Nacos": { - "Enabled": false //Nacos注册中心 - } - }, - "Middleware": { - "RequestResponseLog": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } - }, - "IPLog": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } + "Startup": { + "Domain": "http://localhost:9291", + "Cors": { + "PolicyName": "CorsIpAccess", //策略名称 + "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 + // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 + // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 + "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" + }, + "AppConfigAlert": { + "Enabled": true + }, + "ApiName": "Blog.Core", + "IdentityServer4": { + "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 + "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 + "ApiName": "blog.core.api" // 资源服务器 + }, + "Authing": { + "Enabled": false, + "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", + "Audience": "63d51c4205c2849803be5178", + "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" + }, + "RedisMq": { + "Enabled": false //redis 消息队列 + }, + "MiniProfiler": { + "Enabled": false //性能分析开启 + }, + "Nacos": { + "Enabled": false //Nacos注册中心 + } }, - "RecordAccessLogs": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - }, - "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," + "Middleware": { + "RequestResponseLog": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + } + }, + "IPLog": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + } + }, + "RecordAccessLogs": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + }, + "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," + }, + "SignalR": { + "Enabled": false + }, + "SignalRSendLog": { + "Enabled": false + }, + "QuartzNetJob": { + "Enabled": true + }, + "Consul": { + "Enabled": false + }, + "IpRateLimit": { + "Enabled": true + } }, - "SignalR": { - "Enabled": false + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each + "StackBlockedRequests": false, //False: Number of rejections should be recorded on another counter + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "IpWhitelist": [], //白名单 + "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], + "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], + "QuotaExceededResponse": { + "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + "HttpStatusCode": 429, //返回状态码 + "GeneralRules": [ //api规则,结尾一定要带* + { + "Endpoint": "*:/api/blog*", + "Period": "1m", + "Limit": 20 + }, + { + "Endpoint": "*/api/*", + "Period": "1s", + "Limit": 3 + }, + { + "Endpoint": "*/api/*", + "Period": "1m", + "Limit": 30 + }, + { + "Endpoint": "*/api/*", + "Period": "12h", + "Limit": 500 + } + ] + }, - "SignalRSendLog": { - "Enabled": false + "ConsulSetting": { + "ServiceName": "BlogCoreService", + "ServiceIP": "localhost", + "ServicePort": "9291", + "ServiceHealthCheck": "/healthcheck", + "ConsulAddress": "http://localhost:8500" }, - "QuartzNetJob": { - "Enabled": true + "PayInfo": { //建行聚合支付信息 + "MERCHANTID": "", //商户号 + "POSID": "", //柜台号 + "BRANCHID": "", //分行号 + "pubKey": "", //公钥 + "USER_ID": "", //操作员号 + "PASSWORD": "", //密码 + "OutAddress": "http://127.0.0.1:12345" //外联地址 }, - "Consul": { - "Enabled": false + "nacos": { + "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 + "DefaultTimeOut": 15000, // 默认超时时间 + "Namespace": "public", // 命名空间 + "ListenInterval": 10000, // 监听的频率 + "ServiceName": "blog.Core.Api", // 服务名 + "Port": "9291", // 服务端口号 + "RegisterEnabled": true // 是否直接注册nacos }, - "IpRateLimit": { - "Enabled": true + "LogFiedOutPutConfigs": { + "tcpAddressHost": "", // 输出elk的tcp连接地址 + "tcpAddressPort": 0, // 输出elk的tcp端口号 + "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 + { + "FiedName": "applicationName", + "FiedValue": "Blog.Core.Api" + } + ] } - }, - "IpRateLimiting": { - "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each - "StackBlockedRequests": false, //False: Number of rejections should be recorded on another counter - "RealIpHeader": "X-Real-IP", - "ClientIdHeader": "X-ClientId", - "IpWhitelist": [], //白名单 - "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], - "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], - "QuotaExceededResponse": { - "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", - "ContentType": "application/json", - "StatusCode": 429 - }, - "HttpStatusCode": 429, //返回状态码 - "GeneralRules": [ //api规则,结尾一定要带* - { - "Endpoint": "*:/api/blog*", - "Period": "1m", - "Limit": 20 - }, - { - "Endpoint": "*/api/*", - "Period": "1s", - "Limit": 3 - }, - { - "Endpoint": "*/api/*", - "Period": "1m", - "Limit": 30 - }, - { - "Endpoint": "*/api/*", - "Period": "12h", - "Limit": 500 - } - ] - - }, - "ConsulSetting": { - "ServiceName": "BlogCoreService", - "ServiceIP": "localhost", - "ServicePort": "9291", - "ServiceHealthCheck": "/healthcheck", - "ConsulAddress": "http://localhost:8500" - }, - "PayInfo": { //建行聚合支付信息 - "MERCHANTID": "", //商户号 - "POSID": "", //柜台号 - "BRANCHID": "", //分行号 - "pubKey": "", //公钥 - "USER_ID": "", //操作员号 - "PASSWORD": "", //密码 - "OutAddress": "http://127.0.0.1:12345" //外联地址 - }, - "nacos": { - "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 - "DefaultTimeOut": 15000, // 默认超时时间 - "Namespace": "public", // 命名空间 - "ListenInterval": 10000, // 监听的频率 - "ServiceName": "blog.Core.Api", // 服务名 - "Port": "9291", // 服务端口号 - "RegisterEnabled": true // 是否直接注册nacos - }, - "LogFiedOutPutConfigs": { - "tcpAddressHost": "", // 输出elk的tcp连接地址 - "tcpAddressPort": 0, // 输出elk的tcp端口号 - "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 - { - "FiedName": "applicationName", - "FiedValue": "Blog.Core.Api" - } - ] - } } diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv index 2bb130a1..76c55a22 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Modules.tsv @@ -1535,5 +1535,49 @@ "ModifyBy": null, "ModifyTime": "\/Date(1546272000000+0800)\/", "Id": 72 + }, + { + "IsDeleted": 0, + "ParentId": null, + "Name": "任务日志查询", + "LinkUrl": "\/api\/TasksQz\/GetTaskLogs", + "Area": null, + "Controller": null, + "Action": null, + "Icon": null, + "Code": null, + "OrderSort": 0, + "Description": null, + "IsMenu": 0, + "Enabled": 1, + "CreateId": 23, + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "Id": 73 + }, + { + "IsDeleted": 0, + "ParentId": null, + "Name": "任务概况", + "LinkUrl": "\/api\/TasksQz\/GetTaskOverview", + "Area": null, + "Controller": null, + "Action": null, + "Icon": null, + "Code": null, + "OrderSort": 0, + "Description": null, + "IsMenu": 0, + "Enabled": 1, + "CreateId": 23, + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "Id": 74 } ] diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index 52493a12..d1a5c3f8 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -2571,5 +2571,47 @@ "IsDeleted": 0, "Id": 122, "IsHide": 0 + }, + { + "Code": " ", + "Name": "日志查询", + "IsButton": 1, + "Pid": 76, + "Mid": 73, + "OrderSort": 0, + "Icon": null, + "Description": null, + "Enabled": 1, + "CreateId": 23, + "Func": "handleLog", + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "IsDeleted": 0, + "Id": 123, + "IsHide": 0 + }, + { + "Code": " ", + "Name": "任务概况", + "IsButton": 1, + "Pid": 76, + "Mid": 74, + "OrderSort": 0, + "Icon": null, + "Description": null, + "Enabled": 1, + "CreateId": 23, + "Func": "handleOverview", + "CreateBy": "后台总管理员", + "CreateTime": "\/Date(1546272000000+0800)\/", + "ModifyId": null, + "ModifyBy": null, + "ModifyTime": "\/Date(1546272000000+0800)\/", + "IsDeleted": 0, + "Id": 124, + "IsHide": 0 } ] diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 36bf184c..7b594109 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -313,6 +313,19 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) #endregion + #region TasksLog + + if (!await myContext.Db.Queryable().AnyAsync()) + { + Console.WriteLine("Table:TasksLog created success!"); + } + else + { + Console.WriteLine("Table:TasksLog already exists..."); + } + + #endregion + #region Department if (!await myContext.Db.Queryable().AnyAsync()) diff --git a/Blog.Core.IServices/ITasksLogServices.cs b/Blog.Core.IServices/ITasksLogServices.cs new file mode 100644 index 00000000..fb15c8dd --- /dev/null +++ b/Blog.Core.IServices/ITasksLogServices.cs @@ -0,0 +1,19 @@ + +using System; +using System.Threading.Tasks; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; + +namespace Blog.Core.IServices +{ + /// + /// ITasksLogServices + /// + public interface ITasksLogServices :IBaseServices + { + public Task> GetTaskLogs(int jobId, int page, int intPageSize,DateTime? runTime,DateTime? endTime); + public Task GetTaskOverview(int jobId, DateTime? runTime, DateTime? endTime, string type); + } +} + \ No newline at end of file diff --git a/Blog.Core.Model/Models/TasksLog.cs b/Blog.Core.Model/Models/TasksLog.cs new file mode 100644 index 00000000..b4317c95 --- /dev/null +++ b/Blog.Core.Model/Models/TasksLog.cs @@ -0,0 +1,87 @@ +using SqlSugar; +using System; + +namespace Blog.Core.Model.Models +{ + /// + /// 任务日志表 + /// + public class TasksLog : RootEntityTkey + { + /// + /// 任务ID + /// + public int JobId { get; set; } + /// + /// 任务耗时 + /// + public double TotalTime { get; set; } + /// + /// 执行结果(0-失败 1-成功) + /// + public bool RunResult { get; set; } + /// + /// 运行时间 + /// + public DateTime RunTime { get; set; } + /// + /// 结束时间 + /// + public DateTime EndTime { get; set; } + /// + /// 执行参数 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string RunPars { get; set; } + /// + /// 异常信息 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string ErrMessage { get; set; } + /// + /// 异常堆栈 + /// + [SugarColumn(Length = 2000, IsNullable = true)] + public string ErrStackTrace { get; set; } + /// + /// 创建ID + /// + [SugarColumn(IsNullable = true)] + public int? CreateId { get; set; } + /// + /// 创建者 + /// + [SugarColumn(Length = 50, IsNullable = true)] + public string CreateBy { get; set; } + /// + /// 创建时间 + /// + [SugarColumn(IsNullable = true)] + public DateTime CreateTime { get; set; } = DateTime.Now; + /// + /// 修改ID + /// + [SugarColumn(IsNullable = true)] + public int? ModifyId { get; set; } + /// + /// 修改者 + /// + [SugarColumn(Length = 100, IsNullable = true)] + public string ModifyBy { get; set; } + /// + /// 修改时间 + /// + [SugarColumn(IsNullable = true)] + public DateTime? ModifyTime { get; set; } = DateTime.Now; + /// + /// 任务名称 + /// + [SugarColumn(IsIgnore = true)] + public string Name { get; set; } + /// + /// 任务分组 + /// + [SugarColumn(IsIgnore = true)] + public string JobGroup { get; set; } + } +} diff --git a/Blog.Core.Model/Models/TasksQz.cs b/Blog.Core.Model/Models/TasksQz.cs index 5c812f79..1c07b60e 100644 --- a/Blog.Core.Model/Models/TasksQz.cs +++ b/Blog.Core.Model/Models/TasksQz.cs @@ -65,6 +65,10 @@ public class TasksQz : RootEntityTkey /// public int CycleRunTimes { get; set; } /// + /// 已循环次数 + /// + public int CycleHasRunTimes { get; set; } + /// /// 是否启动 /// public bool IsStart { get; set; } = false; diff --git a/Blog.Core.Services/TasksLogServices.cs b/Blog.Core.Services/TasksLogServices.cs new file mode 100644 index 00000000..eac9a739 --- /dev/null +++ b/Blog.Core.Services/TasksLogServices.cs @@ -0,0 +1,133 @@ +using System.Linq.Expressions; +using System; +using System.Threading.Tasks; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Services.BASE; +using Blog.Core.Common.Extensions; +using System.Drawing.Printing; +using SqlSugar; +using Blog.Core.Model; +using System.Collections.Generic; +using System.Linq; +using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime; + +namespace Blog.Core.Services +{ + public partial class TasksLogServices : BaseServices, ITasksLogServices + { + public async Task> GetTaskLogs(int jobId, int page, int intPageSize, DateTime? runTime, DateTime? endTime) + { + RefAsync totalCount = 0; + Expression> whereExpression = log => true; + if (jobId > 0) whereExpression = whereExpression.And(log => log.JobId == jobId); + var data = await this.Db.Queryable() + .LeftJoin((log, qz) => log.JobId == qz.Id) + .OrderByDescending((log) => log.RunTime) + .WhereIF(jobId > 0, (log) => log.JobId == jobId) + .WhereIF(runTime != null, (log) => log.RunTime >= runTime.Value) + .WhereIF(endTime != null, (log) => log.RunTime <= endTime.Value) + .Select((log, qz) => new TasksLog + { + RunPars = log.RunPars, + RunResult = log.RunResult, + RunTime = log.RunTime, + EndTime = log.EndTime, + ErrMessage = log.ErrMessage, + ErrStackTrace = log.ErrStackTrace, + TotalTime = log.TotalTime, + Name = qz.Name, + JobGroup = qz.JobGroup + + }) + .ToPageListAsync(page, intPageSize, totalCount); + return new PageModel(page, totalCount, intPageSize, data); + } + public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime? endTime,string type) + { + + //按年 + if ("year".Equals(type)) + { + + var days = endTime.Value.Year - runTime.Value.Year; + var dayArray = new List(); + while (days >= 0) + { + dayArray.Add(new DateTime(runTime.Value.Year + days,1,1)); + days--; + } + var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime.Year >= runTime.Value.Year && x.RunTime.Year <= endTime.Value.Year); ; ; //声名表 + + var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, + (x1, x2) => x1.ColumnName.Year == x2.RunTime.Year) + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.Year.ToString()+"年" + }).ToList().OrderBy(t => t.date); + return list; + }else if ("month".Equals(type)) + { + //按月 + var queryableLeft = this.Db.Reportable(ReportableDateType.MonthsInLast1years).ToQueryable(); //生成月份 //ReportableDateType.MonthsInLast1yea 表式近一年月份 并且queryable之后还能在where过滤 + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime.Year == runTime.Value.Year); //声名表 + + //月份和表JOIN + var list = queryableLeft + .LeftJoin(queryableRight, (x1, x2) => x2.RunTime.ToString("MM月") == x1.ColumnName.ToString("MM月")) + + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + //null的数据要为0所以不能用count + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.ToString("MM月") + } + ).ToList().OrderBy(t => t.date); + return list; + }else if ("day".Equals(type)) + { + //按日 + var time = runTime.Value; + var days = DateTime.DaysInMonth(time.Year, time.Month); + var dayArray = Enumerable.Range(1, days).Select(it => Convert.ToDateTime(time.ToString("yyyy-MM-" + it))).ToList();//转成时间数组 + var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); + var star = Convert.ToDateTime(runTime.Value.ToString("yyyy-MM-01 00:00:00")); + var end = Convert.ToDateTime(runTime.Value.ToString($"yyyy-MM-{days} 23:59:59")); + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= star && x.RunTime <= end); ;; //声名表 + + var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, + (x1, x2) => x1.ColumnName.Date == x2.RunTime.Date) + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.Day + }).ToList().OrderBy(t => t.date); + return list; + }else if ("hour".Equals(type)) + { + //按小时 + var time = runTime.Value; + var days = 24; + var dayArray = Enumerable.Range(0, days).Select(it => Convert.ToDateTime(time.ToString($"yyyy-MM-dd {it.ToString().PadLeft(2, '0')}:00:00"))).ToList();//转成时间数组 + var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= runTime.Value.Date && x.RunTime<= runTime.Value.Date.AddDays(1).AddMilliseconds(-1)); //声名表 + + var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, + (x1, x2) => x1.ColumnName.Hour == x2.RunTime.Hour) + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.Hour + }).ToList().OrderBy(t => t.date); + return list; + } + return null; + } + } +} diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs b/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs index 4a71fbf5..b4486dfc 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs @@ -1,5 +1,6 @@ using Blog.Core.Common.Helper; using Blog.Core.IServices; +using Blog.Core.Model.Models; using Quartz; using System; using System.Diagnostics; @@ -10,6 +11,12 @@ namespace Blog.Core.Tasks public class JobBase { public ITasksQzServices _tasksQzServices; + public ITasksLogServices _tasksLogServices; + public JobBase(ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + { + _tasksQzServices = tasksQzServices; + _tasksLogServices = tasksLogServices; + } /// /// 执行指定任务 /// @@ -17,40 +24,50 @@ public class JobBase /// public async Task ExecuteJob(IJobExecutionContext context, Func func) { - //记录Job时间 - Stopwatch stopwatch = new Stopwatch(); + //记录Job + TasksLog tasksLog = new TasksLog(); //JOBID int jobid = context.JobDetail.Key.Name.ObjToInt(); //JOB组名 string groupName = context.JobDetail.Key.Group; //日志 - string jobHistory = $"【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行开始】【Id:{jobid},组别:{groupName}】"; - //耗时 - double taskSeconds = 0; + tasksLog.JobId = jobid; + tasksLog.RunTime = DateTime.Now; + string jobHistory = $"【{tasksLog.RunTime.ToString("yyyy-MM-dd HH:mm:ss")}】【执行开始】【Id:{jobid},组别:{groupName}】"; try { - stopwatch.Start(); await func();//执行任务 - stopwatch.Stop(); - jobHistory += $",【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行成功】"; + tasksLog.EndTime = DateTime.Now; + tasksLog.RunResult = true; + jobHistory += $",【{tasksLog.EndTime.ToString("yyyy-MM-dd HH:mm:ss")}】【执行成功】"; + + JobDataMap jobPars = context.JobDetail.JobDataMap; + tasksLog.RunPars = jobPars.GetString("JobParam"); } catch (Exception ex) { - JobExecutionException e2 = new JobExecutionException(ex); + tasksLog.EndTime = DateTime.Now; + tasksLog.RunResult = false; + //JobExecutionException e2 = new JobExecutionException(ex); //true 是立即重新执行任务 - e2.RefireImmediately = true; - jobHistory += $",【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行失败:{ex.Message}】"; + //e2.RefireImmediately = true; + tasksLog.ErrMessage = ex.Message; + tasksLog.ErrStackTrace = ex.StackTrace; + jobHistory += $",【{tasksLog.EndTime.ToString("yyyy-MM-dd HH:mm:ss")}】【执行失败:{ex.Message}】"; } finally { - taskSeconds = Math.Round(stopwatch.Elapsed.TotalSeconds, 3); - jobHistory += $",【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行结束】(耗时:{taskSeconds}秒)"; + tasksLog.TotalTime = Math.Round((tasksLog.EndTime - tasksLog.RunTime).TotalSeconds,3); + jobHistory += $"(耗时:{tasksLog.TotalTime}秒)"; if (_tasksQzServices != null) { var model = await _tasksQzServices.QueryById(jobid); if (model != null) { + if(_tasksLogServices != null) await _tasksLogServices.Add(tasksLog); model.RunTimes += 1; + if (model.TriggerType == 0) model.CycleHasRunTimes += 1; + if (model.TriggerType == 0 && model.CycleRunTimes != 0 && model.CycleHasRunTimes >= model.CycleRunTimes) model.IsStart = false;//循环完善,当循环任务完成后,停止该任务,防止下次启动再次执行 var separator = "
"; // 这里注意数据库字段的长度问题,超过限制,会造成数据库remark不更新问题。 model.Remark = diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs index 743180c3..1dcc57ed 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs @@ -21,7 +21,8 @@ public class Job_AccessTrendLog_Quartz : JobBase, IJob private readonly IAccessTrendLogServices _accessTrendLogServices; private readonly IWebHostEnvironment _environment; - public Job_AccessTrendLog_Quartz(IAccessTrendLogServices accessTrendLogServices, ITasksQzServices tasksQzServices, IWebHostEnvironment environment) + public Job_AccessTrendLog_Quartz(IAccessTrendLogServices accessTrendLogServices, IWebHostEnvironment environment, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { _accessTrendLogServices = accessTrendLogServices; _environment = environment; diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs index 41310533..f116fe05 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs @@ -12,10 +12,10 @@ public class Job_Blogs_Quartz : JobBase, IJob { private readonly IBlogArticleServices _blogArticleServices; - public Job_Blogs_Quartz(IBlogArticleServices blogArticleServices, ITasksQzServices tasksQzServices) + public Job_Blogs_Quartz(IBlogArticleServices blogArticleServices, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { _blogArticleServices = blogArticleServices; - _tasksQzServices = tasksQzServices; } public async Task Execute(IJobExecutionContext context) { diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs index 8e9b3847..18c4c298 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs @@ -20,11 +20,11 @@ public class Job_OperateLog_Quartz : JobBase, IJob private readonly IOperateLogServices _operateLogServices; private readonly IWebHostEnvironment _environment; - public Job_OperateLog_Quartz(IOperateLogServices operateLogServices, ITasksQzServices tasksQzServices, IWebHostEnvironment environment) + public Job_OperateLog_Quartz(IOperateLogServices operateLogServices,IWebHostEnvironment environment, ITasksQzServices tasksQzServices,ITasksLogServices tasksLogServices) + :base(tasksQzServices, tasksLogServices) { _operateLogServices = operateLogServices; - _environment = environment; - _tasksQzServices = tasksQzServices; + _environment = environment; } public async Task Execute(IJobExecutionContext context) { diff --git a/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs b/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs index 8c21115c..474a2a9a 100644 --- a/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs +++ b/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs @@ -125,6 +125,12 @@ public async Task> AddScheduleJobAsync(TasksQz tasksQz) result.msg = $"该任务计划已经在执行:【{tasksQz.Name}】,请勿重复启动!"; return result; } + if(tasksQz.TriggerType == 0 && tasksQz.CycleRunTimes != 0 && tasksQz.CycleHasRunTimes>=tasksQz.CycleRunTimes) + { + result.success = false; + result.msg = $"该任务计划已完成:【{tasksQz.Name}】,无需重复启动,如需启动请修改已循环次数再提交"; + return result; + } #region 设置开始时间和结束时间 if (tasksQz.BeginTime == null) From 2d8d99e8d3a37084d0fb52c036ee4810d6014bce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Mar 2023 00:12:57 +0000 Subject: [PATCH 136/289] Bump MongoDB.Driver from 2.17.1 to 2.19.0 in /Blog.Core.Repository Bumps [MongoDB.Driver](https://github.com/mongodb/mongo-csharp-driver) from 2.17.1 to 2.19.0. - [Release notes](https://github.com/mongodb/mongo-csharp-driver/releases) - [Commits](https://github.com/mongodb/mongo-csharp-driver/compare/v2.17.1...v2.19.0) --- updated-dependencies: - dependency-name: MongoDB.Driver dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Blog.Core.Repository/Blog.Core.Repository.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Repository/Blog.Core.Repository.csproj b/Blog.Core.Repository/Blog.Core.Repository.csproj index 5c9764b0..19fb6d4c 100644 --- a/Blog.Core.Repository/Blog.Core.Repository.csproj +++ b/Blog.Core.Repository/Blog.Core.Repository.csproj @@ -8,7 +8,7 @@ - + From ff238ed1e71b396fb54c03377b774064d6404a21 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Tue, 7 Mar 2023 21:21:01 +0800 Subject: [PATCH 137/289] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b88b0a0..29d427ec 100644 --- a/README.md +++ b/README.md @@ -235,9 +235,9 @@ Contributions of any kind are welcome! ## 售后服务与支持 -鼓励作者,简单打赏,入微信群,随时随地解答我框架中(NetCore、Vue、DDD、IdentityServer4等)的疑难杂症。 -注意主要是帮忙解决bug和思路,不会远程授课,但是可以适当发我代码,我帮忙调试, -打赏的时候,备注自己的微信号,我拉你进群,两天内没回应,QQ私聊我(3143422472); +鼓励作者,简单打赏~~ +打赏的时候,备注自己的微信号,加个微信,交个朋友,两天内没回应,QQ私聊我(3143422472); +目前精力有限,主要针对企业级用户答疑,或者购买授权版的个人用户。 [赞赏列表](http://apk.neters.club/.doc/Contribution/) From 225b86c0dba29bf13d285792425ea5c052dc0ca5 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Thu, 9 Mar 2023 23:16:34 +0800 Subject: [PATCH 138/289] Update Blog.Core.Repository.csproj --- Blog.Core.Repository/Blog.Core.Repository.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Repository/Blog.Core.Repository.csproj b/Blog.Core.Repository/Blog.Core.Repository.csproj index 19fb6d4c..df05250e 100644 --- a/Blog.Core.Repository/Blog.Core.Repository.csproj +++ b/Blog.Core.Repository/Blog.Core.Repository.csproj @@ -6,8 +6,8 @@ - - + + From 192bdbd59e8d42792ca6140e0f16f4d8ad636909 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 11 Mar 2023 10:14:42 +0800 Subject: [PATCH 139/289] Update ValuesController.cs --- Blog.Core.Api/Controllers/ValuesController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 52eb0ea1..1347ca16 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -298,7 +298,6 @@ public object Post([FromBody] BlogArticle blogArticle, int id) /// /// [HttpPost] - [Route("TestPostPara")] [AllowAnonymous] public object TestPostPara(string name) { From d7190d677cfcf04f05e682deb63e51f9a678bc84 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 11 Mar 2023 11:07:20 +0800 Subject: [PATCH 140/289] feat: :+1: beautiful api --- Blog.Core.Api/Blog.Core.xml | 6 -- Blog.Core.Api/Controllers/BlogController.cs | 6 +- .../Controllers/DepartmentController.cs | 7 -- Blog.Core.Api/Controllers/ModuleController.cs | 5 +- .../Controllers/MonitorController.cs | 7 -- Blog.Core.Api/Controllers/NacosController.cs | 1 - Blog.Core.Api/Controllers/PayController.cs | 4 +- Blog.Core.Api/Controllers/RoleController.cs | 3 +- .../Controllers/TasksQzController.cs | 64 ++++++++----------- Blog.Core.Api/Controllers/TopicController.cs | 4 +- .../Controllers/TopicDetailController.cs | 5 +- .../Controllers/TransactionController.cs | 5 +- Blog.Core.Api/Controllers/UserController.cs | 7 +- .../Controllers/UserRoleController.cs | 3 +- .../Controllers/WeChatCompanyController.cs | 1 - .../Controllers/WeChatConfigController.cs | 1 - Blog.Core.Api/Controllers/WeChatController.cs | 5 +- .../Controllers/WeChatPushLogController.cs | 1 - .../Controllers/WeChatSubController.cs | 1 - Blog.Core.Services/TasksLogServices.cs | 30 +++++---- 20 files changed, 53 insertions(+), 113 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index af6321da..89cb3213 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -580,18 +580,12 @@ 获取任务运行日志 - - - 任务概况 - - - diff --git a/Blog.Core.Api/Controllers/BlogController.cs b/Blog.Core.Api/Controllers/BlogController.cs index 6525448e..83fad967 100644 --- a/Blog.Core.Api/Controllers/BlogController.cs +++ b/Blog.Core.Api/Controllers/BlogController.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Blog.Core.Common.Helper; using Blog.Core.IServices; using Blog.Core.Model; @@ -11,7 +8,6 @@ using Blog.Core.SwaggerHelper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using StackExchange.Profiling; using static Blog.Core.Extensions.CustomApiVersion; diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index 1674883f..bca7bf6d 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -4,17 +4,10 @@ using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Linq.Expressions; using System.Text; -using System.Threading.Tasks; namespace Blog.Core.Api.Controllers { diff --git a/Blog.Core.Api/Controllers/ModuleController.cs b/Blog.Core.Api/Controllers/ModuleController.cs index 334a8ea4..2da284ba 100644 --- a/Blog.Core.Api/Controllers/ModuleController.cs +++ b/Blog.Core.Api/Controllers/ModuleController.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Threading.Tasks; +using System.Linq.Expressions; using Blog.Core.Common.HttpContextUser; using Blog.Core.IServices; using Blog.Core.Model; diff --git a/Blog.Core.Api/Controllers/MonitorController.cs b/Blog.Core.Api/Controllers/MonitorController.cs index c0dbed52..77b4e5bf 100644 --- a/Blog.Core.Api/Controllers/MonitorController.cs +++ b/Blog.Core.Api/Controllers/MonitorController.cs @@ -6,19 +6,12 @@ using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using Blog.Core.Extensions.Middlewares; namespace Blog.Core.Controllers diff --git a/Blog.Core.Api/Controllers/NacosController.cs b/Blog.Core.Api/Controllers/NacosController.cs index a8701b39..e5223851 100644 --- a/Blog.Core.Api/Controllers/NacosController.cs +++ b/Blog.Core.Api/Controllers/NacosController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nacos.V2; -using System.Threading.Tasks; namespace Blog.Core.Api.Controllers { diff --git a/Blog.Core.Api/Controllers/PayController.cs b/Blog.Core.Api/Controllers/PayController.cs index 0cbe0541..6c05c249 100644 --- a/Blog.Core.Api/Controllers/PayController.cs +++ b/Blog.Core.Api/Controllers/PayController.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Blog.Core.Controllers { diff --git a/Blog.Core.Api/Controllers/RoleController.cs b/Blog.Core.Api/Controllers/RoleController.cs index 36df73a4..c96502c8 100644 --- a/Blog.Core.Api/Controllers/RoleController.cs +++ b/Blog.Core.Api/Controllers/RoleController.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Blog.Core.Common.HttpContextUser; +using Blog.Core.Common.HttpContextUser; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/TasksQzController.cs b/Blog.Core.Api/Controllers/TasksQzController.cs index e5c866c3..ca9d37bc 100644 --- a/Blog.Core.Api/Controllers/TasksQzController.cs +++ b/Blog.Core.Api/Controllers/TasksQzController.cs @@ -1,17 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -using System.Threading.Tasks; -using Blog.Core.Common.Extensions; -using Blog.Core.Common.WebApiClients.HttpApis; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Blog.Core.Model.ViewModels; using Blog.Core.Repository.UnitOfWorks; -using Blog.Core.Services; using Blog.Core.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -29,10 +22,10 @@ public class TasksQzController : ControllerBase private readonly ISchedulerCenter _schedulerCenter; private readonly IUnitOfWorkManage _unitOfWorkManage; - public TasksQzController(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IUnitOfWorkManage unitOfWorkManage,ITasksLogServices tasksLogServices) + public TasksQzController(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IUnitOfWorkManage unitOfWorkManage, ITasksLogServices tasksLogServices) { _unitOfWorkManage = unitOfWorkManage; - _tasksQzServices = tasksQzServices; + _tasksQzServices = tasksQzServices; _schedulerCenter = schedulerCenter; _tasksLogServices = tasksLogServices; } @@ -63,7 +56,7 @@ public async Task>> Get(int page = 1, string key item.Triggers = await _schedulerCenter.GetTaskStaus(item); } } - return MessageModel>.Message(data.dataCount >= 0, "获取成功", data); + return MessageModel>.Message(data.dataCount >= 0, "获取成功", data); } /// @@ -91,32 +84,33 @@ public async Task> Post([FromBody] TasksQz tasksQz) var ResuleModel = await _schedulerCenter.AddScheduleJobAsync(tasksQz); data.success = ResuleModel.success; if (ResuleModel.success) - { + { data.msg = $"{data.msg}=>启动成功=>{ResuleModel.msg}"; } else - { + { data.msg = $"{data.msg}=>启动失败=>{ResuleModel.msg}"; } } } else - { + { data.msg = "添加失败"; - } + } } catch (Exception) { throw; } finally - { if(data.success) + { + if (data.success) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); } - return data; + return data; } @@ -140,7 +134,7 @@ public async Task> Put([FromBody] TasksQz tasksQz) data.msg = "修改成功"; data.response = tasksQz?.Id.ObjToString(); if (tasksQz.IsStart) - { + { var ResuleModelStop = await _schedulerCenter.StopScheduleJobAsync(tasksQz); data.msg = $"{data.msg}=>停止:{ResuleModelStop.msg}"; var ResuleModelStar = await _schedulerCenter.AddScheduleJobAsync(tasksQz); @@ -168,7 +162,7 @@ public async Task> Put([FromBody] TasksQz tasksQz) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } return data; } @@ -212,7 +206,7 @@ public async Task> Delete(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -234,7 +228,7 @@ public async Task> StartJob(int jobId) var model = await _tasksQzServices.QueryById(jobId); if (model != null) { - _unitOfWorkManage.BeginTran(); + _unitOfWorkManage.BeginTran(); try { model.IsStart = true; @@ -270,7 +264,7 @@ public async Task> StartJob(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -326,10 +320,10 @@ public async Task> StopJob(int jobId) [HttpGet] public async Task> PauseJob(int jobId) { - var data = new MessageModel(); + var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); if (model != null) - { + { _unitOfWorkManage.BeginTran(); try { @@ -364,7 +358,7 @@ public async Task> PauseJob(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -384,7 +378,7 @@ public async Task> ResumeJob(int jobId) var model = await _tasksQzServices.QueryById(jobId); if (model != null) - { + { _unitOfWorkManage.BeginTran(); try { @@ -420,7 +414,7 @@ public async Task> ResumeJob(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -480,7 +474,7 @@ public async Task> ReCovery(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -493,7 +487,7 @@ public async Task> ReCovery(int jobId) /// 获取任务命名空间 /// /// - [HttpGet] + [HttpGet] public MessageModel> GetTaskNameSpace() { var baseType = typeof(IJob); @@ -506,7 +500,7 @@ public MessageModel> GetTaskNameSpace() var implementTypes = types.Where(x => x.IsClass).Select(item => new QuartzReflectionViewModel { nameSpace = item.Namespace, nameClass = item.Name, remark = "" }).ToList(); return MessageModel>.Success("获取成功", implementTypes); } - + /// /// 立即执行任务 /// @@ -531,12 +525,9 @@ public async Task> ExecuteJob(int jobId) /// /// 获取任务运行日志 /// - /// - /// - /// /// [HttpGet] - public async Task>> GetTaskLogs(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart =null, DateTime? runTimeEnd = null) + public async Task>> GetTaskLogs(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null) { var model = await _tasksLogServices.GetTaskLogs(jobId, page, pageSize, runTimeStart, runTimeEnd); return MessageModel>.Message(model.dataCount >= 0, "获取成功", model); @@ -544,12 +535,9 @@ public async Task>> GetTaskLogs(int jobId, int /// /// 任务概况 /// - /// - /// - /// /// [HttpGet] - public async Task> GetTaskOverview(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null, string type ="month") + public async Task> GetTaskOverview(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null, string type = "month") { var model = await _tasksLogServices.GetTaskOverview(jobId, runTimeStart, runTimeEnd, type); return MessageModel.Message(true, "获取成功", model); diff --git a/Blog.Core.Api/Controllers/TopicController.cs b/Blog.Core.Api/Controllers/TopicController.cs index 1fe2d4a3..308ad082 100644 --- a/Blog.Core.Api/Controllers/TopicController.cs +++ b/Blog.Core.Api/Controllers/TopicController.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; diff --git a/Blog.Core.Api/Controllers/TopicDetailController.cs b/Blog.Core.Api/Controllers/TopicDetailController.cs index 264fe2df..55af1a75 100644 --- a/Blog.Core.Api/Controllers/TopicDetailController.cs +++ b/Blog.Core.Api/Controllers/TopicDetailController.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Blog.Core.Common.Helper; +using Blog.Core.Common.Helper; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/TransactionController.cs b/Blog.Core.Api/Controllers/TransactionController.cs index dd6b0384..67ab576e 100644 --- a/Blog.Core.Api/Controllers/TransactionController.cs +++ b/Blog.Core.Api/Controllers/TransactionController.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Blog.Core.Repository.UnitOfWorks; diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 6a2a18fa..43f7f610 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Blog.Core.AuthHelper.OverWrite; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; @@ -13,7 +9,6 @@ using Blog.Core.Repository.UnitOfWorks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Blog.Core.Controllers { diff --git a/Blog.Core.Api/Controllers/UserRoleController.cs b/Blog.Core.Api/Controllers/UserRoleController.cs index d14d6a73..c21ec6f4 100644 --- a/Blog.Core.Api/Controllers/UserRoleController.cs +++ b/Blog.Core.Api/Controllers/UserRoleController.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatCompanyController.cs b/Blog.Core.Api/Controllers/WeChatCompanyController.cs index 4fa6eea5..dc12930b 100644 --- a/Blog.Core.Api/Controllers/WeChatCompanyController.cs +++ b/Blog.Core.Api/Controllers/WeChatCompanyController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatConfigController.cs b/Blog.Core.Api/Controllers/WeChatConfigController.cs index c597cb3f..1f3b705d 100644 --- a/Blog.Core.Api/Controllers/WeChatConfigController.cs +++ b/Blog.Core.Api/Controllers/WeChatConfigController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatController.cs b/Blog.Core.Api/Controllers/WeChatController.cs index 4c4877f6..9927eb78 100644 --- a/Blog.Core.Api/Controllers/WeChatController.cs +++ b/Blog.Core.Api/Controllers/WeChatController.cs @@ -1,11 +1,8 @@ -using System.IO; -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Blog.Core.Controllers { diff --git a/Blog.Core.Api/Controllers/WeChatPushLogController.cs b/Blog.Core.Api/Controllers/WeChatPushLogController.cs index 1fe1603d..af168091 100644 --- a/Blog.Core.Api/Controllers/WeChatPushLogController.cs +++ b/Blog.Core.Api/Controllers/WeChatPushLogController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatSubController.cs b/Blog.Core.Api/Controllers/WeChatSubController.cs index bd8d1759..94f982d2 100644 --- a/Blog.Core.Api/Controllers/WeChatSubController.cs +++ b/Blog.Core.Api/Controllers/WeChatSubController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Services/TasksLogServices.cs b/Blog.Core.Services/TasksLogServices.cs index eac9a739..49393cd4 100644 --- a/Blog.Core.Services/TasksLogServices.cs +++ b/Blog.Core.Services/TasksLogServices.cs @@ -5,12 +5,10 @@ using Blog.Core.Model.Models; using Blog.Core.Services.BASE; using Blog.Core.Common.Extensions; -using System.Drawing.Printing; using SqlSugar; using Blog.Core.Model; using System.Collections.Generic; using System.Linq; -using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime; namespace Blog.Core.Services { @@ -43,9 +41,8 @@ public async Task> GetTaskLogs(int jobId, int page, int intP .ToPageListAsync(page, intPageSize, totalCount); return new PageModel(page, totalCount, intPageSize, data); } - public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime? endTime,string type) + public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime? endTime, string type) { - //按年 if ("year".Equals(type)) { @@ -54,7 +51,7 @@ public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime var dayArray = new List(); while (days >= 0) { - dayArray.Add(new DateTime(runTime.Value.Year + days,1,1)); + dayArray.Add(new DateTime(runTime.Value.Year + days, 1, 1)); days--; } var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); @@ -66,10 +63,11 @@ public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime .Select((x1, x2) => new { 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), - date = x1.ColumnName.Year.ToString()+"年" + date = x1.ColumnName.Year.ToString() + "年" }).ToList().OrderBy(t => t.date); return list; - }else if ("month".Equals(type)) + } + else if ("month".Equals(type)) { //按月 var queryableLeft = this.Db.Reportable(ReportableDateType.MonthsInLast1years).ToQueryable(); //生成月份 //ReportableDateType.MonthsInLast1yea 表式近一年月份 并且queryable之后还能在where过滤 @@ -78,7 +76,7 @@ public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime //月份和表JOIN var list = queryableLeft .LeftJoin(queryableRight, (x1, x2) => x2.RunTime.ToString("MM月") == x1.ColumnName.ToString("MM月")) - + .GroupBy((x1, x2) => x1.ColumnName) .Select((x1, x2) => new { @@ -87,8 +85,10 @@ public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime date = x1.ColumnName.ToString("MM月") } ).ToList().OrderBy(t => t.date); + await Task.CompletedTask; return list; - }else if ("day".Equals(type)) + } + else if ("day".Equals(type)) { //按日 var time = runTime.Value; @@ -97,7 +97,7 @@ public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); var star = Convert.ToDateTime(runTime.Value.ToString("yyyy-MM-01 00:00:00")); var end = Convert.ToDateTime(runTime.Value.ToString($"yyyy-MM-{days} 23:59:59")); - var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= star && x.RunTime <= end); ;; //声名表 + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= star && x.RunTime <= end); ; ; //声名表 var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, (x1, x2) => x1.ColumnName.Date == x2.RunTime.Date) @@ -107,26 +107,30 @@ public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), date = x1.ColumnName.Day }).ToList().OrderBy(t => t.date); + await Task.CompletedTask; return list; - }else if ("hour".Equals(type)) + } + else if ("hour".Equals(type)) { //按小时 var time = runTime.Value; var days = 24; var dayArray = Enumerable.Range(0, days).Select(it => Convert.ToDateTime(time.ToString($"yyyy-MM-dd {it.ToString().PadLeft(2, '0')}:00:00"))).ToList();//转成时间数组 var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); - var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= runTime.Value.Date && x.RunTime<= runTime.Value.Date.AddDays(1).AddMilliseconds(-1)); //声名表 + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= runTime.Value.Date && x.RunTime <= runTime.Value.Date.AddDays(1).AddMilliseconds(-1)); //声名表 var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, - (x1, x2) => x1.ColumnName.Hour == x2.RunTime.Hour) + (x1, x2) => x1.ColumnName.Hour == x2.RunTime.Hour) .GroupBy((x1, x2) => x1.ColumnName) .Select((x1, x2) => new { 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), date = x1.ColumnName.Hour }).ToList().OrderBy(t => t.date); + await Task.CompletedTask; return list; } + await Task.CompletedTask; return null; } } From d1288b103ecf582d5491712f63e5dbd8c05048dc Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Thu, 23 Mar 2023 16:39:57 +0800 Subject: [PATCH 141/289] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29d427ec..ee36dc2c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x #### 框架模块: - [x] 采用`仓储+服务+接口`的形式封装框架; -- [x] 异步 async/await 开发; +- [x] 自定义项目模板 `CreateYourProject.bat` ,可以一键生成自己的项目;🎶 +- [x] 异步 async/await 开发; - [x] 接入国产数据库ORM组件 —— SqlSugar,封装数据库操作,支持级联操作; - [x] 支持自由切换多种数据库,MySql/SqlServer/Sqlite/Oracle/Postgresql/达梦/人大金仓; - [x] 实现项目启动,自动生成种子数据 ✨; From 798a7f64228960d181fb296aace484a915a2b919 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Fri, 24 Mar 2023 21:24:24 +0800 Subject: [PATCH 142/289] =?UTF-8?q?=E8=AE=A2=E9=98=85=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Controllers/TrojanController.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Controllers/TrojanController.cs b/Blog.Core.Api/Controllers/TrojanController.cs index 8ec64c8a..cdb010e8 100644 --- a/Blog.Core.Api/Controllers/TrojanController.cs +++ b/Blog.Core.Api/Controllers/TrojanController.cs @@ -15,6 +15,7 @@ using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Math; namespace Blog.Core.Controllers { @@ -357,9 +358,12 @@ public async Task>> AddUrlServers(TrojanUrlS private string GetSplice(TrojanServers item,string passwordshow) { if ("0".Equals(item.servertype)) - return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?allowinsecure=0&tfo=0&peer={(string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress)}#{item.servername}"; + return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?allowinsecure=0&tfo=0&fp=chrome&peer={(string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress)}#{item.servername}"; else if ("1".Equals(item.servertype)) - return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?wspath={item.serverpath}&ws=1&peer={(string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress)}#{item.servername}"; + { + var sni = string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress; + return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?wspath={item.serverpath}&ws=1&peer={sni}&path={item.serverpath}&host={sni}&fp=chrome&type=ws&sni={sni}#{item.servername}"; + } else return $"servertype:({item.servertype})错误"; } From 042c4a6c4497437ac09f93d4c9ec8bbb178a92c1 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sun, 26 Mar 2023 15:36:45 +0800 Subject: [PATCH 143/289] fix bug --- Blog.Core.Api/Controllers/TrojanController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Controllers/TrojanController.cs b/Blog.Core.Api/Controllers/TrojanController.cs index cdb010e8..d8d005dc 100644 --- a/Blog.Core.Api/Controllers/TrojanController.cs +++ b/Blog.Core.Api/Controllers/TrojanController.cs @@ -357,11 +357,12 @@ public async Task>> AddUrlServers(TrojanUrlS } private string GetSplice(TrojanServers item,string passwordshow) { + var sni = string.IsNullOrEmpty(item.serverpeer) ? item.serveraddress : item.serverpeer; if ("0".Equals(item.servertype)) - return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?allowinsecure=0&tfo=0&fp=chrome&peer={(string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress)}#{item.servername}"; + return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?allowinsecure=0&tfo=0&fp=chrome&peer={sni}&host={sni}&sni={sni}#{item.servername}"; else if ("1".Equals(item.servertype)) { - var sni = string.IsNullOrEmpty(item.serverpeer) ? item.serverpeer : item.serveraddress; + return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?wspath={item.serverpath}&ws=1&peer={sni}&path={item.serverpath}&host={sni}&fp=chrome&type=ws&sni={sni}#{item.servername}"; } else From 67efee3ee1bd7d8337ee3e4af2667c1139e93b11 Mon Sep 17 00:00:00 2001 From: Nine Date: Thu, 30 Mar 2023 15:38:17 +0800 Subject: [PATCH 144/289] =?UTF-8?q?Sqlsugar=20=E5=88=86=E8=A1=A8=20CRUD=20?= =?UTF-8?q?demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.xml | 44 ++++ .../Controllers/SplitDemoController.cs | 199 ++++++++++++++++++ Blog.Core.Api/Program.cs | 4 + Blog.Core.Api/Startup.cs | 4 + Blog.Core.Common/Helper/NumberConverter.cs | 174 +++++++++++++++ Blog.Core.Common/Seed/DBSeed.cs | 2 +- Blog.Core.IServices/BASE/IBaseServices.cs | 10 +- Blog.Core.IServices/ISplitDemoServices.cs | 15 ++ Blog.Core.Model/Models/SplitDemo.cs | 27 +++ Blog.Core.Repository/BASE/BaseRepository.cs | 72 ++++++- Blog.Core.Repository/BASE/IBaseRepository.cs | 40 ++++ Blog.Core.Services/BASE/BaseServices.cs | 31 +++ Blog.Core.Services/SplitDemoServices.cs | 23 ++ 13 files changed, 642 insertions(+), 3 deletions(-) create mode 100644 Blog.Core.Api/Controllers/SplitDemoController.cs create mode 100644 Blog.Core.Common/Helper/NumberConverter.cs create mode 100644 Blog.Core.IServices/ISplitDemoServices.cs create mode 100644 Blog.Core.Model/Models/SplitDemo.cs create mode 100644 Blog.Core.Services/SplitDemoServices.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 89cb3213..9891d5d9 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1249,6 +1249,50 @@ + + + 分表demo + + + + + 分页获取数据 + + + + + + + + + + + 根据ID获取信息 + + + + + + + 添加一条测试数据 + + + + + + + 修改一条测试数据 + + + + + + + 根据id删除数据 + + + + 多租户-多库方案 测试 diff --git a/Blog.Core.Api/Controllers/SplitDemoController.cs b/Blog.Core.Api/Controllers/SplitDemoController.cs new file mode 100644 index 00000000..fb3c03c6 --- /dev/null +++ b/Blog.Core.Api/Controllers/SplitDemoController.cs @@ -0,0 +1,199 @@ +using Blog.Core.IServices; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Linq.Expressions; + +namespace Blog.Core.Api.Controllers +{ + /// + /// 分表demo + /// + [Route("api/[controller]/[action]")] + [ApiController] + [Authorize(Permissions.Name)] + public class SplitDemoController : ControllerBase + { + readonly ISplitDemoServices splitDemoServices; + readonly IUnitOfWorkManage unitOfWorkManage; + public SplitDemoController(ISplitDemoServices _splitDemoServices, IUnitOfWorkManage _unitOfWorkManage) + { + splitDemoServices = _splitDemoServices; + unitOfWorkManage = _unitOfWorkManage; + } + + /// + /// 分页获取数据 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + [AllowAnonymous] + public async Task>> Get(DateTime beginTime, DateTime endTime, int page = 1, string key = "", int pageSize = 10) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) + { + key = ""; + } + Expression> whereExpression = a => (a.Name != null && a.Name.Contains(key)); + var data = await splitDemoServices.QueryPageSplit(whereExpression, beginTime, endTime, page, pageSize, " Id desc "); + return MessageModel>.Message(data.dataCount >= 0, "获取成功", data); + } + + /// + /// 根据ID获取信息 + /// + /// + /// + [HttpGet] + [AllowAnonymous] + public async Task> GetById(long id) + { + var data = new MessageModel(); + var model = await splitDemoServices.QueryByIdSplit(id); + if (model != null) + { + return MessageModel.Success("获取成功", model); + } + else + { + return MessageModel.Fail("获取失败"); + } + } + + /// + /// 添加一条测试数据 + /// + /// + /// + [HttpPost] + [AllowAnonymous] + public async Task> Post([FromBody] SplitDemo splitDemo) + { + var data = new MessageModel(); + unitOfWorkManage.BeginTran(); + var id = (await splitDemoServices.AddSplit(splitDemo)); + data.success = (id == null ? false : true); + try + { + if (data.success) + { + data.response = id.FirstOrDefault().ToString(); + data.msg = "添加成功"; + } + else + { + data.msg = "添加失败"; + } + } + catch (Exception) + { + throw; + } + finally + { + if (data.success) + unitOfWorkManage.CommitTran(); + else + unitOfWorkManage.RollbackTran(); + } + return data; + } + + /// + /// 修改一条测试数据 + /// + /// + /// + [HttpPut] + [AllowAnonymous] + public async Task> Put([FromBody] SplitDemo splitDemo) + { + var data = new MessageModel(); + if (splitDemo != null && splitDemo.Id > 0) + { + unitOfWorkManage.BeginTran(); + data.success = await splitDemoServices.UpdateSplit(splitDemo, splitDemo.CreateTime); + try + { + if (data.success) + { + data.msg = "修改成功"; + data.response = splitDemo?.Id.ObjToString(); + } + else + { + data.msg = "修改失败"; + } + } + catch (Exception) + { + throw; + } + finally + { + if (data.success) + unitOfWorkManage.CommitTran(); + else + unitOfWorkManage.RollbackTran(); + } + } + return data; + } + + /// + /// 根据id删除数据 + /// + /// + /// + [HttpDelete] + [AllowAnonymous] + public async Task> Delete(long id) + { + var data = new MessageModel(); + + var model = await splitDemoServices.QueryByIdSplit(id); + if (model != null) + { + unitOfWorkManage.BeginTran(); + data.success = await splitDemoServices.DeleteSplit(model,model.CreateTime); + try + { + data.response = id.ObjToString(); + if (data.success) + { + data.msg = "删除成功"; + } + else + { + data.msg = "删除失败"; + } + + } + catch (Exception) + { + throw; + } + finally + { + if (data.success) + unitOfWorkManage.CommitTran(); + else + unitOfWorkManage.RollbackTran(); + } + } + else + { + data.msg = "不存在"; + } + return data; + + } + } +} diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index f5223beb..2a75498e 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -7,6 +7,7 @@ using Blog.Core; using Blog.Core.Common; using Blog.Core.Common.Core; +using Blog.Core.Common.Helper; using Blog.Core.Common.LogHelper; using Blog.Core.Extensions; using Blog.Core.Extensions.Apollo; @@ -14,6 +15,7 @@ using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; +using Blog.Core.Model; using Blog.Core.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -111,6 +113,8 @@ //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; options.SerializerSettings.Converters.Add(new StringEnumConverter()); + //将long类型转为string + options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); }) //.AddFluentValidation(config => //{ diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index bc1630f1..4f99b621 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -3,6 +3,7 @@ using System.Text; using Autofac; using Blog.Core.Common; +using Blog.Core.Common.Helper; using Blog.Core.Common.LogHelper; using Blog.Core.Common.Seed; using Blog.Core.Extensions; @@ -10,6 +11,7 @@ using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; +using Blog.Core.Model; using Blog.Core.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -123,6 +125,8 @@ public void ConfigureServices(IServiceCollection services) options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; //添加Enum转string options.SerializerSettings.Converters.Add(new StringEnumConverter()); + //将long类型转为string + options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); }); services.Replace(ServiceDescriptor.Transient()); diff --git a/Blog.Core.Common/Helper/NumberConverter.cs b/Blog.Core.Common/Helper/NumberConverter.cs new file mode 100644 index 00000000..27890faf --- /dev/null +++ b/Blog.Core.Common/Helper/NumberConverter.cs @@ -0,0 +1,174 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Helper +{ + /// + /// + /// 大数据json序列化重写 + /// + public sealed class NumberConverter : JsonConverter + { + /// + /// 转换成字符串的类型 + /// + private readonly NumberConverterShip _ship; + + /// + /// 大数据json序列化重写实例化 + /// + public NumberConverter() + { + _ship = (NumberConverterShip)0xFF; + } + + /// + /// 大数据json序列化重写实例化 + /// + /// 转换成字符串的类型 + public NumberConverter(NumberConverterShip ship) + { + _ship = ship; + } + + /// + /// + /// 确定此实例是否可以转换指定的对象类型。 + /// + /// 对象的类型。 + /// 如果此实例可以转换指定的对象类型,则为:true,否则为:false + public override bool CanConvert(Type objectType) + { + var typecode = Type.GetTypeCode(objectType.Name.Equals("Nullable`1") ? objectType.GetGenericArguments().First() : objectType); + switch (typecode) + { + case TypeCode.Decimal: + return (_ship & NumberConverterShip.Decimal) == NumberConverterShip.Decimal; + case TypeCode.Double: + return (_ship & NumberConverterShip.Double) == NumberConverterShip.Double; + case TypeCode.Int64: + return (_ship & NumberConverterShip.Int64) == NumberConverterShip.Int64; + case TypeCode.UInt64: + return (_ship & NumberConverterShip.UInt64) == NumberConverterShip.UInt64; + case TypeCode.Single: + return (_ship & NumberConverterShip.Single) == NumberConverterShip.Single; + default: return false; + } + } + + /// + /// + /// 读取对象的JSON表示。 + /// + /// 中读取。 + /// 对象的类型。 + /// 正在读取的对象的现有值。 + /// 调用的序列化器实例。 + /// 对象值。 + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return AsType(reader.Value.ToString(), objectType); + } + + /// + /// 字符串格式数据转其他类型数据 + /// + /// 输入的字符串 + /// 目标格式 + /// 转换结果 + public static object AsType(string input, Type destinationType) + { + try + { + var converter = TypeDescriptor.GetConverter(destinationType); + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(null, null, input); + } + + converter = TypeDescriptor.GetConverter(typeof(string)); + if (converter.CanConvertTo(destinationType)) + { + return converter.ConvertTo(null, null, input, destinationType); + } + } + catch + { + return null; + } + return null; + } + + /// + /// + /// 写入对象的JSON表示形式。 + /// + /// 要写入的 。 + /// 要写入对象值 + /// 调用的序列化器实例。 + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else + { + var objectType = value.GetType(); + var typeCode = Type.GetTypeCode(objectType.Name.Equals("Nullable`1") ? objectType.GetGenericArguments().First() : objectType); + switch (typeCode) + { + case TypeCode.Decimal: + writer.WriteValue(((decimal)value).ToString("f6")); + break; + case TypeCode.Double: + writer.WriteValue(((double)value).ToString("f4")); + break; + case TypeCode.Single: + writer.WriteValue(((float)value).ToString("f2")); + break; + default: + writer.WriteValue(value.ToString()); + break; + } + } + } + } + + /// + /// 转换成字符串的类型 + /// + [Flags] + public enum NumberConverterShip + { + /// + /// 长整数 + /// + Int64 = 1, + + /// + /// 无符号长整数 + /// + UInt64 = 2, + + /// + /// 浮点数 + /// + Single = 4, + + /// + /// 双精度浮点数 + /// + Double = 8, + + /// + /// 大数字 + /// + Decimal =16 + } +} diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 7b594109..fb13768a 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -109,7 +109,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!myContext.Db.DbMaintenance.IsAnyTable(t.Name)) { Console.WriteLine(t.Name); - myContext.Db.CodeFirst.InitTables(t); + myContext.Db.CodeFirst.SplitTables().InitTables(t); } }); ConsoleHelper.WriteSuccessLine($"Tables created successfully!"); diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index 7856f8bf..4091b978 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -23,7 +23,7 @@ public interface IBaseServices where TEntity : class Task DeleteById(object id); Task Delete(TEntity model); - + Task DeleteByIds(object[] ids); Task Update(TEntity model); @@ -59,6 +59,14 @@ Task> QueryMuch( Expression> selectExpression, Expression> whereLambda = null) where T : class, new(); Task> QueryPage(PaginationModel pagination); + + #region 分表 + Task QueryByIdSplit(object objId); + Task> AddSplit(TEntity entity); + Task DeleteSplit(TEntity entity, DateTime dateTime); + Task UpdateSplit(TEntity entity, DateTime dateTime); + Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null); + #endregion } } diff --git a/Blog.Core.IServices/ISplitDemoServices.cs b/Blog.Core.IServices/ISplitDemoServices.cs new file mode 100644 index 00000000..55215761 --- /dev/null +++ b/Blog.Core.IServices/ISplitDemoServices.cs @@ -0,0 +1,15 @@ + + +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; +using System.Threading.Tasks; + +namespace Blog.Core.IServices +{ + /// + /// sysUserInfoServices + /// + public interface ISplitDemoServices : IBaseServices + { + } +} diff --git a/Blog.Core.Model/Models/SplitDemo.cs b/Blog.Core.Model/Models/SplitDemo.cs new file mode 100644 index 00000000..26329935 --- /dev/null +++ b/Blog.Core.Model/Models/SplitDemo.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Blog.Core.Model.Models +{ + [SplitTable(SplitType.Day)]//按年分表 (自带分表支持 年、季、月、周、日) + [SugarTable("SplitDemo_{year}{month}{day}")]//3个变量必须要有,这么设计为了兼容开始按年,后面改成按月、按日 + public class SplitDemo + { + [SugarColumn(IsPrimaryKey = true)] + public long Id { get; set; } + + public string Name { get; set; } + + [SugarColumn(IsNullable = true)]//设置为可空字段 (更多用法看文档 迁移) + public DateTime UpdateTime { get; set; } + + [SplitField] //分表字段 在插入的时候会根据这个字段插入哪个表,在更新删除的时候用这个字段找出相关表 + public DateTime CreateTime { get; set; } + } +} diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 828f9506..3048baa8 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -5,6 +5,7 @@ using Blog.Core.Model.Models; using Blog.Core.Model.Tenants; using Blog.Core.Repository.UnitOfWorks; +using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime; using SqlSugar; using System; using System.Collections.Generic; @@ -127,7 +128,6 @@ public async Task Add(TEntity entity) return await insert.ExecuteReturnIdentityAsync(); } - /// /// 写入实体数据 /// @@ -557,5 +557,75 @@ public async Task> QueryTabsPage( // groupName = s.groupName, // jobName = s.jobName // }, exp, s => new { s.uID, s.uRealName, s.groupName, s.jobName }, model.currentPage, model.pageSize, model.orderField + " " + model.orderType); + #region Split分表基础接口 (基础CRUD) + /// + /// 分页查询[使用版本,其他分页未测试] + /// + /// 条件表达式 + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc + /// + public async Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null) + { + RefAsync totalCount = 0; + var list = await _db.Queryable().SplitTable(beginTime, endTime) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); + var data= new PageModel(pageIndex, totalCount, pageSize, list); + return data; + } + /// + /// 写入实体数据 + /// + /// 数据实体 + /// + public async Task> AddSplit(TEntity entity) + { + var insert = _db.Insertable(entity).SplitTable(); + //插入并返回雪花ID并且自动赋值ID  + return await insert.ExecuteReturnSnowflakeIdListAsync(); + } + + /// + /// 更新实体数据 + /// + /// 数据实体 + /// + public async Task UpdateSplit(TEntity entity, DateTime dateTime) + { + //直接根据实体集合更新 (全自动 找表更新) + //return await _db.Updateable(entity).SplitTable().ExecuteCommandAsync();//,SplitTable不能少 + + //精准找单个表 + var tableName = _db.SplitHelper().GetTableName(dateTime);//根据时间获取表名 + return await _db.Updateable(entity).AS(tableName).ExecuteCommandHasChangeAsync(); + } + /// + /// 删除数据 + /// + /// + /// + /// + public async Task DeleteSplit(TEntity entity,DateTime dateTime) + { + ////直接根据实体集合删除 (全自动 找表插入),返回受影响数 + //return await _db.Deleteable(entity).SplitTable().ExecuteCommandAsync();//,SplitTable不能少 + + //精准找单个表 + var tableName = _db.SplitHelper().GetTableName(dateTime);//根据时间获取表名 + return await _db.Deleteable().AS(tableName).Where(entity).ExecuteCommandHasChangeAsync(); + } + /// + /// 根据ID查找数据 + /// + /// + /// + public async Task QueryByIdSplit(object objId) + { + return await _db.Queryable().In(objId).SplitTable(tabs => tabs).SingleAsync(); + } + #endregion } } \ No newline at end of file diff --git a/Blog.Core.Repository/BASE/IBaseRepository.cs b/Blog.Core.Repository/BASE/IBaseRepository.cs index 29783505..5f70a4be 100644 --- a/Blog.Core.Repository/BASE/IBaseRepository.cs +++ b/Blog.Core.Repository/BASE/IBaseRepository.cs @@ -208,5 +208,45 @@ Task> QueryTabsPage( int pageIndex = 1, int pageSize = 20, string orderByFields = null); + + #region 分表 + /// + /// 通过ID查询 + /// + /// + /// + Task QueryByIdSplit(object objId); + /// + /// 自动分表插入 + /// + /// + /// + Task> AddSplit(TEntity entity); + /// + /// 删除 + /// + /// + /// + /// + Task DeleteSplit(TEntity entity, DateTime dateTime); + /// + /// 更新 + /// + /// + /// + /// + Task UpdateSplit(TEntity entity, DateTime dateTime); + /// + /// 分页查询 + /// + /// + /// + /// + /// + /// + /// + /// + Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null); + #endregion } } diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index f8105165..7ee55eb1 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -332,5 +332,36 @@ public async Task> QueryPage(PaginationModel pagination) var express = DynamicLinqFactory.CreateLambda(pagination.Conditions); return await QueryPage(express, pagination.PageIndex, pagination.PageSize, pagination.OrderByFileds); } + #region 分表 + public async Task> AddSplit(TEntity entity) + { + return await BaseDal.AddSplit(entity); + } + public async Task UpdateSplit(TEntity entity, DateTime dateTime) + { + return await BaseDal.UpdateSplit(entity, dateTime); + } + + /// + /// 根据实体删除一条数据 + /// + /// 博文实体类 + /// + public async Task DeleteSplit(TEntity entity, DateTime dateTime) + { + return await BaseDal.DeleteSplit(entity, dateTime); + } + + public async Task QueryByIdSplit(object objId) + { + return await BaseDal.QueryByIdSplit(objId); + } + public async Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, + int pageIndex = 1, int pageSize = 20, string orderByFields = null) + { + return await BaseDal.QueryPageSplit(whereExpression, beginTime, endTime, + pageIndex, pageSize, orderByFields); + } + #endregion } } \ No newline at end of file diff --git a/Blog.Core.Services/SplitDemoServices.cs b/Blog.Core.Services/SplitDemoServices.cs new file mode 100644 index 00000000..cf8e2cc1 --- /dev/null +++ b/Blog.Core.Services/SplitDemoServices.cs @@ -0,0 +1,23 @@ +using Blog.Core.IRepository.Base; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Services.BASE; +using System.Linq; +using System.Threading.Tasks; + +namespace Blog.Core.FrameWork.Services +{ + /// + /// sysUserInfoServices + /// + public class SplitDemoServices : BaseServices, ISplitDemoServices + { + private readonly IBaseRepository _splitDemoRepository; + public SplitDemoServices(IBaseRepository splitDemoRepository) + { + _splitDemoRepository = splitDemoRepository; + } + + + } +} From 7b1f3a4c740f6cc7302219a12bb1033ab583018c Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 30 Mar 2023 15:49:30 +0800 Subject: [PATCH 145/289] feat: :airplane: change id to long --- Blog.Core.Api/Blog.Core.Model.xml | 5 --- Blog.Core.Api/Blog.Core.xml | 4 +- .../Controllers/DbFirst/MigrateController.cs | 30 ++++++------- .../Controllers/DepartmentController.cs | 2 +- .../Controllers/PermissionController.cs | 24 +++++----- Blog.Core.Api/Controllers/UserController.cs | 6 +-- .../wwwroot/BlogCore.Data.json/Permission.tsv | 44 +++++++++---------- .../RoleModulePermission.tsv | 12 ++--- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 10 ++++- Blog.Core.Common/Helper/RecursionHelper.cs | 12 ++--- Blog.Core.Common/Seed/DBSeed.cs | 30 +++++++------ .../IRoleModulePermissionServices.cs | 2 +- Blog.Core.Model/Models/AccessTrendLog.cs | 2 +- Blog.Core.Model/Models/Advertisement.cs | 2 +- Blog.Core.Model/Models/BlogArticle.cs | 4 +- Blog.Core.Model/Models/BlogArticleComment.cs | 2 +- Blog.Core.Model/Models/Department.cs | 2 +- Blog.Core.Model/Models/GblLogAudit.cs | 4 +- Blog.Core.Model/Models/Guestbook.cs | 6 +-- Blog.Core.Model/Models/Modules.cs | 2 +- Blog.Core.Model/Models/OperateLog.cs | 2 +- Blog.Core.Model/Models/PasswordLib.cs | 6 +-- Blog.Core.Model/Models/Permission.cs | 2 +- Blog.Core.Model/Models/Role.cs | 2 +- .../Models/RoleModulePermission.cs | 2 +- Blog.Core.Model/Models/RootEntity.cs | 15 ------- Blog.Core.Model/Models/TasksLog.cs | 4 +- Blog.Core.Model/Models/TasksQz.cs | 2 +- Blog.Core.Model/Models/TestModels.cs | 6 +-- Blog.Core.Model/Models/Topic.cs | 2 +- Blog.Core.Model/Models/TopicDetail.cs | 2 +- Blog.Core.Model/Models/UserRole.cs | 6 +-- Blog.Core.Model/Models/sysUserInfo.cs | 4 +- Blog.Core.Model/ViewModels/BlogViewModels.cs | 4 +- .../IRoleModulePermissionRepository.cs | 2 +- .../RoleModulePermissionRepository.cs | 2 +- .../RoleModulePermissionServices.cs | 2 +- 37 files changed, 130 insertions(+), 140 deletions(-) delete mode 100644 Blog.Core.Model/Models/RootEntity.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 6bbfd4fe..fe62d8e2 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1795,11 +1795,6 @@ 修改时间 - - - ID - - 部门表 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 89cb3213..faf9d522 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -413,14 +413,14 @@ - + 获取路由树 - + 获取路由树 diff --git a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs index 8006d1fb..7865cc69 100644 --- a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs +++ b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs @@ -81,7 +81,7 @@ public async Task> DataMigrateFromOld2New() InitPermissionTree(permissions, permissionsAllList, apiList); var actionPermissionIds = permissionsAllList.Where(d => d.Id >= filterPermissionId).Select(d => d.Id).ToList(); - List filterPermissionIds = new(); + List filterPermissionIds = new(); FilterPermissionTree(permissionsAllList, actionPermissionIds, filterPermissionIds); permissions = permissions.Where(d => filterPermissionIds.Contains(d.Id)).ToList(); @@ -93,10 +93,10 @@ public async Task> DataMigrateFromOld2New() // 1、保持菜单和接口 await SavePermissionTreeAsync(permissions, pms); - var rid = 0; - var pid = 0; - var mid = 0; - var rpmid = 0; + long rid = 0; + long pid = 0; + long mid = 0; + long rpmid = 0; // 2、保存关系表 foreach (var item in rmps) @@ -116,8 +116,8 @@ public async Task> DataMigrateFromOld2New() } } - pid = (pms.FirstOrDefault(d => d.PidOld == item.PermissionId)?.PidNew).ObjToInt(); - mid = (pms.FirstOrDefault(d => d.MidOld == item.ModuleId)?.MidNew).ObjToInt(); + pid = (pms.FirstOrDefault(d => d.PidOld == item.PermissionId)?.PidNew).ObjToLong(); + mid = (pms.FirstOrDefault(d => d.MidOld == item.ModuleId)?.MidNew).ObjToLong(); // 关系 if (rid > 0 && pid > 0) { @@ -282,7 +282,7 @@ private void InitPermissionTree(List permissionsTree, List permissionsAll, List actionPermissionId, List filterPermissionIds) + private void FilterPermissionTree(List permissionsAll, List actionPermissionId, List filterPermissionIds) { actionPermissionId = actionPermissionId.Distinct().ToList(); var doneIds = permissionsAll.Where(d => actionPermissionId.Contains(d.Id) && d.Pid == 0).Select(d => d.Id).ToList(); @@ -295,7 +295,7 @@ private void FilterPermissionTree(List permissionsAll, List act } } - private async Task SavePermissionTreeAsync(List permissionsTree, List pms, int permissionId = 0) + private async Task SavePermissionTreeAsync(List permissionsTree, List pms, long permissionId = 0) { var parendId = permissionId; @@ -304,9 +304,9 @@ private async Task SavePermissionTreeAsync(List permissionsTree, Lis PM pm = new PM(); // 保留原始主键id pm.PidOld = item.Id; - pm.MidOld = (item.Module?.Id).ObjToInt(); + pm.MidOld = (item.Module?.Id).ObjToLong(); - var mid = 0; + long mid = 0; // 接口 if (item.Module != null) { @@ -351,9 +351,9 @@ private async Task SavePermissionTreeAsync(List permissionsTree, Lis public class PM { - public int PidOld { get; set; } - public int MidOld { get; set; } - public int PidNew { get; set; } - public int MidNew { get; set; } + public long PidOld { get; set; } + public long MidOld { get; set; } + public long PidNew { get; set; } + public long MidNew { get; set; } } } diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index bca7bf6d..b8174e90 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -83,7 +83,7 @@ public async Task>> GetTreeTable(long f = 0, strin foreach (var item in departments) { - List pidarr = new() { }; + List pidarr = new() { }; var parent = departmentList.FirstOrDefault(d => d.Id == item.Pid); while (parent != null) diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 70f8f689..9277a36b 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -99,7 +99,7 @@ public async Task>> Get(int page = 1, string var permissionAll = await _permissionServices.Query(d => d.IsDeleted != true); foreach (var item in permissionsView) { - List pidarr = new List + List pidarr = new() { item.Pid }; @@ -177,7 +177,7 @@ public async Task>> GetTreeTable(int f = 0, string foreach (var item in permissions) { - List pidarr = new List { }; + List pidarr = new() { }; var parent = permissionsList.FirstOrDefault(d => d.Id == item.Pid); while (parent != null) @@ -353,13 +353,13 @@ orderby child.Id /// /// [HttpGet] - public async Task> GetNavigationBar(int uid) + public async Task> GetNavigationBar(long uid) { var data = new MessageModel(); var uidInHttpcontext1 = 0; - var roleIds = new List(); + var roleIds = new List(); // ids4和jwt切换 if (Permissions.IsUseIds4) { @@ -369,13 +369,13 @@ public async Task> GetNavigationBar(int uid) select item.Value).FirstOrDefault().ObjToInt(); roleIds = (from item in _httpContext.HttpContext.User.Claims where item.Type == "role" - select item.Value.ObjToInt()).ToList(); + select item.Value.ObjToLong()).ToList(); } else { // jwt uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); - roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToInt()).Distinct().ToList(); + roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToLong()).Distinct().ToList(); } @@ -383,7 +383,7 @@ public async Task> GetNavigationBar(int uid) { if (roleIds.Any()) { - var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))).Select(d => d.PermissionId.ObjToInt()).Distinct(); + var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))).Select(d => d.PermissionId.ObjToLong()).Distinct(); if (pids.Any()) { var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id))).OrderBy(c => c.OrderSort); @@ -445,12 +445,12 @@ orderby child.Id /// /// [HttpGet] - public async Task>> GetNavigationBarPro(int uid) + public async Task>> GetNavigationBarPro(long uid) { var data = new MessageModel>(); var uidInHttpcontext1 = 0; - var roleIds = new List(); + var roleIds = new List(); // ids4和jwt切换 if (Permissions.IsUseIds4) { @@ -460,13 +460,13 @@ public async Task>> GetNavigationBarPro(int select item.Value).FirstOrDefault().ObjToInt(); roleIds = (from item in _httpContext.HttpContext.User.Claims where item.Type == "role" - select item.Value.ObjToInt()).ToList(); + select item.Value.ObjToLong()).ToList(); } else { // jwt uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); - roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToInt()).Distinct().ToList(); + roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToLong()).Distinct().ToList(); } if (uid > 0 && uid == uidInHttpcontext1) @@ -474,7 +474,7 @@ public async Task>> GetNavigationBarPro(int if (roleIds.Any()) { var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))) - .Select(d => d.PermissionId.ObjToInt()).Distinct(); + .Select(d => d.PermissionId.ObjToLong()).Distinct(); if (pids.Any()) { var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id) && d.IsButton == false)).OrderBy(c => c.OrderSort); diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 43f7f610..27989821 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -103,15 +103,15 @@ public async Task>> Get(int page = 1, str return Success(data.ConvertTo(_mapper)); } - private (string, List) GetFullDepartmentName(List departments, int departmentId) + private (string, List) GetFullDepartmentName(List departments, int departmentId) { var departmentModel = departments.FirstOrDefault(d => d.Id == departmentId); if (departmentModel == null) { - return ("", new List()); + return ("", new List()); } - var pids = departmentModel.CodeRelationship?.TrimEnd(',').Split(',').Select(d => d.ObjToInt()).ToList(); + var pids = departmentModel.CodeRelationship?.TrimEnd(',').Split(',').Select(d => d.ObjToLong()).ToList(); pids.Add(departmentModel.Id); var pnams = departments.Where(d => pids.Contains(d.Id)).ToList().Select(d => d.Name).ToArray(); var fullName = string.Join("/", pnams); diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv index d1a5c3f8..c026f01e 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/Permission.tsv @@ -2323,28 +2323,28 @@ "Pid": 94, "Mid": 63 }, - { - "Id": 114, - "Code": " ", - "Name": "推送文字消息", - "IsButton": 1, - "IsHide": 0, - "IskeepAlive": 0, - "Func": null, - "OrderSort": 0, - "Icon": null, - "Description": null, - "Enabled": 1, - "CreateId": 8, - "CreateBy": "test", - "CreateTime": "2020-04-23 16:22:11", - "ModifyId": null, - "ModifyBy": null, - "ModifyTime": "2021-09-29 00:00:00", - "IsDeleted": 0, - "Pid": 95, - "Mid": 0 - }, + //{ + // "Id": 114, + // "Code": " ", + // "Name": "推送文字消息", + // "IsButton": 1, + // "IsHide": 0, + // "IskeepAlive": 0, + // "Func": null, + // "OrderSort": 0, + // "Icon": null, + // "Description": null, + // "Enabled": 1, + // "CreateId": 8, + // "CreateBy": "test", + // "CreateTime": "2020-04-23 16:22:11", + // "ModifyId": null, + // "ModifyBy": null, + // "ModifyTime": "2021-09-29 00:00:00", + // "IsDeleted": 0, + // "Pid": 95, + // "Mid": 0 + //}, { "Code": "-", "Name": "部门权限管理", diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv index b7923ab4..eb727383 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv @@ -1647,7 +1647,7 @@ "ModifyId": null, "ModifyBy": null, "ModifyTime": "\/Date(1546272000000+0800)\/", - "Id": 128 + "Id": 228 }, { "IsDeleted": false, @@ -1658,7 +1658,7 @@ "RoleId": 6, "ModuleId": 0, "PermissionId": 114, - "Id": 129 + "Id": 229 }, { "IsDeleted": false, @@ -1669,7 +1669,7 @@ "RoleId": 6, "ModuleId": 66, "PermissionId": 115, - "Id": 130 + "Id": 230 }, { "IsDeleted": false, @@ -1680,7 +1680,7 @@ "RoleId": 6, "ModuleId": 70, "PermissionId": 120, - "Id": 131 + "Id": 231 }, { "IsDeleted": false, @@ -1691,7 +1691,7 @@ "RoleId": 6, "ModuleId": 66, "PermissionId": 116, - "Id": 132 + "Id": 232 }, { "IsDeleted": false, @@ -1702,6 +1702,6 @@ "RoleId": 4, "ModuleId": 72, "PermissionId": 122, - "Id": 133 + "Id": 233 } ] diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index 3d83b002..c1417a5a 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -1,4 +1,5 @@ -using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model; +using Blog.Core.Model.Models.RootTkey; using Blog.Core.Model.Tenants; using SqlSugar; using System; @@ -16,6 +17,13 @@ public static void DataExecuting(object oldValue, DataFilterModel entityInfo) root.Id = SnowFlakeSingle.Instance.NextId(); } } + if (entityInfo.EntityValue is RootEntityTkey rootEntity) + { + if (rootEntity.Id == 0) + { + rootEntity.Id = SnowFlakeSingle.Instance.NextId(); + } + } if (entityInfo.EntityValue is BaseEntity baseEntity) { diff --git a/Blog.Core.Common/Helper/RecursionHelper.cs b/Blog.Core.Common/Helper/RecursionHelper.cs index 9b27a37d..f6f21a38 100644 --- a/Blog.Core.Common/Helper/RecursionHelper.cs +++ b/Blog.Core.Common/Helper/RecursionHelper.cs @@ -117,8 +117,8 @@ public static void LoopToAppendChildrenT(List all, T curItem, string paren public class PermissionTree { - public int value { get; set; } - public int Pid { get; set; } + public long value { get; set; } + public long Pid { get; set; } public string label { get; set; } public int order { get; set; } public bool isbtn { get; set; } @@ -139,8 +139,8 @@ public class DepartmentTree public class NavigationBar { - public int id { get; set; } - public int pid { get; set; } + public long id { get; set; } + public long pid { get; set; } public int order { get; set; } public string name { get; set; } public bool IsHide { get; set; } = false; @@ -165,8 +165,8 @@ public class NavigationBarMeta public class NavigationBarPro { - public int id { get; set; } - public int parentId { get; set; } + public long id { get; set; } + public long parentId { get; set; } public int order { get; set; } public string name { get; set; } public bool IsHide { get; set; } = false; diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 7b594109..94893c38 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -175,7 +175,11 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Permission"), Encoding.UTF8), setting); - myContext.GetEntityDB().InsertRange(data); + foreach (var item in data) + { + Console.WriteLine($"{item.Name}:{item.Id}"); + myContext.GetEntityDB().Insert(item); + } Console.WriteLine("Table:Permission created success!"); } else @@ -190,10 +194,10 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); - using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "Role.xlsx"), FileMode.Open); - var result = await importer.Import(stream); - var data = result.Data.ToList(); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); + //using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "Role.xlsx"), FileMode.Open); + //var result = await importer.Import(stream); + //var data = result.Data.ToList(); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Role created success!"); @@ -212,7 +216,11 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "RoleModulePermission"), Encoding.UTF8), setting); - myContext.GetEntityDB().InsertRange(data); + foreach (var item in data) + { + Console.WriteLine($"{item.Id}"); + myContext.GetEntityDB().Insert(item); + } Console.WriteLine("Table:RoleModulePermission created success!"); } else @@ -261,10 +269,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); - using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "UserRole.xlsx"), FileMode.Open); - var result = await importer.Import(stream); - var data = result.Data.ToList(); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:UserRole created success!"); @@ -281,10 +286,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); - using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "SysUserInfo.xlsx"), FileMode.Open); - var result = await importer.Import(stream); - var data = result.Data.ToList(); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:sysUserInfo created success!"); diff --git a/Blog.Core.IServices/IRoleModulePermissionServices.cs b/Blog.Core.IServices/IRoleModulePermissionServices.cs index 22532479..2a5c7345 100644 --- a/Blog.Core.IServices/IRoleModulePermissionServices.cs +++ b/Blog.Core.IServices/IRoleModulePermissionServices.cs @@ -21,6 +21,6 @@ public interface IRoleModulePermissionServices :IBaseServices˵ /// ӿ /// - Task UpdateModuleId(int permissionId, int moduleId); + Task UpdateModuleId(long permissionId, long moduleId); } } diff --git a/Blog.Core.Model/Models/AccessTrendLog.cs b/Blog.Core.Model/Models/AccessTrendLog.cs index 4a87b13e..fd6dbae7 100644 --- a/Blog.Core.Model/Models/AccessTrendLog.cs +++ b/Blog.Core.Model/Models/AccessTrendLog.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 用户访问趋势日志 /// - public class AccessTrendLog : RootEntityTkey + public class AccessTrendLog : RootEntityTkey { /// /// 用户 diff --git a/Blog.Core.Model/Models/Advertisement.cs b/Blog.Core.Model/Models/Advertisement.cs index c2babd74..3b11b21f 100644 --- a/Blog.Core.Model/Models/Advertisement.cs +++ b/Blog.Core.Model/Models/Advertisement.cs @@ -3,7 +3,7 @@ namespace Blog.Core.Model.Models { - public class Advertisement : RootEntityTkey + public class Advertisement : RootEntityTkey { /// diff --git a/Blog.Core.Model/Models/BlogArticle.cs b/Blog.Core.Model/Models/BlogArticle.cs index 66f05bdf..8b75c8df 100644 --- a/Blog.Core.Model/Models/BlogArticle.cs +++ b/Blog.Core.Model/Models/BlogArticle.cs @@ -13,8 +13,8 @@ public class BlogArticle /// 主键 /// /// 这里之所以没用RootEntity,是想保持和之前的数据库一致,主键是bID,不是Id - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int bID { get; set; } + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long bID { get; set; } /// /// 创建人 diff --git a/Blog.Core.Model/Models/BlogArticleComment.cs b/Blog.Core.Model/Models/BlogArticleComment.cs index 08010863..519fb003 100644 --- a/Blog.Core.Model/Models/BlogArticleComment.cs +++ b/Blog.Core.Model/Models/BlogArticleComment.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models; /// public class BlogArticleComment : RootEntityTkey { - public int bID { get; set; } + public long bID { get; set; } public string Comment { get; set; } diff --git a/Blog.Core.Model/Models/Department.cs b/Blog.Core.Model/Models/Department.cs index 3583bcff..424bcf44 100644 --- a/Blog.Core.Model/Models/Department.cs +++ b/Blog.Core.Model/Models/Department.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// /// 部门表 /// - public class Department : DepartmentRoot + public class Department : DepartmentRoot { /// /// Desc:部门关系编码 diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs index 4b1bd9cd..2cecce8b 100644 --- a/Blog.Core.Model/Models/GblLogAudit.cs +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -12,8 +12,8 @@ public class GblLogAudit /// ///ID /// - [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int Id { get; set; } + [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long Id { get; set; } /// ///HttpContext.TraceIdentifier 事件链路ID(获取或设置一个唯一标识符,用于在跟踪日志中表示此请求。) diff --git a/Blog.Core.Model/Models/Guestbook.cs b/Blog.Core.Model/Models/Guestbook.cs index d1f671c0..0cd5dcef 100644 --- a/Blog.Core.Model/Models/Guestbook.cs +++ b/Blog.Core.Model/Models/Guestbook.cs @@ -3,13 +3,13 @@ namespace Blog.Core.Model.Models { - public class Guestbook:RootEntityTkey + public class Guestbook : RootEntityTkey { - + /// 博客ID /// /// - public int? blogId { get; set; } + public long? blogId { get; set; } /// 创建时间 /// /// diff --git a/Blog.Core.Model/Models/Modules.cs b/Blog.Core.Model/Models/Modules.cs index b62c0a47..6e41aaac 100644 --- a/Blog.Core.Model/Models/Modules.cs +++ b/Blog.Core.Model/Models/Modules.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 接口API地址信息表 /// - public class Modules : ModulesRoot + public class Modules : ModulesRoot { public Modules() { diff --git a/Blog.Core.Model/Models/OperateLog.cs b/Blog.Core.Model/Models/OperateLog.cs index 4086781c..3c2fb54c 100644 --- a/Blog.Core.Model/Models/OperateLog.cs +++ b/Blog.Core.Model/Models/OperateLog.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 日志记录 /// - public class OperateLog : RootEntityTkey + public class OperateLog : RootEntityTkey { /// diff --git a/Blog.Core.Model/Models/PasswordLib.cs b/Blog.Core.Model/Models/PasswordLib.cs index 2b43c265..b8b633d6 100644 --- a/Blog.Core.Model/Models/PasswordLib.cs +++ b/Blog.Core.Model/Models/PasswordLib.cs @@ -7,11 +7,11 @@ namespace Blog.Core.Model.Models /// 密码库表 /// [SugarTable("PasswordLib", "密码库表")]//('数据库表名','数据库表备注') - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + [Tenant("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') public class PasswordLib { - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int PLID { get; set; } + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long PLID { get; set; } /// ///获取或设置是否禁用,逻辑上的删除,非物理删除 diff --git a/Blog.Core.Model/Models/Permission.cs b/Blog.Core.Model/Models/Permission.cs index c650cb5e..deece0c0 100644 --- a/Blog.Core.Model/Models/Permission.cs +++ b/Blog.Core.Model/Models/Permission.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// /// 路由菜单表 /// - public class Permission : PermissionRoot + public class Permission : PermissionRoot { public Permission() { diff --git a/Blog.Core.Model/Models/Role.cs b/Blog.Core.Model/Models/Role.cs index e34ccdd9..1357afb0 100644 --- a/Blog.Core.Model/Models/Role.cs +++ b/Blog.Core.Model/Models/Role.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 角色表 /// - public class Role : RootEntityTkey + public class Role : RootEntityTkey { public Role() { diff --git a/Blog.Core.Model/Models/RoleModulePermission.cs b/Blog.Core.Model/Models/RoleModulePermission.cs index 13d82a80..482b9b4e 100644 --- a/Blog.Core.Model/Models/RoleModulePermission.cs +++ b/Blog.Core.Model/Models/RoleModulePermission.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 按钮跟权限关联表 /// - public class RoleModulePermission : RoleModulePermissionRoot + public class RoleModulePermission : RoleModulePermissionRoot { public RoleModulePermission() { diff --git a/Blog.Core.Model/Models/RootEntity.cs b/Blog.Core.Model/Models/RootEntity.cs deleted file mode 100644 index d3874bb4..00000000 --- a/Blog.Core.Model/Models/RootEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using SqlSugar; - -namespace Blog.Core.Model -{ - public class RootEntity - { - /// - /// ID - /// - [SugarColumn(IsNullable = false, IsPrimaryKey = true)] - public int Id { get; set; } - - - } -} \ No newline at end of file diff --git a/Blog.Core.Model/Models/TasksLog.cs b/Blog.Core.Model/Models/TasksLog.cs index b4317c95..c79e8077 100644 --- a/Blog.Core.Model/Models/TasksLog.cs +++ b/Blog.Core.Model/Models/TasksLog.cs @@ -6,12 +6,12 @@ namespace Blog.Core.Model.Models /// /// 任务日志表 /// - public class TasksLog : RootEntityTkey + public class TasksLog : RootEntityTkey { /// /// 任务ID /// - public int JobId { get; set; } + public long JobId { get; set; } /// /// 任务耗时 /// diff --git a/Blog.Core.Model/Models/TasksQz.cs b/Blog.Core.Model/Models/TasksQz.cs index 1c07b60e..b029a995 100644 --- a/Blog.Core.Model/Models/TasksQz.cs +++ b/Blog.Core.Model/Models/TasksQz.cs @@ -8,7 +8,7 @@ namespace Blog.Core.Model.Models /// /// 任务计划表 /// - public class TasksQz : RootEntityTkey + public class TasksQz : RootEntityTkey { /// /// 任务名称 diff --git a/Blog.Core.Model/Models/TestModels.cs b/Blog.Core.Model/Models/TestModels.cs index f5fa7dc7..8a8d123c 100644 --- a/Blog.Core.Model/Models/TestModels.cs +++ b/Blog.Core.Model/Models/TestModels.cs @@ -5,9 +5,9 @@ public class TestMuchTableResult { public string moduleName { get; set; } public string permName { get; set; } - public int rid { get; set; } - public int mid { get; set; } - public int? pid { get; set; } + public long rid { get; set; } + public long mid { get; set; } + public long? pid { get; set; } } } diff --git a/Blog.Core.Model/Models/Topic.cs b/Blog.Core.Model/Models/Topic.cs index 16bf7dad..e57bd561 100644 --- a/Blog.Core.Model/Models/Topic.cs +++ b/Blog.Core.Model/Models/Topic.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// /// Tibug 类别 /// - public class Topic : RootEntityTkey + public class Topic : RootEntityTkey { public Topic() { diff --git a/Blog.Core.Model/Models/TopicDetail.cs b/Blog.Core.Model/Models/TopicDetail.cs index 87e16ebf..1a98f3af 100644 --- a/Blog.Core.Model/Models/TopicDetail.cs +++ b/Blog.Core.Model/Models/TopicDetail.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// Tibug 博文 /// - public class TopicDetail : TopicDetailRoot + public class TopicDetail : TopicDetailRoot { public TopicDetail() { diff --git a/Blog.Core.Model/Models/UserRole.cs b/Blog.Core.Model/Models/UserRole.cs index 996eea2c..7ed9c6be 100644 --- a/Blog.Core.Model/Models/UserRole.cs +++ b/Blog.Core.Model/Models/UserRole.cs @@ -6,11 +6,11 @@ namespace Blog.Core.Model.Models /// /// 用户跟角色关联表 /// - public class UserRole : UserRoleRoot + public class UserRole : UserRoleRoot { public UserRole() { } - public UserRole(int uid, int rid) + public UserRole(long uid, long rid) { UserId = uid; RoleId = rid; @@ -31,7 +31,7 @@ public UserRole(int uid, int rid) /// 创建ID /// [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } + public long? CreateId { get; set; } /// /// 创建者 /// diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index 366b2291..2a417a16 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -9,7 +9,7 @@ namespace Blog.Core.Model.Models /// //[SugarTable("SysUserInfo")] [SugarTable("SysUserInfo", "用户表")] //('数据库表名','数据库表备注') - public class SysUserInfo : SysUserInfoRoot + public class SysUserInfo : SysUserInfoRoot { public SysUserInfo() { @@ -133,7 +133,7 @@ public SysUserInfo(string loginName, string loginPWD) public List RoleNames { get; set; } [SugarColumn(IsIgnore = true)] - public List Dids { get; set; } + public List Dids { get; set; } [SugarColumn(IsIgnore = true)] public string DepartmentName { get; set; } diff --git a/Blog.Core.Model/ViewModels/BlogViewModels.cs b/Blog.Core.Model/ViewModels/BlogViewModels.cs index f959270c..411a8ef7 100644 --- a/Blog.Core.Model/ViewModels/BlogViewModels.cs +++ b/Blog.Core.Model/ViewModels/BlogViewModels.cs @@ -34,7 +34,7 @@ public class BlogViewModels /// /// 上一篇id /// - public int previousID { get; set; } + public long previousID { get; set; } /// /// 下一篇 @@ -44,7 +44,7 @@ public class BlogViewModels /// /// 下一篇id /// - public int nextID { get; set; } + public long nextID { get; set; } /// 类别 /// diff --git a/Blog.Core.Repository/IRoleModulePermissionRepository.cs b/Blog.Core.Repository/IRoleModulePermissionRepository.cs index c66448f0..9ba3d4ed 100644 --- a/Blog.Core.Repository/IRoleModulePermissionRepository.cs +++ b/Blog.Core.Repository/IRoleModulePermissionRepository.cs @@ -19,6 +19,6 @@ public interface IRoleModulePermissionRepository : IBaseRepository菜单主键 /// 接口主键 /// - Task UpdateModuleId(int permissionId, int moduleId); + Task UpdateModuleId(long permissionId, long moduleId); } } diff --git a/Blog.Core.Repository/RoleModulePermissionRepository.cs b/Blog.Core.Repository/RoleModulePermissionRepository.cs index 1cb21ebf..9438ff50 100644 --- a/Blog.Core.Repository/RoleModulePermissionRepository.cs +++ b/Blog.Core.Repository/RoleModulePermissionRepository.cs @@ -99,7 +99,7 @@ public async Task> GetRMPMapsPage() /// 菜单主键 /// 接口主键 /// - public async Task UpdateModuleId(int permissionId, int moduleId) + public async Task UpdateModuleId(long permissionId, long moduleId) { await Db.Updateable(it => it.ModuleId == moduleId).Where( it => it.PermissionId == permissionId).ExecuteCommandAsync(); diff --git a/Blog.Core.Services/RoleModulePermissionServices.cs b/Blog.Core.Services/RoleModulePermissionServices.cs index c0248e17..d3834f89 100644 --- a/Blog.Core.Services/RoleModulePermissionServices.cs +++ b/Blog.Core.Services/RoleModulePermissionServices.cs @@ -83,7 +83,7 @@ public async Task> GetRMPMaps() /// 菜单主键 /// 接口主键 /// - public async Task UpdateModuleId(int permissionId, int moduleId) + public async Task UpdateModuleId(long permissionId, long moduleId) { await _dal.UpdateModuleId(permissionId, moduleId); } From 0ed67675a6bbab5a1b997ed9ea002f60aa3fde32 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Fri, 31 Mar 2023 22:02:30 +0800 Subject: [PATCH 146/289] =?UTF-8?q?=E6=9D=83=E9=99=90=E5=88=86=E9=85=8D?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96=20sqlsug?= =?UTF-8?q?arAop=E9=80=BB=E8=BE=91=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.xml | 3 +- .../Controllers/PermissionController.cs | 79 +++++++++---------- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 41 ++++++++++ 3 files changed, 81 insertions(+), 42 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index c0b52222..79325e37 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -361,7 +361,7 @@ 菜单管理 - + 构造函数 @@ -369,6 +369,7 @@ + diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 9277a36b..f4889c73 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -6,6 +6,8 @@ using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; @@ -21,6 +23,7 @@ namespace Blog.Core.Controllers [Authorize(Permissions.Name)] public class PermissionController : BaseApiController { + readonly IUnitOfWorkManage _unitOfWorkManage; readonly IPermissionServices _permissionServices; readonly IModuleServices _moduleServices; readonly IRoleModulePermissionServices _roleModulePermissionServices; @@ -37,16 +40,19 @@ public class PermissionController : BaseApiController /// /// /// + /// /// /// /// /// public PermissionController(IPermissionServices permissionServices, IModuleServices moduleServices, IRoleModulePermissionServices roleModulePermissionServices, IUserRoleServices userRoleServices, + IUnitOfWorkManage unitOfWorkManage, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContext, IUser user, PermissionRequirement requirement) { _permissionServices = permissionServices; + _unitOfWorkManage = unitOfWorkManage; _moduleServices = moduleServices; _roleModulePermissionServices = roleModulePermissionServices; _userRoleServices = userRoleServices; @@ -239,25 +245,24 @@ public async Task> Post([FromBody] Permission permission) /// [HttpPost] public async Task> Assign([FromBody] AssignView assignView) - { - var data = new MessageModel(); - - + { if (assignView.rid > 0) { - data.success = true; - - var roleModulePermissions = await _roleModulePermissionServices.Query(d => d.RoleId == assignView.rid); + //开启事务 + try + { + var old_rmps = await _roleModulePermissionServices.Query(d => d.RoleId == assignView.rid); - var remove = roleModulePermissions.Where(d => !assignView.pids.Contains(d.PermissionId.ObjToInt())).Select(c => (object)c.Id); - data.success &= remove.Any() ? await _roleModulePermissionServices.DeleteByIds(remove.ToArray()) : true; + _unitOfWorkManage.BeginTran(); + await _permissionServices.Db.Deleteable(t => t.RoleId == assignView.rid).ExecuteCommandAsync(); + var permissions = await _permissionServices.Query(d => d.IsDeleted == false); - foreach (var item in assignView.pids) - { - var rmpitem = roleModulePermissions.Where(d => d.PermissionId == item); - var moduleid = (await _permissionServices.Query(p => p.Id == item)).FirstOrDefault()?.Mid; - if (!rmpitem.Any()) + List new_rmps = new List(); + var nowTime = _permissionServices.Db.GetDate(); + foreach (var item in assignView.pids) { + var moduleid = permissions.Find(p => p.Id == item)?.Mid; + var find_old_rmps = old_rmps.Find(p => p.PermissionId == item); RoleModulePermission roleModulePermission = new RoleModulePermission() { @@ -265,39 +270,31 @@ public async Task> Assign([FromBody] AssignView assignView) RoleId = assignView.rid, ModuleId = moduleid.ObjToInt(), PermissionId = item, - }; - + CreateId = find_old_rmps == null ? _user.ID : find_old_rmps.CreateId, + CreateBy = find_old_rmps == null ? _user.Name : find_old_rmps.CreateBy, + CreateTime = find_old_rmps == null ? nowTime : find_old_rmps.CreateTime, + ModifyId = _user.ID, + ModifyBy = _user.Name, + ModifyTime = nowTime - roleModulePermission.CreateId = _user.ID; - roleModulePermission.CreateBy = _user.Name; - - data.success &= (await _roleModulePermissionServices.Add(roleModulePermission)) > 0; - - } - else - { - foreach (var role in rmpitem) - { - if (!role.ModuleId.Equals(moduleid)) - { - role.ModuleId = moduleid.Value; - await _roleModulePermissionServices.Update(role, new List { "ModuleId" }); - } - } + }; + new_rmps.Add(roleModulePermission); } + if(new_rmps.Count>0) await _roleModulePermissionServices.Add(new_rmps); + _unitOfWorkManage.CommitTran(); } - - if (data.success) + catch (Exception) { - _requirement.Permissions.Clear(); - data.response = ""; - data.msg = "保存成功"; + _unitOfWorkManage.RollbackTran(); + throw; } - + _requirement.Permissions.Clear(); + return Success("保存成功"); } - - - return data; + else + { + return Failed("请选择要操作的角色"); + } } diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index c1417a5a..c70d9283 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -1,6 +1,7 @@ using Blog.Core.Model; using Blog.Core.Model.Models.RootTkey; using Blog.Core.Model.Tenants; +using NetTaste; using SqlSugar; using System; @@ -69,6 +70,46 @@ public static void DataExecuting(object oldValue, DataFilterModel entityInfo) } } } + else + { + //兼容以前的表 + var getType = entityInfo.EntityValue.GetType(); + + + switch (entityInfo.OperationType) + { + + case DataFilterType.InsertByObject: + var dyCreateBy = getType.GetProperty("CreateBy"); + var dyCreateId = getType.GetProperty("CreateId"); + var dyCreateTime = getType.GetProperty("CreateTime"); + + if (App.User?.ID > 0 && dyCreateBy != null && dyCreateBy.GetValue(entityInfo.EntityValue) == null) + dyCreateBy.SetValue(entityInfo.EntityValue, App.User.Name); + + if (App.User?.ID > 0 && dyCreateId != null && dyCreateId.GetValue(entityInfo.EntityValue) == null) + dyCreateId.SetValue(entityInfo.EntityValue, App.User.ID); + + if (dyCreateTime != null && (DateTime)dyCreateTime.GetValue(entityInfo.EntityValue) == DateTime.MinValue) + dyCreateTime.SetValue(entityInfo.EntityValue, DateTime.Now); + + break; + case DataFilterType.UpdateByObject: + var dyModifyBy = getType.GetProperty("ModifyBy"); + var dyModifyId = getType.GetProperty("ModifyId"); + var dyModifyTime = getType.GetProperty("ModifyTime"); + + if (App.User?.ID > 0 && dyModifyBy != null) + dyModifyBy.SetValue(entityInfo.EntityValue, App.User.Name); + + if (App.User?.ID > 0 && dyModifyId != null) + dyModifyId.SetValue(entityInfo.EntityValue, App.User.ID); + + if (dyModifyTime != null) + dyModifyTime.SetValue(entityInfo.EntityValue, DateTime.Now); + break; + } + } } private static string GetWholeSql(SugarParameter[] paramArr, string sql) From 3c0d9f977cc03da275ba7d32a4f27e083b947ce3 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sat, 1 Apr 2023 21:09:47 +0800 Subject: [PATCH 147/289] =?UTF-8?q?=E6=B7=BB=E5=8A=A0update=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.IServices/BASE/IBaseServices.cs | 1 + Blog.Core.Repository/BASE/BaseRepository.cs | 9 +++++++++ Blog.Core.Repository/BASE/IBaseRepository.cs | 6 ++++++ Blog.Core.Services/BASE/BaseServices.cs | 9 +++++++++ 4 files changed, 25 insertions(+) diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index 4091b978..b491614f 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -27,6 +27,7 @@ public interface IBaseServices where TEntity : class Task DeleteByIds(object[] ids); Task Update(TEntity model); + Task Update(List model); Task Update(TEntity entity, string where); Task Update(object operateAnonymousObjects); diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 3048baa8..9ef11ecd 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -170,6 +170,15 @@ public async Task Update(TEntity entity) //这种方式会以主键为条件 return await _db.Updateable(entity).ExecuteCommandHasChangeAsync(); } + /// + /// 更新实体数据 + /// + /// 博文实体类 + /// + public async Task Update(List entity) + { + return await _db.Updateable(entity).ExecuteCommandHasChangeAsync(); + } public async Task Update(TEntity entity, string where) { diff --git a/Blog.Core.Repository/BASE/IBaseRepository.cs b/Blog.Core.Repository/BASE/IBaseRepository.cs index 5f70a4be..8ef05c1e 100644 --- a/Blog.Core.Repository/BASE/IBaseRepository.cs +++ b/Blog.Core.Repository/BASE/IBaseRepository.cs @@ -69,6 +69,12 @@ public interface IBaseRepository where TEntity : class /// /// Task Update(TEntity model); + /// + /// 更新model + /// + /// + /// + Task Update(List model); /// /// 根据model,更新,带where条件 diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index 7ee55eb1..14f69636 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -80,6 +80,15 @@ public async Task Update(TEntity entity) { return await BaseDal.Update(entity); } + /// + /// 更新实体数据 + /// + /// 博文实体类 + /// + public async Task Update(List entity) + { + return await BaseDal.Update(entity); + } public async Task Update(TEntity entity, string where) { From f785d507704275c104a61e8793548644084983ed Mon Sep 17 00:00:00 2001 From: "Lemon.NoCry" <773596523@qq.com> Date: Sat, 1 Apr 2023 21:29:34 +0800 Subject: [PATCH 148/289] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=9B=AA=E8=8A=B1id=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Model.xml | 5 ---- .../Controllers/PermissionController.cs | 4 +-- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 15 ++++------ Blog.Core.IServices/BASE/IBaseServices.cs | 4 +-- Blog.Core.Model/Models/RootTkey/BaseEntity.cs | 8 +----- Blog.Core.Repository/BASE/BaseRepository.cs | 28 +++++++++++-------- Blog.Core.Repository/BASE/IBaseRepository.cs | 4 +-- Blog.Core.Services/BASE/BaseServices.cs | 11 ++++++-- 8 files changed, 37 insertions(+), 42 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index fe62d8e2..79666d28 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -832,11 +832,6 @@ 修改时间 - - - 雪花Id - - 状态
diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index f4889c73..059e9b18 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -754,11 +754,11 @@ public async Task>> MigratePermission(string actio List modules = await _moduleServices.Query(d => d.LinkUrl != null && d.LinkUrl.ToLower() == item.Module.LinkUrl); if (!modules.Any()) { - int mid = await _moduleServices.Add(item.Module); + var mid = await _moduleServices.Add(item.Module); if (mid > 0) { item.Mid = mid; - int permissionid = await _permissionServices.Add(item); + var permissionid = await _permissionServices.Add(item); } } diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index c70d9283..9ab494a4 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -1,7 +1,6 @@ using Blog.Core.Model; using Blog.Core.Model.Models.RootTkey; using Blog.Core.Model.Tenants; -using NetTaste; using SqlSugar; using System; @@ -11,13 +10,6 @@ public static class SqlSugarAop { public static void DataExecuting(object oldValue, DataFilterModel entityInfo) { - if (entityInfo.EntityValue is BaseEntity root) - { - if (root.Id == 0) - { - root.Id = SnowFlakeSingle.Instance.NextId(); - } - } if (entityInfo.EntityValue is RootEntityTkey rootEntity) { if (rootEntity.Id == 0) @@ -73,12 +65,15 @@ public static void DataExecuting(object oldValue, DataFilterModel entityInfo) else { //兼容以前的表 - var getType = entityInfo.EntityValue.GetType(); + //这里要小心 在AOP里用反射 数据量多性能就会有问题 + //要么都统一使用基类 + //要么考虑老的表没必要兼容老的表 + // + var getType = entityInfo.EntityValue.GetType(); switch (entityInfo.OperationType) { - case DataFilterType.InsertByObject: var dyCreateBy = getType.GetProperty("CreateBy"); var dyCreateId = getType.GetProperty("CreateId"); diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index 4091b978..21f1afb4 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -16,9 +16,9 @@ public interface IBaseServices where TEntity : class Task QueryById(object objId, bool blnUseCache = false); Task> QueryByIDs(object[] lstIds); - Task Add(TEntity model); + Task Add(TEntity model); - Task Add(List listEntity); + Task> Add(List listEntity); Task DeleteById(object id); diff --git a/Blog.Core.Model/Models/RootTkey/BaseEntity.cs b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs index efbde8fd..b6dabe54 100644 --- a/Blog.Core.Model/Models/RootTkey/BaseEntity.cs +++ b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs @@ -4,14 +4,8 @@ namespace Blog.Core.Model.Models.RootTkey; -public class BaseEntity : IDeleteFilter +public class BaseEntity : RootEntityTkey, IDeleteFilter { - /// - /// 雪花Id - /// - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] - public long Id { get; set; } - #region 数据状态管理 /// diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 3048baa8..e808690d 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -114,7 +114,7 @@ public async Task> QueryByIDs(object[] lstIds) /// /// 博文实体类 /// - public async Task Add(TEntity entity) + public async Task Add(TEntity entity) { //var i = await Task.Run(() => _db.Insertable(entity).ExecuteReturnBigIdentity()); ////返回的i是long类型,这里你可以根据你的业务需要进行处理 @@ -125,7 +125,7 @@ public async Task Add(TEntity entity) //这里你可以返回TEntity,这样的话就可以获取id值,无论主键是什么类型 //var return3 = await insert.ExecuteReturnEntityAsync(); - return await insert.ExecuteReturnIdentityAsync(); + return await insert.ExecuteReturnSnowflakeIdAsync(); } /// @@ -134,16 +134,16 @@ public async Task Add(TEntity entity) /// 实体类 /// 指定只插入列 /// 返回自增量列 - public async Task Add(TEntity entity, Expression> insertColumns = null) + public async Task Add(TEntity entity, Expression> insertColumns = null) { var insert = _db.Insertable(entity); if (insertColumns == null) { - return await insert.ExecuteReturnIdentityAsync(); + return await insert.ExecuteReturnSnowflakeIdAsync(); } else { - return await insert.InsertColumns(insertColumns).ExecuteReturnIdentityAsync(); + return await insert.InsertColumns(insertColumns).ExecuteReturnSnowflakeIdAsync(); } } @@ -152,9 +152,9 @@ public async Task Add(TEntity entity, Expression> ins /// /// 实体集合 /// 影响行数 - public async Task Add(List listEntity) + public async Task> Add(List listEntity) { - return await _db.Insertable(listEntity.ToArray()).ExecuteCommandAsync(); + return await _db.Insertable(listEntity.ToArray()).ExecuteReturnSnowflakeIdListAsync(); } /// @@ -557,7 +557,9 @@ public async Task> QueryTabsPage( // groupName = s.groupName, // jobName = s.jobName // }, exp, s => new { s.uID, s.uRealName, s.groupName, s.jobName }, model.currentPage, model.pageSize, model.orderField + " " + model.orderType); + #region Split分表基础接口 (基础CRUD) + /// /// 分页查询[使用版本,其他分页未测试] /// @@ -573,9 +575,10 @@ public async Task> QueryPageSplit(Expression(pageIndex, totalCount, pageSize, list); + var data = new PageModel(pageIndex, totalCount, pageSize, list); return data; } + /// /// 写入实体数据 /// @@ -599,24 +602,26 @@ public async Task UpdateSplit(TEntity entity, DateTime dateTime) //return await _db.Updateable(entity).SplitTable().ExecuteCommandAsync();//,SplitTable不能少 //精准找单个表 - var tableName = _db.SplitHelper().GetTableName(dateTime);//根据时间获取表名 + var tableName = _db.SplitHelper().GetTableName(dateTime); //根据时间获取表名 return await _db.Updateable(entity).AS(tableName).ExecuteCommandHasChangeAsync(); } + /// /// 删除数据 /// /// /// /// - public async Task DeleteSplit(TEntity entity,DateTime dateTime) + public async Task DeleteSplit(TEntity entity, DateTime dateTime) { ////直接根据实体集合删除 (全自动 找表插入),返回受影响数 //return await _db.Deleteable(entity).SplitTable().ExecuteCommandAsync();//,SplitTable不能少 //精准找单个表 - var tableName = _db.SplitHelper().GetTableName(dateTime);//根据时间获取表名 + var tableName = _db.SplitHelper().GetTableName(dateTime); //根据时间获取表名 return await _db.Deleteable().AS(tableName).Where(entity).ExecuteCommandHasChangeAsync(); } + /// /// 根据ID查找数据 /// @@ -626,6 +631,7 @@ public async Task QueryByIdSplit(object objId) { return await _db.Queryable().In(objId).SplitTable(tabs => tabs).SingleAsync(); } + #endregion } } \ No newline at end of file diff --git a/Blog.Core.Repository/BASE/IBaseRepository.cs b/Blog.Core.Repository/BASE/IBaseRepository.cs index 5f70a4be..e3e6553b 100644 --- a/Blog.Core.Repository/BASE/IBaseRepository.cs +++ b/Blog.Core.Repository/BASE/IBaseRepository.cs @@ -33,14 +33,14 @@ public interface IBaseRepository where TEntity : class /// /// /// - Task Add(TEntity model); + Task Add(TEntity model); /// /// 批量添加 /// /// /// - Task Add(List listEntity); + Task> Add(List listEntity); /// /// 根据id 删除某一实体 diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index 7ee55eb1..809890fb 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -56,7 +56,7 @@ public async Task> QueryByIDs(object[] lstIds) /// /// 博文实体类 /// - public async Task Add(TEntity entity) + public async Task Add(TEntity entity) { return await BaseDal.Add(entity); } @@ -66,7 +66,7 @@ public async Task Add(TEntity entity) ///
/// 实体集合 /// 影响行数 - public async Task Add(List listEntity) + public async Task> Add(List listEntity) { return await BaseDal.Add(listEntity); } @@ -332,11 +332,14 @@ public async Task> QueryPage(PaginationModel pagination) var express = DynamicLinqFactory.CreateLambda(pagination.Conditions); return await QueryPage(express, pagination.PageIndex, pagination.PageSize, pagination.OrderByFileds); } + #region 分表 + public async Task> AddSplit(TEntity entity) { return await BaseDal.AddSplit(entity); } + public async Task UpdateSplit(TEntity entity, DateTime dateTime) { return await BaseDal.UpdateSplit(entity, dateTime); @@ -356,12 +359,14 @@ public async Task QueryByIdSplit(object objId) { return await BaseDal.QueryByIdSplit(objId); } - public async Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, + + public async Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null) { return await BaseDal.QueryPageSplit(whereExpression, beginTime, endTime, pageIndex, pageSize, orderByFields); } + #endregion } } \ No newline at end of file From 0d2a95e0e94553140f550fc4606ef88c8efab824 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 1 Apr 2023 23:25:56 +0800 Subject: [PATCH 149/289] =?UTF-8?q?=E2=9C=A8=20=E5=88=9D=E6=AD=A5=E8=B0=83?= =?UTF-8?q?=E6=95=B4Serilog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + Blog.Core.Api/Blog.Core.Api.csproj | 6 +- Blog.Core.Api/Blog.Core.xml | 2 +- Blog.Core.Api/Controllers/ValuesController.cs | 2 +- Blog.Core.Api/Filter/GlobalExceptionFilter.cs | 6 +- Blog.Core.Api/Log4net.config | 364 ----------- Blog.Core.Api/Program.cs | 112 ++-- Blog.Core.Api/Startup.cs | 1 - Blog.Core.Api/appsettings.json | 601 +++++++++--------- Blog.Core.Api/skyapm.json | 2 +- Blog.Core.Common/App.cs | 63 +- Blog.Core.Common/Blog.Core.Common.csproj | 14 +- Blog.Core.Common/Const/SqlSugarConst.cs | 9 + Blog.Core.Common/Core/InternalApp.cs | 25 +- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 30 +- Blog.Core.Common/DB/BaseDBConfig.cs | 3 +- Blog.Core.Common/Helper/RecursionHelper.cs | 67 +- .../{ => Https}/HttpPolly/HttpPollyHelper.cs | 20 +- .../{ => Https}/HttpPolly/IHttpPollyHelper.cs | 2 +- Blog.Core.Common/Https/RequestIpUtility.cs | 83 +++ Blog.Core.Common/Hubs/ChatHub.cs | 3 +- .../LogHelper/LogContextExtension.cs | 42 ++ .../LogHelper/LogContextStatic.cs | 42 ++ Blog.Core.Common/LogHelper/LogLock.cs | 170 ++--- .../Blog.Core.Extensions.csproj | 3 +- .../Middlewares/ExceptionHandlerMiddleware.cs | 9 +- .../Middlewares/IpLimitMiddleware.cs | 7 +- .../Middlewares/IpLogMiddleware.cs | 7 +- .../Middlewares/MiniProfilerMiddleware.cs | 7 +- .../Middlewares/SignalRSendMiddleware.cs | 1 + .../Middlewares/SwaggerMiddleware.cs | 21 +- .../ServiceExtensions/AppConfigSetup.cs | 38 +- .../AutofacModuleRegister.cs | 20 +- .../ServiceExtensions/HttpPollySetup.cs | 2 +- .../ServiceExtensions/SerilogSetup.cs | 37 ++ .../ServiceExtensions/SqlsugarSetup.cs | 45 +- .../ServiceExtensions/SwaggerSetup.cs | 26 +- .../Blog.Core.Serilog.Es.csproj | 2 +- Blog.Core.Serilog/Blog.Core.Serilog.csproj | 13 + .../LoggerConfigurationExtensions.cs | 121 ++++ .../Utility/SerilogRequestUtility.cs | 34 + .../Jobs/Job_AccessTrendLog_Quartz.cs | 4 +- .../QuartzNet/Jobs/Job_OperateLog_Quartz.cs | 17 +- .../DependencyInjection/DI_Test.cs | 1 - Blog.Core.sln | 6 + 45 files changed, 1137 insertions(+), 955 deletions(-) delete mode 100644 Blog.Core.Api/Log4net.config create mode 100644 Blog.Core.Common/Const/SqlSugarConst.cs rename Blog.Core.Common/{ => Https}/HttpPolly/HttpPollyHelper.cs (98%) rename Blog.Core.Common/{ => Https}/HttpPolly/IHttpPollyHelper.cs (96%) create mode 100644 Blog.Core.Common/Https/RequestIpUtility.cs create mode 100644 Blog.Core.Common/LogHelper/LogContextExtension.cs create mode 100644 Blog.Core.Common/LogHelper/LogContextStatic.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs create mode 100644 Blog.Core.Serilog/Blog.Core.Serilog.csproj create mode 100644 Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs create mode 100644 Blog.Core.Serilog/Utility/SerilogRequestUtility.cs diff --git a/.gitignore b/.gitignore index b7645c45..4f554bea 100644 --- a/.gitignore +++ b/.gitignore @@ -356,3 +356,5 @@ Blog.Core/Blog.Core*.xml Blog.Core.Api/WMBlog.db Blog.Core.Api/wwwroot/ui/ *.db +/Blog.Core.Api/WMBlog.db-journal +Logs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 77d681f1..3bf64399 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -26,21 +26,25 @@ + + + + @@ -51,8 +55,6 @@ - - diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 89cb3213..488903a0 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -760,7 +760,7 @@ Values控制器
- + ValuesController diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 1347ca16..677eb138 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -1,7 +1,7 @@ using AutoMapper; using Blog.Core.Common; using Blog.Core.Common.HttpContextUser; -using Blog.Core.Common.HttpPolly; +using Blog.Core.Common.Https.HttpPolly; using Blog.Core.Common.WebApiClients.HttpApis; using Blog.Core.EventBus; using Blog.Core.EventBus.EventHandling; diff --git a/Blog.Core.Api/Filter/GlobalExceptionFilter.cs b/Blog.Core.Api/Filter/GlobalExceptionFilter.cs index da119a6e..44c6124c 100644 --- a/Blog.Core.Api/Filter/GlobalExceptionFilter.cs +++ b/Blog.Core.Api/Filter/GlobalExceptionFilter.cs @@ -3,14 +3,10 @@ using Blog.Core.Common.LogHelper; using Blog.Core.Hubs; using Blog.Core.Model; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; using StackExchange.Profiling; -using System; namespace Blog.Core.Filter { @@ -54,7 +50,7 @@ public void OnException(ExceptionContext context) MiniProfiler.Current.CustomTiming("Errors:", json.msg); - //采用log4net 进行错误日志记录 + //进行错误日志记录 _loggerHelper.LogError(json.msg + WriteLog(json.msg, context.Exception)); if (AppSettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) { diff --git a/Blog.Core.Api/Log4net.config b/Blog.Core.Api/Log4net.config deleted file mode 100644 index 61bd3739..00000000 --- a/Blog.Core.Api/Log4net.config +++ /dev/null @@ -1,364 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index f5223beb..f7790a8c 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -1,20 +1,16 @@ // 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Text; + using Autofac; using Autofac.Extensions.DependencyInjection; using Blog.Core; using Blog.Core.Common; using Blog.Core.Common.Core; -using Blog.Core.Common.LogHelper; using Blog.Core.Extensions; using Blog.Core.Extensions.Apollo; using Blog.Core.Extensions.Middlewares; +using Blog.Core.Extensions.ServiceExtensions; using Blog.Core.Filter; using Blog.Core.Hubs; -using Blog.Core.IServices; -using Blog.Core.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -22,34 +18,38 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +using Serilog; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Text; +using Blog.Core.Common.Https; +using Blog.Core.Serilog.Utility; var builder = WebApplication.CreateBuilder(args); + + // 1、配置host与容器 builder.Host -.UseServiceProviderFactory(new AutofacServiceProviderFactory()) -.ConfigureContainer(builder => -{ - builder.RegisterModule(new AutofacModuleRegister()); - builder.RegisterModule(); -}) -.ConfigureLogging((hostingContext, builder) => -{ - builder.AddFilter("System", LogLevel.Error); - builder.AddFilter("Microsoft", LogLevel.Error); - builder.SetMinimumLevel(LogLevel.Error); - builder.AddLog4Net(Path.Combine(Directory.GetCurrentDirectory(), "Log4net.config")); -}) -.ConfigureAppConfiguration((hostingContext, config) => -{ - config.Sources.Clear(); - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); - config.AddConfigurationApollo("appsettings.apollo.json"); -}); + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer(builder => + { + builder.RegisterModule(new AutofacModuleRegister()); + builder.RegisterModule(); + }) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.Sources.Clear(); + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); + config.AddConfigurationApollo("appsettings.apollo.json"); + }); +builder.ConfigureApplication(); // 2、配置服务 builder.Services.AddSingleton(new AppSettings(builder.Configuration)); -builder.Services.AddSingleton(new LogLock(builder.Environment.ContentRootPath)); + + + builder.Services.AddUiFilesZipSetup(builder.Environment); Permissions.IsUseIds4 = AppSettings.app(new string[] { "Startup", "IdentityServer4", "Enabled" }).ObjToBool(); @@ -62,6 +62,9 @@ builder.Services.AddRedisCacheSetup(); builder.Services.AddSqlsugarSetup(); builder.Services.AddDbSetup(); + +builder.Host.AddSerilogSetup(); + builder.Services.AddAutoMapperSetup(); builder.Services.AddCorsSetup(); builder.Services.AddMiniProfilerSetup(); @@ -92,34 +95,34 @@ builder.Services.AddSignalR().AddNewtonsoftJsonProtocol(); builder.Services.AddScoped(); builder.Services.Configure(x => x.AllowSynchronousIO = true) - .Configure(x => x.AllowSynchronousIO = true); + .Configure(x => x.AllowSynchronousIO = true); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(); builder.Services.AddHttpPollySetup(); builder.Services.AddControllers(o => -{ - o.Filters.Add(typeof(GlobalExceptionsFilter)); - //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); - o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); -}) -.AddNewtonsoftJson(options => -{ - options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - options.SerializerSettings.ContractResolver = new DefaultContractResolver(); - options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; - //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); -}) -//.AddFluentValidation(config => -//{ -// //程序集方式添加验证 -// config.RegisterValidatorsFromAssemblyContaining(typeof(UserRegisterVoValidator)); -// //是否与MvcValidation共存 -// config.DisableDataAnnotationsValidation = true; -//}) -; + { + o.Filters.Add(typeof(GlobalExceptionsFilter)); + //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); + o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); + }) + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + }) + //.AddFluentValidation(config => + //{ + // //程序集方式添加验证 + // config.RegisterValidatorsFromAssemblyContaining(typeof(UserRegisterVoValidator)); + // //是否与MvcValidation共存 + // config.DisableDataAnnotationsValidation = true; + //}) + ; builder.Services.AddEndpointsApiExplorer(); @@ -159,12 +162,23 @@ app.UseStaticFiles(); app.UseCookiePolicy(); app.UseStatusCodePages(); +app.UseSerilogRequestLogging(options => +{ + options.GetLevel = SerilogRequestUtility.GetRequestLevel; + options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => + { + diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); + diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme); + diagnosticContext.Set("RequestIp", httpContext.GetRequestIp()); + }; +}); app.UseRouting(); if (builder.Configuration.GetValue("AppSettings:UseLoadTest")) { app.UseMiddleware(); } + app.UseAuthentication(); app.UseAuthorization(); app.UseMiniProfilerMiddleware(); diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index bc1630f1..1364d6a0 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -39,7 +39,6 @@ public void ConfigureServices(IServiceCollection services) { // 以下code可能与文章中不一样,对代码做了封装,具体查看右侧 Extensions 文件夹. services.AddSingleton(new AppSettings(Configuration)); - services.AddSingleton(new LogLock(Env.ContentRootPath)); services.AddUiFilesZipSetup(Env); Permissions.IsUseIds4 = AppSettings.app(new string[] { "Startup", "IdentityServer4", "Enabled" }).ObjToBool(); diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 3c117375..c712054c 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -1,98 +1,92 @@ { - "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 - "Logging": { - "LogLevel": { - "Default": "Information", //加入Default否则log4net本地写入不了日志 - "Blog.Core.AuthHelper.ApiResponseHandler": "Error" - }, - "Debug": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } - }, - "Console": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "Microsoft.Hosting.Lifetime": "Debug" - } - }, - "Log4Net": { - "Name": "Blog.Core" - } - }, - "AllowedHosts": "*", - "Redis": { - "ConnectionString": "127.0.0.1:6319,password=admin" + "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Information", + "Microsoft.AspNetCore": "Warning", + "System": "Warning", + "System.Net.Http.HttpClient": "Warning", + "Hangfire": "Information", + "Magicodes": "Warning", + "DotNetCore.CAP": "Information", + "Savorboard.CAP": "Information", + "Quartz": "Information" + } + } + }, + "AllowedHosts": "*", + "Redis": { + "ConnectionString": "127.0.0.1:6319,password=admin" + }, + "RabbitMQ": { + "Enabled": false, + "Connection": "118.25.251.13", + "UserName": "", + "Password": "!", + "RetryCount": 3 + }, + "Kafka": { + "Enabled": false, + "Servers": "localhost:9092", + "Topic": "blog", + "GroupId": "blog-consumer", + "NumPartitions": 3 //主题分区数量 + }, + "EventBus": { + "Enabled": false, + "SubscriptionClientName": "Blog.Core" + }, + "AppSettings": { + "RedisCachingAOP": { + "Enabled": false }, - "RabbitMQ": { - "Enabled": false, - "Connection": "118.25.251.13", - "UserName": "", - "Password": "!", - "RetryCount": 3 + "MemoryCachingAOP": { + "Enabled": true }, - "Kafka": { - "Enabled": false, - "Servers": "localhost:9092", - "Topic": "blog", - "GroupId": "blog-consumer", - "NumPartitions": 3 //主题分区数量 + "LogAOP": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + } }, - "EventBus": { - "Enabled": false, - "SubscriptionClientName": "Blog.Core" + "TranAOP": { + "Enabled": true }, - "AppSettings": { - "RedisCachingAOP": { - "Enabled": false - }, - "MemoryCachingAOP": { - "Enabled": true - }, - "LogAOP": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } - }, - "TranAOP": { - "Enabled": true - }, - "SqlAOP": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": false - }, - "LogToConsole": { - "Enabled": true - } - }, - "Date": "2018-08-28", - "SeedDBEnabled": true, //只生成表结构 - "SeedDBDataEnabled": true, //生成表,并初始化数据 - "Author": "Blog.Core", - "SvcName": "", // /svc/blog - "UseLoadTest": false + "SqlAOP": { + "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": false + }, + "LogToConsole": { + "Enabled": true + } }, + "Date": "2018-08-28", + "SeedDBEnabled": true, //只生成表结构 + "SeedDBDataEnabled": true, //生成表,并初始化数据 + "Author": "Blog.Core", + "SvcName": "", // /svc/blog + "UseLoadTest": false + }, - // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; - // *** 单库操作,把 MutiDBEnabled 设为false ***; - // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; - // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 - - "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true - "MutiDBEnabled": false, //是否开启多库模式 - "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer - "DBS": [ - /* + // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; + // *** 单库操作,把 MutiDBEnabled 设为false ***; + // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; + // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 + //Log:日志库; + "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true + "MutiDBEnabled": true, //是否开启多库模式 + "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer + "DBS": [ + /* 对应下边的 DBType MySql = 0, SqlServer = 1, @@ -102,225 +96,232 @@ Dm = 5,//达梦 Kdbndp = 6,//人大金仓 */ - { - "ConnId": "WMBLOG_SQLITE", - "DBType": 2, - "Enabled": true, - "HitRate": 50, // 值越大,优先级越高 - "Connection": "WMBlog.db" //sqlite只写数据库名就行 - }, - { - "ConnId": "WMBLOG_MSSQL_1", - "DBType": 1, - "Enabled": false, - "HitRate": 40, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", - "ProviderName": "System.Data.SqlClient" - }, - { - "ConnId": "WMBLOG_MSSQL_2", - "DBType": 1, - "Enabled": false, - "HitRate": 30, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", - "ProviderName": "System.Data.SqlClient" - }, - { - "ConnId": "WMBLOG_MYSQL", - "DBType": 0, - "Enabled": false, - "HitRate": 20, - "Connection": "server=localhost;Database=blog;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" - }, - { - "ConnId": "WMBLOG_MYSQL_2", - "DBType": 0, - "Enabled": false, - "HitRate": 20, - "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" - }, - { - "ConnId": "WMBLOG_ORACLE", - "DBType": 3, - "Enabled": false, - "HitRate": 10, - "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" - }, - { - "ConnId": "WMBLOG_DM", - "DBType": 5, - "Enabled": false, - "HitRate": 10, - "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" - }, - { - "ConnId": "WMBLOG_KDBNDP", - "DBType": 6, - "Enabled": false, - "HitRate": 10, - "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" - } - ], - "Audience": { - "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ - "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret - "Issuer": "Blog.Core", - "Audience": "wr" + { + "ConnId": "WMBLOG_SQLITE", + "DBType": 2, + "Enabled": true, + "HitRate": 50, // 值越大,优先级越高 + "Connection": "WMBlog.db" //sqlite只写数据库名就行 }, - "Mongo": { - "ConnectionString": "mongodb://nosql.data", - "Database": "BlogCoreDb" + { + "ConnId": "Log", + "DBType": 2, + "Enabled": true, + "HitRate": 50, // 值越大,优先级越高 + "Connection": "WMBlogLog.db" //sqlite只写数据库名就行 }, - "Startup": { - "Domain": "http://localhost:9291", - "Cors": { - "PolicyName": "CorsIpAccess", //策略名称 - "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 - // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 - // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 - "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" - }, - "AppConfigAlert": { - "Enabled": true - }, - "ApiName": "Blog.Core", - "IdentityServer4": { - "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 - "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 - "ApiName": "blog.core.api" // 资源服务器 - }, - "Authing": { - "Enabled": false, - "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", - "Audience": "63d51c4205c2849803be5178", - "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" - }, - "RedisMq": { - "Enabled": false //redis 消息队列 - }, - "MiniProfiler": { - "Enabled": false //性能分析开启 - }, - "Nacos": { - "Enabled": false //Nacos注册中心 - } + { + "ConnId": "WMBLOG_MSSQL_1", + "DBType": 1, + "Enabled": false, + "HitRate": 40, + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "ProviderName": "System.Data.SqlClient" }, - "Middleware": { - "RequestResponseLog": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } - }, - "IPLog": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } - }, - "RecordAccessLogs": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - }, - "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," - }, - "SignalR": { - "Enabled": false - }, - "SignalRSendLog": { - "Enabled": false - }, - "QuartzNetJob": { - "Enabled": true - }, - "Consul": { - "Enabled": false - }, - "IpRateLimit": { - "Enabled": true - } + { + "ConnId": "WMBLOG_MSSQL_2", + "DBType": 1, + "Enabled": false, + "HitRate": 30, + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "ProviderName": "System.Data.SqlClient" }, - "IpRateLimiting": { - "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each - "StackBlockedRequests": false, //False: Number of rejections should be recorded on another counter - "RealIpHeader": "X-Real-IP", - "ClientIdHeader": "X-ClientId", - "IpWhitelist": [], //白名单 - "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], - "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], - "QuotaExceededResponse": { - "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", - "ContentType": "application/json", - "StatusCode": 429 - }, - "HttpStatusCode": 429, //返回状态码 - "GeneralRules": [ //api规则,结尾一定要带* - { - "Endpoint": "*:/api/blog*", - "Period": "1m", - "Limit": 20 - }, - { - "Endpoint": "*/api/*", - "Period": "1s", - "Limit": 3 - }, - { - "Endpoint": "*/api/*", - "Period": "1m", - "Limit": 30 - }, - { - "Endpoint": "*/api/*", - "Period": "12h", - "Limit": 500 - } - ] - + { + "ConnId": "WMBLOG_MYSQL", + "DBType": 0, + "Enabled": false, + "HitRate": 20, + "Connection": "server=localhost;Database=blog;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" + }, + { + "ConnId": "WMBLOG_MYSQL_2", + "DBType": 0, + "Enabled": false, + "HitRate": 20, + "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" + }, + { + "ConnId": "WMBLOG_ORACLE", + "DBType": 3, + "Enabled": false, + "HitRate": 10, + "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" + }, + { + "ConnId": "WMBLOG_DM", + "DBType": 5, + "Enabled": false, + "HitRate": 10, + "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" + }, + { + "ConnId": "WMBLOG_KDBNDP", + "DBType": 6, + "Enabled": false, + "HitRate": 10, + "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" + } + ], + "Audience": { + "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ + "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret + "Issuer": "Blog.Core", + "Audience": "wr" + }, + "Mongo": { + "ConnectionString": "mongodb://nosql.data", + "Database": "BlogCoreDb" + }, + "Startup": { + "Domain": "http://localhost:9291", + "Cors": { + "PolicyName": "CorsIpAccess", //策略名称 + "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 + // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 + // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 + "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" + }, + "AppConfigAlert": { + "Enabled": true + }, + "ApiName": "Blog.Core", + "IdentityServer4": { + "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 + "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 + "ApiName": "blog.core.api" // 资源服务器 }, - "ConsulSetting": { - "ServiceName": "BlogCoreService", - "ServiceIP": "localhost", - "ServicePort": "9291", - "ServiceHealthCheck": "/healthcheck", - "ConsulAddress": "http://localhost:8500" + "Authing": { + "Enabled": false, + "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", + "Audience": "63d51c4205c2849803be5178", + "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" }, - "PayInfo": { //建行聚合支付信息 - "MERCHANTID": "", //商户号 - "POSID": "", //柜台号 - "BRANCHID": "", //分行号 - "pubKey": "", //公钥 - "USER_ID": "", //操作员号 - "PASSWORD": "", //密码 - "OutAddress": "http://127.0.0.1:12345" //外联地址 + "RedisMq": { + "Enabled": false //redis 消息队列 }, - "nacos": { - "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 - "DefaultTimeOut": 15000, // 默认超时时间 - "Namespace": "public", // 命名空间 - "ListenInterval": 10000, // 监听的频率 - "ServiceName": "blog.Core.Api", // 服务名 - "Port": "9291", // 服务端口号 - "RegisterEnabled": true // 是否直接注册nacos + "MiniProfiler": { + "Enabled": false //性能分析开启 }, - "LogFiedOutPutConfigs": { - "tcpAddressHost": "", // 输出elk的tcp连接地址 - "tcpAddressPort": 0, // 输出elk的tcp端口号 - "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 - { - "FiedName": "applicationName", - "FiedValue": "Blog.Core.Api" - } - ] + "Nacos": { + "Enabled": false //Nacos注册中心 } + }, + "Middleware": { + "RequestResponseLog": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + } + }, + "IPLog": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + } + }, + "RecordAccessLogs": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": true + }, + "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," + }, + "SignalR": { + "Enabled": false + }, + "SignalRSendLog": { + "Enabled": false + }, + "QuartzNetJob": { + "Enabled": true + }, + "Consul": { + "Enabled": false + }, + "IpRateLimit": { + "Enabled": true + } + }, + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each + "StackBlockedRequests": false, //False: Number of rejections should be recorded on another counter + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "IpWhitelist": [], //白名单 + "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], + "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], + "QuotaExceededResponse": { + "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + "HttpStatusCode": 429, //返回状态码 + "GeneralRules": [ //api规则,结尾一定要带* + { + "Endpoint": "*:/api/blog*", + "Period": "1m", + "Limit": 20 + }, + { + "Endpoint": "*/api/*", + "Period": "1s", + "Limit": 3 + }, + { + "Endpoint": "*/api/*", + "Period": "1m", + "Limit": 30 + }, + { + "Endpoint": "*/api/*", + "Period": "12h", + "Limit": 500 + } + ] + + }, + "ConsulSetting": { + "ServiceName": "BlogCoreService", + "ServiceIP": "localhost", + "ServicePort": "9291", + "ServiceHealthCheck": "/healthcheck", + "ConsulAddress": "http://localhost:8500" + }, + "PayInfo": { //建行聚合支付信息 + "MERCHANTID": "", //商户号 + "POSID": "", //柜台号 + "BRANCHID": "", //分行号 + "pubKey": "", //公钥 + "USER_ID": "", //操作员号 + "PASSWORD": "", //密码 + "OutAddress": "http://127.0.0.1:12345" //外联地址 + }, + "nacos": { + "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 + "DefaultTimeOut": 15000, // 默认超时时间 + "Namespace": "public", // 命名空间 + "ListenInterval": 10000, // 监听的频率 + "ServiceName": "blog.Core.Api", // 服务名 + "Port": "9291", // 服务端口号 + "RegisterEnabled": true // 是否直接注册nacos + }, + "LogFiedOutPutConfigs": { + "tcpAddressHost": "", // 输出elk的tcp连接地址 + "tcpAddressPort": 0, // 输出elk的tcp端口号 + "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 + { + "FiedName": "applicationName", + "FiedValue": "Blog.Core.Api" + } + ] + } } diff --git a/Blog.Core.Api/skyapm.json b/Blog.Core.Api/skyapm.json index cd5ed0ee..cdb0e606 100644 --- a/Blog.Core.Api/skyapm.json +++ b/Blog.Core.Api/skyapm.json @@ -11,7 +11,7 @@ }, "Logging": { "Level": "Information", - "FilePath": "Log/skyapm-{Date}.log" + "FilePath": "Logs/Skyapm/skyapm-{Date}.log" }, "Transport": { "Interval": 3000, diff --git a/Blog.Core.Common/App.cs b/Blog.Core.Common/App.cs index 008aea5d..c2e2e706 100644 --- a/Blog.Core.Common/App.cs +++ b/Blog.Core.Common/App.cs @@ -1,14 +1,24 @@ using Blog.Core.Common.Core; using Blog.Core.Common.HttpContextUser; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using System; +using System.Linq; namespace Blog.Core.Common; public class App { - public static IServiceProvider RootServices => InternalApp.RootServices ; + public static IServiceProvider RootServices => InternalApp.RootServices; + + /// 获取Web主机环境,如,是否是开发环境,生产环境等 + public static IWebHostEnvironment WebHostEnvironment => InternalApp.WebHostEnvironment; + + /// 获取泛型主机环境,如,是否是开发环境,生产环境等 + public static IHostEnvironment HostEnvironment => InternalApp.HostEnvironment; /// /// 获取请求上下文 @@ -16,4 +26,55 @@ public class App public static HttpContext HttpContext => RootServices?.GetService()?.HttpContext; public static IUser User => HttpContext == null ? null : RootServices?.GetService(); + + /// 解析服务提供器 + /// + /// + public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBuild = false) + { + if (App.HostEnvironment == null || App.RootServices != null && + InternalApp.InternalServices + .Where((u => u.ServiceType == (serviceType.IsGenericType ? serviceType.GetGenericTypeDefinition() : serviceType))) + .Any((u => u.Lifetime == ServiceLifetime.Singleton))) + return App.RootServices; + HttpContext httpContext = App.HttpContext; + if (httpContext?.RequestServices != null) + return httpContext.RequestServices; + if (App.RootServices != null) + { + IServiceScope scope = App.RootServices.CreateScope(); + return scope.ServiceProvider; + } + + if (mustBuild) + { + throw new ApplicationException("当前不可用,必须要等到 WebApplication Build后"); + } + + ServiceProvider serviceProvider = InternalApp.InternalServices.BuildServiceProvider(); + return serviceProvider; + } + + + public static TService GetService(bool mustBuild = true) where TService : class => App.GetService(typeof(TService), null, mustBuild) as TService; + + /// 获取请求生存周期的服务 + /// + /// + /// + /// + public static TService GetService(IServiceProvider serviceProvider, bool mustBuild = true) where TService : class => App.GetService(typeof(TService), serviceProvider, mustBuild) as TService; + + /// 获取请求生存周期的服务 + /// + /// + /// + /// + public static object GetService(Type type, IServiceProvider serviceProvider = null, bool mustBuild = true) => (serviceProvider ?? App.GetServiceProvider(type, mustBuild)).GetService(type); + + public static TOptions GetOptions(IServiceProvider serviceProvider = null) where TOptions : class, new() + { + IOptions service = App.GetService>(serviceProvider ?? App.RootServices, false); + return service?.Value; + } } \ No newline at end of file diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index fc0abb9d..0662bac5 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -18,20 +18,23 @@ - + - + + + + + - - - + + @@ -45,6 +48,7 @@ + diff --git a/Blog.Core.Common/Const/SqlSugarConst.cs b/Blog.Core.Common/Const/SqlSugarConst.cs new file mode 100644 index 00000000..f5efd7e2 --- /dev/null +++ b/Blog.Core.Common/Const/SqlSugarConst.cs @@ -0,0 +1,9 @@ +namespace Blog.Core.Common.Const; + +public class SqlSugarConst +{ + /// + /// 默认Log数据库标识 + /// + public const string LogConfigId = "Log"; +} \ No newline at end of file diff --git a/Blog.Core.Common/Core/InternalApp.cs b/Blog.Core.Common/Core/InternalApp.cs index c1ae8dcd..62e04724 100644 --- a/Blog.Core.Common/Core/InternalApp.cs +++ b/Blog.Core.Common/Core/InternalApp.cs @@ -1,17 +1,34 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; namespace Blog.Core.Common.Core; public static class InternalApp { + public static IServiceCollection InternalServices; + /// 根服务 public static IServiceProvider RootServices; - public static void ConfigureApplication(this WebApplication app) + /// 获取Web主机环境 + public static IWebHostEnvironment WebHostEnvironment; + + /// 获取泛型主机环境 + public static IHostEnvironment HostEnvironment; + + public static void ConfigureApplication(this WebApplicationBuilder wab) { - app.Lifetime.ApplicationStarted.Register(() => { InternalApp.RootServices = app.Services; }); + HostEnvironment = wab.Environment; + WebHostEnvironment = wab.Environment; + InternalServices = wab.Services; + } + - app.Lifetime.ApplicationStopped.Register(() => { InternalApp.RootServices = null; }); + public static void ConfigureApplication(this IHost app) + { + RootServices = app.Services; } } \ No newline at end of file diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index 3d83b002..c3d374ae 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -1,12 +1,40 @@ -using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Common.LogHelper; +using Blog.Core.Model.Models.RootTkey; using Blog.Core.Model.Tenants; using SqlSugar; +using StackExchange.Profiling; using System; +using Serilog; namespace Blog.Core.Common.DB.Aop; public static class SqlSugarAop { + public static void OnLogExecuting(ISqlSugarClient sqlSugarScopeProvider, string sql, SugarParameter[] p, ConnectionConfig config) + { + try + { + MiniProfiler.Current.CustomTiming($"ConnId:[{config.ConfigId}] SQL:", GetParas(p) + "【SQL语句】:" + sql); + + if (!AppSettings.app(new string[] { "AppSettings", "SqlAOP", "Enabled" }).ObjToBool()) return; + + if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToConsole", "Enabled" }).ObjToBool() || + AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToFile", "Enabled" }).ObjToBool() || + AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToDB", "Enabled" }).ObjToBool()) + { + using (LogContextExtension.Create.SqlAopPushProperty(sqlSugarScopeProvider)) + { + Log.Information("------------------ \r\n ConnId:[{ConnId}]【SQL语句】: \r\n {Sql}", + config.ConfigId, UtilMethods.GetSqlString(config.DbType, sql, p)); + } + } + } + catch (Exception e) + { + Log.Error("Error occured OnLogExcuting:" + e); + } + } + public static void DataExecuting(object oldValue, DataFilterModel entityInfo) { if (entityInfo.EntityValue is BaseEntity root) diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index 1d86369a..d8c3ee50 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -12,6 +12,7 @@ public class BaseDBConfig * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); + public static ConnectionConfig LogConfig; //日志库 private static string DifDBConnOfSecurity(params string[] conn) { @@ -107,8 +108,6 @@ private static MutiDBOperate SpecialDbString(MutiDBOperate mutiDBOperate) return mutiDBOperate; } - - } diff --git a/Blog.Core.Common/Helper/RecursionHelper.cs b/Blog.Core.Common/Helper/RecursionHelper.cs index 9b27a37d..b4cbd682 100644 --- a/Blog.Core.Common/Helper/RecursionHelper.cs +++ b/Blog.Core.Common/Helper/RecursionHelper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Blog.Core.Common.Helper @@ -8,9 +9,8 @@ namespace Blog.Core.Common.Helper /// public static class RecursionHelper { - public static void LoopToAppendChildren(List all, PermissionTree curItem, int pid, bool needbtn) + public static void LoopToAppendChildren(List all, PermissionTree curItem, long pid, bool needbtn) { - var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); var btnItems = subItems.Where(ss => ss.isbtn == true).ToList(); @@ -28,6 +28,7 @@ public static void LoopToAppendChildren(List all, PermissionTree { subItems = subItems.Where(ss => ss.isbtn == false).ToList(); } + if (subItems.Count > 0) { curItem.children = new List(); @@ -49,14 +50,15 @@ public static void LoopToAppendChildren(List all, PermissionTree { //subItem.disabled = true;//禁用当前节点 } + LoopToAppendChildren(all, subItem, pid, needbtn); } } + public static void LoopToAppendChildren(List all, DepartmentTree curItem, int pid) { - var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); - + if (subItems.Count > 0) { curItem.children = new List(); @@ -73,15 +75,14 @@ public static void LoopToAppendChildren(List all, DepartmentTree { //subItem.disabled = true;//禁用当前节点 } + LoopToAppendChildren(all, subItem, pid); } } - public static void LoopNaviBarAppendChildren(List all, NavigationBar curItem) { - var subItems = all.Where(ee => ee.pid == curItem.id).ToList(); if (subItems.Count > 0) @@ -102,7 +103,6 @@ public static void LoopNaviBarAppendChildren(List all, Navigation } - public static void LoopToAppendChildrenT(List all, T curItem, string parentIdName = "Pid", string idName = "value", string childrenName = "children") { var subItems = all.Where(ee => ee.GetType().GetProperty(parentIdName).GetValue(ee, null).ToString() == curItem.GetType().GetProperty(idName).GetValue(curItem, null).ToString()).ToList(); @@ -113,12 +113,47 @@ public static void LoopToAppendChildrenT(List all, T curItem, string paren LoopToAppendChildrenT(all, subItem); } } + + /// + /// 将父子级数据结构转换为普通list + /// + /// + /// + public static List TreeToList(List list, Action> action = null) + { + List results = new List(); + foreach (var item in list) + { + results.Add(item); + OperationChildData(results, item, action); + } + + return results; + } + + /// + /// 递归子级数据 + /// + /// 树形列表数据 + /// Item + public static void OperationChildData(List allList, T item, Action> action) + { + dynamic dynItem = item; + if (dynItem.Children == null) return; + if (dynItem.Children.Count <= 0) return; + allList.AddRange(dynItem.Children); + foreach (var subItem in dynItem.Children) + { + action?.Invoke(item, subItem, allList); + OperationChildData(allList, subItem, action); + } + } } public class PermissionTree { - public int value { get; set; } - public int Pid { get; set; } + public long value { get; set; } + public long Pid { get; set; } public string label { get; set; } public int order { get; set; } public bool isbtn { get; set; } @@ -139,8 +174,8 @@ public class DepartmentTree public class NavigationBar { - public int id { get; set; } - public int pid { get; set; } + public long id { get; set; } + public long pid { get; set; } public int order { get; set; } public string name { get; set; } public bool IsHide { get; set; } = false; @@ -158,15 +193,13 @@ public class NavigationBarMeta public bool requireAuth { get; set; } = true; public bool NoTabPage { get; set; } = false; public bool keepAlive { get; set; } = false; - - } public class NavigationBarPro { - public int id { get; set; } - public int parentId { get; set; } + public long id { get; set; } + public long parentId { get; set; } public int order { get; set; } public string name { get; set; } public bool IsHide { get; set; } = false; @@ -184,4 +217,4 @@ public class NavigationBarMetaPro public string icon { get; set; } public bool show { get; set; } = false; } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/HttpPolly/HttpPollyHelper.cs b/Blog.Core.Common/Https/HttpPolly/HttpPollyHelper.cs similarity index 98% rename from Blog.Core.Common/HttpPolly/HttpPollyHelper.cs rename to Blog.Core.Common/Https/HttpPolly/HttpPollyHelper.cs index f1a1e84c..1187d711 100644 --- a/Blog.Core.Common/HttpPolly/HttpPollyHelper.cs +++ b/Blog.Core.Common/Https/HttpPolly/HttpPollyHelper.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Blog.Core.Common.HttpPolly +namespace Blog.Core.Common.Https.HttpPolly { public class HttpPollyHelper : IHttpPollyHelper { @@ -35,7 +35,7 @@ public async Task PostAsync(HttpEnum httpEnum, string url, R request, D var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, stringContent); - + if (response.StatusCode == System.Net.HttpStatusCode.OK) { string result = await response.Content.ReadAsStringAsync(); @@ -72,7 +72,7 @@ public async Task PostAsync(HttpEnum httpEnum, string url, string request, var stringContent = new StringContent(request, Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, stringContent); - + if (response.StatusCode == System.Net.HttpStatusCode.OK) { string result = await response.Content.ReadAsStringAsync(); @@ -110,7 +110,7 @@ public async Task PostAsync(HttpEnum httpEnum, string url, R request, var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, stringContent); - + if (response.StatusCode == System.Net.HttpStatusCode.OK) { return await response.Content.ReadAsStringAsync(); @@ -146,7 +146,7 @@ public async Task PostAsync(HttpEnum httpEnum, string url, string reques var stringContent = new StringContent(request, Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, stringContent); - + if (response.StatusCode == System.Net.HttpStatusCode.OK) { return await response.Content.ReadAsStringAsync(); @@ -182,7 +182,7 @@ public async Task GetAsync(HttpEnum httpEnum, string url, Dictionary GetAsync(HttpEnum httpEnum, string url, Dictionary PutAsync(HttpEnum httpEnum, string url, R request, Di var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); var response = await client.PutAsync(url, stringContent); - + if (response.StatusCode == System.Net.HttpStatusCode.OK) { string result = await response.Content.ReadAsStringAsync(); @@ -294,7 +294,7 @@ public async Task PutAsync(HttpEnum httpEnum, string url, string request, var stringContent = new StringContent(request, Encoding.UTF8, "application/json"); var response = await client.PutAsync(url, stringContent); - + if (response.StatusCode == System.Net.HttpStatusCode.OK) { string result = await response.Content.ReadAsStringAsync(); @@ -331,7 +331,7 @@ public async Task DeleteAsync(HttpEnum httpEnum, string url, Dictionary(context, "X-Forwarded-For")).FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(ip)) + ip = SplitCsv(GetHeaderValueAs(context, "X-Real-IP")).FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(ip) && context.Connection?.RemoteIpAddress != null) + ip = context.Connection.RemoteIpAddress.ToString(); + + if (string.IsNullOrWhiteSpace(ip)) + ip = GetHeaderValueAs(context, "REMOTE_ADDR"); + + return ip; + } + + public static bool IsLocal(this HttpContext context) + { + return GetRequestIp(context) is "127.0.0.1" or "::1" || context.Request?.IsLocal() == true; + } + + + public static bool IsLocal(this HttpRequest req) + { + var connection = req.HttpContext.Connection; + if (connection.RemoteIpAddress != null) + { + if (connection.LocalIpAddress != null) + { + return connection.RemoteIpAddress.Equals(connection.LocalIpAddress); + } + else + { + return IPAddress.IsLoopback(connection.RemoteIpAddress); + } + } + + // for in memory TestServer or when dealing with default connection info + if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null) + { + return true; + } + + return false; + } + + + private static T GetHeaderValueAs(HttpContext context, string headerName) + { + if (context.Request?.Headers?.TryGetValue(headerName, out var values) ?? false) + { + string rawValues = values.ToString(); + + if (!string.IsNullOrWhiteSpace(rawValues)) + return (T) Convert.ChangeType(values.ToString(), typeof(T)); + } + + return default; + } + + private static List SplitCsv(string csvList) + { + if (string.IsNullOrWhiteSpace(csvList)) + return new List(); + + return csvList + .TrimEnd(',') + .Split(',') + .AsEnumerable() + .Select(s => s.Trim()) + .ToList(); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Hubs/ChatHub.cs b/Blog.Core.Common/Hubs/ChatHub.cs index ff15c97f..1c58c8a0 100644 --- a/Blog.Core.Common/Hubs/ChatHub.cs +++ b/Blog.Core.Common/Hubs/ChatHub.cs @@ -83,7 +83,8 @@ public async Task GetLatestCount(string random) //2、服务端主动向客户端发送数据,名字千万不能错 if (AppSettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) { - await Clients.All.ReceiveUpdate(LogLock.GetLogData()); + //TODO 主动发送错误消息 + //await Clients.All.ReceiveUpdate(LogLock.GetLogData()); } diff --git a/Blog.Core.Common/LogHelper/LogContextExtension.cs b/Blog.Core.Common/LogHelper/LogContextExtension.cs new file mode 100644 index 00000000..bce80cbb --- /dev/null +++ b/Blog.Core.Common/LogHelper/LogContextExtension.cs @@ -0,0 +1,42 @@ +using Serilog.Context; +using SqlSugar; +using System; +using System.Collections.Generic; + +namespace Blog.Core.Common.LogHelper; + +public class LogContextExtension : IDisposable +{ + private readonly Stack _disposableStack = new Stack(); + + public static LogContextExtension Create => new(); + + public void AddStock(IDisposable disposable) + { + _disposableStack.Push(disposable); + } + + public IDisposable SqlAopPushProperty(ISqlSugarClient db) + { + AddStock(LogContext.PushProperty(LogContextStatic.LogSource, LogContextStatic.AopSql)); + AddStock(LogContext.PushProperty(LogContextStatic.SqlOutToConsole, + AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToConsole", "Enabled" }).ObjToBool())); + AddStock(LogContext.PushProperty(LogContextStatic.SqlOutToFile, + AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToFile", "Enabled" }).ObjToBool())); + AddStock(LogContext.PushProperty(LogContextStatic.OutToDb, + AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToDb", "Enabled" }).ObjToBool())); + + AddStock(LogContext.PushProperty(LogContextStatic.SugarActionType, db.SugarActionType)); + + return this; + } + + + public void Dispose() + { + while (_disposableStack.Count > 0) + { + _disposableStack.Pop().Dispose(); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/LogHelper/LogContextStatic.cs b/Blog.Core.Common/LogHelper/LogContextStatic.cs new file mode 100644 index 00000000..52a8167d --- /dev/null +++ b/Blog.Core.Common/LogHelper/LogContextStatic.cs @@ -0,0 +1,42 @@ +using System.IO; + +namespace Blog.Core.Common.LogHelper; + +public class LogContextStatic +{ + static LogContextStatic() + { + if (!Directory.Exists(BaseLogs)) + { + Directory.CreateDirectory(BaseLogs); + } + } + + public static readonly string BaseLogs = "Logs"; + public static readonly string BasePathLogs = @"Logs"; + + public static readonly string LogSource = "LogSource"; + public static readonly string AopSql = "AopSql"; + public static readonly string SqlOutToConsole = "OutToConsole"; + public static readonly string SqlOutToFile = "SqlOutToFile"; + public static readonly string OutToDb = "OutToDb"; + public static readonly string SugarActionType = "SugarActionType"; + + public static readonly string FileMessageTemplate = "{NewLine}Date:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}LogLevel:{Level}{NewLine}Message:{Message}{NewLine}{Exception}" + new string('-', 100); + + + public static string Combine(string path1) + { + return Path.Combine(BaseLogs, path1); + } + + public static string Combine(string path1, string path2) + { + return Path.Combine(BaseLogs, path1, path2); + } + + public static string Combine(string path1, string path2, string path3) + { + return Path.Combine(BaseLogs, path1, path2, path3); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/LogHelper/LogLock.cs b/Blog.Core.Common/LogHelper/LogLock.cs index 70c5f7f2..2c9be9a9 100644 --- a/Blog.Core.Common/LogHelper/LogLock.cs +++ b/Blog.Core.Common/LogHelper/LogLock.cs @@ -1,5 +1,4 @@ using Blog.Core.Common.Helper; -using log4net; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -7,12 +6,12 @@ using System.Linq; using System.Text; using System.Threading; +using Serilog; namespace Blog.Core.Common.LogHelper { public class LogLock { - private static readonly ILog log = LogManager.GetLogger(typeof(LogLock)); static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); static int WritedCount = 0; static int FailedCount = 0; @@ -53,12 +52,14 @@ public static void OutLogAOP(string prefix, string traceId, string[] dataParas, default: break; } + if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "Enabled" }).ObjToBool()) { if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "LogToDB", "Enabled" }).ObjToBool()) { OutSql2LogToDB(prefix, traceId, dataParas, IsHeader); } + if (AppSettings.app(new string[] { AppSetingNodeName, AppSetingName, "LogToFile", "Enabled" }).ObjToBool()) { OutSql2LogToFile(prefix, traceId, dataParas, IsHeader); @@ -90,6 +91,7 @@ public static void OutSql2LogToFile(string prefix, string traceId, string[] data { Directory.CreateDirectory(folderPath); } + //string logFilePath = Path.Combine(path, $@"{filename}.log"); var logFilePath = FileHelper.GetAvailableFileWithPrefixOrderSize(folderPath, prefix); switch (prefix) @@ -98,47 +100,48 @@ public static void OutSql2LogToFile(string prefix, string traceId, string[] data AOPLogInfo apiLogAopInfo = JsonConvert.DeserializeObject(dataParas[1]); //记录被拦截方法信息的日志信息 var dataIntercept = "" + - $"【操作时间】:{apiLogAopInfo.RequestTime}\r\n" + - $"【当前操作用户】:{ apiLogAopInfo.OpUserName} \r\n" + - $"【当前执行方法】:{ apiLogAopInfo.RequestMethodName} \r\n" + - $"【携带的参数有】: {apiLogAopInfo.RequestParamsName} \r\n" + - $"【携带的参数JSON】: {apiLogAopInfo.RequestParamsData} \r\n" + - $"【响应时间】:{apiLogAopInfo.ResponseIntervalTime}\r\n" + - $"【执行完成时间】:{apiLogAopInfo.ResponseTime}\r\n" + - $"【执行完成结果】:{apiLogAopInfo.ResponseJsonData}\r\n"; + $"【操作时间】:{apiLogAopInfo.RequestTime}\r\n" + + $"【当前操作用户】:{apiLogAopInfo.OpUserName} \r\n" + + $"【当前执行方法】:{apiLogAopInfo.RequestMethodName} \r\n" + + $"【携带的参数有】: {apiLogAopInfo.RequestParamsName} \r\n" + + $"【携带的参数JSON】: {apiLogAopInfo.RequestParamsData} \r\n" + + $"【响应时间】:{apiLogAopInfo.ResponseIntervalTime}\r\n" + + $"【执行完成时间】:{apiLogAopInfo.ResponseTime}\r\n" + + $"【执行完成结果】:{apiLogAopInfo.ResponseJsonData}\r\n"; dataParas = new string[] { dataIntercept }; break; case "AOPLogEx": AOPLogExInfo apiLogAopExInfo = JsonConvert.DeserializeObject(dataParas[1]); var dataInterceptEx = "" + - $"【操作时间】:{apiLogAopExInfo.ApiLogAopInfo.RequestTime}\r\n" + - $"【当前操作用户】:{ apiLogAopExInfo.ApiLogAopInfo.OpUserName} \r\n" + - $"【当前执行方法】:{ apiLogAopExInfo.ApiLogAopInfo.RequestMethodName} \r\n" + - $"【携带的参数有】: {apiLogAopExInfo.ApiLogAopInfo.RequestParamsName} \r\n" + - $"【携带的参数JSON】: {apiLogAopExInfo.ApiLogAopInfo.RequestParamsData} \r\n" + - $"【响应时间】:{apiLogAopExInfo.ApiLogAopInfo.ResponseIntervalTime}\r\n" + - $"【执行完成时间】:{apiLogAopExInfo.ApiLogAopInfo.ResponseTime}\r\n" + - $"【执行完成结果】:{apiLogAopExInfo.ApiLogAopInfo.ResponseJsonData}\r\n" + - $"【执行完成异常信息】:方法中出现异常:{apiLogAopExInfo.ExMessage}\r\n" + - $"【执行完成异常】:方法中出现异常:{apiLogAopExInfo.InnerException}\r\n"; + $"【操作时间】:{apiLogAopExInfo.ApiLogAopInfo.RequestTime}\r\n" + + $"【当前操作用户】:{apiLogAopExInfo.ApiLogAopInfo.OpUserName} \r\n" + + $"【当前执行方法】:{apiLogAopExInfo.ApiLogAopInfo.RequestMethodName} \r\n" + + $"【携带的参数有】: {apiLogAopExInfo.ApiLogAopInfo.RequestParamsName} \r\n" + + $"【携带的参数JSON】: {apiLogAopExInfo.ApiLogAopInfo.RequestParamsData} \r\n" + + $"【响应时间】:{apiLogAopExInfo.ApiLogAopInfo.ResponseIntervalTime}\r\n" + + $"【执行完成时间】:{apiLogAopExInfo.ApiLogAopInfo.ResponseTime}\r\n" + + $"【执行完成结果】:{apiLogAopExInfo.ApiLogAopInfo.ResponseJsonData}\r\n" + + $"【执行完成异常信息】:方法中出现异常:{apiLogAopExInfo.ExMessage}\r\n" + + $"【执行完成异常】:方法中出现异常:{apiLogAopExInfo.InnerException}\r\n"; dataParas = new string[] { dataInterceptEx }; break; } + var now = DateTime.Now; string logContent = String.Join("\r\n", dataParas); if (IsHeader) { logContent = ( - "--------------------------------\r\n" + - DateTime.Now + "|\r\n" + - String.Join("\r\n", dataParas) + "\r\n" - ); + "--------------------------------\r\n" + + DateTime.Now + "|\r\n" + + String.Join("\r\n", dataParas) + "\r\n" + ); } else { logContent = ( - dataParas[1] + ",\r\n" - ); + dataParas[1] + ",\r\n" + ); } //if (logContent.IsNotEmptyOrNull() && logContent.Length > 500) @@ -148,12 +151,12 @@ public static void OutSql2LogToFile(string prefix, string traceId, string[] data if (isWrt) { File.WriteAllText(logFilePath, logContent); - } else { File.AppendAllText(logFilePath, logContent); } + WritedCount++; } catch (Exception e) @@ -170,14 +173,15 @@ public static void OutSql2LogToFile(string prefix, string traceId, string[] data LogWriteLock.ExitWriteLock(); } } + public static void OutSql2LogToDB(string prefix, string traceId, string[] dataParas, bool IsHeader = true) { - log4net.LogicalThreadContext.Properties["LogType"] = prefix; - log4net.LogicalThreadContext.Properties["TraceId"] = traceId; - if (dataParas.Length >= 2) - { - log4net.LogicalThreadContext.Properties["DataType"] = dataParas[0]; - } + //log4net.LogicalThreadContext.Properties["LogType"] = prefix; + //log4net.LogicalThreadContext.Properties["TraceId"] = traceId; + //if (dataParas.Length >= 2) + //{ + // log4net.LogicalThreadContext.Properties["DataType"] = dataParas[0]; + //} dataParas = dataParas.Skip(1).ToArray(); @@ -186,32 +190,37 @@ public static void OutSql2LogToDB(string prefix, string traceId, string[] dataPa { logContent = (String.Join("", dataParas)); } + switch (prefix) { //DEBUG | INFO | WARN | ERROR | FATAL case "AOPLog": - log.Info(logContent); + //TODO 是否需要输出? + //Log.Information(logContent); break; case "AOPLogEx": - log.Error(logContent); + Log.Error(logContent); break; case "RequestIpInfoLog": - log.Debug(logContent); + //TODO 是否需要Debug输出? + //Log.Debug(logContent); break; case "RecordAccessLogs": - log.Debug(logContent); + //TODO 是否需要Debug输出? + //Log.Debug(logContent); break; case "SqlLog": - log.Info(logContent); + Log.Information(logContent); break; case "RequestResponseLog": - log.Debug(logContent); + //TODO 是否需要Debug输出? + //Log.Debug(logContent); break; default: break; } - } + /// /// 读取文件内容 /// @@ -287,6 +296,7 @@ public static string ReadLog(string folderPath, string fileName, Encoding encode { LogWriteLock.ExitReadLock(); } + return s; } @@ -315,7 +325,6 @@ private static List GetRequestInfo(ReadType readType) } } } - } return requestInfos; @@ -336,16 +345,18 @@ public static List GetLogData() if (!string.IsNullOrEmpty(aoplogContent)) { aopLogs = aoplogContent.Split("--------------------------------") - .Where(d => !string.IsNullOrEmpty(d) && d != "\n" && d != "\r\n") - .Select(d => new LogInfo - { - Datetime = d.Split("|")[0].ObjToDate(), - Content = d.Split("|")[1]?.Replace("\r\n", "
"), - LogColor = "AOP", - }).ToList(); + .Where(d => !string.IsNullOrEmpty(d) && d != "\n" && d != "\r\n") + .Select(d => new LogInfo + { + Datetime = d.Split("|")[0].ObjToDate(), + Content = d.Split("|")[1]?.Replace("\r\n", "
"), + LogColor = "AOP", + }).ToList(); } } - catch (Exception) { } + catch (Exception) + { + } try { @@ -354,17 +365,19 @@ public static List GetLogData() if (!string.IsNullOrEmpty(exclogContent)) { excLogs = exclogContent.Split("--------------------------------") - .Where(d => !string.IsNullOrEmpty(d) && d != "\n" && d != "\r\n") - .Select(d => new LogInfo - { - Datetime = (d.Split("|")[0]).Split(',')[0].ObjToDate(), - Content = d.Split("|")[1]?.Replace("\r\n", "
"), - LogColor = "EXC", - Import = 9, - }).ToList(); + .Where(d => !string.IsNullOrEmpty(d) && d != "\n" && d != "\r\n") + .Select(d => new LogInfo + { + Datetime = (d.Split("|")[0]).Split(',')[0].ObjToDate(), + Content = d.Split("|")[1]?.Replace("\r\n", "
"), + LogColor = "EXC", + Import = 9, + }).ToList(); } } - catch (Exception) { } + catch (Exception) + { + } try @@ -374,16 +387,18 @@ public static List GetLogData() if (!string.IsNullOrEmpty(sqllogContent)) { sqlLogs = sqllogContent.Split("--------------------------------") - .Where(d => !string.IsNullOrEmpty(d) && d != "\n" && d != "\r\n") - .Select(d => new LogInfo - { - Datetime = d.Split("|")[0].ObjToDate(), - Content = d.Split("|")[1]?.Replace("\r\n", "
"), - LogColor = "SQL", - }).ToList(); + .Where(d => !string.IsNullOrEmpty(d) && d != "\n" && d != "\r\n") + .Select(d => new LogInfo + { + Datetime = d.Split("|")[0].ObjToDate(), + Content = d.Split("|")[1]?.Replace("\r\n", "
"), + LogColor = "SQL", + }).ToList(); } } - catch (Exception) { } + catch (Exception) + { + } //try //{ @@ -422,14 +437,17 @@ public static List GetLogData() { aopLogs.AddRange(excLogs); } + if (sqlLogs != null) { aopLogs.AddRange(sqlLogs); } + if (reqresLogs != null) { aopLogs.AddRange(reqresLogs); } + aopLogs = aopLogs.OrderByDescending(d => d.Import).ThenByDescending(d => d.Datetime).Take(100).ToList(); return aopLogs; @@ -450,7 +468,8 @@ public static RequestApiWeekView RequestApiinfoByWeek() Logs = GetRequestInfo(ReadType.Prefix); apiWeeks = (from n in Logs - group n by new { n.Week, n.Url } into g + group n by new { n.Week, n.Url } + into g select new ApiWeek { week = g.Key.Week, @@ -459,7 +478,6 @@ public static RequestApiWeekView RequestApiinfoByWeek() }).ToList(); //apiWeeks = apiWeeks.OrderByDescending(d => d.count).Take(8).ToList(); - } catch (Exception) { @@ -489,10 +507,12 @@ public static RequestApiWeekView RequestApiinfoByWeek() jsonBuilder.Append(item.count); jsonBuilder.Append("\","); } + if (apiweeksCurrentWeek.Count > 0) { jsonBuilder.Remove(jsonBuilder.Length - 1, 1); } + jsonBuilder.Append("},"); } @@ -500,6 +520,7 @@ public static RequestApiWeekView RequestApiinfoByWeek() { jsonBuilder.Remove(jsonBuilder.Length - 1, 1); } + jsonBuilder.Append("]"); //columns.AddRange(apiWeeks.OrderByDescending(d => d.count).Take(8).Select(d => d.url).ToList()); @@ -521,7 +542,8 @@ public static AccessApiDateView AccessApiByDate() Logs = GetRequestInfo(ReadType.Prefix); apiDates = (from n in Logs - group n by new { n.Date } into g + group n by new { n.Date } + into g select new ApiDate { date = g.Key.Date, @@ -529,7 +551,6 @@ public static AccessApiDateView AccessApiByDate() }).ToList(); apiDates = apiDates.OrderByDescending(d => d.date).Take(7).ToList(); - } catch (Exception) { @@ -552,7 +573,8 @@ public static AccessApiDateView AccessApiByHour() apiDates = (from n in Logs where n.Datetime.ObjToDate() >= DateTime.Today - group n by new { hour = n.Datetime.ObjToDate().Hour } into g + group n by new { hour = n.Datetime.ObjToDate().Hour } + into g select new ApiDate { date = g.Key.hour.ToString("00"), @@ -560,7 +582,6 @@ where n.Datetime.ObjToDate() >= DateTime.Today }).ToList(); apiDates = apiDates.OrderBy(d => d.date).Take(24).ToList(); - } catch (Exception) { @@ -580,14 +601,15 @@ public enum ReadType /// 精确查找一个 ///
Accurate, + /// /// 指定前缀,模糊查找全部 /// Prefix, + /// /// 指定前缀,最新一个文件 /// PrefixLatest } - -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 451d24f1..9eae3d92 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -18,10 +18,10 @@ - + @@ -35,6 +35,7 @@ + diff --git a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs index 85d96e9b..aed57769 100644 --- a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs @@ -4,14 +4,13 @@ using Blog.Core.Model; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; +using Serilog; namespace Blog.Core.Extensions.Middlewares { public class ExceptionHandlerMiddleware { private readonly RequestDelegate _next; - private static readonly log4net.ILog Log = - log4net.LogManager.GetLogger(typeof(ExceptionHandlerMiddleware)); public ExceptionHandlerMiddleware(RequestDelegate next) { @@ -48,7 +47,9 @@ private static async Task WriteExceptionAsync(HttpContext context, Exception e) context.Response.ContentType = "application/json"; - await context.Response.WriteAsync(JsonConvert.SerializeObject((new ApiResponse(StatusCode.CODE500, e.Message)).MessageModel)).ConfigureAwait(false); + await context.Response + .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, e.Message).MessageModel)) + .ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs b/Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs index 958d6ff3..7fe68fc4 100644 --- a/Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/IpLimitMiddleware.cs @@ -1,8 +1,8 @@ -using System; -using AspNetCoreRateLimit; +using AspNetCoreRateLimit; using Blog.Core.Common; -using log4net; using Microsoft.AspNetCore.Builder; +using System; +using Serilog; namespace Blog.Core.Extensions.Middlewares { @@ -11,7 +11,6 @@ namespace Blog.Core.Extensions.Middlewares ///
public static class IpLimitMiddleware { - private static readonly ILog Log = LogManager.GetLogger(typeof(IpLimitMiddleware)); public static void UseIpLimitMiddle(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); diff --git a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs index b6b91bd5..ccd0d7af 100644 --- a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs @@ -1,10 +1,10 @@ -using System; -using System.Threading.Tasks; -using Blog.Core.Common; +using Blog.Core.Common; using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; +using System; +using System.Threading.Tasks; namespace Blog.Core.Extensions.Middlewares { @@ -19,7 +19,6 @@ public class IpLogMiddleware ///
private readonly RequestDelegate _next; private readonly IWebHostEnvironment _environment; - private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(IpLogMiddleware)); /// /// diff --git a/Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs b/Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs index 068c4a0d..49e3b70d 100644 --- a/Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/MiniProfilerMiddleware.cs @@ -1,7 +1,7 @@ -using System; -using Blog.Core.Common; -using log4net; +using Blog.Core.Common; using Microsoft.AspNetCore.Builder; +using System; +using Serilog; namespace Blog.Core.Extensions.Middlewares { @@ -10,7 +10,6 @@ namespace Blog.Core.Extensions.Middlewares /// public static class MiniProfilerMiddleware { - private static readonly ILog Log = LogManager.GetLogger(typeof(MiniProfilerMiddleware)); public static void UseMiniProfilerMiddleware(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); diff --git a/Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs b/Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs index 1909c08f..f15df7b4 100644 --- a/Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/SignalRSendMiddleware.cs @@ -36,6 +36,7 @@ public async Task InvokeAsync(HttpContext context) { if (AppSettings.app("Middleware", "SignalR", "Enabled").ObjToBool()) { + //TODO 主动发送错误消息 await _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()); } await _next(context); diff --git a/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs b/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs index 099b2576..73af77d2 100644 --- a/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/SwaggerMiddleware.cs @@ -1,10 +1,10 @@ -using System; -using System.IO; -using System.Linq; -using Blog.Core.Common; -using log4net; +using Blog.Core.Common; using Microsoft.AspNetCore.Builder; using Swashbuckle.AspNetCore.SwaggerUI; +using System; +using System.IO; +using System.Linq; +using Serilog; using static Blog.Core.Extensions.CustomApiVersion; namespace Blog.Core.Extensions.Middlewares @@ -14,7 +14,6 @@ namespace Blog.Core.Extensions.Middlewares ///
public static class SwaggerMiddleware { - private static readonly ILog Log = LogManager.GetLogger(typeof(SwaggerMiddleware)); public static void UseSwaggerMiddle(this IApplicationBuilder app, Func streamHtml) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -24,10 +23,7 @@ public static void UseSwaggerMiddle(this IApplicationBuilder app, Func s { //根据版本名称倒序 遍历展示 var apiName = AppSettings.app(new string[] { "Startup", "ApiName" }); - typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version => - { - c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{apiName} {version}"); - }); + typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version => { c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{apiName} {version}"); }); c.SwaggerEndpoint($"https://petstore.swagger.io/v2/swagger.json", $"{apiName} pet"); @@ -38,12 +34,13 @@ public static void UseSwaggerMiddle(this IApplicationBuilder app, Func s Log.Error(msg); throw new Exception(msg); } + c.IndexStream = streamHtml; c.DocExpansion(DocExpansion.None); //->修改界面打开时自动折叠 if (Permissions.IsUseIds4) { - c.OAuthClientId("blogadminjs"); + c.OAuthClientId("blogadminjs"); } @@ -52,4 +49,4 @@ public static void UseSwaggerMiddle(this IApplicationBuilder app, Func s }); } } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs index 0622e766..0336567f 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs @@ -76,10 +76,10 @@ public static void AddAppConfigSetup(this IServiceCollection services, IHostEnvi var ipLogOpen = AppSettings.app(new string[] { "Middleware", "IPLog", "Enabled" }).ObjToBool(); var recordAccessLogsOpen = AppSettings.app(new string[] { "Middleware", "RecordAccessLogs", "Enabled" }).ObjToBool(); ConsoleHelper.WriteSuccessLine($"OPEN Log: " + - (requestResponseLogOpen ? "RequestResponseLog √," : "") + - (ipLogOpen ? "IPLog √," : "") + - (recordAccessLogsOpen ? "RecordAccessLogs √," : "") - ); + (requestResponseLogOpen ? "RequestResponseLog √," : "") + + (ipLogOpen ? "IPLog √," : "") + + (recordAccessLogsOpen ? "RecordAccessLogs √," : "") + ); // 事务AOP if (!AppSettings.app(new string[] { "AppSettings", "TranAOP", "Enabled" }).ObjToBool()) @@ -213,7 +213,6 @@ public static void AddAppConfigSetup(this IServiceCollection services, IHostEnvi Console.WriteLine(); } - } public static void AddAppTableConfigSetup(this IServiceCollection services, IHostEnvironment env) @@ -222,7 +221,6 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos if (AppSettings.app(new string[] { "Startup", "AppConfigAlert", "Enabled" }).ObjToBool()) { - if (env.IsDevelopment()) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -230,6 +228,7 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos } #region 程序配置 + List configInfos = new() { new string[] { "当前环境", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") }, @@ -238,7 +237,7 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos new string[] { "RabbitMQ消息列队", AppSettings.app("RabbitMQ", "Enabled") }, new string[] { "事件总线(必须开启消息列队)", AppSettings.app("EventBus", "Enabled") }, new string[] { "redis消息队列", AppSettings.app("Startup", "RedisMq", "Enabled") }, - new string[] { "是否多库", AppSettings.app("MutiDBEnabled" ) }, + new string[] { "是否多库", AppSettings.app("MutiDBEnabled") }, new string[] { "读写分离", AppSettings.app("CQRSEnabled") }, }; @@ -253,17 +252,19 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos TableStyle = TableStyle.Alternative }.Writer(ConsoleColor.Blue); Console.WriteLine(); + #endregion 程序配置 #region AOP + List aopInfos = new() -{ + { new string[] { "Redis缓存AOP", AppSettings.app("AppSettings", "RedisCachingAOP", "Enabled") }, new string[] { "内存缓存AOP", AppSettings.app("AppSettings", "MemoryCachingAOP", "Enabled") }, - new string[] { "服务日志AOP", AppSettings.app("AppSettings", "LogAOP", "Enabled" ) }, - new string[] { "事务AOP", AppSettings.app("AppSettings", "TranAOP", "Enabled" ) }, - new string[] { "Sql执行AOP", AppSettings.app("AppSettings", "SqlAOP", "OutToLogFile", "Enabled" ) }, - new string[] { "Sql执行AOP控制台输出", AppSettings.app("AppSettings", "SqlAOP", "OutToConsole", "Enabled" ) }, + new string[] { "服务日志AOP", AppSettings.app("AppSettings", "LogAOP", "Enabled") }, + new string[] { "事务AOP", AppSettings.app("AppSettings", "TranAOP", "Enabled") }, + new string[] { "Sql执行AOP", AppSettings.app("AppSettings", "SqlAOP", "Enabled") }, + new string[] { "Sql执行AOP控制台输出", AppSettings.app("AppSettings", "SqlAOP", "LogToConsole", "Enabled") }, }; new ConsoleTable @@ -277,15 +278,17 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos TableStyle = TableStyle.Alternative }.Writer(ConsoleColor.Blue); Console.WriteLine(); + #endregion AOP #region 中间件 + List MiddlewareInfos = new() { new string[] { "请求纪录中间件", AppSettings.app("Middleware", "RecordAccessLogs", "Enabled") }, - new string[] { "IP记录中间件", AppSettings.app("Middleware", "IPLog", "Enabled" ) }, - new string[] { "请求响应日志中间件", AppSettings.app("Middleware", "RequestResponseLog", "Enabled" ) }, - new string[] { "SingnalR实时发送请求数据中间件", AppSettings.app("Middleware", "SignalR", "Enabled" ) }, + new string[] { "IP记录中间件", AppSettings.app("Middleware", "IPLog", "Enabled") }, + new string[] { "请求响应日志中间件", AppSettings.app("Middleware", "RequestResponseLog", "Enabled") }, + new string[] { "SingnalR实时发送请求数据中间件", AppSettings.app("Middleware", "SignalR", "Enabled") }, new string[] { "IP限流中间件", AppSettings.app("Middleware", "IpRateLimit", "Enabled") }, new string[] { "性能分析中间件", AppSettings.app("Startup", "MiniProfiler", "Enabled") }, new string[] { "Consul注册服务", AppSettings.app("Middleware", "Consul", "Enabled") }, @@ -302,10 +305,9 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos TableStyle = TableStyle.Alternative }.Writer(ConsoleColor.Blue); Console.WriteLine(); - #endregion 中间件 + #endregion 中间件 } - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs b/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs index 7f2997bd..4351962b 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs @@ -6,20 +6,18 @@ using Blog.Core.IServices.BASE; using Blog.Core.Model; using Blog.Core.Repository.Base; +using Blog.Core.Repository.UnitOfWorks; using Blog.Core.Services.BASE; -using log4net; using System; using System.Collections.Generic; using System.IO; using System.Reflection; -using Blog.Core.Repository.UnitOfWorks; +using Serilog; namespace Blog.Core.Extensions { public class AutofacModuleRegister : Autofac.Module { - private static readonly ILog log = LogManager.GetLogger(typeof(AutofacModuleRegister)); - protected override void Load(ContainerBuilder builder) { var basePath = AppContext.BaseDirectory; @@ -34,39 +32,39 @@ protected override void Load(ContainerBuilder builder) if (!(File.Exists(servicesDllFile) && File.Exists(repositoryDllFile))) { var msg = "Repository.dll和service.dll 丢失,因为项目解耦了,所以需要先F6编译,再F5运行,请检查 bin 文件夹,并拷贝。"; - log.Error(msg); + Log.Error(msg); throw new Exception(msg); } // AOP 开关,如果想要打开指定的功能,只需要在 appsettigns.json 对应对应 true 就行。 var cacheType = new List(); - if (AppSettings.app(new string[] {"AppSettings", "RedisCachingAOP", "Enabled"}).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "RedisCachingAOP", "Enabled" }).ObjToBool()) { builder.RegisterType(); cacheType.Add(typeof(BlogRedisCacheAOP)); } - if (AppSettings.app(new string[] {"AppSettings", "MemoryCachingAOP", "Enabled"}).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "MemoryCachingAOP", "Enabled" }).ObjToBool()) { builder.RegisterType(); cacheType.Add(typeof(BlogCacheAOP)); } - if (AppSettings.app(new string[] {"AppSettings", "TranAOP", "Enabled"}).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "TranAOP", "Enabled" }).ObjToBool()) { builder.RegisterType(); cacheType.Add(typeof(BlogTranAOP)); } - if (AppSettings.app(new string[] {"AppSettings", "LogAOP", "Enabled"}).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "LogAOP", "Enabled" }).ObjToBool()) { builder.RegisterType(); cacheType.Add(typeof(BlogLogAOP)); } - builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>)).InstancePerDependency();//注册仓储 - builder.RegisterGeneric(typeof(BaseServices<>)).As(typeof(IBaseServices<>)).InstancePerDependency();//注册服务 + builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>)).InstancePerDependency(); //注册仓储 + builder.RegisterGeneric(typeof(BaseServices<>)).As(typeof(IBaseServices<>)).InstancePerDependency(); //注册服务 // 获取 Service.dll 程序集服务,并注册 var assemblysServices = Assembly.LoadFrom(servicesDllFile); diff --git a/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs b/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs index e8e3929f..b3147ca8 100644 --- a/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs @@ -1,4 +1,4 @@ -using Blog.Core.Common.HttpPolly; +using Blog.Core.Common.Https.HttpPolly; using Blog.Core.Model; using Microsoft.Extensions.DependencyInjection; using Polly; diff --git a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs new file mode 100644 index 00000000..a112409c --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs @@ -0,0 +1,37 @@ +using Blog.Core.Common; +using Blog.Core.Common.LogHelper; +using Blog.Core.Serilog.Extensions; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Debugging; +using System; +using System.IO; + +namespace Blog.Core.Extensions.ServiceExtensions; + +public static class SerilogSetup +{ + public static IHostBuilder AddSerilogSetup(this IHostBuilder host) + { + if (host == null) throw new ArgumentNullException(nameof(host)); + + var loggerConfiguration = new LoggerConfiguration() + .ReadFrom.Configuration(AppSettings.Configuration) + .Enrich.FromLogContext() + //输出到控制台 + .WriteToConsole() + //将日志保存到文件中 + .WriteToFile(); + //配置日志库 + //.WriteToLogBatching(); + + Log.Logger = loggerConfiguration.CreateLogger(); + + //Serilog 内部日志 + var file = File.CreateText(LogContextStatic.Combine($"SerilogDebug{DateTime.Now:yyyyMMdd}.txt")); + SelfLog.Enable(TextWriter.Synchronized(file)); + + host.UseSerilog(); + return host; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 3ecf224a..3440b8af 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -1,6 +1,7 @@ using Blog.Core.Common; +using Blog.Core.Common.Const; using Blog.Core.Common.DB; -using Blog.Core.Common.Helper; +using Blog.Core.Common.DB.Aop; using Blog.Core.Common.LogHelper; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; @@ -9,7 +10,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Blog.Core.Common.DB.Aop; namespace Blog.Core.Extensions { @@ -48,7 +48,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) BaseDBConfig.MutiConnectionString.allDbs.ForEach(m => { - listConfig.Add(new ConnectionConfig() + var config = new ConnectionConfig() { ConfigId = m.ConnId.ObjToString().ToLower(), ConnectionString = m.Connection, @@ -56,29 +56,6 @@ public static void AddSqlsugarSetup(this IServiceCollection services) IsAutoCloseConnection = true, // Check out more information: https://github.com/anjoy8/Blog.Core/issues/122 //IsShardSameThread = false, - AopEvents = new AopEvents - { - OnLogExecuting = (sql, p) => - { - if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "Enabled" }).ObjToBool()) - { - if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToFile", "Enabled" }).ObjToBool()) - { - Parallel.For(0, 1, e => - { - MiniProfiler.Current.CustomTiming("SQL:", GetParas(p) + "【SQL语句】:" + sql); - //LogLock.OutSql2Log("SqlLog", new string[] { GetParas(p), "【SQL语句】:" + sql }); - LogLock.OutLogAOP("SqlLog", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); - - }); - } - if (AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToConsole", "Enabled" }).ObjToBool()) - { - ConsoleHelper.WriteColorLine(string.Join("\r\n", new string[] { "--------", $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} :" + GetWholeSql(p, sql) }), ConsoleColor.DarkCyan); - } - } - }, - }, MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, @@ -99,15 +76,29 @@ public static void AddSqlsugarSetup(this IServiceCollection services) } }, InitKeyType = InitKeyType.Attribute + }; + if (SqlSugarConst.LogConfigId.Equals(m.ConnId)) + { + BaseDBConfig.LogConfig = config; } - ); + + listConfig.Add(config); }); + + if (BaseDBConfig.LogConfig is null) + { + throw new ApplicationException("未配置Log库连接"); + } + return new SqlSugarScope(listConfig, db => { listConfig.ForEach(config => { var dbProvider = db.GetConnectionScope((string)config.ConfigId); + // 打印SQL语句 + dbProvider.Aop.OnLogExecuting = (s, parameters) => SqlSugarAop.OnLogExecuting(dbProvider,s, parameters, config); + // 数据审计 dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; diff --git a/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs index 449b6a4d..85dff620 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SwaggerSetup.cs @@ -1,7 +1,7 @@ using Blog.Core.Common; -using log4net; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using Serilog; using Swashbuckle.AspNetCore.Filters; using System; using System.Collections.Generic; @@ -17,10 +17,6 @@ namespace Blog.Core.Extensions /// public static class SwaggerSetup { - - private static readonly ILog log = - LogManager.GetLogger(typeof(SwaggerSetup)); - public static void AddSwaggerSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); @@ -59,7 +55,7 @@ public static void AddSwaggerSetup(this IServiceCollection services) } catch (Exception ex) { - log.Error("Blog.Core.xml和Blog.Core.Model.xml 丢失,请检查并拷贝。\n" + ex.Message); + Log.Error("Blog.Core.xml和Blog.Core.Model.xml 丢失,请检查并拷贝。\n" + ex.Message); } // 开启加权小锁 @@ -82,12 +78,13 @@ public static void AddSwaggerSetup(this IServiceCollection services) Implicit = new OpenApiOAuthFlow { AuthorizationUrl = new Uri($"{AppSettings.app(new string[] { "Startup", "IdentityServer4", "AuthorizationUrl" })}/connect/authorize"), - Scopes = new Dictionary { + Scopes = new Dictionary { - "blog.core.api","ApiResource id" + { + "blog.core.api", "ApiResource id" + } } } - } } }); } @@ -97,14 +94,11 @@ public static void AddSwaggerSetup(this IServiceCollection services) c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)\"", - Name = "Authorization",//jwt默认的参数名称 - In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) + Name = "Authorization", //jwt默认的参数名称 + In = ParameterLocation.Header, //jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.ApiKey }); } - - - }); services.AddSwaggerGenNewtonsoftSupport(); } @@ -124,11 +118,11 @@ public enum ApiVersions /// V1 版本 /// V1 = 1, + /// /// V2 版本 /// V2 = 2, } } - -} +} \ No newline at end of file diff --git a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj index 5cf82020..398aab48 100644 --- a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj +++ b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj @@ -11,7 +11,7 @@ - + diff --git a/Blog.Core.Serilog/Blog.Core.Serilog.csproj b/Blog.Core.Serilog/Blog.Core.Serilog.csproj new file mode 100644 index 00000000..07fa26f3 --- /dev/null +++ b/Blog.Core.Serilog/Blog.Core.Serilog.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs b/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs new file mode 100644 index 00000000..2736aa1f --- /dev/null +++ b/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs @@ -0,0 +1,121 @@ +using Blog.Core.Common; +using Blog.Core.Common.LogHelper; +using Serilog; +using Serilog.Events; +using Serilog.Filters; +using SqlSugar; + +namespace Blog.Core.Serilog.Extensions; + +public static class LoggerConfigurationExtensions +{ + public static LoggerConfiguration WriteToSqlServer(this LoggerConfiguration loggerConfiguration) + { + var logConnectionStrings = AppSettings.app("LogConnectionStrings"); + if (logConnectionStrings.IsNullOrEmpty()) return loggerConfiguration; + + //输出SQL + //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + // lg.FilterSqlLog().WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() + // { + // TableName = "SqlLog", + // AutoCreateSqlTable = true + // })); + + //输出普通日志 + //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + // lg.FilterRemoveSqlLog().Filter.ByIncludingOnly(p => p.Level >= LogEventLevel.Error) + // .WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() + // { + // TableName = "ErrorLog", + // AutoCreateSqlTable = true + // })); + //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + // lg.FilterRemoveSqlLog().Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Warning) + // .WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() + // { + // TableName = "WarningLog", + // AutoCreateSqlTable = true + // })); + //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + // lg.FilterRemoveSqlLog().Filter.ByIncludingOnly(p => p.Level <= LogEventLevel.Information) + // .WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() + // { + // TableName = "InformationLog", + // AutoCreateSqlTable = true + // })); + + return loggerConfiguration; + } + + public static LoggerConfiguration WriteToConsole(this LoggerConfiguration loggerConfiguration) + { + //输出普通日志 + loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + lg.FilterRemoveSqlLog().WriteTo.Console()); + + //输出SQL + loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + lg.FilterSqlLog().Filter.ByIncludingOnly(Matching.WithProperty(LogContextStatic.SqlOutToConsole, s => s)) + .WriteTo.Console()); + + return loggerConfiguration; + } + + public static LoggerConfiguration WriteToFile(this LoggerConfiguration loggerConfiguration) + { + //输出SQL + loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + lg.FilterSqlLog().Filter.ByIncludingOnly(Matching.WithProperty(LogContextStatic.SqlOutToFile, s => s)) + .WriteTo.Async(s => s.File(LogContextStatic.Combine(LogContextStatic.AopSql, @"AopSql.txt"), rollingInterval: RollingInterval.Day, + outputTemplate: LogContextStatic.FileMessageTemplate, retainedFileCountLimit: 31))); + //输出普通日志 + loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => + lg.FilterRemoveSqlLog().WriteTo.Async(s => s.File(LogContextStatic.Combine(LogContextStatic.BasePathLogs, @"Log.txt"), rollingInterval: RollingInterval.Day, + outputTemplate: LogContextStatic.FileMessageTemplate, retainedFileCountLimit: 31))); + return loggerConfiguration; + } + + public static LoggerConfiguration FilterSqlLog(this LoggerConfiguration lc) + { + lc = lc.Filter.ByIncludingOnly(Matching.WithProperty(LogContextStatic.LogSource, s => LogContextStatic.AopSql.Equals(s))); + return lc; + } + + public static IEnumerable FilterSqlLog(this IEnumerable batch) + { + return batch.Where(s => s.WithProperty(LogContextStatic.LogSource, q => LogContextStatic.AopSql.Equals(q))) + .Where(s => s.WithProperty(LogContextStatic.SugarActionType, + q => !new[] { SugarActionType.UnKnown, SugarActionType.Query }.Contains(q))); + } + + public static LoggerConfiguration FilterRemoveSqlLog(this LoggerConfiguration lc) + { + lc = lc.Filter.ByIncludingOnly(WithProperty(LogContextStatic.LogSource, s => !LogContextStatic.AopSql.Equals(s))); + return lc; + } + + public static IEnumerable FilterRemoveOtherLog(this IEnumerable batch) + { + return batch.Where(s => WithProperty(LogContextStatic.LogSource, + q => !LogContextStatic.AopSql.Equals(q))(s)); + } + + public static Func WithProperty(string propertyName, Func predicate) + { + //如果不包含属性 也认为是true + return e => + { + if (!e.Properties.TryGetValue(propertyName, out var propertyValue)) return true; + + return propertyValue is ScalarValue { Value: T value } && predicate(value); + }; + } + + public static bool WithProperty(this LogEvent e, string key, Func predicate) + { + if (!e.Properties.TryGetValue(key, out var propertyValue)) return false; + + return propertyValue is ScalarValue { Value: T value } && predicate(value); + } +} \ No newline at end of file diff --git a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs new file mode 100644 index 00000000..cab7ae55 --- /dev/null +++ b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Serilog.Events; + +namespace Blog.Core.Serilog.Utility; + +public class SerilogRequestUtility +{ + private static readonly List _ignoreUrl = new() + { + "/job", + }; + + private static LogEventLevel DefaultGetLevel( + HttpContext ctx, + double _, + Exception? ex) + { + return ex is null && ctx.Response.StatusCode <= 499 ? LogEventLevel.Information : LogEventLevel.Error; + } + + public static LogEventLevel GetRequestLevel(HttpContext ctx, double _, Exception? ex) => + ex is null && ctx.Response.StatusCode <= 499 ? IgnoreRequest(ctx) : LogEventLevel.Error; + + private static LogEventLevel IgnoreRequest(HttpContext ctx) + { + var path = ctx.Request.Path.Value; + if (path.IsNullOrEmpty()) + { + return LogEventLevel.Information; + } + + return _ignoreUrl.Any(s => path.StartsWith(s)) ? LogEventLevel.Verbose : LogEventLevel.Information; + } +} \ No newline at end of file diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs index 1dcc57ed..1d501c34 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs @@ -34,7 +34,7 @@ public async Task Execute(IJobExecutionContext context) } public async Task Run(IJobExecutionContext context) { - + // 可以直接获取 JobDetail 的值 var jobKey = context.JobDetail.Key; var jobId = jobKey.Name; @@ -94,7 +94,7 @@ await _accessTrendLogServices.Add(new AccessTrendLog() Parallel.For(0, 1, e => { - LogLock.OutLogAOP("ACCESSTRENDLOG","",new string[] { activeUserVMs.GetType().ToString(), JsonConvert.SerializeObject(activeUserVMs) }, false); + LogLock.OutLogAOP("ACCESSTRENDLOG", "", new string[] { activeUserVMs.GetType().ToString(), JsonConvert.SerializeObject(activeUserVMs) }, false); }); } diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs index 18c4c298..abcd81ea 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs @@ -17,19 +17,21 @@ namespace Blog.Core.Tasks { public class Job_OperateLog_Quartz : JobBase, IJob { - private readonly IOperateLogServices _operateLogServices; + private readonly IOperateLogServices _operateLogServices; private readonly IWebHostEnvironment _environment; - public Job_OperateLog_Quartz(IOperateLogServices operateLogServices,IWebHostEnvironment environment, ITasksQzServices tasksQzServices,ITasksLogServices tasksLogServices) - :base(tasksQzServices, tasksLogServices) + public Job_OperateLog_Quartz(IOperateLogServices operateLogServices, IWebHostEnvironment environment, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { - _operateLogServices = operateLogServices; - _environment = environment; + _operateLogServices = operateLogServices; + _environment = environment; } + public async Task Execute(IJobExecutionContext context) { var executeLog = await ExecuteJob(context, async () => await Run(context)); } + public async Task Run(IJobExecutionContext context) { @@ -78,7 +80,4 @@ public async Task Run(IJobExecutionContext context) } } } - - - -} +} \ No newline at end of file diff --git a/Blog.Core.Tests/DependencyInjection/DI_Test.cs b/Blog.Core.Tests/DependencyInjection/DI_Test.cs index ff2c74cd..d425fa01 100644 --- a/Blog.Core.Tests/DependencyInjection/DI_Test.cs +++ b/Blog.Core.Tests/DependencyInjection/DI_Test.cs @@ -59,7 +59,6 @@ public IContainer DICollections() services.AddAutoMapper(typeof(Startup)); services.AddSingleton(new AppSettings(basePath)); - services.AddSingleton(new LogLock(basePath)); services.AddScoped(); services.AddScoped(); diff --git a/Blog.Core.sln b/Blog.Core.sln index c8f61505..bf4f65cb 100644 --- a/Blog.Core.sln +++ b/Blog.Core.sln @@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blog.Core.Serilog.Es", "Blo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Nacos", "Ocelot.Provider.Nacos\Ocelot.Provider.Nacos.csproj", "{6463FB13-5F01-4A1D-8B62-A454FB3812EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blog.Core.Serilog", "Blog.Core.Serilog\Blog.Core.Serilog.csproj", "{7F9057F0-ED8D-4694-B590-7D75C012DF00}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -119,6 +121,10 @@ Global {6463FB13-5F01-4A1D-8B62-A454FB3812EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {6463FB13-5F01-4A1D-8B62-A454FB3812EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {6463FB13-5F01-4A1D-8B62-A454FB3812EB}.Release|Any CPU.Build.0 = Release|Any CPU + {7F9057F0-ED8D-4694-B590-7D75C012DF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F9057F0-ED8D-4694-B590-7D75C012DF00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F9057F0-ED8D-4694-B590-7D75C012DF00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F9057F0-ED8D-4694-B590-7D75C012DF00}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 070e441bc02ef0a1e410d176cf953e6549824721 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 2 Apr 2023 15:16:37 +0800 Subject: [PATCH 150/289] feat: :accept: change api param --- Blog.Core.Api/Blog.Core.xml | 50 ++++++++--------- Blog.Core.Api/Controllers/BlogController.cs | 10 ++-- .../Controllers/DepartmentController.cs | 4 +- Blog.Core.Api/Controllers/ModuleController.cs | 2 +- .../Controllers/PermissionController.cs | 55 +++++++------------ Blog.Core.Api/Controllers/RoleController.cs | 2 +- .../Controllers/TasksQzController.cs | 18 +++--- Blog.Core.Api/Controllers/TopicController.cs | 6 +- .../Controllers/TopicDetailController.cs | 8 +-- .../Controllers/TransactionController.cs | 6 +- Blog.Core.Api/Controllers/UserController.cs | 4 +- .../Controllers/UserRoleController.cs | 2 +- Blog.Core.Common/Helper/RecursionHelper.cs | 4 +- Blog.Core.IServices/IBlogArticleServices.cs | 2 +- Blog.Core.IServices/ITasksLogServices.cs | 4 +- Blog.Core.IServices/IUserRoleServices.cs | 4 +- Blog.Core.Model/ViewModels/BlogViewModels.cs | 2 +- Blog.Core.Model/ViewModels/SysUserInfoDto.cs | 6 +- Blog.Core.Services/BlogArticleServices.cs | 2 +- Blog.Core.Services/TasksLogServices.cs | 4 +- Blog.Core.Services/UserRoleServices.cs | 4 +- .../Controller_Test/BlogController_Should.cs | 2 +- 22 files changed, 93 insertions(+), 108 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 79325e37..19f0ad64 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -26,14 +26,14 @@
- + 获取博客详情 - + 获取详情【无权限】 @@ -67,7 +67,7 @@ - + 删除博客 @@ -276,7 +276,7 @@ - + 删除一条接口 @@ -384,7 +384,7 @@ - + 查询树形 Table @@ -406,7 +406,7 @@ - + 获取菜单树 @@ -428,7 +428,7 @@ - + 通过角色获取菜单 @@ -442,7 +442,7 @@ - + 删除菜单 @@ -456,7 +456,7 @@ - + 系统接口菜单同步接口 @@ -493,7 +493,7 @@ - + 删除角色 @@ -522,42 +522,42 @@ - + 删除一个任务 - + 启动计划任务 - + 停止一个计划任务 - + 暂停一个计划任务 - + 恢复一个计划任务 - + 重启一个计划任务 @@ -570,20 +570,20 @@ - + 立即执行任务 - + 获取任务运行日志 - + 任务概况 @@ -629,7 +629,7 @@ - + 获取详情【无权限】 @@ -650,14 +650,14 @@ - + 删除 bug - + 测试事务在AOP中的使用 @@ -712,7 +712,7 @@ - + 删除用户 @@ -748,7 +748,7 @@ - + 新建用户角色关系 @@ -1203,7 +1203,7 @@ 关键字 - + 获取部门树 diff --git a/Blog.Core.Api/Controllers/BlogController.cs b/Blog.Core.Api/Controllers/BlogController.cs index 83fad967..fbc67e12 100644 --- a/Blog.Core.Api/Controllers/BlogController.cs +++ b/Blog.Core.Api/Controllers/BlogController.cs @@ -83,7 +83,7 @@ public async Task>> Get(int id, int page = 1 [HttpGet("{id}")] //[Authorize(Policy = "Scope_BlogModule_Policy")] [Authorize] - public async Task> Get(int id) + public async Task> Get(long id) { return Success(await _blogArticleServices.GetBlogDetails(id)); } @@ -96,7 +96,7 @@ public async Task> Get(int id) /// [HttpGet] [Route("DetailNuxtNoPer")] - public async Task> DetailNuxtNoPer(int id) + public async Task> DetailNuxtNoPer(long id) { _logger.LogInformation("xxxxxxxxxxxxxxxxxxx"); return Success(await _blogArticleServices.GetBlogDetails(id)); @@ -104,7 +104,7 @@ public async Task> DetailNuxtNoPer(int id) [HttpGet] [Route("GoUrl")] - public async Task GoUrl(int id = 0) + public async Task GoUrl(long id = 0) { var response = await _blogArticleServices.QueryById(id); if (response != null && response.bsubmitter.IsNotEmptyOrNull()) @@ -136,7 +136,7 @@ public async Task>> GetBlogsByTypesForMVP(string [HttpGet] [Route("GetBlogByIdForMVP")] - public async Task> GetBlogByIdForMVP(int id = 0) + public async Task> GetBlogByIdForMVP(long id = 0) { if (id > 0) { @@ -247,7 +247,7 @@ public async Task> Put([FromBody] BlogArticle BlogArticle) [HttpDelete] [Authorize(Permissions.Name)] [Route("Delete")] - public async Task> Delete(int id) + public async Task> Delete(long id) { if (id > 0) { diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index b8174e90..faf1f850 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -109,7 +109,7 @@ public async Task>> GetTreeTable(long f = 0, strin /// /// [HttpGet] - public async Task> GetDepartmentTree(int pid = 0) + public async Task> GetDepartmentTree(long pid = 0) { var departments = await _departmentServices.Query(d => d.IsDeleted == false); var departmentTrees = (from child in departments @@ -168,7 +168,7 @@ public async Task> Put([FromBody] Department request) } [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); var model = await _departmentServices.QueryById(id); diff --git a/Blog.Core.Api/Controllers/ModuleController.cs b/Blog.Core.Api/Controllers/ModuleController.cs index 2da284ba..27e6f4db 100644 --- a/Blog.Core.Api/Controllers/ModuleController.cs +++ b/Blog.Core.Api/Controllers/ModuleController.cs @@ -119,7 +119,7 @@ public async Task> Put([FromBody] Modules module) /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { if (id <= 0) return Failed("缺少参数"); diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 059e9b18..7346cc21 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -79,21 +79,6 @@ public async Task>> Get(int page = 1, string key = ""; } - #region 舍弃 - //var permissions = await _permissionServices.Query(a => a.IsDeleted != true); - //if (!string.IsNullOrEmpty(key)) - //{ - // permissions = permissions.Where(t => (t.Name != null && t.Name.Contains(key))).ToList(); - //} - ////筛选后的数据总数 - //totalCount = permissions.Count; - ////筛选后的总页数 - //pageCount = (Math.Ceiling(totalCount.ObjToDecimal() / intTotalCount.ObjToDecimal())).ObjToInt(); - //permissions = permissions.OrderByDescending(d => d.Id).Skip((page - 1) * intTotalCount).Take(intTotalCount).ToList(); - #endregion - - - permissions = await _permissionServices.QueryPage(a => a.IsDeleted != true && (a.Name != null && a.Name.Contains(key)), page, pageSize, " Id desc "); @@ -162,7 +147,7 @@ public async Task>> Get(int page = 1, string /// [HttpGet] [AllowAnonymous] - public async Task>> GetTreeTable(int f = 0, string key = "") + public async Task>> GetTreeTable(long f = 0, string key = "") { List permissions = new List(); var apiList = await _moduleServices.Query(d => d.IsDeleted == false); @@ -245,7 +230,7 @@ public async Task> Post([FromBody] Permission permission) /// [HttpPost] public async Task> Assign([FromBody] AssignView assignView) - { + { if (assignView.rid > 0) { //开启事务 @@ -258,7 +243,7 @@ public async Task> Assign([FromBody] AssignView assignView) var permissions = await _permissionServices.Query(d => d.IsDeleted == false); List new_rmps = new List(); - var nowTime = _permissionServices.Db.GetDate(); + var nowTime = _permissionServices.Db.GetDate(); foreach (var item in assignView.pids) { var moduleid = permissions.Find(p => p.Id == item)?.Mid; @@ -268,7 +253,7 @@ public async Task> Assign([FromBody] AssignView assignView) { IsDeleted = false, RoleId = assignView.rid, - ModuleId = moduleid.ObjToInt(), + ModuleId = moduleid.ObjToLong(), PermissionId = item, CreateId = find_old_rmps == null ? _user.ID : find_old_rmps.CreateId, CreateBy = find_old_rmps == null ? _user.Name : find_old_rmps.CreateBy, @@ -280,7 +265,7 @@ public async Task> Assign([FromBody] AssignView assignView) }; new_rmps.Add(roleModulePermission); } - if(new_rmps.Count>0) await _roleModulePermissionServices.Add(new_rmps); + if (new_rmps.Count > 0) await _roleModulePermissionServices.Add(new_rmps); _unitOfWorkManage.CommitTran(); } catch (Exception) @@ -294,7 +279,7 @@ public async Task> Assign([FromBody] AssignView assignView) else { return Failed("请选择要操作的角色"); - } + } } @@ -305,7 +290,7 @@ public async Task> Assign([FromBody] AssignView assignView) /// /// [HttpGet] - public async Task> GetPermissionTree(int pid = 0, bool needbtn = false) + public async Task> GetPermissionTree(long pid = 0, bool needbtn = false) { //var data = new MessageModel(); @@ -355,7 +340,7 @@ public async Task> GetNavigationBar(long uid) var data = new MessageModel(); - var uidInHttpcontext1 = 0; + long uidInHttpcontext1 = 0; var roleIds = new List(); // ids4和jwt切换 if (Permissions.IsUseIds4) @@ -363,7 +348,7 @@ public async Task> GetNavigationBar(long uid) // ids4 uidInHttpcontext1 = (from item in _httpContext.HttpContext.User.Claims where item.Type == "sub" - select item.Value).FirstOrDefault().ObjToInt(); + select item.Value).FirstOrDefault().ObjToLong(); roleIds = (from item in _httpContext.HttpContext.User.Claims where item.Type == "role" select item.Value.ObjToLong()).ToList(); @@ -371,7 +356,7 @@ public async Task> GetNavigationBar(long uid) else { // jwt - uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); + uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToLong(); roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToLong()).Distinct().ToList(); } @@ -446,7 +431,7 @@ public async Task>> GetNavigationBarPro(long { var data = new MessageModel>(); - var uidInHttpcontext1 = 0; + long uidInHttpcontext1 = 0; var roleIds = new List(); // ids4和jwt切换 if (Permissions.IsUseIds4) @@ -454,7 +439,7 @@ public async Task>> GetNavigationBarPro(long // ids4 uidInHttpcontext1 = (from item in _httpContext.HttpContext.User.Claims where item.Type == "sub" - select item.Value).FirstOrDefault().ObjToInt(); + select item.Value).FirstOrDefault().ObjToLong(); roleIds = (from item in _httpContext.HttpContext.User.Claims where item.Type == "role" select item.Value.ObjToLong()).ToList(); @@ -462,7 +447,7 @@ public async Task>> GetNavigationBarPro(long else { // jwt - uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); + uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToLong(); roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToLong()).Distinct().ToList(); } @@ -519,14 +504,14 @@ orderby item.Id /// [HttpGet] [AllowAnonymous] - public async Task> GetPermissionIdByRoleId(int rid = 0) + public async Task> GetPermissionIdByRoleId(long rid = 0) { //var data = new MessageModel(); var rmps = await _roleModulePermissionServices.Query(d => d.IsDeleted == false && d.RoleId == rid); var permissionTrees = (from child in rmps orderby child.Id - select child.PermissionId.ObjToInt()).ToList(); + select child.PermissionId.ObjToLong()).ToList(); var permissions = await _permissionServices.Query(d => d.IsDeleted == false); List assignbtns = new List(); @@ -592,7 +577,7 @@ public async Task> Put([FromBody] Permission permission) /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); if (id > 0) @@ -654,7 +639,7 @@ public async Task> BatchPost([FromBody] List pe /// 是否执行迁移到数据 /// [HttpGet] - public async Task>> MigratePermission(string action = "", string controllerName = "", int pid = 0, bool isAction = false) + public async Task>> MigratePermission(string action = "", string controllerName = "", long pid = 0, bool isAction = false) { var data = new MessageModel>(); if (controllerName.IsNullOrEmpty()) @@ -776,12 +761,12 @@ public async Task>> MigratePermission(string actio public class AssignView { - public List pids { get; set; } - public int rid { get; set; } + public List pids { get; set; } + public long rid { get; set; } } public class AssignShow { - public List permissionids { get; set; } + public List permissionids { get; set; } public List assignbtns { get; set; } } diff --git a/Blog.Core.Api/Controllers/RoleController.cs b/Blog.Core.Api/Controllers/RoleController.cs index c96502c8..0b93e943 100644 --- a/Blog.Core.Api/Controllers/RoleController.cs +++ b/Blog.Core.Api/Controllers/RoleController.cs @@ -112,7 +112,7 @@ public async Task> Put([FromBody] Role role) /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); diff --git a/Blog.Core.Api/Controllers/TasksQzController.cs b/Blog.Core.Api/Controllers/TasksQzController.cs index ca9d37bc..887cfcfc 100644 --- a/Blog.Core.Api/Controllers/TasksQzController.cs +++ b/Blog.Core.Api/Controllers/TasksQzController.cs @@ -172,7 +172,7 @@ public async Task> Put([FromBody] TasksQz tasksQz) /// /// [HttpDelete] - public async Task> Delete(int jobId) + public async Task> Delete(long jobId) { var data = new MessageModel(); @@ -221,7 +221,7 @@ public async Task> Delete(int jobId) /// /// [HttpGet] - public async Task> StartJob(int jobId) + public async Task> StartJob(long jobId) { var data = new MessageModel(); @@ -278,7 +278,7 @@ public async Task> StartJob(int jobId) /// /// [HttpGet] - public async Task> StopJob(int jobId) + public async Task> StopJob(long jobId) { var data = new MessageModel(); @@ -318,7 +318,7 @@ public async Task> StopJob(int jobId) /// /// [HttpGet] - public async Task> PauseJob(int jobId) + public async Task> PauseJob(long jobId) { var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); @@ -372,7 +372,7 @@ public async Task> PauseJob(int jobId) /// /// [HttpGet] - public async Task> ResumeJob(int jobId) + public async Task> ResumeJob(long jobId) { var data = new MessageModel(); @@ -428,7 +428,7 @@ public async Task> ResumeJob(int jobId) /// /// [HttpGet] - public async Task> ReCovery(int jobId) + public async Task> ReCovery(long jobId) { var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); @@ -507,7 +507,7 @@ public MessageModel> GetTaskNameSpace() /// /// [HttpGet] - public async Task> ExecuteJob(int jobId) + public async Task> ExecuteJob(long jobId) { var data = new MessageModel(); @@ -527,7 +527,7 @@ public async Task> ExecuteJob(int jobId) /// /// [HttpGet] - public async Task>> GetTaskLogs(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null) + public async Task>> GetTaskLogs(long jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null) { var model = await _tasksLogServices.GetTaskLogs(jobId, page, pageSize, runTimeStart, runTimeEnd); return MessageModel>.Message(model.dataCount >= 0, "获取成功", model); @@ -537,7 +537,7 @@ public async Task>> GetTaskLogs(int jobId, int /// /// [HttpGet] - public async Task> GetTaskOverview(int jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null, string type = "month") + public async Task> GetTaskOverview(long jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null, string type = "month") { var model = await _tasksLogServices.GetTaskOverview(jobId, runTimeStart, runTimeEnd, type); return MessageModel.Message(true, "获取成功", model); diff --git a/Blog.Core.Api/Controllers/TopicController.cs b/Blog.Core.Api/Controllers/TopicController.cs index 308ad082..253f54ff 100644 --- a/Blog.Core.Api/Controllers/TopicController.cs +++ b/Blog.Core.Api/Controllers/TopicController.cs @@ -44,7 +44,7 @@ public async Task>> Get() // GET: api/Topic/5 [HttpGet("{id}")] - public string Get(int id) + public string Get(long id) { return "value"; } @@ -57,13 +57,13 @@ public void Post([FromBody] string value) // PUT: api/Topic/5 [HttpPut("{id}")] - public void Put(int id, [FromBody] string value) + public void Put(long id, [FromBody] string value) { } // DELETE: api/ApiWithActions/5 [HttpDelete("{id}")] - public void Delete(int id) + public void Delete(long id) { } } diff --git a/Blog.Core.Api/Controllers/TopicDetailController.cs b/Blog.Core.Api/Controllers/TopicDetailController.cs index 55af1a75..374aca24 100644 --- a/Blog.Core.Api/Controllers/TopicDetailController.cs +++ b/Blog.Core.Api/Controllers/TopicDetailController.cs @@ -42,7 +42,7 @@ public TopicDetailController(ITopicServices topicServices, ITopicDetailServices [AllowAnonymous] public async Task>> Get(int page = 1, string tname = "", string key = "", int intPageSize = 12) { - int tid = 0; + long tid = 0; if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { @@ -56,7 +56,7 @@ public async Task>> Get(int page = 1, string if (!string.IsNullOrEmpty(tname)) { - tid = ((await _topicServices.Query(ts => ts.tName == tname)).FirstOrDefault()?.Id).ObjToInt(); + tid = ((await _topicServices.Query(ts => ts.tName == tname)).FirstOrDefault()?.Id).ObjToLong(); } @@ -81,7 +81,7 @@ public async Task>> Get(int page = 1, string // GET: api/TopicDetail/5 [HttpGet("{id}")] [AllowAnonymous] - public async Task> Get(int id) + public async Task> Get(long id) { var data = new MessageModel(); var response = id > 0 ? await _topicDetailServices.QueryById(id) : new TopicDetail(); @@ -154,7 +154,7 @@ public async Task> Update([FromBody] TopicDetail topicDetai /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); if (id > 0) diff --git a/Blog.Core.Api/Controllers/TransactionController.cs b/Blog.Core.Api/Controllers/TransactionController.cs index 67ab576e..9853d985 100644 --- a/Blog.Core.Api/Controllers/TransactionController.cs +++ b/Blog.Core.Api/Controllers/TransactionController.cs @@ -95,7 +95,7 @@ public async Task>> Get() // GET: api/Transaction/5 [HttpGet("{id}")] - public async Task> Get(int id) + public async Task> Get(long id) { return await _guestbookServices.TestTranInRepository(); } @@ -126,7 +126,7 @@ public void Post([FromBody] string value) // PUT: api/Transaction/5 [HttpPut("{id}")] - public void Put(int id, [FromBody] string value) + public void Put(long id, [FromBody] string value) { } @@ -136,7 +136,7 @@ public void Put(int id, [FromBody] string value) /// /// [HttpDelete("{id}")] - public async Task Delete(int id) + public async Task Delete(long id) { return await _guestbookServices.TestTranInRepositoryAOP(); } diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 27989821..95137c8e 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -103,7 +103,7 @@ public async Task>> Get(int page = 1, str return Success(data.ConvertTo(_mapper)); } - private (string, List) GetFullDepartmentName(List departments, int departmentId) + private (string, List) GetFullDepartmentName(List departments, long departmentId) { var departmentModel = departments.FirstOrDefault(d => d.Id == departmentId); if (departmentModel == null) @@ -265,7 +265,7 @@ public async Task> Put([FromBody] SysUserInfoDto sysUserInf /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); if (id > 0) diff --git a/Blog.Core.Api/Controllers/UserRoleController.cs b/Blog.Core.Api/Controllers/UserRoleController.cs index c21ec6f4..693a68b8 100644 --- a/Blog.Core.Api/Controllers/UserRoleController.cs +++ b/Blog.Core.Api/Controllers/UserRoleController.cs @@ -80,7 +80,7 @@ public async Task> AddRole(string roleName) /// /// [HttpGet] - public async Task> AddUserRole(int uid, int rid) + public async Task> AddUserRole(long uid, long rid) { return new MessageModel() { diff --git a/Blog.Core.Common/Helper/RecursionHelper.cs b/Blog.Core.Common/Helper/RecursionHelper.cs index f6f21a38..092a0678 100644 --- a/Blog.Core.Common/Helper/RecursionHelper.cs +++ b/Blog.Core.Common/Helper/RecursionHelper.cs @@ -8,7 +8,7 @@ namespace Blog.Core.Common.Helper /// public static class RecursionHelper { - public static void LoopToAppendChildren(List all, PermissionTree curItem, int pid, bool needbtn) + public static void LoopToAppendChildren(List all, PermissionTree curItem, long pid, bool needbtn) { var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); @@ -52,7 +52,7 @@ public static void LoopToAppendChildren(List all, PermissionTree LoopToAppendChildren(all, subItem, pid, needbtn); } } - public static void LoopToAppendChildren(List all, DepartmentTree curItem, int pid) + public static void LoopToAppendChildren(List all, DepartmentTree curItem, long pid) { var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); diff --git a/Blog.Core.IServices/IBlogArticleServices.cs b/Blog.Core.IServices/IBlogArticleServices.cs index 23e6081b..a38826fb 100644 --- a/Blog.Core.IServices/IBlogArticleServices.cs +++ b/Blog.Core.IServices/IBlogArticleServices.cs @@ -9,7 +9,7 @@ namespace Blog.Core.IServices public interface IBlogArticleServices :IBaseServices { Task> GetBlogs(); - Task GetBlogDetails(int id); + Task GetBlogDetails(long id); } diff --git a/Blog.Core.IServices/ITasksLogServices.cs b/Blog.Core.IServices/ITasksLogServices.cs index fb15c8dd..fb6ad8a7 100644 --- a/Blog.Core.IServices/ITasksLogServices.cs +++ b/Blog.Core.IServices/ITasksLogServices.cs @@ -12,8 +12,8 @@ namespace Blog.Core.IServices /// public interface ITasksLogServices :IBaseServices { - public Task> GetTaskLogs(int jobId, int page, int intPageSize,DateTime? runTime,DateTime? endTime); - public Task GetTaskOverview(int jobId, DateTime? runTime, DateTime? endTime, string type); + public Task> GetTaskLogs(long jobId, int page, int intPageSize,DateTime? runTime,DateTime? endTime); + public Task GetTaskOverview(long jobId, DateTime? runTime, DateTime? endTime, string type); } } \ No newline at end of file diff --git a/Blog.Core.IServices/IUserRoleServices.cs b/Blog.Core.IServices/IUserRoleServices.cs index 9e7d3d29..91272a09 100644 --- a/Blog.Core.IServices/IUserRoleServices.cs +++ b/Blog.Core.IServices/IUserRoleServices.cs @@ -10,8 +10,8 @@ namespace Blog.Core.IServices public interface IUserRoleServices :IBaseServices { - Task SaveUserRole(int uid, int rid); - Task GetRoleIdByUid(int uid); + Task SaveUserRole(long uid, long rid); + Task GetRoleIdByUid(long uid); } } diff --git a/Blog.Core.Model/ViewModels/BlogViewModels.cs b/Blog.Core.Model/ViewModels/BlogViewModels.cs index 411a8ef7..86c16618 100644 --- a/Blog.Core.Model/ViewModels/BlogViewModels.cs +++ b/Blog.Core.Model/ViewModels/BlogViewModels.cs @@ -10,7 +10,7 @@ public class BlogViewModels /// /// /// - public int bID { get; set; } + public long bID { get; set; } /// 创建人 /// /// diff --git a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs index 33d84161..3b5451f0 100644 --- a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs +++ b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs @@ -3,13 +3,13 @@ namespace Blog.Core.Model.ViewModels { - public class SysUserInfoDto : SysUserInfoDtoRoot + public class SysUserInfoDto : SysUserInfoDtoRoot { public string uLoginName { get; set; } public string uLoginPWD { get; set; } public string uRealName { get; set; } public int uStatus { get; set; } - public int DepartmentId { get; set; } + public long DepartmentId { get; set; } public string uRemark { get; set; } public System.DateTime uCreateTime { get; set; } = DateTime.Now; public System.DateTime uUpdateTime { get; set; } = DateTime.Now; @@ -22,7 +22,7 @@ public class SysUserInfoDto : SysUserInfoDtoRoot public string addr { get; set; } public bool tdIsDelete { get; set; } public List RoleNames { get; set; } - public List Dids { get; set; } + public List Dids { get; set; } public string DepartmentName { get; set; } } } diff --git a/Blog.Core.Services/BlogArticleServices.cs b/Blog.Core.Services/BlogArticleServices.cs index eeff04e3..67ea2b9e 100644 --- a/Blog.Core.Services/BlogArticleServices.cs +++ b/Blog.Core.Services/BlogArticleServices.cs @@ -23,7 +23,7 @@ public BlogArticleServices(IMapper mapper) /// /// /// - public async Task GetBlogDetails(int id) + public async Task GetBlogDetails(long id) { // 此处想获取上一条下一条数据,因此将全部数据list出来,有好的想法请提出 //var bloglist = await base.Query(a => a.IsDeleted==false, a => a.bID); diff --git a/Blog.Core.Services/TasksLogServices.cs b/Blog.Core.Services/TasksLogServices.cs index 49393cd4..07d95f1f 100644 --- a/Blog.Core.Services/TasksLogServices.cs +++ b/Blog.Core.Services/TasksLogServices.cs @@ -14,7 +14,7 @@ namespace Blog.Core.Services { public partial class TasksLogServices : BaseServices, ITasksLogServices { - public async Task> GetTaskLogs(int jobId, int page, int intPageSize, DateTime? runTime, DateTime? endTime) + public async Task> GetTaskLogs(long jobId, int page, int intPageSize, DateTime? runTime, DateTime? endTime) { RefAsync totalCount = 0; Expression> whereExpression = log => true; @@ -41,7 +41,7 @@ public async Task> GetTaskLogs(int jobId, int page, int intP .ToPageListAsync(page, intPageSize, totalCount); return new PageModel(page, totalCount, intPageSize, data); } - public async Task GetTaskOverview(int jobId, DateTime? runTime, DateTime? endTime, string type) + public async Task GetTaskOverview(long jobId, DateTime? runTime, DateTime? endTime, string type) { //按年 if ("year".Equals(type)) diff --git a/Blog.Core.Services/UserRoleServices.cs b/Blog.Core.Services/UserRoleServices.cs index 38f524da..26194837 100644 --- a/Blog.Core.Services/UserRoleServices.cs +++ b/Blog.Core.Services/UserRoleServices.cs @@ -19,7 +19,7 @@ public class UserRoleServices : BaseServices, IUserRoleServices /// /// /// - public async Task SaveUserRole(int uid, int rid) + public async Task SaveUserRole(long uid, long rid) { UserRole userRole = new UserRole(uid, rid); @@ -42,7 +42,7 @@ public async Task SaveUserRole(int uid, int rid) [Caching(AbsoluteExpiration = 30)] - public async Task GetRoleIdByUid(int uid) + public async Task GetRoleIdByUid(long uid) { return ((await base.Query(d => d.UserId == uid)).OrderByDescending(d => d.Id).LastOrDefault()?.RoleId).ObjToInt(); } diff --git a/Blog.Core.Tests/Controller_Test/BlogController_Should.cs b/Blog.Core.Tests/Controller_Test/BlogController_Should.cs index 3d1e3178..59d42ae0 100644 --- a/Blog.Core.Tests/Controller_Test/BlogController_Should.cs +++ b/Blog.Core.Tests/Controller_Test/BlogController_Should.cs @@ -53,7 +53,7 @@ public async void Get_Blog_Page_Test() [Fact] public async void Get_Blog_Test() { - MessageModel blogVo = await blogController.Get(1); + MessageModel blogVo = await blogController.Get(1.ObjToLong()); Assert.NotNull(blogVo); } From 5e84e11ca84f3bf245ef4307e17b48cd2c20b265 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sun, 2 Apr 2023 15:56:20 +0800 Subject: [PATCH 151/289] =?UTF-8?q?=E2=9C=A8=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Blog.Core.Api/Blog.Core.Api.csproj | 1 + Blog.Core.Model/Models/PasswordLib.cs | 2 +- Blog.Core.Tests/WMBlog.db | Bin 200704 -> 0 bytes 4 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 Blog.Core.Tests/WMBlog.db diff --git a/.gitignore b/.gitignore index 4f554bea..fa4a3448 100644 --- a/.gitignore +++ b/.gitignore @@ -358,3 +358,4 @@ Blog.Core.Api/wwwroot/ui/ *.db /Blog.Core.Api/WMBlog.db-journal Logs +*.db diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 3bf64399..0d32998e 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -52,6 +52,7 @@ + diff --git a/Blog.Core.Model/Models/PasswordLib.cs b/Blog.Core.Model/Models/PasswordLib.cs index 2b43c265..9eb9292f 100644 --- a/Blog.Core.Model/Models/PasswordLib.cs +++ b/Blog.Core.Model/Models/PasswordLib.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// 密码库表 /// [SugarTable("PasswordLib", "密码库表")]//('数据库表名','数据库表备注') - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + //[TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') public class PasswordLib { [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] diff --git a/Blog.Core.Tests/WMBlog.db b/Blog.Core.Tests/WMBlog.db deleted file mode 100644 index 4931b9674dbdfa61661ba06a34e3c55254029529..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200704 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 Date: Sun, 2 Apr 2023 16:01:03 +0800 Subject: [PATCH 152/289] Update SplitDemoController.cs --- Blog.Core.Api/Controllers/SplitDemoController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Blog.Core.Api/Controllers/SplitDemoController.cs b/Blog.Core.Api/Controllers/SplitDemoController.cs index fb3c03c6..f625b202 100644 --- a/Blog.Core.Api/Controllers/SplitDemoController.cs +++ b/Blog.Core.Api/Controllers/SplitDemoController.cs @@ -77,7 +77,7 @@ public async Task> GetById(long id) public async Task> Post([FromBody] SplitDemo splitDemo) { var data = new MessageModel(); - unitOfWorkManage.BeginTran(); + //unitOfWorkManage.BeginTran(); var id = (await splitDemoServices.AddSplit(splitDemo)); data.success = (id == null ? false : true); try @@ -98,10 +98,10 @@ public async Task> Post([FromBody] SplitDemo splitDemo) } finally { - if (data.success) - unitOfWorkManage.CommitTran(); - else - unitOfWorkManage.RollbackTran(); + //if (data.success) + // unitOfWorkManage.CommitTran(); + //else + // unitOfWorkManage.RollbackTran(); } return data; } From 45d538010be8dc826b128a38bad9ff9c91d429b3 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sun, 2 Apr 2023 17:33:59 +0800 Subject: [PATCH 153/289] =?UTF-8?q?=E2=9C=A8=20=E6=9A=82=E6=97=B6=E5=85=88?= =?UTF-8?q?=E5=8F=96=E6=B6=88Log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/appsettings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 3c117375..22b8e84d 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -57,7 +57,7 @@ "Enabled": false }, "LogToDB": { - "Enabled": true + "Enabled": false } }, "TranAOP": { @@ -212,7 +212,7 @@ "Enabled": false }, "LogToDB": { - "Enabled": true + "Enabled": false } }, "IPLog": { @@ -221,7 +221,7 @@ "Enabled": false }, "LogToDB": { - "Enabled": true + "Enabled": false } }, "RecordAccessLogs": { @@ -230,7 +230,7 @@ "Enabled": false }, "LogToDB": { - "Enabled": true + "Enabled": false }, "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," }, From 8cb4df9ac0a7a8ec43b1e2d059a26b57edb3f018 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Thu, 6 Apr 2023 21:43:18 +0800 Subject: [PATCH 154/289] =?UTF-8?q?=E2=9C=A8=20git=20ignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fa4a3448..de419c90 100644 --- a/.gitignore +++ b/.gitignore @@ -355,7 +355,6 @@ Blog.Core/WMBlog.db Blog.Core/Blog.Core*.xml Blog.Core.Api/WMBlog.db Blog.Core.Api/wwwroot/ui/ +Blog.Core.Api/Logs *.db /Blog.Core.Api/WMBlog.db-journal -Logs -*.db From 7c4b76aeea437eec708ff7083c78cd9ab53669ae Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Thu, 6 Apr 2023 22:40:05 +0800 Subject: [PATCH 155/289] =?UTF-8?q?=E2=9C=A8=20Serilog=20log=20to=20db?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/appsettings.json | 5 +- Blog.Core.Common/DB/BaseDBConfig.cs | 4 +- .../LogHelper/LogContextExtension.cs | 2 +- Blog.Core.Common/Seed/DBSeed.cs | 56 ++++++++ .../HostedService/SeedDataHostedService.cs | 3 + .../ServiceExtensions/SerilogSetup.cs | 5 +- .../ServiceExtensions/SqlsugarSetup.cs | 16 ++- Blog.Core.Model/Base/BaseLog.cs | 22 +++ Blog.Core.Model/Logs/AuditSqlLog.cs | 12 ++ Blog.Core.Model/Logs/GlobalErrorLog.cs | 13 ++ Blog.Core.Model/Logs/GlobalInformationLog.cs | 12 ++ Blog.Core.Model/Logs/GlobalWarningLog.cs | 12 ++ Blog.Core.Serilog/Blog.Core.Serilog.csproj | 4 + .../LogBatchingSinkConfiguration.cs | 31 ++++ .../LoggerConfigurationExtensions.cs | 40 +----- Blog.Core.Serilog/Sink/LogBatchingSink.cs | 135 ++++++++++++++++++ 16 files changed, 320 insertions(+), 52 deletions(-) create mode 100644 Blog.Core.Model/Base/BaseLog.cs create mode 100644 Blog.Core.Model/Logs/AuditSqlLog.cs create mode 100644 Blog.Core.Model/Logs/GlobalErrorLog.cs create mode 100644 Blog.Core.Model/Logs/GlobalInformationLog.cs create mode 100644 Blog.Core.Model/Logs/GlobalWarningLog.cs create mode 100644 Blog.Core.Serilog/Configuration/LogBatchingSinkConfiguration.cs create mode 100644 Blog.Core.Serilog/Sink/LogBatchingSink.cs diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index c712054c..98f6d4b0 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -45,10 +45,11 @@ "MemoryCachingAOP": { "Enabled": true }, + "LogToDb": true, "LogAOP": { "Enabled": true, "LogToFile": { - "Enabled": false + "Enabled": true }, "LogToDB": { "Enabled": true @@ -63,7 +64,7 @@ "Enabled": true }, "LogToDB": { - "Enabled": false + "Enabled": true }, "LogToConsole": { "Enabled": true diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index d8c3ee50..5761d91d 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -12,7 +12,9 @@ public class BaseDBConfig * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); - public static ConnectionConfig LogConfig; //日志库 + public static List AllConfig=new(); //所有的库连接 + public static List ValidConfig=new(); //有效的库连接(除去Log库) + public static ConnectionConfig LogConfig; //日志库 private static string DifDBConnOfSecurity(params string[] conn) { diff --git a/Blog.Core.Common/LogHelper/LogContextExtension.cs b/Blog.Core.Common/LogHelper/LogContextExtension.cs index bce80cbb..573ba057 100644 --- a/Blog.Core.Common/LogHelper/LogContextExtension.cs +++ b/Blog.Core.Common/LogHelper/LogContextExtension.cs @@ -24,7 +24,7 @@ public IDisposable SqlAopPushProperty(ISqlSugarClient db) AddStock(LogContext.PushProperty(LogContextStatic.SqlOutToFile, AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToFile", "Enabled" }).ObjToBool())); AddStock(LogContext.PushProperty(LogContextStatic.OutToDb, - AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToDb", "Enabled" }).ObjToBool())); + AppSettings.app(new string[] { "AppSettings", "SqlAOP", "LogToDB", "Enabled" }).ObjToBool())); AddStock(LogContext.PushProperty(LogContextStatic.SugarActionType, db.SugarActionType)); diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 7b594109..5a86f933 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -8,11 +8,13 @@ using SqlSugar; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using Blog.Core.Common.Const; namespace Blog.Core.Common.Seed { @@ -426,6 +428,56 @@ private static async Task SeedDataAsync(ISqlSugarClient db) } } + /// + /// 迁移日志数据库 + /// + /// + public static void MigrationLogs(MyContext myContext) + { + // 创建数据库表,遍历指定命名空间下的class, + // 注意不要把其他命名空间下的也添加进来。 + Console.WriteLine("Create Log Tables..."); + if (!myContext.Db.IsAnyConnection(SqlSugarConst.LogConfigId.ToLower())) + { + throw new ApplicationException("未配置日志数据库,请在appsettings.json中DBS节点中配置"); + } + + var logDb = myContext.Db.GetConnection(SqlSugarConst.LogConfigId.ToLower()); + + var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; + var referencedAssemblies = System.IO.Directory.GetFiles(path, "Blog.Core.Model.dll").Select(Assembly.LoadFrom).ToArray(); + var modelTypes = referencedAssemblies + .SelectMany(a => a.DefinedTypes) + .Select(type => type.AsType()) + .Where(x => x.IsClass && x.Namespace != null && x.Namespace.StartsWith("Blog.Core.Model.Logs")).ToList(); + Stopwatch sw = Stopwatch.StartNew(); + + var tables = logDb.DbMaintenance.GetTableInfoList(); + + modelTypes.ForEach(t => + { + // 这里只支持添加修改表,不支持删除 + // 如果想要删除,数据库直接右键删除,或者联系SqlSugar作者; + if (!tables.Any(s => s.Name.Contains(t.Name))) + { + Console.WriteLine(t.Name); + if (t.GetCustomAttribute() != null) + { + logDb.CodeFirst.SplitTables().InitTables(t); + } + else + { + logDb.CodeFirst.InitTables(t); + } + } + }); + + sw.Stop(); + + $"Log Tables created successfully! {sw.ElapsedMilliseconds}ms".WriteSuccessLine(); + Console.WriteLine(); + } + /// /// 初始化 多租户 @@ -523,6 +575,8 @@ public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig c #endregion + #region 多租户 种子数据 初始化 + private static async Task TenantSeedDataAsync(ISqlSugarClient db, TenantTypeEnum tenantType) { // 获取所有种子配置-初始化数据 @@ -578,5 +632,7 @@ private static async Task TenantSeedDataAsync(ISqlSugarClient db, TenantTypeEnum } } } + + #endregion } } \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs index 868d750e..e1da3d69 100644 --- a/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs +++ b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs @@ -39,6 +39,9 @@ private async Task DoWork() { await DBSeed.SeedAsync(_myContext, _webRootPath); + //日志 + DBSeed.MigrationLogs(_myContext); + //多租户 同步 await DBSeed.TenantSeedAsync(_myContext); } diff --git a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs index a112409c..0d8a0768 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs @@ -1,5 +1,6 @@ using Blog.Core.Common; using Blog.Core.Common.LogHelper; +using Blog.Core.Serilog.Configuration; using Blog.Core.Serilog.Extensions; using Microsoft.Extensions.Hosting; using Serilog; @@ -21,9 +22,9 @@ public static IHostBuilder AddSerilogSetup(this IHostBuilder host) //输出到控制台 .WriteToConsole() //将日志保存到文件中 - .WriteToFile(); + .WriteToFile() //配置日志库 - //.WriteToLogBatching(); + .WriteToLogBatching(); Log.Logger = loggerConfiguration.CreateLogger(); diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 3440b8af..98dd6e24 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -33,8 +33,6 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { var memoryCache = o.GetRequiredService(); - // 连接字符串 - var listConfig = new List(); // 从库 var listConfig_Slave = new List(); BaseDBConfig.MutiConnectionString.slaveDbs.ForEach(s => @@ -77,12 +75,16 @@ public static void AddSqlsugarSetup(this IServiceCollection services) }, InitKeyType = InitKeyType.Attribute }; - if (SqlSugarConst.LogConfigId.Equals(m.ConnId)) + if (SqlSugarConst.LogConfigId.ToLower().Equals(m.ConnId.ToLower())) { BaseDBConfig.LogConfig = config; } + else + { + BaseDBConfig.ValidConfig.Add(config); + } - listConfig.Add(config); + BaseDBConfig.AllConfig.Add(config); }); if (BaseDBConfig.LogConfig is null) @@ -90,14 +92,14 @@ public static void AddSqlsugarSetup(this IServiceCollection services) throw new ApplicationException("未配置Log库连接"); } - return new SqlSugarScope(listConfig, db => + return new SqlSugarScope(BaseDBConfig.AllConfig, db => { - listConfig.ForEach(config => + BaseDBConfig.ValidConfig.ForEach(config => { var dbProvider = db.GetConnectionScope((string)config.ConfigId); // 打印SQL语句 - dbProvider.Aop.OnLogExecuting = (s, parameters) => SqlSugarAop.OnLogExecuting(dbProvider,s, parameters, config); + dbProvider.Aop.OnLogExecuting = (s, parameters) => SqlSugarAop.OnLogExecuting(dbProvider, s, parameters, config); // 数据审计 dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; diff --git a/Blog.Core.Model/Base/BaseLog.cs b/Blog.Core.Model/Base/BaseLog.cs new file mode 100644 index 00000000..829ff09e --- /dev/null +++ b/Blog.Core.Model/Base/BaseLog.cs @@ -0,0 +1,22 @@ +using SqlSugar; +using System; + +namespace Blog.Core.Model.Base; + +public abstract class BaseLog : RootEntityTkey +{ + [SplitField] + public DateTime? DateTime { get; set; } + + [SugarColumn(IsNullable = true)] + public string Level { get; set; } + + [SugarColumn(IsNullable = true, ColumnDataType = "longtext,text,clob")] + public string Message { get; set; } + + [SugarColumn(IsNullable = true, ColumnDataType = "longtext,text,clob")] + public string MessageTemplate { get; set; } + + [SugarColumn(IsNullable = true, ColumnDataType = "longtext,text,clob")] + public string Properties { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Logs/AuditSqlLog.cs b/Blog.Core.Model/Logs/AuditSqlLog.cs new file mode 100644 index 00000000..a6bd4e23 --- /dev/null +++ b/Blog.Core.Model/Logs/AuditSqlLog.cs @@ -0,0 +1,12 @@ +using Blog.Core.Model.Base; +using SqlSugar; + +namespace Blog.Core.Model.Logs; + +[Tenant("log")] +[SplitTable(SplitType.Month)] //按年分表 (自带分表支持 年、季、月、周、日) +[SugarTable($@"{nameof(AuditSqlLog)}_{{year}}{{month}}{{day}}")] +public class AuditSqlLog: BaseLog +{ + +} \ No newline at end of file diff --git a/Blog.Core.Model/Logs/GlobalErrorLog.cs b/Blog.Core.Model/Logs/GlobalErrorLog.cs new file mode 100644 index 00000000..08172eae --- /dev/null +++ b/Blog.Core.Model/Logs/GlobalErrorLog.cs @@ -0,0 +1,13 @@ +using Blog.Core.Model.Base; +using SqlSugar; + +namespace Blog.Core.Model.Logs; + +[Tenant("log")] +[SplitTable(SplitType.Month)] //按年分表 (自带分表支持 年、季、月、周、日) +[SugarTable($@"{nameof(GlobalErrorLog)}_{{year}}{{month}}{{day}}")] +public class GlobalErrorLog : BaseLog +{ + [SugarColumn(IsNullable = true, ColumnDataType = "longtext,text,clob")] + public string Exception { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Logs/GlobalInformationLog.cs b/Blog.Core.Model/Logs/GlobalInformationLog.cs new file mode 100644 index 00000000..14873eed --- /dev/null +++ b/Blog.Core.Model/Logs/GlobalInformationLog.cs @@ -0,0 +1,12 @@ +using Blog.Core.Model.Base; +using SqlSugar; + +namespace Blog.Core.Model.Logs; + +[Tenant("log")] +[SplitTable(SplitType.Month)] //按年分表 (自带分表支持 年、季、月、周、日) +[SugarTable($@"{nameof(GlobalInformationLog)}_{{year}}{{month}}{{day}}")] +public class GlobalInformationLog : BaseLog +{ + +} \ No newline at end of file diff --git a/Blog.Core.Model/Logs/GlobalWarningLog.cs b/Blog.Core.Model/Logs/GlobalWarningLog.cs new file mode 100644 index 00000000..7f4fb945 --- /dev/null +++ b/Blog.Core.Model/Logs/GlobalWarningLog.cs @@ -0,0 +1,12 @@ +using Blog.Core.Model.Base; +using SqlSugar; + +namespace Blog.Core.Model.Logs; + +[Tenant("log")] +[SplitTable(SplitType.Month)] //按年分表 (自带分表支持 年、季、月、周、日) +[SugarTable($@"{nameof(GlobalWarningLog)}_{{year}}{{month}}{{day}}")] +public class GlobalWarningLog: BaseLog +{ + +} \ No newline at end of file diff --git a/Blog.Core.Serilog/Blog.Core.Serilog.csproj b/Blog.Core.Serilog/Blog.Core.Serilog.csproj index 07fa26f3..8df8f16f 100644 --- a/Blog.Core.Serilog/Blog.Core.Serilog.csproj +++ b/Blog.Core.Serilog/Blog.Core.Serilog.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/Blog.Core.Serilog/Configuration/LogBatchingSinkConfiguration.cs b/Blog.Core.Serilog/Configuration/LogBatchingSinkConfiguration.cs new file mode 100644 index 00000000..65aacde7 --- /dev/null +++ b/Blog.Core.Serilog/Configuration/LogBatchingSinkConfiguration.cs @@ -0,0 +1,31 @@ +using Blog.Core.Common; +using Blog.Core.Serilog.Sink; +using Serilog; +using Serilog.Sinks.PeriodicBatching; + +namespace Blog.Core.Serilog.Configuration; + +public static class LogBatchingSinkConfiguration +{ + public static LoggerConfiguration WriteToLogBatching(this LoggerConfiguration loggerConfiguration) + { + if (!AppSettings.app("AppSettings", "LogToDb").ObjToBool()) + { + return loggerConfiguration; + } + + var exampleSink = new LogBatchingSink(); + + var batchingOptions = new PeriodicBatchingSinkOptions + { + BatchSizeLimit = 500, + Period = TimeSpan.FromSeconds(1), + EagerlyEmitFirstEvent = true, + QueueLimit = 10000 + }; + + var batchingSink = new PeriodicBatchingSink(exampleSink, batchingOptions); + + return loggerConfiguration.WriteTo.Sink(batchingSink); + } +} \ No newline at end of file diff --git a/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs b/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs index 2736aa1f..d70616b4 100644 --- a/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs +++ b/Blog.Core.Serilog/Extensions/LoggerConfigurationExtensions.cs @@ -9,45 +9,6 @@ namespace Blog.Core.Serilog.Extensions; public static class LoggerConfigurationExtensions { - public static LoggerConfiguration WriteToSqlServer(this LoggerConfiguration loggerConfiguration) - { - var logConnectionStrings = AppSettings.app("LogConnectionStrings"); - if (logConnectionStrings.IsNullOrEmpty()) return loggerConfiguration; - - //输出SQL - //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => - // lg.FilterSqlLog().WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() - // { - // TableName = "SqlLog", - // AutoCreateSqlTable = true - // })); - - //输出普通日志 - //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => - // lg.FilterRemoveSqlLog().Filter.ByIncludingOnly(p => p.Level >= LogEventLevel.Error) - // .WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() - // { - // TableName = "ErrorLog", - // AutoCreateSqlTable = true - // })); - //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => - // lg.FilterRemoveSqlLog().Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Warning) - // .WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() - // { - // TableName = "WarningLog", - // AutoCreateSqlTable = true - // })); - //loggerConfiguration = loggerConfiguration.WriteTo.Logger(lg => - // lg.FilterRemoveSqlLog().Filter.ByIncludingOnly(p => p.Level <= LogEventLevel.Information) - // .WriteTo.MSSqlServer(logConnectionStrings, new MSSqlServerSinkOptions() - // { - // TableName = "InformationLog", - // AutoCreateSqlTable = true - // })); - - return loggerConfiguration; - } - public static LoggerConfiguration WriteToConsole(this LoggerConfiguration loggerConfiguration) { //输出普通日志 @@ -84,6 +45,7 @@ public static LoggerConfiguration FilterSqlLog(this LoggerConfiguration lc) public static IEnumerable FilterSqlLog(this IEnumerable batch) { + //只记录 Insert、Update、Delete语句 return batch.Where(s => s.WithProperty(LogContextStatic.LogSource, q => LogContextStatic.AopSql.Equals(q))) .Where(s => s.WithProperty(LogContextStatic.SugarActionType, q => !new[] { SugarActionType.UnKnown, SugarActionType.Query }.Contains(q))); diff --git a/Blog.Core.Serilog/Sink/LogBatchingSink.cs b/Blog.Core.Serilog/Sink/LogBatchingSink.cs new file mode 100644 index 00000000..0a6e1223 --- /dev/null +++ b/Blog.Core.Serilog/Sink/LogBatchingSink.cs @@ -0,0 +1,135 @@ +using Blog.Core.Common; +using Blog.Core.Model.Logs; +using Blog.Core.Serilog.Extensions; +using Mapster; +using Serilog.Events; +using Serilog.Sinks.PeriodicBatching; +using SqlSugar; + +namespace Blog.Core.Serilog.Sink; + +public class LogBatchingSink : IBatchedLogEventSink +{ + public async Task EmitBatchAsync(IEnumerable batch) + { + var sugar = App.GetService(false); + + await WriteSqlLog(sugar, batch.FilterSqlLog()); + await WriteLogs(sugar, batch.FilterRemoveOtherLog()); + } + + public Task OnEmptyBatchAsync() + { + return Task.CompletedTask; + } + + #region Write Log + + private async Task WriteLogs(ISqlSugarClient db, IEnumerable batch) + { + if (!batch.Any()) + { + return; + } + + var group = batch.GroupBy(s => s.Level); + foreach (var v in group) + { + switch (v.Key) + { + case LogEventLevel.Information: + await WriteInformationLog(db, v); + break; + case LogEventLevel.Warning: + await WriteWarningLog(db, v); + break; + case LogEventLevel.Error: + case LogEventLevel.Fatal: + await WriteErrorLog(db, v); + break; + } + } + } + + private async Task WriteInformationLog(ISqlSugarClient db, IEnumerable batch) + { + if (!batch.Any()) + { + return; + } + + var logs = new List(); + foreach (var logEvent in batch) + { + var log = logEvent.Adapt(); + log.Message = logEvent.RenderMessage(); + log.Properties = logEvent.Properties.ToJson(); + log.DateTime = logEvent.Timestamp.DateTime; + logs.Add(log); + } + + await db.AsTenant().InsertableWithAttr(logs).SplitTable().ExecuteReturnSnowflakeIdAsync(); + } + + private async Task WriteWarningLog(ISqlSugarClient db, IEnumerable batch) + { + if (!batch.Any()) + { + return; + } + + var logs = new List(); + foreach (var logEvent in batch) + { + var log = logEvent.Adapt(); + log.Message = logEvent.RenderMessage(); + log.Properties = logEvent.Properties.ToJson(); + log.DateTime = logEvent.Timestamp.DateTime; + logs.Add(log); + } + + await db.AsTenant().InsertableWithAttr(logs).SplitTable().ExecuteReturnSnowflakeIdAsync(); + } + + private async Task WriteErrorLog(ISqlSugarClient db, IEnumerable batch) + { + if (!batch.Any()) + { + return; + } + + var logs = new List(); + foreach (var logEvent in batch) + { + var log = logEvent.Adapt(); + log.Message = logEvent.RenderMessage(); + log.Properties = logEvent.Properties.ToJson(); + log.DateTime = logEvent.Timestamp.DateTime; + logs.Add(log); + } + + await db.AsTenant().InsertableWithAttr(logs).SplitTable().ExecuteReturnSnowflakeIdAsync(); + } + + private async Task WriteSqlLog(ISqlSugarClient db, IEnumerable batch) + { + if (!batch.Any()) + { + return; + } + + var logs = new List(); + foreach (var logEvent in batch) + { + var log = logEvent.Adapt(); + log.Message = logEvent.RenderMessage(); + log.Properties = logEvent.Properties.ToJson(); + log.DateTime = logEvent.Timestamp.DateTime; + logs.Add(log); + } + + await db.AsTenant().InsertableWithAttr(logs).SplitTable().ExecuteReturnSnowflakeIdAsync(); + } + + #endregion +} \ No newline at end of file From 36a38694b5396c43f143202175889e78678bef43 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 8 Apr 2023 19:07:00 +0800 Subject: [PATCH 156/289] feat: change readme --- Blog.Core.Api/Blog.Core.Api.csproj | 1 - README.md | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 0d32998e..3bf64399 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -52,7 +52,6 @@ - diff --git a/README.md b/README.md index ee36dc2c..a62a2eb6 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x #### 商业授权付费版下🎁🎁🎁 -- [x] 包含下边框架模块中的所有功能; +- [x] 包含开源版 `框架模块/组件模块` 中的所有功能; - [x] 全部表结构主键底层架构改成`string`类型(默认雪花,支持guid),更方便迁移; - [x] 完善部门数据权限,可以基于策略配置查看数据范围; - [x] 优化权限处理器,解决多实例分布式下,权限不同步问题(必须配置Redis); @@ -82,7 +82,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等,并自动持久化到数据库表🎶; - [x] 支持项目事务处理(若要分布式,用cap即可)✨; - [x] 设计4种 AOP 切面编程,功能涵盖:日志、缓存、审计、事务 ✨; -- [x] Log4net 多种日志自动生成到数据库中,目前支持MySql/SqlServer/Sqlite/Oracle/Postgresql🎉; +- [x] 全局统一封装 Serilog 生成多种日志,并自动生成到数据库中,目前支持MySql/SqlServer/Sqlite/Oracle/Postgresql🎉; - [x] 设计并支持按钮级别的RBAC权限控制,同时支持一键同步接口和菜单 🎶; - [x] 支持 T4 代码模板,自动生成每层代码; - [x] 或使用 DbFirst 一键创建自己项目的四层文件(支持多库); @@ -100,7 +100,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 使用 AutoFac 做依赖注入容器,并提供批量服务注入 ✨; - [x] 支持 CORS 跨域; - [x] 封装 JWT 自定义策略授权; -- [x] 使用 Log4Net 日志框架,集成原生 ILogger 接口做日志记录; +- [x] 使用 Serilog 日志框架,集成原生 ILogger 接口做日志记录; - [x] 使用 SignalR 双工通讯 ✨; - [x] 添加 IpRateLimiting 做 API 限流处理; - [x] 使用 Quartz.net 做任务调度(目前单机多任务,集群调度暂不支持); @@ -115,7 +115,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 新增 Kafka 消息队列,并配合实现EventBus ✨; - [x] 新增 微信公众号管理,并集成到Blog.Admin后台 ✨; - [x] 新增 - 数据部门权限; -- [x] 新增 - Log4net集成日志数据持久化到数据库; +- [x] 新增 - Serilog 集成日志数据持久化到数据库; - [x] 新增 - 多租户模式(单表,多表,多库三种模式); From cf97167537a431376c5e55990c4a049f1761a109 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 8 Apr 2023 20:33:42 +0800 Subject: [PATCH 157/289] Update DBSeed.cs --- Blog.Core.Common/Seed/DBSeed.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index f2795b08..000ff976 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -445,7 +445,9 @@ public static void MigrationLogs(MyContext myContext) } var logDb = myContext.Db.GetConnection(SqlSugarConst.LogConfigId.ToLower()); - + Console.WriteLine($"Create log Database(The Db Id:{SqlSugarConst.LogConfigId.ToLower()})..."); + logDb.DbMaintenance.CreateDatabase(); + ConsoleHelper.WriteSuccessLine($"Log Database created successfully!"); var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; var referencedAssemblies = System.IO.Directory.GetFiles(path, "Blog.Core.Model.dll").Select(Assembly.LoadFrom).ToArray(); var modelTypes = referencedAssemblies From b595ac4a1e36be0de394a853340a881a0a49553c Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sun, 9 Apr 2023 22:46:53 +0800 Subject: [PATCH 158/289] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=BB=E5=88=86?= =?UTF-8?q?=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + Blog.Core.Api/Blog.Core.Api.csproj | 5 +- Blog.Core.Api/Blog.Core.Model.xml | 315 +++++++++ Blog.Core.Api/Blog.Core.xml | 201 +++++- Blog.Core.Api/Controllers/BlogController.cs | 16 +- .../Controllers/DbFirst/MigrateController.cs | 30 +- .../Controllers/DepartmentController.cs | 13 +- Blog.Core.Api/Controllers/LoginController.cs | 37 +- Blog.Core.Api/Controllers/ModuleController.cs | 7 +- .../Controllers/MonitorController.cs | 7 - Blog.Core.Api/Controllers/NacosController.cs | 1 - Blog.Core.Api/Controllers/PayController.cs | 4 +- .../Controllers/PermissionController.cs | 158 ++--- Blog.Core.Api/Controllers/RoleController.cs | 5 +- .../Controllers/SplitDemoController.cs | 199 ++++++ .../Controllers/TasksQzController.cs | 85 ++- .../Tenant/TenantByDbController.cs | 50 ++ .../Tenant/TenantByIdController.cs | 49 ++ .../Tenant/TenantByTableController.cs | 57 ++ .../Tenant/TenantManagerController.cs | 87 +++ Blog.Core.Api/Controllers/TopicController.cs | 10 +- .../Controllers/TopicDetailController.cs | 13 +- .../Controllers/TransactionController.cs | 11 +- Blog.Core.Api/Controllers/UserController.cs | 15 +- .../Controllers/UserRoleController.cs | 5 +- Blog.Core.Api/Controllers/ValuesController.cs | 1 - .../Controllers/WeChatCompanyController.cs | 1 - .../Controllers/WeChatConfigController.cs | 1 - Blog.Core.Api/Controllers/WeChatController.cs | 5 +- .../Controllers/WeChatPushLogController.cs | 1 - .../Controllers/WeChatSubController.cs | 1 - Blog.Core.Api/Program.cs | 34 +- Blog.Core.Api/Startup.cs | 43 +- Blog.Core.Api/appsettings.json | 599 +++++++++--------- .../BlogCore.Data.excel/SysUserInfo.xlsx | Bin 11177 -> 11197 bytes .../wwwroot/BlogCore.Data.json/Modules.tsv | 44 ++ .../wwwroot/BlogCore.Data.json/Permission.tsv | 86 ++- .../RoleModulePermission.tsv | 12 +- .../wwwroot/BlogCore.Data.json/TasksQz.tsv | 8 +- Blog.Core.Build.bat | 1 - Blog.Core.Common/App.cs | 19 + Blog.Core.Common/Blog.Core.Common.csproj | 4 + Blog.Core.Common/Core/InternalApp.cs | 17 + Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 130 ++++ Blog.Core.Common/DB/BaseDBConfig.cs | 22 +- Blog.Core.Common/DB/RepositorySetting.cs | 48 ++ Blog.Core.Common/DB/TenantUtil.cs | 102 +++ .../Extensions/AssemblysExtensions.cs | 24 + .../Extensions/ExpressionExtensions.cs | 13 +- .../Extensions/GenericTypeExtensions.cs | 28 +- .../Extensions/UntilExtensions.cs | 18 + Blog.Core.Common/Helper/DynamicLinqFactory.cs | 100 ++- .../Helper/GenericTypeExtensions.cs | 56 ++ Blog.Core.Common/Helper/NumberConverter.cs | 174 +++++ Blog.Core.Common/Helper/RecursionHelper.cs | 16 +- Blog.Core.Common/Helper/SM/SM4.cs | 3 +- Blog.Core.Common/Helper/UtilConvert.cs | 12 + .../HttpContextUser/AspNetUser.cs | 6 +- Blog.Core.Common/HttpContextUser/IUser.cs | 3 +- Blog.Core.Common/Seed/DBSeed.cs | 305 ++++++++- Blog.Core.Common/Seed/IEntitySeedData.cs | 36 ++ .../Seed/SeedData/BusinessDataSeedData.cs | 79 +++ .../SeedData/MultiBusinessDataSeedData.cs | 38 ++ .../SeedData/MultiBusinessSubDataSeedData.cs | 38 ++ .../Seed/SeedData/SubBusinessDataSeedData.cs | 70 ++ .../Seed/SeedData/TenantSeedData.cs | 72 +++ .../Seed/SeedData/UserInfoSeedData.cs | 80 +++ Blog.Core.EventBus/Blog.Core.EventBus.csproj | 2 +- .../Blog.Core.Extensions.csproj | 2 +- .../HostedService/ConsulHostedService.cs | 71 +++ .../HostedService/EventBusHostedService.cs | 45 ++ .../HostedService/QuartzJobHostedService.cs | 67 ++ .../HostedService/SeedDataHostedService.cs | 58 ++ .../Middlewares/RequRespLogMiddleware.cs | 1 - .../ServiceExtensions/EventBusSetup.cs | 21 +- .../InitializationHostServiceSetup.cs | 24 + .../ServiceExtensions/SqlsugarSetup.cs | 17 +- Blog.Core.Gateway/Blog.Core.Gateway.csproj | 6 +- Blog.Core.Gateway/Program.cs | 4 +- .../appsettings.gw.Development.json | 9 + Blog.Core.Gateway/appsettings.gw.json | 103 +++ Blog.Core.IServices/BASE/IBaseServices.cs | 15 +- Blog.Core.IServices/IBlogArticleServices.cs | 2 +- .../IRoleModulePermissionServices.cs | 2 +- Blog.Core.IServices/ISplitDemoServices.cs | 15 + Blog.Core.IServices/ITasksLogServices.cs | 19 + Blog.Core.IServices/ITenantService.cs | 12 + Blog.Core.IServices/IUserRoleServices.cs | 4 +- Blog.Core.Model/Blog.Core.Model.csproj | 6 +- Blog.Core.Model/Models/AccessTrendLog.cs | 2 +- Blog.Core.Model/Models/Advertisement.cs | 2 +- Blog.Core.Model/Models/BlogArticle.cs | 18 +- Blog.Core.Model/Models/BlogArticleComment.cs | 19 + Blog.Core.Model/Models/Department.cs | 2 +- Blog.Core.Model/Models/GblLogAudit.cs | 4 +- Blog.Core.Model/Models/Guestbook.cs | 6 +- Blog.Core.Model/Models/Modules.cs | 2 +- Blog.Core.Model/Models/OperateLog.cs | 2 +- Blog.Core.Model/Models/PasswordLib.cs | 6 +- Blog.Core.Model/Models/Permission.cs | 2 +- Blog.Core.Model/Models/Role.cs | 2 +- .../Models/RoleModulePermission.cs | 2 +- Blog.Core.Model/Models/RootTkey/BaseEntity.cs | 80 +++ .../RootTkey/Interface/IDeleteFilter.cs | 9 + .../Models/RootTkey/RootEntityTkey.cs | 2 - Blog.Core.Model/Models/SplitDemo.cs | 27 + Blog.Core.Model/Models/SysTenant.cs | 67 ++ Blog.Core.Model/Models/TasksLog.cs | 87 +++ Blog.Core.Model/Models/TasksQz.cs | 6 +- .../Models/Tenant/BusinessTable.cs | 27 + .../Models/Tenant/MultiBusinessSubTable.cs | 14 + .../Models/Tenant/MultiBusinessTable.cs | 26 + .../Models/Tenant/SubLibraryBusinessTable.cs | 22 + Blog.Core.Model/Models/TestModels.cs | 6 +- Blog.Core.Model/Models/Topic.cs | 2 +- Blog.Core.Model/Models/TopicDetail.cs | 2 +- Blog.Core.Model/Models/UserRole.cs | 6 +- Blog.Core.Model/Models/WeChatConfig.cs | 4 +- Blog.Core.Model/Models/WeChatPushLog.cs | 2 +- Blog.Core.Model/Models/sysUserInfo.cs | 14 +- Blog.Core.Model/Tenants/ITenantEntity.cs | 15 + .../Tenants/MultiTenantAttribute.cs | 24 + Blog.Core.Model/Tenants/TenantTypeEnum.cs | 29 + Blog.Core.Model/ViewModels/BlogViewModels.cs | 6 +- Blog.Core.Model/ViewModels/SysUserInfoDto.cs | 6 +- Blog.Core.Publish.Linux.sh | 3 +- Blog.Core.Repository/BASE/BaseRepository.cs | 180 ++++-- Blog.Core.Repository/BASE/IBaseRepository.cs | 44 +- .../Blog.Core.Repository.csproj | 6 +- .../IRoleModulePermissionRepository.cs | 2 +- .../RoleModulePermissionRepository.cs | 2 +- Blog.Core.Services/BASE/BaseServices.cs | 96 ++- Blog.Core.Services/BlogArticleServices.cs | 2 +- .../RoleModulePermissionServices.cs | 2 +- Blog.Core.Services/SplitDemoServices.cs | 23 + Blog.Core.Services/TasksLogServices.cs | 137 ++++ Blog.Core.Services/TenantService.cs | 57 ++ Blog.Core.Services/UserRoleServices.cs | 4 +- Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs | 43 +- .../Jobs/Job_AccessTrendLog_Quartz.cs | 3 +- .../QuartzNet/Jobs/Job_Blogs_Quartz.cs | 6 +- .../QuartzNet/Jobs/Job_OperateLog_Quartz.cs | 6 +- .../QuartzNet/Jobs/Job_Trojan_Quartz.cs | 6 +- .../QuartzNet/Jobs/Job_URL_Quartz.cs | 8 +- .../QuartzNet/SchedulerCenterServer.cs | 6 + .../Common_Test/DynamicLambdaTest.cs | 54 +- .../Controller_Test/BlogController_Should.cs | 2 +- .../DependencyInjection/DI_Test.cs | 35 +- Blog.Core.Tests/Repository_Test/OrmTest.cs | 73 +++ README.md | 17 +- 150 files changed, 4694 insertions(+), 914 deletions(-) create mode 100644 Blog.Core.Api/Controllers/SplitDemoController.cs create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs create mode 100644 Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs create mode 100644 Blog.Core.Common/App.cs create mode 100644 Blog.Core.Common/Core/InternalApp.cs create mode 100644 Blog.Core.Common/DB/Aop/SqlsugarAop.cs create mode 100644 Blog.Core.Common/DB/RepositorySetting.cs create mode 100644 Blog.Core.Common/DB/TenantUtil.cs create mode 100644 Blog.Core.Common/Extensions/AssemblysExtensions.cs create mode 100644 Blog.Core.Common/Extensions/UntilExtensions.cs create mode 100644 Blog.Core.Common/Helper/GenericTypeExtensions.cs create mode 100644 Blog.Core.Common/Helper/NumberConverter.cs create mode 100644 Blog.Core.Common/Seed/IEntitySeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/TenantSeedData.cs create mode 100644 Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs create mode 100644 Blog.Core.Extensions/HostedService/ConsulHostedService.cs create mode 100644 Blog.Core.Extensions/HostedService/EventBusHostedService.cs create mode 100644 Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs create mode 100644 Blog.Core.Extensions/HostedService/SeedDataHostedService.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs create mode 100644 Blog.Core.Gateway/appsettings.gw.Development.json create mode 100644 Blog.Core.Gateway/appsettings.gw.json create mode 100644 Blog.Core.IServices/ISplitDemoServices.cs create mode 100644 Blog.Core.IServices/ITasksLogServices.cs create mode 100644 Blog.Core.IServices/ITenantService.cs create mode 100644 Blog.Core.Model/Models/BlogArticleComment.cs create mode 100644 Blog.Core.Model/Models/RootTkey/BaseEntity.cs create mode 100644 Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs create mode 100644 Blog.Core.Model/Models/SplitDemo.cs create mode 100644 Blog.Core.Model/Models/SysTenant.cs create mode 100644 Blog.Core.Model/Models/TasksLog.cs create mode 100644 Blog.Core.Model/Models/Tenant/BusinessTable.cs create mode 100644 Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs create mode 100644 Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs create mode 100644 Blog.Core.Model/Models/Tenant/SubLibraryBusinessTable.cs create mode 100644 Blog.Core.Model/Tenants/ITenantEntity.cs create mode 100644 Blog.Core.Model/Tenants/MultiTenantAttribute.cs create mode 100644 Blog.Core.Model/Tenants/TenantTypeEnum.cs create mode 100644 Blog.Core.Services/SplitDemoServices.cs create mode 100644 Blog.Core.Services/TasksLogServices.cs create mode 100644 Blog.Core.Services/TenantService.cs create mode 100644 Blog.Core.Tests/Repository_Test/OrmTest.cs diff --git a/.gitignore b/.gitignore index b98046c6..b7645c45 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ bld/ # Visual Studio 2017 auto generated files Generated\ Files/ +# Visual Studio Code +.vscode + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -352,3 +355,4 @@ Blog.Core/WMBlog.db Blog.Core/Blog.Core*.xml Blog.Core.Api/WMBlog.db Blog.Core.Api/wwwroot/ui/ +*.db diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 38575a55..77d681f1 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -26,18 +26,22 @@ + + + + @@ -94,7 +98,6 @@ - diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index ea9857bc..8423cb6b 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -301,6 +301,16 @@ 逻辑删除 + + + 评论 + + + + + 博客文章 评论 + + 部门表 @@ -822,6 +832,122 @@ 修改时间 + + + 状态
+ 中立字段,某些表可使用某些表不使用 +
+
+ + + 中立字段,某些表可使用某些表不使用
+ 逻辑上的删除,非物理删除
+ 例如:单据删除并非直接删除 +
+
+ + + 中立字段
+ 是否内置数据 +
+
+ + + 创建ID + + + + + 创建者 + + + + + 创建时间 + + + + + 修改ID + + + + + 更新者 + + + + + 修改日期 + + + + + 数据版本 + + + + + 软删除 过滤器 + + + + + 系统租户表
+ 根据TenantType 分为两种方案:
+ 1.按租户字段区分
+ 2.按租户分库
+ +
+ + 注意:
+ 使用租户Id方案,无需配置分库的连接 +
+
+ + + 名称 + + + + + 租户类型 + + + + + 数据库/租户标识 不可重复
+ 使用Id方案,可无需配置 +
+
+ + + 主机
+ 使用Id方案,可无需配置 +
+
+ + + 数据库类型
+ 使用Id方案,可无需配置 +
+
+ + + 数据库连接
+ 使用Id方案,可无需配置 +
+
+ + + 状态 + + + + + 备注 + + 用户信息表 @@ -887,6 +1013,96 @@ 登录账号 + + + 租户Id + + + + + 任务日志表 + + + + + 任务ID + + + + + 任务耗时 + + + + + 执行结果(0-失败 1-成功) + + + + + 运行时间 + + + + + 结束时间 + + + + + 执行参数 + + + + + 异常信息 + + + + + 异常堆栈 + + + + + 创建ID + + + + + 创建者 + + + + + 创建时间 + + + + + 修改ID + + + + + 修改者 + + + + + 修改时间 + + + + + 任务名称 + + + + + 任务分组 + + 任务计划表 @@ -952,6 +1168,11 @@ 循环执行次数 + + + 已循环次数 + + 是否启动 @@ -972,6 +1193,63 @@ 任务内存中的状态 + + + 业务数据
+ 多租户 (Id 隔离) +
+
+ + + 无需手动赋值 + + + + + 名称 + + + + + 金额 + + + + + 多租户-多表方案 业务表 子表
+
+
+ + + 多租户-多表方案 业务表
+
+
+ + + 名称 + + + + + 金额 + + + + + 多租户-多库方案 业务表
+ 公共库无需标记[MultiTenant]特性 +
+
+ + + 名称 + + + + + 金额 + + Tibug 类别 @@ -1779,6 +2057,43 @@ 返回数据集 + + + 租户模型接口 + + + + + 租户Id + + + + + 标识 多租户 的业务表
+ 默认设置是多库
+ 公共表无需区分 直接使用主库 各自业务在各自库中
+
+
+ + + 租户隔离方案 + + + + + Id隔离 + + + + + 库隔离 + + + + + 表隔离 + + 广告类 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index a14a9006..c6b33db9 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -26,14 +26,14 @@ - + 获取博客详情 - + 获取详情【无权限】 @@ -67,7 +67,7 @@ - + 删除博客 @@ -276,7 +276,7 @@ - + 删除一条接口 @@ -361,7 +361,7 @@ 菜单管理 - + 构造函数 @@ -369,20 +369,22 @@ + - + 获取菜单 + - + 查询树形 Table @@ -404,7 +406,7 @@ - + 获取菜单树 @@ -412,21 +414,21 @@ - + 获取路由树 - + 获取路由树 - + 通过角色获取菜单 @@ -440,7 +442,7 @@ - + 删除菜单 @@ -454,7 +456,7 @@ - + 系统接口菜单同步接口 @@ -491,7 +493,7 @@ - + 删除角色 @@ -520,42 +522,42 @@ - + 删除一个任务 - + 启动计划任务 - + 停止一个计划任务 - + 暂停一个计划任务 - + 恢复一个计划任务 - + 重启一个计划任务 @@ -568,13 +570,25 @@ - + 立即执行任务 + + + 获取任务运行日志 + + + + + + 任务概况 + + + 类别管理【无权限】 @@ -615,7 +629,7 @@ - + 获取详情【无权限】 @@ -636,14 +650,14 @@ - + 删除 bug - + 测试事务在AOP中的使用 @@ -851,7 +865,7 @@ - + 删除用户 @@ -887,7 +901,7 @@ - + 新建用户角色关系 @@ -1349,7 +1363,7 @@ 关键字 - + 获取部门树 @@ -1396,6 +1410,139 @@ + + + 分表demo + + + + + 分页获取数据 + + + + + + + + + + + 根据ID获取信息 + + + + + + + 添加一条测试数据 + + + + + + + 修改一条测试数据 + + + + + + + 根据id删除数据 + + + + + + + 多租户-多库方案 测试 + + + + + 获取租户下全部业务数据
+
+ +
+ + + 新增数据 + + + + + + 多租户-Id方案 测试 + + + + + 获取租户下全部业务数据
+
+ +
+ + + 新增业务数据 + + + + + + 多租户-多表方案 测试 + + + + + 获取租户下全部业务数据
+
+ +
+ + + 新增数据 + + + + + + 租户管理 + + + + + 获取全部租户 + + + + + + 获取租户信息 + + + + + + 新增租户信息
+ 此处只做演示,具体要以实际业务为准 +
+ +
+ + + 修改租户信息
+ 此处只做演示,具体要以实际业务为准 +
+ +
+ + + 删除租户
+ 此处只做演示,具体要以实际业务为准 +
+ +
自定义路由 /api/{version}/[controler]/[action] diff --git a/Blog.Core.Api/Controllers/BlogController.cs b/Blog.Core.Api/Controllers/BlogController.cs index 6525448e..fbc67e12 100644 --- a/Blog.Core.Api/Controllers/BlogController.cs +++ b/Blog.Core.Api/Controllers/BlogController.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Blog.Core.Common.Helper; using Blog.Core.IServices; using Blog.Core.Model; @@ -11,7 +8,6 @@ using Blog.Core.SwaggerHelper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using StackExchange.Profiling; using static Blog.Core.Extensions.CustomApiVersion; @@ -87,7 +83,7 @@ public async Task>> Get(int id, int page = 1 [HttpGet("{id}")] //[Authorize(Policy = "Scope_BlogModule_Policy")] [Authorize] - public async Task> Get(int id) + public async Task> Get(long id) { return Success(await _blogArticleServices.GetBlogDetails(id)); } @@ -100,7 +96,7 @@ public async Task> Get(int id) /// [HttpGet] [Route("DetailNuxtNoPer")] - public async Task> DetailNuxtNoPer(int id) + public async Task> DetailNuxtNoPer(long id) { _logger.LogInformation("xxxxxxxxxxxxxxxxxxx"); return Success(await _blogArticleServices.GetBlogDetails(id)); @@ -108,7 +104,7 @@ public async Task> DetailNuxtNoPer(int id) [HttpGet] [Route("GoUrl")] - public async Task GoUrl(int id = 0) + public async Task GoUrl(long id = 0) { var response = await _blogArticleServices.QueryById(id); if (response != null && response.bsubmitter.IsNotEmptyOrNull()) @@ -140,7 +136,7 @@ public async Task>> GetBlogsByTypesForMVP(string [HttpGet] [Route("GetBlogByIdForMVP")] - public async Task> GetBlogByIdForMVP(int id = 0) + public async Task> GetBlogByIdForMVP(long id = 0) { if (id > 0) { @@ -251,7 +247,7 @@ public async Task> Put([FromBody] BlogArticle BlogArticle) [HttpDelete] [Authorize(Permissions.Name)] [Route("Delete")] - public async Task> Delete(int id) + public async Task> Delete(long id) { if (id > 0) { diff --git a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs index 8006d1fb..7865cc69 100644 --- a/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs +++ b/Blog.Core.Api/Controllers/DbFirst/MigrateController.cs @@ -81,7 +81,7 @@ public async Task> DataMigrateFromOld2New() InitPermissionTree(permissions, permissionsAllList, apiList); var actionPermissionIds = permissionsAllList.Where(d => d.Id >= filterPermissionId).Select(d => d.Id).ToList(); - List filterPermissionIds = new(); + List filterPermissionIds = new(); FilterPermissionTree(permissionsAllList, actionPermissionIds, filterPermissionIds); permissions = permissions.Where(d => filterPermissionIds.Contains(d.Id)).ToList(); @@ -93,10 +93,10 @@ public async Task> DataMigrateFromOld2New() // 1、保持菜单和接口 await SavePermissionTreeAsync(permissions, pms); - var rid = 0; - var pid = 0; - var mid = 0; - var rpmid = 0; + long rid = 0; + long pid = 0; + long mid = 0; + long rpmid = 0; // 2、保存关系表 foreach (var item in rmps) @@ -116,8 +116,8 @@ public async Task> DataMigrateFromOld2New() } } - pid = (pms.FirstOrDefault(d => d.PidOld == item.PermissionId)?.PidNew).ObjToInt(); - mid = (pms.FirstOrDefault(d => d.MidOld == item.ModuleId)?.MidNew).ObjToInt(); + pid = (pms.FirstOrDefault(d => d.PidOld == item.PermissionId)?.PidNew).ObjToLong(); + mid = (pms.FirstOrDefault(d => d.MidOld == item.ModuleId)?.MidNew).ObjToLong(); // 关系 if (rid > 0 && pid > 0) { @@ -282,7 +282,7 @@ private void InitPermissionTree(List permissionsTree, List permissionsAll, List actionPermissionId, List filterPermissionIds) + private void FilterPermissionTree(List permissionsAll, List actionPermissionId, List filterPermissionIds) { actionPermissionId = actionPermissionId.Distinct().ToList(); var doneIds = permissionsAll.Where(d => actionPermissionId.Contains(d.Id) && d.Pid == 0).Select(d => d.Id).ToList(); @@ -295,7 +295,7 @@ private void FilterPermissionTree(List permissionsAll, List act } } - private async Task SavePermissionTreeAsync(List permissionsTree, List pms, int permissionId = 0) + private async Task SavePermissionTreeAsync(List permissionsTree, List pms, long permissionId = 0) { var parendId = permissionId; @@ -304,9 +304,9 @@ private async Task SavePermissionTreeAsync(List permissionsTree, Lis PM pm = new PM(); // 保留原始主键id pm.PidOld = item.Id; - pm.MidOld = (item.Module?.Id).ObjToInt(); + pm.MidOld = (item.Module?.Id).ObjToLong(); - var mid = 0; + long mid = 0; // 接口 if (item.Module != null) { @@ -351,9 +351,9 @@ private async Task SavePermissionTreeAsync(List permissionsTree, Lis public class PM { - public int PidOld { get; set; } - public int MidOld { get; set; } - public int PidNew { get; set; } - public int MidNew { get; set; } + public long PidOld { get; set; } + public long MidOld { get; set; } + public long PidNew { get; set; } + public long MidNew { get; set; } } } diff --git a/Blog.Core.Api/Controllers/DepartmentController.cs b/Blog.Core.Api/Controllers/DepartmentController.cs index 1674883f..faf1f850 100644 --- a/Blog.Core.Api/Controllers/DepartmentController.cs +++ b/Blog.Core.Api/Controllers/DepartmentController.cs @@ -4,17 +4,10 @@ using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Linq.Expressions; using System.Text; -using System.Threading.Tasks; namespace Blog.Core.Api.Controllers { @@ -90,7 +83,7 @@ public async Task>> GetTreeTable(long f = 0, strin foreach (var item in departments) { - List pidarr = new() { }; + List pidarr = new() { }; var parent = departmentList.FirstOrDefault(d => d.Id == item.Pid); while (parent != null) @@ -116,7 +109,7 @@ public async Task>> GetTreeTable(long f = 0, strin /// /// [HttpGet] - public async Task> GetDepartmentTree(int pid = 0) + public async Task> GetDepartmentTree(long pid = 0) { var departments = await _departmentServices.Query(d => d.IsDeleted == false); var departmentTrees = (from child in departments @@ -175,7 +168,7 @@ public async Task> Put([FromBody] Department request) } [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); var model = await _departmentServices.QueryById(id); diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 25818c13..87f5c0c9 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Blog.Core.AuthHelper; +using Blog.Core.AuthHelper; using Blog.Core.AuthHelper.OverWrite; using Blog.Core.Common.Helper; using Blog.Core.IServices; @@ -12,8 +6,9 @@ using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; namespace Blog.Core.Controllers @@ -52,6 +47,7 @@ public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServic #region 获取token的第1种方法 + /// /// 获取JWT的方法1 /// @@ -62,7 +58,6 @@ public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServic [Route("Token")] public async Task> GetJwtStr(string name, string pass) { - string jwtStr = string.Empty; bool suc = false; //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 @@ -70,7 +65,6 @@ public async Task> GetJwtStr(string name, string pass) var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); if (user != null) { - TokenModelJwt tokenModel = new TokenModelJwt { Uid = 1, Role = user }; jwtStr = JwtHelper.IssueJwt(tokenModel); @@ -119,6 +113,7 @@ public MessageModel GetJwtStrForNuxt(string name, string pass) { jwtStr = "login fail!!!"; } + var result = new { data = new { success = suc, token = jwtStr } @@ -131,8 +126,8 @@ public MessageModel GetJwtStrForNuxt(string name, string pass) response = jwtStr }; } - #endregion + #endregion /// @@ -157,11 +152,14 @@ public async Task> GetJwtToken3(string name = " { var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List { + var claims = new List + { new Claim(ClaimTypes.Name, name), new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), + new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; + new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); @@ -218,11 +216,13 @@ public async Task> RefreshToken(string token = { var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List { - new Claim(ClaimTypes.Name, user.LoginName), - new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; + var claims = new List + { + new Claim(ClaimTypes.Name, user.LoginName), + new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); //用户标识 @@ -233,6 +233,7 @@ public async Task> RefreshToken(string token = return Success(refreshToken, "获取成功"); } } + return Failed("认证失败!"); } diff --git a/Blog.Core.Api/Controllers/ModuleController.cs b/Blog.Core.Api/Controllers/ModuleController.cs index 334a8ea4..27e6f4db 100644 --- a/Blog.Core.Api/Controllers/ModuleController.cs +++ b/Blog.Core.Api/Controllers/ModuleController.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Threading.Tasks; +using System.Linq.Expressions; using Blog.Core.Common.HttpContextUser; using Blog.Core.IServices; using Blog.Core.Model; @@ -122,7 +119,7 @@ public async Task> Put([FromBody] Modules module) /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { if (id <= 0) return Failed("缺少参数"); diff --git a/Blog.Core.Api/Controllers/MonitorController.cs b/Blog.Core.Api/Controllers/MonitorController.cs index c0dbed52..77b4e5bf 100644 --- a/Blog.Core.Api/Controllers/MonitorController.cs +++ b/Blog.Core.Api/Controllers/MonitorController.cs @@ -6,19 +6,12 @@ using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using Blog.Core.Extensions.Middlewares; namespace Blog.Core.Controllers diff --git a/Blog.Core.Api/Controllers/NacosController.cs b/Blog.Core.Api/Controllers/NacosController.cs index a8701b39..e5223851 100644 --- a/Blog.Core.Api/Controllers/NacosController.cs +++ b/Blog.Core.Api/Controllers/NacosController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nacos.V2; -using System.Threading.Tasks; namespace Blog.Core.Api.Controllers { diff --git a/Blog.Core.Api/Controllers/PayController.cs b/Blog.Core.Api/Controllers/PayController.cs index 0cbe0541..6c05c249 100644 --- a/Blog.Core.Api/Controllers/PayController.cs +++ b/Blog.Core.Api/Controllers/PayController.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Blog.Core.Controllers { diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index d3000766..7346cc21 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -6,6 +6,8 @@ using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; @@ -21,6 +23,7 @@ namespace Blog.Core.Controllers [Authorize(Permissions.Name)] public class PermissionController : BaseApiController { + readonly IUnitOfWorkManage _unitOfWorkManage; readonly IPermissionServices _permissionServices; readonly IModuleServices _moduleServices; readonly IRoleModulePermissionServices _roleModulePermissionServices; @@ -37,16 +40,19 @@ public class PermissionController : BaseApiController /// /// /// + /// /// /// /// /// public PermissionController(IPermissionServices permissionServices, IModuleServices moduleServices, IRoleModulePermissionServices roleModulePermissionServices, IUserRoleServices userRoleServices, + IUnitOfWorkManage unitOfWorkManage, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContext, IUser user, PermissionRequirement requirement) { _permissionServices = permissionServices; + _unitOfWorkManage = unitOfWorkManage; _moduleServices = moduleServices; _roleModulePermissionServices = roleModulePermissionServices; _userRoleServices = userRoleServices; @@ -61,34 +67,19 @@ public PermissionController(IPermissionServices permissionServices, IModuleServi /// /// /// + /// /// // GET: api/User [HttpGet] - public async Task>> Get(int page = 1, string key = "") + public async Task>> Get(int page = 1, string key = "", int pageSize = 50) { PageModel permissions = new PageModel(); - int intPageSize = 50; if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { key = ""; } - #region 舍弃 - //var permissions = await _permissionServices.Query(a => a.IsDeleted != true); - //if (!string.IsNullOrEmpty(key)) - //{ - // permissions = permissions.Where(t => (t.Name != null && t.Name.Contains(key))).ToList(); - //} - ////筛选后的数据总数 - //totalCount = permissions.Count; - ////筛选后的总页数 - //pageCount = (Math.Ceiling(totalCount.ObjToDecimal() / intTotalCount.ObjToDecimal())).ObjToInt(); - //permissions = permissions.OrderByDescending(d => d.Id).Skip((page - 1) * intTotalCount).Take(intTotalCount).ToList(); - #endregion - - - - permissions = await _permissionServices.QueryPage(a => a.IsDeleted != true && (a.Name != null && a.Name.Contains(key)), page, intPageSize, " Id desc "); + permissions = await _permissionServices.QueryPage(a => a.IsDeleted != true && (a.Name != null && a.Name.Contains(key)), page, pageSize, " Id desc "); #region 单独处理 @@ -99,7 +90,7 @@ public async Task>> Get(int page = 1, string var permissionAll = await _permissionServices.Query(d => d.IsDeleted != true); foreach (var item in permissionsView) { - List pidarr = new List + List pidarr = new() { item.Pid }; @@ -156,7 +147,7 @@ public async Task>> Get(int page = 1, string /// [HttpGet] [AllowAnonymous] - public async Task>> GetTreeTable(int f = 0, string key = "") + public async Task>> GetTreeTable(long f = 0, string key = "") { List permissions = new List(); var apiList = await _moduleServices.Query(d => d.IsDeleted == false); @@ -177,7 +168,7 @@ public async Task>> GetTreeTable(int f = 0, string foreach (var item in permissions) { - List pidarr = new List { }; + List pidarr = new() { }; var parent = permissionsList.FirstOrDefault(d => d.Id == item.Pid); while (parent != null) @@ -240,64 +231,55 @@ public async Task> Post([FromBody] Permission permission) [HttpPost] public async Task> Assign([FromBody] AssignView assignView) { - var data = new MessageModel(); - - if (assignView.rid > 0) { - data.success = true; - - var roleModulePermissions = await _roleModulePermissionServices.Query(d => d.RoleId == assignView.rid); + //开启事务 + try + { + var old_rmps = await _roleModulePermissionServices.Query(d => d.RoleId == assignView.rid); - var remove = roleModulePermissions.Where(d => !assignView.pids.Contains(d.PermissionId.ObjToInt())).Select(c => (object)c.Id); - data.success &= remove.Any() ? await _roleModulePermissionServices.DeleteByIds(remove.ToArray()) : true; + _unitOfWorkManage.BeginTran(); + await _permissionServices.Db.Deleteable(t => t.RoleId == assignView.rid).ExecuteCommandAsync(); + var permissions = await _permissionServices.Query(d => d.IsDeleted == false); - foreach (var item in assignView.pids) - { - var rmpitem = roleModulePermissions.Where(d => d.PermissionId == item); - var moduleid = (await _permissionServices.Query(p => p.Id == item)).FirstOrDefault()?.Mid; - if (!rmpitem.Any()) + List new_rmps = new List(); + var nowTime = _permissionServices.Db.GetDate(); + foreach (var item in assignView.pids) { + var moduleid = permissions.Find(p => p.Id == item)?.Mid; + var find_old_rmps = old_rmps.Find(p => p.PermissionId == item); RoleModulePermission roleModulePermission = new RoleModulePermission() { IsDeleted = false, RoleId = assignView.rid, - ModuleId = moduleid.ObjToInt(), + ModuleId = moduleid.ObjToLong(), PermissionId = item, - }; - - - roleModulePermission.CreateId = _user.ID; - roleModulePermission.CreateBy = _user.Name; + CreateId = find_old_rmps == null ? _user.ID : find_old_rmps.CreateId, + CreateBy = find_old_rmps == null ? _user.Name : find_old_rmps.CreateBy, + CreateTime = find_old_rmps == null ? nowTime : find_old_rmps.CreateTime, + ModifyId = _user.ID, + ModifyBy = _user.Name, + ModifyTime = nowTime - data.success &= (await _roleModulePermissionServices.Add(roleModulePermission)) > 0; - - } - else - { - foreach (var role in rmpitem) - { - if (!role.ModuleId.Equals(moduleid)) - { - role.ModuleId = moduleid.Value; - await _roleModulePermissionServices.Update(role, new List { "ModuleId" }); - } - } + }; + new_rmps.Add(roleModulePermission); } + if (new_rmps.Count > 0) await _roleModulePermissionServices.Add(new_rmps); + _unitOfWorkManage.CommitTran(); } - - if (data.success) + catch (Exception) { - _requirement.Permissions.Clear(); - data.response = ""; - data.msg = "保存成功"; + _unitOfWorkManage.RollbackTran(); + throw; } - + _requirement.Permissions.Clear(); + return Success("保存成功"); + } + else + { + return Failed("请选择要操作的角色"); } - - - return data; } @@ -308,7 +290,7 @@ public async Task> Assign([FromBody] AssignView assignView) /// /// [HttpGet] - public async Task> GetPermissionTree(int pid = 0, bool needbtn = false) + public async Task> GetPermissionTree(long pid = 0, bool needbtn = false) { //var data = new MessageModel(); @@ -353,29 +335,29 @@ orderby child.Id /// /// [HttpGet] - public async Task> GetNavigationBar(int uid) + public async Task> GetNavigationBar(long uid) { var data = new MessageModel(); - var uidInHttpcontext1 = 0; - var roleIds = new List(); + long uidInHttpcontext1 = 0; + var roleIds = new List(); // ids4和jwt切换 if (Permissions.IsUseIds4) { // ids4 uidInHttpcontext1 = (from item in _httpContext.HttpContext.User.Claims where item.Type == "sub" - select item.Value).FirstOrDefault().ObjToInt(); + select item.Value).FirstOrDefault().ObjToLong(); roleIds = (from item in _httpContext.HttpContext.User.Claims where item.Type == "role" - select item.Value.ObjToInt()).ToList(); + select item.Value.ObjToLong()).ToList(); } else { // jwt - uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); - roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToInt()).Distinct().ToList(); + uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToLong(); + roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToLong()).Distinct().ToList(); } @@ -383,7 +365,7 @@ public async Task> GetNavigationBar(int uid) { if (roleIds.Any()) { - var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))).Select(d => d.PermissionId.ObjToInt()).Distinct(); + var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))).Select(d => d.PermissionId.ObjToLong()).Distinct(); if (pids.Any()) { var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id))).OrderBy(c => c.OrderSort); @@ -445,28 +427,28 @@ orderby child.Id /// /// [HttpGet] - public async Task>> GetNavigationBarPro(int uid) + public async Task>> GetNavigationBarPro(long uid) { var data = new MessageModel>(); - var uidInHttpcontext1 = 0; - var roleIds = new List(); + long uidInHttpcontext1 = 0; + var roleIds = new List(); // ids4和jwt切换 if (Permissions.IsUseIds4) { // ids4 uidInHttpcontext1 = (from item in _httpContext.HttpContext.User.Claims where item.Type == "sub" - select item.Value).FirstOrDefault().ObjToInt(); + select item.Value).FirstOrDefault().ObjToLong(); roleIds = (from item in _httpContext.HttpContext.User.Claims where item.Type == "role" - select item.Value.ObjToInt()).ToList(); + select item.Value.ObjToLong()).ToList(); } else { // jwt - uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToInt(); - roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToInt()).Distinct().ToList(); + uidInHttpcontext1 = ((JwtHelper.SerializeJwt(_httpContext.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", "")))?.Uid).ObjToLong(); + roleIds = (await _userRoleServices.Query(d => d.IsDeleted == false && d.UserId == uid)).Select(d => d.RoleId.ObjToLong()).Distinct().ToList(); } if (uid > 0 && uid == uidInHttpcontext1) @@ -474,7 +456,7 @@ public async Task>> GetNavigationBarPro(int if (roleIds.Any()) { var pids = (await _roleModulePermissionServices.Query(d => d.IsDeleted == false && roleIds.Contains(d.RoleId))) - .Select(d => d.PermissionId.ObjToInt()).Distinct(); + .Select(d => d.PermissionId.ObjToLong()).Distinct(); if (pids.Any()) { var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id) && d.IsButton == false)).OrderBy(c => c.OrderSort); @@ -522,14 +504,14 @@ orderby item.Id /// [HttpGet] [AllowAnonymous] - public async Task> GetPermissionIdByRoleId(int rid = 0) + public async Task> GetPermissionIdByRoleId(long rid = 0) { //var data = new MessageModel(); var rmps = await _roleModulePermissionServices.Query(d => d.IsDeleted == false && d.RoleId == rid); var permissionTrees = (from child in rmps orderby child.Id - select child.PermissionId.ObjToInt()).ToList(); + select child.PermissionId.ObjToLong()).ToList(); var permissions = await _permissionServices.Query(d => d.IsDeleted == false); List assignbtns = new List(); @@ -595,7 +577,7 @@ public async Task> Put([FromBody] Permission permission) /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); if (id > 0) @@ -657,7 +639,7 @@ public async Task> BatchPost([FromBody] List pe /// 是否执行迁移到数据 /// [HttpGet] - public async Task>> MigratePermission(string action = "", string controllerName = "", int pid = 0, bool isAction = false) + public async Task>> MigratePermission(string action = "", string controllerName = "", long pid = 0, bool isAction = false) { var data = new MessageModel>(); if (controllerName.IsNullOrEmpty()) @@ -757,11 +739,11 @@ public async Task>> MigratePermission(string actio List modules = await _moduleServices.Query(d => d.LinkUrl != null && d.LinkUrl.ToLower() == item.Module.LinkUrl); if (!modules.Any()) { - int mid = await _moduleServices.Add(item.Module); + var mid = await _moduleServices.Add(item.Module); if (mid > 0) { item.Mid = mid; - int permissionid = await _permissionServices.Add(item); + var permissionid = await _permissionServices.Add(item); } } @@ -779,12 +761,12 @@ public async Task>> MigratePermission(string actio public class AssignView { - public List pids { get; set; } - public int rid { get; set; } + public List pids { get; set; } + public long rid { get; set; } } public class AssignShow { - public List permissionids { get; set; } + public List permissionids { get; set; } public List assignbtns { get; set; } } diff --git a/Blog.Core.Api/Controllers/RoleController.cs b/Blog.Core.Api/Controllers/RoleController.cs index 36df73a4..0b93e943 100644 --- a/Blog.Core.Api/Controllers/RoleController.cs +++ b/Blog.Core.Api/Controllers/RoleController.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Blog.Core.Common.HttpContextUser; +using Blog.Core.Common.HttpContextUser; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; @@ -113,7 +112,7 @@ public async Task> Put([FromBody] Role role) /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); diff --git a/Blog.Core.Api/Controllers/SplitDemoController.cs b/Blog.Core.Api/Controllers/SplitDemoController.cs new file mode 100644 index 00000000..f625b202 --- /dev/null +++ b/Blog.Core.Api/Controllers/SplitDemoController.cs @@ -0,0 +1,199 @@ +using Blog.Core.IServices; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Linq.Expressions; + +namespace Blog.Core.Api.Controllers +{ + /// + /// 分表demo + /// + [Route("api/[controller]/[action]")] + [ApiController] + [Authorize(Permissions.Name)] + public class SplitDemoController : ControllerBase + { + readonly ISplitDemoServices splitDemoServices; + readonly IUnitOfWorkManage unitOfWorkManage; + public SplitDemoController(ISplitDemoServices _splitDemoServices, IUnitOfWorkManage _unitOfWorkManage) + { + splitDemoServices = _splitDemoServices; + unitOfWorkManage = _unitOfWorkManage; + } + + /// + /// 分页获取数据 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + [AllowAnonymous] + public async Task>> Get(DateTime beginTime, DateTime endTime, int page = 1, string key = "", int pageSize = 10) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) + { + key = ""; + } + Expression> whereExpression = a => (a.Name != null && a.Name.Contains(key)); + var data = await splitDemoServices.QueryPageSplit(whereExpression, beginTime, endTime, page, pageSize, " Id desc "); + return MessageModel>.Message(data.dataCount >= 0, "获取成功", data); + } + + /// + /// 根据ID获取信息 + /// + /// + /// + [HttpGet] + [AllowAnonymous] + public async Task> GetById(long id) + { + var data = new MessageModel(); + var model = await splitDemoServices.QueryByIdSplit(id); + if (model != null) + { + return MessageModel.Success("获取成功", model); + } + else + { + return MessageModel.Fail("获取失败"); + } + } + + /// + /// 添加一条测试数据 + /// + /// + /// + [HttpPost] + [AllowAnonymous] + public async Task> Post([FromBody] SplitDemo splitDemo) + { + var data = new MessageModel(); + //unitOfWorkManage.BeginTran(); + var id = (await splitDemoServices.AddSplit(splitDemo)); + data.success = (id == null ? false : true); + try + { + if (data.success) + { + data.response = id.FirstOrDefault().ToString(); + data.msg = "添加成功"; + } + else + { + data.msg = "添加失败"; + } + } + catch (Exception) + { + throw; + } + finally + { + //if (data.success) + // unitOfWorkManage.CommitTran(); + //else + // unitOfWorkManage.RollbackTran(); + } + return data; + } + + /// + /// 修改一条测试数据 + /// + /// + /// + [HttpPut] + [AllowAnonymous] + public async Task> Put([FromBody] SplitDemo splitDemo) + { + var data = new MessageModel(); + if (splitDemo != null && splitDemo.Id > 0) + { + unitOfWorkManage.BeginTran(); + data.success = await splitDemoServices.UpdateSplit(splitDemo, splitDemo.CreateTime); + try + { + if (data.success) + { + data.msg = "修改成功"; + data.response = splitDemo?.Id.ObjToString(); + } + else + { + data.msg = "修改失败"; + } + } + catch (Exception) + { + throw; + } + finally + { + if (data.success) + unitOfWorkManage.CommitTran(); + else + unitOfWorkManage.RollbackTran(); + } + } + return data; + } + + /// + /// 根据id删除数据 + /// + /// + /// + [HttpDelete] + [AllowAnonymous] + public async Task> Delete(long id) + { + var data = new MessageModel(); + + var model = await splitDemoServices.QueryByIdSplit(id); + if (model != null) + { + unitOfWorkManage.BeginTran(); + data.success = await splitDemoServices.DeleteSplit(model,model.CreateTime); + try + { + data.response = id.ObjToString(); + if (data.success) + { + data.msg = "删除成功"; + } + else + { + data.msg = "删除失败"; + } + + } + catch (Exception) + { + throw; + } + finally + { + if (data.success) + unitOfWorkManage.CommitTran(); + else + unitOfWorkManage.RollbackTran(); + } + } + else + { + data.msg = "不存在"; + } + return data; + + } + } +} diff --git a/Blog.Core.Api/Controllers/TasksQzController.cs b/Blog.Core.Api/Controllers/TasksQzController.cs index 264fa195..887cfcfc 100644 --- a/Blog.Core.Api/Controllers/TasksQzController.cs +++ b/Blog.Core.Api/Controllers/TasksQzController.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; @@ -22,14 +18,16 @@ namespace Blog.Core.Controllers public class TasksQzController : ControllerBase { private readonly ITasksQzServices _tasksQzServices; + private readonly ITasksLogServices _tasksLogServices; private readonly ISchedulerCenter _schedulerCenter; private readonly IUnitOfWorkManage _unitOfWorkManage; - public TasksQzController(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IUnitOfWorkManage unitOfWorkManage) + public TasksQzController(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IUnitOfWorkManage unitOfWorkManage, ITasksLogServices tasksLogServices) { _unitOfWorkManage = unitOfWorkManage; _tasksQzServices = tasksQzServices; _schedulerCenter = schedulerCenter; + _tasksLogServices = tasksLogServices; } /// @@ -58,7 +56,7 @@ public async Task>> Get(int page = 1, string key item.Triggers = await _schedulerCenter.GetTaskStaus(item); } } - return MessageModel>.Message(data.dataCount >= 0, "获取成功", data); + return MessageModel>.Message(data.dataCount >= 0, "获取成功", data); } /// @@ -86,32 +84,33 @@ public async Task> Post([FromBody] TasksQz tasksQz) var ResuleModel = await _schedulerCenter.AddScheduleJobAsync(tasksQz); data.success = ResuleModel.success; if (ResuleModel.success) - { + { data.msg = $"{data.msg}=>启动成功=>{ResuleModel.msg}"; } else - { + { data.msg = $"{data.msg}=>启动失败=>{ResuleModel.msg}"; } } } else - { + { data.msg = "添加失败"; - } + } } catch (Exception) { throw; } finally - { if(data.success) + { + if (data.success) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); } - return data; + return data; } @@ -135,7 +134,7 @@ public async Task> Put([FromBody] TasksQz tasksQz) data.msg = "修改成功"; data.response = tasksQz?.Id.ObjToString(); if (tasksQz.IsStart) - { + { var ResuleModelStop = await _schedulerCenter.StopScheduleJobAsync(tasksQz); data.msg = $"{data.msg}=>停止:{ResuleModelStop.msg}"; var ResuleModelStar = await _schedulerCenter.AddScheduleJobAsync(tasksQz); @@ -163,7 +162,7 @@ public async Task> Put([FromBody] TasksQz tasksQz) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } return data; } @@ -173,7 +172,7 @@ public async Task> Put([FromBody] TasksQz tasksQz) /// /// [HttpDelete] - public async Task> Delete(int jobId) + public async Task> Delete(long jobId) { var data = new MessageModel(); @@ -207,7 +206,7 @@ public async Task> Delete(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -222,14 +221,14 @@ public async Task> Delete(int jobId) /// /// [HttpGet] - public async Task> StartJob(int jobId) + public async Task> StartJob(long jobId) { var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); if (model != null) { - _unitOfWorkManage.BeginTran(); + _unitOfWorkManage.BeginTran(); try { model.IsStart = true; @@ -265,7 +264,7 @@ public async Task> StartJob(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -279,7 +278,7 @@ public async Task> StartJob(int jobId) /// /// [HttpGet] - public async Task> StopJob(int jobId) + public async Task> StopJob(long jobId) { var data = new MessageModel(); @@ -319,12 +318,12 @@ public async Task> StopJob(int jobId) /// /// [HttpGet] - public async Task> PauseJob(int jobId) + public async Task> PauseJob(long jobId) { - var data = new MessageModel(); + var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); if (model != null) - { + { _unitOfWorkManage.BeginTran(); try { @@ -359,7 +358,7 @@ public async Task> PauseJob(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -373,13 +372,13 @@ public async Task> PauseJob(int jobId) /// /// [HttpGet] - public async Task> ResumeJob(int jobId) + public async Task> ResumeJob(long jobId) { var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); if (model != null) - { + { _unitOfWorkManage.BeginTran(); try { @@ -415,7 +414,7 @@ public async Task> ResumeJob(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -429,7 +428,7 @@ public async Task> ResumeJob(int jobId) /// /// [HttpGet] - public async Task> ReCovery(int jobId) + public async Task> ReCovery(long jobId) { var data = new MessageModel(); var model = await _tasksQzServices.QueryById(jobId); @@ -475,7 +474,7 @@ public async Task> ReCovery(int jobId) _unitOfWorkManage.CommitTran(); else _unitOfWorkManage.RollbackTran(); - } + } } else { @@ -488,7 +487,7 @@ public async Task> ReCovery(int jobId) /// 获取任务命名空间 /// /// - [HttpGet] + [HttpGet] public MessageModel> GetTaskNameSpace() { var baseType = typeof(IJob); @@ -501,14 +500,14 @@ public MessageModel> GetTaskNameSpace() var implementTypes = types.Where(x => x.IsClass).Select(item => new QuartzReflectionViewModel { nameSpace = item.Namespace, nameClass = item.Name, remark = "" }).ToList(); return MessageModel>.Success("获取成功", implementTypes); } - + /// /// 立即执行任务 /// /// /// [HttpGet] - public async Task> ExecuteJob(int jobId) + public async Task> ExecuteJob(long jobId) { var data = new MessageModel(); @@ -523,6 +522,26 @@ public async Task> ExecuteJob(int jobId) } return data; } + /// + /// 获取任务运行日志 + /// + /// + [HttpGet] + public async Task>> GetTaskLogs(long jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null) + { + var model = await _tasksLogServices.GetTaskLogs(jobId, page, pageSize, runTimeStart, runTimeEnd); + return MessageModel>.Message(model.dataCount >= 0, "获取成功", model); + } + /// + /// 任务概况 + /// + /// + [HttpGet] + public async Task> GetTaskOverview(long jobId, int page = 1, int pageSize = 10, DateTime? runTimeStart = null, DateTime? runTimeEnd = null, string type = "month") + { + var model = await _tasksLogServices.GetTaskOverview(jobId, runTimeStart, runTimeEnd, type); + return MessageModel.Message(true, "获取成功", model); + } } } diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs new file mode 100644 index 00000000..046f7f7b --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantByDbController.cs @@ -0,0 +1,50 @@ +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Controllers; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 多租户-多库方案 测试 +/// +[Produces("application/json")] +[Route("api/Tenant/ByDb")] +[Authorize] +public class TenantByDbController : BaseApiController +{ + private readonly IBaseServices _services; + private readonly IUser _user; + + public TenantByDbController(IUser user, IBaseServices services) + { + _user = user; + _services = services; + } + + /// + /// 获取租户下全部业务数据
+ ///
+ /// + [HttpGet] + public async Task>> GetAll() + { + var data = await _services.Query(); + return Success(data); + } + + /// + /// 新增数据 + /// + /// + [HttpPost] + public async Task Post(SubLibraryBusinessTable data) + { + await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync(); + + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs new file mode 100644 index 00000000..b015bc6d --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantByIdController.cs @@ -0,0 +1,49 @@ +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Controllers; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 多租户-Id方案 测试 +/// +[Produces("application/json")] +[Route("api/Tenant/ById")] +[Authorize] +public class TenantByIdController : BaseApiController +{ + private readonly IBaseServices _services; + private readonly IUser _user; + + public TenantByIdController(IUser user, IBaseServices services) + { + _user = user; + _services = services; + } + + /// + /// 获取租户下全部业务数据
+ ///
+ /// + [HttpGet] + public async Task>> GetAll() + { + var data = await _services.Query(); + return Success(data); + } + + /// + /// 新增业务数据 + /// + /// + [HttpPost] + public async Task Post([FromBody] BusinessTable data) + { + await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync(); + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs b/Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs new file mode 100644 index 00000000..6c0b110e --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantByTableController.cs @@ -0,0 +1,57 @@ +using Blog.Core.Common.HttpContextUser; +using Blog.Core.Controllers; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 多租户-多表方案 测试 +/// +[Produces("application/json")] +[Route("api/Tenant/ByTable")] +[Authorize] +public class TenantByTableController : BaseApiController +{ + private readonly IBaseServices _services; + private readonly IUser _user; + + public TenantByTableController(IUser user, IBaseServices services) + { + _user = user; + _services = services; + } + + /// + /// 获取租户下全部业务数据
+ ///
+ /// + [HttpGet] + public async Task>> GetAll() + { + //查询 + // var data = await _services.Query(); + + //关联查询 + var data = await _services.Db + .Queryable() + .Includes(s => s.Child) + .ToListAsync(); + return Success(data); + } + + /// + /// 新增数据 + /// + /// + [HttpPost] + public async Task Post(MultiBusinessTable data) + { + await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync(); + + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs b/Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs new file mode 100644 index 00000000..90133fdb --- /dev/null +++ b/Blog.Core.Api/Controllers/Tenant/TenantManagerController.cs @@ -0,0 +1,87 @@ +using Blog.Core.Controllers; +using Blog.Core.IServices; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Tenant; + +/// +/// 租户管理 +/// +[Produces("application/json")] +[Route("api/TenantManager")] +[Authorize] +public class TenantManagerController : BaseApiController +{ + private readonly ITenantService _services; + + public TenantManagerController(ITenantService services) + { + _services = services; + } + + + /// + /// 获取全部租户 + /// + /// + [HttpGet] + public async Task>> GetAll() + { + var data = await _services.Query(); + return Success(data); + } + + + /// + /// 获取租户信息 + /// + /// + [HttpGet("{id}")] + public async Task> GetInfo(long id) + { + var data = await _services.QueryById(id); + return Success(data); + } + + /// + /// 新增租户信息
+ /// 此处只做演示,具体要以实际业务为准 + ///
+ /// + [HttpPost] + public async Task Post(SysTenant tenant) + { + await _services.SaveTenant(tenant); + return Success(); + } + + /// + /// 修改租户信息
+ /// 此处只做演示,具体要以实际业务为准 + ///
+ /// + [HttpPut] + public async Task Put(SysTenant tenant) + { + await _services.SaveTenant(tenant); + return Success(); + } + + /// + /// 删除租户
+ /// 此处只做演示,具体要以实际业务为准 + ///
+ /// + [HttpDelete] + public async Task Delete(long id) + { + //是否删除租户库? + //要根据实际情况而定 + //例如直接删除租户库、备份租户库到xx + await _services.DeleteById(id); + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/TopicController.cs b/Blog.Core.Api/Controllers/TopicController.cs index 1fe2d4a3..253f54ff 100644 --- a/Blog.Core.Api/Controllers/TopicController.cs +++ b/Blog.Core.Api/Controllers/TopicController.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Microsoft.AspNetCore.Authorization; @@ -46,7 +44,7 @@ public async Task>> Get() // GET: api/Topic/5 [HttpGet("{id}")] - public string Get(int id) + public string Get(long id) { return "value"; } @@ -59,13 +57,13 @@ public void Post([FromBody] string value) // PUT: api/Topic/5 [HttpPut("{id}")] - public void Put(int id, [FromBody] string value) + public void Put(long id, [FromBody] string value) { } // DELETE: api/ApiWithActions/5 [HttpDelete("{id}")] - public void Delete(int id) + public void Delete(long id) { } } diff --git a/Blog.Core.Api/Controllers/TopicDetailController.cs b/Blog.Core.Api/Controllers/TopicDetailController.cs index 264fe2df..374aca24 100644 --- a/Blog.Core.Api/Controllers/TopicDetailController.cs +++ b/Blog.Core.Api/Controllers/TopicDetailController.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Blog.Core.Common.Helper; +using Blog.Core.Common.Helper; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; @@ -45,7 +42,7 @@ public TopicDetailController(ITopicServices topicServices, ITopicDetailServices [AllowAnonymous] public async Task>> Get(int page = 1, string tname = "", string key = "", int intPageSize = 12) { - int tid = 0; + long tid = 0; if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) { @@ -59,7 +56,7 @@ public async Task>> Get(int page = 1, string if (!string.IsNullOrEmpty(tname)) { - tid = ((await _topicServices.Query(ts => ts.tName == tname)).FirstOrDefault()?.Id).ObjToInt(); + tid = ((await _topicServices.Query(ts => ts.tName == tname)).FirstOrDefault()?.Id).ObjToLong(); } @@ -84,7 +81,7 @@ public async Task>> Get(int page = 1, string // GET: api/TopicDetail/5 [HttpGet("{id}")] [AllowAnonymous] - public async Task> Get(int id) + public async Task> Get(long id) { var data = new MessageModel(); var response = id > 0 ? await _topicDetailServices.QueryById(id) : new TopicDetail(); @@ -157,7 +154,7 @@ public async Task> Update([FromBody] TopicDetail topicDetai /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); if (id > 0) diff --git a/Blog.Core.Api/Controllers/TransactionController.cs b/Blog.Core.Api/Controllers/TransactionController.cs index dd6b0384..9853d985 100644 --- a/Blog.Core.Api/Controllers/TransactionController.cs +++ b/Blog.Core.Api/Controllers/TransactionController.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; using Blog.Core.Repository.UnitOfWorks; @@ -98,7 +95,7 @@ public async Task>> Get() // GET: api/Transaction/5 [HttpGet("{id}")] - public async Task> Get(int id) + public async Task> Get(long id) { return await _guestbookServices.TestTranInRepository(); } @@ -129,7 +126,7 @@ public void Post([FromBody] string value) // PUT: api/Transaction/5 [HttpPut("{id}")] - public void Put(int id, [FromBody] string value) + public void Put(long id, [FromBody] string value) { } @@ -139,7 +136,7 @@ public void Put(int id, [FromBody] string value) /// /// [HttpDelete("{id}")] - public async Task Delete(int id) + public async Task Delete(long id) { return await _guestbookServices.TestTranInRepositoryAOP(); } diff --git a/Blog.Core.Api/Controllers/UserController.cs b/Blog.Core.Api/Controllers/UserController.cs index 6a2a18fa..95137c8e 100644 --- a/Blog.Core.Api/Controllers/UserController.cs +++ b/Blog.Core.Api/Controllers/UserController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Blog.Core.AuthHelper.OverWrite; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; @@ -13,7 +9,6 @@ using Blog.Core.Repository.UnitOfWorks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Blog.Core.Controllers { @@ -108,15 +103,15 @@ public async Task>> Get(int page = 1, str return Success(data.ConvertTo(_mapper)); } - private (string, List) GetFullDepartmentName(List departments, int departmentId) + private (string, List) GetFullDepartmentName(List departments, long departmentId) { var departmentModel = departments.FirstOrDefault(d => d.Id == departmentId); if (departmentModel == null) { - return ("", new List()); + return ("", new List()); } - var pids = departmentModel.CodeRelationship?.TrimEnd(',').Split(',').Select(d => d.ObjToInt()).ToList(); + var pids = departmentModel.CodeRelationship?.TrimEnd(',').Split(',').Select(d => d.ObjToLong()).ToList(); pids.Add(departmentModel.Id); var pnams = departments.Where(d => pids.Contains(d.Id)).ToList().Select(d => d.Name).ToArray(); var fullName = string.Join("/", pnams); @@ -270,7 +265,7 @@ public async Task> Put([FromBody] SysUserInfoDto sysUserInf /// // DELETE: api/ApiWithActions/5 [HttpDelete] - public async Task> Delete(int id) + public async Task> Delete(long id) { var data = new MessageModel(); if (id > 0) diff --git a/Blog.Core.Api/Controllers/UserRoleController.cs b/Blog.Core.Api/Controllers/UserRoleController.cs index d14d6a73..693a68b8 100644 --- a/Blog.Core.Api/Controllers/UserRoleController.cs +++ b/Blog.Core.Api/Controllers/UserRoleController.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; @@ -81,7 +80,7 @@ public async Task> AddRole(string roleName) /// /// [HttpGet] - public async Task> AddUserRole(int uid, int rid) + public async Task> AddUserRole(long uid, long rid) { return new MessageModel() { diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 52eb0ea1..1347ca16 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -298,7 +298,6 @@ public object Post([FromBody] BlogArticle blogArticle, int id) /// /// [HttpPost] - [Route("TestPostPara")] [AllowAnonymous] public object TestPostPara(string name) { diff --git a/Blog.Core.Api/Controllers/WeChatCompanyController.cs b/Blog.Core.Api/Controllers/WeChatCompanyController.cs index 4fa6eea5..dc12930b 100644 --- a/Blog.Core.Api/Controllers/WeChatCompanyController.cs +++ b/Blog.Core.Api/Controllers/WeChatCompanyController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatConfigController.cs b/Blog.Core.Api/Controllers/WeChatConfigController.cs index c597cb3f..1f3b705d 100644 --- a/Blog.Core.Api/Controllers/WeChatConfigController.cs +++ b/Blog.Core.Api/Controllers/WeChatConfigController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatController.cs b/Blog.Core.Api/Controllers/WeChatController.cs index c215f563..a27762e8 100644 --- a/Blog.Core.Api/Controllers/WeChatController.cs +++ b/Blog.Core.Api/Controllers/WeChatController.cs @@ -1,11 +1,8 @@ -using System.IO; -using System.Threading.Tasks; -using Blog.Core.IServices; +using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Blog.Core.Controllers { diff --git a/Blog.Core.Api/Controllers/WeChatPushLogController.cs b/Blog.Core.Api/Controllers/WeChatPushLogController.cs index 1fe1603d..af168091 100644 --- a/Blog.Core.Api/Controllers/WeChatPushLogController.cs +++ b/Blog.Core.Api/Controllers/WeChatPushLogController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Controllers/WeChatSubController.cs b/Blog.Core.Api/Controllers/WeChatSubController.cs index bd8d1759..94f982d2 100644 --- a/Blog.Core.Api/Controllers/WeChatSubController.cs +++ b/Blog.Core.Api/Controllers/WeChatSubController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Blog.Core.IServices; using Blog.Core.Model; using Blog.Core.Model.Models; diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 8ab4b93d..2a75498e 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -1,19 +1,22 @@ - -// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 +// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Text; using Autofac; using Autofac.Extensions.DependencyInjection; using Blog.Core; using Blog.Core.Common; +using Blog.Core.Common.Core; +using Blog.Core.Common.Helper; using Blog.Core.Common.LogHelper; -using Blog.Core.Common.Seed; using Blog.Core.Extensions; using Blog.Core.Extensions.Apollo; using Blog.Core.Extensions.Middlewares; using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; +using Blog.Core.Model; using Blog.Core.Tasks; -using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -21,9 +24,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Text; var builder = WebApplication.CreateBuilder(args); @@ -49,7 +49,6 @@ config.AddConfigurationApollo("appsettings.apollo.json"); }); - // 2、配置服务 builder.Services.AddSingleton(new AppSettings(builder.Configuration)); builder.Services.AddSingleton(new LogLock(builder.Environment.ContentRootPath)); @@ -79,7 +78,7 @@ builder.Services.AddKafkaSetup(builder.Configuration); builder.Services.AddEventBusSetup(); builder.Services.AddNacosSetup(builder.Configuration); - +builder.Services.AddInitializationHostServiceSetup(); builder.Services.AddAuthorizationSetup(); if (Permissions.IsUseIds4 || Permissions.IsUseAuthing) { @@ -114,6 +113,8 @@ //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; options.SerializerSettings.Converters.Add(new StringEnumConverter()); + //将long类型转为string + options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); }) //.AddFluentValidation(config => //{ @@ -129,9 +130,9 @@ builder.Services.Replace(ServiceDescriptor.Transient()); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - // 3、配置中间件 var app = builder.Build(); +app.ConfigureApplication(); if (app.Environment.IsDevelopment()) { @@ -182,16 +183,5 @@ endpoints.MapHub("/api2/chatHub"); }); - -var scope = app.Services.GetRequiredService().CreateScope(); -var myContext = scope.ServiceProvider.GetRequiredService(); -var tasksQzServices = scope.ServiceProvider.GetRequiredService(); -var schedulerCenter = scope.ServiceProvider.GetRequiredService(); -var lifetime = scope.ServiceProvider.GetRequiredService(); -app.UseSeedDataMiddle(myContext, builder.Environment.WebRootPath); -app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); -app.UseConsulMiddle(builder.Configuration, lifetime); -app.ConfigureEventBus(); - // 4、运行 -app.Run(); +app.Run(); \ No newline at end of file diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index b341819e..4f99b621 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -1,34 +1,30 @@ -using Autofac; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Text; +using Autofac; using Blog.Core.Common; +using Blog.Core.Common.Helper; using Blog.Core.Common.LogHelper; using Blog.Core.Common.Seed; using Blog.Core.Extensions; +using Blog.Core.Extensions.Middlewares; using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; +using Blog.Core.Model; using Blog.Core.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Text; -using Blog.Core.Extensions.Middlewares; namespace Blog.Core { public class Startup { - private IServiceCollection _services; public Startup(IConfiguration configuration, IWebHostEnvironment env) @@ -75,7 +71,7 @@ public void ConfigureServices(IServiceCollection services) services.AddEventBusSetup(); services.AddNacosSetup(Configuration); - + services.AddInitializationHostServiceSetup(); // 授权+认证 (jwt or ids4) services.AddAuthorizationSetup(); if (Permissions.IsUseIds4) @@ -95,7 +91,7 @@ public void ConfigureServices(IServiceCollection services) services.Configure(x => x.AllowSynchronousIO = true) .Configure(x => x.AllowSynchronousIO = true); - + services.AddDistributedMemoryCache(); services.AddSession(); services.AddHttpPollySetup(); @@ -129,12 +125,14 @@ public void ConfigureServices(IServiceCollection services) options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; //添加Enum转string options.SerializerSettings.Converters.Add(new StringEnumConverter()); + //将long类型转为string + options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); }); services.Replace(ServiceDescriptor.Transient()); _services = services; - //支持编码大全 例如:支持 System.Text.Encoding.GetEncoding("GB2312") System.Text.Encoding.GetEncoding("GB18030") + //支持编码大全 例如:支持 System.Text.Encoding.GetEncoding("GB2312") System.Text.Encoding.GetEncoding("GB18030") Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } @@ -150,11 +148,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex { // Ip限流,尽量放管道外层 app.UseIpLimitMiddle(); - // 记录请求与返回数据 + // 记录请求与返回数据 app.UseRequestResponseLogMiddle(); // 用户访问记录(必须放到外层,不然如果遇到异常,会报错,因为不能返回流) app.UseRecordAccessLogsMiddle(); - // signalr + // signalr app.UseSignalRSendMiddle(); // 记录ip请求 app.UseIpLogMiddle(); @@ -214,7 +212,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex // 开启异常中间件,要放到最后 //app.UseExceptionHandlerMidd(); - app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( @@ -225,15 +222,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContex }); // 生成种子数据 - app.UseSeedDataMiddle(myContext, Env.WebRootPath); + //app.UseSeedDataMiddle(myContext, Env.WebRootPath); // 开启QuartzNetJob调度服务 - app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); + //app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); // 服务注册 - app.UseConsulMiddle(Configuration, lifetime); + //app.UseConsulMiddle(Configuration, lifetime); // 事件总线,订阅服务 - app.ConfigureEventBus(); - + //app.ConfigureEventBus(); } - } -} +} \ No newline at end of file diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index ea678174..dd2f2990 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -1,98 +1,98 @@ { - "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 - "Logging": { - "LogLevel": { - "Default": "Information", //加入Default否则log4net本地写入不了日志 - "Blog.Core.AuthHelper.ApiResponseHandler": "Error" + "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 + "Logging": { + "LogLevel": { + "Default": "Information", //加入Default否则log4net本地写入不了日志 + "Blog.Core.AuthHelper.ApiResponseHandler": "Error" + }, + "Debug": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Debug" + } + }, + "Log4Net": { + "Name": "Blog.Core" + } }, - "Debug": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } + "AllowedHosts": "*", + "Redis": { + "ConnectionString": "127.0.0.1:6319,password=admin" }, - "Console": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "Microsoft.Hosting.Lifetime": "Debug" - } + "RabbitMQ": { + "Enabled": false, + "Connection": "118.25.251.13", + "UserName": "", + "Password": "!", + "RetryCount": 3 }, - "Log4Net": { - "Name": "Blog.Core" - } - }, - "AllowedHosts": "*", - "Redis": { - "ConnectionString": "127.0.0.1:6319,password=admin" - }, - "RabbitMQ": { - "Enabled": false, - "Connection": "118.25.251.13", - "UserName": "", - "Password": "!", - "RetryCount": 3 - }, - "Kafka": { - "Enabled": false, - "Servers": "localhost:9092", - "Topic": "blog", - "GroupId": "blog-consumer", - "NumPartitions": 3 //主题分区数量 - }, - "EventBus": { - "Enabled": false, - "SubscriptionClientName": "Blog.Core" - }, - "AppSettings": { - "RedisCachingAOP": { - "Enabled": false - }, - "MemoryCachingAOP": { - "Enabled": true - }, - "LogAOP": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } + "Kafka": { + "Enabled": false, + "Servers": "localhost:9092", + "Topic": "blog", + "GroupId": "blog-consumer", + "NumPartitions": 3 //主题分区数量 }, - "TranAOP": { - "Enabled": true + "EventBus": { + "Enabled": false, + "SubscriptionClientName": "Blog.Core" }, - "SqlAOP": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": false - }, - "LogToConsole": { - "Enabled": true - } + "AppSettings": { + "RedisCachingAOP": { + "Enabled": false + }, + "MemoryCachingAOP": { + "Enabled": true + }, + "LogAOP": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": false + } + }, + "TranAOP": { + "Enabled": true + }, + "SqlAOP": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": false + }, + "LogToConsole": { + "Enabled": true + } + }, + "Date": "2018-08-28", + "SeedDBEnabled": true, //只生成表结构 + "SeedDBDataEnabled": true, //生成表,并初始化数据 + "Author": "Blog.Core", + "SvcName": "", // /svc/blog + "UseLoadTest": false }, - "Date": "2018-08-28", - "SeedDBEnabled": true, //只生成表结构 - "SeedDBDataEnabled": true, //生成表,并初始化数据 - "Author": "Blog.Core", - "SvcName": "", // /svc/blog - "UseLoadTest": false - }, - // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; - // *** 单库操作,把 MutiDBEnabled 设为false ***; - // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; - // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 + // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; + // *** 单库操作,把 MutiDBEnabled 设为false ***; + // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; + // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 - "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true - "MutiDBEnabled": false, //是否开启多库模式 - "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer - "DBS": [ - /* + "MainDB": "WMBLOG_MYSQL", //当前项目的主库,所对应的连接字符串的Enabled必须为true + "MutiDBEnabled": true, //是否开启多库模式 + "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer + "DBS": [ + /* 对应下边的 DBType MySql = 0, SqlServer = 1, @@ -102,225 +102,230 @@ Dm = 5,//达梦 Kdbndp = 6,//人大金仓 */ - { - "ConnId": "WMBLOG_SQLITE", - "DBType": 2, - "Enabled": true, - "HitRate": 50, // 值越大,优先级越高 - "Connection": "WMBlog.db" //sqlite只写数据库名就行 - }, - { - "ConnId": "WMBLOG_MSSQL_1", - "DBType": 1, - "Enabled": false, - "HitRate": 40, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", - "ProviderName": "System.Data.SqlClient" - }, - { - "ConnId": "WMBLOG_MSSQL_2", - "DBType": 1, - "Enabled": false, - "HitRate": 30, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", - "ProviderName": "System.Data.SqlClient" + { + "ConnId": "WMBLOG_SQLITE", + "DBType": 2, + "Enabled": false, + "HitRate": 50, // 值越大,优先级越高 + "Connection": "WMBlog.db" //sqlite只写数据库名就行 + }, + { + "ConnId": "WMBLOG_MSSQL_1", + "DBType": 1, + "Enabled": false, + "HitRate": 40, + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "ProviderName": "System.Data.SqlClient" + }, + { + "ConnId": "WMBLOG_MSSQL_2", + "DBType": 1, + "Enabled": false, + "HitRate": 30, + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "ProviderName": "System.Data.SqlClient" + }, + { + "ConnId": "WMBLOG_MYSQL", + "DBType": 0, + "Enabled": true, + "HitRate": 20, + "Connection": "server=localhost;Database=blog;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" + }, + { + "ConnId": "WMBLOG_MYSQL_2", + "DBType": 0, + "Enabled": true, + "HitRate": 20, + "Connection": "server=localhost;Database=trojan;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" + }, + { + "ConnId": "WMBLOG_ORACLE", + "DBType": 3, + "Enabled": false, + "HitRate": 10, + "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" + }, + { + "ConnId": "WMBLOG_DM", + "DBType": 5, + "Enabled": false, + "HitRate": 10, + "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" + }, + { + "ConnId": "WMBLOG_KDBNDP", + "DBType": 6, + "Enabled": false, + "HitRate": 10, + "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" + } + ], + "Audience": { + "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ + "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret + "Issuer": "Blog.Core", + "Audience": "wr" }, - { - "ConnId": "WMBLOG_MYSQL", - "DBType": 0, - "Enabled": false, - "HitRate": 20, - "Connection": "server=.;Database=ddd;Uid=root;Pwd=123456;Port=10060;Allow User Variables=True;" - }, - { - "ConnId": "WMBLOG_MYSQL_2", - "DBType": 0, - "Enabled": true, - "HitRate": 20, - "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" - }, - { - "ConnId": "WMBLOG_ORACLE", - "DBType": 3, - "Enabled": false, - "HitRate": 10, - "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" - }, - { - "ConnId": "WMBLOG_DM", - "DBType": 5, - "Enabled": false, - "HitRate": 10, - "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" - }, - { - "ConnId": "WMBLOG_KDBNDP", - "DBType": 6, - "Enabled": true, - "HitRate": 10, - "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" - } - ], - "Audience": { - "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ - "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret - "Issuer": "Blog.Core", - "Audience": "wr" - }, - "Mongo": { - "ConnectionString": "mongodb://nosql.data", - "Database": "BlogCoreDb" - }, - "Startup": { - "Domain": "http://localhost:9291", - "Cors": { - "PolicyName": "CorsIpAccess", //策略名称 - "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 - // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 - // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 - "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" + "Mongo": { + "ConnectionString": "mongodb://nosql.data", + "Database": "BlogCoreDb" }, - "AppConfigAlert": { - "Enabled": true + "Startup": { + "Domain": "http://localhost:9291", + "Cors": { + "PolicyName": "CorsIpAccess", //策略名称 + "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 + // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 + // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 + "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" + }, + "AppConfigAlert": { + "Enabled": true + }, + "ApiName": "Blog.Core", + "IdentityServer4": { + "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 + "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 + "ApiName": "blog.core.api" // 资源服务器 + }, + "Authing": { + "Enabled": false, + "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", + "Audience": "63d51c4205c2849803be5178", + "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" + }, + "RedisMq": { + "Enabled": false //redis 消息队列 + }, + "MiniProfiler": { + "Enabled": false //性能分析开启 + }, + "Nacos": { + "Enabled": false //Nacos注册中心 + } }, - "ApiName": "Blog.Core", - "IdentityServer4": { - "Enabled": false, // 这里默认是false,表示使用jwt,如果设置为true,则表示系统使用Ids4模式 - "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 - "ApiName": "blog.core.api" // 资源服务器 + "Middleware": { + "RequestResponseLog": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": false + } + }, + "IPLog": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": false + } + }, + "RecordAccessLogs": { + "Enabled": true, + "LogToFile": { + "Enabled": false + }, + "LogToDB": { + "Enabled": false + }, + "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," + }, + "SignalR": { + "Enabled": false + }, + "SignalRSendLog": { + "Enabled": false + }, + "QuartzNetJob": { + "Enabled": true + }, + "Consul": { + "Enabled": false + }, + "IpRateLimit": { + "Enabled": true + } }, - "Authing": { - "Enabled": false, - "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", - "Audience": "63d51c4205c2849803be5178", - "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" - }, - "RedisMq": { - "Enabled": false //redis 消息队列 - }, - "MiniProfiler": { - "Enabled": false //性能分析开启 - }, - "Nacos": { - "Enabled": false //Nacos注册中心 - } - }, - "Middleware": { - "RequestResponseLog": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } - }, - "IPLog": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - } - }, - "RecordAccessLogs": { - "Enabled": true, - "LogToFile": { - "Enabled": false - }, - "LogToDB": { - "Enabled": true - }, - "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each + "StackBlockedRequests": false, //False: Number of rejections should be recorded on another counter + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "IpWhitelist": [], //白名单 + "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], + "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], + "QuotaExceededResponse": { + "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + "HttpStatusCode": 429, //返回状态码 + "GeneralRules": [ //api规则,结尾一定要带* + { + "Endpoint": "*:/api/blog*", + "Period": "1m", + "Limit": 20 + }, + { + "Endpoint": "*/api/*", + "Period": "1s", + "Limit": 3 + }, + { + "Endpoint": "*/api/*", + "Period": "1m", + "Limit": 30 + }, + { + "Endpoint": "*/api/*", + "Period": "12h", + "Limit": 500 + } + ] + }, - "SignalR": { - "Enabled": false + "ConsulSetting": { + "ServiceName": "BlogCoreService", + "ServiceIP": "localhost", + "ServicePort": "9291", + "ServiceHealthCheck": "/healthcheck", + "ConsulAddress": "http://localhost:8500" }, - "SignalRSendLog": { - "Enabled": false + "PayInfo": { //建行聚合支付信息 + "MERCHANTID": "", //商户号 + "POSID": "", //柜台号 + "BRANCHID": "", //分行号 + "pubKey": "", //公钥 + "USER_ID": "", //操作员号 + "PASSWORD": "", //密码 + "OutAddress": "http://127.0.0.1:12345" //外联地址 }, - "QuartzNetJob": { - "Enabled": true + "nacos": { + "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 + "DefaultTimeOut": 15000, // 默认超时时间 + "Namespace": "public", // 命名空间 + "ListenInterval": 10000, // 监听的频率 + "ServiceName": "blog.Core.Api", // 服务名 + "Port": "9291", // 服务端口号 + "RegisterEnabled": true // 是否直接注册nacos }, - "Consul": { - "Enabled": false + "LogFiedOutPutConfigs": { + "tcpAddressHost": "", // 输出elk的tcp连接地址 + "tcpAddressPort": 0, // 输出elk的tcp端口号 + "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 + { + "FiedName": "applicationName", + "FiedValue": "Blog.Core.Api" + } + ] }, - "IpRateLimit": { - "Enabled": true + "trojan": { //科学上网订阅 + "normalApi": "https://apiurl/api/Trojan/RSS?id=", + "clashApi": "https://clashurl/sub?target=clash&insert=false&url=", + "clashApiBackup": "https://clashurl/sub?target=clash&insert=false&url=" } - }, - "IpRateLimiting": { - "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each - "StackBlockedRequests": false, //False: Number of rejections should be recorded on another counter - "RealIpHeader": "X-Real-IP", - "ClientIdHeader": "X-ClientId", - "IpWhitelist": [], //白名单 - "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], - "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], - "QuotaExceededResponse": { - "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", - "ContentType": "application/json", - "StatusCode": 429 - }, - "HttpStatusCode": 429, //返回状态码 - "GeneralRules": [ //api规则,结尾一定要带* - { - "Endpoint": "*:/api/blog*", - "Period": "1m", - "Limit": 20 - }, - { - "Endpoint": "*/api/*", - "Period": "1s", - "Limit": 3 - }, - { - "Endpoint": "*/api/*", - "Period": "1m", - "Limit": 30 - }, - { - "Endpoint": "*/api/*", - "Period": "12h", - "Limit": 500 - } - ] - - }, - "ConsulSetting": { - "ServiceName": "BlogCoreService", - "ServiceIP": "localhost", - "ServicePort": "9291", - "ServiceHealthCheck": "/healthcheck", - "ConsulAddress": "http://localhost:8500" - }, - "PayInfo": { //建行聚合支付信息 - "MERCHANTID": "", //商户号 - "POSID": "", //柜台号 - "BRANCHID": "", //分行号 - "pubKey": "", //公钥 - "USER_ID": "", //操作员号 - "PASSWORD": "", //密码 - "OutAddress": "http://127.0.0.1:12345" //外联地址 - }, - "nacos": { - "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 - "DefaultTimeOut": 15000, // 默认超时时间 - "Namespace": "public", // 命名空间 - "ListenInterval": 10000, // 监听的频率 - "ServiceName": "blog.Core.Api", // 服务名 - "Port": "9291", // 服务端口号 - "RegisterEnabled": true // 是否直接注册nacos - }, - "LogFiedOutPutConfigs": { - "tcpAddressHost": "", // 输出elk的tcp连接地址 - "tcpAddressPort": 0, // 输出elk的tcp端口号 - "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 - { - "FiedName": "applicationName", - "FiedValue": "Blog.Core.Api" - } - ] - } } diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.excel/SysUserInfo.xlsx b/Blog.Core.Api/wwwroot/BlogCore.Data.excel/SysUserInfo.xlsx index 51c0e125ec7a2b31b7d3e932bee4a0cde662b4d0..6a1e4ca6b01b92e1ab3095a7090a48ed41119a21 100644 GIT binary patch delta 4254 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 delta 4220 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 InternalApp.RootServices ; + + /// + /// 获取请求上下文 + /// + public static HttpContext HttpContext => RootServices?.GetService()?.HttpContext; + + public static IUser User => HttpContext == null ? null : RootServices?.GetService(); +} \ No newline at end of file diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index 43f65ab9..fc0abb9d 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -43,4 +43,8 @@ + + + + diff --git a/Blog.Core.Common/Core/InternalApp.cs b/Blog.Core.Common/Core/InternalApp.cs new file mode 100644 index 00000000..c1ae8dcd --- /dev/null +++ b/Blog.Core.Common/Core/InternalApp.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Builder; +using System; + +namespace Blog.Core.Common.Core; + +public static class InternalApp +{ + /// 根服务 + public static IServiceProvider RootServices; + + public static void ConfigureApplication(this WebApplication app) + { + app.Lifetime.ApplicationStarted.Register(() => { InternalApp.RootServices = app.Services; }); + + app.Lifetime.ApplicationStopped.Register(() => { InternalApp.RootServices = null; }); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs new file mode 100644 index 00000000..9ab494a4 --- /dev/null +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -0,0 +1,130 @@ +using Blog.Core.Model; +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; +using SqlSugar; +using System; + +namespace Blog.Core.Common.DB.Aop; + +public static class SqlSugarAop +{ + public static void DataExecuting(object oldValue, DataFilterModel entityInfo) + { + if (entityInfo.EntityValue is RootEntityTkey rootEntity) + { + if (rootEntity.Id == 0) + { + rootEntity.Id = SnowFlakeSingle.Instance.NextId(); + } + } + + if (entityInfo.EntityValue is BaseEntity baseEntity) + { + // 新增操作 + if (entityInfo.OperationType == DataFilterType.InsertByObject) + { + if (baseEntity.CreateTime == DateTime.MinValue) + { + baseEntity.CreateTime = DateTime.Now; + } + } + + if (entityInfo.OperationType == DataFilterType.UpdateByObject) + { + baseEntity.ModifyTime = DateTime.Now; + } + + + if (App.User?.ID > 0) + { + if (baseEntity is ITenantEntity tenant && App.User.TenantId > 0) + { + if (tenant.TenantId == 0) + { + tenant.TenantId = App.User.TenantId; + } + } + + switch (entityInfo.OperationType) + { + case DataFilterType.UpdateByObject: + baseEntity.ModifyId = App.User.ID; + baseEntity.ModifyBy = App.User.Name; + break; + case DataFilterType.InsertByObject: + if (baseEntity.CreateBy.IsNullOrEmpty() || baseEntity.CreateId is null or <= 0) + { + baseEntity.CreateId = App.User.ID; + baseEntity.CreateBy = App.User.Name; + } + + break; + } + } + } + else + { + //兼容以前的表 + //这里要小心 在AOP里用反射 数据量多性能就会有问题 + //要么都统一使用基类 + //要么考虑老的表没必要兼容老的表 + // + + var getType = entityInfo.EntityValue.GetType(); + + switch (entityInfo.OperationType) + { + case DataFilterType.InsertByObject: + var dyCreateBy = getType.GetProperty("CreateBy"); + var dyCreateId = getType.GetProperty("CreateId"); + var dyCreateTime = getType.GetProperty("CreateTime"); + + if (App.User?.ID > 0 && dyCreateBy != null && dyCreateBy.GetValue(entityInfo.EntityValue) == null) + dyCreateBy.SetValue(entityInfo.EntityValue, App.User.Name); + + if (App.User?.ID > 0 && dyCreateId != null && dyCreateId.GetValue(entityInfo.EntityValue) == null) + dyCreateId.SetValue(entityInfo.EntityValue, App.User.ID); + + if (dyCreateTime != null && (DateTime)dyCreateTime.GetValue(entityInfo.EntityValue) == DateTime.MinValue) + dyCreateTime.SetValue(entityInfo.EntityValue, DateTime.Now); + + break; + case DataFilterType.UpdateByObject: + var dyModifyBy = getType.GetProperty("ModifyBy"); + var dyModifyId = getType.GetProperty("ModifyId"); + var dyModifyTime = getType.GetProperty("ModifyTime"); + + if (App.User?.ID > 0 && dyModifyBy != null) + dyModifyBy.SetValue(entityInfo.EntityValue, App.User.Name); + + if (App.User?.ID > 0 && dyModifyId != null) + dyModifyId.SetValue(entityInfo.EntityValue, App.User.ID); + + if (dyModifyTime != null) + dyModifyTime.SetValue(entityInfo.EntityValue, DateTime.Now); + break; + } + } + } + + private static string GetWholeSql(SugarParameter[] paramArr, string sql) + { + foreach (var param in paramArr) + { + sql = sql.Replace(param.ParameterName, $@"'{param.Value.ObjToString()}'"); + } + + return sql; + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index dc7fee4e..1d86369a 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -1,4 +1,5 @@ -using System; +using SqlSugar; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,6 +12,7 @@ public class BaseDBConfig * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); + private static string DifDBConnOfSecurity(params string[] conn) { foreach (var item in conn) @@ -22,7 +24,9 @@ private static string DifDBConnOfSecurity(params string[] conn) return File.ReadAllText(item).Trim(); } } - catch (System.Exception) { } + catch (System.Exception) + { + } } return conn[conn.Length - 1]; @@ -37,8 +41,9 @@ public static (List, List) MutiInitConn() { SpecialDbString(i); } - List listdatabaseSimpleDB = new List();//单库 - List listdatabaseSlaveDB = new List();//从库 + + List listdatabaseSimpleDB = new List(); //单库 + List listdatabaseSlaveDB = new List(); //从库 // 单库,且不开启读写分离,只保留一个 if (!AppSettings.app(new string[] { "CQRSEnabled" }).ObjToBool() && !AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) @@ -54,6 +59,7 @@ public static (List, List) MutiInitConn() { dbFirst = listdatabase.FirstOrDefault(); } + listdatabaseSimpleDB.Add(dbFirst); return (listdatabaseSimpleDB, listdatabaseSlaveDB); } @@ -70,7 +76,6 @@ public static (List, List) MutiInitConn() } - return (listdatabase, listdatabaseSlaveDB); //} } @@ -102,6 +107,8 @@ private static MutiDBOperate SpecialDbString(MutiDBOperate mutiDBOperate) return mutiDBOperate; } + + } @@ -115,24 +122,29 @@ public enum DataBaseType Dm = 5, Kdbndp = 6, } + public class MutiDBOperate { /// /// 连接启用开关 /// public bool Enabled { get; set; } + /// /// 连接ID /// public string ConnId { get; set; } + /// /// 从库执行级别,越大越先执行 /// public int HitRate { get; set; } + /// /// 连接字符串 /// public string Connection { get; set; } + /// /// 数据库类型 /// diff --git a/Blog.Core.Common/DB/RepositorySetting.cs b/Blog.Core.Common/DB/RepositorySetting.cs new file mode 100644 index 00000000..bfca7174 --- /dev/null +++ b/Blog.Core.Common/DB/RepositorySetting.cs @@ -0,0 +1,48 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Models.RootTkey.Interface; +using Blog.Core.Model.Tenants; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Blog.Core.Common.DB; + +public class RepositorySetting +{ + private static readonly Lazy> AllEntitys = new(() => + { + return typeof(BaseEntity).Assembly + .GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(BaseEntity))) + .Where(it => it.FullName != null && it.FullName.StartsWith("Blog.Core.Model.Models")); + }); + + public static IEnumerable Entitys => AllEntitys.Value; + + /// + /// 配置实体软删除过滤器
+ /// 统一过滤 软删除 无需自己写条件 + ///
+ public static void SetDeletedEntityFilter(SqlSugarScopeProvider db) + { + db.QueryFilter.AddTableFilter(it => it.IsDeleted == false); + } + + /// + /// 配置租户 + /// + public static void SetTenantEntityFilter(SqlSugarScopeProvider db) + { + if (App.User is not { ID: > 0, TenantId: > 0 }) + { + return; + } + + //多租户 单表 + db.QueryFilter.AddTableFilter(it => it.TenantId == App.User.TenantId || it.TenantId == 0); + + //多租户 多表 + db.SetTenantTable(App.User.TenantId.ToString()); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/TenantUtil.cs b/Blog.Core.Common/DB/TenantUtil.cs new file mode 100644 index 00000000..8d57189b --- /dev/null +++ b/Blog.Core.Common/DB/TenantUtil.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; +using SqlSugar; + +namespace Blog.Core.Common.DB; + +public static class TenantUtil +{ + public static SysTenant DefaultTenantConfig(this SysTenant tenant) + { + tenant.DbType ??= DbType.Sqlite; + + //如果没有配置连接 + if (tenant.Connection.IsNullOrEmpty()) + { + //此处默认配置 Sqlite 地址 + //实际业务中 也会有运维、系统管理员等来维护 + switch (tenant.DbType.Value) + { + case DbType.Sqlite: + tenant.Connection = $"DataSource={Path.Combine(Environment.CurrentDirectory, tenant.ConfigId)}.db"; + break; + } + } + + return tenant; + } + + public static ConnectionConfig GetConnectionConfig(this SysTenant tenant) + { + if (tenant.DbType is null) + { + throw new ArgumentException("Tenant DbType Must"); + } + + + return new ConnectionConfig() + { + ConfigId = tenant.ConfigId, + DbType = tenant.DbType.Value, + ConnectionString = tenant.Connection, + IsAutoCloseConnection = true, + MoreSettings = new ConnMoreSettings() + { + IsAutoRemoveDataCache = true + }, + }; + } + + public static List GetTenantEntityTypes(TenantTypeEnum? tenantType = null) + { + return RepositorySetting.Entitys + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(s => IsTenantEntity(s, tenantType)) + .ToList(); + } + + public static bool IsTenantEntity(this Type u, TenantTypeEnum? tenantType = null) + { + var mta = u.GetCustomAttribute(); + if (mta is null) + { + return false; + } + + if (tenantType != null) + { + if (mta.TenantType != tenantType) + { + return false; + } + } + + return true; + } + + public static string GetTenantTableName(this Type type, ISqlSugarClient db, string id) + { + var entityInfo = db.EntityMaintenance.GetEntityInfo(type); + return $@"{entityInfo.DbTableName}_{id}"; + } + + public static string GetTenantTableName(this Type type, ISqlSugarClient db, SysTenant tenant) + { + return GetTenantTableName(type, db, tenant.Id.ToString()); + } + + public static void SetTenantTable(this ISqlSugarClient db, string id) + { + var types = GetTenantEntityTypes(TenantTypeEnum.Tables); + + foreach (var type in types) + { + db.MappingTables.Add(type.Name, type.GetTenantTableName(db, id)); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/AssemblysExtensions.cs b/Blog.Core.Common/Extensions/AssemblysExtensions.cs new file mode 100644 index 00000000..5e5d4349 --- /dev/null +++ b/Blog.Core.Common/Extensions/AssemblysExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyModel; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace Blog.Core.Common.Extensions; + +public static class AssemblysExtensions +{ + public static List GetAllAssemblies() + { + var list = new List(); + var deps = DependencyContext.Default; + var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package" ); + foreach (var lib in libs) + { + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); + list.Add(assembly); + } + + return list; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions.cs b/Blog.Core.Common/Extensions/ExpressionExtensions.cs index 7f9e69e5..4058b95a 100644 --- a/Blog.Core.Common/Extensions/ExpressionExtensions.cs +++ b/Blog.Core.Common/Extensions/ExpressionExtensions.cs @@ -151,6 +151,16 @@ public static Expression Contains(this Expression left, Expression right) return left.Call("Contains", right); } + public static Expression StartContains(this Expression left, Expression right) + { + return left.Call("StartsWith", right); + } + + public static Expression EndContains(this Expression left, Expression right) + { + return left.Call("EndsWith", right); + } + /// /// > /// @@ -201,5 +211,4 @@ public static Expression NotEqual(this Expression left, Expression right) #endregion } - -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/GenericTypeExtensions.cs b/Blog.Core.Common/Extensions/GenericTypeExtensions.cs index 368b6676..6c067773 100644 --- a/Blog.Core.Common/Extensions/GenericTypeExtensions.cs +++ b/Blog.Core.Common/Extensions/GenericTypeExtensions.cs @@ -26,5 +26,31 @@ public static string GetGenericTypeName(this object @object) { return @object.GetType().GetGenericTypeName(); } + + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + // public static bool HasImplementedRawGeneric(this Type type, Type generic) + // { + // // 检查接口类型 + // var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + // if (isTheRawGenericType) return true; + + // // 检查类型 + // while (type != null && type != typeof(object)) + // { + // isTheRawGenericType = IsTheRawGenericType(type); + // if (isTheRawGenericType) return true; + // type = type.BaseType; + // } + + // return false; + + // // 判断逻辑 + // bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); + // } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/UntilExtensions.cs b/Blog.Core.Common/Extensions/UntilExtensions.cs new file mode 100644 index 00000000..1ae503b2 --- /dev/null +++ b/Blog.Core.Common/Extensions/UntilExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Blog.Core.Common.Extensions; + +public static class UntilExtensions +{ + public static void AddOrModify(this IDictionary dic, TKey key, TValue value) + { + if (dic.TryGetValue(key, out _)) + { + dic[key] = value; + } + else + { + dic.Add(key, value); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/DynamicLinqFactory.cs b/Blog.Core.Common/Helper/DynamicLinqFactory.cs index 43fdd7e3..7d45bbd2 100644 --- a/Blog.Core.Common/Helper/DynamicLinqFactory.cs +++ b/Blog.Core.Common/Helper/DynamicLinqFactory.cs @@ -1,15 +1,11 @@ -using Microsoft.AspNetCore.Http; -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; -using System.Net.Http; -using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Blog.Core.Common.Helper { @@ -20,10 +16,10 @@ namespace Blog.Core.Common.Helper /// public static class DynamicLinqFactory { - private static readonly Dictionary _operatingSystems = new(); + private static readonly Dictionary _operatingSystems = new Dictionary(); public static Dictionary OperatingSystems => GetOperationSymbol(); - private static readonly Dictionary _linkSymbols = new(); + private static readonly Dictionary _linkSymbols = new Dictionary(); public static Dictionary LinkSymbols => GetLinkSymbol(); /// @@ -70,11 +66,16 @@ public static Expression ExpressionStudio(Expression left, DynamicLinqHelper Dyn var properties = DynamicLinq.Left.Split('.'); - // 从1开始,是不想用自定义种子,外层种子已经定义好了 - // 暂时也不会有多个自定义种子,先这样 - for (var i = 0; i < properties.Length; i++) + int index = 0; + foreach (var t in properties) { - mainExpression = mainExpression.Property(properties[i]); + if (mainExpression.Type.HasImplementedRawGeneric(typeof(IEnumerable<>))) + { + return ExpressionStudioEnumerable(left, mainExpression, DynamicLinq.Clone(), properties.Skip(index).ToArray()); + } + + mainExpression = mainExpression.Property(t); + index++; } left = left == null @@ -85,6 +86,32 @@ public static Expression ExpressionStudio(Expression left, DynamicLinqHelper Dyn return left; } + public static Expression ExpressionStudioEnumerable(Expression left, Expression property, DynamicLinqHelper dynamicLinq, string[] properties) + { + var realType = property.Type.GenericTypeArguments[0]; + + var parameter = Expression.Parameter(realType, "z"); + Expression mainExpression = property; + if (!properties.Any()) + { + throw new ApplicationException("条件表达式错误,属性为集合时,需要明确具体属性"); + } + + dynamicLinq.Left = string.Join(".", properties); + mainExpression = ExpressionStudio(null, dynamicLinq, parameter); + + var lambda = Expression.Lambda(mainExpression, parameter); + + mainExpression = Expression.Call(typeof(Enumerable), "Any", new[] { realType }, property, lambda); + + left = left == null + ? mainExpression + : ChangeLinkSymbol(dynamicLinq.LinkSymbol, left, mainExpression); + + return left; + } + + /// /// 将字符串装换成动态帮助类(内含递归) /// @@ -132,6 +159,7 @@ public static List SplitOperationSymbol(string str) { var outList = new List(); var tokens = Regex.Matches(FormatString(str), _pattern, RegexOptions.Compiled) + .Cast() .Select(m => m.Groups[1].Value.Trim()) .ToList(); @@ -139,7 +167,7 @@ public static List SplitOperationSymbol(string str) int lastOperatingSymbolIndex = -1; for (int i = tokens.Count - 1; i >= 0; i--) { - var token = tokens[i]; + var token = tokens[i].ToLower(); if (OperatingSystems.ContainsKey(token)) { @@ -189,6 +217,7 @@ public static List SplitOperationSymbol(string str) } } + outList.Reverse(); return outList; } @@ -259,7 +288,7 @@ public static Dictionary GetOperationSymbol() { foreach (var name in attr.Name.Split(';')) { - _operatingSystems.Add(name, (OperationSymbol)item.GetValue(null)); + _operatingSystems.Add(name.ToLower(), (OperationSymbol)item.GetValue(null)); } } } @@ -353,7 +382,14 @@ public static string FormatString(string str) public static readonly string _pattern = @"\s*(" + string.Join("|", new string[] { // operators and punctuation that are longer than one char: longest first - string.Join("|", new[] { "||", "&&", "==", "!=", "<=", ">=", "like", "contains" }.Select(Regex.Escape)), + string.Join("|", new[] + { + "||", "&&", "==", "!=", "<=", ">=", + "in", + "like", "contains", "%=", + "startslike", "startscontains", "%>", + "endlike", "endcontains", "%<", + }.Select(Regex.Escape)), @"""(?:\\.|[^""])*""", // string @"\d+(?:\.\d+)?", // number with optional decimal part @"\w+", // word @@ -365,7 +401,7 @@ public static string FormatString(string str) /// public static OperationSymbol ChangeOperationSymbol(string str) { - switch (str) + switch (str.ToLower()) { case "<": return OperationSymbol.LessThan; @@ -382,7 +418,16 @@ public static OperationSymbol ChangeOperationSymbol(string str) return OperationSymbol.NotEqual; case "contains": case "like": + case "%=": return OperationSymbol.Contains; + case "startslike": + case "startscontains": + case "%>": + return OperationSymbol.StartsContains; + case "endlike": + case "endcontains": + case "%<": + return OperationSymbol.EndContains; } throw new Exception("OperationSymbol IS NULL"); @@ -451,6 +496,10 @@ public static Expression ChangeOperationSymbol(OperationSymbol symbol, Expressio return key.NotEqual(Expression.Constant(newTypeRight)); case OperationSymbol.Contains: return key.Contains(Expression.Constant(newTypeRight)); + case OperationSymbol.StartsContains: + return key.StartContains(Expression.Constant(newTypeRight)); + case OperationSymbol.EndContains: + return key.EndContains(Expression.Constant(newTypeRight)); case OperationSymbol.In: var contains = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) .Single(x => x.Name == "Contains" && x.GetParameters().Length == 2) @@ -480,6 +529,17 @@ public class DynamicLinqHelper [Display(Name = "连接符")] public LinkSymbol LinkSymbol { get; set; } + + public DynamicLinqHelper Clone() + { + return new DynamicLinqHelper() + { + Left = this.Left, + Right = this.Right, + OperationSymbol = this.OperationSymbol, + LinkSymbol = this.LinkSymbol, + }; + } } /// @@ -504,9 +564,15 @@ public enum OperationSymbol [Display(Name = "in")] In, - [Display(Name = "like;contains")] + [Display(Name = "like;contains;%=")] Contains, + [Display(Name = "StartsLike;StartsContains;%>")] + StartsContains, + + [Display(Name = "EndLike;EndContains;%<")] + EndContains, + [Display(Name = ">")] GreaterThan, @@ -528,7 +594,7 @@ public enum OperationSymbol #endregion - + /// /// Queryable扩展 /// diff --git a/Blog.Core.Common/Helper/GenericTypeExtensions.cs b/Blog.Core.Common/Helper/GenericTypeExtensions.cs new file mode 100644 index 00000000..aa095e2a --- /dev/null +++ b/Blog.Core.Common/Helper/GenericTypeExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; + +namespace Blog.Core.Common.Helper +{ + public static class GenericTypeExtensions + { + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + public static bool HasImplementedRawGeneric(this Type type, Type generic) + { + // 检查接口类型 + var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + if (isTheRawGenericType) return true; + + // 检查类型 + while (type != null && type != typeof(object)) + { + isTheRawGenericType = IsTheRawGenericType(type); + if (isTheRawGenericType) return true; + type = type.BaseType; + } + + return false; + + // 判断逻辑 + bool IsTheRawGenericType(Type t) => generic == (t.IsGenericType ? t.GetGenericTypeDefinition() : t); + } + + public static string GetGenericTypeName(this Type type) + { + var typeName = string.Empty; + + if (type.IsGenericType) + { + var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); + typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; + } + else + { + typeName = type.Name; + } + + return typeName; + } + + public static string GetGenericTypeName(this object @object) + { + return @object.GetType().GetGenericTypeName(); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/NumberConverter.cs b/Blog.Core.Common/Helper/NumberConverter.cs new file mode 100644 index 00000000..27890faf --- /dev/null +++ b/Blog.Core.Common/Helper/NumberConverter.cs @@ -0,0 +1,174 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Helper +{ + /// + /// + /// 大数据json序列化重写 + /// + public sealed class NumberConverter : JsonConverter + { + /// + /// 转换成字符串的类型 + /// + private readonly NumberConverterShip _ship; + + /// + /// 大数据json序列化重写实例化 + /// + public NumberConverter() + { + _ship = (NumberConverterShip)0xFF; + } + + /// + /// 大数据json序列化重写实例化 + /// + /// 转换成字符串的类型 + public NumberConverter(NumberConverterShip ship) + { + _ship = ship; + } + + /// + /// + /// 确定此实例是否可以转换指定的对象类型。 + /// + /// 对象的类型。 + /// 如果此实例可以转换指定的对象类型,则为:true,否则为:false + public override bool CanConvert(Type objectType) + { + var typecode = Type.GetTypeCode(objectType.Name.Equals("Nullable`1") ? objectType.GetGenericArguments().First() : objectType); + switch (typecode) + { + case TypeCode.Decimal: + return (_ship & NumberConverterShip.Decimal) == NumberConverterShip.Decimal; + case TypeCode.Double: + return (_ship & NumberConverterShip.Double) == NumberConverterShip.Double; + case TypeCode.Int64: + return (_ship & NumberConverterShip.Int64) == NumberConverterShip.Int64; + case TypeCode.UInt64: + return (_ship & NumberConverterShip.UInt64) == NumberConverterShip.UInt64; + case TypeCode.Single: + return (_ship & NumberConverterShip.Single) == NumberConverterShip.Single; + default: return false; + } + } + + /// + /// + /// 读取对象的JSON表示。 + /// + /// 中读取。 + /// 对象的类型。 + /// 正在读取的对象的现有值。 + /// 调用的序列化器实例。 + /// 对象值。 + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return AsType(reader.Value.ToString(), objectType); + } + + /// + /// 字符串格式数据转其他类型数据 + /// + /// 输入的字符串 + /// 目标格式 + /// 转换结果 + public static object AsType(string input, Type destinationType) + { + try + { + var converter = TypeDescriptor.GetConverter(destinationType); + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(null, null, input); + } + + converter = TypeDescriptor.GetConverter(typeof(string)); + if (converter.CanConvertTo(destinationType)) + { + return converter.ConvertTo(null, null, input, destinationType); + } + } + catch + { + return null; + } + return null; + } + + /// + /// + /// 写入对象的JSON表示形式。 + /// + /// 要写入的 。 + /// 要写入对象值 + /// 调用的序列化器实例。 + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else + { + var objectType = value.GetType(); + var typeCode = Type.GetTypeCode(objectType.Name.Equals("Nullable`1") ? objectType.GetGenericArguments().First() : objectType); + switch (typeCode) + { + case TypeCode.Decimal: + writer.WriteValue(((decimal)value).ToString("f6")); + break; + case TypeCode.Double: + writer.WriteValue(((double)value).ToString("f4")); + break; + case TypeCode.Single: + writer.WriteValue(((float)value).ToString("f2")); + break; + default: + writer.WriteValue(value.ToString()); + break; + } + } + } + } + + /// + /// 转换成字符串的类型 + /// + [Flags] + public enum NumberConverterShip + { + /// + /// 长整数 + /// + Int64 = 1, + + /// + /// 无符号长整数 + /// + UInt64 = 2, + + /// + /// 浮点数 + /// + Single = 4, + + /// + /// 双精度浮点数 + /// + Double = 8, + + /// + /// 大数字 + /// + Decimal =16 + } +} diff --git a/Blog.Core.Common/Helper/RecursionHelper.cs b/Blog.Core.Common/Helper/RecursionHelper.cs index 9b27a37d..092a0678 100644 --- a/Blog.Core.Common/Helper/RecursionHelper.cs +++ b/Blog.Core.Common/Helper/RecursionHelper.cs @@ -8,7 +8,7 @@ namespace Blog.Core.Common.Helper /// public static class RecursionHelper { - public static void LoopToAppendChildren(List all, PermissionTree curItem, int pid, bool needbtn) + public static void LoopToAppendChildren(List all, PermissionTree curItem, long pid, bool needbtn) { var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); @@ -52,7 +52,7 @@ public static void LoopToAppendChildren(List all, PermissionTree LoopToAppendChildren(all, subItem, pid, needbtn); } } - public static void LoopToAppendChildren(List all, DepartmentTree curItem, int pid) + public static void LoopToAppendChildren(List all, DepartmentTree curItem, long pid) { var subItems = all.Where(ee => ee.Pid == curItem.value).ToList(); @@ -117,8 +117,8 @@ public static void LoopToAppendChildrenT(List all, T curItem, string paren public class PermissionTree { - public int value { get; set; } - public int Pid { get; set; } + public long value { get; set; } + public long Pid { get; set; } public string label { get; set; } public int order { get; set; } public bool isbtn { get; set; } @@ -139,8 +139,8 @@ public class DepartmentTree public class NavigationBar { - public int id { get; set; } - public int pid { get; set; } + public long id { get; set; } + public long pid { get; set; } public int order { get; set; } public string name { get; set; } public bool IsHide { get; set; } = false; @@ -165,8 +165,8 @@ public class NavigationBarMeta public class NavigationBarPro { - public int id { get; set; } - public int parentId { get; set; } + public long id { get; set; } + public long parentId { get; set; } public int order { get; set; } public string name { get; set; } public bool IsHide { get; set; } = false; diff --git a/Blog.Core.Common/Helper/SM/SM4.cs b/Blog.Core.Common/Helper/SM/SM4.cs index 4b1f1996..b4aa3ebf 100644 --- a/Blog.Core.Common/Helper/SM/SM4.cs +++ b/Blog.Core.Common/Helper/SM/SM4.cs @@ -10,7 +10,9 @@ public class SM4 private long GET_ULONG_BE(SByte[] b, int i) { +#pragma warning disable CS0675 // 对进行了带符号扩展的操作数使用了按位或运算符 long n2 = (b[i] & 0xFF) << 24 | (b[(i + 1)] & 0xFF) << 16 | (b[(i + 2)] & 0xFF) << 8 | b[(i + 3)] & 0xFF & 0xFFFFFFFF; +#pragma warning restore CS0675 // 对进行了带符号扩展的操作数使用了按位或运算符 return n2; } @@ -207,7 +209,6 @@ public SByte[] sm4_crypt_ecb(SM4_Context ctx, SByte[] input) int length = input.Length; SByte[] bins = new SByte[length]; SByte[] bous = new SByte[length]; - SByte[] output = null; Array.Copy(input, 0, bins, 0, length); diff --git a/Blog.Core.Common/Helper/UtilConvert.cs b/Blog.Core.Common/Helper/UtilConvert.cs index 282989e3..ea3b7c05 100644 --- a/Blog.Core.Common/Helper/UtilConvert.cs +++ b/Blog.Core.Common/Helper/UtilConvert.cs @@ -43,6 +43,18 @@ public static int ObjToInt(this object thisValue, int errorValue) return errorValue; } + public static long ObjToLong(this object thisValue) + { + long reval = 0; + if (thisValue == null) return 0; + if (thisValue != DBNull.Value && long.TryParse(thisValue.ToString(), out reval)) + { + return reval; + } + + return reval; + } + /// /// /// diff --git a/Blog.Core.Common/HttpContextUser/AspNetUser.cs b/Blog.Core.Common/HttpContextUser/AspNetUser.cs index e4e27d87..b37e4a24 100644 --- a/Blog.Core.Common/HttpContextUser/AspNetUser.cs +++ b/Blog.Core.Common/HttpContextUser/AspNetUser.cs @@ -5,6 +5,7 @@ using Blog.Core.Model; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using SqlSugar.Extensions; namespace Blog.Core.Common.HttpContextUser { @@ -40,6 +41,7 @@ private string GetName() } public int ID => GetClaimValueByType("jti").FirstOrDefault().ObjToInt(); + public long TenantId => GetClaimValueByType("TenantId").FirstOrDefault().ObjToLong(); public bool IsAuthenticated() { @@ -87,11 +89,9 @@ public IEnumerable GetClaimsIdentity() public List GetClaimValueByType(string ClaimType) { - return (from item in GetClaimsIdentity() where item.Type == ClaimType select item.Value).ToList(); - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/HttpContextUser/IUser.cs b/Blog.Core.Common/HttpContextUser/IUser.cs index beef3b7d..3849bd38 100644 --- a/Blog.Core.Common/HttpContextUser/IUser.cs +++ b/Blog.Core.Common/HttpContextUser/IUser.cs @@ -8,6 +8,7 @@ public interface IUser { string Name { get; } int ID { get; } + long TenantId { get; } bool IsAuthenticated(); IEnumerable GetClaimsIdentity(); List GetClaimValueByType(string ClaimType); @@ -17,4 +18,4 @@ public interface IUser MessageModel MessageModel { get; set; } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index da313e3c..cb6acc59 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -1,8 +1,11 @@ using Blog.Core.Common.DB; +using Blog.Core.Common.Extensions; using Blog.Core.Common.Helper; using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; using Magicodes.ExporterAndImporter.Excel; using Newtonsoft.Json; +using SqlSugar; using System; using System.Collections.Generic; using System.IO; @@ -96,7 +99,9 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) var modelTypes = referencedAssemblies .SelectMany(a => a.DefinedTypes) .Select(type => type.AsType()) - .Where(x => x.IsClass && x.Namespace != null && x.Namespace.Equals("Blog.Core.Model.Models")).ToList(); + .Where(x => x.IsClass && x.Namespace is "Blog.Core.Model.Models") + .Where(s => !s.IsDefined(typeof(MultiTenantAttribute), false)) + .ToList(); modelTypes.ForEach(t => { // 这里只支持添加表,不支持删除 @@ -104,14 +109,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!myContext.Db.DbMaintenance.IsAnyTable(t.Name)) { Console.WriteLine(t.Name); - myContext.Db.CodeFirst.InitTables(t); + myContext.Db.CodeFirst.SplitTables().InitTables(t); } }); ConsoleHelper.WriteSuccessLine($"Tables created successfully!"); Console.WriteLine(); - - if (AppSettings.app(new string[] { "AppSettings", "SeedDBDataEnabled" }).ObjToBool()) { JsonSerializerSettings setting = new JsonSerializerSettings(); @@ -135,6 +138,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) var importer = new ExcelImporter(); #region BlogArticle + if (!await myContext.Db.Queryable().AnyAsync()) { myContext.GetEntityDB().InsertRange(JsonHelper.ParseFormByJson>(FileHelper.ReadFile(string.Format(SeedDataFolder, "BlogArticle"), Encoding.UTF8))); @@ -144,15 +148,14 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:BlogArticle already exists..."); } + #endregion #region Modules + if (!await myContext.Db.Queryable().AnyAsync()) { - - - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Modules"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); @@ -162,31 +165,39 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Modules already exists..."); } + #endregion #region Permission + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Permission"), Encoding.UTF8), setting); - myContext.GetEntityDB().InsertRange(data); + foreach (var item in data) + { + Console.WriteLine($"{item.Name}:{item.Id}"); + myContext.GetEntityDB().Insert(item); + } Console.WriteLine("Table:Permission created success!"); } else { Console.WriteLine("Table:Permission already exists..."); } + #endregion #region Role + if (!await myContext.Db.Queryable().AnyAsync()) { - //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); - using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "Role.xlsx"), FileMode.Open); - var result = await importer.Import(stream); - var data = result.Data.ToList(); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); + //using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "Role.xlsx"), FileMode.Open); + //var result = await importer.Import(stream); + //var data = result.Data.ToList(); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Role created success!"); @@ -195,25 +206,33 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Role already exists..."); } + #endregion #region RoleModulePermission + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "RoleModulePermission"), Encoding.UTF8), setting); - myContext.GetEntityDB().InsertRange(data); + foreach (var item in data) + { + Console.WriteLine($"{item.Id}"); + myContext.GetEntityDB().Insert(item); + } Console.WriteLine("Table:RoleModulePermission created success!"); } else { Console.WriteLine("Table:RoleModulePermission already exists..."); } + #endregion #region Topic + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Topic"), Encoding.UTF8), setting); @@ -225,10 +244,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Topic already exists..."); } + #endregion #region TopicDetail + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TopicDetail"), Encoding.UTF8), setting); @@ -240,16 +261,15 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:TopicDetail already exists..."); } + #endregion #region UserRole + if (!await myContext.Db.Queryable().AnyAsync()) { - //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); - using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "UserRole.xlsx"), FileMode.Open); - var result = await importer.Import(stream); - var data = result.Data.ToList(); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:UserRole created success!"); @@ -258,16 +278,15 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:UserRole already exists..."); } + #endregion #region sysUserInfo + if (!await myContext.Db.Queryable().AnyAsync()) { - //var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); - using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "SysUserInfo.xlsx"), FileMode.Open); - var result = await importer.Import(stream); - var data = result.Data.ToList(); + var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:sysUserInfo created success!"); @@ -276,10 +295,12 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:sysUserInfo already exists..."); } + #endregion #region TasksQz + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TasksQz"), Encoding.UTF8), setting); @@ -291,9 +312,24 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:TasksQz already exists..."); } + + #endregion + + #region TasksLog + + if (!await myContext.Db.Queryable().AnyAsync()) + { + Console.WriteLine("Table:TasksLog created success!"); + } + else + { + Console.WriteLine("Table:TasksLog already exists..."); + } + #endregion #region Department + if (!await myContext.Db.Queryable().AnyAsync()) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Department"), Encoding.UTF8), setting); @@ -305,13 +341,16 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { Console.WriteLine("Table:Department already exists..."); } + #endregion + //种子初始化 + await SeedDataAsync(myContext.Db); + ConsoleHelper.WriteSuccessLine($"Done seeding database!"); } Console.WriteLine(); - } catch (Exception ex) { @@ -321,5 +360,225 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) "3、其他错误:" + ex.Message); } } + + /// + /// 种子初始化数据 + /// + /// + /// + private static async Task SeedDataAsync(ISqlSugarClient db) + { + // 获取所有种子配置-初始化数据 + var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes) + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(u => + { + var esd = u.GetInterfaces().FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); + if (esd is null) + { + return false; + } + + var eType = esd.GenericTypeArguments[0]; + if (eType.GetCustomAttribute() is null) + { + return true; + } + + return false; + }); + + if (!seedDataTypes.Any()) return; + foreach (var seedType in seedDataTypes) + { + dynamic instance = Activator.CreateInstance(seedType); + //初始化数据 + { + var seedData = instance.InitSeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); + + if (!await db.Queryable(entity.DbTableName, "").AnyAsync()) + { + await db.Insertable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} init success!"); + } + } + } + + //种子数据 + { + var seedData = instance.SeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); + + await db.Storageable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} seedData success!"); + } + } + + //自定义处理 + { + await instance.CustomizeSeedData(db); + } + } + } + + + /// + /// 初始化 多租户 + /// + /// + /// + public static async Task TenantSeedAsync(MyContext myContext) + { + var tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Db).ToListAsync(); + if (tenants.Any()) + { + Console.WriteLine($@"Init Multi Tenant Db"); + foreach (var tenant in tenants) + { + Console.WriteLine($@"Init Multi Tenant Db : {tenant.ConfigId}/{tenant.Name}"); + await InitTenantSeedAsync(myContext.Db.AsTenant(), tenant.GetConnectionConfig()); + } + } + + tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Tables).ToListAsync(); + if (tenants.Any()) + { + await InitTenantSeedAsync(myContext, tenants); + } + } + + #region 多租户 多表 初始化 + + private static async Task InitTenantSeedAsync(MyContext myContext, List tenants) + { + ConsoleHelper.WriteInfoLine($"Init Multi Tenant Tables : {myContext.Db.CurrentConnectionConfig.ConfigId}"); + + // 获取所有实体表-初始化租户业务表 + var entityTypes = TenantUtil.GetTenantEntityTypes(TenantTypeEnum.Tables); + if (!entityTypes.Any()) return; + + foreach (var sysTenant in tenants) + { + foreach (var entityType in entityTypes) + { + myContext.Db.CodeFirst + .As(entityType, entityType.GetTenantTableName(myContext.Db, sysTenant)) + .InitTables(entityType); + + Console.WriteLine($@"Init Tables:{entityType.GetTenantTableName(myContext.Db, sysTenant)}"); + } + + myContext.Db.SetTenantTable(sysTenant.Id.ToString()); + //多租户初始化种子数据 + await TenantSeedDataAsync(myContext.Db, TenantTypeEnum.Tables); + } + + ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Tables : {myContext.Db.CurrentConnectionConfig.ConfigId} created successfully!"); + } + + #endregion + + #region 多租户 多库 初始化 + + /// + /// 初始化多库 + /// + /// + /// + /// + public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig config) + { + itenant.RemoveConnection(config.ConfigId); + itenant.AddConnection(config); + + var db = itenant.GetConnectionScope(config.ConfigId); + + db.DbMaintenance.CreateDatabase(); + ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Db : {config.ConfigId} Database created successfully!"); + + Console.WriteLine($@"Init Multi Tenant Db : {config.ConfigId} Create Tables"); + + // 获取所有实体表-初始化租户业务表 + var entityTypes = TenantUtil.GetTenantEntityTypes(TenantTypeEnum.Db); + if (!entityTypes.Any()) return; + foreach (var entityType in entityTypes) + { + var splitTable = entityType.GetCustomAttribute(); + if (splitTable == null) + db.CodeFirst.InitTables(entityType); + else + db.CodeFirst.SplitTables().InitTables(entityType); + + Console.WriteLine(entityType.Name); + } + + //多租户初始化种子数据 + await TenantSeedDataAsync(db, TenantTypeEnum.Db); + } + + #endregion + + private static async Task TenantSeedDataAsync(ISqlSugarClient db, TenantTypeEnum tenantType) + { + // 获取所有种子配置-初始化数据 + var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes) + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) + .Where(u => + { + var esd = u.GetInterfaces().FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); + if (esd is null) + { + return false; + } + + var eType = esd.GenericTypeArguments[0]; + return eType.IsTenantEntity(tenantType); + }); + if (!seedDataTypes.Any()) return; + foreach (var seedType in seedDataTypes) + { + dynamic instance = Activator.CreateInstance(seedType); + //初始化数据 + { + var seedData = instance.InitSeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); + + if (!await db.Queryable(entity.DbTableName, "").AnyAsync()) + { + await db.Insertable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} init success!"); + } + } + } + + //种子数据 + { + var seedData = instance.SeedData(); + if (seedData != null && Enumerable.Any(seedData)) + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + var entity = db.EntityMaintenance.GetEntityInfo(entityType); + + await db.Storageable(Enumerable.ToList(seedData)).ExecuteCommandAsync(); + Console.WriteLine($"Table:{entity.DbTableName} seedData success!"); + } + } + + //自定义处理 + { + await instance.CustomizeSeedData(db); + } + } + } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/IEntitySeedData.cs b/Blog.Core.Common/Seed/IEntitySeedData.cs new file mode 100644 index 00000000..3e2f4859 --- /dev/null +++ b/Blog.Core.Common/Seed/IEntitySeedData.cs @@ -0,0 +1,36 @@ +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed; + +/// +/// 种子数据 接口 +/// +/// +public interface IEntitySeedData + where T : class, new() +{ + /// + /// 初始化种子数据
+ /// 只要表不存在数据,程序启动就会自动初始化 + ///
+ /// + IEnumerable InitSeedData(); + + /// + /// 种子数据
+ /// 存在不操作、不存在Insert
+ /// 适合系统内置数据,项目开发后续增加内置数据 + ///
+ /// + IEnumerable SeedData(); + + /// + /// 自定义操作
+ /// 以上满不足了,可以自己编写 + ///
+ /// + /// + Task CustomizeSeedData(ISqlSugarClient db); +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs new file mode 100644 index 00000000..361cd725 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/BusinessDataSeedData.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Blog.Core.Model.Models; +using SqlSugar; + +namespace Blog.Core.Common.Seed.SeedData; + +/// +/// 初始化 业务数据 +/// +public class BusinessDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new[] + { + new BusinessTable() + { + Id = 1, + TenantId = 1000001, + Name = "张三的数据01", + Amount = 150, + IsDeleted = true, + }, + new BusinessTable() + { + Id = 2, + TenantId = 1000001, + Name = "张三的数据02", + Amount = 200, + }, + new BusinessTable() + { + Id = 3, + TenantId = 1000001, + Name = "张三的数据03", + Amount = 250, + }, + new BusinessTable() + { + Id = 4, + TenantId = 1000002, + Name = "李四的数据01", + Amount = 300, + }, + new BusinessTable() + { + Id = 5, + TenantId = 1000002, + Name = "李四的数据02", + Amount = 500, + }, + new BusinessTable() + { + Id = 6, + TenantId = 0, + Name = "公共数据01", + Amount = 16600, + }, + new BusinessTable() + { + Id = 7, + TenantId = 0, + Name = "公共数据02", + Amount = 19800, + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs new file mode 100644 index 00000000..4ca1a7dd --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/MultiBusinessDataSeedData.cs @@ -0,0 +1,38 @@ +using Blog.Core.Model.Models; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed.SeedData; + +public class MultiBusinessDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new List() + { + new() + { + Id = 1001, + Name = "业务数据1", + Amount = 100, + }, + new() + { + Id = 1002, + Name = "业务数据2", + Amount = 1000, + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs new file mode 100644 index 00000000..e73d4603 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/MultiBusinessSubDataSeedData.cs @@ -0,0 +1,38 @@ +using Blog.Core.Model.Models; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed.SeedData; + +public class MultiBusinessSubDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new List() + { + new() + { + Id = 100, + MainId = 1001, + Memo = "子数据", + }, + new() + { + Id = 1001, + MainId = 1001, + Memo = "子数据2", + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs b/Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs new file mode 100644 index 00000000..be8462e8 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/SubBusinessDataSeedData.cs @@ -0,0 +1,70 @@ +using Blog.Core.Model.Models; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.Core.Common.Seed.SeedData; + +public class SubBusinessDataSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return default; + } + + public IEnumerable SeedData() + { + return default; + } + + public async Task CustomizeSeedData(ISqlSugarClient db) + { + //初始化分库数据 + //只是用于测试 + if (db.CurrentConnectionConfig.ConfigId == "Tenant_3") + { + if (!await db.Queryable().AnyAsync()) + { + await db.Insertable(new List() + { + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "王五业务数据1", + Amount = 100, + }, + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "王五业务数据2", + Amount = 1000, + }, + }).ExecuteReturnSnowflakeIdListAsync(); + } + } + else if (db.CurrentConnectionConfig.ConfigId == "Tenant_4") + { + if (!await db.Queryable().AnyAsync()) + { + await db.Insertable(new List() + { + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "赵六业务数据1", + Amount = 50, + }, + new() + { + Id = SnowFlakeSingle.Instance.NextId(), + Name = "赵六业务数据2", + Amount = 60, + }, + }).ExecuteReturnSnowflakeIdListAsync(); + } + } + + + await Task.Delay(1); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs new file mode 100644 index 00000000..f33f83b2 --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/TenantSeedData.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Blog.Core.Common.DB; +using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; +using SqlSugar; + +namespace Blog.Core.Common.Seed.SeedData; + +/// +/// 租户 种子数据 +/// +public class TenantSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return new[] + { + new SysTenant() + { + Id = 1000001, + ConfigId = "Tenant_1", + Name = "张三", + TenantType = TenantTypeEnum.Id + }, + new SysTenant() + { + Id = 1000002, + ConfigId = "Tenant_2", + Name = "李四", + TenantType = TenantTypeEnum.Id + }, + new SysTenant() + { + Id = 1000003, + ConfigId = "Tenant_3", + Name = "王五", + TenantType = TenantTypeEnum.Db, + DbType = DbType.Sqlite, + Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, "WangWu.db"), + }, + new SysTenant() + { + Id = 1000004, + ConfigId = "Tenant_4", + Name = "赵六", + TenantType = TenantTypeEnum.Db, + DbType = DbType.Sqlite, + Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, "ZhaoLiu.db"), + }, + new SysTenant() + { + Id = 1000005, + ConfigId = "Tenant_5", + Name = "孙七", + TenantType = TenantTypeEnum.Tables, + }, + }; + } + + public IEnumerable SeedData() + { + return default; + } + + public Task CustomizeSeedData(ISqlSugarClient db) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs new file mode 100644 index 00000000..414a506b --- /dev/null +++ b/Blog.Core.Common/Seed/SeedData/UserInfoSeedData.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Blog.Core.Model.Models; +using SqlSugar; + +namespace Blog.Core.Common.Seed.SeedData; + +public class UserInfoSeedData : IEntitySeedData +{ + public IEnumerable InitSeedData() + { + return default; + } + + public IEnumerable SeedData() + { + return default; + } + + public async Task CustomizeSeedData(ISqlSugarClient db) + { + var data = new List() + { + new SysUserInfo() + { + Id = 10001, + LoginName = "zhangsan", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "张三", + TenantId = 1000001, //租户Id + }, + new SysUserInfo() + { + Id = 10002, + LoginName = "lisi", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "李四", + TenantId = 1000002, //租户Id + }, + new SysUserInfo() + { + Id = 10003, + LoginName = "wangwu", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "王五", + TenantId = 1000003, //租户Id + }, + new SysUserInfo() + { + Id = 10004, + LoginName = "zhaoliu", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "赵六", + TenantId = 1000004, //租户Id + }, + new SysUserInfo() + { + Id = 10005, + LoginName = "sunqi", + LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E", + Name = "孙七", + TenantId = 1000005, //租户Id + }, + }; + + var names = data.Select(s => s.LoginName).ToList(); + names = await db.Queryable() + .Where(s => names.Contains(s.LoginName)) + .Select(s => s.LoginName).ToListAsync(); + + var sysUserInfos = data.Where(s => !names.Contains(s.LoginName)).ToList(); + if (sysUserInfos.Any()) + { + await db.Insertable(sysUserInfos).ExecuteReturnIdentityAsync(); + } + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.EventBus/Blog.Core.EventBus.csproj b/Blog.Core.EventBus/Blog.Core.EventBus.csproj index 070c0a48..09d4e6a2 100644 --- a/Blog.Core.EventBus/Blog.Core.EventBus.csproj +++ b/Blog.Core.EventBus/Blog.Core.EventBus.csproj @@ -12,7 +12,7 @@ - + diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 50969b05..451d24f1 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -21,7 +21,7 @@ - + diff --git a/Blog.Core.Extensions/HostedService/ConsulHostedService.cs b/Blog.Core.Extensions/HostedService/ConsulHostedService.cs new file mode 100644 index 00000000..df866e6a --- /dev/null +++ b/Blog.Core.Extensions/HostedService/ConsulHostedService.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Consul; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions.HostedService; + +public class ConsulHostedService : IHostedService +{ + private readonly IConfiguration _configuration; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILogger _logger; + + public ConsulHostedService(IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) + { + _configuration = configuration; + _hostApplicationLifetime = hostApplicationLifetime; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start Consul Service!"); + await DoWork(); + } + + public async Task DoWork() + { + if (_configuration["Middleware:Consul:Enabled"].ObjToBool()) + { + var consulClient = new ConsulClient(c => + { + //consul地址 + c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]); + }); + + var registration = new AgentServiceRegistration() + { + ID = Guid.NewGuid().ToString(),//服务实例唯一标识 + Name = _configuration["ConsulSetting:ServiceName"],//服务名 + Address = _configuration["ConsulSetting:ServiceIP"], //服务IP + Port = int.Parse(_configuration["ConsulSetting:ServicePort"]),//服务端口 + Check = new AgentServiceCheck() + { + DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册 + Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔 + HTTP = $"http://{_configuration["ConsulSetting:ServiceIP"]}:{_configuration["ConsulSetting:ServicePort"]}{_configuration["ConsulSetting:ServiceHealthCheck"]}",//健康检查地址 + Timeout = TimeSpan.FromSeconds(5)//超时时间 + } + }; + + //服务注册 + await consulClient.Agent.ServiceRegister(registration); + + //应用程序终止时,取消注册 + _hostApplicationLifetime.ApplicationStopping.Register(async () => + { + await consulClient.Agent.ServiceDeregister(registration.ID); + }); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop Consul Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/EventBusHostedService.cs b/Blog.Core.Extensions/HostedService/EventBusHostedService.cs new file mode 100644 index 00000000..7f18ed19 --- /dev/null +++ b/Blog.Core.Extensions/HostedService/EventBusHostedService.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.EventBus; +using Blog.Core.EventBus.EventHandling; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions.HostedService; + +public class EventBusHostedService : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public EventBusHostedService(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start EventBus Service!"); + await DoWork(); + } + + private Task DoWork() + { + if (AppSettings.app(new string[] { "EventBus", "Enabled" }).ObjToBool()) + { + var eventBus = _serviceProvider.GetRequiredService(); + eventBus.Subscribe(); + } + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop EventBus Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs b/Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs new file mode 100644 index 00000000..d8f4d602 --- /dev/null +++ b/Blog.Core.Extensions/HostedService/QuartzJobHostedService.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.IServices; +using Blog.Core.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions.HostedService; + +public class QuartzJobHostedService : IHostedService +{ + private readonly ITasksQzServices _tasksQzServices; + private readonly ISchedulerCenter _schedulerCenter; + private readonly ILogger _logger; + + public QuartzJobHostedService(ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, ILogger logger) + { + _tasksQzServices = tasksQzServices; + _schedulerCenter = schedulerCenter; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start QuartzJob Service!"); + await DoWork(); + } + + private async Task DoWork() + { + try + { + if (AppSettings.app("Middleware", "QuartzNetJob", "Enabled").ObjToBool()) + { + var allQzServices = await _tasksQzServices.Query(); + foreach (var item in allQzServices) + { + if (item.IsStart) + { + var result = await _schedulerCenter.AddScheduleJobAsync(item); + if (result.success) + { + Console.WriteLine($"QuartzNetJob{item.Name}启动成功!"); + } + else + { + Console.WriteLine($"QuartzNetJob{item.Name}启动失败!错误信息:{result.msg}"); + } + } + } + } + } + catch (Exception e) + { + _logger.LogError(e, "An error was reported when starting the job service."); + throw; + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop QuartzJob Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs new file mode 100644 index 00000000..868d750e --- /dev/null +++ b/Blog.Core.Extensions/HostedService/SeedDataHostedService.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Blog.Core.Common; +using Blog.Core.Common.Seed; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Blog.Core.Extensions; + +public sealed class SeedDataHostedService : IHostedService +{ + private readonly MyContext _myContext; + private readonly ILogger _logger; + private readonly string _webRootPath; + + public SeedDataHostedService( + MyContext myContext, + IWebHostEnvironment webHostEnvironment, + ILogger logger) + { + _myContext = myContext; + _logger = logger; + _webRootPath = webHostEnvironment.WebRootPath; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Start Initialization Db Seed Service!"); + await DoWork(); + } + + private async Task DoWork() + { + try + { + if (AppSettings.app("AppSettings", "SeedDBEnabled").ObjToBool() || AppSettings.app("AppSettings", "SeedDBDataEnabled").ObjToBool()) + { + await DBSeed.SeedAsync(_myContext, _webRootPath); + + //多租户 同步 + await DBSeed.TenantSeedAsync(_myContext); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured seeding the Database."); + throw; + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stop Initialization Db Seed Service!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index 6ff8d044..4c61ed3b 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Ubiety.Dns.Core.Common; namespace Blog.Core.Extensions.Middlewares { diff --git a/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs b/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs index 1aae5ed5..4ae98830 100644 --- a/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/EventBusSetup.cs @@ -1,11 +1,9 @@ -using Autofac; +using System; +using Autofac; using Blog.Core.Common; using Blog.Core.EventBus; -using Blog.Core.EventBus.EventHandling; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System; namespace Blog.Core.Extensions { @@ -43,23 +41,12 @@ public static void AddEventBusSetup(this IServiceCollection services) return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); }); } - if(AppSettings.app(new string[] { "Kafka", "Enabled" }).ObjToBool()) + if (AppSettings.app(new string[] { "Kafka", "Enabled" }).ObjToBool()) { services.AddHostedService(); services.AddSingleton(); } } } - - - public static void ConfigureEventBus(this IApplicationBuilder app) - { - if (AppSettings.app(new string[] { "EventBus", "Enabled" }).ObjToBool()) - { - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe(); - } - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs b/Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs new file mode 100644 index 00000000..fea6a5ad --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/InitializationHostServiceSetup.cs @@ -0,0 +1,24 @@ +using System; +using Blog.Core.Extensions.HostedService; +using Microsoft.Extensions.DependencyInjection; + +namespace Blog.Core.Extensions; + +public static class InitializationHostServiceSetup +{ + /// + /// 应用初始化服务注入 + /// + /// + public static void AddInitializationHostServiceSetup(this IServiceCollection services) + { + if (services is null) + { + ArgumentNullException.ThrowIfNull(nameof(services)); + } + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index b3eb4bdc..3ecf224a 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Blog.Core.Common.DB.Aop; namespace Blog.Core.Extensions { @@ -101,7 +102,21 @@ public static void AddSqlsugarSetup(this IServiceCollection services) } ); }); - return new SqlSugarScope(listConfig); + return new SqlSugarScope(listConfig, db => + { + listConfig.ForEach(config => + { + var dbProvider = db.GetConnectionScope((string)config.ConfigId); + + // 数据审计 + dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; + + // 配置实体假删除过滤器 + RepositorySetting.SetDeletedEntityFilter(dbProvider); + // 配置实体数据权限 + RepositorySetting.SetTenantEntityFilter(dbProvider); + }); + }); }); } diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.csproj b/Blog.Core.Gateway/Blog.Core.Gateway.csproj index 47398520..f784da5c 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.csproj +++ b/Blog.Core.Gateway/Blog.Core.Gateway.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/Blog.Core.Gateway/Program.cs b/Blog.Core.Gateway/Program.cs index 1ba46816..9c1ba1ce 100644 --- a/Blog.Core.Gateway/Program.cs +++ b/Blog.Core.Gateway/Program.cs @@ -15,7 +15,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { - config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: true) + config.AddJsonFile("appsettings.gw.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.gw.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false) + .AddJsonFile("ocelot.json", optional: true, reloadOnChange: true) .AddJsonFile($"ocelot.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true); }) .ConfigureWebHostDefaults(webBuilder => diff --git a/Blog.Core.Gateway/appsettings.gw.Development.json b/Blog.Core.Gateway/appsettings.gw.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/Blog.Core.Gateway/appsettings.gw.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Blog.Core.Gateway/appsettings.gw.json b/Blog.Core.Gateway/appsettings.gw.json new file mode 100644 index 00000000..33b99ee8 --- /dev/null +++ b/Blog.Core.Gateway/appsettings.gw.json @@ -0,0 +1,103 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Debug" + } + } + }, + "AllowedHosts": "*", + "Startup": { + "Cors": { + "PolicyName": "CorsIpAccess", + "EnableAllIPs": false, + "IPs": "http://127.0.0.1:2364,http://localhost:2364" + } + }, + "Audience": { + "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", + "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", + "Issuer": "Blog.Core", + "Audience": "wr" + }, + "WhiteList": [ + { "url": "/" }, + { "url": "/illagal/****" }, + { "url": "/api3/****" }, + { "url": "/baseapi/swagger.json" } + ], + "BlackList": [ + { "url": "/favicon.ico" } + ], + "ApiGateWay": { + "OcelotConfig": "OcelotConfig.json", + "OcelotConfigGroup": "DEFAULT_GROUP", + "AppConfig": "****.****.Gateway.json", + "AppConfigGroup": "DEFAULT_GROUP", + "PermissionServName": "****.****.Api", + "PermissionServGroup": "DEFAULT_GROUP", + "PermissionServUrl": "/api/Permission/GetPermissionlist" + }, + "Influxdb": { + "Endpoint": "http://*******:9328", + "uid": "root", + "pwd": "*****", + "dbname": "mndata" + }, + "nacos": { + "ServerAddresses": [ "http://******:8848/" ], + "ServiceName": "*****.****.Gateway", + "DefaultTimeOut": 15000, + "Namespace": "****", + "ListenInterval": 1000, + "GroupName": "DEFAULT_GROUP", + "ClusterName": "DEFAULT", + "Ip": "", + "PreferredNetworks": "", + "Port": 8090, + "Weight": 100, + "RegisterEnabled": true, + "InstanceEnabled": true, + "Ephemeral": true, + "Secure": false, + "AccessKey": "", + "SecretKey": "", + "UserName": "****", + "Password": "*****", + "NamingUseRpc": true, + "NamingLoadCacheAtStart": "", + "LBStrategy": "WeightRandom", + "Metadata": { + "aa": "bb", + "cc": "dd", + "endpoint33": "******:8090" + } + }, + "nacosConfig": { + "ServiceName": "*****.*****.Gateway", + "Optional": false, + "DataId": "options1", + "Tenant": "******", + "Group": "DEFAULT_GROUP", + "Namespace": "*****", + "ServerAddresses": [ "http://******:8848/" ], + "UserName": "****", + "Password": "*****", + "AccessKey": "", + "SecretKey": "", + "EndPoint": "", + "ConfigUseRpc": true, + "ConfigFilterAssemblies": [ "apigateway" ], + "ConfigFilterExtInfo": "{\"JsonPaths\":[\"ConnectionStrings.Default\"],\"Other\":\"xxxxxx\"}" + } + + + +} diff --git a/Blog.Core.IServices/BASE/IBaseServices.cs b/Blog.Core.IServices/BASE/IBaseServices.cs index de4a7a06..a496b59f 100644 --- a/Blog.Core.IServices/BASE/IBaseServices.cs +++ b/Blog.Core.IServices/BASE/IBaseServices.cs @@ -10,19 +10,20 @@ namespace Blog.Core.IServices.BASE { public interface IBaseServices where TEntity : class { + ISqlSugarClient Db { get; } Task QueryById(object objId); Task QueryById(object objId, bool blnUseCache = false); Task> QueryByIDs(object[] lstIds); - Task Add(TEntity model); + Task Add(TEntity model); - Task Add(List listEntity); + Task> Add(List listEntity); Task DeleteById(object id); Task Delete(TEntity model); - + Task DeleteByIds(object[] ids); Task Update(TEntity model); @@ -59,6 +60,14 @@ Task> QueryMuch( Expression> selectExpression, Expression> whereLambda = null) where T : class, new(); Task> QueryPage(PaginationModel pagination); + + #region 分表 + Task QueryByIdSplit(object objId); + Task> AddSplit(TEntity entity); + Task DeleteSplit(TEntity entity, DateTime dateTime); + Task UpdateSplit(TEntity entity, DateTime dateTime); + Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null); + #endregion } } diff --git a/Blog.Core.IServices/IBlogArticleServices.cs b/Blog.Core.IServices/IBlogArticleServices.cs index 23e6081b..a38826fb 100644 --- a/Blog.Core.IServices/IBlogArticleServices.cs +++ b/Blog.Core.IServices/IBlogArticleServices.cs @@ -9,7 +9,7 @@ namespace Blog.Core.IServices public interface IBlogArticleServices :IBaseServices { Task> GetBlogs(); - Task GetBlogDetails(int id); + Task GetBlogDetails(long id); } diff --git a/Blog.Core.IServices/IRoleModulePermissionServices.cs b/Blog.Core.IServices/IRoleModulePermissionServices.cs index 22532479..2a5c7345 100644 --- a/Blog.Core.IServices/IRoleModulePermissionServices.cs +++ b/Blog.Core.IServices/IRoleModulePermissionServices.cs @@ -21,6 +21,6 @@ public interface IRoleModulePermissionServices :IBaseServices˵ /// ӿ /// - Task UpdateModuleId(int permissionId, int moduleId); + Task UpdateModuleId(long permissionId, long moduleId); } } diff --git a/Blog.Core.IServices/ISplitDemoServices.cs b/Blog.Core.IServices/ISplitDemoServices.cs new file mode 100644 index 00000000..55215761 --- /dev/null +++ b/Blog.Core.IServices/ISplitDemoServices.cs @@ -0,0 +1,15 @@ + + +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; +using System.Threading.Tasks; + +namespace Blog.Core.IServices +{ + /// + /// sysUserInfoServices + /// + public interface ISplitDemoServices : IBaseServices + { + } +} diff --git a/Blog.Core.IServices/ITasksLogServices.cs b/Blog.Core.IServices/ITasksLogServices.cs new file mode 100644 index 00000000..fb6ad8a7 --- /dev/null +++ b/Blog.Core.IServices/ITasksLogServices.cs @@ -0,0 +1,19 @@ + +using System; +using System.Threading.Tasks; +using Blog.Core.IServices.BASE; +using Blog.Core.Model; +using Blog.Core.Model.Models; + +namespace Blog.Core.IServices +{ + /// + /// ITasksLogServices + /// + public interface ITasksLogServices :IBaseServices + { + public Task> GetTaskLogs(long jobId, int page, int intPageSize,DateTime? runTime,DateTime? endTime); + public Task GetTaskOverview(long jobId, DateTime? runTime, DateTime? endTime, string type); + } +} + \ No newline at end of file diff --git a/Blog.Core.IServices/ITenantService.cs b/Blog.Core.IServices/ITenantService.cs new file mode 100644 index 00000000..0927b793 --- /dev/null +++ b/Blog.Core.IServices/ITenantService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Blog.Core.IServices.BASE; +using Blog.Core.Model.Models; + +namespace Blog.Core.IServices; + +public interface ITenantService : IBaseServices +{ + public Task SaveTenant(SysTenant tenant); + + public Task InitTenantDb(SysTenant tenant); +} \ No newline at end of file diff --git a/Blog.Core.IServices/IUserRoleServices.cs b/Blog.Core.IServices/IUserRoleServices.cs index 9e7d3d29..91272a09 100644 --- a/Blog.Core.IServices/IUserRoleServices.cs +++ b/Blog.Core.IServices/IUserRoleServices.cs @@ -10,8 +10,8 @@ namespace Blog.Core.IServices public interface IUserRoleServices :IBaseServices { - Task SaveUserRole(int uid, int rid); - Task GetRoleIdByUid(int uid); + Task SaveUserRole(long uid, long rid); + Task GetRoleIdByUid(long uid); } } diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index de7e6467..e66c5c5b 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -16,9 +16,13 @@ - + + + + + diff --git a/Blog.Core.Model/Models/AccessTrendLog.cs b/Blog.Core.Model/Models/AccessTrendLog.cs index 4a87b13e..fd6dbae7 100644 --- a/Blog.Core.Model/Models/AccessTrendLog.cs +++ b/Blog.Core.Model/Models/AccessTrendLog.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 用户访问趋势日志 /// - public class AccessTrendLog : RootEntityTkey + public class AccessTrendLog : RootEntityTkey { /// /// 用户 diff --git a/Blog.Core.Model/Models/Advertisement.cs b/Blog.Core.Model/Models/Advertisement.cs index c2babd74..3b11b21f 100644 --- a/Blog.Core.Model/Models/Advertisement.cs +++ b/Blog.Core.Model/Models/Advertisement.cs @@ -3,7 +3,7 @@ namespace Blog.Core.Model.Models { - public class Advertisement : RootEntityTkey + public class Advertisement : RootEntityTkey { /// diff --git a/Blog.Core.Model/Models/BlogArticle.cs b/Blog.Core.Model/Models/BlogArticle.cs index 52176190..8b75c8df 100644 --- a/Blog.Core.Model/Models/BlogArticle.cs +++ b/Blog.Core.Model/Models/BlogArticle.cs @@ -1,5 +1,6 @@ using SqlSugar; using System; +using System.Collections.Generic; namespace Blog.Core.Model.Models { @@ -12,14 +13,18 @@ public class BlogArticle /// 主键 /// /// 这里之所以没用RootEntity,是想保持和之前的数据库一致,主键是bID,不是Id - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int bID { get; set; } + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long bID { get; set; } + /// /// 创建人 /// [SugarColumn(Length = 600, IsNullable = true)] public string bsubmitter { get; set; } + [Navigate(NavigateType.OneToOne, nameof(bsubmitter))] + public SysUserInfo User { get; set; } + /// /// 标题blog /// @@ -57,6 +62,7 @@ public class BlogArticle /// 创建时间 /// public System.DateTime bCreateTime { get; set; } + /// /// 备注 /// @@ -69,5 +75,11 @@ public class BlogArticle [SugarColumn(IsNullable = true)] public bool? IsDeleted { get; set; } + + /// + /// 评论 + /// + [Navigate(NavigateType.OneToMany, nameof(BlogArticleComment.bID))] + public List Comments { get; set; } } -} +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/BlogArticleComment.cs b/Blog.Core.Model/Models/BlogArticleComment.cs new file mode 100644 index 00000000..519fb003 --- /dev/null +++ b/Blog.Core.Model/Models/BlogArticleComment.cs @@ -0,0 +1,19 @@ +using SqlSugar; + +namespace Blog.Core.Model.Models; + +/// +/// 博客文章 评论 +/// +public class BlogArticleComment : RootEntityTkey +{ + public long bID { get; set; } + + public string Comment { get; set; } + + + public string UserId { get; set; } + + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUserInfo User { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/Department.cs b/Blog.Core.Model/Models/Department.cs index 3583bcff..424bcf44 100644 --- a/Blog.Core.Model/Models/Department.cs +++ b/Blog.Core.Model/Models/Department.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// /// 部门表 /// - public class Department : DepartmentRoot + public class Department : DepartmentRoot { /// /// Desc:部门关系编码 diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs index 4b1bd9cd..2cecce8b 100644 --- a/Blog.Core.Model/Models/GblLogAudit.cs +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -12,8 +12,8 @@ public class GblLogAudit /// ///ID /// - [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int Id { get; set; } + [SugarColumn(ColumnDescription = "ID", IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long Id { get; set; } /// ///HttpContext.TraceIdentifier 事件链路ID(获取或设置一个唯一标识符,用于在跟踪日志中表示此请求。) diff --git a/Blog.Core.Model/Models/Guestbook.cs b/Blog.Core.Model/Models/Guestbook.cs index d1f671c0..0cd5dcef 100644 --- a/Blog.Core.Model/Models/Guestbook.cs +++ b/Blog.Core.Model/Models/Guestbook.cs @@ -3,13 +3,13 @@ namespace Blog.Core.Model.Models { - public class Guestbook:RootEntityTkey + public class Guestbook : RootEntityTkey { - + /// 博客ID /// /// - public int? blogId { get; set; } + public long? blogId { get; set; } /// 创建时间 /// /// diff --git a/Blog.Core.Model/Models/Modules.cs b/Blog.Core.Model/Models/Modules.cs index b62c0a47..6e41aaac 100644 --- a/Blog.Core.Model/Models/Modules.cs +++ b/Blog.Core.Model/Models/Modules.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 接口API地址信息表 /// - public class Modules : ModulesRoot + public class Modules : ModulesRoot { public Modules() { diff --git a/Blog.Core.Model/Models/OperateLog.cs b/Blog.Core.Model/Models/OperateLog.cs index 4086781c..3c2fb54c 100644 --- a/Blog.Core.Model/Models/OperateLog.cs +++ b/Blog.Core.Model/Models/OperateLog.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 日志记录 /// - public class OperateLog : RootEntityTkey + public class OperateLog : RootEntityTkey { /// diff --git a/Blog.Core.Model/Models/PasswordLib.cs b/Blog.Core.Model/Models/PasswordLib.cs index 2b43c265..b8b633d6 100644 --- a/Blog.Core.Model/Models/PasswordLib.cs +++ b/Blog.Core.Model/Models/PasswordLib.cs @@ -7,11 +7,11 @@ namespace Blog.Core.Model.Models /// 密码库表 /// [SugarTable("PasswordLib", "密码库表")]//('数据库表名','数据库表备注') - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') + [Tenant("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') public class PasswordLib { - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int PLID { get; set; } + [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = false)] + public long PLID { get; set; } /// ///获取或设置是否禁用,逻辑上的删除,非物理删除 diff --git a/Blog.Core.Model/Models/Permission.cs b/Blog.Core.Model/Models/Permission.cs index c650cb5e..deece0c0 100644 --- a/Blog.Core.Model/Models/Permission.cs +++ b/Blog.Core.Model/Models/Permission.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// /// 路由菜单表 /// - public class Permission : PermissionRoot + public class Permission : PermissionRoot { public Permission() { diff --git a/Blog.Core.Model/Models/Role.cs b/Blog.Core.Model/Models/Role.cs index e34ccdd9..1357afb0 100644 --- a/Blog.Core.Model/Models/Role.cs +++ b/Blog.Core.Model/Models/Role.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 角色表 /// - public class Role : RootEntityTkey + public class Role : RootEntityTkey { public Role() { diff --git a/Blog.Core.Model/Models/RoleModulePermission.cs b/Blog.Core.Model/Models/RoleModulePermission.cs index 13d82a80..482b9b4e 100644 --- a/Blog.Core.Model/Models/RoleModulePermission.cs +++ b/Blog.Core.Model/Models/RoleModulePermission.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// 按钮跟权限关联表 /// - public class RoleModulePermission : RoleModulePermissionRoot + public class RoleModulePermission : RoleModulePermissionRoot { public RoleModulePermission() { diff --git a/Blog.Core.Model/Models/RootTkey/BaseEntity.cs b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs new file mode 100644 index 00000000..b6dabe54 --- /dev/null +++ b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs @@ -0,0 +1,80 @@ +using Blog.Core.Model.Models.RootTkey.Interface; +using SqlSugar; +using System; + +namespace Blog.Core.Model.Models.RootTkey; + +public class BaseEntity : RootEntityTkey, IDeleteFilter +{ + #region 数据状态管理 + + /// + /// 状态
+ /// 中立字段,某些表可使用某些表不使用 + ///
+ public bool Enabled { get; set; } = true; + + /// + /// 中立字段,某些表可使用某些表不使用
+ /// 逻辑上的删除,非物理删除
+ /// 例如:单据删除并非直接删除 + ///
+ public bool IsDeleted { get; set; } + + /// + /// 中立字段
+ /// 是否内置数据 + ///
+ public bool IsInternal { get; set; } + + #endregion + + #region 创建 + + /// + /// 创建ID + /// + [SugarColumn(IsNullable = true, IsOnlyIgnoreUpdate = true)] + public long? CreateId { get; set; } + + /// + /// 创建者 + /// + [SugarColumn(IsNullable = true, IsOnlyIgnoreUpdate = true)] + public string CreateBy { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(IsOnlyIgnoreUpdate = true)] + public DateTime CreateTime { get; set; } = DateTime.Now; + + #endregion + + #region 修改 + + /// + /// 修改ID + /// + [SugarColumn(IsNullable = true)] + public long? ModifyId { get; set; } + + /// + /// 更新者 + /// + [SugarColumn(IsNullable = true)] + public string ModifyBy { get; set; } + + /// + /// 修改日期 + /// + public DateTime? ModifyTime { get; set; } = DateTime.Now; + + /// + /// 数据版本 + /// + [SugarColumn(DefaultValue = "0", IsEnableUpdateVersionValidation = true)] //标识版本字段 + public long Version { get; set; } + + #endregion +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs b/Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs new file mode 100644 index 00000000..57d421e9 --- /dev/null +++ b/Blog.Core.Model/Models/RootTkey/Interface/IDeleteFilter.cs @@ -0,0 +1,9 @@ +namespace Blog.Core.Model.Models.RootTkey.Interface; + +/// +/// 软删除 过滤器 +/// +public interface IDeleteFilter +{ + public bool IsDeleted { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs b/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs index afecdec9..20bdda0d 100644 --- a/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs +++ b/Blog.Core.Model/Models/RootTkey/RootEntityTkey.cs @@ -11,7 +11,5 @@ public class RootEntityTkey where Tkey : IEquatable ///
[SugarColumn(IsNullable = false, IsPrimaryKey = true)] public Tkey Id { get; set; } - - } } \ No newline at end of file diff --git a/Blog.Core.Model/Models/SplitDemo.cs b/Blog.Core.Model/Models/SplitDemo.cs new file mode 100644 index 00000000..26329935 --- /dev/null +++ b/Blog.Core.Model/Models/SplitDemo.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Blog.Core.Model.Models +{ + [SplitTable(SplitType.Day)]//按年分表 (自带分表支持 年、季、月、周、日) + [SugarTable("SplitDemo_{year}{month}{day}")]//3个变量必须要有,这么设计为了兼容开始按年,后面改成按月、按日 + public class SplitDemo + { + [SugarColumn(IsPrimaryKey = true)] + public long Id { get; set; } + + public string Name { get; set; } + + [SugarColumn(IsNullable = true)]//设置为可空字段 (更多用法看文档 迁移) + public DateTime UpdateTime { get; set; } + + [SplitField] //分表字段 在插入的时候会根据这个字段插入哪个表,在更新删除的时候用这个字段找出相关表 + public DateTime CreateTime { get; set; } + } +} diff --git a/Blog.Core.Model/Models/SysTenant.cs b/Blog.Core.Model/Models/SysTenant.cs new file mode 100644 index 00000000..61d03866 --- /dev/null +++ b/Blog.Core.Model/Models/SysTenant.cs @@ -0,0 +1,67 @@ +using Blog.Core.Model.Tenants; +using SqlSugar; + +namespace Blog.Core.Model.Models; + +/// +/// 系统租户表
+/// 根据TenantType 分为两种方案:
+/// 1.按租户字段区分
+/// 2.按租户分库
+/// +///
+/// +/// 注意:
+/// 使用租户Id方案,无需配置分库的连接 +///
+public class SysTenant : RootEntityTkey +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 租户类型 + /// + public TenantTypeEnum TenantType { get; set; } + + /// + /// 数据库/租户标识 不可重复
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(Length = 64)] + public string ConfigId { get; set; } + + /// + /// 主机
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(IsNullable = true)] + public string Host { get; set; } + + /// + /// 数据库类型
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(IsNullable = true)] + public SqlSugar.DbType? DbType { get; set; } + + /// + /// 数据库连接
+ /// 使用Id方案,可无需配置 + ///
+ [SugarColumn(IsNullable = true)] + public string Connection { get; set; } + + /// + /// 状态 + /// + public bool Status { get; set; } = true; + + /// + /// 备注 + /// + [SugarColumn(IsNullable = true)] + public string Remark { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/TasksLog.cs b/Blog.Core.Model/Models/TasksLog.cs new file mode 100644 index 00000000..c79e8077 --- /dev/null +++ b/Blog.Core.Model/Models/TasksLog.cs @@ -0,0 +1,87 @@ +using SqlSugar; +using System; + +namespace Blog.Core.Model.Models +{ + /// + /// 任务日志表 + /// + public class TasksLog : RootEntityTkey + { + /// + /// 任务ID + /// + public long JobId { get; set; } + /// + /// 任务耗时 + /// + public double TotalTime { get; set; } + /// + /// 执行结果(0-失败 1-成功) + /// + public bool RunResult { get; set; } + /// + /// 运行时间 + /// + public DateTime RunTime { get; set; } + /// + /// 结束时间 + /// + public DateTime EndTime { get; set; } + /// + /// 执行参数 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string RunPars { get; set; } + /// + /// 异常信息 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string ErrMessage { get; set; } + /// + /// 异常堆栈 + /// + [SugarColumn(Length = 2000, IsNullable = true)] + public string ErrStackTrace { get; set; } + /// + /// 创建ID + /// + [SugarColumn(IsNullable = true)] + public int? CreateId { get; set; } + /// + /// 创建者 + /// + [SugarColumn(Length = 50, IsNullable = true)] + public string CreateBy { get; set; } + /// + /// 创建时间 + /// + [SugarColumn(IsNullable = true)] + public DateTime CreateTime { get; set; } = DateTime.Now; + /// + /// 修改ID + /// + [SugarColumn(IsNullable = true)] + public int? ModifyId { get; set; } + /// + /// 修改者 + /// + [SugarColumn(Length = 100, IsNullable = true)] + public string ModifyBy { get; set; } + /// + /// 修改时间 + /// + [SugarColumn(IsNullable = true)] + public DateTime? ModifyTime { get; set; } = DateTime.Now; + /// + /// 任务名称 + /// + [SugarColumn(IsIgnore = true)] + public string Name { get; set; } + /// + /// 任务分组 + /// + [SugarColumn(IsIgnore = true)] + public string JobGroup { get; set; } + } +} diff --git a/Blog.Core.Model/Models/TasksQz.cs b/Blog.Core.Model/Models/TasksQz.cs index 5c812f79..b029a995 100644 --- a/Blog.Core.Model/Models/TasksQz.cs +++ b/Blog.Core.Model/Models/TasksQz.cs @@ -8,7 +8,7 @@ namespace Blog.Core.Model.Models /// /// 任务计划表 /// - public class TasksQz : RootEntityTkey + public class TasksQz : RootEntityTkey { /// /// 任务名称 @@ -65,6 +65,10 @@ public class TasksQz : RootEntityTkey /// public int CycleRunTimes { get; set; } /// + /// 已循环次数 + /// + public int CycleHasRunTimes { get; set; } + /// /// 是否启动 /// public bool IsStart { get; set; } = false; diff --git a/Blog.Core.Model/Models/Tenant/BusinessTable.cs b/Blog.Core.Model/Models/Tenant/BusinessTable.cs new file mode 100644 index 00000000..b3b0140a --- /dev/null +++ b/Blog.Core.Model/Models/Tenant/BusinessTable.cs @@ -0,0 +1,27 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; + +namespace Blog.Core.Model.Models; + +/// +/// 业务数据
+/// 多租户 (Id 隔离) +///
+public class BusinessTable : BaseEntity, ITenantEntity +{ + /// + /// 无需手动赋值 + /// + public long TenantId { get; set; } + + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs b/Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs new file mode 100644 index 00000000..1ada394d --- /dev/null +++ b/Blog.Core.Model/Models/Tenant/MultiBusinessSubTable.cs @@ -0,0 +1,14 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; + +namespace Blog.Core.Model.Models; + +/// +/// 多租户-多表方案 业务表 子表
+///
+[MultiTenant(TenantTypeEnum.Tables)] +public class MultiBusinessSubTable : BaseEntity +{ + public long MainId { get; set; } + public string Memo { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs b/Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs new file mode 100644 index 00000000..619bdaaf --- /dev/null +++ b/Blog.Core.Model/Models/Tenant/MultiBusinessTable.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; +using SqlSugar; + +namespace Blog.Core.Model.Models; + +/// +/// 多租户-多表方案 业务表
+///
+[MultiTenant(TenantTypeEnum.Tables)] +public class MultiBusinessTable : BaseEntity +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } + + [Navigate(NavigateType.OneToMany, nameof(MultiBusinessSubTable.MainId))] + public List Child { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/Tenant/SubLibraryBusinessTable.cs b/Blog.Core.Model/Models/Tenant/SubLibraryBusinessTable.cs new file mode 100644 index 00000000..446fb436 --- /dev/null +++ b/Blog.Core.Model/Models/Tenant/SubLibraryBusinessTable.cs @@ -0,0 +1,22 @@ +using Blog.Core.Model.Models.RootTkey; +using Blog.Core.Model.Tenants; + +namespace Blog.Core.Model.Models; + +/// +/// 多租户-多库方案 业务表
+/// 公共库无需标记[MultiTenant]特性 +///
+[MultiTenant] +public class SubLibraryBusinessTable : BaseEntity +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/TestModels.cs b/Blog.Core.Model/Models/TestModels.cs index f5fa7dc7..8a8d123c 100644 --- a/Blog.Core.Model/Models/TestModels.cs +++ b/Blog.Core.Model/Models/TestModels.cs @@ -5,9 +5,9 @@ public class TestMuchTableResult { public string moduleName { get; set; } public string permName { get; set; } - public int rid { get; set; } - public int mid { get; set; } - public int? pid { get; set; } + public long rid { get; set; } + public long mid { get; set; } + public long? pid { get; set; } } } diff --git a/Blog.Core.Model/Models/Topic.cs b/Blog.Core.Model/Models/Topic.cs index 16bf7dad..e57bd561 100644 --- a/Blog.Core.Model/Models/Topic.cs +++ b/Blog.Core.Model/Models/Topic.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Model.Models /// /// Tibug 类别 /// - public class Topic : RootEntityTkey + public class Topic : RootEntityTkey { public Topic() { diff --git a/Blog.Core.Model/Models/TopicDetail.cs b/Blog.Core.Model/Models/TopicDetail.cs index 87e16ebf..1a98f3af 100644 --- a/Blog.Core.Model/Models/TopicDetail.cs +++ b/Blog.Core.Model/Models/TopicDetail.cs @@ -6,7 +6,7 @@ namespace Blog.Core.Model.Models /// /// Tibug 博文 /// - public class TopicDetail : TopicDetailRoot + public class TopicDetail : TopicDetailRoot { public TopicDetail() { diff --git a/Blog.Core.Model/Models/UserRole.cs b/Blog.Core.Model/Models/UserRole.cs index 996eea2c..7ed9c6be 100644 --- a/Blog.Core.Model/Models/UserRole.cs +++ b/Blog.Core.Model/Models/UserRole.cs @@ -6,11 +6,11 @@ namespace Blog.Core.Model.Models /// /// 用户跟角色关联表 /// - public class UserRole : UserRoleRoot + public class UserRole : UserRoleRoot { public UserRole() { } - public UserRole(int uid, int rid) + public UserRole(long uid, long rid) { UserId = uid; RoleId = rid; @@ -31,7 +31,7 @@ public UserRole(int uid, int rid) /// 创建ID ///
[SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } + public long? CreateId { get; set; } /// /// 创建者 /// diff --git a/Blog.Core.Model/Models/WeChatConfig.cs b/Blog.Core.Model/Models/WeChatConfig.cs index f0aa97c4..16910487 100644 --- a/Blog.Core.Model/Models/WeChatConfig.cs +++ b/Blog.Core.Model/Models/WeChatConfig.cs @@ -51,13 +51,13 @@ public class WeChatConfig /// /// 公众号推送token /// - [SugarColumn(Length = 100, IsNullable = true)] + [SugarColumn(Length = 500, IsNullable = true)] public string token { get; set; } /// /// 验证秘钥(验证消息是否真实) /// - [SugarColumn(Length = 100, IsNullable = false)] + [SugarColumn(Length = 500, IsNullable = false)] public string interactiveToken { get; set; } /// diff --git a/Blog.Core.Model/Models/WeChatPushLog.cs b/Blog.Core.Model/Models/WeChatPushLog.cs index b2b6752e..5368c806 100644 --- a/Blog.Core.Model/Models/WeChatPushLog.cs +++ b/Blog.Core.Model/Models/WeChatPushLog.cs @@ -50,7 +50,7 @@ public partial class WeChatPushLog /// /// 推送内容 /// - [SugarColumn(IsNullable = true)] + [SugarColumn(Length = 2000, IsNullable = true)] public string PushLogContent { get; set; } /// diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index c60e4bce..2a417a16 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -8,8 +8,8 @@ namespace Blog.Core.Model.Models /// 用户信息表 /// //[SugarTable("SysUserInfo")] - [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') - public class SysUserInfo : SysUserInfoRoot + [SugarTable("SysUserInfo", "用户表")] //('数据库表名','数据库表备注') + public class SysUserInfo : SysUserInfoRoot { public SysUserInfo() { @@ -120,12 +120,20 @@ public SysUserInfo(string loginName, string loginPWD) [SugarColumn(IsNullable = true)] public bool IsDeleted { get; set; } + /// + /// 租户Id + /// + [SugarColumn(IsNullable = false,DefaultValue = "0")] + public long TenantId { get; set; } + + [Navigate(NavigateType.OneToOne, nameof(TenantId))] + public SysTenant Tenant { get; set; } [SugarColumn(IsIgnore = true)] public List RoleNames { get; set; } [SugarColumn(IsIgnore = true)] - public List Dids { get; set; } + public List Dids { get; set; } [SugarColumn(IsIgnore = true)] public string DepartmentName { get; set; } diff --git a/Blog.Core.Model/Tenants/ITenantEntity.cs b/Blog.Core.Model/Tenants/ITenantEntity.cs new file mode 100644 index 00000000..2d0c5dc9 --- /dev/null +++ b/Blog.Core.Model/Tenants/ITenantEntity.cs @@ -0,0 +1,15 @@ +using SqlSugar; + +namespace Blog.Core.Model.Tenants; + +/// +/// 租户模型接口 +/// +public interface ITenantEntity +{ + /// + /// 租户Id + /// + [SugarColumn(DefaultValue = "0")] + public long TenantId { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Tenants/MultiTenantAttribute.cs b/Blog.Core.Model/Tenants/MultiTenantAttribute.cs new file mode 100644 index 00000000..443745bf --- /dev/null +++ b/Blog.Core.Model/Tenants/MultiTenantAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace Blog.Core.Model.Tenants; + +/// +/// 标识 多租户 的业务表
+/// 默认设置是多库
+/// 公共表无需区分 直接使用主库 各自业务在各自库中
+///
+[AttributeUsage(AttributeTargets.Class)] +public class MultiTenantAttribute : Attribute +{ + public MultiTenantAttribute() + { + } + + public MultiTenantAttribute(TenantTypeEnum tenantType) + { + TenantType = tenantType; + } + + + public TenantTypeEnum TenantType { get; set; } = TenantTypeEnum.Db; +} \ No newline at end of file diff --git a/Blog.Core.Model/Tenants/TenantTypeEnum.cs b/Blog.Core.Model/Tenants/TenantTypeEnum.cs new file mode 100644 index 00000000..f4af3bda --- /dev/null +++ b/Blog.Core.Model/Tenants/TenantTypeEnum.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; + +namespace Blog.Core.Model.Tenants; + +/// +/// 租户隔离方案 +/// +public enum TenantTypeEnum +{ + None = 0, + + /// + /// Id隔离 + /// + [Description("Id隔离")] + Id = 1, + + /// + /// 库隔离 + /// + [Description("库隔离")] + Db = 2, + + /// + /// 表隔离 + /// + [Description("表隔离")] + Tables = 3, +} \ No newline at end of file diff --git a/Blog.Core.Model/ViewModels/BlogViewModels.cs b/Blog.Core.Model/ViewModels/BlogViewModels.cs index f959270c..86c16618 100644 --- a/Blog.Core.Model/ViewModels/BlogViewModels.cs +++ b/Blog.Core.Model/ViewModels/BlogViewModels.cs @@ -10,7 +10,7 @@ public class BlogViewModels /// /// /// - public int bID { get; set; } + public long bID { get; set; } /// 创建人 /// /// @@ -34,7 +34,7 @@ public class BlogViewModels /// /// 上一篇id /// - public int previousID { get; set; } + public long previousID { get; set; } /// /// 下一篇 @@ -44,7 +44,7 @@ public class BlogViewModels /// /// 下一篇id /// - public int nextID { get; set; } + public long nextID { get; set; } /// 类别 /// diff --git a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs index 33d84161..3b5451f0 100644 --- a/Blog.Core.Model/ViewModels/SysUserInfoDto.cs +++ b/Blog.Core.Model/ViewModels/SysUserInfoDto.cs @@ -3,13 +3,13 @@ namespace Blog.Core.Model.ViewModels { - public class SysUserInfoDto : SysUserInfoDtoRoot + public class SysUserInfoDto : SysUserInfoDtoRoot { public string uLoginName { get; set; } public string uLoginPWD { get; set; } public string uRealName { get; set; } public int uStatus { get; set; } - public int DepartmentId { get; set; } + public long DepartmentId { get; set; } public string uRemark { get; set; } public System.DateTime uCreateTime { get; set; } = DateTime.Now; public System.DateTime uUpdateTime { get; set; } = DateTime.Now; @@ -22,7 +22,7 @@ public class SysUserInfoDto : SysUserInfoDtoRoot public string addr { get; set; } public bool tdIsDelete { get; set; } public List RoleNames { get; set; } - public List Dids { get; set; } + public List Dids { get; set; } public string DepartmentName { get; set; } } } diff --git a/Blog.Core.Publish.Linux.sh b/Blog.Core.Publish.Linux.sh index 05eb8103..7599f204 100644 --- a/Blog.Core.Publish.Linux.sh +++ b/Blog.Core.Publish.Linux.sh @@ -1,8 +1,9 @@ -git pull; + find .PublishFiles/ -type f -and ! -path '*/wwwroot/images/*' ! -name 'appsettings.*' |xargs rm -rf dotnet build; rm -rf /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles; dotnet publish -o /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles; +rm -rf /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles/WMBlog.db; # cp -r /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles ./; awk 'BEGIN { cmd="cp -ri /home/Blog.Core/Blog.Core.Api/bin/Debug/.PublishFiles ./"; print "n" |cmd; }' echo "Successfully!!!! ^ please see the file .PublishFiles"; \ No newline at end of file diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index ead9f9af..9d926ee4 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -1,6 +1,11 @@ using Blog.Core.Common; +using Blog.Core.Common.DB; using Blog.Core.IRepository.Base; using Blog.Core.Model; +using Blog.Core.Model.Models; +using Blog.Core.Model.Tenants; +using Blog.Core.Repository.UnitOfWorks; +using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime; using SqlSugar; using System; using System.Collections.Generic; @@ -8,7 +13,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; -using Blog.Core.Repository.UnitOfWorks; namespace Blog.Core.Repository.Base { @@ -22,11 +26,12 @@ private ISqlSugarClient _db get { ISqlSugarClient db = _dbBase; + /* 如果要开启多库支持, * 1、在appsettings.json 中开启MutiDBEnabled节点为true,必填 * 2、设置一个主连接的数据库ID,节点MainDB,对应的连接字符串的Enabled也必须true,必填 */ - if (AppSettings.app(new[] {"MutiDBEnabled"}).ObjToBool()) + if (AppSettings.app(new[] { "MutiDBEnabled" }).ObjToBool()) { //修改使用 model备注字段作为切换数据库条件,使用sqlsugar TenantAttribute存放数据库ConnId //参考 https://www.donet5.com/Home/Doc?typeId=2246 @@ -35,6 +40,28 @@ private ISqlSugarClient _db { //统一处理 configId 小写 db = _dbBase.GetConnectionScope(tenantAttr.configId.ToString().ToLower()); + return db; + } + } + + //多租户 + var mta = typeof(TEntity).GetCustomAttribute(); + if (mta is { TenantType: TenantTypeEnum.Db }) + { + //获取租户信息 租户信息可以提前缓存下来 + if (App.User is { TenantId: > 0 }) + { + var tenant = db.Queryable().WithCache().Where(s => s.Id == App.User.TenantId).First(); + if (tenant != null) + { + var iTenant = db.AsTenant(); + if (!iTenant.IsAnyConnection(tenant.ConfigId)) + { + iTenant.AddConnection(tenant.GetConnectionConfig()); + } + + return iTenant.GetConnectionScope(tenant.ConfigId); + } } } @@ -51,12 +78,12 @@ public BaseRepository(IUnitOfWorkManage unitOfWorkManage) } - public async Task QueryById(object objId) { //return await Task.Run(() => _db.Queryable().InSingle(objId)); return await _db.Queryable().In(objId).SingleAsync(); } + /// /// 功能描述:根据ID查询一条数据 /// 作  者:Blog.Core @@ -87,7 +114,7 @@ public async Task> QueryByIDs(object[] lstIds) /// /// 博文实体类 /// - public async Task Add(TEntity entity) + public async Task Add(TEntity entity) { //var i = await Task.Run(() => _db.Insertable(entity).ExecuteReturnBigIdentity()); ////返回的i是long类型,这里你可以根据你的业务需要进行处理 @@ -98,26 +125,25 @@ public async Task Add(TEntity entity) //这里你可以返回TEntity,这样的话就可以获取id值,无论主键是什么类型 //var return3 = await insert.ExecuteReturnEntityAsync(); - return await insert.ExecuteReturnIdentityAsync(); + return await insert.ExecuteReturnSnowflakeIdAsync(); } - /// /// 写入实体数据 /// /// 实体类 /// 指定只插入列 /// 返回自增量列 - public async Task Add(TEntity entity, Expression> insertColumns = null) + public async Task Add(TEntity entity, Expression> insertColumns = null) { var insert = _db.Insertable(entity); if (insertColumns == null) { - return await insert.ExecuteReturnIdentityAsync(); + return await insert.ExecuteReturnSnowflakeIdAsync(); } else { - return await insert.InsertColumns(insertColumns).ExecuteReturnIdentityAsync(); + return await insert.InsertColumns(insertColumns).ExecuteReturnSnowflakeIdAsync(); } } @@ -126,9 +152,9 @@ public async Task Add(TEntity entity, Expression> ins /// /// 实体集合 /// 影响行数 - public async Task Add(List listEntity) + public async Task> Add(List listEntity) { - return await _db.Insertable(listEntity.ToArray()).ExecuteCommandAsync(); + return await _db.Insertable(listEntity.ToArray()).ExecuteReturnSnowflakeIdListAsync(); } /// @@ -151,10 +177,6 @@ public async Task Update(TEntity entity) /// public async Task Update(List entity) { - ////这种方式会以主键为条件 - //var i = await Task.Run(() => _db.Updateable(entity).ExecuteCommand()); - //return i > 0; - //这种方式会以主键为条件 return await _db.Updateable(entity).ExecuteCommandHasChangeAsync(); } @@ -174,25 +196,28 @@ public async Task Update(object operateAnonymousObjects) } public async Task Update( - TEntity entity, - List lstColumns = null, - List lstIgnoreColumns = null, - string where = "" - ) + TEntity entity, + List lstColumns = null, + List lstIgnoreColumns = null, + string where = "" + ) { IUpdateable up = _db.Updateable(entity); if (lstIgnoreColumns != null && lstIgnoreColumns.Count > 0) { up = up.IgnoreColumns(lstIgnoreColumns.ToArray()); } + if (lstColumns != null && lstColumns.Count > 0) { up = up.UpdateColumns(lstColumns.ToArray()); } + if (!string.IsNullOrEmpty(where)) { up = up.Where(where); } + return await up.ExecuteCommandHasChangeAsync(); } @@ -227,7 +252,6 @@ public async Task DeleteByIds(object[] ids) } - /// /// 功能描述:查询所有数据 /// 作  者:Blog.Core @@ -297,6 +321,7 @@ public async Task> Query(Expression> whereExpr { return await _db.Queryable().WhereIF(whereExpression != null, whereExpression).OrderByIF(orderByFields != null, orderByFields).ToListAsync(); } + /// /// 功能描述:查询一个列表 /// @@ -406,18 +431,16 @@ public async Task> Query( /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string where, - int pageIndex, - int pageSize, - - string orderByFields) + string where, + int pageIndex, + int pageSize, + string orderByFields) { return await _db.Queryable().OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) .WhereIF(!string.IsNullOrEmpty(where), where).ToPageListAsync(pageIndex, pageSize); } - /// /// 分页查询[使用版本,其他分页未测试] /// @@ -428,12 +451,11 @@ public async Task> Query( /// public async Task> QueryPage(Expression> whereExpression, int pageIndex = 1, int pageSize = 20, string orderByFields = null) { - RefAsync totalCount = 0; var list = await _db.Queryable() - .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) - .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(pageIndex, pageSize, totalCount); + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -459,6 +481,7 @@ public async Task> QueryMuch( { return await _db.Queryable(joinExpression).Select(selectExpression).ToListAsync(); } + return await _db.Queryable(joinExpression).Where(whereLambda).Select(selectExpression).ToListAsync(); } @@ -484,13 +507,12 @@ public async Task> QueryTabsPage( int pageSize = 20, string orderByFields = null) { - RefAsync totalCount = 0; var list = await _db.Queryable(joinExpression) - .Select(selectExpression) - .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) - .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(pageIndex, pageSize, totalCount); + .Select(selectExpression) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -519,10 +541,10 @@ public async Task> QueryTabsPage( { RefAsync totalCount = 0; var list = await _db.Queryable(joinExpression).GroupBy(groupExpression) - .Select(selectExpression) - .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) - .WhereIF(whereExpression != null, whereExpression) - .ToPageListAsync(pageIndex, pageSize, totalCount); + .Select(selectExpression) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); return new PageModel(pageIndex, totalCount, pageSize, list); } @@ -545,6 +567,80 @@ public async Task> QueryTabsPage( // jobName = s.jobName // }, exp, s => new { s.uID, s.uRealName, s.groupName, s.jobName }, model.currentPage, model.pageSize, model.orderField + " " + model.orderType); - } + #region Split分表基础接口 (基础CRUD) -} + /// + /// 分页查询[使用版本,其他分页未测试] + /// + /// 条件表达式 + /// 页码(下标0) + /// 页大小 + /// 排序字段,如name asc,age desc + /// + public async Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null) + { + RefAsync totalCount = 0; + var list = await _db.Queryable().SplitTable(beginTime, endTime) + .OrderByIF(!string.IsNullOrEmpty(orderByFields), orderByFields) + .WhereIF(whereExpression != null, whereExpression) + .ToPageListAsync(pageIndex, pageSize, totalCount); + var data = new PageModel(pageIndex, totalCount, pageSize, list); + return data; + } + + /// + /// 写入实体数据 + /// + /// 数据实体 + /// + public async Task> AddSplit(TEntity entity) + { + var insert = _db.Insertable(entity).SplitTable(); + //插入并返回雪花ID并且自动赋值ID  + return await insert.ExecuteReturnSnowflakeIdListAsync(); + } + + /// + /// 更新实体数据 + /// + /// 数据实体 + /// + public async Task UpdateSplit(TEntity entity, DateTime dateTime) + { + //直接根据实体集合更新 (全自动 找表更新) + //return await _db.Updateable(entity).SplitTable().ExecuteCommandAsync();//,SplitTable不能少 + + //精准找单个表 + var tableName = _db.SplitHelper().GetTableName(dateTime); //根据时间获取表名 + return await _db.Updateable(entity).AS(tableName).ExecuteCommandHasChangeAsync(); + } + + /// + /// 删除数据 + /// + /// + /// + /// + public async Task DeleteSplit(TEntity entity, DateTime dateTime) + { + ////直接根据实体集合删除 (全自动 找表插入),返回受影响数 + //return await _db.Deleteable(entity).SplitTable().ExecuteCommandAsync();//,SplitTable不能少 + + //精准找单个表 + var tableName = _db.SplitHelper().GetTableName(dateTime); //根据时间获取表名 + return await _db.Deleteable().AS(tableName).Where(entity).ExecuteCommandHasChangeAsync(); + } + + /// + /// 根据ID查找数据 + /// + /// + /// + public async Task QueryByIdSplit(object objId) + { + return await _db.Queryable().In(objId).SplitTable(tabs => tabs).SingleAsync(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Blog.Core.Repository/BASE/IBaseRepository.cs b/Blog.Core.Repository/BASE/IBaseRepository.cs index 9b72a00d..77e9a808 100644 --- a/Blog.Core.Repository/BASE/IBaseRepository.cs +++ b/Blog.Core.Repository/BASE/IBaseRepository.cs @@ -33,14 +33,14 @@ public interface IBaseRepository where TEntity : class /// /// /// - Task Add(TEntity model); + Task Add(TEntity model); /// /// 批量添加 /// /// /// - Task Add(List listEntity); + Task> Add(List listEntity); /// /// 根据id 删除某一实体 @@ -214,5 +214,45 @@ Task> QueryTabsPage( int pageIndex = 1, int pageSize = 20, string orderByFields = null); + + #region 分表 + /// + /// 通过ID查询 + /// + /// + /// + Task QueryByIdSplit(object objId); + /// + /// 自动分表插入 + /// + /// + /// + Task> AddSplit(TEntity entity); + /// + /// 删除 + /// + /// + /// + /// + Task DeleteSplit(TEntity entity, DateTime dateTime); + /// + /// 更新 + /// + /// + /// + /// + Task UpdateSplit(TEntity entity, DateTime dateTime); + /// + /// 分页查询 + /// + /// + /// + /// + /// + /// + /// + /// + Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, int pageIndex = 1, int pageSize = 20, string orderByFields = null); + #endregion } } diff --git a/Blog.Core.Repository/Blog.Core.Repository.csproj b/Blog.Core.Repository/Blog.Core.Repository.csproj index 5c9764b0..df05250e 100644 --- a/Blog.Core.Repository/Blog.Core.Repository.csproj +++ b/Blog.Core.Repository/Blog.Core.Repository.csproj @@ -6,9 +6,9 @@ - - - + + + diff --git a/Blog.Core.Repository/IRoleModulePermissionRepository.cs b/Blog.Core.Repository/IRoleModulePermissionRepository.cs index c66448f0..9ba3d4ed 100644 --- a/Blog.Core.Repository/IRoleModulePermissionRepository.cs +++ b/Blog.Core.Repository/IRoleModulePermissionRepository.cs @@ -19,6 +19,6 @@ public interface IRoleModulePermissionRepository : IBaseRepository菜单主键 /// 接口主键 /// - Task UpdateModuleId(int permissionId, int moduleId); + Task UpdateModuleId(long permissionId, long moduleId); } } diff --git a/Blog.Core.Repository/RoleModulePermissionRepository.cs b/Blog.Core.Repository/RoleModulePermissionRepository.cs index 1cb21ebf..9438ff50 100644 --- a/Blog.Core.Repository/RoleModulePermissionRepository.cs +++ b/Blog.Core.Repository/RoleModulePermissionRepository.cs @@ -99,7 +99,7 @@ public async Task> GetRMPMapsPage() /// 菜单主键 /// 接口主键 /// - public async Task UpdateModuleId(int permissionId, int moduleId) + public async Task UpdateModuleId(long permissionId, long moduleId) { await Db.Updateable(it => it.ModuleId == moduleId).Where( it => it.PermissionId == permissionId).ExecuteCommandAsync(); diff --git a/Blog.Core.Services/BASE/BaseServices.cs b/Blog.Core.Services/BASE/BaseServices.cs index 232c6fae..44042382 100644 --- a/Blog.Core.Services/BASE/BaseServices.cs +++ b/Blog.Core.Services/BASE/BaseServices.cs @@ -17,13 +17,17 @@ public BaseServices(IBaseRepository BaseDal = null) { this.BaseDal = BaseDal; } + //public IBaseRepository baseDal = new BaseRepository(); - public IBaseRepository BaseDal { get; set; }//通过在子类的构造函数中注入,这里是基类,不用构造函数 + public IBaseRepository BaseDal { get; set; } //通过在子类的构造函数中注入,这里是基类,不用构造函数 + + public ISqlSugarClient Db => BaseDal.Db; public async Task QueryById(object objId) { return await BaseDal.QueryById(objId); } + /// /// 功能描述:根据ID查询一条数据 /// 作  者:AZLinli.Blog.Core @@ -52,7 +56,7 @@ public async Task> QueryByIDs(object[] lstIds) /// /// 博文实体类 /// - public async Task Add(TEntity entity) + public async Task Add(TEntity entity) { return await BaseDal.Add(entity); } @@ -62,7 +66,7 @@ public async Task Add(TEntity entity) /// /// 实体集合 /// 影响行数 - public async Task Add(List listEntity) + public async Task> Add(List listEntity) { return await BaseDal.Add(listEntity); } @@ -85,21 +89,23 @@ public async Task Update(List entity) { return await BaseDal.Update(entity); } + public async Task Update(TEntity entity, string where) { return await BaseDal.Update(entity, where); } + public async Task Update(object operateAnonymousObjects) { return await BaseDal.Update(operateAnonymousObjects); } public async Task Update( - TEntity entity, - List lstColumns = null, - List lstIgnoreColumns = null, - string where = "" - ) + TEntity entity, + List lstColumns = null, + List lstIgnoreColumns = null, + string where = "" + ) { return await BaseDal.Update(entity, lstColumns, lstIgnoreColumns, where); } @@ -136,7 +142,6 @@ public async Task DeleteByIds(object[] ids) } - /// /// 功能描述:查询所有数据 /// 作  者:AZLinli.Blog.Core @@ -190,7 +195,7 @@ public async Task> Query(Expression查询实体条件 /// 排序条件 /// - public async Task> Query(Expression> expression, Expression> whereExpression,string orderByFileds) + public async Task> Query(Expression> expression, Expression> whereExpression, string orderByFileds) { return await BaseDal.Query(expression, whereExpression, orderByFileds); } @@ -233,7 +238,6 @@ public async Task> Query(string where, string orderByFileds) public async Task> QuerySql(string sql, SugarParameter[] parameters = null) { return await BaseDal.QuerySql(sql, parameters); - } /// @@ -245,8 +249,8 @@ public async Task> QuerySql(string sql, SugarParameter[] parameter public async Task QueryTable(string sql, SugarParameter[] parameters = null) { return await BaseDal.QueryTable(sql, parameters); - } + /// /// 功能描述:查询前N条数据 /// 作  者:AZLinli.Blog.Core @@ -292,10 +296,10 @@ public async Task> Query( string orderByFileds) { return await BaseDal.Query( - whereExpression, - pageIndex, - pageSize, - orderByFileds); + whereExpression, + pageIndex, + pageSize, + orderByFileds); } /// @@ -308,34 +312,70 @@ public async Task> Query( /// 排序字段,如name asc,age desc /// 数据列表 public async Task> Query( - string where, - int pageIndex, - int pageSize, - string orderByFileds) + string where, + int pageIndex, + int pageSize, + string orderByFileds) { return await BaseDal.Query( - where, - pageIndex, - pageSize, - orderByFileds); + where, + pageIndex, + pageSize, + orderByFileds); } public async Task> QueryPage(Expression> whereExpression, - int pageIndex = 1, int pageSize = 20, string orderByFileds = null) + int pageIndex = 1, int pageSize = 20, string orderByFileds = null) { return await BaseDal.QueryPage(whereExpression, - pageIndex, pageSize, orderByFileds); + pageIndex, pageSize, orderByFileds); } public async Task> QueryMuch(Expression> joinExpression, Expression> selectExpression, Expression> whereLambda = null) where T : class, new() { return await BaseDal.QueryMuch(joinExpression, selectExpression, whereLambda); } + public async Task> QueryPage(PaginationModel pagination) { var express = DynamicLinqFactory.CreateLambda(pagination.Conditions); return await QueryPage(express, pagination.PageIndex, pagination.PageSize, pagination.OrderByFileds); } - } -} + #region 分表 + + public async Task> AddSplit(TEntity entity) + { + return await BaseDal.AddSplit(entity); + } + + public async Task UpdateSplit(TEntity entity, DateTime dateTime) + { + return await BaseDal.UpdateSplit(entity, dateTime); + } + + /// + /// 根据实体删除一条数据 + /// + /// 博文实体类 + /// + public async Task DeleteSplit(TEntity entity, DateTime dateTime) + { + return await BaseDal.DeleteSplit(entity, dateTime); + } + + public async Task QueryByIdSplit(object objId) + { + return await BaseDal.QueryByIdSplit(objId); + } + + public async Task> QueryPageSplit(Expression> whereExpression, DateTime beginTime, DateTime endTime, + int pageIndex = 1, int pageSize = 20, string orderByFields = null) + { + return await BaseDal.QueryPageSplit(whereExpression, beginTime, endTime, + pageIndex, pageSize, orderByFields); + } + + #endregion + } +} \ No newline at end of file diff --git a/Blog.Core.Services/BlogArticleServices.cs b/Blog.Core.Services/BlogArticleServices.cs index eeff04e3..67ea2b9e 100644 --- a/Blog.Core.Services/BlogArticleServices.cs +++ b/Blog.Core.Services/BlogArticleServices.cs @@ -23,7 +23,7 @@ public BlogArticleServices(IMapper mapper) /// /// /// - public async Task GetBlogDetails(int id) + public async Task GetBlogDetails(long id) { // 此处想获取上一条下一条数据,因此将全部数据list出来,有好的想法请提出 //var bloglist = await base.Query(a => a.IsDeleted==false, a => a.bID); diff --git a/Blog.Core.Services/RoleModulePermissionServices.cs b/Blog.Core.Services/RoleModulePermissionServices.cs index c0248e17..d3834f89 100644 --- a/Blog.Core.Services/RoleModulePermissionServices.cs +++ b/Blog.Core.Services/RoleModulePermissionServices.cs @@ -83,7 +83,7 @@ public async Task> GetRMPMaps() /// 菜单主键 /// 接口主键 /// - public async Task UpdateModuleId(int permissionId, int moduleId) + public async Task UpdateModuleId(long permissionId, long moduleId) { await _dal.UpdateModuleId(permissionId, moduleId); } diff --git a/Blog.Core.Services/SplitDemoServices.cs b/Blog.Core.Services/SplitDemoServices.cs new file mode 100644 index 00000000..cf8e2cc1 --- /dev/null +++ b/Blog.Core.Services/SplitDemoServices.cs @@ -0,0 +1,23 @@ +using Blog.Core.IRepository.Base; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Services.BASE; +using System.Linq; +using System.Threading.Tasks; + +namespace Blog.Core.FrameWork.Services +{ + /// + /// sysUserInfoServices + /// + public class SplitDemoServices : BaseServices, ISplitDemoServices + { + private readonly IBaseRepository _splitDemoRepository; + public SplitDemoServices(IBaseRepository splitDemoRepository) + { + _splitDemoRepository = splitDemoRepository; + } + + + } +} diff --git a/Blog.Core.Services/TasksLogServices.cs b/Blog.Core.Services/TasksLogServices.cs new file mode 100644 index 00000000..07d95f1f --- /dev/null +++ b/Blog.Core.Services/TasksLogServices.cs @@ -0,0 +1,137 @@ +using System.Linq.Expressions; +using System; +using System.Threading.Tasks; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Services.BASE; +using Blog.Core.Common.Extensions; +using SqlSugar; +using Blog.Core.Model; +using System.Collections.Generic; +using System.Linq; + +namespace Blog.Core.Services +{ + public partial class TasksLogServices : BaseServices, ITasksLogServices + { + public async Task> GetTaskLogs(long jobId, int page, int intPageSize, DateTime? runTime, DateTime? endTime) + { + RefAsync totalCount = 0; + Expression> whereExpression = log => true; + if (jobId > 0) whereExpression = whereExpression.And(log => log.JobId == jobId); + var data = await this.Db.Queryable() + .LeftJoin((log, qz) => log.JobId == qz.Id) + .OrderByDescending((log) => log.RunTime) + .WhereIF(jobId > 0, (log) => log.JobId == jobId) + .WhereIF(runTime != null, (log) => log.RunTime >= runTime.Value) + .WhereIF(endTime != null, (log) => log.RunTime <= endTime.Value) + .Select((log, qz) => new TasksLog + { + RunPars = log.RunPars, + RunResult = log.RunResult, + RunTime = log.RunTime, + EndTime = log.EndTime, + ErrMessage = log.ErrMessage, + ErrStackTrace = log.ErrStackTrace, + TotalTime = log.TotalTime, + Name = qz.Name, + JobGroup = qz.JobGroup + + }) + .ToPageListAsync(page, intPageSize, totalCount); + return new PageModel(page, totalCount, intPageSize, data); + } + public async Task GetTaskOverview(long jobId, DateTime? runTime, DateTime? endTime, string type) + { + //按年 + if ("year".Equals(type)) + { + + var days = endTime.Value.Year - runTime.Value.Year; + var dayArray = new List(); + while (days >= 0) + { + dayArray.Add(new DateTime(runTime.Value.Year + days, 1, 1)); + days--; + } + var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime.Year >= runTime.Value.Year && x.RunTime.Year <= endTime.Value.Year); ; ; //声名表 + + var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, + (x1, x2) => x1.ColumnName.Year == x2.RunTime.Year) + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.Year.ToString() + "年" + }).ToList().OrderBy(t => t.date); + return list; + } + else if ("month".Equals(type)) + { + //按月 + var queryableLeft = this.Db.Reportable(ReportableDateType.MonthsInLast1years).ToQueryable(); //生成月份 //ReportableDateType.MonthsInLast1yea 表式近一年月份 并且queryable之后还能在where过滤 + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime.Year == runTime.Value.Year); //声名表 + + //月份和表JOIN + var list = queryableLeft + .LeftJoin(queryableRight, (x1, x2) => x2.RunTime.ToString("MM月") == x1.ColumnName.ToString("MM月")) + + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + //null的数据要为0所以不能用count + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.ToString("MM月") + } + ).ToList().OrderBy(t => t.date); + await Task.CompletedTask; + return list; + } + else if ("day".Equals(type)) + { + //按日 + var time = runTime.Value; + var days = DateTime.DaysInMonth(time.Year, time.Month); + var dayArray = Enumerable.Range(1, days).Select(it => Convert.ToDateTime(time.ToString("yyyy-MM-" + it))).ToList();//转成时间数组 + var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); + var star = Convert.ToDateTime(runTime.Value.ToString("yyyy-MM-01 00:00:00")); + var end = Convert.ToDateTime(runTime.Value.ToString($"yyyy-MM-{days} 23:59:59")); + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= star && x.RunTime <= end); ; ; //声名表 + + var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, + (x1, x2) => x1.ColumnName.Date == x2.RunTime.Date) + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.Day + }).ToList().OrderBy(t => t.date); + await Task.CompletedTask; + return list; + } + else if ("hour".Equals(type)) + { + //按小时 + var time = runTime.Value; + var days = 24; + var dayArray = Enumerable.Range(0, days).Select(it => Convert.ToDateTime(time.ToString($"yyyy-MM-dd {it.ToString().PadLeft(2, '0')}:00:00"))).ToList();//转成时间数组 + var queryableLeft = this.Db.Reportable(dayArray).ToQueryable(); + var queryableRight = this.Db.Queryable().Where((x) => x.RunTime >= runTime.Value.Date && x.RunTime <= runTime.Value.Date.AddDays(1).AddMilliseconds(-1)); //声名表 + + var list = this.Db.Queryable(queryableLeft, queryableRight, JoinType.Left, + (x1, x2) => x1.ColumnName.Hour == x2.RunTime.Hour) + .GroupBy((x1, x2) => x1.ColumnName) + .Select((x1, x2) => new + { + 执行次数 = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)), + date = x1.ColumnName.Hour + }).ToList().OrderBy(t => t.date); + await Task.CompletedTask; + return list; + } + await Task.CompletedTask; + return null; + } + } +} diff --git a/Blog.Core.Services/TenantService.cs b/Blog.Core.Services/TenantService.cs new file mode 100644 index 00000000..a552442b --- /dev/null +++ b/Blog.Core.Services/TenantService.cs @@ -0,0 +1,57 @@ +using Blog.Core.Common.DB; +using Blog.Core.Common.Seed; +using Blog.Core.IServices; +using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; +using Blog.Core.Services.BASE; +using System.Threading.Tasks; + +namespace Blog.Core.Services; + +public class TenantService : BaseServices, ITenantService +{ + private readonly IUnitOfWorkManage _uowManager; + + public TenantService(IUnitOfWorkManage uowManage) + { + this._uowManager = uowManage; + } + + + public async Task SaveTenant(SysTenant tenant) + { + bool initDb = tenant.Id == 0; + using (var uow = _uowManager.CreateUnitOfWork()) + { + + tenant.DefaultTenantConfig(); + + if (tenant.Id == 0) + { + await Db.Insertable(tenant).ExecuteReturnSnowflakeIdAsync(); + } + else + { + var oldTenant = await QueryById(tenant.Id); + if (oldTenant.Connection != tenant.Connection) + { + initDb = true; + } + + await Db.Updateable(tenant).ExecuteCommandAsync(); + } + + uow.Commit(); + } + + if (initDb) + { + await InitTenantDb(tenant); + } + } + + public async Task InitTenantDb(SysTenant tenant) + { + await DBSeed.InitTenantSeedAsync(Db.AsTenant(), tenant.GetConnectionConfig()); + } +} \ No newline at end of file diff --git a/Blog.Core.Services/UserRoleServices.cs b/Blog.Core.Services/UserRoleServices.cs index 38f524da..26194837 100644 --- a/Blog.Core.Services/UserRoleServices.cs +++ b/Blog.Core.Services/UserRoleServices.cs @@ -19,7 +19,7 @@ public class UserRoleServices : BaseServices, IUserRoleServices /// /// /// - public async Task SaveUserRole(int uid, int rid) + public async Task SaveUserRole(long uid, long rid) { UserRole userRole = new UserRole(uid, rid); @@ -42,7 +42,7 @@ public async Task SaveUserRole(int uid, int rid) [Caching(AbsoluteExpiration = 30)] - public async Task GetRoleIdByUid(int uid) + public async Task GetRoleIdByUid(long uid) { return ((await base.Query(d => d.UserId == uid)).OrderByDescending(d => d.Id).LastOrDefault()?.RoleId).ObjToInt(); } diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs b/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs index 4a71fbf5..b4486dfc 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/JobBase.cs @@ -1,5 +1,6 @@ using Blog.Core.Common.Helper; using Blog.Core.IServices; +using Blog.Core.Model.Models; using Quartz; using System; using System.Diagnostics; @@ -10,6 +11,12 @@ namespace Blog.Core.Tasks public class JobBase { public ITasksQzServices _tasksQzServices; + public ITasksLogServices _tasksLogServices; + public JobBase(ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + { + _tasksQzServices = tasksQzServices; + _tasksLogServices = tasksLogServices; + } /// /// 执行指定任务 /// @@ -17,40 +24,50 @@ public class JobBase /// public async Task ExecuteJob(IJobExecutionContext context, Func func) { - //记录Job时间 - Stopwatch stopwatch = new Stopwatch(); + //记录Job + TasksLog tasksLog = new TasksLog(); //JOBID int jobid = context.JobDetail.Key.Name.ObjToInt(); //JOB组名 string groupName = context.JobDetail.Key.Group; //日志 - string jobHistory = $"【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行开始】【Id:{jobid},组别:{groupName}】"; - //耗时 - double taskSeconds = 0; + tasksLog.JobId = jobid; + tasksLog.RunTime = DateTime.Now; + string jobHistory = $"【{tasksLog.RunTime.ToString("yyyy-MM-dd HH:mm:ss")}】【执行开始】【Id:{jobid},组别:{groupName}】"; try { - stopwatch.Start(); await func();//执行任务 - stopwatch.Stop(); - jobHistory += $",【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行成功】"; + tasksLog.EndTime = DateTime.Now; + tasksLog.RunResult = true; + jobHistory += $",【{tasksLog.EndTime.ToString("yyyy-MM-dd HH:mm:ss")}】【执行成功】"; + + JobDataMap jobPars = context.JobDetail.JobDataMap; + tasksLog.RunPars = jobPars.GetString("JobParam"); } catch (Exception ex) { - JobExecutionException e2 = new JobExecutionException(ex); + tasksLog.EndTime = DateTime.Now; + tasksLog.RunResult = false; + //JobExecutionException e2 = new JobExecutionException(ex); //true 是立即重新执行任务 - e2.RefireImmediately = true; - jobHistory += $",【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行失败:{ex.Message}】"; + //e2.RefireImmediately = true; + tasksLog.ErrMessage = ex.Message; + tasksLog.ErrStackTrace = ex.StackTrace; + jobHistory += $",【{tasksLog.EndTime.ToString("yyyy-MM-dd HH:mm:ss")}】【执行失败:{ex.Message}】"; } finally { - taskSeconds = Math.Round(stopwatch.Elapsed.TotalSeconds, 3); - jobHistory += $",【{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}】【执行结束】(耗时:{taskSeconds}秒)"; + tasksLog.TotalTime = Math.Round((tasksLog.EndTime - tasksLog.RunTime).TotalSeconds,3); + jobHistory += $"(耗时:{tasksLog.TotalTime}秒)"; if (_tasksQzServices != null) { var model = await _tasksQzServices.QueryById(jobid); if (model != null) { + if(_tasksLogServices != null) await _tasksLogServices.Add(tasksLog); model.RunTimes += 1; + if (model.TriggerType == 0) model.CycleHasRunTimes += 1; + if (model.TriggerType == 0 && model.CycleRunTimes != 0 && model.CycleHasRunTimes >= model.CycleRunTimes) model.IsStart = false;//循环完善,当循环任务完成后,停止该任务,防止下次启动再次执行 var separator = "
"; // 这里注意数据库字段的长度问题,超过限制,会造成数据库remark不更新问题。 model.Remark = diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs index 743180c3..1dcc57ed 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs @@ -21,7 +21,8 @@ public class Job_AccessTrendLog_Quartz : JobBase, IJob private readonly IAccessTrendLogServices _accessTrendLogServices; private readonly IWebHostEnvironment _environment; - public Job_AccessTrendLog_Quartz(IAccessTrendLogServices accessTrendLogServices, ITasksQzServices tasksQzServices, IWebHostEnvironment environment) + public Job_AccessTrendLog_Quartz(IAccessTrendLogServices accessTrendLogServices, IWebHostEnvironment environment, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { _accessTrendLogServices = accessTrendLogServices; _environment = environment; diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs index 173e996e..f116fe05 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Blogs_Quartz.cs @@ -1,5 +1,6 @@ using Blog.Core.IServices; using Quartz; +using System; using System.Threading.Tasks; /// @@ -11,10 +12,10 @@ public class Job_Blogs_Quartz : JobBase, IJob { private readonly IBlogArticleServices _blogArticleServices; - public Job_Blogs_Quartz(IBlogArticleServices blogArticleServices, ITasksQzServices tasksQzServices) + public Job_Blogs_Quartz(IBlogArticleServices blogArticleServices, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { _blogArticleServices = blogArticleServices; - _tasksQzServices = tasksQzServices; } public async Task Execute(IJobExecutionContext context) { @@ -22,6 +23,7 @@ public async Task Execute(IJobExecutionContext context) } public async Task Run(IJobExecutionContext context) { + System.Console.WriteLine($"Job_Blogs_Quartz 执行 {DateTime.Now.ToShortTimeString()}"); var list = await _blogArticleServices.Query(); // 也可以通过数据库配置,获取传递过来的参数 JobDataMap data = context.JobDetail.JobDataMap; diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs index 8e9b3847..18c4c298 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_OperateLog_Quartz.cs @@ -20,11 +20,11 @@ public class Job_OperateLog_Quartz : JobBase, IJob private readonly IOperateLogServices _operateLogServices; private readonly IWebHostEnvironment _environment; - public Job_OperateLog_Quartz(IOperateLogServices operateLogServices, ITasksQzServices tasksQzServices, IWebHostEnvironment environment) + public Job_OperateLog_Quartz(IOperateLogServices operateLogServices,IWebHostEnvironment environment, ITasksQzServices tasksQzServices,ITasksLogServices tasksLogServices) + :base(tasksQzServices, tasksLogServices) { _operateLogServices = operateLogServices; - _environment = environment; - _tasksQzServices = tasksQzServices; + _environment = environment; } public async Task Execute(IJobExecutionContext context) { diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs index f9640749..9d2d4cea 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs @@ -1,7 +1,8 @@ -using Blog.Core.Repository.UnitOfWorks; + using Blog.Core.IServices; using Blog.Core.IServices.BASE; using Blog.Core.Model.Models; +using Blog.Core.Repository.UnitOfWorks; using Microsoft.Extensions.Logging; using Quartz; using System; @@ -20,7 +21,8 @@ public class Job_Trojan_Quartz : JobBase, IJob private readonly ITrojanUsersServices _TrojanUsers; private readonly ILogger _logger; - public Job_Trojan_Quartz(IUnitOfWorkManage unitOfWorkManage, IBaseServices iusers_DetailServices, ITrojanUsersServices trojanUsers, ITasksQzServices tasksQzServices, ILogger logger) + public Job_Trojan_Quartz(IUnitOfWorkManage unitOfWorkManage, IBaseServices iusers_DetailServices, ITrojanUsersServices trojanUsers, ILogger logger, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { _tasksQzServices = tasksQzServices; _unitOfWorkManage = unitOfWorkManage; diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs index 666a8d41..1ec225c3 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_URL_Quartz.cs @@ -1,12 +1,7 @@ using Blog.Core.Common.Helper; -using Blog.Core.Repository.UnitOfWorks; using Blog.Core.IServices; -using Blog.Core.IServices.BASE; -using Blog.Core.Model.Models; using Microsoft.Extensions.Logging; using Quartz; -using System; -using System.Collections.Generic; using System.Threading.Tasks; /// @@ -18,7 +13,8 @@ public class Job_URL_Quartz : JobBase, IJob { private readonly ILogger _logger; - public Job_URL_Quartz(ITasksQzServices tasksQzServices, ILogger logger) + public Job_URL_Quartz(ILogger logger, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) + : base(tasksQzServices, tasksLogServices) { _tasksQzServices = tasksQzServices; _logger = logger; diff --git a/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs b/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs index 8c21115c..474a2a9a 100644 --- a/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs +++ b/Blog.Core.Tasks/QuartzNet/SchedulerCenterServer.cs @@ -125,6 +125,12 @@ public async Task> AddScheduleJobAsync(TasksQz tasksQz) result.msg = $"该任务计划已经在执行:【{tasksQz.Name}】,请勿重复启动!"; return result; } + if(tasksQz.TriggerType == 0 && tasksQz.CycleRunTimes != 0 && tasksQz.CycleHasRunTimes>=tasksQz.CycleRunTimes) + { + result.success = false; + result.msg = $"该任务计划已完成:【{tasksQz.Name}】,无需重复启动,如需启动请修改已循环次数再提交"; + return result; + } #region 设置开始时间和结束时间 if (tasksQz.BeginTime == null) diff --git a/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs b/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs index 99757e4f..8f6ad096 100644 --- a/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs +++ b/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs @@ -3,6 +3,7 @@ using Blog.Core.Common.Helper; using Blog.Core.IRepository.Base; using Blog.Core.Model.Models; +using SqlSugar; using Xunit; using Xunit.Abstractions; @@ -21,13 +22,42 @@ public DynamicLambdaTest(ITestOutputHelper testOutputHelper) var container = dI_Test.DICollections(); _baseRepository = container.Resolve>(); - + _baseRepository.Db.Aop.OnLogExecuting = (sql, p) => + { + _testOutputHelper.WriteLine(""); + _testOutputHelper.WriteLine("==================FullSql=====================", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); + _testOutputHelper.WriteLine("【SQL语句】:" + sql); + _testOutputHelper.WriteLine(GetParas(p)); + _testOutputHelper.WriteLine("=============================================="); + _testOutputHelper.WriteLine(""); + }; //DbContext.Init(BaseDBConfig.ConnectionString,(DbType)BaseDBConfig.DbType); + + Init(); + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } + + private void Init() + { + _baseRepository.Db.CodeFirst.InitTables(); + _baseRepository.Db.CodeFirst.InitTables(); } [Fact] public async void Get_Blogs_DynamicTest() { + //方便前端自定义条件查询 + //语法更舒服 var data = await _baseRepository.Query(); _testOutputHelper.WriteLine(data.ToJson()); @@ -39,12 +69,26 @@ public async void Get_Blogs_DynamicTest() await TestConditions("btitle like \"测试数据\" && bId>0"); await TestConditions("btitle like \"测试!@#$%^&*()_+|}{\":<>?LP\"数据\" && bId>0"); await TestConditions("btitle like \"测试!@+)(*()_&%^&^$^%$IUYWIQOJVLXKZM>?Z<>??LP\"数据\" && bId>0"); - - //比如文章下 过滤创建人 - //await TestConditions("btitle.user.name like \"老张\""); - await TestConditions("IsDeleted == false"); await TestConditions("IsDeleted == true"); + + //导航属性 + + //一对一 + + //查询 老张的文章 + await TestConditions("User.RealName like \"老张\""); + //查询 2019年后的老张文章 + await TestConditions("User.RealName like \"老张\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + + //一对多 + + //查询 评论中有"写的不错"的文章 + await TestConditions("Comments.Comment like \"写的不错\""); + //查询 2019后的 评论中有"写的不错"的文章 + await TestConditions("Comments.Comment like \"写的不错\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + //查询 有老张评论的文章 + await TestConditions("Comments.User.LoginName like \"老张\""); } private async Task TestConditions(string conditions) diff --git a/Blog.Core.Tests/Controller_Test/BlogController_Should.cs b/Blog.Core.Tests/Controller_Test/BlogController_Should.cs index 3d1e3178..59d42ae0 100644 --- a/Blog.Core.Tests/Controller_Test/BlogController_Should.cs +++ b/Blog.Core.Tests/Controller_Test/BlogController_Should.cs @@ -53,7 +53,7 @@ public async void Get_Blog_Page_Test() [Fact] public async void Get_Blog_Test() { - MessageModel blogVo = await blogController.Get(1); + MessageModel blogVo = await blogController.Get(1.ObjToLong()); Assert.NotNull(blogVo); } diff --git a/Blog.Core.Tests/DependencyInjection/DI_Test.cs b/Blog.Core.Tests/DependencyInjection/DI_Test.cs index 6fad1f45..ff2c74cd 100644 --- a/Blog.Core.Tests/DependencyInjection/DI_Test.cs +++ b/Blog.Core.Tests/DependencyInjection/DI_Test.cs @@ -74,13 +74,13 @@ public IContainer DICollections() var permission = new List(); var permissionRequirement = new PermissionRequirement( - "/api/denied", - permission, - ClaimTypes.Role, - AppSettings.app(new string[] { "Audience", "Issuer" }), - AppSettings.app(new string[] { "Audience", "Audience" }), - signingCredentials,//签名凭据 - expiration: TimeSpan.FromSeconds(60 * 60)//接口的过期时间 + "/api/denied", + permission, + ClaimTypes.Role, + AppSettings.app(new string[] { "Audience", "Issuer" }), + AppSettings.app(new string[] { "Audience", "Audience" }), + signingCredentials, //签名凭据 + expiration: TimeSpan.FromSeconds(60 * 60) //接口的过期时间 ); services.AddSingleton(permissionRequirement); @@ -88,17 +88,16 @@ public IContainer DICollections() services.AddAuthorization(options => { options.AddPolicy(Permissions.Name, - policy => policy.Requirements.Add(permissionRequirement)); + policy => policy.Requirements.Add(permissionRequirement)); }); services.AddScoped(o => { return new SqlSugar.SqlSugarScope(new SqlSugar.ConnectionConfig() { - ConnectionString = GetMainConnectionDb().Connection,//必填, 数据库连接字符串 - DbType = (SqlSugar.DbType)GetMainConnectionDb().DbType,//必填, 数据库类型 - IsAutoCloseConnection = true,//默认false, 时候知道关闭数据库连接, 设置为true无需使用using或者Close操作 - InitKeyType = SqlSugar.InitKeyType.SystemTable//默认SystemTable, 字段信息读取, 如:该属性是不是主键,标识列等等信息 + ConnectionString = GetMainConnectionDb().Connection, //必填, 数据库连接字符串 + DbType = (SqlSugar.DbType)GetMainConnectionDb().DbType, //必填, 数据库类型 + IsAutoCloseConnection = true, //默认false, 时候知道关闭数据库连接, 设置为true无需使用using或者Close操作 }); }); @@ -121,19 +120,19 @@ public IContainer DICollections() builder.RegisterAssemblyTypes(typeof(Startup).Assembly) .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType) .PropertiesAutowired(); - + var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); var assemblysServices = Assembly.LoadFrom(servicesDllFile); builder.RegisterAssemblyTypes(assemblysServices) - .AsImplementedInterfaces() - .InstancePerLifetimeScope() - .PropertiesAutowired() - .EnableInterfaceInterceptors(); + .AsImplementedInterfaces() + .InstancePerLifetimeScope() + .PropertiesAutowired() + .EnableInterfaceInterceptors(); var repositoryDllFile = Path.Combine(basePath, "Blog.Core.Repository.dll"); var assemblysRepository = Assembly.LoadFrom(repositoryDllFile); builder.RegisterAssemblyTypes(assemblysRepository) - .PropertiesAutowired().AsImplementedInterfaces(); + .PropertiesAutowired().AsImplementedInterfaces(); services.Replace(ServiceDescriptor.Transient()); diff --git a/Blog.Core.Tests/Repository_Test/OrmTest.cs b/Blog.Core.Tests/Repository_Test/OrmTest.cs new file mode 100644 index 00000000..fa4629f7 --- /dev/null +++ b/Blog.Core.Tests/Repository_Test/OrmTest.cs @@ -0,0 +1,73 @@ +using System; +using Autofac; +using Blog.Core.Common.Extensions; +using Blog.Core.IRepository.Base; +using Blog.Core.Model.Models; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Math; +using SqlSugar; +using Xunit; +using Xunit.Abstractions; + +namespace Blog.Core.Tests; + +public class OrmTest +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly IBaseRepository _baseRepository; + DI_Test dI_Test = new DI_Test(); + + public OrmTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + + var container = dI_Test.DICollections(); + + _baseRepository = container.Resolve>(); + _baseRepository.Db.Aop.OnLogExecuting = (sql, p) => + { + _testOutputHelper.WriteLine(""); + _testOutputHelper.WriteLine("==================FullSql=====================", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); + _testOutputHelper.WriteLine("【SQL语句】:" + sql); + _testOutputHelper.WriteLine(GetParas(p)); + _testOutputHelper.WriteLine("=============================================="); + _testOutputHelper.WriteLine(""); + }; + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } + + [Fact] + public void MultiTables() + { + var sql = _baseRepository.Db.Queryable() + .AS($@"{nameof(BlogArticle)}_TenantA") + .ToSqlString(); + //_testOutputHelper.WriteLine(sql); + + _baseRepository.Db.MappingTables.Add(nameof(BlogArticle), $@"{nameof(BlogArticle)}_TenantA"); + + var query = _baseRepository.Db.Queryable() + .LeftJoin((a, c) => a.bID == c.bID); + // query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticle), $@"{nameof(BlogArticle)}_TenantA"); + //query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticleComment), $@"{nameof(BlogArticleComment)}_TenantA"); + // query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticleComment), $@"{nameof(BlogArticleComment)}_TenantA"); + // query.QueryBuilder.AsTables.AddOrModify(nameof(SysUserInfo), $@"{nameof(SysUserInfo)}_TenantA"); + + + sql = query.ToSqlString(); + + _testOutputHelper.WriteLine(sql); + + sql = _baseRepository.Db.Deleteable().ToSqlString(); + _testOutputHelper.WriteLine(sql); + } +} \ No newline at end of file diff --git a/README.md b/README.md index 7f37ce66..ee36dc2c 100644 --- a/README.md +++ b/README.md @@ -73,20 +73,24 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x #### 框架模块: - [x] 采用`仓储+服务+接口`的形式封装框架; -- [x] 异步 async/await 开发; -- [x] 接入国产数据库ORM组件 —— SqlSugar,封装数据库操作; +- [x] 自定义项目模板 `CreateYourProject.bat` ,可以一键生成自己的项目;🎶 +- [x] 异步 async/await 开发; +- [x] 接入国产数据库ORM组件 —— SqlSugar,封装数据库操作,支持级联操作; - [x] 支持自由切换多种数据库,MySql/SqlServer/Sqlite/Oracle/Postgresql/达梦/人大金仓; - [x] 实现项目启动,自动生成种子数据 ✨; - [x] 实现数据库主键类型配置化,什么类型都可以自定义 ✨; - [x] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等,并自动持久化到数据库表🎶; - [x] 支持项目事务处理(若要分布式,用cap即可)✨; - [x] 设计4种 AOP 切面编程,功能涵盖:日志、缓存、审计、事务 ✨; +- [x] Log4net 多种日志自动生成到数据库中,目前支持MySql/SqlServer/Sqlite/Oracle/Postgresql🎉; - [x] 设计并支持按钮级别的RBAC权限控制,同时支持一键同步接口和菜单 🎶; - [x] 支持 T4 代码模板,自动生成每层代码; - [x] 或使用 DbFirst 一键创建自己项目的四层文件(支持多库); - [x] 封装`Blog.Core.Webapi.Template`项目模板,一键重建自己的项目 ✨; - [x] 搭配多个前端案例供参考和借鉴:Blog.Vue、Blog.Admin、Nuxt.tbug、Blog.Mvp.Blazor ✨; - [x] 统一集成 IdentityServer4 认证 ✨; +- [x] 统一实现多租户; + 组件模块: - [x] 提供 Redis 做缓存处理; @@ -111,6 +115,9 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 新增 Kafka 消息队列,并配合实现EventBus ✨; - [x] 新增 微信公众号管理,并集成到Blog.Admin后台 ✨; - [x] 新增 - 数据部门权限; +- [x] 新增 - Log4net集成日志数据持久化到数据库; +- [x] 新增 - 多租户模式(单表,多表,多库三种模式); + 微服务模块: - [x] 可配合 Docker 实现容器化; @@ -229,9 +236,9 @@ Contributions of any kind are welcome! ## 售后服务与支持 -鼓励作者,简单打赏,入微信群,随时随地解答我框架中(NetCore、Vue、DDD、IdentityServer4等)的疑难杂症。 -注意主要是帮忙解决bug和思路,不会远程授课,但是可以适当发我代码,我帮忙调试, -打赏的时候,备注自己的微信号,我拉你进群,两天内没回应,QQ私聊我(3143422472); +鼓励作者,简单打赏~~ +打赏的时候,备注自己的微信号,加个微信,交个朋友,两天内没回应,QQ私聊我(3143422472); +目前精力有限,主要针对企业级用户答疑,或者购买授权版的个人用户。 [赞赏列表](http://apk.neters.club/.doc/Contribution/) From 7b8d60d9d07ce02cc80102fa1fb3795278a2a0e8 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Fri, 14 Apr 2023 11:20:10 +0800 Subject: [PATCH 159/289] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96Http?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=A8=A1=E7=89=88=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=BF=E4=BA=9B=E9=99=84=E5=8A=A0=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Program.cs | 58 +++++++++---------- .../Extensions/HttpRequestExtension.cs | 28 +++++++++ .../Policys/PermissionHandler.cs | 4 +- .../Utility/SerilogRequestUtility.cs | 37 +++++++++++- 4 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 Blog.Core.Common/Extensions/HttpRequestExtension.cs diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 6f326a7c..b36a02ac 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -29,7 +29,6 @@ var builder = WebApplication.CreateBuilder(args); - // 1、配置host与容器 builder.Host .UseServiceProviderFactory(new AutofacServiceProviderFactory()) @@ -50,7 +49,6 @@ builder.Services.AddSingleton(new AppSettings(builder.Configuration)); - builder.Services.AddUiFilesZipSetup(builder.Environment); Permissions.IsUseIds4 = AppSettings.app(new string[] { "Startup", "IdentityServer4", "Enabled" }).ObjToBool(); @@ -102,30 +100,30 @@ builder.Services.AddSession(); builder.Services.AddHttpPollySetup(); builder.Services.AddControllers(o => -{ - o.Filters.Add(typeof(GlobalExceptionsFilter)); - //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); - o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); -}) -.AddNewtonsoftJson(options => -{ - options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - options.SerializerSettings.ContractResolver = new DefaultContractResolver(); - options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; - //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); - //将long类型转为string - options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); -}) -//.AddFluentValidation(config => -//{ -// //程序集方式添加验证 -// config.RegisterValidatorsFromAssemblyContaining(typeof(UserRegisterVoValidator)); -// //是否与MvcValidation共存 -// config.DisableDataAnnotationsValidation = true; -//}) -; + { + o.Filters.Add(typeof(GlobalExceptionsFilter)); + //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); + o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); + }) + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + //将long类型转为string + options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); + }) + //.AddFluentValidation(config => + //{ + // //程序集方式添加验证 + // config.RegisterValidatorsFromAssemblyContaining(typeof(UserRegisterVoValidator)); + // //是否与MvcValidation共存 + // config.DisableDataAnnotationsValidation = true; + //}) + ; builder.Services.AddEndpointsApiExplorer(); @@ -167,13 +165,9 @@ app.UseStatusCodePages(); app.UseSerilogRequestLogging(options => { + options.MessageTemplate = SerilogRequestUtility.HttpMessageTemplate; options.GetLevel = SerilogRequestUtility.GetRequestLevel; - options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => - { - diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); - diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme); - diagnosticContext.Set("RequestIp", httpContext.GetRequestIp()); - }; + options.EnrichDiagnosticContext = SerilogRequestUtility.EnrichFromRequest; }); app.UseRouting(); diff --git a/Blog.Core.Common/Extensions/HttpRequestExtension.cs b/Blog.Core.Common/Extensions/HttpRequestExtension.cs new file mode 100644 index 00000000..7e87fd28 --- /dev/null +++ b/Blog.Core.Common/Extensions/HttpRequestExtension.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using System.IO; +using System.Text; + +namespace Blog.Core.Common.Extensions; + +public static class HttpRequestExtension +{ + public static string GetRequestBody(this HttpRequest request) + { + if (request.Body.Length < 1) + { + return default; + } + + var bodyStr = ""; + // 启用倒带功能,就可以让 Request.Body 可以再次读取 + request.Body.Seek(0, SeekOrigin.Begin); + using (StreamReader reader + = new StreamReader(request.Body, Encoding.UTF8, true, 1024, true)) + { + bodyStr = reader.ReadToEnd(); + } + + request.Body.Position = 0; + return bodyStr; + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs index 4ed55525..121b9b10 100644 --- a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs +++ b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs @@ -1,6 +1,8 @@ using Blog.Core.Common; using Blog.Core.Common.Helper; +using Blog.Core.Common.HttpContextUser; using Blog.Core.IServices; +using Blog.Core.Model; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -12,8 +14,6 @@ using System.Security.Claims; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Blog.Core.Common.HttpContextUser; -using Blog.Core.Model; namespace Blog.Core.AuthHelper { diff --git a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs index cab7ae55..61b652dd 100644 --- a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs +++ b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs @@ -1,17 +1,22 @@ -using Microsoft.AspNetCore.Http; +using Blog.Core.Common.Extensions; +using Blog.Core.Common.Https; +using Microsoft.AspNetCore.Http; +using Serilog; using Serilog.Events; namespace Blog.Core.Serilog.Utility; public class SerilogRequestUtility { + public const string HttpMessageTemplate = + "HTTP {RequestMethod} {RequestPath} QueryString:{QueryString} Body:{Body} responded {StatusCode} in {Elapsed:0.0000} ms"; + private static readonly List _ignoreUrl = new() { "/job", }; - private static LogEventLevel DefaultGetLevel( - HttpContext ctx, + private static LogEventLevel DefaultGetLevel(HttpContext ctx, double _, Exception? ex) { @@ -31,4 +36,30 @@ private static LogEventLevel IgnoreRequest(HttpContext ctx) return _ignoreUrl.Any(s => path.StartsWith(s)) ? LogEventLevel.Verbose : LogEventLevel.Information; } + + /// + /// 从Request中增加附属属性 + /// + /// + /// + public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext) + { + var request = httpContext.Request; + + diagnosticContext.Set("RequestHost", request.Host); + diagnosticContext.Set("RequestScheme", request.Scheme); + diagnosticContext.Set("Protocol", request.Protocol); + diagnosticContext.Set("RequestIp", httpContext.GetRequestIp()); + + diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); + diagnosticContext.Set("Body", request.ContentLength > 0 ? request.GetRequestBody() : string.Empty); + + diagnosticContext.Set("ContentType", httpContext.Response.ContentType); + + var endpoint = httpContext.GetEndpoint(); + if (endpoint != null) + { + diagnosticContext.Set("EndpointName", endpoint.DisplayName); + } + } } \ No newline at end of file From 90157e470ded0a785bb7dee34de32bc780d26265 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Fri, 14 Apr 2023 11:46:44 +0800 Subject: [PATCH 160/289] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0Seq?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Api.csproj | 5 +++++ Blog.Core.Api/appsettings.json | 5 +++++ Blog.Core.Extensions/Blog.Core.Extensions.csproj | 1 + .../ServiceExtensions/SerilogSetup.cs | 16 +++++++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 3bf64399..5ff619d1 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -52,6 +52,11 @@ + + + + + diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 78ede19b..0523d64d 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -324,5 +324,10 @@ "FiedValue": "Blog.Core.Api" } ] + }, + "Seq": { + "Enabled": true, + "Address": "http://localhost:5341/", + "ApiKey": "" } } \ No newline at end of file diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 9eae3d92..1dfbbc48 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -22,6 +22,7 @@ + diff --git a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs index 0d8a0768..94975918 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Debugging; +using Serilog.Events; using System; using System.IO; @@ -26,8 +27,21 @@ public static IHostBuilder AddSerilogSetup(this IHostBuilder host) //配置日志库 .WriteToLogBatching(); + //配置Seq日志中心 + if (AppSettings.app("Seq", "Enabled").ObjToBool()) + { + var address = AppSettings.app("Seq", "Address"); + var apiKey = AppSettings.app("Seq", "ApiKey"); + if (!address.IsNullOrEmpty()) + { + loggerConfiguration = + loggerConfiguration.WriteTo.Seq(address, restrictedToMinimumLevel: LogEventLevel.Verbose, + apiKey: apiKey, eventBodyLimitBytes: 10485760); + } + } + Log.Logger = loggerConfiguration.CreateLogger(); - + //Serilog 内部日志 var file = File.CreateText(LogContextStatic.Combine($"SerilogDebug{DateTime.Now:yyyyMMdd}.txt")); SelfLog.Enable(TextWriter.Synchronized(file)); From 86e5d6f845d9bc98c5c850759b8c886c7a21f628 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 21 Apr 2023 22:13:41 +0800 Subject: [PATCH 161/289] Update swg-login.html --- Blog.Core.Api/wwwroot/swg-login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/wwwroot/swg-login.html b/Blog.Core.Api/wwwroot/swg-login.html index 6e7c6b09..022fec49 100644 --- a/Blog.Core.Api/wwwroot/swg-login.html +++ b/Blog.Core.Api/wwwroot/swg-login.html @@ -3,7 +3,7 @@ 登录 - 接口文档 - + From 720ef2472ada7c5f8bb88a3c3fa39a7f03a9ec04 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 21 Apr 2023 22:15:45 +0800 Subject: [PATCH 162/289] Update swg-login.html --- Blog.Core.Api/wwwroot/swg-login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/wwwroot/swg-login.html b/Blog.Core.Api/wwwroot/swg-login.html index 6e7c6b09..022fec49 100644 --- a/Blog.Core.Api/wwwroot/swg-login.html +++ b/Blog.Core.Api/wwwroot/swg-login.html @@ -3,7 +3,7 @@ 登录 - 接口文档 - + From 50a936bdebfa752b92784192a0aed2b311d0e7c7 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 22 Apr 2023 11:10:30 +0800 Subject: [PATCH 163/289] =?UTF-8?q?=E2=9C=A8=20=E5=A2=9E=E5=8A=A0IOption?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原有AppSetting 手写字符串去读取,极容易出错,扩展性也低:如果要修改或者增加值 就要到处都写 保留AppSetting基础上增加IOption方便读取配置,可反序列对象 增加IConfigurableOptions接口 可参考SeqOptions配置 配置文件名为Option或Options结尾,匹配时会忽略Option或Options SeqOptions->对应 AppSetting中Seq节点 可直接在控制器中注入使用 参考ValuesController private readonly SeqOptions _seqOptions; cto(IOptions seqOptions){ _seqOptions = seqOptions.Value; } --- Blog.Core.Api/Blog.Core.Api.csproj | 9 +- Blog.Core.Api/Blog.Core.xml | 2 +- Blog.Core.Api/Controllers/ValuesController.cs | 39 ++++-- Blog.Core.Api/Program.cs | 7 +- Blog.Core.Common/App.cs | 114 +++++++++++++++++- Blog.Core.Common/Core/InternalApp.cs | 8 ++ .../Extensions/RuntimeExtension.cs | 84 +++++++++++++ Blog.Core.Common/Helper/Appsettings.cs | 21 ++-- .../Option/Core/ConfigurableOptions.cs | 61 ++++++++++ .../Option/Core/IConfigurableOptions.cs | 11 ++ Blog.Core.Common/Option/SeqOptions.cs | 18 +++ .../ServiceExtensions/AllOptionRegister.cs | 21 ++++ .../ServiceExtensions/ApplicationSetup.cs | 24 ++++ .../ServiceExtensions/SerilogSetup.cs | 8 +- 14 files changed, 396 insertions(+), 31 deletions(-) create mode 100644 Blog.Core.Common/Extensions/RuntimeExtension.cs create mode 100644 Blog.Core.Common/Option/Core/ConfigurableOptions.cs create mode 100644 Blog.Core.Common/Option/Core/IConfigurableOptions.cs create mode 100644 Blog.Core.Common/Option/SeqOptions.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/AllOptionRegister.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/ApplicationSetup.cs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 5ff619d1..f3f9befa 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -5,10 +5,11 @@ Exe net6.0 - enable + enable Linux true + @@ -65,8 +66,8 @@ - - + + @@ -114,4 +115,4 @@ - + \ No newline at end of file diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 3cf56019..e664ed17 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -761,7 +761,7 @@ Values控制器 - + ValuesController diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 677eb138..03b37556 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -2,6 +2,7 @@ using Blog.Core.Common; using Blog.Core.Common.HttpContextUser; using Blog.Core.Common.Https.HttpPolly; +using Blog.Core.Common.Option; using Blog.Core.Common.WebApiClients.HttpApis; using Blog.Core.EventBus; using Blog.Core.EventBus.EventHandling; @@ -13,6 +14,7 @@ using Blog.Core.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; using System.Linq.Expressions; @@ -40,6 +42,7 @@ public class ValuesController : ControllerBase private readonly IDoubanApi _doubanApi; readonly IBlogArticleServices _blogArticleServices; private readonly IHttpPollyHelper _httpPollyHelper; + private readonly SeqOptions _seqOptions; /// /// ValuesController @@ -62,7 +65,8 @@ public ValuesController(IBlogArticleServices blogArticleServices , IUser user, IPasswordLibServices passwordLibServices , IBlogApi blogApi , IDoubanApi doubanApi - , IHttpPollyHelper httpPollyHelper) + , IHttpPollyHelper httpPollyHelper + , IOptions seqOptions) { // 测试 Authorize 和 mapper _mapper = mapper; @@ -82,6 +86,7 @@ public ValuesController(IBlogArticleServices blogArticleServices _blogArticleServices = blogArticleServices; // httpPolly _httpPollyHelper = httpPollyHelper; + _seqOptions = seqOptions.Value; } [HttpGet] @@ -127,7 +132,9 @@ public async Task> Get() /* * 测试 sql 查询 */ - var queryBySql = await _blogArticleServices.QuerySql("SELECT bsubmitter,btitle,bcontent,bCreateTime FROM BlogArticle WHERE bID>5"); + var queryBySql = + await _blogArticleServices.QuerySql( + "SELECT bsubmitter,btitle,bcontent,bCreateTime FROM BlogArticle WHERE bID>5"); /* * 测试按照指定列查询 @@ -151,7 +158,8 @@ public async Task> Get() * 【SQL语句】:UPDATE `BlogArticle` SET * `bsubmitter`=@bsubmitter,`IsDeleted`=@IsDeleted WHERE `bID`=@bID */ - var updateSql = await _blogArticleServices.Update(new { bsubmitter = $"laozhang{DateTime.Now.Millisecond}", IsDeleted = false, bID = 5 }); + var updateSql = await _blogArticleServices.Update(new + { bsubmitter = $"laozhang{DateTime.Now.Millisecond}", IsDeleted = false, bID = 5 }); // 测试模拟异常,全局异常过滤器拦截 @@ -364,6 +372,7 @@ public async Task FluentVaTest([FromBody] UserRegisterVo param) public void Put(int id, [FromBody] string value) { } + /// /// Delete方法 /// @@ -375,15 +384,18 @@ public void Delete(int id) } #region Apollo 配置 + /// /// 测试接入Apollo获取配置信息 /// [HttpGet("/apollo")] [AllowAnonymous] - public async Task>> GetAllConfigByAppllo([FromServices] IConfiguration configuration) + public async Task>> GetAllConfigByAppllo( + [FromServices] IConfiguration configuration) { return await Task.FromResult(configuration.AsEnumerable()); } + /// /// 通过此处的key格式为 xx:xx:x /// @@ -393,14 +405,17 @@ public async Task GetConfigByAppllo(string key) { return await Task.FromResult(AppSettings.app(key)); } + #endregion #region HttpPolly + [HttpPost] [AllowAnonymous] public async Task HttpPollyPost() { - var response = await _httpPollyHelper.PostAsync(HttpEnum.LocalHost, "/api/ElasticDemo/EsSearchTest", "{\"from\": 0,\"size\": 10,\"word\": \"非那雄安\"}"); + var response = await _httpPollyHelper.PostAsync(HttpEnum.LocalHost, "/api/ElasticDemo/EsSearchTest", + "{\"from\": 0,\"size\": 10,\"word\": \"非那雄安\"}"); return response; } @@ -409,17 +424,27 @@ public async Task HttpPollyPost() [AllowAnonymous] public async Task HttpPollyGet() { - return await _httpPollyHelper.GetAsync(HttpEnum.LocalHost, "/api/ElasticDemo/GetDetailInfo?esid=3130&esindex=chinacodex"); + return await _httpPollyHelper.GetAsync(HttpEnum.LocalHost, + "/api/ElasticDemo/GetDetailInfo?esid=3130&esindex=chinacodex"); } + #endregion [HttpPost] [AllowAnonymous] public string TestEnum(EnumDemoDto dto) => dto.Type.ToString(); + + [HttpGet] + [AllowAnonymous] + public string TestOption() + { + return _seqOptions.ToJson(); + } } + public class ClaimDto { public string Type { get; set; } public string Value { get; set; } } -} +} \ No newline at end of file diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index b36a02ac..b4929cc7 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -12,6 +12,7 @@ using Blog.Core.Extensions.ServiceExtensions; using Blog.Core.Filter; using Blog.Core.Hubs; +using Blog.Core.Serilog.Utility; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -23,8 +24,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Text; -using Blog.Core.Common.Https; -using Blog.Core.Serilog.Utility; var builder = WebApplication.CreateBuilder(args); @@ -39,6 +38,7 @@ }) .ConfigureAppConfiguration((hostingContext, config) => { + hostingContext.Configuration.ConfigureApplication(); config.Sources.Clear(); config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); config.AddConfigurationApollo("appsettings.apollo.json"); @@ -47,7 +47,7 @@ // 2、配置服务 builder.Services.AddSingleton(new AppSettings(builder.Configuration)); - +builder.Services.AddAllOptionRegister(); builder.Services.AddUiFilesZipSetup(builder.Environment); @@ -133,6 +133,7 @@ // 3、配置中间件 var app = builder.Build(); app.ConfigureApplication(); +app.UseApplicationSetup(); if (app.Environment.IsDevelopment()) { diff --git a/Blog.Core.Common/App.cs b/Blog.Core.Common/App.cs index c2e2e706..07abee9f 100644 --- a/Blog.Core.Common/App.cs +++ b/Blog.Core.Common/App.cs @@ -1,18 +1,45 @@ using Blog.Core.Common.Core; +using Blog.Core.Common.Extensions; using Blog.Core.Common.HttpContextUser; +using Blog.Core.Common.Option.Core; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Blog.Core.Common; public class App { - public static IServiceProvider RootServices => InternalApp.RootServices; + static App() + { + EffectiveTypes = Assemblies.SelectMany(GetTypes); + } + + private static bool _isRun; + + /// 是否正在运行 + public static bool IsBuild { get; set; } + + public static bool IsRun + { + get => _isRun; + set => _isRun = IsBuild = value; + } + + /// 应用有效程序集 + public static readonly IEnumerable Assemblies = RuntimeExtension.GetAllAssemblies(); + + /// 有效程序集类型 + public static readonly IEnumerable EffectiveTypes; + + public static IServiceProvider RootServices => IsRun || IsBuild ? InternalApp.RootServices : null; /// 获取Web主机环境,如,是否是开发环境,生产环境等 public static IWebHostEnvironment WebHostEnvironment => InternalApp.WebHostEnvironment; @@ -20,6 +47,9 @@ public class App /// 获取泛型主机环境,如,是否是开发环境,生产环境等 public static IHostEnvironment HostEnvironment => InternalApp.HostEnvironment; + /// 全局配置选项 + public static IConfiguration Configuration => InternalApp.Configuration; + /// /// 获取请求上下文 /// @@ -27,6 +57,8 @@ public class App public static IUser User => HttpContext == null ? null : RootServices?.GetService(); + #region Service + /// 解析服务提供器 /// /// @@ -34,7 +66,9 @@ public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBui { if (App.HostEnvironment == null || App.RootServices != null && InternalApp.InternalServices - .Where((u => u.ServiceType == (serviceType.IsGenericType ? serviceType.GetGenericTypeDefinition() : serviceType))) + .Where((u => + u.ServiceType == + (serviceType.IsGenericType ? serviceType.GetGenericTypeDefinition() : serviceType))) .Any((u => u.Lifetime == ServiceLifetime.Singleton))) return App.RootServices; HttpContext httpContext = App.HttpContext; @@ -56,25 +90,95 @@ public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBui } - public static TService GetService(bool mustBuild = true) where TService : class => App.GetService(typeof(TService), null, mustBuild) as TService; + public static TService GetService(bool mustBuild = true) where TService : class => + App.GetService(typeof(TService), null, mustBuild) as TService; /// 获取请求生存周期的服务 /// /// /// /// - public static TService GetService(IServiceProvider serviceProvider, bool mustBuild = true) where TService : class => App.GetService(typeof(TService), serviceProvider, mustBuild) as TService; + public static TService GetService(IServiceProvider serviceProvider, bool mustBuild = true) + where TService : class => App.GetService(typeof(TService), serviceProvider, mustBuild) as TService; /// 获取请求生存周期的服务 /// /// /// /// - public static object GetService(Type type, IServiceProvider serviceProvider = null, bool mustBuild = true) => (serviceProvider ?? App.GetServiceProvider(type, mustBuild)).GetService(type); + public static object GetService(Type type, IServiceProvider serviceProvider = null, bool mustBuild = true) => + (serviceProvider ?? App.GetServiceProvider(type, mustBuild)).GetService(type); + + #endregion + #region private + + /// 加载程序集中的所有类型 + /// + /// + private static IEnumerable GetTypes(Assembly ass) + { + Type[] source = Array.Empty(); + try + { + source = ass.GetTypes(); + } + catch + { + $@"Error load `{ass.FullName}` assembly.".WriteErrorLine(); + } + + return source.Where(u => u.IsPublic); + } + + #endregion + + #region Options + + /// 获取配置 + /// 强类型选项类 + /// TOptions + public static TOptions GetConfig() + where TOptions : class, IConfigurableOptions + { + TOptions instance = App.Configuration + .GetSection(ConfigurableOptions.GetConfigurationPath(typeof(TOptions))) + .Get(); + return instance; + } + + /// 获取选项 + /// 强类型选项类 + /// + /// TOptions public static TOptions GetOptions(IServiceProvider serviceProvider = null) where TOptions : class, new() { IOptions service = App.GetService>(serviceProvider ?? App.RootServices, false); return service?.Value; } + + /// 获取选项 + /// 强类型选项类 + /// + /// TOptions + public static TOptions GetOptionsMonitor(IServiceProvider serviceProvider = null) + where TOptions : class, new() + { + IOptionsMonitor service = + App.GetService>(serviceProvider ?? App.RootServices, false); + return service?.CurrentValue; + } + + /// 获取选项 + /// 强类型选项类 + /// + /// TOptions + public static TOptions GetOptionsSnapshot(IServiceProvider serviceProvider = null) + where TOptions : class, new() + { + IOptionsSnapshot service = App.GetService>(serviceProvider, false); + return service?.Value; + } + + #endregion } \ No newline at end of file diff --git a/Blog.Core.Common/Core/InternalApp.cs b/Blog.Core.Common/Core/InternalApp.cs index 62e04724..df16c911 100644 --- a/Blog.Core.Common/Core/InternalApp.cs +++ b/Blog.Core.Common/Core/InternalApp.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; namespace Blog.Core.Common.Core; @@ -19,6 +20,9 @@ public static class InternalApp /// 获取泛型主机环境 public static IHostEnvironment HostEnvironment; + /// 配置对象 + public static IConfiguration Configuration; + public static void ConfigureApplication(this WebApplicationBuilder wab) { HostEnvironment = wab.Environment; @@ -26,6 +30,10 @@ public static void ConfigureApplication(this WebApplicationBuilder wab) InternalServices = wab.Services; } + public static void ConfigureApplication(this IConfiguration configuration) + { + Configuration = configuration; + } public static void ConfigureApplication(this IHost app) { diff --git a/Blog.Core.Common/Extensions/RuntimeExtension.cs b/Blog.Core.Common/Extensions/RuntimeExtension.cs new file mode 100644 index 00000000..c4ddb0c8 --- /dev/null +++ b/Blog.Core.Common/Extensions/RuntimeExtension.cs @@ -0,0 +1,84 @@ +using Microsoft.Extensions.DependencyModel; +using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace Blog.Core.Common.Extensions; + +public static class RuntimeExtension +{ + /// + /// 获取项目程序集,排除所有的系统程序集(Microsoft.***、System.***等)、Nuget下载包 + /// + /// + public static IList GetAllAssemblies() + { + var list = new List(); + var deps = DependencyContext.Default; + var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package"); //排除所有的系统程序集、Nuget下载包 + foreach (var lib in libs) + { + try + { + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); + list.Add(assembly); + } + catch (Exception e) + { + Log.Debug(e, "GetAllAssemblies Exception:{ex}", e.Message); + } + } + return list; + } + + public static Assembly GetAssembly(string assemblyName) + { + return GetAllAssemblies().FirstOrDefault(assembly => assembly.FullName.Contains(assemblyName)); + } + + public static IList GetAllTypes() + { + var list = new List(); + foreach (var assembly in GetAllAssemblies()) + { + var typeInfos = assembly.DefinedTypes; + foreach (var typeInfo in typeInfos) + { + list.Add(typeInfo.AsType()); + } + } + + return list; + } + + public static IList GetTypesByAssembly(string assemblyName) + { + var list = new List(); + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName)); + var typeInfos = assembly.DefinedTypes; + foreach (var typeInfo in typeInfos) + { + list.Add(typeInfo.AsType()); + } + + return list; + } + + public static Type GetImplementType(string typeName, Type baseInterfaceType) + { + return GetAllTypes().FirstOrDefault(t => + { + if (t.Name == typeName && + t.GetTypeInfo().GetInterfaces().Any(b => b.Name == baseInterfaceType.Name)) + { + var typeInfo = t.GetTypeInfo(); + return typeInfo.IsClass && !typeInfo.IsAbstract && !typeInfo.IsGenericType; + } + + return false; + }); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/Appsettings.cs b/Blog.Core.Common/Helper/Appsettings.cs index d6d06651..83e3d7e7 100644 --- a/Blog.Core.Common/Helper/Appsettings.cs +++ b/Blog.Core.Common/Helper/Appsettings.cs @@ -22,9 +22,12 @@ public AppSettings(string contentPath) //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json"; Configuration = new ConfigurationBuilder() - .SetBasePath(contentPath) - .Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//这样的话,可以直接读目录里的json文件,而不是 bin 文件夹下的,所以不用修改复制属性 - .Build(); + .SetBasePath(contentPath) + .Add(new JsonConfigurationSource + { + Path = Path, Optional = false, ReloadOnChange = true + }) //这样的话,可以直接读目录里的json文件,而不是 bin 文件夹下的,所以不用修改复制属性 + .Build(); } public AppSettings(IConfiguration configuration) @@ -41,13 +44,14 @@ public static string app(params string[] sections) { try { - if (sections.Any()) { return Configuration[string.Join(":", sections)]; } } - catch (Exception) { } + catch (Exception) + { + } return ""; } @@ -78,10 +82,11 @@ public static string GetValue(string sectionsPath) { return Configuration[sectionsPath]; } - catch (Exception) { } + catch (Exception) + { + } return ""; - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Option/Core/ConfigurableOptions.cs b/Blog.Core.Common/Option/Core/ConfigurableOptions.cs new file mode 100644 index 00000000..adeea98a --- /dev/null +++ b/Blog.Core.Common/Option/Core/ConfigurableOptions.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; + +namespace Blog.Core.Common.Option.Core; + +public static class ConfigurableOptions +{ + /// 添加选项配置 + /// 选项类型 + /// 服务集合 + /// 服务集合 + public static IServiceCollection AddConfigurableOptions(this IServiceCollection services) + where TOptions : class, IConfigurableOptions + { + Type optionsType = typeof(TOptions); + string path = GetConfigurationPath(optionsType); + services.Configure(App.Configuration.GetSection(path)); + + return services; + } + + public static IServiceCollection AddConfigurableOptions(this IServiceCollection services, Type type) + { + string path = GetConfigurationPath(type); + var config = App.Configuration.GetSection(path); + + Type iOptionsChangeTokenSource = typeof(IOptionsChangeTokenSource<>); + Type iConfigureOptions = typeof(IConfigureOptions<>); + Type configurationChangeTokenSource = typeof(ConfigurationChangeTokenSource<>); + Type namedConfigureFromConfigurationOptions = typeof(NamedConfigureFromConfigurationOptions<>); + iOptionsChangeTokenSource = iOptionsChangeTokenSource.MakeGenericType(type); + iConfigureOptions = iConfigureOptions.MakeGenericType(type); + configurationChangeTokenSource = configurationChangeTokenSource.MakeGenericType(type); + namedConfigureFromConfigurationOptions = namedConfigureFromConfigurationOptions.MakeGenericType(type); + + services.AddOptions(); + services.AddSingleton(iOptionsChangeTokenSource, + Activator.CreateInstance(configurationChangeTokenSource, Options.DefaultName, config) ?? throw new InvalidOperationException()); + return services.AddSingleton(iConfigureOptions, + Activator.CreateInstance(namedConfigureFromConfigurationOptions, Options.DefaultName, config) ?? throw new InvalidOperationException()); + } + + /// 获取配置路径 + /// 选项类型 + /// + public static string GetConfigurationPath(Type optionsType) + { + var endPath = new[] {"Option", "Options"}; + var configurationPath = optionsType.Name; + foreach (var s in endPath) + { + if (configurationPath.EndsWith(s)) + { + return configurationPath[..^s.Length]; + } + } + + return configurationPath; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Option/Core/IConfigurableOptions.cs b/Blog.Core.Common/Option/Core/IConfigurableOptions.cs new file mode 100644 index 00000000..71568268 --- /dev/null +++ b/Blog.Core.Common/Option/Core/IConfigurableOptions.cs @@ -0,0 +1,11 @@ +namespace Blog.Core.Common.Option.Core; + +/// +/// 应用选项依赖接口
+/// 自动注入配置文件
+/// 文件名为Option或Options结尾 +///
+public interface IConfigurableOptions +{ + +} \ No newline at end of file diff --git a/Blog.Core.Common/Option/SeqOptions.cs b/Blog.Core.Common/Option/SeqOptions.cs new file mode 100644 index 00000000..944aa7bc --- /dev/null +++ b/Blog.Core.Common/Option/SeqOptions.cs @@ -0,0 +1,18 @@ +using Blog.Core.Common.Option.Core; + +namespace Blog.Core.Common.Option; + +public class SeqOptions : IConfigurableOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } + + /// + /// 地址 + /// + public string Address { get; set; } + + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/AllOptionRegister.cs b/Blog.Core.Extensions/ServiceExtensions/AllOptionRegister.cs new file mode 100644 index 00000000..28e1739b --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/AllOptionRegister.cs @@ -0,0 +1,21 @@ +using Blog.Core.Common; +using Blog.Core.Common.Option.Core; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +namespace Blog.Core.Extensions.ServiceExtensions; + +public static class AllOptionRegister +{ + public static void AddAllOptionRegister(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + foreach (var optionType in App.EffectiveTypes.Where(s => + !s.IsInterface && typeof(IConfigurableOptions).IsAssignableFrom(s))) + { + services.AddConfigurableOptions(optionType); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/ApplicationSetup.cs b/Blog.Core.Extensions/ServiceExtensions/ApplicationSetup.cs new file mode 100644 index 00000000..793df2c8 --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/ApplicationSetup.cs @@ -0,0 +1,24 @@ +using Blog.Core.Common; +using Microsoft.AspNetCore.Builder; +using Serilog; + +namespace Blog.Core.Extensions.ServiceExtensions; + +public static class ApplicationSetup +{ + public static void UseApplicationSetup(this WebApplication app) + { + app.Lifetime.ApplicationStarted.Register(() => + { + App.IsRun = true; + }); + + app.Lifetime.ApplicationStopped.Register(() => + { + App.IsRun = false; + + //清除日志 + Log.CloseAndFlush(); + }); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs index 94975918..30ab29f8 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs @@ -8,6 +8,7 @@ using Serilog.Events; using System; using System.IO; +using Blog.Core.Common.Option; namespace Blog.Core.Extensions.ServiceExtensions; @@ -27,11 +28,12 @@ public static IHostBuilder AddSerilogSetup(this IHostBuilder host) //配置日志库 .WriteToLogBatching(); + var option = App.GetOptions(); //配置Seq日志中心 - if (AppSettings.app("Seq", "Enabled").ObjToBool()) + if (option.Enabled) { - var address = AppSettings.app("Seq", "Address"); - var apiKey = AppSettings.app("Seq", "ApiKey"); + var address = option.Address; + var apiKey = option.ApiKey; if (!address.IsNullOrEmpty()) { loggerConfiguration = From 014ab8de749b3b1c62408ec91727cabbd2379018 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sat, 29 Apr 2023 12:03:12 +0800 Subject: [PATCH 164/289] remove extra code --- Blog.Core.Api/Blog.Core.xml | 2 -- Blog.Core.Api/Controllers/WeChatController.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index c6b33db9..45e38404 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1147,8 +1147,6 @@ 微信公众号管理 - 作者:胡丁文 - 时间:2020-3-29 21:24:12 diff --git a/Blog.Core.Api/Controllers/WeChatController.cs b/Blog.Core.Api/Controllers/WeChatController.cs index a27762e8..5c7e5c6f 100644 --- a/Blog.Core.Api/Controllers/WeChatController.cs +++ b/Blog.Core.Api/Controllers/WeChatController.cs @@ -8,8 +8,6 @@ namespace Blog.Core.Controllers { /// /// 微信公众号管理 - /// 作者:胡丁文 - /// 时间:2020-3-29 21:24:12 /// [Route("api/[controller]/[action]")] [ApiController] From 7ddec93479903b713b63b0fdd63eead3c15dcf70 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sat, 29 Apr 2023 12:03:12 +0800 Subject: [PATCH 165/289] remove extra code --- Blog.Core.Api/Blog.Core.Model.xml | 137 ------ Blog.Core.Api/Blog.Core.xml | 155 ------ Blog.Core.Api/Controllers/TrojanController.cs | 456 ------------------ Blog.Core.Api/Controllers/WeChatController.cs | 2 - Blog.Core.IServices/ITrojanUsersServices.cs | 14 - Blog.Core.Model/Models/TrojanCusServers.cs | 26 - Blog.Core.Model/Models/TrojanDetails.cs | 63 --- Blog.Core.Model/Models/TrojanServers.cs | 31 -- Blog.Core.Model/Models/TrojanUrlServers.cs | 26 - Blog.Core.Model/Models/TrojanUsers.cs | 39 -- .../ViewModels/TrojanLimitFlowDto.cs | 23 - Blog.Core.Model/ViewModels/TrojanServerDto.cs | 14 - .../ViewModels/TrojanServerSpliceDto.cs | 28 -- .../ViewModels/TrojanUseDetailDto.cs | 35 -- Blog.Core.Services/TrojanUsersServices.cs | 18 - .../QuartzNet/Jobs/Job_Trojan_Quartz.cs | 81 ---- 16 files changed, 1148 deletions(-) delete mode 100644 Blog.Core.Api/Controllers/TrojanController.cs delete mode 100644 Blog.Core.IServices/ITrojanUsersServices.cs delete mode 100644 Blog.Core.Model/Models/TrojanCusServers.cs delete mode 100644 Blog.Core.Model/Models/TrojanDetails.cs delete mode 100644 Blog.Core.Model/Models/TrojanServers.cs delete mode 100644 Blog.Core.Model/Models/TrojanUrlServers.cs delete mode 100644 Blog.Core.Model/Models/TrojanUsers.cs delete mode 100644 Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs delete mode 100644 Blog.Core.Model/ViewModels/TrojanServerDto.cs delete mode 100644 Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs delete mode 100644 Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs delete mode 100644 Blog.Core.Services/TrojanUsersServices.cs delete mode 100644 Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index bf3d350d..79666d28 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1260,76 +1260,6 @@ Tibug 博文
- - - users自定义服务器 - - - - - 用户流量每月汇总表 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Trojan服务器 - - - - - users自定义URL服务器 - - - - - Trojan用户 - - - - - 历史流量记录 - - 用户跟角色关联表 @@ -2787,73 +2717,6 @@ - - - 限制流量dto - 作者:胡丁文 - 时间:2020-4-27 16:57:07 - - - - - 用户 - - - - - 流量(-1为无限,单位为最小单位byte) - - - - - Trojan服务器拼接服务器和订阅地址 - - - - - 普通订阅连接 - - - - - clash订阅连接 - - - - - 备用clash订阅连接 - - - - - Trojan用户流量统计分组 - - - - - 用户ID - - - - - 月度 - - - - - 上传流量 - - - - - 下载流量 - - - - - 下载流量 - - 微信接口消息DTO diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index c6b33db9..05783a69 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -664,159 +664,6 @@ - - - 获取Trojan用户 - - - - - - - - - 获取Trojan用户-下拉列表用 - - - - - - 添加Trojan用户 - - - - - - - 更新Trojan用户 - - - - - - - 删除用户 - - - - - - - 重置流量 - - - - - - - 限制流量 - - - - - - - 重置链接密码 - - - - - - - 获取Trojan服务器 - - - - - - 获取拼接后的Trojan服务器 - - passwordshow - - - - - 删除Trojan服务器 - - - - - - - 更新Trojan服务器 - - - - - - - 添加Trojan服务器 - - - - - - - 获取Cus服务器 - - - - - - 删除Cus服务器 - - - - - - - 更新Cus服务器 - - - - - - - 添加Cus服务器 - - - - - - - 获取Url服务器 - - - - - - 删除Url服务器 - - - - - - - 更新Url服务器 - - - - - - - 添加Url服务器 - - - - - - - 获取订阅数据 - - 链接密码 - 是否使用base64加密 - - 用户管理 @@ -1147,8 +994,6 @@ 微信公众号管理 - 作者:胡丁文 - 时间:2020-3-29 21:24:12 diff --git a/Blog.Core.Api/Controllers/TrojanController.cs b/Blog.Core.Api/Controllers/TrojanController.cs deleted file mode 100644 index d8d005dc..00000000 --- a/Blog.Core.Api/Controllers/TrojanController.cs +++ /dev/null @@ -1,456 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Blog.Core.Common; -using Blog.Core.Common.Extensions; -using Blog.Core.Common.Helper; -using Blog.Core.Common.HttpContextUser; -using Blog.Core.IServices; -using Blog.Core.IServices.BASE; -using Blog.Core.Model; -using Blog.Core.Model.Models; -using Blog.Core.Model.ViewModels; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Math; - -namespace Blog.Core.Controllers -{ - [Route("api/[controller]/[action]")] - [ApiController] - [Authorize(Permissions.Name)] - public class TrojanController : ControllerBase - { - private ITrojanUsersServices _trojanUsersServices; - public IBaseServices _baseServicesServers; - public IBaseServices _baseServicesDetails; - public IBaseServices _baseServicesCusServers; - public IBaseServices _baseServicesUrlServers; - private IUser _user; - public TrojanController(ITrojanUsersServices trojanUsersServices,IUser user - , IBaseServices baseServicesServers - , IBaseServices baseServicesDetails - , IBaseServices baseServicesCusServers - , IBaseServices baseServicesUrlServers) - { - _baseServicesDetails = baseServicesDetails; - _baseServicesServers = baseServicesServers; - _trojanUsersServices = trojanUsersServices; - _baseServicesCusServers = baseServicesCusServers; - _baseServicesUrlServers = baseServicesUrlServers; - _user = user; - } - /// - /// 获取Trojan用户 - /// - /// - /// - /// - /// - [HttpGet] - public async Task>> GetUser([FromQuery]PaginationModel pagination, [FromQuery] string name, [FromQuery] bool isuse) - { - var whereFind = LinqHelper.True(); - if (!string.IsNullOrEmpty(name)) - whereFind = whereFind.And(t=>t.username.Contains(name)); - if (isuse) - whereFind = whereFind.And(t => t.upload > 0 || t.download > 0); - var data = await _trojanUsersServices.QueryPage(whereFind, pagination.PageIndex, pagination.PageSize); - if (data.data.Count > 0) - { - var ids = data.data.Select(t => t.id).ToList(); - var where = LinqHelper.True(); - where = where.And(t => ids.Contains(t.userId));//.And(t => t.calDate < DateTime.Now).And(t => t.calDate > DateTime.Now.AddMonths(-12)); - var userDetails = await _baseServicesDetails.Query(where); - foreach (var trojanUser in data.data) - { - var ls = from t in userDetails - where t.userId == trojanUser.id - group t by new { moth = t.calDate.ToString("yyyy-MM"), id = t.userId } into g - orderby g.Key.moth descending - select new TrojanUseDetailDto { userId = g.Key.id, moth = g.Key.moth, up = g.Sum(t => Convert.ToDecimal(t.upload)), down = g.Sum(t => Convert.ToDecimal(t.download)) }; - var lsData = ls.ToList(); - trojanUser.useList = lsData; - } - } - return MessageModel>.Success("获取成功", data); - } - - /// - /// 获取Trojan用户-下拉列表用 - /// - /// - [HttpGet] - public async Task> GetAllTrojanUser() - { - var data = await _trojanUsersServices.QueryTable("select id,username from users"); - return MessageModel.Success("获取成功", data); - } - /// - /// 添加Trojan用户 - /// - /// - /// - [HttpPost] - public async Task> AddUser([FromBody]TrojanUsers user) - { - var find = await _trojanUsersServices.Query(t => t.username == user.username); - if(find!=null && find.Count>0) return MessageModel.Fail("用户名已存在"); - var pass = StringHelper.GetGUID(); - var passEcrypt = ShaHelper.Sha224(pass); - //user.quota = 0; - user.upload = 0; - user.download = 0; - user.password = passEcrypt; - user.passwordshow = pass; - var data = await _trojanUsersServices.Add(user); - return MessageModel.Success("添加成功", data); - } - /// - /// 更新Trojan用户 - /// - /// - /// - [HttpPut] - public async Task> UpdateUser([FromBody]TrojanUsers user) - { - var find = await _trojanUsersServices.QueryById(user.id); - if (find == null) return MessageModel.Fail("用户名不存在"); - find.username = user.username; - var data = await _trojanUsersServices.Update(find, new List { "username" }); - return MessageModel.Success("更新成功", data); - } - - /// - /// 删除用户 - /// - /// - /// - [HttpPut] - public async Task> DelUser([FromBody]int[] users) - { - var data = await _trojanUsersServices.Query(t => users.Contains(t.id)); - var list = data.Select(t => t.id.ToString()).ToArray(); - await _trojanUsersServices.DeleteByIds(list); - return MessageModel.Success("删除成功"); - } - /// - /// 重置流量 - /// - /// - /// - [HttpPut] - public async Task> ResetFlow([FromBody]int[] users) - { - var data = await _trojanUsersServices.Query(t => users.Contains(t.id)); - foreach (var item in data) - { - item.upload = 0; - item.download = 0; - await _trojanUsersServices.Update(item, new List { "upload", "download" }); - } - return MessageModel.Success("重置流量成功"); - } - /// - /// 限制流量 - /// - /// - /// - [HttpPut] - public async Task> LimitFlow([FromBody] TrojanLimitFlowDto limit) - { - var data = await _trojanUsersServices.Query(t => limit.users.Contains(t.id)); - foreach (var item in data) - { - item.quota = limit.quota; - await _trojanUsersServices.Update(item, new List { "quota" }); - } - return MessageModel.Success("限制流量成功"); - } - /// - /// 重置链接密码 - /// - /// - /// - [HttpPut] - public async Task> ResetPass([FromBody]int[] users) - { - var data = await _trojanUsersServices.Query(t => users.Contains(t.id)); - var pass = StringHelper.GetGUID(); - var passEcrypt = ShaHelper.Sha224(pass); - foreach (var item in data) - { - item.password = passEcrypt; - item.passwordshow = pass; - await _trojanUsersServices.Update(item, new List { "password" , "passwordshow" }); - } - return MessageModel.Success("重置链接密码成功"); - } - /// - /// 获取Trojan服务器 - /// - /// - [HttpGet] - public async Task>> GetServers() - { - var data = await _baseServicesServers.Query(); - data = data.OrderBy(t => t.servername).ToList(); - return MessageModel>.Success("获取成功", data); - } - /// - /// 获取拼接后的Trojan服务器 - /// - /// passwordshow - /// - [HttpGet] - public async Task> GetSpliceServers(string id) - { - var data = await _baseServicesServers.Query(); - data = data.OrderBy(t => t.servername).ToList(); - var res = new TrojanServerSpliceDto(); - res.normalApi = AppSettings.app(new string[] { "trojan", "normalApi" }).ObjToString(); - res.clashApi = AppSettings.app(new string[] { "trojan", "clashApi" }).ObjToString(); - res.clashApiBackup = AppSettings.app(new string[] { "trojan", "clashApiBackup" }).ObjToString(); - foreach (var item in data) - { - var serverSplice = GetSplice(item, id); - res.list.Add(new TrojanServerDto { name = item.servername, value = serverSplice }); - } - return MessageModel.Success("获取成功", res); ; - - } - /// - /// 删除Trojan服务器 - /// - /// - /// - [HttpPut] - public async Task>> DelServers([FromBody]int[] servers) - { - var data = await _baseServicesServers.DeleteByIds(servers.Select(t=>t.ToString()).ToArray()); - if (data) - return MessageModel>.Success("删除成功"); - else - return MessageModel>.Fail("删除失败"); - } - /// - /// 更新Trojan服务器 - /// - /// - /// - [HttpPut] - public async Task>> UpdateServers(TrojanServers server) - { - var data = await _baseServicesServers.Update(server); - return MessageModel>.Success("更新成功"); - } - /// - /// 添加Trojan服务器 - /// - /// - /// - [HttpPost] - public async Task>> AddServers(TrojanServers server) - { - var data = await _baseServicesServers.Add(server); - return MessageModel>.Success("添加成功"); - } - - /// - /// 获取Cus服务器 - /// - /// - [HttpGet] - public async Task>> GetCusServers() - { - var data = await _baseServicesCusServers.Query(); - data = data.OrderBy(t => t.servername).ToList(); - return MessageModel>.Success("获取成功", data); - } - /// - /// 删除Cus服务器 - /// - /// - /// - [HttpPut] - public async Task>> DelCusServers([FromBody] int[] servers) - { - var data = await _baseServicesCusServers.DeleteByIds(servers.Select(t => t.ToString()).ToArray()); - if (data) - return MessageModel>.Success("删除成功"); - else - return MessageModel>.Fail("删除失败"); - } - /// - /// 更新Cus服务器 - /// - /// - /// - [HttpPut] - public async Task>> UpdateCusServers(TrojanCusServers server) - { - var data = await _baseServicesCusServers.Update(server); - return MessageModel>.Success("更新成功"); - } - /// - /// 添加Cus服务器 - /// - /// - /// - [HttpPost] - public async Task>> AddCusServers(TrojanCusServers server) - { - var data = await _baseServicesCusServers.Add(server); - return MessageModel>.Success("添加成功"); - } - - - /// - /// 获取Url服务器 - /// - /// - [HttpGet] - public async Task>> GetUrlServers() - { - var data = await _baseServicesUrlServers.Query(); - data = data.OrderBy(t => t.servername).ToList(); - return MessageModel>.Success("获取成功", data); - } - /// - /// 删除Url服务器 - /// - /// - /// - [HttpPut] - public async Task>> DelUrlServers([FromBody] int[] servers) - { - var data = await _baseServicesUrlServers.DeleteByIds(servers.Select(t => t.ToString()).ToArray()); - if (data) - return MessageModel>.Success("删除成功"); - else - return MessageModel>.Fail("删除失败"); - } - /// - /// 更新Url服务器 - /// - /// - /// - [HttpPut] - public async Task>> UpdateUrlServers(TrojanUrlServers server) - { - var data = await _baseServicesUrlServers.Update(server); - return MessageModel>.Success("更新成功"); - } - /// - /// 添加Url服务器 - /// - /// - /// - [HttpPost] - public async Task>> AddUrlServers(TrojanUrlServers server) - { - var data = await _baseServicesUrlServers.Add(server); - return MessageModel>.Success("添加成功"); - } - private string GetSplice(TrojanServers item,string passwordshow) - { - var sni = string.IsNullOrEmpty(item.serverpeer) ? item.serveraddress : item.serverpeer; - if ("0".Equals(item.servertype)) - return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?allowinsecure=0&tfo=0&fp=chrome&peer={sni}&host={sni}&sni={sni}#{item.servername}"; - else if ("1".Equals(item.servertype)) - { - - return $"trojan://{passwordshow}@{item.serveraddress}:{item.serverport}?wspath={item.serverpath}&ws=1&peer={sni}&path={item.serverpath}&host={sni}&fp=chrome&type=ws&sni={sni}#{item.servername}"; - } - else - return $"servertype:({item.servertype})错误"; - } - private List GetSplice(List items, string passwordshow) - { - List ls = new List(); - foreach (var item in items) - { - ls.Add(GetSplice(item, passwordshow)); - } - return ls; - } - /// - /// 获取订阅数据 - /// - /// 链接密码 - /// 是否使用base64加密 - /// - [HttpGet] - [AllowAnonymous] - public async Task RSS(string id,bool isUseBase64=true) - { - StringBuilder sb = new StringBuilder(); - try - { - var user = (await _trojanUsersServices.Query(t => t.passwordshow == id)).FirstOrDefault(); - if (user == null) throw new Exception("用户不存在"); - var data = await _baseServicesServers.Query(t => (t.userid == user.id || t.userid <= 0) && t.serverenable); - if (data != null) - { - data = data.OrderBy(t => t.servername).ToList(); - foreach (var item in data) - { - sb.AppendLine(GetSplice(item, user.passwordshow)); - } - } - var cusData = await _baseServicesCusServers.Query(t=> (t.userid == user.id || t.userid <=0) && t.serverenable); - if (cusData != null) - { - cusData = cusData.OrderBy(t => t.servername).ToList(); - foreach (var item in cusData) - { - sb.AppendLine(item.serveraddress); - } - } - var urlData = await _baseServicesUrlServers.Query(t => (t.userid == user.id || t.userid <= 0) && t.serverenable); - if (urlData != null) - { - urlData = urlData.OrderBy(t => t.servername).ToList(); - foreach (var item in urlData) - { - try - { - var urlStrObj = await HttpHelper.GetAsync(item.serveraddress); - var lines = ""; - try - { - lines = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(urlStrObj)); - } - catch (Exception) - { - lines = urlStrObj; - } - finally - { - sb.AppendLine(lines); - } - } - catch (Exception ex) - { - sb.AppendLine($"trojan://xxxxxx@xxxxxx.xx:443?allowinsecure=0&tfo=0#{ex.Message}"); - } - } - } - } - catch (Exception ex) - { - sb.AppendLine($"trojan://xxxxxx@xxxxxx.xx:443?allowinsecure=0&tfo=0#{ex.Message}"); - } - if (isUseBase64) - { - return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sb.ToString())); - } - else{ - return sb.ToString(); - } - } - } -} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/WeChatController.cs b/Blog.Core.Api/Controllers/WeChatController.cs index a27762e8..5c7e5c6f 100644 --- a/Blog.Core.Api/Controllers/WeChatController.cs +++ b/Blog.Core.Api/Controllers/WeChatController.cs @@ -8,8 +8,6 @@ namespace Blog.Core.Controllers { /// /// 微信公众号管理 - /// 作者:胡丁文 - /// 时间:2020-3-29 21:24:12 /// [Route("api/[controller]/[action]")] [ApiController] diff --git a/Blog.Core.IServices/ITrojanUsersServices.cs b/Blog.Core.IServices/ITrojanUsersServices.cs deleted file mode 100644 index 92bac1d8..00000000 --- a/Blog.Core.IServices/ITrojanUsersServices.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Blog.Core.IServices.BASE; -using Blog.Core.Model.Models; - -namespace Blog.Core.IServices -{ - /// - /// TrojanUsersServices - /// - public interface ITrojanUsersServices : IBaseServices - { - - } -} - diff --git a/Blog.Core.Model/Models/TrojanCusServers.cs b/Blog.Core.Model/Models/TrojanCusServers.cs deleted file mode 100644 index 03f02539..00000000 --- a/Blog.Core.Model/Models/TrojanCusServers.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using System; -using System.Linq; -using System.Text; -using SqlSugar; - -namespace Blog.Core.Model.Models -{ - /// - ///users自定义服务器 - /// - [SugarTable("users_cus", "users自定义服务器")] - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') - public partial class TrojanCusServers - { - - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int id { set; get; } - public int userid { get; set; } - public string servername { set; get; } - public string serveraddress { set; get; } - [SugarColumn(IsNullable = true)] - public string serverremark { get; set; } - public bool serverenable { get; set; } - } -} diff --git a/Blog.Core.Model/Models/TrojanDetails.cs b/Blog.Core.Model/Models/TrojanDetails.cs deleted file mode 100644 index dda53d9b..00000000 --- a/Blog.Core.Model/Models/TrojanDetails.cs +++ /dev/null @@ -1,63 +0,0 @@ - -//模板自动生成(请勿修改) -//作者:胡丁文 -using System; -using System.Linq; -using System.Text; -using SqlSugar; - -namespace Blog.Core.Model.Models -{ - /// - ///用户流量每月汇总表 - /// - [SugarTable("users_detail", "用户流量每月汇总表")] - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') - public partial class TrojanDetails - { - - /// - /// - /// - [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] - public int id { get; set; } - - /// - /// - /// - public int userId { get; set; } - - /// - /// - /// - public DateTime calDate { get; set; } - - /// - /// - /// - public ulong download { get; set; } - - /// - /// - /// - public ulong upload { get; set; } - - /// - /// - /// - [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } - - /// - /// - /// - [SugarColumn(IsNullable = true)] - public string CreateBy { get; set; } - - /// - /// - /// - [SugarColumn(IsNullable = true)] - public DateTime? CreateTime { get; set; } - } -} diff --git a/Blog.Core.Model/Models/TrojanServers.cs b/Blog.Core.Model/Models/TrojanServers.cs deleted file mode 100644 index d9d8275f..00000000 --- a/Blog.Core.Model/Models/TrojanServers.cs +++ /dev/null @@ -1,31 +0,0 @@ - -using System; -using System.Linq; -using System.Text; -using SqlSugar; - -namespace Blog.Core.Model.Models -{ - /// - ///Trojan服务器 - /// - [SugarTable("servers", "Trojan服务器")] - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') - public partial class TrojanServers - { - - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int id { set; get; } - public int userid { get; set; } - public string servername { set; get; } - public string serveraddress { set; get; } - public int serverport { get; set; } - [SugarColumn(IsNullable = true)] - public string serverremark { get; set; } - public bool serverenable { get; set; } - public string serverpeer { get; set; } - [SugarColumn(IsNullable = true)] - public string serverpath { get; set; } - public string servertype { get; set; } - } -} diff --git a/Blog.Core.Model/Models/TrojanUrlServers.cs b/Blog.Core.Model/Models/TrojanUrlServers.cs deleted file mode 100644 index db48343a..00000000 --- a/Blog.Core.Model/Models/TrojanUrlServers.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using System; -using System.Linq; -using System.Text; -using SqlSugar; - -namespace Blog.Core.Model.Models -{ - /// - ///users自定义URL服务器 - /// - [SugarTable("users_url", "users自定义URL服务器")] - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') - public partial class TrojanUrlServers - { - - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int id { set; get; } - public int userid { get; set; } - public string servername { set; get; } - public string serveraddress { set; get; } - [SugarColumn(IsNullable = true)] - public string serverremark { get; set; } - public bool serverenable { get; set; } - } -} diff --git a/Blog.Core.Model/Models/TrojanUsers.cs b/Blog.Core.Model/Models/TrojanUsers.cs deleted file mode 100644 index 796c044e..00000000 --- a/Blog.Core.Model/Models/TrojanUsers.cs +++ /dev/null @@ -1,39 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Blog.Core.Model.ViewModels; -using SqlSugar; - -namespace Blog.Core.Model.Models -{ - /// - ///Trojan用户 - /// - [SugarTable("users", "Trojan用户表")] - [TenantAttribute("WMBLOG_MYSQL_2")] //('代表是哪个数据库,名字是appsettings.json 的 ConnId') - public partial class TrojanUsers - { - - [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)] - public int id { set; get; } - public string username { set; get; } - public string password { set; get; } - public Int64 quota { set; get; } - public UInt64 download { set; get; } - public UInt64 upload { set; get; } - public string passwordshow { set; get; } - [SugarColumn(IsNullable = true)] - public int CreateId { get; set; } - [SugarColumn(IsNullable = true)] - public string CreateBy { get; set; } - [SugarColumn(IsNullable = true)] - public DateTime? CreateTime { get; set; } - /// - /// 历史流量记录 - /// - [SugarColumn(IsIgnore = true)] - public List useList { get; set; } - } -} diff --git a/Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs b/Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs deleted file mode 100644 index c0efd347..00000000 --- a/Blog.Core.Model/ViewModels/TrojanLimitFlowDto.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Blog.Core.Model.ViewModels -{ - /// - /// 限制流量dto - /// 作者:胡丁文 - /// 时间:2020-4-27 16:57:07 - /// - public class TrojanLimitFlowDto - { - /// - /// 用户 - /// - public int[] users { get; set; } - /// - /// 流量(-1为无限,单位为最小单位byte) - /// - public Int64 quota { get; set; } - } -} diff --git a/Blog.Core.Model/ViewModels/TrojanServerDto.cs b/Blog.Core.Model/ViewModels/TrojanServerDto.cs deleted file mode 100644 index 89cb87e3..00000000 --- a/Blog.Core.Model/ViewModels/TrojanServerDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Blog.Core.Model.ViewModels -{ - public class TrojanServerDto - { - public string name { get; set; } - public string value { get; set; } - } -} diff --git a/Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs b/Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs deleted file mode 100644 index e83adfd5..00000000 --- a/Blog.Core.Model/ViewModels/TrojanServerSpliceDto.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Blog.Core.Model.ViewModels -{ - /// - /// Trojan服务器拼接服务器和订阅地址 - /// - public class TrojanServerSpliceDto - { - /// - /// 普通订阅连接 - /// - public string normalApi { get; set; } - /// - /// clash订阅连接 - /// - public string clashApi { get; set; } - /// - /// 备用clash订阅连接 - /// - public string clashApiBackup { get; set; } - public List list { get; set; } = new List(); - } -} diff --git a/Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs b/Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs deleted file mode 100644 index 7746b105..00000000 --- a/Blog.Core.Model/ViewModels/TrojanUseDetailDto.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Blog.Core.Model.ViewModels -{ - /// - /// Trojan用户流量统计分组 - /// - public class TrojanUseDetailDto - { - /// - /// 用户ID - /// - public int userId { get; set; } - /// - /// 月度 - /// - public string moth { get; set; } - /// - /// 上传流量 - /// - public decimal up { get; set; } - /// - /// 下载流量 - /// - public decimal down { get; set; } - /// - /// 下载流量 - /// - public decimal total { get { return up + down; } } - } -} diff --git a/Blog.Core.Services/TrojanUsersServices.cs b/Blog.Core.Services/TrojanUsersServices.cs deleted file mode 100644 index f42f51d9..00000000 --- a/Blog.Core.Services/TrojanUsersServices.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Blog.Core.Common; -using Blog.Core.IRepository.Base; -using Blog.Core.IServices; -using Blog.Core.Model.Models; -using Blog.Core.Services.BASE; -using System.Linq; -using System.Threading.Tasks; - -namespace Blog.Core.Services -{ - /// - /// TrojanUsersServices - /// - public class TrojanUsersServices : BaseServices, ITrojanUsersServices - { - - } -} diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs deleted file mode 100644 index 9d2d4cea..00000000 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_Trojan_Quartz.cs +++ /dev/null @@ -1,81 +0,0 @@ - -using Blog.Core.IServices; -using Blog.Core.IServices.BASE; -using Blog.Core.Model.Models; -using Blog.Core.Repository.UnitOfWorks; -using Microsoft.Extensions.Logging; -using Quartz; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -/// -/// 这里要注意下,命名空间和程序集是一样的,不然反射不到(任务类要去JobSetup添加注入) -/// -namespace Blog.Core.Tasks -{ - public class Job_Trojan_Quartz : JobBase, IJob - { - private readonly IUnitOfWorkManage _unitOfWorkManage; - public IBaseServices_DetailServices; - private readonly ITrojanUsersServices _TrojanUsers; - private readonly ILogger _logger; - - public Job_Trojan_Quartz(IUnitOfWorkManage unitOfWorkManage, IBaseServices iusers_DetailServices, ITrojanUsersServices trojanUsers, ILogger logger, ITasksQzServices tasksQzServices, ITasksLogServices tasksLogServices) - : base(tasksQzServices, tasksLogServices) - { - _tasksQzServices = tasksQzServices; - _unitOfWorkManage = unitOfWorkManage; - _DetailServices = iusers_DetailServices; - _TrojanUsers = trojanUsers; - _logger = logger; - } - public async Task Execute(IJobExecutionContext context) - { - //var param = context.MergedJobDataMap; - // 可以直接获取 JobDetail 的值 - var jobKey = context.JobDetail.Key; - var jobId = jobKey.Name; - var executeLog = await ExecuteJob(context, async () => await Run(context, jobId.ObjToInt())); - - } - public async Task Run(IJobExecutionContext context, int jobid) - { - if (jobid > 0) - { - try - { - //获取每月用户的数据 - _unitOfWorkManage.BeginTran(); - var now = DateTime.Now.AddMonths(-1); - - var list = await _TrojanUsers.Query(); - List ls = new List(); - foreach (var us in list) - { - TrojanDetails u = new TrojanDetails(); - u.calDate = now; - u.userId = us.id; - u.download = us.download; - u.upload = us.upload; - //清零 - us.download = 0; - us.upload = 0; - ls.Add(u); - } - await _TrojanUsers.Update(list); - await _DetailServices.Add(ls); - _unitOfWorkManage.CommitTran(); - } - catch (Exception) - { - _unitOfWorkManage.RollbackTran(); - throw; - } - } - } - } - - - -} From f062eb09831838fec80b069fbd7157d976c5db9b Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 3 May 2023 17:57:02 +0800 Subject: [PATCH 166/289] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96Http?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Serilog/Utility/SerilogRequestUtility.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs index 61b652dd..289f0aae 100644 --- a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs +++ b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs @@ -51,9 +51,15 @@ public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpC diagnosticContext.Set("Protocol", request.Protocol); diagnosticContext.Set("RequestIp", httpContext.GetRequestIp()); - diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); - diagnosticContext.Set("Body", request.ContentLength > 0 ? request.GetRequestBody() : string.Empty); - + if (request.Method == HttpMethods.Get) + { + diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); + } + else + { + diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); + diagnosticContext.Set("Body", request.ContentLength > 0 ? request.GetRequestBody() : string.Empty); + } diagnosticContext.Set("ContentType", httpContext.Response.ContentType); var endpoint = httpContext.GetEndpoint(); From 8662047671c771f5717e8c44aa65f11678c70f54 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 5 May 2023 18:11:24 +0800 Subject: [PATCH 167/289] feat: new user login --- Blog.Core.Common/HttpContextUser/AspNetUser.cs | 2 +- Blog.Core.Common/HttpContextUser/IUser.cs | 2 +- Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs | 2 +- Blog.Core.Model/Models/Modules.cs | 4 ++-- Blog.Core.Model/Models/Permission.cs | 4 ++-- Blog.Core.Model/Models/Role.cs | 4 ++-- Blog.Core.Model/Models/RoleModulePermission.cs | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Blog.Core.Common/HttpContextUser/AspNetUser.cs b/Blog.Core.Common/HttpContextUser/AspNetUser.cs index b37e4a24..590fda59 100644 --- a/Blog.Core.Common/HttpContextUser/AspNetUser.cs +++ b/Blog.Core.Common/HttpContextUser/AspNetUser.cs @@ -40,7 +40,7 @@ private string GetName() return ""; } - public int ID => GetClaimValueByType("jti").FirstOrDefault().ObjToInt(); + public long ID => GetClaimValueByType("jti").FirstOrDefault().ObjToLong(); public long TenantId => GetClaimValueByType("TenantId").FirstOrDefault().ObjToLong(); public bool IsAuthenticated() diff --git a/Blog.Core.Common/HttpContextUser/IUser.cs b/Blog.Core.Common/HttpContextUser/IUser.cs index 3849bd38..aa6094b1 100644 --- a/Blog.Core.Common/HttpContextUser/IUser.cs +++ b/Blog.Core.Common/HttpContextUser/IUser.cs @@ -7,7 +7,7 @@ namespace Blog.Core.Common.HttpContextUser public interface IUser { string Name { get; } - int ID { get; } + long ID { get; } long TenantId { get; } bool IsAuthenticated(); IEnumerable GetClaimsIdentity(); diff --git a/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs b/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs index 018f4c39..b9659029 100644 --- a/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs +++ b/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs @@ -90,7 +90,7 @@ public static TokenModelJwt SerializeJwt(string jwtStr) tokenModelJwt = new TokenModelJwt { - Uid = (jwtToken.Id).ObjToInt(), + Uid = (jwtToken.Id).ObjToLong(), Role = role != null ? role.ObjToString() : "", }; } diff --git a/Blog.Core.Model/Models/Modules.cs b/Blog.Core.Model/Models/Modules.cs index 6e41aaac..684cfcd0 100644 --- a/Blog.Core.Model/Models/Modules.cs +++ b/Blog.Core.Model/Models/Modules.cs @@ -78,7 +78,7 @@ public Modules() /// 创建ID /// [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } + public long? CreateId { get; set; } /// /// 创建者 /// @@ -93,7 +93,7 @@ public Modules() /// 修改ID /// [SugarColumn(IsNullable = true)] - public int? ModifyId { get; set; } + public long? ModifyId { get; set; } /// /// 修改者 /// diff --git a/Blog.Core.Model/Models/Permission.cs b/Blog.Core.Model/Models/Permission.cs index deece0c0..9dd6238d 100644 --- a/Blog.Core.Model/Models/Permission.cs +++ b/Blog.Core.Model/Models/Permission.cs @@ -69,7 +69,7 @@ public Permission() /// 创建ID /// [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } + public long? CreateId { get; set; } /// /// 创建者 /// @@ -84,7 +84,7 @@ public Permission() /// 修改ID /// [SugarColumn(IsNullable = true)] - public int? ModifyId { get; set; } + public long? ModifyId { get; set; } /// /// 修改者 /// diff --git a/Blog.Core.Model/Models/Role.cs b/Blog.Core.Model/Models/Role.cs index 1357afb0..0e65bcaf 100644 --- a/Blog.Core.Model/Models/Role.cs +++ b/Blog.Core.Model/Models/Role.cs @@ -64,7 +64,7 @@ public Role(string name) /// 创建ID /// [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } + public long? CreateId { get; set; } /// /// 创建者 /// @@ -79,7 +79,7 @@ public Role(string name) /// 修改ID /// [SugarColumn(IsNullable = true)] - public int? ModifyId { get; set; } + public long? ModifyId { get; set; } /// /// 修改者 /// diff --git a/Blog.Core.Model/Models/RoleModulePermission.cs b/Blog.Core.Model/Models/RoleModulePermission.cs index 482b9b4e..f33c1080 100644 --- a/Blog.Core.Model/Models/RoleModulePermission.cs +++ b/Blog.Core.Model/Models/RoleModulePermission.cs @@ -26,7 +26,7 @@ public RoleModulePermission() /// 创建ID /// [SugarColumn(IsNullable = true)] - public int? CreateId { get; set; } + public long? CreateId { get; set; } /// /// 创建者 /// @@ -41,7 +41,7 @@ public RoleModulePermission() /// 修改ID /// [SugarColumn(IsNullable = true)] - public int? ModifyId { get; set; } + public long? ModifyId { get; set; } /// /// 修改者 /// From 48a4adcd371864b3c533bdc981177e6909cfe0aa Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 6 May 2023 09:47:20 +0800 Subject: [PATCH 168/289] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96Http?= =?UTF-8?q?=E6=97=A5=E5=BF=97=EF=BC=8C=E8=AE=B0=E5=BD=95RequestBody?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/Extensions/HttpRequestExtension.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Blog.Core.Common/Extensions/HttpRequestExtension.cs b/Blog.Core.Common/Extensions/HttpRequestExtension.cs index 7e87fd28..285a4a9e 100644 --- a/Blog.Core.Common/Extensions/HttpRequestExtension.cs +++ b/Blog.Core.Common/Extensions/HttpRequestExtension.cs @@ -8,6 +8,16 @@ public static class HttpRequestExtension { public static string GetRequestBody(this HttpRequest request) { + if (!request.Body.CanRead) + { + return default; + } + + if (!request.Body.CanSeek) + { + return default; + } + if (request.Body.Length < 1) { return default; From ddb326763b26ff5b540c310c8d83cb251b73dbb4 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 6 May 2023 10:58:18 +0800 Subject: [PATCH 169/289] feat: change file upload api --- Blog.Core.Api/Controllers/ImgController.cs | 6 +++--- Blog.Core.Model/ViewModels/UploadFileDto.cs | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Blog.Core.Api/Controllers/ImgController.cs b/Blog.Core.Api/Controllers/ImgController.cs index 11d9d209..5ba85388 100644 --- a/Blog.Core.Api/Controllers/ImgController.cs +++ b/Blog.Core.Api/Controllers/ImgController.cs @@ -54,11 +54,11 @@ public FileStreamResult DownImg() public async Task> InsertPicture([FromForm]UploadFileDto dto) { - if (dto.Files == null || !dto.Files.Any()) return Failed("请选择上传的文件。"); + if (dto.file == null || !dto.file.Any()) return Failed("请选择上传的文件。"); //格式限制 var allowType = new string[] { "image/jpg", "image/png", "image/jpeg" }; - var allowedFile = dto.Files.Where(c => allowType.Contains(c.ContentType)); + var allowedFile = dto.file.Where(c => allowType.Contains(c.ContentType)); if (!allowedFile.Any()) return Failed("图片格式错误"); if (allowedFile.Sum(c => c.Length) > 1024 * 1024 * 4) return Failed("图片过大"); @@ -79,7 +79,7 @@ public async Task> InsertPicture([FromForm]UploadFileDto dt } } - var excludeFiles = dto.Files.Except(allowedFile); + var excludeFiles = dto.file.Except(allowedFile); if (excludeFiles.Any()) { diff --git a/Blog.Core.Model/ViewModels/UploadFileDto.cs b/Blog.Core.Model/ViewModels/UploadFileDto.cs index fc2b3cf7..dd0f51bb 100644 --- a/Blog.Core.Model/ViewModels/UploadFileDto.cs +++ b/Blog.Core.Model/ViewModels/UploadFileDto.cs @@ -1,10 +1,5 @@ using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Blog.Core.Model.ViewModels { @@ -12,7 +7,7 @@ public class UploadFileDto { //多文件 [Required] - public IFormFileCollection Files { get; set; } + public IFormFileCollection file { get; set; } //单文件 //public IFormFile File { get; set; } From 8183be9d5888a8cd07f300e6e8d4a44e1dda117e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 8 May 2023 16:55:41 +0800 Subject: [PATCH 170/289] feat: change maindb sort --- Blog.Core.Common/DB/BaseDBConfig.cs | 9 +++++++-- Blog.Core.Common/DB/TenantUtil.cs | 3 ++- Blog.Core.Common/Seed/DBSeed.cs | 6 +++--- Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs | 3 ++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index 5761d91d..313b8a00 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -12,8 +12,8 @@ public class BaseDBConfig * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); - public static List AllConfig=new(); //所有的库连接 - public static List ValidConfig=new(); //有效的库连接(除去Log库) + public static List AllConfig = new(); //所有的库连接 + public static List ValidConfig = new(); //有效的库连接(除去Log库) public static ConnectionConfig LogConfig; //日志库 private static string DifDBConnOfSecurity(params string[] conn) @@ -40,6 +40,11 @@ public static (List, List) MutiInitConn() { List listdatabase = AppSettings.app("DBS") .Where(i => i.Enabled).ToList(); + var mainDbId = AppSettings.app(new string[] { "MainDB" }).ObjToString(); + var mainDbModel = listdatabase.Single(d => d.ConnId == mainDbId); + listdatabase.Remove(mainDbModel); + listdatabase.Insert(0, mainDbModel); + foreach (var i in listdatabase) { SpecialDbString(i); diff --git a/Blog.Core.Common/DB/TenantUtil.cs b/Blog.Core.Common/DB/TenantUtil.cs index 8d57189b..8395c271 100644 --- a/Blog.Core.Common/DB/TenantUtil.cs +++ b/Blog.Core.Common/DB/TenantUtil.cs @@ -47,7 +47,8 @@ public static ConnectionConfig GetConnectionConfig(this SysTenant tenant) IsAutoCloseConnection = true, MoreSettings = new ConnMoreSettings() { - IsAutoRemoveDataCache = true + IsAutoRemoveDataCache = true, + SqlServerCodeFirstNvarchar = true, }, }; } diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 000ff976..e7eb4e27 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -44,9 +44,9 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) Console.WriteLine($"Is multi-DataBase: {AppSettings.app(new string[] { "MutiDBEnabled" })}"); Console.WriteLine($"Is CQRS: {AppSettings.app(new string[] { "CQRSEnabled" })}"); Console.WriteLine(); - Console.WriteLine($"Master DB ConId: {MyContext.ConnId}"); - Console.WriteLine($"Master DB Type: {MyContext.DbType}"); - Console.WriteLine($"Master DB ConnectString: {MyContext.ConnectionString}"); + Console.WriteLine($"Master DB ConId: {myContext.Db.CurrentConnectionConfig.ConfigId}"); + Console.WriteLine($"Master DB Type: {myContext.Db.CurrentConnectionConfig.DbType}"); + Console.WriteLine($"Master DB ConnectString: {myContext.Db.CurrentConnectionConfig.ConnectionString}"); Console.WriteLine(); if (AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) { diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 98dd6e24..83dff774 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -57,7 +57,8 @@ public static void AddSqlsugarSetup(this IServiceCollection services) MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, - IsAutoRemoveDataCache = true + IsAutoRemoveDataCache = true, + SqlServerCodeFirstNvarchar = true, }, // 从库 SlaveConnectionConfigs = listConfig_Slave, From e9f1ef5c01a29dbfea9bf0c0050115216f4a9b06 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Fri, 14 Apr 2023 10:43:47 +0800 Subject: [PATCH 171/289] =?UTF-8?q?=E2=9C=A8=20=E5=A2=9E=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Api.csproj | 1 + Blog.Core.Api/Blog.Core.Model.xml | 5 + Blog.Core.Api/Blog.Core.xml | 40 ++ .../Controllers/BaseApiController.cs | 155 +++---- .../Controllers/Systems/DataBaseController.cs | 188 ++++++++ Blog.Core.Common/Blog.Core.Common.csproj | 3 + Blog.Core.Common/DB/BaseDBConfig.cs | 19 +- Blog.Core.Common/DB/EntityUtility.cs | 53 +++ .../Extensions/DictionaryExtensions.cs | 18 + .../Policys/PermissionHandler.cs | 427 +++++++++--------- .../ServiceExtensions/SqlsugarSetup.cs | 248 +++++----- .../Systems/DataBase/DataBaseReadType.cs | 10 + .../Systems/DataBase/DatabaseOutput.cs | 10 + .../Systems/DataBase/DbColumnInfoOutput.cs | 36 ++ .../Systems/DataBase/EditColumnInput.cs | 9 + .../Systems/DataBase/EditTableInput.cs | 10 + 16 files changed, 822 insertions(+), 410 deletions(-) create mode 100644 Blog.Core.Api/Controllers/Systems/DataBaseController.cs create mode 100644 Blog.Core.Common/DB/EntityUtility.cs create mode 100644 Blog.Core.Common/Extensions/DictionaryExtensions.cs create mode 100644 Blog.Core.Model/Systems/DataBase/DataBaseReadType.cs create mode 100644 Blog.Core.Model/Systems/DataBase/DatabaseOutput.cs create mode 100644 Blog.Core.Model/Systems/DataBase/DbColumnInfoOutput.cs create mode 100644 Blog.Core.Model/Systems/DataBase/EditColumnInput.cs create mode 100644 Blog.Core.Model/Systems/DataBase/EditTableInput.cs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index f3f9befa..2f8c41e9 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -106,6 +106,7 @@ + diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 79666d28..1eb3423b 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -1957,6 +1957,11 @@ 找不到指定资源 + + + 数据库读取类型 + + 表格数据,支持分页 diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index b47bbf53..51ffc1f2 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1299,6 +1299,46 @@ + + + 数据库管理 + + + + + 获取库配置 + + + + + + 获取表信息 + + 配置Id + 读取类型 + + + + + 获取表字段 + + 表名 + ConfigId + 读取类型 + + + + + 编辑表备注 + + + + + + 编辑列备注 + + + 多租户-多库方案 测试 diff --git a/Blog.Core.Api/Controllers/BaseApiController.cs b/Blog.Core.Api/Controllers/BaseApiController.cs index 5a374ded..97a938ea 100644 --- a/Blog.Core.Api/Controllers/BaseApiController.cs +++ b/Blog.Core.Api/Controllers/BaseApiController.cs @@ -4,82 +4,85 @@ namespace Blog.Core.Controllers { - public class BaseApiController : Controller - { - [NonAction] - public MessageModel Success(T data, string msg = "成功") - { - return new MessageModel() - { - success = true, - msg = msg, - response = data, - }; - } - // [NonAction] - //public MessageModel Success(T data, string msg = "成功",bool success = true) - //{ - // return new MessageModel() - // { - // success = success, - // msg = msg, - // response = data, - // }; - //} - [NonAction] - public MessageModel Success(string msg = "成功") - { - return new MessageModel() - { - success = true, - msg = msg, - response = null, - }; - } - [NonAction] - public MessageModel Failed(string msg = "失败", int status = 500) - { - return new MessageModel() - { - success = false, - status = status, - msg = msg, - response = null, - }; - } - [NonAction] - public MessageModel Failed(string msg = "失败", int status = 500) - { - return new MessageModel() - { - success = false, - status = status, - msg = msg, - response = default, - }; - } - [NonAction] - public MessageModel> SuccessPage(int page, int dataCount, int pageSize, List data, int pageCount, string msg = "获取成功") - { + public class BaseApiController : Controller + { + [NonAction] + public MessageModel Success(T data, string msg = "成功") + { + return new MessageModel() + { + success = true, + msg = msg, + response = data, + }; + } - return new MessageModel>() - { - success = true, - msg = msg, - response = new PageModel(page, dataCount, pageSize, data) + // [NonAction] + //public MessageModel Success(T data, string msg = "成功",bool success = true) + //{ + // return new MessageModel() + // { + // success = success, + // msg = msg, + // response = data, + // }; + //} + [NonAction] + public MessageModel Success(string msg = "成功") + { + return new MessageModel() + { + success = true, + msg = msg, + response = null, + }; + } + + [NonAction] + public MessageModel Failed(string msg = "失败", int status = 500) + { + return new MessageModel() + { + success = false, + status = status, + msg = msg, + response = null, + }; + } - }; - } - [NonAction] - public MessageModel> SuccessPage(PageModel pageModel, string msg = "获取成功") - { + [NonAction] + public MessageModel Failed(string msg = "失败", int status = 500) + { + return new MessageModel() + { + success = false, + status = status, + msg = msg, + response = default, + }; + } - return new MessageModel>() - { - success = true, - msg = msg, - response = pageModel - }; - } - } -} + [NonAction] + public MessageModel> SuccessPage(int page, int dataCount, int pageSize, List data, + int pageCount, string msg = "获取成功") + { + return new MessageModel>() + { + success = true, + msg = msg, + response = new PageModel(page, dataCount, pageSize, data) + }; + } + + [NonAction] + public MessageModel> SuccessPage(PageModel pageModel, string msg = "获取成功") + { + return new MessageModel>() + { + success = true, + msg = msg, + response = pageModel + }; + } + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Controllers/Systems/DataBaseController.cs b/Blog.Core.Api/Controllers/Systems/DataBaseController.cs new file mode 100644 index 00000000..434bd195 --- /dev/null +++ b/Blog.Core.Api/Controllers/Systems/DataBaseController.cs @@ -0,0 +1,188 @@ +using System.Diagnostics.CodeAnalysis; +using Blog.Core.Common; +using Blog.Core.Common.DB; +using Blog.Core.Controllers; +using Blog.Core.Model; +using Blog.Core.Model.Models; +using Blog.Core.Model.Systems.DataBase; +using Blog.Core.Model.Tenants; +using Mapster; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SqlSugar; + +namespace Blog.Core.Api.Controllers.Systems; + +/// +/// 数据库管理 +/// +[Route("api/Systems/[controller]/[action]")] +[ApiController] +[Authorize(Permissions.Name)] +public class DataBaseController : BaseApiController +{ + private readonly ISqlSugarClient _db; + + public DataBaseController(ISqlSugarClient db) + { + _db = db; + } + + [return: NotNull] + public ISqlSugarClient GetTenantDb(string configId) + { + if (!_db.AsTenant().IsAnyConnection(configId)) + { + var tenant = _db.Queryable().WithCache() + .Where(s => s.TenantType == TenantTypeEnum.Db) + .Where(s => s.ConfigId == configId) + .First(); + if (tenant != null) + { + _db.AsTenant().AddConnection(tenant.GetConnectionConfig()); + } + } + + var db = _db.AsTenant().GetConnectionScope(configId); + if (db is null) + { + throw new ApplicationException("无效的数据库配置"); + } + + return db; + } + + /// + /// 获取库配置 + /// + /// + [HttpGet] + public async Task>> GetAllConfig() + { + //增加多租户的连接 + var allConfigs = new List(BaseDBConfig.AllConfigs); + var tenants = await _db.Queryable().WithCache() + .Where(s => s.TenantType == TenantTypeEnum.Db) + .ToListAsync(); + if (tenants.Any()) + { + allConfigs.AddRange(tenants.Select(tenant => tenant.GetConnectionConfig())); + } + + var configs = await Task.FromResult(allConfigs); + return Success(configs.Adapt>()); + } + + /// + /// 获取表信息 + /// + /// 配置Id + /// 读取类型 + /// + [HttpGet] + public MessageModel> GetTableInfoList(string configId, + DataBaseReadType readType = DataBaseReadType.Db) + { + if (configId.IsNullOrEmpty()) + { + configId = MainDb.CurrentDbConnId; + } + + var provider = GetTenantDb(configId); + List data = null; + switch (readType) + { + case DataBaseReadType.Db: + data = provider.DbMaintenance.GetTableInfoList(false); + break; + case DataBaseReadType.Entity: + if (EntityUtility.TenantEntitys.TryGetValue(configId, out var types)) + { + data = types.Select(s => provider.EntityMaintenance.GetEntityInfo(s)) + .Select(s => new {Name = s.DbTableName, Description = s.TableDescription}) + .Adapt>(); + } + + break; + } + + + return Success(data); + } + + /// + /// 获取表字段 + /// + /// 表名 + /// ConfigId + /// 读取类型 + /// + [HttpGet] + public MessageModel> GetColumnInfosByTableName(string tableName, string configId = null, + DataBaseReadType readType = DataBaseReadType.Db) + { + if (string.IsNullOrWhiteSpace(tableName)) + return Failed>("表名不能为空"); + + if (configId.IsNullOrEmpty()) + { + configId = MainDb.CurrentDbConnId; + } + + List data = null; + var provider = GetTenantDb(configId); + switch (readType) + { + case DataBaseReadType.Db: + data = provider.DbMaintenance.GetColumnInfosByTableName(tableName, false) + .Adapt>(); + break; + case DataBaseReadType.Entity: + if (EntityUtility.TenantEntitys.TryGetValue(configId, out var types)) + { + var type = types.FirstOrDefault(s => s.Name == tableName); + data = provider.EntityMaintenance.GetEntityInfo(type).Columns.Adapt>(); + } + + break; + } + + + return Success(data); + } + + /// + /// 编辑表备注 + /// + /// + [HttpPut] + public MessageModel PutTableEditRemark([FromBody] EditTableInput input) + { + var provider = GetTenantDb(input.ConfigId); + if (provider.DbMaintenance.IsAnyTableRemark(input.TableName)) + { + provider.DbMaintenance.DeleteTableRemark(input.TableName); + } + + provider.DbMaintenance.AddTableRemark(input.TableName, input.Description); + return Success(); + } + + /// + /// 编辑列备注 + /// + /// + [HttpPut] + public MessageModel PutColumnEditRemark([FromBody] EditColumnInput input) + { + var provider = GetTenantDb(input.ConfigId); + if (provider.DbMaintenance.IsAnyColumnRemark(input.DbColumnName, input.TableName)) + { + provider.DbMaintenance.DeleteColumnRemark(input.DbColumnName, input.TableName); + } + + provider.DbMaintenance.AddColumnRemark(input.DbColumnName, input.TableName, input.ColumnDescription); + + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index 0662bac5..aeba8476 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -18,6 +18,9 @@ + + + diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index 313b8a00..495414fb 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -3,18 +3,23 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using SqlSugar; namespace Blog.Core.Common.DB { public class BaseDBConfig { + public static readonly List AllConfigs = new(); //所有库配置 + public static readonly List AllSlaveConfigs = new(); //从库配置 + public static List ValidConfig = new(); //有效的库连接(除去Log库) + public static ConnectionConfig LogConfig; //日志库 + /* 之前的单库操作已经删除,如果想要之前的代码,可以查看我的GitHub的历史记录 * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); - public static List AllConfig = new(); //所有的库连接 - public static List ValidConfig = new(); //有效的库连接(除去Log库) - public static ConnectionConfig LogConfig; //日志库 + + private static string DifDBConnOfSecurity(params string[] conn) { @@ -54,7 +59,7 @@ public static (List, List) MutiInitConn() List listdatabaseSlaveDB = new List(); //从库 // 单库,且不开启读写分离,只保留一个 - if (!AppSettings.app(new string[] { "CQRSEnabled" }).ObjToBool() && !AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) + if (!AppSettings.app(new string[] {"CQRSEnabled"}).ObjToBool() && !AppSettings.app(new string[] {"MutiDBEnabled"}).ObjToBool()) { if (listdatabase.Count == 1) { @@ -62,7 +67,7 @@ public static (List, List) MutiInitConn() } else { - var dbFirst = listdatabase.FirstOrDefault(d => d.ConnId == AppSettings.app(new string[] { "MainDB" }).ObjToString()); + var dbFirst = listdatabase.FirstOrDefault(d => d.ConnId == AppSettings.app(new string[] {"MainDB"}).ObjToString()); if (dbFirst == null) { dbFirst = listdatabase.FirstOrDefault(); @@ -75,11 +80,11 @@ public static (List, List) MutiInitConn() // 读写分离,且必须是单库模式,获取从库 - if (AppSettings.app(new string[] { "CQRSEnabled" }).ObjToBool() && !AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) + if (AppSettings.app(new string[] {"CQRSEnabled"}).ObjToBool() && !AppSettings.app(new string[] {"MutiDBEnabled"}).ObjToBool()) { if (listdatabase.Count > 1) { - listdatabaseSlaveDB = listdatabase.Where(d => d.ConnId != AppSettings.app(new string[] { "MainDB" }).ObjToString()).ToList(); + listdatabaseSlaveDB = listdatabase.Where(d => d.ConnId != AppSettings.app(new string[] {"MainDB"}).ObjToString()).ToList(); } } diff --git a/Blog.Core.Common/DB/EntityUtility.cs b/Blog.Core.Common/DB/EntityUtility.cs new file mode 100644 index 00000000..f997a1eb --- /dev/null +++ b/Blog.Core.Common/DB/EntityUtility.cs @@ -0,0 +1,53 @@ +using Blog.Core.Common.Extensions; +using Blog.Core.Model; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Blog.Core.Common.DB; + +public class EntityUtility +{ + private static readonly Lazy>> _tenantEntitys = new(() => + { + Dictionary> dic = new Dictionary>(); + var assembly = Assembly.Load("Blog.Core.Model"); + //扫描 实体 + foreach (var type in assembly.GetTypes().Where(s => s.IsClass && !s.IsAbstract)) + { + var tenant = type.GetCustomAttribute(); + if (tenant != null) + { + dic.TryAdd(tenant.configId.ToString(), type); + continue; + } + + if (type.IsSubclassOf(typeof(RootEntityTkey<>))) + { + dic.TryAdd(MainDb.CurrentDbConnId, type); + continue; + } + + var table = type.GetCustomAttribute(); + if (table != null) + { + dic.TryAdd(MainDb.CurrentDbConnId, type); + continue; + } + + Debug.Assert(type.Namespace != null, "type.Namespace != null"); + if (type.Namespace.StartsWith("Blog.Core.Model.Models")) + { + dic.TryAdd(MainDb.CurrentDbConnId, type); + continue; + } + } + + return dic; + }); + + public static Dictionary> TenantEntitys => _tenantEntitys.Value; +} \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/DictionaryExtensions.cs b/Blog.Core.Common/Extensions/DictionaryExtensions.cs new file mode 100644 index 00000000..ffcf910c --- /dev/null +++ b/Blog.Core.Common/Extensions/DictionaryExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Blog.Core.Common.Extensions; + +public static class DictionaryExtensions +{ + public static void TryAdd(this IDictionary> dic, TKey key, TValue value) + { + if (dic.TryGetValue(key, out var old)) + { + old.Add(value); + } + else + { + dic.Add(key, new List {value}); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs index 121b9b10..23627ccd 100644 --- a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs +++ b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs @@ -14,212 +14,229 @@ using System.Security.Claims; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Blog.Core.Model.Models; namespace Blog.Core.AuthHelper { - /// - /// 权限授权处理器 - /// - public class PermissionHandler : AuthorizationHandler - { - /// - /// 验证方案提供对象 - /// - public IAuthenticationSchemeProvider Schemes { get; set; } - - private readonly IRoleModulePermissionServices _roleModulePermissionServices; - private readonly IHttpContextAccessor _accessor; - private readonly ISysUserInfoServices _userServices; - private readonly IUser _user; - - /// - /// 构造函数注入 - /// - /// - /// - /// - /// - /// - public PermissionHandler(IAuthenticationSchemeProvider schemes, IRoleModulePermissionServices roleModulePermissionServices, IHttpContextAccessor accessor, ISysUserInfoServices userServices, IUser user) - { - _accessor = accessor; - _userServices = userServices; - _user = user; - Schemes = schemes; - _roleModulePermissionServices = roleModulePermissionServices; - } - - // 重写异步处理程序 - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) - { - var httpContext = _accessor.HttpContext; - - // 获取系统中所有的角色和菜单的关系集合 - if (!requirement.Permissions.Any()) - { - var data = await _roleModulePermissionServices.RoleModuleMaps(); - var list = new List(); - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - list = (from item in data - where item.IsDeleted == false - orderby item.Id - select new PermissionItem - { - Url = item.Module?.LinkUrl, - Role = item.Role?.Id.ObjToString(), - }).ToList(); - } - // jwt - else - { - list = (from item in data - where item.IsDeleted == false - orderby item.Id - select new PermissionItem - { - Url = item.Module?.LinkUrl, - Role = item.Role?.Name.ObjToString(), - }).ToList(); - } - - requirement.Permissions = list; - } - - if (httpContext != null) - { - var questUrl = httpContext.Request.Path.Value.ToLower(); - - // 整体结构类似认证中间件UseAuthentication的逻辑,具体查看开源地址 - // https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs - httpContext.Features.Set(new AuthenticationFeature - { - OriginalPath = httpContext.Request.Path, - OriginalPathBase = httpContext.Request.PathBase - }); - - // Give any IAuthenticationRequestHandler schemes a chance to handle the request - // 主要作用是: 判断当前是否需要进行远程验证,如果是就进行远程验证 - var handlers = httpContext.RequestServices.GetRequiredService(); - foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) - { - if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler handler && await handler.HandleRequestAsync()) - { - context.Fail(); - return; - } - } - - - //判断请求是否拥有凭据,即有没有登录 - var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); - if (defaultAuthenticate != null) - { - var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); - - // 是否开启测试环境 - var isTestCurrent = AppSettings.app(new string[] { "AppSettings", "UseLoadTest" }).ObjToBool(); - - //result?.Principal不为空即登录成功 - if (result?.Principal != null || isTestCurrent) - { - if (!isTestCurrent) httpContext.User = result.Principal; - - // 获取当前用户的角色信息 - var currentUserRoles = new List(); - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - currentUserRoles = (from item in httpContext.User.Claims - where item.Type == "role" - select item.Value).ToList(); - } - else - { - // jwt - currentUserRoles = (from item in httpContext.User.Claims - where item.Type == requirement.ClaimType - select item.Value).ToList(); - } - - var isMatchRole = false; - var permisssionRoles = requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role)); - foreach (var item in permisssionRoles) - { - try - { - if (Regex.Match(questUrl, item.Url?.ObjToString().ToLower())?.Value == questUrl) - { - isMatchRole = true; - break; - } - } - catch (Exception) - { - // ignored - } - } - - //验证权限 - if (currentUserRoles.Count <= 0 || !isMatchRole) - { - context.Fail(); - return; - } - - // 判断token是否过期,过期则重新登录 - var isExp = false; - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - isExp = (httpContext.User.Claims.SingleOrDefault(s => s.Type == "exp")?.Value) != null && DateHelper.StampToDateTime(httpContext.User.Claims.SingleOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now; - } - else - { - // jwt - isExp = (httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) != null && DateTime.Parse(httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now; - } - - if (!isExp) - { - context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权")); - return; - } - - //校验签发时间 - if (!Permissions.IsUseIds4) - { - var value = httpContext.User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; - if (value != null) - { - var user = await _userServices.QueryById(_user.ID, true); - if (user.CriticalModifyTime > value.ObjToDate()) - { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权").MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } - } - } - - context.Succeed(requirement); - return; - } - } - - //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败 - if (!(questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))) - { - context.Fail(); - return; - } - } - - //context.Succeed(requirement); - } - } + /// + /// 权限授权处理器 + /// + public class PermissionHandler : AuthorizationHandler + { + /// + /// 验证方案提供对象 + /// + public IAuthenticationSchemeProvider Schemes { get; set; } + + private readonly IRoleModulePermissionServices _roleModulePermissionServices; + private readonly IHttpContextAccessor _accessor; + private readonly ISysUserInfoServices _userServices; + private readonly IUser _user; + + /// + /// 构造函数注入 + /// + /// + /// + /// + /// + /// + public PermissionHandler(IAuthenticationSchemeProvider schemes, + IRoleModulePermissionServices roleModulePermissionServices, IHttpContextAccessor accessor, + ISysUserInfoServices userServices, IUser user) + { + _accessor = accessor; + _userServices = userServices; + _user = user; + Schemes = schemes; + _roleModulePermissionServices = roleModulePermissionServices; + } + + // 重写异步处理程序 + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + PermissionRequirement requirement) + { + var httpContext = _accessor.HttpContext; + + // 获取系统中所有的角色和菜单的关系集合 + if (!requirement.Permissions.Any()) + { + var data = await _roleModulePermissionServices.RoleModuleMaps(); + var list = new List(); + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + list = (from item in data + where item.IsDeleted == false + orderby item.Id + select new PermissionItem + { + Url = item.Module?.LinkUrl, + Role = item.Role?.Id.ObjToString(), + }).ToList(); + } + // jwt + else + { + list = (from item in data + where item.IsDeleted == false + orderby item.Id + select new PermissionItem + { + Url = item.Module?.LinkUrl, + Role = item.Role?.Name.ObjToString(), + }).ToList(); + } + + requirement.Permissions = list; + } + + if (httpContext != null) + { + var questUrl = httpContext.Request.Path.Value.ToLower(); + + // 整体结构类似认证中间件UseAuthentication的逻辑,具体查看开源地址 + // https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs + httpContext.Features.Set(new AuthenticationFeature + { + OriginalPath = httpContext.Request.Path, + OriginalPathBase = httpContext.Request.PathBase + }); + + // Give any IAuthenticationRequestHandler schemes a chance to handle the request + // 主要作用是: 判断当前是否需要进行远程验证,如果是就进行远程验证 + var handlers = httpContext.RequestServices.GetRequiredService(); + foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) + { + if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler + handler && await handler.HandleRequestAsync()) + { + context.Fail(); + return; + } + } + + //判断请求是否拥有凭据,即有没有登录 + var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); + if (defaultAuthenticate != null) + { + var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); + + // 是否开启测试环境 + var isTestCurrent = AppSettings.app(new string[] {"AppSettings", "UseLoadTest"}).ObjToBool(); + + //result?.Principal不为空即登录成功 + if (result?.Principal != null || isTestCurrent) + { + if (!isTestCurrent) httpContext.User = result.Principal; + + // 获取当前用户的角色信息 + var currentUserRoles = new List(); + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + currentUserRoles = (from item in httpContext.User.Claims + where item.Type == "role" + select item.Value).ToList(); + } + else + { + // jwt + currentUserRoles = (from item in httpContext.User.Claims + where item.Type == requirement.ClaimType + select item.Value).ToList(); + } + + //超级管理员 默认拥有所有权限 + if (currentUserRoles.All(s => s != "SuperAdmin")) + { + var isMatchRole = false; + var permisssionRoles = requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role)); + foreach (var item in permisssionRoles) + { + try + { + if (Regex.Match(questUrl, item.Url?.ObjToString().ToLower())?.Value == questUrl) + { + isMatchRole = true; + break; + } + } + catch (Exception) + { + // ignored + } + } + + //验证权限 + if (currentUserRoles.Count <= 0 || !isMatchRole) + { + context.Fail(); + return; + } + } + + // 判断token是否过期,过期则重新登录 + var isExp = false; + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + isExp = (httpContext.User.Claims.SingleOrDefault(s => s.Type == "exp")?.Value) != null && + DateHelper.StampToDateTime(httpContext.User.Claims + .SingleOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now; + } + else + { + // jwt + isExp = + (httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration) + ?.Value) != null && + DateTime.Parse(httpContext.User.Claims + .SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now; + } + + if (!isExp) + { + context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权")); + return; + } + + //校验签发时间 + if (!Permissions.IsUseIds4) + { + var value = httpContext.User.Claims + .SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; + if (value != null) + { + var user = await _userServices.QueryById(_user.ID, true); + if (user.CriticalModifyTime > value.ObjToDate()) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权") + .MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + } + } + + context.Succeed(requirement); + return; + } + } + + //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败 + if (!(questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && + (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))) + { + context.Fail(); + return; + } + } + + //context.Succeed(requirement); + } + } } \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 83dff774..4ac2d2e8 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -13,126 +13,130 @@ namespace Blog.Core.Extensions { - /// - /// SqlSugar 启动服务 - /// - public static class SqlsugarSetup - { - private static readonly MemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); - - public static void AddSqlsugarSetup(this IServiceCollection services) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - // 默认添加主数据库连接 - MainDb.CurrentDbConnId = AppSettings.app(new string[] { "MainDB" }); - - // SqlSugarScope是线程安全,可使用单例注入 - // 参考:https://www.donet5.com/Home/Doc?typeId=1181 - services.AddSingleton(o => - { - var memoryCache = o.GetRequiredService(); - - // 从库 - var listConfig_Slave = new List(); - BaseDBConfig.MutiConnectionString.slaveDbs.ForEach(s => - { - listConfig_Slave.Add(new SlaveConnectionConfig() - { - HitRate = s.HitRate, - ConnectionString = s.Connection - }); - }); - - BaseDBConfig.MutiConnectionString.allDbs.ForEach(m => - { - var config = new ConnectionConfig() - { - ConfigId = m.ConnId.ObjToString().ToLower(), - ConnectionString = m.Connection, - DbType = (DbType)m.DbType, - IsAutoCloseConnection = true, - // Check out more information: https://github.com/anjoy8/Blog.Core/issues/122 - //IsShardSameThread = false, - MoreSettings = new ConnMoreSettings() - { - //IsWithNoLockQuery = true, - IsAutoRemoveDataCache = true, - SqlServerCodeFirstNvarchar = true, - }, - // 从库 - SlaveConnectionConfigs = listConfig_Slave, - // 自定义特性 - ConfigureExternalServices = new ConfigureExternalServices() - { - DataInfoCacheService = new SqlSugarMemoryCacheService(memoryCache), - EntityService = (property, column) => - { - if (column.IsPrimarykey && property.PropertyType == typeof(int)) - { - column.IsIdentity = true; - } - } - }, - InitKeyType = InitKeyType.Attribute - }; - if (SqlSugarConst.LogConfigId.ToLower().Equals(m.ConnId.ToLower())) - { - BaseDBConfig.LogConfig = config; - } - else - { - BaseDBConfig.ValidConfig.Add(config); - } - - BaseDBConfig.AllConfig.Add(config); - }); - - if (BaseDBConfig.LogConfig is null) - { - throw new ApplicationException("未配置Log库连接"); - } - - return new SqlSugarScope(BaseDBConfig.AllConfig, db => - { - BaseDBConfig.ValidConfig.ForEach(config => - { - var dbProvider = db.GetConnectionScope((string)config.ConfigId); - - // 打印SQL语句 - dbProvider.Aop.OnLogExecuting = (s, parameters) => SqlSugarAop.OnLogExecuting(dbProvider, s, parameters, config); - - // 数据审计 - dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; - - // 配置实体假删除过滤器 - RepositorySetting.SetDeletedEntityFilter(dbProvider); - // 配置实体数据权限 - RepositorySetting.SetTenantEntityFilter(dbProvider); - }); - }); - }); - } - - private static string GetWholeSql(SugarParameter[] paramArr, string sql) - { - foreach (var param in paramArr) - { - sql.Replace(param.ParameterName, param.Value.ObjToString()); - } - - return sql; - } - - private static string GetParas(SugarParameter[] pars) - { - string key = "【SQL参数】:"; - foreach (var param in pars) - { - key += $"{param.ParameterName}:{param.Value}\n"; - } - - return key; - } - } + /// + /// SqlSugar 启动服务 + /// + public static class SqlsugarSetup + { + private static readonly MemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); + + public static void AddSqlsugarSetup(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + // 默认添加主数据库连接 + MainDb.CurrentDbConnId = AppSettings.app(new string[] {"MainDB"}); + + BaseDBConfig.MutiConnectionString.slaveDbs.ForEach(s => + { + BaseDBConfig.AllSlaveConfigs.Add(new SlaveConnectionConfig() + { + HitRate = s.HitRate, + ConnectionString = s.Connection + }); + }); + + BaseDBConfig.MutiConnectionString.allDbs.ForEach(m => + { + var config = new ConnectionConfig() + { + ConfigId = m.ConnId.ObjToString().ToLower(), + ConnectionString = m.Connection, + DbType = (DbType) m.DbType, + IsAutoCloseConnection = true, + // Check out more information: https://github.com/anjoy8/Blog.Core/issues/122 + //IsShardSameThread = false, + MoreSettings = new ConnMoreSettings() + { + //IsWithNoLockQuery = true, + IsAutoRemoveDataCache = true, + SqlServerCodeFirstNvarchar = true, + }, + // 从库 + SlaveConnectionConfigs = BaseDBConfig.AllSlaveConfigs, + // 自定义特性 + ConfigureExternalServices = new ConfigureExternalServices() + { + EntityService = (property, column) => + { + if (column.IsPrimarykey && property.PropertyType == typeof(int)) + { + column.IsIdentity = true; + } + } + }, + InitKeyType = InitKeyType.Attribute + }; + if (SqlSugarConst.LogConfigId.ToLower().Equals(m.ConnId.ToLower())) + { + BaseDBConfig.LogConfig = config; + } + else + { + BaseDBConfig.ValidConfig.Add(config); + } + + BaseDBConfig.AllConfigs.Add(config); + }); + + if (BaseDBConfig.LogConfig is null) + { + throw new ApplicationException("未配置Log库连接"); + } + + + // SqlSugarScope是线程安全,可使用单例注入 + // 参考:https://www.donet5.com/Home/Doc?typeId=1181 + services.AddSingleton(o => + { + var memoryCache = o.GetRequiredService(); + + foreach (var config in BaseDBConfig.AllConfigs) + { + config.ConfigureExternalServices.DataInfoCacheService = new SqlSugarMemoryCacheService(memoryCache); + } + + return new SqlSugarScope(BaseDBConfig.AllConfigs, db => + { + BaseDBConfig.ValidConfig.ForEach(config => + { + var dbProvider = db.GetConnectionScope((string) config.ConfigId); + + // 打印SQL语句 + dbProvider.Aop.OnLogExecuting = (s, parameters) => + SqlSugarAop.OnLogExecuting(dbProvider, s, parameters, config); + + // 数据审计 + dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; + + // 配置实体假删除过滤器 + RepositorySetting.SetDeletedEntityFilter(dbProvider); + // 配置实体数据权限 + RepositorySetting.SetTenantEntityFilter(dbProvider); + }); + }); + }); + } + + private static string GetWholeSql(SugarParameter[] paramArr, string sql) + { + foreach (var param in paramArr) + { + sql.Replace(param.ParameterName, param.Value.ObjToString()); + } + + return sql; + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } + } } \ No newline at end of file diff --git a/Blog.Core.Model/Systems/DataBase/DataBaseReadType.cs b/Blog.Core.Model/Systems/DataBase/DataBaseReadType.cs new file mode 100644 index 00000000..f2ae1ed0 --- /dev/null +++ b/Blog.Core.Model/Systems/DataBase/DataBaseReadType.cs @@ -0,0 +1,10 @@ +namespace Blog.Core.Model.Systems.DataBase; + +/// +/// 数据库读取类型 +/// +public enum DataBaseReadType +{ + Db, + Entity +} \ No newline at end of file diff --git a/Blog.Core.Model/Systems/DataBase/DatabaseOutput.cs b/Blog.Core.Model/Systems/DataBase/DatabaseOutput.cs new file mode 100644 index 00000000..8cefaeb6 --- /dev/null +++ b/Blog.Core.Model/Systems/DataBase/DatabaseOutput.cs @@ -0,0 +1,10 @@ +using SqlSugar; + +namespace Blog.Core.Model.Systems.DataBase; + +public class DatabaseOutput +{ + public string ConfigId { get; set; } + + public DbType DbType { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Systems/DataBase/DbColumnInfoOutput.cs b/Blog.Core.Model/Systems/DataBase/DbColumnInfoOutput.cs new file mode 100644 index 00000000..6cada2ff --- /dev/null +++ b/Blog.Core.Model/Systems/DataBase/DbColumnInfoOutput.cs @@ -0,0 +1,36 @@ +namespace Blog.Core.Model.Systems.DataBase; + +public class DbColumnInfoOutput +{ + public string TableName { get; set; } + + public int TableId { get; set; } + + public string DbColumnName { get; set; } + + public string PropertyName { get; set; } + + public string DataType { get; set; } + + public int Length { get; set; } + + public string ColumnDescription { get; set; } + + public string DefaultValue { get; set; } + + public bool IsNullable { get; set; } + + public bool IsIdentity { get; set; } + + public bool IsPrimarykey { get; set; } + + public object Value { get; set; } + + public int DecimalDigits { get; set; } + + public int Scale { get; set; } + + public bool IsArray { get; set; } + + internal bool IsJson { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Systems/DataBase/EditColumnInput.cs b/Blog.Core.Model/Systems/DataBase/EditColumnInput.cs new file mode 100644 index 00000000..88c66973 --- /dev/null +++ b/Blog.Core.Model/Systems/DataBase/EditColumnInput.cs @@ -0,0 +1,9 @@ +namespace Blog.Core.Model.Systems.DataBase; + +public class EditColumnInput +{ + public string ConfigId { get; set; } + public string TableName { get; set; } + public string DbColumnName { get; set; } + public string ColumnDescription { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Model/Systems/DataBase/EditTableInput.cs b/Blog.Core.Model/Systems/DataBase/EditTableInput.cs new file mode 100644 index 00000000..496a6dd2 --- /dev/null +++ b/Blog.Core.Model/Systems/DataBase/EditTableInput.cs @@ -0,0 +1,10 @@ +namespace Blog.Core.Model.Systems.DataBase; + +public class EditTableInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string Description { get; set; } +} \ No newline at end of file From a17d6eeb358317e77ed5219e6a0477131111919b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 13 May 2023 12:20:47 +0800 Subject: [PATCH 172/289] Update DataBaseController.cs --- Blog.Core.Api/Controllers/Systems/DataBaseController.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Controllers/Systems/DataBaseController.cs b/Blog.Core.Api/Controllers/Systems/DataBaseController.cs index 434bd195..1f7b3089 100644 --- a/Blog.Core.Api/Controllers/Systems/DataBaseController.cs +++ b/Blog.Core.Api/Controllers/Systems/DataBaseController.cs @@ -29,7 +29,7 @@ public DataBaseController(ISqlSugarClient db) } [return: NotNull] - public ISqlSugarClient GetTenantDb(string configId) + private ISqlSugarClient GetTenantDb(string configId) { if (!_db.AsTenant().IsAnyConnection(configId)) { @@ -88,6 +88,8 @@ public MessageModel> GetTableInfoList(string configId, configId = MainDb.CurrentDbConnId; } + configId = configId.ToLower(); + var provider = GetTenantDb(configId); List data = null; switch (readType) @@ -129,7 +131,9 @@ public MessageModel> GetColumnInfosByTableName(string t configId = MainDb.CurrentDbConnId; } - List data = null; + configId = configId.ToLower(); + + List data = null; var provider = GetTenantDb(configId); switch (readType) { From ca0dda2d0471ab5e0e2d47cbb1b1f93012aaae9a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 15 May 2023 14:48:20 +0800 Subject: [PATCH 173/289] Update appsettings.json --- Blog.Core.Api/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 0523d64d..cabddf53 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -105,7 +105,7 @@ "Connection": "WMBlog.db" //sqlite只写数据库名就行 }, { - "ConnId": "Log", + "ConnId": "Log", //日志库连接固定名称,不要改,其他的可以改 "DBType": 2, "Enabled": true, "HitRate": 50, // 值越大,优先级越高 From 42146e2a04999a37fab456c91f6c35965e81254a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 15 May 2023 15:03:14 +0800 Subject: [PATCH 174/289] Update launchSettings.json --- Blog.Core.Api/Properties/launchSettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Api/Properties/launchSettings.json b/Blog.Core.Api/Properties/launchSettings.json index 425ec45f..e3113d39 100644 --- a/Blog.Core.Api/Properties/launchSettings.json +++ b/Blog.Core.Api/Properties/launchSettings.json @@ -13,8 +13,8 @@ "commandName": "Project", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore" + "ASPNETCORE_ENVIRONMENT": "Development" + //"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore"// 如果要开始skywalking,请取消此行注释 }, "applicationUrl": "http://localhost:9291" }, From 1e4e3ee52df856808a313f6a9acfa88503261f1b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 19 May 2023 14:08:33 +0800 Subject: [PATCH 175/289] Update RoleModulePermission.tsv --- .../RoleModulePermission.tsv | 1391 +++++++++++++++++ 1 file changed, 1391 insertions(+) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv index eb727383..5626b81f 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/RoleModulePermission.tsv @@ -1703,5 +1703,1396 @@ "ModuleId": 72, "PermissionId": 122, "Id": 233 + }, + { + "Id": 1658115520798527489, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 1 + }, + { + "Id": 1658115520798527490, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 114 + }, + { + "Id": 1658115520798527491, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 66, + "PermissionId": 115 + }, + { + "Id": 1658115520798527492, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 2 + }, + { + "Id": 1658115520798527493, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 22, + "PermissionId": 3 + }, + { + "Id": 1658115520798527494, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 7, + "PermissionId": 4 + }, + { + "Id": 1658115520798527495, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 5 + }, + { + "Id": 1658115520798527496, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 13, + "PermissionId": 6 + }, + { + "Id": 1658115520798527497, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 17, + "PermissionId": 7 + }, + { + "Id": 1658115520798527498, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 34 + }, + { + "Id": 1658115520798527499, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 10 + }, + { + "Id": 1658115520798527500, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 11 + }, + { + "Id": 1658115520798527501, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 12 + }, + { + "Id": 1658115520798527502, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 29 + }, + { + "Id": 1658115520798527503, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 27, + "PermissionId": 43 + }, + { + "Id": 1658115520798527504, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 67 + }, + { + "Id": 1658115520798527505, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 37 + }, + { + "Id": 1658115520798527506, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 38 + }, + { + "Id": 1658115520798527507, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 39 + }, + { + "Id": 1658115520798527508, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 41 + }, + { + "Id": 1658115520798527509, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 40 + }, + { + "Id": 1658115520798527510, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 42 + }, + { + "Id": 1658115520798527511, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 26, + "PermissionId": 28 + }, + { + "Id": 1658115520798527512, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 44 + }, + { + "Id": 1658115520798527513, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 45 + }, + { + "Id": 1658115520798527514, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 46 + }, + { + "Id": 1658115520798527515, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 47 + }, + { + "Id": 1658115520798527516, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 48 + }, + { + "Id": 1658115520798527517, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 49 + }, + { + "Id": 1658115520798527518, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 65 + }, + { + "Id": 1658115520798527519, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 33, + "PermissionId": 66 + }, + { + "Id": 1658115520798527520, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 68 + }, + { + "Id": 1658115520798527521, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 69 + }, + { + "Id": 1658115520798527522, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 8 + }, + { + "Id": 1658115520798527523, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 75 + }, + { + "Id": 1658115520798527524, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 37, + "PermissionId": 76 + }, + { + "Id": 1658115520798527525, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 87 + }, + { + "Id": 1658115520798527526, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 88 + }, + { + "Id": 1658115520798527527, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 89 + }, + { + "Id": 1658115520798527528, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 90 + }, + { + "Id": 1658115520798527529, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 91 + }, + { + "Id": 1658115520798527530, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 95 + }, + { + "Id": 1658115520798527531, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 93 + }, + { + "Id": 1658115520798527532, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 94 + }, + { + "Id": 1658115520798527533, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 0, + "PermissionId": 92 + }, + { + "Id": 1658115520798527534, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 7, + "PermissionId": 9 + }, + { + "Id": 1658115520798527535, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 10, + "PermissionId": 13 + }, + { + "Id": 1658115520798527536, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 12, + "PermissionId": 14 + }, + { + "Id": 1658115520798527537, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 11, + "PermissionId": 15 + }, + { + "Id": 1658115520798527538, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 22, + "PermissionId": 16 + }, + { + "Id": 1658115520798527539, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 25, + "PermissionId": 17 + }, + { + "Id": 1658115520798527540, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 24, + "PermissionId": 18 + }, + { + "Id": 1658115520798527541, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 23, + "PermissionId": 19 + }, + { + "Id": 1658115520798527542, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 13, + "PermissionId": 20 + }, + { + "Id": 1658115520798527543, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 16, + "PermissionId": 21 + }, + { + "Id": 1658115520798527544, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 15, + "PermissionId": 22 + }, + { + "Id": 1658115520798527545, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 14, + "PermissionId": 23 + }, + { + "Id": 1658115520798527546, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 17, + "PermissionId": 24 + }, + { + "Id": 1658115520798527547, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 20, + "PermissionId": 25 + }, + { + "Id": 1658115520798527548, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 19, + "PermissionId": 26 + }, + { + "Id": 1658115520798527549, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 18, + "PermissionId": 27 + }, + { + "Id": 1658115520798527550, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 5, + "PermissionId": 30 + }, + { + "Id": 1658115520798527551, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 6, + "PermissionId": 31 + }, + { + "Id": 1658115520798527552, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 26, + "PermissionId": 32 + }, + { + "Id": 1658115520798527553, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 21, + "PermissionId": 33 + }, + { + "Id": 1658115520798527554, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 28, + "PermissionId": 35 + }, + { + "Id": 1658115520798527555, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 29, + "PermissionId": 36 + }, + { + "Id": 1658115520798527556, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 66, + "PermissionId": 116 + }, + { + "Id": 1658115520798527557, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 69, + "PermissionId": 117 + }, + { + "Id": 1658115520798527558, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 68, + "PermissionId": 118 + }, + { + "Id": 1658115520798527559, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 67, + "PermissionId": 119 + }, + { + "Id": 1658115520798527560, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-03-23 19:21:58", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 70, + "PermissionId": 120 + }, + { + "Id": 1658115520798527561, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 32, + "PermissionId": 64 + }, + { + "Id": 1658115520798527562, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 36, + "PermissionId": 72 + }, + { + "Id": 1658115520798527563, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 27, + "PermissionId": 73 + }, + { + "Id": 1658115520798527564, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 27, + "PermissionId": 74 + }, + { + "Id": 1658115520798527565, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2019-01-01 00:00:00", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 71, + "PermissionId": 121 + }, + { + "Id": 1658115520798527566, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2022-04-11 16:08:49", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 72, + "PermissionId": 122 + }, + { + "Id": 1658115520798527567, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 37, + "PermissionId": 77 + }, + { + "Id": 1658115520798527568, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 38, + "PermissionId": 78 + }, + { + "Id": 1658115520798527569, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 39, + "PermissionId": 79 + }, + { + "Id": 1658115520798527570, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 40, + "PermissionId": 80 + }, + { + "Id": 1658115520798527571, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 41, + "PermissionId": 81 + }, + { + "Id": 1658115520798527572, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 42, + "PermissionId": 82 + }, + { + "Id": 1658115520798527573, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 43, + "PermissionId": 83 + }, + { + "Id": 1658115520798527574, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 44, + "PermissionId": 84 + }, + { + "Id": 1658115520798527575, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 45, + "PermissionId": 85 + }, + { + "Id": 1658115520798527576, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 46, + "PermissionId": 86 + }, + { + "Id": 1658115520798527577, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 73, + "PermissionId": 123 + }, + { + "Id": 1658115520798527578, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 74, + "PermissionId": 124 + }, + { + "Id": 1658115520798527579, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 55, + "PermissionId": 108 + }, + { + "Id": 1658115520798527580, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 61, + "PermissionId": 109 + }, + { + "Id": 1658115520798527581, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 50, + "PermissionId": 103 + }, + { + "Id": 1658115520798527582, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 49, + "PermissionId": 104 + }, + { + "Id": 1658115520798527583, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 48, + "PermissionId": 105 + }, + { + "Id": 1658115520798527584, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 51, + "PermissionId": 106 + }, + { + "Id": 1658115520798527585, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 52, + "PermissionId": 107 + }, + { + "Id": 1658115520798527586, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 54, + "PermissionId": 96 + }, + { + "Id": 1658115520798527587, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 64, + "PermissionId": 98 + }, + { + "Id": 1658115520798527588, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 59, + "PermissionId": 99 + }, + { + "Id": 1658115520798527589, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 58, + "PermissionId": 100 + }, + { + "Id": 1658115520798527590, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 53, + "PermissionId": 101 + }, + { + "Id": 1658115520798527591, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 60, + "PermissionId": 102 + }, + { + "Id": 1658115520798527592, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 57, + "PermissionId": 110 + }, + { + "Id": 1658115520798527593, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 56, + "PermissionId": 112 + }, + { + "Id": 1658115520798527594, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 62, + "PermissionId": 111 + }, + { + "Id": 1658115520798527595, + "IsDeleted": 0, + "CreateId": 12, + "CreateBy": "blogadmin", + "CreateTime": "2023-05-15 14:20:12", + "ModifyId": 12, + "ModifyBy": "blogadmin", + "ModifyTime": "2023-05-15 14:20:12", + "RoleId": 4, + "ModuleId": 63, + "PermissionId": 113 } ] From f0bbf06c4725bd29abdf047ce199aab03367c574 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Sun, 21 May 2023 18:22:28 +0800 Subject: [PATCH 176/289] Update SqlsugarAop.cs --- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index 369fc482..3a595e67 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -113,7 +113,7 @@ public static void DataExecuting(object oldValue, DataFilterModel entityInfo) if (App.User?.ID > 0 && dyCreateId != null && dyCreateId.GetValue(entityInfo.EntityValue) == null) dyCreateId.SetValue(entityInfo.EntityValue, App.User.ID); - if (dyCreateTime != null && (DateTime)dyCreateTime.GetValue(entityInfo.EntityValue) == DateTime.MinValue) + if (dyCreateTime != null && dyCreateTime.GetValue(entityInfo.EntityValue) != null && (DateTime)dyCreateTime.GetValue(entityInfo.EntityValue) == DateTime.MinValue) dyCreateTime.SetValue(entityInfo.EntityValue, DateTime.Now); break; @@ -155,4 +155,4 @@ private static string GetParas(SugarParameter[] pars) return key; } -} \ No newline at end of file +} From a979d3646129fd2009517f6f98c7da85e11c3994 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Sun, 21 May 2023 19:23:01 +0800 Subject: [PATCH 177/289] Update NumberConverter.cs --- Blog.Core.Common/Helper/NumberConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Common/Helper/NumberConverter.cs b/Blog.Core.Common/Helper/NumberConverter.cs index 27890faf..4232a75f 100644 --- a/Blog.Core.Common/Helper/NumberConverter.cs +++ b/Blog.Core.Common/Helper/NumberConverter.cs @@ -72,7 +72,7 @@ public override bool CanConvert(Type objectType) /// 对象值。 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return AsType(reader.Value.ToString(), objectType); + return AsType(reader.Value.ObjToString(), objectType); } /// From 0cea9672b5f1ed8ae8ac1bbca221a127be35d2cd Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 24 May 2023 11:19:36 +0800 Subject: [PATCH 178/289] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96Swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.swagger登录可以用用户账号登录,如果登录成功 token存在session中 之前默认admin感觉没什么用 当然也可以扩展User 加个字段是否开发者帐户等类似的 2.优化权限校验 优先读取Header->没有读取Session 中token解析用户 --- Blog.Core.Api/Blog.Core.xml | 2 +- Blog.Core.Api/Controllers/LoginController.cs | 621 +++++++++--------- Blog.Core.Api/appsettings.json | 2 +- Blog.Core.Api/wwwroot/swg-login.html | 225 ++++--- .../Extensions/HttpContextExtension.cs | 19 + .../HttpContextUser/AspNetUser.cs | 41 +- .../Swagger/SwaggerContextExtension.cs | 48 ++ .../Policys/PermissionHandler.cs | 120 ++-- .../Middlewares/SwaggerAuthMiddleware.cs | 5 +- Blog.Core.Model/Models/sysUserInfo.cs | 271 ++++---- .../Controller_Test/LoginController_Should.cs | 136 ++-- 11 files changed, 833 insertions(+), 657 deletions(-) create mode 100644 Blog.Core.Common/Extensions/HttpContextExtension.cs create mode 100644 Blog.Core.Common/Swagger/SwaggerContextExtension.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 51ffc1f2..672ea21f 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -177,7 +177,7 @@ 登录管理【无权限】 - + 构造函数注入 diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 87f5c0c9..8313507d 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -9,307 +9,330 @@ using Microsoft.AspNetCore.Mvc; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using Blog.Core.Common.Swagger; +using Serilog; namespace Blog.Core.Controllers { - /// - /// 登录管理【无权限】 - /// - [Produces("application/json")] - [Route("api/Login")] - [AllowAnonymous] - public class LoginController : BaseApiController - { - readonly ISysUserInfoServices _sysUserInfoServices; - readonly IUserRoleServices _userRoleServices; - readonly IRoleServices _roleServices; - readonly PermissionRequirement _requirement; - private readonly IRoleModulePermissionServices _roleModulePermissionServices; - - - /// - /// 构造函数注入 - /// - /// - /// - /// - /// - /// - public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IRoleServices roleServices, PermissionRequirement requirement, IRoleModulePermissionServices roleModulePermissionServices) - { - this._sysUserInfoServices = sysUserInfoServices; - this._userRoleServices = userRoleServices; - this._roleServices = roleServices; - _requirement = requirement; - _roleModulePermissionServices = roleModulePermissionServices; - } - - - #region 获取token的第1种方法 - - /// - /// 获取JWT的方法1 - /// - /// - /// - /// - [HttpGet] - [Route("Token")] - public async Task> GetJwtStr(string name, string pass) - { - string jwtStr = string.Empty; - bool suc = false; - //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 - - var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); - if (user != null) - { - TokenModelJwt tokenModel = new TokenModelJwt { Uid = 1, Role = user }; - - jwtStr = JwtHelper.IssueJwt(tokenModel); - suc = true; - } - else - { - jwtStr = "login fail!!!"; - } - - return new MessageModel() - { - success = suc, - msg = suc ? "获取成功" : "获取失败", - response = jwtStr - }; - } - - - /// - /// 获取JWT的方法2:给Nuxt提供 - /// - /// - /// - /// - [HttpGet] - [Route("GetTokenNuxt")] - public MessageModel GetJwtStrForNuxt(string name, string pass) - { - string jwtStr = string.Empty; - bool suc = false; - //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 - //这里直接写死了 - if (name == "admins" && pass == "admins") - { - TokenModelJwt tokenModel = new TokenModelJwt - { - Uid = 1, - Role = "Admin" - }; - - jwtStr = JwtHelper.IssueJwt(tokenModel); - suc = true; - } - else - { - jwtStr = "login fail!!!"; - } - - var result = new - { - data = new { success = suc, token = jwtStr } - }; - - return new MessageModel() - { - success = suc, - msg = suc ? "获取成功" : "获取失败", - response = jwtStr - }; - } - - #endregion - - - /// - /// 获取JWT的方法3:整个系统主要方法 - /// - /// - /// - /// - [HttpGet] - [Route("JWTToken3.0")] - public async Task> GetJwtToken3(string name = "", string pass = "") - { - string jwtStr = string.Empty; - - if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(pass)) - return Failed("用户名或密码不能为空"); - - pass = MD5Helper.MD5Encrypt32(pass); - - var user = await _sysUserInfoServices.Query(d => d.LoginName == name && d.LoginPWD == pass && d.IsDeleted == false); - if (user.Count > 0) - { - var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); - //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List - { - new Claim(ClaimTypes.Name, name), - new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), - new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) - }; - claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); - - - // ids4和jwt切换 - // jwt - if (!Permissions.IsUseIds4) - { - var data = await _roleModulePermissionServices.RoleModuleMaps(); - var list = (from item in data - where item.IsDeleted == false - orderby item.Id - select new PermissionItem - { - Url = item.Module?.LinkUrl, - Role = item.Role?.Name.ObjToString(), - }).ToList(); - - _requirement.Permissions = list; - } - - var token = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); - return Success(token, "获取成功"); - } - else - { - return Failed("认证失败"); - } - } - - /// - /// 请求刷新Token(以旧换新) - /// - /// - /// - [HttpGet] - [Route("RefreshToken")] - public async Task> RefreshToken(string token = "") - { - string jwtStr = string.Empty; - - if (string.IsNullOrEmpty(token)) - return Failed("token无效,请重新登录!"); - var tokenModel = JwtHelper.SerializeJwt(token); - if (tokenModel != null && JwtHelper.customSafeVerify(token) && tokenModel.Uid > 0) - { - var user = await _sysUserInfoServices.QueryById(tokenModel.Uid); - var value = User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; - if (value != null && user.CriticalModifyTime > value.ObjToDate()) - { - return Failed("很抱歉,授权已失效,请重新授权!"); - } - - if (user != null && !(value != null && user.CriticalModifyTime > value.ObjToDate())) - { - var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); - //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List - { - new Claim(ClaimTypes.Name, user.LoginName), - new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) - }; - claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); - - //用户标识 - var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); - identity.AddClaims(claims); - - var refreshToken = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); - return Success(refreshToken, "获取成功"); - } - } - - return Failed("认证失败!"); - } - - /// - /// 获取JWT的方法4:给 JSONP 测试 - /// - /// - /// - /// - /// - /// - /// - [HttpGet] - [Route("jsonp")] - public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, int expiresAbsoulute = 30) - { - TokenModelJwt tokenModel = new TokenModelJwt - { - Uid = id, - Role = sub - }; - - string jwtStr = JwtHelper.IssueJwt(tokenModel); - - string response = string.Format("\"value\":\"{0}\"", jwtStr); - string call = callBack + "({" + response + "})"; - Response.WriteAsync(call); - } - - - /// - /// 测试 MD5 加密字符串 - /// - /// - /// - [HttpGet] - [Route("Md5Password")] - public string Md5Password(string password = "") - { - return MD5Helper.MD5Encrypt32(password); - } - - /// - /// swagger登录 - /// - /// - /// - [HttpPost] - [Route("/api/Login/swgLogin")] - public dynamic SwgLogin([FromBody] SwaggerLoginRequest loginRequest) - { - // 这里可以查询数据库等各种校验 - if (loginRequest?.name == "admin" && loginRequest?.pwd == "admin") - { - HttpContext.Session.SetString("swagger-code", "success"); - return new { result = true }; - } - - return new { result = false }; - } - - /// - /// weixin登录 - /// - /// - [HttpGet] - [Route("wxLogin")] - public dynamic WxLogin(string g = "", string token = "") - { - return new { g, token }; - } - } - - public class SwaggerLoginRequest - { - public string name { get; set; } - public string pwd { get; set; } - } + /// + /// 登录管理【无权限】 + /// + [Produces("application/json")] + [Route("api/Login")] + [AllowAnonymous] + public class LoginController : BaseApiController + { + readonly ISysUserInfoServices _sysUserInfoServices; + readonly IUserRoleServices _userRoleServices; + readonly IRoleServices _roleServices; + readonly PermissionRequirement _requirement; + private readonly IRoleModulePermissionServices _roleModulePermissionServices; + private readonly ILogger _logger; + + /// + /// 构造函数注入 + /// + /// + /// + /// + /// + /// + public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, + IRoleServices roleServices, PermissionRequirement requirement, + IRoleModulePermissionServices roleModulePermissionServices, ILogger logger) + { + this._sysUserInfoServices = sysUserInfoServices; + this._userRoleServices = userRoleServices; + this._roleServices = roleServices; + _requirement = requirement; + _roleModulePermissionServices = roleModulePermissionServices; + _logger = logger; + } + + + #region 获取token的第1种方法 + + /// + /// 获取JWT的方法1 + /// + /// + /// + /// + [HttpGet] + [Route("Token")] + public async Task> GetJwtStr(string name, string pass) + { + string jwtStr = string.Empty; + bool suc = false; + //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 + + var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); + if (user != null) + { + TokenModelJwt tokenModel = new TokenModelJwt {Uid = 1, Role = user}; + + jwtStr = JwtHelper.IssueJwt(tokenModel); + suc = true; + } + else + { + jwtStr = "login fail!!!"; + } + + return new MessageModel() + { + success = suc, + msg = suc ? "获取成功" : "获取失败", + response = jwtStr + }; + } + + + /// + /// 获取JWT的方法2:给Nuxt提供 + /// + /// + /// + /// + [HttpGet] + [Route("GetTokenNuxt")] + public MessageModel GetJwtStrForNuxt(string name, string pass) + { + string jwtStr = string.Empty; + bool suc = false; + //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 + //这里直接写死了 + if (name == "admins" && pass == "admins") + { + TokenModelJwt tokenModel = new TokenModelJwt + { + Uid = 1, + Role = "Admin" + }; + + jwtStr = JwtHelper.IssueJwt(tokenModel); + suc = true; + } + else + { + jwtStr = "login fail!!!"; + } + + var result = new + { + data = new {success = suc, token = jwtStr} + }; + + return new MessageModel() + { + success = suc, + msg = suc ? "获取成功" : "获取失败", + response = jwtStr + }; + } + + #endregion + + + /// + /// 获取JWT的方法3:整个系统主要方法 + /// + /// + /// + /// + [HttpGet] + [Route("JWTToken3.0")] + public async Task> GetJwtToken3(string name = "", string pass = "") + + { + string jwtStr = string.Empty; + + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(pass)) + return Failed("用户名或密码不能为空"); + + pass = MD5Helper.MD5Encrypt32(pass); + + var user = await _sysUserInfoServices.Query(d => + d.LoginName == name && d.LoginPWD == pass && d.IsDeleted == false); + if (user.Count > 0) + { + var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); + //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 + var claims = new List + { + new Claim(ClaimTypes.Name, name), + new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), + new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(ClaimTypes.Expiration, + DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; + claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); + + + // ids4和jwt切换 + // jwt + if (!Permissions.IsUseIds4) + { + var data = await _roleModulePermissionServices.RoleModuleMaps(); + var list = (from item in data + where item.IsDeleted == false + orderby item.Id + select new PermissionItem + { + Url = item.Module?.LinkUrl, + Role = item.Role?.Name.ObjToString(), + }).ToList(); + + _requirement.Permissions = list; + } + + var token = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); + return Success(token, "获取成功"); + } + else + { + return Failed("认证失败"); + } + } + + /// + /// 请求刷新Token(以旧换新) + /// + /// + /// + [HttpGet] + [Route("RefreshToken")] + public async Task> RefreshToken(string token = "") + { + string jwtStr = string.Empty; + + if (string.IsNullOrEmpty(token)) + return Failed("token无效,请重新登录!"); + var tokenModel = JwtHelper.SerializeJwt(token); + if (tokenModel != null && JwtHelper.customSafeVerify(token) && tokenModel.Uid > 0) + { + var user = await _sysUserInfoServices.QueryById(tokenModel.Uid); + var value = User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; + if (value != null && user.CriticalModifyTime > value.ObjToDate()) + { + return Failed("很抱歉,授权已失效,请重新授权!"); + } + + if (user != null && !(value != null && user.CriticalModifyTime > value.ObjToDate())) + { + var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); + //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 + var claims = new List + { + new Claim(ClaimTypes.Name, user.LoginName), + new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(ClaimTypes.Expiration, + DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; + claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); + + //用户标识 + var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); + identity.AddClaims(claims); + + var refreshToken = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); + return Success(refreshToken, "获取成功"); + } + } + + return Failed("认证失败!"); + } + + /// + /// 获取JWT的方法4:给 JSONP 测试 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + [Route("jsonp")] + public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, + int expiresAbsoulute = 30) + { + TokenModelJwt tokenModel = new TokenModelJwt + { + Uid = id, + Role = sub + }; + + string jwtStr = JwtHelper.IssueJwt(tokenModel); + + string response = string.Format("\"value\":\"{0}\"", jwtStr); + string call = callBack + "({" + response + "})"; + Response.WriteAsync(call); + } + + + /// + /// 测试 MD5 加密字符串 + /// + /// + /// + [HttpGet] + [Route("Md5Password")] + public string Md5Password(string password = "") + { + return MD5Helper.MD5Encrypt32(password); + } + + /// + /// swagger登录 + /// + /// + /// + [HttpPost] + [Route("/api/Login/swgLogin")] + public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) + { + if (loginRequest is null) + { + return new {result = false}; + } + + try + { + var result = await GetJwtToken3(loginRequest.name, loginRequest.pwd); + if (result.success) + { + HttpContext.SuccessSwagger(); + HttpContext.SuccessSwaggerJwt(result.response.token); + return new {result = true}; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Swagger登录异常"); + } + + return new {result = false}; + } + + /// + /// weixin登录 + /// + /// + [HttpGet] + [Route("wxLogin")] + public dynamic WxLogin(string g = "", string token = "") + { + return new {g, token}; + } + } + + public class SwaggerLoginRequest + { + public string name { get; set; } + public string pwd { get; set; } + } } \ No newline at end of file diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index cabddf53..cef87bed 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -47,7 +47,7 @@ }, "LogToDb": true, "LogAOP": { - "Enabled": true, + "Enabled": false, "LogToFile": { "Enabled": true }, diff --git a/Blog.Core.Api/wwwroot/swg-login.html b/Blog.Core.Api/wwwroot/swg-login.html index 022fec49..93653957 100644 --- a/Blog.Core.Api/wwwroot/swg-login.html +++ b/Blog.Core.Api/wwwroot/swg-login.html @@ -1,128 +1,141 @@  - + 登录 - 接口文档 - + -
-
-
-
+
+
+
+
- -
欢迎使用!
-
用户名:admin,密码:admin
-
-
- - - - - - - - - -
- - - - - -
+ +
欢迎使用!
+
使用用户账号登录
+
+
+ + + + + + + + + +
+ + + + +
+
- + } + }); + } + \ No newline at end of file diff --git a/Blog.Core.Common/Extensions/HttpContextExtension.cs b/Blog.Core.Common/Extensions/HttpContextExtension.cs new file mode 100644 index 00000000..0de018a0 --- /dev/null +++ b/Blog.Core.Common/Extensions/HttpContextExtension.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Blog.Core.Common.Extensions; + +public static class HttpContextExtension +{ + public static ISession GetSession(this HttpContext context) + { + try + { + return context.Session; + } + catch (Exception) + { + return default; + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/HttpContextUser/AspNetUser.cs b/Blog.Core.Common/HttpContextUser/AspNetUser.cs index 590fda59..ba21889e 100644 --- a/Blog.Core.Common/HttpContextUser/AspNetUser.cs +++ b/Blog.Core.Common/HttpContextUser/AspNetUser.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; +using Blog.Core.Common.Swagger; using Blog.Core.Model; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -51,7 +53,25 @@ public bool IsAuthenticated() public string GetToken() { - return _accessor.HttpContext?.Request?.Headers["Authorization"].ObjToString().Replace("Bearer ", ""); + var token = _accessor.HttpContext?.Request?.Headers["Authorization"].ObjToString().Replace("Bearer ", ""); + if (!token.IsNullOrEmpty()) + { + return token; + } + + if (_accessor.HttpContext?.IsSuccessSwagger() == true) + { + token = _accessor.HttpContext.GetSuccessSwaggerJwt(); + if (token.IsNotEmptyOrNull()) + { + var claims = new ClaimsIdentity(GetClaimsIdentity(token)); + _accessor.HttpContext.User.AddIdentity(claims); + + return token; + } + } + + return token; } public List GetUserInfoFromToken(string ClaimType) @@ -77,6 +97,10 @@ public List GetUserInfoFromToken(string ClaimType) public IEnumerable GetClaimsIdentity() { + if (_accessor.HttpContext == null) return ArraySegment.Empty; + + if (!IsAuthenticated()) return GetClaimsIdentity(GetToken()); + var claims = _accessor.HttpContext.User.Claims.ToList(); var headers = _accessor.HttpContext.Request.Headers; foreach (var header in headers) @@ -86,6 +110,19 @@ public IEnumerable GetClaimsIdentity() return claims; } + public IEnumerable GetClaimsIdentity(string token) + { + var jwtHandler = new JwtSecurityTokenHandler(); + // token校验 + if (token.IsNotEmptyOrNull() && jwtHandler.CanReadToken(token)) + { + var jwtToken = jwtHandler.ReadJwtToken(token); + + return jwtToken.Claims; + } + + return new List(); + } public List GetClaimValueByType(string ClaimType) { diff --git a/Blog.Core.Common/Swagger/SwaggerContextExtension.cs b/Blog.Core.Common/Swagger/SwaggerContextExtension.cs new file mode 100644 index 00000000..ad89344d --- /dev/null +++ b/Blog.Core.Common/Swagger/SwaggerContextExtension.cs @@ -0,0 +1,48 @@ +using Blog.Core.Common.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace Blog.Core.Common.Swagger; + +public static class SwaggerContextExtension +{ + public const string SwaggerCodeKey = "swagger-code"; + public const string SwaggerJwt = "swagger-jwt"; + + public static bool IsSuccessSwagger() + { + return App.HttpContext?.GetSession()?.GetString(SwaggerCodeKey) == "success"; + } + + public static bool IsSuccessSwagger(this HttpContext context) + { + return context.GetSession()?.GetString(SwaggerCodeKey) == "success"; + } + + public static void SuccessSwagger() + { + App.HttpContext?.GetSession()?.SetString(SwaggerCodeKey, "success"); + } + + public static void SuccessSwagger(this HttpContext context) + { + context.GetSession()?.SetString(SwaggerCodeKey, "success"); + } + + public static void SuccessSwaggerJwt(this HttpContext context, string token) + { + context.GetSession()?.SetString(SwaggerJwt, token); + } + + public static string GetSuccessSwaggerJwt(this HttpContext context) + { + return context.GetSession()?.GetString(SwaggerJwt); + } + + + public static void RedirectSwaggerLogin(this HttpContext context) + { + var returnUrl = context.Request.GetDisplayUrl(); //获取当前url地址 + context.Response.Redirect("/swg-login.html?returnUrl=" + returnUrl); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs index 23627ccd..d2dbfdf7 100644 --- a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs +++ b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs @@ -14,6 +14,7 @@ using System.Security.Claims; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Blog.Core.Common.Swagger; using Blog.Core.Model.Models; namespace Blog.Core.AuthHelper @@ -116,7 +117,7 @@ orderby item.Id return; } } - + //判断请求是否拥有凭据,即有没有登录 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); if (defaultAuthenticate != null) @@ -127,10 +128,79 @@ orderby item.Id var isTestCurrent = AppSettings.app(new string[] {"AppSettings", "UseLoadTest"}).ObjToBool(); //result?.Principal不为空即登录成功 - if (result?.Principal != null || isTestCurrent) + if (result?.Principal != null || isTestCurrent || httpContext.IsSuccessSwagger()) { if (!isTestCurrent) httpContext.User = result.Principal; + //应该要先校验用户的信息 再校验菜单权限相关的 + + //校验用户 + var user = await _userServices.QueryById(_user.ID, true); + if (user == null) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户不存在或已被删除").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + + if (user.IsDeleted) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被删除,禁止登陆!").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + + if (!user.Enable) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被禁用!禁止登陆!").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + + // 判断token是否过期,过期则重新登录 + var isExp = false; + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + isExp = (httpContext.User.Claims.FirstOrDefault(s => s.Type == "exp")?.Value) != null && + DateHelper.StampToDateTime(httpContext.User.Claims + .FirstOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now; + } + else + { + // jwt + isExp = + (httpContext.User.Claims.FirstOrDefault(s => s.Type == ClaimTypes.Expiration) + ?.Value) != null && + DateTime.Parse(httpContext.User.Claims + .FirstOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now; + } + + if (!isExp) + { + context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权")); + return; + } + + + //校验签发时间 + if (!Permissions.IsUseIds4) + { + var value = httpContext.User.Claims + .FirstOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; + if (value != null) + { + if (user.CriticalModifyTime > value.ObjToDate()) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权") + .MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + } + } + // 获取当前用户的角色信息 var currentUserRoles = new List(); // ids4和jwt切换 @@ -153,7 +223,8 @@ orderby item.Id if (currentUserRoles.All(s => s != "SuperAdmin")) { var isMatchRole = false; - var permisssionRoles = requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role)); + var permisssionRoles = + requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role)); foreach (var item in permisssionRoles) { try @@ -177,50 +248,7 @@ orderby item.Id return; } } - - // 判断token是否过期,过期则重新登录 - var isExp = false; - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - isExp = (httpContext.User.Claims.SingleOrDefault(s => s.Type == "exp")?.Value) != null && - DateHelper.StampToDateTime(httpContext.User.Claims - .SingleOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now; - } - else - { - // jwt - isExp = - (httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration) - ?.Value) != null && - DateTime.Parse(httpContext.User.Claims - .SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now; - } - if (!isExp) - { - context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权")); - return; - } - - //校验签发时间 - if (!Permissions.IsUseIds4) - { - var value = httpContext.User.Claims - .SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; - if (value != null) - { - var user = await _userServices.QueryById(_user.ID, true); - if (user.CriticalModifyTime > value.ObjToDate()) - { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权") - .MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } - } - } context.Succeed(requirement); return; diff --git a/Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs b/Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs index 1438588c..3cc284a1 100644 --- a/Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/SwaggerAuthMiddleware.cs @@ -1,5 +1,6 @@ using System.Net; using System.Threading.Tasks; +using Blog.Core.Common.Swagger; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -28,7 +29,7 @@ public async Task InvokeAsync(HttpContext context) } // 无权限,跳转swagger登录页 - context.Response.Redirect("/swg-login.html"); + context.RedirectSwaggerLogin(); } else { @@ -40,7 +41,7 @@ public bool IsAuthorized(HttpContext context) { // 使用session模式 // 可以使用其他的 - return context.Session.GetString("swagger-code") == "success"; + return context.IsSuccessSwagger(); } /// diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index 2a417a16..574ea311 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -4,138 +4,141 @@ namespace Blog.Core.Model.Models { - /// - /// 用户信息表 - /// - //[SugarTable("SysUserInfo")] - [SugarTable("SysUserInfo", "用户表")] //('数据库表名','数据库表备注') - public class SysUserInfo : SysUserInfoRoot - { - public SysUserInfo() - { - } - - public SysUserInfo(string loginName, string loginPWD) - { - LoginName = loginName; - LoginPWD = loginPWD; - RealName = LoginName; - Status = 0; - CreateTime = DateTime.Now; - UpdateTime = DateTime.Now; - LastErrorTime = DateTime.Now; - ErrorCount = 0; - Name = ""; - } - - /// - /// 登录账号 - /// - [SugarColumn(Length = 200, IsNullable = true, ColumnDescription = "登录账号")] - //:eg model 根据sqlsugar的完整定义可以如下定义,ColumnDescription可定义表字段备注 - //[SugarColumn(IsNullable = false, ColumnDescription = "登录账号", IsPrimaryKey = false, IsIdentity = false, Length = 50)] - //ColumnDescription 表字段备注, 已在MSSQL测试,配合 [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') - //可以完整生成 表备注和各个字段的中文备注 - //2022/10/11 - //测试mssql 发现 不写ColumnDescription,写好注释在mssql下也能生成表字段备注 - public string LoginName { get; set; } - - /// - /// 登录密码 - /// - [SugarColumn(Length = 200, IsNullable = true)] - public string LoginPWD { get; set; } - - /// - /// 真实姓名 - /// - [SugarColumn(Length = 200, IsNullable = true)] - public string RealName { get; set; } - - /// - /// 状态 - /// - public int Status { get; set; } - - /// - /// 部门 - /// - [SugarColumn(IsNullable = true)] - public int DepartmentId { get; set; } = -1; - - /// - /// 备注 - /// - [SugarColumn(Length = 2000, IsNullable = true)] - public string Remark { get; set; } - - /// - /// 创建时间 - /// - public DateTime CreateTime { get; set; } = DateTime.Now; - - /// - /// 更新时间 - /// - public DateTime UpdateTime { get; set; } = DateTime.Now; - - /// - /// 关键业务修改时间 - /// - public DateTime CriticalModifyTime { get; set; } = DateTime.Now; - - /// - ///最后异常时间 - /// - public DateTime LastErrorTime { get; set; } = DateTime.Now; - - /// - ///错误次数 - /// - public int ErrorCount { get; set; } - - - /// - /// 登录账号 - /// - [SugarColumn(Length = 200, IsNullable = true)] - public string Name { get; set; } - - // 性别 - [SugarColumn(IsNullable = true)] - public int Sex { get; set; } = 0; - - // 年龄 - [SugarColumn(IsNullable = true)] - public int Age { get; set; } - - // 生日 - [SugarColumn(IsNullable = true)] - public DateTime Birth { get; set; } = DateTime.Now; - - // 地址 - [SugarColumn(Length = 200, IsNullable = true)] - public string Address { get; set; } - - [SugarColumn(IsNullable = true)] - public bool IsDeleted { get; set; } - - /// - /// 租户Id - /// - [SugarColumn(IsNullable = false,DefaultValue = "0")] - public long TenantId { get; set; } - - [Navigate(NavigateType.OneToOne, nameof(TenantId))] - public SysTenant Tenant { get; set; } - - [SugarColumn(IsIgnore = true)] - public List RoleNames { get; set; } - - [SugarColumn(IsIgnore = true)] - public List Dids { get; set; } - - [SugarColumn(IsIgnore = true)] - public string DepartmentName { get; set; } - } + /// + /// 用户信息表 + /// + //[SugarTable("SysUserInfo")] + [SugarTable("SysUserInfo", "用户表")] //('数据库表名','数据库表备注') + public class SysUserInfo : SysUserInfoRoot + { + public SysUserInfo() + { + } + + public SysUserInfo(string loginName, string loginPWD) + { + LoginName = loginName; + LoginPWD = loginPWD; + RealName = LoginName; + Status = 0; + CreateTime = DateTime.Now; + UpdateTime = DateTime.Now; + LastErrorTime = DateTime.Now; + ErrorCount = 0; + Name = ""; + } + + /// + /// 登录账号 + /// + [SugarColumn(Length = 200, IsNullable = true, ColumnDescription = "登录账号")] + //:eg model 根据sqlsugar的完整定义可以如下定义,ColumnDescription可定义表字段备注 + //[SugarColumn(IsNullable = false, ColumnDescription = "登录账号", IsPrimaryKey = false, IsIdentity = false, Length = 50)] + //ColumnDescription 表字段备注, 已在MSSQL测试,配合 [SugarTable("SysUserInfo", "用户表")]//('数据库表名','数据库表备注') + //可以完整生成 表备注和各个字段的中文备注 + //2022/10/11 + //测试mssql 发现 不写ColumnDescription,写好注释在mssql下也能生成表字段备注 + public string LoginName { get; set; } + + /// + /// 登录密码 + /// + [SugarColumn(Length = 200, IsNullable = true)] + public string LoginPWD { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(Length = 200, IsNullable = true)] + public string RealName { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } + + /// + /// 部门 + /// + [SugarColumn(IsNullable = true)] + public int DepartmentId { get; set; } = -1; + + /// + /// 备注 + /// + [SugarColumn(Length = 2000, IsNullable = true)] + public string Remark { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateTime { get; set; } = DateTime.Now; + + /// + /// 更新时间 + /// + public DateTime UpdateTime { get; set; } = DateTime.Now; + + /// + /// 关键业务修改时间 + /// + public DateTime CriticalModifyTime { get; set; } = DateTime.Now; + + /// + ///最后异常时间 + /// + public DateTime LastErrorTime { get; set; } = DateTime.Now; + + /// + ///错误次数 + /// + public int ErrorCount { get; set; } + + + /// + /// 登录账号 + /// + [SugarColumn(Length = 200, IsNullable = true)] + public string Name { get; set; } + + // 性别 + [SugarColumn(IsNullable = true)] + public int Sex { get; set; } = 0; + + // 年龄 + [SugarColumn(IsNullable = true)] + public int Age { get; set; } + + // 生日 + [SugarColumn(IsNullable = true)] + public DateTime Birth { get; set; } = DateTime.Now; + + // 地址 + [SugarColumn(Length = 200, IsNullable = true)] + public string Address { get; set; } + + [SugarColumn(DefaultValue = "1")] + public bool Enable { get; set; } = true; + + [SugarColumn(IsNullable = true)] + public bool IsDeleted { get; set; } + + /// + /// 租户Id + /// + [SugarColumn(IsNullable = false, DefaultValue = "0")] + public long TenantId { get; set; } + + [Navigate(NavigateType.OneToOne, nameof(TenantId))] + public SysTenant Tenant { get; set; } + + [SugarColumn(IsIgnore = true)] + public List RoleNames { get; set; } + + [SugarColumn(IsIgnore = true)] + public List Dids { get; set; } + + [SugarColumn(IsIgnore = true)] + public string DepartmentName { get; set; } + } } \ No newline at end of file diff --git a/Blog.Core.Tests/Controller_Test/LoginController_Should.cs b/Blog.Core.Tests/Controller_Test/LoginController_Should.cs index 7c0cb1bb..c9387f72 100644 --- a/Blog.Core.Tests/Controller_Test/LoginController_Should.cs +++ b/Blog.Core.Tests/Controller_Test/LoginController_Should.cs @@ -3,72 +3,76 @@ using Xunit; using Autofac; using Blog.Core.AuthHelper; +using Microsoft.Extensions.Logging; namespace Blog.Core.Tests { - public class LoginController_Should - { - LoginController loginController; - - private readonly ISysUserInfoServices _sysUserInfoServices; - private readonly IUserRoleServices _userRoleServices; - private readonly IRoleServices _roleServices; - private readonly PermissionRequirement _requirement; - private readonly IRoleModulePermissionServices _roleModulePermissionServices; - - DI_Test dI_Test = new DI_Test(); - - - - public LoginController_Should() - { - var container = dI_Test.DICollections(); - _sysUserInfoServices = container.Resolve(); - _userRoleServices = container.Resolve(); - _roleServices = container.Resolve(); - _requirement = container.Resolve(); - _roleModulePermissionServices = container.Resolve(); - loginController = new LoginController(_sysUserInfoServices,_userRoleServices,_roleServices,_requirement, _roleModulePermissionServices); - } - - [Fact] - public void GetJwtStrTest() - { - var data = loginController.GetJwtStr("test", "test"); - - Assert.NotNull(data); - } - [Fact] - public void GetJwtStrForNuxtTest() - { - object blogs = loginController.GetJwtStrForNuxt("test", "test"); - - Assert.NotNull(blogs); - } - - [Fact] - public async void GetJwtToken3Test() - { - - var res = await loginController.GetJwtToken3("test", "test"); - - Assert.NotNull(res); - } - - [Fact] - public async void RefreshTokenTest() - { - var res = await loginController.RefreshToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsImp0aSI6IjgiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL2V4cGlyYXRpb24iOiIyMDE5LzEwLzE4IDIzOjI2OjQ5IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5UZXN0IiwibmJmIjoxNTcxNDA4ODA5LCJleHAiOjE1NzE0MTI0MDksImlzcyI6IkJsb2cuQ29yZSIsImF1ZCI6IndyIn0.oz-SPz6UCL78fM09bUecw5rmjcNYEY9dWGtuPs2gdBg"); - - Assert.NotNull(res); - } - - [Fact] - public void Md5PasswordTest() - { - var res = loginController.Md5Password("test"); - - Assert.NotNull(res); - } - } -} + public class LoginController_Should + { + LoginController loginController; + + private readonly ISysUserInfoServices _sysUserInfoServices; + private readonly IUserRoleServices _userRoleServices; + private readonly IRoleServices _roleServices; + private readonly PermissionRequirement _requirement; + private readonly IRoleModulePermissionServices _roleModulePermissionServices; + private readonly ILogger _logger; + + DI_Test dI_Test = new DI_Test(); + + + public LoginController_Should() + { + var container = dI_Test.DICollections(); + _sysUserInfoServices = container.Resolve(); + _userRoleServices = container.Resolve(); + _roleServices = container.Resolve(); + _requirement = container.Resolve(); + _roleModulePermissionServices = container.Resolve(); + _logger = container.Resolve>(); + loginController = new LoginController(_sysUserInfoServices, _userRoleServices, _roleServices, _requirement, + _roleModulePermissionServices, _logger); + } + + [Fact] + public void GetJwtStrTest() + { + var data = loginController.GetJwtStr("test", "test"); + + Assert.NotNull(data); + } + + [Fact] + public void GetJwtStrForNuxtTest() + { + object blogs = loginController.GetJwtStrForNuxt("test", "test"); + + Assert.NotNull(blogs); + } + + [Fact] + public async void GetJwtToken3Test() + { + var res = await loginController.GetJwtToken3("test", "test"); + + Assert.NotNull(res); + } + + [Fact] + public async void RefreshTokenTest() + { + var res = await loginController.RefreshToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsImp0aSI6IjgiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL2V4cGlyYXRpb24iOiIyMDE5LzEwLzE4IDIzOjI2OjQ5IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5UZXN0IiwibmJmIjoxNTcxNDA4ODA5LCJleHAiOjE1NzE0MTI0MDksImlzcyI6IkJsb2cuQ29yZSIsImF1ZCI6IndyIn0.oz-SPz6UCL78fM09bUecw5rmjcNYEY9dWGtuPs2gdBg"); + + Assert.NotNull(res); + } + + [Fact] + public void Md5PasswordTest() + { + var res = loginController.Md5Password("test"); + + Assert.NotNull(res); + } + } +} \ No newline at end of file From 1c700fd90d8a2032114c5390a6681dc3d8689e9e Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Fri, 26 May 2023 15:20:26 +0800 Subject: [PATCH 179/289] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96=E5=A4=9A?= =?UTF-8?q?=E6=AC=A1=E8=A7=A3=E6=9E=90Token=E9=87=8D=E5=A4=8D=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0Claims?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpContextUser/AspNetUser.cs | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/Blog.Core.Common/HttpContextUser/AspNetUser.cs b/Blog.Core.Common/HttpContextUser/AspNetUser.cs index ba21889e..1ceaa45f 100644 --- a/Blog.Core.Common/HttpContextUser/AspNetUser.cs +++ b/Blog.Core.Common/HttpContextUser/AspNetUser.cs @@ -1,13 +1,12 @@ -using System; +using Blog.Core.Common.Swagger; +using Blog.Core.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; -using Blog.Core.Common.Swagger; -using Blog.Core.Model; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using SqlSugar.Extensions; namespace Blog.Core.Common.HttpContextUser { @@ -34,7 +33,9 @@ private string GetName() { if (!string.IsNullOrEmpty(GetToken())) { - var getNameType = Permissions.IsUseIds4 ? "name" : "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; + var getNameType = Permissions.IsUseIds4 + ? "name" + : "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; return GetUserInfoFromToken(getNameType).FirstOrDefault().ObjToString(); } } @@ -58,15 +59,19 @@ public string GetToken() { return token; } - + if (_accessor.HttpContext?.IsSuccessSwagger() == true) { token = _accessor.HttpContext.GetSuccessSwaggerJwt(); if (token.IsNotEmptyOrNull()) { + if (_accessor.HttpContext.User.Claims.Any(s => s.Type == JwtRegisteredClaimNames.Jti)) + { + return token; + } + var claims = new ClaimsIdentity(GetClaimsIdentity(token)); _accessor.HttpContext.User.AddIdentity(claims); - return token; } } @@ -86,8 +91,8 @@ public List GetUserInfoFromToken(string ClaimType) JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(token); return (from item in jwtToken.Claims - where item.Type == ClaimType - select item.Value).ToList(); + where item.Type == ClaimType + select item.Value).ToList(); } return new List() { }; @@ -100,7 +105,7 @@ public IEnumerable GetClaimsIdentity() if (_accessor.HttpContext == null) return ArraySegment.Empty; if (!IsAuthenticated()) return GetClaimsIdentity(GetToken()); - + var claims = _accessor.HttpContext.User.Claims.ToList(); var headers = _accessor.HttpContext.Request.Headers; foreach (var header in headers) @@ -110,6 +115,7 @@ public IEnumerable GetClaimsIdentity() return claims; } + public IEnumerable GetClaimsIdentity(string token) { var jwtHandler = new JwtSecurityTokenHandler(); @@ -127,8 +133,8 @@ public IEnumerable GetClaimsIdentity(string token) public List GetClaimValueByType(string ClaimType) { return (from item in GetClaimsIdentity() - where item.Type == ClaimType - select item.Value).ToList(); + where item.Type == ClaimType + select item.Value).ToList(); } } } \ No newline at end of file From 3f24902521fe64385198bbd4f4f5effb662727c3 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 30 May 2023 10:27:43 +0800 Subject: [PATCH 180/289] Update PermissionHandler.cs --- .../Policys/PermissionHandler.cs | 451 +++++++++--------- 1 file changed, 228 insertions(+), 223 deletions(-) diff --git a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs index d2dbfdf7..27099b7b 100644 --- a/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs +++ b/Blog.Core.Extensions/Authorizations/Policys/PermissionHandler.cs @@ -19,252 +19,257 @@ namespace Blog.Core.AuthHelper { - /// - /// 权限授权处理器 - /// - public class PermissionHandler : AuthorizationHandler - { - /// - /// 验证方案提供对象 - /// - public IAuthenticationSchemeProvider Schemes { get; set; } + /// + /// 权限授权处理器 + /// + public class PermissionHandler : AuthorizationHandler + { + /// + /// 验证方案提供对象 + /// + public IAuthenticationSchemeProvider Schemes { get; set; } - private readonly IRoleModulePermissionServices _roleModulePermissionServices; - private readonly IHttpContextAccessor _accessor; - private readonly ISysUserInfoServices _userServices; - private readonly IUser _user; + private readonly IRoleModulePermissionServices _roleModulePermissionServices; + private readonly IHttpContextAccessor _accessor; + private readonly ISysUserInfoServices _userServices; + private readonly IUser _user; - /// - /// 构造函数注入 - /// - /// - /// - /// - /// - /// - public PermissionHandler(IAuthenticationSchemeProvider schemes, - IRoleModulePermissionServices roleModulePermissionServices, IHttpContextAccessor accessor, - ISysUserInfoServices userServices, IUser user) - { - _accessor = accessor; - _userServices = userServices; - _user = user; - Schemes = schemes; - _roleModulePermissionServices = roleModulePermissionServices; - } + /// + /// 构造函数注入 + /// + /// + /// + /// + /// + /// + public PermissionHandler(IAuthenticationSchemeProvider schemes, + IRoleModulePermissionServices roleModulePermissionServices, IHttpContextAccessor accessor, + ISysUserInfoServices userServices, IUser user) + { + _accessor = accessor; + _userServices = userServices; + _user = user; + Schemes = schemes; + _roleModulePermissionServices = roleModulePermissionServices; + } - // 重写异步处理程序 - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, - PermissionRequirement requirement) - { - var httpContext = _accessor.HttpContext; + // 重写异步处理程序 + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + PermissionRequirement requirement) + { + var httpContext = _accessor.HttpContext; - // 获取系统中所有的角色和菜单的关系集合 - if (!requirement.Permissions.Any()) - { - var data = await _roleModulePermissionServices.RoleModuleMaps(); - var list = new List(); - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - list = (from item in data - where item.IsDeleted == false - orderby item.Id - select new PermissionItem - { - Url = item.Module?.LinkUrl, - Role = item.Role?.Id.ObjToString(), - }).ToList(); - } - // jwt - else - { - list = (from item in data - where item.IsDeleted == false - orderby item.Id - select new PermissionItem - { - Url = item.Module?.LinkUrl, - Role = item.Role?.Name.ObjToString(), - }).ToList(); - } + // 获取系统中所有的角色和菜单的关系集合 + if (!requirement.Permissions.Any()) + { + var data = await _roleModulePermissionServices.RoleModuleMaps(); + var list = new List(); + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + list = (from item in data + where item.IsDeleted == false + orderby item.Id + select new PermissionItem + { + Url = item.Module?.LinkUrl, + Role = item.Role?.Id.ObjToString(), + }).ToList(); + } + // jwt + else + { + list = (from item in data + where item.IsDeleted == false + orderby item.Id + select new PermissionItem + { + Url = item.Module?.LinkUrl, + Role = item.Role?.Name.ObjToString(), + }).ToList(); + } - requirement.Permissions = list; - } + requirement.Permissions = list; + } - if (httpContext != null) - { - var questUrl = httpContext.Request.Path.Value.ToLower(); + if (httpContext != null) + { + var questUrl = httpContext.Request.Path.Value.ToLower(); - // 整体结构类似认证中间件UseAuthentication的逻辑,具体查看开源地址 - // https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs - httpContext.Features.Set(new AuthenticationFeature - { - OriginalPath = httpContext.Request.Path, - OriginalPathBase = httpContext.Request.PathBase - }); + // 整体结构类似认证中间件UseAuthentication的逻辑,具体查看开源地址 + // https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs + httpContext.Features.Set(new AuthenticationFeature + { + OriginalPath = httpContext.Request.Path, + OriginalPathBase = httpContext.Request.PathBase + }); - // Give any IAuthenticationRequestHandler schemes a chance to handle the request - // 主要作用是: 判断当前是否需要进行远程验证,如果是就进行远程验证 - var handlers = httpContext.RequestServices.GetRequiredService(); - foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) - { - if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler - handler && await handler.HandleRequestAsync()) - { - context.Fail(); - return; - } - } + // Give any IAuthenticationRequestHandler schemes a chance to handle the request + // 主要作用是: 判断当前是否需要进行远程验证,如果是就进行远程验证 + var handlers = httpContext.RequestServices.GetRequiredService(); + foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) + { + if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler + handler && await handler.HandleRequestAsync()) + { + context.Fail(); + return; + } + } - //判断请求是否拥有凭据,即有没有登录 - var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); - if (defaultAuthenticate != null) - { - var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); + //判断请求是否拥有凭据,即有没有登录 + var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); + if (defaultAuthenticate != null) + { + var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); - // 是否开启测试环境 - var isTestCurrent = AppSettings.app(new string[] {"AppSettings", "UseLoadTest"}).ObjToBool(); + // 是否开启测试环境 + var isTestCurrent = AppSettings.app(new string[] { "AppSettings", "UseLoadTest" }).ObjToBool(); - //result?.Principal不为空即登录成功 - if (result?.Principal != null || isTestCurrent || httpContext.IsSuccessSwagger()) - { - if (!isTestCurrent) httpContext.User = result.Principal; + //result?.Principal不为空即登录成功 + if (result?.Principal != null || isTestCurrent || httpContext.IsSuccessSwagger()) + { + if (!isTestCurrent) httpContext.User = result.Principal; - //应该要先校验用户的信息 再校验菜单权限相关的 + //应该要先校验用户的信息 再校验菜单权限相关的 + // JWT模式下校验当前用户状态 + // IDS4也可以校验,可以通过服务或者接口形式 + SysUserInfo user = new(); + if (!Permissions.IsUseIds4) + { + //校验用户 + user = await _userServices.QueryById(_user.ID, true); + if (user == null) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户不存在或已被删除").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } - //校验用户 - var user = await _userServices.QueryById(_user.ID, true); - if (user == null) - { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户不存在或已被删除").MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } + if (user.IsDeleted) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被删除,禁止登陆!").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } - if (user.IsDeleted) - { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被删除,禁止登陆!").MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } + if (!user.Enable) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被禁用!禁止登陆!").MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + } - if (!user.Enable) - { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被禁用!禁止登陆!").MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } + // 判断token是否过期,过期则重新登录 + var isExp = false; + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + isExp = (httpContext.User.Claims.FirstOrDefault(s => s.Type == "exp")?.Value) != null && + DateHelper.StampToDateTime(httpContext.User.Claims + .FirstOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now; + } + else + { + // jwt + isExp = + (httpContext.User.Claims.FirstOrDefault(s => s.Type == ClaimTypes.Expiration) + ?.Value) != null && + DateTime.Parse(httpContext.User.Claims + .FirstOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now; + } - // 判断token是否过期,过期则重新登录 - var isExp = false; - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - isExp = (httpContext.User.Claims.FirstOrDefault(s => s.Type == "exp")?.Value) != null && - DateHelper.StampToDateTime(httpContext.User.Claims - .FirstOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now; - } - else - { - // jwt - isExp = - (httpContext.User.Claims.FirstOrDefault(s => s.Type == ClaimTypes.Expiration) - ?.Value) != null && - DateTime.Parse(httpContext.User.Claims - .FirstOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now; - } + if (!isExp) + { + context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权")); + return; + } - if (!isExp) - { - context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权")); - return; - } + //校验签发时间 + if (!Permissions.IsUseIds4) + { + var value = httpContext.User.Claims + .FirstOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; + if (value != null) + { + if (user.CriticalModifyTime > value.ObjToDate()) + { + _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权") + .MessageModel; + context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); + return; + } + } + } - //校验签发时间 - if (!Permissions.IsUseIds4) - { - var value = httpContext.User.Claims - .FirstOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; - if (value != null) - { - if (user.CriticalModifyTime > value.ObjToDate()) - { - _user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权") - .MessageModel; - context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg)); - return; - } - } - } + // 获取当前用户的角色信息 + var currentUserRoles = new List(); + // ids4和jwt切换 + // ids4 + if (Permissions.IsUseIds4) + { + currentUserRoles = (from item in httpContext.User.Claims + where item.Type == "role" + select item.Value).ToList(); + } + else + { + // jwt + currentUserRoles = (from item in httpContext.User.Claims + where item.Type == requirement.ClaimType + select item.Value).ToList(); + } - // 获取当前用户的角色信息 - var currentUserRoles = new List(); - // ids4和jwt切换 - // ids4 - if (Permissions.IsUseIds4) - { - currentUserRoles = (from item in httpContext.User.Claims - where item.Type == "role" - select item.Value).ToList(); - } - else - { - // jwt - currentUserRoles = (from item in httpContext.User.Claims - where item.Type == requirement.ClaimType - select item.Value).ToList(); - } + //超级管理员 默认拥有所有权限 + if (currentUserRoles.All(s => s != "SuperAdmin")) + { + var isMatchRole = false; + var permisssionRoles = + requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role)); + foreach (var item in permisssionRoles) + { + try + { + if (Regex.Match(questUrl, item.Url?.ObjToString().ToLower())?.Value == questUrl) + { + isMatchRole = true; + break; + } + } + catch (Exception) + { + // ignored + } + } - //超级管理员 默认拥有所有权限 - if (currentUserRoles.All(s => s != "SuperAdmin")) - { - var isMatchRole = false; - var permisssionRoles = - requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role)); - foreach (var item in permisssionRoles) - { - try - { - if (Regex.Match(questUrl, item.Url?.ObjToString().ToLower())?.Value == questUrl) - { - isMatchRole = true; - break; - } - } - catch (Exception) - { - // ignored - } - } + //验证权限 + if (currentUserRoles.Count <= 0 || !isMatchRole) + { + context.Fail(); + return; + } + } - //验证权限 - if (currentUserRoles.Count <= 0 || !isMatchRole) - { - context.Fail(); - return; - } - } + context.Succeed(requirement); + return; + } + } - context.Succeed(requirement); - return; - } - } + //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败 + if (!(questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && + (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))) + { + context.Fail(); + return; + } + } - //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败 - if (!(questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && - (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))) - { - context.Fail(); - return; - } - } - - //context.Succeed(requirement); - } - } + //context.Succeed(requirement); + } + } } \ No newline at end of file From 7629527ee987b3d2d081a7423f9a2873cd48d2ee Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Thu, 1 Jun 2023 17:54:54 +0800 Subject: [PATCH 181/289] =?UTF-8?q?=E2=9C=A8=F0=9F=8E=A8=20=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E7=9A=84=E5=AE=8C=E5=96=84=E4=BC=98=E5=8C=96=201.?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=B0=81=E8=A3=85=E5=9F=BA=E4=BA=8E=E5=BE=AE?= =?UTF-8?q?=E8=BD=AF=E5=88=86=E5=B8=83=E5=BC=8F=E7=BC=93=E5=AD=98=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3IDistributedCache=E4=BD=BF=E7=94=A8=202.IDistributedCa?= =?UTF-8?q?che=E5=8F=AA=E9=80=82=E5=90=88=E6=99=AE=E9=80=9A=E7=9A=84?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E4=BD=BF=E7=94=A8,=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E8=A6=81=E4=BD=BF=E7=94=A8redis=E9=98=9F=E5=88=97=E3=80=81?= =?UTF-8?q?=E8=AE=A2=E9=98=85redis=E6=B6=88=E6=81=AF=E7=AD=89,=E5=B0=B1?= =?UTF-8?q?=E8=A6=81=E4=BD=BF=E7=94=A8redis=E5=8E=9F=E7=94=9F=E5=BA=93=203?= =?UTF-8?q?.=E5=A2=9E=E5=8A=A0=E7=BC=93=E5=AD=98=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3[Systems/CacheManageController]=204.=E7=9B=AE?= =?UTF-8?q?=E5=89=8D=E6=94=AF=E6=8C=81=E5=86=85=E5=AD=98=E3=80=81redis?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E5=AE=9E=E7=8E=B0,=E7=90=86=E8=AE=BA?= =?UTF-8?q?=E5=8F=AF=E9=9A=8F=E6=84=8F=E6=89=A9=E5=B1=95=E7=94=9A=E8=87=B3?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E5=AE=9E=E7=8E=B0=205.=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E4=BD=BF=E7=94=A8=E5=86=85=E5=AD=98=E7=BC=93=E5=AD=98?= =?UTF-8?q?,=E5=8F=AF=E5=9C=A8appsetting.json=E4=B8=AD=E9=85=8D=E7=BD=AERe?= =?UTF-8?q?dis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 切换到IDistributedCache好处如下 默认session使用IDistributedCache进行存储,如果你搭配使用IDistributedCache+外部缓存(如Redis),可实现应用程序重启session不丢失 更直观就是,调试的时候登录swagger后即使重启调试也无需在登陆 --- Blog.Core.Api/Blog.Core.Api.csproj | 1 - Blog.Core.Api/Blog.Core.xml | 35 ++ .../Systems/CacheManageController.cs | 81 ++++ Blog.Core.Api/Program.cs | 3 +- Blog.Core.Api/Startup.cs | 5 +- Blog.Core.Api/appsettings.json | 4 +- Blog.Core.Common/Blog.Core.Common.csproj | 1 - Blog.Core.Common/Caches/Caching.cs | 328 ++++++++++++++ Blog.Core.Common/Caches/ICaching.cs | 53 +++ .../Caches/SqlSugarCacheService.cs | 62 +++ Blog.Core.Common/Const/CacheConst.cs | 87 ++++ Blog.Core.Common/DB/BaseDBConfig.cs | 1 - .../Extensions/ExpressionExtensions.cs | 410 +++++++++--------- .../MemoryCache/ICachingProvider.cs | 12 - Blog.Core.Common/MemoryCache/MemoryCaching.cs | 30 -- Blog.Core.Common/Option/RedisOptions.cs | 24 + Blog.Core.Extensions/AOP/BlogCacheAOP.cs | 8 +- .../Blog.Core.Extensions.csproj | 1 + .../Redis/IRedisBasketRepository.cs | 1 + .../Redis/RedisBasketRepository.cs | 1 + .../ServiceExtensions/CacheSetup.cs | 45 ++ .../ServiceExtensions/HttpRuntimeCache.cs | 67 --- .../ServiceExtensions/MemoryCacheSetup.cs | 27 -- .../ServiceExtensions/RedisCacheSetup.cs | 34 -- .../ServiceExtensions/SqlsugarSetup.cs | 12 +- .../Helper/CustomJwtTokenAuthMiddleware.cs | 1 + .../Utility/SerilogRequestUtility.cs | 104 ++--- 27 files changed, 992 insertions(+), 446 deletions(-) create mode 100644 Blog.Core.Api/Controllers/Systems/CacheManageController.cs create mode 100644 Blog.Core.Common/Caches/Caching.cs create mode 100644 Blog.Core.Common/Caches/ICaching.cs create mode 100644 Blog.Core.Common/Caches/SqlSugarCacheService.cs create mode 100644 Blog.Core.Common/Const/CacheConst.cs delete mode 100644 Blog.Core.Common/MemoryCache/ICachingProvider.cs delete mode 100644 Blog.Core.Common/MemoryCache/MemoryCaching.cs create mode 100644 Blog.Core.Common/Option/RedisOptions.cs create mode 100644 Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs delete mode 100644 Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs delete mode 100644 Blog.Core.Extensions/ServiceExtensions/MemoryCacheSetup.cs delete mode 100644 Blog.Core.Extensions/ServiceExtensions/RedisCacheSetup.cs diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 2f8c41e9..f3f9befa 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -106,7 +106,6 @@ - diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 672ea21f..69d3f9b2 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1299,6 +1299,41 @@ + + + 缓存管理 + + + + + 获取全部缓存 + + + + + + 获取缓存 + + + + + + 新增 + + + + + + 删除全部缓存 + + + + + + 删除缓存 + + + 数据库管理 diff --git a/Blog.Core.Api/Controllers/Systems/CacheManageController.cs b/Blog.Core.Api/Controllers/Systems/CacheManageController.cs new file mode 100644 index 00000000..4f400e8f --- /dev/null +++ b/Blog.Core.Api/Controllers/Systems/CacheManageController.cs @@ -0,0 +1,81 @@ +using Blog.Core.Common.Caches; +using Blog.Core.Controllers; +using Blog.Core.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blog.Core.Api.Controllers.Systems; + +/// +/// 缓存管理 +/// +[Route("api/Systems/[controller]")] +[ApiController] +[Authorize(Permissions.Name)] +public class CacheManageController : BaseApiController +{ + private readonly ICaching _caching; + + public CacheManageController(ICaching caching) + { + _caching = caching; + } + + /// + /// 获取全部缓存 + /// + /// + [HttpGet] + public async Task>> Get() + { + return Success(await _caching.GetAllCacheKeysAsync()); + } + + /// + /// 获取缓存 + /// + /// + [HttpGet("{key}")] + public async Task> Get(string key) + { + return Success(await _caching.GetStringAsync(key)); + } + + /// + /// 新增 + /// + /// + [HttpPost] + public async Task Post([FromQuery] string key, [FromQuery] string value, [FromQuery] int? expire) + { + if (expire.HasValue) + await _caching.SetStringAsync(key, value, TimeSpan.FromMilliseconds(expire.Value)); + else + await _caching.SetStringAsync(key, value); + + return Success(); + } + + /// + /// 删除全部缓存 + /// + /// + [HttpDelete] + public async Task Delete() + { + await _caching.RemoveAllAsync(); + return Success(); + } + + /// + /// 删除缓存 + /// + /// + [Route("{key}")] + [HttpDelete] + public async Task Delete(string key) + { + await _caching.RemoveAsync(key); + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index b4929cc7..4311c97c 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -57,8 +57,7 @@ JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); -builder.Services.AddMemoryCacheSetup(); -builder.Services.AddRedisCacheSetup(); +builder.Services.AddCacheSetup(); builder.Services.AddSqlsugarSetup(); builder.Services.AddDbSetup(); diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs index 64e5ee78..2911625d 100644 --- a/Blog.Core.Api/Startup.cs +++ b/Blog.Core.Api/Startup.cs @@ -8,6 +8,7 @@ using Blog.Core.Common.Seed; using Blog.Core.Extensions; using Blog.Core.Extensions.Middlewares; +using Blog.Core.Extensions.ServiceExtensions; using Blog.Core.Filter; using Blog.Core.Hubs; using Blog.Core.IServices; @@ -49,9 +50,7 @@ public void ConfigureServices(IServiceCollection services) // 确保从认证中心返回的ClaimType不被更改,不使用Map映射 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - services.AddMemoryCacheSetup(); - services.AddRedisCacheSetup(); - + services.AddCacheSetup(); services.AddSqlsugarSetup(); services.AddDbSetup(); services.AddAutoMapperSetup(); diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index cef87bed..d220a034 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -18,7 +18,9 @@ }, "AllowedHosts": "*", "Redis": { - "ConnectionString": "127.0.0.1:6319,password=admin" + "Enable": true, + "ConnectionString": "127.0.0.1:6379", + "InstanceName": "" //前缀 }, "RabbitMQ": { "Enabled": false, diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index aeba8476..5f249870 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -51,7 +51,6 @@ - diff --git a/Blog.Core.Common/Caches/Caching.cs b/Blog.Core.Common/Caches/Caching.cs new file mode 100644 index 00000000..e7b4ab07 --- /dev/null +++ b/Blog.Core.Common/Caches/Caching.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Blog.Core.Common.Const; +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; + +namespace Blog.Core.Common.Caches; + +public class Caching : ICaching +{ + private readonly IDistributedCache _cache; + + public Caching(IDistributedCache cache) + { + _cache = cache; + } + + private byte[] GetBytes(T source) + { + return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(source)); + } + + public IDistributedCache Cache => _cache; + + public void AddCacheKey(string cacheKey) + { + var res = _cache.GetString(CacheConst.KeyAll); + var allkeys = string.IsNullOrWhiteSpace(res) ? new List() : JsonConvert.DeserializeObject>(res); + if (!allkeys.Any(m => m == cacheKey)) + { + allkeys.Add(cacheKey); + _cache.SetString(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } + } + + /// + /// 增加缓存Key + /// + /// + /// + public async Task AddCacheKeyAsync(string cacheKey) + { + var res = await _cache.GetStringAsync(CacheConst.KeyAll); + var allkeys = string.IsNullOrWhiteSpace(res) ? new List() : JsonConvert.DeserializeObject>(res); + if (!allkeys.Any(m => m == cacheKey)) + { + allkeys.Add(cacheKey); + await _cache.SetStringAsync(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } + } + + public void DelByPattern(string key) + { + var allkeys = GetAllCacheKeys(); + if (allkeys == null) return; + + var delAllkeys = allkeys.Where(u => u.Contains(key)).ToList(); + delAllkeys.ForEach(u => { _cache.Remove(u); }); + + // 更新所有缓存键 + allkeys = allkeys.Where(u => !u.Contains(key)).ToList(); + _cache.SetString(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } + + /// + /// 删除某特征关键字缓存 + /// + /// + /// + public async Task DelByPatternAsync(string key) + { + var allkeys = await GetAllCacheKeysAsync(); + if (allkeys == null) return; + + var delAllkeys = allkeys.Where(u => u.Contains(key)).ToList(); + delAllkeys.ForEach(u => { _cache.Remove(u); }); + + // 更新所有缓存键 + allkeys = allkeys.Where(u => !u.Contains(key)).ToList(); + await _cache.SetStringAsync(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } + + public void DelCacheKey(string cacheKey) + { + var res = _cache.GetString(CacheConst.KeyAll); + var allkeys = string.IsNullOrWhiteSpace(res) ? new List() : JsonConvert.DeserializeObject>(res); + if (allkeys.Any(m => m == cacheKey)) + { + allkeys.Remove(cacheKey); + _cache.SetString(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } + } + + /// + /// 删除缓存 + /// + /// + /// + public async Task DelCacheKeyAsync(string cacheKey) + { + var res = await _cache.GetStringAsync(CacheConst.KeyAll); + var allkeys = string.IsNullOrWhiteSpace(res) ? new List() : JsonConvert.DeserializeObject>(res); + if (allkeys.Any(m => m == cacheKey)) + { + allkeys.Remove(cacheKey); + await _cache.SetStringAsync(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } + } + + public bool Exists(string cacheKey) + { + var res = _cache.Get(cacheKey); + return res != null; + } + + /// + /// 检查给定 key 是否存在 + /// + /// 键 + /// + public async Task ExistsAsync(string cacheKey) + { + var res = await _cache.GetAsync(cacheKey); + return res != null; + } + + public List GetAllCacheKeys() + { + var res = _cache.GetString(CacheConst.KeyAll); + return string.IsNullOrWhiteSpace(res) ? null : JsonConvert.DeserializeObject>(res); + } + + /// + /// 获取所有缓存列表 + /// + /// + public async Task> GetAllCacheKeysAsync() + { + var res = await _cache.GetStringAsync(CacheConst.KeyAll); + return string.IsNullOrWhiteSpace(res) ? null : JsonConvert.DeserializeObject>(res); + } + + public T Get(string cacheKey) + { + var res = _cache.Get(cacheKey); + return res == null ? default : JsonConvert.DeserializeObject(Encoding.UTF8.GetString(res)); + } + + /// + /// 获取缓存 + /// + /// + /// + /// + public async Task GetAsync(string cacheKey) + { + var res = await _cache.GetAsync(cacheKey); + return res == null ? default : JsonConvert.DeserializeObject(Encoding.UTF8.GetString(res)); + } + + public string GetString(string cacheKey) + { + return _cache.GetString(cacheKey); + } + + /// + /// 获取缓存 + /// + /// + /// + public async Task GetStringAsync(string cacheKey) + { + return await _cache.GetStringAsync(cacheKey); + } + + public void Remove(string key) + { + _cache.Remove(key); + DelCacheKey(key); + } + + /// + /// 删除缓存 + /// + /// + /// + public async Task RemoveAsync(string key) + { + await _cache.RemoveAsync(key); + await DelCacheKeyAsync(key); + } + + public void RemoveAll() + { + var catches = GetAllCacheKeys(); + foreach (var @catch in catches) Remove(@catch); + + catches.Clear(); + _cache.SetString(CacheConst.KeyAll, JsonConvert.SerializeObject(catches)); + } + + public async Task RemoveAllAsync() + { + var catches = await GetAllCacheKeysAsync(); + foreach (var @catch in catches) await RemoveAsync(@catch); + + catches.Clear(); + await _cache.SetStringAsync(CacheConst.KeyAll, JsonConvert.SerializeObject(catches)); + } + + + public void Set(string cacheKey, T value, TimeSpan? expire = null) + { + _cache.Set(cacheKey, GetBytes(value), expire == null ? new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)} : new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); + + AddCacheKey(cacheKey); + } + + /// + /// 增加对象缓存 + /// + /// + /// + /// + public async Task SetAsync(string cacheKey, T value) + { + await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value)), new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)}); + + await AddCacheKeyAsync(cacheKey); + } + + /// + /// 增加对象缓存,并设置过期时间 + /// + /// + /// + /// + /// + public async Task SetAsync(string cacheKey, T value, TimeSpan expire) + { + await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value)), new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); + + await AddCacheKeyAsync(cacheKey); + } + + public void SetPermanent(string cacheKey, T value) + { + _cache.Set(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value))); + AddCacheKey(cacheKey); + } + public async Task SetPermanentAsync(string cacheKey, T value) + { + await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value))); + await AddCacheKeyAsync(cacheKey); + } + + public void SetString(string cacheKey, string value, TimeSpan? expire = null) + { + if (expire == null) + _cache.SetString(cacheKey, value, new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)}); + else + _cache.SetString(cacheKey, value, new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); + + AddCacheKey(cacheKey); + } + + /// + /// 增加字符串缓存 + /// + /// + /// + /// + public async Task SetStringAsync(string cacheKey, string value) + { + await _cache.SetStringAsync(cacheKey, value, new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)}); + + await AddCacheKeyAsync(cacheKey); + } + + /// + /// 增加字符串缓存,并设置过期时间 + /// + /// + /// + /// + /// + public async Task SetStringAsync(string cacheKey, string value, TimeSpan expire) + { + await _cache.SetStringAsync(cacheKey, value, new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); + + await AddCacheKeyAsync(cacheKey); + } + + + /// + /// 缓存最大角色数据范围 + /// + /// + /// + /// + public async Task SetMaxDataScopeType(long userId, int dataScopeType) + { + var cacheKey = CacheConst.KeyMaxDataScopeType + userId; + await SetStringAsync(cacheKey, dataScopeType.ToString()); + + await AddCacheKeyAsync(cacheKey); + } + + /// + /// 根据父键清空 + /// + /// + /// + public async Task DelByParentKeyAsync(string key) + { + var allkeys = await GetAllCacheKeysAsync(); + if (allkeys == null) return; + + var delAllkeys = allkeys.Where(u => u.StartsWith(key)).ToList(); + delAllkeys.ForEach(Remove); + // 更新所有缓存键 + allkeys = allkeys.Where(u => !u.StartsWith(key)).ToList(); + await SetStringAsync(CacheConst.KeyAll, JsonConvert.SerializeObject(allkeys)); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Caches/ICaching.cs b/Blog.Core.Common/Caches/ICaching.cs new file mode 100644 index 00000000..f856aeb6 --- /dev/null +++ b/Blog.Core.Common/Caches/ICaching.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; + +namespace Blog.Core.Common.Caches; + +/// +/// 缓存抽象接口,基于IDistributedCache封装 +/// +public interface ICaching +{ + public IDistributedCache Cache { get; } + void AddCacheKey(string cacheKey); + Task AddCacheKeyAsync(string cacheKey); + + void DelByPattern(string key); + Task DelByPatternAsync(string key); + + void DelCacheKey(string cacheKey); + Task DelCacheKeyAsync(string cacheKey); + + bool Exists(string cacheKey); + Task ExistsAsync(string cacheKey); + + List GetAllCacheKeys(); + Task> GetAllCacheKeysAsync(); + + T Get(string cacheKey); + Task GetAsync(string cacheKey); + + string GetString(string cacheKey); + Task GetStringAsync(string cacheKey); + + void Remove(string key); + Task RemoveAsync(string key); + + void RemoveAll(); + Task RemoveAllAsync(); + + void Set(string cacheKey, T value, TimeSpan? expire = null); + Task SetAsync(string cacheKey, T value); + Task SetAsync(string cacheKey, T value, TimeSpan expire); + + void SetPermanent(string cacheKey, T value); + Task SetPermanentAsync(string cacheKey, T value); + + void SetString(string cacheKey, string value, TimeSpan? expire = null); + Task SetStringAsync(string cacheKey, string value); + Task SetStringAsync(string cacheKey, string value, TimeSpan expire); + + Task DelByParentKeyAsync(string key); +} \ No newline at end of file diff --git a/Blog.Core.Common/Caches/SqlSugarCacheService.cs b/Blog.Core.Common/Caches/SqlSugarCacheService.cs new file mode 100644 index 00000000..c26a164d --- /dev/null +++ b/Blog.Core.Common/Caches/SqlSugarCacheService.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using SqlSugar; + +namespace Blog.Core.Common.Caches; + +/// +/// 实现SqlSugar的ICacheService接口 +/// +public class SqlSugarCacheService : ICacheService +{ + private readonly Lazy _caching = new(() => App.GetService(false)); + private ICaching Caching => _caching.Value; + + public void Add(string key, V value) + { + Caching.Set(key, value); + } + + public void Add(string key, V value, int cacheDurationInSeconds) + { + Caching.Set(key, value, TimeSpan.FromSeconds(cacheDurationInSeconds)); + } + + public bool ContainsKey(string key) + { + return Caching.Exists(key); + } + + public V Get(string key) + { + return Caching.Get(key); + } + + public IEnumerable GetAllKey() + { + return Caching.GetAllCacheKeys(); + } + + public V GetOrCreate(string cacheKey, Func create, int cacheDurationInSeconds = int.MaxValue) + { + if (!ContainsKey(cacheKey)) + { + var value = create(); + Caching.Set(cacheKey, value, TimeSpan.FromSeconds(cacheDurationInSeconds)); + return value; + } + + return Caching.Get(cacheKey); + } + + public void Remove(string key) + { + Caching.Remove(key); + } + + public bool RemoveAll() + { + Caching.RemoveAll(); + return true; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Const/CacheConst.cs b/Blog.Core.Common/Const/CacheConst.cs new file mode 100644 index 00000000..92f0e633 --- /dev/null +++ b/Blog.Core.Common/Const/CacheConst.cs @@ -0,0 +1,87 @@ +namespace Blog.Core.Common.Const; + +/// +/// 缓存相关常量 +/// +public class CacheConst +{ + /// + /// 用户缓存 + /// + public const string KeyUser = "user:"; + + /// + /// 用户部门缓存 + /// + public const string KeyUserDepart = "userDepart:"; + + /// + /// 菜单缓存 + /// + public const string KeyMenu = "menu:"; + + /// + /// 菜单 + /// + public const string KeyPermissions = "permissions"; + + /// + /// 权限缓存 + /// + public const string KeyPermission = "permission:"; + + /// + /// 接口路由 + /// + public const string KeyModules = "modules"; + + /// + /// 系统配置 + /// + public const string KeySystemConfig = "sysConfig"; + + /// + /// 查询过滤器缓存 + /// + public const string KeyQueryFilter = "queryFilter:"; + + /// + /// 机构Id集合缓存 + /// + public const string KeyOrgIdList = "org:"; + + /// + /// 最大角色数据范围缓存 + /// + public const string KeyMaxDataScopeType = "maxDataScopeType:"; + + /// + /// 验证码缓存 + /// + public const string KeyVerCode = "verCode:"; + + /// + /// 所有缓存关键字集合 + /// + public const string KeyAll = "keys"; + + /// + /// 定时任务缓存 + /// + public const string KeyTimer = "timer:"; + + /// + /// 在线用户缓存 + /// + public const string KeyOnlineUser = "onlineuser:"; + + /// + /// 常量下拉框 + /// + public const string KeyConstSelector = "selector:"; + + /// + /// swagger登录缓存 + /// + public const string SwaggerLogin = "swaggerLogin:"; +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index 495414fb..b7153766 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using SqlSugar; namespace Blog.Core.Common.DB { diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions.cs b/Blog.Core.Common/Extensions/ExpressionExtensions.cs index 4058b95a..6591923f 100644 --- a/Blog.Core.Common/Extensions/ExpressionExtensions.cs +++ b/Blog.Core.Common/Extensions/ExpressionExtensions.cs @@ -4,211 +4,213 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Blog.Core.Common.Caches; namespace Blog.Core.Common.Helper { - /// - /// Linq扩展 - /// - public static class ExpressionExtensions - { - #region HttpContext - - /// - /// 返回请求上下文 - /// - /// - /// - /// - /// - /// - public static async Task Cof_SendResponse(this HttpContext context, System.Net.HttpStatusCode code, string message, string ContentType = "text/html;charset=utf-8") - { - context.Response.StatusCode = (int)code; - context.Response.ContentType = ContentType; - await context.Response.WriteAsync(message); - } - - #endregion - - #region ICaching - - /// - /// 从缓存里取数据,如果不存在则执行查询方法, - /// - /// 类型 - /// ICaching - /// 键值 - /// 查询方法 - /// 有效期 单位分钟/param> - /// - public static T Cof_GetICaching(this ICaching cache, string key, Func GetFun, int timeSpanMin) where T : class - { - var obj = cache.Get(key); - obj = GetFun(); - if (obj == null) - { - obj = GetFun(); - cache.Set(key, obj, timeSpanMin); - } - - return obj as T; - } - - /// - /// 异步从缓存里取数据,如果不存在则执行查询方法 - /// - /// 类型 - /// ICaching - /// 键值 - /// 查询方法 - /// 有效期 单位分钟/param> - /// - public static async Task Cof_AsyncGetICaching(this ICaching cache, string key, Func> GetFun, int timeSpanMin) where T : class - { - var obj = cache.Get(key); - if (obj == null) - { - obj = await GetFun(); - cache.Set(key, obj, timeSpanMin); - } - - return obj as T; - } - - #endregion - - #region 常用扩展方法 - - public static bool Cof_CheckAvailable(this IEnumerable Tlist) - { - return Tlist != null && Tlist.Count() > 0; - } - - /// - /// 调用内部方法 - /// - public static Expression Call(this Expression instance, string methodName, params Expression[] arguments) - { - if (instance.Type == typeof(string)) - return Expression.Call(instance, instance.Type.GetMethod(methodName, new Type[] { typeof(string) }), arguments); //修复string contains 出现的问题 Ambiguous match found. - else - return Expression.Call(instance, instance.Type.GetMethod(methodName), arguments); - } - - /// - /// 获取内部成员 - /// - public static Expression Property(this Expression expression, string propertyName) - { - // Todo:左边条件如果是dynamic, - // 则Expression.Property无法获取子内容 - // 报错在这里,由于expression内的对象为Object,所以无法解析到 - // var x = (expression as IQueryable).ElementType; - var exp = Expression.Property(expression, propertyName); - if (exp.Type.IsGenericType && exp.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - return Expression.Convert(exp, exp.Type.GetGenericArguments()[0]); - } - - return exp; - } - - /// - /// 转Lambda - /// - public static Expression ToLambda(this Expression body, - params ParameterExpression[] parameters) - { - return Expression.Lambda(body, parameters); - } - - #endregion - - #region 常用运算符 [ > , >= , == , < , <= , != , || , && ] - - /// - /// && - /// - public static Expression AndAlso(this Expression left, Expression right) - { - return Expression.AndAlso(left, right); - } - - /// - /// || - /// - public static Expression OrElse(this Expression left, Expression right) - { - return Expression.OrElse(left, right); - } - - /// - /// Contains - /// - public static Expression Contains(this Expression left, Expression right) - { - return left.Call("Contains", right); - } - - public static Expression StartContains(this Expression left, Expression right) - { - return left.Call("StartsWith", right); - } - - public static Expression EndContains(this Expression left, Expression right) - { - return left.Call("EndsWith", right); - } - - /// - /// > - /// - public static Expression GreaterThan(this Expression left, Expression right) - { - return Expression.GreaterThan(left, right); - } - - /// - /// >= - /// - public static Expression GreaterThanOrEqual(this Expression left, Expression right) - { - return Expression.GreaterThanOrEqual(left, right); - } - - /// - /// < - /// - public static Expression LessThan(this Expression left, Expression right) - { - return Expression.LessThan(left, right); - } - - /// - /// <= - /// - public static Expression LessThanOrEqual(this Expression left, Expression right) - { - return Expression.LessThanOrEqual(left, right); - } - - /// - /// == - /// - public static Expression Equal(this Expression left, Expression right) - { - return Expression.Equal(left, right); - } - - /// - /// != - /// - public static Expression NotEqual(this Expression left, Expression right) - { - return Expression.NotEqual(left, right); - } - - #endregion - } + /// + /// Linq扩展 + /// + public static class ExpressionExtensions + { + #region HttpContext + + /// + /// 返回请求上下文 + /// + /// + /// + /// + /// + /// + public static async Task Cof_SendResponse(this HttpContext context, System.Net.HttpStatusCode code, string message, + string ContentType = "text/html;charset=utf-8") + { + context.Response.StatusCode = (int) code; + context.Response.ContentType = ContentType; + await context.Response.WriteAsync(message); + } + + #endregion + + #region ICaching + + /// + /// 从缓存里取数据,如果不存在则执行查询方法, + /// + /// 类型 + /// ICaching + /// 键值 + /// 查询方法 + /// 有效期 单位分钟/param> + /// + public static T Cof_GetICaching(this ICaching cache, string key, Func GetFun, int timeSpanMin) where T : class + { + var obj = cache.Get(key); + if (obj == null) + { + obj = GetFun(); + cache.Set(key, obj, TimeSpan.FromMinutes(timeSpanMin)); + } + + return obj; + } + + /// + /// 异步从缓存里取数据,如果不存在则执行查询方法 + /// + /// 类型 + /// ICaching + /// 键值 + /// 查询方法 + /// 有效期 单位分钟/param> + /// + public static async Task Cof_AsyncGetICaching(this ICaching cache, string key, Func> GetFun, int timeSpanMin) where T : class + { + var obj = await cache.GetAsync(key); + if (obj == null) + { + obj = await GetFun(); + cache.Set(key, obj, TimeSpan.FromMinutes(timeSpanMin)); + } + + return obj; + } + + #endregion + + #region 常用扩展方法 + + public static bool Cof_CheckAvailable(this IEnumerable Tlist) + { + return Tlist != null && Tlist.Count() > 0; + } + + /// + /// 调用内部方法 + /// + public static Expression Call(this Expression instance, string methodName, params Expression[] arguments) + { + if (instance.Type == typeof(string)) + return Expression.Call(instance, instance.Type.GetMethod(methodName, new Type[] {typeof(string)}), + arguments); //修复string contains 出现的问题 Ambiguous match found. + else + return Expression.Call(instance, instance.Type.GetMethod(methodName), arguments); + } + + /// + /// 获取内部成员 + /// + public static Expression Property(this Expression expression, string propertyName) + { + // Todo:左边条件如果是dynamic, + // 则Expression.Property无法获取子内容 + // 报错在这里,由于expression内的对象为Object,所以无法解析到 + // var x = (expression as IQueryable).ElementType; + var exp = Expression.Property(expression, propertyName); + if (exp.Type.IsGenericType && exp.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return Expression.Convert(exp, exp.Type.GetGenericArguments()[0]); + } + + return exp; + } + + /// + /// 转Lambda + /// + public static Expression ToLambda(this Expression body, + params ParameterExpression[] parameters) + { + return Expression.Lambda(body, parameters); + } + + #endregion + + #region 常用运算符 [ > , >= , == , < , <= , != , || , && ] + + /// + /// && + /// + public static Expression AndAlso(this Expression left, Expression right) + { + return Expression.AndAlso(left, right); + } + + /// + /// || + /// + public static Expression OrElse(this Expression left, Expression right) + { + return Expression.OrElse(left, right); + } + + /// + /// Contains + /// + public static Expression Contains(this Expression left, Expression right) + { + return left.Call("Contains", right); + } + + public static Expression StartContains(this Expression left, Expression right) + { + return left.Call("StartsWith", right); + } + + public static Expression EndContains(this Expression left, Expression right) + { + return left.Call("EndsWith", right); + } + + /// + /// > + /// + public static Expression GreaterThan(this Expression left, Expression right) + { + return Expression.GreaterThan(left, right); + } + + /// + /// >= + /// + public static Expression GreaterThanOrEqual(this Expression left, Expression right) + { + return Expression.GreaterThanOrEqual(left, right); + } + + /// + /// < + /// + public static Expression LessThan(this Expression left, Expression right) + { + return Expression.LessThan(left, right); + } + + /// + /// <= + /// + public static Expression LessThanOrEqual(this Expression left, Expression right) + { + return Expression.LessThanOrEqual(left, right); + } + + /// + /// == + /// + public static Expression Equal(this Expression left, Expression right) + { + return Expression.Equal(left, right); + } + + /// + /// != + /// + public static Expression NotEqual(this Expression left, Expression right) + { + return Expression.NotEqual(left, right); + } + + #endregion + } } \ No newline at end of file diff --git a/Blog.Core.Common/MemoryCache/ICachingProvider.cs b/Blog.Core.Common/MemoryCache/ICachingProvider.cs deleted file mode 100644 index e6a06529..00000000 --- a/Blog.Core.Common/MemoryCache/ICachingProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Blog.Core.Common -{ - /// - /// 简单的缓存接口,只有查询和添加,以后会进行扩展 - /// - public interface ICaching - { - object Get(string cacheKey); - - void Set(string cacheKey, object cacheValue, int timeSpan); - } -} diff --git a/Blog.Core.Common/MemoryCache/MemoryCaching.cs b/Blog.Core.Common/MemoryCache/MemoryCaching.cs deleted file mode 100644 index fc9a60c9..00000000 --- a/Blog.Core.Common/MemoryCache/MemoryCaching.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using System; - -namespace Blog.Core.Common -{ - /// - /// 实例化缓存接口ICaching - /// - public class MemoryCaching : ICaching - { - //引用Microsoft.Extensions.Caching.Memory;这个和.net 还是不一样,没有了Httpruntime了 - private readonly IMemoryCache _cache; - //还是通过构造函数的方法,获取 - public MemoryCaching(IMemoryCache cache) - { - _cache = cache; - } - - public object Get(string cacheKey) - { - return _cache.Get(cacheKey); - } - - public void Set(string cacheKey, object cacheValue,int timeSpan) - { - _cache.Set(cacheKey, cacheValue, TimeSpan.FromSeconds(timeSpan * 60)); - } - } - -} diff --git a/Blog.Core.Common/Option/RedisOptions.cs b/Blog.Core.Common/Option/RedisOptions.cs new file mode 100644 index 00000000..27326595 --- /dev/null +++ b/Blog.Core.Common/Option/RedisOptions.cs @@ -0,0 +1,24 @@ +using Blog.Core.Common.Option.Core; + +namespace Blog.Core.Common.Option; + +/// +/// 缓存配置选项 +/// +public sealed class RedisOptions : IConfigurableOptions +{ + /// + /// 是否启用 + /// + public bool Enable { get; set; } + + /// + /// Redis连接 + /// + public string ConnectionString { get; set; } + + /// + /// 键值前缀 + /// + public string InstanceName { get; set; } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/AOP/BlogCacheAOP.cs b/Blog.Core.Extensions/AOP/BlogCacheAOP.cs index a3a52f58..d872bab5 100644 --- a/Blog.Core.Extensions/AOP/BlogCacheAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogCacheAOP.cs @@ -1,6 +1,8 @@ -using Blog.Core.Common; +using System; +using Blog.Core.Common; using Castle.DynamicProxy; using System.Linq; +using Blog.Core.Common.Caches; namespace Blog.Core.AOP { @@ -28,7 +30,7 @@ public override void Intercept(IInvocation invocation) //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //根据key获取相应的缓存值 - var cacheValue = _cache.Get(cacheKey); + var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 @@ -40,7 +42,7 @@ public override void Intercept(IInvocation invocation) //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { - _cache.Set(cacheKey, invocation.ReturnValue, qCachingAttribute.AbsoluteExpiration); + _cache.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); } } else diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index 1dfbbc48..b0c2408d 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -15,6 +15,7 @@ + diff --git a/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs b/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs index ca1827fd..08effd1b 100644 --- a/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs +++ b/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs @@ -8,6 +8,7 @@ namespace Blog.Core.Extensions /// /// Redis缓存接口 /// + [Obsolete("普通缓存考虑直接使用ICaching,如果要使用Redis队列等还是使用此类")] public interface IRedisBasketRepository { diff --git a/Blog.Core.Extensions/Redis/RedisBasketRepository.cs b/Blog.Core.Extensions/Redis/RedisBasketRepository.cs index 31c7a030..7f1356e2 100644 --- a/Blog.Core.Extensions/Redis/RedisBasketRepository.cs +++ b/Blog.Core.Extensions/Redis/RedisBasketRepository.cs @@ -9,6 +9,7 @@ namespace Blog.Core.Extensions { + [Obsolete("普通缓存考虑直接使用ICaching,如果要使用Redis队列等还是使用此类")] public class RedisBasketRepository : IRedisBasketRepository { private readonly ILogger _logger; diff --git a/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs b/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs new file mode 100644 index 00000000..3744acd9 --- /dev/null +++ b/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs @@ -0,0 +1,45 @@ +using Blog.Core.Common; +using Blog.Core.Common.Caches; +using Blog.Core.Common.Option; +using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; + +namespace Blog.Core.Extensions.ServiceExtensions; + +public static class CacheSetup +{ + /// + /// 统一注册缓存 + /// + /// + public static void AddCacheSetup(this IServiceCollection services) + { + var cacheOptions = App.GetOptions(); + if (cacheOptions.Enable) + { + //使用Redis + services.AddStackExchangeRedisCache(options => + { + options.Configuration = cacheOptions.ConnectionString; + if (!cacheOptions.InstanceName.IsNullOrEmpty()) options.InstanceName = cacheOptions.InstanceName; + }); + + services.AddTransient(); + // 配置启动Redis服务,虽然可能影响项目启动速度,但是不能在运行的时候报错,所以是合理的 + services.AddSingleton(sp => + { + //获取连接字符串 + var configuration = ConfigurationOptions.Parse(cacheOptions.ConnectionString, true); + configuration.ResolveDns = true; + return ConnectionMultiplexer.Connect(configuration); + }); + } + else + { + //使用内存 + services.AddDistributedMemoryCache(); + } + + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs b/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs deleted file mode 100644 index b9b4d7b3..00000000 --- a/Blog.Core.Extensions/ServiceExtensions/HttpRuntimeCache.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using SqlSugar; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; - -namespace Blog.Core.Extensions -{ - /// - /// 实现SqlSugar的ICacheService接口 - /// - public class SqlSugarMemoryCacheService : ICacheService - { - protected IMemoryCache _memoryCache; - public SqlSugarMemoryCacheService(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } - public void Add(string key, V value) - { - _memoryCache.Set(key, value); - } - public void Add(string key, V value, int cacheDurationInSeconds) - { - _memoryCache.Set(key, value, DateTimeOffset.Now.AddSeconds(cacheDurationInSeconds)); - } - public bool ContainsKey(string key) - { - return _memoryCache.TryGetValue(key, out _); - } - - public V Get(string key) - { - return _memoryCache.Get(key); - } - - public IEnumerable GetAllKey() - { - const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; - var entries = _memoryCache.GetType().GetField("_entries", flags).GetValue(_memoryCache); - var cacheItems = entries as IDictionary; - var keys = new List(); - if (cacheItems == null) return keys; - foreach (DictionaryEntry cacheItem in cacheItems) - { - keys.Add(cacheItem.Key.ToString()); - } - return keys; - } - - public V GetOrCreate(string cacheKey, Func create, int cacheDurationInSeconds = int.MaxValue) - { - if (!_memoryCache.TryGetValue(cacheKey, out V value)) - { - value = create(); - _memoryCache.Set(cacheKey, value, DateTime.Now.AddSeconds(cacheDurationInSeconds)); - } - return value; - } - - public void Remove(string key) - { - _memoryCache.Remove(key); - } - } -} diff --git a/Blog.Core.Extensions/ServiceExtensions/MemoryCacheSetup.cs b/Blog.Core.Extensions/ServiceExtensions/MemoryCacheSetup.cs deleted file mode 100644 index 0e489681..00000000 --- a/Blog.Core.Extensions/ServiceExtensions/MemoryCacheSetup.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Blog.Core.Common; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System; - -namespace Blog.Core.Extensions -{ - /// - /// Memory缓存 启动服务 - /// - public static class MemoryCacheSetup - { - public static void AddMemoryCacheSetup(this IServiceCollection services) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - services.AddScoped(); - services.AddSingleton(factory => - { - var value = factory.GetRequiredService>(); - var cache = new MemoryCache(value); - return cache; - }); - } - } -} diff --git a/Blog.Core.Extensions/ServiceExtensions/RedisCacheSetup.cs b/Blog.Core.Extensions/ServiceExtensions/RedisCacheSetup.cs deleted file mode 100644 index e070cbd6..00000000 --- a/Blog.Core.Extensions/ServiceExtensions/RedisCacheSetup.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Blog.Core.Common; -using Microsoft.Extensions.DependencyInjection; -using StackExchange.Redis; -using System; - -namespace Blog.Core.Extensions -{ - /// - /// Redis缓存 启动服务 - /// - public static class RedisCacheSetup - { - public static void AddRedisCacheSetup(this IServiceCollection services) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - services.AddTransient(); - - // 配置启动Redis服务,虽然可能影响项目启动速度,但是不能在运行的时候报错,所以是合理的 - services.AddSingleton(sp => - { - //获取连接字符串 - string redisConfiguration = AppSettings.app(new string[] { "Redis", "ConnectionString" }); - - var configuration = ConfigurationOptions.Parse(redisConfiguration, true); - - configuration.ResolveDns = true; - - return ConnectionMultiplexer.Connect(configuration); - }); - - } - } -} diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 4ac2d2e8..dd5c02b3 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Blog.Core.Common.Caches; namespace Blog.Core.Extensions { @@ -57,6 +58,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) // 自定义特性 ConfigureExternalServices = new ConfigureExternalServices() { + DataInfoCacheService = new SqlSugarCacheService(), EntityService = (property, column) => { if (column.IsPrimarykey && property.PropertyType == typeof(int)) @@ -83,19 +85,11 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { throw new ApplicationException("未配置Log库连接"); } - - + // SqlSugarScope是线程安全,可使用单例注入 // 参考:https://www.donet5.com/Home/Doc?typeId=1181 services.AddSingleton(o => { - var memoryCache = o.GetRequiredService(); - - foreach (var config in BaseDBConfig.AllConfigs) - { - config.ConfigureExternalServices.DataInfoCacheService = new SqlSugarMemoryCacheService(memoryCache); - } - return new SqlSugarScope(BaseDBConfig.AllConfigs, db => { BaseDBConfig.ValidConfig.ForEach(config => diff --git a/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs b/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs index 88181bc3..b00db95d 100644 --- a/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs +++ b/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Blog.Core.Common; +using Blog.Core.Common.Caches; using Blog.Core.Common.Helper; using Nacos.V2; using Newtonsoft.Json.Linq; diff --git a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs index 289f0aae..888d2dc7 100644 --- a/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs +++ b/Blog.Core.Serilog/Utility/SerilogRequestUtility.cs @@ -8,64 +8,66 @@ namespace Blog.Core.Serilog.Utility; public class SerilogRequestUtility { - public const string HttpMessageTemplate = - "HTTP {RequestMethod} {RequestPath} QueryString:{QueryString} Body:{Body} responded {StatusCode} in {Elapsed:0.0000} ms"; + public const string HttpMessageTemplate = + "HTTP {RequestMethod} {RequestPath} QueryString:{QueryString} Body:{Body} responded {StatusCode} in {Elapsed:0.0000} ms"; - private static readonly List _ignoreUrl = new() - { - "/job", - }; + private static readonly List _ignoreUrl = new() + { + "/job", + }; - private static LogEventLevel DefaultGetLevel(HttpContext ctx, - double _, - Exception? ex) - { - return ex is null && ctx.Response.StatusCode <= 499 ? LogEventLevel.Information : LogEventLevel.Error; - } + private static LogEventLevel DefaultGetLevel(HttpContext ctx, + double _, + Exception? ex) + { + return ex is null && ctx.Response.StatusCode <= 499 ? LogEventLevel.Information : LogEventLevel.Error; + } - public static LogEventLevel GetRequestLevel(HttpContext ctx, double _, Exception? ex) => - ex is null && ctx.Response.StatusCode <= 499 ? IgnoreRequest(ctx) : LogEventLevel.Error; + public static LogEventLevel GetRequestLevel(HttpContext ctx, double _, Exception? ex) => + ex is null && ctx.Response.StatusCode <= 499 ? IgnoreRequest(ctx) : LogEventLevel.Error; - private static LogEventLevel IgnoreRequest(HttpContext ctx) - { - var path = ctx.Request.Path.Value; - if (path.IsNullOrEmpty()) - { - return LogEventLevel.Information; - } + private static LogEventLevel IgnoreRequest(HttpContext ctx) + { + var path = ctx.Request.Path.Value; + if (path.IsNullOrEmpty()) + { + return LogEventLevel.Information; + } - return _ignoreUrl.Any(s => path.StartsWith(s)) ? LogEventLevel.Verbose : LogEventLevel.Information; - } + return _ignoreUrl.Any(s => path.StartsWith(s)) ? LogEventLevel.Verbose : LogEventLevel.Information; + } - /// - /// 从Request中增加附属属性 - /// - /// - /// - public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext) - { - var request = httpContext.Request; + /// + /// 从Request中增加附属属性 + /// + /// + /// + public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext) + { + var request = httpContext.Request; - diagnosticContext.Set("RequestHost", request.Host); - diagnosticContext.Set("RequestScheme", request.Scheme); - diagnosticContext.Set("Protocol", request.Protocol); - diagnosticContext.Set("RequestIp", httpContext.GetRequestIp()); + diagnosticContext.Set("RequestHost", request.Host); + diagnosticContext.Set("RequestScheme", request.Scheme); + diagnosticContext.Set("Protocol", request.Protocol); + diagnosticContext.Set("RequestIp", httpContext.GetRequestIp()); - if (request.Method == HttpMethods.Get) - { - diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); - } - else - { - diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); - diagnosticContext.Set("Body", request.ContentLength > 0 ? request.GetRequestBody() : string.Empty); - } - diagnosticContext.Set("ContentType", httpContext.Response.ContentType); + if (request.Method == HttpMethods.Get) + { + diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); + diagnosticContext.Set("Body", string.Empty); + } + else + { + diagnosticContext.Set("QueryString", request.QueryString.HasValue ? request.QueryString.Value : string.Empty); + diagnosticContext.Set("Body", request.ContentLength > 0 ? request.GetRequestBody() : string.Empty); + } - var endpoint = httpContext.GetEndpoint(); - if (endpoint != null) - { - diagnosticContext.Set("EndpointName", endpoint.DisplayName); - } - } + diagnosticContext.Set("ContentType", httpContext.Response.ContentType); + + var endpoint = httpContext.GetEndpoint(); + if (endpoint != null) + { + diagnosticContext.Set("EndpointName", endpoint.DisplayName); + } + } } \ No newline at end of file From 1f35642400667046aaca2b9ef8ae9ac552996712 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Thu, 1 Jun 2023 17:58:46 +0800 Subject: [PATCH 182/289] =?UTF-8?q?=F0=9F=8E=A8=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=A4=9A=E4=BD=99=E7=9A=84=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 4311c97c..74bcd311 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -95,7 +95,6 @@ builder.Services.Configure(x => x.AllowSynchronousIO = true) .Configure(x => x.AllowSynchronousIO = true); -builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(); builder.Services.AddHttpPollySetup(); builder.Services.AddControllers(o => From 4e23f086542b3dbe29e02e8b0036119de28d91db Mon Sep 17 00:00:00 2001 From: Nine Date: Fri, 2 Jun 2023 14:11:09 +0800 Subject: [PATCH 183/289] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dmini-profiler=20?= =?UTF-8?q?=E4=B8=8D=E6=98=BE=E7=A4=BA=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=B8=8D=E4=B8=80=E8=87=B4=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=20=E4=BF=AE=E6=94=B9=E4=B8=80=E4=BA=9Bswagger=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E6=9D=83=E9=99=90=E5=B0=8F=E9=94=81=EF=BC=8C?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=AE=A4=E8=AF=81=E6=8C=89=E9=92=AE..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/index.html | 103 ++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/Blog.Core.Api/index.html b/Blog.Core.Api/index.html index b0092a62..609a60cf 100644 --- a/Blog.Core.Api/index.html +++ b/Blog.Core.Api/index.html @@ -1,8 +1,6 @@  - + @@ -10,14 +8,25 @@ - + %(DocumentTitle) + %(HeadContent) @@ -95,10 +123,16 @@ From e9b4531ff8bfa0393dd4247376177c6cdea9d930 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 2 Jun 2023 16:34:07 +0800 Subject: [PATCH 184/289] Update index.html --- Blog.Core.Api/index.html | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Blog.Core.Api/index.html b/Blog.Core.Api/index.html index 609a60cf..388d79ad 100644 --- a/Blog.Core.Api/index.html +++ b/Blog.Core.Api/index.html @@ -178,25 +178,27 @@ //修改权限小锁样式,由于这个单页面没去获取token,采取直接修改样式的方法 $(document).ready(function () { - document.querySelector('.auth-wrapper svg use').href.baseVal = '#locked' - let divList = document.querySelectorAll('.opblock-tag') - for (let div of divList) { - //debugger - div.addEventListener('click', function () { - setTimeout(() => { - //修改锁状态 locked 为已认证token unlocked为未认证 - let list = document.querySelectorAll(' .unlocked svg use') - for (let item of list) { - item.href.baseVal = '#locked' - } - //修改锁颜色 locked 黑色 unlocked灰色 - let btnlist = document.querySelectorAll('.authorization__btn') - for (let item of btnlist) { - item.classList = 'authorization__btn locked' - } - }, 0) - }) - } + setTimeout(() => { + document.querySelector('.auth-wrapper svg use').href.baseVal = '#locked' + let divList = document.querySelectorAll('.opblock-tag') + for (let div of divList) { + //debugger + div.addEventListener('click', function () { + setTimeout(() => { + //修改锁状态 locked 为已认证token unlocked为未认证 + let list = document.querySelectorAll(' .unlocked svg use') + for (let item of list) { + item.href.baseVal = '#locked' + } + //修改锁颜色 locked 黑色 unlocked灰色 + let btnlist = document.querySelectorAll('.authorization__btn') + for (let item of btnlist) { + item.classList = 'authorization__btn locked' + } + }, 0) + }) + } + }, 500) }); }, 1000) // 文档logo From 2c74a716ca342f20359e181a27ce0f2f4bbfcd11 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 5 Jun 2023 11:55:23 +0800 Subject: [PATCH 185/289] Update swg-login.html --- Blog.Core.Api/wwwroot/swg-login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/wwwroot/swg-login.html b/Blog.Core.Api/wwwroot/swg-login.html index 93653957..d361cdef 100644 --- a/Blog.Core.Api/wwwroot/swg-login.html +++ b/Blog.Core.Api/wwwroot/swg-login.html @@ -15,7 +15,7 @@
欢迎使用!
-
使用用户账号登录
+
使用真实用户账号登录,测试账号: blogadmin/blogadmin
From 96e21474bcb66c7ffc1379f39c0da06081841788 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 7 Jun 2023 15:25:13 +0800 Subject: [PATCH 186/289] =?UTF-8?q?=F0=9F=8E=A8=E4=BC=98=E5=8C=96=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=B3=A8=E5=85=A5=E3=80=81=E7=A7=BB=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Redis/IRedisCacheManager.cs | 29 --- .../Redis/RedisCacheManager.cs | 165 ------------------ .../ServiceExtensions/CacheSetup.cs | 73 ++++---- 3 files changed, 38 insertions(+), 229 deletions(-) delete mode 100644 Blog.Core.Extensions/Redis/IRedisCacheManager.cs delete mode 100644 Blog.Core.Extensions/Redis/RedisCacheManager.cs diff --git a/Blog.Core.Extensions/Redis/IRedisCacheManager.cs b/Blog.Core.Extensions/Redis/IRedisCacheManager.cs deleted file mode 100644 index 56cd4f84..00000000 --- a/Blog.Core.Extensions/Redis/IRedisCacheManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Blog.Core.Extensions -{ - /// - /// Redis缓存接口 - /// - public interface IRedisCacheManager - { - - //获取 Reids 缓存值 - string GetValue(string key); - - //获取值,并序列化 - TEntity Get(string key); - - //保存 - void Set(string key, object value, TimeSpan cacheTime); - - //判断是否存在 - bool Get(string key); - - //移除某一个缓存值 - void Remove(string key); - - //全部清除 - void Clear(); - } -} diff --git a/Blog.Core.Extensions/Redis/RedisCacheManager.cs b/Blog.Core.Extensions/Redis/RedisCacheManager.cs deleted file mode 100644 index 0e5d7819..00000000 --- a/Blog.Core.Extensions/Redis/RedisCacheManager.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Blog.Core.Common; -using StackExchange.Redis; -using System; - -namespace Blog.Core.Extensions -{ - public class RedisCacheManager : IRedisCacheManager - { - - private readonly string redisConnenctionString; - - public volatile ConnectionMultiplexer redisConnection; - - private readonly object redisConnectionLock = new object(); - - public RedisCacheManager() - { - string redisConfiguration = AppSettings.app(new string[] { "AppSettings", "RedisCachingAOP", "ConnectionString" });//获取连接字符串 - - if (string.IsNullOrWhiteSpace(redisConfiguration)) - { - throw new ArgumentException("redis config is empty", nameof(redisConfiguration)); - } - this.redisConnenctionString = redisConfiguration; - this.redisConnection = GetRedisConnection(); - } - - /// - /// 核心代码,获取连接实例 - /// 通过双if 夹lock的方式,实现单例模式 - /// - /// - private ConnectionMultiplexer GetRedisConnection() - { - //如果已经连接实例,直接返回 - if (this.redisConnection != null && this.redisConnection.IsConnected) - { - return this.redisConnection; - } - //加锁,防止异步编程中,出现单例无效的问题 - lock (redisConnectionLock) - { - if (this.redisConnection != null) - { - //释放redis连接 - this.redisConnection.Dispose(); - } - try - { - var config = new ConfigurationOptions - { - AbortOnConnectFail = false, - AllowAdmin = true, - ConnectTimeout = 15000,//改成15s - SyncTimeout = 5000, - //Password = "Pwd",//Redis数据库密码 - EndPoints = { redisConnenctionString }// connectionString 为IP:Port 如”192.168.2.110:6379” - }; - this.redisConnection = ConnectionMultiplexer.Connect(config); - } - catch (Exception) - { - throw new Exception("Redis服务未启用,请开启该服务,并且请注意端口号,本项目使用的的6319,而且我的是没有设置密码。"); - } - } - return this.redisConnection; - } - /// - /// 清除 - /// - public void Clear() - { - foreach (var endPoint in this.GetRedisConnection().GetEndPoints()) - { - var server = this.GetRedisConnection().GetServer(endPoint); - foreach (var key in server.Keys()) - { - redisConnection.GetDatabase().KeyDelete(key); - } - } - } - /// - /// 判断是否存在 - /// - /// - /// - public bool Get(string key) - { - return redisConnection.GetDatabase().KeyExists(key); - } - - /// - /// 查询 - /// - /// - /// - public string GetValue(string key) - { - return redisConnection.GetDatabase().StringGet(key); - } - - /// - /// 获取 - /// - /// - /// - /// - public TEntity Get(string key) - { - var value = redisConnection.GetDatabase().StringGet(key); - if (value.HasValue) - { - //需要用的反序列化,将Redis存储的Byte[],进行反序列化 - return SerializeHelper.Deserialize(value); - } - else - { - return default(TEntity); - } - } - - /// - /// 移除 - /// - /// - public void Remove(string key) - { - redisConnection.GetDatabase().KeyDelete(key); - } - /// - /// 设置 - /// - /// - /// - /// - public void Set(string key, object value, TimeSpan cacheTime) - { - if (value != null) - { - if (value is string cacheValue) - { - // 字符串无需序列化 - redisConnection.GetDatabase().StringSet(key, cacheValue, cacheTime); - } - else - { - //序列化,将object值生成RedisValue - redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); - } - } - } - - /// - /// 增加/修改 - /// - /// - /// - /// - public bool SetValue(string key, byte[] value) - { - return redisConnection.GetDatabase().StringSet(key, value, TimeSpan.FromSeconds(120)); - } - - } -} diff --git a/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs b/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs index 3744acd9..579da785 100644 --- a/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs @@ -1,4 +1,5 @@ -using Blog.Core.Common; +using System.Threading.Tasks; +using Blog.Core.Common; using Blog.Core.Common.Caches; using Blog.Core.Common.Option; using Microsoft.Extensions.DependencyInjection; @@ -8,38 +9,40 @@ namespace Blog.Core.Extensions.ServiceExtensions; public static class CacheSetup { - /// - /// 统一注册缓存 - /// - /// - public static void AddCacheSetup(this IServiceCollection services) - { - var cacheOptions = App.GetOptions(); - if (cacheOptions.Enable) - { - //使用Redis - services.AddStackExchangeRedisCache(options => - { - options.Configuration = cacheOptions.ConnectionString; - if (!cacheOptions.InstanceName.IsNullOrEmpty()) options.InstanceName = cacheOptions.InstanceName; - }); - - services.AddTransient(); - // 配置启动Redis服务,虽然可能影响项目启动速度,但是不能在运行的时候报错,所以是合理的 - services.AddSingleton(sp => - { - //获取连接字符串 - var configuration = ConfigurationOptions.Parse(cacheOptions.ConnectionString, true); - configuration.ResolveDns = true; - return ConnectionMultiplexer.Connect(configuration); - }); - } - else - { - //使用内存 - services.AddDistributedMemoryCache(); - } - - services.AddSingleton(); - } + /// + /// 统一注册缓存 + /// + /// + public static void AddCacheSetup(this IServiceCollection services) + { + var cacheOptions = App.GetOptions(); + if (cacheOptions.Enable) + { + // 配置启动Redis服务,虽然可能影响项目启动速度,但是不能在运行的时候报错,所以是合理的 + services.AddSingleton(sp => + { + //获取连接字符串 + var configuration = ConfigurationOptions.Parse(cacheOptions.ConnectionString, true); + configuration.ResolveDns = true; + return ConnectionMultiplexer.Connect(configuration); + }); + services.AddSingleton(p => p.GetService() as ConnectionMultiplexer); + //使用Redis + services.AddStackExchangeRedisCache(options => + { + options.ConnectionMultiplexerFactory = () => Task.FromResult(App.GetService()); + if (!cacheOptions.InstanceName.IsNullOrEmpty()) options.InstanceName = cacheOptions.InstanceName; + }); + + services.AddTransient(); + } + else + { + //使用内存 + services.AddMemoryCache(); + services.AddDistributedMemoryCache(); + } + + services.AddSingleton(); + } } \ No newline at end of file From da6a7546aa56b49c90b19ef23f10b136d82f3213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E6=8B=BE=E7=8E=96?= Date: Fri, 9 Jun 2023 17:55:20 +0800 Subject: [PATCH 187/289] =?UTF-8?q?Update=20index.html=20=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E6=8E=A5=E5=8F=A3=E5=90=8D=E7=A7=B0=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E5=A4=8D=E5=88=B6=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=8E=BB=E9=99=A4=E6=8E=A5=E5=8F=A3border=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E9=BB=91=E8=89=B2=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Api/index.html b/Blog.Core.Api/index.html index 388d79ad..eca0b424 100644 --- a/Blog.Core.Api/index.html +++ b/Blog.Core.Api/index.html @@ -17,6 +17,9 @@ %(HeadContent) - + From 3a25de3fc30f91fde762353f072cc5d1b995bc45 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 10 Jun 2023 11:23:54 +0800 Subject: [PATCH 188/289] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96Redis?= =?UTF-8?q?=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs b/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs index 579da785..eb810f52 100644 --- a/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/CacheSetup.cs @@ -30,7 +30,7 @@ public static void AddCacheSetup(this IServiceCollection services) //使用Redis services.AddStackExchangeRedisCache(options => { - options.ConnectionMultiplexerFactory = () => Task.FromResult(App.GetService()); + options.ConnectionMultiplexerFactory = () => Task.FromResult(App.GetService(false)); if (!cacheOptions.InstanceName.IsNullOrEmpty()) options.InstanceName = cacheOptions.InstanceName; }); From 7ea8d3565d8888ca8a66d716ab890a753cdd013f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 10 Jun 2023 11:27:27 +0800 Subject: [PATCH 189/289] Update appsettings.json --- Blog.Core.Api/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index d220a034..73637e19 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -18,7 +18,7 @@ }, "AllowedHosts": "*", "Redis": { - "Enable": true, + "Enable": false, "ConnectionString": "127.0.0.1:6379", "InstanceName": "" //前缀 }, From 70134714669c1bcf0fedf179f034d09b1859aaa6 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 10 Jun 2023 11:53:12 +0800 Subject: [PATCH 190/289] =?UTF-8?q?=F0=9F=90=9B=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E5=B7=B2=E7=9F=A5=E7=9A=84DynamicLambda?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在双引号后又空格会解析错误 --- Blog.Core.Common/Helper/DynamicLinqFactory.cs | 1 + Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Blog.Core.Common/Helper/DynamicLinqFactory.cs b/Blog.Core.Common/Helper/DynamicLinqFactory.cs index 7d45bbd2..248b18ff 100644 --- a/Blog.Core.Common/Helper/DynamicLinqFactory.cs +++ b/Blog.Core.Common/Helper/DynamicLinqFactory.cs @@ -348,6 +348,7 @@ public static string FormatString(string str) } else { + if (i == firstIndex) continue; if (i == str.Length - 1 || str[i + 1].IsNullOrEmpty()) { lastIndex = i; diff --git a/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs b/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs index 8f6ad096..d67a67e0 100644 --- a/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs +++ b/Blog.Core.Tests/Common_Test/DynamicLambdaTest.cs @@ -66,6 +66,7 @@ public async void Get_Blogs_DynamicTest() await TestConditions("bId=2"); await TestConditions("bId in (1,2,3,4,5)"); await TestConditions("bId in (1,2,3,4,5)|| bUpdateTime>=\"2019-01-01 01:01:01\""); + await TestConditions("btitle like \" 测试数据\""); await TestConditions("btitle like \"测试数据\" && bId>0"); await TestConditions("btitle like \"测试!@#$%^&*()_+|}{\":<>?LP\"数据\" && bId>0"); await TestConditions("btitle like \"测试!@+)(*()_&%^&^$^%$IUYWIQOJVLXKZM>?Z<>??LP\"数据\" && bId>0"); From 04d3cd4865e2c018202cc07c99ba6ab3d2ec7c9b Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 10 Jun 2023 11:59:11 +0800 Subject: [PATCH 191/289] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20=E5=8D=87=E7=BA=A7?= =?UTF-8?q?SqlSugar=E4=BE=9D=E8=B5=96=205.1.3.49=20->=205.1.4.83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化Aop Sql日志记录 原有的UtilMethods.GetSqlString 有较大的性能影响,参数越多影响越大 使用最新的建议的 UtilMethods.GetNativeSql 参考 https://www.donet5.com/home/doc?masterId=1&typeId=1204 --- Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 2 +- Blog.Core.Model/Blog.Core.Model.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index 3a595e67..f165ef43 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -26,7 +26,7 @@ public static void OnLogExecuting(ISqlSugarClient sqlSugarScopeProvider, string using (LogContextExtension.Create.SqlAopPushProperty(sqlSugarScopeProvider)) { Log.Information("------------------ \r\n ConnId:[{ConnId}]【SQL语句】: \r\n {Sql}", - config.ConfigId, UtilMethods.GetSqlString(config.DbType, sql, p)); + config.ConfigId, UtilMethods.GetNativeSql( sql, p)); } } } diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index e66c5c5b..ac851de2 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -16,7 +16,7 @@ - + From 0ddaa35bed9ba0536688f356c1766972380c8cf4 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Mon, 12 Jun 2023 10:52:11 +0800 Subject: [PATCH 192/289] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E9=9B=86=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/RuntimeExtension.cs | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/Blog.Core.Common/Extensions/RuntimeExtension.cs b/Blog.Core.Common/Extensions/RuntimeExtension.cs index c4ddb0c8..d66f1183 100644 --- a/Blog.Core.Common/Extensions/RuntimeExtension.cs +++ b/Blog.Core.Common/Extensions/RuntimeExtension.cs @@ -10,75 +10,77 @@ namespace Blog.Core.Common.Extensions; public static class RuntimeExtension { - /// - /// 获取项目程序集,排除所有的系统程序集(Microsoft.***、System.***等)、Nuget下载包 - /// - /// - public static IList GetAllAssemblies() - { - var list = new List(); - var deps = DependencyContext.Default; - var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package"); //排除所有的系统程序集、Nuget下载包 - foreach (var lib in libs) - { - try - { - var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); - list.Add(assembly); - } - catch (Exception e) - { - Log.Debug(e, "GetAllAssemblies Exception:{ex}", e.Message); - } - } - return list; - } + /// + /// 获取项目程序集,排除所有的系统程序集(Microsoft.***、System.***等)、Nuget下载包 + /// + /// + public static IList GetAllAssemblies() + { + var list = new List(); + var deps = DependencyContext.Default; + //只加载项目中的程序集 + var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type == "project"); //排除所有的系统程序集、Nuget下载包 + foreach (var lib in libs) + { + try + { + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); + list.Add(assembly); + } + catch (Exception e) + { + Log.Debug(e, "GetAllAssemblies Exception:{ex}", e.Message); + } + } - public static Assembly GetAssembly(string assemblyName) - { - return GetAllAssemblies().FirstOrDefault(assembly => assembly.FullName.Contains(assemblyName)); - } + return list; + } - public static IList GetAllTypes() - { - var list = new List(); - foreach (var assembly in GetAllAssemblies()) - { - var typeInfos = assembly.DefinedTypes; - foreach (var typeInfo in typeInfos) - { - list.Add(typeInfo.AsType()); - } - } + public static Assembly GetAssembly(string assemblyName) + { + return GetAllAssemblies().FirstOrDefault(assembly => assembly.FullName.Contains(assemblyName)); + } - return list; - } + public static IList GetAllTypes() + { + var list = new List(); + foreach (var assembly in GetAllAssemblies()) + { + var typeInfos = assembly.DefinedTypes; + foreach (var typeInfo in typeInfos) + { + list.Add(typeInfo.AsType()); + } + } - public static IList GetTypesByAssembly(string assemblyName) - { - var list = new List(); - var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName)); - var typeInfos = assembly.DefinedTypes; - foreach (var typeInfo in typeInfos) - { - list.Add(typeInfo.AsType()); - } + return list; + } - return list; - } + public static IList GetTypesByAssembly(string assemblyName) + { + var list = new List(); + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName)); + var typeInfos = assembly.DefinedTypes; + foreach (var typeInfo in typeInfos) + { + list.Add(typeInfo.AsType()); + } - public static Type GetImplementType(string typeName, Type baseInterfaceType) - { - return GetAllTypes().FirstOrDefault(t => - { - if (t.Name == typeName && - t.GetTypeInfo().GetInterfaces().Any(b => b.Name == baseInterfaceType.Name)) - { - var typeInfo = t.GetTypeInfo(); - return typeInfo.IsClass && !typeInfo.IsAbstract && !typeInfo.IsGenericType; - } + return list; + } - return false; - }); - } + public static Type GetImplementType(string typeName, Type baseInterfaceType) + { + return GetAllTypes().FirstOrDefault(t => + { + if (t.Name == typeName && + t.GetTypeInfo().GetInterfaces().Any(b => b.Name == baseInterfaceType.Name)) + { + var typeInfo = t.GetTypeInfo(); + return typeInfo.IsClass && !typeInfo.IsAbstract && !typeInfo.IsGenericType; + } + + return false; + }); + } } \ No newline at end of file From 60cade83d4da87642e70720ad42be85de4cc85e6 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Mon, 12 Jun 2023 11:16:45 +0800 Subject: [PATCH 193/289] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BC=98=E5=8C=96AOP?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/Caches/Caching.cs | 32 ++++-- Blog.Core.Common/Caches/ICaching.cs | 5 +- Blog.Core.Extensions/AOP/BlogCacheAOP.cs | 122 ++++++++++++++--------- 3 files changed, 104 insertions(+), 55 deletions(-) diff --git a/Blog.Core.Common/Caches/Caching.cs b/Blog.Core.Common/Caches/Caching.cs index e7b4ab07..36e4ce3a 100644 --- a/Blog.Core.Common/Caches/Caching.cs +++ b/Blog.Core.Common/Caches/Caching.cs @@ -142,13 +142,13 @@ public async Task> GetAllCacheKeysAsync() var res = await _cache.GetStringAsync(CacheConst.KeyAll); return string.IsNullOrWhiteSpace(res) ? null : JsonConvert.DeserializeObject>(res); } - + public T Get(string cacheKey) { var res = _cache.Get(cacheKey); return res == null ? default : JsonConvert.DeserializeObject(Encoding.UTF8.GetString(res)); } - + /// /// 获取缓存 /// @@ -161,6 +161,18 @@ public async Task GetAsync(string cacheKey) return res == null ? default : JsonConvert.DeserializeObject(Encoding.UTF8.GetString(res)); } + public object Get(Type type, string cacheKey) + { + var res = _cache.Get(cacheKey); + return res == null ? default : JsonConvert.DeserializeObject(Encoding.UTF8.GetString(res), type); + } + + public async Task GetAsync(Type type, string cacheKey) + { + var res = await _cache.GetAsync(cacheKey); + return res == null ? default : JsonConvert.DeserializeObject(Encoding.UTF8.GetString(res), type); + } + public string GetString(string cacheKey) { return _cache.GetString(cacheKey); @@ -175,7 +187,7 @@ public async Task GetStringAsync(string cacheKey) { return await _cache.GetStringAsync(cacheKey); } - + public void Remove(string key) { _cache.Remove(key); @@ -214,7 +226,10 @@ public async Task RemoveAllAsync() public void Set(string cacheKey, T value, TimeSpan? expire = null) { - _cache.Set(cacheKey, GetBytes(value), expire == null ? new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)} : new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); + _cache.Set(cacheKey, GetBytes(value), + expire == null + ? new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)} + : new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); AddCacheKey(cacheKey); } @@ -227,7 +242,8 @@ public void Set(string cacheKey, T value, TimeSpan? expire = null) /// public async Task SetAsync(string cacheKey, T value) { - await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value)), new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)}); + await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value)), + new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)}); await AddCacheKeyAsync(cacheKey); } @@ -241,7 +257,8 @@ public async Task SetAsync(string cacheKey, T value) /// public async Task SetAsync(string cacheKey, T value, TimeSpan expire) { - await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value)), new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); + await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value)), + new DistributedCacheEntryOptions() {AbsoluteExpirationRelativeToNow = expire}); await AddCacheKeyAsync(cacheKey); } @@ -251,6 +268,7 @@ public void SetPermanent(string cacheKey, T value) _cache.Set(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value))); AddCacheKey(cacheKey); } + public async Task SetPermanentAsync(string cacheKey, T value) { await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value))); @@ -293,7 +311,7 @@ public async Task SetStringAsync(string cacheKey, string value, TimeSpan expire) await AddCacheKeyAsync(cacheKey); } - + /// /// 缓存最大角色数据范围 diff --git a/Blog.Core.Common/Caches/ICaching.cs b/Blog.Core.Common/Caches/ICaching.cs index f856aeb6..9ce92632 100644 --- a/Blog.Core.Common/Caches/ICaching.cs +++ b/Blog.Core.Common/Caches/ICaching.cs @@ -25,10 +25,13 @@ public interface ICaching List GetAllCacheKeys(); Task> GetAllCacheKeysAsync(); - + T Get(string cacheKey); Task GetAsync(string cacheKey); + object Get(Type type, string cacheKey); + Task GetAsync(Type type, string cacheKey); + string GetString(string cacheKey); Task GetStringAsync(string cacheKey); diff --git a/Blog.Core.Extensions/AOP/BlogCacheAOP.cs b/Blog.Core.Extensions/AOP/BlogCacheAOP.cs index d872bab5..d3d91f2a 100644 --- a/Blog.Core.Extensions/AOP/BlogCacheAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogCacheAOP.cs @@ -2,54 +2,82 @@ using Blog.Core.Common; using Castle.DynamicProxy; using System.Linq; +using System.Threading.Tasks; using Blog.Core.Common.Caches; namespace Blog.Core.AOP { - /// - /// 面向切面的缓存使用 - /// - public class BlogCacheAOP : CacheAOPbase - { - //通过注入的方式,把缓存操作接口通过构造函数注入 - private readonly ICaching _cache; - public BlogCacheAOP(ICaching cache) - { - _cache = cache; - } - - //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 - public override void Intercept(IInvocation invocation) - { - var method = invocation.MethodInvocationTarget ?? invocation.Method; - //对当前方法的特性验证 - //如果需要验证 - var CachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)); - if (CachingAttribute is CachingAttribute qCachingAttribute) - { - //获取自定义缓存键 - var cacheKey = CustomCacheKey(invocation); - //根据key获取相应的缓存值 - var cacheValue = _cache.Get(cacheKey); - if (cacheValue != null) - { - //将当前获取到的缓存值,赋值给当前执行方法 - invocation.ReturnValue = cacheValue; - return; - } - //去执行当前的方法 - invocation.Proceed(); - //存入缓存 - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - _cache.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); - } - } - else - { - invocation.Proceed();//直接执行被拦截方法 - } - } - } - -} + /// + /// 面向切面的缓存使用 + /// + public class BlogCacheAOP : CacheAOPbase + { + //通过注入的方式,把缓存操作接口通过构造函数注入 + private readonly ICaching _cache; + + public BlogCacheAOP(ICaching cache) + { + _cache = cache; + } + + //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 + public override void Intercept(IInvocation invocation) + { + var method = invocation.MethodInvocationTarget ?? invocation.Method; + //对当前方法的特性验证 + //如果需要验证 + var CachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)); + if (CachingAttribute is CachingAttribute qCachingAttribute) + { + //获取自定义缓存键 + var cacheKey = CustomCacheKey(invocation); + if (_cache.Exists(cacheKey)) + { + //将当前获取到的缓存值,赋值给当前执行方法 + Type returnType; + if (typeof(Task).IsAssignableFrom(method.ReturnType)) + { + returnType = method.ReturnType.GenericTypeArguments.FirstOrDefault(); + } + else + { + returnType = method.ReturnType; + } + + //根据key获取相应的缓存值 + dynamic cacheValue = _cache.Get(returnType, cacheKey); + invocation.ReturnValue = (typeof(Task).IsAssignableFrom(method.ReturnType)) ? Task.FromResult(cacheValue) : cacheValue; + return; + } + + //去执行当前的方法 + invocation.Proceed(); + //存入缓存 + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + object response; + + //Type type = invocation.ReturnValue?.GetType(); + var type = invocation.Method.ReturnType; + if (typeof(Task).IsAssignableFrom(type)) + { + var resultProperty = type.GetProperty("Result"); + response = resultProperty?.GetValue(invocation.ReturnValue); + } + else + { + response = invocation.ReturnValue; + } + + if (response == null) response = string.Empty; + + _cache.Set(cacheKey, response, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); + } + } + else + { + invocation.Proceed(); //直接执行被拦截方法 + } + } + } +} \ No newline at end of file From c1ce9e59728b8f0592fa7bd100466fd2c393ad1b Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Mon, 12 Jun 2023 11:34:39 +0800 Subject: [PATCH 194/289] =?UTF-8?q?=F0=9F=8E=A8=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E7=9A=84RedisCacheAop=20=E4=B8=8ECacheAop?= =?UTF-8?q?=E6=9C=89=E4=BA=9B=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Controllers/ValuesController.cs | 2 +- Blog.Core.Api/appsettings.json | 5 +- Blog.Core.Extensions/AOP/BlogCacheAOP.cs | 4 +- Blog.Core.Extensions/AOP/BlogRedisCacheAOP.cs | 89 ------------------- .../Redis/IRedisBasketRepository.cs | 3 +- .../ServiceExtensions/AppConfigSetup.cs | 22 ++--- .../AutofacModuleRegister.cs | 8 +- Blog.Core.Services/AdvertisementServices.cs | 2 +- Blog.Core.Tests/appsettings.json | 5 +- 9 files changed, 14 insertions(+), 126 deletions(-) delete mode 100644 Blog.Core.Extensions/AOP/BlogRedisCacheAOP.cs diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 03b37556..ae5cc81f 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -164,7 +164,7 @@ await _blogArticleServices.QuerySql( // 测试模拟异常,全局异常过滤器拦截 var i = 0; - var d = 3 / i; + // var d = 3 / i; // 测试 AOP 缓存 diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 73637e19..def53cfc 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -41,10 +41,7 @@ "SubscriptionClientName": "Blog.Core" }, "AppSettings": { - "RedisCachingAOP": { - "Enabled": false - }, - "MemoryCachingAOP": { + "CachingAOP": { "Enabled": true }, "LogToDb": true, diff --git a/Blog.Core.Extensions/AOP/BlogCacheAOP.cs b/Blog.Core.Extensions/AOP/BlogCacheAOP.cs index d3d91f2a..1d52a2de 100644 --- a/Blog.Core.Extensions/AOP/BlogCacheAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogCacheAOP.cs @@ -61,8 +61,8 @@ public override void Intercept(IInvocation invocation) var type = invocation.Method.ReturnType; if (typeof(Task).IsAssignableFrom(type)) { - var resultProperty = type.GetProperty("Result"); - response = resultProperty?.GetValue(invocation.ReturnValue); + dynamic result = invocation.ReturnValue; + response = result.Result; } else { diff --git a/Blog.Core.Extensions/AOP/BlogRedisCacheAOP.cs b/Blog.Core.Extensions/AOP/BlogRedisCacheAOP.cs deleted file mode 100644 index 9bc7f7f7..00000000 --- a/Blog.Core.Extensions/AOP/BlogRedisCacheAOP.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Blog.Core.Common; -using Blog.Core.Extensions; -using Castle.DynamicProxy; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace Blog.Core.AOP -{ - /// - /// 面向切面的缓存使用 - /// - public class BlogRedisCacheAOP : CacheAOPbase - { - //通过注入的方式,把缓存操作接口通过构造函数注入 - private readonly IRedisBasketRepository _cache; - public BlogRedisCacheAOP(IRedisBasketRepository cache) - { - _cache = cache; - } - - //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 - //代码已经合并 ,学习pr流程 - public override void Intercept(IInvocation invocation) - { - var method = invocation.MethodInvocationTarget ?? invocation.Method; - if (method.ReturnType == typeof(void) || method.ReturnType == typeof(Task)) - { - invocation.Proceed(); - return; - } - //对当前方法的特性验证 - var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; - - if (qCachingAttribute != null) - { - //获取自定义缓存键 - var cacheKey = CustomCacheKey(invocation); - //注意是 string 类型,方法GetValue - var cacheValue = _cache.GetValue(cacheKey).Result; - if (cacheValue != null) - { - //将当前获取到的缓存值,赋值给当前执行方法 - Type returnType; - if (typeof(Task).IsAssignableFrom(method.ReturnType)) - { - returnType = method.ReturnType.GenericTypeArguments.FirstOrDefault(); - } - else - { - returnType = method.ReturnType; - } - - dynamic _result = Newtonsoft.Json.JsonConvert.DeserializeObject(cacheValue, returnType); - invocation.ReturnValue = (typeof(Task).IsAssignableFrom(method.ReturnType)) ? Task.FromResult(_result) : _result; - return; - } - //去执行当前的方法 - invocation.Proceed(); - - //存入缓存 - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - object response; - - //Type type = invocation.ReturnValue?.GetType(); - var type = invocation.Method.ReturnType; - if (typeof(Task).IsAssignableFrom(type)) - { - var resultProperty = type.GetProperty("Result"); - response = resultProperty.GetValue(invocation.ReturnValue); - } - else - { - response = invocation.ReturnValue; - } - if (response == null) response = string.Empty; - - _cache.Set(cacheKey, response, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)).Wait(); - } - } - else - { - invocation.Proceed();//直接执行被拦截方法 - } - } - } - -} diff --git a/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs b/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs index 08effd1b..85408160 100644 --- a/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs +++ b/Blog.Core.Extensions/Redis/IRedisBasketRepository.cs @@ -1,6 +1,7 @@ using StackExchange.Redis; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading.Tasks; namespace Blog.Core.Extensions @@ -8,7 +9,7 @@ namespace Blog.Core.Extensions /// /// Redis缓存接口 /// - [Obsolete("普通缓存考虑直接使用ICaching,如果要使用Redis队列等还是使用此类")] + [Description("普通缓存考虑直接使用ICaching,如果要使用Redis队列等还是使用此类")] public interface IRedisBasketRepository { diff --git a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs index 0336567f..d7a1f90d 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs @@ -40,25 +40,14 @@ public static void AddAppConfigSetup(this IServiceCollection services, IHostEnvi { Console.WriteLine($"Current authorization scheme: " + (Permissions.IsUseIds4 ? "Ids4" : "JWT")); } - - // Redis缓存AOP - if (!AppSettings.app(new string[] { "AppSettings", "RedisCachingAOP", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"Redis Caching AOP: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Redis Caching AOP: True"); - } - - // 内存缓存AOP - if (!AppSettings.app(new string[] { "AppSettings", "MemoryCachingAOP", "Enabled" }).ObjToBool()) + // 缓存AOP + if (!AppSettings.app(new string[] { "AppSettings", "CachingAOP", "Enabled" }).ObjToBool()) { - Console.WriteLine($"Memory Caching AOP: False"); + Console.WriteLine($"Caching AOP: False"); } else { - ConsoleHelper.WriteSuccessLine($"Memory Caching AOP: True"); + ConsoleHelper.WriteSuccessLine($"Caching AOP: True"); } // 服务日志AOP @@ -259,8 +248,7 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos List aopInfos = new() { - new string[] { "Redis缓存AOP", AppSettings.app("AppSettings", "RedisCachingAOP", "Enabled") }, - new string[] { "内存缓存AOP", AppSettings.app("AppSettings", "MemoryCachingAOP", "Enabled") }, + new string[] { "缓存AOP", AppSettings.app("AppSettings", "CachingAOP", "Enabled") }, new string[] { "服务日志AOP", AppSettings.app("AppSettings", "LogAOP", "Enabled") }, new string[] { "事务AOP", AppSettings.app("AppSettings", "TranAOP", "Enabled") }, new string[] { "Sql执行AOP", AppSettings.app("AppSettings", "SqlAOP", "Enabled") }, diff --git a/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs b/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs index 4351962b..4836c402 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs @@ -39,13 +39,7 @@ protected override void Load(ContainerBuilder builder) // AOP 开关,如果想要打开指定的功能,只需要在 appsettigns.json 对应对应 true 就行。 var cacheType = new List(); - if (AppSettings.app(new string[] { "AppSettings", "RedisCachingAOP", "Enabled" }).ObjToBool()) - { - builder.RegisterType(); - cacheType.Add(typeof(BlogRedisCacheAOP)); - } - - if (AppSettings.app(new string[] { "AppSettings", "MemoryCachingAOP", "Enabled" }).ObjToBool()) + if (AppSettings.app(new string[] { "AppSettings", "CachingAOP", "Enabled" }).ObjToBool()) { builder.RegisterType(); cacheType.Add(typeof(BlogCacheAOP)); diff --git a/Blog.Core.Services/AdvertisementServices.cs b/Blog.Core.Services/AdvertisementServices.cs index c95f4142..348048e8 100644 --- a/Blog.Core.Services/AdvertisementServices.cs +++ b/Blog.Core.Services/AdvertisementServices.cs @@ -13,7 +13,7 @@ public void ReturnExp() int a = 1; int b = 0; - int c = a / b; + // int c = a / b; } //public IAdvertisementRepository dal = new AdvertisementRepository(); diff --git a/Blog.Core.Tests/appsettings.json b/Blog.Core.Tests/appsettings.json index 23ab9c65..8c0305c7 100644 --- a/Blog.Core.Tests/appsettings.json +++ b/Blog.Core.Tests/appsettings.json @@ -45,10 +45,7 @@ "SubscriptionClientName": "Blog.Core" }, "AppSettings": { - "RedisCachingAOP": { - "Enabled": false - }, - "MemoryCachingAOP": { + "CachingAOP": { "Enabled": true }, "LogAOP": { From a338c56e7ae43d657c23bc3374aba4332ea86c56 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Mon, 12 Jun 2023 17:45:15 +0800 Subject: [PATCH 195/289] =?UTF-8?q?=E2=9C=A8=F0=9F=8E=A8=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=90=9E=E5=BC=82=E5=B8=B8=E9=97=AE=E9=A2=98=E3=80=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96HttpResponse=20Body=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.处理掉中间件过度try catch 吞掉异常,建议直接往上抛不要过度try 2.优雅处理HttpResponse读取问题,原生[HttpResponseStream]实际上只是个包装类,内部包装了[HttpResponsePipeWriter]来进行写入响应数据,由此封装一个[FluentHttpResponseStream],内部扩展使用[MemoryStream]来读取 --- Blog.Core.Api/Program.cs | 1 + .../Extensions/HttpResponseExceptions.cs | 34 ++ .../Https/FluentHttpResponseStream.cs | 75 +++ Blog.Core.Extensions/AOP/BlogLogAOP.cs | 486 +++++++++--------- .../Middlewares/ExceptionHandlerMiddleware.cs | 89 ++-- .../FluentResponseBodyMiddleware.cs | 21 + .../Middlewares/IpLogMiddleware.cs | 241 +++++---- .../Middlewares/RecordAccessLogsMiddleware.cs | 262 +++++----- .../Middlewares/RequRespLogMiddleware.cs | 245 +++++---- .../Redis/RedisBasketRepository.cs | 3 +- 10 files changed, 784 insertions(+), 673 deletions(-) create mode 100644 Blog.Core.Common/Extensions/HttpResponseExceptions.cs create mode 100644 Blog.Core.Common/Https/FluentHttpResponseStream.cs create mode 100644 Blog.Core.Extensions/Middlewares/FluentResponseBodyMiddleware.cs diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 74bcd311..67d4524e 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -132,6 +132,7 @@ var app = builder.Build(); app.ConfigureApplication(); app.UseApplicationSetup(); +app.UseResponseBodyRead(); if (app.Environment.IsDevelopment()) { diff --git a/Blog.Core.Common/Extensions/HttpResponseExceptions.cs b/Blog.Core.Common/Extensions/HttpResponseExceptions.cs new file mode 100644 index 00000000..b9d25f81 --- /dev/null +++ b/Blog.Core.Common/Extensions/HttpResponseExceptions.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using Blog.Core.Common.Https; +using Microsoft.AspNetCore.Http; + +namespace Blog.Core.Common.Extensions; + +public static class HttpResponseExceptions +{ + public static string GetResponseBody(this HttpResponse response) + { + if (response is null) + { + return default; + } + + if (response.Body is FluentHttpResponseStream responseBody) + { + response.Body.Position = 0; + using StreamReader stream = new StreamReader(responseBody); + string body = stream.ReadToEnd(); + response.Body.Position = 0; + return body; + } + else + { + //原始HttpResponseStream 无法读取 + //实际上只是个包装类,内部使用了HttpResponsePipeWriter write + throw new ApplicationException("The response body is not a FluentHttpResponseStream"); + } + + return default; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/Https/FluentHttpResponseStream.cs b/Blog.Core.Common/Https/FluentHttpResponseStream.cs new file mode 100644 index 00000000..c668f47d --- /dev/null +++ b/Blog.Core.Common/Https/FluentHttpResponseStream.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Blog.Core.Common.Https; + +/// +/// 扩展 HttpResponseStream
+/// 原始[HttpResponseStream]实际上只是个包装类,内部包装了[HttpResponsePipeWriter]来进行写入响应数据 +/// +public class FluentHttpResponseStream : Stream +{ + private readonly IHttpBodyControlFeature _bodyControl; + private readonly IHttpResponseBodyFeature _pipeWriter; + private readonly MemoryStream _stream = new(); + + public FluentHttpResponseStream(IHttpResponseBodyFeature pipeWriter, IHttpBodyControlFeature bodyControl) + { + _pipeWriter = pipeWriter; + _bodyControl = bodyControl; + } + + public override bool CanRead => _stream.CanRead; + + public override bool CanSeek => _stream.CanSeek; + + public override bool CanWrite => _stream.CanWrite; + + public override long Length => _stream.Length; + + public override long Position { get => _stream.Position; set => _stream.Position = value; } + + public override void Flush() + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException("SynchronousWritesDisallowed "); + } + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + _stream.Write(buffer, offset, count); + return _pipeWriter.Writer.WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + } + + protected override void Dispose(bool disposing) + { + _stream.Dispose(); + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/AOP/BlogLogAOP.cs b/Blog.Core.Extensions/AOP/BlogLogAOP.cs index 29861578..09d798bf 100644 --- a/Blog.Core.Extensions/AOP/BlogLogAOP.cs +++ b/Blog.Core.Extensions/AOP/BlogLogAOP.cs @@ -13,271 +13,279 @@ namespace Blog.Core.AOP { - /// - /// 拦截器BlogLogAOP 继承IInterceptor接口 - /// - public class BlogLogAOP : IInterceptor - { - private readonly IHubContext _hubContext; - private readonly IHttpContextAccessor _accessor; + /// + /// 拦截器BlogLogAOP 继承IInterceptor接口 + /// + public class BlogLogAOP : IInterceptor + { + private readonly IHubContext _hubContext; + private readonly IHttpContextAccessor _accessor; - public BlogLogAOP(IHubContext hubContext, IHttpContextAccessor accessor) - { - _hubContext = hubContext; - _accessor = accessor; - } + public BlogLogAOP(IHubContext hubContext, IHttpContextAccessor accessor) + { + _hubContext = hubContext; + _accessor = accessor; + } - /// - /// 实例化IInterceptor唯一方法 - /// - /// 包含被拦截方法的信息 - public void Intercept(IInvocation invocation) - { - string UserName = _accessor.HttpContext?.User?.Identity?.Name; - string json; - try - { - json = JsonConvert.SerializeObject(invocation.Arguments); - } - catch (Exception ex) - { - json = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); - } - DateTime startTime = DateTime.Now; - AOPLogInfo apiLogAopInfo = new AOPLogInfo - { - RequestTime = startTime.ToString("yyyy-MM-dd hh:mm:ss fff"), - OpUserName = UserName, - RequestMethodName = invocation.Method.Name, - RequestParamsName = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()), - ResponseJsonData = json - }; + /// + /// 实例化IInterceptor唯一方法 + /// + /// 包含被拦截方法的信息 + public void Intercept(IInvocation invocation) + { + string UserName = _accessor.HttpContext?.User?.Identity?.Name; + string json; + try + { + json = JsonConvert.SerializeObject(invocation.Arguments); + } + catch (Exception ex) + { + json = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); + } - //测试异常记录 - //Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff")); + DateTime startTime = DateTime.Now; + AOPLogInfo apiLogAopInfo = new AOPLogInfo + { + RequestTime = startTime.ToString("yyyy-MM-dd hh:mm:ss fff"), + OpUserName = UserName, + RequestMethodName = invocation.Method.Name, + RequestParamsName = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()), + ResponseJsonData = json + }; - //记录被拦截方法信息的日志信息 - //var dataIntercept = "" + - // $"【当前操作用户】:{ UserName} \r\n" + - // $"【当前执行方法】:{ invocation.Method.Name} \r\n" + - // $"【携带的参数有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; + //测试异常记录 + //Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff")); - try - { - MiniProfiler.Current.Step($"执行Service方法:{invocation.Method.Name}() -> "); - //在被拦截的方法执行完毕后 继续执行当前方法,注意是被拦截的是异步的 - invocation.Proceed(); + //记录被拦截方法信息的日志信息 + //var dataIntercept = "" + + // $"【当前操作用户】:{ UserName} \r\n" + + // $"【当前执行方法】:{ invocation.Method.Name} \r\n" + + // $"【携带的参数有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; + try + { + MiniProfiler.Current.Step($"执行Service方法:{invocation.Method.Name}() -> "); + //在被拦截的方法执行完毕后 继续执行当前方法,注意是被拦截的是异步的 + invocation.Proceed(); - // 异步获取异常,先执行 - if (IsAsyncMethod(invocation.Method)) - { - #region 方案一 - //Wait task execution and modify return value - if (invocation.Method.ReturnType == typeof(Task)) - { - invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally( - (Task)invocation.ReturnValue, - async () => await SuccessAction(invocation, apiLogAopInfo, startTime),/*成功时执行*/ - ex => - { - LogEx(ex, apiLogAopInfo); - }); - } - //Task - else - { - invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult( - invocation.Method.ReturnType.GenericTypeArguments[0], - invocation.ReturnValue, - //async () => await SuccessAction(invocation, dataIntercept),/*成功时执行*/ - async (o) => await SuccessAction(invocation, apiLogAopInfo, startTime, o),/*成功时执行*/ - ex => - { - LogEx(ex, apiLogAopInfo); - }); - } - #endregion + // 异步获取异常,先执行 + if (IsAsyncMethod(invocation.Method)) + { + #region 方案一 + //Wait task execution and modify return value + if (invocation.Method.ReturnType == typeof(Task)) + { + invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally( + (Task) invocation.ReturnValue, + async () => await SuccessAction(invocation, apiLogAopInfo, startTime), /*成功时执行*/ + ex => + { + LogEx(ex, apiLogAopInfo); + }); + } + //Task + else + { + invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult( + invocation.Method.ReturnType.GenericTypeArguments[0], + invocation.ReturnValue, + //async () => await SuccessAction(invocation, dataIntercept),/*成功时执行*/ + async (o) => await SuccessAction(invocation, apiLogAopInfo, startTime, o), /*成功时执行*/ + ex => + { + LogEx(ex, apiLogAopInfo); + }); + } - // 如果方案一不行,试试这个方案 - //#region 方案二 + #endregion - //var type = invocation.Method.ReturnType; - //var resultProperty = type.GetProperty("Result"); - //DateTime endTime = DateTime.Now; - //string ResponseTime = (endTime - startTime).Milliseconds.ToString(); - //apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); - //apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; - //apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue)); - ////dataIntercept += ($"【响应时间】:{ResponseTime}ms\r\n"); - ////dataIntercept += ($"【执行完成时间】:{endTime.ToString("yyyy-MM-dd hh:mm:ss fff")}\r\n"); - ////dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue))}\r\n"); + // 如果方案一不行,试试这个方案 + //#region 方案二 - //Parallel.For(0, 1, e => - //{ - // //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); - // LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString() + " - ResponseJsonDataType:" + type, JsonConvert.SerializeObject(apiLogAopInfo) }); - //}); + //var type = invocation.Method.ReturnType; + //var resultProperty = type.GetProperty("Result"); + //DateTime endTime = DateTime.Now; + //string ResponseTime = (endTime - startTime).Milliseconds.ToString(); + //apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); + //apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; + //apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue)); - //#endregion - } - else - { - // 同步1 - string jsonResult; - try - { - jsonResult = JsonConvert.SerializeObject(invocation.ReturnValue); - } - catch (Exception ex) - { - jsonResult = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); - } - var type = invocation.Method.ReturnType; - var resultProperty = type.GetProperty("Result"); - DateTime endTime = DateTime.Now; - string ResponseTime = (endTime - startTime).Milliseconds.ToString(); - apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); - apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; - //apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue)); - apiLogAopInfo.ResponseJsonData = jsonResult; - //dataIntercept += ($"【执行完成结果】:{jsonResult}"); - Parallel.For(0, 1, e => - { - //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); - LogLock.OutLogAOP("AOPLog", _accessor.HttpContext?.TraceIdentifier, new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); - }); - } - } - catch (Exception ex)// 同步2 - { - LogEx(ex, apiLogAopInfo); - } - if (AppSettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) - { - _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); - } - } + ////dataIntercept += ($"【响应时间】:{ResponseTime}ms\r\n"); + ////dataIntercept += ($"【执行完成时间】:{endTime.ToString("yyyy-MM-dd hh:mm:ss fff")}\r\n"); + ////dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue))}\r\n"); - private async Task SuccessAction(IInvocation invocation, AOPLogInfo apiLogAopInfo, DateTime startTime, object o = null) - { - //invocation.ReturnValue = o; - //var type = invocation.Method.ReturnType; - //if (typeof(Task).IsAssignableFrom(type)) - //{ - // //var resultProperty = type.GetProperty("Result"); - // //类型错误 都可以不要invocation参数,直接将o系列化保存到日记中 - // dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(invocation.ReturnValue)}"); - //} - //else - //{ - // dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}"); - //} - DateTime endTime = DateTime.Now; - string ResponseTime = (endTime - startTime).Milliseconds.ToString(); - apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); - apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; - apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(o); + //Parallel.For(0, 1, e => + //{ + // //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); + // LogLock.OutLogAOP("AOPLog", new string[] { apiLogAopInfo.GetType().ToString() + " - ResponseJsonDataType:" + type, JsonConvert.SerializeObject(apiLogAopInfo) }); + //}); + //#endregion + } + else + { + // 同步1 + string jsonResult; + try + { + jsonResult = JsonConvert.SerializeObject(invocation.ReturnValue); + } + catch (Exception ex) + { + jsonResult = "无法序列化,可能是兰姆达表达式等原因造成,按照框架优化代码" + ex.ToString(); + } - await Task.Run(() => - { - Parallel.For(0, 1, e => - { - //LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); - LogLock.OutLogAOP("AOPLog", _accessor.HttpContext?.TraceIdentifier, new string[] { apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo) }); - }); - }); - } + var type = invocation.Method.ReturnType; + var resultProperty = type.GetProperty("Result"); + DateTime endTime = DateTime.Now; + string ResponseTime = (endTime - startTime).Milliseconds.ToString(); + apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); + apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; + //apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(resultProperty.GetValue(invocation.ReturnValue)); + apiLogAopInfo.ResponseJsonData = jsonResult; + //dataIntercept += ($"【执行完成结果】:{jsonResult}"); + Parallel.For(0, 1, e => + { + //LogLock.OutLogAOP("AOPLog", new string[] { dataIntercept }); + LogLock.OutLogAOP("AOPLog", _accessor.HttpContext?.TraceIdentifier, + new string[] {apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo)}); + }); + } + } + catch (Exception ex) // 同步2 + { + LogEx(ex, apiLogAopInfo); + throw; + } - private void LogEx(Exception ex, AOPLogInfo dataIntercept) - { - if (ex != null) - { - //执行的 service 中,收录异常 - MiniProfiler.Current.CustomTiming("Errors:", ex.Message); - //执行的 service 中,捕获异常 - //dataIntercept += ($"【执行完成结果】:方法中出现异常:{ex.Message + ex.InnerException}\r\n"); - AOPLogExInfo apiLogAopExInfo = new AOPLogExInfo - { - ExMessage = ex.Message, - InnerException = "InnerException-内部异常:\r\n" + (ex.InnerException == null ? "" : ex.InnerException.InnerException.ToString()) + ("\r\nStackTrace-堆栈跟踪:\r\n") + (ex.StackTrace == null ? "" : ex.StackTrace.ToString()), - ApiLogAopInfo = dataIntercept - }; - // 异常日志里有详细的堆栈信息 - Parallel.For(0, 1, e => - { - //LogLock.OutLogAOP("AOPLogEx", new string[] { dataIntercept }); - LogLock.OutLogAOP("AOPLogEx", _accessor.HttpContext?.TraceIdentifier, new string[] { apiLogAopExInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopExInfo) }); + if (AppSettings.app(new string[] {"Middleware", "SignalRSendLog", "Enabled"}).ObjToBool()) + { + _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait(); + } + } - }); - } - } + private async Task SuccessAction(IInvocation invocation, AOPLogInfo apiLogAopInfo, DateTime startTime, object o = null) + { + //invocation.ReturnValue = o; + //var type = invocation.Method.ReturnType; + //if (typeof(Task).IsAssignableFrom(type)) + //{ + // //var resultProperty = type.GetProperty("Result"); + // //类型错误 都可以不要invocation参数,直接将o系列化保存到日记中 + // dataIntercept += ($"【执行完成结果】:{JsonConvert.SerializeObject(invocation.ReturnValue)}"); + //} + //else + //{ + // dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}"); + //} + DateTime endTime = DateTime.Now; + string ResponseTime = (endTime - startTime).Milliseconds.ToString(); + apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff"); + apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms"; + apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(o); - public static bool IsAsyncMethod(MethodInfo method) - { - return ( - method.ReturnType == typeof(Task) || - (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) - ); - } + await Task.Run(() => + { + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("AOPLog", new string[] { JsonConvert.SerializeObject(apiLogAopInfo) }); + LogLock.OutLogAOP("AOPLog", _accessor.HttpContext?.TraceIdentifier, + new string[] {apiLogAopInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopInfo)}); + }); + }); + } - } + private void LogEx(Exception ex, AOPLogInfo dataIntercept) + { + if (ex != null) + { + //执行的 service 中,收录异常 + MiniProfiler.Current.CustomTiming("Errors:", ex.Message); + //执行的 service 中,捕获异常 + //dataIntercept += ($"【执行完成结果】:方法中出现异常:{ex.Message + ex.InnerException}\r\n"); + AOPLogExInfo apiLogAopExInfo = new AOPLogExInfo + { + ExMessage = ex.Message, + InnerException = "InnerException-内部异常:\r\n" + (ex.InnerException == null ? "" : ex.InnerException.InnerException.ToString()) + + ("\r\nStackTrace-堆栈跟踪:\r\n") + (ex.StackTrace == null ? "" : ex.StackTrace.ToString()), + ApiLogAopInfo = dataIntercept + }; + // 异常日志里有详细的堆栈信息 + Parallel.For(0, 1, e => + { + //LogLock.OutLogAOP("AOPLogEx", new string[] { dataIntercept }); + LogLock.OutLogAOP("AOPLogEx", _accessor.HttpContext?.TraceIdentifier, + new string[] {apiLogAopExInfo.GetType().ToString(), JsonConvert.SerializeObject(apiLogAopExInfo)}); + }); + } + } - internal static class InternalAsyncHelper - { - public static async Task AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func postAction, Action finalAction) - { - Exception exception = null; + public static bool IsAsyncMethod(MethodInfo method) + { + return ( + method.ReturnType == typeof(Task) || + (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + ); + } + } - try - { - await actualReturnValue; - await postAction(); - } - catch (Exception ex) - { - exception = ex; - } - finally - { - finalAction(exception); - } - } - public static async Task AwaitTaskWithPostActionAndFinallyAndGetResult(Task actualReturnValue, Func postAction, Action finalAction) - { - Exception exception = null; - try - { - var result = await actualReturnValue; - await postAction(result); - return result; - } - catch (Exception ex) - { - exception = ex; - throw; - } - finally - { - finalAction(exception); - } - } + internal static class InternalAsyncHelper + { + public static async Task AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func postAction, Action finalAction) + { + Exception exception = null; - public static object CallAwaitTaskWithPostActionAndFinallyAndGetResult(Type taskReturnType, object actualReturnValue, Func action, Action finalAction) - { - return typeof(InternalAsyncHelper) - .GetMethod("AwaitTaskWithPostActionAndFinallyAndGetResult", BindingFlags.Public | BindingFlags.Static) - .MakeGenericMethod(taskReturnType) - .Invoke(null, new object[] { actualReturnValue, action, finalAction }); - } - } + try + { + await actualReturnValue; + await postAction(); + } + catch (Exception ex) + { + exception = ex; + } + finally + { + finalAction(exception); + } + } -} + public static async Task AwaitTaskWithPostActionAndFinallyAndGetResult(Task actualReturnValue, Func postAction, + Action finalAction) + { + Exception exception = null; + try + { + var result = await actualReturnValue; + await postAction(result); + return result; + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + finalAction(exception); + } + } + + public static object CallAwaitTaskWithPostActionAndFinallyAndGetResult(Type taskReturnType, object actualReturnValue, + Func action, Action finalAction) + { + return typeof(InternalAsyncHelper) + .GetMethod("AwaitTaskWithPostActionAndFinallyAndGetResult", BindingFlags.Public | BindingFlags.Static) + .MakeGenericMethod(taskReturnType) + .Invoke(null, new object[] {actualReturnValue, action, finalAction}); + } + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs index aed57769..7a7e1fb6 100644 --- a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs @@ -8,48 +8,49 @@ namespace Blog.Core.Extensions.Middlewares { - public class ExceptionHandlerMiddleware - { - private readonly RequestDelegate _next; - - public ExceptionHandlerMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext context) - { - try - { - await _next(context); - } - catch (Exception ex) - { - await HandleExceptionAsync(context, ex); - } - } - - private async Task HandleExceptionAsync(HttpContext context, Exception e) - { - if (e == null) return; - - Log.Error(e.GetBaseException().ToString()); - - await WriteExceptionAsync(context, e).ConfigureAwait(false); - } - - private static async Task WriteExceptionAsync(HttpContext context, Exception e) - { - if (e is UnauthorizedAccessException) - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; - else if (e is Exception) - context.Response.StatusCode = (int)HttpStatusCode.BadRequest; - - context.Response.ContentType = "application/json"; - - await context.Response - .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, e.Message).MessageModel)) - .ConfigureAwait(false); - } - } + public class ExceptionHandlerMiddleware + { + private readonly RequestDelegate _next; + + public ExceptionHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + throw; + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception e) + { + if (e == null) return; + + Log.Error(e.GetBaseException().ToString()); + + await WriteExceptionAsync(context, e).ConfigureAwait(false); + } + + private static async Task WriteExceptionAsync(HttpContext context, Exception e) + { + if (e is UnauthorizedAccessException) + context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; + else if (e is Exception) + context.Response.StatusCode = (int) HttpStatusCode.BadRequest; + + context.Response.ContentType = "application/json"; + + await context.Response + .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, e.Message).MessageModel)) + .ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/FluentResponseBodyMiddleware.cs b/Blog.Core.Extensions/Middlewares/FluentResponseBodyMiddleware.cs new file mode 100644 index 00000000..dfb5d19e --- /dev/null +++ b/Blog.Core.Extensions/Middlewares/FluentResponseBodyMiddleware.cs @@ -0,0 +1,21 @@ +using System.IO; +using Blog.Core.Common.Https; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; + +namespace Blog.Core.Extensions.Middlewares; + +public static class FluentResponseBodyMiddleware +{ + public static IApplicationBuilder UseResponseBodyRead(this IApplicationBuilder app) + { + return app.Use(async (context, next) => + { + await using var swapStream = new FluentHttpResponseStream(context!.Features!.Get()!, + context!.Features!.Get()!); + context.Response.Body = swapStream; + await next(context); + context.Response.Body.Seek(0, SeekOrigin.Begin); + }); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs index ccd0d7af..61e9861c 100644 --- a/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/IpLogMiddleware.cs @@ -8,139 +8,134 @@ namespace Blog.Core.Extensions.Middlewares { - /// - /// 中间件 - /// 记录IP请求数据 - /// - public class IpLogMiddleware - { - /// - /// - /// - private readonly RequestDelegate _next; - private readonly IWebHostEnvironment _environment; + /// + /// 中间件 + /// 记录IP请求数据 + /// + public class IpLogMiddleware + { + /// + /// + /// + private readonly RequestDelegate _next; - /// - /// - /// - /// - public IpLogMiddleware(RequestDelegate next, IWebHostEnvironment environment) - { - _next = next; - _environment = environment; - } + private readonly IWebHostEnvironment _environment; - public async Task InvokeAsync(HttpContext context) - { - if (AppSettings.app("Middleware", "IPLog", "Enabled").ObjToBool()) - { - // 过滤,只有接口 - if (context.Request.Path.Value.Contains("api")) - { - context.Request.EnableBuffering(); + /// + /// + /// + /// + public IpLogMiddleware(RequestDelegate next, IWebHostEnvironment environment) + { + _next = next; + _environment = environment; + } - try - { - // 存储请求数据 - var request = context.Request; + public async Task InvokeAsync(HttpContext context) + { + if (AppSettings.app("Middleware", "IPLog", "Enabled").ObjToBool()) + { + // 过滤,只有接口 + if (context.Request.Path.Value.Contains("api")) + { + context.Request.EnableBuffering(); - var requestInfo = JsonConvert.SerializeObject(new RequestInfo() - { - Ip = GetClientIP(context), - Url = request.Path.ObjToString().TrimEnd('/').ToLower(), - Datetime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), - Date = DateTime.Now.ToString("yyyy-MM-dd"), - Week = GetWeek(), - }); - if (!string.IsNullOrEmpty(requestInfo)) - { - // 自定义log输出 - Parallel.For(0, 1, e => - { - //LogLock.OutSql2Log("RequestIpInfoLog", new string[] { requestInfo + "," }, false); - LogLock.OutLogAOP("RequestIpInfoLog", context.TraceIdentifier, new string[] { requestInfo.GetType().ToString(), requestInfo }, false); - }); + // 存储请求数据 + var request = context.Request; - //try - //{ - // var testLogMatchRequestInfo = JsonConvert.DeserializeObject(requestInfo); - // if (testLogMatchRequestInfo != null) - // { - // var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RequestIpInfoLog"); - // SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false, "", true); - // } - //} - //catch (Exception e) - //{ - // log.Error(requestInfo + "\r\n" + e.GetBaseException().ToString()); - //} + var requestInfo = JsonConvert.SerializeObject(new RequestInfo() + { + Ip = GetClientIP(context), + Url = request.Path.ObjToString().TrimEnd('/').ToLower(), + Datetime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), + Date = DateTime.Now.ToString("yyyy-MM-dd"), + Week = GetWeek(), + }); + if (!string.IsNullOrEmpty(requestInfo)) + { + // 自定义log输出 + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RequestIpInfoLog", new string[] { requestInfo + "," }, false); + LogLock.OutLogAOP("RequestIpInfoLog", context.TraceIdentifier, + new string[] {requestInfo.GetType().ToString(), requestInfo}, false); + }); - request.Body.Position = 0; - } + //try + //{ + // var testLogMatchRequestInfo = JsonConvert.DeserializeObject(requestInfo); + // if (testLogMatchRequestInfo != null) + // { + // var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RequestIpInfoLog"); + // SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false, "", true); + // } + //} + //catch (Exception e) + //{ + // log.Error(requestInfo + "\r\n" + e.GetBaseException().ToString()); + //} + + request.Body.Position = 0; + } - await _next(context); - } - catch (Exception) - { - // ignored - } - } - else - { - await _next(context); - } - } - else - { - await _next(context); - } - } + await _next(context); + } + else + { + await _next(context); + } + } + else + { + await _next(context); + } + } - private string GetWeek() - { - string week = string.Empty; - switch (DateTime.Now.DayOfWeek) - { - case DayOfWeek.Monday: - week = "周一"; - break; - case DayOfWeek.Tuesday: - week = "周二"; - break; - case DayOfWeek.Wednesday: - week = "周三"; - break; - case DayOfWeek.Thursday: - week = "周四"; - break; - case DayOfWeek.Friday: - week = "周五"; - break; - case DayOfWeek.Saturday: - week = "周六"; - break; - case DayOfWeek.Sunday: - week = "周日"; - break; - default: - week = "N/A"; - break; - } - return week; - } + private string GetWeek() + { + string week = string.Empty; + switch (DateTime.Now.DayOfWeek) + { + case DayOfWeek.Monday: + week = "周一"; + break; + case DayOfWeek.Tuesday: + week = "周二"; + break; + case DayOfWeek.Wednesday: + week = "周三"; + break; + case DayOfWeek.Thursday: + week = "周四"; + break; + case DayOfWeek.Friday: + week = "周五"; + break; + case DayOfWeek.Saturday: + week = "周六"; + break; + case DayOfWeek.Sunday: + week = "周日"; + break; + default: + week = "N/A"; + break; + } - public static string GetClientIP(HttpContext context) - { - var ip = context.Request.Headers["X-Forwarded-For"].ObjToString(); - if (string.IsNullOrEmpty(ip)) - { - ip = context.Connection.RemoteIpAddress.ObjToString(); - } - return ip; - } + return week; + } - } -} + public static string GetClientIP(HttpContext context) + { + var ip = context.Request.Headers["X-Forwarded-For"].ObjToString(); + if (string.IsNullOrEmpty(ip)) + { + ip = context.Connection.RemoteIpAddress.ObjToString(); + } + return ip; + } + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs index 4e12dd93..5c203fb3 100644 --- a/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RecordAccessLogsMiddleware.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web; using Blog.Core.Common; +using Blog.Core.Common.Extensions; using Blog.Core.Common.Helper; using Blog.Core.Common.HttpContextUser; using Blog.Core.Common.LogHelper; @@ -15,146 +16,121 @@ namespace Blog.Core.Extensions.Middlewares { - /// - /// 中间件 - /// 记录用户方访问数据 - /// - public class RecordAccessLogsMiddleware - { - /// - /// - /// - private readonly RequestDelegate _next; - private readonly IUser _user; - private readonly ILogger _logger; - private readonly IWebHostEnvironment _environment; - private Stopwatch _stopwatch; - - /// - /// - /// - /// - public RecordAccessLogsMiddleware(RequestDelegate next, IUser user, ILogger logger, IWebHostEnvironment environment) - { - _next = next; - _user = user; - _logger = logger; - _environment = environment; - _stopwatch = new Stopwatch(); - } - - public async Task InvokeAsync(HttpContext context) - { - if (AppSettings.app("Middleware", "RecordAccessLogs", "Enabled").ObjToBool()) - { - var api = context.Request.Path.ObjToString().TrimEnd('/').ToLower(); - var ignoreApis = AppSettings.app("Middleware", "RecordAccessLogs", "IgnoreApis"); - - // 过滤,只有接口 - if (api.Contains("api") && !ignoreApis.Contains(api)) - { - _stopwatch.Restart(); - var userAccessModel = new UserAccessModel(); - - HttpRequest request = context.Request; - - userAccessModel.API = api; - userAccessModel.User = _user.Name; - userAccessModel.IP = IpLogMiddleware.GetClientIP(context); - userAccessModel.BeginTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - userAccessModel.RequestMethod = request.Method; - userAccessModel.Agent = request.Headers["User-Agent"].ObjToString(); - - - // 获取请求body内容 - if (request.Method.ToLower().Equals("post") || request.Method.ToLower().Equals("put")) - { - // 启用倒带功能,就可以让 Request.Body 可以再次读取 - request.EnableBuffering(); - - Stream stream = request.Body; - byte[] buffer = new byte[request.ContentLength.Value]; - stream.Read(buffer, 0, buffer.Length); - userAccessModel.RequestData = Encoding.UTF8.GetString(buffer); - - request.Body.Position = 0; - } - else if (request.Method.ToLower().Equals("get") || request.Method.ToLower().Equals("delete")) - { - userAccessModel.RequestData = HttpUtility.UrlDecode(request.QueryString.ObjToString(), Encoding.UTF8); - } - - // 获取Response.Body内容 - var originalBodyStream = context.Response.Body; - using (var responseBody = new MemoryStream()) - { - context.Response.Body = responseBody; - - await _next(context); - - var responseBodyData = await GetResponse(context.Response); - - await responseBody.CopyToAsync(originalBodyStream); - } - - // 响应完成记录时间和存入日志 - context.Response.OnCompleted(() => - { - _stopwatch.Stop(); - - userAccessModel.OPTime = _stopwatch.ElapsedMilliseconds + "ms"; - - // 自定义log输出 - var requestInfo = JsonConvert.SerializeObject(userAccessModel); - Parallel.For(0, 1, e => - { - //LogLock.OutSql2Log("RecordAccessLogs", new string[] { requestInfo + "," }, false); - LogLock.OutLogAOP("RecordAccessLogs", context.TraceIdentifier, new string[] { userAccessModel.GetType().ToString(), requestInfo }, false); - }); - //var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RecordAccessLogs"); - //SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false); - return Task.CompletedTask; - }); - - } - else - { - await _next(context); - } - } - else - { - await _next(context); - } - } - - - /// - /// 获取响应内容 - /// - /// - /// - public async Task GetResponse(HttpResponse response) - { - response.Body.Seek(0, SeekOrigin.Begin); - var text = await new StreamReader(response.Body).ReadToEndAsync(); - response.Body.Seek(0, SeekOrigin.Begin); - return text; - } - } - - public class UserAccessModel - { - public string User { get; set; } - public string IP { get; set; } - public string API { get; set; } - public string BeginTime { get; set; } - public string OPTime { get; set; } - public string RequestMethod { get; set; } - public string RequestData { get; set; } - public string Agent { get; set; } - - } - -} - + /// + /// 中间件 + /// 记录用户方访问数据 + /// + public class RecordAccessLogsMiddleware + { + /// + /// + /// + private readonly RequestDelegate _next; + + private readonly IUser _user; + private readonly ILogger _logger; + private readonly IWebHostEnvironment _environment; + private Stopwatch _stopwatch; + + /// + /// + /// + /// + public RecordAccessLogsMiddleware(RequestDelegate next, IUser user, ILogger logger, + IWebHostEnvironment environment) + { + _next = next; + _user = user; + _logger = logger; + _environment = environment; + _stopwatch = new Stopwatch(); + } + + public async Task InvokeAsync(HttpContext context) + { + if (AppSettings.app("Middleware", "RecordAccessLogs", "Enabled").ObjToBool()) + { + var api = context.Request.Path.ObjToString().TrimEnd('/').ToLower(); + var ignoreApis = AppSettings.app("Middleware", "RecordAccessLogs", "IgnoreApis"); + + // 过滤,只有接口 + if (api.Contains("api") && !ignoreApis.Contains(api)) + { + _stopwatch.Restart(); + var userAccessModel = new UserAccessModel(); + + HttpRequest request = context.Request; + + userAccessModel.API = api; + userAccessModel.User = _user.Name; + userAccessModel.IP = IpLogMiddleware.GetClientIP(context); + userAccessModel.BeginTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + userAccessModel.RequestMethod = request.Method; + userAccessModel.Agent = request.Headers["User-Agent"].ObjToString(); + + + // 获取请求body内容 + if (request.Method.ToLower().Equals("post") || request.Method.ToLower().Equals("put")) + { + // 启用倒带功能,就可以让 Request.Body 可以再次读取 + request.EnableBuffering(); + + Stream stream = request.Body; + byte[] buffer = new byte[request.ContentLength.Value]; + stream.Read(buffer, 0, buffer.Length); + userAccessModel.RequestData = Encoding.UTF8.GetString(buffer); + + request.Body.Position = 0; + } + else if (request.Method.ToLower().Equals("get") || request.Method.ToLower().Equals("delete")) + { + userAccessModel.RequestData = HttpUtility.UrlDecode(request.QueryString.ObjToString(), Encoding.UTF8); + } + + await _next(context); + + // 响应完成记录时间和存入日志 + context.Response.OnCompleted(() => + { + _stopwatch.Stop(); + + userAccessModel.OPTime = _stopwatch.ElapsedMilliseconds + "ms"; + + // 自定义log输出 + var requestInfo = JsonConvert.SerializeObject(userAccessModel); + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RecordAccessLogs", new string[] { requestInfo + "," }, false); + LogLock.OutLogAOP("RecordAccessLogs", context.TraceIdentifier, + new string[] {userAccessModel.GetType().ToString(), requestInfo}, false); + }); + //var logFileName = FileHelper.GetAvailableFileNameWithPrefixOrderSize(_environment.ContentRootPath, "RecordAccessLogs"); + //SerilogServer.WriteLog(logFileName, new string[] { requestInfo + "," }, false); + return Task.CompletedTask; + }); + } + else + { + await _next(context); + } + } + else + { + await _next(context); + } + } + + } + + public class UserAccessModel + { + public string User { get; set; } + public string IP { get; set; } + public string API { get; set; } + public string BeginTime { get; set; } + public string OPTime { get; set; } + public string RequestMethod { get; set; } + public string RequestData { get; set; } + public string Agent { get; set; } + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index 4c61ed3b..26971a3a 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Blog.Core.Common; +using Blog.Core.Common.Extensions; using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -10,126 +11,124 @@ namespace Blog.Core.Extensions.Middlewares { - /// - /// 中间件 - /// 记录请求和响应数据 - /// - public class RequRespLogMiddleware - { - /// - /// - /// - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - /// - /// - /// - /// - public RequRespLogMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - - - - public async Task InvokeAsync(HttpContext context) - { - if (AppSettings.app("Middleware", "RequestResponseLog", "Enabled").ObjToBool()) - { - // 过滤,只有接口 - if (context.Request.Path.Value.Contains("api")) - { - context.Request.EnableBuffering(); - Stream originalBody = context.Response.Body; - - try - { - // 存储请求数据 - await RequestDataLog(context); - - using (var ms = new MemoryStream()) - { - context.Response.Body = ms; - - await _next(context); - - // 存储响应数据 - ResponseDataLog(context.Response, ms); - - ms.Position = 0; - await ms.CopyToAsync(originalBody); - } - } - catch (Exception ex) - { - // 记录异常 - _logger.LogError(ex.Message + "" + ex.InnerException); - } - finally - { - context.Response.Body = originalBody; - } - } - else - { - await _next(context); - } - } - else - { - await _next(context); - } - } - - private async Task RequestDataLog(HttpContext context) - { - var request = context.Request; - var sr = new StreamReader(request.Body); - RequestLogInfo requestResponse = new RequestLogInfo() - { - Path = request.Path, - QueryString = request.QueryString.ToString(), - BodyData = await sr.ReadToEndAsync() - }; - var content = JsonConvert.SerializeObject(requestResponse); - //var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{await sr.ReadToEndAsync()}"; - - if (!string.IsNullOrEmpty(content)) - { - Parallel.For(0, 1, e => - { - //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Request Data:", content }); - LogLock.OutLogAOP("RequestResponseLog", context.TraceIdentifier, new string[] { "Request Data - RequestJsonDataType:" + requestResponse.GetType().ToString(), content }); - - }); - //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Request Data:", content }); - - request.Body.Position = 0; - } - } - - private void ResponseDataLog(HttpResponse response, MemoryStream ms) - { - ms.Position = 0; - var responseBody = new StreamReader(ms).ReadToEnd(); - - // 去除 Html - var reg = "<[^>]+>"; - var isHtml = Regex.IsMatch(responseBody, reg); - - if (!string.IsNullOrEmpty(responseBody)) - { - Parallel.For(0, 1, e => - { - //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); - LogLock.OutLogAOP("RequestResponseLog", response.HttpContext.TraceIdentifier, new string[] { "Response Data - ResponseJsonDataType:" + responseBody.GetType().ToString(), responseBody }); - - }); - //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); - } - } - } -} - + /// + /// 中间件 + /// 记录请求和响应数据 + /// + public class RequRespLogMiddleware + { + /// + /// + /// + private readonly RequestDelegate _next; + + private readonly ILogger _logger; + + /// + /// + /// + /// + public RequRespLogMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + + public async Task InvokeAsync(HttpContext context) + { + if (AppSettings.app("Middleware", "RequestResponseLog", "Enabled").ObjToBool()) + { + // 过滤,只有接口 + if (context.Request.Path.Value.Contains("api")) + { + context.Request.EnableBuffering(); + + // 存储请求数据 + await RequestDataLog(context); + + await _next(context); + + // 存储响应数据 + ResponseDataLog(context.Response); + } + else + { + await _next(context); + } + } + else + { + await _next(context); + } + } + + private async Task RequestDataLog(HttpContext context) + { + var request = context.Request; + var sr = new StreamReader(request.Body); + RequestLogInfo requestResponse = new RequestLogInfo() + { + Path = request.Path, + QueryString = request.QueryString.ToString(), + BodyData = await sr.ReadToEndAsync() + }; + var content = JsonConvert.SerializeObject(requestResponse); + //var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{await sr.ReadToEndAsync()}"; + + if (!string.IsNullOrEmpty(content)) + { + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Request Data:", content }); + LogLock.OutLogAOP("RequestResponseLog", context.TraceIdentifier, + new string[] {"Request Data - RequestJsonDataType:" + requestResponse.GetType().ToString(), content}); + }); + //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Request Data:", content }); + + request.Body.Position = 0; + } + } + + private void ResponseDataLog(HttpResponse response) + { + var responseBody = response.GetResponseBody(); + + // 去除 Html + var reg = "<[^>]+>"; + var isHtml = Regex.IsMatch(responseBody, reg); + + if (!string.IsNullOrEmpty(responseBody)) + { + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); + LogLock.OutLogAOP("RequestResponseLog", response.HttpContext.TraceIdentifier, + new string[] {"Response Data - ResponseJsonDataType:" + responseBody.GetType().ToString(), responseBody}); + }); + //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); + } + } + + private void ResponseDataLog(HttpResponse response, MemoryStream ms) + { + ms.Position = 0; + var responseBody = new StreamReader(ms).ReadToEnd(); + + // 去除 Html + var reg = "<[^>]+>"; + var isHtml = Regex.IsMatch(responseBody, reg); + + if (!string.IsNullOrEmpty(responseBody)) + { + Parallel.For(0, 1, e => + { + //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); + LogLock.OutLogAOP("RequestResponseLog", response.HttpContext.TraceIdentifier, + new string[] {"Response Data - ResponseJsonDataType:" + responseBody.GetType().ToString(), responseBody}); + }); + //SerilogServer.WriteLog("RequestResponseLog", new string[] { "Response Data:", responseBody }); + } + } + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/Redis/RedisBasketRepository.cs b/Blog.Core.Extensions/Redis/RedisBasketRepository.cs index 7f1356e2..822f9253 100644 --- a/Blog.Core.Extensions/Redis/RedisBasketRepository.cs +++ b/Blog.Core.Extensions/Redis/RedisBasketRepository.cs @@ -4,12 +4,13 @@ using StackExchange.Redis; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; namespace Blog.Core.Extensions { - [Obsolete("普通缓存考虑直接使用ICaching,如果要使用Redis队列等还是使用此类")] + [Description("普通缓存考虑直接使用ICaching,如果要使用Redis队列等还是使用此类")] public class RedisBasketRepository : IRedisBasketRepository { private readonly ILogger _logger; From 45116fe5afbd480d85c1bec233ac67293b6a04fd Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Tue, 13 Jun 2023 17:29:41 +0800 Subject: [PATCH 196/289] =?UTF-8?q?=F0=9F=8E=A8=20=E5=90=AF=E7=94=A8?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E4=B8=AD=E9=97=B4=E4=BB=B6?= =?UTF-8?q?,=E7=BB=9F=E4=B8=80=E6=8B=A6=E6=88=AA=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Program.cs | 1 + .../Middlewares/ExceptionHandlerMiddleware.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 67d4524e..eee48a6e 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -144,6 +144,7 @@ //app.UseHsts(); } +app.UseExceptionHandlerMiddle(); app.UseIpLimitMiddle(); app.UseRequestResponseLogMiddle(); app.UseRecordAccessLogsMiddle(); diff --git a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs index 7a7e1fb6..03168882 100644 --- a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs @@ -26,7 +26,6 @@ public async Task Invoke(HttpContext context) catch (Exception ex) { await HandleExceptionAsync(context, ex); - throw; } } @@ -34,22 +33,26 @@ private async Task HandleExceptionAsync(HttpContext context, Exception e) { if (e == null) return; - Log.Error(e.GetBaseException().ToString()); - await WriteExceptionAsync(context, e).ConfigureAwait(false); } private static async Task WriteExceptionAsync(HttpContext context, Exception e) { - if (e is UnauthorizedAccessException) - context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; - else if (e is Exception) - context.Response.StatusCode = (int) HttpStatusCode.BadRequest; + var message = e.Message; + switch (e) + { + case UnauthorizedAccessException: + context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; + break; + default: + context.Response.StatusCode = (int) HttpStatusCode.BadRequest; + break; + } context.Response.ContentType = "application/json"; await context.Response - .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, e.Message).MessageModel)) + .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, message).MessageModel)) .ConfigureAwait(false); } } From 0c837cd50e9c4b0f86e81ca75a11da0f0a929a23 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Tue, 13 Jun 2023 17:58:57 +0800 Subject: [PATCH 197/289] :a: change MigratePermission api --- Blog.Core.Api/Blog.Core.xml | 6 +--- .../Controllers/PermissionController.cs | 36 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 69d3f9b2..ad812e58 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -456,14 +456,10 @@ - + 系统接口菜单同步接口 - - 接口module的控制器名称 - 菜单permission的父id - 是否执行迁移到数据 diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index 7346cc21..a3429163 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -7,7 +7,6 @@ using Blog.Core.Model; using Blog.Core.Model.Models; using Blog.Core.Repository.UnitOfWorks; -using Blog.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; @@ -633,13 +632,9 @@ public async Task> BatchPost([FromBody] List pe /// /// 系统接口菜单同步接口 /// - /// - /// 接口module的控制器名称 - /// 菜单permission的父id - /// 是否执行迁移到数据 /// [HttpGet] - public async Task>> MigratePermission(string action = "", string controllerName = "", long pid = 0, bool isAction = false) + public async Task>> MigratePermission(string action = "", string token = "", string gatewayPrefix = "", string swaggerDomain = "", string controllerName = "", long pid = 0, bool isAction = false) { var data = new MessageModel>(); if (controllerName.IsNullOrEmpty()) @@ -648,18 +643,23 @@ public async Task>> MigratePermission(string actio return data; } - controllerName = controllerName.ToLower(); + controllerName = controllerName.TrimEnd('/').ToLower(); - using var client = _httpClientFactory.CreateClient(); - var jsonFileDomain = AppSettings.GetValue("Startup:Domain"); + gatewayPrefix = gatewayPrefix.Trim(); + swaggerDomain = swaggerDomain.Trim(); + controllerName = controllerName.Trim(); - if (jsonFileDomain.IsNullOrEmpty()) + using var client = _httpClientFactory.CreateClient(); + var Configuration = swaggerDomain.IsNotEmptyOrNull() ? swaggerDomain : AppSettings.GetValue("SystemCfg:Domain"); + var url = $"{Configuration}/swagger/V2/swagger.json"; + if (Configuration.IsNullOrEmpty()) { data.msg = "Swagger.json在线文件域名不能为空"; return data; } - - var url = $"{jsonFileDomain}/swagger/V2/swagger.json"; + if (token.IsNullOrEmpty()) token = Request.Headers.Authorization; + token = token.Trim(); + client.DefaultRequestHeaders.Add("Authorization", $"{token}"); var response = await client.GetAsync(url); var body = await response.Content.ReadAsStringAsync(); @@ -671,9 +671,10 @@ public async Task>> MigratePermission(string actio List permissions = new List(); foreach (JProperty jProperty in pathsJObj.Properties()) { - var apiPath = jProperty.Name.ToLower(); + var apiPath = gatewayPrefix + jProperty.Name.ToLower(); if (action.IsNotEmptyOrNull()) { + action = action.Trim(); if (!apiPath.Contains(action.ToLower())) { continue; @@ -697,12 +698,12 @@ public async Task>> MigratePermission(string actio httpmethod = "delete"; } - var summary = jProperty.Value.SelectToken($"{httpmethod}.summary").ObjToString(); + var summary = jProperty.Value?.SelectToken($"{httpmethod}.summary")?.ObjToString() ?? ""; var subIx = summary.IndexOf("(Auth"); - if (subIx > 0) + if (subIx >= 0) { - summary = summary.Substring(0, subIx - 1); + summary = summary.Substring(0, subIx); } permissions.Add(new Permission() @@ -715,7 +716,6 @@ public async Task>> MigratePermission(string actio CreateTime = DateTime.Now, IsDeleted = false, Pid = pid, - MName = apiPath ?? "", Module = new Modules() { LinkUrl = apiPath ?? "", @@ -748,6 +748,7 @@ public async Task>> MigratePermission(string actio } } + data.msg = "同步完成"; } data.response = permissions; @@ -756,7 +757,6 @@ public async Task>> MigratePermission(string actio return data; } - } public class AssignView From bb0150e96a1d605382a917b173cccb91fb903c59 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Mon, 19 Jun 2023 16:43:29 +0800 Subject: [PATCH 198/289] =?UTF-8?q?=F0=9F=8E=A8=20=E4=B8=8D=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E5=BA=95=E5=B1=82=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/Extensions/HttpResponseExceptions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Blog.Core.Common/Extensions/HttpResponseExceptions.cs b/Blog.Core.Common/Extensions/HttpResponseExceptions.cs index b9d25f81..67deee45 100644 --- a/Blog.Core.Common/Extensions/HttpResponseExceptions.cs +++ b/Blog.Core.Common/Extensions/HttpResponseExceptions.cs @@ -17,7 +17,8 @@ public static string GetResponseBody(this HttpResponse response) if (response.Body is FluentHttpResponseStream responseBody) { response.Body.Position = 0; - using StreamReader stream = new StreamReader(responseBody); + //不关闭底层流 + using StreamReader stream = new StreamReader(responseBody, leaveOpen: true); string body = stream.ReadToEnd(); response.Body.Position = 0; return body; From 0d967086d9d3742328bd07dd4f293735340ec689 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 21 Jun 2023 17:35:13 +0800 Subject: [PATCH 199/289] :accept: feat: add request access etc. log --- Blog.Core.Api/Controllers/ValuesController.cs | 22 ++++++++++++++++--- Blog.Core.Api/appsettings.json | 6 ++--- Blog.Core.Common/LogHelper/LogLock.cs | 9 ++++---- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index ae5cc81f..072ab39e 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -30,7 +30,7 @@ namespace Blog.Core.Controllers //[Authorize(Policy = "SystemOrAdmin")] //[Authorize(PermissionNames.Permission)] [Authorize] - public class ValuesController : ControllerBase + public class ValuesController : BaseApiController { private IMapper _mapper; private readonly IAdvertisementServices _advertisementServices; @@ -159,12 +159,12 @@ await _blogArticleServices.QuerySql( * `bsubmitter`=@bsubmitter,`IsDeleted`=@IsDeleted WHERE `bID`=@bID */ var updateSql = await _blogArticleServices.Update(new - { bsubmitter = $"laozhang{DateTime.Now.Millisecond}", IsDeleted = false, bID = 5 }); + { bsubmitter = $"laozhang{DateTime.Now.Millisecond}", IsDeleted = false, bID = 5 }); // 测试模拟异常,全局异常过滤器拦截 var i = 0; - // var d = 3 / i; + // var d = 3 / i; // 测试 AOP 缓存 @@ -188,6 +188,22 @@ await _blogArticleServices.QuerySql( return data; } + + [HttpGet] + [AllowAnonymous] + public async Task>> Test_Aop_Cache() + { + // 测试 AOP 缓存 + var blogArticles = await _blogArticleServices.GetBlogs(); + + if (blogArticles.Any()) + { + return Success(blogArticles); + } + + return Failed>(); + } + /// /// 测试Redis消息队列 /// diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index def53cfc..941d0125 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -210,7 +210,7 @@ "RequestResponseLog": { "Enabled": true, "LogToFile": { - "Enabled": false + "Enabled": true }, "LogToDB": { "Enabled": true @@ -219,7 +219,7 @@ "IPLog": { "Enabled": true, "LogToFile": { - "Enabled": false + "Enabled": true }, "LogToDB": { "Enabled": true @@ -228,7 +228,7 @@ "RecordAccessLogs": { "Enabled": true, "LogToFile": { - "Enabled": false + "Enabled": true }, "LogToDB": { "Enabled": true diff --git a/Blog.Core.Common/LogHelper/LogLock.cs b/Blog.Core.Common/LogHelper/LogLock.cs index 2c9be9a9..e03c6366 100644 --- a/Blog.Core.Common/LogHelper/LogLock.cs +++ b/Blog.Core.Common/LogHelper/LogLock.cs @@ -195,26 +195,25 @@ public static void OutSql2LogToDB(string prefix, string traceId, string[] dataPa { //DEBUG | INFO | WARN | ERROR | FATAL case "AOPLog": - //TODO 是否需要输出? - //Log.Information(logContent); + Log.Information(logContent); break; case "AOPLogEx": Log.Error(logContent); break; case "RequestIpInfoLog": //TODO 是否需要Debug输出? - //Log.Debug(logContent); + Log.Information(logContent); break; case "RecordAccessLogs": //TODO 是否需要Debug输出? - //Log.Debug(logContent); + Log.Information(logContent); break; case "SqlLog": Log.Information(logContent); break; case "RequestResponseLog": //TODO 是否需要Debug输出? - //Log.Debug(logContent); + Log.Information(logContent); break; default: break; From 274f3c1760cf80532c76fafc38cdb5c32ff7f813 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 25 Jun 2023 16:44:16 +0800 Subject: [PATCH 200/289] Update Program.cs --- Blog.Core.Api/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index eee48a6e..876a83a8 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -180,7 +180,6 @@ app.UseAuthentication(); app.UseAuthorization(); app.UseMiniProfilerMiddleware(); -//app.UseExceptionHandlerMidd(); app.UseEndpoints(endpoints => { From 990dcfd756d520600501ca9deba0878d36c4fd72 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 25 Jun 2023 17:01:14 +0800 Subject: [PATCH 201/289] Update ExceptionHandlerMiddleware.cs --- .../Middlewares/ExceptionHandlerMiddleware.cs | 97 +++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs index 03168882..dc2cd17d 100644 --- a/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/ExceptionHandlerMiddleware.cs @@ -4,56 +4,55 @@ using Blog.Core.Model; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; -using Serilog; namespace Blog.Core.Extensions.Middlewares { - public class ExceptionHandlerMiddleware - { - private readonly RequestDelegate _next; - - public ExceptionHandlerMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext context) - { - try - { - await _next(context); - } - catch (Exception ex) - { - await HandleExceptionAsync(context, ex); - } - } - - private async Task HandleExceptionAsync(HttpContext context, Exception e) - { - if (e == null) return; - - await WriteExceptionAsync(context, e).ConfigureAwait(false); - } - - private static async Task WriteExceptionAsync(HttpContext context, Exception e) - { - var message = e.Message; - switch (e) - { - case UnauthorizedAccessException: - context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; - break; - default: - context.Response.StatusCode = (int) HttpStatusCode.BadRequest; - break; - } - - context.Response.ContentType = "application/json"; - - await context.Response - .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, message).MessageModel)) - .ConfigureAwait(false); - } - } + public class ExceptionHandlerMiddleware + { + private readonly RequestDelegate _next; + + public ExceptionHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception e) + { + if (e == null) return; + + await WriteExceptionAsync(context, e).ConfigureAwait(false); + } + + private static async Task WriteExceptionAsync(HttpContext context, Exception e) + { + var message = e.Message; + switch (e) + { + case UnauthorizedAccessException: + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + break; + default: + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + break; + } + + context.Response.ContentType = "application/json"; + + await context.Response + .WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE500, message).MessageModel)) + .ConfigureAwait(false); + } + } } \ No newline at end of file From f5a2631d7ef31f004234563743f805c73ebbdfab Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 5 Jul 2023 17:34:40 +0800 Subject: [PATCH 202/289] =?UTF-8?q?=E2=9C=A8=20SignalR=20Demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化SignalR,通过传递token确认身份 --- Blog.Core.Api/Blog.Core.xml | 21 ++++++++ .../Controllers/SignalRTestController.cs | 49 +++++++++++++++++++ Blog.Core.Api/appsettings.json | 4 +- Blog.Core.Common/Hubs/ChatHub.cs | 31 ++++++++---- .../Authentication_JWTSetup.cs | 14 ++++++ 5 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 Blog.Core.Api/Controllers/SignalRTestController.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index ad812e58..6dd7f16b 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1251,6 +1251,27 @@ + + + SignalR测试 + + + + + 向指定用户发送消息 + + + + + + + + 向指定角色发送消息 + + + + + 分表demo diff --git a/Blog.Core.Api/Controllers/SignalRTestController.cs b/Blog.Core.Api/Controllers/SignalRTestController.cs new file mode 100644 index 00000000..16ba47f0 --- /dev/null +++ b/Blog.Core.Api/Controllers/SignalRTestController.cs @@ -0,0 +1,49 @@ +using Blog.Core.Controllers; +using Blog.Core.Hubs; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; + +namespace Blog.Core.Api.Controllers; + +/// +/// SignalR测试 +/// +[Route("api/[controller]/[action]")] +[ApiController] +[Authorize] +public class SignalRTestController : BaseApiController +{ + private readonly IHubContext _hubContext; + + public SignalRTestController(IHubContext hubContext) + { + _hubContext = hubContext; + } + + /// + /// 向指定用户发送消息 + /// + /// + /// + /// + [HttpPost] + public async Task SendMessageToUser(string user, string message) + { + await _hubContext.Clients.Group(user).ReceiveMessage(user, message); + return Ok(); + } + + /// + /// 向指定角色发送消息 + /// + /// + /// + /// + [HttpPost] + public async Task SendMessageToRole(string role, string message) + { + await _hubContext.Clients.Group(role).ReceiveMessage(role, message); + return Ok(); + } +} \ No newline at end of file diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 941d0125..5fdf0000 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -236,10 +236,10 @@ "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," }, "SignalR": { - "Enabled": false + "Enabled": true }, "SignalRSendLog": { - "Enabled": false + "Enabled": true }, "QuartzNetJob": { "Enabled": true diff --git a/Blog.Core.Common/Hubs/ChatHub.cs b/Blog.Core.Common/Hubs/ChatHub.cs index 1c58c8a0..f30e6b7b 100644 --- a/Blog.Core.Common/Hubs/ChatHub.cs +++ b/Blog.Core.Common/Hubs/ChatHub.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; using Blog.Core.Common; using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.SignalR; @@ -53,10 +55,23 @@ public async Task SendPrivateMessage(string user, string message) /// 当连接建立时运行 /// /// - public override Task OnConnectedAsync() + public override async Task OnConnectedAsync() { - //TODO.. - return base.OnConnectedAsync(); + await base.OnConnectedAsync(); + if (Context.User?.Identity?.IsAuthenticated == true) + { + //按用户分组 + //是有必要的 例如多个浏览器、多个标签页使用同个用户登录 应当归属于一组 + await AddToGroup(Context.User.Identity.Name); + + //加入角色组 + //根据角色分组 例如管理员分组发送管理员的消息 + var roles = Context.User.Claims.Where(s => s.Type == ClaimTypes.Role).ToList(); + foreach (var role in roles) + { + await AddToGroup(role.Value); + } + } } /// @@ -81,16 +96,14 @@ public async Task SendMessage(string user, string message) public async Task GetLatestCount(string random) { //2、服务端主动向客户端发送数据,名字千万不能错 - if (AppSettings.app(new string[] { "Middleware", "SignalRSendLog", "Enabled" }).ObjToBool()) + if (AppSettings.app(new string[] {"Middleware", "SignalRSendLog", "Enabled"}).ObjToBool()) { //TODO 主动发送错误消息 - //await Clients.All.ReceiveUpdate(LogLock.GetLogData()); + await Clients.All.ReceiveUpdate(LogLock.GetLogData()); } - //3、客户端再通过 ReceiveUpdate ,来接收 - } } -} +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs b/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs index 930bfb6e..98fa0b82 100644 --- a/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs @@ -58,6 +58,20 @@ public static void AddAuthentication_JWTSetup(this IServiceCollection services) o.TokenValidationParameters = tokenValidationParameters; o.Events = new JwtBearerEvents { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + // If the request is for our hub... + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && + (path.StartsWithSegments("/api2/chathub"))) + { + // Read the token out of the query string + context.Token = accessToken; + } + return Task.CompletedTask; + }, OnChallenge = context => { context.Response.Headers.Add("Token-Error", context.ErrorDescription); From b9e24a84e652f217a28b98ea1beaf899ac3b9be3 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Tue, 11 Jul 2023 12:14:16 +0800 Subject: [PATCH 203/289] =?UTF-8?q?=E2=9C=A8=E2=9C=A8=E2=9C=A8=20=E5=8F=B2?= =?UTF-8?q?=E8=AF=97=E7=BA=A7=E6=9B=B4=E6=96=B0,=E5=AE=8C=E7=BE=8E?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=90=84=E7=A7=8D=E5=A4=8D=E6=9D=82=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 例子看单元测试DynamicLambdaTest 支持复杂链表 动态条件拼接 从此后端可只定义一种接口,条件交给前端拼接 后端接口就不在需要定义一堆参数等 --- Blog.Core.Common/Helper/DynamicLinqFactory.cs | 367 +++++++----------- .../Common_Test/DynamicLambdaTest.cs | 108 ++++-- 2 files changed, 227 insertions(+), 248 deletions(-) diff --git a/Blog.Core.Common/Helper/DynamicLinqFactory.cs b/Blog.Core.Common/Helper/DynamicLinqFactory.cs index 248b18ff..adb57074 100644 --- a/Blog.Core.Common/Helper/DynamicLinqFactory.cs +++ b/Blog.Core.Common/Helper/DynamicLinqFactory.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using Mapster; namespace Blog.Core.Common.Helper { @@ -40,10 +41,9 @@ public static Expression> CreateLambda(string prope // 第一个判断条件,固定一个判断条件作为最左边 - Expression mainExpressin = ExpressionStudio(null, strArr.FirstOrDefault(x => x.LinkSymbol == LinkSymbol.Empty), parameter); - + Expression mainExpressin = ExpressionStudio(null, strArr[0], parameter); // 将需要放置在最左边的判断条件从列表中去除,因为已经合成到表达式最左边了 - strArr.Remove(strArr.FirstOrDefault(x => x.LinkSymbol == LinkSymbol.Empty)); + strArr.RemoveAt(0); foreach (var x in strArr) { @@ -57,32 +57,50 @@ public static Expression> CreateLambda(string prope /// 组合条件判断表达式 /// /// 左边的表达式 - /// + /// /// /// - public static Expression ExpressionStudio(Expression left, DynamicLinqHelper DynamicLinq, ParameterExpression key) + public static Expression ExpressionStudio(Expression left, DynamicLinqHelper dynamicLinq, ParameterExpression key) { Expression mainExpression = key; - var properties = DynamicLinq.Left.Split('.'); - - int index = 0; - foreach (var t in properties) + if (!dynamicLinq.Left.IsNullOrEmpty()) { - if (mainExpression.Type.HasImplementedRawGeneric(typeof(IEnumerable<>))) + var properties = dynamicLinq.Left.Split('.'); + + int index = 0; + foreach (var t in properties) { - return ExpressionStudioEnumerable(left, mainExpression, DynamicLinq.Clone(), properties.Skip(index).ToArray()); + if (mainExpression.Type.HasImplementedRawGeneric(typeof(IEnumerable<>))) + { + return ExpressionStudioEnumerable(left, mainExpression, dynamicLinq.Adapt(), + properties.Skip(index).ToArray()); + } + + mainExpression = mainExpression.Property(t); + index++; } + } - mainExpression = mainExpression.Property(t); - index++; + Expression right = null; + if (dynamicLinq.IsMerge && dynamicLinq.Child.Any()) + { + right = ExpressionStudio(null, dynamicLinq.Child[0], key); + for (var i = 1; i < dynamicLinq.Child.Count; i++) + { + right = ChangeLinkSymbol(dynamicLinq.Child[i].LinkSymbol, right, ExpressionStudio(null, dynamicLinq.Child[i], key)); + } + } + else + { + right = ChangeOperationSymbol(dynamicLinq.OperationSymbol, mainExpression, dynamicLinq.Right); } left = left == null // 如果左边表达式为空,则当前的表达式就为最左边 - ? ChangeOperationSymbol(DynamicLinq.OperationSymbol, mainExpression, DynamicLinq.Right) + ? right // 如果不为空,则将当前的表达式连接到左边 - : ChangeLinkSymbol(DynamicLinq.LinkSymbol, left, ChangeOperationSymbol(DynamicLinq.OperationSymbol, mainExpression, DynamicLinq.Right)); + : ChangeLinkSymbol(dynamicLinq.LinkSymbol, left, right); return left; } @@ -102,7 +120,7 @@ public static Expression ExpressionStudioEnumerable(Expression left, Expression var lambda = Expression.Lambda(mainExpression, parameter); - mainExpression = Expression.Call(typeof(Enumerable), "Any", new[] { realType }, property, lambda); + mainExpression = Expression.Call(typeof(Enumerable), "Any", new[] {realType}, property, lambda); left = left == null ? mainExpression @@ -112,155 +130,112 @@ public static Expression ExpressionStudioEnumerable(Expression left, Expression } - /// - /// 将字符串装换成动态帮助类(内含递归) - /// - public static List SpiltStrings(string propertyStr) - { - // 定义返回用List - var outList = new List(); - - // 当最后已经没有连接运算符的时候,进入该条件 - if (!propertyStr.Contains("&") & !propertyStr.Contains("|")) - { - // 当前的条件是不具备连接符号的 - var lastStr = propertyStr.Trim().Split(' '); - outList.Add(new DynamicLinqHelper - { - LinkSymbol = LinkSymbol.Empty, - Left = lastStr[0], - Right = lastStr[2], - OperationSymbol = ChangeOperationSymbol(lastStr[1]) - }); - return outList; - } - - // 判断当前 & | 哪个符号在最后一个判断逻辑内 - var key = propertyStr.LastIndexOf('&') > propertyStr.LastIndexOf('|') ? '&' : '|'; - - var nowStrArr = propertyStr.Substring(propertyStr.LastIndexOf(key)).Trim().Split(' '); - - outList.Add(new DynamicLinqHelper - { - LinkSymbol = ChangeLinkSymbol(nowStrArr[0]), - Left = nowStrArr[1], - OperationSymbol = ChangeOperationSymbol(nowStrArr[2]), - Right = nowStrArr[3] - }); - // 将剩余部分继续切割 - propertyStr = propertyStr.Substring(0, propertyStr.LastIndexOf(key)).Trim(); - // 递归 由后彺前 - outList.AddRange(SpiltStrings(propertyStr)); - - return outList; - } - public static List SplitOperationSymbol(string str) { var outList = new List(); var tokens = Regex.Matches(FormatString(str), _pattern, RegexOptions.Compiled) - .Cast() .Select(m => m.Groups[1].Value.Trim()) .ToList(); + SplitOperationSymbol(tokens, outList); + return outList; + } - int lastIndex = tokens.Count - 1; - int lastOperatingSymbolIndex = -1; - for (int i = tokens.Count - 1; i >= 0; i--) + private static void SplitOperationSymbol(List tokens, List outList, int start = 0, int end = 0) + { + var dys = new Stack(); + var dynamicLinqHelper = new DynamicLinqHelper(); + if (end == 0) { - var token = tokens[i].ToLower(); + end = tokens.Count - 1; + } - if (OperatingSystems.ContainsKey(token)) - { - //比较运算符 - lastOperatingSymbolIndex = i; - } - else if (LinkSymbols.ContainsKey(token)) + for (int i = start; i <= end; i++) + { + var token = tokens[i]; + + if (LinkSymbols.TryGetValue(token, out var symbol)) { - var left = ""; - for (int j = i + 1; j < lastOperatingSymbolIndex; j++) + if (dys.Count > 0) { - left += tokens[j]; + var linqHelper = dys.Peek(); + linqHelper.Child.Add(dynamicLinqHelper); } - - var right = ""; - for (int j = lastOperatingSymbolIndex + 1; j <= lastIndex; j++) + else { - right += tokens[j]; + outList.Add(dynamicLinqHelper); } - outList.Add(GetDynamicLinqHelper(LinkSymbols[token], - OperatingSystems[tokens[lastOperatingSymbolIndex]], - left, - right)); - lastIndex = i - 1; - lastOperatingSymbolIndex = -1; + dynamicLinqHelper = new DynamicLinqHelper() + { + LinkSymbol = symbol, + }; + continue; } - else if (i == 0 && lastOperatingSymbolIndex != -1) + + if (OperatingSystems.TryGetValue(token.ToLower(), out var system)) { - var left = ""; - for (int j = i; j < lastOperatingSymbolIndex; j++) - { - left += tokens[j]; - } + dynamicLinqHelper!.OperationSymbol = system; + continue; + } + - var right = ""; - for (int j = lastOperatingSymbolIndex + 1; j <= lastIndex; j++) + if (dynamicLinqHelper!.OperationSymbol != OperationSymbol.In) + { + if (string.Equals(token.Trim(), "(")) { - right += tokens[j]; + dynamicLinqHelper!.IsMerge = true; + dynamicLinqHelper.Child = new List(); + dys.Push(dynamicLinqHelper); + dynamicLinqHelper = new DynamicLinqHelper(); + continue; } + if (string.Equals(token.Trim(), ")")) + { + if (dys.Count > 1) + { + var dya = dys.Pop(); + dya.Child.Add(dynamicLinqHelper); - outList.Add(GetDynamicLinqHelper(LinkSymbol.Empty, - OperatingSystems[tokens[lastOperatingSymbolIndex]], - left, - right)); + dynamicLinqHelper = dya; + continue; + } + else + { + var dya = dys.Pop(); + dya.Child.Add(dynamicLinqHelper); + outList.Add(dya); + dynamicLinqHelper = null; + continue; + } + } } - } - outList.Reverse(); - return outList; - } - public static DynamicLinqHelper GetDynamicLinqHelper(LinkSymbol linkSymbol, OperationSymbol operationSymbol, string left, string right) - { - var dynamic = new DynamicLinqHelper - { - LinkSymbol = linkSymbol, - OperationSymbol = operationSymbol, - Left = left, - Right = right - }; + if (dynamicLinqHelper!.OperationSymbol is null) + { + dynamicLinqHelper.Left += token; + } + else + { + dynamicLinqHelper.Right += FormatValue(token); + } - if (dynamic.Right.StartsWith("\"") && dynamic.Right.EndsWith("\"")) - { - dynamic.Right = dynamic.Right.Remove(0, 1) - .Remove(dynamic.Right.Length - 2, 1) - .Replace(@"\""", @""""); + if (i == end) + { + outList.Add(dynamicLinqHelper); + dynamicLinqHelper = null; + } } - - return dynamic; } - - /// - /// 将字符串符号转成运算枚举符号 - /// - public static LinkSymbol ChangeLinkSymbol(string str) + public static string FormatValue(string str) { - // 这里判断链接符号 - // 当链接符号为Empty,则说明当前对象为表达式的最左边 - // 如果一个表达式出现两次链接符号为空,则说明输入的字符串格式有问题 - switch (str) - { - case "|": - return LinkSymbol.OrElse; - case "&": - return LinkSymbol.AndAlso; - default: - return LinkSymbol.Empty; - } + return str.TrimStart('"').TrimEnd('"'); + // return str.TrimStart('"').TrimEnd('"').Replace(@"\""", @""""); } + /// /// 将运算枚举符号转成具体使用方法 /// @@ -288,7 +263,7 @@ public static Dictionary GetOperationSymbol() { foreach (var name in attr.Name.Split(';')) { - _operatingSystems.Add(name.ToLower(), (OperationSymbol)item.GetValue(null)); + _operatingSystems.Add(name.ToLower(), (OperationSymbol) item.GetValue(null)); } } } @@ -307,7 +282,7 @@ public static Dictionary GetLinkSymbol() { foreach (var name in attr.Name.Split(';')) { - _linkSymbols.Add(name, (LinkSymbol)item.GetValue(null)); + _linkSymbols.Add(name, (LinkSymbol) item.GetValue(null)); } } } @@ -318,22 +293,18 @@ public static Dictionary GetLinkSymbol() public static string FormatString(string str) { - StringBuilder sb = new StringBuilder(); - int firstIndex = -1; - int lastIndex = -1; - for (int i = 0; i < str.Length; i++) + var sb = new StringBuilder(); + var firstIndex = -1; + var lastIndex = -1; + for (var i = 0; i < str.Length; i++) { var character = str[i]; if (firstIndex == -1) { if (character.IsNullOrEmpty() && i < str.Length - 2) - { if ('"'.Equals(str[i + 1])) - { firstIndex = i + 1; - } - } } else { @@ -341,7 +312,9 @@ public static string FormatString(string str) { var andIndex = str.IndexOf("\" &", firstIndex); var orIndex = str.IndexOf("\" |", firstIndex); - var andOrIndex = andIndex > 0 ? andIndex : orIndex; + var andOrIndex = Math.Min(andIndex, orIndex); + andOrIndex = andOrIndex == -1 ? Math.Max(andOrIndex, orIndex) : andOrIndex; + if (andOrIndex != -1) { lastIndex = andOrIndex; @@ -349,10 +322,7 @@ public static string FormatString(string str) else { if (i == firstIndex) continue; - if (i == str.Length - 1 || str[i + 1].IsNullOrEmpty()) - { - lastIndex = i; - } + if (i == str.Length - 1 || str[i + 1].IsNullOrEmpty()) lastIndex = i; } } @@ -368,10 +338,7 @@ public static string FormatString(string str) } } - if (firstIndex != -1) - { - continue; - } + if (firstIndex != -1) continue; sb.Append(character); } @@ -388,8 +355,8 @@ public static string FormatString(string str) "||", "&&", "==", "!=", "<=", ">=", "in", "like", "contains", "%=", - "startslike", "startscontains", "%>", - "endlike", "endcontains", "%<", + "startslike", "StartsLike", "startscontains", "StartsContains", "%>", + "endlike", "EndLike", "endcontains", "EndContains", "%<", }.Select(Regex.Escape)), @"""(?:\\.|[^""])*""", // string @"\d+(?:\.\d+)?", // number with optional decimal part @@ -397,47 +364,11 @@ public static string FormatString(string str) @"\S", // other 1-char tokens (or eat up one character in case of an error) }) + @")\s*"; - /// - /// 将字符串符号转成运算枚举符号 - /// - public static OperationSymbol ChangeOperationSymbol(string str) - { - switch (str.ToLower()) - { - case "<": - return OperationSymbol.LessThan; - case "<=": - return OperationSymbol.LessThanOrEqual; - case ">": - return OperationSymbol.GreaterThan; - case ">=": - return OperationSymbol.GreaterThanOrEqual; - case "==": - case "=": - return OperationSymbol.Equal; - case "!=": - return OperationSymbol.NotEqual; - case "contains": - case "like": - case "%=": - return OperationSymbol.Contains; - case "startslike": - case "startscontains": - case "%>": - return OperationSymbol.StartsContains; - case "endlike": - case "endcontains": - case "%<": - return OperationSymbol.EndContains; - } - - throw new Exception("OperationSymbol IS NULL"); - } /// /// 将运算枚举符号转成具体使用方法 /// - public static Expression ChangeOperationSymbol(OperationSymbol symbol, Expression key, object right) + public static Expression ChangeOperationSymbol(OperationSymbol? symbol, Expression key, object right) { // 将右边数据类型强行转换成左边一样的类型 // 两者如果Type不匹配则无法接下去的运算操作,抛出异常 @@ -468,30 +399,26 @@ public static Expression ChangeOperationSymbol(OperationSymbol symbol, Expressio { if (key.Type == typeof(string)) return key.Contains(Expression.Constant(newTypeRight)); //对string 特殊处理 由于string - else - return key.GreaterThan(Expression.Constant((newTypeRight))); + return key.GreaterThan(Expression.Constant((newTypeRight))); } case OperationSymbol.GreaterThanOrEqual: { if (key.Type == typeof(string)) return key.Contains(Expression.Constant(newTypeRight, typeof(string))); - else - return key.GreaterThanOrEqual(Expression.Constant(newTypeRight)); + return key.GreaterThanOrEqual(Expression.Constant(newTypeRight)); } case OperationSymbol.LessThan: { if (key.Type == typeof(string)) return key.Contains(Expression.Constant(newTypeRight, typeof(string))); - else - return key.LessThan(Expression.Constant((newTypeRight))); + return key.LessThan(Expression.Constant((newTypeRight))); } case OperationSymbol.LessThanOrEqual: { if (key.Type == typeof(string)) return key.Contains(Expression.Constant(newTypeRight, typeof(string))); - else - return key.LessThanOrEqual(Expression.Constant((newTypeRight))); + return key.LessThanOrEqual(Expression.Constant((newTypeRight))); } case OperationSymbol.NotEqual: return key.NotEqual(Expression.Constant(newTypeRight)); @@ -502,15 +429,10 @@ public static Expression ChangeOperationSymbol(OperationSymbol symbol, Expressio case OperationSymbol.EndContains: return key.EndContains(Expression.Constant(newTypeRight)); case OperationSymbol.In: - var contains = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) - .Single(x => x.Name == "Contains" && x.GetParameters().Length == 2) - .MakeGenericMethod(key.Type); return Expression.Constant(newTypeRight).Contains(key); - - //return Expression.Call(contains, , key); + default: + throw new ArgumentException("OperationSymbol IS NULL"); } - - throw new NotImplementedException("OperationSymbol IS NULL"); } } @@ -526,21 +448,20 @@ public class DynamicLinqHelper public string Right { get; set; } [Display(Name = "运算符")] - public OperationSymbol OperationSymbol { get; set; } + public OperationSymbol? OperationSymbol { get; set; } [Display(Name = "连接符")] public LinkSymbol LinkSymbol { get; set; } - public DynamicLinqHelper Clone() - { - return new DynamicLinqHelper() - { - Left = this.Left, - Right = this.Right, - OperationSymbol = this.OperationSymbol, - LinkSymbol = this.LinkSymbol, - }; - } + /// + /// 是否是合并 用于括号 + /// + public bool IsMerge { get; set; } = false; + + /// + /// 再有括号时候使用 + /// + public List Child { get; set; } } /// @@ -638,8 +559,9 @@ public static IOrderedQueryable ISort(this IQueryable var parameter = Expression.Parameter(type, "p"); var propertyAccess = Expression.MakeMemberAccess(parameter, property); var orderByExpression = Expression.Lambda(propertyAccess, parameter); - var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExpression)); - return (IOrderedQueryable)source.Provider.CreateQuery(resultExpression); + var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] {type, property.PropertyType}, source.Expression, + Expression.Quote(orderByExpression)); + return (IOrderedQueryable) source.Provider.CreateQuery(resultExpression); } /// @@ -666,7 +588,7 @@ public static IQueryable ISkip(this IQueryable source // 调用的方法 "Skip", // 元素类别 - new Type[] { source.ElementType }, + new Type[] {source.ElementType}, // 调用的表达树 source.Expression, // 参数 @@ -684,7 +606,7 @@ public static IQueryable ITake(this IQueryable source // 调用的方法 "Take", // 元素类别 - new Type[] { source.ElementType }, + new Type[] {source.ElementType}, // 调用的表达树 source.Expression, // 参数 @@ -716,7 +638,8 @@ public static IEnumerable IDistinctBy(this IEnumerable>(); _baseRepository.Db.Aop.OnLogExecuting = (sql, p) => { - _testOutputHelper.WriteLine(""); - _testOutputHelper.WriteLine("==================FullSql=====================", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】:" + sql }); - _testOutputHelper.WriteLine("【SQL语句】:" + sql); - _testOutputHelper.WriteLine(GetParas(p)); - _testOutputHelper.WriteLine("=============================================="); - _testOutputHelper.WriteLine(""); + _testOutputHelper.WriteLine(UtilMethods.GetNativeSql(sql, p)); }; - //DbContext.Init(BaseDBConfig.ConnectionString,(DbType)BaseDBConfig.DbType); Init(); } - - private static string GetParas(SugarParameter[] pars) - { - string key = "【SQL参数】:"; - foreach (var param in pars) - { - key += $"{param.ParameterName}:{param.Value}\n"; - } - - return key; - } - + private void Init() { _baseRepository.Db.CodeFirst.InitTables(); _baseRepository.Db.CodeFirst.InitTables(); + _baseRepository.Db.CodeFirst.InitTables(); } + /// + /// 普通查询 例子
+ /// 没有复杂链表 主要使用导航属性
+ /// 推荐将条件拼接交给前端 后端只定义个接口就很方便 维护也很简单
+ ///
[Fact] public async void Get_Blogs_DynamicTest() { @@ -69,34 +59,100 @@ public async void Get_Blogs_DynamicTest() await TestConditions("btitle like \" 测试数据\""); await TestConditions("btitle like \"测试数据\" && bId>0"); await TestConditions("btitle like \"测试!@#$%^&*()_+|}{\":<>?LP\"数据\" && bId>0"); - await TestConditions("btitle like \"测试!@+)(*()_&%^&^$^%$IUYWIQOJVLXKZM>?Z<>??LP\"数据\" && bId>0"); + await TestConditions( + "btitle like \"测试!@+)(*()_&%^&^$^%$IUYWIQOJVLXKZM>?Z<>??LP\"数据\" && bId>0"); await TestConditions("IsDeleted == false"); await TestConditions("IsDeleted == true"); + await TestConditions("IsDeleted == true && ( btitle like \"张三\" || btitle like \"李四\" )"); + await TestConditions( + "IsDeleted == true && ( btitle like \"张三\" || btitle like \"李四\" || ( btitle StartsLike \"王五\" && btitle EndLike \"赵六\" ) )"); //导航属性 //一对一 //查询 老张的文章 - await TestConditions("User.RealName like \"老张\""); + await TestConditions("User.RealName like \"老张\""); //查询 2019年后的老张文章 - await TestConditions("User.RealName like \"老张\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + await TestConditions("User.RealName like \"老张\" && bUpdateTime>=\"2019-01-01 01:01:01\""); //一对多 //查询 评论中有"写的不错"的文章 - await TestConditions("Comments.Comment like \"写的不错\""); + await TestConditions("Comments.Comment like \"写的不错\""); //查询 2019后的 评论中有"写的不错"的文章 - await TestConditions("Comments.Comment like \"写的不错\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + await TestConditions("Comments.Comment like \"写的不错\" && bUpdateTime>=\"2019-01-01 01:01:01\""); //查询 有老张评论的文章 await TestConditions("Comments.User.LoginName like \"老张\""); } + /// + /// 复杂链表 也能使用动态条件
+ /// 存在复杂的链表 left join等 + ///
+ [Fact] + public async void Get_Blogs_DynamicJoinTest() + { + //方便前端自定义条件查询 + //语法更舒服 + var data = await _baseRepository.Query(); + _testOutputHelper.WriteLine(data.ToJson()); + + await TestJoinConditions(""); + await TestJoinConditions("bId=1"); + await TestJoinConditions("bId=2"); + await TestJoinConditions("bId in (1,2,3,4,5)"); + await TestJoinConditions("bId in (1,2,3,4,5)|| bUpdateTime>=\"2019-01-01 01:01:01\""); + await TestJoinConditions("btitle like \" 测试数据\""); + await TestJoinConditions("btitle like \"测试数据\" && bId>0"); + await TestJoinConditions("btitle like \"测试!@#$%^&*()_+|}{\":<>?LP\"数据\" && bId>0"); + await TestJoinConditions( + "btitle like \"测试!@+)(*()_&%^&^$^%$IUYWIQOJVLXKZM>?Z<>??LP\"数据\" && bId>0"); + await TestJoinConditions("IsDeleted == false"); + await TestJoinConditions("IsDeleted == true"); + await TestJoinConditions("IsDeleted == true && ( btitle like \"张三\" || btitle like \"李四\" )"); + await TestJoinConditions( + "IsDeleted == true && ( btitle like \"张三\" || btitle like \"李四\" || ( btitle StartsLike \"王五\" && btitle EndLike \"赵六\" ) )"); + + //导航属性 + + //一对一 + + //查询 老张的文章 + await TestJoinConditions("User.RealName like \"老张\""); + //查询 2019年后的老张文章 + await TestJoinConditions("User.RealName like \"老张\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + + //一对多 + + //查询 评论中有"写的不错"的文章 + await TestJoinConditions("Comments.Comment like \"写的不错\""); + //查询 2019后的 评论中有"写的不错"的文章 + await TestJoinConditions("Comments.Comment like \"写的不错\" && bUpdateTime>=\"2019-01-01 01:01:01\""); + //查询 有老张评论的文章 + await TestJoinConditions("Comments.User.LoginName like \"老张\""); + } + + private async Task TestConditions(string conditions) { var express = DynamicLinqFactory.CreateLambda(conditions); + _testOutputHelper.WriteLine(new string('=', 100)); var product = await _baseRepository.Query(express); + _testOutputHelper.WriteLine($"条件:{DynamicLinqFactory.FormatString(conditions)}\r\nLambda:{express}\r\n结果:{product.Count}"); + _testOutputHelper.WriteLine(new string('=', 100)); + } + + private async Task TestJoinConditions(string conditions) + { + var express = DynamicLinqFactory.CreateLambda(conditions); + _testOutputHelper.WriteLine(new string('=', 100)); + var product = await _baseRepository.Db.Queryable() + .LeftJoin((b, u) => Convert.ToInt64(b.bsubmitter) == u.Id) + .MergeTable() + .Where(express) + .ToListAsync(); + _testOutputHelper.WriteLine($"条件:{DynamicLinqFactory.FormatString(conditions)}\r\nLambda:{express}\r\n结果:{product.Count}"); _testOutputHelper.WriteLine(new string('=', 100)); - _testOutputHelper.WriteLine($"原条件:{conditions}\r\nLambda:{express}\r\n结果:{product.Count}"); } } \ No newline at end of file From 8b4e378a4931bb17849bb21a66698afe651b5c04 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 20 Jul 2023 12:48:49 +0800 Subject: [PATCH 204/289] feat: dm database --- Blog.Core.Api/wwwroot/BlogCore.Data.json/TopicDetail.tsv | 2 +- Blog.Core.Common/Seed/DBSeed.cs | 4 ++-- Blog.Core.Model/Models/GblLogAudit.cs | 4 ++-- Blog.Core.Model/Models/TopicDetail.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Blog.Core.Api/wwwroot/BlogCore.Data.json/TopicDetail.tsv b/Blog.Core.Api/wwwroot/BlogCore.Data.json/TopicDetail.tsv index 2cee8073..ff5f8d68 100644 --- a/Blog.Core.Api/wwwroot/BlogCore.Data.json/TopicDetail.tsv +++ b/Blog.Core.Api/wwwroot/BlogCore.Data.json/TopicDetail.tsv @@ -3,7 +3,7 @@ "TopicId": 1, "tdLogo": null, "tdName": "第一章 罗马的诞生 第一节 传说的年代", - "tdContent": "

第一节 传说的年代<\/p>

每个民族都有自己的神话传说。大概希望知道本民族的来源是个很自然的愿望吧。但这是一个难题,因为这几乎不可能用科学的方法来解释清楚。不过所有的民族都没有这样的奢求。他们只要有一个具有一定的条理性,而又能振奋其民族精神的浪漫故事就行,别抬杠,象柏杨那样将中国的三皇五帝都来个科学分析,来评论他们的执政之优劣是大可不必的。<\/p>

对於罗马人,他们有一个和特洛伊城的陷落相关的传说。<\/p>

位於小亚细亚西岸的繁荣的城市特洛伊,在遭受了阿加美农统帅的希腊联军的十年围攻之後,仍未陷落。希腊联军於是留下一个巨大的木马後假装撤兵。特洛伊人以为那是希腊联军留给自己的礼物,就将它拉入城内。<\/p>

当庆祝胜利的狂欢结束,特洛伊人满怀对明日的和平生活的希望熟睡後,藏在木马内的希腊士兵一个又一个地爬了出来。就在这天夜里,特洛伊城便在火光和叫喊中陷落了。全城遭到大屠杀 ,幸免於死的人全都沦为奴隶。混乱之中只有特洛伊国王的驸马阿伊尼阿斯带着老父,儿子等数人在女神维娜斯的帮助下成功地逃了出来。这驸马爷乃是女神维娜斯与凡人男子之间的儿子,女神维娜斯不忍心看着自己的儿子被希腊士兵屠杀 。<\/p>

这阿驸马一行人分乘几条船,离开了火光冲天的特洛伊城。在女神维娜斯的指引下,浪迹地中海,最後在意大利西岸登陆。当地的国王看上了阿伊尼阿斯并把自己的女儿嫁给了他。他又是驸马了,与他的新妻过起了幸福的生活。难民们也安定了下来。<\/p>

阿伊尼阿斯死後,跟随他逃难来的儿子继承了王位。新王在位三十年後,离开了这块地方,到台伯河(Tiber)下游建了一个新城亚尔巴龙迦城。这便是罗马城的前身了。<\/p>

罗马人自古相信罗马城是公元前731年4月21日由罗莫路和勒莫(Romulus and Remus)建设的。而这两个孪生兄弟是从特洛伊逃出的阿伊尼阿斯的子孙。後来,罗马人接触了希腊文化後才知道特洛伊的陷落是在公元前十三世纪,老早的事了。罗马人好象并没有对这段空白有任何烦恼,随手编出一串传说,把那空白给填补了。反正传说这事荒唐一点的更受欢迎。经过了一堆搞不清谁是谁的王的统治,出现了一个什麽王的公主。<\/p>

公主的叔父在篡夺了王位後,为了防止公主结婚生子威胁自己的王位,便任命未婚的公主为巫女。这是主管祭神的职位,象修女一样不得结婚。<\/p>

不巧一日这美丽的公主在祭事的空余,来到小河边午睡。也是合当有事,被过往的战神玛尔斯(Mars)一见钟情。这玛尔斯本是靠挑起战争混饭吃的,但也常勾引 良家妇女。这天战神也没错过机会,立刻由天而降,与公主一试云雨。据说战神的技术特神,公主还没来得及醒便完事升天去了。後来公主生了一双胞胎,起名罗莫路和勒莫。<\/p>

叔父闻知此事大怒,将公主投入大牢,又把那双胞胎放在篮子里抛入台伯河,指望那篮子漂入大海将那双胞胎淹死。类似的故事在旧约圣经里也有,那是关於摩西的事,好象这类传说在当地十分流行。<\/p>

再说那兄弟俩的篮子被河口附近茂密的灌木丛钩住而停了下来,俩人哭声引来的一只过路的母狼。意大利的狼都带点慈悲心,不但没吃了俩人当点心,还用自己的奶去喂他们,这才救了俩小命。<\/p>

不过,总是由狼养活也没法交 待,於是又一日一放羊的在这地盘上溜哒,发现了兄弟俩,将他们抱了回去扶养成人 。据说现在这一带仍有许多放羊的。<\/p>

兄弟俩长大後成了这一带放羊人的头,在与别的放羊人的圈子的打斗中不断地扩展自己的势力范围。圈子大了,情报也就多了,终于有一天,罗莫路和勒莫知道了自己身事。<\/p>

兄弟俩就带着手下的放羊人呼啸着去打破了亚尔巴龙迦城,杀了那国王,将王位又交 还给了自己祖父。他们的母亲似乎已经死在了大牢里。但兄弟俩也没在亚尔巴龙迦城多住,他们认为亚尔巴龙迦城位於山地,虽然易守难攻,却不利发展。加上兄弟俩是在台伯河的下游长大的,所以便回到原地,建了个新城。除了手下的放羊人又加上了附近的放羊人和农民。<\/p>

消灭了共同的敌人後,兄弟俩的关系开始恶化。有人说是为了新城的命名,有人说是为了新城的城址,也有人说是为了争夺王位。兄弟俩於是分割统治,各占一小山包。但纷争又开始了,勒莫跳过了罗莫路为表示势力范围而挖的沟。对於这种侵犯他人权力的行为,罗莫路大义灭亲地在自己兄弟的後脑上重重地来了一锄头,勒莫便被灭了。<\/p>

<\/p>

於是这城便以罗莫路的名字命名为罗马,这就是公元前731年4月21日的事了,到现在这天仍是意大利的节日,罗马人会欢天喜地的庆祝罗莫路杀了自己的…不,是庆祝罗马建城。王位当然也得由罗莫路来坐,一切问题都没了。这时四年一度的奥林匹克运动会在希腊已经开了六回,罗马也从传说的时代走出,近入了历史时代。<\/p>


<\/p>", + "tdContent": "

第一节 传说的年代<\/p>

每个民族都有自己的神话传说。大概希望知道本民族的来源是个很自然的愿望吧。但这是一个难题,因为这几乎不可能用科学的方法来解释清楚。", "tdDetail": "标题", "tdSectendDetail": null, "tdIsDelete": 0, diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index e7eb4e27..6fa8f901 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -81,7 +81,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) // 创建数据库 Console.WriteLine($"Create Database(The Db Id:{MyContext.ConnId})..."); - if (MyContext.DbType != SqlSugar.DbType.Oracle) + if (MyContext.DbType != SqlSugar.DbType.Oracle && MyContext.DbType != SqlSugar.DbType.Dm) { myContext.Db.DbMaintenance.CreateDatabase(); ConsoleHelper.WriteSuccessLine($"Database created successfully!"); @@ -89,7 +89,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) else { //Oracle 数据库不支持该操作 - ConsoleHelper.WriteSuccessLine($"Oracle 数据库不支持该操作,可手动创建Oracle数据库!"); + ConsoleHelper.WriteSuccessLine($"Oracle 数据库不支持该操作,可手动创建Oracle/Dm数据库!"); } // 创建数据库表,遍历指定命名空间下的class, diff --git a/Blog.Core.Model/Models/GblLogAudit.cs b/Blog.Core.Model/Models/GblLogAudit.cs index 2cecce8b..d4a85411 100644 --- a/Blog.Core.Model/Models/GblLogAudit.cs +++ b/Blog.Core.Model/Models/GblLogAudit.cs @@ -57,13 +57,13 @@ public class GblLogAudit ///

///错误信息 /// - [SugarColumn(ColumnDescription = "错误信息", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 5000)] + [SugarColumn(ColumnDescription = "错误信息", IsNullable = false, IsPrimaryKey = false, IsIdentity = false, Length = 2000)] public string Message { get; set; } /// ///异常 /// - [SugarColumn(ColumnDescription = "异常", IsNullable = true, IsPrimaryKey = false, IsIdentity = false, Length = 5000)] + [SugarColumn(ColumnDescription = "异常", IsNullable = true, IsPrimaryKey = false, IsIdentity = false, Length = 2000)] public string Exception { get; set; } } diff --git a/Blog.Core.Model/Models/TopicDetail.cs b/Blog.Core.Model/Models/TopicDetail.cs index 1a98f3af..6cb69c67 100644 --- a/Blog.Core.Model/Models/TopicDetail.cs +++ b/Blog.Core.Model/Models/TopicDetail.cs @@ -19,7 +19,7 @@ public TopicDetail() [SugarColumn(Length = 200, IsNullable = true)] public string tdName { get; set; } - [SugarColumn(Length = 6000, IsNullable = true)] + [SugarColumn(Length = 2000, IsNullable = true)] public string tdContent { get; set; } [SugarColumn(Length = 2000, IsNullable = true)] From e360a4bbe747942c236daa1e50e9b98c93daac0f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 20 Jul 2023 17:13:08 +0800 Subject: [PATCH 205/289] =?UTF-8?q?feat=EF=BC=9Achange=20access=20trend=20?= =?UTF-8?q?log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Api.csproj | 2 -- Blog.Core.Api/Blog.Core.Model.xml | 2 +- Blog.Core.Model/Blog.Core.Model.csproj | 2 +- Blog.Core.Model/Models/AccessTrendLog.cs | 2 +- .../QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs | 8 ++++---- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index f3f9befa..dc7680ca 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -61,8 +61,6 @@ - - diff --git a/Blog.Core.Api/Blog.Core.Model.xml b/Blog.Core.Api/Blog.Core.Model.xml index 1eb3423b..8de49a7b 100644 --- a/Blog.Core.Api/Blog.Core.Model.xml +++ b/Blog.Core.Api/Blog.Core.Model.xml @@ -200,7 +200,7 @@ 用户访问趋势日志
- + 用户 diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index ac851de2..f1e8764d 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -16,7 +16,7 @@ - + diff --git a/Blog.Core.Model/Models/AccessTrendLog.cs b/Blog.Core.Model/Models/AccessTrendLog.cs index fd6dbae7..bc4848cf 100644 --- a/Blog.Core.Model/Models/AccessTrendLog.cs +++ b/Blog.Core.Model/Models/AccessTrendLog.cs @@ -12,7 +12,7 @@ public class AccessTrendLog : RootEntityTkey /// 用户 ///
[SugarColumn(Length = 128, IsNullable = true)] - public string User { get; set; } + public string UserInfo { get; set; } /// /// 次数 diff --git a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs index 1d501c34..68a9aa53 100644 --- a/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs +++ b/Blog.Core.Tasks/QuartzNet/Jobs/Job_AccessTrendLog_Quartz.cs @@ -60,7 +60,7 @@ public async Task Run(IJobExecutionContext context) foreach (var item in activeUsers) { - var user = (await _accessTrendLogServices.Query(d => d.User != "" && d.User == item.user)).FirstOrDefault(); + var user = (await _accessTrendLogServices.Query(d => d.UserInfo != "" && d.UserInfo == item.user)).FirstOrDefault(); if (user != null) { user.Count += item.count; @@ -73,13 +73,13 @@ await _accessTrendLogServices.Add(new AccessTrendLog() { Count = item.count, UpdateTime = logUpdate, - User = item.user + UserInfo = item.user }); } } // 重新拉取 - var actUsers = await _accessTrendLogServices.Query(d => d.User != "", d => d.Count, false); + var actUsers = await _accessTrendLogServices.Query(d => d.UserInfo != "", d => d.Count, false); actUsers = actUsers.Take(15).ToList(); List activeUserVMs = new(); @@ -87,7 +87,7 @@ await _accessTrendLogServices.Add(new AccessTrendLog() { activeUserVMs.Add(new ActiveUserVM() { - user = item.User, + user = item.UserInfo, count = item.Count }); } From 3396b6b34d09ab097fd56d4aaee34cf4a1cbc363 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 21 Jul 2023 10:53:32 +0800 Subject: [PATCH 206/289] Update appsettings.json --- Blog.Core.Api/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 5fdf0000..52229055 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -152,7 +152,7 @@ "DBType": 5, "Enabled": false, "HitRate": 10, - "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" + "Connection": "Server=49.232.247.202:5236;User Id=TESTDBA;PWD=TESTDBA123654;SCHEMA=TESTDBA;" }, { "ConnId": "WMBLOG_KDBNDP", From b5ea86faa3717b91e02dc4b365f3f69077119529 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 21 Jul 2023 11:13:44 +0800 Subject: [PATCH 207/289] Update appsettings.json --- Blog.Core.Api/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 52229055..d94a7606 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -152,7 +152,7 @@ "DBType": 5, "Enabled": false, "HitRate": 10, - "Connection": "Server=49.232.247.202:5236;User Id=TESTDBA;PWD=TESTDBA123654;SCHEMA=TESTDBA;" + "Connection": "Server=xxxxx:5236;User Id=xxxxx;PWD=xxxxx;SCHEMA=TESTDBA;" }, { "ConnId": "WMBLOG_KDBNDP", From 7cf94998c294291e676dbbb85f25726fd43b0341 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Fri, 21 Jul 2023 18:03:09 +0800 Subject: [PATCH 208/289] test --- .../Systems/DynamicCodeFirstController.cs | 35 +++++++++++++ .../DB/Extension/DynamicBuildException.cs | 50 +++++++++++++++++++ Blog.Core.Model/Models/RootTkey/BaseEntity.cs | 2 + 3 files changed, 87 insertions(+) create mode 100644 Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs create mode 100644 Blog.Core.Common/DB/Extension/DynamicBuildException.cs diff --git a/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs b/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs new file mode 100644 index 00000000..e2d56377 --- /dev/null +++ b/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs @@ -0,0 +1,35 @@ +using Blog.Core.Controllers; +using Blog.Core.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SqlSugar; + +namespace Blog.Core.Api.Controllers.Systems; + +/// +/// 缓存管理 +/// +[Route("api/Systems/[controller]/[action]")] +[ApiController] +[Authorize(Permissions.Name)] +public class DynamicCodeFirstController : BaseApiController +{ + private readonly ISqlSugarClient _db; + + public DynamicCodeFirstController(ISqlSugarClient db) + { + _db = db; + } + + + /// + /// 测试建表 + /// + /// + [HttpPost] + public MessageModel TestCreateTable() + { + _db.DynamicBuilder(); + return Success(); + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/Extension/DynamicBuildException.cs b/Blog.Core.Common/DB/Extension/DynamicBuildException.cs new file mode 100644 index 00000000..6f3a1bad --- /dev/null +++ b/Blog.Core.Common/DB/Extension/DynamicBuildException.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Blog.Core.Common.Extensions; +using SqlSugar; + +namespace Blog.Core.Common.DB.Extension; + +public static class DynamicBuildException +{ + private static List GetEntityAttr(this DynamicBuilder builder) + { + FieldInfo fieldInfo = builder.GetType().GetField("entityAttr", BindingFlags.Instance | BindingFlags.NonPublic); + List entityAttr = (List) fieldInfo.GetValue(builder); + return entityAttr; + } + + private static CustomAttributeBuilder CreateIndex(SugarIndexAttribute indexAttribute) + { + Type type = typeof(SugarIndexAttribute); + return new CustomAttributeBuilder(type.GetConstructor(new[] + { + typeof(string), typeof(string), typeof(OrderByType), typeof(bool) + })!, + new object[] + { + indexAttribute.IndexName, indexAttribute.IndexFields.First().Key, indexAttribute.IndexFields.First().Value, indexAttribute.IsUnique + }, + new PropertyInfo[] + { + type.GetProperty("IndexName"), + type.GetProperty("IndexFields"), + type.GetProperty("IsUnique"), + }, + new object[] + { + indexAttribute.IndexName, indexAttribute.IndexFields, indexAttribute.IsUnique + }); + } + + public static DynamicProperyBuilder CreateIndex(this DynamicProperyBuilder builder, SugarIndexAttribute indexAttribute) + { + var classBuilder = builder.baseBuilder; + var entityAttr = classBuilder.GetEntityAttr(); + entityAttr.Add(CreateIndex(indexAttribute)); + return builder; + } +} \ No newline at end of file diff --git a/Blog.Core.Model/Models/RootTkey/BaseEntity.cs b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs index b6dabe54..5d5d4414 100644 --- a/Blog.Core.Model/Models/RootTkey/BaseEntity.cs +++ b/Blog.Core.Model/Models/RootTkey/BaseEntity.cs @@ -4,6 +4,8 @@ namespace Blog.Core.Model.Models.RootTkey; +[SugarIndex("index_{table}_Enabled", nameof(Enabled), OrderByType.Asc)] +[SugarIndex("index_{table}_IsDeleted", nameof(IsDeleted), OrderByType.Asc)] public class BaseEntity : RootEntityTkey, IDeleteFilter { #region 数据状态管理 From 667cc8fafcfe350709235cb17401c537452a7b19 Mon Sep 17 00:00:00 2001 From: LemonNoCry Date: Fri, 21 Jul 2023 20:18:51 +0800 Subject: [PATCH 209/289] test --- Blog.Core.Api/Blog.Core.xml | 23 +++++++ .../Systems/DynamicCodeFirstController.cs | 63 ++++++++++++++++++- Blog.Core.Api/appsettings.json | 30 ++++----- .../DB/Extension/DynamicBuildException.cs | 34 +++++----- 4 files changed, 117 insertions(+), 33 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 6dd7f16b..566d345a 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1391,6 +1391,29 @@
+ + + 缓存管理 + + + + + 测试建表 + + + + + + 测试查询 + + + + + + 测试写入 + + + 多租户-多库方案 测试 diff --git a/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs b/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs index e2d56377..29260f03 100644 --- a/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs +++ b/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs @@ -1,7 +1,11 @@ +using Blog.Core.Common.DB.Extension; using Blog.Core.Controllers; using Blog.Core.Model; +using Blog.Core.Model.Models.RootTkey; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NetTaste; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Math; using SqlSugar; namespace Blog.Core.Api.Controllers.Systems; @@ -21,6 +25,36 @@ public DynamicCodeFirstController(ISqlSugarClient db) _db = db; } + /// + /// 动态type + /// + /// + private Type GetDynamicType() + { + return _db.DynamicBuilder().CreateClass("DynamicTestTable") + //{table} 占位符会自动替换成表名 + .CreateIndex(new SugarIndexAttribute("idx_{table}_Code", "Code", OrderByType.Desc)) + .CreateProperty("Id", typeof(int), new SugarColumn() {IsPrimaryKey = true, IsIdentity = true}) + .CreateProperty("Code", typeof(string), new SugarColumn() {Length = 50}) + .CreateProperty("Name", typeof(string), new SugarColumn() {Length = 50}) + .WithCache() + .BuilderType(); + } + + /// + /// 动态type 继承BaseEntity + /// + /// + private Type GetDynamicType2() + { + return _db.DynamicBuilder().CreateClass("DynamicTestTable2", null, typeof(BaseEntity)) + //{table} 占位符会自动替换成表名 + .CreateIndex(new SugarIndexAttribute("idx_{table}_Code", "Code", OrderByType.Desc)) + .CreateProperty("Code", typeof(string), new SugarColumn() {Length = 50}) + .CreateProperty("Name", typeof(string), new SugarColumn() {Length = 50}) + .WithCache() + .BuilderType(); + } /// /// 测试建表 @@ -29,7 +63,34 @@ public DynamicCodeFirstController(ISqlSugarClient db) [HttpPost] public MessageModel TestCreateTable() { - _db.DynamicBuilder(); + var type = GetDynamicType(); + _db.CodeFirst.InitTables(type); + return Success(); + } + + /// + /// 测试查询 + /// + /// + [HttpGet] + public MessageModel TestQuery() + { + var type = GetDynamicType(); + return Success(_db.QueryableByObject(type).ToList()); + } + + /// + /// 测试写入 + /// + /// + [HttpPost] + public MessageModel TestInsert(string code, string name) + { + var type = GetDynamicType(); + var entity = Activator.CreateInstance(type); + type.GetProperty("Code")!.SetValue(entity, code); + type.GetProperty("Name")!.SetValue(entity, name); + _db.InsertableByObject(entity).ExecuteCommand(); return Success(); } } \ No newline at end of file diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index d94a7606..3b3b4041 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -76,13 +76,12 @@ "SvcName": "", // /svc/blog "UseLoadTest": false }, - // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; // *** 单库操作,把 MutiDBEnabled 设为false ***; // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 //Log:日志库; - "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true + "MainDB": "WMBLOG_MSSQL_1", //当前项目的主库,所对应的连接字符串的Enabled必须为true "MutiDBEnabled": true, //是否开启多库模式 "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer "DBS": [ @@ -99,23 +98,24 @@ { "ConnId": "WMBLOG_SQLITE", "DBType": 2, - "Enabled": true, + "Enabled": false, "HitRate": 50, // 值越大,优先级越高 "Connection": "WMBlog.db" //sqlite只写数据库名就行 }, { "ConnId": "Log", //日志库连接固定名称,不要改,其他的可以改 - "DBType": 2, + "DBType": 1, "Enabled": true, "HitRate": 50, // 值越大,优先级越高 - "Connection": "WMBlogLog.db" //sqlite只写数据库名就行 + "Connection": "Server=localhost;Database=BlogCoreLog;Trusted_Connection=True;", + "ProviderName": "System.Data.SqlClient" }, { "ConnId": "WMBLOG_MSSQL_1", "DBType": 1, - "Enabled": false, + "Enabled": true, "HitRate": 40, - "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", + "Connection": "Server=localhost;Database=BlogCore;Trusted_Connection=True;", "ProviderName": "System.Data.SqlClient" }, { @@ -257,15 +257,16 @@ "RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "IpWhitelist": [], //白名单 - "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], - "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], + "EndpointWhitelist": ["get:/api/xxx", "*:/api/yyy"], + "ClientWhitelist": ["dev-client-1", "dev-client-2"], "QuotaExceededResponse": { "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", "ContentType": "application/json", "StatusCode": 429 }, "HttpStatusCode": 429, //返回状态码 - "GeneralRules": [ //api规则,结尾一定要带* + "GeneralRules": [ + //api规则,结尾一定要带* { "Endpoint": "*:/api/blog*", "Period": "1m", @@ -287,7 +288,6 @@ "Limit": 500 } ] - }, "ConsulSetting": { "ServiceName": "BlogCoreService", @@ -296,7 +296,8 @@ "ServiceHealthCheck": "/healthcheck", "ConsulAddress": "http://localhost:8500" }, - "PayInfo": { //建行聚合支付信息 + "PayInfo": { + //建行聚合支付信息 "MERCHANTID": "", //商户号 "POSID": "", //柜台号 "BRANCHID": "", //分行号 @@ -306,7 +307,7 @@ "OutAddress": "http://127.0.0.1:12345" //外联地址 }, "nacos": { - "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 + "ServerAddresses": ["http://localhost:8848"], // nacos 连接地址 "DefaultTimeOut": 15000, // 默认超时时间 "Namespace": "public", // 命名空间 "ListenInterval": 10000, // 监听的频率 @@ -317,7 +318,8 @@ "LogFiedOutPutConfigs": { "tcpAddressHost": "", // 输出elk的tcp连接地址 "tcpAddressPort": 0, // 输出elk的tcp端口号 - "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 + "ConfigsInfo": [ + // 配置的输出elk节点内容 常用语动态标识 { "FiedName": "applicationName", "FiedValue": "Blog.Core.Api" diff --git a/Blog.Core.Common/DB/Extension/DynamicBuildException.cs b/Blog.Core.Common/DB/Extension/DynamicBuildException.cs index 6f3a1bad..15e638a4 100644 --- a/Blog.Core.Common/DB/Extension/DynamicBuildException.cs +++ b/Blog.Core.Common/DB/Extension/DynamicBuildException.cs @@ -20,24 +20,22 @@ private static List GetEntityAttr(this DynamicBuilder bu private static CustomAttributeBuilder CreateIndex(SugarIndexAttribute indexAttribute) { Type type = typeof(SugarIndexAttribute); - return new CustomAttributeBuilder(type.GetConstructor(new[] - { - typeof(string), typeof(string), typeof(OrderByType), typeof(bool) - })!, - new object[] - { - indexAttribute.IndexName, indexAttribute.IndexFields.First().Key, indexAttribute.IndexFields.First().Value, indexAttribute.IsUnique - }, - new PropertyInfo[] - { - type.GetProperty("IndexName"), - type.GetProperty("IndexFields"), - type.GetProperty("IsUnique"), - }, - new object[] - { - indexAttribute.IndexName, indexAttribute.IndexFields, indexAttribute.IsUnique - }); + var constructorTypes = new List() {typeof(string)}; + for (int i = 0; i < indexAttribute.IndexFields.Count; i++) + { + constructorTypes.AddRange(new[] {typeof(string), typeof(OrderByType)}); + } + + constructorTypes.Add(typeof(bool)); + + var values = new List() {indexAttribute.IndexName}; + foreach (var indexField in indexAttribute.IndexFields) + { + values.AddRange(new object[] {indexField.Key, indexField.Value}); + } + + values.Add(indexAttribute.IsUnique); + return new CustomAttributeBuilder(type.GetConstructor(constructorTypes.ToArray())!, values.ToArray()); } public static DynamicProperyBuilder CreateIndex(this DynamicProperyBuilder builder, SugarIndexAttribute indexAttribute) From 6e136fe861c01dcdf392fefbf5cd55010bf29a90 Mon Sep 17 00:00:00 2001 From: LemonNoCry Date: Fri, 21 Jul 2023 20:23:33 +0800 Subject: [PATCH 210/289] =?UTF-8?q?=E2=9C=A8=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Systems/DynamicCodeFirstController.cs | 2 +- Blog.Core.Api/appsettings.json | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs b/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs index 29260f03..37c84791 100644 --- a/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs +++ b/Blog.Core.Api/Controllers/Systems/DynamicCodeFirstController.cs @@ -11,7 +11,7 @@ namespace Blog.Core.Api.Controllers.Systems; /// -/// 缓存管理 +/// 动态建表 CURD /// [Route("api/Systems/[controller]/[action]")] [ApiController] diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 3b3b4041..d94a7606 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -76,12 +76,13 @@ "SvcName": "", // /svc/blog "UseLoadTest": false }, + // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; // *** 单库操作,把 MutiDBEnabled 设为false ***; // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 //Log:日志库; - "MainDB": "WMBLOG_MSSQL_1", //当前项目的主库,所对应的连接字符串的Enabled必须为true + "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true "MutiDBEnabled": true, //是否开启多库模式 "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer "DBS": [ @@ -98,24 +99,23 @@ { "ConnId": "WMBLOG_SQLITE", "DBType": 2, - "Enabled": false, + "Enabled": true, "HitRate": 50, // 值越大,优先级越高 "Connection": "WMBlog.db" //sqlite只写数据库名就行 }, { "ConnId": "Log", //日志库连接固定名称,不要改,其他的可以改 - "DBType": 1, + "DBType": 2, "Enabled": true, "HitRate": 50, // 值越大,优先级越高 - "Connection": "Server=localhost;Database=BlogCoreLog;Trusted_Connection=True;", - "ProviderName": "System.Data.SqlClient" + "Connection": "WMBlogLog.db" //sqlite只写数据库名就行 }, { "ConnId": "WMBLOG_MSSQL_1", "DBType": 1, - "Enabled": true, + "Enabled": false, "HitRate": 40, - "Connection": "Server=localhost;Database=BlogCore;Trusted_Connection=True;", + "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", "ProviderName": "System.Data.SqlClient" }, { @@ -257,16 +257,15 @@ "RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "IpWhitelist": [], //白名单 - "EndpointWhitelist": ["get:/api/xxx", "*:/api/yyy"], - "ClientWhitelist": ["dev-client-1", "dev-client-2"], + "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ], + "ClientWhitelist": [ "dev-client-1", "dev-client-2" ], "QuotaExceededResponse": { "Content": "{{\"status\":429,\"msg\":\"访问过于频繁,请稍后重试\",\"success\":false}}", "ContentType": "application/json", "StatusCode": 429 }, "HttpStatusCode": 429, //返回状态码 - "GeneralRules": [ - //api规则,结尾一定要带* + "GeneralRules": [ //api规则,结尾一定要带* { "Endpoint": "*:/api/blog*", "Period": "1m", @@ -288,6 +287,7 @@ "Limit": 500 } ] + }, "ConsulSetting": { "ServiceName": "BlogCoreService", @@ -296,8 +296,7 @@ "ServiceHealthCheck": "/healthcheck", "ConsulAddress": "http://localhost:8500" }, - "PayInfo": { - //建行聚合支付信息 + "PayInfo": { //建行聚合支付信息 "MERCHANTID": "", //商户号 "POSID": "", //柜台号 "BRANCHID": "", //分行号 @@ -307,7 +306,7 @@ "OutAddress": "http://127.0.0.1:12345" //外联地址 }, "nacos": { - "ServerAddresses": ["http://localhost:8848"], // nacos 连接地址 + "ServerAddresses": [ "http://localhost:8848" ], // nacos 连接地址 "DefaultTimeOut": 15000, // 默认超时时间 "Namespace": "public", // 命名空间 "ListenInterval": 10000, // 监听的频率 @@ -318,8 +317,7 @@ "LogFiedOutPutConfigs": { "tcpAddressHost": "", // 输出elk的tcp连接地址 "tcpAddressPort": 0, // 输出elk的tcp端口号 - "ConfigsInfo": [ - // 配置的输出elk节点内容 常用语动态标识 + "ConfigsInfo": [ // 配置的输出elk节点内容 常用语动态标识 { "FiedName": "applicationName", "FiedValue": "Blog.Core.Api" From 4638fc7633963980da056525c909facf1e19b37d Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Wed, 26 Jul 2023 18:15:31 +0800 Subject: [PATCH 211/289] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a62a2eb6..66ce9e7f 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,9 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 封装`Blog.Core.Webapi.Template`项目模板,一键重建自己的项目 ✨; - [x] 搭配多个前端案例供参考和借鉴:Blog.Vue、Blog.Admin、Nuxt.tbug、Blog.Mvp.Blazor ✨; - [x] 统一集成 IdentityServer4 认证 ✨; -- [x] 统一实现多租户; +- [x] 统一实现多租户; +- [x] 实现分表案例,支持分表的增删改查哈分页查询,具体查看SplitDemoController.cs; +- [x] 支持signalR对指定用户通讯; 组件模块: From a3759986993de51344e3b6431d6f627241eb2dbc Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Sat, 5 Aug 2023 12:56:46 +0800 Subject: [PATCH 212/289] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 66ce9e7f..2ab20732 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x - [x] 可配合 Jenkins 实现CI / CD; - [x] 可配合 Consul 实现服务发现; - [x] 可配合 Nacos 实现服务发现; -- [x] 可配合 Ocelot 实现网关处理; +- [x] 可配合 apisix/Ocelot 实现网关处理; - [x] 可配合 Nginx 实现负载均衡; - [x] 可配合 Ids4 实现认证中心; From 8372a3a0d4d0c618dec3c0abd38b83a16d8f8024 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 23 Aug 2023 16:13:42 +0800 Subject: [PATCH 213/289] feat: :tada: test log sql operate log --- Blog.Core.Api/Blog.Core.xml | 14 +- Blog.Core.Api/appsettings.json | 3 + Blog.Core.Common/DB/Aop/SqlsugarAop.cs | 6 +- .../HttpContextUser/AspNetUser.cs | 2 +- .../ServiceExtensions/AppConfigSetup.cs | 10 + .../AutofacModuleRegister.cs | 6 + .../ServiceExtensions/SqlsugarSetup.cs | 265 ++++++++++-------- 7 files changed, 182 insertions(+), 124 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 566d345a..667dcfdc 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -1393,9 +1393,21 @@ - 缓存管理 + 动态建表 CURD + + + 动态type + + + + + + 动态type 继承BaseEntity + + + 测试建表 diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index d94a7606..82fc206a 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -57,6 +57,9 @@ "TranAOP": { "Enabled": true }, + "UserAuditAOP": { + "Enabled": false + }, "SqlAOP": { "Enabled": true, "LogToFile": { diff --git a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs index f165ef43..826984ed 100644 --- a/Blog.Core.Common/DB/Aop/SqlsugarAop.cs +++ b/Blog.Core.Common/DB/Aop/SqlsugarAop.cs @@ -11,7 +11,7 @@ namespace Blog.Core.Common.DB.Aop; public static class SqlSugarAop { - public static void OnLogExecuting(ISqlSugarClient sqlSugarScopeProvider, string sql, SugarParameter[] p, ConnectionConfig config) + public static void OnLogExecuting(ISqlSugarClient sqlSugarScopeProvider, string user, string table, string operate, string sql, SugarParameter[] p, ConnectionConfig config) { try { @@ -25,8 +25,8 @@ public static void OnLogExecuting(ISqlSugarClient sqlSugarScopeProvider, string { using (LogContextExtension.Create.SqlAopPushProperty(sqlSugarScopeProvider)) { - Log.Information("------------------ \r\n ConnId:[{ConnId}]【SQL语句】: \r\n {Sql}", - config.ConfigId, UtilMethods.GetNativeSql( sql, p)); + Log.Information("------------------ \r\n User:[{User}] Table:[{Table}] Operate:[{Operate}] ConnId:[{ConnId}]【SQL语句】: \r\n {Sql}", + user, table, operate, config.ConfigId, UtilMethods.GetNativeSql(sql, p)); } } } diff --git a/Blog.Core.Common/HttpContextUser/AspNetUser.cs b/Blog.Core.Common/HttpContextUser/AspNetUser.cs index 1ceaa45f..dfa87949 100644 --- a/Blog.Core.Common/HttpContextUser/AspNetUser.cs +++ b/Blog.Core.Common/HttpContextUser/AspNetUser.cs @@ -48,7 +48,7 @@ private string GetName() public bool IsAuthenticated() { - return _accessor.HttpContext.User.Identity.IsAuthenticated; + return _accessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false; } diff --git a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs index d7a1f90d..680a1912 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs @@ -79,6 +79,15 @@ public static void AddAppConfigSetup(this IServiceCollection services, IHostEnvi { ConsoleHelper.WriteSuccessLine($"Transaction AOP: True"); } + // 审计AOP + if (!AppSettings.app(new string[] { "AppSettings", "UserAuditAOP", "Enabled" }).ObjToBool()) + { + Console.WriteLine($"UserAudit AOP: False"); + } + else + { + ConsoleHelper.WriteSuccessLine($"UserAudit AOP: True"); + } // 数据库Sql执行AOP if (!AppSettings.app(new string[] { "AppSettings", "SqlAOP", "OutToLogFile", "Enabled" }).ObjToBool()) @@ -251,6 +260,7 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos new string[] { "缓存AOP", AppSettings.app("AppSettings", "CachingAOP", "Enabled") }, new string[] { "服务日志AOP", AppSettings.app("AppSettings", "LogAOP", "Enabled") }, new string[] { "事务AOP", AppSettings.app("AppSettings", "TranAOP", "Enabled") }, + new string[] { "服务审计AOP", AppSettings.app("AppSettings", "UserAuditAOP", "Enabled") }, new string[] { "Sql执行AOP", AppSettings.app("AppSettings", "SqlAOP", "Enabled") }, new string[] { "Sql执行AOP控制台输出", AppSettings.app("AppSettings", "SqlAOP", "LogToConsole", "Enabled") }, }; diff --git a/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs b/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs index 4836c402..4236bfb7 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AutofacModuleRegister.cs @@ -57,6 +57,12 @@ protected override void Load(ContainerBuilder builder) cacheType.Add(typeof(BlogLogAOP)); } + if (AppSettings.app(new string[] { "AppSettings", "UserAuditAOP", "Enabled" }).ObjToBool()) + { + builder.RegisterType(); + cacheType.Add(typeof(BlogUserAuditAOP)); + } + builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>)).InstancePerDependency(); //注册仓储 builder.RegisterGeneric(typeof(BaseServices<>)).As(typeof(IBaseServices<>)).InstancePerDependency(); //注册服务 diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index dd5c02b3..9cf99036 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -11,126 +11,153 @@ using System.Collections.Generic; using System.Threading.Tasks; using Blog.Core.Common.Caches; +using Blog.Core.Common.Core; +using Blog.Core.Common.HttpContextUser; +using static Grpc.Core.ChannelOption; +using System.Text.RegularExpressions; namespace Blog.Core.Extensions { - /// - /// SqlSugar 启动服务 - /// - public static class SqlsugarSetup - { - private static readonly MemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); - - public static void AddSqlsugarSetup(this IServiceCollection services) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - // 默认添加主数据库连接 - MainDb.CurrentDbConnId = AppSettings.app(new string[] {"MainDB"}); - - BaseDBConfig.MutiConnectionString.slaveDbs.ForEach(s => - { - BaseDBConfig.AllSlaveConfigs.Add(new SlaveConnectionConfig() - { - HitRate = s.HitRate, - ConnectionString = s.Connection - }); - }); - - BaseDBConfig.MutiConnectionString.allDbs.ForEach(m => - { - var config = new ConnectionConfig() - { - ConfigId = m.ConnId.ObjToString().ToLower(), - ConnectionString = m.Connection, - DbType = (DbType) m.DbType, - IsAutoCloseConnection = true, - // Check out more information: https://github.com/anjoy8/Blog.Core/issues/122 - //IsShardSameThread = false, - MoreSettings = new ConnMoreSettings() - { - //IsWithNoLockQuery = true, - IsAutoRemoveDataCache = true, - SqlServerCodeFirstNvarchar = true, - }, - // 从库 - SlaveConnectionConfigs = BaseDBConfig.AllSlaveConfigs, - // 自定义特性 - ConfigureExternalServices = new ConfigureExternalServices() - { - DataInfoCacheService = new SqlSugarCacheService(), - EntityService = (property, column) => - { - if (column.IsPrimarykey && property.PropertyType == typeof(int)) - { - column.IsIdentity = true; - } - } - }, - InitKeyType = InitKeyType.Attribute - }; - if (SqlSugarConst.LogConfigId.ToLower().Equals(m.ConnId.ToLower())) - { - BaseDBConfig.LogConfig = config; - } - else - { - BaseDBConfig.ValidConfig.Add(config); - } - - BaseDBConfig.AllConfigs.Add(config); - }); - - if (BaseDBConfig.LogConfig is null) - { - throw new ApplicationException("未配置Log库连接"); - } - - // SqlSugarScope是线程安全,可使用单例注入 - // 参考:https://www.donet5.com/Home/Doc?typeId=1181 - services.AddSingleton(o => - { - return new SqlSugarScope(BaseDBConfig.AllConfigs, db => - { - BaseDBConfig.ValidConfig.ForEach(config => - { - var dbProvider = db.GetConnectionScope((string) config.ConfigId); - - // 打印SQL语句 - dbProvider.Aop.OnLogExecuting = (s, parameters) => - SqlSugarAop.OnLogExecuting(dbProvider, s, parameters, config); - - // 数据审计 - dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; - - // 配置实体假删除过滤器 - RepositorySetting.SetDeletedEntityFilter(dbProvider); - // 配置实体数据权限 - RepositorySetting.SetTenantEntityFilter(dbProvider); - }); - }); - }); - } - - private static string GetWholeSql(SugarParameter[] paramArr, string sql) - { - foreach (var param in paramArr) - { - sql.Replace(param.ParameterName, param.Value.ObjToString()); - } - - return sql; - } - - private static string GetParas(SugarParameter[] pars) - { - string key = "【SQL参数】:"; - foreach (var param in pars) - { - key += $"{param.ParameterName}:{param.Value}\n"; - } - - return key; - } - } + /// + /// SqlSugar 启动服务 + /// + public static class SqlsugarSetup + { + private static readonly MemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); + + public static void AddSqlsugarSetup(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + // 默认添加主数据库连接 + MainDb.CurrentDbConnId = AppSettings.app(new string[] { "MainDB" }); + + BaseDBConfig.MutiConnectionString.slaveDbs.ForEach(s => + { + BaseDBConfig.AllSlaveConfigs.Add(new SlaveConnectionConfig() + { + HitRate = s.HitRate, + ConnectionString = s.Connection + }); + }); + + BaseDBConfig.MutiConnectionString.allDbs.ForEach(m => + { + var config = new ConnectionConfig() + { + ConfigId = m.ConnId.ObjToString().ToLower(), + ConnectionString = m.Connection, + DbType = (DbType)m.DbType, + IsAutoCloseConnection = true, + // Check out more information: https://github.com/anjoy8/Blog.Core/issues/122 + //IsShardSameThread = false, + MoreSettings = new ConnMoreSettings() + { + //IsWithNoLockQuery = true, + IsAutoRemoveDataCache = true, + SqlServerCodeFirstNvarchar = true, + }, + // 从库 + SlaveConnectionConfigs = BaseDBConfig.AllSlaveConfigs, + // 自定义特性 + ConfigureExternalServices = new ConfigureExternalServices() + { + DataInfoCacheService = new SqlSugarCacheService(), + EntityService = (property, column) => + { + if (column.IsPrimarykey && property.PropertyType == typeof(int)) + { + column.IsIdentity = true; + } + } + }, + InitKeyType = InitKeyType.Attribute + }; + if (SqlSugarConst.LogConfigId.ToLower().Equals(m.ConnId.ToLower())) + { + BaseDBConfig.LogConfig = config; + } + else + { + BaseDBConfig.ValidConfig.Add(config); + } + + BaseDBConfig.AllConfigs.Add(config); + }); + + if (BaseDBConfig.LogConfig is null) + { + throw new ApplicationException("未配置Log库连接"); + } + + // SqlSugarScope是线程安全,可使用单例注入 + // 参考:https://www.donet5.com/Home/Doc?typeId=1181 + services.AddSingleton(o => + { + return new SqlSugarScope(BaseDBConfig.AllConfigs, db => + { + BaseDBConfig.ValidConfig.ForEach(config => + { + var dbProvider = db.GetConnectionScope((string)config.ConfigId); + + // 打印SQL语句 + dbProvider.Aop.OnLogExecuting = (s, parameters) => + { + var user = InternalApp.RootServices.GetService(); + SqlSugarAop.OnLogExecuting(dbProvider, user?.Name.ObjToString(), ExtractTableName(s), Enum.GetName(typeof(SugarActionType), dbProvider.SugarActionType), s, parameters, config); + }; + + // 数据审计 + dbProvider.Aop.DataExecuting = SqlSugarAop.DataExecuting; + + // 配置实体假删除过滤器 + RepositorySetting.SetDeletedEntityFilter(dbProvider); + // 配置实体数据权限 + RepositorySetting.SetTenantEntityFilter(dbProvider); + }); + }); + }); + } + + private static string GetWholeSql(SugarParameter[] paramArr, string sql) + { + foreach (var param in paramArr) + { + sql.Replace(param.ParameterName, param.Value.ObjToString()); + } + + return sql; + } + + private static string GetParas(SugarParameter[] pars) + { + string key = "【SQL参数】:"; + foreach (var param in pars) + { + key += $"{param.ParameterName}:{param.Value}\n"; + } + + return key; + } + + private static string ExtractTableName(string sql) + { + // 匹配 SQL 语句中的表名的正则表达式 + //string regexPattern = @"\s*(?:UPDATE|DELETE\s+FROM|SELECT\s+\*\s+FROM)\s+(\w+)"; + string regexPattern = @"(?i)(?:FROM|UPDATE|DELETE\s+FROM)\s+`(.+?)`"; + Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase); + Match match = regex.Match(sql); + + if (match.Success) + { + // 提取匹配到的表名 + return match.Groups[1].Value; + } + else + { + // 如果没有匹配到表名,则返回空字符串或者抛出异常等处理 + return string.Empty; + } + } + } } \ No newline at end of file From 6d0a3dfc092307fcb0273a0de319076dd6dcc00c Mon Sep 17 00:00:00 2001 From: LemonNoCry Date: Wed, 30 Aug 2023 19:40:54 +0800 Subject: [PATCH 214/289] =?UTF-8?q?=F0=9F=90=9B=F0=9F=90=9B=F0=9F=90=9B=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DRootService=E5=86=85=E5=AD=98=E6=BA=A2?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/App.cs | 29 ++++++++++++------- Blog.Core.Common/Core/InternalApp.cs | 13 +++++---- .../ServiceExtensions/SqlsugarSetup.cs | 3 +- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Blog.Core.Common/App.cs b/Blog.Core.Common/App.cs index 07abee9f..ecdb1b49 100644 --- a/Blog.Core.Common/App.cs +++ b/Blog.Core.Common/App.cs @@ -39,6 +39,7 @@ public static bool IsRun /// 有效程序集类型 public static readonly IEnumerable EffectiveTypes; + /// 优先使用App.GetService()手动获取服务 public static IServiceProvider RootServices => IsRun || IsBuild ? InternalApp.RootServices : null; /// 获取Web主机环境,如,是否是开发环境,生产环境等 @@ -55,14 +56,16 @@ public static bool IsRun /// public static HttpContext HttpContext => RootServices?.GetService()?.HttpContext; - public static IUser User => HttpContext == null ? null : RootServices?.GetService(); + public static IUser User => GetService(); #region Service /// 解析服务提供器 /// + /// + /// /// - public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBuild = false) + public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBuild = false, bool throwException = true) { if (App.HostEnvironment == null || App.RootServices != null && InternalApp.InternalServices @@ -71,25 +74,31 @@ public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBui (serviceType.IsGenericType ? serviceType.GetGenericTypeDefinition() : serviceType))) .Any((u => u.Lifetime == ServiceLifetime.Singleton))) return App.RootServices; - HttpContext httpContext = App.HttpContext; - if (httpContext?.RequestServices != null) - return httpContext.RequestServices; + + //获取请求生存周期的服务 + if (HttpContext?.RequestServices != null) + return HttpContext.RequestServices; + if (App.RootServices != null) { - IServiceScope scope = App.RootServices.CreateScope(); + IServiceScope scope = RootServices.CreateScope(); return scope.ServiceProvider; } if (mustBuild) { - throw new ApplicationException("当前不可用,必须要等到 WebApplication Build后"); + if (throwException) + { + throw new ApplicationException("当前不可用,必须要等到 WebApplication Build后"); + } + + return default; } ServiceProvider serviceProvider = InternalApp.InternalServices.BuildServiceProvider(); return serviceProvider; } - public static TService GetService(bool mustBuild = true) where TService : class => App.GetService(typeof(TService), null, mustBuild) as TService; @@ -99,7 +108,7 @@ public static TService GetService(bool mustBuild = true) where TServic /// /// public static TService GetService(IServiceProvider serviceProvider, bool mustBuild = true) - where TService : class => App.GetService(typeof(TService), serviceProvider, mustBuild) as TService; + where TService : class => (serviceProvider ?? App.GetServiceProvider(typeof(TService), mustBuild, false))?.GetService(); /// 获取请求生存周期的服务 /// @@ -107,7 +116,7 @@ public static TService GetService(IServiceProvider serviceProvider, bo /// /// public static object GetService(Type type, IServiceProvider serviceProvider = null, bool mustBuild = true) => - (serviceProvider ?? App.GetServiceProvider(type, mustBuild)).GetService(type); + (serviceProvider ?? App.GetServiceProvider(type, mustBuild, false))?.GetService(type); #endregion diff --git a/Blog.Core.Common/Core/InternalApp.cs b/Blog.Core.Common/Core/InternalApp.cs index df16c911..b8a7736a 100644 --- a/Blog.Core.Common/Core/InternalApp.cs +++ b/Blog.Core.Common/Core/InternalApp.cs @@ -7,21 +7,24 @@ namespace Blog.Core.Common.Core; +/// +/// 内部只用于初始化使用 +/// public static class InternalApp { - public static IServiceCollection InternalServices; + internal static IServiceCollection InternalServices; /// 根服务 - public static IServiceProvider RootServices; + internal static IServiceProvider RootServices; /// 获取Web主机环境 - public static IWebHostEnvironment WebHostEnvironment; + internal static IWebHostEnvironment WebHostEnvironment; /// 获取泛型主机环境 - public static IHostEnvironment HostEnvironment; + internal static IHostEnvironment HostEnvironment; /// 配置对象 - public static IConfiguration Configuration; + internal static IConfiguration Configuration; public static void ConfigureApplication(this WebApplicationBuilder wab) { diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 9cf99036..6574e70e 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -103,8 +103,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) // 打印SQL语句 dbProvider.Aop.OnLogExecuting = (s, parameters) => { - var user = InternalApp.RootServices.GetService(); - SqlSugarAop.OnLogExecuting(dbProvider, user?.Name.ObjToString(), ExtractTableName(s), Enum.GetName(typeof(SugarActionType), dbProvider.SugarActionType), s, parameters, config); + SqlSugarAop.OnLogExecuting(dbProvider, App.User?.Name.ObjToString(), ExtractTableName(s), Enum.GetName(typeof(SugarActionType), dbProvider.SugarActionType), s, parameters, config); }; // 数据审计 From 39fc0ab4e8699e4285f75c3774949c7f1cb5fd4c Mon Sep 17 00:00:00 2001 From: LemonNoCry Date: Wed, 30 Aug 2023 19:42:56 +0800 Subject: [PATCH 215/289] =?UTF-8?q?=F0=9F=8E=A8=20=E8=B0=83=E6=95=B4DbSeed?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/Seed/DBSeed.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 6fa8f901..a70bf847 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -177,11 +177,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Permission"), Encoding.UTF8), setting); - foreach (var item in data) - { - Console.WriteLine($"{item.Name}:{item.Id}"); - myContext.GetEntityDB().Insert(item); - } + myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Permission created success!"); } else @@ -218,11 +214,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) { var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "RoleModulePermission"), Encoding.UTF8), setting); - foreach (var item in data) - { - Console.WriteLine($"{item.Id}"); - myContext.GetEntityDB().Insert(item); - } + myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:RoleModulePermission created success!"); } else From 1e9865cfef1adf5db59a3be4980e1bf36cec91a0 Mon Sep 17 00:00:00 2001 From: LemonNoCry Date: Thu, 31 Aug 2023 21:59:39 +0800 Subject: [PATCH 216/289] =?UTF-8?q?=F0=9F=8E=A8=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Test/SqlsugarTestController.cs | 29 +++++++++++++++++++ .../ServiceExtensions/SqlsugarSetup.cs | 1 + 2 files changed, 30 insertions(+) create mode 100644 Blog.Core.Api/Controllers/Test/SqlsugarTestController.cs diff --git a/Blog.Core.Api/Controllers/Test/SqlsugarTestController.cs b/Blog.Core.Api/Controllers/Test/SqlsugarTestController.cs new file mode 100644 index 00000000..774a9b12 --- /dev/null +++ b/Blog.Core.Api/Controllers/Test/SqlsugarTestController.cs @@ -0,0 +1,29 @@ +using Blog.Core.Common; +using Blog.Core.Controllers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SqlSugar; + +namespace Blog.Core.Api.Controllers.Test; + +[Route("api/[Controller]/[Action]")] +[AllowAnonymous] +public class SqlsugarTestController : BaseApiController +{ + private readonly SqlSugarScope _db; + + public SqlsugarTestController(SqlSugarScope db) + { + _db = db; + } + + [HttpGet] + public async Task Get() + { + Console.WriteLine(App.HttpContext.Request.Path); + Console.WriteLine(App.HttpContext.RequestServices.ToString()); + Console.WriteLine(App.User?.ID); + await Task.CompletedTask; + return Ok(); + } +} \ No newline at end of file diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 6574e70e..6e8a148c 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -116,6 +116,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) }); }); }); + services.AddTransient(s => s.GetService() as SqlSugarScope); } private static string GetWholeSql(SugarParameter[] paramArr, string sql) From dfa067d21446409a83abd9a71f76afb0cd0757bd Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 18 Oct 2023 09:55:54 +0800 Subject: [PATCH 217/289] =?UTF-8?q?=F0=9F=90=9B=20HttpClient=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8D=95=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/ExpressionExtensions_Nacos.cs | 38 ++++++++++--------- Blog.Core.Common/Helper/HttpHelper.cs | 20 +++++----- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs b/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs index c187ee6a..705d4c15 100644 --- a/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs +++ b/Blog.Core.Common/Extensions/ExpressionExtensions_Nacos.cs @@ -15,9 +15,8 @@ public static class ExpressionExtensions_Nacos { #region Nacos NamingService - private static readonly HttpClient httpclient = new HttpClient(); - - private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl) + private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string ServiceName, string Group, + string apiurl) { try { @@ -45,7 +44,8 @@ private static string GetServiceUrl(Nacos.V2.INacosNamingService serv, string Se return ""; } - public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters = null) + public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService serv, string ServiceName, + string Group, string apiurl, Dictionary Parameters = null) { try { @@ -62,8 +62,9 @@ public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService url = $"{url}?{sb.ToString().Trim('&')}"; } - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.GetAsync(url); + HttpHelper.Httpclient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + var result = await HttpHelper.Httpclient.GetAsync(url); return await result.Content.ReadAsStringAsync(); } catch (Exception e) @@ -74,7 +75,8 @@ public static async Task Cof_NaoceGet(this Nacos.V2.INacosNamingService return ""; } - public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) + public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingService serv, string ServiceName, + string Group, string apiurl, Dictionary Parameters) { try { @@ -82,8 +84,8 @@ public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingSer if (string.IsNullOrEmpty(url)) return ""; var content = (Parameters != null && Parameters.Any()) ? new FormUrlEncodedContent(Parameters) : null; - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, content); + HttpHelper.Httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await HttpHelper.Httpclient.PostAsync(url, content); return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); } catch (Exception e) @@ -94,14 +96,16 @@ public static async Task Cof_NaocePostForm(this Nacos.V2.INacosNamingSer return ""; } - public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, string jSonData) + public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingService serv, string ServiceName, + string Group, string apiurl, string jSonData) { try { var url = GetServiceUrl(serv, ServiceName, Group, apiurl); if (string.IsNullOrEmpty(url)) return ""; - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, new StringContent(jSonData, Encoding.UTF8, "application/json")); + HttpHelper.Httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = + await HttpHelper.Httpclient.PostAsync(url, new StringContent(jSonData, Encoding.UTF8, "application/json")); return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); //httpClient.BaseAddress = new Uri("https://www.testapi.com"); @@ -116,7 +120,8 @@ public static async Task Cof_NaocePostJson(this Nacos.V2.INacosNamingSer return ""; } - public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingService serv, string ServiceName, string Group, string apiurl, Dictionary Parameters) + public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingService serv, string ServiceName, + string Group, string apiurl, Dictionary Parameters) { try { @@ -129,8 +134,8 @@ public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingSer content.Add(new ByteArrayContent(pitem.Value), "files", pitem.Key); } - httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var result = await httpclient.PostAsync(url, content); + HttpHelper.Httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var result = await HttpHelper.Httpclient.PostAsync(url, content); return await result.Content.ReadAsStringAsync(); //.GetAwaiter().GetResult(); } catch (Exception e) @@ -144,5 +149,4 @@ public static async Task Cof_NaocePostFile(this Nacos.V2.INacosNamingSer #endregion } - -} +} \ No newline at end of file diff --git a/Blog.Core.Common/Helper/HttpHelper.cs b/Blog.Core.Common/Helper/HttpHelper.cs index 4bd14883..82f9c32f 100644 --- a/Blog.Core.Common/Helper/HttpHelper.cs +++ b/Blog.Core.Common/Helper/HttpHelper.cs @@ -10,21 +10,23 @@ namespace Blog.Core.Common.Helper /// public class HttpHelper { + public static readonly HttpClient Httpclient = new HttpClient(); + public static async Task GetAsync(string serviceAddress) { try { string result = string.Empty; Uri getUrl = new Uri(serviceAddress); - using var httpClient = new HttpClient(); - httpClient.Timeout = new TimeSpan(0, 0, 60); - result = await httpClient.GetAsync(serviceAddress).Result.Content.ReadAsStringAsync(); + Httpclient.Timeout = new TimeSpan(0, 0, 60); + result = await Httpclient.GetAsync(serviceAddress).Result.Content.ReadAsStringAsync(); return result; } catch (Exception e) { Console.WriteLine(e.Message); } + return null; } @@ -38,19 +40,19 @@ public static async Task PostAsync(string serviceAddress, string request using (HttpContent httpContent = new StringContent(requestJson)) { httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - using var httpClient = new HttpClient(); - httpClient.Timeout = new TimeSpan(0, 0, 60); - result = await httpClient.PostAsync(serviceAddress, httpContent).Result.Content.ReadAsStringAsync(); + + Httpclient.Timeout = new TimeSpan(0, 0, 60); + result = await Httpclient.PostAsync(serviceAddress, httpContent).Result.Content.ReadAsStringAsync(); } + return result; } catch (Exception e) { Console.WriteLine(e.Message); } + return null; } } - - -} +} \ No newline at end of file From 0901de2fbfd8d8128a4ecf3f8b27daf2054c35c2 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Thu, 19 Oct 2023 16:28:20 +0800 Subject: [PATCH 218/289] =?UTF-8?q?=F0=9F=8E=A8=E2=9C=A8=F0=9F=8E=89=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8E=9F=E6=9C=89=E7=9A=84DBS=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E3=80=81=E6=96=B0=E5=A2=9E=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=95=85=E9=9A=9C=E8=BD=AC=E7=A7=BB=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.优化原有的DBS配置,破坏性修改,原有的DBS配置在多库和读写分离无法兼容,配置写法不是合适,故此优化 2.新增数据库故障转移方案,例如主库挂了自动切换到备用库,备用库不会由程序维护,需要运维、dba去做数据库同步方案,比如Sqlserver事务日志传输等 故障转移方案兼容多种方式 1.数据库主从方案 在配置主从之后,需要将从库配置为备用链接就行了 一般就是:修改、写入、删除走主库,查询操作走从库,在主库挂了后则所有操作走从库 2.数据库主备方案 日常使用主数据库操作,备用库只是备份,只有主库挂了才会用备用库 从库和备库都属于slave库功能 --- .../Controllers/DbFirst/DbFirstController.cs | 26 +-- .../Controllers/MonitorController.cs | 2 +- Blog.Core.Api/appsettings.json | 51 ++++-- Blog.Core.Common/DB/Aop/SqlSugarReuse.cs | 23 +++ Blog.Core.Common/DB/BaseDBConfig.cs | 102 +++++------ .../DB/Extension/DbEntityException.cs | 14 ++ Blog.Core.Common/DB/MainDb.cs | 2 +- Blog.Core.Common/Helper/UtilConvert.cs | 8 + Blog.Core.Common/Seed/DBSeed.cs | 94 +++++----- .../ServiceExtensions/AppConfigSetup.cs | 16 +- .../ServiceExtensions/SqlsugarSetup.cs | 39 ++-- .../IDS4Db/IApplicationUserServices.cs | 4 +- Blog.Core.Repository/BASE/BaseRepository.cs | 21 +-- .../IDS4Db/ApplicationUserServices.cs | 11 +- Blog.Core.Tests/Blog.Core.Tests.csproj | 15 +- Blog.Core.Tests/appsettings.json | 169 ++++++++++++------ 16 files changed, 363 insertions(+), 234 deletions(-) create mode 100644 Blog.Core.Common/DB/Aop/SqlSugarReuse.cs create mode 100644 Blog.Core.Common/DB/Extension/DbEntityException.cs diff --git a/Blog.Core.Api/Controllers/DbFirst/DbFirstController.cs b/Blog.Core.Api/Controllers/DbFirst/DbFirstController.cs index 4f51856d..83b0beb5 100644 --- a/Blog.Core.Api/Controllers/DbFirst/DbFirstController.cs +++ b/Blog.Core.Api/Controllers/DbFirst/DbFirstController.cs @@ -36,19 +36,19 @@ public MessageModel GetFrameFiles() { var data = new MessageModel() { success = true, msg = "" }; data.response += @"file path is:C:\my-file\}"; - var isMuti = AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool(); + var isMuti = BaseDBConfig.IsMulti; if (Env.IsDevelopment()) { data.response += $"Controller层生成:{FrameSeed.CreateControllers(_sqlSugarClient)} || "; - BaseDBConfig.MutiConnectionString.allDbs.ToList().ForEach(m => + BaseDBConfig.ValidConfig.ForEach(m => { - _sqlSugarClient.ChangeDatabase(m.ConnId.ToLower()); - data.response += $"库{m.ConnId}-Model层生成:{FrameSeed.CreateModels(_sqlSugarClient, m.ConnId, isMuti)} || "; - data.response += $"库{m.ConnId}-IRepositorys层生成:{FrameSeed.CreateIRepositorys(_sqlSugarClient, m.ConnId, isMuti)} || "; - data.response += $"库{m.ConnId}-IServices层生成:{FrameSeed.CreateIServices(_sqlSugarClient, m.ConnId, isMuti)} || "; - data.response += $"库{m.ConnId}-Repository层生成:{FrameSeed.CreateRepository(_sqlSugarClient, m.ConnId, isMuti)} || "; - data.response += $"库{m.ConnId}-Services层生成:{FrameSeed.CreateServices(_sqlSugarClient, m.ConnId, isMuti)} || "; + _sqlSugarClient.ChangeDatabase(m.ConfigId.ToLower()); + data.response += $"库{m.ConfigId}-Model层生成:{FrameSeed.CreateModels(_sqlSugarClient, m.ConfigId, isMuti)} || "; + data.response += $"库{m.ConfigId}-IRepositorys层生成:{FrameSeed.CreateIRepositorys(_sqlSugarClient, m.ConfigId, isMuti)} || "; + data.response += $"库{m.ConfigId}-IServices层生成:{FrameSeed.CreateIServices(_sqlSugarClient, m.ConfigId, isMuti)} || "; + data.response += $"库{m.ConfigId}-Repository层生成:{FrameSeed.CreateRepository(_sqlSugarClient, m.ConfigId, isMuti)} || "; + data.response += $"库{m.ConfigId}-Services层生成:{FrameSeed.CreateServices(_sqlSugarClient, m.ConfigId, isMuti)} || "; }); // 切回主库 @@ -74,7 +74,7 @@ public MessageModel GetFrameFilesByTableNames([FromBody]string[] tableNa { ConnID = ConnID == null ? MainDb.CurrentDbConnId.ToLower() : ConnID; - var isMuti = AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool(); + var isMuti = BaseDBConfig.IsMulti; var data = new MessageModel() { success = true, msg = "" }; if (Env.IsDevelopment()) { @@ -102,7 +102,7 @@ public MessageModel GetFrameFilesByTableNamesForEntity([FromBody] string { ConnID = ConnID == null ? MainDb.CurrentDbConnId.ToLower() : ConnID; - var isMuti = AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool(); + var isMuti = BaseDBConfig.IsMulti; var data = new MessageModel() { success = true, msg = "" }; if (Env.IsDevelopment()) { @@ -112,7 +112,7 @@ public MessageModel GetFrameFilesByTableNamesForEntity([FromBody] string { data.success = false; data.msg = "当前不处于开发模式,代码生成不可用!"; - } + } return data; } /// @@ -126,7 +126,7 @@ public MessageModel GetFrameFilesByTableNamesForController([FromBody] st { ConnID = ConnID == null ? MainDb.CurrentDbConnId.ToLower() : ConnID; - var isMuti = AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool(); + var isMuti = BaseDBConfig.IsMulti; var data = new MessageModel() { success = true, msg = "" }; if (Env.IsDevelopment()) { @@ -151,7 +151,7 @@ public MessageModel GetAllFrameFilesByTableNames([FromBody]string[] tabl { ConnID = ConnID == null ? MainDb.CurrentDbConnId.ToLower() : ConnID; - var isMuti = AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool(); + var isMuti = BaseDBConfig.IsMulti; var data = new MessageModel() { success = true, msg = "" }; if (Env.IsDevelopment()) { diff --git a/Blog.Core.Api/Controllers/MonitorController.cs b/Blog.Core.Api/Controllers/MonitorController.cs index 77b4e5bf..9ed96672 100644 --- a/Blog.Core.Api/Controllers/MonitorController.cs +++ b/Blog.Core.Api/Controllers/MonitorController.cs @@ -219,7 +219,7 @@ public async Task> GetIds4Users() { List apiDates = new List(); - if (AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) + if (_applicationUserServices.IsEnable()) { var users = await _applicationUserServices.Query(d => d.tdIsDelete == false); diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 82fc206a..f91f6df8 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -80,14 +80,17 @@ "UseLoadTest": false }, - // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; - // *** 单库操作,把 MutiDBEnabled 设为false ***; - // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; - // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 - //Log:日志库; - "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true - "MutiDBEnabled": true, //是否开启多库模式 - "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer + //优化DB配置、不会再区分单库多库 + //MainDb:标识当前项目的主库,所对应的连接字符串的Enabled必须为true + //Log:标识日志库,所对应的连接字符串的Enabled必须为true + //从库只需配置Slaves数组,要求数据库类型一致!,比如都是SqlServer + // + //新增,故障转移方案 + //如果主库挂了,会自动切换到备用连接(比如说主库+备用库) + //备用连接的ConnId配置为主库的ConnId+数字即可,比如主库的ConnId为Main,那么备用连接的ConnId为Mian1 + //主库、备用库无需数据库类型一致! + //备用库不会有程序维护,需要手动维护 + "MainDB": "Main", //当前项目的主库,所对应的连接字符串的Enabled必须为true "DBS": [ /* 对应下边的 DBType @@ -100,24 +103,40 @@ Kdbndp = 6,//人大金仓 */ { - "ConnId": "WMBLOG_SQLITE", + "ConnId": "Main", "DBType": 2, "Enabled": true, - "HitRate": 50, // 值越大,优先级越高 - "Connection": "WMBlog.db" //sqlite只写数据库名就行 + "Connection": "WMBlog.db", //sqlite只写数据库名就行 + "Slaves": [ + { + "HitRate": 0,// 值越大,优先级越高 0不使用 + "Connection": "WMBlog2.db" + } + ] + }, + { + "ConnId": "Main2", + "DBType": 2, + "Enabled": true, + "Connection": "WMBlog3.db", //sqlite只写数据库名就行 + "Slaves": [ + { + "HitRate": 0,// 值越大,优先级越高 0不使用 + "Connection": "WMBlog4.db" + } + ] }, { "ConnId": "Log", //日志库连接固定名称,不要改,其他的可以改 "DBType": 2, "Enabled": true, - "HitRate": 50, // 值越大,优先级越高 + "HitRate": 50, "Connection": "WMBlogLog.db" //sqlite只写数据库名就行 }, { "ConnId": "WMBLOG_MSSQL_1", "DBType": 1, "Enabled": false, - "HitRate": 40, "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", "ProviderName": "System.Data.SqlClient" }, @@ -125,7 +144,6 @@ "ConnId": "WMBLOG_MSSQL_2", "DBType": 1, "Enabled": false, - "HitRate": 30, "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", "ProviderName": "System.Data.SqlClient" }, @@ -133,35 +151,30 @@ "ConnId": "WMBLOG_MYSQL", "DBType": 0, "Enabled": false, - "HitRate": 20, "Connection": "server=localhost;Database=blog;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" }, { "ConnId": "WMBLOG_MYSQL_2", "DBType": 0, "Enabled": false, - "HitRate": 20, "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" }, { "ConnId": "WMBLOG_ORACLE", "DBType": 3, "Enabled": false, - "HitRate": 10, "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" }, { "ConnId": "WMBLOG_DM", "DBType": 5, "Enabled": false, - "HitRate": 10, "Connection": "Server=xxxxx:5236;User Id=xxxxx;PWD=xxxxx;SCHEMA=TESTDBA;" }, { "ConnId": "WMBLOG_KDBNDP", "DBType": 6, "Enabled": false, - "HitRate": 10, "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" } ], diff --git a/Blog.Core.Common/DB/Aop/SqlSugarReuse.cs b/Blog.Core.Common/DB/Aop/SqlSugarReuse.cs new file mode 100644 index 00000000..949af640 --- /dev/null +++ b/Blog.Core.Common/DB/Aop/SqlSugarReuse.cs @@ -0,0 +1,23 @@ +using System.Linq; +using SqlSugar; + +namespace Blog.Core.Common.DB.Aop; + +public class SqlSugarReuse +{ + public static void AutoChangeAvailableConnect(SqlSugarClient db) + { + if (db == null) return; + if (db.Ado.IsValidConnection()) return; + if (!BaseDBConfig.ReuseConfigs.Any()) return; + + foreach (var connectionConfig in BaseDBConfig.ReuseConfigs) + { + var config = db.CurrentConnectionConfig.ConfigId; + db.ChangeDatabase(connectionConfig.ConfigId); + //移除旧的连接,只会在本次上下文移除,因为主库已经故障会导致多库事务无法使用 + db.RemoveConnection(config); + if (db.Ado.IsValidConnection()) return; + } + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/BaseDBConfig.cs b/Blog.Core.Common/DB/BaseDBConfig.cs index b7153766..7eb74fb8 100644 --- a/Blog.Core.Common/DB/BaseDBConfig.cs +++ b/Blog.Core.Common/DB/BaseDBConfig.cs @@ -1,24 +1,46 @@ -using SqlSugar; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using SqlSugar; namespace Blog.Core.Common.DB { public class BaseDBConfig { - public static readonly List AllConfigs = new(); //所有库配置 - public static readonly List AllSlaveConfigs = new(); //从库配置 - public static List ValidConfig = new(); //有效的库连接(除去Log库) - public static ConnectionConfig LogConfig; //日志库 - + /// + /// 所有库配置 + /// + public static readonly List AllConfigs = new(); + + /// + /// 主库的备用连接配置 + /// + public static readonly List ReuseConfigs = new(); + + /// + /// 有效的库连接(除去Log库) + /// + public static List ValidConfig = new(); + + public static ConnectionConfig MainConfig; + public static ConnectionConfig LogConfig; //日志库 + + public static bool IsMulti => ValidConfig.Count > 1; + /* 之前的单库操作已经删除,如果想要之前的代码,可以查看我的GitHub的历史记录 * 目前是多库操作,默认加载的是appsettings.json设置为true的第一个db连接。 + * + * 优化配置连接 + * 老的配置方式,再多库和从库中有些冲突 + * 直接在单个配置中可以配置从库 + * + * 新增故障转移方案 + * 增加主库备用连接,配置方式为ConfigId为主库的ConfigId+随便数字 只要不重复就好 + * + * 主库在无法连接后会自动切换到备用链接 */ public static (List allDbs, List slaveDbs) MutiConnectionString => MutiInitConn(); - - private static string DifDBConnOfSecurity(params string[] conn) { @@ -44,52 +66,13 @@ public static (List, List) MutiInitConn() { List listdatabase = AppSettings.app("DBS") .Where(i => i.Enabled).ToList(); - var mainDbId = AppSettings.app(new string[] { "MainDB" }).ObjToString(); + var mainDbId = AppSettings.app(new string[] {"MainDB"}).ObjToString(); var mainDbModel = listdatabase.Single(d => d.ConnId == mainDbId); listdatabase.Remove(mainDbModel); listdatabase.Insert(0, mainDbModel); - foreach (var i in listdatabase) - { - SpecialDbString(i); - } - - List listdatabaseSimpleDB = new List(); //单库 - List listdatabaseSlaveDB = new List(); //从库 - - // 单库,且不开启读写分离,只保留一个 - if (!AppSettings.app(new string[] {"CQRSEnabled"}).ObjToBool() && !AppSettings.app(new string[] {"MutiDBEnabled"}).ObjToBool()) - { - if (listdatabase.Count == 1) - { - return (listdatabase, listdatabaseSlaveDB); - } - else - { - var dbFirst = listdatabase.FirstOrDefault(d => d.ConnId == AppSettings.app(new string[] {"MainDB"}).ObjToString()); - if (dbFirst == null) - { - dbFirst = listdatabase.FirstOrDefault(); - } - - listdatabaseSimpleDB.Add(dbFirst); - return (listdatabaseSimpleDB, listdatabaseSlaveDB); - } - } - - - // 读写分离,且必须是单库模式,获取从库 - if (AppSettings.app(new string[] {"CQRSEnabled"}).ObjToBool() && !AppSettings.app(new string[] {"MutiDBEnabled"}).ObjToBool()) - { - if (listdatabase.Count > 1) - { - listdatabaseSlaveDB = listdatabase.Where(d => d.ConnId != AppSettings.app(new string[] {"MainDB"}).ObjToString()).ToList(); - } - } - - - return (listdatabase, listdatabaseSlaveDB); - //} + foreach (var i in listdatabase) SpecialDbString(i); + return (listdatabase, mainDbModel.Slaves); } /// @@ -102,19 +85,23 @@ private static MutiDBOperate SpecialDbString(MutiDBOperate mutiDBOperate) { if (mutiDBOperate.DbType == DataBaseType.Sqlite) { - mutiDBOperate.Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, mutiDBOperate.Connection); + mutiDBOperate.Connection = + $"DataSource=" + Path.Combine(Environment.CurrentDirectory, mutiDBOperate.Connection); } else if (mutiDBOperate.DbType == DataBaseType.SqlServer) { - mutiDBOperate.Connection = DifDBConnOfSecurity(@"D:\my-file\dbCountPsw1_SqlserverConn.txt", mutiDBOperate.Connection); + mutiDBOperate.Connection = DifDBConnOfSecurity(@"D:\my-file\dbCountPsw1_SqlserverConn.txt", + mutiDBOperate.Connection); } else if (mutiDBOperate.DbType == DataBaseType.MySql) { - mutiDBOperate.Connection = DifDBConnOfSecurity(@"D:\my-file\dbCountPsw1_MySqlConn.txt", mutiDBOperate.Connection); + mutiDBOperate.Connection = + DifDBConnOfSecurity(@"D:\my-file\dbCountPsw1_MySqlConn.txt", mutiDBOperate.Connection); } else if (mutiDBOperate.DbType == DataBaseType.Oracle) { - mutiDBOperate.Connection = DifDBConnOfSecurity(@"D:\my-file\dbCountPsw1_OracleConn.txt", mutiDBOperate.Connection); + mutiDBOperate.Connection = + DifDBConnOfSecurity(@"D:\my-file\dbCountPsw1_OracleConn.txt", mutiDBOperate.Connection); } return mutiDBOperate; @@ -159,5 +146,10 @@ public class MutiDBOperate /// 数据库类型 /// public DataBaseType DbType { get; set; } + + /// + /// 从库 + /// + public List Slaves { get; set; } } } \ No newline at end of file diff --git a/Blog.Core.Common/DB/Extension/DbEntityException.cs b/Blog.Core.Common/DB/Extension/DbEntityException.cs new file mode 100644 index 00000000..6d06f291 --- /dev/null +++ b/Blog.Core.Common/DB/Extension/DbEntityException.cs @@ -0,0 +1,14 @@ +using System; +using System.Reflection; +using SqlSugar; + +namespace Blog.Core.Common.DB.Extension; + +public static class DbEntityException +{ + public static object GetEntityTenant(this Type type) + { + var tenant = type.GetCustomAttribute(); + return tenant?.configId; + } +} \ No newline at end of file diff --git a/Blog.Core.Common/DB/MainDb.cs b/Blog.Core.Common/DB/MainDb.cs index f132b710..3de863f3 100644 --- a/Blog.Core.Common/DB/MainDb.cs +++ b/Blog.Core.Common/DB/MainDb.cs @@ -2,6 +2,6 @@ { public static class MainDb { - public static string CurrentDbConnId = "1"; + public static string CurrentDbConnId = "Main"; } } diff --git a/Blog.Core.Common/Helper/UtilConvert.cs b/Blog.Core.Common/Helper/UtilConvert.cs index ea3b7c05..530c4fd2 100644 --- a/Blog.Core.Common/Helper/UtilConvert.cs +++ b/Blog.Core.Common/Helper/UtilConvert.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; namespace Blog.Core @@ -288,5 +289,12 @@ public static string ToJson(this object value) { return JsonConvert.SerializeObject(value); } + + public static bool AnyNoException(this ICollection source) + { + if (source == null) return false; + + return source.Any() && source.All(s => s != null); + } } } \ No newline at end of file diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index a70bf847..082b9b20 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -41,40 +41,33 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) SeedDataFolder = Path.Combine(WebRootPath, SeedDataFolder); Console.WriteLine("************ Blog.Core DataBase Set *****************"); - Console.WriteLine($"Is multi-DataBase: {AppSettings.app(new string[] { "MutiDBEnabled" })}"); - Console.WriteLine($"Is CQRS: {AppSettings.app(new string[] { "CQRSEnabled" })}"); - Console.WriteLine(); Console.WriteLine($"Master DB ConId: {myContext.Db.CurrentConnectionConfig.ConfigId}"); Console.WriteLine($"Master DB Type: {myContext.Db.CurrentConnectionConfig.DbType}"); Console.WriteLine($"Master DB ConnectString: {myContext.Db.CurrentConnectionConfig.ConnectionString}"); Console.WriteLine(); - if (AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) + if (BaseDBConfig.MainConfig.SlaveConnectionConfigs.AnyNoException()) { - var slaveIndex = 0; - BaseDBConfig.MutiConnectionString.allDbs.Where(x => x.ConnId != MainDb.CurrentDbConnId).ToList().ForEach(m => + var index = 0; + BaseDBConfig.MainConfig.SlaveConnectionConfigs.ForEach(m => { - slaveIndex++; - Console.WriteLine($"Slave{slaveIndex} DB ID: {m.ConnId}"); - Console.WriteLine($"Slave{slaveIndex} DB Type: {m.DbType}"); - Console.WriteLine($"Slave{slaveIndex} DB ConnectString: {m.Connection}"); + index++; + Console.WriteLine($"Slave{index} DB HitRate: {m.HitRate}"); + Console.WriteLine($"Slave{index} DB ConnectString: {m.ConnectionString}"); Console.WriteLine($"--------------------------------------"); }); } - else if (AppSettings.app(new string[] { "CQRSEnabled" }).ObjToBool()) + else if (BaseDBConfig.ReuseConfigs.AnyNoException()) { - var slaveIndex = 0; - BaseDBConfig.MutiConnectionString.slaveDbs.Where(x => x.ConnId != MainDb.CurrentDbConnId).ToList().ForEach(m => + var index = 0; + BaseDBConfig.ReuseConfigs.ForEach(m => { - slaveIndex++; - Console.WriteLine($"Slave{slaveIndex} DB ID: {m.ConnId}"); - Console.WriteLine($"Slave{slaveIndex} DB Type: {m.DbType}"); - Console.WriteLine($"Slave{slaveIndex} DB ConnectString: {m.Connection}"); + index++; + Console.WriteLine($"Reuse{index} DB ID: {m.ConfigId}"); + Console.WriteLine($"Reuse{index} DB Type: {m.DbType}"); + Console.WriteLine($"Reuse{index} DB ConnectString: {m.ConnectionString}"); Console.WriteLine($"--------------------------------------"); }); } - else - { - } Console.WriteLine(); @@ -97,7 +90,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) Console.WriteLine("Create Tables..."); var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; - var referencedAssemblies = System.IO.Directory.GetFiles(path, "Blog.Core.Model.dll").Select(Assembly.LoadFrom).ToArray(); + var referencedAssemblies = System.IO.Directory.GetFiles(path, "Blog.Core.Model.dll") + .Select(Assembly.LoadFrom).ToArray(); var modelTypes = referencedAssemblies .SelectMany(a => a.DefinedTypes) .Select(type => type.AsType()) @@ -117,7 +111,7 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) ConsoleHelper.WriteSuccessLine($"Tables created successfully!"); Console.WriteLine(); - if (AppSettings.app(new string[] { "AppSettings", "SeedDBDataEnabled" }).ObjToBool()) + if (AppSettings.app(new string[] {"AppSettings", "SeedDBDataEnabled"}).ObjToBool()) { JsonSerializerSettings setting = new JsonSerializerSettings(); JsonConvert.DefaultSettings = new Func(() => @@ -143,7 +137,9 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - myContext.GetEntityDB().InsertRange(JsonHelper.ParseFormByJson>(FileHelper.ReadFile(string.Format(SeedDataFolder, "BlogArticle"), Encoding.UTF8))); + myContext.GetEntityDB().InsertRange( + JsonHelper.ParseFormByJson>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "BlogArticle"), Encoding.UTF8))); Console.WriteLine("Table:BlogArticle created success!"); } else @@ -158,7 +154,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Modules"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "Modules"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Modules created success!"); @@ -175,7 +172,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Permission"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "Permission"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Permission created success!"); @@ -192,7 +190,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "Role"), Encoding.UTF8), setting); //using var stream = new FileStream(Path.Combine(WebRootPath, "BlogCore.Data.excel", "Role.xlsx"), FileMode.Open); //var result = await importer.Import(stream); //var data = result.Data.ToList(); @@ -212,7 +211,9 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "RoleModulePermission"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "RoleModulePermission"), Encoding.UTF8), + setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:RoleModulePermission created success!"); @@ -229,7 +230,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Topic"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "Topic"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Topic created success!"); @@ -246,7 +248,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TopicDetail"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "TopicDetail"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:TopicDetail created success!"); @@ -263,7 +266,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "UserRole"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:UserRole created success!"); @@ -280,7 +284,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "sysUserInfo"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:sysUserInfo created success!"); @@ -297,7 +302,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TasksQz"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "TasksQz"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:TasksQz created success!"); @@ -326,7 +332,8 @@ public static async Task SeedAsync(MyContext myContext, string WebRootPath) if (!await myContext.Db.Queryable().AnyAsync()) { - var data = JsonConvert.DeserializeObject>(FileHelper.ReadFile(string.Format(SeedDataFolder, "Department"), Encoding.UTF8), setting); + var data = JsonConvert.DeserializeObject>( + FileHelper.ReadFile(string.Format(SeedDataFolder, "Department"), Encoding.UTF8), setting); myContext.GetEntityDB().InsertRange(data); Console.WriteLine("Table:Department created success!"); @@ -367,7 +374,8 @@ private static async Task SeedDataAsync(ISqlSugarClient db) .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass) .Where(u => { - var esd = u.GetInterfaces().FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); + var esd = u.GetInterfaces() + .FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); if (esd is null) { return false; @@ -441,11 +449,13 @@ public static void MigrationLogs(MyContext myContext) logDb.DbMaintenance.CreateDatabase(); ConsoleHelper.WriteSuccessLine($"Log Database created successfully!"); var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; - var referencedAssemblies = System.IO.Directory.GetFiles(path, "Blog.Core.Model.dll").Select(Assembly.LoadFrom).ToArray(); + var referencedAssemblies = System.IO.Directory.GetFiles(path, "Blog.Core.Model.dll") + .Select(Assembly.LoadFrom).ToArray(); var modelTypes = referencedAssemblies .SelectMany(a => a.DefinedTypes) .Select(type => type.AsType()) - .Where(x => x.IsClass && x.Namespace != null && x.Namespace.StartsWith("Blog.Core.Model.Logs")).ToList(); + .Where(x => x.IsClass && x.Namespace != null && x.Namespace.StartsWith("Blog.Core.Model.Logs")) + .ToList(); Stopwatch sw = Stopwatch.StartNew(); var tables = logDb.DbMaintenance.GetTableInfoList(); @@ -482,7 +492,8 @@ public static void MigrationLogs(MyContext myContext) /// public static async Task TenantSeedAsync(MyContext myContext) { - var tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Db).ToListAsync(); + var tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Db) + .ToListAsync(); if (tenants.Any()) { Console.WriteLine($@"Init Multi Tenant Db"); @@ -493,7 +504,8 @@ public static async Task TenantSeedAsync(MyContext myContext) } } - tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Tables).ToListAsync(); + tenants = await myContext.Db.Queryable().Where(s => s.TenantType == TenantTypeEnum.Tables) + .ToListAsync(); if (tenants.Any()) { await InitTenantSeedAsync(myContext, tenants); @@ -526,7 +538,8 @@ private static async Task InitTenantSeedAsync(MyContext myContext, List !u.IsInterface && !u.IsAbstract && u.IsClass) .Where(u => { - var esd = u.GetInterfaces().FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); + var esd = u.GetInterfaces() + .FirstOrDefault(i => i.HasImplementedRawGeneric(typeof(IEntitySeedData<>))); if (esd is null) { return false; diff --git a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs index 680a1912..e5e9f339 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Text; +using Blog.Core.Common.DB; namespace Blog.Core.Extensions { @@ -189,18 +190,8 @@ public static void AddAppConfigSetup(this IServiceCollection services, IHostEnvi ConsoleHelper.WriteSuccessLine($"EventBus: True"); } - // 多库 - if (!AppSettings.app(new string[] { "MutiDBEnabled" }).ObjToBool()) - { - Console.WriteLine($"Is multi-DataBase: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Is multi-DataBase: True"); - } - // 读写分离 - if (!AppSettings.app(new string[] { "CQRSEnabled" }).ObjToBool()) + if (!BaseDBConfig.MainConfig.SlaveConnectionConfigs.AnyNoException()) { Console.WriteLine($"Is CQRS: False"); } @@ -235,8 +226,7 @@ public static void AddAppTableConfigSetup(this IServiceCollection services, IHos new string[] { "RabbitMQ消息列队", AppSettings.app("RabbitMQ", "Enabled") }, new string[] { "事件总线(必须开启消息列队)", AppSettings.app("EventBus", "Enabled") }, new string[] { "redis消息队列", AppSettings.app("Startup", "RedisMq", "Enabled") }, - new string[] { "是否多库", AppSettings.app("MutiDBEnabled") }, - new string[] { "读写分离", AppSettings.app("CQRSEnabled") }, + new string[] { "读写分离", BaseDBConfig.MainConfig.SlaveConnectionConfigs.AnyNoException()? "True" : "False" }, }; new ConsoleTable() diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 6574e70e..82f74378 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -9,6 +9,7 @@ using StackExchange.Profiling; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Blog.Core.Common.Caches; using Blog.Core.Common.Core; @@ -30,16 +31,10 @@ public static void AddSqlsugarSetup(this IServiceCollection services) if (services == null) throw new ArgumentNullException(nameof(services)); // 默认添加主数据库连接 - MainDb.CurrentDbConnId = AppSettings.app(new string[] { "MainDB" }); - - BaseDBConfig.MutiConnectionString.slaveDbs.ForEach(s => + if (!AppSettings.app("MainDB").IsNullOrEmpty()) { - BaseDBConfig.AllSlaveConfigs.Add(new SlaveConnectionConfig() - { - HitRate = s.HitRate, - ConnectionString = s.Connection - }); - }); + MainDb.CurrentDbConnId = AppSettings.app("MainDB"); + } BaseDBConfig.MutiConnectionString.allDbs.ForEach(m => { @@ -47,7 +42,7 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { ConfigId = m.ConnId.ObjToString().ToLower(), ConnectionString = m.Connection, - DbType = (DbType)m.DbType, + DbType = (DbType) m.DbType, IsAutoCloseConnection = true, // Check out more information: https://github.com/anjoy8/Blog.Core/issues/122 //IsShardSameThread = false, @@ -58,7 +53,11 @@ public static void AddSqlsugarSetup(this IServiceCollection services) SqlServerCodeFirstNvarchar = true, }, // 从库 - SlaveConnectionConfigs = BaseDBConfig.AllSlaveConfigs, + SlaveConnectionConfigs = m.Slaves?.Where(s => s.HitRate > 0).Select(s => new SlaveConnectionConfig + { + ConnectionString = s.Connection, + HitRate = s.HitRate + }).ToList(), // 自定义特性 ConfigureExternalServices = new ConfigureExternalServices() { @@ -79,6 +78,16 @@ public static void AddSqlsugarSetup(this IServiceCollection services) } else { + if (string.Equals(SqlSugarConst.LogConfigId, MainDb.CurrentDbConnId, + StringComparison.CurrentCultureIgnoreCase)) + { + BaseDBConfig.MainConfig = config; + } + + //复用连接 + if (m.ConnId.ToLower().StartsWith(SqlSugarConst.LogConfigId.ToLower())) + BaseDBConfig.ReuseConfigs.Add(config); + BaseDBConfig.ValidConfig.Add(config); } @@ -98,12 +107,14 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { BaseDBConfig.ValidConfig.ForEach(config => { - var dbProvider = db.GetConnectionScope((string)config.ConfigId); + var dbProvider = db.GetConnectionScope((string) config.ConfigId); // 打印SQL语句 dbProvider.Aop.OnLogExecuting = (s, parameters) => { - SqlSugarAop.OnLogExecuting(dbProvider, App.User?.Name.ObjToString(), ExtractTableName(s), Enum.GetName(typeof(SugarActionType), dbProvider.SugarActionType), s, parameters, config); + SqlSugarAop.OnLogExecuting(dbProvider, App.User?.Name.ObjToString(), ExtractTableName(s), + Enum.GetName(typeof(SugarActionType), dbProvider.SugarActionType), s, parameters, + config); }; // 数据审计 @@ -114,6 +125,8 @@ public static void AddSqlsugarSetup(this IServiceCollection services) // 配置实体数据权限 RepositorySetting.SetTenantEntityFilter(dbProvider); }); + //故障转移,检查主库链接自动切换备用连接 + SqlSugarReuse.AutoChangeAvailableConnect(db); }); }); } diff --git a/Blog.Core.IServices/IDS4Db/IApplicationUserServices.cs b/Blog.Core.IServices/IDS4Db/IApplicationUserServices.cs index c6d784ff..4be6a876 100644 --- a/Blog.Core.IServices/IDS4Db/IApplicationUserServices.cs +++ b/Blog.Core.IServices/IDS4Db/IApplicationUserServices.cs @@ -1,9 +1,11 @@ -using Blog.Core.IServices.BASE; +using System.Threading.Tasks; +using Blog.Core.IServices.BASE; using Blog.Core.Model.IDS4DbModels; namespace Blog.Core.IServices { public partial interface IApplicationUserServices : IBaseServices { + bool IsEnable(); } } \ No newline at end of file diff --git a/Blog.Core.Repository/BASE/BaseRepository.cs b/Blog.Core.Repository/BASE/BaseRepository.cs index 9d926ee4..3a1c1ee8 100644 --- a/Blog.Core.Repository/BASE/BaseRepository.cs +++ b/Blog.Core.Repository/BASE/BaseRepository.cs @@ -27,21 +27,14 @@ private ISqlSugarClient _db { ISqlSugarClient db = _dbBase; - /* 如果要开启多库支持, - * 1、在appsettings.json 中开启MutiDBEnabled节点为true,必填 - * 2、设置一个主连接的数据库ID,节点MainDB,对应的连接字符串的Enabled也必须true,必填 - */ - if (AppSettings.app(new[] { "MutiDBEnabled" }).ObjToBool()) + //修改使用 model备注字段作为切换数据库条件,使用sqlsugar TenantAttribute存放数据库ConnId + //参考 https://www.donet5.com/Home/Doc?typeId=2246 + var tenantAttr = typeof(TEntity).GetCustomAttribute(); + if (tenantAttr != null) { - //修改使用 model备注字段作为切换数据库条件,使用sqlsugar TenantAttribute存放数据库ConnId - //参考 https://www.donet5.com/Home/Doc?typeId=2246 - var tenantAttr = typeof(TEntity).GetCustomAttribute(); - if (tenantAttr != null) - { - //统一处理 configId 小写 - db = _dbBase.GetConnectionScope(tenantAttr.configId.ToString().ToLower()); - return db; - } + //统一处理 configId 小写 + db = _dbBase.GetConnectionScope(tenantAttr.configId.ToString().ToLower()); + return db; } //多租户 diff --git a/Blog.Core.Services/IDS4Db/ApplicationUserServices.cs b/Blog.Core.Services/IDS4Db/ApplicationUserServices.cs index 1cda7d36..bfaf5dfc 100644 --- a/Blog.Core.Services/IDS4Db/ApplicationUserServices.cs +++ b/Blog.Core.Services/IDS4Db/ApplicationUserServices.cs @@ -1,4 +1,7 @@ -using Blog.Core.IRepository.Base; +using System.Threading.Tasks; +using Blog.Core.Common.DB; +using Blog.Core.Common.DB.Extension; +using Blog.Core.IRepository.Base; using Blog.Core.Model.IDS4DbModels; using Blog.Core.Services.BASE; @@ -6,6 +9,10 @@ namespace Blog.Core.IServices { public class ApplicationUserServices : BaseServices, IApplicationUserServices { - + public bool IsEnable() + { + var configId = typeof(ApplicationUser).GetEntityTenant(); + return Db.AsTenant().IsAnyConnection(configId); + } } } \ No newline at end of file diff --git a/Blog.Core.Tests/Blog.Core.Tests.csproj b/Blog.Core.Tests/Blog.Core.Tests.csproj index 2ceef163..5619bea8 100644 --- a/Blog.Core.Tests/Blog.Core.Tests.csproj +++ b/Blog.Core.Tests/Blog.Core.Tests.csproj @@ -11,13 +11,6 @@ - - - Always - PreserveNewest - - - @@ -38,6 +31,14 @@ + + + true + Always + PreserveNewest + + + diff --git a/Blog.Core.Tests/appsettings.json b/Blog.Core.Tests/appsettings.json index 8c0305c7..f91f6df8 100644 --- a/Blog.Core.Tests/appsettings.json +++ b/Blog.Core.Tests/appsettings.json @@ -1,30 +1,26 @@ { "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉 - "Logging": { - "LogLevel": { - "Default": "Information", //加入Default否则log4net本地写入不了日志 - "Blog.Core.AuthHelper.ApiResponseHandler": "Error" - }, - "Debug": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } - }, - "Console": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "Microsoft.Hosting.Lifetime": "Debug" + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Information", + "Microsoft.AspNetCore": "Warning", + "System": "Warning", + "System.Net.Http.HttpClient": "Warning", + "Hangfire": "Information", + "Magicodes": "Warning", + "DotNetCore.CAP": "Information", + "Savorboard.CAP": "Information", + "Quartz": "Information" } - }, - "Log4Net": { - "Name": "Blog.Core" } }, "AllowedHosts": "*", "Redis": { - "ConnectionString": "127.0.0.1:6319,password=admin" + "Enable": false, + "ConnectionString": "127.0.0.1:6379", + "InstanceName": "" //前缀 }, "RabbitMQ": { "Enabled": false, @@ -48,24 +44,34 @@ "CachingAOP": { "Enabled": true }, + "LogToDb": true, "LogAOP": { - "Enabled": false + "Enabled": false, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + } }, "TranAOP": { + "Enabled": true + }, + "UserAuditAOP": { "Enabled": false }, "SqlAOP": { "Enabled": true, - "OutToLogFile": { - "Enabled": false + "LogToFile": { + "Enabled": true }, - "OutToConsole": { + "LogToDB": { + "Enabled": true + }, + "LogToConsole": { "Enabled": true } }, - "LogToDb": { - "Enabled": true - }, "Date": "2018-08-28", "SeedDBEnabled": true, //只生成表结构 "SeedDBDataEnabled": true, //生成表,并初始化数据 @@ -74,14 +80,17 @@ "UseLoadTest": false }, - // 请配置MainDB为你想要的主库的ConnId值,并设置对应的Enabled为true; - // *** 单库操作,把 MutiDBEnabled 设为false ***; - // *** 多库操作,把 MutiDBEnabled 设为true,其他的从库Enabled也为true **; - // 具体配置看视频:https://www.bilibili.com/video/BV1BJ411B7mn?p=6 - - "MainDB": "WMBLOG_SQLITE", //当前项目的主库,所对应的连接字符串的Enabled必须为true - "MutiDBEnabled": false, //是否开启多库模式 - "CQRSEnabled": false, //是否开启读写分离模式,必须是单库模式,且数据库类型一致,比如都是SqlServer + //优化DB配置、不会再区分单库多库 + //MainDb:标识当前项目的主库,所对应的连接字符串的Enabled必须为true + //Log:标识日志库,所对应的连接字符串的Enabled必须为true + //从库只需配置Slaves数组,要求数据库类型一致!,比如都是SqlServer + // + //新增,故障转移方案 + //如果主库挂了,会自动切换到备用连接(比如说主库+备用库) + //备用连接的ConnId配置为主库的ConnId+数字即可,比如主库的ConnId为Main,那么备用连接的ConnId为Mian1 + //主库、备用库无需数据库类型一致! + //备用库不会有程序维护,需要手动维护 + "MainDB": "Main", //当前项目的主库,所对应的连接字符串的Enabled必须为true "DBS": [ /* 对应下边的 DBType @@ -94,17 +103,40 @@ Kdbndp = 6,//人大金仓 */ { - "ConnId": "WMBLOG_SQLITE", + "ConnId": "Main", "DBType": 2, "Enabled": true, - "HitRate": 50, // 值越大,优先级越高 - "Connection": "WMBlog.db" //sqlite只写数据库名就行 + "Connection": "WMBlog.db", //sqlite只写数据库名就行 + "Slaves": [ + { + "HitRate": 0,// 值越大,优先级越高 0不使用 + "Connection": "WMBlog2.db" + } + ] + }, + { + "ConnId": "Main2", + "DBType": 2, + "Enabled": true, + "Connection": "WMBlog3.db", //sqlite只写数据库名就行 + "Slaves": [ + { + "HitRate": 0,// 值越大,优先级越高 0不使用 + "Connection": "WMBlog4.db" + } + ] + }, + { + "ConnId": "Log", //日志库连接固定名称,不要改,其他的可以改 + "DBType": 2, + "Enabled": true, + "HitRate": 50, + "Connection": "WMBlogLog.db" //sqlite只写数据库名就行 }, { "ConnId": "WMBLOG_MSSQL_1", "DBType": 1, "Enabled": false, - "HitRate": 40, "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_1;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", "ProviderName": "System.Data.SqlClient" }, @@ -112,7 +144,6 @@ "ConnId": "WMBLOG_MSSQL_2", "DBType": 1, "Enabled": false, - "HitRate": 30, "Connection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=WMBLOG_MSSQL_2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False", "ProviderName": "System.Data.SqlClient" }, @@ -120,35 +151,30 @@ "ConnId": "WMBLOG_MYSQL", "DBType": 0, "Enabled": false, - "HitRate": 20, - "Connection": "server=.;Database=ddd;Uid=root;Pwd=123456;Port=10060;Allow User Variables=True;" + "Connection": "server=localhost;Database=blog;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" }, { "ConnId": "WMBLOG_MYSQL_2", "DBType": 0, - "Enabled": true, - "HitRate": 20, - "Connection": "server=.;Database=blogcore001;Uid=root;Pwd=123456;Port=3096;Allow User Variables=True;" + "Enabled": false, + "Connection": "server=localhost;Database=blogcore001;Uid=root;Pwd=root;Port=3306;Allow User Variables=True;" }, { "ConnId": "WMBLOG_ORACLE", "DBType": 3, "Enabled": false, - "HitRate": 10, "Connection": "Data Source=127.0.0.1/ops;User ID=OPS;Password=123456;Persist Security Info=True;Connection Timeout=60;" }, { "ConnId": "WMBLOG_DM", "DBType": 5, "Enabled": false, - "HitRate": 10, - "Connection": "PORT=5236;DATABASE=DAMENG;HOST=localhost;PASSWORD=SYSDBA;USER ID=SYSDBA;" + "Connection": "Server=xxxxx:5236;User Id=xxxxx;PWD=xxxxx;SCHEMA=TESTDBA;" }, { "ConnId": "WMBLOG_KDBNDP", "DBType": 6, - "Enabled": true, - "HitRate": 10, + "Enabled": false, "Connection": "Server=127.0.0.1;Port=54321;UID=SYSTEM;PWD=system;database=SQLSUGAR4XTEST1;" } ], @@ -163,12 +189,13 @@ "Database": "BlogCoreDb" }, "Startup": { + "Domain": "http://localhost:9291", "Cors": { "PolicyName": "CorsIpAccess", //策略名称 "EnableAllIPs": false, //当为true时,开放所有IP均可访问。 // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 - "IPs": "http://127.0.0.1:2364,http://localhost:2364" + "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://127.0.0.1:6688,http://localhost:6688" }, "AppConfigAlert": { "Enabled": true @@ -179,6 +206,12 @@ "AuthorizationUrl": "http://localhost:5004", // 认证中心域名 "ApiName": "blog.core.api" // 资源服务器 }, + "Authing": { + "Enabled": false, + "Issuer": "https://uldr24esx31h-demo.authing.cn/oidc", + "Audience": "63d51c4205c2849803be5178", + "JwksUri": "https://uldr24esx31h-demo.authing.cn/oidc/.well-known/jwks.json" + }, "RedisMq": { "Enabled": false //redis 消息队列 }, @@ -191,17 +224,38 @@ }, "Middleware": { "RequestResponseLog": { - "Enabled": false + "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + } }, "IPLog": { - "Enabled": true + "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + } }, "RecordAccessLogs": { "Enabled": true, + "LogToFile": { + "Enabled": true + }, + "LogToDB": { + "Enabled": true + }, "IgnoreApis": "/api/permission/getnavigationbar,/api/monitor/getids4users,/api/monitor/getaccesslogs,/api/monitor/server,/api/monitor/getactiveusers,/api/monitor/server," }, "SignalR": { - "Enabled": false + "Enabled": true + }, + "SignalRSendLog": { + "Enabled": true }, "QuartzNetJob": { "Enabled": true @@ -285,5 +339,10 @@ "FiedValue": "Blog.Core.Api" } ] + }, + "Seq": { + "Enabled": true, + "Address": "http://localhost:5341/", + "ApiKey": "" } -} +} \ No newline at end of file From f87b33a4a80ecc817eba410f79e52c563bcb7fc2 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 25 Oct 2023 11:20:51 +0800 Subject: [PATCH 219/289] =?UTF-8?q?=F0=9F=90=9B=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 82f74378..eb012394 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -78,14 +78,14 @@ public static void AddSqlsugarSetup(this IServiceCollection services) } else { - if (string.Equals(SqlSugarConst.LogConfigId, MainDb.CurrentDbConnId, + if (string.Equals(config.ConfigId, MainDb.CurrentDbConnId, StringComparison.CurrentCultureIgnoreCase)) { BaseDBConfig.MainConfig = config; } //复用连接 - if (m.ConnId.ToLower().StartsWith(SqlSugarConst.LogConfigId.ToLower())) + if (m.ConnId.ToLower().StartsWith(MainDb.CurrentDbConnId.ToLower())) BaseDBConfig.ReuseConfigs.Add(config); BaseDBConfig.ValidConfig.Add(config); From f6cee97f899852f34008424b4c50d7b762d62d55 Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Fri, 27 Oct 2023 15:25:37 +0800 Subject: [PATCH 220/289] Update sysUserInfo.cs --- Blog.Core.Model/Models/sysUserInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index 574ea311..1137d91c 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -61,7 +61,7 @@ public SysUserInfo(string loginName, string loginPWD) /// 部门 /// [SugarColumn(IsNullable = true)] - public int DepartmentId { get; set; } = -1; + public long DepartmentId { get; set; } = -1; /// /// 备注 @@ -141,4 +141,4 @@ public SysUserInfo(string loginName, string loginPWD) [SugarColumn(IsIgnore = true)] public string DepartmentName { get; set; } } -} \ No newline at end of file +} From eed5d5cfab71d7baf4f270ee52cfb007954e315c Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Fri, 27 Oct 2023 15:26:17 +0800 Subject: [PATCH 221/289] Update sysUserInfo.cs --- Blog.Core.Model/Models/sysUserInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blog.Core.Model/Models/sysUserInfo.cs b/Blog.Core.Model/Models/sysUserInfo.cs index 574ea311..1137d91c 100644 --- a/Blog.Core.Model/Models/sysUserInfo.cs +++ b/Blog.Core.Model/Models/sysUserInfo.cs @@ -61,7 +61,7 @@ public SysUserInfo(string loginName, string loginPWD) /// 部门 /// [SugarColumn(IsNullable = true)] - public int DepartmentId { get; set; } = -1; + public long DepartmentId { get; set; } = -1; /// /// 备注 @@ -141,4 +141,4 @@ public SysUserInfo(string loginName, string loginPWD) [SugarColumn(IsIgnore = true)] public string DepartmentName { get; set; } } -} \ No newline at end of file +} From 1b7d13aa6db48929c413244d2c555be36ef15ee2 Mon Sep 17 00:00:00 2001 From: hudingwen <765472804@qq.com> Date: Sat, 28 Oct 2023 18:21:49 +0800 Subject: [PATCH 222/289] =?UTF-8?q?xml=E5=BA=8F=E5=88=97=E5=8C=96=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Common/Helper/XmlHelper.cs | 52 +++++++++++++++++----------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/Blog.Core.Common/Helper/XmlHelper.cs b/Blog.Core.Common/Helper/XmlHelper.cs index 510c84a1..f81d6817 100644 --- a/Blog.Core.Common/Helper/XmlHelper.cs +++ b/Blog.Core.Common/Helper/XmlHelper.cs @@ -1,33 +1,40 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Xml.Serialization; namespace Blog.Core.Common.Helper { + /// + /// xml序列化帮助类 + /// public class XmlHelper { + /// + /// 存储序列类型,防止内存泄漏 + /// + private static ConcurrentDictionary hasTypes = new ConcurrentDictionary(); /// /// 转换对象为JSON格式数据 /// /// /// 对象 /// 字符格式的JSON数据 - public static string GetXML(object obj) + public static string GetXML(object obj, string rootName = "root") { - try + XmlSerializer xs; + var xsType = typeof(T); + hasTypes.TryGetValue(xsType, out xs); + if(xs == null) { - XmlSerializer xs = new XmlSerializer(typeof(T)); - - using (TextWriter tw = new StringWriter()) - { - xs.Serialize(tw, obj); - return tw.ToString(); - } + xs = new XmlSerializer(typeof(T)); + hasTypes.TryAdd(xsType, xs); } - catch (Exception) + using (TextWriter tw = new StringWriter()) { - return string.Empty; + xs.Serialize(tw, obj); + return tw.ObjToString(); } } @@ -37,15 +44,20 @@ public static string GetXML(object obj) /// /// /// - public static T ParseFormByXml(string xml,string rootName="root") + public static T ParseFormByXml(string xml, string rootName = "root") { - XmlSerializer serializer = new XmlSerializer(typeof(T), new XmlRootAttribute(rootName)); - StringReader reader = new StringReader(xml); - - T res = (T)serializer.Deserialize(reader); - reader.Close(); - reader.Dispose(); - return res; - } + XmlSerializer xs; + var xsType = typeof(T); + hasTypes.TryGetValue(xsType, out xs); + if (xs == null) + { + xs = new XmlSerializer(xsType, new XmlRootAttribute(rootName)); + hasTypes.TryAdd(xsType, xs); + } + using (StringReader reader = new StringReader(xml)) + { + return (T)xs.Deserialize(reader); + } + } } } From 164946d0bd3c24f479f9615b09508cefe6d8db8e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 1 Nov 2023 11:05:28 +0800 Subject: [PATCH 223/289] =?UTF-8?q?feat=EF=BC=9Aupdate=20common.targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.Api.csproj | 2 -- Blog.Core.Serilog/Blog.Core.Serilog.csproj | 20 ++++++++------------ build/common.targets | 4 +++- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index 3b913630..e022f1a2 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -61,8 +61,6 @@ - - diff --git a/Blog.Core.Serilog/Blog.Core.Serilog.csproj b/Blog.Core.Serilog/Blog.Core.Serilog.csproj index 8df8f16f..70fe14fd 100644 --- a/Blog.Core.Serilog/Blog.Core.Serilog.csproj +++ b/Blog.Core.Serilog/Blog.Core.Serilog.csproj @@ -1,17 +1,13 @@ - + - - net6.0 - enable - enable - + - - - + + + - - - + + + diff --git a/build/common.targets b/build/common.targets index bfd5899f..68825d9d 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,5 +1,7 @@ - net7.0 + net7.0 + enable + enable \ No newline at end of file From 6655c65924e3408e2f5c0a9ab44bf12507769559 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 1 Nov 2023 15:06:32 +0800 Subject: [PATCH 224/289] Update common.targets --- build/common.targets | 1 - 1 file changed, 1 deletion(-) diff --git a/build/common.targets b/build/common.targets index 68825d9d..5f035173 100644 --- a/build/common.targets +++ b/build/common.targets @@ -2,6 +2,5 @@ net7.0 enable - enable \ No newline at end of file From afb9a0d9f5da0fdfe1d1478a5c7ec23bd39f4bff Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Fri, 3 Nov 2023 09:22:02 +0800 Subject: [PATCH 225/289] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=B7=BB=E5=8A=A0=E5=A4=87=E7=94=A8=E9=93=BE?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index eb012394..99133fe1 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -83,10 +83,12 @@ public static void AddSqlsugarSetup(this IServiceCollection services) { BaseDBConfig.MainConfig = config; } - - //复用连接 - if (m.ConnId.ToLower().StartsWith(MainDb.CurrentDbConnId.ToLower())) + else if (m.ConnId.ToLower().StartsWith(MainDb.CurrentDbConnId.ToLower())) + { + //复用连接 BaseDBConfig.ReuseConfigs.Add(config); + } + BaseDBConfig.ValidConfig.Add(config); } From c85f51a9013323f82b4e3a9a635d6c79c4730788 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Sat, 11 Nov 2023 15:57:56 +0800 Subject: [PATCH 226/289] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E6=B5=81=E8=AF=BB=E5=8F=96,=E5=85=BC?= =?UTF-8?q?=E5=AE=B9MemoryStream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/HttpResponseExceptions.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Blog.Core.Common/Extensions/HttpResponseExceptions.cs b/Blog.Core.Common/Extensions/HttpResponseExceptions.cs index 67deee45..34c3baae 100644 --- a/Blog.Core.Common/Extensions/HttpResponseExceptions.cs +++ b/Blog.Core.Common/Extensions/HttpResponseExceptions.cs @@ -7,29 +7,29 @@ namespace Blog.Core.Common.Extensions; public static class HttpResponseExceptions { - public static string GetResponseBody(this HttpResponse response) - { - if (response is null) - { - return default; - } + public static string GetResponseBody(this HttpResponse response) + { + if (response is null) + { + return string.Empty; + } - if (response.Body is FluentHttpResponseStream responseBody) - { - response.Body.Position = 0; - //不关闭底层流 - using StreamReader stream = new StreamReader(responseBody, leaveOpen: true); - string body = stream.ReadToEnd(); - response.Body.Position = 0; - return body; - } - else - { - //原始HttpResponseStream 无法读取 - //实际上只是个包装类,内部使用了HttpResponsePipeWriter write - throw new ApplicationException("The response body is not a FluentHttpResponseStream"); - } - - return default; - } + //原始HttpResponseStream 无法读取 + //实际上只是个包装类,内部使用了HttpResponsePipeWriter write + switch (response.Body) + { + case FluentHttpResponseStream: + case MemoryStream: + { + response.Body.Position = 0; + using var stream = new StreamReader(response.Body, leaveOpen: true); + var body = stream.ReadToEnd(); + response.Body.Position = 0; + return body; + } + default: + // throw new ApplicationException("The response body is not a FluentHttpResponseStream"); + return string.Empty; + } + } } \ No newline at end of file From 908e170a792538d3614dae433154674009069926 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 11 Nov 2023 16:57:32 +0800 Subject: [PATCH 227/289] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=8F=82=E6=95=B0=E8=A7=A3=E5=AF=86=E5=92=8C?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=8A=A0=E5=AF=86=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Controllers/LoginController.cs | 648 +++++++++--------- Blog.Core.Api/Program.cs | 3 + Blog.Core.Api/appsettings.json | 14 + .../EncryptionRequestMiddleware.cs | 116 ++++ .../EncryptionResponseMiddleware.cs | 114 +++ .../Middlewares/RequRespLogMiddleware.cs | 4 +- 6 files changed, 577 insertions(+), 322 deletions(-) create mode 100644 Blog.Core.Extensions/Middlewares/EncryptionRequestMiddleware.cs create mode 100644 Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 8313507d..4838c3c9 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -15,324 +15,332 @@ namespace Blog.Core.Controllers { - /// - /// 登录管理【无权限】 - /// - [Produces("application/json")] - [Route("api/Login")] - [AllowAnonymous] - public class LoginController : BaseApiController - { - readonly ISysUserInfoServices _sysUserInfoServices; - readonly IUserRoleServices _userRoleServices; - readonly IRoleServices _roleServices; - readonly PermissionRequirement _requirement; - private readonly IRoleModulePermissionServices _roleModulePermissionServices; - private readonly ILogger _logger; - - /// - /// 构造函数注入 - /// - /// - /// - /// - /// - /// - public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, - IRoleServices roleServices, PermissionRequirement requirement, - IRoleModulePermissionServices roleModulePermissionServices, ILogger logger) - { - this._sysUserInfoServices = sysUserInfoServices; - this._userRoleServices = userRoleServices; - this._roleServices = roleServices; - _requirement = requirement; - _roleModulePermissionServices = roleModulePermissionServices; - _logger = logger; - } - - - #region 获取token的第1种方法 - - /// - /// 获取JWT的方法1 - /// - /// - /// - /// - [HttpGet] - [Route("Token")] - public async Task> GetJwtStr(string name, string pass) - { - string jwtStr = string.Empty; - bool suc = false; - //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 - - var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); - if (user != null) - { - TokenModelJwt tokenModel = new TokenModelJwt {Uid = 1, Role = user}; - - jwtStr = JwtHelper.IssueJwt(tokenModel); - suc = true; - } - else - { - jwtStr = "login fail!!!"; - } - - return new MessageModel() - { - success = suc, - msg = suc ? "获取成功" : "获取失败", - response = jwtStr - }; - } - - - /// - /// 获取JWT的方法2:给Nuxt提供 - /// - /// - /// - /// - [HttpGet] - [Route("GetTokenNuxt")] - public MessageModel GetJwtStrForNuxt(string name, string pass) - { - string jwtStr = string.Empty; - bool suc = false; - //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 - //这里直接写死了 - if (name == "admins" && pass == "admins") - { - TokenModelJwt tokenModel = new TokenModelJwt - { - Uid = 1, - Role = "Admin" - }; - - jwtStr = JwtHelper.IssueJwt(tokenModel); - suc = true; - } - else - { - jwtStr = "login fail!!!"; - } - - var result = new - { - data = new {success = suc, token = jwtStr} - }; - - return new MessageModel() - { - success = suc, - msg = suc ? "获取成功" : "获取失败", - response = jwtStr - }; - } - - #endregion - - - /// - /// 获取JWT的方法3:整个系统主要方法 - /// - /// - /// - /// - [HttpGet] - [Route("JWTToken3.0")] - public async Task> GetJwtToken3(string name = "", string pass = "") - - { - string jwtStr = string.Empty; - - if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(pass)) - return Failed("用户名或密码不能为空"); - - pass = MD5Helper.MD5Encrypt32(pass); - - var user = await _sysUserInfoServices.Query(d => - d.LoginName == name && d.LoginPWD == pass && d.IsDeleted == false); - if (user.Count > 0) - { - var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); - //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List - { - new Claim(ClaimTypes.Name, name), - new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), - new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, - DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) - }; - claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); - - - // ids4和jwt切换 - // jwt - if (!Permissions.IsUseIds4) - { - var data = await _roleModulePermissionServices.RoleModuleMaps(); - var list = (from item in data - where item.IsDeleted == false - orderby item.Id - select new PermissionItem - { - Url = item.Module?.LinkUrl, - Role = item.Role?.Name.ObjToString(), - }).ToList(); - - _requirement.Permissions = list; - } - - var token = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); - return Success(token, "获取成功"); - } - else - { - return Failed("认证失败"); - } - } - - /// - /// 请求刷新Token(以旧换新) - /// - /// - /// - [HttpGet] - [Route("RefreshToken")] - public async Task> RefreshToken(string token = "") - { - string jwtStr = string.Empty; - - if (string.IsNullOrEmpty(token)) - return Failed("token无效,请重新登录!"); - var tokenModel = JwtHelper.SerializeJwt(token); - if (tokenModel != null && JwtHelper.customSafeVerify(token) && tokenModel.Uid > 0) - { - var user = await _sysUserInfoServices.QueryById(tokenModel.Uid); - var value = User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; - if (value != null && user.CriticalModifyTime > value.ObjToDate()) - { - return Failed("很抱歉,授权已失效,请重新授权!"); - } - - if (user != null && !(value != null && user.CriticalModifyTime > value.ObjToDate())) - { - var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); - //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 - var claims = new List - { - new Claim(ClaimTypes.Name, user.LoginName), - new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), - new Claim(ClaimTypes.Expiration, - DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) - }; - claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); - - //用户标识 - var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); - identity.AddClaims(claims); - - var refreshToken = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); - return Success(refreshToken, "获取成功"); - } - } - - return Failed("认证失败!"); - } - - /// - /// 获取JWT的方法4:给 JSONP 测试 - /// - /// - /// - /// - /// - /// - /// - [HttpGet] - [Route("jsonp")] - public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, - int expiresAbsoulute = 30) - { - TokenModelJwt tokenModel = new TokenModelJwt - { - Uid = id, - Role = sub - }; - - string jwtStr = JwtHelper.IssueJwt(tokenModel); - - string response = string.Format("\"value\":\"{0}\"", jwtStr); - string call = callBack + "({" + response + "})"; - Response.WriteAsync(call); - } - - - /// - /// 测试 MD5 加密字符串 - /// - /// - /// - [HttpGet] - [Route("Md5Password")] - public string Md5Password(string password = "") - { - return MD5Helper.MD5Encrypt32(password); - } - - /// - /// swagger登录 - /// - /// - /// - [HttpPost] - [Route("/api/Login/swgLogin")] - public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) - { - if (loginRequest is null) - { - return new {result = false}; - } - - try - { - var result = await GetJwtToken3(loginRequest.name, loginRequest.pwd); - if (result.success) - { - HttpContext.SuccessSwagger(); - HttpContext.SuccessSwaggerJwt(result.response.token); - return new {result = true}; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Swagger登录异常"); - } - - return new {result = false}; - } - - /// - /// weixin登录 - /// - /// - [HttpGet] - [Route("wxLogin")] - public dynamic WxLogin(string g = "", string token = "") - { - return new {g, token}; - } - } - - public class SwaggerLoginRequest - { - public string name { get; set; } - public string pwd { get; set; } - } + /// + /// 登录管理【无权限】 + /// + [Produces("application/json")] + [Route("api/Login")] + [AllowAnonymous] + public class LoginController : BaseApiController + { + readonly ISysUserInfoServices _sysUserInfoServices; + readonly IUserRoleServices _userRoleServices; + readonly IRoleServices _roleServices; + readonly PermissionRequirement _requirement; + private readonly IRoleModulePermissionServices _roleModulePermissionServices; + private readonly ILogger _logger; + + /// + /// 构造函数注入 + /// + /// + /// + /// + /// + /// + public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, + IRoleServices roleServices, PermissionRequirement requirement, + IRoleModulePermissionServices roleModulePermissionServices, ILogger logger) + { + this._sysUserInfoServices = sysUserInfoServices; + this._userRoleServices = userRoleServices; + this._roleServices = roleServices; + _requirement = requirement; + _roleModulePermissionServices = roleModulePermissionServices; + _logger = logger; + } + + + #region 获取token的第1种方法 + + /// + /// 获取JWT的方法1 + /// + /// + /// + /// + [HttpGet] + [Route("Token")] + public async Task> GetJwtStr(string name, string pass) + { + string jwtStr = string.Empty; + bool suc = false; + //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 + + var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); + if (user != null) + { + TokenModelJwt tokenModel = new TokenModelJwt { Uid = 1, Role = user }; + + jwtStr = JwtHelper.IssueJwt(tokenModel); + suc = true; + } + else + { + jwtStr = "login fail!!!"; + } + + return new MessageModel() + { + success = suc, + msg = suc ? "获取成功" : "获取失败", + response = jwtStr + }; + } + + + /// + /// 获取JWT的方法2:给Nuxt提供 + /// + /// + /// + /// + [HttpGet] + [Route("GetTokenNuxt")] + public MessageModel GetJwtStrForNuxt(string name, string pass) + { + string jwtStr = string.Empty; + bool suc = false; + //这里就是用户登陆以后,通过数据库去调取数据,分配权限的操作 + //这里直接写死了 + if (name == "admins" && pass == "admins") + { + TokenModelJwt tokenModel = new TokenModelJwt + { + Uid = 1, + Role = "Admin" + }; + + jwtStr = JwtHelper.IssueJwt(tokenModel); + suc = true; + } + else + { + jwtStr = "login fail!!!"; + } + + var result = new + { + data = new { success = suc, token = jwtStr } + }; + + return new MessageModel() + { + success = suc, + msg = suc ? "获取成功" : "获取失败", + response = jwtStr + }; + } + + #endregion + + + /// + /// 获取JWT的方法3:整个系统主要方法 + /// + /// + /// + /// + [HttpGet] + [Route("JWTToken3.0")] + public async Task> GetJwtToken3(string name = "", string pass = "") + + { + string jwtStr = string.Empty; + + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(pass)) + return Failed("用户名或密码不能为空"); + + pass = MD5Helper.MD5Encrypt32(pass); + + var user = await _sysUserInfoServices.Query(d => + d.LoginName == name && d.LoginPWD == pass && d.IsDeleted == false); + if (user.Count > 0) + { + var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); + //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 + var claims = new List + { + new Claim(ClaimTypes.Name, name), + new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), + new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(ClaimTypes.Expiration, + DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; + claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); + + + // ids4和jwt切换 + // jwt + if (!Permissions.IsUseIds4) + { + var data = await _roleModulePermissionServices.RoleModuleMaps(); + var list = (from item in data + where item.IsDeleted == false + orderby item.Id + select new PermissionItem + { + Url = item.Module?.LinkUrl, + Role = item.Role?.Name.ObjToString(), + }).ToList(); + + _requirement.Permissions = list; + } + + var token = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); + return Success(token, "获取成功"); + } + else + { + return Failed("认证失败"); + } + } + + [HttpGet] + [Route("GetJwtTokenSecret")] + public async Task> GetJwtTokenSecret(string name = "", string pass = "") + { + var rlt = await GetJwtToken3(name, pass); + return rlt; + } + + /// + /// 请求刷新Token(以旧换新) + /// + /// + /// + [HttpGet] + [Route("RefreshToken")] + public async Task> RefreshToken(string token = "") + { + string jwtStr = string.Empty; + + if (string.IsNullOrEmpty(token)) + return Failed("token无效,请重新登录!"); + var tokenModel = JwtHelper.SerializeJwt(token); + if (tokenModel != null && JwtHelper.customSafeVerify(token) && tokenModel.Uid > 0) + { + var user = await _sysUserInfoServices.QueryById(tokenModel.Uid); + var value = User.Claims.SingleOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value; + if (value != null && user.CriticalModifyTime > value.ObjToDate()) + { + return Failed("很抱歉,授权已失效,请重新授权!"); + } + + if (user != null && !(value != null && user.CriticalModifyTime > value.ObjToDate())) + { + var userRoles = await _sysUserInfoServices.GetUserRoleNameStr(user.LoginName, user.LoginPWD); + //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色 + var claims = new List + { + new Claim(ClaimTypes.Name, user.LoginName), + new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(ClaimTypes.Expiration, + DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) + }; + claims.AddRange(userRoles.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); + + //用户标识 + var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); + identity.AddClaims(claims); + + var refreshToken = JwtToken.BuildJwtToken(claims.ToArray(), _requirement); + return Success(refreshToken, "获取成功"); + } + } + + return Failed("认证失败!"); + } + + /// + /// 获取JWT的方法4:给 JSONP 测试 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + [Route("jsonp")] + public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, + int expiresAbsoulute = 30) + { + TokenModelJwt tokenModel = new TokenModelJwt + { + Uid = id, + Role = sub + }; + + string jwtStr = JwtHelper.IssueJwt(tokenModel); + + string response = string.Format("\"value\":\"{0}\"", jwtStr); + string call = callBack + "({" + response + "})"; + Response.WriteAsync(call); + } + + + /// + /// 测试 MD5 加密字符串 + /// + /// + /// + [HttpGet] + [Route("Md5Password")] + public string Md5Password(string password = "") + { + return MD5Helper.MD5Encrypt32(password); + } + + /// + /// swagger登录 + /// + /// + /// + [HttpPost] + [Route("/api/Login/swgLogin")] + public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) + { + if (loginRequest is null) + { + return new { result = false }; + } + + try + { + var result = await GetJwtToken3(loginRequest.name, loginRequest.pwd); + if (result.success) + { + HttpContext.SuccessSwagger(); + HttpContext.SuccessSwaggerJwt(result.response.token); + return new { result = true }; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Swagger登录异常"); + } + + return new { result = false }; + } + + /// + /// weixin登录 + /// + /// + [HttpGet] + [Route("wxLogin")] + public dynamic WxLogin(string g = "", string token = "") + { + return new { g, token }; + } + } + + public class SwaggerLoginRequest + { + public string name { get; set; } + public string pwd { get; set; } + } } \ No newline at end of file diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 876a83a8..4c2274ee 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -144,6 +144,9 @@ //app.UseHsts(); } +app.UseEncryptionRequest(); +app.UseEncryptionResponse(); + app.UseExceptionHandlerMiddle(); app.UseIpLimitMiddle(); app.UseRequestResponseLogMiddle(); diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index f91f6df8..ff2c178a 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -265,6 +265,20 @@ }, "IpRateLimit": { "Enabled": true + }, + "EncryptionResponse": { + "Enabled": true, + "AllApis": false, + "LimitApis": [ + "/api/Login/GetJwtTokenSecret" + ] + }, + "EncryptionRequest": { + "Enabled": true, + "AllApis": false, + "LimitApis": [ + "/api/Login/GetJwtTokenSecret" + ] } }, "IpRateLimiting": { diff --git a/Blog.Core.Extensions/Middlewares/EncryptionRequestMiddleware.cs b/Blog.Core.Extensions/Middlewares/EncryptionRequestMiddleware.cs new file mode 100644 index 00000000..9cb78a30 --- /dev/null +++ b/Blog.Core.Extensions/Middlewares/EncryptionRequestMiddleware.cs @@ -0,0 +1,116 @@ +using Blog.Core.Common; +using Blog.Core.Common.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Extensions +{ + /// + /// 自定义中间件 + /// 通过配置,对指定接口返回数据进行加密返回 + /// 可过滤文件流 + /// + public class EncryptionRequestMiddleware + { + private readonly RequestDelegate _next; + + public EncryptionRequestMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + // 配置开关,过滤接口 + if (AppSettings.app("Middleware", "EncryptionRequest", "Enabled").ObjToBool()) + { + var isAllApis = AppSettings.app("Middleware", "EncryptionRequest", "AllApis").ObjToBool(); + var needEnApis = AppSettings.app("Middleware", "EncryptionRequest", "LimitApis"); + var path = context.Request.Path.Value.ToLower(); + if (isAllApis || (path.Length > 5 && needEnApis.Any(d => d.ToLower().Contains(path)))) + { + Console.WriteLine($"{isAllApis} -- {path}"); + + if (context.Request.Method.ToLower() == "post") + { + // 读取请求主体 + using StreamReader reader = new(context.Request.Body, Encoding.UTF8); + string requestBody = await reader.ReadToEndAsync(); + + // 检查是否有要解密的数据 + if (!string.IsNullOrEmpty(requestBody) && context.Request.Headers.ContainsKey("Content-Type") && + context.Request.Headers["Content-Type"].ToString().ToLower().Contains("application/json")) + { + // 解密数据 + string decryptedString = DecryptData(requestBody); + + // 更新请求主体中的数据 + context.Request.Body = GenerateStreamFromString(decryptedString); + } + } + else if (context.Request.Method.ToLower() == "get") + { + // 获取url参数 + string param = context.Request.Query["param"]; + + // 检查是否有要解密的数据 + if (!string.IsNullOrEmpty(param)) + { + // 解密数据 + string decryptedString = DecryptData(param); + + // 更新url参数值 + context.Request.QueryString = new QueryString($"?{decryptedString}"); + } + } + + await _next(context); + } + else + { + await _next(context); + } + } + else + { + await _next(context); + } + } + private string DecryptData(string encryptedData) + { + // 解密逻辑实现,可以根据你使用的加密算法和密钥进行自定义 + byte[] bytes = Convert.FromBase64String(encryptedData); + string originalString = Encoding.UTF8.GetString(bytes); + Console.WriteLine(originalString); + return originalString; + } + private static Stream GenerateStreamFromString(string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + } + + public static class EncryptionRequestExtensions + { + /// + /// 自定义中间件 + /// 通过配置,对指定接口入参进行解密操作 + /// 注意:放到管道最外层 + /// + public static IApplicationBuilder UseEncryptionRequest(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs b/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs new file mode 100644 index 00000000..cb701b1f --- /dev/null +++ b/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs @@ -0,0 +1,114 @@ +using Blog.Core.Common; +using Blog.Core.Common.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.Core.Extensions +{ + /// + /// 自定义中间件 + /// 通过配置,对指定接口返回数据进行加密返回 + /// 可过滤文件流 + /// + public class EncryptionResponseMiddleware + { + private readonly RequestDelegate _next; + + public EncryptionResponseMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + // 配置开关,过滤接口 + if (AppSettings.app("Middleware", "EncryptionResponse", "Enabled").ObjToBool()) + { + var isAllApis = AppSettings.app("Middleware", "EncryptionResponse", "AllApis").ObjToBool(); + var needEnApis = AppSettings.app("Middleware", "EncryptionResponse", "LimitApis"); + var path = context.Request.Path.Value.ToLower(); + if (isAllApis || (path.Length > 5 && needEnApis.Any(d => d.ToLower().Contains(path)))) + { + Console.WriteLine($"{isAllApis} -- {path}"); + var responseCxt = context.Response; + var originalBodyStream = responseCxt.Body; + + // 创建一个新的内存流用于存储加密后的数据 + using var encryptedBodyStream = new MemoryStream(); + // 用新的内存流替换 responseCxt.Body + responseCxt.Body = encryptedBodyStream; + + // 执行下一个中间件请求管道 + await _next(context); + + //encryptedBodyStream.Seek(0, SeekOrigin.Begin); + //encryptedBodyStream.Position = 0; + + // 可以去掉某些流接口 + if (!context.Response.ContentType.ToLower().Contains("application/json")) + { + Console.WriteLine($"非json返回格式 {context.Response.ContentType}"); + //await encryptedBodyStream.CopyToAsync(originalBodyStream); + context.Response.Body = originalBodyStream; + return; + } + + // 读取加密后的数据 + //var encryptedBody = await new StreamReader(encryptedBodyStream).ReadToEndAsync(); + var encryptedBody = responseCxt.GetResponseBody(); + + if (encryptedBody.IsNotEmptyOrNull()) + { + dynamic jsonObject = JsonConvert.DeserializeObject(encryptedBody); + string statusCont = jsonObject.status; + var status = statusCont.ObjToInt(); + string msg = jsonObject.msg; + string successCont = jsonObject.success; + var success = successCont.ObjToBool(); + dynamic responseCnt = success ? jsonObject.response : ""; + string s = "1"; + // 这里换成自己的任意加密方式 + var response = responseCnt.ToString() != "" ? Convert.ToBase64String(Encoding.UTF8.GetBytes(responseCnt.ToString())) : ""; + string resJson = JsonConvert.SerializeObject(new { response, msg, status, s, success }); + + context.Response.Clear(); + await using var streamlriter = new StreamWriter(originalBodyStream, leaveOpen: true); + await streamlriter.WriteAsync(resJson); + + //var encryptedData = Encoding.UTF8.GetBytes(resJson); + //responseCxt.ContentLength = encryptedData.Length; + //await originalBodyStream.WriteAsync(encryptedData, 0, encryptedData.Length); + } + } + else + { + await _next(context); + } + } + else + { + await _next(context); + } + } + } + + public static class EncryptionResponseExtensions + { + /// + /// 自定义中间件 + /// 通过配置,对指定接口返回数据进行加密返回 + /// 可过滤文件流 + /// 注意:放到管道最外层 + /// + public static IApplicationBuilder UseEncryptionResponse(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs index 26971a3a..510b87ef 100644 --- a/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/RequRespLogMiddleware.cs @@ -96,11 +96,11 @@ private void ResponseDataLog(HttpResponse response) // 去除 Html var reg = "<[^>]+>"; - var isHtml = Regex.IsMatch(responseBody, reg); if (!string.IsNullOrEmpty(responseBody)) { - Parallel.For(0, 1, e => + var isHtml = Regex.IsMatch(responseBody, reg); + Parallel.For(0, 1, e => { //LogLock.OutSql2Log("RequestResponseLog", new string[] { "Response Data:", ResponseBody }); LogLock.OutLogAOP("RequestResponseLog", response.HttpContext.TraceIdentifier, From d0fe732331368ff71191d3d89d4a28f3d17f99d5 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 11 Nov 2023 17:56:31 +0800 Subject: [PATCH 228/289] Update EncryptionResponseMiddleware.cs --- .../Middlewares/EncryptionResponseMiddleware.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs b/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs index cb701b1f..188c6f8d 100644 --- a/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs +++ b/Blog.Core.Extensions/Middlewares/EncryptionResponseMiddleware.cs @@ -78,12 +78,16 @@ public async Task InvokeAsync(HttpContext context) string resJson = JsonConvert.SerializeObject(new { response, msg, status, s, success }); context.Response.Clear(); - await using var streamlriter = new StreamWriter(originalBodyStream, leaveOpen: true); - await streamlriter.WriteAsync(resJson); + responseCxt.ContentType = "application/json"; - //var encryptedData = Encoding.UTF8.GetBytes(resJson); - //responseCxt.ContentLength = encryptedData.Length; - //await originalBodyStream.WriteAsync(encryptedData, 0, encryptedData.Length); + //await using var streamlriter = new StreamWriter(originalBodyStream, leaveOpen: true); + //await streamlriter.WriteAsync(resJson); + + var encryptedData = Encoding.UTF8.GetBytes(resJson); + responseCxt.ContentLength = encryptedData.Length; + await originalBodyStream.WriteAsync(encryptedData, 0, encryptedData.Length); + + responseCxt.Body = originalBodyStream; } } else From 7ca3e1ec22dcf3812c2114a502f977264cba1535 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 15 Nov 2023 16:15:21 +0800 Subject: [PATCH 229/289] feat: update to 8.0 --- .github/workflows/dotnetcore.yml | 2 +- Blog.Core.Api/Blog.Core.Api.csproj | 8 ++-- Blog.Core.Api/Program.cs | 3 ++ Blog.Core.Common/Blog.Core.Common.csproj | 32 ++++++++-------- Blog.Core.EventBus/Blog.Core.EventBus.csproj | 14 +++---- .../Blog.Core.Extensions.csproj | 38 +++++++++---------- Blog.Core.Gateway/Blog.Core.Gateway.csproj | 12 +++--- Blog.Core.Model/Blog.Core.Model.csproj | 4 +- .../Blog.Core.Repository.csproj | 8 ++-- .../Blog.Core.Serilog.Es.csproj | 12 +++--- Blog.Core.Serilog/Blog.Core.Serilog.csproj | 2 +- Blog.Core.Tasks/Blog.Core.Tasks.csproj | 2 +- Blog.Core.Tests/Blog.Core.Tests.csproj | 8 ++-- Dockerfile | 4 +- Ocelot.Provider.Nacos/Nacos.cs | 11 +++--- .../Ocelot.Provider.Nacos.csproj | 10 ++--- build/common.targets | 2 +- 17 files changed, 88 insertions(+), 84 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 2dd1ed9c..f11fee1f 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -12,7 +12,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Build with dotnet run: dotnet build --configuration Release - name: Build image diff --git a/Blog.Core.Api/Blog.Core.Api.csproj b/Blog.Core.Api/Blog.Core.Api.csproj index e022f1a2..92a680da 100644 --- a/Blog.Core.Api/Blog.Core.Api.csproj +++ b/Blog.Core.Api/Blog.Core.Api.csproj @@ -61,11 +61,11 @@ - + - - - + + + diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 875b4f2e..13d7cea2 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.IdentityModel.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; @@ -130,6 +131,8 @@ // 3、配置中间件 var app = builder.Build(); +IdentityModelEventSource.ShowPII = true; + app.ConfigureApplication(); app.UseApplicationSetup(); app.UseResponseBodyRead(); diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index 457e5722..ce966e59 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -14,29 +14,29 @@ - - + + - - + + - - + + - - - - + + + + - - + + - - + + - - + + diff --git a/Blog.Core.EventBus/Blog.Core.EventBus.csproj b/Blog.Core.EventBus/Blog.Core.EventBus.csproj index 8c50f8e2..17bebf20 100644 --- a/Blog.Core.EventBus/Blog.Core.EventBus.csproj +++ b/Blog.Core.EventBus/Blog.Core.EventBus.csproj @@ -5,15 +5,15 @@ - - + + - + - - - - + + + + diff --git a/Blog.Core.Extensions/Blog.Core.Extensions.csproj b/Blog.Core.Extensions/Blog.Core.Extensions.csproj index a68f7f12..96183e46 100644 --- a/Blog.Core.Extensions/Blog.Core.Extensions.csproj +++ b/Blog.Core.Extensions/Blog.Core.Extensions.csproj @@ -5,32 +5,32 @@ - - - - - - + + + + + + - - - - - - - - - + + + + + + + + + - - - + + + - + diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.csproj b/Blog.Core.Gateway/Blog.Core.Gateway.csproj index ca3a7f72..8af7a7e3 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.csproj +++ b/Blog.Core.Gateway/Blog.Core.Gateway.csproj @@ -13,12 +13,12 @@ - - - - - - + + + + + + diff --git a/Blog.Core.Model/Blog.Core.Model.csproj b/Blog.Core.Model/Blog.Core.Model.csproj index 6762bbcd..841a4665 100644 --- a/Blog.Core.Model/Blog.Core.Model.csproj +++ b/Blog.Core.Model/Blog.Core.Model.csproj @@ -14,9 +14,9 @@ - + - + diff --git a/Blog.Core.Repository/Blog.Core.Repository.csproj b/Blog.Core.Repository/Blog.Core.Repository.csproj index 45931d9f..84b5dfad 100644 --- a/Blog.Core.Repository/Blog.Core.Repository.csproj +++ b/Blog.Core.Repository/Blog.Core.Repository.csproj @@ -4,9 +4,9 @@ - - - + + + @@ -19,7 +19,7 @@ - + diff --git a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj index cd71cf26..a53e98f5 100644 --- a/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj +++ b/Blog.Core.Serilog.Es/Blog.Core.Serilog.Es.csproj @@ -4,12 +4,12 @@ - - - - - - + + + + + + diff --git a/Blog.Core.Serilog/Blog.Core.Serilog.csproj b/Blog.Core.Serilog/Blog.Core.Serilog.csproj index 70fe14fd..9994dce2 100644 --- a/Blog.Core.Serilog/Blog.Core.Serilog.csproj +++ b/Blog.Core.Serilog/Blog.Core.Serilog.csproj @@ -3,7 +3,7 @@ - + diff --git a/Blog.Core.Tasks/Blog.Core.Tasks.csproj b/Blog.Core.Tasks/Blog.Core.Tasks.csproj index 3292ff0d..abc0d331 100644 --- a/Blog.Core.Tasks/Blog.Core.Tasks.csproj +++ b/Blog.Core.Tasks/Blog.Core.Tasks.csproj @@ -3,7 +3,7 @@ - + diff --git a/Blog.Core.Tests/Blog.Core.Tests.csproj b/Blog.Core.Tests/Blog.Core.Tests.csproj index 4db51c4f..22c227e0 100644 --- a/Blog.Core.Tests/Blog.Core.Tests.csproj +++ b/Blog.Core.Tests/Blog.Core.Tests.csproj @@ -18,10 +18,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dockerfile b/Dockerfile index c80e6896..15ba36e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Blog.Core.Api/Blog.Core.Api.csproj", "Blog.Core.Api/"] COPY ["Blog.Core.Extensions/Blog.Core.Extensions.csproj", "Blog.Core.Extensions/"] diff --git a/Ocelot.Provider.Nacos/Nacos.cs b/Ocelot.Provider.Nacos/Nacos.cs index d8357920..b1d4cd4f 100644 --- a/Ocelot.Provider.Nacos/Nacos.cs +++ b/Ocelot.Provider.Nacos/Nacos.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using Ocelot.ServiceDiscovery.Providers; +using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; using Nacos.V2; using Microsoft.Extensions.Options; @@ -52,5 +48,10 @@ public async Task> Get() return await Task.FromResult(services); } + + public Task> GetAsync() + { + throw new NotImplementedException(); + } } } diff --git a/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj b/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj index 6bdeb841..541ee1e9 100644 --- a/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj +++ b/Ocelot.Provider.Nacos/Ocelot.Provider.Nacos.csproj @@ -11,10 +11,10 @@ - - - - - + + + + + diff --git a/build/common.targets b/build/common.targets index 5f035173..eeb37544 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,6 +1,6 @@ - net7.0 + net8.0 enable \ No newline at end of file From 6331e2082021440aa3e675ba687f7e20bf50e7b5 Mon Sep 17 00:00:00 2001 From: LemonNoCry <773596523@qq.com> Date: Wed, 15 Nov 2023 18:09:54 +0800 Subject: [PATCH 230/289] =?UTF-8?q?=F0=9F=90=9B=20=E4=BC=98=E5=8C=96JWT?= =?UTF-8?q?=E7=AD=BE=E5=8F=91=E5=B1=9E=E6=80=A7=20iat=20=E4=B8=BA=E7=AD=BE?= =?UTF-8?q?=E5=8F=91=E6=97=B6=E9=97=B4=E6=88=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Controllers/LoginController.cs | 16 ++++++++-------- Blog.Core.Common/Helper/UtilConvert.cs | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index 4838c3c9..e6b4ee4f 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -70,7 +70,7 @@ public async Task> GetJwtStr(string name, string pass) var user = await _sysUserInfoServices.GetUserRoleNameStr(name, MD5Helper.MD5Encrypt32(pass)); if (user != null) { - TokenModelJwt tokenModel = new TokenModelJwt { Uid = 1, Role = user }; + TokenModelJwt tokenModel = new TokenModelJwt {Uid = 1, Role = user}; jwtStr = JwtHelper.IssueJwt(tokenModel); suc = true; @@ -121,7 +121,7 @@ public MessageModel GetJwtStrForNuxt(string name, string pass) var result = new { - data = new { success = suc, token = jwtStr } + data = new {success = suc, token = jwtStr} }; return new MessageModel() @@ -164,7 +164,7 @@ public async Task> GetJwtToken3(string name = " new Claim(ClaimTypes.Name, name), new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString()), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; @@ -236,7 +236,7 @@ public async Task> RefreshToken(string token = { new Claim(ClaimTypes.Name, user.LoginName), new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString()), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; @@ -305,7 +305,7 @@ public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) { if (loginRequest is null) { - return new { result = false }; + return new {result = false}; } try @@ -315,7 +315,7 @@ public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) { HttpContext.SuccessSwagger(); HttpContext.SuccessSwaggerJwt(result.response.token); - return new { result = true }; + return new {result = true}; } } catch (Exception ex) @@ -323,7 +323,7 @@ public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) _logger.LogWarning(ex, "Swagger登录异常"); } - return new { result = false }; + return new {result = false}; } /// @@ -334,7 +334,7 @@ public async Task SwgLogin([FromBody] SwaggerLoginRequest loginRequest) [Route("wxLogin")] public dynamic WxLogin(string g = "", string token = "") { - return new { g, token }; + return new {g, token}; } } diff --git a/Blog.Core.Common/Helper/UtilConvert.cs b/Blog.Core.Common/Helper/UtilConvert.cs index 530c4fd2..a75005ac 100644 --- a/Blog.Core.Common/Helper/UtilConvert.cs +++ b/Blog.Core.Common/Helper/UtilConvert.cs @@ -107,7 +107,8 @@ public static string ObjToString(this object thisValue) /// public static bool IsNotEmptyOrNull(this object thisValue) { - return ObjToString(thisValue) != "" && ObjToString(thisValue) != "undefined" && ObjToString(thisValue) != "null"; + return ObjToString(thisValue) != "" && ObjToString(thisValue) != "undefined" && + ObjToString(thisValue) != "null"; } /// @@ -122,7 +123,8 @@ public static string ObjToString(this object thisValue, string errorValue) return errorValue; } - public static bool IsNullOrEmpty(this object thisValue) => thisValue == null || thisValue == DBNull.Value || string.IsNullOrWhiteSpace(thisValue.ToString()); + public static bool IsNullOrEmpty(this object thisValue) => thisValue == null || thisValue == DBNull.Value || + string.IsNullOrWhiteSpace(thisValue.ToString()); /// /// @@ -169,6 +171,16 @@ public static DateTime ObjToDate(this object thisValue) { reval = Convert.ToDateTime(thisValue); } + else + { + //时间戳转为时间 + var seconds = ObjToLong(thisValue); + if (seconds > 0) + { + var startTime = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Local); + reval = startTime.AddSeconds(Convert.ToDouble(thisValue)); + } + } return reval; } @@ -235,7 +247,7 @@ public static object ChangeType(this object value, Type type) { Type innerType = type.GetGenericArguments()[0]; object innerValue = ChangeType(value, innerType); - return Activator.CreateInstance(type, new object[] { innerValue }); + return Activator.CreateInstance(type, new object[] {innerValue}); } if (value is string && type == typeof(Guid)) return new Guid(value as string); @@ -278,7 +290,7 @@ public static object ChangeTypeList(this object value, Type type) .Remove(split.Length - 2, 1); } - addMethod.Invoke(lis, new object[] { ChangeType(str, type) }); + addMethod.Invoke(lis, new object[] {ChangeType(str, type)}); } } From b3e7fbca5436f7390dc4eb3bd26b4794932a2689 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Thu, 16 Nov 2023 10:31:21 +0800 Subject: [PATCH 231/289] feat: :apple: change iat value --- Blog.Core.Api/Controllers/LoginController.cs | 4 ++-- Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index e6b4ee4f..abb495c9 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -164,7 +164,7 @@ public async Task> GetJwtToken3(string name = " new Claim(ClaimTypes.Name, name), new Claim(JwtRegisteredClaimNames.Jti, user.FirstOrDefault().Id.ToString()), new Claim("TenantId", user.FirstOrDefault().TenantId.ToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.DateToTimeStamp()), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; @@ -236,7 +236,7 @@ public async Task> RefreshToken(string token = { new Claim(ClaimTypes.Name, user.LoginName), new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ObjToString()), - new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.DateToTimeStamp()), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(_requirement.Expiration.TotalSeconds).ToString()) }; diff --git a/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs b/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs index b9659029..6fa9c9b6 100644 --- a/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs +++ b/Blog.Core.Extensions/Authorizations/Helpers/JwtHelper.cs @@ -36,8 +36,8 @@ public static string IssueJwt(TokenModelJwt tokenModel) new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()), - new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"), - new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , + new Claim(JwtRegisteredClaimNames.Iat, $"{DateTime.Now.DateToTimeStamp()}"), + new Claim(JwtRegisteredClaimNames.Nbf,$"{DateTime.Now.DateToTimeStamp()}") , //这个就是过期时间,目前是过期1000秒,可自定义,注意JWT有自己的缓冲过期时间 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(1000).ToString()), From c93c3eb44b57b7414a486f86aba336dcb02d7e5a Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sat, 18 Nov 2023 08:53:47 +0800 Subject: [PATCH 232/289] Update Authentication_JWTSetup.cs --- .../ServiceExtensions/Authentication_JWTSetup.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs b/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs index 98fa0b82..d9048c6e 100644 --- a/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/Authentication_JWTSetup.cs @@ -5,11 +5,8 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; -using System; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace Blog.Core.Extensions { @@ -74,7 +71,7 @@ public static void AddAuthentication_JWTSetup(this IServiceCollection services) }, OnChallenge = context => { - context.Response.Headers.Add("Token-Error", context.ErrorDescription); + context.Response.Headers["Token-Error"] = context.ErrorDescription; return Task.CompletedTask; }, OnAuthenticationFailed = context => @@ -88,12 +85,12 @@ public static void AddAuthentication_JWTSetup(this IServiceCollection services) if (jwtToken.Issuer != Issuer) { - context.Response.Headers.Add("Token-Error-Iss", "issuer is wrong!"); + context.Response.Headers["Token-Error-Iss"] = "issuer is wrong!"; } if (jwtToken.Audiences.FirstOrDefault() != Audience) { - context.Response.Headers.Add("Token-Error-Aud", "Audience is wrong!"); + context.Response.Headers["Token-Error-Aud"] = "Audience is wrong!"; } } @@ -101,7 +98,7 @@ public static void AddAuthentication_JWTSetup(this IServiceCollection services) // 如果过期,则把<是否过期>添加到,返回头信息中 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { - context.Response.Headers.Add("Token-Expired", "true"); + context.Response.Headers["Token-Expired"] = "true"; } return Task.CompletedTask; } From 1b3ab297f36ea6906a6a13518531c1e7532d4f46 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Mon, 20 Nov 2023 16:38:36 +0800 Subject: [PATCH 233/289] =?UTF-8?q?feat=EF=BC=9A=E5=B0=86=E5=AE=98?= =?UTF-8?q?=E6=96=B9=E6=96=87=E6=A1=A3=E8=BF=81=E7=A7=BB=E5=88=B0=E6=96=B0?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docs/README.md | 2 + .docs/contents/.vuepress/config.js | 46 -- .../.vuepress/public/bcvphomelogo.png | Bin 5644 -> 0 bytes .docs/contents/.vuepress/public/favicon.ico | Bin 1556 -> 0 bytes .docs/contents/Contribution/README.md | 143 ----- .docs/contents/PressureTest/README.md | 80 --- .docs/contents/QQ/README.md | 18 - .docs/contents/README.md | 14 - .docs/contents/Update/README.md | 198 ------ .docs/contents/guide/README.md | 121 ---- .docs/contents/guide/cheat-sheet.md | 580 ------------------ .docs/contents/guide/function-sheet.md | 471 -------------- .docs/contents/guide/getting-started.md | 132 ---- .docs/package.json | 12 - .gitignore | 1 + 15 files changed, 3 insertions(+), 1815 deletions(-) create mode 100644 .docs/README.md delete mode 100644 .docs/contents/.vuepress/config.js delete mode 100644 .docs/contents/.vuepress/public/bcvphomelogo.png delete mode 100644 .docs/contents/.vuepress/public/favicon.ico delete mode 100644 .docs/contents/Contribution/README.md delete mode 100644 .docs/contents/PressureTest/README.md delete mode 100644 .docs/contents/QQ/README.md delete mode 100644 .docs/contents/README.md delete mode 100644 .docs/contents/Update/README.md delete mode 100644 .docs/contents/guide/README.md delete mode 100644 .docs/contents/guide/cheat-sheet.md delete mode 100644 .docs/contents/guide/function-sheet.md delete mode 100644 .docs/contents/guide/getting-started.md delete mode 100644 .docs/package.json diff --git a/.docs/README.md b/.docs/README.md new file mode 100644 index 00000000..c7aaa452 --- /dev/null +++ b/.docs/README.md @@ -0,0 +1,2 @@ +BlogCore官方文档仓库地址已经迁移到: +https://gitee.com/laozhangIsPhi/Blog.Core.E-Book \ No newline at end of file diff --git a/.docs/contents/.vuepress/config.js b/.docs/contents/.vuepress/config.js deleted file mode 100644 index 6cb23754..00000000 --- a/.docs/contents/.vuepress/config.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = { - title: 'Blog.Core', - description: 'Hello, 欢迎使用前后端分离之 ASP.NET Core 后端全家桶框架!', - base : '/.doc/', - head: [ - ['link', { - rel: 'icon', - href: `/favicon.ico` - }] - ], - dest: './contents/.vuepress/dist', - ga: '', - evergreen: true, - themeConfig: { - nav: [ - { text: '首页', link: '/' }, - { text: '指南', link: '/guide/' }, - { text: '更新日志', link: '/Update/' }, - { text: '压测', link: '/PressureTest/' }, - { text: '参与贡献', link: '/Contribution/' }, - { text: 'BCVP社区', link: '/QQ/' }, - { text: '接口API', link: 'http://apk.neters.club' }, - { text: '管理后台', link: 'http://vueadmin.neters.club' }, - { text: 'Github', link: 'https://github.com/anjoy8/Blog.Core' }, - ], - sidebarDepth: 2, - sidebar: { - '/guide/': getGuideSidebar('Guide'), - } - } -} - -function getGuideSidebar (groupA) { - return [ - { - title: groupA, - collapsable: false, - children: [ - '', - 'getting-started', - 'function-sheet', - 'cheat-sheet' - ] - } - ] - } \ No newline at end of file diff --git a/.docs/contents/.vuepress/public/bcvphomelogo.png b/.docs/contents/.vuepress/public/bcvphomelogo.png deleted file mode 100644 index e1bf0f79d16b27c9f911c995cdd0f15a849c1d14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5644 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 Ip26V%1Gkl@sQ>@~ diff --git a/.docs/contents/.vuepress/public/favicon.ico b/.docs/contents/.vuepress/public/favicon.ico deleted file mode 100644 index 68062fe0577bebf8065cfc59d83f29f40a8bd7f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1556 zcmV+v2J88WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf1*u6yK~z{r?N(WA zR8<)MX6~J(v$Qi4x`7m0T3QN4s8R%i5L{Ri6D5e4078sDXynC>5EB)BktcjIp@s+= zqL8R5Dq&GE2&GtB#ZsUP6l6MWXz4PY&fTy7IdiA9(qaq5^ucd(=H7GubI$*rf4j$K z5-?6+x1Qqy|C1RBN?e1}CBPqYBP-vJ$;Cd{?f36IBakss zeW(@9HC<2@9fD|s%}&im5}IuqCUo6^U>7jGEP&GGlSpRcV~iYozZ={}rZW=5(cMkZ zGy|eT-~>hjTWaVgC;HfiX~@X&!7vhp?gm51M6UGW2oZ6*M|V1=m`!7ZRLR7Om-CS| zHO-Qa9?1g?kcsum*#FTvI8(?P^cw(4nUxIfmI28-mepvU&9q!cn?lFx*Ji^TbXqe< zhs2>sYm&@2AJ-w;B|#8wnOLVu8lAYHG#$CKL+}S(i1o;5sq4nYs&+U%cA8JjES@aB zGIRY{zIl3LV2mI#x_P}JibJ1XfZHcp!$gFv;dDB%=8f6pHd07XWSwVI^8MSL^^heQ zf+KMNL$$#d6!G+y65eNE^O&6-*19`^Tjq!Ii)g#r%}vi>P_~%54Vyol$NQ%bUc>p5 z5h#iVQ548h8Cbe5kIVMHR}EEDV5f|eq6U_4oPoRtL+EOc149=>uDKkjQFR|;6XCJ!rLEaF6ZZb;a_ zqn3!+D9~-zo-F-AF$Ns}s);LRIQU6Dc5OY0-P@{oSlE@gPGmG*$V>`Yg9?}1lgNfh zOD}C`ks#UN#?rsxY=kyDh40Jj@XLWFe7U0UVn&m`4R!V_x;m|#ctIY=D%RN59{7~mT z#U+ZBL{u|K{VL`x&c)KT`Mlr!M+Z8?eMn1BLE*eiKK5%xBhDOdgv(9q=%lKwA*Ub< ztGCSM@^1RlH`jLX_uP^YLRnt^J`y+?_OkQUW0WP7-V|g>FhW_AgH#_1k(rZfjRGCv zD30yBh}M=)xG1xdU=|Hb@MGgU3$2l{VSr41RRrIa*TNfc4b&SZUKq7H`$(Pf9O zm^_A9Ad7_dwdhxgW@=I1N$raFhXY&h^*LQj^;GZ#(7 zs*Q7af5=yV47iO9LqknF4wl#O6lNbdRu2Z>Z`cy%K6b6}r@Qgw`dP>=${__?-%Zg3^VHeH0(HX6Yw|3()tChQ+DtG0000 - - - -## 2、测试准备 -因为 `JMeter` 是使用 `JAVA` 写的,所以使用 `JMeter` 之前,先安装 `JAVA` 环境。 -安装好后,在 `bin` 文件夹下,点击 `jmeter.bat` 启动程序。 -启动之后会有两个窗口,一个`cmd`窗口,一个`JMeter`的 `GUI`。前面不要忽略`CMD`窗口的提示信息,不要关闭它。 -注意:使用`API`模式,不要使用`GUI`模式。 - - -## 3、测试配置 -本地发布后的 `windows` 环境,直接用 `kestrel` 启动。 -线程数:100 -循环数:1000 -HTTP默认值:协议:`http`;服务器或IP:`localhost`;端口号:`9291`; -HTTP请求:方法:GET;路径:`/api/blog/ApacheTestUpdate` -HTTP信息请求管理器:无 -响应断言:无 - - - -## 4、项目初始化 -目前采用 `Blog.Core` 默认的配置, -只开启了内存 `AOP` , -其他的都是默认的,然后也把任务调度也关闭了, -最后注意要把 `IP限流`给关闭,不然压测没效果,因为限流了: - - - -## 5、压测过程 - -##### 第一阶段 - - - - -##### 第二阶段 - - - - -##### 第三阶段 - - - - -##### 第四阶段(压测后,检测内存是否降低,20m后) - - - -##### 第五阶段(停止压测1h后) - - - -## 6、测试结果 -内存方面,`100*1000` 的 **压测过程中** (写操作),项目保证所占内存在 `350m~500m` 之间 -然后停止一个小时后,内存将为`150m~200m`: - - - - - - -## 7、压测配置文件下载 - [配置文件](https://img.neters.club/doc/blogcore_blog_ApacheTestUpdate.jmx) - 下载后,导入到工具里,可以直接测试,察看结果树。 - -## 8、Docker 镜像 - 已经提交到 `docker hub` 自行拉取操作即可: - ``` - docker pull laozhangisphi/apkimg:latest - ``` diff --git a/.docs/contents/QQ/README.md b/.docs/contents/QQ/README.md deleted file mode 100644 index 31b7f909..00000000 --- a/.docs/contents/QQ/README.md +++ /dev/null @@ -1,18 +0,0 @@ -## 开源社区 - -bcvp - -[https://github.com/BaseCoreVueProject/Home](https://github.com/BaseCoreVueProject/Home) - -Base netCore (Vue) Project Team, -基于Net/Core 和Vue(react/ng),快速搭建 MVC & SPA 及微服务应用 -如果你有关于dotNet/core 的,不错的,可以正常运行,且一年内维护的,均可以加入。 -唯一宗旨:我们来自社区,服务社区,反哺社区。 - - - -## 微信公众号 - -公众号 - - diff --git a/.docs/contents/README.md b/.docs/contents/README.md deleted file mode 100644 index 597f16d9..00000000 --- a/.docs/contents/README.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -home: true -heroImage: /bcvphomelogo.png -actionText: 快速上手 → -actionLink: /guide/ -features: -- title: 详尽的文档 - details: 通过详细的文章和视频讲解,将知识点各个击破,入门ASP.Net Core不再难 -- title: 强大的社区 - details: 通过 QQ 群,和数千位同业大佬一起切磋交流。 -- title: 丰富的内容 - details: 框架涵盖ASP.Net Core开发中常见的基本知识点,不仅适合初学者入门,同时也适用于企业级别的开发。 -footer: MIT Licensed | Copyright © 2018-2020-老张的哲学 Powered by VUEPRESS on CentOS 7.6 ---- \ No newline at end of file diff --git a/.docs/contents/Update/README.md b/.docs/contents/Update/README.md deleted file mode 100644 index 870f98ec..00000000 --- a/.docs/contents/Update/README.md +++ /dev/null @@ -1,198 +0,0 @@ - -## 更新日志 - - -### 2021-08-21 - -重要功能增加:项目增加 `Apollo` 配置中心; - -### 2021-08-03 - -重要功能增加:项目增加 `ES` 搜索,增加 `Serilog` 使用 `tcp` 的方式自定义格式化,写入 `elk` 的实现; - -### 2021-06-28 - -功能增加:项目增加 `nacos` 配置,支持将项目注册到 `nacos` 服务中心,搭建微服务之子服务; - -### 2021-06-04 - -小功能更新:执行的时候,将 `Sql` 日志输出到控制台,方便查看,支持配置文件关闭; - -### 2021-05-01 - -组件更新:多项日志中间件,由自写组件,转为使用`serilog`组件记录日志; - -### 2021-03-03 - -项目目录调整:新增测试文件夹和模板文件夹; - -### 2021-02-09 - -重大项目更新:新增建行聚合支付; - -### 2021-01-11 - -更新:优化任务调度功能,新增暂停和停止; - -### 2020-12-02 - -更新:新增调用`MongoDB`功能,功能可用待完善中; - - -### 2020-11-19 -> 重大内容更新:更新项目模板 `Update Blog.Core.Webapi.Template.2.5.2.nupkg` -> 主要内容:1、泛型主键;2、通过测试中间件;3、`RabbitMQ`消息队列 - - -### 2020-11-18 - -项目更新:新增`RabbitMQ`消息队列和`EventBus`事件总线,功能可用待完善。 - - -### 2020-11-11 - -项目重大更新:更新至`.NET 5.0`。 - - -### 2020-11-05 - -项目更新:增加`测试用户`中间件,通过一键操作可以跳过权限限制,方便调试,文章[使用测试用户中间件](http://apk.neters.club/api/Blog/GoUrl?id=156)。 - - - -### 2020-10-11 - -项目更新:设计泛型主键功能,可以在项目初始化的时候设计主键类型。 - - - -### 2020-09-18 - -项目更新:更新项目模板 `Update Blog.Core.Webapi.Template.2.2.3.nupkg` 。 -> 1、增加 `Redis` 消息队列功能; - - - -### 2020-09-04 - -项目更新:增加 `Redis` 消息队列功能; - - -### 2020-08-06 - -项目更新:更新项目模板 `Update Blog.Core.Webapi.Template.2.2.0.nupkg` 。 -> 1、根据解决方案名,来自动导入model; -> 2、单独封装服务扩展层 `Blog.Core.Extensions` ; -> 3、代码生成器,支持控制器文件的生成; -> 4、弱化仓储层,用泛型仓储基类注入服务; - - - - -### 2020-08-01 - -> 重大结构更新:弱化仓储层,通过泛型仓储基类,来实现仓储服务注入,并去掉`Blog.Core.IRepository` 接口层; - -### 2020-07-03 - -> 更新:`DbFirstController` 生成四层文件,目前新增支持 `控制器Controller` 文件的输出; - - -### 2020-06-22 - -> 项目更新:将服务扩展和自定义中间件,单独封装一层 `Blog.Core.Extensions` ,更解耦。 - - - -### 2020-06-08 - -> 简单项目更新:生成数据库表结构的时候,利用反射机制,自动生成固定命名空间 `Blog.Core.Model.Models` 下的全部实体. -> 同时判断表是否存在,如果存在下次不再重复生成。 - - -### 2020-06-06 - -项目更新:更新项目模板 `Update Blog.Core.Webapi.Template.2.1.0.nupkg` [1a726f8](https://github.com/anjoy8/Blog.Core/commit/1a726f890e527c978982071462e82db4478632f0),更新项目即可 。 -> 1、配置内容展示到控制台; -> 2、简化封装 `Startup.cs` 类文件; -> 3、`DbFirst` 模式支持多库模式; -> 4、`Log4net` 讲异常和 `Info` 分开; -> 5、修复 `BlogLogAop` 偶尔卡顿问题; -> 6、将生成种子数据和任务调度功能,封装到中间件; -> 7、获取当前项目在服务器中的运行信息; -> 8、删除所有的不需要的 `using` 指令; - - - - -### 2020-05-29 -项目启动开启 `QuzrtzNet` 调度任务,并且在 `Admin` 后台管理中配置操作界面; -> 内容更新:封装生成种子数据的入口方法; - - - -### 2020-05-12 -修复:支持多库模式下,生成项目模板代码 `DbFirstController` [102c6d6](https://github.com/anjoy8/Blog.Core/commit/102c6d6bfcafd06bf5241844759dea5e7a6815da) -> 注意:`T4` 模板不能此功能,一次只能一个数据库,且只能 `SqlServer` - - -### 2020-05-07 -> 重大内容更新:更新项目模板 `Update Blog.Core.Webapi.Template.2.1.0.nupkg` [7f64fde](https://github.com/anjoy8/Blog.Core/commit/7f64fde5507f7a8572372dcadb6af5110bd37d68) - - -### 2020-05-06 -> 重大内容更新:优化Log4Net使用方案,完美配合 `NetCore` 官方的 `ILogger`, [ecaffb6](https://github.com/anjoy8/Blog.Core/commit/ecaffb66bdf10a90c087d01e6e817e54f23a97d4) - - -### 2020-05-01 - -> 重要内容更新:配合Admin全部完成按钮级别权限,更新初始化种子数据 - -### 2020-04-27 - -增加功能:配合前端Admin,增加页面 `KeepAlive` 功能; -增加功能:增加 `Sql` 语句查询Demo,支持返回 `DataTable`; - - -### 2020-04-25 - -增加功能:`Http api` 接口调用,满足微服务需求 -> 重要内容更新:优化 `Appsettings.app()` 方法,通过官方 `IConfiguration` 接口来获取DBS连接字符串; -> 优化 `BlogLogAOP.cs` - - -### 2020-04-15 - -> 重大内容更新:更新项目模板 `Update Blog.Core.Webapi.Template.1.11.30.nupkg` - - -### 2020-04-14 -> 重大内容更新:主分支,可以通过配置,一键切换JWT和Ids4认证授权模式 - - -### 2020-03-30 -> 重大内容更新:统一所有接口返回格式 - - -### 2020-03-25 -增加功能:支持读写分离(目前是三种模式:单库、多库、读写分离) -> 重大BUG更新:系统登录接口,未对用户软删除进行判断,现已修复 -> API: /api/login/GetJwtToken3 -> Code: await _sysUserInfoServices.Query(d => d.uLoginName == name && d.uLoginPWD == pass && d.tdIsDelete == false); - - - -### 2020-03-18 -增加功能:创建 Quartz.net 任务调度服务 - - -### 2020-01-09 -增加功能:项目迁移到IdentityServer4,统一授权认证中心 - - -### 2020-01-05 -增加功能:设计一个简单的中间件,可以查看所有已经注入的服务 - - -### 2020-01-04 -增加功能:Ip限流,防止过多刷数据 diff --git a/.docs/contents/guide/README.md b/.docs/contents/guide/README.md deleted file mode 100644 index 30950e0c..00000000 --- a/.docs/contents/guide/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# W 文档指南 -## 亮点与优势 - -Blog.Core 是一个开箱即用的企业级权限管理应用框架。 -采用最新的前后端完全分离技术【 ASP.NET Core Api 5.0 + Vue 2.x 】。 -并结合 `IdentityServer4` ,可快速解决多客户端和多资源服务的统一认证与鉴权的问题。 - -## 其他资料 - -博客园,早期架构搭建:[博客园](https://www.cnblogs.com/laozhang-is-phi/p/9495618.html) -公众号,后期调整:[文章](https://mvp.neters.club/MVP_aspnetcore_2020/2020) -视频:[B站](https://www.bilibili.com/video/BV1vC4y1p7Za) - - -## 配套站点 - -本资源服务器,配合多个项目,构建前后端权限一体化平台,前端用 `VUE` 框架。 -前端-客户端:[预览](https://vueadmin.neters.club/)、[源码](https://github.com/anjoy8/Blog.Admin) -前端-管理后台:[预览](http://vueblog.neters.club/)、[源码](https://github.com/anjoy8/Blog.Vue) -认证平台:[预览](https://ids.neters.club/)、[源码](https://github.com/anjoy8/Blog.IdentityServer) - - -### 为什么选择 ASPNET.Core -1、【开源】`ASPNET.NET Core` 是由 `Microsoft` 和 `.NET` 社区在 `GitHub` 上开源并维护的一个跨平台(支持 Windows、macOS 和 Linux)的新一代高性能框架, -拥有十分广泛的社区与支持者,可用于构建web应用、物联网IOT应用和移动端应用。 -2、【高效】Asp.net core(.net core)来源于.net,很容易迁移,而且也很容易上手, -但是又是不同的一个框架,除了上述对.net开发者十分友好以外,相对于之前的.net项目,速度上有巨大的改进, -相比与原来的`Web(.net framework 4.6)`程序性能提升了`2300%`。跟`python`、`java`等相同环境比较,性能都要优越, -参考[www.techempower.com](https://www.techempower.com/benchmarks/)。 -3、【跨平台】可以在`Windows`、`Mac`和`Linux`构建和运行跨平台的`Asp.Net Core`应用。 -4、【云原生】在云原生领域拥有天然的优势,搭配Azure云服务,配合K8s,更好的实现分布式应用,以及微服务应用。 -5、【微服务】`ASP.NET Core`尤其适用于微服务架构,也就是说ASP.NET Core不仅适合于中小型项目而且还特别适合于大型,超大型项目。 -6、【大公司】目前国内采用`ASP.NET Core`的大公司比如腾讯、网易,国际的有Bing,GoDaddy,Stackoverflow,Adobe,Microsoft -7、【总结来说】,`java`支持的,`ASPNET.Core`都支持,而且更轻量级、更高效跨,并且对.net开发者十分友好,微服务案例成熟。 - - - -### 框架功能点 -1、丰富完整的接口文档,在查看的基础上,可以模拟前端调用,更方便。 -2、采用多层开发,隔离性更好,封装更完善。 -3、基于项目模板,可以一键创建自己的项目。 -4、搭配代码生成器,实现快速开发,节省成本。 -5、项目集成多库模式以及读写分离模式,可以同时处理多个数据库的不同模块,更快更安全。 -6、集成统一认证平台 `IdentityServer4` ,实现多个项目的统一认证管理,解决了之前一个项目, -一套用户的弊端,更适用微服务的开发。 -7、丰富的审计日志处理,方便线上项目快速定位异常点。 -8、支持自由切换多种数据库,Sqlite/SqlServer/MySql/PostgreSQL/Oracle; -9、支持 `Docker` 容器化开发,可以搭配 k8s 更好的实现微服务。 - - -### 应用领域 -1、【对接第三方api】项目通过`webapi`,可以快速对接第三方`api`服务,实现业务逻辑。 -2、【前后端分离】 采用的是`API`+前端的完全分离的开发模式,满足平时开发的所有需求, -你可以对接任何的自定义前端项目:无论是微信小程序,还是授权APP,无论是PC网页, -还是手机H5。 -3、【多项目】同时框架还集成了一套鉴权平台,采用IdentityServer4,可以快速的实现多个客户端的认证与授权服务, -从而大大的减少了平时的工作量,可以快速的进行产品迭代。 -4、【微服务】当然,因为采用的是API模式,所以同样适用于微服务项目,实现高并发的产品需求。 - - - -### 市场前景 -1、前后端分离模式已经是目前的主流开发模式,框架已经是一套可行的方案,开箱即用。 -2、拥有几十篇技术文档和3000人的技术社区,方便快捷的解决问题。 -3、目前已经有超过20多家公司在生产环境中使用,当然实际中更多,具体查看 [点击查看使用的情况](https://github.com/anjoy8/Blog.Core/issues/75)。 -4、同时可以搭配自己的业务,实现微服务的开发,在大数据高并发中,占有更好的优势。 -5、本项目直接作者由微软MVP“老张的哲学”出品,并长久维护,不会断更,有保障。 - - - -## 功能与进度 - -框架模块: -- [√] 采用`仓储+服务+接口`的形式封装框架; -- [√] 异步 async/await 开发; -- [√] 接入国产数据库ORM组件 —— SqlSugar,封装数据库操作; -- [√] 支持自由切换多种数据库,MySql/SqlServer/Sqlite/Oracle/Postgresql/达梦/人大金仓; -- [√] 实现项目启动,自动生成种子数据 ✨; -- [√] 五种日志记录,审计/异常/请求响应/服务操作/Sql记录等; -- [√] 支持项目事务处理(若要分布式,用cap即可)✨; -- [√] 设计4种 AOP 切面编程,功能涵盖:日志、缓存、审计、事务 ✨; -- [√] 支持 T4 代码模板,自动生成每层代码; -- [√] 或使用 DbFirst 一键创建自己项目的四层文件(支持多库); -- [√] 封装`Blog.Core.Webapi.Template`项目模板,一键重建自己的项目 ✨; -- [√] 搭配多个前端案例供参考和借鉴:Blog.Vue、Blog.Admin、Nuxt.tbug、Blog.Mvp.Blazor ✨; -- [√] 统一集成 IdentityServer4 认证 ✨; - -组件模块: -- [√] 提供 Redis 做缓存处理; -- [√] 使用 Swagger 做api文档; -- [√] 使用 MiniProfiler 做接口性能分析 ✨; -- [√] 使用 Automapper 处理对象映射; -- [√] 使用 AutoFac 做依赖注入容器,并提供批量服务注入 ✨; -- [√] 支持 CORS 跨域; -- [√] 封装 JWT 自定义策略授权; -- [√] 使用 Log4Net 日志框架,集成原生 ILogger 接口做日志记录; -- [√] 使用 SignalR 双工通讯 ✨; -- [√] 添加 IpRateLimiting 做 API 限流处理; -- [√] 使用 Quartz.net 做任务调度(目前单机多任务,集群调度暂不支持); -- [√] 支持 数据库`读写分离`和多库操作 ✨; -- [√] 新增 Redis 消息队列 ✨; -- [√] 新增 RabbitMQ 消息队列 ✨; -- [√] 新增 EventBus 事件总线 ✨; -- [√] 新增 实现聚合支付; -- [ ] 计划 - 数据部门权限; -- [ ] 计划 - ES 搜索; - -微服务模块: -- [√] 可配合 Docker 实现容器化; -- [√] 可配合 Jenkins 实现CI / CD; -- [√] 可配合 Consul 实现服务发现; -- [√] 可配合 Ocelot 实现网关处理; -- [√] 可配合 Nginx 实现负载均衡; -- [√] 可配合 Ids4 实现认证中心; - - -  - - - - diff --git a/.docs/contents/guide/cheat-sheet.md b/.docs/contents/guide/cheat-sheet.md deleted file mode 100644 index 36f95e36..00000000 --- a/.docs/contents/guide/cheat-sheet.md +++ /dev/null @@ -1,580 +0,0 @@ -# Z 主要知识点 - - - -## AOP - -本项目多处采用面向切面编程思想——AOP,除了广义上的过滤器和中间件以外,主要通过动态代理的形式来实现AOP编程思想,主要的案例共有四个,分别是: -1、服务日志AOP; -2、服务InMemory缓存AOP; -3、服务Redis缓存AOP; -4、服务事务AOP; - - -具体的代码可以在 `Blog.Core\Blog.Core\AOP` 文件夹下查看。 - -与此同时,多个AOP也设置了阀门来控制是否开启,具体的可以查看 `appsettings.json` 中的: - -``` - "AppSettings": { - "RedisCachingAOP": { - "Enabled": false, - "ConnectionString": "127.0.0.1:6319" - }, - "MemoryCachingAOP": { - "Enabled": true - }, - "LogAOP": { - "Enabled": false - }, - "TranAOP": { - "Enabled": false - }, - "SqlAOP": { - "Enabled": false - } - }, - -``` - -## Appsettings - -整个系统通过一个封装的操作类 `Appsettings.cs` 来控制配置文件 `appsettings.json` 文件, -操作类地址在:`\Blog.Core.Common\Helper` 文件夹下。 -具体的使用方法是: - -``` -Appsettings.app(new string[] { "AppSettings", "RedisCachingAOP", "Enabled" }) - -// 里边的参数,按照 appsettings.json 中设置的层级顺序来写,可以获取到指定的任意内容。 - -``` - - - -## AspNetCoreRateLimit - -系统使用 `AspNetCoreRateLimit` 组件来实现ip限流: -1、添加 `nuget` 包: -``` - -``` - -2、注入服务 `IpPolicyRateLimitSetup.cs` -``` -services.AddIpPolicyRateLimitSetup(Configuration); -``` - -3、配置中间件 -``` - // Ip限流,尽量放管道外层 - app.UseIpRateLimiting(); -``` - -4、配置数据 - -具体的内容,自行百度即可 -``` - "IpRateLimiting": { - "EnableEndpointRateLimiting": true, - "StackBlockedRequests": false, - "RealIpHeader": "X-Real-IP", - "ClientIdHeader": "X-ClientId", - "HttpStatusCode": 429,//返回状态码 - "GeneralRules": [//规则,结尾一定要带* - { - "Endpoint": "*", - "Period": "1m", - "Limit": 120 - }, - { - "Endpoint": "*:/api/blog*", - "Period": "1m", - "Limit": 30 - } - ] - - } -``` - - - -## Async-Await - -整个系统采用 async/await 异步编程,符合主流的开发模式, -特别是对多线程开发很友好。 - - - -## Authorization-Ids4 - -本系统 v2.0 版本(目前的系统已经集成 `ids4` 和 `jwt`,并且可以自由切换),已经支持了统一授权认证,和 `blog` 项目、`Admin` 项目、`DDD` 项目等一起,使用一个统一的认证中心。 - -具体的代码参考:`.\Blog.Core\Extensions` 文件夹下的 `Authorization_Ids4Setup.cs` ,注意需要引用指定的 `nuget` 包,核心代码如下: - -``` - //【认证】 - services.AddAuthentication(o => - { - o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - o.DefaultChallengeScheme = nameof(ApiResponseHandler); - o.DefaultForbidScheme = nameof(ApiResponseHandler); - }) - // 2.添加Identityserver4认证 - .AddIdentityServerAuthentication(options => - { - options.Authority = Appsettings.app(new string[] { "Startup", "IdentityServer4", "AuthorizationUrl" }); - options.RequireHttpsMetadata = false; - options.ApiName = Appsettings.app(new string[] { "Startup", "IdentityServer4", "ApiName" }); - options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt; - options.ApiSecret = "api_secret"; - - }) - - -``` - -### 如何在Swagger中配置Ids4? -很简单,直接在 `SwaggerSetup.cs` 中直接接入 `oauth、Implicit` 即可: - -``` - //接入identityserver4 - c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme - { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows - { - Implicit = new OpenApiOAuthFlow - { - AuthorizationUrl = new Uri($"{Appsettings.app(new string[] { "Startup", "IdentityServer4", "AuthorizationUrl" })}/connect/authorize"), - Scopes = new Dictionary { - { - "blog.core.api","ApiResource id" - } - } - } - } - }); - -``` - -然后在 `IdentityServer4` 项目中,做指定的修改,配置 `9291` 的回调地址: - -``` - new Client { - ClientId = "blogadminjs", - ClientName = "Blog.Admin JavaScript Client", - AllowedGrantTypes = GrantTypes.Implicit, - AllowAccessTokensViaBrowser = true, - - RedirectUris = - { - "http://vueadmin.neters.club/callback", - // 这里要配置回调地址 - "http://localhost:9291/oauth2-redirect.html" - }, - PostLogoutRedirectUris = { "http://vueadmin.neters.club" }, - AllowedCorsOrigins = { "http://vueadmin.neters.club" }, - - AllowedScopes = { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - "roles", - "blog.core.api" - } - }, - -``` - -然后再 `Swagger` 中,配置登录授权: - -swagger - - -## Authorization-JWT - -如果你不想使用 `IdentityServer4` 的话,也可以使用 `JWT` 认证,同样是是`Blog.Core\Blog.Core\Extensions` 文件夹下的 `AuthorizationSetup.cs` 中有关认证的部分: - -``` - 1.添加JwtBearer认证服务 -.AddJwtBearer(o => -{ - o.TokenValidationParameters = tokenValidationParameters; - o.Events = new JwtBearerEvents - { - OnAuthenticationFailed = context => - { - // 如果过期,则把<是否过期>添加到,返回头信息中 - if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) - { - context.Response.Headers.Add("Token-Expired", "true"); - } - return Task.CompletedTask; - } - }; -}) - -``` - - -## AutoMapper - -使用 `AutoMapper` 组件来实现 `Dto` 模型的传输转换,具体的用法,可以查看: -`Blog.Core\Blog.Core\Extensions` 文件夹下的 `AutoMapperSetup.cs` 扩展类, -通过引用 `AutoMapper` 和 `AutoMapper.Extensions.Microsoft.DependencyInjection` 两个 `nuget` 包,并设置指定的 `profile` 文件,来实现模型转换控制。 - -``` -// 比如如何定义: - public class CustomProfile : Profile - { - /// - /// 配置构造函数,用来创建关系映射 - /// - public CustomProfile() - { - CreateMap(); - CreateMap(); - } - } - - -// 比如如何使用 -models = _mapper.Map(blogArticle); - -``` - -具体的查看项目中代码即可。 - - - - -## CORS - -在线项目使用的是 `nginx` 跨域代理,但是同时也是支持 `CORS` 代理: -1、注入服务 `services.AddCorsSetup();` 具体代码 `Blog.Core\Blog.Core\Extensions` 文件夹下的 `CorsSetup.cs` 扩展类; -2、配置中间件 `app.UseCors("LimitRequests");` ,要注意中间件顺序; -3、配置自己项目的前端端口,通过在 `appsettings.json` 文件中配置自己的前端项目 `ip:端口` ,来实现跨域: - -``` - "Startup": { - "Cors": { - "IPs": "http://127.0.0.1:2364,http://localhost:2364,http://localhost:8080,http://localhost:8021,http://localhost:1818" - } - }, - -``` - - -## DI-AutoFac - -项目使用了依赖注入,除了原生的依赖注入以外,更多的使用的是第三方组件 `Autofac` : -1、引用依赖包: -``` - - - -``` -主要是第一个 `nuget` 包,下边的是为了实现动态代理 `AOP` 操作; - -2、项目之间采用引用解耦的方式,通过反射来注入服务层和仓储层的程序集 `dll` 来实现批量注入,更方便,以后每次新增和修改 `Service` 层和 `Repository` 层,只需要 `F6` 编译一下即可,具体代码查看 `Startup.cs`: - -``` - - - // 注意在CreateDefaultBuilder中,添加Autofac服务工厂 - public void ConfigureContainer(ContainerBuilder builder) - { - var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; - //builder.RegisterType().As(); - - - #region 带有接口层的服务注入 - - - var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); - var repositoryDllFile = Path.Combine(basePath, "Blog.Core.Repository.dll"); - - if (!(File.Exists(servicesDllFile) && File.Exists(repositoryDllFile))) - { - throw new Exception("Repository.dll和service.dll 丢失,因为项目解耦了,所以需要先F6编译,再F5运行,请检查 bin 文件夹,并拷贝。"); - } - - - - // AOP 开关,如果想要打开指定的功能,只需要在 appsettigns.json 对应对应 true 就行。 - var cacheType = new List(); - if (Appsettings.app(new string[] { "AppSettings", "RedisCachingAOP", "Enabled" }).ObjToBool()) - { - builder.RegisterType(); - cacheType.Add(typeof(BlogRedisCacheAOP)); - } - if (Appsettings.app(new string[] { "AppSettings", "MemoryCachingAOP", "Enabled" }).ObjToBool()) - { - builder.RegisterType(); - cacheType.Add(typeof(BlogCacheAOP)); - } - if (Appsettings.app(new string[] { "AppSettings", "TranAOP", "Enabled" }).ObjToBool()) - { - builder.RegisterType(); - cacheType.Add(typeof(BlogTranAOP)); - } - if (Appsettings.app(new string[] { "AppSettings", "LogAOP", "Enabled" }).ObjToBool()) - { - builder.RegisterType(); - cacheType.Add(typeof(BlogLogAOP)); - } - - // 获取 Service.dll 程序集服务,并注册 - var assemblysServices = Assembly.LoadFrom(servicesDllFile); - builder.RegisterAssemblyTypes(assemblysServices) - .AsImplementedInterfaces() - .InstancePerDependency() - .EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy; - .InterceptedBy(cacheType.ToArray());//允许将拦截器服务的列表分配给注册。 - - // 获取 Repository.dll 程序集服务,并注册 - var assemblysRepository = Assembly.LoadFrom(repositoryDllFile); - builder.RegisterAssemblyTypes(assemblysRepository) - .AsImplementedInterfaces() - .InstancePerDependency(); - - #endregion - - #region 没有接口层的服务层注入 - - //因为没有接口层,所以不能实现解耦,只能用 Load 方法。 - //注意如果使用没有接口的服务,并想对其使用 AOP 拦截,就必须设置为虚方法 - //var assemblysServicesNoInterfaces = Assembly.Load("Blog.Core.Services"); - //builder.RegisterAssemblyTypes(assemblysServicesNoInterfaces); - - #endregion - - #region 没有接口的单独类 class 注入 - - //只能注入该类中的虚方法 - builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love))) - .EnableClassInterceptors() - .InterceptedBy(cacheType.ToArray()); - - #endregion - - - // 这里和注入没关系,只是获取注册列表,请忽略 - tsDIAutofac.AddRange(assemblysServices.GetTypes().ToList()); - tsDIAutofac.AddRange(assemblysRepository.GetTypes().ToList()); - } - -``` - -3、然后 `Program.cs` 中也要加一句话:` .UseServiceProviderFactory(new AutofacServiceProviderFactory()) //<--NOTE THIS ` - - - -## DI-NetCore - -除了主要的 `Autofac` 依赖注入以外,也减少的使用了原生的依赖注入方式,很简单,比如这样的: -``` - - services.AddSingleton(); - // 注入权限处理器 - services.AddScoped(); - services.AddSingleton(permissionRequirement); -``` - - -## Filter - -项目中一共有四个过滤器 -``` -1、GlobalAuthorizeFilter.cs —— 全局授权配置,添加后,就可以不用在每一个控制器上添加 [Authorize] 特性,但是3.1版本好像有些问题,【暂时放弃使用】; -2、GlobalExceptionFilter.cs —— 全局异常处理,实现 actionContext 级别的异常日志收集; -3、GlobalRoutePrefixFilter.cs —— 全局路由前缀公约,统计在路由上加上前缀; -4、UseServiceDIAttribute.cs —— 测试注入,【暂时无用】; -``` -文件地址在 `.\Blog.Core\Filter` 文件夹下,其中核心的是 `2` 个,重点使用的是 `1` 个 —— 全局异常错误日志 `GlobalExceptionsFilter`: -通过注册在 `MVC` 服务 `services.AddControllers()` 中,实现全局异常过滤: -``` - services.AddControllers(o => - { - // 全局异常过滤 - o.Filters.Add(typeof(GlobalExceptionsFilter)); - // 全局路由权限公约 - //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); - // 全局路由前缀,统一修改路由 - o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); - }) -``` - - - -## Framework - -项目采用 `服务+仓储+接口` 的多层结构,使用依赖注入,并且通过解耦项目,较完整的实现了 `DIP` 原则: -高层模块不应该依赖于底层模块,二者都应该依赖于抽象。 -抽象不应该依赖于细节,细节应该依赖于抽象。 - -同时项目也封装了: -`CodeFirst` 初始化数据库以及数据; -`DbFirst` 根据数据库(支持多库),生成多层代码,算是简单代码生成器; -其他功能,[核心功能与进度](http://apk.neters.club/.doc/guide/#%E5%8A%9F%E8%83%BD%E4%B8%8E%E8%BF%9B%E5%BA%A6) - - - - -## Log - -通过集成 `Log4Net` 组件,完美配合 `NetCore` 官方的 `ILogger` 接口,实现对日志的管控,引用 `nuget` 包 `Microsoft.Extensions.Logging.Log4Net.AspNetCore`: -Program.cs -``` - webBuilder - .UseStartup() - .ConfigureLogging((hostingContext, builder) => - { - //该方法需要引入Microsoft.Extensions.Logging名称空间 - builder.AddFilter("System", LogLevel.Error); //过滤掉系统默认的一些日志 - builder.AddFilter("Microsoft", LogLevel.Error);//过滤掉系统默认的一些日志 - - //添加Log4Net - //var path = Directory.GetCurrentDirectory() + "\\log4net.config"; - //不带参数:表示log4net.config的配置文件就在应用程序根目录下,也可以指定配置文件的路径 - //需要添加nuget包:Microsoft.Extensions.Logging.Log4Net.AspNetCore - builder.AddLog4Net(); - }); - -``` - -然后直接在需要的地方注入使用,比如在控制器中 -` public UserController(ILogger logger)` - -然后就可以使用了。 - -> 注意:日志 其实是分为两部分的: -> netcore输出(控制台、输出窗口等) 和 `ILogger` 持久化 -> 两者对应配置也不一样,就比如上边的过滤,是针对日志持久化的,如果想要对控制台进行控制,需要配置 `appsettings.json` 中的 `Logging` 节点 - - -## MemoryCache - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 - -## Middleware - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## MiniProfiler - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 - -## publish -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 - - -## Redis - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## Repository -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## SeedData - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## SignalR - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## SqlSugar - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## SqlSugar-Codefirst&DataSeed - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## SqlSugar-SqlAOP - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## Swagger - -精力有限,还是更新中... -如果你愿意帮忙,可以直接在GitHub中,提交pull request, -我会在后边的贡献者页面里,列出你的名字和项目地址做推广 -## T4 - -项目集成 `T4` 模板 `.\Blog.Core.FrameWork` 层,目的是可以一键生成项目模板代码。 -1、需要在 `DbHelper.ttinclude` 中配置连接数据库连接字符串; -2、针对每一层的代码,就去指定的 `.tt` 模板,直接 `CTRL+S` 保存即可; - -> 注意,目前的代码是 `SqlServer` 版本的,其他数据库版本的,可以去群文件查看。 - - -## Test-xUnit - -项目简单使用了单元测试,通过 `xUnit` 组件,具体的可以查看 `Blog.Core.Tests` 层相关代码。 -目前单元测试用例还比较少,大家可以自行添加。 - - -## Temple-Nuget - -本项目封装了 `Nuget` 自定义模板,你可以根据这个模板,一键创建自己的项目名,具体的操作,可以双击项目根目录下的 `CreateYourProject.bat` ,可以参考 [#如何项目重命名](http://apk.neters.club/.doc/guide/getting-started.html#%E5%A6%82%E4%BD%95%E9%A1%B9%E7%9B%AE%E9%87%8D%E5%91%BD%E5%90%8D) - -同时,你也可以再 `Nuget` 管理器中,搜索到: -nuget - - - -## UserInfo - - -项目中封装了获取用户信息的代码: -在 `.\Blog.Core.Common\HttpContextUser` 文件夹下 `AspNetUser.cs` 实现类和 `IUser.cs` 接口。 - -如果使用,首先需要注册相应的服务,参见:`.\Blog.Core\Extensions` 文件夹下的 `HttpContextSetup.cs`; -然后,就直接在控制器构造函数中,注入接口 `IUser` 即可; - -> `注意`: -> 1、如果要想获取指定的服务,必须登录,也就是必须要在 `Header` 中传递有效 `Token` ,这是肯定的。 -> 2、如果要获取用户信息,一定要在中间件 `app.UseAuthentication()` 之后(不要问为什么),控制器肯定在它之后,所以能获取到; -> 3、`【并不是】`一定需要添加 `[Authorize]` 特性,如果你加了这个特性,可以直接获取,但是如果不加,可以从我的 `AspNetUser.cs` 方法中,有一个直接从 `Header` 中解析的方法 `List GetUserInfoFromToken(string ClaimType);`: - -``` - public string GetToken() - { - return _accessor.HttpContext.Request.Headers["Authorization"].ObjToString().Replace("Bearer ", ""); - } - - public List GetUserInfoFromToken(string ClaimType) - { - - var jwtHandler = new JwtSecurityTokenHandler(); - if (!string.IsNullOrEmpty(GetToken())) - { - JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(GetToken()); - - return (from item in jwtToken.Claims - where item.Type == ClaimType - select item.Value).ToList(); - } - else - { - return new List() { }; - } - } - -``` diff --git a/.docs/contents/guide/function-sheet.md b/.docs/contents/guide/function-sheet.md deleted file mode 100644 index d81ad13f..00000000 --- a/.docs/contents/guide/function-sheet.md +++ /dev/null @@ -1,471 +0,0 @@ -# H 核心功能一览表 - -## 一、表结构解析 - -`Blog.Core` 项目共包含四部分的数据库表结构,分别是:用户角色管理部分、接口菜单权限管理部分、博客文章管理部分、以及其他不重要部分。 -> 注意:目前不提供与维护数据库数据,直接通过 `SeedData` 生成种子数据; - -### 1、用户角色管理部分[必须] -主要是三个表:分别对应用户表(sysUserInfo)、角色表(Role)、用户角色关系表(UserRole)。 - -usermanager - - - -### 2、接口菜单权限管理部分[必须] - -主要是四个表:分别对应接口表(Module)、菜单表(Permission)、接口菜单关系表(ModulePermission)暂时没用到、角色接口菜单关系表(RoleModulePermission)。 - -permissionmanager - - - - -### 3、博客文章管理部分[可选] -主要是三个表:分别对应博客表(BlogArticle)、Bug专题表(Topic)、Bug内容表(TopicDetail)。 - -blogmanager - - - - -### 4、其他不重要部分 - -主要是三个表:分别对应Job调度表(TasksQz)、密码库表(PasswordLib)、操作日志表(OperateLog)、广告表(Advertisement)、公告表(Guestbook)。 - -othersmanager - - - - - - -## 二、日志记录 - -本框架涵盖了不同领域的日志记录,共五个,分别是: - -1、全局异常日志 - - 开启方式:无需操作。 - 文件路径:web目录下,Log/GlobalExcepLogs_{日期}.log。 - 功能描述:记录项目启动后出现的所有异常日志,不包括中间件中异常。 - - -2、IP 请求日志 - - 开启方式:无需操作。 - 文件路径:web目录下,Log/RequestIpInfoLog.log。 - 功能描述:记录项目启动后客户端请求的ip和接口信息。 - 举例来说: - {"Ip":"xxx.xx.xx.x","Url":"/api/values","Datetime":"2020-01-06 18:02:19","Date":"2020-01-06","Week":"周一"} - - -3、用户API访问日志 - - 开启方式:appsettings.json -> Middlewar -> RecordAccessLogs 节点为true。 - 文件路径:web目录下,Log/RecordAccessLogs_{日期}.log。 - 功能描述:记录项目启动后客户端所有的API访问日志,包括参数、body以及用户信息。 - - -4、服务层请求响应AOP日志 - - 开启方式:appsettings.json -> AppSettings -> LogAOP 节点为true。 - 文件路径:web目录下,Log/AOPLog.log。 - 功能描述:记录项目启动请求api后,所有的service层日志,包括方法名、参数、响应结果或用户(非必须)。 - - -5、数据库操作日志 - - 开启方式:appsettings.json -> AppSettings -> SqlAOP 节点为true。 - 文件路径:web目录下,Log/SqlLog.log。 - 功能描述:记录项目启动请求api并访问service后,所有的db操作日志,包括Sql参数与Sql语句。 - 举例来说: - -------------------------------- - 1/6/2020 6:13:04 PM| - 【SQL参数】:@bID0:1 - 【SQL语句】:SELECT `bID`,`bsubmitter`,`btitle`,`bcategory`,`bcontent`,`btraffic`,`bcommentNum`,`bUpdateTime`,`bCreateTime`,`bRemark`,`IsDeleted` FROM `BlogArticle` WHERE ( `bID` = @bID0 ) - - - ## 三、控制台信息展示 - - 配置 - - - - ## 四、Nginx一览表 - - - -``` -#user nobody; -worker_processes 1; - -#error_log logs/error.log; -#error_log logs/error.log notice; -#error_log logs/error.log info; - -#pid logs/nginx.pid; -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - server_names_hash_bucket_size 64; - - #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - # '$status $body_bytes_sent "$http_referer" ' - # '"$http_user_agent" "$http_x_forwarded_for"'; - - #access_log logs/access.log main; - sendfile on; - #tcp_nopush on; - - #keepalive_timeout 0; - keepalive_timeout 600; - proxy_read_timeout 600; - proxy_send_timeout 600; - - proxy_buffer_size 128k; - proxy_buffers 32 32k; - proxy_busy_buffers_size 128k; - - #gzip on; - - ###################################################################### - server { - listen 80; - server_name www.neters.club; - - #charset koi8-r; - - #access_log logs/host.access.log main; - location / { - root C:\code\Code\Neters\home; - index index.html index.htm; - } - } - - server { - listen 80; - server_name neters.club; - - #charset koi8-r; - - #access_log logs/host.access.log main; - location / { - root C:\code\Code\Neters\home; - - index index.html index.htm; - } - } - - server { - listen 80; - server_name ids.neters.club; - rewrite ^(.*)$ https://$host$1 permanent;#把http的域名请求转成https,第二种写法在此节的末端 - - #charset koi8-r; - - #access_log logs/host.access.log main; - location / { - #proxy_pass http://localhost:5004; - root html; - index index.html index.htm; - } - } - - server { - listen 443 ssl; - server_name ids.neters.club; #网站域名,和80端口保持一致 - ssl on; - ssl_certificate 1_ids.neters.club_bundle.crt; #证书公钥 - ssl_certificate_key 2_ids.neters.club.key; #证书私钥 - ssl_session_cache shared:SSL:1m; - ssl_session_timeout 5m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers ECDH:AESGCM:HIGH:!RC4:!DH:!MD5:!3DES:!aNULL:!eNULL; - ssl_prefer_server_ciphers on; - - error_page 497 https://$host$uri?$args; - - location / { - proxy_pass http://localhost:5004; - proxy_redirect off; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_set_header Cookie $http_cookie; - #proxy_cookie_path - chunked_transfer_encoding off; - } - } - - server { - listen 80; - server_name apk.neters.club; - - #charset koi8-r; - - #access_log logs/host.access.log main; - location / { - root html; - proxy_pass http://localhost:9291; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection keep-alive; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - index index.html index.htm; - } - - location /.doc/ { - proxy_pass http://docs.neters.club/; - } - } - - server { - listen 80; - server_name docs.neters.club; - - location / { - root C:\code\Code\Blog.Core\.docs\contents\.vuepress\dist; - index index.html index.htm; - } - } - - server { - listen 80; - server_name vueadmin.neters.club; - - location / { - try_files $uri $uri/ /index.html; - root C:\code\Code\Blog.Admin\distis; - #proxy_pass http://localhost:2364; - index index.html index.htm; - } - - location /api/ { - rewrite ^.+apb/?(.*)$ /$1 break; - include uwsgi_params; - proxy_pass http://localhost:9291; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - #proxy_set_header Connection "upgrade"; - #proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - location /api2/ { - rewrite ^.+apb/?(.*)$ /$1 break; - include uwsgi_params; - proxy_pass http://localhost:9291; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - location /images/ { - include uwsgi_params; - proxy_pass http://localhost:9291; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - #proxy_set_header Connection "upgrade"; - #proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - location /.doc/ { - proxy_pass http://docsadmin.neters.club/; - } - - error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root html; - } - } - - server { - listen 80; - server_name docsadmin.neters.club; - - location / { - root C:\code\Code\Blog.Admin\.doc\contents\.vuepress\dist; - index index.html index.htm; - } - } - - - server { - listen 80; - server_name ddd.neters.club; - location / { - proxy_pass http://localhost:4773; - index index.php index.html index.htm; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection keep-alive; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - } - } - - - server { - listen 80; - server_name ask.neters.club; - - #charset koi8-r; - - #access_log logs/host.access.log main; - location / { - root html; - proxy_pass http://localhost:5020; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - #proxy_set_header Connection "upgrade"; - #proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - index index.html index.htm; - } - } - - - server { - listen 80; - server_name vueblog.neters.club; - - location / { - try_files $uri $uri/ /index.html; - root C:\code\Code\Blog.Vue\dist; - index index.html index.htm; - } - - - location /api { - rewrite ^.+apb/?(.*)$ /$1 break; - include uwsgi_params; - proxy_pass http://localhost:9291; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - - location /images { - include uwsgi_params; - proxy_pass http://localhost:9291; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root html; - } - } - - upstream nodenuxt { - server 127.0.0.1:3089; # nuxt 项目监听PC端端口 - keepalive 64; - } - server { - listen 80; - server_name tibug.neters.club; - - location / { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Nginx-Proxy true; - proxy_cache_bypass $http_upgrade; - proxy_pass http://nodenuxt; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root html; - } - } - - server { - listen 80; - server_name jwt.neters.club; - - location / { - root C:\code\Code\jwttoken; - index index.html index.htm; - } - - error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root html; - } - } -} - -``` -> 这里说明下,我的 `Nginx` 文件中,`Ids4` 项目强制使用 `Https` ,采用的是直接跳转,这也是一个办法,当然还有第二种办法(感谢 `tibos`): -``` -server { - listen 80; - server_name admin.wmowm.com; - location / { - proxy_pass http://localhost:9002; - index index.php index.html index.htm; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection keep-alive; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - } -} - -server { - listen 443 ssl;#监听443端口(https默认端口) - server_name admin.wmowm.com; #填写绑定证书的域名 - ssl_certificate /etc/nginx/conf.d/key/admin.wm.crt;#填写你的证书所在的位置 - ssl_certificate_key /etc/nginx/conf.d/key/admin.wm.key;#填写你的key所在的位置 - ssl_session_timeout 5m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置 - ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置 - ssl_prefer_server_ciphers on; - location / { - proxy_pass http://localhost:9002; - index index.php index.html index.htm; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection keep-alive; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - -} -``` \ No newline at end of file diff --git a/.docs/contents/guide/getting-started.md b/.docs/contents/guide/getting-started.md deleted file mode 100644 index 9c4f6b0d..00000000 --- a/.docs/contents/guide/getting-started.md +++ /dev/null @@ -1,132 +0,0 @@ -# K 快速上手 -注意 - -请确保你的 `Visual Studio 2019` 版本 >= `16.8.2`。 -并安装 `.NET 5.0 SDK` - - -## 下载 -Github(国际) 下载 [https://github.com/anjoy8/Blog.Core](https://github.com/anjoy8/Blog.Core) - -Gitee(国内) 下载 [https://gitee.com/laozhangIsPhi/Blog.Core](https://gitee.com/laozhangIsPhi/Blog.Core) - - -## 编译与运行 -1、拿到项目后,双击 `Blog.Core.sln` 解决方案; -2、首先 `F6` 编译,看是否有错误; -3、然后 `F5` 运行,调起 `9291` 端口,浏览器查看效果; -4、因为系统默认的是 `sqlite` 数据库,如果你想换其他数据库,请看下边; -5、注意:本系统是直接自动生成数据库和数据的,不用手动创建数据库; - - - - -## CodeFirst 与 DbFirst -1、项目同时支持两个常见开发模式:`CodeFirst` 和 `DbFirst`; -2、首先 如果你是第一次下载我的项目,肯定是想要浏览效果和直接使用对应的权限相关的内容,这个时候肯定需要用到数据库表结构,那就肯定需要 `CodeFirst` ,只需要在`appsettings.json` 里配置好数据库连接字符串(下文会说到如何配置),就能正确运行; -3、浏览器查看效果,或者配合 `Admin` 项目查看效果后,如果感觉项目可行,并打算在此基础上二次开发,那肯定会在你刚刚创建的数据库种去创建新的表结构,这个时候就需要使用 `DbFirst` 模式,来生成四层项目问题:Model+Service+Repository等; -4、你可以使用T4模板,但是我更建议使用 `/api/DbFirst/GetFrameFiles` 接口来生成,不仅支持多种类型的数据库,还支持同时多库模式的输出; -5、如果你不想用我的表结构和实体类,在项目启动的时候,把配置文件的 `SeedDBEnabled`节点设置成False即可,然后配置对应的你自己的数据库连接字符串,比如是商城的,然后使用 `/api/DbFirst/GetFrameFiles` 接口来生成你的数据库四层类文件; - - - -## 如何配置数据库连接字符串 - -1、打开 `Blog.Core` 项目下的 `appsettings.json` 文件; -2、修改 `DBS` 字节内容,配置对应的连接字符串,注意`DBType`对应不同的数据库类型; -3、把你想要运行的数据库 `Enabled` 为 `true` 即可,其他都要设置 `false`; -4、然后 `MainDB` 设置为下边你使用的指定 `ConnId`: - -``` - "MainDB": "WMBLOG_MSSQL", //当前项目的主库,所对应的连接字符串的Enabled必须为true - "MutiDBEnabled": false, //是否开启多库 - "DBS": [ - { - "ConnId": "WMBLOG_SQLITE", - "DBType": 2,// sqlite数据库 - "Enabled": true,// 设置为true,启用1 - "Connection": "WMBlog.db" //只写数据库名就行 - }, - { - "ConnId": "WMBLOG_MSSQL", - "DBType": 1,// sqlserver数据库 - "Enabled": true,// 设置为true,启用2 - "Connection": "Server=.;Database=WMBlogDB;User ID=sa;Password=123;", - "ProviderName": "System.Data.SqlClient" - }, - { - "ConnId": "WMBLOG_MYSQL", - "DBType": 0,// mysql - "Enabled": false,// false 不启用 - "Connection": "Server=localhost; Port=3306;Stmt=; Database=wmblogdb; Uid=root; Pwd=456;" - }, - { - "ConnId": "WMBLOG_ORACLE", - "DBType": 3,// Oracle - "Enabled": false,// 不启用 - "Connection": "Provider=OraOLEDB.Oracle; Data Source=WMBlogDB; User Id=sss; Password=789;" - } - ], -``` - - -5、如果你想多库操作,需要配置 -``` - a:MainDB 设置为主库的 ConnId; - b:MutiDBEnabled设置为true, - c:把下边想要连接的多个连接字符串都设置为true -``` - -## 如何配置项目端口号 -1、在 `Blog.Core` 层下的 `program.cs` 文件中,将 `9291`端口,修改为自己想要的端口号; -2、或者在 `launchSettings.json` 中设置(`注意,如果仅仅修改这里,publish后,端口访问无效`); - -## 如何项目重命名 -1、双击项目根目录下的 `CreateYourProject.bat` 批处理文件; -2、根据提示,`在Dos窗口内`输入自己想要的项目名称即可; -3、在根目录会有一个 `.1YourProject` 文件夹,里边即你的项目; - - -## 新增实体模块后如何迁移到数据库 -1、在 `Blog.Core.Model` 项目目录下的 `Seed` 文件夹下,找到 `DBSeed` 类; -2、根据提示,找到生成table的地方 `myContext.CreateTableByEntity`; -3、添加进去你新增的实体类,当然也可以用下边的单独写法; -4、编译项目,没错后,运行,则数据库更新完毕; - - -## 新增实体,如何进行增删改查CURD操作 -1、随便找一个含有业务逻辑的 `controller` 参考一下即可; -2、主要 `api` 是通过 `Service` 服务层提供业务逻辑; -3、然后服务层通过 `Repository` 仓储层封装持久化操作; -4、每一个表基本上对应一个仓储类,基本的操作都封装到了 `BaseRepository.cs` 基类仓储中; -5、添加完业务逻辑,记得要 `F6` 重新编译一下,因为项目间引用解耦了; -6、项目已经自动注入了,直接在控制器使用对应的服务层接口就行: `IxxxxService` ; - - -## 新增数据库表,如何反向生成四层文件 -1、可以通过 `T4` 模板来生成,在 `Blog.Core.FrameWork` 层,使用方法: [9757999.html](https://www.cnblogs.com/laozhang-is-phi/p/9757999.html#autoid-4-3-0) ; -> 注意:这种方案,目前默认的只能是 `SqlServer` ,其他类型的数据库,可以看上边文章中的代码,或者群文件里对应的代码。 - -> 1、修改`DbHelper.ttinclude`文件中的连接字符串,注意是`SqlServer`的: public static readonly string ConnectionString; -> 2、然后去各个层模板文件,点击`Ctrl+S`; -> 3、就会在对应的层内,看到新文件,比如:Blog.Core.Model/Model_NEW - - - -2、也可以通过 `Sqlsugar` 所带的方法来实现 `DbFirst`,具体查看 `Controller` 层下的 `DbFirstController.cs`; - -3、总体操作过程,可以参考我的视频:[av77612407](https://www.bilibili.com/video/av77612407?p=2) ; - - -## 发布与部署 -1、双击项目根目录下的 `Blog.Core.Publish.bat`批处理文件; -2、执行完成后,根目录会有一个`.PublishFiles` 文件夹,就是发布后的项目; - - -## 如何更新项目模板 -1、着急的话自己打包,不着急就提 `issue`,等我更新; -2、我的开源项目中,有个模板项目 `BlogCoreTempl` [地址](https://github.com/anjoy8/BlogCoreTempl),下载下来; -3、下载最新的 `Blog.Core` 源代码; -4、将源代码拷贝到模板项目的 `content` 文件夹下; -5、双击 `Package.bat` 文件,就生成了最新的模板了; - diff --git a/.docs/package.json b/.docs/package.json deleted file mode 100644 index 3f0483bf..00000000 --- a/.docs/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "BCVP", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC" -} diff --git a/.gitignore b/.gitignore index de419c90..97072da3 100644 --- a/.gitignore +++ b/.gitignore @@ -358,3 +358,4 @@ Blog.Core.Api/wwwroot/ui/ Blog.Core.Api/Logs *.db /Blog.Core.Api/WMBlog.db-journal +.docs/.vuepress/dist/ From 2ce3e6b7ffa79c8a05cdd155a1a589c6a19c560c Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 24 Nov 2023 20:12:02 +0800 Subject: [PATCH 234/289] =?UTF-8?q?feat=EF=BC=9Aremove=20startup.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Program.cs | 4 +- Blog.Core.Api/Startup.cs | 232 ------------------ .../DependencyInjection/DI_Test.cs | 13 +- 3 files changed, 6 insertions(+), 243 deletions(-) delete mode 100644 Blog.Core.Api/Startup.cs diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 13d7cea2..3138e5ad 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -1,4 +1,5 @@ -// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件 +// 以下为asp.net 6.0的写法,如果用5.0,请看Program.five.cs文件, +// 或者参考github上的.net6.0分支相关代码 using Autofac; using Autofac.Extensions.DependencyInjection; @@ -69,7 +70,6 @@ builder.Services.AddMiniProfilerSetup(); builder.Services.AddSwaggerSetup(); builder.Services.AddJobSetup(); -//builder.Services.AddJobSetup_HostedService(); builder.Services.AddHttpContextSetup(); builder.Services.AddAppTableConfigSetup(builder.Environment); builder.Services.AddHttpApi(); diff --git a/Blog.Core.Api/Startup.cs b/Blog.Core.Api/Startup.cs deleted file mode 100644 index 2911625d..00000000 --- a/Blog.Core.Api/Startup.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Text; -using Autofac; -using Blog.Core.Common; -using Blog.Core.Common.Helper; -using Blog.Core.Common.LogHelper; -using Blog.Core.Common.Seed; -using Blog.Core.Extensions; -using Blog.Core.Extensions.Middlewares; -using Blog.Core.Extensions.ServiceExtensions; -using Blog.Core.Filter; -using Blog.Core.Hubs; -using Blog.Core.IServices; -using Blog.Core.Model; -using Blog.Core.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; - -namespace Blog.Core -{ - public class Startup - { - private IServiceCollection _services; - - public Startup(IConfiguration configuration, IWebHostEnvironment env) - { - Configuration = configuration; - Env = env; - } - - public IConfiguration Configuration { get; } - public IWebHostEnvironment Env { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - // 以下code可能与文章中不一样,对代码做了封装,具体查看右侧 Extensions 文件夹. - services.AddSingleton(new AppSettings(Configuration)); - services.AddUiFilesZipSetup(Env); - - Permissions.IsUseIds4 = AppSettings.app(new string[] { "Startup", "IdentityServer4", "Enabled" }).ObjToBool(); - RoutePrefix.Name = AppSettings.app(new string[] { "AppSettings", "SvcName" }).ObjToString(); - - // 确保从认证中心返回的ClaimType不被更改,不使用Map映射 - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - services.AddCacheSetup(); - services.AddSqlsugarSetup(); - services.AddDbSetup(); - services.AddAutoMapperSetup(); - services.AddCorsSetup(); - services.AddMiniProfilerSetup(); - services.AddSwaggerSetup(); - services.AddJobSetup(); - services.AddHttpContextSetup(); - //services.AddAppConfigSetup(Env); - services.AddAppTableConfigSetup(Env);//表格打印配置 - services.AddHttpApi(); - services.AddRedisInitMqSetup(); - - services.AddRabbitMQSetup(); - services.AddKafkaSetup(Configuration); - services.AddEventBusSetup(); - - services.AddNacosSetup(Configuration); - services.AddInitializationHostServiceSetup(); - // 授权+认证 (jwt or ids4) - services.AddAuthorizationSetup(); - if (Permissions.IsUseIds4) - { - services.AddAuthentication_Ids4Setup(); - } - else - { - services.AddAuthentication_JWTSetup(); - } - - services.AddIpPolicyRateLimitSetup(Configuration); - - services.AddSignalR().AddNewtonsoftJsonProtocol(); - - services.AddScoped(); - - services.Configure(x => x.AllowSynchronousIO = true) - .Configure(x => x.AllowSynchronousIO = true); - - services.AddDistributedMemoryCache(); - services.AddSession(); - services.AddHttpPollySetup(); - - services.AddControllers(o => - { - // 全局异常过滤 - o.Filters.Add(typeof(GlobalExceptionsFilter)); - // 全局路由权限公约 - //o.Conventions.Insert(0, new GlobalRouteAuthorizeConvention()); - // 全局路由前缀,统一修改路由 - o.Conventions.Insert(0, new GlobalRoutePrefixFilter(new RouteAttribute(RoutePrefix.Name))); - }) - // 这种写法也可以 - //.AddJsonOptions(options => - //{ - // options.JsonSerializerOptions.PropertyNamingPolicy = null; - //}) - //MVC全局配置Json序列化处理 - .AddNewtonsoftJson(options => - { - //忽略循环引用 - options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - //不使用驼峰样式的key - options.SerializerSettings.ContractResolver = new DefaultContractResolver(); - //设置时间格式 - options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; - //忽略Model中为null的属性 - //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - //设置本地时间而非UTC时间 - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local; - //添加Enum转string - options.SerializerSettings.Converters.Add(new StringEnumConverter()); - //将long类型转为string - options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); - }); - - services.Replace(ServiceDescriptor.Transient()); - - _services = services; - //支持编码大全 例如:支持 System.Text.Encoding.GetEncoding("GB2312") System.Text.Encoding.GetEncoding("GB18030") - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - } - - // 注意在Program.CreateHostBuilder,添加Autofac服务工厂 - public void ConfigureContainer(ContainerBuilder builder) - { - builder.RegisterModule(new AutofacModuleRegister()); - builder.RegisterModule(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyContext myContext, ITasksQzServices tasksQzServices, ISchedulerCenter schedulerCenter, IHostApplicationLifetime lifetime) - { - // Ip限流,尽量放管道外层 - app.UseIpLimitMiddle(); - // 记录请求与返回数据 - app.UseRequestResponseLogMiddle(); - // 用户访问记录(必须放到外层,不然如果遇到异常,会报错,因为不能返回流) - app.UseRecordAccessLogsMiddle(); - // signalr - app.UseSignalRSendMiddle(); - // 记录ip请求 - app.UseIpLogMiddle(); - // 查看注入的所有服务 - app.UseAllServicesMiddle(_services); - - if (env.IsDevelopment()) - { - // 在开发环境中,使用异常页面,这样可以暴露错误堆栈信息,所以不要放在生产环境。 - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - // 在非开发环境中,使用HTTP严格安全传输(or HSTS) 对于保护web安全是非常重要的。 - // 强制实施 HTTPS 在 ASP.NET Core,配合 app.UseHttpsRedirection - //app.UseHsts(); - } - - app.UseSession(); - app.UseSwaggerAuthorized(); - // 封装Swagger展示 - app.UseSwaggerMiddle(() => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Blog.Core.Api.index.html")); - - // ↓↓↓↓↓↓ 注意下边这些中间件的顺序,很重要 ↓↓↓↓↓↓ - - // CORS跨域 - app.UseCors(AppSettings.app(new string[] { "Startup", "Cors", "PolicyName" })); - // 跳转https - //app.UseHttpsRedirection(); - // 使用静态文件 - DefaultFilesOptions defaultFilesOptions = new DefaultFilesOptions(); - defaultFilesOptions.DefaultFileNames.Clear(); - defaultFilesOptions.DefaultFileNames.Add("index.html"); - app.UseDefaultFiles(defaultFilesOptions); - app.UseStaticFiles(); - // 使用cookie - app.UseCookiePolicy(); - // 返回错误码 - app.UseStatusCodePages(); - // Routing - app.UseRouting(); - // 这种自定义授权中间件,可以尝试,但不推荐 - // app.UseJwtTokenAuth(); - - // 测试用户,用来通过鉴权 - if (Configuration.GetValue("AppSettings:UseLoadTest")) - { - app.UseMiddleware(); - } - // 先开启认证 - app.UseAuthentication(); - // 然后是授权中间件 - app.UseAuthorization(); - //开启性能分析 - app.UseMiniProfilerMiddleware(); - // 开启异常中间件,要放到最后 - //app.UseExceptionHandlerMidd(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - - endpoints.MapHub("/api2/chatHub"); - }); - - // 生成种子数据 - //app.UseSeedDataMiddle(myContext, Env.WebRootPath); - // 开启QuartzNetJob调度服务 - //app.UseQuartzJobMiddleware(tasksQzServices, schedulerCenter); - // 服务注册 - //app.UseConsulMiddle(Configuration, lifetime); - // 事件总线,订阅服务 - //app.ConfigureEventBus(); - } - } -} \ No newline at end of file diff --git a/Blog.Core.Tests/DependencyInjection/DI_Test.cs b/Blog.Core.Tests/DependencyInjection/DI_Test.cs index d425fa01..1ac4d5a7 100644 --- a/Blog.Core.Tests/DependencyInjection/DI_Test.cs +++ b/Blog.Core.Tests/DependencyInjection/DI_Test.cs @@ -1,12 +1,10 @@ using Autofac; using Autofac.Extensions.DependencyInjection; using Autofac.Extras.DynamicProxy; -using AutoMapper; using Blog.Core.AuthHelper; using Blog.Core.Common; using Blog.Core.Common.AppConfig; using Blog.Core.Common.DB; -using Blog.Core.Common.LogHelper; using Blog.Core.Common.Seed; using Blog.Core.Extensions; using Blog.Core.IRepository.Base; @@ -17,9 +15,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; -using System; -using System.Collections.Generic; -using System.IO; using System.Reflection; using System.Security.Claims; using System.Text; @@ -56,7 +51,7 @@ public IContainer DICollections() var basePath = AppContext.BaseDirectory; IServiceCollection services = new ServiceCollection(); - services.AddAutoMapper(typeof(Startup)); + services.AddAutoMapperSetup(); services.AddSingleton(new AppSettings(basePath)); services.AddScoped(); @@ -116,9 +111,9 @@ public IContainer DICollections() // 属性注入 var controllerBaseType = typeof(ControllerBase); - builder.RegisterAssemblyTypes(typeof(Startup).Assembly) - .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType) - .PropertiesAutowired(); + //builder.RegisterAssemblyTypes(typeof(Program).Assembly) + // .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType) + // .PropertiesAutowired(); var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); var assemblysServices = Assembly.LoadFrom(servicesDllFile); From 8188403b60d9b8641ebc0bfee13e3f2590515e11 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 24 Nov 2023 22:49:56 +0800 Subject: [PATCH 235/289] feat: remove some needless code --- Blog.Core.Api/Blog.Core.xml | 1 + Blog.Core.Api/Controllers/BlogController.cs | 2 + Blog.Core.Api/Controllers/LoginController.cs | 2 +- .../LogHelper/Seri/SerilogServer.cs | 76 ---------------- .../LogHelper/Seri/SerilogServer_Es.cs | 89 ------------------- Blog.Core.Common/Seed/DBSeed.cs | 5 -- .../ServiceExtensions/DbSetup.cs | 1 - .../ServiceExtensions/SerilogSetup.cs | 2 - .../ServiceExtensions/SqlsugarSetup.cs | 9 -- 9 files changed, 4 insertions(+), 183 deletions(-) delete mode 100644 Blog.Core.Common/LogHelper/Seri/SerilogServer.cs delete mode 100644 Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 667dcfdc..5b904c4c 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -186,6 +186,7 @@ + diff --git a/Blog.Core.Api/Controllers/BlogController.cs b/Blog.Core.Api/Controllers/BlogController.cs index fbc67e12..d0e9a235 100644 --- a/Blog.Core.Api/Controllers/BlogController.cs +++ b/Blog.Core.Api/Controllers/BlogController.cs @@ -8,6 +8,7 @@ using Blog.Core.SwaggerHelper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Serilog; using StackExchange.Profiling; using static Blog.Core.Extensions.CustomApiVersion; @@ -99,6 +100,7 @@ public async Task> Get(long id) public async Task> DetailNuxtNoPer(long id) { _logger.LogInformation("xxxxxxxxxxxxxxxxxxx"); + Log.Information("yyyyyyyyyyyyyyyyy"); return Success(await _blogArticleServices.GetBlogDetails(id)); } diff --git a/Blog.Core.Api/Controllers/LoginController.cs b/Blog.Core.Api/Controllers/LoginController.cs index abb495c9..f3e9accc 100644 --- a/Blog.Core.Api/Controllers/LoginController.cs +++ b/Blog.Core.Api/Controllers/LoginController.cs @@ -10,7 +10,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Blog.Core.Common.Swagger; -using Serilog; namespace Blog.Core.Controllers @@ -38,6 +37,7 @@ public class LoginController : BaseApiController /// /// /// + /// public LoginController(ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IRoleServices roleServices, PermissionRequirement requirement, IRoleModulePermissionServices roleModulePermissionServices, ILogger logger) diff --git a/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs b/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs deleted file mode 100644 index cb5ec0ff..00000000 --- a/Blog.Core.Common/LogHelper/Seri/SerilogServer.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Blog.Core.Common.Helper; -using Serilog; -using Serilog.Events; -using System; -using System.IO; - -namespace Blog.Core.Common.LogHelper -{ - public class SerilogServer - { - /// - /// 记录日常日志 - /// - /// - /// - /// - public static void WriteLog(string filename, string[] dataParas, bool IsHeader = true, string defaultFolder = "", bool isJudgeJsonFormat = false) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - //.WriteTo.File(Path.Combine($"log/Serilog/{filename}/", ".log"), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}") - .WriteTo.File(Path.Combine("Log", defaultFolder, $"{filename}.log"), - rollingInterval: RollingInterval.Infinite, - outputTemplate: "{Message}{NewLine}{Exception}") - .CreateLogger(); - - var now = DateTime.Now; - string logContent = String.Join("\r\n", dataParas); - var isJsonFormat = true; - if (isJudgeJsonFormat) - { - var judCont = logContent.Substring(0, logContent.LastIndexOf(",")); - isJsonFormat = JsonHelper.IsJson(judCont); - } - - if (isJsonFormat) - { - if (IsHeader) - { - logContent = ( - "--------------------------------\r\n" + - DateTime.Now + "|\r\n" + - String.Join("\r\n", dataParas) + "\r\n" - ); - } - // 展示elk支持输出4种日志级别 - Log.Information(logContent); - //Log.Warning(logContent); - //Log.Error(logContent); - //Log.Debug(logContent); - } - else - { - Console.WriteLine("【JSON格式异常:】"+logContent + now.ObjToString()); - } - Log.CloseAndFlush(); - } - /// - /// 记录异常日志 - /// - /// - /// - /// - public static void WriteErrorLog(string filename, string message, Exception ex) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .WriteTo.File(Path.Combine($"log/Error/{filename}/", ".txt"), rollingInterval: RollingInterval.Day) - .CreateLogger(); - Log.Error(ex, message); - Log.CloseAndFlush(); - } - } -} diff --git a/Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs b/Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs deleted file mode 100644 index 048d5597..00000000 --- a/Blog.Core.Common/LogHelper/Seri/SerilogServer_Es.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Blog.Core.Common.Helper; -using Blog.Core.Serilog.Es; -using Blog.Core.Serilog.Es.Formatters; -using Serilog; -using Serilog.Events; -using System; -using System.IO; - -namespace Blog.Core.Common.LogHelper -{ - public class SerilogServer_Es - { - /// - /// 记录日常日志 - /// - /// - /// - /// - public static void WriteLog(string filename, string[] dataParas, bool IsHeader = true, string defaultFolder = "", bool isJudgeJsonFormat = false) - { - Log.Logger = new LoggerConfiguration() - // TCPSink 集成Serilog 使用tcp的方式向elk 输出log日志 LogstashJsonFormatter 这个是按照自定义格式化输出内容 - .WriteTo.TCPSink(new LogstashJsonFormatter()) - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - //.WriteTo.File(Path.Combine($"log/Serilog/{filename}/", ".log"), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}") - .WriteTo.File(Path.Combine("Log", defaultFolder, $"{filename}.log"), - rollingInterval: RollingInterval.Infinite, - outputTemplate: "{Message}{NewLine}{Exception}") - - // 将日志托送到远程ES - // docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d --name ES01 elasticsearch:7.2.0 - //.Enrich.FromLogContext() - //.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://x.xxx.xx.xx:9200/")) - //{ - // AutoRegisterTemplate = true, - //}) - - .CreateLogger(); - - var now = DateTime.Now; - string logContent = String.Join("\r\n", dataParas); - var isJsonFormat = true; - if (isJudgeJsonFormat) - { - var judCont = logContent.Substring(0, logContent.LastIndexOf(",")); - isJsonFormat = JsonHelper.IsJson(judCont); - } - - if (isJsonFormat) - { - if (IsHeader) - { - logContent = ( - "--------------------------------\r\n" + - DateTime.Now + "|\r\n" + - String.Join("\r\n", dataParas) + "\r\n" - ); - } - // 展示elk支持输出4种日志级别 - Log.Information(logContent); - //Log.Warning(logContent); - //Log.Error(logContent); - //Log.Debug(logContent); - } - else - { - Console.WriteLine("【JSON格式异常:】"+logContent + now.ObjToString()); - } - Log.CloseAndFlush(); - } - /// - /// 记录异常日志 - /// - /// - /// - /// - public static void WriteErrorLog(string filename, string message, Exception ex) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .WriteTo.File(Path.Combine($"log/Error/{filename}/", ".txt"), rollingInterval: RollingInterval.Day) - .CreateLogger(); - Log.Error(ex, message); - Log.CloseAndFlush(); - } - } -} diff --git a/Blog.Core.Common/Seed/DBSeed.cs b/Blog.Core.Common/Seed/DBSeed.cs index 082b9b20..f3f023a0 100644 --- a/Blog.Core.Common/Seed/DBSeed.cs +++ b/Blog.Core.Common/Seed/DBSeed.cs @@ -6,14 +6,9 @@ using Magicodes.ExporterAndImporter.Excel; using Newtonsoft.Json; using SqlSugar; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Reflection; using System.Text; -using System.Threading.Tasks; using Blog.Core.Common.Const; namespace Blog.Core.Common.Seed diff --git a/Blog.Core.Extensions/ServiceExtensions/DbSetup.cs b/Blog.Core.Extensions/ServiceExtensions/DbSetup.cs index b00470ab..1f377bcc 100644 --- a/Blog.Core.Extensions/ServiceExtensions/DbSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/DbSetup.cs @@ -1,6 +1,5 @@ using Blog.Core.Common.Seed; using Microsoft.Extensions.DependencyInjection; -using System; namespace Blog.Core.Extensions { diff --git a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs index 30ab29f8..89cbedd6 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SerilogSetup.cs @@ -6,8 +6,6 @@ using Serilog; using Serilog.Debugging; using Serilog.Events; -using System; -using System.IO; using Blog.Core.Common.Option; namespace Blog.Core.Extensions.ServiceExtensions; diff --git a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs index 99133fe1..0700a74c 100644 --- a/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/SqlsugarSetup.cs @@ -2,19 +2,10 @@ using Blog.Core.Common.Const; using Blog.Core.Common.DB; using Blog.Core.Common.DB.Aop; -using Blog.Core.Common.LogHelper; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using SqlSugar; -using StackExchange.Profiling; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Blog.Core.Common.Caches; -using Blog.Core.Common.Core; -using Blog.Core.Common.HttpContextUser; -using static Grpc.Core.ChannelOption; using System.Text.RegularExpressions; namespace Blog.Core.Extensions From 325baf8e9e2b37485ebb005f80889e5a7467850e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 26 Nov 2023 00:13:23 +0800 Subject: [PATCH 236/289] feat: little change --- Blog.Core.Api/Program.cs | 19 +++++++------------ .../ServiceExtensions/JobSetup.cs | 7 ------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 3138e5ad..61ebc6e7 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -70,15 +70,14 @@ builder.Services.AddMiniProfilerSetup(); builder.Services.AddSwaggerSetup(); builder.Services.AddJobSetup(); + builder.Services.AddHttpContextSetup(); builder.Services.AddAppTableConfigSetup(builder.Environment); builder.Services.AddHttpApi(); builder.Services.AddRedisInitMqSetup(); -builder.Services.AddRabbitMQSetup(); -builder.Services.AddKafkaSetup(builder.Configuration); -builder.Services.AddEventBusSetup(); builder.Services.AddNacosSetup(builder.Configuration); builder.Services.AddInitializationHostServiceSetup(); + builder.Services.AddAuthorizationSetup(); if (Permissions.IsUseIds4 || Permissions.IsUseAuthing) { @@ -114,15 +113,11 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter()); //将long类型转为string options.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64)); - }) - //.AddFluentValidation(config => - //{ - // //程序集方式添加验证 - // config.RegisterValidatorsFromAssemblyContaining(typeof(UserRegisterVoValidator)); - // //是否与MvcValidation共存 - // config.DisableDataAnnotationsValidation = true; - //}) - ; + }); + +builder.Services.AddRabbitMQSetup(); +builder.Services.AddKafkaSetup(builder.Configuration); +builder.Services.AddEventBusSetup(); builder.Services.AddEndpointsApiExplorer(); diff --git a/Blog.Core.Extensions/ServiceExtensions/JobSetup.cs b/Blog.Core.Extensions/ServiceExtensions/JobSetup.cs index 318168d2..da881cb3 100644 --- a/Blog.Core.Extensions/ServiceExtensions/JobSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/JobSetup.cs @@ -2,8 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using Quartz; using Quartz.Spi; -using System; -using System.Linq; using System.Reflection; namespace Blog.Core.Extensions @@ -17,12 +15,7 @@ public static void AddJobSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); - //services.AddHostedService(); - //services.AddHostedService(); - services.AddSingleton(); - //services.AddTransient();//Job使用瞬时依赖注入 - //services.AddTransient();//Job使用瞬时依赖注入 services.AddSingleton(); //任务注入 var baseType = typeof(IJob); From 62cdfb3a5674a453a982f8dde1b7d4b22675d62b Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 26 Nov 2023 15:50:27 +0800 Subject: [PATCH 237/289] =?UTF-8?q?feat=EF=BC=9A=20some=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Blog.Core.xml | 11 +- Blog.Core.Api/Controllers/ValuesController.cs | 31 +-- Blog.Core.Api/Filter/UseServiceDIAttribute.cs | 9 +- Blog.Core.Api/Program.cs | 12 +- Blog.Core.Common/Blog.Core.Common.csproj | 2 - .../ServiceExtensions/AppConfigSetup.cs | 192 ------------------ .../ServiceExtensions/HttpPollySetup.cs | 2 - .../IpPolicyRateLimitSetup.cs | 1 - .../ServiceExtensions/WebApiClientSetup.cs | 33 --- .../DoubanApis/DoubanViewModel.cs | 96 --------- .../WebApiClients/DoubanApis/IDoubanApi.cs | 22 -- .../WebApiClients/HttpApis/IBlogApi.cs | 84 -------- 12 files changed, 14 insertions(+), 481 deletions(-) delete mode 100644 Blog.Core.Extensions/ServiceExtensions/WebApiClientSetup.cs delete mode 100644 Blog.Core.IServices/WebApiClients/DoubanApis/DoubanViewModel.cs delete mode 100644 Blog.Core.IServices/WebApiClients/DoubanApis/IDoubanApi.cs delete mode 100644 Blog.Core.IServices/WebApiClients/HttpApis/IBlogApi.cs diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 5b904c4c..64ee4586 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -758,7 +758,7 @@ Values控制器 - + ValuesController @@ -769,9 +769,8 @@ - - + @@ -854,12 +853,6 @@ - - - 测试http请求 WebApiClient Get - - - 测试Fulent做参数校验 diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 072ab39e..c9cd8dfc 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -3,7 +3,6 @@ using Blog.Core.Common.HttpContextUser; using Blog.Core.Common.Https.HttpPolly; using Blog.Core.Common.Option; -using Blog.Core.Common.WebApiClients.HttpApis; using Blog.Core.EventBus; using Blog.Core.EventBus.EventHandling; using Blog.Core.Extensions; @@ -38,8 +37,6 @@ public class ValuesController : BaseApiController private readonly IRoleModulePermissionServices _roleModulePermissionServices; private readonly IUser _user; private readonly IPasswordLibServices _passwordLibServices; - private readonly IBlogApi _blogApi; - private readonly IDoubanApi _doubanApi; readonly IBlogArticleServices _blogArticleServices; private readonly IHttpPollyHelper _httpPollyHelper; private readonly SeqOptions _seqOptions; @@ -54,17 +51,14 @@ public class ValuesController : BaseApiController /// /// /// - /// - /// /// + /// public ValuesController(IBlogArticleServices blogArticleServices , IMapper mapper , IAdvertisementServices advertisementServices , Love love , IRoleModulePermissionServices roleModulePermissionServices , IUser user, IPasswordLibServices passwordLibServices - , IBlogApi blogApi - , IDoubanApi doubanApi , IHttpPollyHelper httpPollyHelper , IOptions seqOptions) { @@ -77,9 +71,6 @@ public ValuesController(IBlogArticleServices blogArticleServices _user = user; // 测试多库 _passwordLibServices = passwordLibServices; - // 测试http请求 - _blogApi = blogApi; - _doubanApi = doubanApi; // 测试AOP加载顺序,配合 return _blogArticleServices = blogArticleServices; // 测试redis消息队列 @@ -162,11 +153,6 @@ await _blogArticleServices.QuerySql( { bsubmitter = $"laozhang{DateTime.Now.Millisecond}", IsDeleted = false, bID = 5 }); - // 测试模拟异常,全局异常过滤器拦截 - var i = 0; - // var d = 3 / i; - - // 测试 AOP 缓存 var blogArticles = await _blogArticleServices.GetBlogs(); @@ -240,7 +226,6 @@ public void EventBusTry([FromServices] IEventBus _eventBus, string blogId = "1") // GET api/values/5 [HttpGet("{id}")] [AllowAnonymous] - //[TypeFilter(typeof(DeleteSubscriptionCache),Arguments =new object[] { "1"})] [TypeFilter(typeof(UseServiceDIAttribute), Arguments = new object[] { "laozhang" })] public ActionResult Get(int id) { @@ -351,20 +336,6 @@ public async Task TestMutiDBAPI() }; } - /// - /// 测试http请求 WebApiClient Get - /// - /// - [HttpGet("WebApiClientGetAsync")] - [AllowAnonymous] - public async Task WebApiClientGetAsync() - { - int id = 1; - string isbn = "9787544270878"; - var doubanVideoDetail = await _doubanApi.VideoDetailAsync(isbn); - return await _blogApi.DetailNuxtNoPerAsync(id); - } - /// /// 测试Fulent做参数校验 /// diff --git a/Blog.Core.Api/Filter/UseServiceDIAttribute.cs b/Blog.Core.Api/Filter/UseServiceDIAttribute.cs index 867ca0cd..2c487872 100644 --- a/Blog.Core.Api/Filter/UseServiceDIAttribute.cs +++ b/Blog.Core.Api/Filter/UseServiceDIAttribute.cs @@ -1,6 +1,5 @@ using Blog.Core.IServices; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Logging; namespace Blog.Core.Filter { @@ -11,7 +10,7 @@ public class UseServiceDIAttribute : ActionFilterAttribute private readonly IBlogArticleServices _blogArticleServices; private readonly string _name; - public UseServiceDIAttribute(ILogger logger, IBlogArticleServices blogArticleServices,string Name="") + public UseServiceDIAttribute(ILogger logger, IBlogArticleServices blogArticleServices, string Name = "") { _logger = logger; _blogArticleServices = blogArticleServices; @@ -21,14 +20,16 @@ public UseServiceDIAttribute(ILogger logger, IBlogArticle public override void OnActionExecuted(ActionExecutedContext context) { - //var dd =await _blogArticleServices.Query(); + var dd = _blogArticleServices.Query().Result; + _logger.LogInformation("测试自定义服务特性"); + Console.WriteLine(_name); base.OnActionExecuted(context); DeleteSubscriptionFiles(); } private void DeleteSubscriptionFiles() { - + } } } diff --git a/Blog.Core.Api/Program.cs b/Blog.Core.Api/Program.cs index 61ebc6e7..0b9dfd3f 100644 --- a/Blog.Core.Api/Program.cs +++ b/Blog.Core.Api/Program.cs @@ -62,6 +62,7 @@ builder.Services.AddCacheSetup(); builder.Services.AddSqlsugarSetup(); builder.Services.AddDbSetup(); +builder.Services.AddInitializationHostServiceSetup(); builder.Host.AddSerilogSetup(); @@ -73,10 +74,12 @@ builder.Services.AddHttpContextSetup(); builder.Services.AddAppTableConfigSetup(builder.Environment); -builder.Services.AddHttpApi(); -builder.Services.AddRedisInitMqSetup(); +builder.Services.AddHttpPollySetup(); builder.Services.AddNacosSetup(builder.Configuration); -builder.Services.AddInitializationHostServiceSetup(); +builder.Services.AddRedisInitMqSetup(); + +builder.Services.AddIpPolicyRateLimitSetup(builder.Configuration); +builder.Services.AddSignalR().AddNewtonsoftJsonProtocol(); builder.Services.AddAuthorizationSetup(); if (Permissions.IsUseIds4 || Permissions.IsUseAuthing) @@ -89,14 +92,11 @@ builder.Services.AddAuthentication_JWTSetup(); } -builder.Services.AddIpPolicyRateLimitSetup(builder.Configuration); -builder.Services.AddSignalR().AddNewtonsoftJsonProtocol(); builder.Services.AddScoped(); builder.Services.Configure(x => x.AllowSynchronousIO = true) .Configure(x => x.AllowSynchronousIO = true); builder.Services.AddSession(); -builder.Services.AddHttpPollySetup(); builder.Services.AddControllers(o => { o.Filters.Add(typeof(GlobalExceptionsFilter)); diff --git a/Blog.Core.Common/Blog.Core.Common.csproj b/Blog.Core.Common/Blog.Core.Common.csproj index ce966e59..7ac6dfb8 100644 --- a/Blog.Core.Common/Blog.Core.Common.csproj +++ b/Blog.Core.Common/Blog.Core.Common.csproj @@ -37,8 +37,6 @@ - - diff --git a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs index e5e9f339..7cb098ff 100644 --- a/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/AppConfigSetup.cs @@ -1,11 +1,7 @@ using Blog.Core.Common; -using Blog.Core.Common.Helper; -using Blog.Core.Common.LogHelper; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; using System.Text; using Blog.Core.Common.DB; @@ -16,194 +12,6 @@ namespace Blog.Core.Extensions /// public static class AppConfigSetup { - public static void AddAppConfigSetup(this IServiceCollection services, IHostEnvironment env) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - if (AppSettings.app(new string[] { "Startup", "AppConfigAlert", "Enabled" }).ObjToBool()) - { - if (env.IsDevelopment()) - { - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - Console.OutputEncoding = Encoding.GetEncoding("GB2312"); - } - - Console.WriteLine("************ Blog.Core Config Set *****************"); - - ConsoleHelper.WriteSuccessLine("Current environment: " + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")); - - // 授权策略方案 - if (Permissions.IsUseIds4) - { - ConsoleHelper.WriteSuccessLine($"Current authorization scheme: " + (Permissions.IsUseIds4 ? "Ids4" : "JWT")); - } - else - { - Console.WriteLine($"Current authorization scheme: " + (Permissions.IsUseIds4 ? "Ids4" : "JWT")); - } - // 缓存AOP - if (!AppSettings.app(new string[] { "AppSettings", "CachingAOP", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"Caching AOP: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Caching AOP: True"); - } - - // 服务日志AOP - if (!AppSettings.app(new string[] { "AppSettings", "LogAOP", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"Service Log AOP: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Service Log AOP: True"); - } - - // 开启的中间件日志 - var requestResponseLogOpen = AppSettings.app(new string[] { "Middleware", "RequestResponseLog", "Enabled" }).ObjToBool(); - var ipLogOpen = AppSettings.app(new string[] { "Middleware", "IPLog", "Enabled" }).ObjToBool(); - var recordAccessLogsOpen = AppSettings.app(new string[] { "Middleware", "RecordAccessLogs", "Enabled" }).ObjToBool(); - ConsoleHelper.WriteSuccessLine($"OPEN Log: " + - (requestResponseLogOpen ? "RequestResponseLog √," : "") + - (ipLogOpen ? "IPLog √," : "") + - (recordAccessLogsOpen ? "RecordAccessLogs √," : "") - ); - - // 事务AOP - if (!AppSettings.app(new string[] { "AppSettings", "TranAOP", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"Transaction AOP: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Transaction AOP: True"); - } - // 审计AOP - if (!AppSettings.app(new string[] { "AppSettings", "UserAuditAOP", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"UserAudit AOP: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"UserAudit AOP: True"); - } - - // 数据库Sql执行AOP - if (!AppSettings.app(new string[] { "AppSettings", "SqlAOP", "OutToLogFile", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"DB Sql AOP To LogFile: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"DB Sql AOP To LogFile: True"); - } - - // Sql执行日志输出到控制台 - if (!AppSettings.app(new string[] { "AppSettings", "SqlAOP", "OutToConsole", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"DB Sql AOP To Console: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"DB Sql AOP To Console: True"); - } - - // SingnalR发送数据 - if (!AppSettings.app(new string[] { "Middleware", "SignalR", "Enabled" }).ObjToBool()) - { - Console.WriteLine($"SignalR send data: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"SignalR send data: True"); - } - - // IP限流 - if (!AppSettings.app("Middleware", "IpRateLimit", "Enabled").ObjToBool()) - { - Console.WriteLine($"IpRateLimiting: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"IpRateLimiting: True"); - } - - // 性能分析 - if (!AppSettings.app("Startup", "MiniProfiler", "Enabled").ObjToBool()) - { - Console.WriteLine($"MiniProfiler: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"MiniProfiler: True"); - } - - // CORS跨域 - if (!AppSettings.app("Startup", "Cors", "EnableAllIPs").ObjToBool()) - { - Console.WriteLine($"EnableAllIPs For CORS: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"EnableAllIPs For CORS: True"); - } - - // redis消息队列 - if (!AppSettings.app("Startup", "RedisMq", "Enabled").ObjToBool()) - { - Console.WriteLine($"Redis MQ: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Redis MQ: True"); - } - - // RabbitMQ 消息队列 - if (!AppSettings.app("RabbitMQ", "Enabled").ObjToBool()) - { - Console.WriteLine($"RabbitMQ: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"RabbitMQ: True"); - } - - // Consul 注册服务 - if (!AppSettings.app("Middleware", "Consul", "Enabled").ObjToBool()) - { - Console.WriteLine($"Consul service: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Consul service: True"); - } - - // EventBus 事件总线 - if (!AppSettings.app("EventBus", "Enabled").ObjToBool()) - { - Console.WriteLine($"EventBus: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"EventBus: True"); - } - - // 读写分离 - if (!BaseDBConfig.MainConfig.SlaveConnectionConfigs.AnyNoException()) - { - Console.WriteLine($"Is CQRS: False"); - } - else - { - ConsoleHelper.WriteSuccessLine($"Is CQRS: True"); - } - - Console.WriteLine(); - } - } - public static void AddAppTableConfigSetup(this IServiceCollection services, IHostEnvironment env) { if (services == null) throw new ArgumentNullException(nameof(services)); diff --git a/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs b/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs index b3147ca8..8c1d637b 100644 --- a/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/HttpPollySetup.cs @@ -4,8 +4,6 @@ using Polly; using Polly.Extensions.Http; using Polly.Timeout; -using System; -using System.Net.Http; namespace Blog.Core.Extensions { diff --git a/Blog.Core.Extensions/ServiceExtensions/IpPolicyRateLimitSetup.cs b/Blog.Core.Extensions/ServiceExtensions/IpPolicyRateLimitSetup.cs index 61dabc3e..e75e587d 100644 --- a/Blog.Core.Extensions/ServiceExtensions/IpPolicyRateLimitSetup.cs +++ b/Blog.Core.Extensions/ServiceExtensions/IpPolicyRateLimitSetup.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using System; namespace Blog.Core.Extensions { diff --git a/Blog.Core.Extensions/ServiceExtensions/WebApiClientSetup.cs b/Blog.Core.Extensions/ServiceExtensions/WebApiClientSetup.cs deleted file mode 100644 index 7cee91a4..00000000 --- a/Blog.Core.Extensions/ServiceExtensions/WebApiClientSetup.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using WebApiClient.Extensions.DependencyInjection; -using Blog.Core.Common.WebApiClients.HttpApis; - -namespace Blog.Core.Extensions -{ - /// - /// WebApiClientSetup 启动服务 - /// - public static class WebApiClientSetup - { - /// - /// 注册WebApiClient接口 - /// - /// - public static void AddHttpApi(this IServiceCollection services) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - services.AddHttpApi().ConfigureHttpApiConfig(c => - { - c.HttpHost = new Uri("http://apk.neters.club/"); - c.FormatOptions.DateTimeFormat = "yyyy-MM-dd HH:mm:ss.fff"; - }); - services.AddHttpApi().ConfigureHttpApiConfig(c => - { - c.HttpHost = new Uri("http://api.xiaomafeixiang.com/"); - c.FormatOptions.DateTimeFormat = "yyyy-MM-dd HH:mm:ss.fff"; - }); - } - } -} diff --git a/Blog.Core.IServices/WebApiClients/DoubanApis/DoubanViewModel.cs b/Blog.Core.IServices/WebApiClients/DoubanApis/DoubanViewModel.cs deleted file mode 100644 index 0ddb402e..00000000 --- a/Blog.Core.IServices/WebApiClients/DoubanApis/DoubanViewModel.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace Blog.Core.Common.WebApiClients.HttpApis -{ - public class Data - { - /// - /// - /// - public string isbn { get; set; } - /// - /// 解忧杂货店 - /// - public string title { get; set; } - /// - /// ナミヤ雑貨店の奇蹟 - /// - public string origintitle { get; set; } - /// - /// - /// - public string subtitle { get; set; } - /// - /// - /// - public string image { get; set; } - /// - /// [日]东野圭吾 - /// - public string author { get; set; } - /// - /// 李盈春 - /// - public string translator { get; set; } - /// - /// 南海出版公司 - /// - public string publisher { get; set; } - /// - /// - /// - public string pubdate { get; set; } - /// - /// <东野圭吾><治愈><温暖><小说><日本><日本文学><東野圭吾><推理> - /// - public string tags { get; set; } - /// - /// - /// - public string kaiben { get; set; } - /// - /// - /// - public string zhizhang { get; set; } - /// - /// 精装 - /// - public string binding { get; set; } - /// - /// - /// - public string taozhuang { get; set; } - /// - /// 新经典文库·东野圭吾作品 - /// - public string series { get; set; } - /// - /// - /// - public string pages { get; set; } - /// - /// 39.50元 - /// - public string price { get; set; } - - public string author_intro { get; set; } - - public string summary { get; set; } - - public string catalog { get; set; } - } - - public class DoubanViewModel - { - /// - /// - /// - public string status { get; set; } - /// - /// - /// - public Data data { get; set; } - /// - /// 获取图书数据成功 - /// - public string msg { get; set; } - } -} diff --git a/Blog.Core.IServices/WebApiClients/DoubanApis/IDoubanApi.cs b/Blog.Core.IServices/WebApiClients/DoubanApis/IDoubanApi.cs deleted file mode 100644 index c2f1d42d..00000000 --- a/Blog.Core.IServices/WebApiClients/DoubanApis/IDoubanApi.cs +++ /dev/null @@ -1,22 +0,0 @@ -using WebApiClient; -using WebApiClient.Attributes; - -namespace Blog.Core.Common.WebApiClients.HttpApis -{ - /// - /// 豆瓣视频管理 - /// - [TraceFilter] - public interface IDoubanApi : IHttpApi - { - /// - /// 获取电影详情 - /// - /// - [HttpGet("api/bookinfo")] - ITask VideoDetailAsync(string isbn); - - } - - -} diff --git a/Blog.Core.IServices/WebApiClients/HttpApis/IBlogApi.cs b/Blog.Core.IServices/WebApiClients/HttpApis/IBlogApi.cs deleted file mode 100644 index 52ff8aa4..00000000 --- a/Blog.Core.IServices/WebApiClients/HttpApis/IBlogApi.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Blog.Core.Model; -using Blog.Core.Model.Models; -using Blog.Core.Model.ViewModels; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using WebApiClient; -using WebApiClient.Attributes; - -namespace Blog.Core.Common.WebApiClients.HttpApis -{ - /// - /// 博客管理 - /// - [TraceFilter] - public interface IBlogApi : IHttpApi - { - /// - /// 获取博客列表【无权限】 - /// - /// - /// - /// - /// - /// Success - [HttpGet("api/Blog")] - Task>> BlogAsync(int? id, int page, string bcategory, string key); - - /// - /// 添加博客【无权限】 - /// - /// - /// Success - [HttpPost("api/Blog")] - Task> Blog2Async([JsonContent] BlogArticle body); - - /// - /// 获取博客详情 (Auth) - /// - /// - /// Success - [HttpGet("api/Blog/{id}")] - Task> Blog3Async([Required] int id); - - /// - /// apache jemeter 压力测试 - /// 更新接口 - /// - /// Success - [HttpGet("api/Blog/ApacheTestUpdate")] - Task> ApacheTestUpdateAsync(); - - /// - /// 删除博客 (Auth policies: Permission) - /// - /// - /// Success - [HttpDelete("api/Blog/Delete")] - Task> DeleteAsync(int? id); - - /// - /// 获取详情【无权限】 - /// - /// - /// Success - [HttpGet("api/Blog/DetailNuxtNoPer")] - Task> DetailNuxtNoPerAsync(int? id); - - /// - /// 更新博客信息 (Auth) - /// - /// - /// Success - [HttpPut("api/Blog/Update")] - Task> UpdateAsync([JsonContent] BlogArticle body); - - /// - /// 获取博客测试信息 v2版本 - /// - /// Success - [HttpGet("api/V2/Blog/Blogtest")] - Task> BlogtestAsync(); - - } -} From 7a07b85b80e367dea5b8c06b4c52f507f70e2b9f Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 26 Nov 2023 22:17:09 +0800 Subject: [PATCH 238/289] feat: add rabbitmq demo --- Blog.Core.Api/Blog.Core.xml | 13 +++- Blog.Core.Api/Controllers/ValuesController.cs | 66 +++++++++++++++++++ Blog.Core.Api/appsettings.json | 15 +++-- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/Blog.Core.Api/Blog.Core.xml b/Blog.Core.Api/Blog.Core.xml index 64ee4586..dfe9950a 100644 --- a/Blog.Core.Api/Blog.Core.xml +++ b/Blog.Core.Api/Blog.Core.xml @@ -758,7 +758,7 @@ Values控制器 - + ValuesController @@ -770,8 +770,19 @@ + + + + 测试Rabbit消息队列发送 + + + + + 测试Rabbit消息队列订阅 + + 测试SqlSugar二级缓存 diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index c9cd8dfc..1ab4d3c8 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -14,8 +14,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; using System.ComponentModel.DataAnnotations; using System.Linq.Expressions; +using System.Text; namespace Blog.Core.Controllers { @@ -39,6 +42,7 @@ public class ValuesController : BaseApiController private readonly IPasswordLibServices _passwordLibServices; readonly IBlogArticleServices _blogArticleServices; private readonly IHttpPollyHelper _httpPollyHelper; + private readonly IRabbitMQPersistentConnection _persistentConnection; private readonly SeqOptions _seqOptions; /// @@ -52,6 +56,7 @@ public class ValuesController : BaseApiController /// /// /// + /// /// public ValuesController(IBlogArticleServices blogArticleServices , IMapper mapper @@ -60,6 +65,7 @@ public ValuesController(IBlogArticleServices blogArticleServices , IRoleModulePermissionServices roleModulePermissionServices , IUser user, IPasswordLibServices passwordLibServices , IHttpPollyHelper httpPollyHelper + , IRabbitMQPersistentConnection persistentConnection , IOptions seqOptions) { // 测试 Authorize 和 mapper @@ -77,9 +83,69 @@ public ValuesController(IBlogArticleServices blogArticleServices _blogArticleServices = blogArticleServices; // httpPolly _httpPollyHelper = httpPollyHelper; + _persistentConnection = persistentConnection; _seqOptions = seqOptions.Value; } + /// + /// 测试Rabbit消息队列发送 + /// + [HttpGet] + [AllowAnonymous] + public void TestRabbitMqPublish() + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + using var channel = _persistentConnection.CreateModel(); + var message = " < i am a sender! > "; + var body = Encoding.UTF8.GetBytes(message); + var properties = channel.CreateBasicProperties(); + channel.BasicPublish( + exchange: "blogcore", + routingKey: "eventName", + mandatory: true, + basicProperties: properties, + body: body); + } + + /// + /// 测试Rabbit消息队列订阅 + /// + [HttpGet] + [AllowAnonymous] + public void TestRabbitMqSubscribe() + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + string QueueName = "testq"; + using var channel = _persistentConnection.CreateModel(); + var consumer = new AsyncEventingBasicConsumer(channel); + + consumer.Received += new AsyncEventHandler( + async (a, b) => + { + var Headers = b.BasicProperties.Headers; + var msgBody = b.Body.ToArray(); + bool Dealresult = await Dealer(b.Exchange, b.RoutingKey, msgBody, Headers); + if (Dealresult) channel.BasicAck(b.DeliveryTag, false); + else channel.BasicNack(b.DeliveryTag, false, true); + } + ); + channel.BasicConsume(QueueName, false, consumer); + } + + private async Task Dealer(string exchange, string routingKey, byte[] msgBody, IDictionary headers) + { + await Task.CompletedTask; + Console.WriteLine("我是消费者,这里消费了一条信息是:" + Encoding.UTF8.GetString(msgBody)); + return true; + } + [HttpGet] public MessageModel> MyClaims() { diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index ff2c178a..9b599af0 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -23,11 +23,12 @@ "InstanceName": "" //前缀 }, "RabbitMQ": { - "Enabled": false, - "Connection": "118.25.251.13", - "UserName": "", - "Password": "!", - "RetryCount": 3 + "Enabled": true, + "Connection": "101.35.125.157", + "UserName": "admin", + "Password": "admin", + "Port": "5672", + "RetryCount": 2 }, "Kafka": { "Enabled": false, @@ -181,8 +182,8 @@ "Audience": { "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", //不要太短,16位+ "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", //安全。内容就是Secret - "Issuer": "Blog.Core", - "Audience": "wr" + "Issuer": "Blog.Core", //这个值一定要在自己的项目里修改!! + "Audience": "wr" //这个值一定要在自己的项目里修改!! }, "Mongo": { "ConnectionString": "mongodb://nosql.data", From e4eb46a644b7df47813e2a0cbdb4c5ec9991114e Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 26 Nov 2023 22:22:23 +0800 Subject: [PATCH 239/289] Update appsettings.json --- Blog.Core.Api/appsettings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Blog.Core.Api/appsettings.json b/Blog.Core.Api/appsettings.json index 9b599af0..673340be 100644 --- a/Blog.Core.Api/appsettings.json +++ b/Blog.Core.Api/appsettings.json @@ -24,9 +24,9 @@ }, "RabbitMQ": { "Enabled": true, - "Connection": "101.35.125.157", - "UserName": "admin", - "Password": "admin", + "Connection": "101xxxx57", + "UserName": "xxxx", + "Password": "xxxxx", "Port": "5672", "RetryCount": 2 }, From 59e729fa2c97bc26b3ad0d57f5b200a45ae2e18d Mon Sep 17 00:00:00 2001 From: ansonzhang <3143422472@qq.com> Date: Tue, 28 Nov 2023 09:15:24 +0800 Subject: [PATCH 240/289] Update README.md --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2ab20732..b69a5621 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Blog.Core 开箱即用的企业级前后端分离【 .NET Core6.0 Api + Vue 2.x ### 功能与进度 -#### 商业授权付费版下🎁🎁🎁 +#### 企业使用高级版本 - [x] 包含开源版 `框架模块/组件模块` 中的所有功能; - [x] 全部表结构主键底层架构改成`string`类型(默认雪花,支持guid),更方便迁移; @@ -239,10 +239,7 @@ Contributions of any kind are welcome! ## 售后服务与支持 鼓励作者,简单打赏~~ -打赏的时候,备注自己的微信号,加个微信,交个朋友,两天内没回应,QQ私聊我(3143422472); -目前精力有限,主要针对企业级用户答疑,或者购买授权版的个人用户。 - -[赞赏列表](http://apk.neters.club/.doc/Contribution/) +如果你喜欢,就给作者加个鸡腿吧 赞赏码 From c4a6c84d969bd0ef2c4b1a84078ffe6e396d5824 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Fri, 1 Dec 2023 11:18:21 +0800 Subject: [PATCH 241/289] =?UTF-8?q?feat=EF=BC=9A=20:aerial=5Ftramway:=20?= =?UTF-8?q?=20RabbitMQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Api/Controllers/ValuesController.cs | 33 +++----------- .../IRabbitMQPersistentConnection.cs | 25 +++++++++++ .../RabbitMQPersistentConnection.cs | 45 +++++++++++++++++++ 3 files changed, 76 insertions(+), 27 deletions(-) diff --git a/Blog.Core.Api/Controllers/ValuesController.cs b/Blog.Core.Api/Controllers/ValuesController.cs index 1ab4d3c8..9d0c1558 100644 --- a/Blog.Core.Api/Controllers/ValuesController.cs +++ b/Blog.Core.Api/Controllers/ValuesController.cs @@ -92,22 +92,14 @@ public ValuesController(IBlogArticleServices blogArticleServices /// [HttpGet] [AllowAnonymous] - public void TestRabbitMqPublish() + public IActionResult TestRabbitMqPublish() { if (!_persistentConnection.IsConnected) { _persistentConnection.TryConnect(); } - using var channel = _persistentConnection.CreateModel(); - var message = " < i am a sender! > "; - var body = Encoding.UTF8.GetBytes(message); - var properties = channel.CreateBasicProperties(); - channel.BasicPublish( - exchange: "blogcore", - routingKey: "eventName", - mandatory: true, - basicProperties: properties, - body: body); + _persistentConnection.PublishMessage("Hello, RabbitMQ!", exchangeName: "blogcore", routingKey: "myRoutingKey"); + return Ok(); } /// @@ -115,28 +107,15 @@ public void TestRabbitMqPublish() /// [HttpGet] [AllowAnonymous] - public void TestRabbitMqSubscribe() + public IActionResult TestRabbitMqSubscribe() { if (!_persistentConnection.IsConnected) { _persistentConnection.TryConnect(); } - string QueueName = "testq"; - using var channel = _persistentConnection.CreateModel(); - var consumer = new AsyncEventingBasicConsumer(channel); - - consumer.Received += new AsyncEventHandler( - async (a, b) => - { - var Headers = b.BasicProperties.Headers; - var msgBody = b.Body.ToArray(); - bool Dealresult = await Dealer(b.Exchange, b.RoutingKey, msgBody, Headers); - if (Dealresult) channel.BasicAck(b.DeliveryTag, false); - else channel.BasicNack(b.DeliveryTag, false, true); - } - ); - channel.BasicConsume(QueueName, false, consumer); + _persistentConnection.StartConsuming("myQueue"); + return Ok(); } private async Task Dealer(string exchange, string routingKey, byte[] msgBody, IDictionary headers) diff --git a/Blog.Core.EventBus/RabbitMQPersistent/IRabbitMQPersistentConnection.cs b/Blog.Core.EventBus/RabbitMQPersistent/IRabbitMQPersistentConnection.cs index c90b0d4a..bad482ae 100644 --- a/Blog.Core.EventBus/RabbitMQPersistent/IRabbitMQPersistentConnection.cs +++ b/Blog.Core.EventBus/RabbitMQPersistent/IRabbitMQPersistentConnection.cs @@ -10,10 +10,35 @@ namespace Blog.Core.EventBus public interface IRabbitMQPersistentConnection : IDisposable { + /// + /// 是否已经连接 + /// bool IsConnected { get; } + /// + /// 尝试重连 + /// + /// bool TryConnect(); + /// + /// 创建Model + /// + /// IModel CreateModel(); + + /// + /// 发布消息 + /// + /// + /// + /// + void PublishMessage(string message, string exchangeName, string routingKey); + + /// + /// 订阅消息 + /// + /// + void StartConsuming(string queueName); } } diff --git a/Blog.Core.EventBus/RabbitMQPersistent/RabbitMQPersistentConnection.cs b/Blog.Core.EventBus/RabbitMQPersistent/RabbitMQPersistentConnection.cs index be2d8de3..1fa3bd08 100644 --- a/Blog.Core.EventBus/RabbitMQPersistent/RabbitMQPersistentConnection.cs +++ b/Blog.Core.EventBus/RabbitMQPersistent/RabbitMQPersistentConnection.cs @@ -7,6 +7,7 @@ using System; using System.IO; using System.Net.Sockets; +using System.Text; namespace Blog.Core.EventBus { @@ -162,5 +163,49 @@ void OnConnectionShutdown(object sender, ShutdownEventArgs reason) TryConnect(); } + + /// + /// 发布消息 + /// + /// + /// + /// + public void PublishMessage(string message, string exchangeName, string routingKey) + { + using var channel = CreateModel(); + channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct, true); + var body = Encoding.UTF8.GetBytes(message); + channel.BasicPublish(exchange: exchangeName, routingKey: routingKey, basicProperties: null, body: body); + } + + /// + /// 订阅消息 + /// + /// + public void StartConsuming(string queueName) + { + using var channel = CreateModel(); + channel.QueueDeclare(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null); + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.Received += new AsyncEventHandler( + async (a, b) => + { + var Headers = b.BasicProperties.Headers; + var msgBody = b.Body.ToArray(); + var message = Encoding.UTF8.GetString(msgBody); + await Task.CompletedTask; + Console.WriteLine("Received message: {0}", message); + + //bool Dealresult = await Dealer(b.Exchange, b.RoutingKey, msgBody, Headers); + //if (Dealresult) channel.BasicAck(b.DeliveryTag, false); + //else channel.BasicNack(b.DeliveryTag, false, true); + } + ); + + channel.BasicConsume(queue: queueName, autoAck: true, consumer: consumer); + + Console.WriteLine("Consuming messages..."); + } } } From 5f132f03863e1356d22029d5cc4e3840d7826153 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Sun, 3 Dec 2023 18:16:27 +0800 Subject: [PATCH 242/289] =?UTF-8?q?feat=EF=BC=9A=E6=9B=B4=E6=96=B0gateway?= =?UTF-8?q?=EF=BC=8C=E5=8E=BB=E9=99=A4nacos=E7=9B=B8=E5=85=B3=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Blog.Core.Gateway/Blog.Core.Gateway.csproj | 8 + Blog.Core.Gateway/Blog.Core.Gateway.xml | 45 ------ .../Extensions/CustomOcelotSetup.cs | 9 +- .../Extensions/CustomSwaggerSetup.cs | 30 ++-- .../Helper/CustomJwtTokenAuthMiddleware.cs | 38 +---- .../Helper/OcelotConfigurationTask.cs | 147 ------------------ Blog.Core.Gateway/Startup.cs | 23 +-- Blog.Core.Gateway/appsettings.gw.json | 68 +------- Blog.Core.Gateway/ocelot.Development.json | 7 +- 9 files changed, 54 insertions(+), 321 deletions(-) delete mode 100644 Blog.Core.Gateway/Helper/OcelotConfigurationTask.cs diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.csproj b/Blog.Core.Gateway/Blog.Core.Gateway.csproj index 8af7a7e3..a21feb4b 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.csproj +++ b/Blog.Core.Gateway/Blog.Core.Gateway.csproj @@ -12,6 +12,14 @@ + + + + + + + + diff --git a/Blog.Core.Gateway/Blog.Core.Gateway.xml b/Blog.Core.Gateway/Blog.Core.Gateway.xml index 41507d7a..34543342 100644 --- a/Blog.Core.Gateway/Blog.Core.Gateway.xml +++ b/Blog.Core.Gateway/Blog.Core.Gateway.xml @@ -51,50 +51,5 @@ │ 作 者:anson zhang └──────────────────────────────────────────────────────────────┘ - - - Nacos配置文件变更事件 - - - - - Nacos 配置文件监听事件 - - - - - - - - - - - - - - - 执行 - - - - - - - 停止 - - - - - - - 配置监听事件 - - - - - 收到配置文件变更 - - - diff --git a/Blog.Core.Gateway/Extensions/CustomOcelotSetup.cs b/Blog.Core.Gateway/Extensions/CustomOcelotSetup.cs index f04df2a5..b197c824 100644 --- a/Blog.Core.Gateway/Extensions/CustomOcelotSetup.cs +++ b/Blog.Core.Gateway/Extensions/CustomOcelotSetup.cs @@ -16,11 +16,12 @@ public static void AddCustomOcelotSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); - var basePath = AppContext.BaseDirectory; - services.AddAuthentication_JWTSetup(); - services.AddOcelot().AddDelegatingHandler().AddNacosDiscovery().AddPolly(); - //.AddConsul().AddPolly(); + services.AddOcelot() + .AddDelegatingHandler() + //.AddNacosDiscovery() + //.AddConsul() + .AddPolly(); } public static async Task UseCustomOcelotMildd(this IApplicationBuilder app) diff --git a/Blog.Core.Gateway/Extensions/CustomSwaggerSetup.cs b/Blog.Core.Gateway/Extensions/CustomSwaggerSetup.cs index 92956ff1..033feeb5 100644 --- a/Blog.Core.Gateway/Extensions/CustomSwaggerSetup.cs +++ b/Blog.Core.Gateway/Extensions/CustomSwaggerSetup.cs @@ -1,11 +1,16 @@ -using Microsoft.AspNetCore.Builder; +using Blog.Core.Common; +using Blog.Core.Extensions.Middlewares; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Filters; +using Swashbuckle.AspNetCore.SwaggerUI; using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using static Blog.Core.Extensions.CustomApiVersion; namespace Blog.Core.Gateway.Extensions { public static class CustomSwaggerSetup @@ -44,23 +49,30 @@ public static void AddCustomSwaggerSetup(this IServiceCollection services) }); } - public static void UseCustomSwaggerMildd(this IApplicationBuilder app) + public static void UseCustomSwaggerMildd(this IApplicationBuilder app, Func streamHtml) { if (app == null) throw new ArgumentNullException(nameof(app)); var apis = new List { "blog-svc" }; - app.UseMvc().UseSwagger(); - app.UseSwaggerUI(options => + app.UseSwagger(); + app.UseSwaggerUI(c => { - options.SwaggerEndpoint($"/swagger/v1/swagger.json", $"Blog.Core.Gateway-v1"); - + c.SwaggerEndpoint($"/swagger/v1/swagger.json", "gateway"); apis.ForEach(m => { - options.SwaggerEndpoint($"/swagger/apiswg/{m}/swagger.json", m); - options.IndexStream = () => app.GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Blog.Core.ApiGateway.index.html"); + c.SwaggerEndpoint($"/swagger/apiswg/{m}/swagger.json", m); }); - options.RoutePrefix = ""; + + if (streamHtml.Invoke() == null) + { + var msg = "index.html的属性,必须设置为嵌入的资源"; + throw new Exception(msg); + } + + c.IndexStream = streamHtml; + + c.RoutePrefix = ""; }); } diff --git a/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs b/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs index b00db95d..f9120ec0 100644 --- a/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs +++ b/Blog.Core.Gateway/Helper/CustomJwtTokenAuthMiddleware.cs @@ -1,16 +1,9 @@ -using System; -using System.Net; -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Net; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; using Blog.Core.Common; using Blog.Core.Common.Caches; using Blog.Core.Common.Helper; -using Nacos.V2; -using Newtonsoft.Json.Linq; namespace Blog.Core.AuthHelper { @@ -23,7 +16,6 @@ public class CustomJwtTokenAuthMiddleware { private readonly ICaching _cache; - private readonly INacosNamingService NacosServClient; /// /// 验证方案提供对象 @@ -36,13 +28,11 @@ public class CustomJwtTokenAuthMiddleware private readonly RequestDelegate _next; - public CustomJwtTokenAuthMiddleware(INacosNamingService serv, RequestDelegate next, IAuthenticationSchemeProvider schemes, AppSettings appset,ICaching cache) + public CustomJwtTokenAuthMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes, AppSettings appset,ICaching cache) { - NacosServClient = serv; _cache = cache; _next = next; Schemes = schemes; - List Permissions = _cache.Cof_AsyncGetICaching>("Permissions", GetPermitionData, 10).GetAwaiter().GetResult(); } /// @@ -66,7 +56,7 @@ public async Task Invoke(HttpContext httpContext) return; } - List Permissions= await _cache.Cof_AsyncGetICaching>("Permissions", GetPermitionData, 10); + List Permissions= new(); httpContext.Features.Set(new AuthenticationFeature { @@ -126,28 +116,6 @@ public async Task Invoke(HttpContext httpContext) await _next.Invoke(httpContext); } - private async Task> GetPermitionData() - { - try - { - string PermissionServName = AppSettings.GetValue("ApiGateWay:PermissionServName"); - string PermissionServGroup = AppSettings.GetValue("ApiGateWay:PermissionServGroup"); - string PermissionServUrl = AppSettings.GetValue("ApiGateWay:PermissionServUrl"); - - string requestdata = await NacosServClient.Cof_NaoceGet(PermissionServName, PermissionServGroup, PermissionServUrl); - if (string.IsNullOrEmpty(requestdata)) return null; - JToken perJt = JToken.Parse(requestdata); - if(perJt["response"]!=null) return perJt["response"].ToObject>(); - return perJt["data"].ToObject>(); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return null; - } - /// /// 返回相应 /// diff --git a/Blog.Core.Gateway/Helper/OcelotConfigurationTask.cs b/Blog.Core.Gateway/Helper/OcelotConfigurationTask.cs deleted file mode 100644 index a95df86e..00000000 --- a/Blog.Core.Gateway/Helper/OcelotConfigurationTask.cs +++ /dev/null @@ -1,147 +0,0 @@ - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Nacos.V2; -using System; -using System.Threading; -using System.Threading.Tasks; -using Blog.Core.Common.Helper; -using Ocelot.Configuration.Repository; -using Ocelot.Configuration.Creator; -using Newtonsoft.Json.Linq; -using Ocelot.Configuration.File; -using Blog.Core.Common; - -namespace ApiGateway.Helper -{ - /// - /// Nacos配置文件变更事件 - /// - public class OcelotConfigurationTask : BackgroundService - { - private readonly INacosConfigService _configClient; - private readonly INacosNamingService _servClient; - /// - /// Nacos 配置文件监听事件 - /// - private OcelotConfigListener nacosConfigListener = new OcelotConfigListener(); - private AppConfigListener AppConfigListener = new AppConfigListener(); - private string OcelotConfig = ""; - private string OcelotConfigGroup = ""; - private string AppConfig = ""; - private string AppConfigGroup = ""; - - - /// - /// - /// - /// - /// - /// - /// - /// - public OcelotConfigurationTask(INacosNamingService serv, INacosConfigService configClient, IServiceProvider serviceProvider, IInternalConfigurationRepository _internalConfigurationRepo, IInternalConfigurationCreator _internalConfigurationCreator) - { - _configClient = configClient; - _servClient = serv; - nacosConfigListener.internalConfigurationRepo = _internalConfigurationRepo; - nacosConfigListener.internalConfigurationCreator = _internalConfigurationCreator; - OcelotConfig = AppSettings.GetValue("ApiGateWay:OcelotConfig"); - OcelotConfigGroup = AppSettings.GetValue("ApiGateWay:OcelotConfigGroup"); - AppConfig = AppSettings.GetValue("ApiGateWay:AppConfig"); - AppConfigGroup = AppSettings.GetValue("ApiGateWay:AppConfigGroup"); - - - - - string OcelotCfg = configClient.GetConfig(OcelotConfig, OcelotConfigGroup, 10000).GetAwaiter().GetResult(); - nacosConfigListener.ReceiveConfigInfo(OcelotCfg); - string AppCfg= configClient.GetConfig(AppConfig, AppConfigGroup, 10000).GetAwaiter().GetResult(); - AppConfigListener.ReceiveConfigInfo(AppCfg); - //string sss = serv.Cof_NaoceGet("fld-cloud-datax", "DEFAULT_GROUP", "/api/base/deviceList?limit=10&page=1").GetAwaiter().GetResult(); - } - - - - /// - /// 执行 - /// - /// - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - // Add listener OcelotConfig.json" - await _configClient.AddListener(OcelotConfig, OcelotConfigGroup, nacosConfigListener); - await _configClient.AddListener(AppConfig, AppConfigGroup, AppConfigListener); - } - catch (Exception) - { - } - } - - /// - /// 停止 - /// - /// - /// - public override async Task StopAsync(CancellationToken cancellationToken) - { - // Remove listener - await _configClient.RemoveListener(OcelotConfig, OcelotConfigGroup, nacosConfigListener); - await _configClient.RemoveListener(AppConfig, AppConfigGroup, AppConfigListener); - await base.StopAsync(cancellationToken); - } - } - - /// - /// 配置监听事件 - /// - public class OcelotConfigListener : IListener - { - public IInternalConfigurationRepository internalConfigurationRepo { get; set; } - public IInternalConfigurationCreator internalConfigurationCreator { get; set; } - /// - /// 收到配置文件变更 - /// - /// - public void ReceiveConfigInfo(string configInfo) - { - Task.Run(async () => - { - FileConfiguration filecfg = JToken.Parse(configInfo).ToObject(); - var internalConfiguration = await internalConfigurationCreator.Create(filecfg); - if (!internalConfiguration.IsError) - { - - internalConfigurationRepo.AddOrReplace(internalConfiguration.Data); - } - }); - - - } - } - - public class AppConfigListener : IListener - { - public void ReceiveConfigInfo(string configInfo) - { - var _configurationBuilder = new ConfigurationBuilder(); - _configurationBuilder.Sources.Clear(); - var buffer = System.Text.Encoding.Default.GetBytes(configInfo); - System.IO.MemoryStream ms = new System.IO.MemoryStream(buffer); - _configurationBuilder.AddJsonStream(ms); - var configuration = _configurationBuilder.Build(); - ms.Dispose(); - - - - // 读取配置 将nacos配置中心读取到的配置 替换掉.net core 内存中的 configuration - // 当前监听到配置配置 应该重新断开 重连 刷新等一些中间件操作 - // 比如 mq redis 等其他跟配置相关的中间件 - JsonConfigSettings.Configuration = configuration; - AppSettings.Configuration = configuration; - } - } -} diff --git a/Blog.Core.Gateway/Startup.cs b/Blog.Core.Gateway/Startup.cs index 552b4ef1..1a37cc9f 100644 --- a/Blog.Core.Gateway/Startup.cs +++ b/Blog.Core.Gateway/Startup.cs @@ -1,14 +1,10 @@ using Blog.Core.AuthHelper; using Blog.Core.Common; +using Blog.Core.Common.Caches; using Blog.Core.Extensions; using Blog.Core.Gateway.Extensions; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Nacos.V2.DependencyInjection; +using System.Reflection; namespace Blog.Core.AdminMvc { @@ -34,17 +30,10 @@ public void ConfigureServices(IServiceCollection services) { services.AddSingleton(new AppSettings(Configuration)); - services.AddAuthentication_JWTSetup(); - services.AddAuthentication() .AddScheme(Permissions.GWName, _ => { }); - services.AddNacosV2Config(Configuration, null, "nacosConfig"); - services.AddNacosV2Naming(Configuration, null, "nacos"); - services.AddHostedService(); - - services.AddCustomSwaggerSetup(); services.AddControllers(); @@ -53,6 +42,10 @@ public void ConfigureServices(IServiceCollection services) services.AddCorsSetup(); + services.AddMemoryCache(); + services.AddDistributedMemoryCache(); + services.AddSingleton(); + services.AddCustomOcelotSetup(); } @@ -69,7 +62,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseAuthorization(); - app.UseCustomSwaggerMildd(); + app.UseCustomSwaggerMildd(() => Assembly.GetExecutingAssembly().GetManifestResourceStream("Blog.Core.Gateway.index.html")); app.UseCors(AppSettings.app(new string[] { "Startup", "Cors", "PolicyName" })); @@ -79,7 +72,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); app.UseMiddleware(); - + app.UseCustomOcelotMildd().Wait(); } } diff --git a/Blog.Core.Gateway/appsettings.gw.json b/Blog.Core.Gateway/appsettings.gw.json index 33b99ee8..93e64636 100644 --- a/Blog.Core.Gateway/appsettings.gw.json +++ b/Blog.Core.Gateway/appsettings.gw.json @@ -1,6 +1,5 @@ { "Logging": { - "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" @@ -21,6 +20,11 @@ "IPs": "http://127.0.0.1:2364,http://localhost:2364" } }, + "Redis": { + "Enable": false, + "ConnectionString": "127.0.0.1:6379", + "InstanceName": "" //前缀 + }, "Audience": { "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", "SecretFile": "C:\\my-file\\blog.core.audience.secret.txt", @@ -31,73 +35,17 @@ { "url": "/" }, { "url": "/illagal/****" }, { "url": "/api3/****" }, - { "url": "/baseapi/swagger.json" } + { "url": "/baseapi/swagger.json" }, + { "url": "/swagger/v1/swagger.json" }, + { "url": "/swagger/apiswg/blog-svc/swagger.json" } ], "BlackList": [ { "url": "/favicon.ico" } ], - "ApiGateWay": { - "OcelotConfig": "OcelotConfig.json", - "OcelotConfigGroup": "DEFAULT_GROUP", - "AppConfig": "****.****.Gateway.json", - "AppConfigGroup": "DEFAULT_GROUP", - "PermissionServName": "****.****.Api", - "PermissionServGroup": "DEFAULT_GROUP", - "PermissionServUrl": "/api/Permission/GetPermissionlist" - }, "Influxdb": { "Endpoint": "http://*******:9328", "uid": "root", "pwd": "*****", "dbname": "mndata" - }, - "nacos": { - "ServerAddresses": [ "http://******:8848/" ], - "ServiceName": "*****.****.Gateway", - "DefaultTimeOut": 15000, - "Namespace": "****", - "ListenInterval": 1000, - "GroupName": "DEFAULT_GROUP", - "ClusterName": "DEFAULT", - "Ip": "", - "PreferredNetworks": "", - "Port": 8090, - "Weight": 100, - "RegisterEnabled": true, - "InstanceEnabled": true, - "Ephemeral": true, - "Secure": false, - "AccessKey": "", - "SecretKey": "", - "UserName": "****", - "Password": "*****", - "NamingUseRpc": true, - "NamingLoadCacheAtStart": "", - "LBStrategy": "WeightRandom", - "Metadata": { - "aa": "bb", - "cc": "dd", - "endpoint33": "******:8090" - } - }, - "nacosConfig": { - "ServiceName": "*****.*****.Gateway", - "Optional": false, - "DataId": "options1", - "Tenant": "******", - "Group": "DEFAULT_GROUP", - "Namespace": "*****", - "ServerAddresses": [ "http://******:8848/" ], - "UserName": "****", - "Password": "*****", - "AccessKey": "", - "SecretKey": "", - "EndPoint": "", - "ConfigUseRpc": true, - "ConfigFilterAssemblies": [ "apigateway" ], - "ConfigFilterExtInfo": "{\"JsonPaths\":[\"ConnectionStrings.Default\"],\"Other\":\"xxxxxx\"}" } - - - } diff --git a/Blog.Core.Gateway/ocelot.Development.json b/Blog.Core.Gateway/ocelot.Development.json index 6af5171b..589a5a6e 100644 --- a/Blog.Core.Gateway/ocelot.Development.json +++ b/Blog.Core.Gateway/ocelot.Development.json @@ -49,11 +49,6 @@ ], "GlobalConfiguration": { - "BaseUrl": "http://localhost:9000", - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8500, - "Type": "Consul" - } + "BaseUrl": "http://localhost:9000" } } \ No newline at end of file From 5e4987d2f6921da920c29841d24cea5977f5a465 Mon Sep 17 00:00:00 2001 From: anjoy8 <3143422472@qq.com> Date: Wed, 13 Dec 2023 11:04:04 +0800 Subject: [PATCH 243/289] feat: :100: change ClaimTypes.Role --- .../Controllers/PermissionController.cs | 17 +++++++++++++++-- Blog.Core.Api/index.html | 4 ++-- .../Authorizations/Policys/PermissionHandler.cs | 14 ++++---------- Blog.Core.Model/Logs/AuditSqlLog.cs | 2 +- Blog.Core.Model/Logs/GlobalErrorLog.cs | 2 +- Blog.Core.Model/Logs/GlobalInformationLog.cs | 2 +- Blog.Core.Model/Logs/GlobalWarningLog.cs | 2 +- Blog.Core.Model/Models/SplitDemo.cs | 2 +- 8 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Blog.Core.Api/Controllers/PermissionController.cs b/Blog.Core.Api/Controllers/PermissionController.cs index a3429163..7f412a04 100644 --- a/Blog.Core.Api/Controllers/PermissionController.cs +++ b/Blog.Core.Api/Controllers/PermissionController.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Security.Claims; namespace Blog.Core.Controllers { @@ -349,8 +350,14 @@ public async Task> GetNavigationBar(long uid) where item.Type == "sub" select item.Value).FirstOrDefault().ObjToLong(); roleIds = (from item in _httpContext.HttpContext.User.Claims - where item.Type == "role" + where item.Type == ClaimTypes.Role select item.Value.ObjToLong()).ToList(); + if (!roleIds.Any()) + { + roleIds = (from item in _httpContext.HttpContext.User.Claims + where item.Type == "role" + select item.Value.ObjToLong()).ToList(); + } } else { @@ -440,8 +447,14 @@ public async Task>> GetNavigationBarPro(long where item.Type == "sub" select item.Value).FirstOrDefault().ObjToLong(); roleIds = (from item in _httpContext.HttpContext.User.Claims - where item.Type == "role" + where item.Type == ClaimTypes.Role select item.Value.ObjToLong()).ToList(); + if (!roleIds.Any()) + { + roleIds = (from item in _httpContext.HttpContext.User.Claims + where item.Type == "role" + select item.Value.ObjToLong()).ToList(); + } } else { diff --git a/Blog.Core.Api/index.html b/Blog.Core.Api/index.html index eca0b424..99b4caa4 100644 --- a/Blog.Core.Api/index.html +++ b/Blog.Core.Api/index.html @@ -111,9 +111,9 @@