staging #20

Merged
hy merged 24 commits from staging into master 2025-10-14 11:13:23 +08:00
7 changed files with 211 additions and 44 deletions
Showing only changes of commit 12c022e05c - Show all commits

View File

@ -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<Admin> baseService;
readonly Repository<MenuRelation> menuRelationDB;
readonly Repository<Menu> menuDB;
public AdminController(Repository<Admin> baseService, Repository<MenuRelation> menuRelationDB, Repository<Menu> menuDB) : base(baseService)
readonly Repository<AdminRole> roleDB;
readonly LiveUserInfo userInfo;
readonly IHttpContextAccessor accessor;
public AdminController(Repository<Admin> baseService, Repository<MenuRelation> menuRelationDB, Repository<Menu> menuDB, IHttpContextAccessor accessor, Repository<AdminRole> roleDB, LiveUserInfo userInfo = null) : base(baseService)
{
this.baseService = baseService;
this.menuRelationDB = menuRelationDB;
this.menuDB = menuDB;
this.accessor = accessor;
this.roleDB = roleDB;
this.userInfo = userInfo;
}
/// <summary>
/// 管理员登录
@ -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<Menu>((mr, m) => mr.MenuId == m.Id)
@ -73,12 +85,107 @@ namespace Learn.Archives.API.Controllers
}
public override Task<bool> Edit([FromBody] Admin model)
public override async Task<bool> 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);
}
/// <summary>
/// 下载导入模板
/// </summary>
/// <returns></returns>
[HttpGet, ResultIgnore, AllowAnonymous]
public IActionResult DwImportTemplate()
{
var resultList = new List<AdminImport>() { new AdminImport()
{
Account = "登录账号[建议使用手机号]",
Name = "必填:用户名称",
Phone = "联系方式",
Role = "必填:与系统的角色名称匹配\r\n普通成员 管理员",
Password = "必填: 登录密码",
} };
return File(resultList.ExportExcel(), "application/ms-excel",
$"导入管理员模板{DateTime.Now.ToString("MMddHHmm")}.xlsx");
}
/// <summary>
/// 导入考试信息
/// </summary>
/// <returns></returns>
[HttpPost, ResultIgnore]
[HttpLogEnable]
public async Task<IActionResult> 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<AdminImportError> dataList;
using var stream = new MemoryStream();
{
await fl.CopyToAsync(stream);
dataList = stream.Query<AdminImportError>()
.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<AdminImportError>();
var insertInfo = new List<Admin>();
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>();
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();
}
}
}

View File

@ -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);
}
}

View File

@ -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; }
/// <summary>
/// 密码
/// </summary>
[ExcelColumn(Name = "密码", Width = 20)]
public string Password { get; set; }
}
}

View File

@ -347,7 +347,7 @@ namespace Learn.Archives.API.Controllers
{
var resultList = new List<StudentInfoImport>() { 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[初一初二初三,高一高二高山]",

View File

@ -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;
}
/// <summary>
/// 执行接口前文件做缓存处理
/// </summary>
/// <param name="context"></param>
/// <exception cref="CustomException"></exception>
public void ExecutingFileCached(ActionExecutingContext context)
{
//特殊处理ResultIgnore不进行返回结果包装原样输出
var endpoint = context.HttpContext.GetEndpoint();
// 直接返回原始结果,不封装
if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == 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();
}
}
/// <summary>
/// 执行接口前400 处理
/// </summary>
@ -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
}
/// <summary>
/// 在Controller的Action执行前执行
/// </summary>
/// <param name="context"></param>
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
/// <param name="context"></param>
public override async void OnActionExecuted(ActionExecutedContext context)
{
try
{
BaseReturn<object>? res = ApiResultFormatting(context);

View File

@ -17,17 +17,6 @@ builder.Services.AddLogging(loggingBuilder =>
loggingBuilder.SetMinimumLevel(LogLevel.Warning); // 设置最小日志级别为 Warning
});
builder.Services.AddControllers(options =>
{
// 全局模型赋值默认值 和 统一返回格式处理
options.Filters.Add<HttpLogAttribute>();
})
.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<HttpLogAttribute>();
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//中文转换时不使用Unicode
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// 默认小驼峰 null 大驼峰
});
var app = builder.Build();
AppCommon.Services = app.Services;

View File

@ -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;
}