602 lines
20 KiB
C#
602 lines
20 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using XiangYi.Application.DTOs.Requests;
|
||
using XiangYi.Application.DTOs.Responses;
|
||
using XiangYi.Application.Interfaces;
|
||
using XiangYi.Core.Constants;
|
||
using XiangYi.Core.Entities.Biz;
|
||
using XiangYi.Core.Enums;
|
||
using XiangYi.Core.Exceptions;
|
||
using XiangYi.Core.Interfaces;
|
||
using XiangYi.Infrastructure.RealName;
|
||
using XiangYi.Infrastructure.WeChat;
|
||
using XiangYi.Infrastructure.Cache;
|
||
|
||
namespace XiangYi.Application.Services;
|
||
|
||
/// <summary>
|
||
/// 实名认证服务实现
|
||
/// </summary>
|
||
public class RealNameService : IRealNameService
|
||
{
|
||
private readonly IRepository<User> _userRepository;
|
||
private readonly IRepository<Order> _orderRepository;
|
||
private readonly IRepository<RealNameAuth> _realNameAuthRepository;
|
||
private readonly IWeChatService _weChatService;
|
||
private readonly IRealNameProvider _realNameProvider;
|
||
private readonly ICacheService _cacheService;
|
||
private readonly ILogger<RealNameService> _logger;
|
||
|
||
/// <summary>
|
||
/// 实名认证价格(单位:元)
|
||
/// </summary>
|
||
private const decimal RealNamePrice = 88m;
|
||
|
||
/// <summary>
|
||
/// 临时OCR数据缓存前缀
|
||
/// </summary>
|
||
private const string OcrCachePrefix = "realname:ocr:";
|
||
|
||
/// <summary>
|
||
/// 临时OCR数据缓存时间(分钟)
|
||
/// </summary>
|
||
private const int OcrCacheMinutes = 30;
|
||
|
||
/// <summary>
|
||
/// 认证状态名称
|
||
/// </summary>
|
||
private static readonly Dictionary<int, string> StatusNames = new()
|
||
{
|
||
{ 0, "未认证" },
|
||
{ 1, "待审核" },
|
||
{ 2, "已通过" },
|
||
{ 3, "已拒绝" }
|
||
};
|
||
|
||
public RealNameService(
|
||
IRepository<User> userRepository,
|
||
IRepository<Order> orderRepository,
|
||
IRepository<RealNameAuth> realNameAuthRepository,
|
||
IWeChatService weChatService,
|
||
IRealNameProvider realNameProvider,
|
||
ICacheService cacheService,
|
||
ILogger<RealNameService> logger)
|
||
{
|
||
_userRepository = userRepository;
|
||
_orderRepository = orderRepository;
|
||
_realNameAuthRepository = realNameAuthRepository;
|
||
_weChatService = weChatService;
|
||
_realNameProvider = realNameProvider;
|
||
_cacheService = cacheService;
|
||
_logger = logger;
|
||
}
|
||
|
||
|
||
/// <inheritdoc />
|
||
public async Task<RealNameOrderResponse?> CreateRealNameOrderAsync(long userId)
|
||
{
|
||
var user = await _userRepository.GetByIdAsync(userId);
|
||
if (user == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||
}
|
||
|
||
// 已实名用户不能重复认证
|
||
if (user.IsRealName)
|
||
{
|
||
throw new BusinessException(ErrorCodes.RealNameAlreadyVerified, "您已完成实名认证");
|
||
}
|
||
|
||
// 检查是否有待处理的认证记录
|
||
var existingAuth = (await _realNameAuthRepository.GetListAsync(
|
||
a => a.UserId == userId && a.Status == (int)RealNameAuthStatus.Pending))
|
||
.FirstOrDefault();
|
||
|
||
if (existingAuth != null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.InvalidParameter, "您有待处理的实名认证申请");
|
||
}
|
||
|
||
// 会员用户免费认证
|
||
if (IRealNameService.ShouldBeFreeForMember(user.IsMember, user.IsRealName))
|
||
{
|
||
_logger.LogInformation("会员用户免费实名认证: UserId={UserId}", userId);
|
||
return new RealNameOrderResponse
|
||
{
|
||
IsFree = true,
|
||
Amount = 0
|
||
};
|
||
}
|
||
|
||
// 非会员用户创建付费订单
|
||
var orderNo = GenerateOrderNo();
|
||
var order = new Order
|
||
{
|
||
OrderNo = orderNo,
|
||
UserId = userId,
|
||
OrderType = (int)OrderType.RealName,
|
||
ProductName = "实名认证",
|
||
Amount = RealNamePrice,
|
||
PayAmount = RealNamePrice,
|
||
Status = (int)OrderStatus.Pending,
|
||
ExpireTime = DateTime.Now.AddMinutes(30),
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now
|
||
};
|
||
|
||
order = await _orderRepository.AddAsync(order);
|
||
_logger.LogInformation("创建实名认证订单: OrderId={OrderId}, OrderNo={OrderNo}, UserId={UserId}",
|
||
order.Id, orderNo, userId);
|
||
|
||
// 调用微信支付创建预支付订单
|
||
var payRequest = new CreatePaymentRequest
|
||
{
|
||
OutTradeNo = orderNo,
|
||
Amount = (int)(RealNamePrice * 100),
|
||
Description = "实名认证",
|
||
OpenId = user.OpenId,
|
||
Attach = "realname"
|
||
};
|
||
|
||
var payResult = await _weChatService.CreatePaymentAsync(payRequest);
|
||
if (!payResult.Success)
|
||
{
|
||
_logger.LogWarning("创建微信支付订单失败: OrderNo={OrderNo}, Error={Error}",
|
||
orderNo, payResult.ErrorMessage);
|
||
throw new BusinessException(ErrorCodes.PaymentServiceError,
|
||
payResult.ErrorMessage ?? "创建支付订单失败");
|
||
}
|
||
|
||
return new RealNameOrderResponse
|
||
{
|
||
OrderId = order.Id,
|
||
OrderNo = orderNo,
|
||
Amount = RealNamePrice,
|
||
IsFree = false,
|
||
PayParams = payResult.PayParams != null ? new DTOs.Responses.WeChatPayParams
|
||
{
|
||
TimeStamp = payResult.PayParams.TimeStamp,
|
||
NonceStr = payResult.PayParams.NonceStr,
|
||
Package = payResult.PayParams.Package,
|
||
SignType = payResult.PayParams.SignType,
|
||
PaySign = payResult.PayParams.PaySign
|
||
} : null
|
||
};
|
||
}
|
||
|
||
|
||
/// <inheritdoc />
|
||
public async Task<RealNameSubmitResponse> SubmitRealNameAsync(long userId, RealNameSubmitRequest request)
|
||
{
|
||
var user = await _userRepository.GetByIdAsync(userId);
|
||
if (user == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||
}
|
||
|
||
// 已实名用户不能重复认证
|
||
if (user.IsRealName)
|
||
{
|
||
throw new BusinessException(ErrorCodes.RealNameAlreadyVerified, "您已完成实名认证");
|
||
}
|
||
|
||
// 验证参数
|
||
if (string.IsNullOrWhiteSpace(request.RealName))
|
||
{
|
||
throw new BusinessException(ErrorCodes.ValidationFailed, "请输入真实姓名");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(request.IdCard))
|
||
{
|
||
throw new BusinessException(ErrorCodes.ValidationFailed, "请输入身份证号");
|
||
}
|
||
|
||
// 非会员用户需要检查是否已支付
|
||
if (!user.IsMember)
|
||
{
|
||
var paidOrder = (await _orderRepository.GetListAsync(
|
||
o => o.UserId == userId &&
|
||
o.OrderType == (int)OrderType.RealName &&
|
||
o.Status == (int)OrderStatus.Paid))
|
||
.FirstOrDefault();
|
||
|
||
if (paidOrder == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.InvalidParameter, "请先完成实名认证费用支付");
|
||
}
|
||
}
|
||
|
||
// 调用第三方实名认证接口验证
|
||
var verifyResult = await _realNameProvider.VerifyIdCardAsync(request.RealName, request.IdCard);
|
||
|
||
if (!verifyResult.IsVerified)
|
||
{
|
||
_logger.LogWarning("实名认证验证失败: UserId={UserId}, Error={Error}",
|
||
userId, verifyResult.ErrorMessage);
|
||
|
||
return new RealNameSubmitResponse
|
||
{
|
||
Success = false,
|
||
Message = verifyResult.ErrorMessage ?? "身份信息验证失败",
|
||
Status = (int)RealNameAuthStatus.Rejected
|
||
};
|
||
}
|
||
|
||
// 脱敏存储身份信息
|
||
var maskedName = IRealNameService.MaskName(request.RealName);
|
||
var maskedIdCard = IRealNameService.MaskIdCard(request.IdCard);
|
||
|
||
// 查找关联订单
|
||
long orderId = 0;
|
||
if (!user.IsMember)
|
||
{
|
||
var order = (await _orderRepository.GetListAsync(
|
||
o => o.UserId == userId &&
|
||
o.OrderType == (int)OrderType.RealName &&
|
||
o.Status == (int)OrderStatus.Paid))
|
||
.FirstOrDefault();
|
||
orderId = order?.Id ?? 0;
|
||
}
|
||
|
||
// 创建实名认证记录
|
||
var realNameAuth = new RealNameAuth
|
||
{
|
||
UserId = userId,
|
||
OrderId = orderId,
|
||
RealName = maskedName,
|
||
IdCard = maskedIdCard,
|
||
Status = (int)RealNameAuthStatus.Approved,
|
||
VerifyTime = DateTime.Now,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now
|
||
};
|
||
|
||
await _realNameAuthRepository.AddAsync(realNameAuth);
|
||
|
||
// 更新用户实名状态
|
||
user.IsRealName = true;
|
||
user.UpdateTime = DateTime.Now;
|
||
await _userRepository.UpdateAsync(user);
|
||
|
||
_logger.LogInformation("实名认证成功: UserId={UserId}", userId);
|
||
|
||
return new RealNameSubmitResponse
|
||
{
|
||
Success = true,
|
||
Message = "实名认证成功",
|
||
Status = (int)RealNameAuthStatus.Approved,
|
||
MaskedName = maskedName,
|
||
MaskedIdCard = maskedIdCard
|
||
};
|
||
}
|
||
|
||
|
||
/// <inheritdoc />
|
||
public async Task<RealNameStatusResponse> GetRealNameStatusAsync(long userId)
|
||
{
|
||
var user = await _userRepository.GetByIdAsync(userId);
|
||
if (user == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||
}
|
||
|
||
// 检查是否已支付实名认证费用
|
||
var hasPaidOrder = false;
|
||
if (!user.IsMember)
|
||
{
|
||
var paidOrder = (await _orderRepository.GetListAsync(
|
||
o => o.UserId == userId &&
|
||
o.OrderType == (int)OrderType.RealName &&
|
||
o.Status == (int)OrderStatus.Paid))
|
||
.FirstOrDefault();
|
||
hasPaidOrder = paidOrder != null;
|
||
}
|
||
|
||
// 查找最新的实名认证记录
|
||
var authRecords = await _realNameAuthRepository.GetListAsync(a => a.UserId == userId);
|
||
var latestAuth = authRecords.OrderByDescending(a => a.CreateTime).FirstOrDefault();
|
||
|
||
if (latestAuth == null)
|
||
{
|
||
// 未认证
|
||
return new RealNameStatusResponse
|
||
{
|
||
IsRealName = false,
|
||
IsPaid = user.IsMember || hasPaidOrder,
|
||
Status = 0,
|
||
StatusName = GetStatusName(0),
|
||
NeedPayment = !user.IsMember && !hasPaidOrder,
|
||
PaymentAmount = user.IsMember ? null : RealNamePrice
|
||
};
|
||
}
|
||
|
||
return new RealNameStatusResponse
|
||
{
|
||
IsRealName = user.IsRealName,
|
||
IsPaid = user.IsMember || hasPaidOrder,
|
||
Status = latestAuth.Status,
|
||
StatusName = GetStatusName(latestAuth.Status),
|
||
MaskedName = latestAuth.RealName,
|
||
MaskedIdCard = latestAuth.IdCard,
|
||
Name = latestAuth.RealName,
|
||
IdNumber = latestAuth.IdCard,
|
||
RejectReason = latestAuth.RejectReason,
|
||
VerifyTime = latestAuth.VerifyTime,
|
||
NeedPayment = !user.IsMember && latestAuth.Status != (int)RealNameAuthStatus.Approved,
|
||
PaymentAmount = user.IsMember ? null : RealNamePrice
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<RealNameVerifyResponse> VerifyWithImagesAsync(long userId, string frontImageBase64, string? backImageBase64)
|
||
{
|
||
var user = await _userRepository.GetByIdAsync(userId);
|
||
if (user == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||
}
|
||
|
||
// 已实名用户不能重复认证
|
||
if (user.IsRealName)
|
||
{
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = "您已完成实名认证"
|
||
};
|
||
}
|
||
|
||
// 非会员用户需要检查是否已支付
|
||
if (!user.IsMember)
|
||
{
|
||
var paidOrder = (await _orderRepository.GetListAsync(
|
||
o => o.UserId == userId &&
|
||
o.OrderType == (int)OrderType.RealName &&
|
||
o.Status == (int)OrderStatus.Paid))
|
||
.FirstOrDefault();
|
||
|
||
if (paidOrder == null)
|
||
{
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = "请先完成实名认证费用支付"
|
||
};
|
||
}
|
||
}
|
||
|
||
// OCR识别身份证正面
|
||
var frontOcrResult = await _realNameProvider.OcrIdCardFrontAsync(frontImageBase64);
|
||
if (!frontOcrResult.Success)
|
||
{
|
||
_logger.LogWarning("身份证正面OCR识别失败: UserId={UserId}, Error={Error}", userId, frontOcrResult.ErrorMessage);
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = frontOcrResult.ErrorMessage ?? "身份证正面识别失败,请重新拍照"
|
||
};
|
||
}
|
||
|
||
var name = frontOcrResult.Name!;
|
||
var idNumber = frontOcrResult.IdNumber!;
|
||
|
||
// 如果没有反面图片,需要分步上传
|
||
if (string.IsNullOrEmpty(backImageBase64))
|
||
{
|
||
// 缓存正面OCR结果,等待反面上传
|
||
var cacheKey = $"{OcrCachePrefix}{userId}";
|
||
await _cacheService.SetAsync(cacheKey, new OcrCacheData
|
||
{
|
||
Name = name,
|
||
IdNumber = idNumber
|
||
}, OcrCacheMinutes * 60);
|
||
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = true,
|
||
Message = "身份证正面识别成功,请上传反面",
|
||
NeedBackImage = true
|
||
};
|
||
}
|
||
|
||
// OCR识别身份证反面
|
||
var backOcrResult = await _realNameProvider.OcrIdCardBackAsync(backImageBase64);
|
||
if (!backOcrResult.Success)
|
||
{
|
||
_logger.LogWarning("身份证反面OCR识别失败: UserId={UserId}, Error={Error}", userId, backOcrResult.ErrorMessage);
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = backOcrResult.ErrorMessage ?? "身份证反面识别失败,请重新拍照"
|
||
};
|
||
}
|
||
|
||
// 调用实名认证接口验证
|
||
return await CompleteVerificationAsync(userId, user, name, idNumber);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<RealNameVerifyResponse> UploadBackImageAsync(long userId, string backImageBase64)
|
||
{
|
||
var user = await _userRepository.GetByIdAsync(userId);
|
||
if (user == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||
}
|
||
|
||
// 已实名用户不能重复认证
|
||
if (user.IsRealName)
|
||
{
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = "您已完成实名认证"
|
||
};
|
||
}
|
||
|
||
// 获取缓存的正面OCR数据
|
||
var cacheKey = $"{OcrCachePrefix}{userId}";
|
||
var cachedData = await _cacheService.GetAsync<OcrCacheData>(cacheKey);
|
||
if (cachedData == null)
|
||
{
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = "请先上传身份证正面"
|
||
};
|
||
}
|
||
|
||
// OCR识别身份证反面
|
||
var backOcrResult = await _realNameProvider.OcrIdCardBackAsync(backImageBase64);
|
||
if (!backOcrResult.Success)
|
||
{
|
||
_logger.LogWarning("身份证反面OCR识别失败: UserId={UserId}, Error={Error}", userId, backOcrResult.ErrorMessage);
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = backOcrResult.ErrorMessage ?? "身份证反面识别失败,请重新拍照"
|
||
};
|
||
}
|
||
|
||
// 清除缓存
|
||
await _cacheService.RemoveAsync(cacheKey);
|
||
|
||
// 调用实名认证接口验证
|
||
return await CompleteVerificationAsync(userId, user, cachedData.Name, cachedData.IdNumber);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 完成实名认证验证
|
||
/// </summary>
|
||
private async Task<RealNameVerifyResponse> CompleteVerificationAsync(long userId, User user, string name, string idNumber)
|
||
{
|
||
// 调用第三方实名认证接口验证
|
||
var verifyResult = await _realNameProvider.VerifyIdCardAsync(name, idNumber);
|
||
|
||
if (!verifyResult.IsVerified)
|
||
{
|
||
_logger.LogWarning("实名认证验证失败: UserId={UserId}, Error={Error}", userId, verifyResult.ErrorMessage);
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = false,
|
||
Message = verifyResult.ErrorMessage ?? "身份信息验证失败,请确认身份证信息正确"
|
||
};
|
||
}
|
||
|
||
// 脱敏存储身份信息
|
||
var maskedName = IRealNameService.MaskName(name);
|
||
var maskedIdCard = IRealNameService.MaskIdCard(idNumber);
|
||
|
||
// 查找关联订单
|
||
long orderId = 0;
|
||
if (!user.IsMember)
|
||
{
|
||
var order = (await _orderRepository.GetListAsync(
|
||
o => o.UserId == userId &&
|
||
o.OrderType == (int)OrderType.RealName &&
|
||
o.Status == (int)OrderStatus.Paid))
|
||
.FirstOrDefault();
|
||
orderId = order?.Id ?? 0;
|
||
}
|
||
|
||
// 创建实名认证记录
|
||
var realNameAuth = new RealNameAuth
|
||
{
|
||
UserId = userId,
|
||
OrderId = orderId,
|
||
RealName = maskedName,
|
||
IdCard = maskedIdCard,
|
||
Status = (int)RealNameAuthStatus.Approved,
|
||
VerifyTime = DateTime.Now,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now
|
||
};
|
||
|
||
await _realNameAuthRepository.AddAsync(realNameAuth);
|
||
|
||
// 更新用户实名状态
|
||
user.IsRealName = true;
|
||
user.UpdateTime = DateTime.Now;
|
||
await _userRepository.UpdateAsync(user);
|
||
|
||
_logger.LogInformation("实名认证成功: UserId={UserId}", userId);
|
||
|
||
return new RealNameVerifyResponse
|
||
{
|
||
Success = true,
|
||
Message = "实名认证成功",
|
||
NeedBackImage = false,
|
||
Data = new RealNameVerifyData
|
||
{
|
||
Name = maskedName,
|
||
IdNumber = maskedIdCard
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// OCR缓存数据
|
||
/// </summary>
|
||
private class OcrCacheData
|
||
{
|
||
public string Name { get; set; } = string.Empty;
|
||
public string IdNumber { get; set; } = string.Empty;
|
||
}
|
||
|
||
#region 私有方法
|
||
|
||
/// <summary>
|
||
/// 生成订单号
|
||
/// </summary>
|
||
private static string GenerateOrderNo()
|
||
{
|
||
var randomSuffix = Random.Shared.Next(1000, 9999);
|
||
return $"R{DateTime.Now:yyyyMMddHHmmss}{randomSuffix:D4}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取状态名称
|
||
/// </summary>
|
||
private static string GetStatusName(int status)
|
||
{
|
||
return StatusNames.TryGetValue(status, out var name) ? name : "未知";
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 静态辅助方法(用于属性测试)
|
||
|
||
/// <summary>
|
||
/// 脱敏姓名
|
||
/// </summary>
|
||
public static string MaskName(string name)
|
||
{
|
||
return IRealNameService.MaskName(name);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 脱敏身份证号
|
||
/// </summary>
|
||
public static string MaskIdCard(string idCard)
|
||
{
|
||
return IRealNameService.MaskIdCard(idCard);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证脱敏是否正确
|
||
/// </summary>
|
||
public static bool ValidateMasking(string originalName, string maskedName, string originalIdCard, string maskedIdCard)
|
||
{
|
||
return IRealNameService.ValidateMasking(originalName, maskedName, originalIdCard, maskedIdCard);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查会员是否应该免费实名认证
|
||
/// </summary>
|
||
public static bool ShouldBeFreeForMember(bool isMember, bool isAlreadyRealName)
|
||
{
|
||
return IRealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName);
|
||
}
|
||
|
||
#endregion
|
||
}
|