209 lines
8.0 KiB
C#
209 lines
8.0 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|