diff --git a/LMS.Common/Dictionary/AllOptions.cs b/LMS.Common/Dictionary/AllOptions.cs index 5eb2f57..12c825a 100644 --- a/LMS.Common/Dictionary/AllOptions.cs +++ b/LMS.Common/Dictionary/AllOptions.cs @@ -5,12 +5,47 @@ namespace LMS.Common.Dictionary; public class AllOptions { + + public static class AllOptionKey + { + /// + /// 获取所有的 Option + /// + public const string All = "all"; + + /// + /// 获取TTS相关的 Option + /// + public const string TTS = "tts"; + + /// + /// 获取软件相关的 Option + /// + public const string Software = "software"; + + /// + /// 软件试用相关 Option + /// + public const string Trial = "trial"; + + /// + /// 出图相关的 Option + /// + public const string Image = "image"; + + /// + /// 邮件设置相关 Option + /// + public const string MailSetting = "mailSetting"; + } + public static readonly Dictionary> AllOptionsRequestQuery = new() { - { "all", [] }, - { "tts", ["EdgeTTsRoles"] }, - { "software", ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent","LaitoolVersion"]}, - { "trial" , ["LaiToolTrialDays"] }, - { "image", [OptionKeyName.LaitoolFluxApiModelList] } + { AllOptionKey.All, [] }, + { AllOptionKey.TTS, ["EdgeTTsRoles"] }, + { AllOptionKey.Software, ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent","LaitoolVersion"]}, + { AllOptionKey.Trial , ["LaiToolTrialDays"] }, + { AllOptionKey.Image, [OptionKeyName.LaitoolFluxApiModelList] }, + { AllOptionKey.MailSetting , [OptionKeyName.SMTPMailSetting] } }; } diff --git a/LMS.Common/Dictionary/SimpleOptions.cs b/LMS.Common/Dictionary/SimpleOptions.cs index 3834b93..a4e59de 100644 --- a/LMS.Common/Dictionary/SimpleOptions.cs +++ b/LMS.Common/Dictionary/SimpleOptions.cs @@ -4,10 +4,35 @@ namespace LMS.Common.Dictionary; public class SimpleOptions { + + public static class SimpleOptionKey + { + /// + /// TTS的角色Option + /// + public const string Ttsrole = "ttsrole"; + + /// + /// LaiTool信息相关的配置 + /// + public const string Laitoolinfo = "laitoolinfo"; + + /// + /// LaiTool FluxAPI对应的模型信息 + /// + public const string LaitoolFluxApiModelList = "LaitoolFluxApiModelList"; + + /// + /// 是否开启邮箱信息 + /// + public const string EnableMailService = "EnableMailService"; + } + public static readonly Dictionary> SimpleOptionsRequestQuery = new() { - { "ttsrole", ["EdgeTTsRoles"] }, - { "laitoolinfo", ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent", "LaitoolVersion"] }, - { OptionKeyName.LaitoolFluxApiModelList, [OptionKeyName.LaitoolFluxApiModelList] } + { SimpleOptionKey.Ttsrole, ["EdgeTTsRoles"] }, + { SimpleOptionKey.Laitoolinfo, ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent", "LaitoolVersion"] }, + { SimpleOptionKey.LaitoolFluxApiModelList, [OptionKeyName.LaitoolFluxApiModelList] }, + { SimpleOptionKey.EnableMailService, [OptionKeyName.EnableMailService]} }; } \ No newline at end of file diff --git a/LMS.Common/Enum/OptionTypeEnum.cs b/LMS.Common/Enum/OptionTypeEnum.cs index 8648191..318d89d 100644 --- a/LMS.Common/Enum/OptionTypeEnum.cs +++ b/LMS.Common/Enum/OptionTypeEnum.cs @@ -5,6 +5,7 @@ public enum OptionTypeEnum String = 1, JSON = 2, Number = 3, + Boolean = 4 } public static class OptionKeyName @@ -13,4 +14,14 @@ public static class OptionKeyName /// LaiTool Flux API 模型列表的Option Key /// public const string LaitoolFluxApiModelList = "LaitoolFluxApiModelList"; + + /// + /// SMTP的邮件设置 + /// + public const string SMTPMailSetting = "SMTPMailSetting"; + + /// + /// 是否开启邮箱服务 + /// + public const string EnableMailService = "EnableMailService"; } diff --git a/LMS.Common/LMS.Common.csproj b/LMS.Common/LMS.Common.csproj index fa71b7a..67198ff 100644 --- a/LMS.Common/LMS.Common.csproj +++ b/LMS.Common/LMS.Common.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/LMS.Common/Templates/EmailTemplateService.cs b/LMS.Common/Templates/EmailTemplateService.cs new file mode 100644 index 0000000..a1d5fb6 --- /dev/null +++ b/LMS.Common/Templates/EmailTemplateService.cs @@ -0,0 +1,37 @@ +namespace LMS.Common.Templates +{ + public class EmailTemplateService() + { + /// + /// 注册邮件模板 + /// + public const string RegisterHtmlTemplates = """ + + + +

LMS Registration Code

+

Hi there,

+

Your code is {RegisterCode}

+

The code is valid for 10 minutes, if it is not you, please ignore it.

+ + + """; + + + /// + /// 替换模板的占位符 + /// + /// + /// + /// + public static string ReplaceTemplate(string template, Dictionary parameters) + { + // 替换占位符 + foreach (var param in parameters) + { + template = template.Replace($"{{{param.Key}}}", param.Value); + } + return template; + } + } +} diff --git a/LMS.DAO/ApplicationDbContext.cs b/LMS.DAO/ApplicationDbContext.cs index 6c45f11..178a037 100644 --- a/LMS.DAO/ApplicationDbContext.cs +++ b/LMS.DAO/ApplicationDbContext.cs @@ -4,6 +4,7 @@ using LMS.Repository.DB; using LMS.Repository.Models.DB; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using System.Text.Json; namespace LMS.DAO @@ -52,6 +53,15 @@ namespace LMS.DAO v => string.IsNullOrEmpty(v) || v == "[]" ? new List() // 如果存储的是空字符串或空数组,则返回空列表 : JsonSerializer.Deserialize>(v, (JsonSerializerOptions?)null) ?? new List() + ).Metadata.SetValueComparer( + new ValueComparer>( + // 比较两个集合是否相等 + (c1, c2) => c1 != null && c2 != null && c1.SequenceEqual(c2), + // 计算集合的哈希码 - 这里修复了问题 + c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v != null ? v.GetHashCode() : 0)), + // 创建集合的副本 + c => c == null ? null : c.ToList() + ) ); modelBuilder.Entity() .HasKey(us => new { us.UserId, us.SoftwareId }); diff --git a/LMS.DAO/LMS.DAO.csproj b/LMS.DAO/LMS.DAO.csproj index c3d9456..76fc1e0 100644 --- a/LMS.DAO/LMS.DAO.csproj +++ b/LMS.DAO/LMS.DAO.csproj @@ -8,6 +8,7 @@ + diff --git a/LMS.DAO/UserDAO/UserBasicDAO.cs b/LMS.DAO/UserDAO/UserBasicDAO.cs index 2c18e3a..ad46373 100644 --- a/LMS.DAO/UserDAO/UserBasicDAO.cs +++ b/LMS.DAO/UserDAO/UserBasicDAO.cs @@ -55,6 +55,42 @@ namespace LMS.DAO.UserDAO bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin"); return isSuperAdmin; } + + /// + /// 检查用户是不是管理员 + /// + /// + /// + /// + public async Task CheckUserIsAdmin(long? userId) + { + if (userId == null) + { + return false; + } + User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在"); + + bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Admin"); + return isSuperAdmin; + } + + /// + /// 检查用户是不是代理 + /// + /// + /// + /// + public async Task CheckUserIsAgent(long? userId) + { + if (userId == null) + { + return false; + } + User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在"); + + bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Agent User"); + return isSuperAdmin; + } } } diff --git a/LMS.Repository/User/RegisterModel.cs b/LMS.Repository/User/RegisterModel.cs index 281ba9f..d06155d 100644 --- a/LMS.Repository/User/RegisterModel.cs +++ b/LMS.Repository/User/RegisterModel.cs @@ -6,7 +6,8 @@ namespace LMS.Repository.Models.User { [Required] public required string UserName { get; set; } - public string? Email { get; set; } + [Required] + public required string Email { get; set; } [Required] public required string Password { get; set; } [Required] @@ -14,5 +15,7 @@ namespace LMS.Repository.Models.User [Required] public required string AffiliateCode { get; set; } + + public string? VerificationCode { get; set; } } } diff --git a/LMS.service/Configuration/AddLoggerConfig.cs b/LMS.service/Configuration/AddLoggerConfig.cs new file mode 100644 index 0000000..45fa1c6 --- /dev/null +++ b/LMS.service/Configuration/AddLoggerConfig.cs @@ -0,0 +1,29 @@ +using Serilog; + +namespace LMS.service.Configuration +{ + public static class AddLoggerConfig + { + public static void AddLoggerService(this IServiceCollection services) + { + // 确保logs目录存在 + Directory.CreateDirectory("logs"); + // 加载配置 + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + // 配置Serilog + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + // 添加Serilog到.NET Core的日志系统 + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddSerilog(dispose: true); + }); + } + } +} diff --git a/LMS.service/Configuration/AuthenticationExtensions.cs b/LMS.service/Configuration/AuthenticationExtensions.cs index c7ab974..cb1e120 100644 --- a/LMS.service/Configuration/AuthenticationExtensions.cs +++ b/LMS.service/Configuration/AuthenticationExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.IdentityModel.Tokens; using System.Text; -namespace Lai_server.Configuration +namespace LMS.service.Configuration { public static class AuthenticationExtensions { diff --git a/LMS.service/Configuration/InitConfiguration/DatabaseConfiguration.cs b/LMS.service/Configuration/InitConfiguration/DatabaseConfiguration.cs index 9bcec72..a44a91c 100644 --- a/LMS.service/Configuration/InitConfiguration/DatabaseConfiguration.cs +++ b/LMS.service/Configuration/InitConfiguration/DatabaseConfiguration.cs @@ -101,7 +101,9 @@ public class DatabaseConfiguration(IServiceProvider serviceProvider) : IHostedSe new Options { Key = "LaitoolNotice", Value = string.Empty, Type = OptionTypeEnum.String }, new Options { Key = "LaitoolVersion", Value = string.Empty, Type = OptionTypeEnum.String }, new Options { Key = "LaiToolTrialDays", Value = "2" , Type = OptionTypeEnum.Number}, - new Options { Key = OptionKeyName.LaitoolFluxApiModelList, Value = "{}" , Type = OptionTypeEnum.JSON } + new Options { Key = OptionKeyName.LaitoolFluxApiModelList, Value = "{}" , Type = OptionTypeEnum.JSON }, + new Options {Key = OptionKeyName.EnableMailService, Value = false.ToString(), Type = OptionTypeEnum.Boolean}, + new Options {Key = OptionKeyName.SMTPMailSetting, Value ="{}" , Type = OptionTypeEnum.JSON } ]; // 遍历所有的配置项,如果没有则添加 diff --git a/LMS.service/Configuration/ServiceConfiguration.cs b/LMS.service/Configuration/ServiceConfiguration.cs index f965608..a587beb 100644 --- a/LMS.service/Configuration/ServiceConfiguration.cs +++ b/LMS.service/Configuration/ServiceConfiguration.cs @@ -3,6 +3,7 @@ using LMS.DAO.PermissionDAO; using LMS.DAO.RoleDAO; using LMS.DAO.UserDAO; using LMS.service.Configuration.InitConfiguration; +using LMS.service.Extensions.Mail; using LMS.service.Service; using LMS.service.Service.PermissionService; using LMS.service.Service.PromptService; @@ -44,6 +45,13 @@ namespace Lai_server.Configuration services.AddScoped(); services.AddScoped(); + + // 注入 Extensions + services.AddScoped(); + services.AddScoped(); + + // 添加分布式缓存(用于存储验证码) + services.AddDistributedMemoryCache(); } } } diff --git a/LMS.service/Controllers/OptionsController.cs b/LMS.service/Controllers/OptionsController.cs index 9ae222b..83b7ae9 100644 --- a/LMS.service/Controllers/OptionsController.cs +++ b/LMS.service/Controllers/OptionsController.cs @@ -1,5 +1,6 @@ using LMS.Repository.DB; using LMS.Repository.DTO; +using LMS.Repository.Models.DB; using LMS.Repository.Options; using LMS.service.Service; using LMS.Tools.Extensions; @@ -61,5 +62,16 @@ namespace LMS.service.Controllers #endregion + #region 测试邮箱发送 + + [HttpPost] + [Authorize] + public async Task>> TestSendMail() + { + long userId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0); + return await _optionsService.TestSendMail(userId); + } + + #endregion } } diff --git a/LMS.service/Controllers/UserController.cs b/LMS.service/Controllers/UserController.cs index f9e3d18..24a0cde 100644 --- a/LMS.service/Controllers/UserController.cs +++ b/LMS.service/Controllers/UserController.cs @@ -77,7 +77,7 @@ namespace LMS.service.Controllers var cookieOptions = new CookieOptions { HttpOnly = true, - Secure = false, // 如果使用 HTTPS + Secure = true, // 如果使用 HTTPS SameSite = SameSiteMode.None, Expires = DateTime.UtcNow.AddDays(7), }; @@ -129,6 +129,19 @@ namespace LMS.service.Controllers #endregion + #region 获取邮箱验证码 + + [HttpPost] + public async Task>> SendVerificationCode([FromBody] EmailVerificationService.SendVerificationCodeDto model) + { + if (!ModelState.IsValid) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError); + + return await _userService.SendVerificationCode(model); + } + + #endregion + #region 刷新token [HttpPost] diff --git a/LMS.service/Extensions/Mail/EmailService.cs b/LMS.service/Extensions/Mail/EmailService.cs new file mode 100644 index 0000000..1c8c282 --- /dev/null +++ b/LMS.service/Extensions/Mail/EmailService.cs @@ -0,0 +1,180 @@ +using LMS.Common.Enum; +using LMS.DAO; +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.EntityFrameworkCore; +using MimeKit; +using Newtonsoft.Json; +using ILogger = Serilog.ILogger; // 明确使用 Serilog 的 ILogger +namespace LMS.service.Extensions.Mail +{ + + public class MailSetting + { + /// + /// 是否开启邮箱服务 + /// + public bool EnableMailService { get; set; } + /// + /// 是否开启SSL + /// + public bool EnableSSL { get; set; } + /// + /// SMTP服务器 + /// + public string SmtpServer { get; set; } + /// + /// 端口 + /// + public int Port { get; set; } + /// + /// 发送用户名 + /// + public string Username { get; set; } + /// + /// 密码 + /// + public string Password { get; set; } + /// + /// 发送者邮箱 + /// + public string SenderEmail { get; set; } + /// + /// 测试接收邮箱 + /// + public string TestReceiveMail { get; set; } + } + + public class EmailService + { + private readonly ILogger _logger; + private readonly ApplicationDbContext _context; + + // 构造函数注入 + public EmailService(ILogger logger, ApplicationDbContext context) + { + _logger = logger; + _context = context; + } + + /// + /// 发送安全邮件 + /// + /// 收信邮箱 + /// 邮件主题 + /// 邮件信息 body + /// + public async Task SendEmailSafelyAsync(string to, string subject, string body, bool isTest = false) + { + if (string.IsNullOrEmpty(to)) + { + _logger.Information("收件人地址为空,邮件未发送"); + return; + } + + try + { + await SendEmailInternalAsync(to, subject, body, isTest); + _logger.Information($"邮件已成功发送至: {to}"); + } + catch (Exception ex) + { + // 记录错误但不抛出异常,不影响业务流程 + _logger.Error(ex, $"发送邮件至 {to} 时出错: {ex.Message}"); + } + } + + /// + /// 发送邮件,捕获异常并记录 + /// + /// 收信邮箱 + /// 邮件主题 + /// 邮件信息 body + /// + /// + public async Task SendEmailAsync(string to, string subject, string body, bool isTest = false) + { + if (string.IsNullOrEmpty(to)) + { + _logger.Information("收件人地址为空,邮件未发送"); + throw new Exception("收件人地址为空,邮件未发送"); + } + + try + { + await SendEmailInternalAsync(to, subject, body, isTest); + _logger.Information($"邮件已成功发送至: {to}"); + } + catch (Exception ex) + { + // 记录错误但不抛出异常,不影响业务流程 + _logger.Error(ex, $"发送邮件至 {to} 时出错: {ex.Message}"); + throw new Exception(ex.Message); + } + } + + private async Task SendEmailInternalAsync(string to, string subject, string body, bool isTest = false) + { + try + { + + var mailSettingString = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.SMTPMailSetting) ?? throw new Exception("邮件配置不存在,请先配置"); + + MailSetting? mailSetting = JsonConvert.DeserializeObject(mailSettingString.Value ?? "{}") ?? throw new Exception("邮件配置不存在,请先配置"); + string smtpServer = mailSetting.SmtpServer; + int port = mailSetting.Port; + string username = mailSetting.Username; + string password = mailSetting.Password; + string senderEmail = mailSetting.SenderEmail; + // 加载邮件设置 + + if (mailSetting.EnableMailService == false) + { + throw new Exception("邮件服务未开启,请先开启"); + } + + if (isTest) + { + if (string.IsNullOrEmpty(mailSetting.TestReceiveMail)) + { + throw new Exception("测试接收邮箱未设置,请先设置"); + } + to = mailSetting.TestReceiveMail; + } + + // 邮箱信息检查成功,开始发送邮件 + + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(username, senderEmail)); + message.To.Add(new MailboxAddress("", to)); + message.Subject = subject; + + var bodyBuilder = new BodyBuilder + { + HtmlBody = body + }; + message.Body = bodyBuilder.ToMessageBody(); + + // 发送邮件 + using var client = new SmtpClient(); + + // 使用配置的服务器和端口 + await client.ConnectAsync(smtpServer, port, SecureSocketOptions.SslOnConnect); + + // 登录 + await client.AuthenticateAsync(senderEmail, password); + + // 设置超时 + client.Timeout = 30000; // 30秒超时 + + // 发送 + await client.SendAsync(message); + await client.DisconnectAsync(true); + } + catch (Exception ex) + { + throw new Exception("邮件发送失败,失败信息" + ex.Message); + } + } + } +} diff --git a/LMS.service/Extensions/Mail/EmailVerificationService.cs b/LMS.service/Extensions/Mail/EmailVerificationService.cs new file mode 100644 index 0000000..cb097db --- /dev/null +++ b/LMS.service/Extensions/Mail/EmailVerificationService.cs @@ -0,0 +1,71 @@ +using LMS.Common.Templates; +using LMS.service.Extensions.Mail; +using Microsoft.Extensions.Caching.Distributed; +using System.ComponentModel.DataAnnotations; + +public class EmailVerificationService +{ + private readonly IDistributedCache _cache; + private readonly Random _random; + private readonly EmailService _emailService; + + public EmailVerificationService(IDistributedCache cache, EmailService emailService) + { + _cache = cache; + _random = new Random(); + _emailService = emailService; + } + + public class SendVerificationCodeDto + { + [Required(ErrorMessage = "电子邮件地址是必填项")] + [EmailAddress(ErrorMessage = "请输入有效的电子邮件地址")] + public string Email { get; set; } + } + + // 生成并发送验证码 + public async Task SendVerificationCodeAsync(string email) + { + // 生成6位数验证码 + string verificationCode = GenerateVerificationCode(); + + // 将验证码保存到分布式缓存,设置10分钟过期 + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) + }; + + await _cache.SetStringAsync($"EmailVerification_{email}", verificationCode, options); + + var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.RegisterHtmlTemplates, new Dictionary + { + { "RegisterCode", verificationCode } + }); + + // 发送验证码邮件 + await _emailService.SendEmailAsync(email, "LMS注册验证码", emailBody); + } + + // 验证用户提交的验证码 + public async Task VerifyCodeAsync(string email, string code) + { + var storedCode = await _cache.GetStringAsync($"EmailVerification_{email}"); + + if (string.IsNullOrEmpty(storedCode)) + return false; + + // 使用完就删除验证码 + if (storedCode == code) + { + await _cache.RemoveAsync($"EmailVerification_{email}"); + return true; + } + + return false; + } + + private string GenerateVerificationCode() + { + return _random.Next(100000, 999999).ToString(); + } +} \ No newline at end of file diff --git a/LMS.service/Extensions/Middleware/DynamicPermissionMiddleware.cs b/LMS.service/Extensions/Middleware/DynamicPermissionMiddleware.cs index 2f798d9..e20d221 100644 --- a/LMS.service/Extensions/Middleware/DynamicPermissionMiddleware.cs +++ b/LMS.service/Extensions/Middleware/DynamicPermissionMiddleware.cs @@ -4,14 +4,9 @@ using System.Security.Claims; namespace LMS.service.Extensions.Middleware { - public class DynamicPermissionMiddleware + public class DynamicPermissionMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - - public DynamicPermissionMiddleware(RequestDelegate next) - { - _next = next; - } + private readonly RequestDelegate _next = next; public async Task InvokeAsync(HttpContext context, PremissionValidationService _premissionValidationServices) { @@ -45,7 +40,7 @@ namespace LMS.service.Extensions.Middleware } } - private long GetUserIdFromContext(HttpContext context) + private static long GetUserIdFromContext(HttpContext context) { var userIdClaim = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); var userId = userIdClaim?.Value; diff --git a/LMS.service/LMS.service.csproj b/LMS.service/LMS.service.csproj index d71f536..5be8ef6 100644 --- a/LMS.service/LMS.service.csproj +++ b/LMS.service/LMS.service.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,11 +7,10 @@ ed64fb6f-9c93-43d0-b418-61f507f28420 Linux . - 1.0.3 - + @@ -23,8 +22,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/LMS.service/Program.cs b/LMS.service/Program.cs index 0c38641..8402867 100644 --- a/LMS.service/Program.cs +++ b/LMS.service/Program.cs @@ -6,8 +6,7 @@ using LMS.service.Configuration.InitConfiguration; using LMS.service.Extensions.Middleware; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; +using Serilog; var builder = WebApplication.CreateBuilder(args); @@ -33,6 +32,10 @@ builder.Services.ConfigureApplicationCookie(options => //JWT builder.Services.AddJWTAuthentication(); builder.Services.AddAutoMapper(typeof(AutoMapperConfig)); +builder.Services.AddLoggerService(); +builder.Host.UseSerilog(); +// ؼ裺ע Serilog.ILogger DI +builder.Services.AddSingleton(Log.Logger); builder.Services.AddDbContext(options => { @@ -76,6 +79,8 @@ builder.Services.AddHostedService(); var app = builder.Build(); +var version = builder.Configuration["Version"]; + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -102,5 +107,6 @@ app.UseEndpoints(endpoints => _ = endpoints.MapControllers(); }); +Log.Information("̨ɹϵͳ汾" + version); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/LMS.service/Service/MachineService.cs b/LMS.service/Service/MachineService.cs index 685c7db..50f14c0 100644 --- a/LMS.service/Service/MachineService.cs +++ b/LMS.service/Service/MachineService.cs @@ -10,6 +10,8 @@ using LMS.Tools.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; using static LMS.Common.Enums.MachineEnum; using static LMS.Common.Enums.ResponseCodeEnum; using static LMS.Repository.DTO.MachineResponse.MachineDto; @@ -321,7 +323,7 @@ namespace LMS.service.Service /// /// /// - internal async Task>>> QueryMachineCollection(int page, int pageSize, string? machineId, string? createdUserName, MachineStatus? status, MachineUseStatus? useStatus, string? remark, string? ownUserName, long requestUserId) + public async Task>>> QueryMachineCollection(int page, int pageSize, string? machineId, string? createdUserName, MachineStatus? status, MachineUseStatus? useStatus, string? remark, string? ownUserName, long requestUserId) { try { @@ -332,35 +334,94 @@ namespace LMS.service.Service } bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin"); bool isAdmin = await _userManager.IsInRoleAsync(user, "Admin"); + bool isAgent = await _userManager.IsInRoleAsync(user, "Agent User"); IQueryable query = _context.Machine; - if (isAdmin) - { - List superAdminUserIds = ((List)await _userManager.GetUsersInRoleAsync("Super Admin")).Select(x => x.Id).ToList(); - - //.Result.Select(x => x.Id).ToList(); - query = query.Where(x => !superAdminUserIds.Contains(x.UserID)); - } - else if (!isSuperAdmin) - { - query = query.Where(x => x.UserID == requestUserId); - } // 添加其他的查询条件 if (!string.IsNullOrWhiteSpace(machineId)) { query = query.Where(x => x.MachineId == machineId); } - // 管理员和超级管理员可以使用该字段查询所有创建者的机器码 - if (!string.IsNullOrWhiteSpace(createdUserName) && (isAdmin || isSuperAdmin)) + + // 更具用户角色判断当前可能查询那些用户的机器码 + + if (!isAdmin && !isSuperAdmin && !isAgent) { - List queryUserId = (await _userManager.Users.Where(x => x.UserName.Contains(createdUserName)).ToListAsync()).Select(x => x.Id).ToList(); - query = query.Where(x => queryUserId.Contains(x.CreateId)); + // 普通用户只能查看所属自己的机器码,不具备查询创建者和所属者的权限 + query = query.Where(x => x.UserID == user.Id); } - // 普通用户只能查找自己创建的机器码 - else if (!string.IsNullOrWhiteSpace(createdUserName)) + else { - query = query.Where(x => x.CreateId == user.Id); + // 获取相关用户ID + var userLookupQuery = _userManager.Users.AsNoTracking(); + + HashSet filteredCreatorIds = null; + HashSet filteredOwnerIds = null; + + if (!string.IsNullOrWhiteSpace(createdUserName)) + { + // 获取列表后直接转换为HashSet + var list = await userLookupQuery + .Where(u => u.UserName.Contains(createdUserName)) + .Select(u => u.Id) + .ToListAsync(); + + filteredCreatorIds = new HashSet(list); + } + + if (!string.IsNullOrWhiteSpace(ownUserName)) + { + var list = await userLookupQuery + .Where(u => u.UserName.Contains(ownUserName)) + .Select(u => u.Id) + .ToListAsync(); + + filteredOwnerIds = new HashSet(list); + } + + // 数据过滤 + if (filteredCreatorIds?.Count > 0) + { + query = query.Where(x => filteredCreatorIds.Contains(x.CreateId)); + } + + if (filteredOwnerIds?.Count > 0) + { + query = query.Where(x => filteredOwnerIds.Contains(x.UserID)); + } + + + if (isAdmin && !isSuperAdmin) + { + // 除了超级管理员的代理 其他都能看到 + IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); + List superUserIds = superUsers.Select(x => x.Id).ToList(); + var list = await userLookupQuery + .Where(u => u.ParentId == null || superUserIds.Contains((long)u.ParentId) || superUserIds.Contains(u.Id)) + .Select(u => u.Id) + .ToListAsync(); + + HashSet filteredParentIds = new(list); + if (filteredParentIds?.Count > 0) + { + query = query.Where(x => !filteredParentIds.Contains(x.UserID) || x.UserID == requestUserId); + } + } + else if (isAgent && !isSuperAdmin) + { + // 代理只能看到自己下面的用户 + var list = await userLookupQuery + .Where(u => u.ParentId == requestUserId) + .Select(u => u.Id) + .ToListAsync(); + + HashSet filteredParentIds = new(list); + if (filteredParentIds?.Count > 0) + { + query = query.Where(x => filteredParentIds.Contains(x.UserID) || x.UserID == requestUserId); + } + } } if (status != null) @@ -376,18 +437,6 @@ namespace LMS.service.Service query = query.Where(x => x.Remark.Contains(remark)); } - // 管理员和超级管理员可以使用该字段查询所有的机器码的拥有者 - if (!string.IsNullOrWhiteSpace(ownUserName) && (isAdmin || isSuperAdmin)) - { - List queryUserId = (await _userManager.Users.Where(x => x.UserName.Contains(ownUserName)).ToListAsync()).Select(x => x.Id).ToList(); - query = query.Where(x => queryUserId.Contains(x.UserID)); - } - // 普通用户只能查找自己拥有的机器码 - else if (!string.IsNullOrWhiteSpace(ownUserName)) - { - query = query.Where(x => x.UserID == user.Id); - } - int total = await query.CountAsync(); // 降序,取指定的条数的数据 diff --git a/LMS.service/Service/OptionsService.cs b/LMS.service/Service/OptionsService.cs index f234527..d4fea9a 100644 --- a/LMS.service/Service/OptionsService.cs +++ b/LMS.service/Service/OptionsService.cs @@ -1,11 +1,13 @@ using AutoMapper; using LMS.Common.Dictionary; +using LMS.Common.Templates; using LMS.DAO; +using LMS.DAO.UserDAO; using LMS.Repository.DB; using LMS.Repository.DTO; -using LMS.Repository.DTO.UserDto; using LMS.Repository.Models.DB; using LMS.Repository.Options; +using LMS.service.Extensions.Mail; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -13,11 +15,13 @@ using static LMS.Common.Enums.ResponseCodeEnum; namespace LMS.service.Service { - public class OptionsService(ApplicationDbContext context, UserManager userManager, IMapper mapper) + public class OptionsService(ApplicationDbContext context, UserManager userManager, IMapper mapper, UserBasicDao userBasicDao, EmailService emailService) { private readonly ApplicationDbContext _context = context; private readonly UserManager _userManager = userManager; private readonly IMapper _mapper = mapper; + private readonly UserBasicDao _userBasicDao = userBasicDao; + private readonly EmailService _emailService = emailService; #region 获取简单的配置项,无需权限 @@ -159,5 +163,42 @@ namespace LMS.service.Service } } #endregion + + #region 测试邮箱发送 + + /// + /// 测试邮箱发送 + /// + /// + /// + public async Task>> TestSendMail(long userId) + { + try + { + // 判断是不是超级管理员 + bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(userId); + if (!isSuperAdmin) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.NotPermissionAction); + } + + var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.RegisterHtmlTemplates, new Dictionary + { + { "RegisterCode", "验证码" } + }); + + // 调用发送邮件的方法 + await _emailService.SendEmailAsync("user@example.com", "邮件连通测试", emailBody, true); + + + return APIResponseModel.CreateSuccessResponseModel("邮箱测试发送成功"); + } + catch (Exception ex) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, ex.Message); + } + } + + #endregion } } diff --git a/LMS.service/Service/PromptService/PromptService.cs b/LMS.service/Service/PromptService/PromptService.cs index 7a1fb61..49c8df5 100644 --- a/LMS.service/Service/PromptService/PromptService.cs +++ b/LMS.service/Service/PromptService/PromptService.cs @@ -12,7 +12,6 @@ using LMS.Tools.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; using static LMS.Common.Enums.PromptEnum; using static LMS.Common.Enums.ResponseCodeEnum; diff --git a/LMS.service/Service/SoftwareService/SoftwareControlService.cs b/LMS.service/Service/SoftwareService/SoftwareControlService.cs index aa0230a..f3e26e8 100644 --- a/LMS.service/Service/SoftwareService/SoftwareControlService.cs +++ b/LMS.service/Service/SoftwareService/SoftwareControlService.cs @@ -10,17 +10,19 @@ using LMS.Repository.Models.DB; using LMS.Repository.Software; using LMS.Tools; using LMS.Tools.Extensions; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static LMS.Common.Enums.ResponseCodeEnum; namespace LMS.service.Service.SoftwareService { - public class SoftwareControlService(UserBasicDao userBasicDao, ApplicationDbContext dbContext, IMapper mapper) + public class SoftwareControlService(UserBasicDao userBasicDao, ApplicationDbContext dbContext, IMapper mapper, UserManager userManager) { private readonly UserBasicDao _userBasicDao = userBasicDao; private readonly ApplicationDbContext _dbContext = dbContext; private readonly IMapper _mapper = mapper; + private readonly UserManager _userManager = userManager; #region 软件控制-同步用户的软件控制权限 /// @@ -251,9 +253,13 @@ namespace LMS.service.Service.SoftwareService { // 判断权限,如果不是管理员或超级管理员,就判断是不是自己的数据,不是的话,返回无权限操作 IQueryable query = _dbContext.SoftwareControl.AsQueryable(); - bool isAdminOrSuperAdmin = await _userBasicDao.CheckUserIsAdminOrSuperAdmin(requestUserId); - if (!isAdminOrSuperAdmin && userId != requestUserId) + bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId); + bool isAdmin = await _userBasicDao.CheckUserIsAdmin(requestUserId); + bool isAgent = await _userBasicDao.CheckUserIsAgent(requestUserId); + + + if (!(isSuperAdmin || isAdmin || isAgent) && userId != requestUserId) { return APIResponseModel>.CreateErrorResponseModel(ResponseCode.NotPermissionAction); } @@ -275,14 +281,52 @@ namespace LMS.service.Service.SoftwareService query = query.Where(x => x.Remark.Contains(remark)); } - - if (!isAdminOrSuperAdmin) + if (!(isSuperAdmin || isAdmin)) { // 通过 softwareId 过滤掉isUse为false的数 List softwareIds = await _dbContext.Software.Where(x => x.IsUse == true).Select(x => x.Id).ToListAsync(); query = query.Where(x => softwareIds.Contains(x.SoftwareId)); } + // 做筛选权限 + // 获取相关用户ID + var userLookupQuery = _userManager.Users.AsNoTracking(); + if (isAdmin && !isSuperAdmin) + { + // 除了超级管理员的代理 其他都能看到 + IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); + List superUserIds = superUsers.Select(x => x.Id).ToList(); + var list = await userLookupQuery + .Where(u => u.ParentId == null || superUserIds.Contains((long)u.ParentId) || superUserIds.Contains(u.Id)) + .Select(u => u.Id) + .ToListAsync(); + + HashSet filteredParentIds = new(list); + if (filteredParentIds?.Count > 0) + { + query = query.Where(x => !filteredParentIds.Contains(x.UserId) || x.UserId == requestUserId); + } + } + else if (isAgent && !isSuperAdmin) + { + // 代理只能看到自己下面的用户 + var list = await userLookupQuery + .Where(u => u.ParentId == requestUserId) + .Select(u => u.Id) + .ToListAsync(); + + HashSet filteredParentIds = new(list); + if (filteredParentIds?.Count > 0) + { + query = query.Where(x => filteredParentIds.Contains(x.UserId)); + } + } + else if (!isSuperAdmin) + { + // 普通用户只能看到自己的 + query = query.Where(x => x.UserId == requestUserId); + } + // 通过ID降序 query = query.OrderByDescending(x => x.CreatedTime); diff --git a/LMS.service/Service/UserService/UserService.cs b/LMS.service/Service/UserService/UserService.cs index 5bdbbd5..3ca6168 100644 --- a/LMS.service/Service/UserService/UserService.cs +++ b/LMS.service/Service/UserService/UserService.cs @@ -1,5 +1,8 @@ -using LMS.Common.RSAKey; +using LMS.Common.Enum; +using LMS.Common.RSAKey; using LMS.DAO; +using LMS.DAO.UserDAO; +using LMS.Repository.DB; using LMS.Repository.DTO; using LMS.Repository.DTO.UserDto; using LMS.Repository.Models.DB; @@ -7,7 +10,6 @@ using LMS.Repository.Models.User; using LMS.Repository.User; using LMS.Tools.Extensions; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Collections.Concurrent; @@ -15,12 +17,14 @@ using static LMS.Common.Enums.ResponseCodeEnum; namespace LMS.service.Service.UserService { - public class UserService(UserManager userManager, RoleManager roleManager, ApplicationDbContext context, SecurityService securityService) + public class UserService(UserManager userManager, RoleManager roleManager, ApplicationDbContext context, SecurityService securityService, EmailVerificationService emailVerificationService, UserBasicDao userBasicDao) { private readonly UserManager _userManager = userManager; private readonly RoleManager _roleManager = roleManager; private readonly ApplicationDbContext _context = context; private readonly SecurityService _securityService = securityService; + private readonly EmailVerificationService _verificationService = emailVerificationService; + private readonly UserBasicDao _userBasicDao = userBasicDao; #region 获取用户信息 /// @@ -97,11 +101,10 @@ namespace LMS.service.Service.UserService { return APIResponseModel>.CreateErrorResponseModel(ResponseCode.FindUserByIdFail); } - - bool isAdminOrSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin") || await _userManager.IsInRoleAsync(user, "Admin"); + bool isAdmin = await _userBasicDao.CheckUserIsAdmin(reuqertUserId); bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin"); - bool isAgent = !isAdminOrSuperAdmin && await _userManager.IsInRoleAsync(user, "Agent User"); - if (!isAdminOrSuperAdmin && !isAgent) + bool isAgent = await _userManager.IsInRoleAsync(user, "Agent User"); + if (!isAdmin && !isSuperAdmin && !isAgent) { return APIResponseModel>.CreateErrorResponseModel(ResponseCode.NotPermissionAction); } @@ -128,21 +131,46 @@ namespace LMS.service.Service.UserService // 开始查询数据 IQueryable? query = _userManager.Users; - if (isAgent) - { - query = query.Where(x => x.ParentId == user.Id); - } - // 判断是不是管理员 - IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); - List superUserIds = superUsers.Select(x => x.Id).ToList(); // 默认把自己排除 - //query = query.Where(x => x.Id != reuqertUserId); if (!isSuperAdmin) { - // 不是草鸡管理员,就把超级管理员排除 - query = query.Where(x => reuqertUserId == x.Id || (!superUserIds.Contains(x.ParentId ?? 0) && !superUserIds.Contains(x.Id))); + query = query.Where(x => x.Id != reuqertUserId); } + // 获取相关用户ID + var userLookupQuery = _userManager.Users.AsNoTracking(); + if (isAdmin && !isSuperAdmin) + { + // 除了超级管理员的代理 其他都能看到 + IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); + List superUserIds = superUsers.Select(x => x.Id).ToList(); + var list = await userLookupQuery + .Where(u => u.ParentId == null || superUserIds.Contains((long)u.ParentId) || superUserIds.Contains(u.Id)) + .Select(u => u.Id) + .ToListAsync(); + + HashSet filteredParentIds = new(list); + if (filteredParentIds?.Count > 0) + { + query = query.Where(x => !filteredParentIds.Contains(x.Id)); + } + } + else if (isAgent && !isSuperAdmin) + { + // 代理只能看到自己下面的用户 + var list = await userLookupQuery + .Where(u => u.ParentId == reuqertUserId) + .Select(u => u.Id) + .ToListAsync(); + + HashSet filteredParentIds = new(list); + if (filteredParentIds?.Count > 0) + { + query = query.Where(x => filteredParentIds.Contains(x.Id)); + } + } + + // 添加查询条件 if (!string.IsNullOrWhiteSpace(userName)) { @@ -214,7 +242,7 @@ namespace LMS.service.Service.UserService { List? roles = [.. (await _userManager.GetRolesAsync(users[i]))]; userCollections[i].RoleNames = roles; - if (!isAdminOrSuperAdmin) + if (!isSuperAdmin || isAdmin) { userCollections[i].PhoneNumber = "***********"; userCollections[i].Email = "***********"; @@ -448,6 +476,25 @@ namespace LMS.service.Service.UserService return APIResponseModel.CreateErrorResponseModel(ResponseCode.InvalidAffiliateCode); } + // 判断邮箱是不是被使用了 + var existingUser = await _userManager.FindByEmailAsync(model.Email); + if (existingUser != null) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱已注册,请直接登录!"); + + // 验证验证码 + // 判断是不是需要校验验证码 + Options? enaleMailService = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.EnableMailService); + if (enaleMailService != null) + { + _ = bool.TryParse(enaleMailService.Value, out bool enableMail); + if (enableMail) + { + var isCodeValid = await _verificationService.VerifyCodeAsync(model.Email, model.VerificationCode); + if (!isCodeValid) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "验证码无效或已过期"); + } + } + var rsaKeyId = keyInfo.Key; var privateKey = _securityService.DecryptWithAES(rsaKeyId); @@ -491,5 +538,26 @@ namespace LMS.service.Service.UserService } #endregion + + #region 发送用户注册验证码 + public async Task>> SendVerificationCode(EmailVerificationService.SendVerificationCodeDto model) + { + try + { + // 检查邮箱是否已被使用 + var existingUser = await _userManager.FindByEmailAsync(model.Email); + if (existingUser != null) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱已注册,请直接登录!"); + + // 发送验证码 + await _verificationService.SendVerificationCodeAsync(model.Email); + return APIResponseModel.CreateSuccessResponseModel(ResponseCode.Success, "验证码发送成功,请在邮箱中查收!"); + } + catch (Exception e) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, e.Message); + } + } + #endregion } } diff --git a/LMS.service/appsettings.json b/LMS.service/appsettings.json index 10f68b8..d3a6715 100644 --- a/LMS.service/appsettings.json +++ b/LMS.service/appsettings.json @@ -5,5 +5,27 @@ "Microsoft.AspNetCore": "Warning" } }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "logs/app-.log", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}", + "retainedFileCountLimit": 31 + } + } + ], + "Enrich": [ "FromLogContext" ] + }, + "Version": "1.0.4", "AllowedHosts": "*" }