新增注册流程

This commit is contained in:
南浔 2024-10-29 09:28:01 +08:00
commit f462f0086d
36 changed files with 2001 additions and 143 deletions

View File

@ -8,11 +8,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.MariaDB" Version="1.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,5 +1,10 @@
using Apimanager_backend.Services; using Apimanager_backend.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis;
using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
namespace Apimanager_backend.Config namespace Apimanager_backend.Config
{ {
@ -8,7 +13,36 @@ namespace Apimanager_backend.Config
public static IServiceCollection AddAllService(this IServiceCollection services,IConfiguration configuration) public static IServiceCollection AddAllService(this IServiceCollection services,IConfiguration configuration)
{ {
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
services.AddJWTService(configuration);
services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserService, UserService>();
services.AddScoped<IAuthService, AuthService>();
services.AddSingleton<ITokenService, TokenService>();
services.AddSingleton<IRefreshTokenService, RefreshTokenService>();
services.AddSingleton<IEmailService, EmailService>();
return services;
}
public static IServiceCollection AddJWTService(this IServiceCollection services,IConfiguration configuration)
{
var jwtSettings = configuration.GetSection("JwtSettings");
var key = Encoding.ASCII.GetBytes(jwtSettings["Secret"]);
// JWT配置
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
//redis配置
services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(configuration["Redis:ConnectionString"]));
return services; return services;
} }
} }

View File

@ -0,0 +1,158 @@
using Apimanager_backend.Dtos;
using Apimanager_backend.Exceptions;
using Apimanager_backend.Models;
using Apimanager_backend.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualBasic;
namespace Apimanager_backend.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IAuthService authService;
private readonly ITokenService tokenService;
private readonly IRefreshTokenService refreshTokenService;
private readonly IUserService userService;
public AuthController(IAuthService authService, ITokenService tokenService, IRefreshTokenService refreshTokenService,IUserService userService)
{
this.authService = authService;
this.tokenService = tokenService;
this.refreshTokenService = refreshTokenService;
this.userService = userService;
}
/// <summary>
/// 用户登录控制器
/// </summary>
/// <param name="dto">登录信息</param>
/// <returns>通用返回信息格式</returns>
[HttpPost]
public async Task<ActionResult<ResponseBase<UserInfoDto>>> Login([FromBody] UserLoginDto dto)
{
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<LoginResponseDto>(
code: 2000,
message: "Login successful",
data: new LoginResponseDto
{
UserInfo = user,
Token = token,
RefreshToken = refreshToken
}
);
return Ok(responseInfo);
}
/// <summary>
/// 令牌刷新
/// </summary>
/// <param name="dto">传入用户令牌</param>
/// <returns>返回新令牌</returns>
[HttpPost]
public async Task<ActionResult<ResponseBase<RefreshResponseDto?>>> Refresh([FromBody]RefreshResponseDto dto)
{
var IsRefreshToken = await refreshTokenService.ValidateRefreshTokenAsync(dto.UserId.ToString(),dto.RefreshToken);
//刷新令牌无效
if (!IsRefreshToken)
{
var ret = new ResponseBase<RefreshResponseDto?>(
code: 2008,
message: "Refresh expires or is invalid",
data: null
);
return Unauthorized(ret);
}
//获取刷新令牌对应用户信息
var userInfo = await userService.GetUserAsync(dto.UserId);
//重新生成令牌
var token = tokenService.GenerateAccessToken(userInfo.Id.ToString(), userInfo.Roles);
//刷新刷新令牌有效期(小于三天才会刷新)
await refreshTokenService.UpdateRefreshTokenAsync(userInfo.Id.ToString());
var result = new ResponseBase<RefreshResponseDto?>(
code: 1000,
message: "Success",
data: new RefreshResponseDto
{
UserId = dto.UserId,
Token = token,
RefreshToken = dto.RefreshToken
}
);
return Ok(result);
}
/// <summary>
/// 用户注册
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
[HttpPost]
public async Task<ActionResult<ResponseBase<UserInfoDto?>>> Register(RegisterRequestDto requestDto)
{
var isUsernameExist = await userService.IsUsernameExist(requestDto.Username);
if (isUsernameExist)
{
var errorRes = new ResponseBase<UserInfoDto?>(
code:2003,
message:"用户名已存在",
data:null
);
return StatusCode(409,errorRes);
}
try
{
var userInfo = await authService.RegisterAsync(requestDto);
var res = new ResponseBase<UserInfoDto?>(
code:1000,
message:"Success",
data:userInfo
);
return Ok(res);
}catch(BaseException e)
{
var res = new ResponseBase<UserInfoDto?>(
code:e.code,
message:e.message,
data: null
);
return StatusCode(500,res);
}
}
/// <summary>
/// 发送邮箱校验码
/// </summary>
/// <param name="registerRequestDto"></param>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult<ResponseBase<object?>>> SendValidateCode([FromQuery]string email)
{
//检测邮箱是否被使用
var emailIsUse = await userService.IsEmailExist(email);
if (emailIsUse)
{
var errorRes = new ResponseBase<object?>(
code:2005,
message: "邮箱已存在",
data:null
);
return StatusCode(409,errorRes);
}
//发送注册验证码
await authService.SendRegisterCodeAsync(email);
var res = new ResponseBase<object?>(
code:1000,
message:"Success",
data: null
);
return Ok(res);
}
}
}

