feat: 人证人脸对比

This commit is contained in:
2026-06-23 15:21:47 +08:00
parent e720cefaf7
commit 3514610857
10 changed files with 732 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
using Dlhis.Basic.Organization.Request;
using Dlhis.Basic.Organization.Response;
using Dlhis.Basic.Organization.Services;
using Kurisu.AspNetCore.DataAccess.Dto;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Aop;
using Kurisu.AspNetCore.UnifyResultAndValidation.Aop;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Dlhis.Basic.Organization.Controllers;
/// <summary>
/// 人证人脸对比
/// </summary>
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class GainfoIdCardFaceCompareController : ControllerBase
{
private readonly IGainfoIdCardFaceCompareService _service;
public GainfoIdCardFaceCompareController(IGainfoIdCardFaceCompareService service)
{
_service = service;
}
/// <summary>
/// 人证人脸对比记录分页
/// </summary>
[DisableApiLog]
[HttpPost("page")]
public async Task<Pagination<IdCardFaceCompareRecordResponse>> GetPageAsync(IdCardFaceComparePageRequest request)
{
return await _service.GetPageAsync(request);
}
/// <summary>
/// 人证人脸对比
/// </summary>
[HttpPost("compare")]
[Transactional]
public async Task<IdCardFaceCompareRecordResponse> CompareAsync(IdCardFaceCompareRequest request)
{
return await _service.CompareAsync(request);
}
}

View File

