1. 新增用户注册需要邮箱验证码
2. 机器码、软件权限控制、用户 隔离,除非超级管理员,其他用户只能看到自己下面的用户,管理员可以看到除超级管理员以外的所有
This commit is contained in:
lq1405 2025-03-16 23:01:50 +08:00
parent 1c5b9ed3c8
commit 57402e0dda
26 changed files with 790 additions and 83 deletions

View File

@ -5,12 +5,47 @@ namespace LMS.Common.Dictionary;
public class AllOptions
{
public static class AllOptionKey
{
/// <summary>
/// 获取所有的 Option
/// </summary>
public const string All = "all";
/// <summary>
/// 获取TTS相关的 Option
/// </summary>
public const string TTS = "tts";
/// <summary>
/// 获取软件相关的 Option
/// </summary>
public const string Software = "software";
/// <summary>
/// 软件试用相关 Option
/// </summary>
public const string Trial = "trial";
/// <summary>
/// 出图相关的 Option
/// </summary>
public const string Image = "image";
/// <summary>
/// 邮件设置相关 Option
/// </summary>
public const string MailSetting = "mailSetting";
}
public static readonly Dictionary<string, List<string>> 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] }
};
}

View File

@ -4,10 +4,35 @@ namespace LMS.Common.Dictionary;
public class SimpleOptions
{
public static class SimpleOptionKey
{
/// <summary>
/// TTS的角色Option
/// </summary>
public const string Ttsrole = "ttsrole";
/// <summary>
/// LaiTool信息相关的配置
/// </summary>
public const string Laitoolinfo = "laitoolinfo";
/// <summary>
/// LaiTool FluxAPI对应的模型信息
/// </summary>
public const string LaitoolFluxApiModelList = "LaitoolFluxApiModelList";
/// <summary>
/// 是否开启邮箱信息
/// </summary>
public const string EnableMailService = "EnableMailService";
}
public static readonly Dictionary<string, List<string>> 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]}
};
}

View File

@ -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
/// </summary>
public const string LaitoolFluxApiModelList = "LaitoolFluxApiModelList";
/// <summary>
/// SMTP的邮件设置
/// </summary>
public const string SMTPMailSetting = "SMTPMailSetting";
/// <summary>
/// 是否开启邮箱服务
/// </summary>
public const string EnableMailService = "EnableMailService";
}

View File

@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="4.11.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@
namespace LMS.Common.Templates
{
public class EmailTemplateService()
{
/// <summary>
/// 注册邮件模板
/// </summary>
public const string RegisterHtmlTemplates = """
<!DOCTYPE html>
<html>
<body>
<h1>LMS Registration Code</h1>
<p>Hi there,</p>
<p>Your code is <strong>{RegisterCode}</strong></p>
<p>The code is valid for 10 minutes, if it is not you, please ignore it.</p>
</body>
</html>
""";
/// <summary>
/// 替换模板的占位符
/// </summary>
/// <param name="template"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public static string ReplaceTemplate(string template, Dictionary<string, string> parameters)
{
// 替换占位符
foreach (var param in parameters)
{
template = template.Replace($"{{{param.Key}}}", param.Value);
}
return template;
}
}
}

View File

@ -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<string>() // 如果存储的是空字符串或空数组,则返回空列表
: JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>()
).Metadata.SetValueComparer(
new ValueComparer<List<string>>(
// 比较两个集合是否相等
(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<UserSoftware>()
.HasKey(us => new { us.UserId, us.SoftwareId });

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="OneOf" Version="3.0.271" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup>

View File

@ -55,6 +55,42 @@ namespace LMS.DAO.UserDAO
bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin");
return isSuperAdmin;
}
/// <summary>
/// 检查用户是不是管理员
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<bool> 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;
}
/// <summary>
/// 检查用户是不是代理
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<bool> 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;
}
}
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace Lai_server.Configuration
namespace LMS.service.Configuration
{
public static class AuthenticationExtensions
{

View File

@ -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 }
];
// 遍历所有的配置项,如果没有则添加

View File

@ -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<PermissionBasicDao>();
services.AddScoped<PermissionTypeDao>();
// 注入 Extensions
services.AddScoped<EmailService>();
services.AddScoped<EmailVerificationService>();
// 添加分布式缓存(用于存储验证码)
services.AddDistributedMemoryCache();
}
}
}

View File

@ -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<ActionResult<APIResponseModel<string>>> TestSendMail()
{
long userId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _optionsService.TestSendMail(userId);
}
#endregion
}
}

View File

@ -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<ActionResult<APIResponseModel<string>>> SendVerificationCode([FromBody] EmailVerificationService.SendVerificationCodeDto model)
{
if (!ModelState.IsValid)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError);
return await _userService.SendVerificationCode(model);
}
#endregion
#region token
[HttpPost]

View File

@ -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
{
/// <summary>
/// 是否开启邮箱服务
/// </summary>
public bool EnableMailService { get; set; }
/// <summary>
/// 是否开启SSL
/// </summary>
public bool EnableSSL { get; set; }
/// <summary>
/// SMTP服务器
/// </summary>
public string SmtpServer { get; set; }
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; }
/// <summary>
/// 发送用户名
/// </summary>
public string Username { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }
/// <summary>
/// 发送者邮箱
/// </summary>
public string SenderEmail { get; set; }
/// <summary>
/// 测试接收邮箱
/// </summary>
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;
}
/// <summary>
/// 发送安全邮件
/// </summary>
/// <param name="to"> 收信邮箱 </param>
/// <param name="subject"> 邮件主题 </param>
/// <param name="body"> 邮件信息 body </param>
/// <returns></returns>
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}");
}
}
/// <summary>
/// 发送邮件,捕获异常并记录
/// </summary>
/// <param name="to"> 收信邮箱 </param>
/// <param name="subject"> 邮件主题 </param>
/// <param name="body"> 邮件信息 body </param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
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<MailSetting>(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);
}
}
}
}