View File

@ -7,8 +7,7 @@ using Apimanager_backend.Filters;
namespace Apimanager_backend.Controllers namespace Apimanager_backend.Controllers
{ {
[ModelValidationFilter()] [Route("api/[controller]/[action]")]
[Route("api/[controller]")]
[ApiController] [ApiController]
public class UserController : ControllerBase public class UserController : ControllerBase
{ {
@ -17,37 +16,6 @@ namespace Apimanager_backend.Controllers
{ {
this.userService = userService; this.userService = userService;
} }
/// <summary>
/// 用户登录控制器
/// </summary>
/// <param name="dto">登录信息</param>
/// <returns>通用返回信息格式</returns>
[HttpPost("Login")]
public async Task<ActionResult<ResponseBase<UserInfoDto>>> Login([FromBody]UserLoginDto dto)
{
try
{
UserInfoDto user = await userService.LoginAsync(dto.UserName, dto.Password);
var responseInfo = new ResponseBase<UserInfoDto>(
code: 2000,
message: "Login successful",
data: user
);
return Ok(responseInfo);
}
catch (BaseException e)
{
//错误时,构建错误信息对象
var responseInfo = new ResponseBase<object?>(
code:e.code,
message: e.message,
data: null
);
return Unauthorized(responseInfo);
}
}
} }
} }

View File

@ -20,6 +20,10 @@ namespace Apimanager_backend.Data
public DbSet<Order> Orders { get; set; } public DbSet<Order> Orders { get; set; }
//用户已订购套餐表 //用户已订购套餐表
public DbSet<UserPackage> UserPackages { get; set; } public DbSet<UserPackage> UserPackages { get; set; }
//用户角色表
public DbSet<UserRole> UserRoles { get; set; }
//日志表
public DbSet<Log> Logs { get; set; }
public ApiContext(DbContextOptions<ApiContext> options) : base(options) { } public ApiContext(DbContextOptions<ApiContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -27,6 +31,11 @@ namespace Apimanager_backend.Data
// 配置全局查询筛选器 // 配置全局查询筛选器
modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsDelete); modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsDelete);
modelBuilder.Entity<Api>().HasQueryFilter(a => !a.IsDelete); modelBuilder.Entity<Api>().HasQueryFilter(a => !a.IsDelete);
//配置日志表
modelBuilder.Entity<Log>().HasKey(x => x.Id);
modelBuilder.Entity<Log>()
.Property(x => x.Id)
.ValueGeneratedOnAdd();
} }
} }

View File