@@ -0,0 +1,22 @@
using Dlhis.Entity.Basic.Enums;
using Kurisu.AspNetCore.DataAccess.Dto;
namespace Dlhis.Basic.Organization.Request;
public class IdCardFaceComparePageRequest : PageDto
{
/// <summary>
/// 厂区
/// </summary>
public List<string> Factories { get; set; }
/// <summary>
/// 人员类型
/// </summary>
public GainfoUserTypeEnumType? UserType { get; set; }
/// <summary>
/// 对比结果
/// </summary>
public IdCardFaceCompareResultEnumType? CompareResult { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Dlhis.Basic.Organization.Request;
public class IdCardFaceCompareRequest
{
/// <summary>
/// 用户code
/// </summary>
[Required(ErrorMessage = "用户code不可为空")]
public string UserCode { get; set; }
}

View File

@@ -0,0 +1,39 @@
namespace Dlhis.Basic.Organization.Response;
public class AliyunId3MetaVerifyResponse
{
/// <summary>
/// 请求ID
/// </summary>
public string RequestId { get; set; }
/// <summary>
/// 接口调用返回码,200表示接口调用成功
/// </summary>
public string Code { get; set; }
/// <summary>
/// 接口调用返回信息,仅表示接口是否异常
/// </summary>
public string Message { get; set; }
/// <summary>
/// 业务核验结果,1一致、2不一致、3查无记录
/// </summary>
public string BizCode { get; set; }
/// <summary>
/// 业务核验是否通过,来源于BizCode
/// </summary>
public bool? Passed { get; set; }
/// <summary>
/// 人像比对分值,来源于FaceDetail.verifyScore,可能为空
/// </summary>
public double? Similarity { get; set; }
/// <summary>
/// 阿里云原始响应
/// </summary>
public string RawResponse { get; set; }
}

View File

@@ -0,0 +1,81 @@
using Dlhis.Entity.Basic.Enums;
namespace Dlhis.Basic.Organization.Response;
public class IdCardFaceCompareRecordResponse
{
/// <summary>
/// 记录ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 用户code
/// </summary>
public string UserCode { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 人员类型
/// </summary>
public GainfoUserTypeEnumType UserType { get; set; }
/// <summary>
/// 证件号
/// </summary>
public string IdNo { get; set; }
/// <summary>
/// 部门名称
/// </summary>
public string DepartmentName { get; set; }
/// <summary>
/// 厂区
/// </summary>
public string Factory { get; set; }
/// <summary>
/// 对比结果
/// </summary>
public IdCardFaceCompareResultEnumType CompareResult { get; set; }
/// <summary>
/// 业务核验是否通过,来源于阿里云ResultObject.BizCode
/// </summary>
public bool? IsPassed { get; set; }
/// <summary>
/// 人像比对分值,来源于阿里云ResultObject.FaceDetail.verifyScore,可能为空
/// </summary>
public double? Similarity { get; set; }
/// <summary>
/// 当前用户累计对比次数
/// </summary>
public int CompareCount { get; set; }
/// <summary>
/// 最近对比时间
/// </summary>
public DateTime? LastCompareTime { get; set; }
/// <summary>
/// 阿里云原始响应
/// </summary>
public string ApiResponse { get; set; }
/// <summary>
/// 对比异常信息
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 记录创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}

View File

@@ -0,0 +1,218 @@
using System.Security.Cryptography;
using System.Text;
using Dlhis.Basic.Organization.Response;
using Kurisu.AspNetCore.CustomClass;
using Newtonsoft.Json;
namespace Dlhis.Basic.Organization.Services;
public interface IAliyunClient : IScopeDependency
{
Task<AliyunId3MetaVerifyResponse> Id3MetaVerifyAsync(
string identifyNum,
string userName,
string faceBase64,
string crop = "F");
}
public class AliyunClient : IAliyunClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly string _accessKeyId;
private readonly string _accessKeySecret;
private const string Endpoint = "cloudauth.aliyuncs.com";
private const string ApiVersion = "2019-03-07";
public AliyunClient(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<AliyunClient> logger)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
}
public AliyunClient(string accessKeyId, string accessKeySecret)
{
_accessKeyId = accessKeyId ?? "LTAI5tAqyWKcBcSvzpc1ZM72";
_accessKeySecret = accessKeySecret ?? "YZMSqsmMGlGZuHw5WU2tqt0mmIBX6h";
}
/// <summary>
/// 使用 Base64 图片进行三要素核验
/// </summary>
public async Task<AliyunId3MetaVerifyResponse> Id3MetaVerifyAsync(
string identifyNum,
string userName,
string faceBase64,
string crop = "F")
{
var accessKeyId = _accessKeyId ?? GetRequiredConfig("AliyunOptions:AccessKeyId", "Aliyun:AccessKeyId");
var accessKeySecret = _accessKeySecret ?? GetRequiredConfig("AliyunOptions:AccessKeySecret", "Aliyun:AccessKeySecret");
if (faceBase64.Contains(","))
{
faceBase64 = faceBase64[(faceBase64.IndexOf(',') + 1)..];
}
var parameters = new SortedDictionary<string, string>
{
{ "Action", "Id3MetaVerify" },
{ "Format", "JSON" },
{ "Version", ApiVersion },
{ "AccessKeyId", accessKeyId },
{ "SignatureMethod", "HMAC-SHA1" },
{ "SignatureVersion", "1.0" },
{ "SignatureNonce", Guid.NewGuid().ToString("N") },
{ "Timestamp", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") },
{ "ParamType", "normal" },
{ "IdentifyNum", identifyNum },
{ "UserName", userName },
{ "FacePicture", faceBase64 },
{ "Crop", crop }
};
var signature = ComputeSignature(parameters, "POST", accessKeySecret);
var url = $"https://{Endpoint}/?Signature={PercentEncode(signature)}";
var bodyString = BuildBodyString(parameters);
using var fallbackClient = _httpClientFactory == null ? new HttpClient() : null;
var client = _httpClientFactory?.CreateClient() ?? fallbackClient;
client.DefaultRequestHeaders.Add("Host", Endpoint);
var content = new StringContent(bodyString, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = await client.PostAsync(url, content);
var rawResponse = await response.Content.ReadAsStringAsync();
return ParseResponse(rawResponse);
}
private string GetRequiredConfig(params string[] keys)
{
foreach (var key in keys)
{
var value = _configuration.GetSection(key).Value;
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
throw new UserFriendlyException($"阿里云配置缺失:{string.Join("或", keys)}");
}
private static AliyunId3MetaVerifyResponse ParseResponse(string rawResponse)
{
var result = new AliyunId3MetaVerifyResponse
{
RawResponse = rawResponse
};
if (string.IsNullOrWhiteSpace(rawResponse))
{
return result;
}
try
{
var response = JsonConvert.DeserializeObject<AliyunId3MetaVerifyApiResponse>(rawResponse);
result.RequestId = response?.RequestId;
result.Code = response?.Code;
result.Message = response?.Message;
result.BizCode = response?.ResultObject?.BizCode;
result.Similarity = ParseVerifyScore(response?.ResultObject?.FaceDetail);
result.Passed = response?.ResultObject?.BizCode switch
{
"1" => true,
"2" or "3" => false,
_ => null
};
}
catch
{
result.Message = "阿里云响应不是合法JSON";
}
return result;
}
private static double? ParseVerifyScore(string faceDetail)
{
if (string.IsNullOrWhiteSpace(faceDetail))
{
return null;
}
var detail = JsonConvert.DeserializeObject<AliyunId3MetaVerifyFaceDetail>(faceDetail);
return detail?.VerifyScore;
}
private static string ComputeSignature(SortedDictionary<string, string> parameters, string httpMethod, string accessKeySecret)
{
var canonicalized = new StringBuilder();
foreach (var kv in parameters)
{
if (kv.Key == "Signature")
{
continue;
}
if (canonicalized.Length > 0)
{
canonicalized.Append('&');
}
canonicalized.Append(PercentEncode(kv.Key))
.Append('=')
.Append(PercentEncode(kv.Value));
}
var stringToSign = httpMethod + "&" + PercentEncode("/") + "&" + PercentEncode(canonicalized.ToString());
var signKey = accessKeySecret + "&";
using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(signKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
return Convert.ToBase64String(hash);
}
private static string BuildBodyString(SortedDictionary<string, string> parameters)
{
return string.Join("&", parameters
.Where(x => x.Key != "Signature")
.Select(x => PercentEncode(x.Key) + "=" + PercentEncode(x.Value)));
}
private static string PercentEncode(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return Uri.EscapeDataString(value)
.Replace("+", "%20")
.Replace("*", "%2A")
.Replace("%7E", "~");
}
private class AliyunId3MetaVerifyApiResponse
{
public string RequestId { get; set; }
public string Message { get; set; }
public string Code { get; set; }
public AliyunId3MetaVerifyResultObject ResultObject { get; set; }
}
private class AliyunId3MetaVerifyResultObject
{
public string BizCode { get; set; }
public string FaceDetail { get; set; }
}
private class AliyunId3MetaVerifyFaceDetail
{
[JsonProperty("verifyScore")]
public double? VerifyScore { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using Dlhis.Basic.Organization.Request;
using Dlhis.Basic.Organization.Response;
using Kurisu.AspNetCore.DataAccess.Dto;
namespace Dlhis.Basic.Organization.Services;
public interface IGainfoIdCardFaceCompareService : IScopeDependency
{
/// <summary>
/// 人证人脸对比记录分页查询
/// </summary>
/// <param name="request">分页查询条件</param>
/// <returns>人证人脸对比记录分页结果</returns>
Task<Pagination<IdCardFaceCompareRecordResponse>> GetPageAsync(IdCardFaceComparePageRequest request);
/// <summary>
/// 执行人证人脸对比
/// </summary>
/// <param name="request">人证人脸对比请求</param>
/// <returns>人证人脸对比记录</returns>
Task<IdCardFaceCompareRecordResponse> CompareAsync(IdCardFaceCompareRequest request);
}

View File

@@ -0,0 +1,191 @@
using Dlhis.Basic.Organization.Request;
using Dlhis.Basic.Organization.Response;
using Dlhis.Entity.Basic.Entity;
using Dlhis.Entity.Basic.Enums;
using Kurisu.AspNetCore.Authentication.Abstractions;
using Kurisu.AspNetCore.CustomClass;
using Kurisu.AspNetCore.DataAccess.Dto;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.Utils.Extensions;
using SqlSugar;
namespace Dlhis.Basic.Organization.Services.Implements;
public class GainfoIdCardFaceCompareService : IGainfoIdCardFaceCompareService
{
private readonly IDbContext _dbContext;
private readonly IDbContext _faceDbContext;
private readonly IAliyunClient _aliyunClient;
private readonly ICurrentUser _currentUser;
public GainfoIdCardFaceCompareService(
IDbContext dbContext,
IAliyunClient aliyunClient,
ICurrentUser currentUser)
{
_dbContext = dbContext;
_faceDbContext = dbContext.ChangeDb("face");
_aliyunClient = aliyunClient;
_currentUser = currentUser;
}
public async Task<Pagination<IdCardFaceCompareRecordResponse>> GetPageAsync(IdCardFaceComparePageRequest request)
{
var factories = GetFactories(request.Factories);
return await _dbContext.Queryable<IdCardFaceCompareRecordEntity>()
.LeftJoin<GainfoEntity>((record, gainfo) => record.UserCode == gainfo.Code)
.LeftJoin<DepartmentEntity>((record, gainfo, department) => gainfo.DepartmentNo == department.No)
.WhereIF(factories.Any(), (record, gainfo) => factories.Contains(gainfo.Factory))
.WhereIF(request.UserType.HasValue, record => record.UserType == request.UserType.Value)
.WhereIF(request.CompareResult.HasValue, record => record.CompareResult == request.CompareResult.Value)
.WhereIF(request.Search.IsPresent(), (record, gainfo) =>
record.UserCode == request.Search
|| record.IdNo == request.Search
|| record.UserName.Contains(request.Search)
|| gainfo.Name.Contains(request.Search))
.OrderBy(record => record.CreateTime, OrderByType.Desc)
.Select((record, gainfo, department) => new IdCardFaceCompareRecordResponse
{
Id = record.Id,
UserCode = record.UserCode,
UserName = record.UserName,
UserType = record.UserType,
IdNo = record.IdNo,
DepartmentName = department.Name,
Factory = gainfo.Factory,
CompareResult = record.CompareResult,
IsPassed = record.IsPassed,
Similarity = record.Similarity,
CompareCount = record.CompareCount,
LastCompareTime = record.LastCompareTime,
ApiResponse = record.ApiResponse,
ErrorMessage = record.ErrorMessage,
CreateTime = record.CreateTime
})
.ToPageListAsync(request);
}
public async Task<IdCardFaceCompareRecordResponse> CompareAsync(IdCardFaceCompareRequest request)
{
var gainfo = await _dbContext.Queryable<GainfoEntity>()
.Where(x => x.Code == request.UserCode)
.SingleAsync();
gainfo.ThrowIfNull("人员档案信息不存在");
gainfo.IdNo.IsPresent().ThrowIfFalse("人员证件号为空,无法对比");
var now = DateTime.Now;
var userId = _currentUser.GetIntSubjectId();
var record = await _dbContext.Queryable<IdCardFaceCompareRecordEntity>()
.Where(x => x.UserCode == gainfo.Code)
.SingleAsync();
if (record == null)
{
record = new IdCardFaceCompareRecordEntity
{
UserCode = gainfo.Code,
UserType = gainfo.Type,
UserName = gainfo.Name,
IdNo = gainfo.IdNo,
CompareResult = IdCardFaceCompareResultEnumType.NotCompared,
CompareCount = 0,
CreateTime = now,
ModifiedTime = now,
CreatedBy = userId,
ModifiedBy = userId
};
await _dbContext.InsertAsync(record);
}
else
{
record.UserType = gainfo.Type;
record.UserName = gainfo.Name;
record.IdNo = gainfo.IdNo;
record.CompareResult = IdCardFaceCompareResultEnumType.NotCompared;
record.ErrorMessage = null;
record.ModifiedTime = now;
record.ModifiedBy = userId;
await _dbContext.UpdateAsync(record);
}
var face = await _faceDbContext.Queryable<GainfoFaceEntity>()
.Where(x => x.UserCode == gainfo.Code)
.Select(x => x.Face)
.SingleAsync();
if (!face.IsPresent())
{
record.ErrorMessage = "人脸照片尚未录入";
record.ModifiedTime = DateTime.Now;
record.ModifiedBy = userId;
await _dbContext.UpdateAsync(record);
throw new UserFriendlyException(record.ErrorMessage);
}
try
{
var response = await _aliyunClient.Id3MetaVerifyAsync(gainfo.IdNo, gainfo.Name, face);
record.CompareResult = IdCardFaceCompareResultEnumType.Compared;
record.ApiResponse = response.RawResponse;
record.IsPassed = response.Passed;
record.Similarity = response.Similarity;
record.CompareCount += 1;
record.LastCompareTime = now;
record.ModifiedTime = DateTime.Now;
record.ModifiedBy = userId;
}
catch (Exception ex)
{
record.ErrorMessage = ex.Message;
record.ModifiedTime = DateTime.Now;
record.ModifiedBy = userId;
await _dbContext.UpdateAsync(record);
throw;
}
await _dbContext.UpdateAsync(record);
return await GetRecordAsync(record.Id);
}
private async Task<IdCardFaceCompareRecordResponse> GetRecordAsync(long id)
{
return await _dbContext.Queryable<IdCardFaceCompareRecordEntity>()
.LeftJoin<GainfoEntity>((record, gainfo) => record.UserCode == gainfo.Code)
.LeftJoin<DepartmentEntity>((record, gainfo, department) => gainfo.DepartmentNo == department.No)
.Where(record => record.Id == id)
.Select((record, gainfo, department) => new IdCardFaceCompareRecordResponse
{
Id = record.Id,
UserCode = record.UserCode,
UserName = record.UserName,
UserType = record.UserType,
IdNo = record.IdNo,
DepartmentName = department.Name,
Factory = gainfo.Factory,
CompareResult = record.CompareResult,
IsPassed = record.IsPassed,
Similarity = record.Similarity,
CompareCount = record.CompareCount,
LastCompareTime = record.LastCompareTime,
ApiResponse = record.ApiResponse,
ErrorMessage = record.ErrorMessage,
CreateTime = record.CreateTime
})
.SingleAsync();
}
private List<string> GetFactories(List<string> requestFactories)
{
var currentFactories = _currentUser.GetUserClaim("tenants")?.Split(",").Where(x => x.IsPresent()).ToList() ?? new List<string>();
if (requestFactories == null || !requestFactories.Any())
{
return currentFactories;
}
return requestFactories.Where(x => currentFactories.Contains(x)).ToList();
}
}