From 12c022e05cbf9b7872ea3c55eda880788ed3ae30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com> Date: Tue, 14 Oct 2025 10:41:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20=E7=AE=A1=E7=90=86=E5=91=98=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AdminController.cs | 115 +++++++++++++++++- .../Controllers/Dto/AdminDto.cs | 10 ++ .../Controllers/Dto/ClassDto.cs | 33 ++++- .../Controllers/StudentController.cs | 4 +- Learn.Archives.API/Expand/HttpFilter.cs | 67 ++++++---- Learn.Archives.API/Program.cs | 22 ++-- .../Common/BasicAuthMiddleware.cs | 4 +- 7 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 Learn.Archives.API/Controllers/Dto/AdminDto.cs diff --git a/Learn.Archives.API/Controllers/AdminController.cs b/Learn.Archives.API/Controllers/AdminController.cs index 08a15a1..ae41ea9 100644 --- a/Learn.Archives.API/Controllers/AdminController.cs +++ b/Learn.Archives.API/Controllers/AdminController.cs @@ -3,10 +3,16 @@ using Learn.Archives.API.Controllers.Dto; using Learn.Archives.API.Expand; using Learn.Archives.Core.Common; using Learn.Archives.Core.Model; +using Learn.Archives.Core.Model.Dto; +using Learn.Archives.Core.Model.Enum; +using Mapster; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using MiniExcelLibs; using System.Diagnostics; using System.Security.Claims; +using System.Text.RegularExpressions; +using UserCenter.Model.Common; namespace Learn.Archives.API.Controllers { @@ -15,11 +21,17 @@ namespace Learn.Archives.API.Controllers readonly Repository baseService; readonly Repository menuRelationDB; readonly Repository menuDB; - public AdminController(Repository baseService, Repository menuRelationDB, Repository menuDB) : base(baseService) + readonly Repository roleDB; + readonly LiveUserInfo userInfo; + readonly IHttpContextAccessor accessor; + public AdminController(Repository baseService, Repository menuRelationDB, Repository menuDB, IHttpContextAccessor accessor, Repository roleDB, LiveUserInfo userInfo = null) : base(baseService) { this.baseService = baseService; this.menuRelationDB = menuRelationDB; this.menuDB = menuDB; + this.accessor = accessor; + this.roleDB = roleDB; + this.userInfo = userInfo; } /// /// 管理员登录 @@ -44,7 +56,7 @@ namespace Learn.Archives.API.Controllers if (admin.Password != model.Password.GetMD5()) Oh.Error("登录失败,密码错误"); // 获取租户信息 - var buttonRole = admin.RoleId==1 + var buttonRole = admin.RoleId == 1 ? ["*:*:*"] : await menuRelationDB.AsQueryable() .LeftJoin((mr, m) => mr.MenuId == m.Id) @@ -73,12 +85,107 @@ namespace Learn.Archives.API.Controllers } - public override Task Edit([FromBody] Admin model) + public override async Task Edit([FromBody] Admin model) { //创建用户时 密码加密 if (model.Id == 0) model.Password = model.Password.GetMD5(); - return base.Edit(model); + if (string.IsNullOrEmpty(model.Account) || model.Account.Length < 2 || + string.IsNullOrEmpty(model.Phone) || model.Phone.Length < 11 || + string.IsNullOrEmpty(model.Name) || model.Phone.Length < 2) + { + Oh.ModelError("账号/手机号/名称 不合法"); + } + if (await baseService.IsAnyAsync(s => s.Account == model.Account && s.Id != model.Id)) + Oh.ModelError($"账号 {model.Account} 已被使用!"); + return await base.Edit(model); + } + + + /// + /// 下载导入模板 + /// + /// + [HttpGet, ResultIgnore, AllowAnonymous] + public IActionResult DwImportTemplate() + { + var resultList = new List() { new AdminImport() + { + Account = "登录账号[建议使用手机号]", + Name = "必填:用户名称", + Phone = "联系方式", + Role = "必填:与系统的角色名称匹配\r\n普通成员 管理员", + Password = "必填: 登录密码", + } }; + return File(resultList.ExportExcel(), "application/ms-excel", + $"导入管理员模板{DateTime.Now.ToString("MMddHHmm")}.xlsx"); + } + + /// + /// 导入考试信息 + /// + /// + [HttpPost, ResultIgnore] + [HttpLogEnable] + public async Task Import(IFormFile? file) + { + if(!userInfo.IsSa) + Oh.ModelError("只允许管理员使用本功能!"); + var fl = file != null ? file : accessor.HttpContext?.Request.Form.Files[0]; + if (fl == null) Oh.ModelError("传入无效的数据"); + if (!Path.GetExtension(fl.FileName).Equals(".xlsx", StringComparison.OrdinalIgnoreCase)) + Oh.ModelError("请选择导入文件为.xlsx的后缀名!"); + //分析excel + IEnumerable dataList; + using var stream = new MemoryStream(); + { + await fl.CopyToAsync(stream); + dataList = stream.Query() + .Where(s => !string.IsNullOrEmpty(s.Account)); + } + if (dataList == null || dataList.Count() == 0) + Oh.ModelError("导入失败:无有效数据"); + + //处理数据 + var accountArr = await baseService.AsQueryable() + .Select(s => s.Account).Distinct() + .ToArrayAsync(); + var accountH = accountArr.ToHashSet(); + var roleDic = await roleDB.AsQueryable() + .ToDictionaryAsync(s => s.Name, s => s.Id); + var errorExcelInfo = new List(); + var insertInfo = new List(); + foreach (var imp in dataList) + { + imp.Account = imp.Account.Trim(); + imp.Phone = imp.Phone.Trim(); + imp.Name = imp.Name.Trim(); + imp.Role = imp.Role.Trim(); + if (accountH.Contains(imp.Account)) + { + imp.Error = $"导入失败:账号已被使用!"; + errorExcelInfo.Add(imp); + continue; + } + else if (!roleDic.ContainsKey(imp.Role)) + { + imp.Error = $"导入失败:无效的 角色名称!"; + errorExcelInfo.Add(imp); + continue; + } + var admin = imp.Adapt(); + admin.Enable = true; + admin.RoleId = (long)roleDic[imp.Role]; + admin.Password = imp.Password.Trim().GetMD5(); + insertInfo.Add(admin); + } + + if (errorExcelInfo.Count != 0) + return File(errorExcelInfo.ExportExcel(), "application/ms-excel" + , $"错误管理员信息{DateTime.Now.ToString("MMddHHmm")}.xlsx"); + //写入数据库 + await baseService.InsertRangeAsync(insertInfo); + return Ok(); } } } diff --git a/Learn.Archives.API/Controllers/Dto/AdminDto.cs b/Learn.Archives.API/Controllers/Dto/AdminDto.cs new file mode 100644 index 0000000..d7b3f72 --- /dev/null +++ b/Learn.Archives.API/Controllers/Dto/AdminDto.cs @@ -0,0 +1,10 @@ +using UserCenter.Model; +using UserCenter.Model.Common; + +namespace Learn.Archives.API.Controllers.Dto +{ + public class ClassDto:Classes + { + public string Grade => GradeHelper.GetGrade(this.GradeLevel, GraduationYear); + } +} diff --git a/Learn.Archives.API/Controllers/Dto/ClassDto.cs b/Learn.Archives.API/Controllers/Dto/ClassDto.cs index d7b3f72..2ed5b38 100644 --- a/Learn.Archives.API/Controllers/Dto/ClassDto.cs +++ b/Learn.Archives.API/Controllers/Dto/ClassDto.cs @@ -1,10 +1,37 @@ -using UserCenter.Model; +using MiniExcelLibs.Attributes; +using UserCenter.Model; using UserCenter.Model.Common; namespace Learn.Archives.API.Controllers.Dto { - public class ClassDto:Classes + + + public class AdminImportError : AdminImport { - public string Grade => GradeHelper.GetGrade(this.GradeLevel, GraduationYear); + + [ExcelColumn(Name = "错误原因", Width = 25)] + public string Error { get; set; } + } + public class AdminImport + { + + [ExcelColumn(Name = "账号", Width = 30)] + public string Account { get; set; } + + [ExcelColumn(Name = "名称", Width = 15)] + public string Name { get; set; } + + [ExcelColumn(Name = "电话号码", Width = 30)] + public string Phone { get; set; } + + [ExcelColumn(Name = "角色", Width = 30)] + public string Role { get; set; } + /// + /// 密码 + /// + [ExcelColumn(Name = "密码", Width = 20)] + public string Password { get; set; } + + } } diff --git a/Learn.Archives.API/Controllers/StudentController.cs b/Learn.Archives.API/Controllers/StudentController.cs index 6f65129..cf10306 100644 --- a/Learn.Archives.API/Controllers/StudentController.cs +++ b/Learn.Archives.API/Controllers/StudentController.cs @@ -347,7 +347,7 @@ namespace Learn.Archives.API.Controllers { var resultList = new List() { new StudentInfoImport() { - RealName = "导入规范[导入时请删除本列]", + RealName = "导入规范[导入时请删除本行]", School = "必填:与系统匹配", Grade = "必填:可选值\r\n[初一初二初三,高一高二高山]", Class = "必填:与系统匹配\r\n格式:10班[数字+班]", @@ -377,7 +377,7 @@ namespace Learn.Archives.API.Controllers new TeacherInfoImport() { Phone="必填", - RealName = "导入规范[导入时请删除本列]", + RealName = "导入规范[导入时请删除本行]", UserType = "必填 可选值\r\n[年级主任,班主任,教师]", School = "必填:与系统匹配", Grade = "必填:可选值\r\n[初一初二初三,高一高二高山]", diff --git a/Learn.Archives.API/Expand/HttpFilter.cs b/Learn.Archives.API/Expand/HttpFilter.cs index 6353a10..415aede 100644 --- a/Learn.Archives.API/Expand/HttpFilter.cs +++ b/Learn.Archives.API/Expand/HttpFilter.cs @@ -21,6 +21,7 @@ using Learn.Archives.Core.Common; using Learn.Archives.Core.Model.Dto; using Learn.Archives.Core.Model; using SqlSugar.IOC; +using static System.Net.Mime.MediaTypeNames; namespace Learn.Archives.API.Expand { @@ -50,8 +51,32 @@ namespace Learn.Archives.API.Expand this.userInfo = userInfo; } + /// + /// 执行接口前文件做缓存处理 + /// + /// + /// + public void ExecutingFileCached(ActionExecutingContext context) + { + //特殊处理:ResultIgnore,不进行返回结果包装,原样输出 + var endpoint = context.HttpContext.GetEndpoint(); + // 直接返回原始结果,不封装 + if (endpoint?.Metadata.GetMetadata() == null) return; + if (context.HttpContext.Request.HasFormContentType && + context.HttpContext.Request.Form.Files != null && + context.HttpContext.Request.Form.Files.Count() > 0) + { + context.HttpContext.Items["FileCached"]= + context.HttpContext.Request.Form.Files.Select(s => + { + var stream = new MemoryStream(); + s.CopyTo(stream); + stream.Position = 0; + return (s, stream); + }).ToArray(); + } - + } /// /// 执行接口前400 处理 /// @@ -132,43 +157,43 @@ namespace Learn.Archives.API.Expand string request = null; var logId = Yitter.IdGenerator.YitIdHelper.NextId(); - if (!context.Request.Method - .Equals("GET", StringComparison.InvariantCultureIgnoreCase)) + if (!context.Request.Method.Equals("GET", StringComparison.InvariantCultureIgnoreCase)) { - context.Request.EnableBuffering(); //记录请求参数 if (context.Request.Body.CanSeek) { try { - if (context.Request.HasFormContentType && context.Request?.Form?.Files?.Count() > 0) + var fileArr = context.Items.ContainsKey("FileCached") ? context.Items["FileCached"] as (IFormFile file, MemoryStream stream)[] : null; + if (context.Request.HasFormContentType && fileArr != null) { // 设置保存目录(例如:项目根目录下的Uploads文件夹) string uploadsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadLogs", logId.ToString()); // 创建目录(如果不存在) if (!Directory.Exists(uploadsFolder)) Directory.CreateDirectory(uploadsFolder); - foreach (var file in context.Request.Form.Files) + foreach (var fileInfo in fileArr) { // 生成安全文件名(防止路径遍历攻击) - string uniqueFileName = Guid.NewGuid().ToString().Substring(0, 5) + "_" + Path.GetFileName(file.FileName); - string filePath = Path.Combine(uploadsFolder, uniqueFileName); + string uniqueFileName = Guid.NewGuid().ToString().Substring(0, 5) + "_" + Path.GetFileName(fileInfo.file.FileName); // 保存文件 - using var stream = new FileStream(filePath, FileMode.Create); - await file.CopyToAsync(stream); + using var stream = new FileStream(Path.Combine(uploadsFolder, uniqueFileName), FileMode.Create, FileAccess.Write); + fileInfo.stream.Position = 0; + await fileInfo.stream.CopyToAsync(stream); + fileInfo.stream.Dispose(); } request = $"请求体包含{context.Request.Form.Files.Count()}个文件 目录 {uploadsFolder}"; } else { context.Request.Body.Position = 0; - using var sr = new StreamReader(context.Request.Body); + using var sr = new System.IO.StreamReader(context.Request.Body); request = await sr.ReadToEndAsync(); } } catch (Exception ex) { - request = "处理请求日志时发生了错误 \r\n" + ex.ToString(); + request = "处理请求入参时发生了错误 \r\n" + ex.ToString() + "\r\n 原有请求数据 " + request; } } } @@ -181,7 +206,7 @@ namespace Learn.Archives.API.Expand Request = request, IP = context.Connection?.RemoteIpAddress?.ToString(), ResponseCode = result?.Code ?? -1, - Response = result != null ? JsonSerializer.Serialize(result) : null, + Response = (result != null ? JsonSerializer.Serialize(result) : null) , Authorization = context.Request.Headers.ContainsKey("Authorization") ? context.Request.Headers["Authorization"].ToString() : string.Empty, @@ -193,18 +218,12 @@ namespace Learn.Archives.API.Expand } - - - - - - /// - /// 在Controller的Action执行前执行 - /// - /// - public override void OnActionExecuting(ActionExecutingContext context) + public override async void OnActionExecuting(ActionExecutingContext context) { + + Executing400(context); + ExecutingFileCached(context); base.OnActionExecuting(context); } @@ -214,6 +233,8 @@ namespace Learn.Archives.API.Expand /// public override async void OnActionExecuted(ActionExecutedContext context) { + + try { BaseReturn? res = ApiResultFormatting(context); diff --git a/Learn.Archives.API/Program.cs b/Learn.Archives.API/Program.cs index 0918320..0a3d6b4 100644 --- a/Learn.Archives.API/Program.cs +++ b/Learn.Archives.API/Program.cs @@ -17,17 +17,6 @@ builder.Services.AddLogging(loggingBuilder => loggingBuilder.SetMinimumLevel(LogLevel.Warning); // С־Ϊ Warning }); -builder.Services.AddControllers(options => -{ - // ȫģ͸ֵĬֵ ͳһظʽ - options.Filters.Add(); -}) -.AddJsonOptions(options => -{ - - options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//תʱʹUnicode - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// ĬСշ null շ -}); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerExpand("ѧУϵͳ"); builder.Configuration.AddAppConfig(args); @@ -43,6 +32,17 @@ builder.Services.AddHttpContextAccessor(); +builder.Services.AddControllers(options => +{ + // ȫģ͸ֵĬֵ ͳһظʽ + options.Filters.Add(); +}) +.AddJsonOptions(options => +{ + + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//תʱʹUnicode + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// ĬСշ null շ +}); var app = builder.Build(); AppCommon.Services = app.Services; diff --git a/Learn.Archives.Core/Common/BasicAuthMiddleware.cs b/Learn.Archives.Core/Common/BasicAuthMiddleware.cs index 9414eec..9705a5f 100644 --- a/Learn.Archives.Core/Common/BasicAuthMiddleware.cs +++ b/Learn.Archives.Core/Common/BasicAuthMiddleware.cs @@ -24,6 +24,9 @@ namespace Learn.Archives.Core.Common public async Task InvokeAsync(HttpContext context) { + if (!context.Request.Body.CanSeek) + context.Request.EnableBuffering(); // 允许重新读取请求体 + if (context.Request.Path.StartsWithSegments("/swagger") && (context.Request.Path.Value?.Contains("swagger.json") ?? true)) { @@ -36,7 +39,6 @@ namespace Learn.Archives.Core.Common if (await IsAuthorized(usernamePassword[0], usernamePassword[1])) { - await _next(context); return; }