@ -0,0 +1,21 @@
using Apimanager_backend.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Apimanager_backend.Data
{
public class UserRoleConfig : IEntityTypeConfiguration<UserRole>
{
public void Configure(EntityTypeBuilder<UserRole> builder)
{
builder.HasKey(x => x.Id);
//自增
builder.Property(x => x.Id)
.ValueGeneratedOnAdd();
//外键
builder.HasOne(x => x.User)
.WithMany(u => u.Roles)
.HasForeignKey(x => x.UserId);
}
}
}

View File

@ -0,0 +1,11 @@
using Apimanager_backend.Models;
namespace Apimanager_backend.Dtos
{
public class LoginResponseDto
{
public UserInfoDto UserInfo { get; set; }
public string Token { get; set; }
public string RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Apimanager_backend.Dtos
{
public class RefreshResponseDto
{
[Required(ErrorMessage = "用户ID必填")]
public int UserId { get; set; }
public string? Token { get; set; }
[Required(ErrorMessage = "刷新令牌必填!")]
public string RefreshToken { get; set; }
}
}

View File

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

View File

@ -0,0 +1,16 @@
using Apimanager_backend.Models;
namespace Apimanager_backend.Dtos
{
public class UserInfoBaseDto
{
public UserInfoDto UserInfo { get; set; }
public List<UserRole> Roles { get; set; }
public UserInfoBaseDto(UserInfoDto userInfo, List<UserRole> roles)
{
UserInfo = userInfo;
Roles = roles;
}
public UserInfoBaseDto() { }
}
}

View File

@ -7,7 +7,7 @@ namespace Apimanager_backend.Dtos
public int Id { get; set; } public int Id { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string Email { get; set; } public string Email { get; set; }
public UserRole Role { get; set; } public List<UserRole> Roles { get; set; }
public bool IsBan { get; set; } public bool IsBan { get; set; }
public decimal Balance { get; set; } public decimal Balance { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }

View File

@ -0,0 +1,30 @@
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
{
public class generalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if(context.Exception is BaseException)
{
//构造通用错误返回结果
BaseException exception = (BaseException)context.Exception;
var res = new ResponseBase<object?>(
code:exception.code,
message: exception.message,
data:null
);
int httpCode = StatusCodeHelper.GetHttpStatusCode(exception.code);
context.Result = new JsonResult(res) {
StatusCode = httpCode
};
context.ExceptionHandled = true;
}
}
}
}

View File

@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
namespace Apimanager_backend.Filters namespace Apimanager_backend.Filters
{ {
public class ModelValidationFilter : Attribute,IActionFilter public class ModelValidationFilter :IActionFilter
{ {
public void OnActionExecuted(ActionExecutedContext context) { } public void OnActionExecuted(ActionExecutedContext context) { }

View File

@ -0,0 +1,408 @@
// <auto-generated />
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("20241030160723_add-userrole-table")]
partial class adduserroletable
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsDelete")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsThirdParty")
.HasColumnType("tinyint(1)");
b.Property<int>("Method")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<int?>("PackageId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("PackageId");
b.ToTable("Apis");
});
modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("ApiId")
.HasColumnType("int");
b.Property<int>("CallResult")
.HasColumnType("int");
b.Property<DateTime>("CallTime")
.HasColumnType("datetime(6)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ApiId");
b.HasIndex("UserId");
b.ToTable("CallLogs");
});
modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("CallLimit")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("ExpiryDate")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<decimal>("Price")
.HasColumnType("decimal(65,30)");
b.HasKey("Id");
b.ToTable("Apipackages");
});
modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("varchar(45)");
b.Property<string>("Operation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<int>("TargetId")
.HasColumnType("int");
b.Property<string>("TargetType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("UserAgent")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("OperationLogs");
});
modelBuilder.Entity("Apimanager_backend.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<string>("OrderNumber")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<int>("OrderType")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<string>("ThirdPartyOrderId")
.HasColumnType("varchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Balance")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<bool>("IsBan")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsDelete")
.HasColumnType("tinyint(1)");
b.Property<string>("PassHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("PackageId")
.HasColumnType("int");
b.Property<DateTime>("PurchasedAt")
.HasColumnType("datetime(6)");
b.Property<int>("RemainingCalls")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("UserId");
b.ToTable("UserPackages");
});
modelBuilder.Entity("Apimanager_backend.Models.UserRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("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
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Apimanager_backend.Migrations
{
/// <inheritdoc />
public partial class adduserroletable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Role",
table: "Users");
migrationBuilder.CreateTable(
name: "UserRoles",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<int>(type: "int", nullable: false),
Role = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_UserRoles", x => x.Id);
table.ForeignKey(
name: "FK_UserRoles_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_UserRoles_UserId",
table: "UserRoles",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserRoles");
migrationBuilder.AddColumn<int>(
name: "Role",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View File

@ -0,0 +1,442 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsDelete")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsThirdParty")
.HasColumnType("tinyint(1)");
b.Property<int>("Method")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<int?>("PackageId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("PackageId");
b.ToTable("Apis");
});
modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("ApiId")
.HasColumnType("int");
b.Property<int>("CallResult")
.HasColumnType("int");
b.Property<DateTime>("CallTime")
.HasColumnType("datetime(6)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ApiId");
b.HasIndex("UserId");
b.ToTable("CallLogs");
});
modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("CallLimit")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("ExpiryDate")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<decimal>("Price")
.HasColumnType("decimal(65,30)");
b.HasKey("Id");
b.ToTable("Apipackages");
});
modelBuilder.Entity("Apimanager_backend.Models.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("LogLevel")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.ToTable("Logs");
});
modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("varchar(45)");
b.Property<string>("Operation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<int>("TargetId")
.HasColumnType("int");
b.Property<string>("TargetType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("UserAgent")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("OperationLogs");
});
modelBuilder.Entity("Apimanager_backend.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<string>("OrderNumber")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<int>("OrderType")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<string>("ThirdPartyOrderId")
.HasColumnType("varchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Balance")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<bool>("IsBan")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsDelete")
.HasColumnType("tinyint(1)");
b.Property<string>("PassHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("PackageId")
.HasColumnType("int");
b.Property<DateTime>("PurchasedAt")
.HasColumnType("datetime(6)");
b.Property<int>("RemainingCalls")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("UserId");
b.ToTable("UserPackages");
});
modelBuilder.Entity("Apimanager_backend.Models.UserRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("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
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Apimanager_backend.Migrations
{
/// <inheritdoc />
public partial class updatelogtable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -114,6 +114,40 @@ namespace Apimanager_backend.Migrations
b.ToTable("Apipackages"); b.ToTable("Apipackages");
}); });
modelBuilder.Entity("Apimanager_backend.Models.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("LogLevel")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.ToTable("Logs");
});
modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -233,9 +267,6 @@ namespace Apimanager_backend.Migrations
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnType("varchar(255)"); .HasColumnType("varchar(255)");
b.Property<int>("Role")
.HasColumnType("int");
b.Property<string>("Username") b.Property<string>("Username")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(255)");
@ -278,6 +309,26 @@ namespace Apimanager_backend.Migrations
b.ToTable("UserPackages"); b.ToTable("UserPackages");
}); });
modelBuilder.Entity("Apimanager_backend.Models.UserRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserRoles");
});
modelBuilder.Entity("Apimanager_backend.Models.Api", b => modelBuilder.Entity("Apimanager_backend.Models.Api", b =>
{ {
b.HasOne("Apimanager_backend.Models.Apipackage", "Package") b.HasOne("Apimanager_backend.Models.Apipackage", "Package")
@ -347,6 +398,17 @@ namespace Apimanager_backend.Migrations
b.Navigation("User"); 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 => modelBuilder.Entity("Apimanager_backend.Models.Api", b =>
{ {
b.Navigation("ApiCalls"); b.Navigation("ApiCalls");
@ -367,6 +429,8 @@ namespace Apimanager_backend.Migrations
b.Navigation("Packages"); b.Navigation("Packages");
b.Navigation("Roles");
b.Navigation("operationLogs"); b.Navigation("operationLogs");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618

View File

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

View File

@ -30,7 +30,7 @@ namespace Apimanager_backend.Models
/// <summary> /// <summary>
/// 用户角色 /// 用户角色
/// </summary> /// </summary>
public UserRole Role { get; set; } // Enum('Admin','User') //public UserRole Role { get; set; } // Enum('Admin','User')
/// <summary> /// <summary>
/// 是否禁用 /// 是否禁用
@ -56,5 +56,6 @@ namespace Apimanager_backend.Models
public ICollection<OperationLog> operationLogs { get; set; } public ICollection<OperationLog> operationLogs { get; set; }
public ICollection<ApiCallLog> CallLogs { get; set; } public ICollection<ApiCallLog> CallLogs { get; set; }
public ICollection<Order> Orders { get; set; } public ICollection<Order> Orders { get; set; }
public ICollection<UserRole> Roles { get; set; } = new List<UserRole>();
} }
} }

View File

@ -1,8 +1,15 @@
namespace Apimanager_backend.Models using System.Text.Json.Serialization;
namespace Apimanager_backend.Models
{ {
public enum UserRole public class UserRole
{ {
Admin = 0, public int Id { get; set; }
User = 1 public int UserId { get; set; }
public string Role { get; set; }
//导航属性
[JsonIgnore]
public User User { get; set; }
} }
} }

View File

@ -3,6 +3,8 @@ using Apimanager_backend.Data;
using Apimanager_backend.Filters; using Apimanager_backend.Filters;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Sinks.MariaDB.Extensions;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -12,8 +14,19 @@ IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build(); .Build();
string? redStr = configuration["Redis:ConnectionString"];
string? constr = configuration.GetConnectionString("DefaultConnection"); 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<ApiContext>(option => builder.Services.AddDbContext<ApiContext>(option =>
option.UseMySql(constr, MySqlServerVersion.AutoDetect(constr)) option.UseMySql(constr, MySqlServerVersion.AutoDetect(constr))
); );
@ -21,11 +34,18 @@ builder.Services.AddAllService(configuration);
builder.Services.AddControllers(options => builder.Services.AddControllers(options =>
{ {
options.Filters.Add<ModelValidationFilter>(); options.Filters.Add<ModelValidationFilter>();
}).ConfigureApiBehaviorOptions(option =>
{
option.SuppressModelStateInvalidFilter = true;
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.MaxDepth = 64;
}); });
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@ -37,6 +57,7 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();

View File

@ -0,0 +1,105 @@
using Apimanager_backend.Data;
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<IAuthService> logger;
private readonly IConnectionMultiplexer redis;
private readonly IEmailService emailService;
private readonly IMapper mapper;
private readonly int DbIndex = 1;
public AuthService(ApiContext apiContext, IMapper automapper,ILogger<AuthService> logger,IConnectionMultiplexer redis,IEmailService emailService)
{
this.apiContext = apiContext;
this.mapper = automapper;
this.logger = logger;
this.redis = redis;
this.emailService = emailService;
}
public async Task<UserInfoDto> LoginAsync(string username, string password)
{
//查找用户
User? user = await apiContext.Users.Include(x => x.Roles).SingleOrDefaultAsync(x =>
x.Username == username && x.PassHash == password
);
//用户不存在或密码错误都为登录失败
if (user == null)
{
throw new BaseException(2001, "Invalid username or password");
}
//用户被禁用
if (user.IsBan)
{
throw new BaseException(2002, "User account is disabled");
}
return mapper.Map<UserInfoDto>(user);
}
public async Task<UserInfoDto> 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<UserInfoDto>(user);
}catch(Exception e)
{
throw new BaseException(1005,e.Message);
}
}
public async Task SendRegisterCodeAsync(string email)
{
//生成随机码
string code = RandomCodeHelper.GetRandomCodeStr();
string subject = "注册验证码";
string body = $"您的注册验证码为:{code}<br>有效期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);
}
}
}