View File

@ -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<string, string>
{
{ "RegisterCode", verificationCode }
});
// 发送验证码邮件
await _emailService.SendEmailAsync(email, "LMS注册验证码", emailBody);
}
// 验证用户提交的验证码
public async Task<bool> 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();
}
}

View File

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

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@ -7,11 +7,10 @@
<UserSecretsId>ed64fb6f-9c93-43d0-b418-61f507f28420</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
<Version>1.0.3</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Betalgo.Ranul.OpenAI" Version="8.9.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
@ -23,8 +22,15 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />
</ItemGroup>

View File

@ -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<ApplicationDbContext>(options =>
{
@ -76,6 +79,8 @@ builder.Services.AddHostedService<DatabaseConfiguration>();
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();

View File

@ -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
/// <param name="ownUserName"></param>
/// <param name="requestUserId"></param>
/// <returns></returns>
internal async Task<ActionResult<APIResponseModel<CollectionResponse<Machine>>>> QueryMachineCollection(int page, int pageSize, string? machineId, string? createdUserName, MachineStatus? status, MachineUseStatus? useStatus, string? remark, string? ownUserName, long requestUserId)
public async Task<ActionResult<APIResponseModel<CollectionResponse<Machine>>>> 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<Machine> query = _context.Machine;
if (isAdmin)
{
List<long> superAdminUserIds = ((List<User>)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<long> 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<long> filteredCreatorIds = null;
HashSet<long> 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<long>(list);
}
if (!string.IsNullOrWhiteSpace(ownUserName))
{
var list = await userLookupQuery
.Where(u => u.UserName.Contains(ownUserName))
.Select(u => u.Id)
.ToListAsync();
filteredOwnerIds = new HashSet<long>(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<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
List<long> 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<long> 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<long> 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<long> 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();
// 降序,取指定的条数的数据

View File

@ -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<User> userManager, IMapper mapper)
public class OptionsService(ApplicationDbContext context, UserManager<User> userManager, IMapper mapper, UserBasicDao userBasicDao, EmailService emailService)
{
private readonly ApplicationDbContext _context = context;
private readonly UserManager<User> _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
/// <summary>
/// 测试邮箱发送
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<ActionResult<APIResponseModel<string>>> TestSendMail(long userId)
{
try
{
// 判断是不是超级管理员
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(userId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.RegisterHtmlTemplates, new Dictionary<string, string>
{
{ "RegisterCode", "验证码" }
});
// 调用发送邮件的方法
await _emailService.SendEmailAsync("user@example.com", "邮件连通测试", emailBody, true);
return APIResponseModel<string>.CreateSuccessResponseModel("邮箱测试发送成功");
}
catch (Exception ex)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, ex.Message);
}
}
#endregion
}
}

View File

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

View File

@ -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<User> userManager)
{
private readonly UserBasicDao _userBasicDao = userBasicDao;
private readonly ApplicationDbContext _dbContext = dbContext;
private readonly IMapper _mapper = mapper;
private readonly UserManager<User> _userManager = userManager;
#region -
/// <summary>
@ -251,9 +253,13 @@ namespace LMS.service.Service.SoftwareService
{
// 判断权限,如果不是管理员或超级管理员,就判断是不是自己的数据,不是的话,返回无权限操作
IQueryable<SoftwareControl> 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<CollectionResponse<SoftwareControlCollectionDto>>.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<string> 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<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
List<long> 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<long> 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<long> 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);

View File

@ -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<User> userManager, RoleManager<Role> roleManager, ApplicationDbContext context, SecurityService securityService)
public class UserService(UserManager<User> userManager, RoleManager<Role> roleManager, ApplicationDbContext context, SecurityService securityService, EmailVerificationService emailVerificationService, UserBasicDao userBasicDao)
{
private readonly UserManager<User> _userManager = userManager;
private readonly RoleManager<Role> _roleManager = roleManager;
private readonly ApplicationDbContext _context = context;
private readonly SecurityService _securityService = securityService;
private readonly EmailVerificationService _verificationService = emailVerificationService;
private readonly UserBasicDao _userBasicDao = userBasicDao;
#region
/// <summary>
@ -97,11 +101,10 @@ namespace LMS.service.Service.UserService
{
return APIResponseModel<CollectionResponse<UserCollectionDto>>.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<CollectionResponse<UserCollectionDto>>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
@ -128,21 +131,46 @@ namespace LMS.service.Service.UserService
// 开始查询数据
IQueryable<User>? query = _userManager.Users;
if (isAgent)
{
query = query.Where(x => x.ParentId == user.Id);
}
// 判断是不是管理员
IList<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
List<long> 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<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
List<long> 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<long> 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<long> 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<string>? 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<string>.CreateErrorResponseModel(ResponseCode.InvalidAffiliateCode);
}
// 判断邮箱是不是被使用了
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser != null)
return APIResponseModel<string>.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<string>.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<ActionResult<APIResponseModel<string>>> SendVerificationCode(EmailVerificationService.SendVerificationCodeDto model)
{
try
{
// 检查邮箱是否已被使用
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser != null)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱已注册,请直接登录!");
// 发送验证码
await _verificationService.SendVerificationCodeAsync(model.Email);
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, "验证码发送成功,请在邮箱中查收!");
}
catch (Exception e)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
}
}
#endregion
}
}

View File

@ -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": "*"
}