初始化

This commit is contained in:
2025-10-16 09:56:36 +08:00
commit de704db577
272 changed files with 37331 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.AspNetCore.Http;
using System.Reflection;
using System.Linq;
namespace FutureMailAPI.Helpers
{
/// <summary>
/// Swagger文件上传操作过滤器
/// </summary>
public class FileUploadOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var fileUploadMime = "multipart/form-data";
if (operation.RequestBody == null || !operation.RequestBody.Content.Any(x => x.Key.Equals(fileUploadMime, StringComparison.InvariantCultureIgnoreCase)))
return;
var fileParams = context.MethodInfo.GetParameters()
.Where(p => p.ParameterType == typeof(IFormFile) ||
(p.ParameterType.IsClass && p.ParameterType.GetProperties().Any(prop => prop.PropertyType == typeof(IFormFile))))
.ToList();
if (!fileParams.Any())
return;
operation.RequestBody.Content[fileUploadMime].Schema.Properties =
fileParams.ToDictionary(k => k.Name!, v => new OpenApiSchema()
{
Type = "string",
Format = "binary"
});
}
}
}

View File

@@ -0,0 +1,97 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using FutureMailAPI.Models;
namespace FutureMailAPI.Helpers
{
public interface IJwtHelper
{
string GenerateToken(User user);
string GenerateToken(int userId, string username, string email);
ClaimsPrincipal? ValidateToken(string token);
}
public class JwtHelper : IJwtHelper
{
private readonly IConfiguration _configuration;
public JwtHelper(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email)
}),
Expires = DateTime.UtcNow.AddDays(7),
Issuer = _configuration["Jwt:Issuer"],
Audience = _configuration["Jwt:Audience"],
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public string GenerateToken(int userId, string username, string email)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Email, email)
}),
Expires = DateTime.UtcNow.AddDays(7),
Issuer = _configuration["Jwt:Issuer"],
Audience = _configuration["Jwt:Audience"],
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!);
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = _configuration["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
return principal;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,102 @@
using Microsoft.EntityFrameworkCore;
using Quartz;
using FutureMailAPI.Data;
using FutureMailAPI.Models;
namespace FutureMailAPI.Helpers
{
[DisallowConcurrentExecution]
public class MailDeliveryJob : IJob
{
private readonly FutureMailDbContext _context;
private readonly ILogger<MailDeliveryJob> _logger;
public MailDeliveryJob(FutureMailDbContext context, ILogger<MailDeliveryJob> logger)
{
_context = context;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("开始执行邮件投递任务: {time}", DateTime.Now);
try
{
// 查找所有需要投递的邮件
var mailsToDeliver = await _context.SentMails
.Where(m => m.Status == 1 && m.DeliveryTime <= DateTime.UtcNow)
.Include(m => m.Sender)
.ToListAsync();
_logger.LogInformation("找到 {count} 封待投递邮件", mailsToDeliver.Count);
foreach (var mail in mailsToDeliver)
{
try
{
// 更新邮件状态为投递中
mail.Status = 2;
await _context.SaveChangesAsync();
// 根据收件人类型创建接收邮件记录
if (mail.RecipientType == 0) // 发给自己
{
var receivedMail = new ReceivedMail
{
SentMailId = mail.Id,
RecipientId = mail.SenderId,
ReceivedAt = DateTime.UtcNow
};
_context.ReceivedMails.Add(receivedMail);
}
else if (mail.RecipientType == 1 && mail.RecipientId.HasValue) // 发给指定用户
{
var receivedMail = new ReceivedMail
{
SentMailId = mail.Id,
RecipientId = mail.RecipientId.Value,
ReceivedAt = DateTime.UtcNow
};
_context.ReceivedMails.Add(receivedMail);
}
else if (mail.RecipientType == 2) // 公开时间胶囊
{
// 公开时间胶囊的处理逻辑,可能需要更复杂的机制
// 这里简化处理,暂时不实现
}
// 更新邮件状态为已送达
mail.Status = 3;
// 更新对应的时间胶囊状态
var timeCapsule = await _context.TimeCapsules
.FirstOrDefaultAsync(tc => tc.SentMailId == mail.Id);
if (timeCapsule != null)
{
timeCapsule.Status = 3; // 已开启
}
await _context.SaveChangesAsync();
_logger.LogInformation("邮件 {mailId} 投递成功", mail.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "投递邮件 {mailId} 时发生错误", mail.Id);
// 可以添加重试逻辑或错误通知
}
}
_logger.LogInformation("邮件投递任务完成: {time}", DateTime.Now);
}
catch (Exception ex)
{
_logger.LogError(ex, "执行邮件投递任务时发生错误");
}
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Security.Cryptography;
using System.Text;
namespace FutureMailAPI.Helpers
{
public interface IPasswordHelper
{
string HashPassword(string password);
bool VerifyPassword(string password, string hash);
string HashPassword(string password, string salt);
string GenerateSalt();
}
public class PasswordHelper : IPasswordHelper
{
public string HashPassword(string password)
{
// 生成随机盐值
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);
// 使用PBKDF2算法生成哈希
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000);
byte[] hash = pbkdf2.GetBytes(20);
// 组合盐值和哈希值
byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);
// 转换为Base64字符串
return Convert.ToBase64String(hashBytes);
}
public string GenerateSalt()
{
// 生成随机盐值
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);
// 转换为Base64字符串
return Convert.ToBase64String(salt);
}
public string HashPassword(string password, string salt)
{
// 将Base64盐值转换为字节数组
byte[] saltBytes = Convert.FromBase64String(salt);
// 使用PBKDF2算法生成哈希
var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, 10000);
byte[] hash = pbkdf2.GetBytes(20);
// 转换为Base64字符串
return Convert.ToBase64String(hash);
}
public bool VerifyPassword(string password, string hash)
{
try
{
// 从存储的哈希中提取盐值
byte[] hashBytes = Convert.FromBase64String(hash);
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
// 使用相同的盐值计算密码的哈希
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000);
byte[] computedHash = pbkdf2.GetBytes(20);
// 比较两个哈希值
for (int i = 0; i < 20; i++)
{
if (hashBytes[i + 16] != computedHash[i])
return false;
}
return true;
}
catch
{
return false;
}
}
}
}