View File

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

View File

@ -0,0 +1,27 @@
using Apimanager_backend.Dtos;
namespace Apimanager_backend.Services
{
public interface IAuthService
{
/// <summary>
/// 登录用户,根据用户名和密码进行身份验证。
/// </summary>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
/// <returns>包含用户信息的 <see cref="UserInfoBaseDto"/></returns>
Task<UserInfoDto> LoginAsync(string username, string password);
/// <summary>
/// 用户注册邮箱验证码
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
Task SendRegisterCodeAsync(string email);
/// <summary>
/// 用户注册
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<UserInfoDto> RegisterAsync(RegisterRequestDto dto);
}
}

View File

@ -0,0 +1,14 @@
namespace Apimanager_backend.Services
{
public interface IEmailService
{
/// <summary>
/// 发送邮件
/// </summary>
/// <param name="toEmail">收件人邮箱</param>
/// <param name="subject">主题</param>
/// <param name="body">正文</param>
/// <returns></returns>
public Task SendEmailAsync(string toEmail,string subject,string body);
}
}

View File

@ -0,0 +1,30 @@
namespace Apimanager_backend.Services
{
public interface IRefreshTokenService
{
/// <summary>
/// 创建刷新令牌
/// </summary>
/// <param name="userId">用户id</param>
/// <returns>刷新令牌</returns>
Task<string> CreateRefereshTokenAsync(string userId);
/// <summary>
/// 验证刷新令牌
/// </summary>
/// <param name="refreshToken">刷新令牌</param>
/// <returns>是否验证通过</returns>
Task<bool> ValidateRefreshTokenAsync(string userId,string refreshToken);
/// <summary>
/// 删除刷新令牌
/// </summary>
/// <param name="refreshToken">刷新令牌</param>
/// <returns>是否删除成功</returns>
Task DeleterRefreshTokenAsync(string userId);
/// <summary>
/// 更新刷新令牌有效期
/// </summary>
/// <param name="refreshToken">刷新令牌</param>
/// <returns>是否成功</returns>
Task UpdateRefreshTokenAsync(string userId);
}
}

