From 140705968893f72c9123fc2cbe20ddfebff9407f Mon Sep 17 00:00:00 2001 From: shiningrise Date: Wed, 17 Sep 2025 13:49:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B2=A1=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin.NET.Core/Entity/SysFileContent.cs | 52 +- .../Admin.NET.Core/Entity/SysWechatRefund.cs | 262 +-- .../Admin.NET.Core/Option/UploadOptions.cs | 10 +- .../File/FileProvider/DbFileProvider.cs | 68 +- .../Service/File/SysFileService.cs | 926 +++++------ .../Service/Log/SysLogDiffService.cs | 116 +- .../Service/Region/SysRegionService.cs | 1470 ++++++++--------- .../Service/User/Dto/UserInput.cs | 16 +- 8 files changed, 1460 insertions(+), 1460 deletions(-) diff --git a/Admin.NET/Admin.NET.Core/Entity/SysFileContent.cs b/Admin.NET/Admin.NET.Core/Entity/SysFileContent.cs index 896e6ac1..9c298421 100644 --- a/Admin.NET/Admin.NET.Core/Entity/SysFileContent.cs +++ b/Admin.NET/Admin.NET.Core/Entity/SysFileContent.cs @@ -1,27 +1,27 @@ -// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 -// -// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 -// -// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! - -namespace Admin.NET.Core; - -/// -/// 系统文件内容表 -/// -[SugarTable(null, "系统文件内容表")] -[SysTable] -public partial class SysFileContent : EntityBase -{ - /// - /// SysFile里的Id - /// - [SugarColumn(ColumnDescription = "SysFile里的Id")] - public long SysFileId { get; set; } - - /// - /// 文件内容 - /// - [SugarColumn(ColumnDescription = "文件内容", IsNullable = false)] - public byte[] Content { get; set; } +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统文件内容表 +/// +[SugarTable(null, "系统文件内容表")] +[SysTable] +public partial class SysFileContent : EntityBase +{ + /// + /// SysFile里的Id + /// + [SugarColumn(ColumnDescription = "SysFile里的Id")] + public long SysFileId { get; set; } + + /// + /// 文件内容 + /// + [SugarColumn(ColumnDescription = "文件内容", IsNullable = false)] + public byte[] Content { get; set; } } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs b/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs index fe13f77e..cb87b732 100644 --- a/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs +++ b/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs @@ -1,132 +1,132 @@ -// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 -// -// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 -// -// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! - -namespace Admin.NET.Core; - -/// -/// 系统微信退款表 -/// -[SugarTable(null, "系统微信退款表")] -[SysTable] -[SugarIndex("i_{table}_o", nameof(OrderId), OrderByType.Desc)] -public class SysWechatRefund : EntityBase -{ - /// - /// 微信支付订单号(原支付交易对应的微信订单号) - /// - [SugarColumn(ColumnDescription = "微信支付订单号", Length = 32)] - [Required] - public string TransactionId { get; set; } - - /// - /// 商户订单号(原交易对应的商户付款单号) - /// - [SugarColumn(ColumnDescription = "商户付款单号", Length = 32)] - [Required] - public string OutTradeNumber { get; set; } - - /// - /// 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 - /// - [SugarColumn(ColumnDescription = "商户退款单号", Length = 64)] - [Required] - public string OutRefundNumber { get; set; } - - /// - /// 微信接口退款ID - /// - public string RefundId { get; set; } - - /// - /// 退款原因,示例:商品已售完 - /// - [SugarColumn(ColumnDescription = "退款原因", Length = 80)] - public string Reason { get; set; } - - /// - /// 退款金额 - /// - [SugarColumn(ColumnDescription = "退款金额")] - public int Refund { get; set; } - - /// - /// 原订单总金额 - /// - [SugarColumn(ColumnDescription = "订单总金额")] - public int Total { get; set; } - - /// - /// 退款结果回调url - /// - [SugarColumn(ColumnDescription = "退款结果回调url", Length = 256)] - public string? NotifyUrl { get; set; } - - /// - /// 退款资金来源, 可不传,默认使用未结算资金退款(仅对老资金流商户适用) - /// - [SugarColumn(ColumnDescription = "退款资金来源", Length = 32)] - public string? FundsAccount { get; set; } - - /// - /// 关联的商户订单号 - /// - [SugarColumn(ColumnDescription = "关联的用户订单号", Length = 256)] - public string? OrderId { get; set; } - - /// - /// 关联的商户订单状态(或者为第几次支付,有些订单涉及多次支付,比如先付预付款,后补尾款) - /// - [SugarColumn(ColumnDescription = "关联的商户订单状态", Length = 32)] - public string? RefundStatus { get; set; } - - /// - /// 支完成时间 - /// - [SugarColumn(ColumnDescription = "完成时间")] - public DateTime? SuccessTime { get; set; } - - /// - /// 关联的商户商品编码 - /// - [SugarColumn(ColumnDescription = "关联的商户商品编码", Length = 32)] - public string? MerchantGoodsId { get; set; } - - /// - /// 关联的商户商品名称 - /// - [SugarColumn(ColumnDescription = "关联的商户商品名称", Length = 256)] - public string? GoodsName { get; set; } - - /// - /// 关联的商户商品单价 - /// - [SugarColumn(ColumnDescription = "关联的商户商品单价")] - public int UnitPrice { get; set; } - - /// - /// 关联的商户商品退款金额 - /// - [SugarColumn(ColumnDescription = "关联的商户商品退款金额")] - public int RefundAmount { get; set; } - - /// - /// 关联的商户商品退货数量 - /// - [SugarColumn(ColumnDescription = "关联的商户商品退货数量")] - public int RefundQuantity { get; set; } = 1; - - /// - /// 附加数据 - /// - [SugarColumn(ColumnDescription = "附加数据")] - public string? Attachment { get; set; } - - /// - /// 备注 - /// - [SugarColumn(ColumnDescription = "备注", ColumnDataType = StaticConfig.CodeFirst_BigString)] - public string? Remark { get; set; } +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信退款表 +/// +[SugarTable(null, "系统微信退款表")] +[SysTable] +[SugarIndex("i_{table}_o", nameof(OrderId), OrderByType.Desc)] +public class SysWechatRefund : EntityBase +{ + /// + /// 微信支付订单号(原支付交易对应的微信订单号) + /// + [SugarColumn(ColumnDescription = "微信支付订单号", Length = 32)] + [Required] + public string TransactionId { get; set; } + + /// + /// 商户订单号(原交易对应的商户付款单号) + /// + [SugarColumn(ColumnDescription = "商户付款单号", Length = 32)] + [Required] + public string OutTradeNumber { get; set; } + + /// + /// 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 + /// + [SugarColumn(ColumnDescription = "商户退款单号", Length = 64)] + [Required] + public string OutRefundNumber { get; set; } + + /// + /// 微信接口退款ID + /// + public string RefundId { get; set; } + + /// + /// 退款原因,示例:商品已售完 + /// + [SugarColumn(ColumnDescription = "退款原因", Length = 80)] + public string Reason { get; set; } + + /// + /// 退款金额 + /// + [SugarColumn(ColumnDescription = "退款金额")] + public int Refund { get; set; } + + /// + /// 原订单总金额 + /// + [SugarColumn(ColumnDescription = "订单总金额")] + public int Total { get; set; } + + /// + /// 退款结果回调url + /// + [SugarColumn(ColumnDescription = "退款结果回调url", Length = 256)] + public string? NotifyUrl { get; set; } + + /// + /// 退款资金来源, 可不传,默认使用未结算资金退款(仅对老资金流商户适用) + /// + [SugarColumn(ColumnDescription = "退款资金来源", Length = 32)] + public string? FundsAccount { get; set; } + + /// + /// 关联的商户订单号 + /// + [SugarColumn(ColumnDescription = "关联的用户订单号", Length = 256)] + public string? OrderId { get; set; } + + /// + /// 关联的商户订单状态(或者为第几次支付,有些订单涉及多次支付,比如先付预付款,后补尾款) + /// + [SugarColumn(ColumnDescription = "关联的商户订单状态", Length = 32)] + public string? RefundStatus { get; set; } + + /// + /// 支完成时间 + /// + [SugarColumn(ColumnDescription = "完成时间")] + public DateTime? SuccessTime { get; set; } + + /// + /// 关联的商户商品编码 + /// + [SugarColumn(ColumnDescription = "关联的商户商品编码", Length = 32)] + public string? MerchantGoodsId { get; set; } + + /// + /// 关联的商户商品名称 + /// + [SugarColumn(ColumnDescription = "关联的商户商品名称", Length = 256)] + public string? GoodsName { get; set; } + + /// + /// 关联的商户商品单价 + /// + [SugarColumn(ColumnDescription = "关联的商户商品单价")] + public int UnitPrice { get; set; } + + /// + /// 关联的商户商品退款金额 + /// + [SugarColumn(ColumnDescription = "关联的商户商品退款金额")] + public int RefundAmount { get; set; } + + /// + /// 关联的商户商品退货数量 + /// + [SugarColumn(ColumnDescription = "关联的商户商品退货数量")] + public int RefundQuantity { get; set; } = 1; + + /// + /// 附加数据 + /// + [SugarColumn(ColumnDescription = "附加数据")] + public string? Attachment { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Remark { get; set; } } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs b/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs index 4ff9e1a0..dc64448c 100644 --- a/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs +++ b/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs @@ -32,11 +32,11 @@ public sealed class UploadOptions : IConfigurableOptions /// 启用文件MD5验证 /// /// 防止重复上传 - public bool EnableMd5 { get; set; } - - /// - /// 启用文件存储到数据库 - /// + public bool EnableMd5 { get; set; } + + /// + /// 启用文件存储到数据库 + /// public bool EnableSaveDb { get; set; } } diff --git a/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DbFileProvider.cs b/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DbFileProvider.cs index ed3f0430..6872a3f2 100644 --- a/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DbFileProvider.cs +++ b/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DbFileProvider.cs @@ -1,9 +1,9 @@ -// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 -// -// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 -// -// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! - +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + namespace Admin.NET.Core.Service; public class DbFileProvider : ICustomFileProvider, ITransient @@ -22,54 +22,54 @@ public class DbFileProvider : ICustomFileProvider, ITransient } public async Task DownloadFileBase64Async(SysFile sysFile) - { - // 若先前配置成保存到本地文件 - if (string.IsNullOrEmpty(sysFile.Provider) || sysFile.Provider == "Local") - { - var provider = App.GetService(); - return await provider.DownloadFileBase64Async(sysFile); - } - else - { - // 从数据库获取文件内容 + { + // 若先前配置成保存到本地文件 + if (string.IsNullOrEmpty(sysFile.Provider) || sysFile.Provider == "Local") + { + var provider = App.GetService(); + return await provider.DownloadFileBase64Async(sysFile); + } + else + { + // 从数据库获取文件内容 var fileContent = await _sysFileContentRep.CopyNew().GetFirstAsync(u => u.SysFileId == sysFile.Id); if (fileContent == null || fileContent.Content == null) { Log.Error($"DbFileProvider.DownloadFileBase64:文件[{sysFile.Id},{sysFile.Url}]内容不存在"); throw Oops.Oh($"文件[{sysFile.FilePath}]内容不存在"); } - return Convert.ToBase64String(fileContent.Content); + return Convert.ToBase64String(fileContent.Content); } } public async Task GetFileStreamResultAsync(SysFile sysFile, string fileName) { - // 若先前配置成保存到本地文件 - if (string.IsNullOrEmpty(sysFile.Provider) || sysFile.Provider == "Local") - { - var provider = App.GetService(); - return await provider.GetFileStreamResultAsync(sysFile, fileName); - } - else - { - // 从数据库获取文件内容 - var fileContent = await _sysFileContentRep.GetFirstAsync(u => u.SysFileId == sysFile.Id); + // 若先前配置成保存到本地文件 + if (string.IsNullOrEmpty(sysFile.Provider) || sysFile.Provider == "Local") + { + var provider = App.GetService(); + return await provider.GetFileStreamResultAsync(sysFile, fileName); + } + else + { + // 从数据库获取文件内容 + var fileContent = await _sysFileContentRep.GetFirstAsync(u => u.SysFileId == sysFile.Id); if (fileContent == null || fileContent.Content == null) { Log.Error($"DbFileProvider.GetFileStreamResultAsync:文件[{sysFile.Id},{sysFile.Url}]内容不存在"); throw Oops.Oh($"文件[{sysFile.FilePath}]内容不存在"); - } - // 创建内存流 - var memoryStream = new MemoryStream(fileContent.Content); - return new FileStreamResult(memoryStream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix }; + } + // 创建内存流 + var memoryStream = new MemoryStream(fileContent.Content); + return new FileStreamResult(memoryStream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix }; } } public async Task UploadFileAsync(IFormFile file, SysFile newFile, string path, string finalName) { - newFile.Provider = "Database"; // 数据库存储 Provider 显示为Database - - // 读取文件内容到字节数组 + newFile.Provider = "Database"; // 数据库存储 Provider 显示为Database + + // 读取文件内容到字节数组 byte[] fileContent; using (var memoryStream = new MemoryStream()) { diff --git a/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs b/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs index 063ccc45..5f2aae9e 100644 --- a/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs +++ b/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs @@ -1,464 +1,464 @@ -// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 -// -// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 -// -// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! - -using Furion.AspNetCore; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Admin.NET.Core.Service; - -/// -/// 系统文件服务 🧩 -/// -[ApiDescriptionSettings(Order = 410, Description = "系统文件")] -public class SysFileService : IDynamicApiController, ITransient -{ - private readonly UserManager _userManager; - private readonly SqlSugarRepository _sysFileRep; - private readonly OSSProviderOptions _OSSProviderOptions; - private readonly UploadOptions _uploadOptions; - private readonly INamedServiceProvider _namedServiceProvider; - private readonly ICustomFileProvider _customFileProvider; - private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif.svg"; - - public SysFileService(UserManager userManager, - SqlSugarRepository sysFileRep, - IOptions oSSProviderOptions, - IOptions uploadOptions, - INamedServiceProvider namedServiceProvider) - { - _namedServiceProvider = namedServiceProvider; - _userManager = userManager; - _sysFileRep = sysFileRep; - _OSSProviderOptions = oSSProviderOptions.Value; - _uploadOptions = uploadOptions.Value; - if (_OSSProviderOptions.Enabled) - { - _customFileProvider = _namedServiceProvider.GetService(nameof(OSSFileProvider)); - } - else if (App.Configuration["SSHProvider:Enabled"].ToBoolean()) - { - _customFileProvider = _namedServiceProvider.GetService(nameof(SSHFileProvider)); - } - else if (_uploadOptions.EnableSaveDb) - { - _customFileProvider = _namedServiceProvider.GetService(nameof(DbFileProvider)); - } - else - { - _customFileProvider = _namedServiceProvider.GetService(nameof(DefaultFileProvider)); - } - } - - /// - /// 获取文件分页列表 🔖 - /// - /// - /// - [DisplayName("获取文件分页列表")] - public async Task> Page(PageFileInput input) - { - // 获取所有公开文件 - var publicList = _sysFileRep.AsQueryable().ClearFilter().Where(u => u.IsPublic == true); - // 获取私有文件 - var privateList = _sysFileRep.AsQueryable().Where(u => u.IsPublic == false); - // 合并公开和私有并分页 - return await _sysFileRep.Context.UnionAll(publicList, privateList) - .WhereIF(!string.IsNullOrWhiteSpace(input.FileName), u => u.FileName.Contains(input.FileName.Trim())) - .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()), - u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime) - .OrderBy(u => u.CreateTime, OrderByType.Desc) - .ToPagedListAsync(input.Page, input.PageSize); - } - - /// - /// 上传文件Base64 🔖 - /// - /// - /// - [DisplayName("上传文件Base64")] - public async Task UploadFileFromBase64(UploadFileFromBase64Input input) - { - var pattern = @"data:(?.+?);base64,(?[^""]+)"; - var regex = new Regex(pattern, RegexOptions.Compiled); - var match = regex.Match(input.FileDataBase64); - - byte[] fileData = Convert.FromBase64String(match.Groups["data"].Value); - var contentType = match.Groups["type"].Value; - if (string.IsNullOrEmpty(input.FileName)) - input.FileName = $"{YitIdHelper.NextId()}.{contentType.AsSpan(contentType.LastIndexOf('/') + 1)}"; - - var ms = new MemoryStream(); - ms.Write(fileData); - ms.Seek(0, SeekOrigin.Begin); - IFormFile formFile = new FormFile(ms, 0, fileData.Length, "file", input.FileName) - { - Headers = new HeaderDictionary(), - ContentType = contentType - }; - var uploadFileInput = input.Adapt(); - uploadFileInput.File = formFile; - return await UploadFile(uploadFileInput); - } - - /// - /// 上传多文件 🔖 - /// - /// - /// - [DisplayName("上传多文件")] - public List UploadFiles([Required] List files) - { - var fileList = new List(); - files.ForEach(file => fileList.Add(UploadFile(new UploadFileInput { File = file }).Result)); - return fileList; - } - - /// - /// 根据文件Id或Url下载 🔖 - /// - /// - /// - [DisplayName("根据文件Id或Url下载")] - public async Task DownloadFile(SysFile input) - { - var file = input.Id > 0 ? await GetFile(input.Id) : await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == input.Url); - var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); - return await GetFileStreamResult(file, fileName); - } - - /// - /// 根据文件Id和MD5下载(与db存储模式路径对应) 🔖 - /// - /// - /// - /// - [HttpGet("/upload/downloadfile")] - [DisplayName("根据文件Id和MD5下载")] - [AllowAnonymous] - public async Task DownloadFile2([FromQuery] long id, [FromQuery] string fileMd5) - { - var file = await GetFile(id); - if (file.FileMd5 != null && file.FileMd5 != fileMd5) throw Oops.Oh("文件校验信息不符"); - - var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); - return await GetFileStreamResult(file, fileName); - } - - /// - /// 文件预览 🔖 - /// - /// - /// - [DisplayName("文件预览")] - public async Task GetPreview([FromRoute] long id) - { - var file = await GetFile(id); - //var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); - return await GetFileStreamResult(file, file.Id + ""); - } - - /// - /// 获取文件流 - /// - /// - /// - /// - private async Task GetFileStreamResult(SysFile file, string fileName) - { - return await _customFileProvider.GetFileStreamResultAsync(file, fileName); - } - - /// - /// 下载指定文件Base64格式 🔖 - /// - /// - /// - [DisplayName("下载指定文件Base64格式")] - public async Task DownloadFileBase64([FromBody] string url) - { - var sysFile = await _sysFileRep.AsQueryable().ClearFilter().FirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在"); - return await _customFileProvider.DownloadFileBase64Async(sysFile); - } - - /// - /// 删除文件 🔖 - /// - /// - /// - [ApiDescriptionSettings(Name = "Delete"), HttpPost] - [DisplayName("删除文件")] - public async Task DeleteFile(BaseIdInput input) - { - var file = await _sysFileRep.GetByIdAsync(input.Id) ?? throw Oops.Oh($"文件不存在"); - await _sysFileRep.DeleteAsync(file); - await _customFileProvider.DeleteFileAsync(file); - } - - /// - /// 更新文件 🔖 - /// - /// - /// - [ApiDescriptionSettings(Name = "Update"), HttpPost] - [DisplayName("更新文件")] - public async Task UpdateFile(SysFile input) - { - var isExist = await _sysFileRep.IsAnyAsync(u => u.Id == input.Id); - if (!isExist) throw Oops.Oh(ErrorCodeEnum.D8000); - - await _sysFileRep.AsUpdateable(input).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); - } - - /// - /// 获取文件 🔖 - /// - /// - /// - /// - [DisplayName("获取文件")] - public async Task GetFile([FromQuery] long id, [FromQuery] bool isClearFilter = false) - { - var file = isClearFilter - ? await _sysFileRep.CopyNew().AsQueryable().ClearFilter().FirstAsync(u => u.Id == id) - : await _sysFileRep.CopyNew().GetByIdAsync(id); - return file ?? throw Oops.Oh(ErrorCodeEnum.D8000); - } - - /// - /// 根据文件Id集合获取文件 🔖 - /// - /// - /// - [DisplayName("根据文件Id集合获取文件")] - public async Task> GetFileByIds([FromQuery][FlexibleArray] List ids) - { - return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(); - } - - /// - /// 获取文件路径 🔖 - /// - /// - [DisplayName("获取文件路径")] - public async Task> GetFolder() - { - var files = await _sysFileRep.AsQueryable().ToListAsync(); - var folders = files.GroupBy(u => u.FilePath).Select(u => u.First().FilePath).ToList(); - - var pathTreeBuilder = new PathTreeBuilder(); - var tree = pathTreeBuilder.BuildTree(folders); - return tree.Children; - } - - /// - /// 上传文件 🔖 - /// - /// - /// 存储目标路径 - /// - [DisplayName("上传文件")] - public async Task UploadFile([FromForm] UploadFileInput input, [BindNever] string targetPath = "") - { - if (input.File == null || input.File.Length <= 0) throw Oops.Oh(ErrorCodeEnum.D8000); - - if (input.File.FileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) throw Oops.Oh(ErrorCodeEnum.D8005); - - // 判断是否重复上传的文件 - var sizeKb = input.File.Length / 1024; // 大小KB - var fileMd5 = string.Empty; - // 计算文件MD5(长度为32的固定字符串) - await using (var fileStream = input.File.OpenReadStream()) - { - byte[] bsFile = new byte[fileStream.Length]; - _ = fileStream.Read(bsFile, 0, bsFile.Length); - fileMd5 = MD5Encryption.Encrypt(bsFile); - } - if (_uploadOptions.EnableMd5) - { - // Mysql8 中如果使用了 utf8mb4_general_ci 之外的编码会出错,尽量避免在条件里使用.ToString() - // 因为 Squsugar 并不是把变量转换为字符串来构造SQL语句,而是构造了CAST(123 AS CHAR)这样的语句,这样这个返回值是utf8mb4_general_ci,所以容易出错。 - var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileMd5 == fileMd5 && u.SizeKb == sizeKb); - if (sysFile != null) return sysFile; - } - - // 验证文件类型 - if (!_uploadOptions.ContentType.Contains(input.File.ContentType)) throw Oops.Oh(ErrorCodeEnum.D8001, input.File.ContentType); - - // 验证文件大小 - if (sizeKb > _uploadOptions.MaxSize) throw Oops.Oh($"{ErrorCodeEnum.D8002},允许最大:{_uploadOptions.MaxSize}KB"); - - // 获取文件后缀 - var suffix = Path.GetExtension(input.File.FileName).ToLower(); - if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); - //suffix = string.Concat(".", input.File.ContentType.AsSpan(input.File.ContentType.LastIndexOf('/') + 1)); - if (!string.IsNullOrWhiteSpace(suffix)) - { - //var contentTypeProvider = FS.GetFileExtensionContentTypeProvider(); - //suffix = contentTypeProvider.Mappings.FirstOrDefault(u => u.Value == file.ContentType).Key; - // 修改 image/jpeg 类型返回的 .jpeg、jpe 后缀 - if (suffix == ".jpeg" || suffix == ".jpe") - suffix = ".jpg"; - } - if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); - - // 防止客户端伪造文件类型 - if (!string.IsNullOrWhiteSpace(input.AllowSuffix) && !input.AllowSuffix.Contains(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); - //if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix)) throw Oops.Oh(ErrorCodeEnum.D8001); - - // 文件存储位置 - var path = string.IsNullOrWhiteSpace(targetPath) ? _uploadOptions.Path : targetPath; - path = path.ParseToDateTimeForRep(); - - var newFile = input.Adapt(); - newFile.Id = YitIdHelper.NextId(); - newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local"; // 阿里云对bucket名称有要求,1.只能包括小写字母,数字,短横线(-)2.必须以小写字母或者数字开头 3.长度必须在3-63字节之间 - newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName); - newFile.Suffix = suffix; - newFile.ContentType = input.File.ContentType; - newFile.SizeKb = sizeKb; - newFile.FilePath = path; - newFile.FileMd5 = fileMd5; - - var finalName = newFile.Id + suffix; // 文件最终名称 - - newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName); - await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync(); - return newFile; - } - - /// - /// 上传头像 🔖 - /// - /// - /// - [DisplayName("上传头像")] - public async Task UploadAvatar([Required] IFormFile file) - { - var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar"); - - var sysUserRep = _sysFileRep.ChangeRepository>(); - var user = await sysUserRep.GetByIdAsync(_userManager.UserId); - await sysUserRep.UpdateAsync(u => new SysUser() { Avatar = sysFile.Url }, u => u.Id == user.Id); - // 删除已有头像文件 - if (!string.IsNullOrWhiteSpace(user.Avatar)) - { - var fileId = Path.GetFileNameWithoutExtension(user.Avatar); - await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); - } - return sysFile; - } - - /// - /// 上传电子签名 🔖 - /// - /// - /// - [DisplayName("上传电子签名")] - public async Task UploadSignature([Required] IFormFile file) - { - var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature"); - - var sysUserRep = _sysFileRep.ChangeRepository>(); - var user = await sysUserRep.GetByIdAsync(_userManager.UserId); - await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id); - // 删除已有电子签名文件 - if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png")) - { - var fileId = Path.GetFileNameWithoutExtension(user.Signature); - await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); - } - return sysFile; - } - - /// - /// 上传Logo 🔖 - /// - /// - /// - /// - [DisplayName("上传Logo")] - public async Task UploadLogo([Required] IFormFile file, [Required] long tenantId) - { - // 先清空 - var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileType == "Logo" && u.DataId == tenantId); - if (sysFile != null) - await DeleteFile(new BaseIdInput { Id = sysFile.Id }); - - return await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType, FileType = "Logo", DataId = tenantId }, $"upload/system/{tenantId}"); - } - - /// - /// 上传轮播图 🔖 - /// - /// - /// - /// - [DisplayName("上传轮播图")] - public async Task> UploadCarousel([Required] List files, [Required] long tenantId) - { - var sysFileList = new List(); - foreach (var file in files) - { - var tFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType, FileType = "Carousel", DataId = tenantId }, $"upload/system/{tenantId}"); - sysFileList.Add(tFile); - } - return sysFileList; - } - - #region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 - - /// - /// 更新文件的业务数据Id - /// - /// - /// - /// - [NonAction] - public async Task UpdateFileByDataId(long dataId, List sysFiles) - { - var newFileIds = sysFiles.Select(u => u.Id).ToList(); - - // 求文件Id差集并删除(无效文件) - var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId); - var tmpFileIds = tmpFiles.Select(u => u.Id).ToList(); - var deleteFileIds = tmpFileIds.Except(newFileIds); - foreach (var fileId in deleteFileIds) - await DeleteFile(new BaseIdInput() { Id = fileId }); - - await _sysFileRep.UpdateAsync(u => new SysFile() { DataId = dataId }, u => newFileIds.Contains(u.Id)); - } - - /// - /// 删除业务数据对应的文件 - /// - /// - /// - [NonAction] - public async Task DeteleFileByDataId(long dataId) - { - // 删除冗余无效的物理文件 - var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId); - foreach (var file in tmpFiles) - await _customFileProvider.DeleteFileAsync(file); - await _sysFileRep.AsDeleteable().Where(u => u.DataId == dataId).ExecuteCommandAsync(); - } - - /// - /// 根据业务数据Id获取文件列表 - /// - /// - /// - /// - [NonAction] - public async Task> GetFileListByDataId([FromQuery] long dataId, [FromQuery] bool isClearFilter = false) - { - return isClearFilter - ? await _sysFileRep.CopyNew().AsQueryable().ClearFilter().Where(u => u.DataId == dataId).ToListAsync() - : await _sysFileRep.CopyNew().GetListAsync(u => u.DataId == dataId); - } - - #endregion 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.AspNetCore; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Admin.NET.Core.Service; + +/// +/// 系统文件服务 🧩 +/// +[ApiDescriptionSettings(Order = 410, Description = "系统文件")] +public class SysFileService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysFileRep; + private readonly OSSProviderOptions _OSSProviderOptions; + private readonly UploadOptions _uploadOptions; + private readonly INamedServiceProvider _namedServiceProvider; + private readonly ICustomFileProvider _customFileProvider; + private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif.svg"; + + public SysFileService(UserManager userManager, + SqlSugarRepository sysFileRep, + IOptions oSSProviderOptions, + IOptions uploadOptions, + INamedServiceProvider namedServiceProvider) + { + _namedServiceProvider = namedServiceProvider; + _userManager = userManager; + _sysFileRep = sysFileRep; + _OSSProviderOptions = oSSProviderOptions.Value; + _uploadOptions = uploadOptions.Value; + if (_OSSProviderOptions.Enabled) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(OSSFileProvider)); + } + else if (App.Configuration["SSHProvider:Enabled"].ToBoolean()) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(SSHFileProvider)); + } + else if (_uploadOptions.EnableSaveDb) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(DbFileProvider)); + } + else + { + _customFileProvider = _namedServiceProvider.GetService(nameof(DefaultFileProvider)); + } + } + + /// + /// 获取文件分页列表 🔖 + /// + /// + /// + [DisplayName("获取文件分页列表")] + public async Task> Page(PageFileInput input) + { + // 获取所有公开文件 + var publicList = _sysFileRep.AsQueryable().ClearFilter().Where(u => u.IsPublic == true); + // 获取私有文件 + var privateList = _sysFileRep.AsQueryable().Where(u => u.IsPublic == false); + // 合并公开和私有并分页 + return await _sysFileRep.Context.UnionAll(publicList, privateList) + .WhereIF(!string.IsNullOrWhiteSpace(input.FileName), u => u.FileName.Contains(input.FileName.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()), + u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 上传文件Base64 🔖 + /// + /// + /// + [DisplayName("上传文件Base64")] + public async Task UploadFileFromBase64(UploadFileFromBase64Input input) + { + var pattern = @"data:(?.+?);base64,(?[^""]+)"; + var regex = new Regex(pattern, RegexOptions.Compiled); + var match = regex.Match(input.FileDataBase64); + + byte[] fileData = Convert.FromBase64String(match.Groups["data"].Value); + var contentType = match.Groups["type"].Value; + if (string.IsNullOrEmpty(input.FileName)) + input.FileName = $"{YitIdHelper.NextId()}.{contentType.AsSpan(contentType.LastIndexOf('/') + 1)}"; + + var ms = new MemoryStream(); + ms.Write(fileData); + ms.Seek(0, SeekOrigin.Begin); + IFormFile formFile = new FormFile(ms, 0, fileData.Length, "file", input.FileName) + { + Headers = new HeaderDictionary(), + ContentType = contentType + }; + var uploadFileInput = input.Adapt(); + uploadFileInput.File = formFile; + return await UploadFile(uploadFileInput); + } + + /// + /// 上传多文件 🔖 + /// + /// + /// + [DisplayName("上传多文件")] + public List UploadFiles([Required] List files) + { + var fileList = new List(); + files.ForEach(file => fileList.Add(UploadFile(new UploadFileInput { File = file }).Result)); + return fileList; + } + + /// + /// 根据文件Id或Url下载 🔖 + /// + /// + /// + [DisplayName("根据文件Id或Url下载")] + public async Task DownloadFile(SysFile input) + { + var file = input.Id > 0 ? await GetFile(input.Id) : await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == input.Url); + var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + return await GetFileStreamResult(file, fileName); + } + + /// + /// 根据文件Id和MD5下载(与db存储模式路径对应) 🔖 + /// + /// + /// + /// + [HttpGet("/upload/downloadfile")] + [DisplayName("根据文件Id和MD5下载")] + [AllowAnonymous] + public async Task DownloadFile2([FromQuery] long id, [FromQuery] string fileMd5) + { + var file = await GetFile(id); + if (file.FileMd5 != null && file.FileMd5 != fileMd5) throw Oops.Oh("文件校验信息不符"); + + var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + return await GetFileStreamResult(file, fileName); + } + + /// + /// 文件预览 🔖 + /// + /// + /// + [DisplayName("文件预览")] + public async Task GetPreview([FromRoute] long id) + { + var file = await GetFile(id); + //var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + return await GetFileStreamResult(file, file.Id + ""); + } + + /// + /// 获取文件流 + /// + /// + /// + /// + private async Task GetFileStreamResult(SysFile file, string fileName) + { + return await _customFileProvider.GetFileStreamResultAsync(file, fileName); + } + + /// + /// 下载指定文件Base64格式 🔖 + /// + /// + /// + [DisplayName("下载指定文件Base64格式")] + public async Task DownloadFileBase64([FromBody] string url) + { + var sysFile = await _sysFileRep.AsQueryable().ClearFilter().FirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在"); + return await _customFileProvider.DownloadFileBase64Async(sysFile); + } + + /// + /// 删除文件 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除文件")] + public async Task DeleteFile(BaseIdInput input) + { + var file = await _sysFileRep.GetByIdAsync(input.Id) ?? throw Oops.Oh($"文件不存在"); + await _sysFileRep.DeleteAsync(file); + await _customFileProvider.DeleteFileAsync(file); + } + + /// + /// 更新文件 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新文件")] + public async Task UpdateFile(SysFile input) + { + var isExist = await _sysFileRep.IsAnyAsync(u => u.Id == input.Id); + if (!isExist) throw Oops.Oh(ErrorCodeEnum.D8000); + + await _sysFileRep.AsUpdateable(input).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); + } + + /// + /// 获取文件 🔖 + /// + /// + /// + /// + [DisplayName("获取文件")] + public async Task GetFile([FromQuery] long id, [FromQuery] bool isClearFilter = false) + { + var file = isClearFilter + ? await _sysFileRep.CopyNew().AsQueryable().ClearFilter().FirstAsync(u => u.Id == id) + : await _sysFileRep.CopyNew().GetByIdAsync(id); + return file ?? throw Oops.Oh(ErrorCodeEnum.D8000); + } + + /// + /// 根据文件Id集合获取文件 🔖 + /// + /// + /// + [DisplayName("根据文件Id集合获取文件")] + public async Task> GetFileByIds([FromQuery][FlexibleArray] List ids) + { + return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(); + } + + /// + /// 获取文件路径 🔖 + /// + /// + [DisplayName("获取文件路径")] + public async Task> GetFolder() + { + var files = await _sysFileRep.AsQueryable().ToListAsync(); + var folders = files.GroupBy(u => u.FilePath).Select(u => u.First().FilePath).ToList(); + + var pathTreeBuilder = new PathTreeBuilder(); + var tree = pathTreeBuilder.BuildTree(folders); + return tree.Children; + } + + /// + /// 上传文件 🔖 + /// + /// + /// 存储目标路径 + /// + [DisplayName("上传文件")] + public async Task UploadFile([FromForm] UploadFileInput input, [BindNever] string targetPath = "") + { + if (input.File == null || input.File.Length <= 0) throw Oops.Oh(ErrorCodeEnum.D8000); + + if (input.File.FileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) throw Oops.Oh(ErrorCodeEnum.D8005); + + // 判断是否重复上传的文件 + var sizeKb = input.File.Length / 1024; // 大小KB + var fileMd5 = string.Empty; + // 计算文件MD5(长度为32的固定字符串) + await using (var fileStream = input.File.OpenReadStream()) + { + byte[] bsFile = new byte[fileStream.Length]; + _ = fileStream.Read(bsFile, 0, bsFile.Length); + fileMd5 = MD5Encryption.Encrypt(bsFile); + } + if (_uploadOptions.EnableMd5) + { + // Mysql8 中如果使用了 utf8mb4_general_ci 之外的编码会出错,尽量避免在条件里使用.ToString() + // 因为 Squsugar 并不是把变量转换为字符串来构造SQL语句,而是构造了CAST(123 AS CHAR)这样的语句,这样这个返回值是utf8mb4_general_ci,所以容易出错。 + var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileMd5 == fileMd5 && u.SizeKb == sizeKb); + if (sysFile != null) return sysFile; + } + + // 验证文件类型 + if (!_uploadOptions.ContentType.Contains(input.File.ContentType)) throw Oops.Oh(ErrorCodeEnum.D8001, input.File.ContentType); + + // 验证文件大小 + if (sizeKb > _uploadOptions.MaxSize) throw Oops.Oh($"{ErrorCodeEnum.D8002},允许最大:{_uploadOptions.MaxSize}KB"); + + // 获取文件后缀 + var suffix = Path.GetExtension(input.File.FileName).ToLower(); + if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); + //suffix = string.Concat(".", input.File.ContentType.AsSpan(input.File.ContentType.LastIndexOf('/') + 1)); + if (!string.IsNullOrWhiteSpace(suffix)) + { + //var contentTypeProvider = FS.GetFileExtensionContentTypeProvider(); + //suffix = contentTypeProvider.Mappings.FirstOrDefault(u => u.Value == file.ContentType).Key; + // 修改 image/jpeg 类型返回的 .jpeg、jpe 后缀 + if (suffix == ".jpeg" || suffix == ".jpe") + suffix = ".jpg"; + } + if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); + + // 防止客户端伪造文件类型 + if (!string.IsNullOrWhiteSpace(input.AllowSuffix) && !input.AllowSuffix.Contains(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); + //if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix)) throw Oops.Oh(ErrorCodeEnum.D8001); + + // 文件存储位置 + var path = string.IsNullOrWhiteSpace(targetPath) ? _uploadOptions.Path : targetPath; + path = path.ParseToDateTimeForRep(); + + var newFile = input.Adapt(); + newFile.Id = YitIdHelper.NextId(); + newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local"; // 阿里云对bucket名称有要求,1.只能包括小写字母,数字,短横线(-)2.必须以小写字母或者数字开头 3.长度必须在3-63字节之间 + newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName); + newFile.Suffix = suffix; + newFile.ContentType = input.File.ContentType; + newFile.SizeKb = sizeKb; + newFile.FilePath = path; + newFile.FileMd5 = fileMd5; + + var finalName = newFile.Id + suffix; // 文件最终名称 + + newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName); + await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync(); + return newFile; + } + + /// + /// 上传头像 🔖 + /// + /// + /// + [DisplayName("上传头像")] + public async Task UploadAvatar([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + var user = await sysUserRep.GetByIdAsync(_userManager.UserId); + await sysUserRep.UpdateAsync(u => new SysUser() { Avatar = sysFile.Url }, u => u.Id == user.Id); + // 删除已有头像文件 + if (!string.IsNullOrWhiteSpace(user.Avatar)) + { + var fileId = Path.GetFileNameWithoutExtension(user.Avatar); + await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); + } + return sysFile; + } + + /// + /// 上传电子签名 🔖 + /// + /// + /// + [DisplayName("上传电子签名")] + public async Task UploadSignature([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + var user = await sysUserRep.GetByIdAsync(_userManager.UserId); + await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id); + // 删除已有电子签名文件 + if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png")) + { + var fileId = Path.GetFileNameWithoutExtension(user.Signature); + await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); + } + return sysFile; + } + + /// + /// 上传Logo 🔖 + /// + /// + /// + /// + [DisplayName("上传Logo")] + public async Task UploadLogo([Required] IFormFile file, [Required] long tenantId) + { + // 先清空 + var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileType == "Logo" && u.DataId == tenantId); + if (sysFile != null) + await DeleteFile(new BaseIdInput { Id = sysFile.Id }); + + return await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType, FileType = "Logo", DataId = tenantId }, $"upload/system/{tenantId}"); + } + + /// + /// 上传轮播图 🔖 + /// + /// + /// + /// + [DisplayName("上传轮播图")] + public async Task> UploadCarousel([Required] List files, [Required] long tenantId) + { + var sysFileList = new List(); + foreach (var file in files) + { + var tFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType, FileType = "Carousel", DataId = tenantId }, $"upload/system/{tenantId}"); + sysFileList.Add(tFile); + } + return sysFileList; + } + + #region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 + + /// + /// 更新文件的业务数据Id + /// + /// + /// + /// + [NonAction] + public async Task UpdateFileByDataId(long dataId, List sysFiles) + { + var newFileIds = sysFiles.Select(u => u.Id).ToList(); + + // 求文件Id差集并删除(无效文件) + var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId); + var tmpFileIds = tmpFiles.Select(u => u.Id).ToList(); + var deleteFileIds = tmpFileIds.Except(newFileIds); + foreach (var fileId in deleteFileIds) + await DeleteFile(new BaseIdInput() { Id = fileId }); + + await _sysFileRep.UpdateAsync(u => new SysFile() { DataId = dataId }, u => newFileIds.Contains(u.Id)); + } + + /// + /// 删除业务数据对应的文件 + /// + /// + /// + [NonAction] + public async Task DeteleFileByDataId(long dataId) + { + // 删除冗余无效的物理文件 + var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId); + foreach (var file in tmpFiles) + await _customFileProvider.DeleteFileAsync(file); + await _sysFileRep.AsDeleteable().Where(u => u.DataId == dataId).ExecuteCommandAsync(); + } + + /// + /// 根据业务数据Id获取文件列表 + /// + /// + /// + /// + [NonAction] + public async Task> GetFileListByDataId([FromQuery] long dataId, [FromQuery] bool isClearFilter = false) + { + return isClearFilter + ? await _sysFileRep.CopyNew().AsQueryable().ClearFilter().Where(u => u.DataId == dataId).ToListAsync() + : await _sysFileRep.CopyNew().GetListAsync(u => u.DataId == dataId); + } + + #endregion 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs b/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs index 5c375804..3407cc69 100644 --- a/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs +++ b/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs @@ -1,59 +1,59 @@ -// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 -// -// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 -// -// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! - -namespace Admin.NET.Core.Service; - -/// -/// 系统差异日志服务 🧩 -/// -[ApiDescriptionSettings(Order = 330, Description = "差异日志")] -public class SysLogDiffService : IDynamicApiController, ITransient -{ - private readonly SqlSugarRepository _sysLogDiffRep; - - public SysLogDiffService(SqlSugarRepository sysLogDiffRep) - { - _sysLogDiffRep = sysLogDiffRep; - } - - /// - /// 获取差异日志分页列表 🔖 - /// - /// - [SuppressMonitor] - [DisplayName("获取差异日志分页列表")] - public async Task> Page(PageLogInput input) - { - return await _sysLogDiffRep.AsQueryable() - .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime) - .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime) - .IgnoreColumns(u => new { u.BeforeData, u.AfterData }) - .OrderBy(u => u.CreateTime, OrderByType.Desc) - .ToPagedListAsync(input.Page, input.PageSize); - } - - /// - /// 获取差异日志详情 🔖 - /// - /// - [SuppressMonitor] - [DisplayName("获取差异日志详情")] - public async Task GetDetail(long id) - { - return await _sysLogDiffRep.GetByIdAsync(id); - } - - ///// - ///// 清空差异日志 🔖 - ///// - ///// - //[ApiDescriptionSettings(Name = "Clear"), HttpPost] - //[DisplayName("清空差异日志")] - //public void Clear() - //{ - // _sysLogDiffRep.AsSugarClient().DbMaintenance.TruncateTable(); - //} +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统差异日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 330, Description = "差异日志")] +public class SysLogDiffService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogDiffRep; + + public SysLogDiffService(SqlSugarRepository sysLogDiffRep) + { + _sysLogDiffRep = sysLogDiffRep; + } + + /// + /// 获取差异日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取差异日志分页列表")] + public async Task> Page(PageLogInput input) + { + return await _sysLogDiffRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime) + .IgnoreColumns(u => new { u.BeforeData, u.AfterData }) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取差异日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取差异日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogDiffRep.GetByIdAsync(id); + } + + ///// + ///// 清空差异日志 🔖 + ///// + ///// + //[ApiDescriptionSettings(Name = "Clear"), HttpPost] + //[DisplayName("清空差异日志")] + //public void Clear() + //{ + // _sysLogDiffRep.AsSugarClient().DbMaintenance.TruncateTable(); + //} } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs b/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs index 39ee1eb6..d8ba393a 100644 --- a/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs +++ b/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs @@ -1,736 +1,736 @@ -// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 -// -// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 -// -// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! - -using Furion.Shapeless; - -namespace Admin.NET.Core.Service; - -/// -/// 系统行政区划服务 🧩 -/// -[ApiDescriptionSettings(Order = 310, Description = "行政区划")] -public class SysRegionService : IDynamicApiController, ITransient -{ - private readonly SqlSugarRepository _sysRegionRep; - private readonly IHttpRemoteService _httpRemoteService; - - public SysRegionService(SqlSugarRepository sysRegionRep, IHttpRemoteService httpRemoteService) - { - _sysRegionRep = sysRegionRep; - _httpRemoteService = httpRemoteService; - } - - /// - /// 获取行政区划分页列表 🔖 - /// - /// - /// - [DisplayName("获取行政区划分页列表")] - public async Task> Page(PageRegionInput input) - { - return await _sysRegionRep.AsQueryable() - .WhereIF(input.Pid > 0, u => u.Pid == input.Pid || u.Id == input.Pid) - .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) - .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) - .OrderBy(u => new { u.Code }) - .ToPagedListAsync(input.Page, input.PageSize); - } - - /// - /// 获取行政区划列表 🔖 - /// - /// - /// - [DisplayName("获取行政区划列表")] - public async Task> GetList([FromQuery] RegionInput input) - { - return await _sysRegionRep.GetListAsync(u => u.Pid == input.Id); - } - - /// - /// 获取指定层级行政区划子树 🔖 - /// - /// - /// - /// - [DisplayName("获取指定层级行政区划子树")] - public async Task> GetChildTree(long pid, int level) - { - var iSugarQueryable = _sysRegionRep.AsQueryable().OrderBy(u => new { u.Code }); - return await iSugarQueryable.Where(u => u.Level < level).ToTreeAsync(u => u.Children, u => u.Pid, pid); - } - - /// - /// 获取指定层级行政区划子列表 🔖 - /// - /// - /// - [DisplayName("获取指定层级行政区划子列表")] - public async Task> GetChildList(long pid) - { - return await _sysRegionRep.AsQueryable().Where(u => u.Pid == pid).OrderBy(u => new { u.Code }).ToListAsync(); - } - - /// - /// 查询行政区划列表 🔖 - /// - /// - /// - [ApiDescriptionSettings(Name = "Query"), HttpPost] - [DisplayName("查询行政区划列表")] - public async Task> QueryList(QueryRegionInput input) - { - return await _sysRegionRep.AsQueryable() - .WhereIF(input.Pid.HasValue, u => u.Pid == input.Pid) - .WhereIF(!string.IsNullOrWhiteSpace(input.Type), u => u.Type == input.Type) - .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) - .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) - .OrderBy(u => new { u.Code }) - .ToListAsync(); - } - - /// - /// 增加行政区划 🔖 - /// - /// - /// - [ApiDescriptionSettings(Name = "Add"), HttpPost] - [DisplayName("增加行政区划")] - public async Task AddRegion(AddRegionInput input) - { - input.Code = input.Code?.Trim() ?? ""; - if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); - - if (input.Pid != 0) - { - var pRegion = await _sysRegionRep.GetByIdAsync(input.Pid); - pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); - if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); - input.Pid = pRegion.Id; - } - - var isExist = await _sysRegionRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code); - if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); - - var sysRegion = input.Adapt(); - var newRegion = await _sysRegionRep.AsInsertable(sysRegion).ExecuteReturnEntityAsync(); - return newRegion.Id; - } - - /// - /// 更新行政区划 🔖 - /// - /// - /// - [ApiDescriptionSettings(Name = "Update"), HttpPost] - [DisplayName("更新行政区划")] - public async Task UpdateRegion(UpdateRegionInput input) - { - input.Code = input.Code?.Trim() ?? ""; - if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); - - var sysRegion = await _sysRegionRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); - - if (sysRegion.Pid != input.Pid && input.Pid != 0) - { - var pRegion = await _sysRegionRep.GetByIdAsync(input.Pid); - pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); - if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); - - input.Pid = pRegion.Id; - var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); - var childIdList = regionTreeList.Select(u => u.Id).ToList(); - if (childIdList.Contains(input.Pid)) throw Oops.Oh(ErrorCodeEnum.R2004); - } - - if (input.Id == input.Pid) throw Oops.Oh(ErrorCodeEnum.R2001); - - var isExist = await _sysRegionRep.IsAnyAsync(u => (u.Name == input.Name && u.Code == input.Code) && u.Id != sysRegion.Id); - if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); - - //// 父Id不能为自己的子节点 - //var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); - //var childIdList = regionTreeList.Select(u => u.Id).ToList(); - //if (childIdList.Contains(input.Pid)) - // throw Oops.Oh(ErrorCodeEnum.R2001); - - await _sysRegionRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); - } - - /// - /// 删除行政区划 🔖 - /// - /// - /// - [ApiDescriptionSettings(Name = "Delete"), HttpPost] - [DisplayName("删除行政区划")] - public async Task DeleteRegion(DeleteRegionInput input) - { - var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); - var regionIdList = regionTreeList.Select(u => u.Id).ToList(); - await _sysRegionRep.DeleteAsync(u => regionIdList.Contains(u.Id)); - } - - /// - /// 同步行政区划(民政部) 🔖 - /// - /// - /// - [DisplayName("同步行政区划(民政部)")] - public async Task SyncRegionMzb(MzbInput input) - { - try - { - var html = await _httpRemoteService.GetAsStringAsync("http://xzqh.mca.gov.cn/map"); - var municipalityList = new List { "北京", "天津", "上海", "重庆" }; - - var proJson = Regex.Match(html, @"(?<=var json = )(\[\{.*?\}\])(?=;)").Value; - dynamic provList = Clay.Parse(proJson); - - var list = new List(); - foreach (var proItem in provList) - { - var provName = proItem.shengji; - var province = new SysRegion - { - Id = YitIdHelper.NextId(), - Name = Regex.Replace(provName, "[((].*?[))]", ""), - Code = proItem.quHuaDaiMa, - CityCode = proItem.quhao, - Level = 1, - Pid = 0, - }; - //if (municipalityList.Any(u => province.Name.StartsWith(u))) province.Name += "(省)"; - list.Add(province); - - if (input.Level <= 1) continue; - - var cityList = await GetSelectList(provName); - foreach (var cityItem in cityList) - { - var cityName = cityItem.diji; - var city = new SysRegion - { - Id = YitIdHelper.NextId(), - Code = cityItem.quHuaDaiMa, - CityCode = cityItem.quhao, - Pid = province.Id, - Name = cityName, - Level = 2 - }; - if (municipalityList.Any(u => city.Name.StartsWith(u))) - { - city.Name = "市辖区"; - if (province.Code == city.Code) city.Code = province.Code.Substring(0, 2) + "0100"; - } - list.Add(city); - - if (input.Level <= 2) continue; - - var countyList = await GetSelectList(provName, cityName); - foreach (var countyItem in countyList) - { - var countyName = countyItem.xianji; - var county = new SysRegion - { - Id = YitIdHelper.NextId(), - Code = countyItem.quHuaDaiMa, - CityCode = countyItem.quhao, - Name = countyName, - Pid = city.Id, - Level = 3 - }; - if (city.Code.IsNullOrEmpty()) - { - // 省直辖县级行政单位 节点无Code编码处理 - city.Code = county.Code.Substring(0, 3).PadRight(6, '0'); - } - list.Add(county); - } - } - } - - if (list.Count > 0) - { - await _sysRegionRep.AsDeleteable().ExecuteCommandAsync(); - await _sysRegionRep.Context.Fastest().BulkCopyAsync(list); - } - } - catch (Exception ex) - { - throw Oops.Oh(ex); - } - - // 获取选择数据 - async Task GetSelectList(string prov, string prefecture = null) - { - var json = await _httpRemoteService.PostAsStringAsync("http://xzqh.mca.gov.cn/selectJson", builder => builder.SetJsonContent(new - { - shengji = prov, - diji = prefecture, - })); - return Clay.Parse(json); - } - } - - /// - /// 同步行政区划(高德) 🔖 - /// - /// - /// - [DisplayName("同步行政区划(高德)")] - public async Task SyncRegionGD(GDInput input) - { - if (string.IsNullOrWhiteSpace(input.Key) || input.Key.Length < 30) - throw Oops.Oh("请正确输入高德地图开发者 Key 值"); - - var res = await _httpRemoteService.GetAsync($"https://restapi.amap.com/v3/config/district?keywords={input.Keywords}&subdistrict={input.Level}&key={input.Key}"); - if (!res.IsSuccessStatusCode) return; - - var gdResponse = JSON.Deserialize>>(res.Content.ReadAsStringAsync().Result); - if (gdResponse.info != "OK" || gdResponse.districts == null || gdResponse.districts.Count < 1) return; - - var regionList = new List(); - foreach (var item in gdResponse.districts) - { - GetChildren(regionList, item.districts, 1, 0); // 排除一级目录(国家) - } - - await _sysRegionRep.AsDeleteable().ExecuteCommandAsync(); - await _sysRegionRep.Context.Fastest().BulkCopyAsync(regionList); - } - - private static void GetChildren(List regionList, List responses, int level, long pid) - { - foreach (var region in responses) - { - var sysRegion = new SysRegion { Id = YitIdHelper.NextId(), Pid = pid, Name = region.name, Code = region.adcode, CityCode = region.adcode, Level = level }; - regionList.Add(sysRegion); - - if (region.districts.Count > 0) - GetChildren(regionList, region.districts, level++, sysRegion.Id); - } - } - - /// - /// 同步行政区划数据(国家地名信息库,最多支持2级深度) 🔖 - /// - /// - [DisplayName("同步行政区划数据(国家地名信息库)")] - public async Task SyncRegionMca(McaInput input) - { - var url = $"https://dmfw.mca.gov.cn/9095/xzqh/getList?code={input.Code}&maxLevel={input.Level}"; - - var res = await _httpRemoteService.GetAsStreamAsync(url); - SysRegion regionLevel0 = ((dynamic)Clay.Parse(res)).data; - if (regionLevel0 == null) return 0; - - var areaList = new List(); - if (regionLevel0.Code != "00" && regionLevel0.Level > 0 && !string.IsNullOrEmpty(regionLevel0.Name)) - { - areaList.Add(new SysRegion - { - Id = Convert.ToInt64(regionLevel0.Code), - Pid = 0, - Code = regionLevel0.Code, - Name = regionLevel0.Name, - Type = regionLevel0.Type, - Level = regionLevel0.Level, - }); - } - if (regionLevel0.Children != null) - { - foreach (var regionLevel1 in regionLevel0.Children) - { - var region1 = new SysRegion - { - Id = Convert.ToInt64(regionLevel1.Code), - Pid = Convert.ToInt64(regionLevel0.Code), - Code = regionLevel1.Code, - Name = regionLevel1.Name, - Type = regionLevel1.Type, - Level = regionLevel1.Level, - }; - if (areaList.Any(u => u.Id == region1.Id)) - Console.WriteLine($"1 级:{region1.Id} - {region1.Name} 已存在"); - else - areaList.Add(region1); - if (regionLevel1.Children == null) continue; - foreach (var regionLevel2 in regionLevel1.Children) - { - var region2 = new SysRegion - { - Id = Convert.ToInt64(regionLevel2.Code), - Pid = Convert.ToInt64(regionLevel1.Code), - Code = regionLevel2.Code, - Name = regionLevel2.Name, - Type = regionLevel2.Type, - Level = regionLevel2.Level, - }; - if (areaList.Any(u => u.Id == region2.Id)) - Console.WriteLine($"2 级:{region2.Id} - {region2.Name} 已存在"); - else - areaList.Add(region2); - if (regionLevel2.Children == null) continue; - foreach (var regionLevel3 in regionLevel2.Children) - { - var region3 = new SysRegion - { - Id = Convert.ToInt64(regionLevel3.Code), - Pid = Convert.ToInt64(regionLevel2.Code), - Code = regionLevel3.Code, - Name = regionLevel3.Name, - Type = regionLevel3.Type, - Level = regionLevel3.Level, - }; - if (areaList.Any(u => u.Id == region3.Id)) - Console.WriteLine($"3 级:{region3.Id} - {region3.Name}"); - else - areaList.Add(region3); - if (regionLevel3.Children == null) continue; - foreach (var regionLevel4 in regionLevel3.Children) - { - var region4 = new SysRegion - { - Id = Convert.ToInt64(regionLevel4.Code), - Pid = Convert.ToInt64(regionLevel3.Code), - Code = regionLevel4.Code, - Name = regionLevel4.Name, - Type = regionLevel4.Type, - Level = regionLevel4.Level, - }; - areaList.Add(region4); - } - } - } - } - } - - if (input.Code == 0) - await _sysRegionRep.AsDeleteable().ExecuteCommandAsync(); - else if (await _sysRegionRep.IsAnyAsync(u => u.Id == input.Code)) // 如果存在指定行政区划则删除 - await DeleteRegion(new DeleteRegionInput { Id = input.Code }); - return await _sysRegionRep.AsInsertable(areaList).ExecuteCommandAsync(); - } - - /// - /// 同步行政区划数据(天地图行政区划) 🔖 - /// - /// - [DisplayName("同步行政区划数据(天地图行政区划)")] - public async Task SyncRegionTianditu(TiandituInput input) - { - // 接口说明及地址:http://lbs.tianditu.gov.cn/server/administrative2.html - var url = $"http://api.tianditu.gov.cn/v2/administrative?keyword={input.Keyword}&childLevel={input.ChildLevel}&extensions={input.Extensions}&tk={input.Tk}"; - - var res = await _httpRemoteService.GetAsAsync(url); - if (res == null || res.District == null) return 0; - - var parent = res.District[0]; - var areaList = new List() - { - new() - { - Id = Convert.ToInt64(parent.Gb), - Pid = 0, - Code = parent.Gb, - Name = parent.Name, - Level = parent.Level, - Longitude = parent.Center.Lng, - Latitude = parent.Center.Lat - } - }; - - foreach (var item in parent.Children) - { - var region = new SysRegion - { - Id = Convert.ToInt64(item.Gb), - Pid = Convert.ToInt64(parent.Gb), - Code = item.Gb, - Name = item.Name, - Level = item.Level, - Longitude = item.Center.Lng, - Latitude = item.Center.Lat - }; - areaList.Add(region); - - foreach (var child in item.Children) - { - areaList.Add(new SysRegion - { - Id = Convert.ToInt64(child.Gb), - Pid = region.Id, - Code = child.Gb, - Name = child.Name, - Level = child.Level, - Longitude = child.Center.Lng, - Latitude = child.Center.Lat - }); - } - } - - // 若存在指定行政区划则删除 - if (await _sysRegionRep.IsAnyAsync(u => u.Name.Contains(input.Keyword) || u.Id.ToString() == input.Keyword)) - { - var region = await _sysRegionRep.GetFirstAsync(u => u.Name.Contains(input.Keyword) || u.Id.ToString() == input.Keyword); - await DeleteRegion(new DeleteRegionInput { Id = region.Id }); - } - - return await _sysRegionRep.AsInsertable(areaList).ExecuteCommandAsync(); - } - - /// - /// 生成组织架构 🔖 - /// - /// - /// - [DisplayName("生成组织架构")] - public async Task GenOrg(GenOrgInput input) - { - var region = await _sysRegionRep.GetByIdAsync(input.Id); - var orgRep = _sysRegionRep.ChangeRepository>(); - if (!await orgRep.IsAnyAsync(u => u.Id == region.Pid)) - region.Pid = 0; - - var regionList = await GetRegionListByLevel(region, input.Level); - var orgList = regionList.Adapt>(); - await orgRep.InsertOrUpdateAsync(orgList); - } - - /// - /// 根据层级获取行政区划数据 - /// - /// - /// - /// - private async Task> GetRegionListByLevel(SysRegion region, int level) - { - var regionList = new List(); - if (level > 5) level = 5; - regionList.Add(region); - - if (level == 1) return regionList; - var regionList2 = await GetList(new RegionInput { Id = region.Id }); - regionList.AddRange(regionList2); - - if (level == 2) return regionList; - foreach (var item in regionList2) - { - var regionList3 = await GetList(new RegionInput { Id = item.Id }); - if (regionList3 == null) continue; - regionList.AddRange(regionList3); - - if (level == 3) continue; - foreach (var item3 in regionList3) - { - var regionList4 = await GetList(new RegionInput { Id = item3.Id }); - if (regionList4 == null) continue; - regionList.AddRange(regionList4); - - if (level == 4) continue; - foreach (var item4 in regionList4) - { - var regionList5 = await GetList(new RegionInput { Id = item4.Id }); - if (regionList5 == null) continue; - regionList.AddRange(regionList5); - } - } - } - return regionList; - } - - /// - /// 从 china.sqlite 中获取区划数据 - /// - /// 区划编码 - /// 级数(从当前code所在级别往下级数) - /// - [DisplayName("从 china.sqlite 中获取区划数据")] - public async Task GetRegionTree(string code, int level) - { - level = level > 5 ? 5 : level; - - var sqlitePath = "C:\\china.sqlite"; - var db = new SqlSugarScope(new ConnectionConfig() - { - ConnectionString = $"Data Source={sqlitePath};Cache=Shared", - DbType = SqlSugar.DbType.Sqlite, - IsAutoCloseConnection = true, - InitKeyType = InitKeyType.Attribute - }); - - var regionList = new List(); - - // 判断编码所属层级 - int startLevel = 1; // 省 - switch (code.Length) - { - case 4: - startLevel = 2; // 市 - break; - - case 6: - startLevel = 3; // 区县 - break; - - case 9: - startLevel = 4; // 街道 - break; - - case 12: - startLevel = 5; // 社区/村 - break; - - default: - break; - } - var region1List = GetRegionList(code, startLevel, db); - if (region1List.Count == 0) - return; - region1List.ForEach(u => u.Pid = 0); - regionList.AddRange(region1List); - - if (level == 1 || startLevel == 5) - goto result; - startLevel++; - - var region2List = new List(); - foreach (var item in region1List) - { - region2List.AddRange(GetRegionList(item.Code, startLevel, db)); - } - regionList.AddRange(region2List); - - if (level == 2 || startLevel == 5 || region2List.Count == 0) - goto result; - startLevel++; - - var region3List = new List(); - foreach (var item in region2List) - { - region3List.AddRange(GetRegionList(item.Code, startLevel, db)); - } - regionList.AddRange(region3List); - - if (level == 3 || startLevel == 5 || region3List.Count == 0) - goto result; - startLevel++; - - var region4List = new List(); - foreach (var item in region3List) - { - region4List.AddRange(GetRegionList(item.Code, startLevel, db)); - } - regionList.AddRange(region4List); - - if (level == 4 || startLevel == 5 || region4List.Count == 0) - goto result; - startLevel++; - - var region5List = new List(); - region5List.AddRange(GetVillageList(region4List.Select(u => u.Code).ToList(), db)); - regionList.AddRange(region5List); - - result: - - // 保存行政区划树 - var defaultDbConfig = App.GetOptions().ConnectionConfigs[0]; - db = new SqlSugarScope(new ConnectionConfig() - { - ConnectionString = defaultDbConfig.ConnectionString, - DbType = defaultDbConfig.DbType, - IsAutoCloseConnection = true, - InitKeyType = InitKeyType.Attribute - }); - await db.Deleteable().ExecuteCommandAsync(); - await db.Insertable(regionList).ExecuteCommandAsync(); - } - - /// - /// 根据层级及父级编码获取区域集合 - /// - /// - /// - /// - /// - private static List GetRegionList(string pCode, int level, SqlSugarScope db) - { - string table = ""; - switch (level) - { - case 1: - table = "province"; - break; - - case 2: - table = "city"; - break; - - case 3: - table = "area"; - break; - - case 4: - table = "street"; - break; - - case 5: - table = "village"; - break; - - default: - break; - } - if (string.IsNullOrWhiteSpace(table)) - return []; - - var condition = string.IsNullOrWhiteSpace(pCode) || pCode == "0" ? "" : $" and code like '{pCode}%'"; - var sql = $"select * from {table} where 1=1 {condition}"; - var regions = db.Ado.SqlQuery(sql); - if (regions.Count == 0) - return []; - - foreach (var item in regions) - { - item.Pid = string.IsNullOrWhiteSpace(pCode) || item.Code == pCode || level == 1 ? 0 : Convert.ToInt64(pCode); - item.Level = level; - item.Id = Convert.ToInt64(item.Code); - } - - return regions; - } - - /// - /// 获取社区/村集合 - /// - /// - /// - /// - private static List GetVillageList(List pCodes, SqlSugarScope db) - { - var condition = pCodes == null || pCodes.Count == 0 ? "" : $" and streetCode in ('{pCodes.Join("','")}')"; - var sql = $"select * from village where 1=1 {condition}"; - var regions = db.Ado.SqlQuery(sql); - if (regions.Count == 0) - return []; - - var regionList = new List(); - foreach (var item in regions) - { - var region = new SysRegion - { - Name = item.name, - Code = item.code, - Pid = Convert.ToInt64(item.streetCode), - Level = 5, - Id = Convert.ToInt64(item.code) - }; - regionList.Add(region); - } - return regionList; - } +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Shapeless; + +namespace Admin.NET.Core.Service; + +/// +/// 系统行政区划服务 🧩 +/// +[ApiDescriptionSettings(Order = 310, Description = "行政区划")] +public class SysRegionService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysRegionRep; + private readonly IHttpRemoteService _httpRemoteService; + + public SysRegionService(SqlSugarRepository sysRegionRep, IHttpRemoteService httpRemoteService) + { + _sysRegionRep = sysRegionRep; + _httpRemoteService = httpRemoteService; + } + + /// + /// 获取行政区划分页列表 🔖 + /// + /// + /// + [DisplayName("获取行政区划分页列表")] + public async Task> Page(PageRegionInput input) + { + return await _sysRegionRep.AsQueryable() + .WhereIF(input.Pid > 0, u => u.Pid == input.Pid || u.Id == input.Pid) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .OrderBy(u => new { u.Code }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取行政区划列表 🔖 + /// + /// + /// + [DisplayName("获取行政区划列表")] + public async Task> GetList([FromQuery] RegionInput input) + { + return await _sysRegionRep.GetListAsync(u => u.Pid == input.Id); + } + + /// + /// 获取指定层级行政区划子树 🔖 + /// + /// + /// + /// + [DisplayName("获取指定层级行政区划子树")] + public async Task> GetChildTree(long pid, int level) + { + var iSugarQueryable = _sysRegionRep.AsQueryable().OrderBy(u => new { u.Code }); + return await iSugarQueryable.Where(u => u.Level < level).ToTreeAsync(u => u.Children, u => u.Pid, pid); + } + + /// + /// 获取指定层级行政区划子列表 🔖 + /// + /// + /// + [DisplayName("获取指定层级行政区划子列表")] + public async Task> GetChildList(long pid) + { + return await _sysRegionRep.AsQueryable().Where(u => u.Pid == pid).OrderBy(u => new { u.Code }).ToListAsync(); + } + + /// + /// 查询行政区划列表 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Query"), HttpPost] + [DisplayName("查询行政区划列表")] + public async Task> QueryList(QueryRegionInput input) + { + return await _sysRegionRep.AsQueryable() + .WhereIF(input.Pid.HasValue, u => u.Pid == input.Pid) + .WhereIF(!string.IsNullOrWhiteSpace(input.Type), u => u.Type == input.Type) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .OrderBy(u => new { u.Code }) + .ToListAsync(); + } + + /// + /// 增加行政区划 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加行政区划")] + public async Task AddRegion(AddRegionInput input) + { + input.Code = input.Code?.Trim() ?? ""; + if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); + + if (input.Pid != 0) + { + var pRegion = await _sysRegionRep.GetByIdAsync(input.Pid); + pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); + if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); + input.Pid = pRegion.Id; + } + + var isExist = await _sysRegionRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); + + var sysRegion = input.Adapt(); + var newRegion = await _sysRegionRep.AsInsertable(sysRegion).ExecuteReturnEntityAsync(); + return newRegion.Id; + } + + /// + /// 更新行政区划 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新行政区划")] + public async Task UpdateRegion(UpdateRegionInput input) + { + input.Code = input.Code?.Trim() ?? ""; + if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); + + var sysRegion = await _sysRegionRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + + if (sysRegion.Pid != input.Pid && input.Pid != 0) + { + var pRegion = await _sysRegionRep.GetByIdAsync(input.Pid); + pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); + if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); + + input.Pid = pRegion.Id; + var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var childIdList = regionTreeList.Select(u => u.Id).ToList(); + if (childIdList.Contains(input.Pid)) throw Oops.Oh(ErrorCodeEnum.R2004); + } + + if (input.Id == input.Pid) throw Oops.Oh(ErrorCodeEnum.R2001); + + var isExist = await _sysRegionRep.IsAnyAsync(u => (u.Name == input.Name && u.Code == input.Code) && u.Id != sysRegion.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); + + //// 父Id不能为自己的子节点 + //var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + //var childIdList = regionTreeList.Select(u => u.Id).ToList(); + //if (childIdList.Contains(input.Pid)) + // throw Oops.Oh(ErrorCodeEnum.R2001); + + await _sysRegionRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除行政区划 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除行政区划")] + public async Task DeleteRegion(DeleteRegionInput input) + { + var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var regionIdList = regionTreeList.Select(u => u.Id).ToList(); + await _sysRegionRep.DeleteAsync(u => regionIdList.Contains(u.Id)); + } + + /// + /// 同步行政区划(民政部) 🔖 + /// + /// + /// + [DisplayName("同步行政区划(民政部)")] + public async Task SyncRegionMzb(MzbInput input) + { + try + { + var html = await _httpRemoteService.GetAsStringAsync("http://xzqh.mca.gov.cn/map"); + var municipalityList = new List { "北京", "天津", "上海", "重庆" }; + + var proJson = Regex.Match(html, @"(?<=var json = )(\[\{.*?\}\])(?=;)").Value; + dynamic provList = Clay.Parse(proJson); + + var list = new List(); + foreach (var proItem in provList) + { + var provName = proItem.shengji; + var province = new SysRegion + { + Id = YitIdHelper.NextId(), + Name = Regex.Replace(provName, "[((].*?[))]", ""), + Code = proItem.quHuaDaiMa, + CityCode = proItem.quhao, + Level = 1, + Pid = 0, + }; + //if (municipalityList.Any(u => province.Name.StartsWith(u))) province.Name += "(省)"; + list.Add(province); + + if (input.Level <= 1) continue; + + var cityList = await GetSelectList(provName); + foreach (var cityItem in cityList) + { + var cityName = cityItem.diji; + var city = new SysRegion + { + Id = YitIdHelper.NextId(), + Code = cityItem.quHuaDaiMa, + CityCode = cityItem.quhao, + Pid = province.Id, + Name = cityName, + Level = 2 + }; + if (municipalityList.Any(u => city.Name.StartsWith(u))) + { + city.Name = "市辖区"; + if (province.Code == city.Code) city.Code = province.Code.Substring(0, 2) + "0100"; + } + list.Add(city); + + if (input.Level <= 2) continue; + + var countyList = await GetSelectList(provName, cityName); + foreach (var countyItem in countyList) + { + var countyName = countyItem.xianji; + var county = new SysRegion + { + Id = YitIdHelper.NextId(), + Code = countyItem.quHuaDaiMa, + CityCode = countyItem.quhao, + Name = countyName, + Pid = city.Id, + Level = 3 + }; + if (city.Code.IsNullOrEmpty()) + { + // 省直辖县级行政单位 节点无Code编码处理 + city.Code = county.Code.Substring(0, 3).PadRight(6, '0'); + } + list.Add(county); + } + } + } + + if (list.Count > 0) + { + await _sysRegionRep.AsDeleteable().ExecuteCommandAsync(); + await _sysRegionRep.Context.Fastest().BulkCopyAsync(list); + } + } + catch (Exception ex) + { + throw Oops.Oh(ex); + } + + // 获取选择数据 + async Task GetSelectList(string prov, string prefecture = null) + { + var json = await _httpRemoteService.PostAsStringAsync("http://xzqh.mca.gov.cn/selectJson", builder => builder.SetJsonContent(new + { + shengji = prov, + diji = prefecture, + })); + return Clay.Parse(json); + } + } + + /// + /// 同步行政区划(高德) 🔖 + /// + /// + /// + [DisplayName("同步行政区划(高德)")] + public async Task SyncRegionGD(GDInput input) + { + if (string.IsNullOrWhiteSpace(input.Key) || input.Key.Length < 30) + throw Oops.Oh("请正确输入高德地图开发者 Key 值"); + + var res = await _httpRemoteService.GetAsync($"https://restapi.amap.com/v3/config/district?keywords={input.Keywords}&subdistrict={input.Level}&key={input.Key}"); + if (!res.IsSuccessStatusCode) return; + + var gdResponse = JSON.Deserialize>>(res.Content.ReadAsStringAsync().Result); + if (gdResponse.info != "OK" || gdResponse.districts == null || gdResponse.districts.Count < 1) return; + + var regionList = new List(); + foreach (var item in gdResponse.districts) + { + GetChildren(regionList, item.districts, 1, 0); // 排除一级目录(国家) + } + + await _sysRegionRep.AsDeleteable().ExecuteCommandAsync(); + await _sysRegionRep.Context.Fastest().BulkCopyAsync(regionList); + } + + private static void GetChildren(List regionList, List responses, int level, long pid) + { + foreach (var region in responses) + { + var sysRegion = new SysRegion { Id = YitIdHelper.NextId(), Pid = pid, Name = region.name, Code = region.adcode, CityCode = region.adcode, Level = level }; + regionList.Add(sysRegion); + + if (region.districts.Count > 0) + GetChildren(regionList, region.districts, level++, sysRegion.Id); + } + } + + /// + /// 同步行政区划数据(国家地名信息库,最多支持2级深度) 🔖 + /// + /// + [DisplayName("同步行政区划数据(国家地名信息库)")] + public async Task SyncRegionMca(McaInput input) + { + var url = $"https://dmfw.mca.gov.cn/9095/xzqh/getList?code={input.Code}&maxLevel={input.Level}"; + + var res = await _httpRemoteService.GetAsStreamAsync(url); + SysRegion regionLevel0 = ((dynamic)Clay.Parse(res)).data; + if (regionLevel0 == null) return 0; + + var areaList = new List(); + if (regionLevel0.Code != "00" && regionLevel0.Level > 0 && !string.IsNullOrEmpty(regionLevel0.Name)) + { + areaList.Add(new SysRegion + { + Id = Convert.ToInt64(regionLevel0.Code), + Pid = 0, + Code = regionLevel0.Code, + Name = regionLevel0.Name, + Type = regionLevel0.Type, + Level = regionLevel0.Level, + }); + } + if (regionLevel0.Children != null) + { + foreach (var regionLevel1 in regionLevel0.Children) + { + var region1 = new SysRegion + { + Id = Convert.ToInt64(regionLevel1.Code), + Pid = Convert.ToInt64(regionLevel0.Code), + Code = regionLevel1.Code, + Name = regionLevel1.Name, + Type = regionLevel1.Type, + Level = regionLevel1.Level, + }; + if (areaList.Any(u => u.Id == region1.Id)) + Console.WriteLine($"1 级:{region1.Id} - {region1.Name} 已存在"); + else + areaList.Add(region1); + if (regionLevel1.Children == null) continue; + foreach (var regionLevel2 in regionLevel1.Children) + { + var region2 = new SysRegion + { + Id = Convert.ToInt64(regionLevel2.Code), + Pid = Convert.ToInt64(regionLevel1.Code), + Code = regionLevel2.Code, + Name = regionLevel2.Name, + Type = regionLevel2.Type, + Level = regionLevel2.Level, + }; + if (areaList.Any(u => u.Id == region2.Id)) + Console.WriteLine($"2 级:{region2.Id} - {region2.Name} 已存在"); + else + areaList.Add(region2); + if (regionLevel2.Children == null) continue; + foreach (var regionLevel3 in regionLevel2.Children) + { + var region3 = new SysRegion + { + Id = Convert.ToInt64(regionLevel3.Code), + Pid = Convert.ToInt64(regionLevel2.Code), + Code = regionLevel3.Code, + Name = regionLevel3.Name, + Type = regionLevel3.Type, + Level = regionLevel3.Level, + }; + if (areaList.Any(u => u.Id == region3.Id)) + Console.WriteLine($"3 级:{region3.Id} - {region3.Name}"); + else + areaList.Add(region3); + if (regionLevel3.Children == null) continue; + foreach (var regionLevel4 in regionLevel3.Children) + { + var region4 = new SysRegion + { + Id = Convert.ToInt64(regionLevel4.Code), + Pid = Convert.ToInt64(regionLevel3.Code), + Code = regionLevel4.Code, + Name = regionLevel4.Name, + Type = regionLevel4.Type, + Level = regionLevel4.Level, + }; + areaList.Add(region4); + } + } + } + } + } + + if (input.Code == 0) + await _sysRegionRep.AsDeleteable().ExecuteCommandAsync(); + else if (await _sysRegionRep.IsAnyAsync(u => u.Id == input.Code)) // 如果存在指定行政区划则删除 + await DeleteRegion(new DeleteRegionInput { Id = input.Code }); + return await _sysRegionRep.AsInsertable(areaList).ExecuteCommandAsync(); + } + + /// + /// 同步行政区划数据(天地图行政区划) 🔖 + /// + /// + [DisplayName("同步行政区划数据(天地图行政区划)")] + public async Task SyncRegionTianditu(TiandituInput input) + { + // 接口说明及地址:http://lbs.tianditu.gov.cn/server/administrative2.html + var url = $"http://api.tianditu.gov.cn/v2/administrative?keyword={input.Keyword}&childLevel={input.ChildLevel}&extensions={input.Extensions}&tk={input.Tk}"; + + var res = await _httpRemoteService.GetAsAsync(url); + if (res == null || res.District == null) return 0; + + var parent = res.District[0]; + var areaList = new List() + { + new() + { + Id = Convert.ToInt64(parent.Gb), + Pid = 0, + Code = parent.Gb, + Name = parent.Name, + Level = parent.Level, + Longitude = parent.Center.Lng, + Latitude = parent.Center.Lat + } + }; + + foreach (var item in parent.Children) + { + var region = new SysRegion + { + Id = Convert.ToInt64(item.Gb), + Pid = Convert.ToInt64(parent.Gb), + Code = item.Gb, + Name = item.Name, + Level = item.Level, + Longitude = item.Center.Lng, + Latitude = item.Center.Lat + }; + areaList.Add(region); + + foreach (var child in item.Children) + { + areaList.Add(new SysRegion + { + Id = Convert.ToInt64(child.Gb), + Pid = region.Id, + Code = child.Gb, + Name = child.Name, + Level = child.Level, + Longitude = child.Center.Lng, + Latitude = child.Center.Lat + }); + } + } + + // 若存在指定行政区划则删除 + if (await _sysRegionRep.IsAnyAsync(u => u.Name.Contains(input.Keyword) || u.Id.ToString() == input.Keyword)) + { + var region = await _sysRegionRep.GetFirstAsync(u => u.Name.Contains(input.Keyword) || u.Id.ToString() == input.Keyword); + await DeleteRegion(new DeleteRegionInput { Id = region.Id }); + } + + return await _sysRegionRep.AsInsertable(areaList).ExecuteCommandAsync(); + } + + /// + /// 生成组织架构 🔖 + /// + /// + /// + [DisplayName("生成组织架构")] + public async Task GenOrg(GenOrgInput input) + { + var region = await _sysRegionRep.GetByIdAsync(input.Id); + var orgRep = _sysRegionRep.ChangeRepository>(); + if (!await orgRep.IsAnyAsync(u => u.Id == region.Pid)) + region.Pid = 0; + + var regionList = await GetRegionListByLevel(region, input.Level); + var orgList = regionList.Adapt>(); + await orgRep.InsertOrUpdateAsync(orgList); + } + + /// + /// 根据层级获取行政区划数据 + /// + /// + /// + /// + private async Task> GetRegionListByLevel(SysRegion region, int level) + { + var regionList = new List(); + if (level > 5) level = 5; + regionList.Add(region); + + if (level == 1) return regionList; + var regionList2 = await GetList(new RegionInput { Id = region.Id }); + regionList.AddRange(regionList2); + + if (level == 2) return regionList; + foreach (var item in regionList2) + { + var regionList3 = await GetList(new RegionInput { Id = item.Id }); + if (regionList3 == null) continue; + regionList.AddRange(regionList3); + + if (level == 3) continue; + foreach (var item3 in regionList3) + { + var regionList4 = await GetList(new RegionInput { Id = item3.Id }); + if (regionList4 == null) continue; + regionList.AddRange(regionList4); + + if (level == 4) continue; + foreach (var item4 in regionList4) + { + var regionList5 = await GetList(new RegionInput { Id = item4.Id }); + if (regionList5 == null) continue; + regionList.AddRange(regionList5); + } + } + } + return regionList; + } + + /// + /// 从 china.sqlite 中获取区划数据 + /// + /// 区划编码 + /// 级数(从当前code所在级别往下级数) + /// + [DisplayName("从 china.sqlite 中获取区划数据")] + public async Task GetRegionTree(string code, int level) + { + level = level > 5 ? 5 : level; + + var sqlitePath = "C:\\china.sqlite"; + var db = new SqlSugarScope(new ConnectionConfig() + { + ConnectionString = $"Data Source={sqlitePath};Cache=Shared", + DbType = SqlSugar.DbType.Sqlite, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute + }); + + var regionList = new List(); + + // 判断编码所属层级 + int startLevel = 1; // 省 + switch (code.Length) + { + case 4: + startLevel = 2; // 市 + break; + + case 6: + startLevel = 3; // 区县 + break; + + case 9: + startLevel = 4; // 街道 + break; + + case 12: + startLevel = 5; // 社区/村 + break; + + default: + break; + } + var region1List = GetRegionList(code, startLevel, db); + if (region1List.Count == 0) + return; + region1List.ForEach(u => u.Pid = 0); + regionList.AddRange(region1List); + + if (level == 1 || startLevel == 5) + goto result; + startLevel++; + + var region2List = new List(); + foreach (var item in region1List) + { + region2List.AddRange(GetRegionList(item.Code, startLevel, db)); + } + regionList.AddRange(region2List); + + if (level == 2 || startLevel == 5 || region2List.Count == 0) + goto result; + startLevel++; + + var region3List = new List(); + foreach (var item in region2List) + { + region3List.AddRange(GetRegionList(item.Code, startLevel, db)); + } + regionList.AddRange(region3List); + + if (level == 3 || startLevel == 5 || region3List.Count == 0) + goto result; + startLevel++; + + var region4List = new List(); + foreach (var item in region3List) + { + region4List.AddRange(GetRegionList(item.Code, startLevel, db)); + } + regionList.AddRange(region4List); + + if (level == 4 || startLevel == 5 || region4List.Count == 0) + goto result; + startLevel++; + + var region5List = new List(); + region5List.AddRange(GetVillageList(region4List.Select(u => u.Code).ToList(), db)); + regionList.AddRange(region5List); + + result: + + // 保存行政区划树 + var defaultDbConfig = App.GetOptions().ConnectionConfigs[0]; + db = new SqlSugarScope(new ConnectionConfig() + { + ConnectionString = defaultDbConfig.ConnectionString, + DbType = defaultDbConfig.DbType, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute + }); + await db.Deleteable().ExecuteCommandAsync(); + await db.Insertable(regionList).ExecuteCommandAsync(); + } + + /// + /// 根据层级及父级编码获取区域集合 + /// + /// + /// + /// + /// + private static List GetRegionList(string pCode, int level, SqlSugarScope db) + { + string table = ""; + switch (level) + { + case 1: + table = "province"; + break; + + case 2: + table = "city"; + break; + + case 3: + table = "area"; + break; + + case 4: + table = "street"; + break; + + case 5: + table = "village"; + break; + + default: + break; + } + if (string.IsNullOrWhiteSpace(table)) + return []; + + var condition = string.IsNullOrWhiteSpace(pCode) || pCode == "0" ? "" : $" and code like '{pCode}%'"; + var sql = $"select * from {table} where 1=1 {condition}"; + var regions = db.Ado.SqlQuery(sql); + if (regions.Count == 0) + return []; + + foreach (var item in regions) + { + item.Pid = string.IsNullOrWhiteSpace(pCode) || item.Code == pCode || level == 1 ? 0 : Convert.ToInt64(pCode); + item.Level = level; + item.Id = Convert.ToInt64(item.Code); + } + + return regions; + } + + /// + /// 获取社区/村集合 + /// + /// + /// + /// + private static List GetVillageList(List pCodes, SqlSugarScope db) + { + var condition = pCodes == null || pCodes.Count == 0 ? "" : $" and streetCode in ('{pCodes.Join("','")}')"; + var sql = $"select * from village where 1=1 {condition}"; + var regions = db.Ado.SqlQuery(sql); + if (regions.Count == 0) + return []; + + var regionList = new List(); + foreach (var item in regions) + { + var region = new SysRegion + { + Name = item.name, + Code = item.code, + Pid = Convert.ToInt64(item.streetCode), + Level = 5, + Id = Convert.ToInt64(item.code) + }; + regionList.Add(region); + } + return regionList; + } } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs b/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs index 43ba3a2f..17019cb7 100644 --- a/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs +++ b/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs @@ -45,13 +45,13 @@ public class AddUserInput : SysUser /// /// 账号 /// - [Required(ErrorMessage = "账号不能为空")] + [Required(ErrorMessage = "账号不能为空")] public override string Account { get; set; } /// /// 真实姓名 /// - [Required(ErrorMessage = "真实姓名不能为空")] + [Required(ErrorMessage = "真实姓名不能为空")] public override string RealName { get; set; } /// @@ -92,10 +92,10 @@ public class DeleteUserInput : BaseIdInput /// 重置用户密码输入参数 /// public class ResetPwdUserInput : BaseIdInput -{ - /// - /// 新密码(若空则用默认密码) - /// +{ + /// + /// 新密码(若空则用默认密码) + /// public string NewPassword { get; set; } } @@ -107,12 +107,12 @@ public class ChangePwdInput /// /// 当前密码 /// - [Required(ErrorMessage = "当前密码不能为空")] + [Required(ErrorMessage = "当前密码不能为空")] public string PasswordOld { get; set; } /// /// 新密码 /// - [Required(ErrorMessage = "新密码不能为空"), MinLength(5, ErrorMessage = "密码需要大于5个字符")] + [Required(ErrorMessage = "新密码不能为空"), MinLength(5, ErrorMessage = "密码需要大于5个字符")] public string PasswordNew { get; set; } } \ No newline at end of file