diff --git a/Apimanager_backend/Apimanager_backend.csproj b/Apimanager_backend/Apimanager_backend.csproj index 80575c2..059696b 100644 --- a/Apimanager_backend/Apimanager_backend.csproj +++ b/Apimanager_backend/Apimanager_backend.csproj @@ -13,7 +13,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Apimanager_backend/Config/MyAutomapper.cs b/Apimanager_backend/Config/MyAutomapper.cs index 159ff8c..aaccdb9 100644 --- a/Apimanager_backend/Config/MyAutomapper.cs +++ b/Apimanager_backend/Config/MyAutomapper.cs @@ -9,6 +9,8 @@ namespace Apimanager_backend.Config public MyAutomapper() { CreateMap(); + CreateMap() + .ForMember(dest => dest.PassHash, opt => opt.MapFrom(src => src.Password)); } } } diff --git a/Apimanager_backend/Config/ServiceCollectionExtensions.cs b/Apimanager_backend/Config/ServiceCollectionExtensions.cs index 4864289..8d90732 100644 --- a/Apimanager_backend/Config/ServiceCollectionExtensions.cs +++ b/Apimanager_backend/Config/ServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ -using Apimanager_backend.Services; +using Apimanager_backend.Dtos; +using Apimanager_backend.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; using StackExchange.Redis; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -18,6 +20,7 @@ namespace Apimanager_backend.Config services.AddScoped(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } public static IServiceCollection AddJWTService(this IServiceCollection services,IConfiguration configuration) @@ -28,6 +31,7 @@ namespace Apimanager_backend.Config services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { + //jwt参数 options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, @@ -38,11 +42,45 @@ namespace Apimanager_backend.Config ValidAudience = jwtSettings["Audience"], IssuerSigningKey = new SymmetricSecurityKey(key) }; + //添加自定义响应处理函数 + options.Events = new JwtBearerEvents + { + OnChallenge = new Func(JwtTokenErrorEventFunc), + OnForbidden = new Func(JwtPermissionEventFunc) + }; }); //redis配置 services.AddSingleton(ConnectionMultiplexer.Connect(configuration["Redis:ConnectionString"])); return services; } + /// + /// token无效事件处理函数 + /// + /// + /// + public async static Task JwtTokenErrorEventFunc(JwtBearerChallengeContext context) + { + context.Response.ContentType = "application/json"; + var res = new ResponseBase( + code: 1002, + message: "用户未登录或认证失败", + data: null + ); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync(JsonConvert.SerializeObject(res)); + context.HandleResponse(); + } + public async static Task JwtPermissionEventFunc(ForbiddenContext context) + { + context.Response.ContentType = "application/json"; + var res = new ResponseBase( + code: 2006, + message: "用户无权限进行该操作", + data: null + ); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync(JsonConvert.SerializeObject(res)); + } } } diff --git a/Apimanager_backend/Controllers/AdminController.cs b/Apimanager_backend/Controllers/AdminController.cs new file mode 100644 index 0000000..6d04c07 --- /dev/null +++ b/Apimanager_backend/Controllers/AdminController.cs @@ -0,0 +1,133 @@ +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using StackExchange.Redis; + +namespace Apimanager_backend.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class AdminController : ControllerBase + { + private readonly IAdminService adminService; + private readonly IUserService userService; + public AdminController(IAdminService service,IUserService userService) + { + this.adminService = service; + this.userService = userService; + } + #region 获取用户列表 + [HttpGet] + [Authorize(Roles = "Admin")] + public async Task>>> UserList(int pageIndex,int pageSize,bool desc) + { + var users = await adminService.GetUsersAsync(pageIndex,pageSize,desc); + var res = new ResponseBase>( + code:1000, + message:"Success", + data:users + ); + return Ok(res); + } + #endregion + #region 获取用户信息 + [HttpGet] + [Authorize(Roles = "Admin")] + public async Task>> UserInfo(int userId) + { + var userInfo = await userService.GetUserAsync(userId); + var res = new ResponseBase( + code: 1000, + message: "Success", + data: userInfo + ); + return Ok(res); + } + #endregion + #region 删除用户 + [HttpDelete] + [Authorize(Roles = "Admin")] + public async Task>> DeleteUser(int userId) + { + await adminService.DeleteUserAsync(userId); + var res = new ResponseBase( + code:1000, + message:"Success", + data: null + ); + return Ok(res); + } + #endregion + #region 添加用户 + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task>> AddUser([FromBody]CreateUserDto dto) + { + var userInfo = await adminService.CreateUserAsync(dto); + var res = new ResponseBase( + code:1000, + message:"Success", + data: userInfo + ); + return Ok(res); + } + #endregion + #region 禁用用户 + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task>> Ban(int userId) + { + await adminService.BanUserAsync(userId); + var res = new ResponseBase( + code:1000, + message:"Success", + data: null + ); + return Ok(res); + } + #endregion + #region 取消禁用用户 + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task>> UnBan(int userId) + { + await adminService.UnbanUserAsync(userId); + var res = new ResponseBase( + code:1000, + message:"Success", + data:null + ); + } + #endregion + #region 更新用户信息 + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task>> UpdateUser([FromQuery]int userId,[FromBody]AdminUpdateUserDto dto) + { + try + { + var userInfo = await adminService.UpdateUserAsync(userId, dto); + var res = new ResponseBase( + code: 1000, + message: "Success", + data: userInfo + ); + } + catch(BaseException e) + { + var res = new ResponseBase( + code: e.code, + message:e.message, + data:null + ); + return NotFound(res); + } + + + } + #endregion + } +} diff --git a/Apimanager_backend/Controllers/AuthController.cs b/Apimanager_backend/Controllers/AuthController.cs index a662105..cbbca15 100644 --- a/Apimanager_backend/Controllers/AuthController.cs +++ b/Apimanager_backend/Controllers/AuthController.cs @@ -5,6 +5,7 @@ using Apimanager_backend.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualBasic; namespace Apimanager_backend.Controllers { @@ -31,52 +32,35 @@ namespace Apimanager_backend.Controllers [HttpPost] public async Task>> Login([FromBody] UserLoginDto dto) { - try - { - UserInfoDto user = await authService.LoginAsync(dto.UserName, dto.Password); - //生成token - string token = tokenService.GenerateAccessToken(user.Id.ToString(),user.Roles); - //生成refreshtoken - string refreshToken = await refreshTokenService.CreateRefereshTokenAsync(user.Id.ToString()); - var responseInfo = new ResponseBase( - code: 2000, - message: "Login successful", - data: new LoginResponseDto - { - UserInfo = user, - Token = token, - RefreshToken = refreshToken - } - ); - return Ok(responseInfo); - } - catch (BaseException e) - { + UserInfoDto user = await authService.LoginAsync(dto.UserName, dto.Password); + //生成token + string token = tokenService.GenerateAccessToken(user.Id.ToString(), user.Roles); + //生成refreshtoken + string refreshToken = await refreshTokenService.CreateRefereshTokenAsync(user.Id.ToString()); + var responseInfo = new ResponseBase( + code: 2000, + message: "Login successful", + data: new LoginResponseDto + { + UserInfo = user, + Token = token, + RefreshToken = refreshToken + } + ); + return Ok(responseInfo); - //错误时,构建错误信息对象 - var responseInfo = new ResponseBase( - code: e.code, - message: e.message, - data: null - ); - - return e.code switch - { - 2001 => Unauthorized(responseInfo), - 2002 => Unauthorized(responseInfo), - _ => StatusCode(503) - }; - } } - + /// + /// 令牌刷新 + /// + /// 传入用户令牌 + /// 返回新令牌 [HttpPost] public async Task>> Refresh([FromBody]RefreshResponseDto dto) { - try - { - var userId = await refreshTokenService.ValidateRefreshTokenAsync(dto.RefreshToken); + var IsRefreshToken = await refreshTokenService.ValidateRefreshTokenAsync(dto.UserId.ToString(),dto.RefreshToken); //刷新令牌无效 - if (userId == null) + if (!IsRefreshToken) { var ret = new ResponseBase( code: 2008, @@ -86,27 +70,103 @@ namespace Apimanager_backend.Controllers return Unauthorized(ret); } //获取刷新令牌对应用户信息 - var userInfo = await userService.GetUserAsync(int.Parse(userId)); + var userInfo = await userService.GetUserAsync(dto.UserId); //重新生成令牌 var token = tokenService.GenerateAccessToken(userInfo.Id.ToString(), userInfo.Roles); //刷新刷新令牌有效期(小于三天才会刷新) - await refreshTokenService.UpdateRefreshTokenAsync(dto.RefreshToken); + await refreshTokenService.UpdateRefreshTokenAsync(userInfo.Id.ToString()); var result = new ResponseBase( code: 1000, message: "Success", data: new RefreshResponseDto { + UserId = dto.UserId, Token = token, RefreshToken = dto.RefreshToken } ); return Ok(result); - }catch(BaseException e) + } + /// + /// 用户注册 + /// + /// + /// + [HttpPost] + public async Task>> Register(RegisterRequestDto requestDto) + { + var isUsernameExist = await userService.IsUsernameExist(requestDto.Username); + if (isUsernameExist) { - + var errorRes = new ResponseBase( + code: 2003, + message: "用户名已存在", + data: null + ); + return StatusCode(409, errorRes); } - + try + { + var userInfo = await authService.RegisterAsync(requestDto); + var res = new ResponseBase( + code: 1000, + message: "Success", + data: userInfo + ); + return Ok(res); + } + catch (BaseException e) + { + var res = new ResponseBase( + code: e.code, + message: e.message, + data: null + ); + return StatusCode(500, res); + } + } + /// + /// 发送邮箱校验码 + /// + /// + /// + [HttpPost] + public async Task>> SendValidateCode([FromQuery]string email) + { + //检测邮箱是否被使用 + var emailIsUse = await userService.IsEmailExist(email); + if (emailIsUse) + { + var errorRes = new ResponseBase( + code:2005, + message: "邮箱已存在", + data:null + ); + return StatusCode(409,errorRes); + } + //发送注册验证码 + await authService.SendRegisterCodeAsync(email); + var res = new ResponseBase( + code:1000, + message:"Success", + data: null + ); + return Ok(res); + } + + [HttpDelete] + [Authorize(Roles = "User")] + public async Task>> Logout() + { + var userId = User.Claims.First(x => x.ValueType == "userId").Value; + await refreshTokenService.DeleterRefreshTokenAsync(userId); + var res = new ResponseBase( + code:1000, + message:"Success", + data: null + ); + return Ok(res); } } } diff --git a/Apimanager_backend/Controllers/UserController.cs b/Apimanager_backend/Controllers/UserController.cs index 6babc8f..d6ce368 100644 --- a/Apimanager_backend/Controllers/UserController.cs +++ b/Apimanager_backend/Controllers/UserController.cs @@ -4,6 +4,9 @@ using Apimanager_backend.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Apimanager_backend.Filters; +using Microsoft.AspNetCore.Authorization; +using Apimanager_backend.Models; +using System.Security.Claims; namespace Apimanager_backend.Controllers { @@ -16,6 +19,73 @@ namespace Apimanager_backend.Controllers { this.userService = userService; } - + /// + /// 获取用户个人信息 + /// + /// + [HttpGet] + [Authorize(Roles = "User")] + public async Task>> UserInfo() + { + var userId = User.Claims.First(x => x.Type == "userId").Value; + var userInfo = await userService.GetUserAsync(int.Parse(userId)); + var res = new ResponseBase( + code:1000, + message:"Success", + data:userInfo + ); + return Ok(res); + } + /// + /// 重置用户密码 + /// + /// + /// + [HttpPost] + public async Task>> Resetpassword([FromBody]ResetPasswordDto dto) + { + try + { + await userService.ResetPasswordAsync(dto.Email, dto.Code, dto.NewPassword); + var res = new ResponseBase( + code:1000, + message:"Success", + data: null + ); + return Ok(res); + }catch(BaseException e) + { + var res = new ResponseBase( + code:e.code, + message:e.message, + data:null + ); + return StatusCode(400,res); + } + } + [HttpPost] + public async Task>> SendResetEmail([FromQuery]string email) + { + await userService.SendResetPasswordEmailAsync(email); + var res = new ResponseBase( + code: 1000, + message: "Success", + data: null + ); + return Ok(res); + } + [HttpPost] + [Authorize(Roles = "User")] + public async Task>> Update([FromBody]UpdateUserDto dto) + { + var userId = User.Claims.First(x => x.ValueType == "userId").Value; + var userInfo = await userService.UpdateUserAsync(int.Parse(userId),dto); + var res = new ResponseBase( + code:1000, + message:"Success", + data:userInfo + ); + return Ok(res); + } } } diff --git a/Apimanager_backend/Data/ApiContext.cs b/Apimanager_backend/Data/ApiContext.cs index 8c86493..f43a410 100644 --- a/Apimanager_backend/Data/ApiContext.cs +++ b/Apimanager_backend/Data/ApiContext.cs @@ -22,6 +22,8 @@ namespace Apimanager_backend.Data public DbSet UserPackages { get; set; } //用户角色表 public DbSet UserRoles { get; set; } + //日志表 + public DbSet Logs { get; set; } public ApiContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -29,6 +31,11 @@ namespace Apimanager_backend.Data // 配置全局查询筛选器 modelBuilder.Entity().HasQueryFilter(u => !u.IsDelete); modelBuilder.Entity().HasQueryFilter(a => !a.IsDelete); + //配置日志表 + modelBuilder.Entity().HasKey(x => x.Id); + modelBuilder.Entity() + .Property(x => x.Id) + .ValueGeneratedOnAdd(); } } diff --git a/Apimanager_backend/Dtos/AdminUpdateUserDto.cs b/Apimanager_backend/Dtos/AdminUpdateUserDto.cs new file mode 100644 index 0000000..e82b1e7 --- /dev/null +++ b/Apimanager_backend/Dtos/AdminUpdateUserDto.cs @@ -0,0 +1,8 @@ +namespace Apimanager_backend.Dtos +{ + public class AdminUpdateUserDto + { + public string Password { get; set; } + public decimal Balance { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/CreateUserDto.cs b/Apimanager_backend/Dtos/CreateUserDto.cs index 9c8b9d2..95aae53 100644 --- a/Apimanager_backend/Dtos/CreateUserDto.cs +++ b/Apimanager_backend/Dtos/CreateUserDto.cs @@ -1,6 +1,16 @@ -namespace Apimanager_backend.Dtos +using System.ComponentModel.DataAnnotations; + +namespace Apimanager_backend.Dtos { public class CreateUserDto { + [Required(ErrorMessage = "用户名必填")] + [MaxLength(20,ErrorMessage = "用户名最大长度20字符")] + public string Username { get; set; } + [Required(ErrorMessage = "密码必填")] + public string Password { get; set; } + [EmailAddress(ErrorMessage = "邮箱格式错误")] + public string Email { get; set; } + } } diff --git a/Apimanager_backend/Dtos/RefreshResponseDto.cs b/Apimanager_backend/Dtos/RefreshResponseDto.cs index a62a8a0..642b43c 100644 --- a/Apimanager_backend/Dtos/RefreshResponseDto.cs +++ b/Apimanager_backend/Dtos/RefreshResponseDto.cs @@ -4,8 +4,10 @@ namespace Apimanager_backend.Dtos { public class RefreshResponseDto { + [Required(ErrorMessage = "用户ID必填!")] + public int UserId { get; set; } public string? Token { get; set; } - [Required(ErrorMessage = "RefreshToken is required")] + [Required(ErrorMessage = "刷新令牌必填!")] public string RefreshToken { get; set; } } } diff --git a/Apimanager_backend/Dtos/RegisterRequestDto.cs b/Apimanager_backend/Dtos/RegisterRequestDto.cs new file mode 100644 index 0000000..192b02f --- /dev/null +++ b/Apimanager_backend/Dtos/RegisterRequestDto.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Apimanager_backend.Dtos +{ + public class RegisterRequestDto + { + [Required(ErrorMessage = "用户名必填!")] + [MaxLength(20,ErrorMessage = "用户名最长20字符!")] + public string Username { get; set; } + [Required(ErrorMessage = "密码必填!")] + public string Password { get; set; } + [Required(ErrorMessage = "邮箱必填!")] + public string Email { get; set; } + [Required(ErrorMessage = "验证码必填!")] + public string VerificationCode { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/ResetPasswordDto.cs b/Apimanager_backend/Dtos/ResetPasswordDto.cs new file mode 100644 index 0000000..7b4d394 --- /dev/null +++ b/Apimanager_backend/Dtos/ResetPasswordDto.cs @@ -0,0 +1,9 @@ +namespace Apimanager_backend.Dtos +{ + public class ResetPasswordDto + { + public string Email { get; set; } + public string NewPassword { get; set; } + public string Code { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/UpdateUserDto.cs b/Apimanager_backend/Dtos/UpdateUserDto.cs index 3c53eed..7f3411d 100644 --- a/Apimanager_backend/Dtos/UpdateUserDto.cs +++ b/Apimanager_backend/Dtos/UpdateUserDto.cs @@ -1,6 +1,10 @@ -namespace Apimanager_backend.Dtos +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Apimanager_backend.Dtos { public class UpdateUserDto { + public string? password { get; set; } } } diff --git a/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs b/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs index d4a7a0f..67d376f 100644 --- a/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs +++ b/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs @@ -1,5 +1,7 @@ using Apimanager_backend.Dtos; using Apimanager_backend.Exceptions; +using Apimanager_backend.Tools; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace Apimanager_backend.Filters.ExceptionFilter @@ -17,25 +19,11 @@ namespace Apimanager_backend.Filters.ExceptionFilter message: exception.message, data:null ); - //根据自定义错误码返回对应http状态码 - int code = 0; - switch (exception.code) - { - case 1001: - case 1005: - case 2007: - case 4001: - code = 400; - break; - case 1002: - case 2001: - code = 401; - break; - case 1003: - case 2006: - case 3001: - - } + int httpCode = StatusCodeHelper.GetHttpStatusCode(exception.code); + context.Result = new JsonResult(res) { + StatusCode = httpCode + }; + context.ExceptionHandled = true; } } } diff --git a/Apimanager_backend/Migrations/20241103110714_update-logtable.Designer.cs b/Apimanager_backend/Migrations/20241103110714_update-logtable.Designer.cs new file mode 100644 index 0000000..f6857aa --- /dev/null +++ b/Apimanager_backend/Migrations/20241103110714_update-logtable.Designer.cs @@ -0,0 +1,442 @@ +// +using System; +using Apimanager_backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + [DbContext(typeof(ApiContext))] + [Migration("20241103110714_update-logtable")] + partial class updatelogtable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("IsThirdParty") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.ToTable("Apis"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiId") + .HasColumnType("int"); + + b.Property("CallResult") + .HasColumnType("int"); + + b.Property("CallTime") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApiId"); + + b.HasIndex("UserId"); + + b.ToTable("CallLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CallLimit") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Apipackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Exception") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MessageTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("Operation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TargetId") + .HasColumnType("int"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UserAgent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("OperationLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OrderType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThirdPartyOrderId") + .HasColumnType("varchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("ThirdPartyOrderId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Balance") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsBan") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("PassHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("PurchasedAt") + .HasColumnType("datetime(6)"); + + b.Property("RemainingCalls") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Apis") + .HasForeignKey("PackageId"); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.HasOne("Apimanager_backend.Models.Api", "Api") + .WithMany("ApiCalls") + .HasForeignKey("ApiId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("CallLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Api"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("operationLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Packages") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Packages") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Navigation("ApiCalls"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Navigation("Apis"); + + b.Navigation("Packages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Navigation("CallLogs"); + + b.Navigation("Orders"); + + b.Navigation("Packages"); + + b.Navigation("Roles"); + + b.Navigation("operationLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apimanager_backend/Migrations/20241103110714_update-logtable.cs b/Apimanager_backend/Migrations/20241103110714_update-logtable.cs new file mode 100644 index 0000000..81ee381 --- /dev/null +++ b/Apimanager_backend/Migrations/20241103110714_update-logtable.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + /// + public partial class updatelogtable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs b/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs index 1d156eb..06c0db0 100644 --- a/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs +++ b/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs @@ -114,6 +114,40 @@ namespace Apimanager_backend.Migrations b.ToTable("Apipackages"); }); + modelBuilder.Entity("Apimanager_backend.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Exception") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MessageTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => { b.Property("Id") diff --git a/Apimanager_backend/Models/Log.cs b/Apimanager_backend/Models/Log.cs new file mode 100644 index 0000000..ed15d8d --- /dev/null +++ b/Apimanager_backend/Models/Log.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Apimanager_backend.Models +{ + [Table("Logs")] + public class Log + { + public int Id { get; set; } + public DateTime Timestamp { get; set; } + public string Message { get; set; } + public string Exception { get; set; } + public string MessageTemplate { get; set; } + public string Properties { get; set; } + public string LogLevel { get; set; } + + } +} diff --git a/Apimanager_backend/Models/User.cs b/Apimanager_backend/Models/User.cs index c3c3eb1..0366bea 100644 --- a/Apimanager_backend/Models/User.cs +++ b/Apimanager_backend/Models/User.cs @@ -35,16 +35,16 @@ namespace Apimanager_backend.Models /// /// 是否禁用 /// - public bool IsBan { get; set; } // boolean + public bool IsBan { get; set; } = false; // boolean /// /// 是否删除 /// - public bool IsDelete { get; set; } // boolean + public bool IsDelete { get; set; } = false; // boolean /// /// 余额 /// - public decimal Balance { get; set; } // Decimal(10) + public decimal Balance { get; set; } = 0; // Decimal(10) /// /// 创建时间,默认当前时间 diff --git a/Apimanager_backend/Program.cs b/Apimanager_backend/Program.cs index 7c8df94..6fee951 100644 --- a/Apimanager_backend/Program.cs +++ b/Apimanager_backend/Program.cs @@ -1,8 +1,11 @@ using Apimanager_backend.Config; using Apimanager_backend.Data; using Apimanager_backend.Filters; +using Apimanager_backend.Filters.ExceptionFilter; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Sinks.MariaDB.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -12,15 +15,29 @@ IConfiguration configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); - +string? redStr = configuration["Redis:ConnectionString"]; string? constr = configuration.GetConnectionString("DefaultConnection"); +//־ +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.MariaDB( + connectionString: constr, + tableName: "Logs", + autoCreateTable:true + ).CreateLogger(); +builder.Host.UseSerilog(); + builder.Services.AddDbContext(option => option.UseMySql(constr, MySqlServerVersion.AutoDetect(constr)) ); builder.Services.AddAllService(configuration); builder.Services.AddControllers(options => { + //ģ֤ options.Filters.Add(); + //Exception + options.Filters.Add(); }).ConfigureApiBehaviorOptions(option => { option.SuppressModelStateInvalidFilter = true; @@ -33,7 +50,6 @@ builder.Services.AddControllers(options => // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/Apimanager_backend/Services/AdminService.cs b/Apimanager_backend/Services/AdminService.cs new file mode 100644 index 0000000..5cd7898 --- /dev/null +++ b/Apimanager_backend/Services/AdminService.cs @@ -0,0 +1,112 @@ +using Apimanager_backend.Data; +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Models; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel; + +namespace Apimanager_backend.Services +{ + public class AdminService : IAdminService + { + private readonly ApiContext context; + private readonly IMapper mapper; + private readonly ILogger logger; + public AdminService(ApiContext context, IMapper mapper, ILogger logger) + { + this.context = context; + this.mapper = mapper; + this.logger = logger; + } + #region 禁用用户 + public async Task BanUserAsync(int userId) + { + var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId); + if (user == null) + { + throw new BaseException(2004,"用户不存在"); + } + user.IsBan = true; + context.Users.Update(user); + await context.SaveChangesAsync(); + } + #endregion + #region 新建用户 + public async Task CreateUserAsync(CreateUserDto dto) + { + //添加用户 + var user = mapper.Map(dto); + context.Users.Add(user); + await context.SaveChangesAsync(); + //添加默认角色 + UserRole userRole = new UserRole + { + UserId = user.Id, + Role = "User" + }; + + context.UserRoles.Add(userRole); + await context.SaveChangesAsync(); + return mapper.Map(user); + } + #endregion + #region 删除用户 + public async Task DeleteUserAsync(int userId) + { + var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId); + if (user == null) + { + throw new BaseException(2004, "用户不存在"); + } + user.IsDelete = true; + context.Users.Update(user); + await context.SaveChangesAsync(); + } + #endregion + #region 获取用户列表 + public async Task> GetUsersAsync(int page, int pageSize, bool desc) + { + var query = context.Users.Where(x => true) + .OrderBy(x => x.Id); + //倒序 + if (desc) + { + query = query.OrderByDescending(x => x.Id); + } + //分页 + var users = await query.Skip((page - 1) * pageSize) + .Take(pageSize).ToListAsync(); + return mapper.Map>(users); + } + #endregion + #region 禁用用户 + public async Task UnbanUserAsync(int userId) + { + var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId); + if (user == null) + { + throw new BaseException(2004, "用户不存在"); + } + user.IsBan = false; + context.Users.Update(user); + await context.SaveChangesAsync(); + } + #endregion + #region 更新用户信息 + public async Task UpdateUserAsync(int userId,AdminUpdateUserDto dto) + { + var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId); + if(user == null) + { + throw new BaseException(2004,"用户不存在"); + } + user.PassHash = dto.Password; + user.Balance = dto.Balance; + context.Users.Update(user); + await context.SaveChangesAsync(); + return mapper.Map(user); + } + #endregion + } +} diff --git a/Apimanager_backend/Services/AuthService.cs b/Apimanager_backend/Services/AuthService.cs index 156309d..d4993e2 100644 --- a/Apimanager_backend/Services/AuthService.cs +++ b/Apimanager_backend/Services/AuthService.cs @@ -2,20 +2,30 @@ using Apimanager_backend.Dtos; using Apimanager_backend.Exceptions; using Apimanager_backend.Models; +using Apimanager_backend.Tools; using AutoMapper; using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; namespace Apimanager_backend.Services { public class AuthService:IAuthService { private readonly ApiContext apiContext; + private readonly ILogger logger; + private readonly IConnectionMultiplexer redis; + private readonly IEmailService emailService; private readonly IMapper mapper; - public AuthService(ApiContext apiContext, IMapper automapper) + private readonly int DbIndex = 1; + public AuthService(ApiContext apiContext, IMapper automapper,ILogger logger,IConnectionMultiplexer redis,IEmailService emailService) { this.apiContext = apiContext; this.mapper = automapper; + this.logger = logger; + this.redis = redis; + this.emailService = emailService; } + #region 用户登录 public async Task LoginAsync(string username, string password) { //查找用户 @@ -37,5 +47,63 @@ namespace Apimanager_backend.Services return mapper.Map(user); } + #endregion + #region 用户注册 + public async Task RegisterAsync(RegisterRequestDto dto) + { + var db = redis.GetDatabase(DbIndex); + //获取邮箱对应验证码 + var code = await db.StringGetAsync(dto.Email); + if(!code.HasValue || code.ToString() != dto.VerificationCode) + { + throw new BaseException(5005,"验证码错误"); + } + User user = new User + { + Username = dto.Username, + PassHash = dto.Password, + Email = dto.Email, + IsBan = false, + IsDelete = false, + Balance = 0, + }; + try + { + //添加新用户 + await apiContext.Users.AddAsync(user); + await apiContext.SaveChangesAsync(); + UserRole userRole = new UserRole + { + UserId = user.Id, + Role = "User" + }; + await apiContext.UserRoles.AddAsync(userRole); + await apiContext.SaveChangesAsync(); + return mapper.Map(user); + }catch(Exception e) + { + throw new BaseException(1005,e.Message); + } + + } + #endregion + #region 发送注册验证码 + public async Task SendRegisterCodeAsync(string email) + { + //生成随机码 + string code = RandomCodeHelper.GetRandomCodeStr(); + string subject = "注册验证码"; + string body = $"您的注册验证码为:{code}
有效期60分钟!"; + //随机码写入redis + var db = redis.GetDatabase(DbIndex); + bool redisSuccess = await db.StringSetAsync(email,code,TimeSpan.FromHours(1)); + if (!redisSuccess) + { + throw new BaseException(1005,"Redis Str Set Error"); + } + //发送邮件 + await emailService.SendEmailAsync(email,subject,body); + } + #endregion } } diff --git a/Apimanager_backend/Services/EmailSerivce.cs b/Apimanager_backend/Services/EmailSerivce.cs new file mode 100644 index 0000000..7205208 --- /dev/null +++ b/Apimanager_backend/Services/EmailSerivce.cs @@ -0,0 +1,49 @@ +using Apimanager_backend.Exceptions; +using System.Net; +using System.Net.Mail; + +namespace Apimanager_backend.Services +{ + public class EmailService:IEmailService + { + private readonly IConfiguration _configuration; + public EmailService(IConfiguration configuration) + { + _configuration = configuration; + SmtpHost = _configuration["EmailSettings:Server"]; + Port = int.Parse(_configuration["EmailSettings:Port"]); + Username = _configuration["EmailSettings:Username"]; + Password = _configuration["EmailSettings:Password"]; + EnableSSL = bool.Parse(_configuration["EmailSettings:Ssl"]); + } + private string SmtpHost { get; set; } + private int Port { get; set; } + public bool EnableSSL { get; set; } + private string Username { get; set; } + private string Password { get; set; } + public async Task SendEmailAsync(string toEmail,string subject,string body) + { + try + { + using SmtpClient smtpClient = new SmtpClient(SmtpHost, Port) + { + Credentials = new NetworkCredential(Username, Password), + EnableSsl = EnableSSL, //启用ssl + Timeout = 30000 + }; + using var emailMessage = new MailMessage + { + From = new MailAddress(Username), + Subject = subject, + Body = body, + IsBodyHtml = true + }; + emailMessage.To.Add(toEmail); + await smtpClient.SendMailAsync(emailMessage); + }catch(Exception e) + { + throw new BaseException(5004,e.Message); + } + } + } +} diff --git a/Apimanager_backend/Services/IAdminService.cs b/Apimanager_backend/Services/IAdminService.cs new file mode 100644 index 0000000..e19fb34 --- /dev/null +++ b/Apimanager_backend/Services/IAdminService.cs @@ -0,0 +1,45 @@ +using Apimanager_backend.Dtos; + +namespace Apimanager_backend.Services +{ + public interface IAdminService + { + /// + /// 禁用用户,使其无法登录。 + /// + /// 要禁用的用户ID + /// 异步操作 + Task BanUserAsync(int userId); + /// + /// 取消禁用用户,恢复登录权限。 + /// + /// 要取消禁用的用户ID + /// 异步操作 + Task UnbanUserAsync(int userId); + /// + /// 获取分页的用户列表。 + /// + /// 要获取的页码,从1开始 + /// 每页的用户数量 + /// 是否按降序排序 + /// 包含用户信息的 + Task> GetUsersAsync(int page, int pageSize, bool desc); + /// + /// 创建新用户。 + /// + /// 包含新用户信息的 + /// 创建成功的用户信息 + Task CreateUserAsync(CreateUserDto user); + /// + /// 删除指定的用户。 + /// + /// 用户ID + /// 异步操作 + Task DeleteUserAsync(int userId); + /// + /// 修改用户信息 + /// + /// + Task UpdateUserAsync(int userId,AdminUpdateUserDto dto); + } +} diff --git a/Apimanager_backend/Services/IAuthService.cs b/Apimanager_backend/Services/IAuthService.cs index 17c035c..19d7ab4 100644 --- a/Apimanager_backend/Services/IAuthService.cs +++ b/Apimanager_backend/Services/IAuthService.cs @@ -11,5 +11,17 @@ namespace Apimanager_backend.Services /// 密码 /// 包含用户信息的 Task LoginAsync(string username, string password); + /// + /// 用户注册邮箱验证码 + /// + /// + /// + Task SendRegisterCodeAsync(string email); + /// + /// 用户注册 + /// + /// + /// + Task RegisterAsync(RegisterRequestDto dto); } } diff --git a/Apimanager_backend/Services/IEmailService.cs b/Apimanager_backend/Services/IEmailService.cs new file mode 100644 index 0000000..3dac127 --- /dev/null +++ b/Apimanager_backend/Services/IEmailService.cs @@ -0,0 +1,14 @@ +namespace Apimanager_backend.Services +{ + public interface IEmailService + { + /// + /// 发送邮件 + /// + /// 收件人邮箱 + /// 主题 + /// 正文 + /// + public Task SendEmailAsync(string toEmail,string subject,string body); + } +} diff --git a/Apimanager_backend/Services/IRefreshTokenService.cs b/Apimanager_backend/Services/IRefreshTokenService.cs index 343c434..577078e 100644 --- a/Apimanager_backend/Services/IRefreshTokenService.cs +++ b/Apimanager_backend/Services/IRefreshTokenService.cs @@ -13,18 +13,18 @@ ///
/// 刷新令牌 /// 是否验证通过 - Task ValidateRefreshTokenAsync(string refreshToken); + Task ValidateRefreshTokenAsync(string userId,string refreshToken); /// /// 删除刷新令牌 /// /// 刷新令牌 /// 是否删除成功 - Task DeleterRefreshTokenAsync(string refreshToken); + Task DeleterRefreshTokenAsync(string userId); /// /// 更新刷新令牌有效期 /// - /// 刷新令牌 + /// 用户id /// 是否成功 - Task UpdateRefreshTokenAsync(string refreshToken); + Task UpdateRefreshTokenAsync(string userId); } } diff --git a/Apimanager_backend/Services/IUserService.cs b/Apimanager_backend/Services/IUserService.cs index 1162650..7da04dc 100644 --- a/Apimanager_backend/Services/IUserService.cs +++ b/Apimanager_backend/Services/IUserService.cs @@ -6,73 +6,46 @@ namespace Apimanager_backend.Services { public interface IUserService { - + /// + /// 发送密码重置邮件到指定邮箱。 + /// + /// 用户注册的邮箱地址 + /// 异步操作 + Task SendResetPasswordEmailAsync(string email); - /// - /// 发送密码重置邮件到指定邮箱。 - /// - /// 用户注册的邮箱地址 - /// 异步操作 - Task SendResetPasswordEmailAsync(string email); + /// + /// 重置用户密码,验证重置令牌的有效性并更新密码。 + /// + /// 用户邮箱地址 + /// 重置密码的令牌 + /// 新的密码 + /// 异步操作 + Task ResetPasswordAsync(string email, string code, string newPassword); - /// - /// 重置用户密码,验证重置令牌的有效性并更新密码。 - /// - /// 用户邮箱地址 - /// 重置密码的令牌 - /// 新的密码 - /// 异步操作 - Task ResetPasswordAsync(string email, string token, string newPassword); + /// + /// 获取用户信息。 + /// + /// 用户ID + /// 包含用户信息的 + Task GetUserAsync(int userId); - /// - /// 获取用户信息。 - /// - /// 用户ID - /// 包含用户信息的 - Task GetUserAsync(int userId); - - /// - /// 更新用户信息。 - /// - /// 包含更新信息的 - /// 更新后的 - Task UpdateUserAsync(UpdateUserDto user); - - /// - /// 删除指定的用户。 - /// - /// 要删除的用户名 - /// 异步操作 - Task DeleteUserAsync(string username); - - /// - /// 创建新用户。 - /// - /// 包含新用户信息的 - /// 创建成功的用户信息 - Task CreateUserAsync(CreateUserDto user); - - /// - /// 禁用用户,使其无法登录。 - /// - /// 要禁用的用户名 - /// 异步操作 - Task BanUserAsync(string username); - - /// - /// 取消禁用用户,恢复登录权限。 - /// - /// 要取消禁用的用户名 - /// 异步操作 - Task UnbanUserAsync(string username); - - /// - /// 获取分页的用户列表。 - /// - /// 要获取的页码,从1开始 - /// 每页的用户数量 - /// 是否按降序排序 - /// 包含用户信息的 - Task> GetUsersAsync(int page, int pageSize, bool desc); + /// + /// 更新用户信息。 + /// + /// 包含更新信息的 + /// 更新后的 + Task UpdateUserAsync(int userId,UpdateUserDto user); + /// + /// 检测用户名是否被使用 + /// + /// 用户名 + /// + Task IsUsernameExist(string username); + /// + /// 检测邮箱是否被使用 + /// + /// 邮箱 + /// + Task IsEmailExist(string email); } } diff --git a/Apimanager_backend/Services/RefreshTokenService.cs b/Apimanager_backend/Services/RefreshTokenService.cs index 5fc458e..ea58d97 100644 --- a/Apimanager_backend/Services/RefreshTokenService.cs +++ b/Apimanager_backend/Services/RefreshTokenService.cs @@ -7,62 +7,72 @@ namespace Apimanager_backend.Services { private readonly IConnectionMultiplexer redis; private readonly IConfiguration configuration; + private readonly int DbIndex = 0; public RefreshTokenService(IConnectionMultiplexer redis, IConfiguration configuration) { this.redis = redis; this.configuration = configuration; } - + #region 创建刷新令牌 public async Task CreateRefereshTokenAsync(string userId) { var refreshToken = Guid.NewGuid().ToString(); var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]); // 保存到Redis,设置过期时间 - var db = redis.GetDatabase(); - var res = await db.StringSetAsync(refreshToken, userId, TimeSpan.FromDays(expiryDays)); + var db = redis.GetDatabase(DbIndex); + var res = await db.StringSetAsync( userId , refreshToken, TimeSpan.FromDays(expiryDays)); if (!res) { throw new BaseException(1006, "Service unavailable"); } return refreshToken; } - - public async Task DeleterRefreshTokenAsync(string refreshToken) + #endregion + #region 删除刷新令牌 + public async Task DeleterRefreshTokenAsync(string userId) { - var db = redis.GetDatabase(); - bool res = await db.KeyDeleteAsync(refreshToken); + var db = redis.GetDatabase(DbIndex); + bool res = await db.KeyDeleteAsync(userId); if (!res) { throw new BaseException(1006, "Service unavailable"); } } - - public async Task UpdateRefreshTokenAsync(string refreshToken) + #endregion + #region 刷新令牌有效期 + public async Task UpdateRefreshTokenAsync(string userId) { - var db = redis.GetDatabase(); + var db = redis.GetDatabase(DbIndex); var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]); //获取refresh剩余有效时间 - var time =await db.KeyTimeToLiveAsync(refreshToken); + var time =await db.KeyTimeToLiveAsync(userId); //判断有效时间是否大于零天小于三天,否则不刷新有效期 if(time <= TimeSpan.Zero || time >= TimeSpan.FromDays(3)) { return; } //刷新过期时间 - await db.KeyExpireAsync(refreshToken,TimeSpan.FromDays(expiryDays)); + await db.KeyExpireAsync(userId,TimeSpan.FromDays(expiryDays)); } - - public async Task ValidateRefreshTokenAsync(string refreshToken) + #endregion + #region 验证令牌 + public async Task ValidateRefreshTokenAsync(string userId,string refreshToken) { - var db = redis.GetDatabase(); - var redisValue = await db.StringGetAsync(refreshToken); + var db = redis.GetDatabase(DbIndex); + var redisValue = await db.StringGetAsync(userId); //验证refreshToken是否存在 if (!redisValue.HasValue) { - return null; + return false; } - return redisValue.ToString(); + string refreshTokenTrue = redisValue.ToString(); + if (!refreshToken.Equals(refreshTokenTrue)) + { + return false; + } + return true; } + #endregion } } diff --git a/Apimanager_backend/Services/UserService.cs b/Apimanager_backend/Services/UserService.cs index 35fe2e2..76528c1 100644 --- a/Apimanager_backend/Services/UserService.cs +++ b/Apimanager_backend/Services/UserService.cs @@ -3,9 +3,12 @@ using Apimanager_backend.Data; using Apimanager_backend.Dtos; using Apimanager_backend.Exceptions; using Apimanager_backend.Models; +using Apimanager_backend.Tools; using AutoMapper; using Microsoft.AspNetCore.Connections.Features; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using StackExchange.Redis; using System.ComponentModel; namespace Apimanager_backend.Services @@ -14,26 +17,18 @@ namespace Apimanager_backend.Services { private readonly ApiContext apiContext; private readonly IMapper mapper; - public UserService(ApiContext apiContext,IMapper automapper) + private readonly ILogger logger; + private readonly IConnectionMultiplexer redis; + private readonly IEmailService emailService; + private readonly int DbSet = 2; + public UserService(ApiContext apiContext,IMapper automapper,ILogger logger,IConnectionMultiplexer redis,IEmailService emailService) { this.apiContext = apiContext; this.mapper = automapper; + this.logger = logger; + this.redis = redis; + this.emailService = emailService; } - public Task BanUserAsync(string username) - { - throw new NotImplementedException(); - } - - public Task CreateUserAsync(CreateUserDto user) - { - throw new NotImplementedException(); - } - - public Task DeleteUserAsync(string username) - { - throw new NotImplementedException(); - } - public async Task GetUserAsync(int userId) { User? user = await apiContext.Users.SingleOrDefaultAsync(x => x.Id == userId); @@ -45,30 +40,66 @@ namespace Apimanager_backend.Services return mapper.Map(user); } - public Task> GetUsersAsync(int page, int pageSize, bool desc) + public async Task IsEmailExist(string email) { - throw new NotImplementedException(); + return await apiContext.Users.AnyAsync(x => x.Email == email); } - - public Task ResetPasswordAsync(string email, string token, string newPassword) + public async Task IsUsernameExist(string username) { - throw new NotImplementedException(); + return await apiContext.Users.AnyAsync(x => x.Username == username); } - public Task SendResetPasswordEmailAsync(string email) + public async Task ResetPasswordAsync(string email, string code, string newPassword) { - throw new NotImplementedException(); + //校验验证码 + var db = redis.GetDatabase(DbSet); + var value = await db.StringGetAsync(email); + if (!value.HasValue || value.ToString() != code) + { + throw new BaseException(5005, "验证码错误"); + } + //验证成功,开始重置流程 + var user = await apiContext.Users.FirstOrDefaultAsync(x => x.Email == email); + if(user == null) + { + throw new BaseException(2004, "用户不存在"); + } + //修改密码 + user.PassHash = newPassword; + apiContext.Users.Update(user); + await apiContext.SaveChangesAsync(); } - - public Task UnbanUserAsync(string username) + #region 发送密码重置邮件 + public async Task SendResetPasswordEmailAsync(string email) { - throw new NotImplementedException(); + var randomCode = RandomCodeHelper.GetRandomCodeStr(); + //记录到redis + var db = redis.GetDatabase(DbSet); + bool redisSuccess = await db.StringSetAsync(email,randomCode,TimeSpan.FromHours(1)); + if (!redisSuccess) + { + throw new BaseException(1005, "Redis Str Set Error"); + } + string subject = "重置验证码"; + string body = $"您的重置验证码为:{randomCode}
有效期60分钟!"; + //发送邮件 + await emailService.SendEmailAsync(email,subject,body); } + #endregion - public Task UpdateUserAsync(UpdateUserDto user) + public async Task UpdateUserAsync(int userId,UpdateUserDto dto) { - throw new NotImplementedException(); + var user = await apiContext.Users.FirstOrDefaultAsync(x => x.Id == userId); + if (user == null) + { + throw new BaseException(2004, "用户不存在"); + } + user.PassHash = dto.password == null ? user.PassHash : dto.password; + + apiContext.Users.Update(user); + await apiContext.SaveChangesAsync(); + return mapper.Map(user); } } } diff --git a/Apimanager_backend/Tools/RandomCodeHelper.cs b/Apimanager_backend/Tools/RandomCodeHelper.cs new file mode 100644 index 0000000..3392290 --- /dev/null +++ b/Apimanager_backend/Tools/RandomCodeHelper.cs @@ -0,0 +1,15 @@ +namespace Apimanager_backend.Tools +{ + public static class RandomCodeHelper + { + /// + /// 生成随机数字符串 + /// + /// + public static string GetRandomCodeStr() + { + Random random = new Random(); + return random.Next(10000, 99999).ToString(); + } + } +} diff --git a/Apimanager_backend/Tools/StatusCodeHelper.cs b/Apimanager_backend/Tools/StatusCodeHelper.cs new file mode 100644 index 0000000..e0728a6 --- /dev/null +++ b/Apimanager_backend/Tools/StatusCodeHelper.cs @@ -0,0 +1,133 @@ +namespace Apimanager_backend.Tools +{ + public static class StatusCodeHelper + { + public static int GetHttpStatusCode(int customErrorCode) + { + int httpStatusCode; + + switch (customErrorCode) + { + // 通用错误码 + case 1000: // 成功 + httpStatusCode = 200; + break; + case 1001: // 参数错误 + httpStatusCode = 400; + break; + case 1002: // 用户未登录或认证失败 + httpStatusCode = 401; + break; + case 1003: // 无权限访问 + httpStatusCode = 403; + break; + case 1004: // 资源未找到 + httpStatusCode = 404; + break; + case 1005: // 服务器内部错误 + httpStatusCode = 500; + break; + case 1006: // 服务暂时不可用 + httpStatusCode = 503; + break; + + // 用户模块错误码 + case 2000: // 登录成功 + httpStatusCode = 200; + break; + case 2001: // 用户名或密码错误 + httpStatusCode = 401; + break; + case 2002: // 用户账户被禁用 + httpStatusCode = 401; + break; + case 2003: // 用户名已存在 + httpStatusCode = 409; + break; + case 2004: // 用户不存在 + httpStatusCode = 404; + break; + case 2005: // 邮箱已存在 + httpStatusCode = 409; + break; + case 2006: // 用户无权限进行该操作 + httpStatusCode = 403; + break; + case 2007: // 密码重置失败 + httpStatusCode = 400; + break; + case 2008: // 凭证到期或无效 + httpStatusCode = 403; + break; + case 2009: // 刷新令牌到期或无效 + httpStatusCode = 403; + break; + + // API模块错误码 + case 3000: // API调用成功 + httpStatusCode = 200; + break; + case 3001: // API访问受限 + httpStatusCode = 403; + break; + case 3002: // API不存在 + httpStatusCode = 404; + break; + case 3003: // API调用次数超限 + httpStatusCode = 429; + break; + case 3004: // 未购买该API套餐或权限不足 + httpStatusCode = 403; + break; + case 3005: // API调用失败,服务器错误 + httpStatusCode = 500; + break; + + // 套餐与支付模块错误码 + case 4000: // 支付成功 + httpStatusCode = 200; + break; + case 4001: // 支付请求无效 + httpStatusCode = 400; + break; + case 4002: // 支付失败,余额不足 + httpStatusCode = 402; + break; + case 4003: // 订单未找到 + httpStatusCode = 404; + break; + case 4004: // 重复支付或订单冲突 + httpStatusCode = 409; + break; + case 4005: // 支付系统错误 + httpStatusCode = 500; + break; + case 4006: // 退款成功 + httpStatusCode = 200; + break; + + // 日志与系统模块错误码 + case 5000: // 日志查询成功 + httpStatusCode = 200; + break; + case 5001: // 日志记录未找到 + httpStatusCode = 404; + break; + case 5002: // 日志服务异常 + httpStatusCode = 500; + break; + case 5003: // 无权限查看操作日志 + httpStatusCode = 403; + break; + + default: + // 未知错误码,返回 500 + httpStatusCode = 500; + break; + } + + return httpStatusCode; + } + + } +} diff --git a/Apimanager_backend/appsettings.json b/Apimanager_backend/appsettings.json index fb79bdb..eb6d675 100644 --- a/Apimanager_backend/appsettings.json +++ b/Apimanager_backend/appsettings.json @@ -18,5 +18,12 @@ }, "redis": { "ConnectionString": "192.168.5.200:6379" + }, + "EmailSettings": { + "Server": "smtp.exmail.qq.com", + "Port": "587", + "Ssl": true, + "Username": "nanxun@nxsir.cn", + "Password": "2919054393Dyw" } } diff --git a/ErrorCode.md b/ErrorCode.md index 7b5d551..4b832d6 100644 --- a/ErrorCode.md +++ b/ErrorCode.md @@ -57,4 +57,6 @@ | 5000 | 200 | 日志查询成功 | Log retrieval successful | | 5001 | 404 | 日志记录未找到 | Log record not found | | 5002 | 500 | 日志服务异常 | Log service error | -| 5003 | 403 | 无权限查看操作日志 | No permission to view logs | \ No newline at end of file +| 5003 | 403 | 无权限查看操作日志 | No permission to view logs | +| 5004 | 500 | 邮件发送错误 | Email send error | +| 5005 | 400 | 验证码错误 | ValidateCode Error | \ No newline at end of file