View File

@ -0,0 +1,16 @@
using Apimanager_backend.Models;
namespace Apimanager_backend.Services
{
public interface ITokenService
{
/// <summary>
/// 拥护凭证
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="username">用户名</param>
/// <param name="role">角色</param>
/// <returns>token</returns>
string GenerateAccessToken(string userId, List<UserRole> roles);
}
}

View File

@ -7,14 +7,6 @@ namespace Apimanager_backend.Services
public interface IUserService public interface IUserService
{ {
/// <summary> /// <summary>
/// 登录用户,根据用户名和密码进行身份验证。
/// </summary>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
/// <returns>包含用户信息的 <see cref="UserInfoDto"/></returns>
Task<UserInfoDto> LoginAsync(string username, string password);
/// <summary>
/// 发送密码重置邮件到指定邮箱。 /// 发送密码重置邮件到指定邮箱。
/// </summary> /// </summary>
/// <param name="email">用户注册的邮箱地址</param> /// <param name="email">用户注册的邮箱地址</param>
@ -33,9 +25,9 @@ namespace Apimanager_backend.Services
/// <summary> /// <summary>
/// 获取用户信息。 /// 获取用户信息。
/// </summary> /// </summary>
/// <param name="username">用户名</param> /// <param name="userId">用户ID</param>
/// <returns>包含用户信息的 <see cref="UserInfoDto"/></returns> /// <returns>包含用户信息的 <see cref="UserInfoDto"/></returns>
Task<UserInfoDto> GetUserAsync(string username); Task<UserInfoDto> GetUserAsync(int userId);
/// <summary> /// <summary>
/// 更新用户信息。 /// 更新用户信息。
@ -80,5 +72,17 @@ namespace Apimanager_backend.Services
/// <param name="desc">是否按降序排序</param> /// <param name="desc">是否按降序排序</param>
/// <returns>包含用户信息的 <see cref="List{UserInfoDto}"/></returns> /// <returns>包含用户信息的 <see cref="List{UserInfoDto}"/></returns>
Task<List<UserInfoDto>> GetUsersAsync(int page, int pageSize, bool desc); Task<List<UserInfoDto>> GetUsersAsync(int page, int pageSize, bool desc);
/// <summary>
/// 检测用户名是否被使用
/// </summary>
/// <param name="username">用户名</param>
/// <returns></returns>
Task<bool> IsUsernameExist(string username);
/// <summary>
/// 检测邮箱是否被使用
/// </summary>
/// <param name="email">邮箱</param>
/// <returns></returns>
Task<bool> IsEmailExist(string email);
} }
} }

