live-forum/server/webapi/LiveForum/LiveForum.Service/RealName/RealNameService.cs
2026-03-24 11:27:37 +08:00

209 lines
8.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FreeSql;
using LiveForum.IService.Others;
using LiveForum.IService.RealName;
using LiveForum.Model;
using LiveForum.Model.Dto.RealName;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography;
using System.Text;
namespace LiveForum.Service.RealName
{
/// <summary>
/// 实名认证业务服务
/// </summary>
public class RealNameService : IRealNameService
{
private readonly IBaseRepository<T_Users> _userRepository;
private readonly IBaseRepository<T_RealNameVerifyLogs> _logRepository;
private readonly ISystemSettingsService _settings;
private readonly ILogger<RealNameService> _logger;
private readonly IEnumerable<IRealNameVerifyProvider> _providers;
private const string REAL_NAME_ENABLED_KEY = "real_name_enabled";
private const string REAL_NAME_PROVIDER_KEY = "real_name_provider";
private const string DAILY_LIMIT_KEY = "real_name_daily_limit";
private const string INTERVAL_KEY = "real_name_interval_seconds";
// AES加密密钥实际项目应从配置读取
private const string AES_KEY = "LiveForum@RealName2024!AesKey32B";
public RealNameService(
IBaseRepository<T_Users> userRepository,
IBaseRepository<T_RealNameVerifyLogs> logRepository,
ISystemSettingsService settings,
ILogger<RealNameService> logger,
IEnumerable<IRealNameVerifyProvider> providers)
{
_userRepository = userRepository;
_logRepository = logRepository;
_settings = settings;
_logger = logger;
_providers = providers;
}
public async Task<RealNameStatusDto> CheckStatusAsync(long userId)
{
var enabled = await IsEnabledAsync();
var user = await _userRepository.Select.Where(x => x.Id == userId).FirstAsync();
return new RealNameStatusDto
{
RealNameEnabled = enabled,
IsVerified = user?.IsRealNameVerified ?? false
};
}
public async Task<bool> RequiresRealNameAsync(long userId)
{
var enabled = await IsEnabledAsync();
if (!enabled) return false;
var user = await _userRepository.Select.Where(x => x.Id == userId).FirstAsync();
return !(user?.IsRealNameVerified ?? false);
}
public async Task<string> VerifyAsync(long userId, string realName, string idCardNumber, string? clientIp)
{
// 1. 限流检查
var rateLimitMsg = await CheckRateLimitAsync(userId);
if (rateLimitMsg != null) return rateLimitMsg;
// 2. 获取当前服务商
var provider = await GetProviderAsync();
if (provider == null) return "实名认证服务未配置,请联系管理员";
// 3. 调用服务商验证
var result = await provider.VerifyAsync(realName, idCardNumber);
// 4. 记录日志
await _logRepository.InsertAsync(new T_RealNameVerifyLogs
{
UserId = userId,
RealName = MaskName(realName),
IdCardNumberMasked = MaskIdCard(idCardNumber),
IsSuccess = result.IsMatch,
FailReason = result.IsMatch ? null : result.Message,
ClientIp = clientIp
});
// 5. 成功则更新用户状态
if (result.IsMatch)
{
var isMinor = IsMinorByIdCard(idCardNumber);
await _userRepository.UpdateDiy
.Where(x => x.Id == userId)
.Set(x => x.IsRealNameVerified, true)
.Set(x => x.RealName, MaskName(realName))
.Set(x => x.IdCardNumber, EncryptAes(idCardNumber))
.Set(x => x.RealNameVerifiedAt, DateTime.Now)
.Set(x => x.IsMinor, isMinor)
.ExecuteAffrowsAsync();
return "实名认证成功";
}
return result.Message;
}
private async Task<bool> IsEnabledAsync()
{
var val = await _settings.GetSettingAsync(REAL_NAME_ENABLED_KEY);
return string.Equals(val, "true", StringComparison.OrdinalIgnoreCase);
}
private async Task<IRealNameVerifyProvider?> GetProviderAsync()
{
var providerName = await _settings.GetSettingAsync(REAL_NAME_PROVIDER_KEY) ?? "aliyun";
return _providers.FirstOrDefault(p => p.ProviderName.Equals(providerName, StringComparison.OrdinalIgnoreCase));
}
private async Task<string?> CheckRateLimitAsync(long userId)
{
var dailyLimitStr = await _settings.GetSettingAsync(DAILY_LIMIT_KEY) ?? "5";
var intervalStr = await _settings.GetSettingAsync(INTERVAL_KEY) ?? "60";
int.TryParse(dailyLimitStr, out var dailyLimit);
int.TryParse(intervalStr, out var intervalSeconds);
if (dailyLimit <= 0) dailyLimit = 5;
if (intervalSeconds <= 0) intervalSeconds = 60;
var today = DateTime.Today;
var todayCount = await _logRepository.Select
.Where(x => x.UserId == userId && x.CreatedAt >= today)
.CountAsync();
if (todayCount >= dailyLimit)
return "今日验证次数已用完,请明天再试";
var lastLog = await _logRepository.Select
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.CreatedAt)
.FirstAsync();
if (lastLog != null)
{
var elapsed = (DateTime.Now - lastLog.CreatedAt).TotalSeconds;
if (elapsed < intervalSeconds)
{
var remaining = (int)Math.Ceiling(intervalSeconds - elapsed);
return $"操作过于频繁,请 {remaining} 秒后再试";
}
}
return null;
}
private static string MaskName(string name)
{
if (string.IsNullOrEmpty(name)) return "";
if (name.Length <= 1) return name;
if (name.Length == 2) return name[0] + "*";
return name[0] + new string('*', name.Length - 2) + name[^1];
}
private static string MaskIdCard(string idCard)
{
if (string.IsNullOrEmpty(idCard) || idCard.Length < 4) return "****";
return "**************" + idCard[^4..];
}
/// <summary>
/// 根据身份证号判断是否未成年不满18周岁
/// 身份证号第7-14位为出生日期yyyyMMdd
/// </summary>
private static bool IsMinorByIdCard(string idCardNumber)
{
if (string.IsNullOrEmpty(idCardNumber) || idCardNumber.Length != 18)
return false;
var birthStr = idCardNumber.Substring(6, 8);
if (!DateTime.TryParseExact(birthStr, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var birthDate))
return false;
var today = DateTime.Today;
var age = today.Year - birthDate.Year;
if (birthDate.Date > today.AddYears(-age))
age--;
return age < 18;
}
private static string EncryptAes(string plainText)
{
using var aes = Aes.Create();
aes.Key = Encoding.UTF8.GetBytes(AES_KEY);
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var encrypted = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
// IV + 密文 一起存储
var result = new byte[aes.IV.Length + encrypted.Length];
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
Buffer.BlockCopy(encrypted, 0, result, aes.IV.Length, encrypted.Length);
return Convert.ToBase64String(result);
}
}
}