View File

@ -0,0 +1,74 @@
using Apimanager_backend.Exceptions;
using StackExchange.Redis;
namespace Apimanager_backend.Services
{
public class RefreshTokenService : IRefreshTokenService
{
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;
}
public async Task<string> CreateRefereshTokenAsync(string userId)
{
var refreshToken = Guid.NewGuid().ToString();
var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]);
// 保存到Redis设置过期时间
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 userId)
{
var db = redis.GetDatabase(DbIndex);
bool res = await db.KeyDeleteAsync(userId);
if (!res)
{
throw new BaseException(1006, "Service unavailable");
}
}
public async Task UpdateRefreshTokenAsync(string userId)
{
var db = redis.GetDatabase(DbIndex);
var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]);
//获取refresh剩余有效时间
var time =await db.KeyTimeToLiveAsync(userId);
//判断有效时间是否大于零天小于三天,否则不刷新有效期
if(time <= TimeSpan.Zero || time >= TimeSpan.FromDays(3))
{
return;
}
//刷新过期时间
await db.KeyExpireAsync(userId,TimeSpan.FromDays(expiryDays));
}
public async Task<bool> ValidateRefreshTokenAsync(string userId,string refreshToken)
{
var db = redis.GetDatabase(DbIndex);
var redisValue = await db.StringGetAsync(userId);
//验证refreshToken是否存在
if (!redisValue.HasValue)
{
return false;
}
string refreshTokenTrue = redisValue.ToString();
if (!refreshToken.Equals(refreshTokenTrue))
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,50 @@

using Apimanager_backend.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Apimanager_backend.Services
{
public class TokenService:ITokenService
{
public readonly IConfiguration configuration;
public TokenService(IConfiguration configuration)
{
this.configuration = configuration;
}
public string GenerateAccessToken(string userId,List<UserRole> roles)
{
var jwtSettings = configuration.GetSection("JwtSettings");
// 创建Claims列表包含用户名和角色信息
var claims = new List<Claim>
{
new Claim("userId", userId), // 使用userId作为唯一标识
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
//添加用户角色
foreach(var role in roles)
{
var claim = new Claim(ClaimTypes.Role, role.Role.ToString());
claims.Add(claim);
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Secret"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: jwtSettings["Issuer"],
audience: jwtSettings["Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings["AccessTokenExpiryMinutes"])),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

View File

@ -34,9 +34,15 @@ namespace Apimanager_backend.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<UserInfoDto> GetUserAsync(string username) public async Task<UserInfoDto> GetUserAsync(int userId)
{ {
throw new NotImplementedException(); User? user = await apiContext.Users.SingleOrDefaultAsync(x => x.Id == userId);
//未找到用户
if (user == null)
{
throw new BaseException(2004, "User not found");
}
return mapper.Map<UserInfoDto>(user);
} }
public Task<List<UserInfoDto>> GetUsersAsync(int page, int pageSize, bool desc) public Task<List<UserInfoDto>> GetUsersAsync(int page, int pageSize, bool desc)
@ -44,26 +50,14 @@ namespace Apimanager_backend.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async Task<UserInfoDto> LoginAsync(string username, string password) public async Task<bool> IsEmailExist(string email)
{ {
//查找用户 return await apiContext.Users.AnyAsync(x => x.Email == email);
User? user = await apiContext.Users.SingleOrDefaultAsync(x =>
x.Username == username && x.PassHash == password
);
//用户不存在或密码错误都为登录失败
if(user == null)
{
throw new BaseException(2001, "Invalid username or password");
} }
//用户被禁用 public async Task<bool> IsUsernameExist(string username)
if (user.IsBan)
{ {
throw new BaseException(2002, "User account is disabled"); return await apiContext.Users.AnyAsync(x => x.Username == username);
}
return mapper.Map<UserInfoDto>(user);
} }
public Task ResetPasswordAsync(string email, string token, string newPassword) public Task ResetPasswordAsync(string email, string token, string newPassword)

View File

@ -0,0 +1,15 @@
namespace Apimanager_backend.Tools
{
public static class RandomCodeHelper
{
/// <summary>
/// 生成随机数字符串
/// </summary>
/// <returns></returns>
public static string GetRandomCodeStr()
{
Random random = new Random();
return random.Next(10000, 99999).ToString();
}
}
}

View File

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

View File

@ -8,5 +8,22 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "server=192.168.5.200;username=root;password=768788Dyw@;port=3306;database=api_billing_system;SslMode=Preferred;" "DefaultConnection": "server=192.168.5.200;username=root;password=768788Dyw@;port=3306;database=api_billing_system;SslMode=Preferred;"
},
"JwtSettings": {
"Secret": "deXtdXode6hv0SI1o6xRw1ALkn0vYsWn",
"Issuer": "qinglan",
"Audience": "qinglan",
"AccessTokenExpiryMinutes": 60,
"RefreshTokenExpiryDays": 7
},
"redis": {
"ConnectionString": "192.168.5.200:6379"
},
"EmailSettings": {
"Server": "smtp.exmail.qq.com",
"Port": "587",
"Ssl": true,
"Username": "nanxun@nxsir.cn",
"Password": "2919054393Dyw"
} }
} }

View File

@ -15,7 +15,7 @@
#### 用户模块错误码2xxx #### 用户模块错误码2xxx
| 错误码 | HTTP状态码 | 描述 | Message | | 错误码 | HTTP状态码 | 描述 | Message |
| ------ | ---------- | -------------------- | ---------------------------- | | ------ | ---------- | -------------------- | ----------------------------- |
| 2000 | 200 | 登录成功 | Login successful | | 2000 | 200 | 登录成功 | Login successful |
| 2001 | 401 | 用户名或密码错误 | Invalid username or password | | 2001 | 401 | 用户名或密码错误 | Invalid username or password |
| 2002 | 401 | 用户账户被禁用 | User account is disabled | | 2002 | 401 | 用户账户被禁用 | User account is disabled |
@ -24,6 +24,8 @@
| 2005 | 409 | 邮箱已存在 | Email already exists | | 2005 | 409 | 邮箱已存在 | Email already exists |
| 2006 | 403 | 用户无权限进行该操作 | Permission denied | | 2006 | 403 | 用户无权限进行该操作 | Permission denied |
| 2007 | 400 | 密码重置失败 | Password reset failed | | 2007 | 400 | 密码重置失败 | Password reset failed |
| 2008 | 403 | 凭证到期或无效 | Token expires or is invalid |
| 2009 | 403 | 刷新令牌到期或无效 | Refresh expires or is invalid |
#### API模块错误码3xxx #### API模块错误码3xxx
@ -56,3 +58,5 @@
| 5001 | 404 | 日志记录未找到 | Log record not found | | 5001 | 404 | 日志记录未找到 | Log record not found |
| 5002 | 500 | 日志服务异常 | Log service error | | 5002 | 500 | 日志服务异常 | Log service error |
| 5003 | 403 | 无权限查看操作日志 | No permission to view logs | | 5003 | 403 | 无权限查看操作日志 | No permission to view logs |
| 5004 | 500 | 邮件发送错误 | Email send error |
| 5005 | 400 | 验证码错误 | ValidateCode Error |